Linux 进程间通讯(IPC)——总结

Linux 进程间通讯(IPC)

系列简介:本系列涵盖 Linux 下常见的 7 种进程间通信(IPC)机制,包括无名管道、有名管道、信号、消息队列、共享内存和信号量,适合物联网/嵌入式工程师系统学习。


为什么需要 IPC?

进程的地址空间隔离

每个进程都有自己独立的虚拟地址空间,彼此之间无法直接访问对方的内存。这是操作系统保护进程安全的核心机制。

graph TB subgraph P1[Process A - PID 1001] A1[Code] A2[Data] A3[Stack] A4[Heap] end subgraph P2[Process B - PID 1002] B1[Code] B2[Data] B3[Stack] B4[Heap] end P1 -.- X[Can NOT access!] X -.- P2

图注:进程 A 和进程 B 各自拥有独立的虚拟地址空间,彼此隔离。如果需要交换数据,必须通过内核提供的 IPC 机制。

IPC 机制全景图

graph TB IPC[IPC IPC] --> PIPE[Pipe] IPC --> SIGNAL[Signal] IPC --> SYSV[System V IPC] PIPE --> ANON[Anonymous Pipe<br/>Related processes only] PIPE --> FIFO[Named Pipe FIFO<br/>Any processes] SIGNAL --> SIG[Signal<br/>Async notification] SYSV --> MSGQ[Message Queue<br/>Type-based comm] SYSV --> SHM[Shared Memory<br/>Zero-copy fastest] SYSV --> SEM[Semaphore<br/>Sync & Mutex]

图注:Linux IPC 机制分为三大类:管道(无名/有名)、信号、System V IPC(消息队列/共享内存/信号量)。选择哪种取决于应用场景------大数据量选共享内存,同步选信号量,简单通信选管道。

IPC 与日常生活类比

通过生活化的场景来理解每种 IPC 机制的核心特征:

graph TB subgraph real[现实世界] A1[两个纸杯一根线] A2[公司走廊的公共信箱] A3[手机收到推送通知] A4[邮局按邮编分拣邮件] A5[办公室的公共白板] A6[十字路口的红绿灯] end subgraph ipc[IPC 机制] B1[无名管道] B2[有名管道] B3[信号] B4[消息队列] B5[共享内存] B6[信号量] end subgraph key[核心特征] C1[单向·限亲缘] C2[单向·任意进程] C3[异步·仅通知] C4[有类型·FIFO] C5[零拷贝·共享] C6[控制访问顺序] end A1 --- B1 --- C1 A2 --- B2 --- C2 A3 --- B3 --- C3 A4 --- B4 --- C4 A5 --- B5 --- C5 A6 --- B6 --- C6

图注:每种 IPC 机制都可以在现实生活中找到对应------无名管道像传话筒(只能父子间用),有名管道像公共信箱(谁都能用),信号像手机通知(异步到达),消息队列像邮局分拣(按类型取件),共享内存像公共白板(直接看),信号量像红绿灯(控制通行顺序)。


第一部分:管道(Pipe)


第 1 节:无名管道

核心概念

无名管道(Anonymous Pipe) 是 Linux 下最基础的 IPC 方式,本质是内核维护的一块内存缓冲区 。它只能在具有亲缘关系的进程(如父子进程)之间使用。

一句话总结 :无名管道像一条单向水管,一头进水(写端)、一头出水(读端),只能连通父子进程。

通信模型

graph LR subgraph Parent[Parent Process] A[write-end fd1] end subgraph Kernel[Kernel Space] B[(pipe buffer)] end subgraph Child[Child Process] C[read-end fd0] end A -- write --> B -- read --> C

图注 :父进程通过 write() 将数据写入内核管道缓冲区,子进程通过 read() 从缓冲区读取数据。

无名管道的特点

特性 说明
单向通信 数据只能从写端流向读端,不能双向同时传输
亲缘限制 仅用于父子进程或有共同祖先的进程之间
文件接口 读写端抽象为文件描述符,pipe() 返回 pipefd0(读)和 pipefd1(写)
阻塞特性 管道空则读阻塞,管道满则写阻塞
原子性 写入不超过 PIPE_BUF(POSIX 要求 ≥ 512 字节,Linux 为 4096 字节)时保证原子操作

管道文件描述符的继承

pipe() 必须在 fork() 之前调用,这样子进程才能继承父进程的文件描述符。

sequenceDiagram participant P as Parent Process participant K as Kernel participant C as Child Process P->>K: pipe(pipefd) create pipe Note over P: pipefd0=3(read-end), pipefd1=4(write-end) P->>K: fork() create child K->>C: inherit parents fd table Note over C: also has pipefd0=3, pipefd1=4 Note over P,C: now close unused ends P->>P: close(pipefd0) close read-end C->>C: close(pipefd1) close write-end Note over P: uses write-end pipefd1 only Note over C: uses read-end pipefd0 only

图注pipe() 创建管道后,父子进程各有一对读写 fd。必须关闭各自不需要的一端,否则数据流向会混乱。

管道阻塞行为详解

stateDiagram-v2 [*] --> pipe_created: pipe() success pipe_created --> reading: read() called reading --> data_available: pipe has data data_available --> read_ok: read data, return bytes reading --> blocked: pipe empty, no writer blocked --> data_arrives: other process writes data_arrives --> read_ok: read data, return bytes reading --> eof: write-end closed, pipe empty eof --> [*]: read returns 0 pipe_created --> writing: write() called writing --> write_ok: pipe has space writing --> write_blocked: pipe full write_blocked --> has_space: other process reads has_space --> write_ok: write data, return bytes writing --> sigpipe: read-end closed sigpipe --> [*]: SIGPIPE, process terminated

图注:管道读操作在空管道时阻塞(直到有数据),写操作在满管道时阻塞(直到有空间)。读端关闭后写操作会收到 SIGPIPE 信号。

核心函数:pipe()

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

int pipe(int pipefd[2]);
// 参数:pipefd[0] --- 读端, pipefd[1] --- 写端
// 返回值:成功 0,失败 -1 并设置 errno

经典示例:模拟 ls | sort -r

场景 :这是 Shell 管道 ls | sort -r 的 C 语言模拟。父进程执行 ls 命令,将输出写入管道;子进程从管道读取数据,执行 sort -r 反向排序输出。这是无名管道最经典的用法------一个进程的输出作为另一个进程的输入

sequenceDiagram participant P as Parent(ls writer) participant K as Kernel(pipe buffer) participant C as Child(sort reader) P->>K: write(file list) Note over P: close read-end K->>C: read(file list) Note over C: close write-end C->>C: sort reverse C->>C: print result P->>P: wait() for child
完整代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t cpid;
    int ret;
    int pipefd[2];

    // 1. 创建管道(必须在 fork 之前!)
    ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe() failed");
        exit(EXIT_FAILURE);
    }

    // 2. 创建子进程
    cpid = fork();
    if (cpid == -1) {
        perror("fork() failed");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ========== 子进程:扮演 sort -r ========== */
        char buffer[4096] = {0};
        ssize_t rbytes;
        int total = 0;

        close(pipefd[1]);                      // 关闭写端,只用读端

        // 从管道读取所有数据(可能分多次)
        while ((rbytes = read(pipefd[0], buffer + total,
                              sizeof(buffer) - total)) > 0) {
            total += rbytes;
        }
        buffer[total] = '\0';
        close(pipefd[0]);

        // 反向排序并输出(模拟 sort -r 效果)
        printf(">>> 子进程收到 %d 字节,反向排序如下:\n", total);
        printf("----------------------------------------\n");

        // 将数据按行分割并反向打印
        char *lines[128];
        int count = 0;
        lines[count++] = buffer;               // 第一行起始
        for (char *p = buffer; *p; p++) {
            if (*p == '\n') {
                *p = '\0';                     // 替换换行为结束符
                if (*(p+1)) lines[count++] = p + 1;
            }
        }
        // 反向打印
        for (int i = count - 1; i >= 0; i--) {
            printf("%s\n", lines[i]);
        }
        printf("----------------------------------------\n");
        exit(EXIT_SUCCESS);
    }
    else if (cpid > 0) {
        /* ========== 父进程:扮演 ls ========== */
        char *files[] = {"bash", "zsh", "dash", "fish", "ksh", "tcsh", NULL};

        close(pipefd[0]);                      // 关闭读端,只用写端

        // 将文件列表写入管道
        for (int i = 0; files[i] != NULL; i++) {
            dprintf(pipefd[1], "%s\n", files[i]);
        }

        close(pipefd[1]);                      // 告诉子进程:我写完了
        wait(NULL);                            // 等待子进程结束
        printf(">>> 父进程:子进程已结束\n");
    }

    return 0;
}
执行结果
markdown 复制代码
>>> 子进程收到 30 字节,反向排序如下:
----------------------------------------
zsh
tcsh
ksh
fish
dash
bash
----------------------------------------
>>> 父进程:子进程已结束

关键点:父进程写入后关闭写端,子进程的 read() 才能返回 0 表示结束。如果父进程不关闭写端,子进程将永远阻塞在 read() 上。


管道操作的三个关键行为

场景 行为
读空管道 read() 阻塞,直到有数据写入
写端已关闭,读端读数据 read() 读完剩余数据后返回 0(类似 EOF)
读端已关闭,写端写数据 内核向写入进程发送 SIGPIPE 信号,进程默认终止
写入 > PIPE_BUF 写操作可能被其他进程的写入穿插(非原子),需自行管理

严谨的社会类比

🏢 比喻:公司内部传话

无名管道就像同一家公司内部两个相邻工位之间的专用传话筒(单向)。

  • 父进程是发送消息的人 ,子进程是接收消息的人
  • 管道就是那根绑在两个纸杯之间的绳子
  • 因为绳子必须两个纸杯都在同一家公司(亲缘关系)才能拉直,所以只能用于父子进程
  • 消息只能从一个纸杯传到另一个,不能同时反向传(单向通信
  • 当纸杯空了(管道为空),接收方只能等待直到发送方说了什么(读阻塞)
  • 当发送方把纸杯拿走了(写端关闭),接收方听完最后一句就知道结束了(read 返回 0)

第 2 节:有名管道(FIFO)

核心概念

有名管道(Named Pipe / FIFO) 突破了无名管道的亲缘限制,允许任意两个进程通过同一个管道文件进行通信。

一句话总结 :有名管道像公共场所的对讲机------任何两个人都可以用,但数据仍然是单向流动的。

与无名管道的对比

特性 无名管道 有名管道(FIFO)
使用范围 仅亲缘关系进程 任意进程
文件系统可见 不可见 可见,有路径名
存储位置 内核内存 内核内存(文件系统仅作标识)
创建方式 pipe() mkfifo()mkfifo 命令
操作接口 文件描述符 文件描述符(open/read/write

通信模型

graph LR subgraph PA[Process A] A1[write-end] end subgraph FS[File System] F[(fifo file)] end subgraph PB[Process B] B1[read-end] end A1 -- write --> F -- read --> B1

图注:有名管道在文件系统中有可见路径,任意两个进程可通过 FIFO 文件通信。

核心函数:mkfifo()

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
// pathname : FIFO 文件路径
// mode     : 文件访问权限(如 0644)
// 返回值   : 成功 0,失败 -1 并设置 errno

经典示例:时间服务器(任意进程间通信)

场景:一个进程持续获取当前时间并写入 FIFO(时间服务器),另一个进程从 FIFO 读取并显示时间(时间客户端)。两个进程没有亲缘关系,通过 FIFO 文件通信。

sequenceDiagram participant S as Time Server participant K as Kernel(FIFO) participant C as Time Client Note over S: mkfifo(&#34;./time_fifo&#34;) S->>K: open(O_RDWR) Note over C: open(O_RDWR) also works loop every 2 seconds S->>S: get current time S->>K: write(time string) K->>C: read(time string) C->>C: print time end
time_server.c(时间服务器)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_NAME "./time_fifo"

int main(void)
{
    int fd;
    time_t now;
    char buffer[128];

    // 创建 FIFO(如果不存在)
    if (access(FIFO_NAME, F_OK) == -1) {
        mkfifo(FIFO_NAME, 0644);
    }

    printf("[Server] 等待客户端连接...\n");

    // 以只写方式打开(会阻塞直到有读端打开)
    fd = open(FIFO_NAME, O_WRONLY);
    if (fd == -1) {
        perror("open() failed");
        exit(EXIT_FAILURE);
    }

    printf("[Server] 客户端已连接,开始发送时间...\n");

    // 每 2 秒发送一次当前时间
    for (int i = 0; i < 5; i++) {
        time(&now);
        struct tm *local = localtime(&now);
        int len = strftime(buffer, sizeof(buffer),
                           "[Server] %Y-%m-%d %H:%M:%S\n", local);

        write(fd, buffer, len);
        printf("[Server] 已发送: %s", buffer);
        sleep(2);
    }

    close(fd);
    printf("[Server] 发送完毕\n");
    return 0;
}
time_client.c(时间客户端)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_NAME "./time_fifo"

int main(void)
{
    int fd;
    char buffer[128];
    ssize_t bytes;

    // 确保 FIFO 存在
    if (access(FIFO_NAME, F_OK) == -1) {
        mkfifo(FIFO_NAME, 0644);
    }

    printf("[Client] 等待服务器数据...\n");

    // 以只读方式打开(会阻塞直到有写端打开)
    fd = open(FIFO_NAME, O_RDONLY);
    if (fd == -1) {
        perror("open() failed");
        exit(EXIT_FAILURE);
    }

    // 读取并打印服务器发来的时间
    while ((bytes = read(fd, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[bytes] = '\0';
        printf("[Client] 收到: %s", buffer);
    }

    close(fd);
    printf("[Client] 服务器已断开\n");
    return 0;
}
编译与执行
bash 复制代码
# 终端 1:启动服务器
$ gcc time_server.c -o server
$ ./server

# 终端 2:启动客户端
$ gcc time_client.c -o client
$ ./client
执行结果
ini 复制代码
=== 终端 1(服务器) ===
[Server] 等待客户端连接...
[Server] 客户端已连接,开始发送时间...
[Server] 已发送: [Server] 2026-07-09 14:30:15
[Server] 已发送: [Server] 2026-07-09 14:30:17
...

=== 终端 2(客户端)===
[Client] 等待服务器数据...
[Client] 收到: [Server] 2026-07-09 14:30:15
[Client] 收到: [Server] 2026-07-09 14:30:17
...

关键点 :FIFO 以 O_WRONLY 打开时会阻塞 直到另一端以 O_RDONLY 打开。这不是"程序卡死",而是 FIFO 的同步机制!

编译与执行
bash 复制代码
# 先启动读取端(阻塞等待数据)
$ gcc fifo_read.c -o read
$ ./read &          # 后台运行

# 再启动写入端
$ gcc fifo_write.c -o write
$ ./write
执行结果
yaml 复制代码
rbytes : 11 rbuffer : fifo pipe.

有名管道的打开行为

FIFO 的 open() 具有同步配对的特性------读端和写端必须同时存在才能成功打开。

sequenceDiagram participant R as Reader participant K as Kernel(FIFO) participant W as Writer Note over R: open(O_RDONLY) R->>K: open fifo (read-only) Note over R: blocked: waiting for writer... K->>W: (writer opens later) W->>K: open(O_WRONLY) K->>R: writer found! unblock K->>W: reader found! unblock Note over R,W: both open success! can communicate R->>K: read() W->>K: write()

图注 :以 O_RDONLYO_WRONLY 打开 FIFO 时,会阻塞直到另一端也打开。这个"配对阻塞"是 FIFO 的内置同步机制。

打开方式 行为
只读打开(O_RDONLY 阻塞,直到另一端以写方式打开
只写打开(O_WRONLY 阻塞,直到另一端以读方式打开
读写打开(O_RDWR 立即返回,不阻塞(但实践中不推荐,破坏了 FIFO 的设计意图)

⚠️ 注意 :如果以 O_RDONLY 打开 FIFO 而没有任何写端打开,open() 会一直阻塞------这正是 FIFO 作为同步机制的设计。

有名管道的优缺点

优点 缺点
任意进程间通信 打开时若未配对会阻塞
操作接口与文件一致 半双工,多进程通信需多个管道
文件系统可见,便于管理 管道缓冲区有大小限制

严谨的社会类比

📮 比喻:单位内部信箱

有名管道就像公司走廊上挂的一个公共信箱

  • 任何一个部门的员工(任意进程)都可以往信箱里投信(写入)
  • 任何一个部门的员工都可以从信箱里取信(读取)
  • 信箱在走廊上是可见的(文件系统可见),但信件本身由后勤(内核)保管,并不占用办公桌空间(不占磁盘)
  • 但是,如果你打开信箱时发现它是空的(读打开时无数据),你只能等着直到有人投信(阻塞)
  • 如果你想去寄信但发现信箱口被封住了(写打开时无读端),你也只能等着直到有人来打开信箱

第二部分:信号(Signal)


第 3 节:信号基础

核心概念

信号(Signal) 是在软件层次上对硬件中断机制的一种模拟 ,是一种异步通信方式

一句话总结 :信号就像手机的通知推送------你正在做一件事时随时可能收到通知,可以选择忽略、按默认方式处理,或者打开应用自定义处理。

信号的特点

  • 异步性:进程运行时随时可能被信号打断,无法预测精确到达时间
  • 三种处理方式:忽略、捕捉(自定义处理)、执行默认操作
  • 实时性:信号会立即投递,不像消息队列需要排队

信号的来源

来源 示例
程序执行错误 内存访问越界 → SIGSEGV,除 0 → SIGFPE
其他进程发送 kill() 函数
终端操作 Ctrl+CSIGINTCtrl+\SIGQUITCtrl+ZSIGTSTP
子进程状态变化 子进程退出 → 父进程收到 SIGCHLD
定时器到期 alarm()SIGALRM

常用信号一览

c 复制代码
#define SIGHUP    1   // 终端挂起
#define SIGINT    2   // 键盘中断(Ctrl+C)
#define SIGQUIT   3   // 键盘退出(Ctrl+\)
#define SIGILL    4   // 非法指令
#define SIGABRT   6   // 异常终止(abort())
#define SIGFPE    8   // 浮点异常/除 0
#define SIGKILL   9   // 强制终止(不可忽略/捕捉)
#define SIGUSR1  10   // 用户自定义信号 1
#define SIGSEGV  11   // 段错误(无效内存引用)
#define SIGUSR2  12   // 用户自定义信号 2
#define SIGPIPE  13   // 管道破裂(读端已关闭时写入)
#define SIGALRM  14   // 定时器到期
#define SIGTERM  15   // 终止信号(kill 默认发送)
#define SIGCHLD  17   // 子进程状态变化
#define SIGCONT  18   // 继续执行已停止的进程
#define SIGSTOP  19   // 暂停进程(不可忽略/捕捉)
#define SIGTSTP  20   // 终端暂停(Ctrl+Z)

⚠️ 特别说明SIGKILL(9)和 SIGSTOP(19)不能被忽略、被捕捉或被阻塞,这是系统提供的终极控制手段。

信号的生命周期

从信号产生到被进程处理,完整经历四个阶段:

graph LR G[1. Generation] --> D[2. Delivery] D --> P[3. Handling] P --> E[4. Done] D -.-> B[2.5 Blocked] B -.-> D

图注:信号产生后,如果进程当前正在执行关键代码且阻塞了该信号,信号会保持"pending(待处理)"状态,直到进程解除阻塞后才递达。

信号的 pending 与阻塞机制

graph TB subgraph PCB[Process task_struct] subgraph mask[Signal Mask] M1[&#34;SIGINT: blocked&#34;] M2[&#34;SIGQUIT: unblocked&#34;] M3[&#34;SIGUSR1: blocked&#34;] end subgraph pending[Pending Set] P1[&#34;SIGINT: pending&#34;] P2[&#34;SIGUSR1: pending&#34;] end end subgraph events[Signal Generation] E1[&#34;Ctrl+C sends SIGINT&#34;] --> P1 E2[&#34;kill(pid, SIGUSR1)&#34;] --> P2 end P1 -.->|blocked by mask| X1[&#34;waiting...&#34;] P2 -.->|blocked by mask| X2[&#34;waiting...&#34;] mask -.->|SIGINT unblocked| DEL[&#34;deliver to handler&#34;] DEL -.->|handler executes| DONE[&#34;Done&#34;]

图注:每个进程维护一个信号屏蔽字(决定哪些信号被阻塞)和一个挂起信号集(记录已到达但被阻塞的信号)。当进程解除某个信号的阻塞后,内核会立即递送该信号。

信号处理流程

sequenceDiagram participant P1 as Sender participant K as Kernel participant P2 as Receiver Note over P2: signal() register handler P1->>K: kill(pid, SIGUSR1) K->>K: check pending signals K->>P2: deliver signal alt default action P2->>P2: execute default else custom handler P2->>P2: call registered func end

图注 :进程 A 通过 kill() 发送信号,内核检查目标进程的信号集后投递,进程 B 按已注册的处理方式响应。


核心函数:kill()raise()

c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);   // 向指定进程发送信号
int raise(int sig);             // 向当前进程自身发送信号
函数 功能 成功 失败
kill(pid, sig) 向 pid 进程发送 sig 信号 0 -1,设置 errno
raise(sig) 向自己发送 sig 信号 0 -1,设置 errno

核心函数:pause()

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

int pause(void);   // 使进程进入睡眠,直到捕获到一个信号

⚠️ 更正 :原始文档中描述 pause() 成功返回 0 是错误的。

  • pause() 只有收到信号并执行完信号处理函数后才会返回
  • 返回值为 -1 ,且 errno 被设置为 EINTR(被信号中断)
  • 从不返回 0 ------ 这是一个典型的"调用永远不成功返回"的函数

示例 1:使用 kill()raise() 控制子进程

需求 :创建一个子进程,子进程通过 raise(SIGSTOP) 暂停自己;父进程通过 kill(cpid, SIGKILL) 终止它。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main(void)
{
    pid_t cpid;

    cpid = fork();
    if (cpid < 0) {
        perror("[ERROR] fork() :");
        exit(0);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        fprintf(stdout, "\tchild %d running.\n", getpid());

        raise(SIGSTOP);                      // 暂停自己

        // 下面的代码在 SIGSTOP→SIGCONT 之后才会执行
        fprintf(stdout, "\t child %d exit\n", getpid());
        exit(EXIT_SUCCESS);
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        int status, ret;

        sleep(1);

        ret = kill(cpid, SIGKILL);           // 强制终止子进程
        if (ret == 0) {
            fprintf(stdout, "Father %d Killed child %d\n", getpid(), cpid);
        }

        waitpid(cpid, NULL, 0);
        fprintf(stdout, "father %d exit\n", getpid());
        exit(EXIT_SUCCESS);
    }

    return 0;
}
执行结果
yaml 复制代码
    child 1234 running.
Father 1233 Killed child 1234
father 1233 exit

注意 :子进程在执行 raise(SIGSTOP) 后被暂停,因此不会 打印 "child xx exit"。父进程发送 SIGKILL 直接将其终止。


示例 2:使用 pause() 等待信号

需求 :创建子进程,父进程调用 pause() 等待;子进程在 3 秒后给父进程发送 SIGUSR1 信号,唤醒父进程。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
    pid_t cpid;

    cpid = fork();
    if (cpid == -1) {
        perror("[ERROR] fork() ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        fprintf(stdout, "Child Process Start.\n");
        sleep(3);
        kill(getppid(), SIGUSR1);            // 3 秒后唤醒父进程
        exit(EXIT_SUCCESS);
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        sleep(1);
        fprintf(stdout, "Main Process Start..\n");

        pause();                             // 等待信号,陷入睡眠

        fprintf(stdout, "Main Process End.\n");
    }

    return 0;
}
执行结果
arduino 复制代码
Child Process Start.
Main Process Start..
(等待约 2 秒...)
Main Process End.

注意pause() 一定要在收到信号之前调用,否则会错过信号导致进程一直休眠。


严谨的社会类比

🚨 比喻:消防演习

信号就像火灾报警器,是一种异步通知机制。

  • SIGKILL 就像"立即撤离 "的红色警报------不可忽略、不可延迟,必须执行
  • SIGINT(Ctrl+C)就像"暂停操作"的黄色警报------可以响应也可以忽略
  • SIGALRM 就像定时器闹铃------到了设定的时间,系统自动发出通知
  • SIGCHLD 就像老师听到学生敲门------学生(子进程)离开教室时,老师(父进程)会收到通知
  • pause() 就像消防员在值班室待命------什么也不做,就等着铃声响起
  • raise(SIGSTOP) 就像自己主动按了暂停键------让自己停下来
  • kill(pid, sig) 就像远程遥控别人的设备------向另一个进程发送指令

第 4 节:信号处理 ------ signal() 函数

核心概念

信号处理有三种方式:

  1. 忽略(Ignore):对信号不做任何处理
  2. 默认(Default):按内核预设的默认行为处理
  3. 用户自定义(Custom):实现自定义处理函数,由内核在信号到达时调用

一句话总结signal() 就像你在手机上为不同联系人设置专属来电铃声------每个信号(联系人)都可以有不同的处理方式(铃声)。

三种处理方式互斥

graph TB S[Signal arrives] --> Choice{Handler type} Choice -- SIG_IGN --> A[Ignore] Choice -- SIG_DFL --> B[Default action] Choice -- Custom func --> C[Call user handler]

图注:信号到达时,内核根据进程注册的处理方式决定动作:SIG_IGN 忽略、SIG_DFL 执行默认动作、自定义函数地址则调用用户处理函数。

核心函数:signal()

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

typedef void (*sighandler_t)(int);              // 信号处理函数类型定义

sighandler_t signal(int signum, sighandler_t handler);
// signum  : 信号编号
// handler : SIG_IGN(忽略)、SIG_DFL(默认)、自定义函数地址
// 返回值  : 成功返回之前的处理函数地址,失败返回 SIG_ERR 并设置 errno

⚠️ 更正 :原始文档中存在 typedef void (*)(int) sighandler_t 的语法错误(缺少函数指针名)。

内核中的信号处理函数类型定义:

c 复制代码
typedef void __signalfn_t(int);
typedef __signalfn_t *__sighandler_t;

#define SIG_DFL ((__sighandler_t)0)     // 默认处理
#define SIG_IGN ((__sighandler_t)1)     // 忽略处理
#define SIG_ERR ((__sighandler_t)-1)    // 错误返回值

完整示例:自定义信号处理

需求 :创建一个子进程,父进程给子进程发送 SIGUSR1 信号,子进程使用自定义处理函数。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

// 自定义信号处理函数
void do_sig_usr(int sig)
{
    printf("Receive %s\n", strsignal(sig));   // strsignal 将信号编号转为可读字符串
}

int main(void)
{
    pid_t cpid;

    // 注册信号处理函数(在 fork 之前!)
    if (signal(SIGUSR1, do_sig_usr) == SIG_ERR) {
        perror("[ERROR] signal(): ");
        exit(EXIT_FAILURE);
    }

    cpid = fork();
    if (cpid == -1) {
        perror("fork(): ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        printf("Child Process <%d> start.\n", getpid());

        pause();                             // 等待信号

        exit(EXIT_SUCCESS);
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        sleep(1);
        kill(cpid, SIGUSR1);                 // 给子进程发送自定义信号
        wait(NULL);
    }

    return 0;
}
执行结果
arduino 复制代码
Child Process <1234> start.
Receive User defined signal 1

严谨的社会类比

📱 比喻:手机来电管理

信号处理方式就像你在手机上设置的联系人来电策略:

  • SIG_IGN(忽略) = 把某个联系人设为静默------来电时手机完全不响不震,就像没发生过一样
  • SIG_DFL(默认) = 使用系统默认铃声------不特别设置,来电时按标准方式提醒
  • 自定义处理函数 = 为联系人设置专属铃声+特殊震动模式------当这个人来电时,手机会执行你预设的特定动作
  • signal(SIGINT, handler) 就像设置"当来电显示是 XX 时,播放自定义铃声"

经典应用:守护进程热加载配置(SIGHUP)

场景 :服务器程序(如 Nginx、Apache)运行时,需要在不重启的情况下重新加载配置文件。业界标准做法是监听 SIGHUP 信号。

sequenceDiagram participant Admin as Admin participant Daemon as Server Daemon participant Config as Config File Note over Daemon: signal(SIGHUP, reload_config) Daemon->>Config: load config on startup Note over Daemon: serving requests... Admin->>Daemon: kill -HUP Daemon->>Daemon: call reload_config() Daemon->>Config: re-read config file Note over Daemon: apply new config Note over Daemon: continue serving...
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>

// 模拟配置项
int g_log_level = 1;        // 0=debug, 1=info, 2=error
int g_reload_count = 0;
volatile sig_atomic_t g_reload_flag = 0;

// SIGHUP 处理函数:标记重载(不直接做复杂操作)
void handle_sighup(int sig)
{
    g_reload_flag = 1;       // 仅设置标志,主循环处理
}

// 模拟加载配置文件
void load_config(void)
{
    g_reload_count++;
    // 真实场景中这里会读取配置文件
    printf("[Config] 第 %d 次加载配置完成 (log_level=%d)\n",
           g_reload_count, g_log_level);
}

int main(void)
{
    // 注册 SIGHUP 处理函数
    if (signal(SIGHUP, handle_sighup) == SIG_ERR) {
        perror("signal(SIGHUP) failed");
        exit(EXIT_FAILURE);
    }

    // 首次加载配置
    load_config();

    printf("[Server] PID=%d, 运行中... 发送 kill -HUP %d 热加载配置\n",
           getpid(), getpid());

    // 主循环:模拟服务器运行
    int ticks = 0;
    while (1) {
        // 检查是否需要重载配置
        if (g_reload_flag) {
            g_reload_flag = 0;
            load_config();
        }

        // 模拟正常工作
        printf("[Server] 正在处理请求... (运行 %d 秒)\n", ++ticks);
        sleep(1);

        // 运行 10 秒后自动退出(避免无限循环)
        if (ticks >= 10) {
            printf("[Server] 演示结束\n");
            break;
        }
    }

    return 0;
}
执行结果
ini 复制代码
[Config] 第 1 次加载配置完成 (log_level=1)
[Server] PID=12345, 运行中... 发送 kill -HUP 12345 热加载配置
[Server] 正在处理请求... (运行 1 秒)
[Server] 正在处理请求... (运行 2 秒)
(此时在另一个终端执行 kill -HUP 12345)
[Config] 第 2 次加载配置完成 (log_level=1)
[Server] 正在处理请求... (运行 3 秒)
[Server] 正在处理请求... (运行 4 秒)
...
[Server] 演示结束

关键点SIGHUP 热加载是 Linux 服务器编程的经典模式。处理函数中只设置标志位,实际重载操作在主循环中完成------这是因为信号处理函数应该保持简单,避免调用不可重入的函数。

第 5 节:定时器与子进程管理

核心函数:alarm()

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

unsigned int alarm(unsigned int seconds);
// seconds : 定时秒数
// 返回值  : 上一次定时器剩余的秒数(如果没有之前的定时器则返回 0)

要点

  • 定时器由内核管理alarm() 只负责设置定时时间
  • 定时超时后,内核向进程发送 SIGALRM 信号
  • 每个进程只有一个 实时定时器;再次调用 alarm() 会覆盖之前的设置
  • 设置 alarm(0) 可以取消当前定时器

一句话总结alarm() 就像厨房定时器------设好时间(秒),到点铃响(SIGALRM),且一次只能设一个倒计时。

sequenceDiagram participant P as Process participant K as Kernel P->>K: alarm(5) set 5s timer K->>K: start kernel timer Note over K: countdown 5s... K->>P: send SIGALRM P->>P: exec signal handler

图注 :进程调用 alarm(5) 通知内核启动 5 秒定时器,倒计时结束后内核自动发送 SIGALRM 信号。


示例 1:验证 alarm() 的返回值

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

int main(void)
{
    unsigned int ret;

    ret = alarm(5);      // 设置 5 秒定时器
    sleep(2);            // 休眠 2 秒
    printf("ret = %d\n", ret);   // 上一轮 alarm 无剩余 → 0

    ret = alarm(3);      // 覆盖:新设 3 秒,返回上次剩余(约 3 秒)
    sleep(1);
    printf("ret = %d\n", ret);   // 打印上次剩余(约 3 秒,取决于调度精度)

    return 0;
}
执行结果
ini 复制代码
ret = 0
ret = 3

解释

  1. 第一次 alarm(5) 之前没有定时器,返回 0
  2. 休眠 2 秒后,定时器还有约 3 秒剩余
  3. alarm(3) 覆盖原有定时器,返回上次剩下的约 3 秒

示例 2:配合 SIGALRM 实现定时功能

需求 :设置 3 秒定时器,定时到期后处理 SIGALRM 信号。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

void do_alarm(int sig)
{
    printf("Receive signal <%s>\n", strsignal(sig));
}

int main(void)
{
    __sighandler_t sigret;

    sigret = signal(SIGALRM, do_alarm);     // 注册 SIGALRM 处理函数
    if (sigret == SIG_ERR) {
        perror("[ERROR] signal(): ");
        exit(EXIT_FAILURE);
    }

    alarm(3);                               // 设置 3 秒定时器
    pause();                                // 等待信号

    return 0;
}
执行结果
lua 复制代码
(等待约 3 秒...)
Receive signal <Alarm clock>

SIGCHLD 信号:优雅管理子进程退出

问题

传统使用 wait() / waitpid() 的痛点:

  • 阻塞等待:父进程什么也不能做
  • 非阻塞轮询:消耗 CPU 资源
  • 子进程退出是一个异步事件,轮询或阻塞都不是理想方案
解决方案

子进程退出时,内核自动 向父进程发送 SIGCHLD 信号。父进程只需捕获此信号并在处理函数中调用 wait() 即可。

sequenceDiagram participant F as Parent participant C as Child participant K as Kernel Note over F: signal(SIGCHLD, handler) F->>K: fork() K->>C: create child Note over F: parent continues working C->>K: child exits K->>F: kernel sends SIGCHLD F->>F: call do_sig_child() F->>F: wait(NULL) recycle child

图注 :子进程退出时内核自动向父进程发送 SIGCHLD 信号,父进程在信号处理函数中调用 wait() 回收子进程,无需阻塞等待。

完整示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void do_sig_child(int sig)
{
    printf("Receive signal <%s>\n", strsignal(sig));
    wait(NULL);                             // 回收子进程
}

int main(void)
{
    pid_t cpid;
    __sighandler_t sigret;

    sigret = signal(SIGCHLD, do_sig_child); // 注册 SIGCHLD 处理函数
    if (sigret == SIG_ERR) {
        perror("[ERROR] signal(): ");
        exit(EXIT_FAILURE);
    }

    cpid = fork();
    if (cpid == -1) {
        perror("[ERROR] fork(): ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        printf("Child process <%d> start.\n", getpid());
        sleep(2);
        exit(EXIT_SUCCESS);
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        while (1) {
            // 父进程可以做其他工作,无需阻塞或轮询
        }
    }

    return 0;
}
执行结果
arduino 复制代码
Child process <1234> start.
(约 2 秒后)
Receive signal <Child exited>

⚠️ 注意 :在信号处理函数中调用 wait() 需要谨慎。如果多个子进程同时退出,SIGCHLD 信号可能合并(不可靠信号不排队),此时应在处理函数中循环调用 waitpid(-1, NULL, WNOHANG) 直到返回 0。


严谨的社会类比

⏰ 比喻:快递定时取件

alarm() + SIGALRM 的方案就像我们在网上购物时设置的快递定时提醒

  • alarm(3) = 你设了一个3 小时的倒计时提醒
  • 内核 = 快递平台的后台系统,帮你计时
  • SIGALRM = 短信通知------系统到点自动给你发消息
  • 信号处理函数 = 你收到短信后做的事情(比如去取快递)

SIGCHLD 的用法就像老师让学生自主自习

  • 传统方式 = 老师(父进程)一直盯着 学生(子进程)做完作业再走(阻塞 wait),或者每隔一分钟去看一眼(轮询 waitpid)
  • SIGCHLD 方式 = 老师告诉学生"做完来找我 "(注册信号处理函数),然后自己安心备课(做其他工作)
  • 学生做完后主动敲门(子进程退出 → 内核发送 SIGCHLD)
  • 老师听到敲门后去签字验收(调用 wait 回收)

第三部分:System V IPC


第三部分导览 :前两部分我们学习了管道 (第 1-2 节)和信号(第 3-5 节),它们都是 Linux 下重要的 IPC 手段。

本部分聚焦 System V IPC ------由 Unix System V 引入的三套高级 IPC 机制,由内核统一维护,共用 ftok() 生成 key。包括消息队列(第 6-7 节)共享内存(第 8-9 节)信号量(第 10-12 节) ,最后通过生产者-消费者经典问题(第 13 节) 综合运用。


第三部分概述:System V IPC

一句话定位:如果说管道和信号是 Linux IPC 的"轻量级工具",那么 System V IPC 就是"重型武器库"------功能更强、效率更高,但使用也更复杂。

管道、信号、System V IPC 的角色分工

前面我们已经学过的 IPC 机制:

已学部分 机制 核心特点
第一部分:管道 无名管道、有名管道 简单单向通信,基于文件接口
第二部分:信号 信号(Signal) 异步通知,不传数据只传信号编号

它们都是 IPC,但各有局限。System V IPC 填补了它们力所不能及的领域:

System V IPC 三件套

graph TB subgraph kernel[Kernel Space] MSG[Message Queue] SHM[Shared Memory] SEM[Semaphore] end subgraph user[User Space] P1[Process A] P2[Process B] P3[Process C] end P1 -- msgsnd/msgrcv --> MSG P2 -- msgsnd/msgrcv --> MSG P1 -- shmat/shmdt --> SHM P2 -- shmat/shmdt --> SHM P1 -- semop --> SEM P2 -- semop --> SEM P3 -- semop --> SEM

图注 :三种 System V IPC 对象都由内核维护,消息队列支持按类型收发消息,共享内存实现零拷贝高速通信,信号量控制进程同步与互斥。它们通过 key 来唯一标识,用 ipcs 命令查询。

三种 IPC 对象

IPC 对象 功能 查询命令
消息队列(Message Queue) 以消息为单位的 FIFO 通信 ipcs -q
共享内存(Shared Memory) 进程间共享内存区域 ipcs -m
信号量(Semaphore) 进程同步与互斥 ipcs -s

ipcs 命令查看示例

lua 复制代码
------ Message Queues --------
key        msqid  owner   perms  used-bytes  messages
0x00019879 1      ben     666    0           0

------ Shared Memory Segments --------
key        shmid  owner   perms  bytes     nattch  status
0x00000000 11     ben     600    524288    2       dest

------ Semaphore Arrays --------
key        semid  owner   perms  nsems

核心工具函数:ftok() ------ 生成 IPC 键值

所有 System V IPC 对象都需要一个唯一的 key 来标识,ftok() 负责生成这个 key。

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

key_t ftok(const char *pathname, int proj_id);
// pathname : 一个已存在的文件路径
// proj_id  : 8 位整数(0~255)
// 返回值   : 成功返回合成的 key_t 值,失败返回 -1

key 的生成原理

ini 复制代码
key = 文件的 inode 节点号(低 24 位) + proj_id(低 8 位) 组合而成
shell 复制代码
$ ls -i
3804600 install_for_developers.sh
3804597 INSTALL.py
3804598 LICENSE

一句话总结ftok() 就像用经纬度定位一个地点------文件路径(经度)+ 项目 ID(纬度),唯一确定一个 IPC 对象。


第 6 节:消息队列(一)------ 创建与删除

核心概念

消息队列(Message Queue) 是一个由内核维护的消息链表 ,具有 FIFO 特性,但比管道更灵活------消息有类型 ,支持按类型读取

一句话总结 :消息队列就像邮局的分类信箱------每个消息都贴了"类型标签",收件人可以选择只取某一类邮件。

通信模型

graph LR subgraph PA[Process A] A[Send type=100] end subgraph Kernel[Kernel Space] Q[(Message Queue)] end subgraph PB[Process B] B[Recv type=100] end subgraph PC[Process C] C[Recv type=200] end A --> Q Q --> B Q --> C

图注:消息队列位于内核空间,支持按消息类型(type)选择性读取。进程 A 发送 type=100 的消息,进程 B 只接收 type=100 的消息,进程 C 只接收 type=200 的消息。

消息队列的核心特性------按类型读取

graph TB subgraph Queue[Queue internal structure] M1[Msg1: type=100 data=Hello] M2[Msg2: type=200 data=World] M3[Msg3: type=100 data=Foo] M4[Msg4: type=300 data=Bar] M1 --> M2 --> M3 --> M4 end subgraph Reader1[Process calling msgrcv] R1[&#34;recv type=100&#34;] R1 -.->|read type 100 only| M1 R1 -.-> M3 end subgraph Reader2[Process calling msgrcv] R2[&#34;recv type=200&#34;] R2 -.->|read type 200 only| M2 end subgraph Reader3[Process calling msgrcv] R3[&#34;recv type=0 all&#34;] R3 -.->|read earliest msg| M1 end

图注msgrcv() 的第四个参数 msgtyp 控制读取行为:> 0 精确匹配该类型;= 0 读取最早消息(FIFO);< 0 读取类型 ≤ |msgtyp| 的最小值。


核心函数:msgget() ------ 创建/获取消息队列

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

int msgget(key_t key, int msgflg);
// key    : ftok() 生成的 key
// msgflg : IPC_CREAT(创建)、IPC_EXCL(排他)、权限位
// 返回值 : 成功返回消息队列 ID,失败返回 -1

核心函数:msgctl() ------ 控制消息队列

c 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// msqid : 消息队列 ID
// cmd   : IPC_STAT(获取属性)、IPC_SET(设置属性)、IPC_RMID(删除队列)
// buf   : 消息队列属性结构体指针(IPC_RMID 时传 NULL)
// 返回值: 成功 0,失败 -1

消息队列属性结构体:

c 复制代码
struct ipc_perm {
    key_t          __key;    /* 创建时使用的 key */
    uid_t          uid;      /* 所有者有效 UID */
    gid_t          gid;      /* 所有者有效 GID */
    uid_t          cuid;     /* 创建者有效 UID */
    gid_t          cgid;     /* 创建者有效 GID */
    unsigned short mode;     /* 权限位 */
    unsigned short __seq;    /* 序列号 */
};

示例:创建消息队列并删除

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

#define PATHNAME "."
#define PRO_ID 10

int main(void)
{
    key_t key;
    int msgid;
    int ret;

    // 1. 生成 key
    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");    // 原始文档中误写为 fotk
        exit(EXIT_FAILURE);
    }

    // 2. 创建消息队列
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget(): ");
        exit(EXIT_FAILURE);
    }

    printf("msg id : %d\n", msgid);

    // 3. 删除消息队列
    ret = msgctl(msgid, IPC_RMID, NULL);
    if (ret == -1) {
        perror("msgctl(): ");
        exit(EXIT_FAILURE);
    }

    return 0;
}
执行结果
bash 复制代码
msg id : 1

使用 ipcs -q 验证:

diff 复制代码
------ Message Queues --------
key        msqid  owner   perms  used-bytes  messages
0x0a051dd0 1      ben     666    0           0         ← 刚创建
(程序运行后该队列已被 IPC_RMID 删除)

严谨的社会类比

🏣 比喻:公司收发室

消息队列像公司的收发室(内核维护的邮件架):

  • ftok() = 给收发室分配一个房间号(唯一标识)
  • msgget(IPC_CREAT) = 租用这个房间放置邮件架
  • msgsnd() = 往架子上放一封信(贴好类型标签)
  • msgrcv(type=100) = 只取走"技术部"的邮件(按类型读取)
  • msgctl(IPC_RMID) = 退租,清空并关闭收发室
  • ipcs -q = 去行政部查询当前有哪些房间被租用

第 7 节:消息队列(二)------ 发送与接收

核心函数:msgsnd() ------ 发送消息

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

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// msqid  : 消息队列 ID
// msgp   : 消息结构体指针
// msgsz  : 消息内容长度(不含 mtype)
// msgflg : 0(阻塞)或 IPC_NOWAIT(非阻塞)
// 返回值 : 成功 0,失败 -1

核心函数:msgrcv() ------ 接收消息

c 复制代码
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// msqid  : 消息队列 ID
// msgp   : 消息结构体指针
// msgsz  : 消息缓冲区大小
// msgtyp : 消息类型(>0 只接收该类型,0 接收最早消息,<0 接收类型≤|msgtyp| 的最小值)
// msgflg : 0(阻塞)或 IPC_NOWAIT(非阻塞)
// 返回值 : 成功返回实际读取的字节数,失败返回 -1

消息结构体

c 复制代码
struct msgbuf {
    long mtype;          /* 消息类型,必须 > 0 */
    char mtext[1];       /* 消息数据(可定义为更大的数组) */
};

注意mtype 必须为正整数(>0),这是消息队列按类型读取的基础。


完整示例:两个无血缘关系的进程通过消息队列通信

msg_send.c(发送端)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define PATHNAME "."
#define PRO_ID   10
#define MSG_TYPE 100
#define MSG_SZ   64

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

int main(void)
{
    key_t key;
    int msgid, ret;
    struct msgbuf msg;

    // 1. 生成 key
    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        exit(EXIT_FAILURE);
    }

    // 2. 获取消息队列
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget(): ");
        exit(EXIT_FAILURE);
    }

    printf("msg id : %d\n", msgid);

    // 3. 构造消息
    msg.mtype = MSG_TYPE;                    // 消息类型
    strcpy(msg.mtext, "Hello msg queue");    // 消息内容

    // 4. 发送消息
    ret = msgsnd(msgid, (const void *)&msg, strlen(msg.mtext) + 1, 0);
    if (ret == -1) {
        perror("msgsnd(): ");
        exit(EXIT_FAILURE);
    }

    printf("Message sent: %s\n", msg.mtext);

    return 0;
}
msg_recv.c(接收端)------ 原始文档误命名为 msg_write.c
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define PATHNAME "."
#define PRO_ID   10
#define MSG_TYPE 100
#define MSG_SZ   64

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

int main(void)
{
    key_t key;
    int msgid, ret;
    ssize_t rbytes;
    struct msgbuf rcv_msg;

    // 1. 生成 key
    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        exit(EXIT_FAILURE);
    }

    // 2. 获取消息队列
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget(): ");
        exit(EXIT_FAILURE);
    }

    printf("msg id : %d\n", msgid);

    // 3. 按类型接收消息
    rbytes = msgrcv(msgid, (void *)&rcv_msg, MSG_SZ, MSG_TYPE, 0);
    if (rbytes == -1) {
        perror("[ERROR] msgrcv(): ");
        exit(EXIT_FAILURE);
    }

    printf("mtype : %ld\n", rcv_msg.mtype);
    printf("mtext : %s\n", rcv_msg.mtext);

    // 4. 删除消息队列
    ret = msgctl(msgid, IPC_RMID, NULL);
    if (ret == -1) {
        perror("msgctl(): ");
        exit(EXIT_FAILURE);
    }

    return 0;
}
编译与执行
bash 复制代码
# 先启动接收端(阻塞等待)
$ gcc msg_recv.c -o recv
$ ./recv &
# 再启动发送端
$ gcc msg_send.c -o send
$ ./send
执行结果
yaml 复制代码
(接收端)msg id : 1
(发送端)msg id : 1
(发送端)Message sent: Hello msg queue
(接收端)mtype : 100
(接收端)mtext : Hello msg queue

消息队列 vs 管道

特性 管道 消息队列
数据单位 字节流(无边界) 消息(有结构、有类型)
读取方式 全部读出,无法选择 可按类型读取
通信范围 无名:父子;有名:任意 任意进程
生命周期 随进程 内核维护,显式删除
双向通信 需两个管道 一个队列即可(通过类型区分)

严谨的社会类比

📬 比喻:公司内部邮件系统

消息队列的有类型读取能力非常像企业的内部邮件分拣系统

  • msgsnd() = 你在邮件上贴了标签"财务部"(mtype=100)并投进收发室
  • msgrcv(type=100) = 财务部的同事只取标签为"财务部"的邮件
  • 即使"技术部"的邮件堆满了架子,也不会影响财务部取走自己的邮件
  • 这就是消息队列比管道更灵活 的地方------管道就像一根水管,所有数据混在一起流过去;而消息队列像一个带分类标签的邮件架,可以按主题提取

第 8 节:共享内存(一)------ 创建与删除

核心概念

共享内存(Shared Memory) 是效率最高的 IPC 方式之一------它将一块物理内存直接映射 到多个进程的虚拟地址空间中,进程间通信时无需内核介入拷贝数据。

一句话总结 :共享内存就像两个同事共用一块白板------A 写上去,B 直接就能看到,不需要经过任何中间人传达。

共享内存 vs 其他 IPC

graph TB subgraph copy_mode[Pipe/MsgQ - With Copy] P1[Process A] -- write --> K[kernel buffer] K -- read --> P2[Process B] end subgraph zero_copy[Shared Mem - Zero Copy] M[Physical Memory] P3[Process A] -- shmat/mmap --> M P4[Process B] -- shmat/mmap --> M end

图注:管道和消息队列需要数据在用户态和内核态之间拷贝两次(write + read);共享内存直接将物理内存映射到进程地址空间,实现零拷贝,效率最高。

通信模型

graph TB subgraph VA[Process A Addr Space] A[Virtual Addr] -- shmat --> SM end subgraph PM[Physical Memory] SM[(Shared Memory)] end subgraph VB[Process B Addr Space] B[Virtual Addr] -- shmat --> SM end A --- SM SM --- B

图注 :进程 A 和进程 B 分别通过 shmat() 将同一块物理内存映射到各自的虚拟地址空间,双方对这块内存的读写直接生效。

共享内存的完整生命周期

graph LR C[1. shmget create] --> A[2. shmat attach] A --> U[3. read/write via ptr] U --> D[4. shmdt detach] D --> R[5. shmctl IPC_RMID] R -.->|all detached| F[6. kernel frees]

图注:共享内存的完整生命周期:创建(shmget)→ 挂载(shmat)→ 读写(指针操作)→ 卸载(shmdt)→ 标记删除(shmctl IPC_RMID)。注意:IPC_RMID 只是标记删除,直到所有进程都 detach 后才真正释放。

共享内存 vs 其他 IPC 的零拷贝原理

sequenceDiagram participant PA as Process A participant K as Kernel participant PB as Process B Note over PA: Pipe/MsgQ approach PA->>PA: prepare data PA->>K: write/msgsnd copy to kernel K->>PB: read/msgrcv copy to user Note over K: 2 copies! slow Note over PA: Shared Mem approach PA->>K: shmget + shmat K->>PB: shmget + shmat Note over PA,PB: both map to same phys page PA->>PA: direct write *addr = data Note over PB: direct read data = *addr Note over K: zero copy! fastest

查询共享内存

bash 复制代码
$ ipcs -m

------ Shared Memory Segments --------
key        shmid  owner   perms  bytes     nattch  status
0x00000000 11     ben     600    524288    2       dest
0x00000000 16     ben     600    524288    2       dest
0x00000000 35     ben     600    4194304   2       dest

核心函数:shmget() ------ 创建/获取共享内存

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

int shmget(key_t key, size_t size, int shmflg);
// key    : ftok() 生成的 key
// size   : 共享内存大小(字节)
// shmflg : IPC_CREAT、IPC_EXCL、权限位
// 返回值 : 成功返回共享内存 ID,失败返回 -1

核心函数:shmctl() ------ 控制共享内存

c 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// shmid : 共享内存 ID
// cmd   : IPC_STAT(获取属性)、IPC_SET(设置属性)、IPC_RMID(标记删除)
// buf   : 共享内存属性结构体指针
// 返回值: 成功 0,失败 -1

⚠️ 更正 :原始文档中 shmctl 的 cmd 描述误写为"消息队列属性",应为"共享内存属性"。


示例:创建共享内存,打印 ID 并删除

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

#define PATHNAME "."
#define PRO_ID   100
#define SZ       256

int main(void)
{
    key_t key;
    int shmid, ret;

    // 1. 生成 key
    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        exit(EXIT_FAILURE);
    }

    // 2. 创建共享内存
    shmid = shmget(key, SZ, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget(): ");
        exit(EXIT_FAILURE);
    }

    printf("shmid = %d\n", shmid);

    // 3. 删除共享内存(IPC_RMID 标记删除,所有进程 detach 后真正释放)
    ret = shmctl(shmid, IPC_RMID, NULL);
    if (ret == -1) {
        perror("[ERROR] shmctl(): ");
        exit(EXIT_FAILURE);
    }

    return 0;
}
执行结果
ini 复制代码
shmid = 0

使用 ipcs -m 进行验证:

bash 复制代码
# 创建后
$ ipcs -m
key        shmid  owner  perms  bytes   nattch  status
0x0a051dd0 0      ben    666    256     0

# 删除后(IPC_RMID 标记了 dest)
0x0a051dd0 0      ben    666    256     0       dest

严谨的社会类比

🏢 比喻:共享办公室的白板

共享内存就像办公室里的一块公共白板

  • shmget() = 行政部买了一块新白板挂到公共区域
  • shmat() = 你走到白板前面,准备看/写
  • 直接读写共享内存 = 你在白板上直接写字或擦字,其他人立刻就能看到
  • shmdt() = 你离开白板前,回到自己工位
  • shmctl(IPC_RMID) = 行政部撤走白板(但等人都不看了才真的搬走)
  • 整个过程中没有邮递员传递信息 (零拷贝),这就是共享内存为什么最快

第 9 节:共享内存(二)------ 映射与读写

核心函数:shmat() ------ 挂载共享内存

c 复制代码
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
// shmid   : 共享内存 ID
// shmaddr : 指定映射地址(传 NULL 由系统选择)
// shmflg  : 0(读写)、SHM_RDONLY(只读)
// 返回值  : 成功返回映射地址,失败返回 (void *) -1

核心函数:shmdt() ------ 卸载共享内存

c 复制代码
int shmdt(const void *shmaddr);
// shmaddr : shmat() 返回的映射地址
// 返回值  : 成功 0,失败 -1

完整示例:使用共享内存进行进程间通信

需求:进程 A 向共享内存写入数据,进程 B 从共享内存读取数据。

共享内存写入端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
#define PRO_ID   100
#define SZ       256

int main(void)
{
    key_t key;
    int shmid;
    void *addr = NULL;

    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        exit(EXIT_FAILURE);
    }

    shmid = shmget(key, SZ, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget(): ");
        exit(EXIT_FAILURE);
    }

    printf("shmid = %d\n", shmid);

    // 映射共享内存到本进程地址空间
    addr = shmat(shmid, NULL, 0);
    if (addr == (void *)-1) {
        perror("[ERROR] shmat(): ");
        exit(EXIT_FAILURE);
    }

    // 向共享内存写入数据
    memset(addr, 'A', 10);           // 写入 10 个 'A'
    memcpy(addr + 10, "Hello", 6);   // 继续写入 "Hello"

    printf("Data written to shared memory.\n");

    // 解除映射
    shmdt(addr);

    return 0;
}
共享内存读取端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
#define PRO_ID   100
#define SZ       256

int main(void)
{
    key_t key;
    int shmid, i;
    char buffer[20] = {0};
    void *addr = NULL;

    key = ftok(PATHNAME, PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        exit(EXIT_FAILURE);
    }

    shmid = shmget(key, SZ, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget(): ");
        exit(EXIT_FAILURE);
    }

    printf("shmid = %d\n", shmid);

    // 映射共享内存
    addr = shmat(shmid, NULL, 0);
    if (addr == (void *)-1) {
        perror("[ERROR] shmat(): ");
        exit(EXIT_FAILURE);
    }

    // 从共享内存读取数据
    memcpy(buffer, addr, 10);        // 读取前 10 字节

    printf("Data read from shared memory:\n");
    for (i = 0; i < 10; i++) {
        printf(" %c ", buffer[i]);
    }
    printf("\n");

    printf("String at offset 10: %s\n", (char *)addr + 10);

    // 解除映射
    shmdt(addr);

    return 0;
}
编译与执行
bash 复制代码
# 先运行写入端
$ gcc shm_write.c -o shm_w
$ ./shm_w

# 再运行读取端
$ gcc shm_read.c -o shm_r
$ ./shm_r
执行结果
less 复制代码
(写入端)
shmid = 0
Data written to shared memory.

(读取端)
shmid = 0
Data read from shared memory:
 A  A  A  A  A  A  A  A  A  A
String at offset 10: Hello

共享内存使用要点

操作 说明
shmget() 创建共享内存,返回 ID
shmat() 将共享内存挂载到进程地址空间
直接指针读写 操作返回的地址如同操作 malloc 得到的内存
shmdt() 卸载共享内存(进程不再能访问)
shmctl(IPC_RMID) 标记删除(等到所有进程都 detach 后才真正释放)

⚠️ 重要 :共享内存本身不提供同步机制 !如果多个进程同时读写同一块共享内存,会发生数据竞争。需要结合信号量来保证同步与互斥。


严谨的社会类比

📋 比喻:公告栏

共享内存就像公司大厅的公共公告栏

  • shmget() = 行政部门安装了一个公告栏
  • shmat() = 你走到公告栏前面,可以看清上面的内容
  • 直接写内存 = 你在公告栏上贴通知
  • 直接读内存 = 你看公告栏上的通知
  • shmdt() = 你走回自己工位,不再看公告栏
  • shmctl(IPC_RMID) = 行政部门撤走公告栏(要等所有人都走开后才搬走)

但是!公告栏有一个大问题 ------如果两个人同时去贴通知,会把对方的通知搞乱。这就需要一个管理员 来控制"同一时间只有一个人能贴通知"------那就是信号量要做的事。


第 10 节:信号量(一)------ 概念与创建

核心概念

当多个进程同时访问共享资源时,会产生资源竞争,最终导致数据混乱。

术语 定义 举例
临界资源 不允许同时有多个进程访问的资源 共享内存、打印机、文件
临界区 访问临界资源的代码段 读写共享内存的代码
互斥 同一时刻只有一个进程能访问临界资源 一个人用打印机
同步 在互斥基础上,控制访问顺序 A 写完 B 才能读

一句话总结 :信号量就像交通信号灯------它不负责运输数据,而是控制进程访问共享资源的"通行顺序",避免混乱。

竞争问题演示

以下代码展示了父子进程同时访问终端(共享资源)导致的混乱:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t cpid;

    cpid = fork();
    if (cpid == -1) {
        perror("[ERROR] fork(): ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        while (1) {
            printf("------------------------\n");
            printf("C Start.\n");
            sleep(1);
            printf("C End.\n");
            printf("------------------------\n");
        }
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        while (1) {
            printf("------------------------\n");
            printf("P Start.\n");
            sleep(1);
            printf("P End.\n");
            printf("------------------------\n");
        }
        wait(NULL);
    }

    return 0;
}
执行结果(混乱的输出)
markdown 复制代码
------------------------
C Start.
------------------------
P Start.
C End.
P End.
------------------------
------------------------

父子进程的输出交叉混合在一起,因为终端是共享资源,没有互斥保护。


信号量的定义

信号量(Semaphore) 是由内核维护的一个非负整数,它支持以下操作:

  1. 设置 :将信号量设为一个具体值(SETVAL
  2. 增加 :在信号量当前值上加一个数值(V 操作 / 释放资源
  3. 减少 :在信号量当前值上减一个数值(P 操作 / 占用资源
  4. 等待 0:等待信号量的值变为 0

信号量分类

类型 含义
二值信号量 0 或 1 对应单个资源(如一台打印机)
计数信号量 ≥ 2 对应多个资源(如 5 个数据库连接)

查询信号量

bash 复制代码
$ ipcs -s

------ Semaphore Arrays --------
key        semid  owner   perms  nsems

核心函数:semget() ------ 创建/获取信号量集合

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

int semget(key_t key, int nsems, int semflg);
// key    : ftok() 生成的 key
// nsems  : 信号量集合中的信号量数量(≥ 1)
// semflg : IPC_CREAT、IPC_EXCL、权限位
// 返回值 : 成功返回信号量集合 ID,失败返回 -1

核心函数:semctl() ------ 控制信号量集合

c 复制代码
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
// semid   : 信号量集合 ID
// semnum  : 信号量编号(从 0 开始)
// cmd     : 控制命令
//           SETVAL --- 设置信号量的值
//           GETVAL --- 获取信号量的值
//           IPC_RMID --- 删除信号量集合
// ...     : 可变参数,取决于 cmd

// SETVAL 命令需要使用的 union semun
union semun {
    int              val;      /* SETVAL 使用的值 */
    struct semid_ds *buf;      /* IPC_STAT/IPC_SET 使用的缓冲区 */
    unsigned short  *array;    /* GETALL/SETALL 使用的数组 */
    struct seminfo  *__buf;    /* IPC_INFO(Linux 特有) */
};

示例:创建信号量集合并初始化

需求:创建一个信号量集合,包含 1 个信号量,并将其值设为 1(二值信号量)。

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

#define SEM_PATHNAME "."
#define SEM_PRO_ID   100

union semun {
    int val;
};

int main(void)
{
    int semid, ret;
    union semun s;
    key_t key;

    // 1. 生成 key
    key = ftok(SEM_PATHNAME, SEM_PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        return -1;
    }

    // 2. 创建信号量集合(1 个信号量)
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("[ERROR] semget(): ");
        return -1;
    }

    // 3. 将信号量 0 的值设置为 1
    s.val = 1;
    ret = semctl(semid, 0, SETVAL, s);
    if (ret == -1) {
        perror("[ERROR] semctl(): ");
        return -1;
    }

    printf("Semaphore created and initialized to 1.\n");
    printf("semid = %d\n", semid);

    return 0;
}
执行结果
ini 复制代码
Semaphore created and initialized to 1.
semid = 0

使用 ipcs -s 验证:

diff 复制代码
------ Semaphore Arrays --------
key        semid  owner   perms  nsems
0x0a051dd0 0      ben     666    1

严谨的社会类比

🚦 比喻:交通信号灯

信号量就是对交通信号灯的完美模拟:

  • 二值信号量(值为 1) = 一座单车道桥 ,一次只允许一辆车通过
    • P 操作(减 1)= 绿灯变红灯,一辆车进入桥面
    • V 操作(加 1)= 红灯变绿灯,车离开桥面,下一辆可以进入
  • 计数信号量(值为 N) = N 车道高速公路 ,允许 N 辆车同时通行
    • P 操作 = 占用一个车道,可用车道数减 1
    • V 操作 = 释放一个车道,可用车道数加 1
    • 当所有车道都被占满(信号量为 0)时,后续车辆必须等待(P 操作阻塞)

没有信号量的共享资源访问就像没有红绿灯的十字路口------车辆(进程)各自为政,必然发生碰撞(数据混乱)。


第 11 节:信号量(二)------ P/V 操作

核心函数:semop() ------ 信号量操作

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

int semop(int semid, struct sembuf *sops, size_t nsops);
// semid : 信号量集合 ID
// sops  : 信号量操作数组
// nsops : 操作的信号量数量
// 返回值: 成功 0,失败 -1

struct sembuf 结构体

c 复制代码
struct sembuf {
    unsigned short sem_num;   /* 信号量编号(从 0 开始)*/
    short          sem_op;    /* 信号量操作:
                                 -1 : P 操作(占用/等待资源)
                                 +1 : V 操作(释放资源)
                                  0 : 等待信号量变为 0  */
    short          sem_flg;   /* 操作标志:
                                 IPC_NOWAIT --- 非阻塞
                                 SEM_UNDO   --- 进程退出时自动释放 */
};

P/V 操作语义

操作 sem_op 含义 行为
P 操作(占用) -1 semval -= 1 若 semval ≥ 1 则成功;若 semval = 0 则阻塞
V 操作(释放) +1 semval += 1 总是成功,可能唤醒等待的进程
等待 0 0 等待 semval = 0 若 semval ≠ 0 则阻塞

P/V 操作流程

stateDiagram-v2 [*] --> Available: semval = 1 Available --> Occupied: P(sem_op = -1) Occupied --> Available: V(sem_op = +1) Available --> Blocked: P when semval = 0 Blocked --> Available: V by other process

图注:P 操作将信号量减 1,若为 0 则阻塞;V 操作将信号量加 1,唤醒等待进程。初始值为 1 时实现互斥。

删除信号量

c 复制代码
ret = semctl(semid, 0, IPC_RMID, NULL);
// 注意:IPC_RMID 时第三个参数被忽略(帮助文档已说明)

完整实战:使用信号量解决父子进程的终端竞争

我们将之前的竞争示例升级,用信号量实现互斥

第一步:封装信号量操作(sem.h)
c 复制代码
#ifndef __SEM_H_
#define __SEM_H_

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define SEM_PATHNAME "."     // ftok 路径
#define SEM_PRO_ID   100     // ftok 项目 ID

extern int sem_create(int nsems, unsigned short values[]);
extern int sem_p(int semid, int semnum);    // P 操作
extern int sem_v(int semid, int semnum);    // V 操作
extern int sem_del(int semid);              // 删除信号量集合

#endif
第二步:实现信号量操作(sem.c)
c 复制代码
#include "sem.h"

union semun {
    int val;
    unsigned short *array;
};

// 创建信号量集合并初始化所有信号量的值
int sem_create(int nsems, unsigned short values[])
{
    int semid, ret;
    key_t key;
    union semun s;

    key = ftok(SEM_PATHNAME, SEM_PRO_ID);
    if (key == -1) {
        perror("[ERROR] ftok(): ");
        return -1;
    }

    semid = semget(key, nsems, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("[ERROR] semget(): ");
        return -1;
    }

    // SETALL:批量设置所有信号量的值
    s.array = values;
    ret = semctl(semid, 0, SETALL, s);
    if (ret == -1) {
        perror("[ERROR] semctl(SETALL): ");
        return -1;
    }

    return semid;
}

// P 操作:占用资源(减 1)
int sem_p(int semid, int semnum)
{
    struct sembuf sops;

    sops.sem_num = semnum;     // 信号量编号
    sops.sem_op  = -1;         // P 操作
    sops.sem_flg = SEM_UNDO;   // 进程退出时自动释放

    return semop(semid, &sops, 1);
}

// V 操作:释放资源(加 1)
int sem_v(int semid, int semnum)
{
    struct sembuf sops;

    sops.sem_num = semnum;
    sops.sem_op  = 1;          // V 操作
    sops.sem_flg = SEM_UNDO;

    return semop(semid, &sops, 1);
}

// 删除信号量集合
int sem_del(int semid)
{
    return semctl(semid, 0, IPC_RMID, NULL);
}
第三步:使用信号量保护临界区(main.c)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include "sem.h"

int main(void)
{
    pid_t cpid;
    int semid;
    unsigned short values[] = {1};    // 初始值为 1(二值信号量)

    // 创建信号量集合(1 个信号量,初值 1)
    semid = sem_create(1, values);
    if (semid == -1)
        return -1;

    cpid = fork();
    if (cpid == -1) {
        perror("[ERROR] fork(): ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程 ---------- */
        while (1) {
            sem_p(semid, 0);          // P操作:占用(信号量 1→0)
            printf("------------------------\n");
            printf("C Start.\n");
            sleep(1);
            printf("C End.\n");
            printf("------------------------\n");
            sem_v(semid, 0);          // V操作:释放(信号量 0→1)
        }
    }
    else if (cpid > 0) {
        /* ---------- 父进程 ---------- */
        sleep(1);                     // 保证子进程先运行
        while (1) {
            sem_p(semid, 0);          // P操作:占用(信号量 1→0)
            printf("------------------------\n");
            printf("P Start.\n");
            sleep(1);
            printf("P End.\n");
            printf("------------------------\n");
            sem_v(semid, 0);          // V操作:释放(信号量 0→1)
        }

        wait(NULL);
        sem_del(semid);
    }

    return 0;
}
执行结果(有序输出)
markdown 复制代码
------------------------
C Start.
C End.
------------------------
------------------------
P Start.
P End.
------------------------
------------------------
C Start.
C End.
------------------------
...

输出不再交叉混乱,父子进程严格互斥地访问终端。


严谨的社会类比

🎫 比喻:景区厕所的管理员

信号量的 P/V 操作就像景区厕所的一位管理员

  • 信号量 = 厕所的可用坑位数
  • 初始值 = 厕所的总坑位数(假设 1 个坑位,即二值信号量)
  • P 操作(sem_op = -1) = 你推开厕所门,管理员把可用数减 1
    • 如果还有空位(信号量 > 0),你直接进去
    • 如果没有空位了(信号量 = 0),你需要在外面排队等待
  • V 操作(sem_op = +1) = 你从厕所出来,管理员把可用数加 1
    • 如果有其他人在排队,现在他可以进去了
  • SEM_UNDO 标志 = 假如你上到一半突然肚子疼跑了,管理员会自动释放这个坑位(进程退出时自动归还信号量),不会一直占着

没有信号量的共享资源就像没有管理员的厕所------两个人同时冲进去,必然发生冲突。


第 12 节:信号量同步------实现 "ABA" 输出

核心概念

前面我们学习了信号量的互斥 (mutual exclusion),本节进一步实现同步 (synchronization)------不仅保证同一时间只有一个进程访问资源,还控制访问的顺序

一句话总结:互斥是"不能同时用",同步是"谁先谁后都要排好"。

需求

创建父子进程,实现循环输出 ABA 字符串:

  • 父进程 输出第一个 A
  • 子进程 输出 B
  • 父进程 输出第二个 A 并换行
  • 循环输出 ABA\n

控制思路

使用两个信号量来分别控制父进程和子进程的执行节奏:

sequenceDiagram participant P as Parent(SEM_P) participant C as Child(SEM_C) Note over P: Init: SEM_P=1, SEM_C=0 P->>P: P(SEM_P) 1->>0 can run P->>P: print A P->>C: V(SEM_C) 0->>1 wake child Note over P: P(SEM_P) 0->>-1 wait C->>C: P(SEM_C) 1->>0 can run C->>C: print B C->>P: V(SEM_P) -1->>0 wake parent Note over P: woken up P->>P: print A P->>P: V(SEM_P) 0->>1 P->>P: print newline

图注 :通过两个信号量(SEM_P 控制父进程、SEM_C 控制子进程)的 P/V 操作交替,实现父→子→父的严格执行顺序,输出 ABA

初始化

c 复制代码
#define SEM_CONTROL_P  0     // 控制父进程的信号量(编号 0)
#define SEM_CONTROL_C  1     // 控制子进程的信号量(编号 1)

unsigned short values[2] = {1, 0};
// SEM_CONTROL_P = 1 : 父进程初始可以执行
// SEM_CONTROL_C = 0 : 子进程初始阻塞

完整代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "sem.h"            // 复用上一节封装的 sem.h

#define SEM_CONTROL_P  0    // 控制父进程的信号量
#define SEM_CONTROL_C  1    // 控制子进程的信号量

int main(void)
{
    pid_t cpid;
    int semid;
    unsigned short values[2] = {1, 0};   // 父进程可用,子进程阻塞

    // 创建包含 2 个信号量的集合
    semid = sem_create(2, values);
    if (semid == -1)
        exit(EXIT_FAILURE);

    cpid = fork();
    if (cpid == -1) {
        perror("[ERROR] fork(): ");
        exit(EXIT_FAILURE);
    }
    else if (cpid == 0) {
        /* ---------- 子进程:输出 B ---------- */
        while (1) {
            sem_p(semid, SEM_CONTROL_C);   // 占用 C,等待父进程释放
            printf("B");
            fflush(stdout);
            sem_v(semid, SEM_CONTROL_P);   // 释放 P,唤醒父进程
        }
    }
    else {
        /* ---------- 父进程:输出 A A ---------- */
        while (1) {
            sem_p(semid, SEM_CONTROL_P);   // 占用 P(初值 1,第一次不阻塞)
            printf("A");
            fflush(stdout);
            sem_v(semid, SEM_CONTROL_C);   // 释放 C,允许子进程输出 B

            sem_p(semid, SEM_CONTROL_P);   // 再次占用 P(此时已被子进程释放)
            printf("A");
            fflush(stdout);
            sem_v(semid, SEM_CONTROL_P);   // 释放 P(下次循环自己用)

            sleep(1);
            putchar('\n');
        }

        wait(NULL);
    }

    return 0;
}
执行结果
erlang 复制代码
ABA
ABA
ABA
ABA
...

每行严格输出 ABA\n,顺序永不混乱。

同步原理详解

步骤 父进程信号量 P 子进程信号量 C 说明
初始 1 0 父进程可执行,子进程阻塞
父进程 P(P), 输出 A 0 0 父进程占用 P,输出第一个 A
父进程 V(C) 0 1 释放子进程
父进程 P(P)(阻塞) -1 1 父进程等待子进程释放 P
子进程 P(C), 输出 B -1 0 子进程被激活,输出 B
子进程 V(P) 0 0 子进程释放 P,父进程被唤醒
父进程输出 A -1 0 父进程输出第二个 A
父进程 V(P) 0 0 父进程释放 P(下次自己用),输出换行
(循环回到步骤 2) 0 0 ...

严谨的社会类比

🎭 比喻:双人合唱

这个信号量同步的 ABA 问题就像两位歌手在舞台上交替演唱

  • 信号量 SEM_CONTROL_P = 歌手 P 的麦克风开关(初始为 ON)
  • 信号量 SEM_CONTROL_C = 歌手 C 的麦克风开关(初始为 OFF)
makefile 复制代码
歌手P: (拿到自己的麦克风,唱 A)→ 打开C的麦克风 → 等自己的麦克风静音
歌手C: (拿到自己的麦克风,唱 B)→ 打开P的麦克风 → 等自己的麦克风静音
歌手P: (拿到自己的麦克风,唱 A)→ 打开自己的麦克风备用 → 鞠躬(换行)

这就是**同步(Synchronization)互斥(Mutual Exclusion)**的区别:

  • 互斥 = 只保证"同一时间只有一个人在唱"
  • 同步 = 不仅保证一个人唱,还规定了"P先唱 → C再唱 → P再唱"的严格顺序

第 13 节:经典 IPC 问题------生产者-消费者

问题描述

生产者-消费者(Producer-Consumer) 是 IPC 领域最经典的同步问题。一个或多个生产者进程生产数据放入共享缓冲区,一个或多个消费者进程从缓冲区取出数据消费。

graph LR subgraph Producer[Producer] P[produce data] end subgraph Buffer[Shared Buffer] B[(ring buffer)] end subgraph Consumer[Consumer] C[consume data] end P -- put data --> B B -- get data --> C

需要解决的三个问题

问题 原因 解决方案
互斥 生产者和消费者不能同时访问缓冲区 一个互斥信号量(初值 1)
缓冲区满 生产者不能往满的缓冲区放数据 一个空位计数信号量(初值 = 缓冲区大小)
缓冲区空 消费者不能从空的缓冲区取数据 一个数据计数信号量(初值 0)

完整实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <string.h>

#define BUFFER_SIZE  5          // 缓冲区大小
#define SHM_PATHNAME "."
#define SHM_PRO_ID   200
#define SEM_PATHNAME "."
#define SEM_PRO_ID   300

// 共享内存中的缓冲区结构
struct shared_buffer {
    int data[BUFFER_SIZE];      // 数据缓冲区
    int in;                     // 生产者写入位置
    int out;                    // 消费者读取位置
};

// 信号量索引
#define SEM_MUTEX   0           // 互斥信号量 (初值 1)
#define SEM_EMPTY   1           // 空位信号量 (初值 BUFFER_SIZE)
#define SEM_FULL    2           // 数据信号量 (初值 0)
#define SEM_COUNT   3           // 信号量总数

union semun { int val; };

// 初始化信号量
int init_semaphores(void)
{
    key_t key = ftok(SEM_PATHNAME, SEM_PRO_ID);
    int semid = semget(key, SEM_COUNT, IPC_CREAT | 0666);
    if (semid == -1) return -1;

    union semun s;
    s.val = 1;              // 互斥信号量
    semctl(semid, SEM_MUTEX, SETVAL, s);
    s.val = BUFFER_SIZE;    // 空位信号量
    semctl(semid, SEM_EMPTY, SETVAL, s);
    s.val = 0;              // 数据信号量
    semctl(semid, SEM_FULL, SETVAL, s);

    return semid;
}

// P 操作
void P(int semid, int idx)
{
    struct sembuf op = {idx, -1, 0};
    semop(semid, &op, 1);
}

// V 操作
void V(int semid, int idx)
{
    struct sembuf op = {idx, 1, 0};
    semop(semid, &op, 1);
}

int main(void)
{
    // 创建共享内存
    key_t shm_key = ftok(SHM_PATHNAME, SHM_PRO_ID);
    int shmid = shmget(shm_key, sizeof(struct shared_buffer),
                       IPC_CREAT | 0666);
    struct shared_buffer *buf = shmat(shmid, NULL, 0);
    buf->in = 0;
    buf->out = 0;

    // 创建信号量
    int semid = init_semaphores();
    if (semid == -1) {
        perror("semaphore init failed");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == 0) {
        /* ========== 生产者(子进程)========== */
        srand(getpid());
        for (int i = 1; i <= 10; i++) {
            int item = rand() % 100;          // 生产一个数据

            P(semid, SEM_EMPTY);              // 申请空位
            P(semid, SEM_MUTEX);              // 互斥访问

            buf->data[buf->in] = item;        // 放入缓冲区
            printf("[Producer] 放入: %3d  at[%d]\n", item, buf->in);
            buf->in = (buf->in + 1) % BUFFER_SIZE;

            V(semid, SEM_MUTEX);              // 释放互斥
            V(semid, SEM_FULL);               // 增加数据计数

            sleep(rand() % 2);                 // 随机等待
        }
        printf("[Producer] 生产完毕\n");
        shmdt(buf);
        exit(0);
    }
    else {
        /* ========== 消费者(父进程)========= */
        srand(getpid() + 100);
        for (int i = 1; i <= 10; i++) {
            P(semid, SEM_FULL);               // 申请数据
            P(semid, SEM_MUTEX);              // 互斥访问

            int item = buf->data[buf->out];   // 从缓冲区取出
            printf("[Consumer] 取出: %3d from[%d]\n", item, buf->out);
            buf->out = (buf->out + 1) % BUFFER_SIZE;

            V(semid, SEM_MUTEX);              // 释放互斥
            V(semid, SEM_EMPTY);              // 增加空位计数

            sleep(rand() % 2);                 // 随机等待
        }

        wait(NULL);

        // 清理
        shmdt(buf);
        shmctl(shmid, IPC_RMID, NULL);
        semctl(semid, 0, IPC_RMID, NULL);

        printf("[Main] 所有操作完成,资源已清理\n");
    }

    return 0;
}
执行结果
ini 复制代码
[Producer] 放入:  83  at[0]
[Consumer] 取出:  83  from[0]
[Producer] 放入:  86  at[1]
[Producer] 放入:  77  at[2]
[Consumer] 取出:  86  from[1]
[Producer] 放入:  15  at[3]
[Consumer] 取出:  77  from[2]
[Producer] 放入:  93  at[4]
[Producer] 放入:  35  at[0]    ← 环形缓冲区回到开头
[Consumer] 取出:  15  from[3]
...

关键点 :三个信号量各司其职------SEM_MUTEX 保证对缓冲区的互斥访问,SEM_EMPTY 防止缓冲区满时生产者继续写入,SEM_FULL 防止缓冲区空时消费者尝试读取。这是信号量解决同步 + 互斥的经典范式。


附录


附 1:六大 IPC 机制全景对比表

基础属性对比

对比维度 🚰 无名管道 📁 有名管道 📡 信号 📬 消息队列 🖥️ 共享内存 🚦 信号量
本质 内核缓冲区 内核缓冲区(文件系统可见) 软件中断模拟 内核维护的消息链表 物理内存直接映射 内核维护的非负整数
通信方向 单向 单向 单向 双向(按类型区分) 双向 不传数据,仅同步
进程范围 仅亲缘关系 任意进程 任意进程 任意进程 任意进程 任意进程
数据单位 字节流(无边界) 字节流(无边界) 信号编号(整数) 结构化消息(有类型) 任意数据 整数(计数器)
是否需要内核拷贝 ✅ 是(2 次) ✅ 是(2 次) ❌ 否(仅通知) ✅ 是(2 次) 零拷贝 ❌ 否
通信效率 ⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
是否有内核缓冲区 ✅ 有 ✅ 有 ❌ 无 ✅ 有 ❌ 无 ❌ 无
生命周期 随进程(关闭即销毁) 随文件系统(手动删除) 瞬时 随内核(手动删除) 随内核(标记删除) 随内核(手动删除)
是否自带同步 ❌ 否 ❌ 否 ❌ 否 ❌ 否 ❌ 否 ✅ 是(自身即同步工具)

相同点与不同点

相同点
共同特征 涉及的 IPC 机制
都用于进程间数据交换或协调 全部 6 种
都是 Linux 内核提供的基础设施 全部 6 种
都需要通过系统调用(API)来使用 全部 6 种
都支持阻塞操作 管道、消息队列、共享内存(配合信号量)、信号量本身
在应用中通过文件描述符或 ID 来操作 管道(fd)、消息队列(msqid)、共享内存(shmid)、信号量(semid)
操作失败时均返回 -1 并设置 errno 全部 6 种
核心差异
差异维度 管道 信号 消息队列 共享内存 信号量
数据 vs 控制 传输数据 传递通知 传输数据 传输数据 控制同步
通信 vs 同步 通信 通信 通信 通信 同步
有结构 vs 无结构 无结构(字节流) 无结构(整数) 有结构(类型+数据) 无结构(原始内存) 无结构(整数)
持续 vs 瞬时 持续(数据停留) 瞬时(不排队) 持续(排队存储) 持续(直到删除) 持续(直到删除)
数据量大小 有限(几十 KB) 极小(仅编号) 有限(系统限制) 大(可自定义) 极小(仅整数)

注意点(易错点)

IPC 机制 ⚠️ 最常犯的错误 正确做法
无名管道 先 fork() 再 pipe() 必须在 fork() 之前 调用 pipe(),否则子进程无法继承管道 fd
无名管道 读写端不关闭 父子进程应分别关闭不需要的一端,否则 read() 不会返回 0 标志结束
有名管道 以 O_RDONLY 打开后认为程序卡死 FIFO 的 O_RDONLY 会阻塞等待写端打开,这是正常行为
信号 在信号处理函数中调用非可重入函数 信号处理函数中应避免使用 printf()malloc()不可重入函数
信号 认为 SIGKILL/SIGSTOP 可以被忽略 这两个信号不可被忽略、不可被捕捉、不可被阻塞
消息队列 msgrcv 读取时 msgtyp 参数传错 msgtyp > 0 表示精确匹配类型,= 0 取最早消息,< 0 取类型 ≤ |msgtyp| 的最小值
消息队列 消息队列不删除导致资源泄漏 System V IPC 对象不会随进程退出而销毁,必须调用 IPC_RMID
共享内存 认为 shmdt 会删除共享内存 shmdt 只是解除映射,不会删除共享内存,需要用 shmctl(IPC_RMID)
共享内存 不加同步直接读写 共享内存不提供任何同步机制,必须配合信号量使用
信号量 忘记初始化信号量的值 semget 创建后信号量的值未定义,必须用 SETVAL 或 SETALL 初始化
信号量 P/V 操作不配对 有多少个 P 操作就要有对应数量的 V 操作,否则会导致死锁
信号量 死锁 多个信号量时,应确保所有进程按相同顺序进行 P 操作

典型使用场景

IPC 机制 最适合的场景 实际应用举例
🚰 无名管道 父子进程间简单数据传输 Shell 命令的管道符 `grep xxx
📁 有名管道 无血缘关系的进程间简单通信 服务端和客户端交换简单命令
📡 信号 异步事件通知、进程控制 kill -9 强制终止进程、Ctrl+C 终止前台程序
📬 消息队列 需要按类型分类的可靠通信 客户端请求排队、按优先级处理任务
🖥️ 共享内存 大数据量、高频次的数据交换 视频帧共享、数据库缓存、游戏引擎状态共享
🚦 信号量 保护共享资源、同步进程执行顺序 多线程/多进程互斥访问打印机、生产者-消费者模型

选型决策树

markdown 复制代码
需要通信还是同步?
├── 同步(控制进程执行顺序)
│   └── 🚦 信号量
│
└── 通信(交换数据)
    ├── 只是通知/控制?(数据量极小)
    │   └── 📡 信号
    │
    ├── 需要高效的大数据量交换?
    │   ├── 进程间有亲缘关系?
    │   │   ├── 🚰 无名管道(简单场景)
    │   │   └── 🖥️ 共享内存 + 🚦 信号量(高性能场景)
    │   └── 任意进程?
    │       └── 🖥️ 共享内存 + 🚦 信号量
    │
    └── 需要按类型/优先级读取?
        ├── 📬 消息队列
        └── 不需要按类型?
            ├── 有亲缘关系?→ 🚰 无名管道
            └── 任意进程?→ 📁 有名管道

附 2:管道专题对比

无名管道 vs 有名管道

| 对比维度 | 无名管道 | 有名管道 |
|--------------------|:-----------------------:|:------------------------------:|-----------------|
| 创建方式 | pipe() | mkfifo() |
| 文件系统可见 | ❌ 不可见 | ✅ 可见(ls -l 可看到 p 类型文件) |
| 占用磁盘空间 | ❌ 不占用 | ❌ 不占用(仅 inode,数据仍在内存) |
| 进程范围 | 仅亲缘关系 | 任意进程 |
| 打开方式 | 无需手动 open,pipe() 即返回 fd | 需要 open() 打开,且可能阻塞 |
| 关闭方式 | close(fd) | 同普通文件 close(fd) |
| 删除方式 | 进程退出即销毁 | 需 unlink()remove() 删除文件 |
| 缓冲区大小 | Linux 默认 65536 字节(可调整) | 同无名管道 |
| PIPE_BUF(原子写入) | 4096 字节(POSIX 要求) | 4096 字节 |
| 使用场景 | 父子进程间临时通信 | 无血缘关系的进程间通信 |
| 典型命令 | `ls | grep txt` | mkfifo myfifo |

管道使用注意事项对比

场景 无名管道 有名管道
读空管道时 read() 阻塞 read() 阻塞
写满管道时 write() 阻塞 write() 阻塞
写端关闭时读 read() 读完返回 0 read() 读完返回 0
读端关闭时写 收到 SIGPIPE,进程终止 收到 SIGPIPE,进程终止
打开时无对应端 不适用(pipe 即创建一对) open() 阻塞等待配对

附 3:信号专题对比

信号处理方式对比

处理方式 宏/函数 行为 适用范围
忽略 SIG_IGN 信号到达时丢弃,不做任何处理 除 SIGKILL/SIGSTOP 外的所有信号
默认 SIG_DFL 按内核预设的默认动作处理 所有信号
自定义 函数地址 调用用户注册的处理函数 除 SIGKILL/SIGSTOP 外的所有信号

信号默认行为分类

默认行为 包含的信号
进程终止 SIGALRM, SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGTERM, SIGUSR1, SIGUSR2, SIGFPE, SIGSEGV, SIGILL 等
进程忽略 SIGCHLD, SIGPWR, SIGURG, SIGWINCH
进程暂停 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU
进程继续 SIGCONT

不可被忽略/捕捉的信号

信号 编号 原因
SIGKILL 9 系统终极终止手段,确保管理员能杀死任何进程
SIGSTOP 19 系统终极暂停手段,确保能暂停任何进程

附 4:System V IPC 三件套对比

消息队列 vs 共享内存 vs 信号量

对比维度 📬 消息队列 🖥️ 共享内存 🚦 信号量
核心功能 按消息类型传递数据 高效率共享数据 同步与互斥
数据是否拷贝 用户→内核→用户(2 次拷贝) 直接映射,零拷贝 仅整数值,无数据拷贝
通信效率 中等 最高 不传数据,仅同步
是否自带同步 ❌ 否 ❌ 否 ✅ 是
数据大小限制 系统限制(通常 ≤ 8KB/条) 可自定义(大至数 GB) 仅整数
数据持久性 内核中排队 物理内存中常驻 内核中常驻
按类型读取 ✅ 支持按 mtype 选择性读取 ❌ 不支持 ❌ 不支持
FIFO 特性 ✅ 有(但不强制) ❌ 无 ❌ 无
创建函数 msgget() shmget() semget()
控制函数 msgctl() shmctl() semctl()
操作函数 msgsnd() / msgrcv() shmat() / shmdt() semop()
查询命令 ipcs -q ipcs -m ipcs -s
删除命令 ipcrm -q <id> ipcrm -m <id> ipcrm -s <id>

三件套协作方式

scss 复制代码
                 ┌─────────────────┐
                 │   共享内存       │  ← 高效传输数据
                 │  (Shared Memory) │
                 └────────┬────────┘
                          │ 需要保护
                          ▼
                 ┌─────────────────┐
                 │   信号量         │  ← 控制访问顺序
                 │  (Semaphore)     │
                 └─────────────────┘

                 ┌─────────────────┐
                 │   消息队列       │  ← 独立使用,用于
                 │  (Message Queue) │     按类型通信
                 └─────────────────┘

协作模式 :共享内存负责传数据(快),信号量负责同步(稳),两者搭配使用是最经典的高性能 IPC 方案。消息队列则独立使用,适合需要按类型分类的中等量数据通信。


附 5:同步与互斥概念对比

概念 定义 比喻 实现方式 信号量初值
互斥(Mutual Exclusion) 同一时刻只允许一个进程访问临界资源 单车道桥,一次只过一辆车 二值信号量(P/V 操作) 1
同步(Synchronization) 按特定顺序访问临界资源 合唱团按谱演唱,不能抢拍 多个信号量配合,控制执行节奏 根据初始状态设 0 或 1
死锁(Deadlock) 多个进程互相等待对方释放资源,全部阻塞 十字路口四辆车互不相让 避免:固定加锁顺序 / 使用 trylock ---

互斥与同步的关键区别

特性 互斥 同步
目标 防止同时访问 控制访问顺序
信号量数量 1 个 ≥ 2 个
典型模式 P → 临界区 → V P1 → V2 → P2 → V1
类比 厕所门锁 舞台上的轮流表演

附 6:阻塞与非阻塞操作对比

IPC 操作 阻塞模式 非阻塞模式
管道 read() 管道空时阻塞 通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置,空时返回 -1, errno = EAGAIN
管道 write() 管道满时阻塞 同上,满时返回 -1, errno = EAGAIN
msgrcv() 队列无符合条件消息时阻塞 设置 IPC_NOWAIT,无消息时返回 -1, errno = ENOMSG
msgsnd() 队列满时阻塞 设置 IPC_NOWAIT,满时返回 -1, errno = EAGAIN
semop() 信号量值不足时阻塞 设置 IPC_NOWAIT,不满足时返回 -1, errno = EAGAIN
open(FIFO) 读写不配对时阻塞 O_RDWR 打开可避免阻塞(但不推荐)

附 7:效率排序与性能比较

IPC 效率从高到低

scss 复制代码
最快 ────────────────────────────────────────── 最慢
  │                                             │
  ▼                                             ▼
共享内存 > 信号量 > 信号 > 消息队列 > 有名管道 > 无名管道
(零拷贝)  (仅整数)  (仅通知)  (2次拷贝)  (2次拷贝)  (2次拷贝)

效率差异详解

  • 共享内存 :零拷贝,进程直接读写物理内存,效率最高
  • 信号量 :仅操作整数,无需拷贝数据,效率次之
  • 信号 :仅传递编号,异步立即投递,效率很高
  • 消息队列 :涉及 2 次用户态↔内核态拷贝,且消息有类型管理开销,效率中等
  • 管道 :涉及 2 次拷贝,且是字节流无结构,效率偏低

延迟对比(典型值,非精确测量)

IPC 机制 单次延迟(约) 适用数据量 适用频率
共享内存 纳秒级 大到非常大 极高
信号量 微秒级 极小(仅同步) 极高
信号 微秒级 极小(仅通知)
消息队列 微秒~毫秒级 中(≤ 8KB)
管道 微秒~毫秒级 中(≤ 64KB)

附 8:常用命令速查

命令 用途 典型用法
ipcs -q 查看消息队列 ipcs -q
ipcs -m 查看共享内存 ipcs -m
ipcs -s 查看信号量 ipcs -s
ipcs -a 查看所有 IPC 对象 ipcs -a
ipcrm -q <msqid> 删除消息队列 ipcrm -q 0
ipcrm -m <shmid> 删除共享内存 ipcrm -m 11
ipcrm -s <semid> 删除信号量 ipcrm -s 1
kill -l 列出所有信号 kill -l
kill -<sig> <pid> 发送信号 kill -9 1234
mkfifo <name> 创建有名管道 mkfifo myfifo
ls -l <fifo> 查看管道文件类型 ls -l myfifo(显示 p 类型)
ps aux 查看进程列表 `ps aux
strace -e <syscall> 追踪系统调用 strace -e pipe ./a.out

附 9:快速记忆口诀

IPC 选择口诀

css 复制代码
小数据通知用信号,
亲缘管道最轻巧。
任意进程用 FIFO,
分类消息队列好。
量大频繁共享存,
别忘了加信号量。
同步互斥信号量,
P 减 V 加不能忘。

信号量 P/V 操作口诀

css 复制代码
P 操作,减一减,
资源不够就休眠。
V 操作,加一加,
唤醒等待把队插。
初值设一为互斥,
初值设零为同步。

相关推荐
想你依然心痛2 小时前
嵌入式Linux安全加固:SELinux、Capabilities与Seccomp——强制访问控制与沙箱
linux·运维·安全
REDcker4 小时前
macOS 挂载 Linux 远程目录详解
linux·运维·macos
2601_961593424 小时前
Mac 上搭建Linux环境吗?VMware + CentOS Stream 9 镜像快速部署
linux·运维·ide·macos·centos
dddwjzx4 小时前
嵌入式Linux C应用编程入门——线程 (二)
linux·嵌入式
写代码的学渣4 小时前
Linux systemd 开机启动日志逐行详细解析报告
linux·运维·服务器
阿成学长_Cain5 小时前
Linux telinit 命令详解:运行级别切换|关机重启|系统维护一站式掌握
linux·运维·前端·网络
qeen876 小时前
【Linux】指令补充与shell的简单介绍
linux·运维·服务器
阿成学长_Cain7 小时前
Linux talk 命令详解:经典终端实时聊天工具,无需图形界面即可对话
linux·运维·前端
一拳一个娘娘腔7 小时前
【后渗透-Linux篇】Linux 提权与逃逸:从 www-data到 root的蜕变
linux·运维·服务器