1. 进程间通信(IPC)基础概念
1.1 什么是进程间通信
进程间通信(IPC)指两个或多个进程进行信息相互传递的过程。
- 进程独立性:每个进程拥有独立的内核数据结构与代码数据空间。
- 非 IPC 示例:父子进程间的全局变量共享仅是
fork()时的写时复制初始状态,并非双向、持续的通信机制。一个进程崩溃通常不会直接影响另一个进程。
1.2 为什么需要 IPC
- 数据传输:进程间传递数据。
- 资源共享:多个进程共享同一资源。
- 通知事件:进程间发送状态或事件通知(如子进程终止通知父进程)。
- 进程控制:如调试器拦截被调试进程的异常与状态。
1.3 如何实现 IPC
核心前提:让不同的进程看到同一份资源。操作系统必须提供相应的系统调用来创建和管理这份共享资源。
2. IPC 标准与发展
早期 Unix 系统缺乏统一标准,后逐渐形成 System V IPC 标准(包含共享内存、消息队列、信号量)。随着技术发展,POSIX 标准及现代网络通信机制逐渐成为主流,但 System V 的核心思想仍具重要参考价值。
3. 匿名管道(Pipe)
3.1 核心特点
- 基于文件,单向通信:数据只能单向流动。
- 血缘限制:仅适用于具有亲缘关系的进程(通常为父子进程)。
- 生命周期随进程:管道本质是内核缓冲区,随相关进程退出而自动销毁。
- 自带同步机制:读写阻塞特性天然实现进程同步。
- 面向字节流:读写次数无需严格匹配,数据以字节流形式传输。
3.2 典型读写场景
- 写慢读快:读端阻塞,等待写端数据就绪。
- 写快读慢:读端一次性读取缓冲区中所有可用数据。
- 写端关闭:读端
read()返回0(EOF)。 - 读端关闭:写端继续写入会触发
SIGPIPE信号,导致写端进程终止。
4. 进程池与管道协同
进程池通过父进程预先创建多个子进程,利用管道分发任务。
- 常见陷阱:父进程关闭管道写端后,必须循环调用
waitpid()回收所有子进程,否则会产生僵尸进程。 - 任务分发:通常采用轮询或哈希方式将任务码通过管道发送给空闲子进程。
5. 命名管道(FIFO)
命名管道在文件系统中创建一个特殊文件(类型为 p),使无血缘关系的进程也能通过路径名找到同一份资源。
5.1 核心机制
- 客户端无需创建:只需使用
open()打开已存在的 FIFO 文件。 - 打开阻塞同步:
open()默认阻塞,直到读写两端均打开该管道。 - 字节流传输:无需手动处理 C 风格字符串的
\0,按实际读取字节数处理即可。 - 生命周期:随文件系统存在,但数据仅驻留内存缓冲区,不刷盘。
5.2 代码示例
服务端(写端)
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
const char* fifo = "myfifo";
int wfd = open(fifo, O_WRONLY);
if (wfd < 0) {
perror("open");
return 1;
}
std::string msg;
while (std::cin >> msg) {
write(wfd, msg.c_str(), msg.size());
}
close(wfd);
return 0;
}
客户端(读端)
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
int main() {
const char* fifo = "myfifo";
umask(0);
if (mkfifo(fifo, 0666) == -1 && errno != EEXIST) {
perror("mkfifo");
return 1;
}
int rfd = open(fifo, O_RDONLY);
if (rfd < 0) {
perror("open");
return 2;
}
char buf[1024];
ssize_t n;
while ((n = read(rfd, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
std::cout << "Received: " << buf << std::endl;
}
(rfd);
(fifo);
;
}
6. 共享内存(Shared Memory)
共享内存是 System V IPC 中速度最快的通信方式。
6.1 工作原理
- 物理内存分配:OS 在物理内存中开辟一块区域。
- 页表映射:将该物理区域映射到不同进程的虚拟地址空间(通常位于堆栈之间的共享区)。
- 直接访问:映射完成后,进程可直接通过指针读写该内存,无需系统调用(创建/删除除外)。
6.2 核心系统调用
| 函数 | 作用 | 关键参数说明 |
|---|---|---|
shmget(key, size, flags) | 创建或获取共享内存 | key: 唯一标识(常用 ftok() 生成)flags: IPC_CREAT | IPC_EXCL | 0666 |
shmat(shmid, addr, flags) | 挂接(映射到虚拟地址) | 返回映射后的虚拟地址指针 |
shmdt(addr) | 去关联(解除映射) | 传入 shmat 返回的地址 |
shmctl(shmid, IPC_RMID, NULL) | 删除共享内存 | 标记删除,引用计数为 0 时释放 |
6.3 关键特性与注意事项
- 生命周期随内核:进程退出不会自动释放共享内存,必须显式调用
shmctl删除或使用ipcrm -m shmid命令清理。 - Key 值生成:推荐使用
ftok(path, proj_id)算法,结合文件 inode 和项目 ID 生成唯一key_t。 - 无内置同步机制:共享内存本身不提供互斥或同步保护。多进程并发读写时,必须配合信号量或互斥锁使用,否则会导致数据覆盖或脏读。
- 大小对齐:建议设置为系统页大小(通常 4096 字节)的整数倍。OS 会向上对齐,非整数倍会浪费空间且可能掩盖越界访问。
6.4 C++ 封装示例
#ifndef SHM_HPP
#define SHM_HPP
#include <iostream>
#include <string>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
class Shm {
public:
Shm(int size = 4096) : _shmid(-1), _size(size), _key(0), _addr(nullptr) {}
~Shm() { if (_addr) Detach(); }
bool Create() {
_key = ftok("/tmp", 0x66);
if (_key < 0) { perror("ftok"); return false; }
_shmid = shmget(_key, _size, IPC_CREAT | IPC_EXCL | 0666);
return _shmid >= 0;
}
bool Get() {
_key = ftok("/tmp", 0x66);
if (_key < 0) { (); ; }
_shmid = (_key, _size, IPC_CREAT);
_shmid >= ;
}
{
_addr = (_shmid, , );
(_addr == (*)) ? : _addr;
}
{
(_addr) (_addr);
_addr = ;
}
{
(_shmid, IPC_RMID, ) == ;
}
{
ds;
(_shmid, IPC_STAT, &ds);
std::cout << << std::hex << ds.shm_perm.__key << std::dec
<< << ds.shm_segsz << << std::endl;
}
:
_key;
_shmid;
_size;
* _addr;
};
6.5 总结
- 零拷贝/少拷贝:数据直接写入对方可见的内存,无需经过内核缓冲区二次拷贝。
- 速度最快:是进程间大数据量传输的首选方案。
- 需外部同步:必须结合信号量等机制保证读写安全。


