Linux 进程间通信(IPC)
所有进程间通信的前提是:先让需要进行通信的进程看到同一份资源(内存)
匿名管道(通常用于父子通信)
-
可使用pipe()系统调用函数创建匿名管道,函数原型为
int pipe(int fd[2]),函数功能为创建一个匿名管道文件,并分别使用读、写两种方式打开,并将参数fd[0]指向读打开的文件描述符,fd[1]指向写打开的文件描述符,若创建失败则返回-1,成功则返回0 -
在创建子进程之前打开一个文件且未关闭时,创建子进程后子进程会对父进程的文件描述符表进行深拷贝,确保文件描述符指向的文件对象中维护的引用计数++,因此,若想不依赖RAII以及进程结束自动释放struct_file,就需要在父子进程中各进行一次close操作(如下图,创建子进程会复制父进程PCB,深拷贝文件描述符表,且其实图中的fiules_struct本身也是内存级的,创建子进程时也会拷贝一份)
-
建立匿名管道流程图示

注:
图中的第三步,关闭父子进程中各自不需要的文件描述符是必要步骤,原因是,若都不关闭,则会导致子进程写入数据完毕,退出,父进程read函数会读取管道文件中的数据,当读取完毕时,发现管道的写操作引用计数不为1(因为父进程的写没关),则会直接阻塞(注:一般文件由于本身大小确定,就算同时被读写方式打开,当读取结束,即读到文件结束符时,read函数会直接返回0,而非阻塞,但管道文件是特例) -
匿名管道是可以允许多个进程共同使用的,但在实际应用中,为避免数据混乱,一般仅用来进行一对一通信
匿名管道代码示例:
https://gitee.com/xiao-dongs-code-repository/gitee_dmk/tree/master/Linux/2026_2_27
匿名管道的5种特性及4种通信情况:
5种特性:
- 匿名管道,只能用来进行具有血缘关系的进程进行进程间通信
- 管道文件,自带同步机制(管道的读写操作具有阻塞特性,因此内核能够在读写双方之间提供基本的同步机制)
- 管道是面向字节流的
- 所有的管道都是单向通信的,也就是半双工中的一种特殊情况(注,若创建匿名管道时不进行第三步(即上面提到的关闭父子中不需要的文件描述符),这样的管道也不能称为双向通信,因为
双向通信有"数据相互独立,不会造成混乱"的隐含条件 - 所有文件(包括管道文件)的fd的生命周期,是随进程的(即若不使用close,dup2等函数主动关闭进程中打开的文件,则该文件会一直存在至该进程结束,进程结束时则会自动关闭)
4种情况:
-
写慢读快:类似创建匿名管道时注释说明的情况,读端被阻塞,直到写端关闭或继续写入数据

-
写快读慢:当管道文件被写满时,为防止再次写入覆盖上面未被读取的数据,会将写端阻塞

-
写关读继续:会将管道中的全部数据读完,然后检测到写端的引用计数为0,则read函数会直接返回0(类似普通文件读取到文件结束符的行为)

-
读关写继续:很显然,这是一种无用功,就算再怎么写,也不会被其他进程得知,而由于操作系统会检测无用行为,因此会直接使用13(SIGPIPE)信号杀掉写端进程,不过值得一提的是,这个信号并不是在读端关闭后直接发送信号(即不是close()后就立刻杀死对应读端进程),而是
当读端被关闭,写端再一次尝试写入后,发现对应读端被关闭,才会发送13信号(可将写端使用getline函数手动输入数据测试出这个性质)
4种情况的测试代码:
命名管道
- 创建命名管道:可使用位于sys/stat.h库的
mkfifo(const char *pathname, mode_t mode)系统调用,函数功能为创建一个命名管道,参数pathname为管道文件名,mode为管道文件的权限(常使用八进制数0666,但需注意系统的权限掩码umask),返回值为0表示成功,返回-1表示失败 - 与匿名管道不同,命名管道与普通文件类似,会在指定目录下创建一个文件类型为p的命名管道文件,且匿名管道仅能实现具备血缘关系的进程(如父子进程中需要子进程继承父进程文件描述符表来实现通信),
但命名管道由于其原理与一般文件极其相似,可允许任意进程之间进行通信 - 使用命名管道进行通信的方式很简单,
首先使用mkfifo函数创建一个命名管道文件,然后想通信的进程间仅需要对该命名管道使用常规文件操作即可,如一个进程server使用read方式打开命名管道,另一个进程client使用write方式打开同一个命名管道,接下来,server与client之间就可以通过这个命名管道fifo实现通信了 - 命名管道也支持多个进程相互通信
- 可使用unlink系统调用函数删除文件(本质是删除硬连接,当删除后引用计数为0时会删除文件,可用于删除普通文件、命名管道等除目录外的文件)
命名管道的4种特性及4种通信情况
命名管道其实与匿名管道的5种特性及4种通信情况一样,只不过没有匿名管道只能血缘通信的限制
命名管道代码示例:
https://gitee.com/xiao-dongs-code-repository/gitee_dmk/blob/master/Linux/2026_3_7/mkfifo_server.cpp
命名管道的RAII封装示例:
https://gitee.com/xiao-dongs-code-repository/gitee_dmk/tree/master/Linux/2026_3_8
systemV共享内存
共享内存的特性
- 共享内存会挂载到内存中的共享区
- 共享内存不会随着进程的结束而自动销毁,其生命周期是跟随操作系统的
- 共享内存具备shmid编号,从1开始递增,就算删除的前面分配的共享内存,再次创建也不会重复使用前面的编号(除非编号到达极限,则会巡回使用)
- 建议使用shmid控制共享内存,而非创建共享内存时使用的key
- 共享内存的使用方式类似malloc函数申请的堆区空间,只不过shmget申请的空间会被挂载至共享区
- systemV共享内存本身为通信速度舍弃了信息的准确传递(不具备信息的保护性),在现代工程种很少使用,但老项目中可能常见
共享内存的创建
- 创建:使用
shmget(key_t key, size_t size, int shmflg)系统调用,参数key为命名键值,size为申请内存的字节数,shmflg为共享内存的权限,若创建成功,则返回值为共享内存在系统中的编号(shmid),失败则返回-1,其中key一般需要通过ftok(const char *pathname, int proj_id)函数获取,第一个参数建议传一般文件路径 + 名称,而非目录文件(当然目录文件也可以生成key),不过切记,不能使用不存在的文件进行创建,第二个参数没有要求(ftok函数本质是一个算法,其依赖pathname中的inode编号,因此,若传递相同的pathname与proj_id,则返回的key相同) - 挂载:使用
shmat(int shmid, const void *shmaddr, int shmflg)系统调用,参数shmid为共享内存的ID,shmaddr为共享内存的起始地址,shmflg为共享内存属性,若成功则返回值为共享内存的映射到调用的进程的起始虚拟地址,挂载失败则返回-1 - 卸载:使用
shmdt(const void *shmaddr)系统调用,参数shmaddr为共享内存映射到调用进程的起始虚拟地址,返回值为0表示成功,返回-1表示失败 - 删除:使用
shmctl(int shmid, int cmd, struct shmid_ds *buf)系统调用,其具备多种功能,但需要删除时,参数shmid为信号量的ID,cmd应填入IPC_RMID,,buf处传入nullptr即可,返回值为0表示成功,返回-1表示失败,需要注意的是,该函数的更准确功能是,将共享内存标记为延迟删除,当没有任何一个进程挂载该共享内存时,该内存才会被删除
创建共享内存代码示例:
https://gitee.com/xiao-dongs-code-repository/gitee_dmk/tree/master/Linux/2026_3_10