一、什么是进程间通信?

说白了,进程间通信就是让两个或多个进程之间能互相传数据、发信号。你想想看,每个进程都有自己的独立地址空间,一个进程根本看不到另一个进程的变量。那它们怎么协作呢?

我刚开始学嵌入式Linux时,总觉得这玩意儿离我很远。直到有一次做多进程数据采集系统,主进程要等三个子进程都采集完数据才能做融合处理。嗯,那时候我才意识到——没有IPC,多进程就是个摆设。

为什么需要IPC?

  • 数据共享:一个进程算出来的结果,另一个进程要用
  • 任务协作:多个进程分工干活,最后汇总
  • 资源互斥:防止两个进程同时写同一个文件
  • 事件通知:一个进程干完了,告诉另一个进程可以开始了

核心要点:IPC不是锦上添花,而是多进程编程的刚需。没有IPC,进程之间就是孤岛。

二、IPC的分类全景图

我习惯把IPC分成两大类:同一台机器上的通信跨机器的通信。前者包括管道、信号、共享内存、消息队列,后者就是套接字。

下面这张图帮你理清脉络:

进程间通信 (IPC) 本地IPC(同一台机器) 网络IPC(跨机器) 管道 信号 共享内存 消息队列 信号量 套接字 (Socket) 常用IPC分类 各IPC特性速览 管道:最简单,单向流,父子进程用 共享内存:最快,但需要同步机制 套接字:最通用,支持跨网络通信

三、五种IPC机制详解

1. 管道(Pipe)

管道是最古老的IPC方式。我当年学Linux编程时,第一个接触的就是它。说白了,管道就是一个内核缓冲区,一个进程往里写,另一个从里读。

两种管道:

  • 匿名管道:只能用于父子进程或有亲缘关系的进程。用 pipe() 创建。
  • 命名管道(FIFO):通过文件系统中的一个特殊文件通信,无亲缘关系的进程也能用。
// 匿名管道示例
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd[2];
    pid_t pid;
    char buf[128];

    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid = fork();
    if (pid == 0) {
        // 子进程:写
        close(fd[0]);
        write(fd[1], "Hello from child", 17);
        close(fd[1]);
    } else {
        // 父进程:读
        close(fd[1]);
        read(fd[0], buf, sizeof(buf));
        printf("Parent received: %s\n", buf);
        close(fd[0]);
    }
    return 0;
}

避坑指南:我曾经在项目中忘记关闭不需要的管道端,结果子进程一直阻塞在read上。记住:写端用完要关,读端用完也要关。不用的端先关掉,这是铁律。

2. 信号(Signal)

信号是一种异步通信方式。进程收到信号时,会打断当前执行流,去处理信号。有点像硬件中断的软件版。

常见的信号:

  • SIGINT:Ctrl+C 触发,终止进程
  • SIGKILL:强制杀死进程,不能捕获
  • SIGUSR1/SIGUSR2:用户自定义信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    signal(SIGINT, handler);
    while(1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

注意:信号处理函数里不要调用 printf 这类不可重入函数。我踩过这个坑——信号处理时堆栈状态不确定,printf 可能把整个程序搞崩。建议只设置标志位,在主循环里处理。

3. 共享内存(Shared Memory)

共享内存是效率最高的IPC方式。为什么?因为它不需要数据拷贝。多个进程直接映射同一块物理内存,读写就像操作本地变量一样快。

我做过一个实时视频处理项目,用管道传数据太慢,换成共享内存后延迟从毫秒级降到了微秒级。嗯,差距就是这么大。

#include <sys/shm.h>
#include <stdio.h>

int main() {
    key_t key = 1234;
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);
    char *data = (char*)shmat(shmid, NULL, 0);

    sprintf(data, "Hello from process");
    printf("Data: %s\n", data);

    shmdt(data);
    return 0;
}

核心要点:共享内存本身不提供同步机制。多个进程同时写同一块内存,数据就乱了。必须配合信号量或互斥锁使用。

4. 消息队列(Message Queue)

消息队列是管道的一种升级版。它支持按消息类型读取,不像管道那样只能先进先出。每个消息有类型标识,接收方可以只读自己关心的类型。

特性管道消息队列
数据格式字节流结构化消息
读取方式FIFO按类型选择
生命周期随进程内核持久
最大长度有限可配置
#include <sys/msg.h>
#include <stdio.h>

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

int main() {
    key_t key = 5678;
    int msqid = msgget(key, IPC_CREAT | 0666);
    struct msgbuf msg = {1, "Hello"};

    msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
    printf("Message sent\n");

    return 0;
}

5. 套接字(Socket)

套接字是IPC的终极方案。它不仅能用于同一台机器的进程通信,还能跨网络通信。你上网、发微信、看视频,底层都是套接字在干活。

套接字分两种:

  • Unix域套接字:用于本地通信,效率高,比TCP快
  • 网络套接字:用于跨网络通信,基于TCP/UDP
// 简化的TCP客户端
#include <sys/socket.h>
#include <netinet/in.h>

int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");

connect(sock, (struct sockaddr*)&addr, sizeof(addr));
send(sock, "Hello", 5, 0);
close(sock);

四、如何选择?

我个人的选择原则很简单:

  • 简单场景:父子进程传数据,用管道就够了
  • 高性能场景:大量数据频繁交换,上共享内存
  • 异步通知:信号是最轻量的方式
  • 结构化通信:消息队列比管道好用
  • 跨机器:别无选择,套接字

我的经验:初学者容易一上来就用共享内存,觉得它快。但其实90%的场景管道或消息队列就够用了。共享内存带来的同步问题,往往比性能提升更让人头疼。先选简单的,不够再升级。

好了,IPC的入门就讲到这里。记住一句话:IPC不是技术难题,而是设计选择。选对了,代码清爽;选错了,调试到天亮。


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