23、多进程编程:fork()的使用、进程间通信(IPC)、共享内存、消息队列
多进程编程,说白了就是让程序“分身”。
你写一个程序跑起来,它是一个进程。如果你调用 fork(),它就变成了两个——几乎一模一样,但各自独立。我刚开始学的时候觉得这玩意儿挺魔幻的,一个函数调用完,后面代码居然有两个进程在跑。嗯,今天我们就把它彻底讲透。
23.1 fork():进程的“分身术”
fork() 是 Unix/Linux 系统里创建子进程的核心函数。调用一次,返回两次。为什么?因为父进程和子进程都会从 fork() 返回的地方继续执行。
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程
printf("我是子进程,PID=%d,父进程PID=%d\n", getpid(), getppid());
} else {
// 父进程
printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
}
return 0;
}
运行结果大致是:
我是父进程,PID=1234,子进程PID=1235
我是子进程,PID=1235,父进程PID=1234
注意,两个进程的执行顺序是不确定的。谁先跑,看调度器的心情。我在项目中遇到过一个问题:父进程先退出了,子进程变成了“孤儿进程”,被 init 进程收养。这本身不是 bug,但如果你依赖父进程做清理工作,就得小心了。
我曾经在线上服务里忘记处理子进程的退出状态,结果子进程变成了僵尸进程。系统进程表被占满,新进程创建失败。记住:父进程一定要调用
wait() 或 waitpid() 回收子进程。
23.2 进程间通信(IPC):为什么需要它?
fork() 之后,父子进程各自拥有独立的地址空间。你改一个变量,另一个进程完全看不到。那怎么协作?
Linux 提供了多种 IPC 机制。我按使用频率排个序:
| IPC 方式 | 特点 | 适用场景 |
|---|---|---|
| 管道(Pipe) | 单向通信,简单 | 父子进程间传递数据流 |
| 信号(Signal) | 异步通知,信息量少 | 通知事件,如退出、挂起 |
| 共享内存 | 速度最快,需同步 | 大量数据交换 |
| 消息队列 | 有格式的消息,可靠 | 结构化消息传递 |
| 信号量 | 同步原语 | 保护共享资源 |
| Socket | 跨网络,通用 | 不同机器间通信 |
我个人习惯:能不用 IPC 就不用。如果非要通信,优先考虑共享内存 + 信号量。为什么?因为快。但代价是你要自己处理同步,容易出 bug。
23.3 共享内存:最快的 IPC
共享内存让多个进程直接读写同一块物理内存。没有数据拷贝,所以快得离谱。
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
key_t key = ftok("/tmp", 'A');
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
char *data = (char *)shmat(shmid, NULL, 0);
if (data == (char *)-1) {
perror("shmat");
return 1;
}
// 写入数据
strcpy(data, "你好,共享内存!");
printf("写入完成: %s\n", data);
shmdt(data);
return 0;
}
另一个进程可以 attach 同一块共享内存,直接读取。你想想看,这比管道快了多少倍?
我在做视频处理中间件时,用共享内存传递帧数据。每帧 2MB,如果用管道,每秒只能传 30 帧。换成共享内存后,直接飙到 120 帧。但要注意:共享内存本身不提供同步,必须配合信号量或互斥锁。
23.4 消息队列:有结构的通信
消息队列比共享内存更“高级”一点。它传递的是有类型的消息,内核帮你管理队列。你不用操心同步问题。
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[100]; // 消息内容
};
int main() {
key_t key = ftok("/tmp", 'B');
int msqid = msgget(key, IPC_CREAT | 0666);
if (msqid == -1) {
perror("msgget");
return 1;
}
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "这是一条消息");
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
return 1;
}
printf("消息发送成功\n");
return 0;
}
接收端用 msgrcv() 按类型取消息。你可以给不同进程分配不同的消息类型,实现多对多通信。
消息队列有系统限制。我记得有一次在生产环境里,消息队列满了,发送端被阻塞,导致整个服务卡死。后来我加上了
IPC_NOWAIT 标志,并做了降级处理。
23.5 知识体系总览
下面这张图把多进程编程的核心脉络画出来了。你一看就明白:fork 是起点,IPC 是桥梁,共享内存和消息队列是两种主流方式。
23.6 实战建议
- 能用多线程就别用多进程——除非你需要更强的隔离性。进程挂了不会影响其他进程,线程挂了整个进程可能崩。
- 共享内存一定要加锁。我见过太多人图省事不加锁,结果数据错乱,查 bug 查到怀疑人生。
- 消息队列注意容量。默认限制可能很小,用
ipcs -q查看,必要时通过/proc/sys/kernel/msgmax调整。 - fork() 之后,文件描述符会被继承。如果不小心,父子进程可能同时操作同一个 socket,导致数据混乱。记得在子进程里关闭不需要的 fd。
一句话总结
多进程编程的核心就三件事:用 fork 创建进程,用 IPC 让它们说话,用 wait 回收孩子。别搞复杂了,越简单越不容易出错。