--- title: 进程间通信 tags: [ 嵌入式Linux, Linux应用编程, IPC, 管道, FIFO, 消息队列, 信号量, 共享内存, Socket, IMX6ULL, ] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十章 进程间通信简介" --- # 进程间通信 > 💡 **关联知识**:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/19. 管道]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/20. 共享内存与mmap]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/21. 消息队列]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/22. 信号量]] 所谓进程间通信(interprocess communication,IPC),指的是系统中两个进程之间的通信。不同进程处于各自独立、隔离的地址空间中,因此相互通信比较难,Linux 内核提供了多种 IPC 机制。 > **本篇定位说明**:教材第十章以**了解为主**,系统介绍了 IPC 的分类与各种机制的概念,未展开各机制的 API 细节。因此本篇结构为:**第 1~3 章与第 5 章为教材内容**(IPC 概念、机制分类、机制总览);**具体的 pipe/mkfifo/msgget/semget/shmget 等 API 与完整源码为扩展知识**,各扩展小节均带来源说明,便于你真正上手查询与使用。 --- ## 1. 进程间通信简介 系统的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以: - **同一进程内部的模块(譬如不同函数)之间通信很简单**,用全局变量即可; - **两个不同进程之间通信通常比较难**,因为它们处于不同的地址空间中。 大多数程序是单进程程序(可以有多线程),不需要考虑进程间通信;复杂、大型的应用程序(如 GUI、服务器应用)则会根据实际需要设计成多进程程序,这时才需要 IPC。 --- ## 2. 内核提供了哪些 IPC 机制 Linux 内核提供的多种 IPC 机制基本从 UNIX 系统继承而来。对 UNIX 发展做出重大贡献的两大主力——AT&T 贝尔实验室与 BSD(加州大学伯克利分校的伯克利软件发布中心)——在进程间通信方面侧重点不同: - **前者**对早期 UNIX 的进程间通信手段进行了系统的改进和扩充,形成 **System V IPC**,通信进程局限在单个计算机内; - **后者**跳过了该限制,形成了基于**套接字(Socket,即网络)**的进程间通信机制。 Linux 把两者都继承了下来: ```mermaid flowchart TD root["Linux IPC 机制"] root --> u["早期 UNIX IPC"] root --> sv["System V IPC"] root --> posix["POSIX IPC"] root --> sock["Socket IPC"] u --> u1["管道 pipe"] u --> u2["FIFO 有名管道"] u --> u3["信号 signal"] sv --> s1["System V 信号量"] sv --> s2["System V 消息队列"] sv --> s3["System V 共享内存"] posix --> p1["POSIX 信号量"] posix --> p2["POSIX 消息队列"] posix --> p3["POSIX 共享内存"] sock --> k1["基于 Socket 的进程间通信"] ``` 总结: - **UNIX IPC**:管道、FIFO、信号; - **System V IPC**:信号量、消息队列、共享内存; - **POSIX IPC**:信号量、消息队列、共享内存; - **Socket IPC**:基于 Socket 的进程间通信。 > 较早的 System V IPC 存在一些不足之处,而 POSIX IPC 是在 System V IPC 的基础上改进形成的,弥补了它的一些不足。 ### 2.1 机制总览对比 > ⚠️ **来源说明**:下表"适用场景与限制"的细节为扩展知识,机制分类本身来自教材。 | 机制 | 类别 | 通信方向 | 能否跨非亲缘进程 | 数据形式 | 典型适用场景 | 主要限制 | | ------------------------- | -------------- | ----------- | ---------------- | ------------------- | ------------------------------ | ------------------ | | 管道 pipe | UNIX IPC | 单工 | 否,仅父子/兄弟 | 无格式字节流 | shell 管道、父子进程简单数据流 | 只能亲缘进程、单向 | | FIFO 有名管道 | UNIX IPC | 半双工/双向 | 是 | 无格式字节流 | 无亲缘关系进程间简单数据流 | 字节流无消息边界 | | 信号 signal | UNIX IPC | 单向通知 | 是 | 编号 + 少量伴随数据 | 异步事件通知、进程同步 | 传递信息少 | | System V / POSIX 消息队列 | System V/POSIX | 双向 | 是 | 有格式的消息 | 需要消息边界、异步收发 | 内核中受系统限制 | | System V / POSIX 信号量 | System V/POSIX | 不传数据 | 是 | 计数器 | 访问共享资源的同步/互斥 | 只做同步,不传数据 | | System V / POSIX 共享内存 | System V/POSIX | 双向 | 是 | 裸内存 | 大数据量、高性能共享 | 需自行配合同步机制 | | Socket | Socket IPC | 全双工 | 是(可跨主机) | 字节流/数据报 | 网络通信、跨主机、跨语言 | 协议栈开销 | --- ## 3. 管道和 FIFO 管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。**把一个进程连接到另一个进程的数据流称为管道**,管道被抽象成一个文件(pipe 文件类型)。 管道包括三种: | 类型 | 单工/双工 | 适用范围 | | -------------------------- | ---------------------- | --------------------------------------- | | 普通管道 pipe | 单工,数据只能单向传输 | 只能在父子或兄弟进程间使用 | | 流管道 s_pipe | 半双工,可以双向传输 | 只能在父子或兄弟进程间使用 | | 有名管道 name_pipe(FIFO) | 双向 | 允许在不相关(非父子/兄弟)的进程间通信 | - **普通管道**:用于具有亲缘关系的进程间通信,数据只能单向传输;要实现双向传输必须使用**两个管道**。 - **流管道**:去除了普通管道的单向限制,以半双工方式双向传输,但仍只能在亲缘进程间通信。 - **有名管道 FIFO**:同时突破普通管道的两种限制,既可双向传输,又能在非亲缘关系进程间通信。 ### 3.1 管道(pipe)读写细节 > ⚠️ **来源说明**:本节 API 原型与阻塞规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ```c #include int pipe(int pipefd[2]); ``` | 参数 | 含义 | | -------- | ------------------------------------------------ | | `pipefd` | 输出参数,`pipefd[0]` 为读端,`pipefd[1]` 为写端 | | 返回值 | 含义 | | ------ | ----------------------- | | 成功 | 返回 0 | | 失败 | 返回 -1,并设置 `errno` | 典型用法:父进程 `pipe()` 后 `fork()`,子进程继承两个 fd;关闭不用的一端后即可单向通信(父写 `pipefd[1]`,子读 `pipefd[0]`,或反之)。 ```mermaid flowchart LR P["父进程"] -->|"write(pipefd[1], buf, n)"| PIPE[("管道
内核缓冲区")] PIPE -->|"read(pipefd[0], buf, n)"| C["子进程"] ``` **读写阻塞规则**: | 情况 | 行为 | | -------------------------------------- | ---------------------------------------------------------------------------------- | | 管道为空,仍有写端打开 | `read()` 阻塞,直到有数据写入 | | 所有写端都已关闭 | `read()` 不再阻塞,返回 0(读到 EOF) | | 管道缓冲区已满 | `write()` 阻塞,直到有空间 | | 所有读端都已关闭 | `write()` 触发 `SIGPIPE` 信号,默认终止进程(`write` 返回 -1,`errno` 为 `EPIPE`) | | 写入量 ≤ `PIPE_BUF`(Linux 默认 4096) | 写入是原子的,不会与其它进程的写交错 | 要点:管道是**无格式字节流**,没有消息边界;读端关闭后写端继续写会收到 `SIGPIPE`,因此健壮的程序应处理或忽略 `SIGPIPE`。 ### 3.2 有名管道 FIFO > ⚠️ **来源说明**:本节 API 原型与打开规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 FIFO 在文件系统中以一个**文件**存在(文件类型为 `p`),因此无亲缘关系的进程只要知道路径就能打开它通信。 ```c #include #include int mkfifo(const char *pathname, mode_t mode); ``` | 参数 | 含义 | | ---------- | --------------------------- | | `pathname` | FIFO 文件路径 | | `mode` | 权限位(会与 `umask` 作用) | | 返回值 | 含义 | | ------ | -------------------------------------- | | 成功 | 返回 0 | | 失败 | 返回 -1,并设置 `errno`(如 `EEXIST`) | 创建后,用 `open()`、`read()`、`write()`、`close()` 像普通文件一样操作,规则与 pipe 一致。**打开阻塞规则**: | 打开方式 | 行为 | | ------------------------ | ------------------------------------- | | `open(path, O_RDONLY)` | 阻塞,直到有进程以写方式打开该 FIFO | | `open(path, O_WRONLY)` | 阻塞,直到有进程以读方式打开该 FIFO | | 加 `O_NONBLOCK` 只读打开 | 立即成功返回 | | 加 `O_NONBLOCK` 只写打开 | 若无读者,返回 -1,`errno` 为 `ENXIO` | ### 3.3 shell 中的管道 shell 的 `|` 就是 pipe 的经典应用:`ls -l | grep .c` 中,shell 创建管道并让 `ls` 的标准输出接到管道写端、`grep` 的标准输入接到读端。 --- ## 4. 信号 关于信号的内容在第八章已经介绍过:信号用于通知接收信号的进程有某种事件发生,因此**可用于进程间通信**;除了用于进程间通信之外,进程还可以发送信号给进程本身。 信号作为 IPC 手段的特点: - 传递的信息量少(一个信号编号,加实时信号的少量伴随数据); - 主要作为**异步事件通知**与进程同步手段,而不是数据传输手段; - 可靠信号支持排队与伴随数据。 详细用法见 [[02-进程线程与IPC/01-信号机制]]。 --- ## 5. 消息队列 消息队列是**消息的链表**,存放在内核中并由**消息队列标识符**标识。它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和 System V 消息队列。 消息队列是 UNIX 下不同进程之间实现共享资源的一种机制:UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程;**有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息**。 ### 5.1 System V 消息队列 API > ⚠️ **来源说明**:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ```c #include #include #include key_t ftok(const char *pathname, int proj_id); /* 生成 IPC key */ int msgget(key_t key, int msgflg); /* 创建/获取消息队列 */ int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); /* 发送消息 */ ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); /* 接收 */ int msgctl(int msqid, int cmd, struct msqid_ds *buf); /* 控制 */ ``` | 函数 | 参数 | 返回值 | | -------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------- | | `msgget` | `key` IPC 键;`msgflg` 如 `IPC_CREAT \| 0666` | 成功返回消息队列 ID;失败 -1 并设 `errno` | | `msgsnd` | `msqid` 队列 ID;`msgp` 消息指针;`msgsz` 消息正文长度(不含 `mtype`);`msgflg` 如 `IPC_NOWAIT` | 成功 0;失败 -1 并设 `errno` | | `msgrcv` | `msqid`;`msgp` 接收缓冲区;`msgsz` 缓冲区正文容量;`msgtyp` 指定接收哪类消息;`msgflg` | 成功返回实际接收字节数;失败 -1 并设 `errno` | | `msgctl` | `msqid`;`cmd` 如 `IPC_RMID`(删除队列)、`IPC_STAT`、`IPC_SET`;`buf` | 成功 0;失败 -1 并设 `errno` | 消息结构(用户自定义,`mtype` 必须为正): ```c struct msgbuf { long mtype; /* 消息类型,必须 > 0 */ char mtext[1]; /* 消息正文,长度由 msgsz 指定 */ }; ``` `msgtyp` 取值规则: | msgtyp | 含义 | | ------ | -------------------------------- | | `0` | 接收队列中的第一条消息 | | `> 0` | 接收第一条类型为 `msgtyp` 的消息 | | `< 0` | 接收类型值 ≤ ` | msgtyp | ` 的消息中类型最小的第一条 | `msgsnd` / `msgrcv` 在队列满 / 为空时默认阻塞,加 `IPC_NOWAIT` 则非阻塞返回(`errno` 为 `EAGAIN` / `ENOMSG`)。 --- ## 6. 信号量 信号量是一个**计数器**,与其它 IPC 方式不同:它主要用于控制多个进程间或一个进程内多个线程间对**共享资源**的访问,相当于内存中的标志。进程可根据它判定是否能够访问某些共享资源,同时也可以修改该标志。除了用于共享资源的访问控制外,还可用于**进程同步**。 它常作为一种**锁机制**,防止某进程访问资源时其它进程也访问该资源,因此主要作为进程间以及同一进程内不同线程之间的同步手段。Linux 提供了一组接口来操作信号量,声明在头文件 `sys/sem.h` 中。 ### 6.1 System V 信号量 API 与 PV 操作 > ⚠️ **来源说明**:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ```c #include #include #include int semget(key_t key, int nsems, int semflg); /* 创建/获取信号量集 */ int semop(int semid, struct sembuf *sops, size_t nsops); /* PV 操作 */ int semctl(int semid, int semnum, int cmd, ...); /* 控制 */ ``` | 函数 | 参数 | 返回值 | | -------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `semget` | `key` IPC 键;`nsems` 信号量个数;`semflg` 如 `IPC_CREAT \| 0666` | 成功返回信号量集 ID;失败 -1 并设 `errno` | | `semop` | `semid`;`sops` 操作数组;`nsops` 操作个数 | 成功 0;失败 -1 并设 `errno` | | `semctl` | `semid`;`semnum` 第几个信号量;`cmd` 如 `SETVAL`/`GETVAL`/`IPC_RMID`;第四个参数为 `union semun` | 依 `cmd` 而定 | ```c struct sembuf { unsigned short sem_num; /* 信号量在集合中的下标 */ short sem_op; /* 操作值:>0 表示 V 操作(释放);<0 表示 P 操作(申请);0 表示等待为 0 */ short sem_flg; /* 如 IPC_NOWAIT、SEM_UNDO */ }; ``` **P / V 语义**(扩展): - **P 操作(申请资源)**:`sem_op = -1`,信号量减 1;若当前值为 0,则阻塞等待,直到有进程释放。 - **V 操作(释放资源)**:`sem_op = +1`,信号量加 1,唤醒等待者。 - **等待为 0**:`sem_op = 0`,阻塞直到信号量值为 0,用于同步等待。 `semctl` 的常用 `cmd`: | cmd | 含义 | | ---------------------- | ---------------------------------------------- | | `SETVAL` | 设置某个信号量的初值(需传 `union semun.val`) | | `GETVAL` | 获取某个信号量的当前值 | | `IPC_RMID` | 删除整个信号量集 | | `IPC_STAT` / `IPC_SET` | 获取/设置状态信息 | `union semun` 需由用户自行定义(部分系统在头文件中提供): ```c union semun { int val; struct semid_ds *buf; unsigned short *array; }; ``` --- ## 7. 共享内存 共享内存就是映射一段能被其它进程所访问的内存:这段共享内存由一个进程创建,但其它多个进程都可以访问,使得多个进程可以访问**同一块内存空间**。 共享内存是**最快的 IPC 方式**,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(譬如结合信号量)来使用,以实现进程间的同步和通信——因为共享内存本身不提供互斥,多个进程同时写会产生竞态。 ### 7.1 System V 共享内存 API > ⚠️ **来源说明**:本节 API 原型与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ```c #include #include int shmget(key_t key, size_t size, int shmflg); /* 创建/获取 */ void *shmat(int shmid, const void *shmaddr, int shmflg); /* 挂载到进程地址空间 */ int shmdt(const void *shmaddr); /* 脱离 */ int shmctl(int shmid, int cmd, struct shmid_ds *buf); /* 控制 */ ``` | 函数 | 参数 | 返回值 | | -------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | | `shmget` | `key` IPC 键;`size` 共享内存大小;`shmflg` 如 `IPC_CREAT \| 0666` | 成功返回共享内存 ID;失败 -1 并设 `errno` | | `shmat` | `shmid`;`shmaddr` 一般为 `NULL`(由内核选择地址);`shmflg` 如 `0` / `SHM_RDONLY` | 成功返回映射后的虚拟地址;失败返回 `(void *)-1` | | `shmdt` | `shmaddr` 由 `shmat` 返回的地址 | 成功 0;失败 -1 并设 `errno` | | `shmctl` | `shmid`;`cmd` 如 `IPC_RMID`、`IPC_STAT`;`buf` | 成功 0;失败 -1 并设 `errno` | 使用流程: ```mermaid flowchart LR A["进程A: shmget 创建"] --> B["shmat 挂载"] B --> C["读写共享内存"] D["进程B: shmget 以同一 key 获取"] --> E["shmat 挂载"] E --> C C --> F["shmdt 脱离"] F --> G["shmctl IPC_RMID 删除"] ``` ### 7.2 与 mmap 对比 > ⚠️ **来源说明**:本节为扩展知识。 `mmap()` 也能实现内存映射,常用于**共享内存**与文件映射。两者定位不同: | 维度 | System V 共享内存 | mmap | | -------------- | --------------------------------- | -------------------------------------------------- | | 载体 | 匿名内核共享内存段 | 文件或匿名映射(`MAP_SHARED`) | | 持久化 | 直到 `IPC_RMID`,不随进程退出消失 | 文件映射随文件持久化;匿名映射随进程组消失 | | 生命周期管理 | `shmctl(IPC_RMID)` | `unlink` 文件 / 自动回收 | | 接口 | `shmget/shmat/shmdt/shmctl` | `mmap/munmap/msync` | | 与文件 IO 关系 | 无文件对象 | 可与文件内容直接关联,读写即文件读写 | | 常见用途 | 纯进程间大块数据共享 | 文件映射、设备映射(如 FrameBuffer)、匿名共享内存 | 二者都需要自行配合同步机制(信号量、互斥锁、信号)使用。 --- ## 8. 套接字(Socket) Socket 是一种 IPC 方法,是基于**网络**的 IPC 方法,允许位于**同一主机**或使用网络连接起来的**不同主机**上的应用程序之间交换数据,说白了就是网络通信。 在一个典型的客户端/服务器场景中,应用使用 socket 通信的方式如下: - 各个应用程序创建一个 socket。socket 是一个允许通信的"设备",两个应用程序都需要用到它; - 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上,使得客户端能够定位到它的位置。 Socket 的具体内容在网络编程章节介绍。其最大优势是**全双工、可跨主机、语言无关**;代价是协议栈开销。 --- ## 9. 完整可复制源码 ### 9.1 管道:父子进程通信 ```c #include #include #include #include #include #include int main(void) { int pipefd[2]; pid_t pid; char buf[128] = {0}; if (-1 == pipe(pipefd)) { perror("pipe error"); exit(-1); } pid = fork(); if (-1 == pid) { perror("fork error"); exit(-1); } if (0 == pid) { /* 子进程:只读,关闭写端 */ close(pipefd[1]); ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("子进程读到: %s\n", buf); } close(pipefd[0]); _exit(0); } /* 父进程:只写,关闭读端 */ close(pipefd[0]); const char *msg = "Hello from parent via pipe"; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); wait(NULL); exit(0); } ``` **逐段解释**: - `pipe(pipefd)` 创建管道,`pipefd[0]` 读、`pipefd[1]` 写。 - `fork()` 后子进程继承两个 fd;子进程关闭写端、父进程关闭读端,形成单向数据流。 - 父进程 `write` 后关闭写端;子进程 `read` 到数据后打印。若父进程关闭写端时子进程还没读,`read` 仍能读到缓冲数据,读完再次 `read` 会返回 0(EOF)。 ### 9.2 FIFO:两个独立进程通信 写端 `fifo_write.c`: ```c #include #include #include #include #include #include #include #include #define FIFO_PATH "./my_fifo" int main(void) { int fd; const char *msg = "Hello from FIFO writer"; /* 不存在则创建,EEXIST 忽略 */ if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) { perror("mkfifo error"); exit(-1); } /* 写方式打开会阻塞,直到有进程以读方式打开 */ fd = open(FIFO_PATH, O_WRONLY); if (-1 == fd) { perror("open error"); exit(-1); } write(fd, msg, strlen(msg)); close(fd); return 0; } ``` 读端 `fifo_read.c`: ```c #include #include #include #include #include #include #include #define FIFO_PATH "./my_fifo" int main(void) { int fd; char buf[128] = {0}; if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) { perror("mkfifo error"); exit(-1); } fd = open(FIFO_PATH, O_RDONLY); if (-1 == fd) { perror("open error"); exit(-1); } ssize_t n = read(fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("FIFO 读端收到: %s\n", buf); } close(fd); return 0; } ``` 编译与运行(需两个终端,先运行读端再运行写端): ```bash gcc -Wall -o fifo_write fifo_write.c gcc -Wall -o fifo_read fifo_read.c # 终端1 ./fifo_read # 终端2 ./fifo_write ``` ### 9.3 System V 消息队列:发送与接收 发送端 `msg_send.c`: ```c #include #include #include #include #include #include #define MSG_KEY 0x1234 struct msgbuf { long mtype; char mtext[128]; }; int main(void) { int msqid; struct msgbuf msg; /* 打开或创建消息队列 */ msqid = msgget(MSG_KEY, IPC_CREAT | 0666); if (-1 == msqid) { perror("msgget error"); exit(-1); } msg.mtype = 1; strcpy(msg.mtext, "Hello System V message queue"); if (-1 == msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0)) { perror("msgsnd error"); exit(-1); } puts("消息发送成功"); return 0; } ``` 接收端 `msg_recv.c`: ```c #include #include #include #include #include #define MSG_KEY 0x1234 struct msgbuf { long mtype; char mtext[128]; }; int main(void) { int msqid; struct msgbuf msg; msqid = msgget(MSG_KEY, IPC_CREAT | 0666); if (-1 == msqid) { perror("msgget error"); exit(-1); } /* msgtyp = 0:接收第一条消息 */ if (-1 == msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0)) { perror("msgrcv error"); exit(-1); } printf("收到消息(type=%ld): %s\n", msg.mtype, msg.mtext); /* 删除消息队列 */ msgctl(msqid, IPC_RMID, NULL); return 0; } ``` **逐段解释**: - `msgget(MSG_KEY, IPC_CREAT | 0666)`:以固定 key 创建/获取队列,双方用同一 key 找到同一队列。 - `msgsnd` 的 `msgsz` 是**正文长度**,不含 `mtype`;`mtype` 必须大于 0。 - `msgrcv` 的 `msgtyp = 0` 表示取第一条消息;`msgsz` 是接收缓冲区正文容量。 - 接收端 `msgctl(msqid, IPC_RMID, NULL)` 删除队列,释放内核资源。 ### 9.4 System V 信号量:PV 互斥 ```c #include #include #include #include #include #include #define SEM_KEY 0x5678 union semun { int val; struct semid_ds *buf; unsigned short *array; }; /* P 操作:申请资源,信号量 -1 */ static int sem_p(int semid) { struct sembuf op = { .sem_num = 0, .sem_op = -1, .sem_flg = 0 }; return semop(semid, &op, 1); } /* V 操作:释放资源,信号量 +1 */ static int sem_v(int semid) { struct sembuf op = { .sem_num = 0, .sem_op = 1, .sem_flg = 0 }; return semop(semid, &op, 1); } int main(void) { int semid; union semun arg; /* 创建含 1 个信号量的集合 */ semid = semget(SEM_KEY, 1, IPC_CREAT | 0666); if (-1 == semid) { perror("semget error"); exit(-1); } /* 初值设为 1(当作互斥锁) */ arg.val = 1; if (-1 == semctl(semid, 0, SETVAL, arg)) { perror("semctl error"); exit(-1); } sem_p(semid); /* 进入临界区 */ puts("进入临界区,持有信号量..."); sleep(2); sem_v(semid); /* 退出临界区 */ puts("退出临界区"); semctl(semid, 0, IPC_RMID); /* 删除信号量集 */ return 0; } ``` ### 9.5 System V 共享内存 ```c #include #include #include #include #include #include #include #define SHM_KEY 0x9abc #define SHM_SIZE 4096 int main(void) { int shmid; char *addr; /* 创建共享内存段 */ shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666); if (-1 == shmid) { perror("shmget error"); exit(-1); } /* 挂载到当前进程地址空间(shmaddr 为 NULL) */ addr = shmat(shmid, NULL, 0); if ((void *)-1 == addr) { perror("shmat error"); exit(-1); } /* 直接像普通内存一样读写 */ strcpy(addr, "Hello shared memory"); printf("写入共享内存: %s\n", addr); /* 脱离 */ if (-1 == shmdt(addr)) { perror("shmdt error"); exit(-1); } /* 删除共享内存段 */ shmctl(shmid, IPC_RMID, NULL); return 0; } ``` **逐段解释**: - `shmget` 以 key 创建共享内存,其它进程用同一 key + `shmget` 即可获取同一段内存。 - `shmat` 返回映射后的虚拟地址,之后直接当普通内存读写(无需 `read`/`write`),这是共享内存高效的原因。 - `shmdt` 脱离映射;`shmctl(IPC_RMID)` 删除共享内存段。注意删除后已挂载的进程仍可继续访问,直到最后脱离。 --- ## 10. 实验步骤与调试方法 ### 10.1 编译运行 ```bash gcc -Wall -o pipe_demo pipe_demo.c gcc -Wall -o msg_send msg_send.c gcc -Wall -o msg_recv msg_recv.c gcc -Wall -o sem_demo sem_demo.c gcc -Wall -o shm_demo shm_demo.c ./pipe_demo ./msg_send && ./msg_recv ./sem_demo ./shm_demo ``` ### 10.2 常用调试命令 | 命令 | 用途 | | ------------------ | ------------------------------------------------------ | | `ls -l ` | 查看 FIFO 文件,类型为 `p` | | `ipcs` | 查看所有 System V IPC 对象(共享内存/消息队列/信号量) | | `ipcs -q` | 只看消息队列 | | `ipcs -m` | 只看共享内存 | | `ipcs -s` | 只看信号量 | | `ipcrm -q ` | 删除消息队列 | | `ipcrm -m ` | 删除共享内存 | | `ipcrm -s ` | 删除信号量集 | | `strace ./app` | 跟踪 `pipe`/`read`/`write`/`msgget` 等系统调用 | ### 10.3 实验一:验证管道 EOF 与 SIGPIPE - 父进程写完立即 `close(pipefd[1])`,子进程 `read` 完数据后再次 `read` 会返回 0,说明读到 EOF; - 反过来,让读端提前关闭,写端继续 `write`,进程会收到 `SIGPIPE` 默认终止。用 `signal(SIGPIPE, SIG_IGN)` 忽略后,`write` 返回 -1、`errno` 为 `EPIPE`。 ### 10.4 实验二:验证消息队列持久化 先运行 `./msg_send`,再在另一个时刻运行 `./msg_recv`:消息仍能收到,因为**消息队列存在于内核中**,不随发送进程退出而消失。用 `ipcs -q` 可看到队列,`ipcrm -q ` 清理。 ### 10.5 实验三:观察 FIFO 打开阻塞 先运行 `./fifo_read`(只读打开阻塞等待写者),此时进程挂起;再运行 `./fifo_write`,两端同时被唤醒完成通信。用 `ps` 可看到读端处于睡眠状态。 ### 10.6 实验四:共享内存 + 信号量配合 共享内存本身不互斥。可写一个程序模拟"多个进程同时写同一块共享内存",无信号量保护时观察数据错乱;再用信号量 P/V 包住临界区,数据保持一致。 --- ## 11. 跨平台对比:IMX6ULL vs STM32 vs RK3568 | 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568(Linux) | | ---------- | ---------------------- | ----------------------------------------- | --------------- | | 进程隔离 | 完整 MMU 隔离,需 IPC | 通常单地址空间,全局变量即可 | 同 I.MX6ULL | | 管道/FIFO | 支持 | 无(可用环形缓冲区替代) | 支持 | | 消息队列 | System V / POSIX | RTOS 消息队列(如 FreeRTOS `xQueueSend`) | 同 I.MX6ULL | | 信号量 | System V / POSIX | RTOS 信号量/互斥量 | 同 I.MX6ULL | | 共享内存 | `shmget` 系列、`mmap` | 直接访问全局内存/双缓冲 | 同 I.MX6ULL | | 跨主机通信 | Socket | 需外接网络模块 + 协议栈 | Socket | | 调试工具 | `ipcs`/`ipcrm`/`ls -l` | 调试器观察内存 | 同 I.MX6ULL | 要点: - **STM32 裸机上"进程间通信"概念基本不成立**,因为没有进程隔离,全局变量、RTOS 队列/信号量就够用;只有到 Linux 这类带 MMU 的系统,IPC 才是刚需。 - I.MX6ULL 与 RK3568 都是 Linux,System V IPC 接口通用(RK3568 通常内核配置更全,POSIX IPC、`mq_*`、`sem_*` 都可用)。 - 嵌入式 Linux 上使用 System V IPC 要注意:**内核资源有限**(`msgmni`、`shmmax`、`semmni` 等参数),对象用完要及时 `IPC_RMID`,否则会泄漏内核资源。 --- ## 12. 面试精选(5 题) ### Q1:Linux 有哪些进程间通信机制?各自适合什么场景? **答案**:分四类。**UNIX IPC**:管道(亲缘进程、单向字节流)、FIFO 有名管道(无亲缘进程、可双向)、信号(异步事件通知、信息量少)。**System V IPC**:消息队列(有格式消息、异步收发、有消息边界)、信号量(计数器,做同步/互斥,不传数据)、共享内存(大块数据、最快的 IPC,需自配同步)。**POSIX IPC**:与 System V 对应,接口更现代(`sem_open`/`mq_open`/`shm_open`)。**Socket IPC**:全双工、可跨主机、跨语言,适合网络与复杂场景。选型原则:小数据同步用信号/信号量;简单字节流用管道/FIFO;需要消息边界用消息队列;大数据量高性能用共享内存 + 信号量;跨主机用 Socket。 ### Q2:管道的读写分别在什么情况下阻塞?读端关闭后写端继续写会怎样? **答案**:`read`:管道为空且仍有写端打开时阻塞;所有写端关闭后不再阻塞,返回 0(EOF)。`write`:管道缓冲区满时阻塞,直到有空间。若**所有读端都已关闭**,写端继续 `write` 会触发 `SIGPIPE` 信号,默认终止进程,`write` 返回 -1 且 `errno = EPIPE`。因此健壮程序应忽略或处理 `SIGPIPE`,例如 `signal(SIGPIPE, SIG_IGN)` 后再判断返回值。此外管道写入量不超过 `PIPE_BUF`(Linux 默认 4096 字节)时是原子写,多进程写不会交错。 ### Q3:消息队列相对管道有什么优势?`msgrcv` 的 `msgtyp` 参数怎么用? **答案**:消息队列存放在**内核中**,有消息边界(保留消息长度),支持按类型选择性接收,还能突破管道"缓冲区大小受限"和"只能承载无格式字节流"的缺陷;且不要求收发进程有亲缘关系,发送方发完即可退出(消息保留在内核)。`msgrcv` 的 `msgtyp`:等于 0 接收队列中第一条消息;大于 0 接收第一条类型等于 `msgtyp` 的消息;小于 0 接收类型值 ≤ `|msgtyp|` 的消息中类型最小的那一条。配合 `IPC_NOWAIT` 可非阻塞读取。 ### Q4:共享内存为什么是最快的 IPC?为什么它通常要和信号量配合使用? **答案**:共享内存把同一块物理内存映射到多个进程各自的虚拟地址空间,进程读写它就像读写自己的内存——**没有任何内核数据拷贝**。管道、消息队列等都要经历"用户空间→内核缓冲区→用户空间"的两次拷贝,因此共享内存最快。但它本身**不提供任何同步或互斥**:多个进程同时读写会产生竞态、读到不一致数据。所以通常配一个信号量(或互斥锁)来保证同一时刻只有一个进程进入临界区,实现"高效数据传输 + 正确同步"的组合。 ### Q5:`fork()` 之后父子进程如何通信?请描述一个基于管道的实现,并说明为什么要关闭不用的文件描述符。 **答案**:先 `pipe(pipefd)` 创建管道,再 `fork()`。由于子进程继承父进程的文件描述符,父子都能访问 `pipefd[0]`(读端)和 `pipefd[1]`(写端)。实现单向通信:若要父写子读,父进程关闭 `pipefd[0]`、子进程关闭 `pipefd[1]`,父进程 `write(pipefd[1], ...)`、子进程 `read(pipefd[0], ...)`。**必须关闭不用的端**,否则会有两个问题:① 无法正确产生 EOF——只有当所有写端都关闭后,读端的 `read` 才返回 0,如果不关闭子进程里继承的写端,父进程关闭了自己的写端,管道仍被视为有写者打开,读端会一直阻塞;② 无谓地占用文件描述符、影响资源回收。这正是"每个进程只保留自己需要的一端"的原因。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十章 进程间通信简介