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 是桥梁,共享内存和消息队列是两种主流方式。

多进程编程核心知识体系 fork() 创建进程 父子进程关系 · 孤儿进程 · 僵尸进程 进程间通信(IPC)—— 六大方式 共享内存 最快 · 需同步 消息队列 有结构 · 可靠 管道/信号/Socket 特定场景使用 核心原则:选对 IPC,处理好同步,避免资源泄漏

23.6 实战建议

  • 能用多线程就别用多进程——除非你需要更强的隔离性。进程挂了不会影响其他进程,线程挂了整个进程可能崩。
  • 共享内存一定要加锁。我见过太多人图省事不加锁,结果数据错乱,查 bug 查到怀疑人生。
  • 消息队列注意容量。默认限制可能很小,用 ipcs -q 查看,必要时通过 /proc/sys/kernel/msgmax 调整。
  • fork() 之后,文件描述符会被继承。如果不小心,父子进程可能同时操作同一个 socket,导致数据混乱。记得在子进程里关闭不需要的 fd。

一句话总结

多进程编程的核心就三件事:用 fork 创建进程,用 IPC 让它们说话,用 wait 回收孩子。别搞复杂了,越简单越不容易出错。

公众号:蓝海资料掘金营,微信 deep3321