Linux 进程间通信(IPC)| 原理、函数与源码示例

注:本文为 "Linux 进程间通信(IPC)" 相关合辑。

图片清晰度受引文原图所限。

略作重排,未整理去重。

如有内容异常,请看原文。


Linux 基础------Linux 进程间通信(IPC)机制总结

yexz 原创于 2016-07-23 21:33:12 发布

在 Linux 系统中,多个进程之间的数据交互机制统称为 IPC(Inter-Process Communication),该机制为进程间的数据交互提供实现方式。Linux 系统内置多种进程间通信方式,包含半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件、套接字等。各类通信机制可用于搭建 Linux 网络服务程序的运行框架。

1. 管道(PIPE)

管道本质为一段用于进程交互的共享内存区域。创建管道的进程称为管道服务端,接入管道的进程称为管道客户端。数据由一端进程写入管道后,可由另一端进程读取。

管道特性

  1. 管道为半双工模式,数据仅支持单向传输;若需双向数据交互,需要创建两组管道。
  2. 管道仅适用于存在亲缘关系的进程,如 forkexec 生成的子进程。使用 exec 创建新进程时,需将管道文件描述符作为参数传入新进程。基于 fork 创建父子进程进行通信时,数据发送端关闭管道读端,数据接收端关闭管道写端。
  3. 管道拥有独立的文件体系。对于接入管道的进程,管道表现为文件形态,但不属于常规文件系统,仅驻留于内存空间。
  4. 数据读写遵循固定规则:写入的数据追加至管道缓冲区尾部,读取操作从缓冲区头部提取数据。

管道实现原理

管道由操作系统内核统一管理,本质为内存缓冲区。管道一端关联进程输出流,负责写入数据;另一端关联进程输入流,负责读取数据。缓冲区采用环形数据结构设计,可循环复用存储空间。

管道内无数据时,读取进程进入阻塞状态,直至有数据写入;管道缓冲区写满时,写入进程进入阻塞状态,直至数据被读取。所有关联进程终止后,管道资源自动释放。管道仅可在本地主机使用,无法跨网络完成通信。

pipe 函数原型

cpp 复制代码
#include <unistd.h>

int pipe(int file_descriptor[2]);

函数功能:创建管道。调用成功时,数组内填充两个文件描述符,函数返回 0 0 0;调用失败时,函数返回 − 1 -1 −1。

调用示例:

cpp 复制代码
int fd[2];
int result = pipe(fd);

管道通过 readwrite 系统调用完成数据读写。数据写入使用 file_descriptor[1],数据读取使用 file_descriptor[0],数据流转遵循先进先出规则。

管道读写规则

  • 管道无待读取数据

    • O_NONBLOCK 状态关闭:read 调用触发阻塞,进程暂停运行,直至数据写入管道。
    • O_NONBLOCK 状态开启:read 调用返回 − 1 -1 −1,全局变量 errno 赋值为 EAGAIN
  • 管道缓冲区已满

    • O_NONBLOCK 状态关闭:write 调用触发阻塞,直至数据被其他进程读取。
    • O_NONBLOCK 状态开启:write 调用返回 − 1 -1 −1,全局变量 errno 赋值为 EAGAIN
  • 管道所有写端文件描述符关闭时,read 调用返回 0 0 0。

  • 管道所有读端文件描述符关闭时,write 操作会向进程发送 SIGPIPE 信号。

单次写入数据长度小于等于 PIPE_BUF 时,Linux 系统保障写入操作的原子性。POSIX.1 标准规定 PIPE_BUF 最小值为 512 512 512 字节。单次写入数据长度大于 PIPE_BUF 时,系统不再保障写入操作的原子性。

2. 命名管道(FIFO)

命名管道属于特殊文件,以文件形式存储于系统中。该机制突破普通管道的使用限制,支持无亲缘关系的进程完成通信。

系统调用原型

mkfifo
cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);

函数功能:创建命名管道。filename 为管道文件路径,mode 为文件权限,最终权限取值为 mode & ∼ umask \text{mode} \& \sim \text{umask} mode&∼umask。调用成功返回 0 0 0,调用失败返回 − 1 -1 −1,错误信息存储于 errno

调用示例:

cpp 复制代码
mkfifo("/tmp/cmd_pipe", S_IFIFO | 0666);
mknod
cpp 复制代码
int mknod(const char *path, mode_t mode, dev_t dev);

函数功能:创建指定类型文件。path 为文件路径,mode 为文件类型与权限,dev 为设备编号。仅字符设备、块设备需要填写设备编号,创建命名管道时该参数赋值为 0 0 0。

调用示例:

cpp 复制代码
mknod(FIFO_FILE, S_IFIFO | 0666, 0);

命名管道创建完成后,可通过 openreadwrite 等标准文件操作接口进行数据交互。命名管道支持手动创建与代码创建两种方式。

管道与命名管道的差异

命名管道的 IO 操作逻辑与普通管道基本一致,二者区别体现在创建与接入方式。命名管道可提前通过命令行执行 mkfifo myfifo 完成创建,进程需要主动调用 open 函数建立连接。普通管道由主进程创建,fork 生成子进程时会自动复制管道文件描述符,使用 exec 生成新进程时可传递文件描述符参数。

默认状态下,FIFO 与 PIPE 均为阻塞模式。若以读权限打开命名管道,读进程会保持阻塞,直至其他进程写入数据;反之同理。在 open 调用中添加 O_NONBLOCK 标志,可关闭默认阻塞特性。

3. 信号(signal)

信号是 UNIX 体系中出现较早的进程通信方式,用于在进程间传递异步事件指令。键盘中断等异步事件均可触发信号,Shell 程序也会借助信号向子进程下发作业控制指令。

相关函数原型

cpp 复制代码
#include <sys/types.h>
#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

函数功能:捕获系统信号。sig 为信号编号,func 为自定义信号处理函数指针。函数返回原有信号处理函数指针。

调用示例:

cpp 复制代码
int ret = signal(SIGSTOP, sig_handle);

signal 函数运行稳定性存在局限,工程开发中可选用 sigaction 函数完成同类功能。

cpp 复制代码
int kill(pid_t pid, int sig);

函数功能:向指定进程发送信号。pid 为目标进程 ID,sig 为信号编号。pid 赋值为 0 0 0 时,信号发送至系统内所有进程。

cpp 复制代码
int raise(int sig);

函数功能:向当前进程发送指定信号。

cpp 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

函数功能:定时触发 SIGALRM 信号。seconds 为定时时长,单位为 秒。seconds 赋值为 0 0 0 时,取消已设置的定时任务,函数返回剩余定时时长。同一进程内多次调用 alarm 会覆盖上一次的定时配置。

cpp 复制代码
int pause(void);

函数功能:将调用进程或线程置为休眠状态,直至接收到信号。进程会根据信号配置执行退出或调用信号捕获函数。

4. 消息队列(Message queues)

消息队列是内核空间维护的链式存储结构,依托内核完成进程间数据转发。数据按顺序写入队列,进程可通过多种规则读取队列数据。单个消息队列通过唯一 IPC 标识符区分,系统内不同消息队列相互独立,队列内部数据同样以链表形式组织。

消息队列补充了信号数据承载量不足、管道仅支持无格式字节流的使用局限。

头文件引用

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/msg.h>

数据结构

消息缓冲区
cpp 复制代码
struct msgbuf{
    long mtype;
    char mtext[1];  // 柔性数组
};

mtype 用于标识消息类型,可实现定向收发数据;mtext 为消息数据存储区域,采用柔性数组设计,支持自定义结构体。

Linux 系统对单条消息长度存在限制,该约束定义于 linux/msg.h

c 复制代码
#define MSGMAX 8192

单条消息总长度不得超过 8192 8192 8192 字节,该长度包含 mtype 成员占用的 4 4 4 字节。

msqid_ds 结构体

该结构体由内核为每个消息队列维护,用于记录队列运行状态,定义于 linux/msg.h

cpp 复制代码
struct msqid_ds{
    struct ipc_perm msg_perm;
    time_t msg_stime;
    time_t msg_rtime;
    time_t msg_ctime;
    unsigned long _msg_cbuyes;
    // 其余成员省略
};
ipc_perm 结构体

该结构体记录 IPC 对象权限与归属信息,定义于 linux/ipc.h

cpp 复制代码
struct ipc_perm{
    key_t key;
    uid_t uid;
    gid_t gid;
    // 其余成员省略
};

结构体包含 IPC 对象键值、所属用户 ID、所属用户组 ID 等信息。

常用函数

创建消息队列、信号量、共享内存这类 XSI IPC 对象时,通常借助 ftok 函数生成键值。

cpp 复制代码
key_t ftok(const char * fname, int id);

函数说明:fname 为文件或目录路径,id 为自定义标识。函数结合文件索引节点与标识值生成 key_t 类型键值。

调用示例:

cpp 复制代码
key_t key = ftok(".", 1);
cpp 复制代码
int msgget(key_t key, int msgflag);

函数功能:创建或接入消息队列,通过键值匹配目标队列。

调用示例:

cpp 复制代码
int msg_id = msgget(key, IPC_CREAT | IPC_EXCL | 0666);

参数说明:IPC_CREAT 表示创建新队列,IPC_EXCL 表示队列已存在则调用失败,0666 为队列访问权限。

cpp 复制代码
int msgsnd(int msgid, const void *msgptr, size_t msg_sz, int msgflg);

函数功能:向消息队列写入数据。msgid 为队列标识符,msgptr 为消息缓冲区指针,缓冲区首成员必须为长整型,msg_sz 为消息数据长度,msgflg 为操作标志。

调用示例:

cpp 复制代码
struct msgmbuf{
    int mtype;
    char mtext[10];
};
struct msgmbuf msg_mbuf;
msg_mbuf.mtype = 10;
memcpy(msg_mbuf.mtext, "测试消息", sizeof("测试消息"));
int ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"), IPC_NOWAIT);
cpp 复制代码
int msgrcv(int msgid, void *msgptr, size_t msg_sz, long int msgtype, int msgflg);

函数功能:从消息队列读取数据。msgtype 用于指定读取对应类型的消息,msg_sz 仅计算数据区域长度,不含 mtype 成员。

调用示例:

cpp 复制代码
int ret = msgrcv(msg_id, &msg_mbuf, 10, 10, IPC_NOWAIT | MSG_NOERROR);
cpp 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

函数功能:执行消息队列控制操作。常用指令如下:

  • IPC_STAT:读取队列状态至 buf 指向的结构体;
  • IPC_SET:用 buf 数据更新队列配置;
  • IPC_RMID:删除消息队列,此时 buf 赋值为 NULL

消息队列工作逻辑

消息队列采用链表结构组织数据,通过队列标识符进行管理。msgget 负责创建或接入队列,msgsnd 向队列尾部追加消息,msgrcv 读取消息。消息读取不强制遵循先进先出规则,可依据消息类型筛选数据。

消息队列与命名管道对比

两类机制均支持无亲缘关系进程通信,数据交互均采用收发模式,且单条数据存在长度上限。命名管道使用 writeread 完成读写,消息队列使用 msgsndmsgrcv 完成读写。

消息队列具备以下特征:

  1. 运行周期独立于收发进程,可减少命名管道启停同步引发的问题;
  2. 自带同步逻辑,无需开发者额外编写同步代码;
  3. 支持按消息类型筛选接收数据。

5. 信号量(Semaphore)

信号量属于计数器,用于管控多进程对共享资源的访问,常作为互斥锁使用,限制多个进程同时操作同一共享资源。

信号量取值为非负整数,仅支持两类基础操作,即 P 操作与 V 操作:

  • P 操作(申请资源):若信号量数值大于 0 0 0,数值减 1 1 1;若数值等于 0 0 0,当前进程阻塞。
  • V 操作(释放资源):若存在因该信号量阻塞的进程,唤醒对应进程;若无阻塞进程,信号量数值加 1 1 1。

头文件引用

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sem.h>

数据结构

semid_ds 结构体

内核为每组信号量集合维护该结构体,记录集合状态:

cpp 复制代码
struct semid_ds{
    struct ipc_perm sem_perm;
    unsigned short sem_nsems;
    time_t sem_otime;
    time_t sem_ctime;
    // 其余成员省略
};
semun 联合体

用于信号量配置与状态读取:

cpp 复制代码
union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};
sembuf 结构体

用于定义信号量操作行为:

cpp 复制代码
struct sembuf{
    ushort sem_num;    // 信号量在集合中的编号
    short sem_op;      // 信号量操作值
    short sem_flg;     // 操作标志,常用 IPC_NOWAIT
};

sem_op 取值规则:正数代表信号量数值增加,负数代表信号量数值减少, 0 0 0 代表进程阻塞直至信号量数值为 0 0 0。

常用函数

cpp 复制代码
int semget(key_t key, int num_sems, int sem_flags);

函数功能:创建或接入信号量集合。num_sems 为集合内信号量个数,相同 key 值的进程可访问同一组信号量。

调用示例:

cpp 复制代码
int semid = semget(key, 0, IPC_CREAT | IPC_EXCL | 0666);
cpp 复制代码
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

函数功能:执行信号量运算。sem_ops 为操作数组,num_sem_ops 为数组元素个数。

调用示例:

cpp 复制代码
struct sembuf sops = {0, +1, IPC_NOWAIT};
semop(semid, &sops, 1);
cpp 复制代码
int semctl(int sem_id, int sem_num, int command, ...);

函数功能:信号量集合控制操作。常用指令:

  • IPC_STAT:读取信号量集合状态;
  • IPC_SET:修改信号量集合配置;
  • IPC_RMID:删除信号量集合;
  • GETVAL:读取单个信号量数值;
  • SETVAL:设置单个信号量数值。

6. 共享内存(Share Memory)

共享内存在进程地址空间中划分出一段公共内存区域,多个进程可将该物理内存映射至自身地址空间。任意进程修改区域内数据后,其他接入的进程可立即感知数据变化。

共享内存的数据转发链路较短。管道、消息队列等机制需要内核中转数据,共享内存直接完成物理内存映射,数据无需额外拷贝。该机制本身不提供同步控制,同步逻辑需要开发者自行实现。

头文件引用

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/shm.h>

shmid_ds 结构体

内核用于记录共享内存段状态:

cpp 复制代码
struct shmid_ds{
    struct ipc_perm shm_perm;
    size_t shm_segsz;
    time_t shm_atime;
    time_t shm_dtime;
    // 其余成员省略
};

常用函数

cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);

函数功能:创建或接入共享内存段。size 为内存段字节长度,相同 key 值的进程可接入同一段共享内存。

调用示例:

cpp 复制代码
int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0666);
cpp 复制代码
void *shmat(int shm_id, const void *shm_addr, int shmflg);

函数功能:将共享内存映射至当前进程地址空间。shm_addr 赋值为 0 0 0 时,由内核自动分配映射地址,函数返回映射后的内存首地址。

调用示例:

cpp 复制代码
char *shms = (char *)shmat(shmid, 0, 0);
cpp 复制代码
int shmdt(const void *shm_addr);

函数功能:解除当前进程与共享内存的映射关系。

调用示例:

cpp 复制代码
shmdt(shms);
cpp 复制代码
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

函数功能:共享内存控制操作,使用方式与 msgctl 保持一致。

XSI IPC 共性(消息队列、信号量、共享内存)

三类机制统称为 XSI IPC,具备如下特征:

  1. 内核均维护专属状态结构体,分别为 msgid_dssemid_dsshmid_ds
  2. 通过非负整数标识符引用对象,标识符由对应创建函数返回;
  3. 每个 IPC 对象绑定 key_t 类型键值,作为外部访问标识。

XSI IPC 与管道、命名管道的差异

  1. 生命周期:XSI IPC 对象无引用计数,进程终止后对象及内部数据会持续保留;普通管道在所有关联进程终止后直接销毁;命名管道文件会保留,但内部数据在所有进程断开连接后清空。
  2. 访问方式:XSI IPC 不依赖文件描述符,无法使用常规文件指令管理,需借助 ipcsipcrm 工具查看与删除。管道与命名管道遵循文件体系规则。

7. 内存映射(Memory Map)

内存映射文件将磁盘文件与进程虚拟地址空间建立映射关系。进程操作映射内存等价于直接操作磁盘文件,可减少常规文件读写的数据拷贝次数。该机制既可以实现进程间通信,也可优化大文件的读写效率。

常规文件读写存在四次数据拷贝,内存映射借助缺页中断完成数据加载,仅执行一次数据拷贝。多进程映射同一个磁盘文件时,可通过映射内存完成数据交互。

头文件引用

cpp 复制代码
#include <sys/mman.h>

常用函数

cpp 复制代码
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

函数功能:创建内存映射。start 为映射起始地址,赋值为 0 0 0 由内核分配;length 为映射长度;prot 为内存访问权限;flags 为映射类型;fd 为文件描述符;offset 为文件内偏移量。调用成功返回映射地址,失败返回 MAP_FAILED

cpp 复制代码
int munmap(void *start, size_t length);

函数功能:解除内存映射。调用成功返回 0 0 0,调用失败返回 − 1 -1 −1。

cpp 复制代码
int msync(void *addr, size_t len, int flags);

函数功能:同步映射内存数据至磁盘文件,保障数据一致性。

共享内存与内存映射文件对比

内存映射文件以磁盘文件为载体,依托虚拟地址空间实现文件访问,适用于大文件、高频文件读写场景。共享内存可看作内存映射的特殊形式,其映射对象为物理内存,而非磁盘文件,应用于进程间数据交互场景。

内存映射文件与虚拟内存对比

关联

二者均属于操作系统内存管理模块,都会将部分数据驻留内存、其余数据存放至磁盘,相关处理逻辑对上层应用透明。

区别

虚拟内存是内存与磁盘的数据交换区域,基于分页机制与局部性原理工作,用于扩充物理内存的可用空间。内存映射文件直接将磁盘文件映射至虚拟地址空间,跳过文件缓冲区,直接通过内存地址访问文件内容。

8. 套接字

套接字支持本地进程通信与跨网络主机进程通信。该机制划分服务端与客户端架构,单台服务端可同时对接多个客户端。

通信流程

  1. 服务端流程

    • 调用 socket 创建套接字;
    • 调用 bind 绑定地址与端口;
    • 调用 listen 监听连接请求,维护连接队列;
    • 客户端发起连接后,调用 accept 新建套接字,用于和对应客户端通信。
  2. 客户端流程

    • 调用 socket 创建套接字;
    • 调用 connect 向服务端发起连接请求。

连接建立完成后,可像操作普通文件一样通过套接字完成数据收发。

基础函数原型

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr *address, size_t address_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);
int connect(int socket, const struct sockaddr *addrsss, size_t address_len);

套接字编程可参考消息队列的消息类型设计,对不同客户端的数据进行分类处理。

参考文献

1 mickole. Linux 进程间通信总结EB/OL. (2013-07-15). https://www.cnblogs.com/mickole/p/3192210.html.

2 nodeathphoenix. Linux 进程间通信详解EB/OL. (2014-04-16). https://blog.csdn.net/nodeathphoenix/article/details/23284157.

3 ljianhui. Linux 进程间通信(IPC)总结EB/OL. (2013-04-20). https://blog.csdn.net/ljianhui/article/details/10287879.

4 kunhu. Linux 进程间通信方式详解EB/OL. (2014-07-15). https://www.cnblogs.com/kunhu/p/3608589.html.

5 kobejayandy. Linux IPC 机制详解EB/OL. (2013-01-20). https://blog.csdn.net/kobejayandy/article/details/18863543.

6 lbsx. Linux 下进程间通信方式介绍EB/OL. (2009-08-03). https://www.cnblogs.com/lbsx/archive/2009/08/03/1537698.html.

7 hongchangfirst. Linux 进程间通信全面讲解EB/OL. (2015-04-15). https://blog.csdn.net/hongchangfirst/article/details/11599369.

8 Linux IPC 通信机制分析EB/OL. (2009-06-12). https://blog.sina.com.cn/s/blog_4eee98350100abbr.html.

9 a987073381. Linux Socket 编程详解EB/OL. (2016-06-20). https://blog.csdn.net/a987073381/article/details/51869000.

10 作者: 宋敬彬. Linux 网络编程M. 北京: 清华大学出版社, 2014.

11 史蒂文斯. UNIX 环境高级编程M. 北京: 人民邮电出版社, 2012.


Linux 进程间通信 (Linux IPC)

CoreDump 丶. 2023-08-22 修订

本文介绍 Linux 环境下各类进程间通信机制,包含匿名管道、命名管道、信号、共享内存映射、共享内存、消息队列、信号量、UNIX 域套接字。各类机制可实现不同进程的数据交互与协同运行。文中附代码示例,展示接口用法与运行特征。

前言

Linux 系统内各进程拥有独立的地址空间,进程无法直接访问彼此的数据。数据交互需要依托内核完成:进程将数据从用户空间拷贝至内核缓冲区,另一进程再从内核缓冲区读取数据。该类由内核提供的数据交互方式,统称为进程间通信(IPC, InterProcess Communication)。

Linux 系统提供多种进程间通信方式,下文介绍八种常用类型:匿名管道、命名管道、信号、共享内存映射、共享内存、消息队列、信号量、UNIX 域套接字。日常使用频次较高的类型如下:

  1. 管道
  2. 信号
  3. 共享内存
  4. UNIX 域套接字

1 匿名管道 (PIPE)

管道是基础的 IPC 实现形式,多用于存在亲缘关系的进程之间。

系统调用

c 复制代码
int pipe(int pipefd[2]);

基本特征

  • 管道对应内核缓冲区,属于伪文件类型。
  • 管道关联两个文件描述符,分别对应写入端与读取端。
  • 数据从写入端流入,从读取端流出。
  • 采用半双工传输模式,数据仅支持单向流动。双向交互场景可创建两组管道。

实现原理

管道基于内核环形队列实现,缓冲区默认大小为 4K。

代码示例

c 复制代码
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    int fds[2];
    pid_t pid;
    int ret;
    char buf[1024];

    if (pipe(fds) < 0) {
        perror("open pipe error:");
        exit(1);
    }
    pid = fork();
    if (pid < 0) {
        perror("fork error:");
        exit(2);
    } else if (pid > 0) {
        // 父进程逻辑
        close(fds[0]);
        ret = write(fds[1], "I am your father", strlen("I am your father"));
        if (ret < 0) {
            perror("parent write pipe error:");
            exit(3);
        }
        printf("parent process id:%u, son:%u, write success\n", getpid(), pid);
        wait(NULL);
    } else {
        // 子进程逻辑
        close(fds[1]);
        ret = read(fds[0], buf, sizeof(buf));
        if (ret < 0) {
            perror("child read pipe error:");
            exit(3);
        }
        buf[ret] = '\0';
        printf("child process id:%u, father:%u, read from pipe:%s\n", getpid(), getppid(), buf);
    }

    return 0;
}

运行结果

复制代码
parent process id:2663, son:2664, write success
child process id:2664, father:2663, read from pipe:I am your father

读写规则

读取操作
  • 管道内存在数据:读取数据并返回实际读取字节数。
  • 管道内无数据:所有写入端关闭时,read 返回 0;写入端未关闭时,read 进入阻塞状态。
写入操作
  • 管道所有读取端关闭:进程收到 SIGPIPE 信号并终止运行,工程场景中可捕获该信号避免进程退出。
  • 管道读取端未全部关闭:缓冲区已满时,write 进入阻塞状态;缓冲区未满时,写入数据并返回实际写入字节数。

适用场景说明

管道部署与调用流程简洁。使用范围存在限制,仅支持单向传输,且仅可在具备亲缘关系的进程间使用。

2 命名管道 (FIFO)

命名管道可区分于匿名管道,匿名管道仅适用于有亲缘关系的进程,命名管道可实现无关联进程之间的通信。

命名管道属于 Linux 文件体系内的特殊文件,磁盘中不会存储实际数据,仅作为内核通信通道的标识。进程通过标准文件读写接口操作该文件,实际交互对象为内核通道。

系统调用

c 复制代码
int mkfifo(const char *pathname, mode_t mode);

mkfifo 创建命名管道后,可使用 openclosereadwriteunlink 等常规文件接口完成操作。

代码示例

读取端 reader.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main() {
    int fd;
    char buf[1024];
    int n;

    if (mkfifo("/tmp/myfifo", 0644) < 0) {
        perror("mkfifo error:");
        exit(1);
    }

    if ((fd = open("/tmp/myfifo", O_RDONLY)) < 0) {
        perror("open fifo error:");
        exit(1);
    }
    if ( ( n = read(fd, buf, sizeof(buf))) < 0) {
        perror("read msg error:");
        exit(1);
    }
    buf[n] = '\0';

    printf("msg:%s\n", buf);

    unlink("/tmp/myfifo");
    return 0;
}
写入端 writer.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main() {
    int fd;
    const char *msg = "hello from writer";

    if ((fd = open("/tmp/myfifo", O_WRONLY)) < 0) {
        perror("open fifo error:");
        exit(1);
    }
    if (write(fd, msg, strlen(msg)) < 0) {
        perror("write msg error:");
        exit(1);
    }

    return 0;
}

3 信号

信号是用户、系统或进程向目标进程发送的通知,用于提示进程状态变更或系统异常。

在 Shell 终端执行 kill -l 指令,可查看系统支持的信号列表:

shell 复制代码
[root@master ~]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

信号触发场景

  • 终端操作:前台进程运行时,按下 Ctrl+C 等组合键可发送对应信号。
  • 系统异常:浮点运算错误、非法内存访问等场景会触发信号。
  • 状态变化:alarm 定时器计时结束,触发 SIGALRM 信号。
  • 指令与接口调用:终端执行 kill 命令,或程序内部调用 kill 函数发送信号;kill -9 pid 用于向指定进程发送 SIGKILL 信号。

系统调用

c 复制代码
int kill(pid_t pid, int sig);

该接口用于向进程 ID 为 pid 的进程发送编号为 sig 的信号。

运行机制

进程收到信号前,按原有逻辑正常运行。检测到信号后,进程暂停当前执行流程,调用对应的信号处理函数。处理完成后,恢复原有代码执行。该运行模式与硬件中断类似,属于软件层面的异步通知机制。

信号依托软件逻辑实现,存在一定时延,该时延通常难以被用户感知。所有信号的分发与处理均由内核完成。

4 共享内存映射

存储映射 I/O(Memory-mapped I/O)将磁盘文件与进程缓冲区建立映射关系。读取映射缓冲区的数据,等价于读取对应磁盘文件内容;向缓冲区写入数据,对应内容会同步写入磁盘文件。该方式可直接通过内存地址完成文件读写,无需调用 readwrite 接口。

系统调用

c 复制代码
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

参数说明:

  • addr:映射区起始地址,传入空指针时由内核自动分配地址。
  • length:映射区域长度。
  • prot:映射区域访问权限。
  • flags:标志位,用于设置数据同步规则、共享属性、匿名映射等。
  • fd:待映射文件的文件描述符。
  • offset:文件内偏移量,取值需为 4K 的整数倍。

munmap 用于释放 mmap 创建的映射区域。

进程通信用法

亲缘进程通信

创建映射区时通过 flags 参数配置属性:

  • MAP_PRIVATE:私有映射,父子进程拥有独立的映射区域。
  • MAP_SHARED:共享映射,父子进程共用同一片映射区域。
无关联进程通信

多个进程可基于同一磁盘文件创建 MAP_SHARED 类型映射区,依托内核共享空间完成数据交互。

5 共享内存

共享内存允许多个进程访问同一段物理内存。操作系统将该段物理内存映射至不同进程的地址空间,任一进程修改数据后,其余进程可直接读取变更后的内容。进程可解除与共享内存的映射,解除后无法继续操作该内存区域。

共享内存未内置互斥与同步逻辑,多进程并发读写时,需要搭配信号量等机制保障数据一致性。

终端执行 ipcs 指令,可查看系统内共享内存、消息队列、信号量的状态信息:

shell 复制代码
[root@master uds]# ipcs

--- Message Queues --
key        msqid      owner      perms      used-bytes   messages

--- Shared Memory Segments --
key        shmid      owner      perms      bytes      nattch     status

--- Semaphore Arrays --
key        semid      owner      perms      nsems

系统调用

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

  • shmget:创建或获取共享内存段。key 为共享内存标识,size 为内存长度,shmflg 配置访问权限与创建规则。调用成功返回共享内存标识符,失败返回 -1。
  • shmat:将共享内存映射至当前进程地址空间。shmid 为共享内存标识符,shmaddr 可指定映射地址,置空时由内核分配。调用成功返回映射地址,失败返回 (void *)-1
  • shmdt:解除当前进程与共享内存的映射关系。
  • shmctl:执行共享内存管理操作,传入 IPC_RMID 可删除共享内存段。

代码示例

示例实现功能:一个进程读取本地文件并写入共享内存,另一进程读取共享内存数据并写入新文件。

读取并写入文件 sharemem_r.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define MAXBUFSIZE 4096
#define SHAREMEMESIZE 4096

int write_file(const char *buf, int size, const char *filename);

int main() {
    int shm_id;
    char buf[MAXBUFSIZE] = {0};
    char *shm_p = NULL;

    if ( (shm_id = shmget(0x666, SHAREMEMESIZE, 0644)) < 0) {
        perror("shmget error");
        return 1;
    }

    shm_p = (char *)shmat(shm_id, NULL, 0);
    if (shm_p == (void *) -1) {
        perror("shmat error:");
        return 1;
    }

    memcpy(buf, shm_p, MAXBUFSIZE);
    write_file(buf, strlen(buf), "write_text");
    printf("write to file: %s\n", buf);

    shmdt(shm_p);
    if (shmctl(shm_id, IPC_RMID, 0) == -1) {
        printf("shmctl failed\n");
        return -1;
    }

    return 0;
}

int write_file(const char *buf, int size, const char *filename) {
    int fd, n;
    if ((fd = open(filename, O_WRONLY | O_CREAT, 0644)) < 0) {
        perror("open file error:");
        return -1;
    }
    n = write(fd, buf, size);
    if (n < 0) {
        perror("write file error");
        return -1;
    }
    close(fd);
    return n;
}
读取文件并写入共享内存 sharemem_w.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define MAXBUFSIZE 4096
#define SHAREMEMESIZE 4096

int read_file(char *buf, int size, const char *filename);

int main() {
    int n, shm_id;
    char buf[MAXBUFSIZE] = {0};
    char *shm_p = NULL;

    if ( (shm_id = shmget(0x666, SHAREMEMESIZE, 0644 | IPC_CREAT)) < 0) {
        perror("shmget error");
        return 1;
    }

    shm_p = (char *)shmat(shm_id, NULL, 0);
    if (shm_p == (void *) -1) {
        perror("shmat error:");
        return 1;
    }

    n = read_file(buf, MAXBUFSIZE, "./read_text");
    if (n == -1) {
        return 1;
    }
    printf("read from file: %s\n", buf);
    memcpy(shm_p, buf, strlen(buf));

    shmdt(shm_p);
    return 0;
}

int read_file(char *buf, int size, const char *filename) {
    int fd, n;
    if ( (fd = open(filename, O_RDONLY)) < 0) {
        perror("open file_read_test error:");
        return -1;
    }
    n = read(fd, buf, size);
    if (n < 0) {
        perror("read file error:");
        return -1;
    }
    close(fd);
    return n;
}

6 消息队列

消息队列是内核维护的链表结构,每个队列拥有独立标识符。队列生命周期不依附于创建进程,进程退出后队列及内部数据可继续留存。进程可按照规则向队列写入或读取数据。

系统调用

c 复制代码
int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

消息数据需遵循固定结构:

c 复制代码
struct msgbuf {
    long mtype;
    char mtext[1];
};

代码示例(生产者-消费者模型)

生产者 producer.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

typedef struct {
    long mtype;
    char mtext[1024];
}msgbuf;

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;

    if ((key = ftok(".", 666)) == -1) {
        perror("ftok error:");
        return 1;
    }

    if ((msgid = msgget(key, 0644 | IPC_CREAT)) == -1) {
        perror("msgget error:");
        return 1;
    }

    msgbuf buf = {1, "surprise, mother fucker"};
    if (msgsnd(msgid, &buf, strlen(buf.mtext), 0) == -1) {
        perror("msgsnd error");
        return 1;
    }

    return 0;
}
消费者 consumer.c
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

typedef struct {
    long mtype;
    char mtext[1024];
}msgbuf;

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;

    if ((key = ftok(".", 666)) == -1) {
        perror("ftok error:");
        return 1;
    }

    if ((msgid = msgget(key, 0644 | IPC_CREAT)) == -1) {
        perror("msgget error:");
        return 1;
    }

    msgbuf buf;
    if (msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0) == -1) {
        perror("msgrcv error");
        return 1;
    }

    printf("recv msg, type:%ld, text:%s\n", buf.mtype, buf.mtext);
    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

7 信号量

信号量本质为计数器,多用于多进程之间的同步控制。

PV 操作

  • P 操作:执行计数器减一操作。若计数器当前值为 0,执行操作的进程进入阻塞状态。
  • V 操作:执行计数器加一操作。若存在因该信号量阻塞的进程,唤醒对应进程。

分类与接口

System V 信号量

多用于进程间同步:

c 复制代码
int semget(key_t key, int nsems, int semflg);
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);
int semctl(int semid, int semnum, int cmd, ...);
Posix 信号量

多用于线程间同步:

c 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_getvalue(sem_t *sem, int *sval);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);

8 UNIX 域套接字 (Unix Domain Socket)

套接字接口最初用于网络通信,后续衍生出 UNIX 域套接字,作为本机进程间通信方案。该方式无需经过网络协议栈,省去数据封装、校验、序号维护等流程,数据拷贝流程精简。

UNIX 域套接字分为面向连接、面向数据报两种模式,两类模式均可以保障数据可靠传输。该机制为全双工通信,接口功能丰富,容器组件等场景常采用该方式实现通信,例如 Docker 客户端与守护进程的交互。

UNIX 域套接字的调用流程与网络套接字相近,区别在于地址格式。网络套接字使用 IP 地址加端口号,UNIX 域套接字使用文件系统路径作为地址,对应文件由 bind 接口创建。

代码示例(客户端-服务端模型)

服务端 server.c
c 复制代码
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int server_listen(const char *name) {
    int fd, len, ret;
    struct sockaddr_un un;

    if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        return -1;
    }
    unlink(name);
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);

    ret = bind(fd, (struct sockaddr *)&un, len);
    if( ret == -1) {
        close(fd);
        perror("bind error");
        return -1;
    }
    if(listen(fd, 128) < 0) {
        close(fd);
        perror("listen error");
        return -1;
    }
    return fd;
}

int server_accept(int listenfd, uid_t *uidptr)
{
	int clifd, len;
	struct sockaddr_un un;
	struct stat statbuf;

	len = sizeof(un);
	if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
        return -1;
    }
	len -= offsetof(struct sockaddr_un, sun_path);
	un.sun_path[len] = 0;

	if (stat(un.sun_path, &statbuf) < 0) {
		close(clifd);
        return -1;
	}
	if (S_ISSOCK(statbuf.st_mode) == 0) {
		close(clifd);
        return -1;
	}
	if (uidptr != NULL)
		*uidptr = statbuf.st_uid;

	unlink(un.sun_path);
	return clifd;
}

int main(int argc, char const *argv[])
{
    int lfd, cfd, n, i;
	uid_t cuid;
	char buf[1024];

	lfd = server_listen("tmp.sock");
    if (lfd < 0) {
         exit(-1);
     }
    while (1) {
        cfd = server_accept(lfd, &cuid);
        if (cfd < 0) {
            exit(-1);
        }
        while (1) {
            n = read(cfd, buf, 1024);
            if (n == -1) {
                break;
            } else if (n == 0) {
                printf("the other side has been closed.\n");
                break;
            }
            for (i = 0; i < n; i++) {
                buf[i] = toupper(buf[i]);
            }
            write(cfd, buf, n);
        }
    }
	close(cfd);
	close(lfd);
	return 0;
}
客户端 client.c
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

#define CLI_PATH "/var/tmp/"

int cli_conn(const char *name)
{
	int fd, len;
	struct sockaddr_un un;

	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        return -1;
    }
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
	len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

	unlink(un.sun_path);
	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        perror("bind error");
		close(fd);
        return -1;
	}

	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
	if (connect(fd, (struct sockaddr *)&un, len) < 0) {
		perror("connect error");
        close(fd);
        return -1;
	}
    return fd;
}

int main(void)
{
	int fd, n;
	char buf[1024];

	fd = cli_conn("tmp.sock");
	if (fd < 0) {
		exit(-1);
	}
	while (fgets(buf, sizeof(buf), stdin) != NULL) {
		write(fd, buf, strlen(buf));
		n = read(fd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, n);
	}
	close(fd);
	return 0;
}

socketpair

socketpair 可创建一对相互连接的无名套接字,多用于有亲缘关系的进程间通信,接口使用方式与管道类似,且支持全双工传输。

系统调用
c 复制代码
int socketpair(int domain, int type, int protocol, int sv[2]);

Linux 环境下,该接口仅支持 AF_UNIX 协议域。

参考文献

1 毛德操, 胡希明. Linux 内核源代码情景分析M. 杭州: 浙江大学出版社, 2001.

2 史蒂文斯 W R. UNIX 环境高级编程M. 3 版. 戚正伟, 张亚辉, 译. 北京: 人民邮电出版社, 2014.

3 宋敬彬. Linux 网络编程 (第 2 版)M. 北京:清华大学出版社, 2014.


Linux 进程间通信(IPC):管道、共享内存与信号量的原理与实战

Linux 进程间通信 (IPC)

每个进程拥有独立虚拟地址空间,进程之间无法直接访问彼此数据。复杂业务场景下,多进程协同、数据交互属于常规需求。进程间通信(IPC,Inter Process Communication)是操作系统提供的一系列机制,用于打通进程隔离边界,实现数据交换与状态同步。

IPC 分类

依据通信用途与实现形式,IPC 机制可划分为三类:

数据传输

  • 管道 (Pipes):包含匿名管道、命名管道 (FIFO),提供单向字节流通信能力。
  • 消息队列 (Message Queues):以结构化消息为单位完成通信。
  • 共享内存 (Shared Memory):多个进程映射同一块物理内存,数据交互效率较高。
  • 套接字 (Sockets):适用范围广,可实现同主机进程通信,也是网络通信的底层基础。

事件通知

  • 信号 (Signals):用于进程间异步事件下发。

同步与互斥

  • 信号量 (Semaphores):管控共享资源的并发访问行为。
  • 文件锁 (File Locks):依托文件或文件局部加锁,实现进程同步。

下文将按照由浅至深的顺序,逐一介绍各类 IPC 机制。

第一部分:管道 (Pipes)

管道是 UNIX 体系中诞生较早的 IPC 形式,本质为内核维护的单向字节流缓冲区,一端负责写入数据,一端负责读取数据。管道默认采用阻塞 I/O 模式。

1. 匿名管道 (pipe())

匿名管道不在文件系统中生成实体文件,仅可用于存在亲缘关系的进程通信,常见组合为父子进程。

匿名管道对亲缘关系的约束,源于通信进程需要从共同祖先进程继承管道文件描述符 (file descriptor)。

原理说明

进程调用 pipe() 系统调用后,内核在内存中分配缓冲区,并返回两个文件描述符:fd[0] 用于读取,fd[1] 用于写入。

实现父子进程通信,需要遵循 先调用 pipe(),再调用 fork() 的顺序。执行后,父子进程会复制得到这两个文件描述符。通过关闭闲置描述符,即可划定数据传输方向:

  • 父进程向子进程传输:父进程关闭读端 fd[0],子进程关闭写端 fd[1]
  • 子进程向父进程传输:父进程关闭写端 fd[1],子进程关闭读端 fd[0]
函数原型
c 复制代码
#include <unistd.h>
int pipe(int pipefd[2]);
  • pipefd:整型数组,调用成功后,pipefd[0] 对应读端文件描述符,pipefd[1] 对应写端文件描述符。
  • 返回值:调用成功返回 0,调用失败返回 -1。
命令行验证

Shell 中的 | 符号基于匿名管道实现。

bash 复制代码
# 将 ls 进程标准输出重定向至管道写端
# 将 grep 进程标准输入重定向至管道读端
ls -l | grep ".c"
编程练习:父子进程通信

实现逻辑:创建匿名管道,父进程向管道写入数据,子进程读取并打印数据。

c 复制代码
// practice_pipe.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[256];
    const char *message = "Hello from parent!";

    if (pipe(pipefd) == -1) {
        perror("pipe failed");
        exit(EXIT_FAILURE);
    }

    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }

    if (pid > 0) { // 父进程
        printf("Parent: Closing read end...\n");
        close(pipefd[0]); // 关闭读端

        printf("Parent: Writing message to pipe...\n");
        write(pipefd[1], message, strlen(message));

        close(pipefd[1]); // 写完后关闭写端,向读端发送 EOF 信号
        wait(NULL); // 等待子进程结束
        printf("Parent: Child finished.\n");
    } else { // 子进程
        printf("Child: Closing write end...\n");
        close(pipefd[1]); // 关闭写端

        ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
        buffer[bytes_read] = '\0';
        printf("Child: Read message from pipe: '%s'\n", buffer);

        close(pipefd[0]); // 读完后关闭读端
        exit(EXIT_SUCCESS);
    }

    return 0;
}
读写规则:关闭写端与 EOF

read 函数返回 0(即 EOF)的触发条件:管道所有写端文件描述符全部关闭后,读端执行 read 会返回 0。

若父进程完成写入后未关闭写端,管道处于空状态时,子进程执行 read 会进入阻塞。内核检测到存在可用写端,会持续等待新数据写入。

执行 close(写端描述符) 后,当前进程放弃写入权限。当管道所有写端引用计数归 0,内核判定管道不会再有新数据,唤醒阻塞的读端进程,read 返回 0。

使用规范:数据写入完成后,写入方需要关闭写端描述符,让读取方正常识别数据流结束标识。

编译与运行
bash 复制代码
gcc practice_pipe.c -o practice_pipe && ./practice_pipe
小结

匿名管道适用于存在亲缘关系的进程,实现简单的单向字节流传输。Shell 管道符 | 依托匿名管道工作,Shell 会创建两个子进程,将前一个进程标准输出对接管道写端,后一个进程标准输入对接管道读端。匿名管道无法用于无亲缘进程通信,命名管道由此产生。

2. 命名管道 (FIFO)

命名管道(FIFO - First In, First Out)用于实现无亲缘关系进程的通信,会在文件系统中生成路径节点,属于特殊文件。具备访问权限的进程,可通过文件路径调用 open() 完成读写操作,使用方式与普通文件类似。

原理说明

FIFO 依托文件系统路径作为访问入口,底层依旧是内核缓冲区。进程以只读模式打开 FIFO 时会进入阻塞,直至其他进程以只写模式打开;进程以只写模式打开 FIFO 时同样会阻塞,直至其他进程以只读模式打开。

创建方式
  1. 命令行创建
bash 复制代码
mkfifo <fifo_name>
  1. 代码创建
c 复制代码
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
  • pathname:FIFO 文件的路径与名称,调用进程需要对目标目录具备写权限与执行权限。
  • mode:FIFO 访问权限,格式与 open()mkdir() 一致,采用八进制位掩码。
    • 0666:所有用户拥有读写权限,多用于多进程通信场景。
    • 0640:文件所有者读写,所属组只读,其他用户无权限。
  • 权限计算规则:FIFO 最终权限 = mode & ~umask。系统默认 umask 通常为 0022,若设置 mode 为 0666,实际生效权限为 0644。
命令行验证

终端 1:创建 FIFO 并开启读取,进程进入阻塞状态

bash 复制代码
mkfifo my_fifo
cat my_fifo

终端 2:向 FIFO 写入数据

bash 复制代码
echo "Hello, named pipe!" > my_fifo

数据写入后,终端 1 解除阻塞并打印对应内容。

编程练习:独立进程通信

编写两个独立程序,分别作为数据写入端、数据读取端。

写入端 (writer_fifo.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FIFO_PATH "/tmp/my_fifo"

int main() {
    mkfifo(FIFO_PATH, 0666);

    printf("Writer: Opening FIFO for writing...\n");
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open fifo failed");
        exit(EXIT_FAILURE);
    }

    const char *message = "Message from writer process.";
    printf("Writer: Writing message: '%s'\n", message);
    write(fd, message, strlen(message));

    close(fd);
    printf("Writer: Finished and closed FIFO.\n");
    return 0;
}

读取端 (reader_fifo.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FIFO_PATH "/tmp/my_fifo"

int main() {
    char buffer[256];

    printf("Reader: Opening FIFO for reading...\n");
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open fifo failed");
        exit(EXIT_FAILURE);
    }

    ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
    buffer[bytes_read] = '\0';
    printf("Reader: Read message: '%s'\n", buffer);

    close(fd);
    unlink(FIFO_PATH);
    printf("Reader: Finished and removed FIFO.\n");
    return 0;
}
编译与运行
bash 复制代码
gcc writer_fifo.c -o writer_fifo
gcc reader_fifo.c -o reader_fifo
  1. 终端 1 执行 ./reader_fifo,进程阻塞等待写入;
  2. 终端 2 执行 ./writer_fifo,完成数据写入;
  3. 观察两个终端输出内容。
小结

命名管道(FIFO)拓展了管道的使用场景,可实现无亲缘进程的单向字节流通信,常应用于客户端/服务端架构,服务端创建固定 FIFO 接收多个客户端请求。数据传输过程需要在用户空间与内核空间之间完成两次拷贝,传输效率存在局限,同时不支持双向通信。

3. 管道的 I/O 行为:阻塞与非阻塞

匿名管道与命名管道的文件描述符,默认采用阻塞 I/O 模式。

默认阻塞行为
  • read():管道无数据时,调用进入阻塞,直至管道写入新数据,或所有写端描述符关闭(read 返回 0)。
  • write():管道缓冲区写满时,调用进入阻塞,直至其他进程读取数据、释放缓冲区空间。
  • FIFO 的 open():单独以只读或只写模式打开 FIFO,open() 进入阻塞,直至对应读写端进程接入。
非阻塞模式设置

业务场景需要并发处理多路 I/O 时,可将文件描述符设置为非阻塞模式。

  1. 匿名管道、已打开的 FIFO:通过 fcntl() 修改文件描述符属性
c 复制代码
#include <fcntl.h>
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
  1. 命名管道:open() 时直接追加非阻塞标识
c 复制代码
#include <fcntl.h>
int fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
非阻塞模式行为
  • read():管道无数据时,调用立即返回 -1,errno 赋值为 EAGAINEWOULDBLOCK
  • write():管道缓冲区写满时,调用立即返回 -1,errno 赋值为 EAGAINEWOULDBLOCK
  • FIFO 的 open()
    • O_RDONLY | O_NONBLOCK:无论是否存在写端进程,open() 直接执行成功。
    • O_WRONLY | O_NONBLOCK:无读端进程时,open() 返回 -1,errno 赋值为 ENXIO

4. 管道汇总

特性 匿名管道 (pipe) 命名管道 (FIFO)
生命周期 随进程结束销毁 依托文件系统存在,需手动删除
通信范围 仅限亲缘进程 系统内任意进程
载体形式 内核缓冲区,无实体文件 文件系统特殊节点
使用复杂度 较低,单次系统调用即可创建 略高,需执行创建、打开、删除操作

第二部分:System V IPC

System V IPC 诞生于早期 UNIX System V 系统,功能稳定,在 Linux 中沿用至今。

基础概念

  • IPC Key(键) :进程无法直接创建 IPC 对象,需要传入唯一键值,内核通过键值区分不同 IPC 实例。ftok() 是生成键值的常用函数。
  • IPC 标识符:IPC 对象创建后,内核分配唯一整型 ID,后续所有操作均通过该 ID 执行。
  • 内核持久性:System V IPC 对象生命周期独立于进程。对象创建后,即便发起创建的进程退出,只要未手动删除、系统未重启,对象会持续留存于内核中。
  • 管理命令ipcs 查看系统内 IPC 对象状态,ipcrm 手动删除 IPC 对象。

1. 消息队列 (Message Queue)

消息队列是内核维护的消息链表,弥补了管道无结构化数据的不足。消息队列不占用文件系统节点,无法通过普通文件操作指令查看、删除。

特性说明
  • 面向消息:读写以结构化消息为单位,内核保证消息读写的原子性,单次操作要么读取整条消息,要么不读取任何数据。
  • 消息类型:每条消息可配置正整型类型标识 mtype,接收端可依据类型筛选消息,不严格遵循先进先出顺序。
  • 内核持久性:消息队列属于内核数据结构,生命周期不受进程影响。
  • 数据拷贝:数据需要在用户空间与内核空间之间完成两次拷贝,大数据传输场景表现一般。
命令行工具
  • 查看消息队列:ipcs -q
  • 删除消息队列:ipcrm -q <msqid>
基本函数
  1. ftok():生成 IPC 唯一键值
c 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

依托已存在文件的路径与 0~255 范围内的项目 ID,生成 key_t 类型键值。

  1. msgget():创建或获取消息队列
c 复制代码
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
  • keyftok() 生成的键值。
  • msgflg:标志位,IPC_CREAT 表示队列不存在则创建,同时可搭配权限位(如 0666)。
  • 返回值:调用成功返回消息队列标识符 msqid,调用失败返回 -1。
  1. msgsnd():向队列发送消息
c 复制代码
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

消息结构体规范:首成员必须为 long 类型,用于标记消息类型。

c 复制代码
struct msgbuf {
    long mtype;       /* 消息类型,数值大于 0 */
    char mtext[...];  /* 消息数据 */
};
  • msgsz:消息正文长度,不包含 mtype 成员。
  • msgflg
    • 0:阻塞模式,队列已满时调用阻塞。
    • IPC_NOWAIT:非阻塞模式,队列已满时调用立即返回 -1,errno 赋值为 EAGAIN
  1. msgrcv():从队列接收消息
c 复制代码
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msgsz:缓冲区可容纳的消息正文最大长度。
  • msgtyp
    • 等于 0:读取队列头部第一条消息。
    • 大于 0:读取队列中类型匹配的第一条消息。
    • 小于 0:读取类型小于等于该数值绝对值的消息中,类型数值最小的消息。
  • msgflg
    • 0:阻塞模式,无匹配消息时调用阻塞。
    • IPC_NOWAIT:非阻塞模式,无匹配消息时调用立即返回 -1,errno 赋值为 ENOMSG
    • MSG_NOERROR:消息长度超出缓冲区容量时,截断消息并正常返回。
  1. msgctl():管控消息队列属性
c 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • IPC_RMID:删除消息队列。
  • IPC_STAT:读取队列状态信息至指定缓冲区。
  • IPC_SET:修改队列属性,仅队列所有者或 root 用户可执行。
编程练习:简易消息聊天室

分为通用头文件、发送端程序、接收端程序。发送端指定消息类型与内容,接收端按类型筛选消息。

通用头文件 common.h

c 复制代码
#ifndef __COMMON_H__
#define __COMMON_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

#define FIFO_PATH "/tmp"
#define PROJ_ID 0x6666
#define MSG_SIZE 256

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

#endif // __COMMON_H__

发送端 msg_sender.c

c 复制代码
#include "common.h"

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <mtype> <message>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    key_t key = ftok(FIFO_PATH, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    int msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid < 0) {
        perror("msgget");
        exit(EXIT_FAILURE);
    }

    struct msgbuf msg;
    msg.mtype = atoi(argv[1]);
    strncpy(msg.mtext, argv[2], MSG_SIZE - 1);
    msg.mtext[MSG_SIZE - 1] = '\0';

    if (msgsnd(msqid, &msg, strlen(msg.mtext), 0) < 0) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }

    printf("Message sent successfully! type=%ld, text='%s'\n", msg.mtype, msg.mtext);
    return 0;
}

接收端 msg_receiver.c

c 复制代码
#include "common.h"

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <mtype>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    long receive_type = atoi(argv[1]);

    key_t key = ftok(FIFO_PATH, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    int msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid < 0) {
        perror("msgget");
        exit(EXIT_FAILURE);
    }

    struct msgbuf msg;
    printf("Waiting for message of type %ld...\n", receive_type);

    while (1) {
        if (msgrcv(msqid, &msg, sizeof(msg.mtext), receive_type, 0) < 0) {
            perror("msgrcv");
            exit(EXIT_FAILURE);
        }
        printf("Received! type=%ld, text='%s'\n", msg.mtype, msg.mtext);
    }

    return 0;
}
编译与运行
bash 复制代码
gcc msg_sender.c -o sender
gcc msg_receiver.c -o receiver
  1. 终端 1 执行 ./receiver 1,监听类型为 1 的消息;
  2. 终端 2 执行 ./sender 2 "This is for channel 2",发送类型为 2 的消息;
  3. 终端 2 执行 ./sender 1 "Hello channel 1!",发送类型为 1 的消息;
  4. 观察接收端输出内容。
资源清理
bash 复制代码
ipcs -q
ipcrm -q 对应msqid
小结

消息队列支持结构化、带类型的消息传输,适用于消息分类、优先级处理场景。数据传输存在两次空间拷贝,大规模数据传输场景表现一般,接口调用逻辑相比管道更为复杂。

2. 共享内存 (Shared Memory)

管道、消息队列的数据传输,均需要在用户空间与内核空间之间完成两次拷贝。共享内存可规避拷贝流程,数据传输速度在各类 IPC 机制中表现突出。

原理说明

共享内存依托虚拟内存管理机制工作:

  1. 进程向内核申请分配物理内存区域;
  2. 进程调用接口,将物理内存映射至自身虚拟地址空间,内核修改进程页表,建立虚拟地址与物理地址的关联;
  3. 多个进程可映射同一块物理内存,各进程使用的虚拟地址可以不同,但最终指向同一物理内存。

任意进程修改物理内存数据后,其他已完成映射的进程可直接感知变化。读写操作等价于进程内部内存访问,无需调用读写类系统调用。

特性与使用限制
  • 零拷贝:数据无需在用户空间、内核空间来回拷贝,传输速度快。
  • 非阻塞访问:共享内存读写为指针操作,不存在阻塞状态。
  • 无同步机制:共享内存未内置互斥、同步能力。多进程同时读写时,会出现数据不完整、数据损坏等问题。使用共享内存时,需要搭配同步类机制保障数据正常。
命令行工具
  • 查看共享内存段:ipcs -m
    • perms:共享内存访问权限,八进制格式。
    • nattch:当前完成映射的进程数量。
    • statusdest 表示内存段已标记待删除,所有进程解除映射后,内核回收内存。
  • 删除共享内存段:ipcrm -m <shmid>
基本函数
  1. shmget():创建或获取共享内存段
c 复制代码
int shmget(key_t key, size_t size, int shmflg);
  • keyftok() 生成的键值。
  • size:内存段字节大小。
  • shmflg:标志位,IPC_CREAT 用于创建内存段,搭配权限位使用。
  • 返回值:调用成功返回共享内存标识符 shmid,调用失败返回 -1。
  1. shmat():映射共享内存至进程虚拟地址空间
c 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmaddr:指定映射的虚拟地址,传入 NULL 由内核自动分配。
  • shmflgSHM_RDONLY 表示只读模式,常规场景传入 0。
  • 返回值:调用成功返回内存起始指针,调用失败返回 (void *)-1
  1. shmdt():解除内存映射
c 复制代码
int shmdt(const void *shmaddr);

解除映射不会删除内核中的共享内存段。

  1. shmctl():管控共享内存段
c 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

IPC_RMID 用于标记内存段待删除。标记完成后,内存段不会立即销毁,所有进程解除映射后,内核执行回收。

编程练习:共享内存读写

分为通用头文件、写入程序、读取程序、清理程序。

通用头文件 common.h

c 复制代码
#ifndef __COMMON_H__
#define __COMMON_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>

#define KEY_PATH "/tmp"
#define PROJ_ID 0x1234
#define SHM_SIZE 1024

#endif

写入端 shm_write.c

c 复制代码
#include "common.h"

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <message>\n", argv[0]);
        exit(1);
    }

    key_t key = ftok(KEY_PATH, PROJ_ID);
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    char* shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void*)-1) {
        perror("shmat");
        exit(1);
    }

    printf("Writer attached at virtual address %p\n", shm_addr);
    strncpy(shm_addr, argv[1], SHM_SIZE - 1);
    shm_addr[SHM_SIZE - 1] = '\0';
    printf("Message '%s' written to shared memory.\n", argv[1]);

    if (shmdt(shm_addr) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}

读取端 shm_read.c

c 复制代码
#include "common.h"

int main() {
    key_t key = ftok(KEY_PATH, PROJ_ID);
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        fprintf(stderr, "Is the writer running first?\n");
        exit(1);
    }

    char* shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void*)-1) {
        perror("shmat");
        exit(1);
    }

    printf("Reader attached at virtual address %p\n", shm_addr);
    printf("Message read from shared memory: '%s'\n", shm_addr);

    if (shmdt(shm_addr) == -1) {
        perror("shmdt");
        exit(1);
    }
    return 0;
}

清理程序 shm_clean.c

c 复制代码
#include "common.h"

int main() {
    key_t key = ftok(KEY_PATH, PROJ_ID);
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    printf("Shared memory segment %d marked for deletion.\n", shmid);
    return 0;
}
编译与运行
bash 复制代码
touch /tmp/shm_key
gcc shm_write.c -o write
gcc shm_read.c -o read
gcc shm_clean.c -o clean
  1. 执行 ./write "Hello World" 写入数据;
  2. 执行 ./read 读取数据;
  3. 执行 ipcs -m 查看内存段状态;
  4. 再次执行写入、读取操作,验证数据覆盖效果;
  5. 执行 ./clean 清理共享内存。
小结

共享内存适用于大数据量、高频率交互的场景,数据库、图像处理、实时系统常会选用该机制。共享内存缺少同步能力,开发阶段需要额外实现互斥、同步逻辑,增加了代码编写与问题排查的难度。

3. 信号量 (Semaphore)

多进程并发访问共享资源时,会出现竞争问题。信号量用于实现进程间同步与互斥,规避竞争问题。

  • 互斥:同一时间段内,仅允许单个进程执行访问共享资源的代码片段。
  • 同步:调整多个进程的执行顺序,配合完成协作任务。

信号量是内核维护的整型计数器,数值修改仅能通过两类原子操作完成,操作执行过程不会被中断。

基础操作
  1. P 操作 :计数器数值减 1
    • 运算结果大于等于 0,进程继续执行;
    • 运算结果小于 0,进程阻塞,等待其他进程触发唤醒。
  2. V 操作 :计数器数值加 1
    • 数值增加后,若计数器小于等于 0,唤醒一个处于阻塞状态的进程。
应用场景
  1. 二元信号量(实现互斥):信号量初始值设为 1。同一时间仅单个进程可访问共享资源,等效于互斥锁。
  2. 计数信号量(实现同步/资源限流):信号量初始值设为 N。可限制并发访问资源的进程数量;初始值设为 0 时,可实现生产者与消费者的执行顺序管控。

System V 信号量以集合形式存在,一个信号量标识符可管理多个独立信号量,常规开发仅使用集合内索引为 0 的信号量。

命令行工具
  • 查看信号量:ipcs -s
  • 删除信号量:ipcrm -s <semid>
基本函数
  1. semget():创建或获取信号量集合
c 复制代码
int semget(key_t key, int nsems, int semflg);
  • nsems:集合内信号量的数量,创建集合时该参数必须大于等于 1。
  1. semctl():管控信号量
c 复制代码
int semctl(int semid, int semnum, int cmd, ...);
  • semnum:信号量在集合内的索引。
  • SETVAL:设置信号量初始值;IPC_RMID:删除信号量集合。

使用该函数需要手动定义联合体:

c 复制代码
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
    struct seminfo  *__buf;
};
  1. semop():执行 P、V 操作
c 复制代码
int semop(int semid, struct sembuf *sops, size_t nsops);

操作结构体定义:

c 复制代码
struct sembuf {
    unsigned short sem_num;
    short          sem_op;
    short          sem_flg;
};
  • sem_op:取值 -1 对应 P 操作,取值 1 对应 V 操作。
  • sem_flgSEM_UNDO 为常用标识。进程异常退出时,内核自动补充执行 V 操作,避免死锁。
编程练习:信号量保护共享内存

改造共享内存程序,新增二元信号量实现互斥访问。

通用头文件 common.h

c 复制代码
#ifndef __COMMON_H__
#define __COMMON_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <errno.h>

#define KEY_PATH "/tmp/shm_key"
#define PROJ_ID 0x1234
#define SHM_SIZE 1024

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void sem_p(int semid) {
    struct sembuf sop = {0, -1, SEM_UNDO};
    if (semop(semid, &sop, 1) == -1) {
        perror("semop P");
        exit(1);
    }
}

void sem_v(int semid) {
    struct sembuf sop = {0, 1, SEM_UNDO};
    if (semop(semid, &sop, 1) == -1) {
        perror("semop V");
        exit(1);
    }
}

#endif // __COMMON_H__

初始化程序 init.c

c 复制代码
#include "common.h"

int main() {
    key_t key = ftok(KEY_PATH, PROJ_ID);

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    printf("Shared memory created. shmid: %d\n", shmid);

    int semid = semget(key, 1, IPC_CREAT | 0666);
    printf("Semaphore set created. semid: %d\n", semid);

    union semun su;
    su.val = 1;
    semctl(semid, 0, SETVAL, su);
    printf("Semaphore at index 0 initialized to 1.\n");
    return 0;
}

写入端 shm_sem_write.c

c 复制代码
#include "common.h"
#include <unistd.h>

int main(int argc, char *argv[]) {
    key_t key = ftok(KEY_PATH, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, 0666);
    int semid = semget(key, 1, 0666);
    char* shm_addr = shmat(shmid, NULL, 0);

    printf("Writer trying to lock...\n");
    sem_p(semid);
    printf("Writer locked.\n");

    printf("Writing message '%s'...\n", argv[1]);
    strncpy(shm_addr, argv[1], SHM_SIZE);
    sleep(5);

    printf("Writer unlocking...\n");
    sem_v(semid);
    printf("Writer unlocked.\n");

    shmdt(shm_addr);
    return 0;
}

读取端 shm_sem_read.c

c 复制代码
#include "common.h"

int main() {
    key_t key = ftok(KEY_PATH, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, 0666);
    int semid = semget(key, 1, 0666);
    char* shm_addr = shmat(shmid, NULL, SHM_RDONLY);

    printf("Reader trying to lock...\n");
    sem_p(semid);
    printf("Reader locked.\n");

    printf("Reading message: '%s'\n", shm_addr);

    printf("Reader unlocking...\n");
    sem_v(semid);
    printf("Reader unlocked.\n");

    shmdt(shm_addr);
    return 0;
}

清理程序 clean.c

c 复制代码
#include "common.h"

int main() {
    key_t key = ftok(KEY_PATH, PROJ_ID);
    int shmid = shmget(key, SHM_SIZE, 0666);
    int semid = semget(key, 1, 0666);

    if (shmid != -1) shmctl(shmid, IPC_RMID, NULL);
    if (semid != -1) semctl(semid, 0, IPC_RMID);

    printf("Shared memory and semaphore cleaned.\n");
    return 0;
}
编译与运行
bash 复制代码
gcc init.c -o init
gcc shm_sem_write.c -o write_sem
gcc shm_sem_read.c -o read_sem
gcc clean.c -o clean
  1. 执行 ./init 初始化共享内存与信号量;
  2. 终端 1 执行 ./write_sem "Data from T1"
  3. 终端 2 同步执行 ./read_sem,观察阻塞与唤醒过程;
  4. 执行 ./clean 清理资源。
小结

信号量可解决并发场景下的同步与互斥问题,SEM_UNDO 标识可降低死锁出现概率。共享内存搭配信号量,是 Linux 环境下常用的组合方案,兼顾数据传输速度与并发访问稳定性。

第三部分:数据传输 IPC 机制对比

行为与生命周期说明

  • 管道/FIFO:read 执行后,对应数据从缓冲区移除。管道数据随进程销毁,FIFO 文件节点会留存。
  • 消息队列:msgrcv 执行后,对应消息从队列移除。消息队列留存于内核,未读取消息在进程退出后依旧保留,系统重启后数据清空。
  • 共享内存:读取操作不会移除数据,新写入内容会覆盖原有数据。内存段留存于内核,内容持续保留直至被覆盖,系统重启后数据清空。

对比表格

IPC 机制 读取行为 数据生命周期 特点
管道/FIFO 读取后数据消失 数据随进程消失,FIFO 节点持久 字节流形式,适配文件 I/O
消息队列 读取后数据消失 队列留存内核,系统重启数据清空 结构化消息,支持类型筛选
共享内存 数据可重复读取 内存段留存内核,系统重启数据清空 传输速度快,需自行同步

第四部分:信号 (Signals)

管道、消息队列等机制,需要接收方主动发起读取操作。信号用于处理突发高优先级事件,实现异步通知。终端按下 Ctrl+C 终止程序,就是信号的典型应用。

原理说明

信号属于软件层面的中断,系统检测到指定事件后,内核向目标进程发送信号。进程暂停当前执行逻辑,运行信号对应的处理函数,处理完成后恢复原有逻辑。

特性说明
  • 异步性:信号可在进程任意运行阶段触发,打断正常执行流程。
  • 信息载体:信号仅传递整型编号,用于标识事件类型,不支持复杂数据传输。
  • 特殊信号:SIGKILL(9)、SIGSTOP(19) 无法被捕获、忽略,用于强制终止或暂停进程。
  • 信号丢失:传统信号存在丢失问题;实时信号(SIGRTMIN ~ SIGRTMAX)支持信号排队。
常用信号列表
信号名 编号 默认动作 说明
SIGHUP 1 终止进程 终端断开或控制进程退出
SIGINT 2 终止进程 Ctrl+C 触发中断
SIGQUIT 3 终止并生成转储 Ctrl+\ 触发退出
SIGFPE 8 终止并生成转储 浮点运算异常
SIGKILL 9 终止进程 强制结束进程
SIGSEGV 11 终止并生成转储 访问非法内存地址
SIGPIPE 13 终止进程 向无读端管道写入数据
SIGALRM 14 终止进程 定时器超时触发
SIGTERM 15 终止进程 kill 指令默认信号
SIGCHLD 17 忽略 子进程状态发生变化
SIGCONT 18 恢复运行 唤醒暂停的进程
SIGSTOP 19 暂停进程 强制暂停进程
SIGTSTP 20 暂停进程 Ctrl+Z 触发暂停
SIGUSR1 10 终止进程 自定义信号 1
SIGUSR2 12 终止进程 自定义信号 2
命令行工具
bash 复制代码
# 向 PID 为 1234 的进程发送 SIGTERM
kill 1234
# 强制终止进程,发送 SIGKILL
kill -9 1234

# 终止所有同名进程
killall -9 my_program
基本函数
  1. signal():注册信号处理函数
c 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • handler:函数指针,也可传入 SIG_IGN(忽略信号)、SIG_DFL(恢复默认处理)。
  1. raise():进程向自身发送信号
c 复制代码
int raise(int sig);
  1. kill():向指定进程发送信号
c 复制代码
int kill(pid_t pid, int sig);
  • pid > 0:发送至对应 PID 进程;
  • pid == 0:发送至同进程组所有进程;
  • pid == -1:发送至当前用户有权限访问的所有进程(init 进程除外)。
  1. alarm():设置定时器,超时后发送 SIGALRM
c 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

重复调用会覆盖原有定时,入参为 0 时取消定时。

编程练习 1:捕获 Ctrl+C

程序捕获 SIGINT 信号,统计触发次数,不执行默认终止逻辑。

c 复制代码
// practice_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t interrupt_count = 0;

void handle_sigint(int signum) {
    interrupt_count++;
    printf("\nCaught SIGINT (%d)! You have interrupted me %ld times.\n",
           signum, interrupt_count);
}

int main() {
    if (signal(SIGINT, handle_sigint) == SIG_ERR) {
        perror("signal");
        exit(EXIT_FAILURE);
    }

    printf("Signal handler registered for SIGINT (Ctrl+C).\n");
    printf("Process PID: %d\n", getpid());
    printf("Running an infinite loop. Press Ctrl+C to trigger the handler.\n");

    while (1) {
        sleep(1);
    }

    return 0;
}

编译运行后,多次按下 Ctrl+C 观察输出,按下 Ctrl+\ 触发默认退出逻辑。

编程练习 2:alarm 实现超时控制

借助定时器实现任务超时判断。

c 复制代码
// practice_alarm.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_sigalrm(int signum) {
    printf("\nTimeout! The operation took too long. Exiting.\n");
    exit(EXIT_FAILURE);
}

int main() {
    signal(SIGALRM, handle_sigalrm);

    printf("Starting a long operation...\n");
    alarm(5);

    printf("Simulating a 10-second task (e.g., waiting for network).\n");
    sleep(10);

    alarm(0);
    printf("Great! The operation finished within the time limit.\n");

    return 0;
}
编译与运行
bash 复制代码
gcc practice_alarm.c -o alarm_test
./alarm_test

程序运行5秒后,触发 SIGALRM 信号并退出。

修改 sleep 时长,对比不同运行结果。将 sleep(10) 改为 sleep(3),重新编译执行,程序执行完毕后正常退出。

小结

信号可实现异步事件响应,适配中断处理、异常捕获、定时任务、进程管控等场景。信号仅能传递事件标识,不适合传输数据。信号处理逻辑复杂时,容易出现重入问题与信号丢失。实际开发中,实时信号、多线程同步原语、signalfd() 会作为替代方案使用。signalfd() 可将信号转化为文件描述符,接入 select/poll/epoll 多路复用框架。

总结

信号可以完成异步事件处理。该方式可以响应突发事件,改变程序原有执行流程,多用于中断处理、异常捕获、定时任务与进程间简易控制。

信号无法承载业务数据。处理逻辑较为繁杂时,会出现可重入问题,也就是信号处理函数执行过程中,被同类型信号再次触发,造成数据异常,同时传统信号会出现丢失情况。

Linux 环境中,信号依旧保留使用。面对复杂的同步与通信需求,可选用以下方式:

  • 实时信号:编号处于 SIGRTMINSIGRTMAX 区间内,支持排队机制,附带少量数据传递能力。
  • 多线程同步原语:多线程程序内,pthread_mutex_t 互斥锁、pthread_cond_t 条件变量,可完成线程同步。
  • signalfd():Linux 提供的接口,能够将信号转为文件描述符。异步信号事件可对接 select/poll/epoll 多路复用模型,简化信号处理逻辑。

【Linux 之旅】Linux 进程间通信(IPC)全解析:从管道到共享内存,进程协作实现

code monkey 原创 已于 2026-02-07 20:38:26 修改

前言

Linux 系统中,进程为资源分配单元。各进程相互独立,拥有独立地址空间,无法直接访问彼此数据。实际运行环境中存在大量进程协作场景,例如终端执行 who | wc -l 完成管道数据交互、服务端进程与客户端进程传输数据、多进程读取同一配置文件等。进程间通信(IPC,Inter-Process Communication)机制可以打破进程隔离,完成数据传输、资源共享与事件通知。

本文从原理、使用方式、代码示例、场景选择四个维度,介绍 Linux 主流 IPC 实现形式,涵盖匿名管道、命名管道、共享内存、消息队列、信号量。

一、IPC 基础:通信的必要性与分类

进程间通信(Inter-Process Communication, IPC)是同一主机或不同主机内多个进程实现数据交互的技术。虚拟内存机制为每个进程划分独立地址空间,形成进程隔离,进程间默认无法直接访问数据,需借助专用机制完成通信。

IPC 的实现方式为操作系统开辟公共资源区域。该区域不属于单个进程,由操作系统统一管理,多个进程通过访问该区域完成交互。

可将进程类比为独立封闭的工作空间,空间内部仅能操作自身数据,空间之间无法直接传递内容。设置公共区域供所有空间交互信息,与 IPC 的实现逻辑一致。

进程通信不会直接开放进程私有数据,此类操作会破坏系统隔离机制,引发运行异常。

操作系统承担公共资源分配与访问规则制定工作,具体分为两项:

  1. 分配共享内存、管道缓冲区等公共资源;
  2. 提供系统调用接口,作为进程访问公共资源的入口。

1.1 进程间通信的应用场景

  • 数据传输:进程向其他进程发送数据,例如客户端向服务端传递请求参数;
  • 资源共享:多个进程同时访问同一资源,例如多进程读写配置文件;
  • 事件通知:一个进程向其他进程推送事件消息,例如子进程结束后通知父进程回收资源;
  • 进程控制:一个进程管控其他进程运行状态,例如调试程序捕获目标进程异常。

1.2 Linux IPC 分类

Linux 系统提供多种 IPC 实现形式。按照技术发展脉络与功能特征,可分为三类。不同形式在运行速率、适用场景、编码复杂度上存在差异。

分类 具体机制 特性说明
传统 IPC 匿名管道、命名管道(FIFO) 依托文件系统实现,接口简洁,适用于简易通信场景
System V IPC 共享内存、消息队列、信号量 依托内核对象实现,运行效率较高,生命周期与内核绑定
POSIX IPC 共享内存、信号量、互斥量、条件变量 具备跨平台特性,接口标准化程度更高

匿名管道、命名管道与共享内存,在工程环境中使用频次较高。本文围绕 System V 标准对应的实现形式展开介绍。

System V IPC 在现代开发中的应用范围逐步收窄。下文以原理介绍为主,代码示例仅作参考。POSIX IPC 将在后续内容中单独说明。

二、管道通信:经典 IPC 实现(匿名管道 + 命名管道)

2.1 管道的发展与基本定义

管道(Pipe)诞生于 Unix 系统设计体系,用于简化多程序组合执行流程,改善早期进程数据传递效率偏低的问题。

早期进程数据传递依托临时文件,执行流程如下:

  1. 程序将输出内容写入临时文件;
  2. 其他程序读取临时文件内的数据;
  3. 任务执行完毕后删除临时文件。

该运行模式存在诸多问题,磁盘读写会影响运行效率,临时文件会占用磁盘存储空间,文件创建与删除会增加流程步骤。

管道机制于 1973 年左右集成至 Unix V3 版本。该机制基于内核缓冲区搭建内存通道,进程输出数据可直接作为另一进程的输入,全程不产生磁盘读写操作。该设计契合 Unix 组合小程序完成复杂任务的设计思路,who | wc -l 为典型应用案例。

管道是操作系统提供的内核级通信通道,用于进程间传输字节流数据,分为两类:

  1. 匿名管道(Pipe)
    • 本质为内核维护的环形缓冲区,仅支持存在亲缘关系的进程通信;
    • 采用半双工通信模式,同一时段仅可执行单向数据传输,双向通信需创建两组管道;
    • 数据以字节流形式传输,无消息边界,数据完成读取后会从缓冲区清除。
  2. 命名管道(FIFO)
    • 以特殊文件形式存在于文件系统中,可支持无亲缘关系的进程通信;
    • 底层依托内核缓冲区运行,文件本身不存储业务数据,仅作为进程连接管道的标识。
字节流概念说明

流(Stream)指数据在数据源与程序之间的传输通道。字节流(Byte Stream)以字节作为最小传输单元,1 字节对应 8 位二进制数据,是计算机基础的数据存储与传输单位。

字节流具备相应特征,可直接操作原始二进制数据,不区分数据编码与数据类型,能够适配文本、图片、音视频等各类文件。字符流、对象流等传输形式,均在字节流基础上封装实现。

使用字节流传输数据时,通信双方需要提前约定数据解析规则,保障接收端正常还原有效数据。

管道遵循 Linux 一切皆文件的设计思路,进程通过管道读端文件描述符 fd[0]、写端文件描述符 fd[1] 完成数据读写。

2.2 匿名管道(pipe):亲缘进程通信通道

匿名管道仅可在拥有共同祖先的进程之间使用,通过 pipe() 系统调用创建,生命周期跟随进程。

2.2.1 工作原理
  1. 创建管道 :调用 int pipe(int fd[2]),参数 fd[2] 为输出参数。系统生成两个文件描述符,fd[0] 对应读端,fd[1] 对应写端。内核同步创建环形缓冲区,管道生命周期与进程绑定。
  2. 共享管道资源 :父进程调用 fork() 创建子进程,子进程完整复制父进程的文件描述符表,父子进程可共享同一管道的读写端口。
  3. 单向通信配置 :结合半双工特性,关闭闲置端口。例如父进程负责写入、子进程负责读取时,父进程关闭 fd[0],子进程关闭 fd[1]

步骤 1 父进程创建管道

父进程执行 pipe() 系统调用,内核生成匿名管道与对应文件描述符。系统默认文件描述符 0、1、2 绑定终端设备,新增的 fd[0]fd[1] 分别绑定管道读端与写端。

步骤 2 父进程创建子进程

fork() 执行后,子进程复制父进程的文件描述符表,父子进程持有相同文件描述符,指向同一个内核管道缓冲区。文件对应的内核结构体设置引用计数,每新增一个持有文件描述符的进程,引用计数加 1。引用计数归零后,内核回收对应资源。

步骤 3 关闭闲置端口

父进程关闭读端 fd[0],保留写端;子进程关闭写端 fd[1],保留读端,形成父进程向子进程单向传输数据的通道。数据全程在内存缓冲区流转,不触发磁盘读写。

2.2.2 代码示例:父子进程管道通信
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdlib>

void ChildTask(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "Parent say: \n" << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "写端已经关闭,且管道没有数据可读,本进程退出" << std::endl;
            break;
        }
        else
        {
            break;
        }
    }
}

void ParentTask(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        int len = snprintf(buffer, sizeof(buffer), "I am Parent, my pid is %d, cnt = %d\n", getpid(), cnt++);
        write(wfd, buffer, len);

        if(cnt == 10)
            break;
    }
}

int main()
{
    int fd[2] = {0};
    int n = pipe(fd);
    if(n == -1)
    {
        std::cerr << "pipe create false" << std::endl;
        exit(1);
    }

    pid_t id = fork();
    if(id == 0)
    {
        close(fd[1]);
        ChildTask(fd[0]);
        close(fd[0]);
        exit(0);
    }

    close(fd[0]);
    ParentTask(fd[1]);
    close(fd[1]);

    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret > 0)
    {
        printf("exit code: %d, exit signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
    }

    return 0;
}

执行流程说明

  1. 初始化管道main 函数调用 pipe(fd) 创建管道,分配读写文件描述符。创建失败时程序直接退出,规避资源泄漏。
  2. 创建子进程fork() 拆分进程,子进程关闭写端并执行数据读取逻辑,父进程关闭读端并执行数据写入逻辑。闲置端口需要关闭,避免读写状态检测逻辑异常。
  3. 并发数据交互 :父进程循环写入 10 组数据,单次写入数据长度不超过 PIPE_BUF,保障写入操作完整性。子进程持续读取数据,并补充字符串结束符。父进程完成写入后关闭写端。
  4. 资源回收 :父进程调用 waitpid() 等待子进程退出,回收子进程资源,避免僵尸进程出现,同时解析子进程退出状态信息。
  5. 程序终止:子进程正常退出,父进程完成资源回收后结束运行。
2.2.3 补充说明
2.2.3.1 PIPE_BUF 与原子性

PIPE_BUF 为系统预定义常量,Linux 系统默认取值为 4096 字节,用于划分管道写入操作的原子性范围。

原子性指单次操作不可拆分,执行结果分为全部完成、全部未完成两种状态。

  • 写入数据长度 ≤ PIPE_BUF:写入操作具备原子性,多进程并发写入时,单组数据不会被拆分、交错;
  • 写入数据长度 > PIPE_BUF:写入操作不具备原子性,数据会被内核拆分,多进程并发场景下易出现数据交错问题。

该机制用于处理多进程并发写入管道时的数据错乱问题,在日志收集、命令行组合等场景中应用较多。

2.2.3.2 字符串结束符补充规则

read() 属于二进制安全系统调用,仅执行字节拷贝,不会自动添加 \0 字符串结束符。C 语言字符串处理函数依靠 \0 判断字符串边界。缓冲区缺失结束符,会引发内存越界、乱码、程序崩溃等问题。

编码执行规范:数据经由 readmemcpy 等二进制读写接口获取,且后续按照字符串处理时,手动补充 \0;数据为标准 C 语言字符串时,无需额外处理。该编写方式属于防御性编程,可规避各类边界异常。

2.2.3.3 匿名管道读写规则

管道读写行为受缓冲区状态、文件描述符引用计数影响。闲置文件描述符未关闭时,内核会判定仍有进程持有对应端口,读写逻辑会出现阻塞异常。

匿名管道缓冲区存在固定容量,Linux 系统默认约 64 KB,典型读写场景分为四类:

场景 1 写入速率大于读取速率

写入端执行 write 调用后快速返回,数据存入管道缓冲区。读取端按自身速率读取数据,数据不会丢失。缓冲区未满时,写入操作不会阻塞。

场景 2 读取速率大于写入速率

管道缓冲区无数据时,读取端的 read 调用进入阻塞状态,内核挂起读取进程。写入端存入数据后,内核唤醒读取进程,执行数据读取。

场景 3 缓冲区被写满

缓冲区达到容量上限后,写入端的 write 调用进入阻塞状态。读取端读取数据、释放缓冲区空间后,写入进程恢复运行。

场景 4 端口提前关闭

  • 所有写端关闭:读取端读完缓冲区剩余数据后,read 返回数值 0,读取进程可正常退出;
  • 所有读端关闭:写入端继续执行 write 操作时,进程收到 SIGPIPE 信号,程序默认终止,终端提示 Broken pipe。

匿名管道分为阻塞模式与非阻塞模式,系统默认启用阻塞模式。开启 O_NONBLOCK 标志可切换为非阻塞模式。非阻塞模式下读写调用立即返回结果,通过返回值与错误码判定读写状态。

2.2.4 匿名管道特性与适用场景
  • 运行特点:实现流程简洁,内核完成同步与互斥控制;
  • 运行限制:仅支持亲缘进程通信,通信模式为半双工,数据以无边界字节流形式传输;
  • 适用场景:父子进程、兄弟进程之间的简易数据交互。

两组匿名管道配合使用,可实现进程间双向通信。

2.3 命名管道(FIFO):跨亲缘进程通信

匿名管道仅适用于存在亲缘关系的进程。命名管道(FIFO)依托文件系统内的特殊文件,解除该限制,支持任意进程开展通信。

多个进程打开同一文件时,内核仅保留一份 inode 与内核缓冲区,各进程独立维护文件上下文,资源由全局共享。命名管道遵循 Linux 一切皆文件的设计思路:

  1. 命名管道在文件系统内拥有独立路径与 inode,多个进程通过路径访问同一内核管道资源;
  2. 命名管道仅将数据暂存至内存缓冲区,数据不会写入磁盘,磁盘刷新类接口对其不生效。
2.3.1 命名管道基本定义

命名管道全称 First In First Out,属于文件系统内的特殊文件对象,底层依托内核字节缓冲区运行。相关特征如下:

  1. 文件系统可见:以文件形式存在,具备独立访问路径,文件仅作为访问标识,不存储业务数据;
  2. 适配范围广:任意进程具备文件访问权限,即可完成通信,不限制进程亲缘关系。
2.3.2 命名管道特性
  1. 通信模式为半双工,双向通信需要创建两组命名管道;
  2. 数据传输形式为字节流,无内置消息边界,需要处理粘包、拆包问题;
  3. 支持阻塞、非阻塞两种运行模式,默认启用阻塞模式;
  4. 单次写入长度不超过 PIPE_BUF 时,写入操作具备原子性;
  5. 生命周期:所有进程关闭管道后,内核缓冲区资源释放,管道文件需要手动删除;
  6. 支持权限管控,仅具备对应权限的进程可访问管道。
2.3.3 命名管道创建方式
2.3.3.1 命令行创建
bash 复制代码
mkfifo filename

命名管道文件标识为 p,可通过文件查看指令区分文件类型。

2.3.3.2 代码创建
cpp 复制代码
#include <sys/stat.h>
#include <sys/types.h>

int mkfifo(const char *pathname, mode_t mode);

参数说明:pathname 为管道文件路径,mode 为文件权限;返回值 0 代表创建成功,-1 代表创建失败。

命名管道沿用 Linux 文件权限管控机制,可划分属主、同组用户、其他用户的读写权限,降低未授权访问、数据泄露、通信干扰等问题出现的概率,遵循最小权限设计原则。

命名管道的操作接口与普通文件一致,openreadwriteclose 均可直接调用。

2.3.4 命名管道打开规则

默认阻塞模式:

  • 以只读模式 O_RDONLY 打开:进程阻塞,直至其他进程以只写模式打开管道;
  • 以只写模式 O_WRONLY 打开:进程阻塞,直至其他进程以只读模式打开管道;
  • 以读写模式 O_RDWR 打开:进程不会阻塞,该方式不推荐使用。

非阻塞模式(开启 O_NONBLOCK):

  • 只读模式打开:调用立即返回,无写入进程时,后续 read 操作返回错误码 EAGAIN
  • 只写模式打开:调用立即返回,无读取进程时,write 操作返回错误码 ENXIO

工程环境中默认使用阻塞模式。

2.3.5 代码示例:无亲缘进程通信

写进程代码

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdlib>
#include<cerrno>

#define FIFO_PATH "./myfifo"
#define FIFO_MODE 0666

int main()
{
    int ret = mkfifo(FIFO_PATH, FIFO_MODE);
    if(ret == -1 && errno != EEXIST)
    {
        std::cerr << "mkfifo create false" << std::endl;
        exit(1);
    }

    int wfd = open(FIFO_PATH, O_WRONLY);
    if(wfd == -1)
    {
        std::cerr << "open fifo failed" << std::endl;
        exit(1);
    }
    std::cout << "写进程:FIFO 打开成功,开始写入数据..." << std::endl;

    char buffer[1024];
    for(int i = 0; i < 10; i++)
    {
        int len = snprintf(buffer, sizeof(buffer), "写进程 PID = %d, 第%d 条数据\n", getpid(), i);
        ssize_t n = write(wfd, buffer, len);
        if(n < 0)
        {
            std::cerr << "write failed" << std::endl;
            break;
        }
        std::cout << "写进程:已写入第" << i + 1 << "条数据" << std::endl;
        sleep(2);
    }

    close(wfd);
    std::cout << "写进程:数据写入完成,关闭 FIFO" << std::endl;
    return 0;
}

读进程代码

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdlib>
#include<cstring>

#define FIFO_PATH "./myfifo"
#define FIFO_MODE 0666

int main()
{
    int ret = mkfifo(FIFO_PATH, FIFO_MODE);
    if(ret == -1 && errno != EEXIST)
    {
        std::cerr << "mkfifo create false" << std::endl;
        exit(1);
    }

    int rfd = open(FIFO_PATH, O_RDONLY);
    if(rfd == -1)
    {
        std::cerr << "open fifo failed" << std::endl;
        exit(1);
    }
    std::cout << "读进程:FIFO 打开成功,开始读出数据..." << std::endl;

    char buffer[1024];
    while (true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "读进程:读取到数据:" << buffer;
        }
        else if (n == 0)
        {
            std::cout << "读进程:写端已关闭,退出" << std::endl;
            break;
        }
        else
        {
            perror("read failed");
            break;
        }
    }

    close(rfd);
    std::cout << "读进程:数据读取完成,关闭 FIFO" << std::endl;
    return 0;
}

编译与运行指令

bash 复制代码
# 编译程序
g++ -o fifo_writer fifo_writer.cpp -std=c++11
g++ -o fifo_reader fifo_reader.cpp -std=c++11

# 终端 1 执行读进程
./fifo_reader
# 终端 2 执行写进程
./fifo_writer
2.3.6 管道类机制对比

匿名管道与命名管道底层均为内核字节缓冲区,数据不会写入磁盘,通信模式为半双工。小数据写入场景下,写入操作具备原子性,同步与互斥逻辑由内核完成。

  • 匿名管道:仅支持亲缘进程,无文件实体,使用后不会留存文件;
  • 命名管道:支持任意进程,存在文件实体,使用完毕后需要手动删除文件。

三、共享内存:高吞吐 IPC 实现

共享内存(Shared Memory,shm)由内核在物理内存中划分连续内存区域,内核为该区域分配唯一标识 shmid。多个进程可将该内存区域映射至自身虚拟地址空间,直接读写内存数据。该形式无需在用户态与内核态之间拷贝数据。

管道通信需要执行两次数据拷贝,共享内存仅完成地址映射,不存在数据拷贝动作,数据吞吐能力存在差异。

3.1 共享内存工作原理

  1. 分配物理内存 :调用 shmget() 后,内核从系统物理内存中划分连续区域。该区域属于内核公共资源,不归属任意进程。内核分配唯一标识 shmid,用于定位内存区域。内存创建完成后,进程暂无法访问该区域。
  2. 地址映射 :进程调用 shmat()mmap(),内核将共享物理内存映射至进程虚拟地址空间的空闲区域,返回虚拟地址指针。进程操作该指针,等同于操作共享物理内存。

不同进程映射得到的虚拟地址可以不同,所有虚拟地址最终指向同一块物理内存。地址映射仅建立地址关联,不产生数据拷贝。

多进程通信需要访问同一资源,进程无法直接操作物理内存地址,依靠统一标识符定位共享内存区域。

3.2 System V 共享内存接口

System V 共享内存使用流程:创建内存区域 → 地址映射 → 数据读写 → 解除映射 → 删除内存区域。系统调用接口 包含 ftokshmgetshmatshmdtshmctl

3.2.1 ftok:生成通信密钥

多个进程依靠相同密钥定位同一块共享内存。密钥可通过 ftok() 生成,也可手动指定固定数值。

cpp 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

参数说明:pathname 为已存在的文件路径,proj_id 为非零整数;返回值为密钥,失败返回 -1。

ftok 基于文件 inode 编号与 proj_id 低 8 位生成密钥。文件删除重建会造成 inode 变更,密钥同步改变。对运行稳定性要求较高的场景,可手动定义固定密钥。

3.2.2 shmget:创建/获取共享内存
cpp 复制代码
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数说明:

  • key:通信密钥,多进程保持一致;
  • size:共享内存字节大小;
  • shmflg:标志位与权限组合。

常用标志位:

  • IPC_CREAT:内存不存在则创建,已存在则直接获取;
  • IPC_CREAT | IPC_EXCL:内存不存在则创建,已存在则调用失败;
  • 权限位:格式与文件权限一致,例如 0666

返回值为共享内存标识 shmid,失败返回 -1。

3.2.3 shmat:映射内存至进程地址空间
cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

  • shmid:共享内存标识;
  • shmaddr:指定映射虚拟地址,传 NULL 由内核自动分配;
  • shmflg:操作标志,传 0 代表读写模式。

返回值为映射后的虚拟地址指针,失败返回 (void*)-1

3.2.4 数据读写

映射完成后,直接通过指针读写内存,无需额外系统调用。

cpp 复制代码
// 写入数据
strcpy(shm_ptr, "Hello System V Shared Memory!");
// 读取数据
printf("读取到共享内存数据:%s\n", shm_ptr);
3.2.5 shmdt:解除地址映射

进程不再使用共享内存时,解除映射关系。该操作不会删除共享内存本身。

cpp 复制代码
int shmdt(const void *shmaddr);

参数为 shmat 返回的地址指针,返回 0 代表成功,-1 代表失败。

3.2.6 shmctl:管理/删除共享内存
cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

  • shmid:共享内存标识;
  • cmd:操作指令;
  • buf:元信息结构体指针。

常用指令:

  1. IPC_RMID:标记删除共享内存,所有进程解除映射后,内核回收内存资源,buf 可传 NULL
  2. IPC_STAT:读取共享内存元信息,buf 必须传入有效地址;
  3. IPC_SET:修改共享内存权限、属主等属性。

struct shmid_ds 存储共享内存大小、创建进程 PID、映射进程数量、权限等元信息,内部包含 struct ipc_perm 通用权限结构体,该结构体为 System V IPC 体系通用组件。

3.3 共享内存通信示例(搭配命名管道实现同步)

共享内存未内置同步互斥机制,多进程同时读写会造成数据异常。可结合命名管道完成读写同步,命名管道承担信号通知工作。

公共头文件 comm.h

cpp 复制代码
#pragma once

#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<cstdlib>

#define PATH_NAME "."
#define PROJ_ID 0x6666
#define MAX_SIZE 4096
#define FIFO_NAME "./myfifo"
#define FIFO_MODE 0666

int createShm(int size);
int getShm(int size);
int destroyShm(int shmid);

void Wait(int fd);
void Signal(int fd);

功能实现 comm.cc

cpp 复制代码
#include"comm.h"
#include<cerrno>
#include<cstring>

static int commShm(int size, int shmflg)
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if(key < 0)
    {
        std::cerr << "ftok failed" << std::endl;
        exit(1);
    }

    int shmid = shmget(key, size, shmflg);
    if(shmid < 0)
    {
        std::cerr << "shmget failed: " << strerror(errno) << std::endl;
        exit(1);
    }
    return shmid;
}

int createShm(int size)
{
    return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}

int getShm(int size)
{
    return commShm(size, IPC_CREAT | 0666);
}

int destroyShm(int shmid)
{
    return shmctl(shmid, IPC_RMID, nullptr);
}

void Wait(int fd)
{
    char buffer[1];
    read(fd, buffer, 1);
}

void Signal(int fd)
{
    char x = 1;
    write(fd, &x, sizeof(x));
}

服务端 Server.cc

cpp 复制代码
#include "comm.h"

int main()
{
    mkfifo(FIFO_NAME, FIFO_MODE);
    int fifo_fd = open(FIFO_NAME, O_RDONLY);

    int shmid = createShm(MAX_SIZE);
    char *shm_ptr = (char*)shmat(shmid, nullptr, 0);

    while(true)
    {
        Wait(fifo_fd);
        std::cout << "收到:" << shm_ptr << std::endl;
        if (strcmp(shm_ptr, "quit") == 0) break;
    }

    close(fifo_fd);
    shmdt(shm_ptr);
    destroyShm(shmid);
    unlink(FIFO_NAME);
    return 0;
}

客户端 Client.cc

cpp 复制代码
#include "comm.h"

int main()
{
    int fifo_fd = open(FIFO_NAME, O_WRONLY);
    sleep(2);

    int shmid = getShm(MAX_SIZE);
    char *shm_ptr = (char*)shmat(shmid, nullptr, 0);

    while(true)
    {
        ssize_t s = read(0, shm_ptr, MAX_SIZE - 1);
        if (s > 0)
        {
            shm_ptr[s - 1] = 0;
            Signal(fifo_fd);
            if (strcmp(shm_ptr, "quit") == 0)
                break;
        }
    }

    close(fifo_fd);
    shmdt(shm_ptr);
    return 0;
}

3.4 补充说明

3.4.1 共享内存生命周期

共享内存生命周期与内核绑定。进程退出后,未主动执行删除操作时,共享内存会持续占用系统资源。

  • 查看指令:ipcs -m 查看系统内所有共享内存;
  • 删除方式:代码调用 shmctl(IPC_RMID),或终端执行 ipcrm -m shmid
3.4.2 共享内存容量规则

创建共享内存时,内核会按照 4 KB 向上对齐容量。例如申请 3096 字节,实际分配 4 KB;申请 4097 字节,实际分配 8 KB。程序可使用的有效容量为代码中指定的大小。

四、其他 System V IPC 机制:消息队列与信号量

4.1 System V 消息队列

消息队列是内核维护的链式结构,队列内数据按照消息类型分类存储。进程可按照消息类型定向接收数据,数据传输需要经过内核拷贝。

运行逻辑可参照分类信箱,进程向信箱存入不同类型消息,接收进程读取指定类型消息。内核完成数据存储,数据保留至被读取或队列删除。

相关特征:

  • 支持结构化数据传输,可按照类型筛选消息;
  • 数据传输过程包含用户态与内核态之间的双向拷贝;
  • 系统调用接口msggetmsgsndmsgrcvmsgctl

4.2 System V 信号量

信号量不执行数据传输,本质为内核维护的计数器,用于实现进程同步与互斥,管控多进程对临界资源的访问。

运行逻辑可参照资源锁。进程访问临界资源前执行 P 操作(计数器减 1),使用完毕后执行 V 操作(计数器加 1)。计数器数值为 0 时,后续访问进程进入等待状态。

相关特征:

  • 作用为同步、互斥控制,不具备数据传输能力;
  • 常与共享内存搭配使用,补充共享内存同步机制缺失的问题;
  • 系统调用接口semgetsemopsemctl

4.3 System V IPC 整体共性

共享内存、消息队列、信号量同属 System V IPC 体系,底层架构与使用逻辑存在诸多相同点:

  1. 标识体系统一 :均使用 key_t 作为通信密钥,内核分配专属 ID,接口命名规则保持一致;
  2. 权限管理统一 :依托 struct ipc_perm 结构体管控权限、属主信息,权限校验逻辑相同;
  3. 生命周期统一 :资源不会随进程退出自动销毁,需要手动调用接口删除,可通过 ipcsipcrm 指令管理;
  4. 内核管理统一 :内核为三类资源维护独立管理结构体,可通过 xxxctl 接口读取元信息。

4.4 IPC 选型参考

IPC 机制 运行速率 适配进程 数据结构 同步能力 典型场景
匿名管道 中等 亲缘进程 字节流 内核自带 父子进程简易通信
命名管道 中等 任意进程 字节流 内核自带 无亲缘进程简易通信
共享内存 任意进程 字节流 需手动实现 大数据、高频率数据传输
消息队列 偏低 任意进程 结构化消息 内核自带 低频、带分类的业务数据传输
信号量 - 任意进程 计数器 基本功能 临界资源同步互斥

选型参考:

  1. 少量数据、简易交互场景,选用管道;
  2. 大数据量、高频传输场景,选用共享内存,搭配信号量或管道实现同步;
  3. 带分类规则的结构化数据传输场景,选用消息队列;
  4. 多进程临界资源管控场景,搭配信号量。

五、总结

Linux IPC 依托内核公共资源打破进程地址空间隔离,实现进程交互。各类机制功能定位存在区别,管道实现流程简洁,共享内存适用于大吞吐量数据传输,信号量用于同步互斥控制。开发阶段结合进程关系、数据规模、交互频率选用对应方案。

System V IPC 在现代开发中的应用场景逐步减少,多用于老旧项目维护。其底层运行逻辑具备通用性,POSIX IPC 在原有逻辑基础上优化接口设计,提升跨平台适配能力。熟悉 IPC 技术底层运行逻辑与同步问题处理方式,可适配各类进程协作场景。


reference