Linux 进程间通信(IPC)学习笔记

1. 引言

进程间通信(Inter-Process Communication,IPC)是 Linux 多进程编程中的核心话题。每个进程拥有独立的地址空间,一个进程无法直接访问另一个进程的内存数据,因此需要借助操作系统提供的特定机制在进程之间传递数据、同步状态或协调行为。本文从基础概念出发,系统梳理 Linux 下常用的进程间通信方式,并结合代码示例帮助理解每种机制的使用场景与注意事项。

2. 为什么需要进程间通信

在单进程程序中,所有数据都共享同一份内存,函数之间通过参数和返回值即可交换信息。但多进程程序面临两个基本问题:

  • 地址空间隔离:每个进程拥有独立的虚拟地址空间,默认情况下无法互相访问对方的内存变量。
  • 协作需求:实际业务中,多个进程往往需要分工协作,例如一个进程负责采集数据,另一个进程负责处理数据,两者之间必须传递数据或通知状态。

因此,操作系统需要提供一套受控的通信机制,让进程在保证安全隔离的前提下完成数据交换与同步。这也是 IPC 机制存在的根本原因。

3. 进程间通信方式概览

Linux 下常见的进程间通信方式可以归纳为以下几类:

通信方式 数据形态 典型场景
管道(Pipe) 字节流 父子进程之间的简单数据传递
命名管道(FIFO) 字节流 无亲缘关系的进程之间通信
消息队列(Message Queue) 结构化消息 需要按消息边界收发数据的场景
共享内存(Shared Memory) 内存块 大数据量、高频次的数据交换
信号量(Semaphore) 计数器 进程间同步与互斥控制
信号(Signal) 异步通知 事件通知、异常处理
套接字(Socket) 字节流/数据报 跨主机或本机网络通信

不同方式在传输效率、易用性、适用范围上各有差异,实际选型时需要结合数据量、实时性、进程关系等因素综合判断。

4. 管道

管道是最早出现的 IPC 方式之一,本质上是内核维护的一块缓冲区,以字节流的形式在进程间传递数据。管道分为匿名管道和命名管道两种。

4.1 匿名管道

匿名管道通过 pipe() 系统调用创建,返回两个文件描述符:一个用于读端,一个用于写端。匿名管道只能在具有亲缘关系的进程之间使用,典型场景是父子进程通信。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

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

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

    pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // 子进程:关闭写端,从读端读取数据
        close(fd[1]);
        read(fd[0], buffer, sizeof(buffer));
        printf("子进程收到:%s\n", buffer);
        close(fd[0]);
    } else {
        // 父进程:关闭读端,向写端写入数据
        close(fd[0]);
        const char *msg = "Hello from parent";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
    }

    return 0;
}

使用匿名管道时需要注意:管道中的数据是单向流动的,如果需要双向通信,通常需要创建两个管道。此外,管道读写默认是阻塞的,当读端没有数据时会阻塞等待,写端缓冲区满时也会阻塞。

4.2 命名管道(FIFO)

命名管道通过 mkfifo() 创建,在文件系统中以一个特殊文件的形式存在。由于它有一个路径名,因此可以用于无亲缘关系的进程之间通信。

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";

    // 创建命名管道
    mkfifo(fifo_path, 0666);

    // 以写方式打开
    int fd = open(fifo_path, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    const char *msg = "Hello via FIFO";
    write(fd, msg, strlen(msg) + 1);
    close(fd);

    // 清理
    unlink(fifo_path);
    return 0;
}

命名管道的一个特点是:打开操作默认是阻塞的。例如以只写方式打开时,会一直阻塞到有另一个进程以只读方式打开同一管道为止。这一特性在编写通信双方时需要特别留意。

5. 消息队列

消息队列以消息为单位传递数据,每条消息有独立的类型和长度,接收方可以按类型读取消息,而不必严格遵循先进先出的顺序。相比管道,消息队列天然具备消息边界,适合传递结构化数据。

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msg_buffer {
    long msg_type;
    char msg_text[128];
};

int main() {
    key_t key = ftok("/tmp", 65);
    int msgid = msgget(key, 0666 | IPC_CREAT);

    struct msg_buffer message;
    message.msg_type = 1;
    strcpy(message.msg_text, "Hello via Message Queue");

    // 发送消息
    msgsnd(msgid, &message, sizeof(message.msg_text), 0);

    // 接收消息
    struct msg_buffer received;
    msgrcv(msgid, &received, sizeof(received.msg_text), 1, 0);
    printf("收到消息:%s\n", received.msg_text);

    // 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

消息队列适合需要按类型区分消息、或者对消息顺序有特定要求的场景。不过,消息队列在内核中维护,数据需要在内核与用户空间之间多次拷贝,因此在高频大数据量场景下效率不如共享内存。

6. 共享内存

共享内存是最快的 IPC 方式。它让多个进程将同一块物理内存映射到各自的虚拟地址空间,进程可以直接读写这块内存,无需内核参与数据拷贝。

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main() {
    key_t key = ftok("/tmp", 66);
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

    // 将共享内存映射到进程地址空间
    char *data = (char *)shmat(shmid, NULL, 0);

    // 写入数据
    strcpy(data, "Hello via Shared Memory");

    // 读取数据
    printf("共享内存内容:%s\n", data);

    // 解除映射并删除共享内存
    shmdt(data);
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

共享内存虽然效率高,但存在明显的并发问题:多个进程同时读写同一块内存时,可能出现数据竞争。因此共享内存通常需要配合信号量等同步机制一起使用,以保证数据的一致性。

7. 信号量

信号量本质上是一个计数器,用于控制多个进程对共享资源的访问。它本身不传递业务数据,而是提供一种同步与互斥的手段。常见的操作包括 P 操作(等待,计数减一)和 V 操作(释放,计数加一)。

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main() {
    key_t key = ftok("/tmp", 67);
    int semid = semget(key, 1, 0666 | IPC_CREAT);

    // 初始化信号量值为 1
    union semun arg;
    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);

    struct sembuf op;

    // P 操作:申请资源
    op.sem_num = 0;
    op.sem_op = -1;
    op.sem_flg = 0;
    semop(semid, &op, 1);

    printf("进入临界区\n");

    // V 操作:释放资源
    op.sem_op = 1;
    semop(semid, &op, 1);

    // 删除信号量
    semctl(semid, 0, IPC_RMID);
    return 0;
}

信号量常用于保护共享内存、共享文件等临界资源。将信号量初始化为 1 时,它退化为互斥锁,保证同一时刻只有一个进程访问临界区。

8. 信号

信号是一种异步通知机制,内核可以通过信号通知进程某个事件已经发生,例如用户按下 Ctrl+C 会向前台进程发送 SIGINT 信号。进程可以捕获信号并执行自定义处理函数,也可以忽略或使用默认行为。

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig) {
    printf("收到信号:%d\n", sig);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, handler);

    printf("进程运行中,按 Ctrl+C 触发信号...\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

信号适合传递简单的通知信息,例如进程退出、定时器到期、用户中断等。由于信号携带的信息量有限,它不适合传递大量业务数据,更多用于事件通知和异常处理。

9. 套接字

套接字(Socket)是一种通用的通信机制,既支持本机进程间通信,也支持跨主机的网络通信。本机通信时可以使用 Unix Domain Socket,它基于文件系统路径寻址,效率高于网络套接字。

c 复制代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>

int main() {
    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "/tmp/my_socket");

    unlink("/tmp/my_socket");
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 5);

    printf("Unix Socket 服务端已启动\n");

    int client_fd = accept(server_fd, NULL, NULL);
    char buffer[128];
    read(client_fd, buffer, sizeof(buffer));
    printf("收到客户端消息:%s\n", buffer);

    close(client_fd);
    close(server_fd);
    unlink("/tmp/my_socket");
    return 0;
}

套接字功能强大、使用灵活,但接口相对复杂。对于本机内的高效通信,Unix Domain Socket 是一个很好的选择;对于跨主机通信,则需要使用 TCP 或 UDP 套接字。

10. 各方式对比与选型建议

为了更直观地比较各种 IPC 方式的特点,整理如下:

通信方式 传输效率 是否支持无亲缘进程 是否支持跨主机 典型用途
匿名管道 父子进程简单通信
命名管道 本机无亲缘进程通信
消息队列 结构化消息传递
共享内存 大数据量高频交换
信号量 同步与互斥
信号 异步事件通知
套接字 本机或跨主机通信

选型时可以参考以下思路:

  • 如果只是父子进程之间传递简单数据,优先考虑匿名管道。
  • 如果需要传递结构化消息且对顺序有要求,消息队列更合适。
  • 如果数据量大、频率高,共享内存配合信号量是效率最高的方案。
  • 如果只是需要事件通知,信号即可满足需求。
  • 如果需要跨主机通信,套接字是唯一选择。

11. 总结

Linux 进程间通信方式丰富多样,每种机制都有其适用的场景和局限性。管道简单易用,适合基础的数据传递;消息队列提供了消息边界,适合结构化通信;共享内存效率最高,但需要配合同步机制;信号量负责协调资源访问;信号适合异步通知;套接字则提供了最通用的通信能力。在实际开发中,应根据数据量、实时性、进程关系和部署环境等因素综合选型,必要时还可以组合多种机制,例如用共享内存传数据、用信号量做同步,从而构建高效可靠的多进程应用。

相关推荐
zhonyu鱼21 分钟前
Drawpile:多人实时协作绘画,一起在同一块画布上画画
笔记·pdf·开源·开源软件
张文君30 分钟前
ubuntu26.04 apt 命令补全
linux
yunwei3735 分钟前
CPU 噪声会拖慢 GPU 推理吗:用 eBPF 定量测量调度器与 IRQ 影响
linux·人工智能·性能优化
2601_9499506338 分钟前
在线刷题用什么小程序?认识一下能导资料、AI出题的练题簿
人工智能·学习·小程序·刷题·小程序推荐
HugoStudio_SWAN41 分钟前
【按键反馈】C++ 实现键盘烟花秀
开发语言·c++·学习·程序人生
帷幕落秋1 小时前
# 实验:Nginx 日志自动轮转(logrotate)
linux
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】DeepSeek API 基础概述
c++·人工智能·学习·ai·面试·deepseek
知识分享小能手1 小时前
概理论与数理统计学习教程,从入门到精通,马尔可夫链(25)
学习·机器学习·概率论
Java.熵减码农1 小时前
MySQL 远程连接故障排查全记录(Linux + Docker + Navicat)
linux·mysql·docker