一、初步了解进程间通信
1、进程间通信的目的
-
数据传输:一个进程需要将自身数据发送至另一个进程,完成数据交互。
-
资源共享:多个进程共用系统同一类资源,避免资源重复分配、浪费。
-
通知事件:一个进程向单个或一组进程发送消息,传递状态变更通知,典型场景为子进程退出后通知父进程。
-
进程控制:部分进程需要全权管控另一个进程的执行流程、运行状态。
2、进程间通信的分类
- 管道通信:匿名管道、命名管道
下文将按上述分类,逐一详细介绍各类进程间通信方式的原理、使用规则、代码实例及核心特点。
二、匿名管道
头文件:#include <unistd.h>
功能:创建无名管道,实现进程间数据传输
函数原型:int pipe(int fd2);
参数说明:fd 为文件描述符数组,其中 fd0 对应管道读端,fd1 对应管道写端
返回值:创建成功返回 0,失败返回错误编码
1、初识匿名管道
匿名管道无文件名、文件路径等可被其他进程检索的标识,无法被任意进程直接访问。它主要依靠 fork() 函数,让子进程继承父进程的管道文件描述符,因此仅支持有亲缘关系的进程通信。
匿名管道通信的核心本质:通信双方必须持有指向内核中同一个管道对象的文件描述符,才能完成数据交互。
2、父子进程共享管道的操作流程
-
父进程调用 pipe() 函数创建匿名管道,将管道读写两端绑定到文件描述符数组 fds 中;
-
父进程调用 fork() 创建子进程,子进程会完整继承父进程的 fds 数组,即同时拥有管道的读写文件描述符;
-
父子进程根据通信需求,各自关闭自身不需要的管道端口,形成单向通信的管道链路。
3、基于文件描述符的全程解析
系统默认进程的 0、1、2 号文件描述符分别对应标准输入、标准输出、标准错误,处于已占用状态。
第一步:父进程调用 pipe() 创建管道,系统会分配空闲的文件描述符,通常读端为 3 号、写端为 4 号,并将 3 号存入 fd0、4 号存入 fd1,此时父进程持有管道完整的读写权限。
第二步:父进程执行 fork() 生成子进程,子进程会完全复刻父进程的文件描述符表,同步继承管道的 3 号读端、4 号写端。此时内核中的同一个管道对象,被父子进程共同持有,存在两组读写端口。
第三步:为实现规范的单向通信,父子进程分别关闭无用端口。最终形成一个进程独占读端、一个进程独占写端的通信结构,避免数据读写混乱。
4、内核角度本质示意图

5、测试样例
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
// 创建匿名管道
if(pipe(pipefd)==-1)
ERR_EXIT("pipe error");
pid_t pid;
// 创建子进程
pid=fork();
if(pid==-1)
ERR_EXIT("fork error");
// 子进程逻辑:关闭读端,向管道写入数据
if(pid==0)
{
close(pipefd[0]);
write(pipefd[1],"hello",5);
close(pipefd[1]);
exit(EXIT_SUCCESS);
}
// 父进程逻辑:关闭写端,从管道读取数据
close(pipefd[1]);
char buf[10] = {0};
read(pipefd[0], buf, 10);
printf("buf=%s\n", buf);
return 0;
}
6、管道读写规则
(1)管道无数据可读时
-
非阻塞模式关闭(默认状态):read 函数阻塞,进程暂停执行,持续等待直到有进程向管道写入数据;
-
非阻塞模式开启:read 函数直接返回 -1,错误码 errno 赋值为 EAGAIN。
(2)管道缓冲区写满时
-
非阻塞模式关闭(默认状态):write 函数阻塞,进程暂停执行,等待其他进程读取数据、释放缓冲区空间;
-
非阻塞模式开启:write 函数直接返回 -1,错误码 errno 赋值为 EAGAIN。
(3)特殊读写场景
-
若管道所有写端文件描述符全部关闭,read 函数读取完现有数据后会返回 0,代表管道数据传输结束;
-
若管道所有读端文件描述符全部关闭,进程执行 write 操作会触发 SIGPIPE 信号,默认会导致当前写进程异常退出;
-
单次写入数据量小于等于 PIPE_BUF 时,Linux 系统保证数据写入的原子性,数据不会穿插混乱;
-
单次写入数据量大于 PIPE_BUF 时,系统不再保证写入原子性,多进程并发写入时可能出现数据交错。
7、匿名管道核心特点
-
通信范围受限:仅支持有共同祖先、存在亲缘关系的进程通信,通常由父进程创建管道,通过 fork 传递给子进程使用;
-
服务类型:提供流式数据传输服务,数据以字节流形式传递;
-
生命周期:管道资源随进程消亡,所有关联进程退出后,内核自动释放管道资源;
-
同步互斥机制:内核自动对管道的读写操作做同步与互斥处理,避免并发读写冲突;
-
通信方式:半双工通信,数据仅能单向流动;若需双向交互,必须创建两个独立管道。
8、必须掌握的四种核心状态规则
| 管道状态 | 执行操作 | 最终执行结果 |
|---|---|---|
| 读端正常,管道缓冲区已满 | write() | 进程阻塞,等待缓冲区释放空闲空间 |
| 所有写端全部关闭 | read() | 管道有数据则正常读取,数据读取完毕后返回 0 |
| 所有读端全部关闭 | write() | 触发 SIGPIPE 信号;若忽略该信号,函数返回 -1,错误码为 EPIPE |
三、命名管道(FIFO)
匿名管道的核心局限是仅支持亲缘进程通信,为实现无关联、独立进程之间的通信,系统提供了命名管道(FIFO)。命名管道本质是一种特殊的文件类型,可通过文件路径被任意进程访问。
命名管道的特殊性:普通文件的文件名对应磁盘上的实体数据,而命名管道的文件名仅存储在文件系统目录项中,用于让进程定位内核管道对象;真正的通信数据不存储在磁盘,而是存放在内核的管道缓冲区中。
简单映射关系: 普通文件:文件名 → 磁盘文件数据 命名管道:文件名 → 内核管道对象 → 内核管道缓冲区
1、命名管道的创建方式
(1)终端命令创建
$ mkfifo filename
(2)系统调用函数创建
函数原型:int mkfifo(const char *filename,mode_t mode);
功能:创建指定权限的命名管道文件
2、命名管道与匿名管道的核心区别
-
匿名管道:通过 pipe() 函数直接创建并完成初始化打开,无实体文件;
-
命名管道:通过 mkfifo() 函数创建管道文件,需通过 open() 函数手动打开后才能使用;
-
核心共性:二者内核缓冲区、数据读写语义、通信规则完全一致;
-
唯一差异:获取管道对象的方式不同,匿名管道依靠进程继承文件描述符,命名管道依靠文件路径查找内核管道对象。
3、命名管道打开规则
(1)以读模式打开 FIFO
-
非阻塞模式关闭:进程阻塞等待,直到有其他进程以写模式打开该管道;
-
非阻塞模式开启:直接打开成功,可立即等待读取数据。
(2)以写模式打开 FIFO
-
非阻塞模式关闭:进程阻塞等待,直到有其他进程以读模式打开该管道;
-
非阻塞模式开启:打开失败,函数返回 -1,错误码为 ENXIO。
4、代码实例
实例1:读取本地文件,写入命名管道
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
// 创建命名管道
mkfifo("tp", 0644);
// 打开本地待读取文件
int infd; infd = open("abc", O_RDONLY);
if (infd == -1)
ERR_EXIT("open");
// 以写模式打开命名管道
int outfd;
outfd = open("tp", O_WRONLY);
if (outfd == -1)
ERR_EXIT("open");
// 循环读取文件数据,写入管道
char buf[1024];
int n;
while ((n=read(infd, buf, 1024))>0)
{
write(outfd, buf, n);
}
// 关闭文件描述符
close(infd);
close(outfd);
return 0;
}
实例2:读取命名管道数据,写入目标文件
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
// 创建并打开备份目标文件
int outfd;
outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1)
ERR_EXIT("open");
// 以读模式打开命名管道
int infd;
infd = open("tp", O_RDONLY); if (infd == -1)
ERR_EXIT("open");
// 循环读取管道数据,写入目标文件
char buf[1024];
int n;
while ((n=read(infd, buf, 1024))>0)
{
write(outfd, buf, n);
}
// 关闭资源、删除管道文件
close(infd);
close(outfd);
unlink("tp");
return 0;
}
5、命名管道删除规则
通过 unlink 或 rm 命令删除命名管道文件时,仅会删除文件系统中的目录项,不会立刻回收内核中的管道资源。对于已经打开该管道的进程,仍可通过已持有的文件描述符正常通信。
只有当所有持有该管道文件描述符的进程全部关闭端口后,内核才会彻底回收管道缓冲区资源。删除目录项后,新的进程将无法通过路径找到该管道。
四、System V 共享内存
共享内存是 System V IPC 中效率最高的通信方式,核心原理是让多个进程映射同一块内核内存区域,进程可直接读写该内存,无需数据拷贝,极大提升通信效率。
1、共享内存示意图

2、共享内存核心数据结构
内核会为每一块共享内存维护一个 shmid_ds 结构体,用于记录内存属性、权限、运行状态等信息:
cpp
struct shmid_ds
{
struct ipc_perm shm_perm; // 共享内存操作权限
int shm_segsz; // 共享内存段大小(字节)
__kernel_time_t shm_atime; // 最后一次挂载时间
__kernel_time_t shm_dtime; // 最后一次卸载时间
__kernel_time_t shm_ctime; // 最后一次属性修改时间
__kernel_ipc_pid_t shm_cpid; // 创建进程PID
__kernel_ipc_pid_t shm_lpid; // 最后一次操作进程PID
unsigned short shm_nattch; // 当前挂载的进程数量
unsigned short shm_unused; // 兼容保留字段
void *shm_unused2; // 兼容保留字段
void *shm_unused3; // 闲置保留字段
};
3、共享内存核心操作函数
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存连接到进程地址空间
-
shmid:共享内存标识
-
shmaddr:指定连接的地址
-
shmflg:可能取值是
SHM_RND和SHM_RDONLY返回值 :成功返回一个指针,指向共享内存首地址;失败返回(void *)-1int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离
-
shmaddr:由shmat返回的指针 返回值:成功返回0;失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:用于控制共享内存
- shmid:由shmget返回的共享内存标识码
- cmd:将要采取的动作(有三个可取值)
IPC_STAT获取共享内存属性IPC_SET修改共享内存属性IPC_RMID标记删除共享内存 补充:共享内存对象被标记为删除;已经映射它的进程仍然可以通过原来的映射访问这块内存,直到最后一个进程解除映射后,内核才真正回收它。(与命名管道类似)
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0,失败返回-1
4、实例
makefile
.PHONY:all
all:server client
client:client.c comm.c
gcc -o $@ $^
server:server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server
comm.h
cpp
#ifndef _COMM_H_
#define _COMM_H_
# include <stdio.h>
# include <sys/types.h>
# include <sys/ipc.h>
# include <sys/shm.h>
# define PATHNAME "."
# define PROJ_ID 0x6666
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
# endif
comm.c
cpp
#include "comm.h"
static int commShm(int size, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0){
perror("ftok");
return -1;
}
int shmid = 0;
if( (shmid = shmget(key, size, flags)) < 0){
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0){
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT|IPC_EXCL|0666);
}
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}
server.c
cpp
#include "comm.h"
int main()
{
int shmid = createShm(4096);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i++<26){
printf("client# %s\n", addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}
client.c
cpp
#include "comm.h"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i<26){
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}