1.进程间通信介绍
1.1 进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.2 进程间通信的发展和分类
管道->System V进程间通信->POSIX进程间通信
管道分为:匿名管道pipe、命名管道
System V IPC分为:System V消息队列、System V共享内存、SystemV信号量
POSIX IPC分为:消息队列、共享内存、信号量、互斥量、条件变量、读写锁
2.管道
2.1 什么是管道
管道是Unix中最古老的进程间通信的方式
我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

2.2 匿名管道
cpp
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

示例:
cpp
// 例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fds[2];
char buf[100];
int len;
if (pipe(fds) == -1)
perror("make pipe"), exit(1);
// read from stdin
while (fgets(buf, 100, stdin))
{
len = strlen(buf);
// write into pipe
if (write(fds[1], buf, len) != len)
{
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if ((len = read(fds[0], buf, 100)) == -1)
{
perror("read from pipe");
break;
}
// write to stdout
if (write(1, buf, len) != len)
{
perror("write to stdout");
break;
}
}
return 0;
}


通过fork来共享管道,其实就是在父进程创建管道后再创建子进程,那么子进程会继承父进程的文件描述符表,也就是子进程可以拿到父进程创建的管道的读端描述符和写段描述符,我们一般会让子进程关闭读端,父进程关闭写端这样就能保证数据的单向流动
这里讲一下为什么管道本质上是一个文件,但是却又两个问价描述符
在 Linux 中,管道(pipe)确实是一种特殊的文件,但它与普通文件(如磁盘文件)有一个关键区别:管道是单向的、基于内存的字节流。因此,它需要两个文件描述符(一个用于读,一个用于写)来支持进程间的单向通信。以下是详细解释:
1. 管道的本质是单向字节流
管道的数据是单向流动的:一端写入数据,另一端读取数据。这与普通文件(可随机读写)不同。
如果只有一个文件描述符,就无法同时支持读和写操作,因为:
若允许读写,可能会导致进程自己写入的数据又被自己读取(不符合管道"进程间通信"的设计初衷)。
单向性简化了同步和资源管理(例如,无需处理读写冲突)。
2. 两个文件描述符的作用
pipefd[0]
(读端):用于从管道中读取数据。
pipefd[1]
(写端):用于向管道中写入数据。这种设计使得:
父子进程或兄弟进程可以通过继承文件描述符实现通信(例如:父进程写,子进程读)。
数据流向明确:避免混淆读写方向。
3. 为什么不像普通文件一样用一个文件描述符?
普通文件(如磁盘文件)的读写通常是双向的 (通过
lseek
调整位置),但管道是无结构的字节流:
管道没有"位置"概念,数据一旦被读取就会被消耗。
如果允许一个文件描述符同时读写,可能导致数据混乱(例如:进程读取了自己刚写入的数据)。
性能与同步:分离读写端可以优化内核的实现(例如:写端关闭时,读端可以立即感知到EOF)。
4. 与命名管道的对比
- 命名管道(FIFO,通过
mkfifo
创建)在文件系统中有一个路径,但本质上也是单向的。它同样需要两个文件描述符(分别以O_RDONLY
和O_WRONLY
打开)才能完成通信。
5. 技术实现角度
在 Linux 内核中,管道通过
struct pipe_inode_info
管理,内部维护了一个环形缓冲区。读写端通过两个独立的文件描述符操作同一个缓冲区,但内核会确保:
写端写入数据时,唤醒等待读端的进程。
读端读取数据时,唤醒等待写端的进程(如果缓冲区满)。
站在文件描述符角度---深度理解管道

站在内核角度---管道本质

在linux下一切皆文件,我们需要先描述在组织,描述就是通过file结构体来完成,组织是通过其他的数据结构来完成的比如链表。
2.3 管道读写规则
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。 O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
2.4管道特点
只能用于具有共同祖先的进程(可以理解为同一个进程fork产生的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以使用该管道
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步和异步
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

2.5 命名管道(内存级文件)
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
创建一个命名管道:命令行方法创建命名管道
cpp
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
cpp
int mkfifo(const char *filename,mode_t mode);
cpp
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
2.6 匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与 pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
2.7 命名管道的打开规则
如果当前打开操作是为读而打开 FIFO 时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开 FIFO 时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该 FIFO
O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO
3.System V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存数据结构
cpp
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
共享内存函数


shmat说明:
shmaddr 为 NULL ,核心自动选择一个地址
shmaddr 不为 NULL 且 shmflg 无 SHM_RND 标记,则以 shmaddr 为连接地址。
shmaddr 不为 NULL 且 shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式: shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY ,表示连接操作用来只读共享内存