Linux进程间通信总结
每个进程都有自己独立的虚拟地址空间,一个进程不能直接访问另一个进程的内存。但在很多应用场景中,多个进程需要协同工作,交换数据。IPC 机制就是操作系统提供的一套让不同进程能够互相通信、同步操作的机制。
进程间通信方式主要有:
- 匿名管道
- 命名管道
- 消息队列
- 共享内存
- 信号量
- 套接字
匿名管道
管道是内核中的一块缓冲区,它有一个读取端和一个写入端。一个进程从写入端写入数据,另一个进程从读取端读出数据。数据以先进先出(FIFO)的方式流动。
int pipe(int pipefd[2]); 
特点:
- 半双工: 数据只能在一个方向上流动。如果需要双向通信,需要创建两个管道。
- 亲缘关系: 匿名管道通常用于父子进程或者兄弟进程之间。因为子进程会继承父进程打开的文件描述符,从而共享这个管道。
- 生命周期: 管道的生命周期随进程,当所有使用它的进程都关闭了对它的引用(文件描述符)后,管道就会被销毁。
- 字节流: 管道传输的是无格式的字节流,没有消息边界的概念。
int main() {
    int pipe_fd[2]; // pipe_fd[0] is for reading, pipe_fd[1] is for writing
    pid_t pid;
    char buffer[128];
    char *message = "Hello from parent!";
    // 1. 创建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    // 2. 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) { // 父进程
        // 关闭不使用的读取端
        close(pipe_fd[0]);
        // 3. 向管道写入数据
        printf("Parent (PID: %d) is sending message: '%s'\n", getpid(), message);
        write(pipe_fd[1], message, strlen(message));
        
        // 关闭写入端,这样子进程的read才会返回0(EOF)
        close(pipe_fd[1]);
        
        // 等待子进程结束
        wait(NULL);
        printf("Parent process finished.\n");
    } else { // 子进程
        // 关闭不使用的写入端
        close(pipe_fd[1]);
        // 4. 从管道读取数据
        int n = read(pipe_fd[0], buffer, sizeof(buffer));
        buffer[n] = '\0'; // Add null terminator
        printf("Child (PID: %d) received message: '%s'\n", getpid(), buffer);
        
        // 关闭读取端
        close(pipe_fd[0]);
        exit(EXIT_SUCCESS);
    }
    return 0;
}
命名管道
命名管道,也叫FIFO,克服了匿名管道只能用于亲缘进程的限制。
FIFO 是一种特殊的文件类型,它在文件系统中以一个路径名存在。任何知道这个路径名的进程都可以打开它进行读写,从而实现通信。
int mkfifo(const char *pathname, mode_t mode);
特点:
- 无关进程: 可以在完全不相关的进程之间进行通信。
- 文件系统实体: 作为一个文件存在,可以使用 ls -l查看,其文件类型为p。
- 遵循FIFO规则: 仍然是先进先出的字节流通信。
- 阻塞行为: 默认情况下,openFIFO 用来读取的进程会阻塞,直到有另一个进程open同一个 FIFO 用来写入。
当 open 一个 FIFO 时,是否设置非阻塞标志(O_NONBLOCK)的区别:
- 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它。
- 若指定了O_NONBLOCK,则只读open立即返回。而只写open将出错返回-1, 如果没有进程已经为读而打开该·FIFO·,其errno置ENXIO。
#define FIFO_PATH "/tmp/myfifo"
// write.c
int main() {
    // 1. 创建命名管道
    mkfifo(FIFO_PATH, 0666);
    // 2. 打开FIFO进行写入
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    } 
    char *message = "Hello from writer!";
    printf("Writer: Sending message: '%s'\n", message); 
    // 3. 写入数据
    write(fd, message, strlen(message));
    // 4. 关闭FIFO
    close(fd);
    printf("Writer: Finished.\n");
}
// reader.c
int main() {
    char buffer[128];
    // 1. 打开FIFO进行读取 (会阻塞直到有写入者打开)
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    // 2. 读取数据
    int n = read(fd, buffer, sizeof(buffer));
    buffer[n] = '\0';
    printf("Reader: Received message: '%s'\n", buffer);
    // 3. 关闭并删除FIFO
    close(fd);
    unlink(FIFO_PATH); // 删除FIFO文件
    printf("Reader: Finished and cleaned up FIFO.\n");
}
消息队列
消息队列是内核中维护的一个消息链表。它克服了管道无格式字节流的缺点,允许进程发送和接收带有类型和特定格式的消息。
每个消息队列由一个唯一的标识符(key)来标识。进程通过这个 key 来获取队列ID,然后向队列中发送(msgsnd)或接收(msgrcv)消息。
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
特点:
- 消息结构: 存放在队列中的是带有特定结构和类型的消息,而不仅仅是字节流。
- 类型筛选: 接收方可以按消息类型选择性地接收消息,而不是必须按先进先出的顺序。
- 独立生命周期: 生命周期随内核,即使所有通信进程都退出了,消息队列仍然存在,直到被显式删除或系统重启。
// 定义消息结构
struct msg_buffer {
    long msg_type;
    char msg_text[100];
};
// write.c
int main() {
    key_t key;
    int msgid;
    struct msg_buffer message;
    // 1. 生成唯一的key
    key = ftok("progfile", 65);
    // 2. 获取消息队列ID (如果不存在则创建)
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        exit(EXIT_FAILURE);
    }
    // 3. 准备要发送的消息
    message.msg_type = 1; // 消息类型必须大于0
    strcpy(message.msg_text, "Hello from Message Queue!");
    printf("Sender: Sending message...\n");
    // 4. 发送消息
    if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }
    printf("Sender: Message sent.\n");
    return 0;
}
// read.c
int main() {
    key_t key;
    int msgid;
    struct msg_buffer message;
    key = ftok("progfile", 65);
    msgid = msgget(key, 0666 | IPC_CREAT);
    printf("Receiver: Waiting for message...\n");
    // 1. 接收消息 (类型为1)
    if (msgrcv(msgid, &message, sizeof(message.msg_text), 1, 0) == -1) {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Received message: '%s'\n", message.msg_text);
    // 2. 删除消息队列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Message queue removed.\n"); 
    return 0;
}
共享内存
共享内存是最高效的IPC方式,因为它允许多个进程直接读写同一块物理内存,避免了在内核和用户空间之间复制数据的开销。
系统在内存中创建一段特殊的区域,然后多个进程可以将这个区域映射到自己的虚拟地址空间中。之后,一个进程写入的数据可以被另一个进程立即看到,就像操作自己的本地内存一样。
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr); 
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
特点:
- 速度最快: 数据不需要在进程之间进行任何拷贝,是速度最快的IPC。
- 无同步机制: 共享内存本身不提供任何同步机制。必须自己处理并发访问的问题,通常结合信号量**来保证数据的一致性。
- 生命周期: 同消息队列一样,生命周期随内核,需要显式删除。
// write.c
int main() {
    key_t key = ftok("shmfile", 65);
    // 1. 创建或获取共享内存段
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }
    // 2. 将共享内存附加到进程的地址空间
    char *str = (char*) shmat(shmid, (void*)0, 0);
    if (str == (char*)-1) {
        perror("shmat");
        exit(1);
    }
    // 3. 写入数据到共享内存
    printf("Writer: Writing to shared memory: 'Hello from Shared Memory!'\n");
    strcpy(str, "Hello from Shared Memory!");
    // 4. 分离共享内存
    shmdt(str);
    printf("Writer: Finished.\n");
    return 0;
}
// read.c
int main() {
    key_t key = ftok("shmfile", 65);
    // 1. 获取共享内存段ID
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
    // 2. 附加共享内存
    char *str = (char*) shmat(shmid, (void*)0, 0);
    // 3. 读取数据
    printf("Reader: Reading from shared memory: '%s'\n", str);
    // 4. 分离共享内存
    shmdt(str);
    // 5. 删除共享内存段
    shmctl(shmid, IPC_RMID, NULL);
    printf("Reader: Shared memory segment removed.\n");
    return 0;
}
信号量
信号量本质上是一个计数器,它不用于传输数据,而是用于控制多个进程对共享资源的访问,即用于进程同步。
信号量的值表示可用资源的数量。
- P操作 (等待 wait): 尝试获取资源。如果信号量的值大于0,则将其减1并继续执行。如果值为0,则进程阻塞,直到有其他进程释放资源。
- V操作 (信号 signal): 释放资源。将信号量的值加1。如果有进程因为等待该资源而阻塞,则唤醒其中一个。
主要用于解决进程/线程间的同步与互斥问题,常与共享内存结合使用。
struct sembuf {
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
};
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
套接字
套接字是目前最通用、最强大的IPC机制。
通信的进程创建各自的套接字,其中一个作为服务器,另一个作为客户端。连接建立后,就可以像读写文件一样进行双向通信。
特点:
- 通用性: 既能用于本地IPC,也能用于网络IPC。
- 双向通信: 一个连接建立后,客户端和服务器都可以自由地收发数据。
- 多种协议: 支持TCP和UDP等多种协议。
- UNIX域套接字: 当用于本地通信时,使用AF_UNIX地址族,它通过文件系统中的一个特殊文件进行通信。
#define SOCKET_PATH "/tmp/my_socket"
//  write
int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[256];
    // 1. 创建 UNIX 域套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 2. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
    unlink(SOCKET_PATH); // 如果已存在,先删除
    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    // 3. 监听连接
    listen(server_fd, 5);
    printf("Server is listening...\n");
    // 4. 接受客户端连接
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
    printf("Server accepted connection.\n");
    // 5. 读取数据
    read(client_fd, buffer, sizeof(buffer));
    printf("Server received: %s\n", buffer);
    // 6. 发送数据
    write(client_fd, "Hello from server!", 18);
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}
// read
int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    char buffer[256];
    // 1. 创建套接字
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 2. 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
    connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    // 3. 发送数据
    write(client_fd, "Hello from client!", 18);
    // 4. 接收数据
    read(client_fd, buffer, sizeof(buffer));
    printf("Client received: %s\n", buffer);
    close(client_fd);
    return 0;
}
对比
性能对比
| 性能维度 | 管道 | 消息队列 | 共享内存 | 信号量 | 套接字 | 
|---|---|---|---|---|---|
| 传输速率 | 中等。 受限于缓冲区大小,涉及两次数据拷贝(用户区→内核区→用户区)。 | 较慢。 与管道一样有两次数据拷贝,且内核管理更复杂,开销更大。 | 最快 (🥇)。 数据直接在内存中读写,无数据拷贝,速率接近内存读写速度。 | 不适用。 用于同步和互斥,不传输数据。但其自身操作非常轻量。 | 极快。 本地通信(UNIX域)非常快,但因仍涉及数据拷贝,速率慢于共享内存。网络通信速率受网络条件限制。 | 
| 延迟 | 较低。 延迟主要受数据量和内核调度影响。 | 较高。 延迟受消息大小、队列长度和内核调度影响,是延迟最高的IPC之一。 | 最低 (🥇)。 几乎没有延迟,因为它避免了所有数据复制和内核中转。 | 极低。 操作延迟非常低,尤其是在没有锁竞争的情况下。 | 较低。 本地通信延迟很低。网络通信延迟取决于网络状况。 | 
数据量对比
| 数据量维度 | 管道 | 消息队列 | 共享内存 | 信号量 | 套接字 | 
|---|---|---|---|---|---|
| 数据种类 | 字节流。 无消息边界,适用于文本和二进制流。 | 结构化消息。 可传递带类型的、有明确边界的消息(如结构体)。 | 任意类型。 可以共享任何数据类型,从简单变量到复杂的数据结构。 | 整数计数。 传递的是一个信号或状态(计数值),不传递业务数据。 | 字节流。 主要传递字节流,复杂结构需应用层进行序列化/反序列化。 | 
| 数据上限 | 小。 受限于管道缓冲区大小(通常为64KB)。 | 中等。 受系统对队列总大小和消息最大尺寸的限制。 | 极大。 受限于可用物理内存和系统配置,是容量最大的IPC方式。 | 无上限。 不传输数据。 | 大。 上限主要受网络带宽、发送/接收缓冲区大小和系统资源限制。 | 
| 能否并发 | 有限支持。 通常只支持一个写进程和一个读进程。 | 支持。 多个进程可以并发读写,队列操作本身是原子的。 | 支持 (⚠️)。 天然支持并发访问,但必须使用信号量等外部同步机制,否则会产生严重数据竞争。 | 核心功能。 其本身就是用于管理并发、实现同步和互斥的工具。 | 支持。 服务器可并发处理多个客户端连接和数据传输。 | 
| 是否阻塞 | 是。 读空管道或写满管道时,默认会阻塞。 | 是。 读空队列或写满队列时,默认会阻塞。 | 否 (⚠️)。 数据访问本身不会阻塞。但用于同步的信号量等操作会阻塞。 | 是。 当资源不可用时(P操作),进程会阻塞等待。 | 是。 默认的读写操作在数据未准备好或连接未建立时会阻塞。 | 
稳定性对比
| 稳定性维度 | 管道 | 消息队列 | 共享内存 | 信号量 | 套接字 | 
|---|---|---|---|---|---|
| 数据丢失风险 | 低。 写满时,写入操作会阻塞,不会丢数据。只有在非阻塞模式下或进程异常退出时可能丢失。 | 中等。 当队列满时,若未做错误处理(如重试),新消息可能会发送失败而被丢弃。 | 中等 (⚠️)。 机制本身不丢数据,但无同步的并发写入会导致数据覆盖或不一致,这是一种逻辑上的数据丢失。 | 无风险。 不传输数据,不存在数据丢失问题。 | 低 (流式) / 高 (数据报)。 流式套接字(如TCP)有可靠性保证,连接中断前不丢数据。数据报套接字(如UDP)无此保证,可能丢包。 | 
| 中断处理 | 需要。 write操作可能被信号中断,需检查返回值并重试。 | 需要。 msgsnd操作可能被信号中断,需检查错误码(如EINTR)。 | 不适用。 数据传输不涉及特定系统调用,但同步操作(如 semop)可能被中断。 | 需要。 semop操作可能被信号中断,需检查错误码。 | 需要。 send/recv等操作可能被信号中断,需检查返回值并重试。 | 
| 能否续传 | ❌ 不支持。 中断后需重新发送整个数据块。 | ❌ 不支持。 中断后需重新发送整个消息。 | 不适用。 无“传输”概念,它只是内存状态的共享。 | 不适用。 无数据传输。 | ❌ 机制本身不支持。 TCP协议负责单个数据包的重传,但若连接中断,应用需自行实现断点续传逻辑。 |