一、IPC 总览
1. IPC 目的
- 数据传输:一个进程把数据发给另一个进程。
- 资源共享:多个进程共享同一资源。
- 通知事件:通知另一个或一组进程发生了某事件。
- 进程控制:一个进程控制另一个进程,如 Debug 进程。
2. IPC 发展
- 管道
- System V IPC
- POSIX IPC
3. IPC 分类
- 管道
- 匿名管道:
pipe - 命名管道:
FIFO、mkfifo
- 匿名管道:
- System V IPC
- 消息队列
- 共享内存
- 信号量
- POSIX IPC
- 消息队列、共享内存、信号量
- 互斥量、条件变量、读写锁
4. 本节重点
- 掌握匿名/命名管道的原理与操作。
- 编写进程池。
- 掌握共享内存。
- 了解消息队列、信号量。
- 理解内核如何管理 IPC 资源。
- 了解 C/C++ 中回调、多态在进程池中的体现。
二、管道
1. 匿名管道 pipe
函数原型:
c
int pipe(int fd[2]);
fd[0]:读端fd[1]:写端- 成功返回
0,失败返回错误码。
本质:
- 管道是内核中的一块缓冲区。
- 从文件描述符角度看,管道也像文件,符合"Linux 一切皆文件"。
- 读写管道使用
read、write、close。
fork 后共享管道:
- 父进程创建管道。
fork后子进程继承fd[0]、fd[1]。- 通常父进程关闭读端,子进程关闭写端,实现单向通信。
- 双向通信需要两个管道。
管道特点:
- 只能用于有亲缘关系的进程,通常是父子进程。
- 半双工,数据单向流动。
- 提供流式服务,没有消息边界。
- 生命周期随进程,进程退出管道释放。
- 内核会对管道操作进行同步与互斥。
- 写端全关,
read返回0。 - 读端全关,
write会产生SIGPIPE。
2. 管道读写规则
没有数据可读:
O_NONBLOCK关闭:read阻塞。O_NONBLOCK开启:read返回-1,errno = EAGAIN。
管道满:
O_NONBLOCK关闭:write阻塞,直到有进程读走数据。O_NONBLOCK开启:write返回-1,errno = EAGAIN。
关闭情况:
- 所有写端关闭:
read返回0。 - 所有读端关闭:
write触发SIGPIPE,可能导致进程退出。
原子性:
- 写入数据量
<= PIPE_BUF:Linux 保证写入原子性。 - 写入数据量
> PIPE_BUF:不保证原子性。
3. 命名管道 FIFO
创建方式:
bash
mkfifo filename
程序内:
c
int mkfifo(const char *filename, mode_t mode);
特点:
- 命名管道是文件系统中的特殊文件。
- 可用于无亲缘关系进程间通信。
- 匿名管道用
pipe创建并打开;命名管道用mkfifo创建,用open打开。 - 创建方式不同,但打开之后语义相同。
打开规则:
- 读打开:
O_NONBLOCK关闭:阻塞,直到有进程为写而打开 FIFO。O_NONBLOCK开启:立刻返回成功。
- 写打开:
O_NONBLOCK关闭:阻塞,直到有进程为读而打开 FIFO。O_NONBLOCK开启:立刻返回失败,错误码ENXIO。
典型用途:
- 文件拷贝。
- server/client 通信。
三、进程池
目标:
- 预先创建多个子进程。
- 父进程通过管道向子进程派发任务。
- 避免频繁
fork,提高效率。
核心结构:
Channel:封装写端 fd、子进程 pid,提供Send、Close。ProcessPool:负责创建管道、fork子进程、保存 channel、派发任务、清理。TaskManager:任务函数表,随机选择任务编号。Worker:子进程循环从标准输入读任务编号并执行。Main:初始化进程池、派发任务、清理进程池。
关键流程:
- 父进程创建管道。
fork子进程。- 子进程关闭历史写端,关闭当前管道写端,
dup2(pipefd[0], 0),然后执行Worker。 - 父进程关闭读端,保存写端。
- 父进程轮询选择 Channel,发送任务编号。
- 清理时父进程关闭所有写端,子进程
read返回0后退出,父进程waitpid回收。
要点:
- 子进程只保留读端,父进程只保留写端。
- 管道用于任务派发。
- 回调函数
std::function体现多态思想。 - 清理时关闭写端是让子进程退出的关键。
四、System V 共享内存
共享内存是最快的 IPC 形式:
- 共享内存映射到进程地址空间后,进程间数据传递不再经过内核。
- 进程像访问普通内存一样读写共享内存。
1. 相关函数
shmget
c
int shmget(key_t key, size_t size, int shmflg);
作用:
- 创建或获取共享内存。
参数:
key:共享内存名字,通常用ftok生成。size:共享内存大小。shmflg:IPC_CREAT:不存在则创建,存在则获取。IPC_CREAT | IPC_EXCL:不存在则创建,存在则报错。- 权限标志如
0666。
返回值:
- 成功返回共享内存标识符
shmid。 - 失败返回
-1。
shmat
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用:
- 将共享内存段连接到当前进程地址空间。
参数:
shmid:共享内存标识。shmaddr:NULL:内核自动选择地址。- 非
NULL:按规则连接。
shmflg:SHM_RNDSHM_RDONLY
返回值:
- 成功返回指向共享内存首地址的指针。
- 失败返回
-1。
shmdt
c
int shmdt(const void *shmaddr);
作用:
- 将共享内存段与当前进程脱离。
注意:
shmdt不等于删除共享内存。- 只是当前进程不再映射它。
shmctl
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用命令:
IPC_STAT:获取共享内存状态。IPC_SET:设置共享内存状态。IPC_RMID:删除共享内存段。
2. 使用流程
text
ftok 生成 key
↓
shmget 创建/获取共享内存
↓
shmat 挂接到进程地址空间
↓
读写共享内存
↓
shmdt 去关联
↓
shmctl(IPC_RMID) 删除
3. 重要注意点
- 共享内存没有同步与互斥。
- 共享内存缺乏访问控制,会带来并发问题。
- 需要配合管道、信号量等实现访问顺序。
- IPC 资源生命周期随内核,必须显式删除,否则不会自动清除,除非重启。
- 查看共享内存:
bash
ipcs -m
删除:
bash
ipcrm -m shmid
4. 示例思路
- server 创建共享内存,并创建 FIFO。
- client 获取共享内存,写入数据。
- 通过 FIFO 进行访问控制,让 server 按顺序读取。
- server 最后
shmdt,再shmctl(IPC_RMID)删除共享内存。
五、System V 消息队列(了解)
- 消息队列提供从一个进程向另一个进程发送数据块的方法。
- 每个数据块都有类型。
- 接收者可以按类型接收数据。
- IPC 资源必须删除,否则不会自动清除。
- System V IPC 资源生命周期随内核。
六、System V 信号量(了解)
信号量主要用于同步和互斥。
基本概念:
- 共享资源:多个执行流能看到的同一份公共资源。
- 临界资源:被保护起来的共享资源。
- 临界区:访问临界资源的代码。
- 互斥:任何时刻只允许一个执行流访问资源。
- 同步:多个执行流访问临界资源时具有顺序性。
信号量理解:
- 本质是一个计数器。
- 作用:保护临界区。
- 本质:对资源的预订机制。
- P 操作:申请资源,计数器
--。 - V 操作:释放资源,计数器
++。
七、内核如何组织管理 IPC 资源
参考 Linux 2.6.18 内核源码。
关键结构:
struct ipc_idsin_usemax_idseqmutexipc_id_ary
struct ipc_id_arysizestruct kern_ipc_perm *p[0]
struct kern_ipc_permlockdeletedkeyuidgidcuidcgidmodeseqsecurity
具体资源结构:
- 消息队列:
struct msg_queue - 信号量:
struct sem_array - 共享内存:
struct shmid_kernel
理解:
- 内核用统一的
kern_ipc_perm管理 IPC 资源的权限和公共属性。 - 具体资源结构内嵌
kern_ipc_perm,类似面向对象中的"继承"或"多态"。