进程间通信:管道、System V

进程间通信 (Inter-Process Communication, IPC)

一、 进程间通信介绍

1.1 为什么需要 IPC?

进程具有独立性 。每个进程有独立的虚拟地址空间,即便父进程 fork 出子进程,子进程能看到父进程的数据,但由于写时拷贝 (Copy-on-Write) 机制,一方的数据修改对另一方不可见,父子进程也无法直接传递数据。

举个具体的例子:父进程中有一个全局变量 int g_val = 100fork 之后,子进程能读到 g_val == 100。但当子进程执行 g_val = 200 时,内核会为子进程的 g_val 所在页表分配一份新的物理页,子进程的修改只作用于自己的副本,父进程中 g_val 依然为 100。父子进程的地址空间至此分道扬镳。

所以进程间通信要解决的核心问题是:打破进程独立性壁垒,让不同进程"看到"同一份共享资源。

1.2 IPC 的目的

  • 数据传输: 一个进程将数据发送给另一个进程(如 who | wc -lwho 的输出传给 wc)。
  • 资源共享: 多个进程共享同一份资源(如多个 worker 进程共享同一份任务队列)。
  • 通知事件: 向一个或一组进程发送消息,通知发生了某种事件(如子进程终止时向父进程发送 SIGCHLD)。
  • 进程控制: 某些进程希望完全控制另一个进程的执行(如 gdb 调试时拦截目标进程的陷⼊和异常)。

1.3 IPC 的分类

分类 典型机制 适用场景
管道 匿名管道 (pipe)、命名管道 (mkfifo) 简单数据流,亲缘 / 非亲缘进程
System V IPC 共享内存、消息队列、信号量 一台机器内部高效通信
POSIX IPC 消息队列、共享内存、信号量、互斥锁、条件变量 跨平台,兼顾本地与网络通信

1.4 IPC 的本质

IPC 通信的共享资源位于内存中。进程要访问这份资源,必须经过操作系统,因此需要系统调用。而不同的 IPC 机制由不同的系统调用接口(即通信协议)来规范。
#mermaid-svg-CQAAg8kAEM08pbEZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CQAAg8kAEM08pbEZ .error-icon{fill:#552222;}#mermaid-svg-CQAAg8kAEM08pbEZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CQAAg8kAEM08pbEZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CQAAg8kAEM08pbEZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CQAAg8kAEM08pbEZ .marker.cross{stroke:#333333;}#mermaid-svg-CQAAg8kAEM08pbEZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CQAAg8kAEM08pbEZ p{margin:0;}#mermaid-svg-CQAAg8kAEM08pbEZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster-label text{fill:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster-label span{color:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster-label span p{background-color:transparent;}#mermaid-svg-CQAAg8kAEM08pbEZ .label text,#mermaid-svg-CQAAg8kAEM08pbEZ span{fill:#333;color:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ .node rect,#mermaid-svg-CQAAg8kAEM08pbEZ .node circle,#mermaid-svg-CQAAg8kAEM08pbEZ .node ellipse,#mermaid-svg-CQAAg8kAEM08pbEZ .node polygon,#mermaid-svg-CQAAg8kAEM08pbEZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CQAAg8kAEM08pbEZ .rough-node .label text,#mermaid-svg-CQAAg8kAEM08pbEZ .node .label text,#mermaid-svg-CQAAg8kAEM08pbEZ .image-shape .label,#mermaid-svg-CQAAg8kAEM08pbEZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-CQAAg8kAEM08pbEZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CQAAg8kAEM08pbEZ .rough-node .label,#mermaid-svg-CQAAg8kAEM08pbEZ .node .label,#mermaid-svg-CQAAg8kAEM08pbEZ .image-shape .label,#mermaid-svg-CQAAg8kAEM08pbEZ .icon-shape .label{text-align:center;}#mermaid-svg-CQAAg8kAEM08pbEZ .node.clickable{cursor:pointer;}#mermaid-svg-CQAAg8kAEM08pbEZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CQAAg8kAEM08pbEZ .arrowheadPath{fill:#333333;}#mermaid-svg-CQAAg8kAEM08pbEZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CQAAg8kAEM08pbEZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CQAAg8kAEM08pbEZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CQAAg8kAEM08pbEZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CQAAg8kAEM08pbEZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CQAAg8kAEM08pbEZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster text{fill:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ .cluster span{color:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CQAAg8kAEM08pbEZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CQAAg8kAEM08pbEZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-CQAAg8kAEM08pbEZ .icon-shape,#mermaid-svg-CQAAg8kAEM08pbEZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CQAAg8kAEM08pbEZ .icon-shape p,#mermaid-svg-CQAAg8kAEM08pbEZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CQAAg8kAEM08pbEZ .icon-shape .label rect,#mermaid-svg-CQAAg8kAEM08pbEZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CQAAg8kAEM08pbEZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CQAAg8kAEM08pbEZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CQAAg8kAEM08pbEZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 系统调用

write / send
② 系统调用

read / recv
进程 A

(用户态)
内核

共享资源

(缓冲区 / 内存块)
进程 B

(用户态)

一句话总结:IPC = 打破独立性壁垒 + 内核提供共享资源 + 系统调用规范操作。

二、 管道 (Pipe)

2.1 匿名管道 (pipe)

2.1.1 原理:管道就是文件

Linux 中一切皆文件,管道也不例外。进程打开一个普通磁盘文件时,内核会创建 struct file 来描述它,包含 inode 指针、操作表(f_op)、文件内核缓冲区等结构。

管道的设计正是复用这套文件框架 :创建一个 struct file,也分配对应的 inode 和操作表,但不关联任何磁盘上的数据块 ------管道是纯内存文件,数据只在内核缓冲区流转,write 不会触发磁盘 I/O,sync 也不会刷盘。

所以管道的本质是:基于文件结构、在内核缓冲区中完成的数据传输。 打开一个管道和打开一个普通文件在内核中的代码路径高度重合,这正是 Linux "一切皆文件" 哲学在 IPC 中的体现。

2.1.2 管道如何实现进程间通信
  1. 父进程调用 pipe(int fd[2]) 创建管道,得到两个 fd:fd[0](读端)、fd[1](写端)。
  2. 父进程 fork 出子进程,子进程拷贝父进程的文件描述符表 (浅拷贝),父子进程的 fd 指向同一个 内核 struct file
  3. 此时 inode、操作表、文件内核缓冲区不需要重新拷贝,父子进程共享同一份内核缓冲区。
  4. 确定通信方向:一方关闭读端,一方关闭写端,形成单向数据流

pipe() 创建后(仅父进程)

下标 父进程 fd 表
0 stdin
1 stdout
2 stderr
3 → 读端
4 → 写端

#mermaid-svg-SDC3So0KxpqrdNO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SDC3So0KxpqrdNO7 .error-icon{fill:#552222;}#mermaid-svg-SDC3So0KxpqrdNO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SDC3So0KxpqrdNO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SDC3So0KxpqrdNO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SDC3So0KxpqrdNO7 .marker.cross{stroke:#333333;}#mermaid-svg-SDC3So0KxpqrdNO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SDC3So0KxpqrdNO7 p{margin:0;}#mermaid-svg-SDC3So0KxpqrdNO7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster-label text{fill:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster-label span{color:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster-label span p{background-color:transparent;}#mermaid-svg-SDC3So0KxpqrdNO7 .label text,#mermaid-svg-SDC3So0KxpqrdNO7 span{fill:#333;color:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 .node rect,#mermaid-svg-SDC3So0KxpqrdNO7 .node circle,#mermaid-svg-SDC3So0KxpqrdNO7 .node ellipse,#mermaid-svg-SDC3So0KxpqrdNO7 .node polygon,#mermaid-svg-SDC3So0KxpqrdNO7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SDC3So0KxpqrdNO7 .rough-node .label text,#mermaid-svg-SDC3So0KxpqrdNO7 .node .label text,#mermaid-svg-SDC3So0KxpqrdNO7 .image-shape .label,#mermaid-svg-SDC3So0KxpqrdNO7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-SDC3So0KxpqrdNO7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SDC3So0KxpqrdNO7 .rough-node .label,#mermaid-svg-SDC3So0KxpqrdNO7 .node .label,#mermaid-svg-SDC3So0KxpqrdNO7 .image-shape .label,#mermaid-svg-SDC3So0KxpqrdNO7 .icon-shape .label{text-align:center;}#mermaid-svg-SDC3So0KxpqrdNO7 .node.clickable{cursor:pointer;}#mermaid-svg-SDC3So0KxpqrdNO7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SDC3So0KxpqrdNO7 .arrowheadPath{fill:#333333;}#mermaid-svg-SDC3So0KxpqrdNO7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SDC3So0KxpqrdNO7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SDC3So0KxpqrdNO7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SDC3So0KxpqrdNO7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SDC3So0KxpqrdNO7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SDC3So0KxpqrdNO7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster text{fill:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 .cluster span{color:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SDC3So0KxpqrdNO7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SDC3So0KxpqrdNO7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-SDC3So0KxpqrdNO7 .icon-shape,#mermaid-svg-SDC3So0KxpqrdNO7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SDC3So0KxpqrdNO7 .icon-shape p,#mermaid-svg-SDC3So0KxpqrdNO7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SDC3So0KxpqrdNO7 .icon-shape .label rect,#mermaid-svg-SDC3So0KxpqrdNO7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SDC3So0KxpqrdNO7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SDC3So0KxpqrdNO7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SDC3So0KxpqrdNO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} fd3
struct file(读)
内核文件缓冲区
fd4
struct file(写)

fork() 后(父子 fd 表完全一致,浅拷贝)

下标 父进程 fd 表 子进程 fd 表
0 stdin stdin
1 stdout stdout
2 stderr stderr
3 → 读端 → 读端
4 → 写端 → 写端

#mermaid-svg-op4s8jtO3LHfYTLb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-op4s8jtO3LHfYTLb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-op4s8jtO3LHfYTLb .error-icon{fill:#552222;}#mermaid-svg-op4s8jtO3LHfYTLb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-op4s8jtO3LHfYTLb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-op4s8jtO3LHfYTLb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-op4s8jtO3LHfYTLb .marker.cross{stroke:#333333;}#mermaid-svg-op4s8jtO3LHfYTLb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-op4s8jtO3LHfYTLb p{margin:0;}#mermaid-svg-op4s8jtO3LHfYTLb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-op4s8jtO3LHfYTLb .cluster-label text{fill:#333;}#mermaid-svg-op4s8jtO3LHfYTLb .cluster-label span{color:#333;}#mermaid-svg-op4s8jtO3LHfYTLb .cluster-label span p{background-color:transparent;}#mermaid-svg-op4s8jtO3LHfYTLb .label text,#mermaid-svg-op4s8jtO3LHfYTLb span{fill:#333;color:#333;}#mermaid-svg-op4s8jtO3LHfYTLb .node rect,#mermaid-svg-op4s8jtO3LHfYTLb .node circle,#mermaid-svg-op4s8jtO3LHfYTLb .node ellipse,#mermaid-svg-op4s8jtO3LHfYTLb .node polygon,#mermaid-svg-op4s8jtO3LHfYTLb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-op4s8jtO3LHfYTLb .rough-node .label text,#mermaid-svg-op4s8jtO3LHfYTLb .node .label text,#mermaid-svg-op4s8jtO3LHfYTLb .image-shape .label,#mermaid-svg-op4s8jtO3LHfYTLb .icon-shape .label{text-anchor:middle;}#mermaid-svg-op4s8jtO3LHfYTLb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-op4s8jtO3LHfYTLb .rough-node .label,#mermaid-svg-op4s8jtO3LHfYTLb .node .label,#mermaid-svg-op4s8jtO3LHfYTLb .image-shape .label,#mermaid-svg-op4s8jtO3LHfYTLb .icon-shape .label{text-align:center;}#mermaid-svg-op4s8jtO3LHfYTLb .node.clickable{cursor:pointer;}#mermaid-svg-op4s8jtO3LHfYTLb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-op4s8jtO3LHfYTLb .arrowheadPath{fill:#333333;}#mermaid-svg-op4s8jtO3LHfYTLb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-op4s8jtO3LHfYTLb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-op4s8jtO3LHfYTLb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-op4s8jtO3LHfYTLb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-op4s8jtO3LHfYTLb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-op4s8jtO3LHfYTLb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-op4s8jtO3LHfYTLb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-op4s8jtO3LHfYTLb .cluster text{fill:#333;}#mermaid-svg-op4s8jtO3LHfYTLb .cluster span{color:#333;}#mermaid-svg-op4s8jtO3LHfYTLb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-op4s8jtO3LHfYTLb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-op4s8jtO3LHfYTLb rect.text{fill:none;stroke-width:0;}#mermaid-svg-op4s8jtO3LHfYTLb .icon-shape,#mermaid-svg-op4s8jtO3LHfYTLb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-op4s8jtO3LHfYTLb .icon-shape p,#mermaid-svg-op4s8jtO3LHfYTLb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-op4s8jtO3LHfYTLb .icon-shape .label rect,#mermaid-svg-op4s8jtO3LHfYTLb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-op4s8jtO3LHfYTLb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-op4s8jtO3LHfYTLb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-op4s8jtO3LHfYTLb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父 fd3
struct file(读)
子 fd3
内核文件缓冲区
父 fd4
struct file(写)
子 fd4

close() 后(单向通道形成)

下标 父进程 fd 表 子进程 fd 表
0 stdin stdin
1 stdout stdout
2 stderr stderr
3 → 读端 ◀ 关闭
4 关闭 → 写端 ▶

#mermaid-svg-ZPYHB8QnGUzisgMA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZPYHB8QnGUzisgMA .error-icon{fill:#552222;}#mermaid-svg-ZPYHB8QnGUzisgMA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZPYHB8QnGUzisgMA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZPYHB8QnGUzisgMA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZPYHB8QnGUzisgMA .marker.cross{stroke:#333333;}#mermaid-svg-ZPYHB8QnGUzisgMA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZPYHB8QnGUzisgMA p{margin:0;}#mermaid-svg-ZPYHB8QnGUzisgMA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster-label text{fill:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster-label span{color:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster-label span p{background-color:transparent;}#mermaid-svg-ZPYHB8QnGUzisgMA .label text,#mermaid-svg-ZPYHB8QnGUzisgMA span{fill:#333;color:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA .node rect,#mermaid-svg-ZPYHB8QnGUzisgMA .node circle,#mermaid-svg-ZPYHB8QnGUzisgMA .node ellipse,#mermaid-svg-ZPYHB8QnGUzisgMA .node polygon,#mermaid-svg-ZPYHB8QnGUzisgMA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZPYHB8QnGUzisgMA .rough-node .label text,#mermaid-svg-ZPYHB8QnGUzisgMA .node .label text,#mermaid-svg-ZPYHB8QnGUzisgMA .image-shape .label,#mermaid-svg-ZPYHB8QnGUzisgMA .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZPYHB8QnGUzisgMA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZPYHB8QnGUzisgMA .rough-node .label,#mermaid-svg-ZPYHB8QnGUzisgMA .node .label,#mermaid-svg-ZPYHB8QnGUzisgMA .image-shape .label,#mermaid-svg-ZPYHB8QnGUzisgMA .icon-shape .label{text-align:center;}#mermaid-svg-ZPYHB8QnGUzisgMA .node.clickable{cursor:pointer;}#mermaid-svg-ZPYHB8QnGUzisgMA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZPYHB8QnGUzisgMA .arrowheadPath{fill:#333333;}#mermaid-svg-ZPYHB8QnGUzisgMA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZPYHB8QnGUzisgMA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZPYHB8QnGUzisgMA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZPYHB8QnGUzisgMA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZPYHB8QnGUzisgMA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZPYHB8QnGUzisgMA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster text{fill:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA .cluster span{color:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZPYHB8QnGUzisgMA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZPYHB8QnGUzisgMA rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZPYHB8QnGUzisgMA .icon-shape,#mermaid-svg-ZPYHB8QnGUzisgMA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZPYHB8QnGUzisgMA .icon-shape p,#mermaid-svg-ZPYHB8QnGUzisgMA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZPYHB8QnGUzisgMA .icon-shape .label rect,#mermaid-svg-ZPYHB8QnGUzisgMA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZPYHB8QnGUzisgMA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZPYHB8QnGUzisgMA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZPYHB8QnGUzisgMA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父 fd3
struct file(读)
内核文件缓冲区
子 fd4
struct file(写)

三个阶段:pipe 创建 → fork 浅拷贝 → 关闭指定端形成单向通道。注意父子进程的 fd3 都指向同一份 内核 struct file(读),fd4 都指向同一份(写),缓冲区自始至终只有一份。

2.1.3 为什么要拷贝 struct file?

因为文件有 modepos(读写位置)。管道是单向通信(单工) ,需要限制父子进程各自只能读或只能写。两份 struct file 各自维护自己的读写模式,从底层杜绝误操作------就像指针初始化为 null,从源头避免野指针。

当然你不关闭也可以通信,但万一误写就会破坏单向条件。从底层限制远比靠自觉可靠。

2.1.4 几个关键细节
  1. 单工 vs 半双工 vs 全双工:

    • 管道属于单工,数据只能向一个方向流动(像上课,老师讲学生听)
    • 如果需要双向通信,必须创建两个管道
    • 全双工:双方可同时发送(像吵架)
    • 半双工:同一时刻只能一方发(像正常对话)
  2. 为什么叫"匿名"管道?

    • 普通文件打开需要路径+文件名
    • 管道不需要关联任何磁盘路径,没有文件名,所以是"匿名"的
    • 只能用于有亲缘关系的进程(父子、兄弟等),因为需要共享文件描述符表
  3. 经典案例:who | wc -l

    • whowc 是两个独立进程
    • shell 创建管道,who 的标准输出重定向到管道写端,wc 的标准输入重定向到管道读端
    • who 写入数据,wc 从管道读取,完成跨进程通信

#mermaid-svg-4eI9NybWVflZ40tj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4eI9NybWVflZ40tj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4eI9NybWVflZ40tj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4eI9NybWVflZ40tj .error-icon{fill:#552222;}#mermaid-svg-4eI9NybWVflZ40tj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4eI9NybWVflZ40tj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4eI9NybWVflZ40tj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4eI9NybWVflZ40tj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4eI9NybWVflZ40tj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4eI9NybWVflZ40tj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4eI9NybWVflZ40tj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4eI9NybWVflZ40tj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4eI9NybWVflZ40tj .marker.cross{stroke:#333333;}#mermaid-svg-4eI9NybWVflZ40tj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4eI9NybWVflZ40tj p{margin:0;}#mermaid-svg-4eI9NybWVflZ40tj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4eI9NybWVflZ40tj .cluster-label text{fill:#333;}#mermaid-svg-4eI9NybWVflZ40tj .cluster-label span{color:#333;}#mermaid-svg-4eI9NybWVflZ40tj .cluster-label span p{background-color:transparent;}#mermaid-svg-4eI9NybWVflZ40tj .label text,#mermaid-svg-4eI9NybWVflZ40tj span{fill:#333;color:#333;}#mermaid-svg-4eI9NybWVflZ40tj .node rect,#mermaid-svg-4eI9NybWVflZ40tj .node circle,#mermaid-svg-4eI9NybWVflZ40tj .node ellipse,#mermaid-svg-4eI9NybWVflZ40tj .node polygon,#mermaid-svg-4eI9NybWVflZ40tj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4eI9NybWVflZ40tj .rough-node .label text,#mermaid-svg-4eI9NybWVflZ40tj .node .label text,#mermaid-svg-4eI9NybWVflZ40tj .image-shape .label,#mermaid-svg-4eI9NybWVflZ40tj .icon-shape .label{text-anchor:middle;}#mermaid-svg-4eI9NybWVflZ40tj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4eI9NybWVflZ40tj .rough-node .label,#mermaid-svg-4eI9NybWVflZ40tj .node .label,#mermaid-svg-4eI9NybWVflZ40tj .image-shape .label,#mermaid-svg-4eI9NybWVflZ40tj .icon-shape .label{text-align:center;}#mermaid-svg-4eI9NybWVflZ40tj .node.clickable{cursor:pointer;}#mermaid-svg-4eI9NybWVflZ40tj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4eI9NybWVflZ40tj .arrowheadPath{fill:#333333;}#mermaid-svg-4eI9NybWVflZ40tj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4eI9NybWVflZ40tj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4eI9NybWVflZ40tj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4eI9NybWVflZ40tj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4eI9NybWVflZ40tj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4eI9NybWVflZ40tj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4eI9NybWVflZ40tj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4eI9NybWVflZ40tj .cluster text{fill:#333;}#mermaid-svg-4eI9NybWVflZ40tj .cluster span{color:#333;}#mermaid-svg-4eI9NybWVflZ40tj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4eI9NybWVflZ40tj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4eI9NybWVflZ40tj rect.text{fill:none;stroke-width:0;}#mermaid-svg-4eI9NybWVflZ40tj .icon-shape,#mermaid-svg-4eI9NybWVflZ40tj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4eI9NybWVflZ40tj .icon-shape p,#mermaid-svg-4eI9NybWVflZ40tj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4eI9NybWVflZ40tj .icon-shape .label rect,#mermaid-svg-4eI9NybWVflZ40tj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4eI9NybWVflZ40tj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4eI9NybWVflZ40tj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4eI9NybWVflZ40tj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} write
read
who 进程

stdout → 管道写端
管道

内核缓冲区
wc -l 进程

管道读端 → stdin

2.1.5 关键 API
c 复制代码
#include <unistd.h>
int pipe(int fd[2]);
// fd[0] --- 读端
// fd[1] --- 写端
// 返回值:成功 0,失败 -1
2.1.6 实战演练:父子进程通信

四步走: ① 创建管道 → ② fork 子进程 → ③ 关闭指定读写端 → ④ 通信

Step 1:创建管道

c 复制代码
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
    perror("pipe");
    return 1;
}
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);

每个进程默认打开 fd 0(stdin)、1(stdout)、2(stderr),所以管道得到的 fd 从 3 开始:

shell 复制代码
# ./test
pipefd[0]: 3, pipefd[1]: 4

记忆技巧: fd[0] 像嘴巴(读),fd[1] 像笔(写)。

Step 2 & 3:fork 子进程 + 关闭指定端

c 复制代码
pid_t id = fork();
if (id == 0)
{
    // 子进程:write
    close(pipefd[0]);  // 关闭读端,只写
}
else
{
    // 父进程:read
    close(pipefd[1]);  // 关闭写端,只读
}

这三步本质是通信前的"布线" ,还没有真正的数据流动。子进程 fork 后继承了父进程的 fd 表,两个 struct file 指向同一份内核缓冲区,关闭指定端后形成单向通道。

Step 4:真正通信

c 复制代码
// 子进程 ------ 向管道写数据
const char *msg = "Hello, I'm child";
write(pipefd[1], msg, strlen(msg));  // 不 +1,不写 \0

// 父进程 ------ 从管道读数据
char buf[1024];
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
//                   注意 -1:留一字节,因为 read 不管 \0
buf[n] = '\0';  // 手动补 \0
printf("parent got: %s\n", buf);

snprintf 是 C 库函数自动补 \0read/write 是系统调用,只传递裸字节。所以读的时候 -1 留一位,读完后手动加 \0

下面是四个完整的实验代码,每个都可以独立编译运行。


实验 1:基本通信(情况一:管道空 → 读阻塞)

完整代码 pipe_basic.c

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

int main()
{
    // Step 1:创建管道
    int pipefd[2];
    int n = pipe(pipefd);
    if (n < 0)
    {
        perror("pipe");
        return 1;
    }
    // 每个进程默认打开 0(stdin)、1(stdout)、2(stderr)
    // 所以管道 fd 从 3 开始:fd[0]==3, fd[1]==4
    printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);

    // Step 2:fork 子进程
    pid_t id = fork();

    if (id == 0)
    {
        // ============ 子进程:写端 ============
        close(pipefd[0]);  // 关闭读端

        char outbuff[1024];
        char *msg = "Hello, I'm child process";
        int cnt = 10;
        while (cnt)
        {
            snprintf(outbuff, sizeof(outbuff),
                     "c->f# %s %d %d", msg, cnt--, getpid());
            write(pipefd[1], outbuff, strlen(outbuff));
            //              不 +1:不把 \0 写入管道
            sleep(1);  // 写慢一点,模拟情况一
        }

        close(pipefd[1]);  // 写完关闭
        exit(0);
    }
    else
    {
        // ============ 父进程:读端 ============
        close(pipefd[1]);  // 关闭写端

        char inbuff[1024];
        while (1)
        {
            // sizeof(inbuff)-1:留一字节,因为 read 不管 \0
            ssize_t n = read(pipefd[0], inbuff, sizeof(inbuff) - 1);

            if (n > 0)
            {
                inbuff[n] = '\0';  // 手动补 \0
                printf("%s\n", inbuff);
            }
            else if (n == 0)
            {
                // 写端关闭,管道读空,read 返回 0
                printf("read EOF, child quit\n");
                break;
            }
            else
            {
                perror("read");
                break;
            }
        }

        int status;
        waitpid(id, &status, 0);
    }
    return 0;
}

运行结果:

shell 复制代码
root@ALiServer:Pipe# ./pipe_basic
pipefd[0]: 3, pipefd[1]: 4
c->f# Hello, I'm child process 10 183601
c->f# Hello, I'm child process 9 183601
...
c->f# Hello, I'm child process 1 183601
read EOF, child quit

知识点: 子进程 sleep(1) 写得慢,父进程 read 快------管道空时就阻塞等,这就是情况一(读端等待)snprintf 是 C 库函数,最多写 1023 有效字节自动补 \0read/write 是系统调用,只传递裸字节,不关心 \0


实验 2:管道写满(情况二:写端阻塞)

完整代码 pipe_full.c。子进程疯狂写、父进程不读,观察管道何时写满:

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

int main()
{
    int pipefd[2];
    pipe(pipefd);

    pid_t id = fork();

    if (id == 0)
    {
        // ============ 子进程:疯狂写 ============
        close(pipefd[0]);  // 关闭读端

        int size = 0;
        while (1)
        {
            char ch = 'A';
            write(pipefd[1], &ch, 1);  // 每次只写 1 字节
            size++;
            printf("%d\n", size);
        }
        // 永远到不了这里:父进程不读,管道写满后 write 阻塞
        close(pipefd[1]);
    }
    else
    {
        // ============ 父进程:不读! ============
        close(pipefd[1]);  // 关闭写端(不写)
        // 但读端也不读,只是等待子进程
        waitpid(id, NULL, 0);
    }
    return 0;
}
shell 复制代码
...
65534
65535
65536   ← 写入 65536 字节后子进程阻塞

知识点: 管道容量 4KB(PIPE_BUF=4096),但实际能写 65536 = 64KB = 16 × PIPE_BUF ,说明内核为管道分配的环形缓冲区是 64KB。写满后子进程阻塞,让出机会给读端------这就是情况二


实验 3:写端关闭(情况三:read 返回 0)

完整代码 pipe_write_close.c。子进程发一条消息后立即关闭写端:

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

int main()
{
    int pipefd[2];
    pipe(pipefd);

    pid_t id = fork();

    if (id == 0)
    {
        // ============ 子进程:写一条就关 ============
        close(pipefd[0]);

        char buf[1024];
        snprintf(buf, sizeof(buf), "c->f# Hello from %d", getpid());
        write(pipefd[1], buf, strlen(buf));

        close(pipefd[1]);  // 立即关闭写端
        printf("write endpoint quit!\n");
        exit(0);
    }
    else
    {
        // ============ 父进程:一直读 ============
        close(pipefd[1]);

        char inbuff[1024];
        while (1)
        {
            ssize_t n = read(pipefd[0], inbuff, sizeof(inbuff) - 1);
            printf("read returned: %ld\n", n);

            if (n > 0)
            {
                inbuff[n] = '\0';
                printf("data: %s\n", inbuff);
            }
            else if (n == 0)
            {
                printf("child closed write end, pipe EOF\n");
                break;
            }
            else
            {
                perror("read");
                break;
            }
            sleep(1);
        }

        waitpid(id, NULL, 0);
    }
    return 0;
}
shell 复制代码
write endpoint quit!
read returned: 28
data: c->f# Hello from 187308
read returned: 0      ← read 返回 0,管道读到结尾
child closed write end, pipe EOF

知识点: 写端关闭后,管道中剩余数据被读完后,再 read 返回 0(EOF)------这就是情况三


实验 4:读端关闭(情况四:SIGPIPE)

完整代码 pipe_read_close.c。父进程读一条后关闭读端,观察子进程被信号杀死:

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

int main()
{
    int pipefd[2];
    pipe(pipefd);

    pid_t id = fork();

    if (id == 0)
    {
        // ============ 子进程:一直写 ============
        close(pipefd[0]);

        char buf[1024];
        int cnt = 0;
        while (1)
        {
            snprintf(buf, sizeof(buf), "c->f# msg %d from %d", ++cnt, getpid());
            write(pipefd[1], buf, strlen(buf));
            sleep(1);
        }
        // 永远到不了这里:读端关闭后收到 SIGPIPE,进程被杀
        close(pipefd[1]);
    }
    else
    {
        // ============ 父进程:读一条就关读端 ============
        close(pipefd[1]);

        char inbuff[1024];
        ssize_t n = read(pipefd[0], inbuff, sizeof(inbuff) - 1);
        inbuff[n] = '\0';
        printf("received: %s\n", inbuff);

        close(pipefd[0]);  // 关闭读端!
        printf("parent closed read end\n");

        // 等待子进程退出,查看退出原因
        int status;
        pid_t wid = waitpid(id, &status, 0);
        if (wid > 0)
        {
            printf("child pid=%d, exit code=%d, signal=%d\n",
                   wid, WEXITSTATUS(status), WTERMSIG(status));
        }
    }
    return 0;
}
shell 复制代码
root@ALiServer:Pipe# ./pipe_read_close
received: c->f# msg 1 from 187307
parent closed read end
child pid=187307, exit code=0, signal=13   ← 子进程被 SIGPIPE(13) 杀死

知识点: 读端关闭后,写端继续写毫无意义,操作系统向写进程发送 SIGPIPE(信号 13) 将其终止------这就是情况四

2.1.7 管道读写规则速查表
场景 默认行为(阻塞) 非阻塞 (O_NONBLOCK)
管道空,读端读 read 阻塞等待 read 返回 -1,errno = EAGAIN
管道满,写端写 write 阻塞等待 write 返回 -1,errno = EAGAIN
写端关闭,读端读 read 返回 0(EOF) ---
读端关闭,写端写 收到 SIGPIPE(13),进程终止 ---

原子性: 单次写入 ≤ PIPE_BUF(4096) 时,Linux 保证写入不被打断。

2.1.8 管道特性总结

① 单工通信 --- 数据只能单向流动。匿名管道只能用于父子/兄弟进程(通过子进程继承父进程的内核资源实现),不能用于两个毫不相关的进程。

② 面向字节流 --- 管道不关心内容是文本、图像还是音频。写的次数和读的次数不是一一对应的 ------写 10 次,可能一次 read 就读完。与此相对的是数据报(写 3 次必须读 3 次,每次读到的是一个完整的"报文")。

理解字节流的关键:站在管道角度,它只是一段内核缓冲区。write(fd, "hello", 5) 就是把 5 个字节塞进缓冲区尾部,read(fd, buf, 1024) 就是从缓冲区头部取出最多 1024 个字节。管道没有"消息边界"的概念 ------它不知道也不关心这 5 个字节是独立的一条消息还是某个更大消息的一部分。如果发送方写 3 次 "hello",接收方一次 read 可能读到 "hellohellohello"。

③ 生命周期随进程 --- 文件由进程打开,进程退出时持有的 fd 自动 close。当所有引用管道的进程都退出(写端和读端的引用计数都归零),管道由系统自动回收。这与 System V IPC 的"随内核"生命周期截然不同------管道不用担心泄露,但也不能跨会话复用。

④ 内置同步与互斥 --- 管道的共享资源是内核文件缓冲区。如果子进程写了一半父进程就读,会造成数据不一致(并发问题)。因此内核自动处理:

  • 互斥: 同一时刻,读写操作不能同时进行(单次写入 ≤ PIPE_BUF 时保证原子性)
  • 同步: 读端和写端按"管道空/满"的状态协调谁阻塞、谁唤醒,保证顺序性

2.2 命名管道 (FIFO)

2.2.1 原理:用文件名替代继承

匿名管道的局限在于只能通过 fork 继承传递,无法用于两个毫不相关的进程。

命名管道解决了这个问题。核心思路不变------让不同进程的 struct file 指向同一份内核缓冲区。 区别在于:不需要继承,而是通过路径 + 文件名来定位同一个管道文件。

只要两个进程打开同一个路径下的管道文件,内核就会让它们的 struct file 指向同一块缓冲区。管道文件的 inode 由路径唯一确定,所以"看到同一个文件" = "看到同一份内核资源"。
#mermaid-svg-HfiMLtQEUIUvU3K7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HfiMLtQEUIUvU3K7 .error-icon{fill:#552222;}#mermaid-svg-HfiMLtQEUIUvU3K7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HfiMLtQEUIUvU3K7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .marker.cross{stroke:#333333;}#mermaid-svg-HfiMLtQEUIUvU3K7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HfiMLtQEUIUvU3K7 p{margin:0;}#mermaid-svg-HfiMLtQEUIUvU3K7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster-label text{fill:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster-label span{color:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster-label span p{background-color:transparent;}#mermaid-svg-HfiMLtQEUIUvU3K7 .label text,#mermaid-svg-HfiMLtQEUIUvU3K7 span{fill:#333;color:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .node rect,#mermaid-svg-HfiMLtQEUIUvU3K7 .node circle,#mermaid-svg-HfiMLtQEUIUvU3K7 .node ellipse,#mermaid-svg-HfiMLtQEUIUvU3K7 .node polygon,#mermaid-svg-HfiMLtQEUIUvU3K7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .rough-node .label text,#mermaid-svg-HfiMLtQEUIUvU3K7 .node .label text,#mermaid-svg-HfiMLtQEUIUvU3K7 .image-shape .label,#mermaid-svg-HfiMLtQEUIUvU3K7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-HfiMLtQEUIUvU3K7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .rough-node .label,#mermaid-svg-HfiMLtQEUIUvU3K7 .node .label,#mermaid-svg-HfiMLtQEUIUvU3K7 .image-shape .label,#mermaid-svg-HfiMLtQEUIUvU3K7 .icon-shape .label{text-align:center;}#mermaid-svg-HfiMLtQEUIUvU3K7 .node.clickable{cursor:pointer;}#mermaid-svg-HfiMLtQEUIUvU3K7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .arrowheadPath{fill:#333333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HfiMLtQEUIUvU3K7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HfiMLtQEUIUvU3K7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HfiMLtQEUIUvU3K7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster text{fill:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 .cluster span{color:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HfiMLtQEUIUvU3K7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HfiMLtQEUIUvU3K7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-HfiMLtQEUIUvU3K7 .icon-shape,#mermaid-svg-HfiMLtQEUIUvU3K7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HfiMLtQEUIUvU3K7 .icon-shape p,#mermaid-svg-HfiMLtQEUIUvU3K7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HfiMLtQEUIUvU3K7 .icon-shape .label rect,#mermaid-svg-HfiMLtQEUIUvU3K7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HfiMLtQEUIUvU3K7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HfiMLtQEUIUvU3K7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HfiMLtQEUIUvU3K7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内核
VFS 层
进程 B(读端)
进程 A(写端)
fd1
fd0
inode

(由路径 ./fifo 唯一确定)
struct file

(写模式)
struct file

(读模式)
内核文件缓冲区

命名管道和匿名管道的底层完全一样------两个 struct file 指向同一块缓冲区。唯一的区别是定位方式:命名管道通过文件系统路径找到同一个 inode,匿名管道通过 fork 继承 fd 表。
命名管道是一个真实存在 于文件系统中的特殊类型文件(ls -l 显示为 p),但它只存在于内存,数据不会刷新到磁盘。大小为 0,有独立的 inode。

深入理解:mkfifo 调用在内核中创建的是一个 S_IFIFO 类型的 inode,而不是 S_IFREG(普通文件)。当内核通过 open 解析到这个 inode 时,根据它的类型走到了管道的打开路径 而非磁盘文件的打开路径------分配的是纯内存缓冲区(pipe_buffer 环形队列),不会调用块设备驱动。管道文件只是提供了一个"路径 → inode → 内核缓冲区"的索引入口,文件系统中的文件名是钥匙,锁孔背后是内存。

做一个对比:如果用 open 打开一个普通文件,内核会查找 inode 对应的磁盘块号,准备好 block I/O;如果用 open 打开一个管道文件,内核看到 S_IFIFO 标志,直接分配内存环形缓冲区,不碰磁盘。

2.2.2 命令行演示
shell 复制代码
# 创建命名管道
$ mkfifo named_pipe
$ ls -li named_pipe
# prw-r--r-- 1 root root 0 ... named_pipe   ← 类型为 p,大小为 0

# 终端 1:写数据(会阻塞,等待读端打开)
$ echo "hello" > named_pipe

# 终端 2:读数据(两个互不相关的进程完成通信)
$ cat < named_pipe
hello

echo "hello" > named_pipe 会把 stdout 重定向到管道写端;cat < named_pipe 把管道读端重定向到 stdin。两个独立进程通过同一个路径名找到同一块内核缓冲区------这就是命名管道。

2.2.3 关键 API
c 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
// pathname:管道文件的路径
// mode:权限(如 0666)
// 成功返回 0,失败返回 -1

创建后打开方式和普通文件一样用 open(),区别只在打开规则:读打开会阻塞直到有写端,写打开会阻塞直到有读端。

2.2.4 实战演练:Server & Client 通信

用命名管道实现一个简单的 C/S 模型:Client 从标准输入读用户消息发给 Server,Server 收到后打印。
#mermaid-svg-HvebWtCG8vJHZutx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HvebWtCG8vJHZutx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HvebWtCG8vJHZutx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HvebWtCG8vJHZutx .error-icon{fill:#552222;}#mermaid-svg-HvebWtCG8vJHZutx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HvebWtCG8vJHZutx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HvebWtCG8vJHZutx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HvebWtCG8vJHZutx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HvebWtCG8vJHZutx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HvebWtCG8vJHZutx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HvebWtCG8vJHZutx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HvebWtCG8vJHZutx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HvebWtCG8vJHZutx .marker.cross{stroke:#333333;}#mermaid-svg-HvebWtCG8vJHZutx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HvebWtCG8vJHZutx p{margin:0;}#mermaid-svg-HvebWtCG8vJHZutx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HvebWtCG8vJHZutx .cluster-label text{fill:#333;}#mermaid-svg-HvebWtCG8vJHZutx .cluster-label span{color:#333;}#mermaid-svg-HvebWtCG8vJHZutx .cluster-label span p{background-color:transparent;}#mermaid-svg-HvebWtCG8vJHZutx .label text,#mermaid-svg-HvebWtCG8vJHZutx span{fill:#333;color:#333;}#mermaid-svg-HvebWtCG8vJHZutx .node rect,#mermaid-svg-HvebWtCG8vJHZutx .node circle,#mermaid-svg-HvebWtCG8vJHZutx .node ellipse,#mermaid-svg-HvebWtCG8vJHZutx .node polygon,#mermaid-svg-HvebWtCG8vJHZutx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HvebWtCG8vJHZutx .rough-node .label text,#mermaid-svg-HvebWtCG8vJHZutx .node .label text,#mermaid-svg-HvebWtCG8vJHZutx .image-shape .label,#mermaid-svg-HvebWtCG8vJHZutx .icon-shape .label{text-anchor:middle;}#mermaid-svg-HvebWtCG8vJHZutx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HvebWtCG8vJHZutx .rough-node .label,#mermaid-svg-HvebWtCG8vJHZutx .node .label,#mermaid-svg-HvebWtCG8vJHZutx .image-shape .label,#mermaid-svg-HvebWtCG8vJHZutx .icon-shape .label{text-align:center;}#mermaid-svg-HvebWtCG8vJHZutx .node.clickable{cursor:pointer;}#mermaid-svg-HvebWtCG8vJHZutx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HvebWtCG8vJHZutx .arrowheadPath{fill:#333333;}#mermaid-svg-HvebWtCG8vJHZutx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HvebWtCG8vJHZutx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HvebWtCG8vJHZutx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HvebWtCG8vJHZutx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HvebWtCG8vJHZutx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HvebWtCG8vJHZutx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HvebWtCG8vJHZutx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HvebWtCG8vJHZutx .cluster text{fill:#333;}#mermaid-svg-HvebWtCG8vJHZutx .cluster span{color:#333;}#mermaid-svg-HvebWtCG8vJHZutx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HvebWtCG8vJHZutx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HvebWtCG8vJHZutx rect.text{fill:none;stroke-width:0;}#mermaid-svg-HvebWtCG8vJHZutx .icon-shape,#mermaid-svg-HvebWtCG8vJHZutx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HvebWtCG8vJHZutx .icon-shape p,#mermaid-svg-HvebWtCG8vJHZutx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HvebWtCG8vJHZutx .icon-shape .label rect,#mermaid-svg-HvebWtCG8vJHZutx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HvebWtCG8vJHZutx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HvebWtCG8vJHZutx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HvebWtCG8vJHZutx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Server 进程
命名管道
Client 进程
write(fd)
read(fd)
stdin
Pipe::Send()
文件: ./fifo

(S_IFIFO)
内核缓冲区
Pipe::Recv()
stdout

Server 负责 Build(mkfifo)+ Delete(unlink),Client 只负责 Open。管道文件 ./fifo 是桥梁,数据在内存中流转,不落盘。

Pipe.hpp --- 管道类封装

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

const std::string gcommfile = "./fifo";

#define ForRead  1
#define ForWrite 2

class Pipe
{
public:
    Pipe(const std::string &commfile = gcommfile)
        : _commfile(commfile), _mode(0666), _fd(-1)
    {}

    // 创建命名管道(同匿名管道的 pipe)
    void Build()
    {
        if (IsExists())
            return;

        umask(0);
        int n = mkfifo(_commfile.c_str(), _mode);
        if (n < 0)
        {
            std::cerr << "mkfifo error: " << strerror(errno)
                      << " errno: " << errno << std::endl;
            exit(1);
        }
        std::cout << "mkfifo success" << std::endl;
    }

    // 删除管道文件
    void Delete()
    {
        if (!IsExists())
            return;
        unlink(_commfile.c_str());
        std::cout << "unlink success" << std::endl;
    }

    // 打开管道(同匿名管道的 open)
    void Open(int mode)
    {
        if (mode == ForRead)
            _fd = open(_commfile.c_str(), O_RDONLY);
        else if (mode == ForWrite)
            _fd = open(_commfile.c_str(), O_WRONLY);
        else {}

        if (_fd < 0)
        {
            std::cerr << "open error: " << strerror(errno)
                      << " errno: " << errno << std::endl;
            exit(2);
        }
        std::cout << "open file success" << std::endl;
    }

    void Send(const std::string &msgin)
    {
        ssize_t n = write(_fd, msgin.c_str(), msgin.size());
        (void)n;
    }

    int Recv(std::string *msgout)
    {
        char buffer[128];
        ssize_t n = read(_fd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            *msgout = buffer;
            return n;
        }
        else if (n == 0)
            return 0;
        else
            return -1;
    }

    ~Pipe() {}

private:
    // 不能用 open 判断管道是否存在(打开会阻塞),用 stat
    bool IsExists()
    {
        struct stat st;
        int n = stat(_commfile.c_str(), &st);
        if (n == 0)
            return true;
        else
        {
            errno = 0;
            return false;
        }
    }

private:
    std::string _commfile;
    mode_t _mode;
    int _fd;
};

Server.cc --- 服务端(读端)

cpp 复制代码
#include "Pipe.hpp"

int main()
{
    // 1. 创建命名管道
    // 2. 以读方式打开
    Pipe *pp = new Pipe();
    pp->Build();
    pp->Open(ForRead);

    std::string msg;
    while (true)
    {
        int n = pp->Recv(&msg);
        if (n > 0)
            std::cout << "Client Say# " << msg << std::endl;
        else
            break;
    }

    pp->Delete();  // Server 退出前清理管道文件
    delete pp;
    return 0;
}

Client.cc --- 客户端(写端)

cpp 复制代码
#include "Pipe.hpp"

int main()
{
    // 只打开管道(由 Server 负责创建和删除)
    Pipe *pp = new Pipe();
    pp->Open(ForWrite);

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string msg;
        std::getline(std::cin, msg);
        pp->Send(msg);
    }

    delete pp;
    return 0;
}

Makefile

makefile 复制代码
.PHONY:all
all:Client Server

Server:Server.cc
	g++ -o $@ $^ -std=c++11
Client:Client.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f Server Client

运行: 先启动 Server(阻塞等待 Client 连接),再启动 Client:

shell 复制代码
# 终端 1
$ ./Server
mkfifo success
open file success      ← 阻塞直到 Client 打开写端

# 终端 2
$ ./Client
open file success
Please Enter@ hello
Please Enter@ world

# 终端 1 输出
Client Say# hello
Client Say# world
2.2.5 设计要点

Build vs Open 的分离

匿名管道里 pipe() 同时完成"创建"和"打开",命名管道则分两步:mkfifo 创建文件 + open 打开文件。这是因为命名管道的创建者和使用者可能不是同一个进程------Server 负责 Build + Delete,Client 只负责 Open。

IsExists 为什么不能用 open

命名管道的打开规则是:读打开会阻塞直到有写端打开,写打开会阻塞直到有读端打开。如果 IsExists 里用 open 去探测文件是否存在,会当场阻塞。stat 只读文件的 inode 元信息,不会触发管道打开逻辑。

③ 连续 Build 两次会怎样?

第一次 mkfifo 成功,第二次调用时文件已存在,mkfifo 返回 -1,errno = EEXIST(17)。所以 Build 开头先调用 IsExists(),已存在就直接返回。

④ 为什么数据不落盘?

命名管道虽然是文件系统中的一个文件,但它被标记为 S_IFIFO 类型。内核在 open 这类文件时,不会创建磁盘缓存,而是直接分配一块内存缓冲区。 所有 read/write 操作直接在这块内存上完成。管道文件只是提供了一个"路径 → inode → 内核缓冲区"的索引入口。

2.2.6 命名管道的打开规则
操作 默认行为(阻塞) 非阻塞 (O_NONBLOCK)
读打开 阻塞直到有进程以写方式打开 立即返回成功
写打开 阻塞直到有进程以读方式打开 立即返回失败,errno = ENXIO

一旦两端都打开完毕,命名管道和匿名管道具有完全相同的读写语义。

2.2.7 匿名管道 vs 命名管道 对比
对比维度 匿名管道 (pipe) 命名管道 (FIFO)
创建方式 pipe(fd) mkfifo + open
定位方式 fork 继承 fd 表 路径名 → inode
适用范围 有亲缘关系的进程 任意进程
文件系统可见 不可见 可见(ls -l 显示为 p
生命周期 随进程 随内核(需手动 unlink
底层机制 同一内核缓冲区,相同读写规则 同一内核缓冲区,相同读写规则

三、 实战:进程池 (Process Pool)

3.1 设计思想

管道是单工的,如果想用管道控制多个子进程,每个子进程需要一条独立的管道。父进程通过管道向子进程发送任务编号,子进程收到后执行对应的任务。

核心模块:

模块 文件 职责
Channel Channel.hpp 封装一个管道写端 + 对应子进程的 PID,父进程用它向某个子进程发任务
ProcessPool ProcessPool.hpp 管理 N 个子进程 + N 个 Channel,负责初始化、派发任务、回收
Task Task.hpp 定义任务列表 + 子进程执行任务的 Worker() 函数
Main Main.cc 入口,指定子进程数量,启动进程池

通信方式: 每个子进程通过 dup2(pipefd[0], 0) 把管道的读端重定向到自己的标准输入,之后直接 read(0, &cmd, sizeof(cmd)) 读任务编号------把管道当 stdin 用,简洁统一。
#mermaid-svg-W9lKMSmXjrfOaVPX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-W9lKMSmXjrfOaVPX .error-icon{fill:#552222;}#mermaid-svg-W9lKMSmXjrfOaVPX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W9lKMSmXjrfOaVPX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W9lKMSmXjrfOaVPX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W9lKMSmXjrfOaVPX .marker.cross{stroke:#333333;}#mermaid-svg-W9lKMSmXjrfOaVPX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W9lKMSmXjrfOaVPX p{margin:0;}#mermaid-svg-W9lKMSmXjrfOaVPX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster-label text{fill:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster-label span{color:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster-label span p{background-color:transparent;}#mermaid-svg-W9lKMSmXjrfOaVPX .label text,#mermaid-svg-W9lKMSmXjrfOaVPX span{fill:#333;color:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX .node rect,#mermaid-svg-W9lKMSmXjrfOaVPX .node circle,#mermaid-svg-W9lKMSmXjrfOaVPX .node ellipse,#mermaid-svg-W9lKMSmXjrfOaVPX .node polygon,#mermaid-svg-W9lKMSmXjrfOaVPX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-W9lKMSmXjrfOaVPX .rough-node .label text,#mermaid-svg-W9lKMSmXjrfOaVPX .node .label text,#mermaid-svg-W9lKMSmXjrfOaVPX .image-shape .label,#mermaid-svg-W9lKMSmXjrfOaVPX .icon-shape .label{text-anchor:middle;}#mermaid-svg-W9lKMSmXjrfOaVPX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-W9lKMSmXjrfOaVPX .rough-node .label,#mermaid-svg-W9lKMSmXjrfOaVPX .node .label,#mermaid-svg-W9lKMSmXjrfOaVPX .image-shape .label,#mermaid-svg-W9lKMSmXjrfOaVPX .icon-shape .label{text-align:center;}#mermaid-svg-W9lKMSmXjrfOaVPX .node.clickable{cursor:pointer;}#mermaid-svg-W9lKMSmXjrfOaVPX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-W9lKMSmXjrfOaVPX .arrowheadPath{fill:#333333;}#mermaid-svg-W9lKMSmXjrfOaVPX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-W9lKMSmXjrfOaVPX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-W9lKMSmXjrfOaVPX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W9lKMSmXjrfOaVPX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-W9lKMSmXjrfOaVPX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W9lKMSmXjrfOaVPX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster text{fill:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX .cluster span{color:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-W9lKMSmXjrfOaVPX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-W9lKMSmXjrfOaVPX rect.text{fill:none;stroke-width:0;}#mermaid-svg-W9lKMSmXjrfOaVPX .icon-shape,#mermaid-svg-W9lKMSmXjrfOaVPX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W9lKMSmXjrfOaVPX .icon-shape p,#mermaid-svg-W9lKMSmXjrfOaVPX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-W9lKMSmXjrfOaVPX .icon-shape .label rect,#mermaid-svg-W9lKMSmXjrfOaVPX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W9lKMSmXjrfOaVPX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-W9lKMSmXjrfOaVPX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-W9lKMSmXjrfOaVPX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 子进程 (Workers)
内核(管道)
父进程 (Master)
SelectTask()
SelectTask()
SelectTask()
write(4, task)
read(3)
write(5, task)
read(3)
write(6, task)
read(3)
TaskManager

随机选任务
Channel0

wfd=4
Channel1

wfd=5
Channel2

wfd=6
pipe0 缓冲区
pipe1 缓冲区
pipe2 缓冲区
child1

stdin=3 ← pipe0 读端

read(0) 等任务
child2

stdin=3 ← pipe1 读端

read(0) 等任务
child3

stdin=3 ← pipe2 读端

read(0) 等任务

每个子进程对应一条独立的管道:父进程通过 Channel 持有写端 fd 写入任务编号,数据流经内核管道缓冲区,子进程通过 dup2 将读端重定向到 stdin 后 read(0) 读取。三条管道互不干扰。

3.2 完整代码

Channel.hpp --- 信道封装

cpp 复制代码
#ifndef __CHANNEL_HPP__
#define __CHANNEL_HPP__

#include <iostream>
#include <string>
#include <unistd.h>

class Channel
{
public:
    Channel(int wfd, pid_t who)
        : _wfd(wfd), _who(who)
    {
        _name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
    }

    std::string Name() { return _name; }

    // 父进程通过信道向子进程发送一个任务编号(int)
    void Send(int cmd)
    {
        ::write(_wfd, &cmd, sizeof(cmd));
    }

    void Close() { ::close(_wfd); }

    pid_t Id()  { return _who; }
    int   wFd() { return _wfd; }

    ~Channel() {}

private:
    int         _wfd;   // 管道写端(父进程持有)
    std::string _name;  // "Channel-wfd-pid"
    pid_t       _who;   // 对应子进程的 PID
};

#endif

Task.hpp --- 任务管理 + Worker 入口

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <functional>
#include <ctime>
#include <unistd.h>

using task_t = std::function<void()>;

class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
        tasks.push_back([]() {
            std::cout << "sub process[" << getpid()
                      << "] 执行访问数据库的任务\n" << std::endl;
        });
        tasks.push_back([]() {
            std::cout << "sub process[" << getpid()
                      << "] 执行 URL 解析\n" << std::endl;
        });
        tasks.push_back([]() {
            std::cout << "sub process[" << getpid()
                      << "] 执行加密任务\n" << std::endl;
        });
        tasks.push_back([]() {
            std::cout << "sub process[" << getpid()
                      << "] 执行数据持久化任务\n" << std::endl;
        });
    }

    int SelectTask()
    {
        return rand() % tasks.size();
    }

    void Execute(unsigned long number)
    {
        if (number < tasks.size())
            tasks[number]();
    }

    ~TaskManager() {}

private:
    std::vector<task_t> tasks;
};

// 全局任务管理器
TaskManager tm;

// 子进程入口:从 stdin 读任务编号并执行
// stdin 已经被 dup2 重定向到管道读端,所以 read(0, ...) 读的就是父进程发来的命令
void Worker()
{
    while (true)
    {
        int cmd = 0;
        int n = ::read(0, &cmd, sizeof(cmd));
        if (n == sizeof(cmd))
        {
            tm.Execute(cmd);
        }
        else if (n == 0)
        {
            // 管道写端关闭(父进程发完 quit),子进程退出
            std::cout << "pid: " << getpid() << " quit..." << std::endl;
            break;
        }
    }
}

ProcessPool.hpp --- 进程池核心

cpp 复制代码
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Task.hpp"
#include "Channel.hpp"

using work_t = std::function<void()>;

enum
{
    OK = 0,
    UsageError,
    PipeError,
    ForkError
};

class ProcessPool
{
public:
    ProcessPool(int n, work_t w)
        : processnum(n), work(w)
    {}

    // 初始化进程池:为每个子进程创建独立管道 + fork
    int InitProcessPool()
    {
        for (int i = 0; i < processnum; i++)
        {
            // 1. 创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
                return PipeError;

            // 2. fork 子进程
            pid_t id = fork();
            if (id < 0)
                return ForkError;

            if (id == 0)
            {
                // ========== 子进程 ==========
                // 关闭继承自父进程的所有历史 wfd(不属于自己)
                std::cout << getpid() << ", child close history fd: ";
                for (auto &c : channels)
                {
                    std::cout << c.wFd() << " ";
                    c.Close();
                }
                std::cout << " over" << std::endl;

                // 关闭写端,读端重定向到 stdin
                ::close(pipefd[1]);
                std::cout << "debug: " << pipefd[0] << std::endl;
                dup2(pipefd[0], 0);

                // 执行工作函数(Worker 中 read(0, ...) 接收任务)
                work();
                ::exit(0);
            }

            // ========== 父进程 ==========
            ::close(pipefd[0]);  // 父进程只写,关闭读端
            channels.emplace_back(pipefd[1], id);
        }
        return OK;
    }

    // 轮询派发任务
    void DispatchTask()
    {
        int who = 0;
        int num = 20;  // 共派发 20 个任务
        while (num--)
        {
            int task = tm.SelectTask();          // 随机选一个任务
            Channel &curr = channels[who++];      // 轮询选一个子进程
            who %= channels.size();

            std::cout << "######################" << std::endl;
            std::cout << "send " << task << " to " << curr.Name()
                      << ", 任务还剩: " << num << std::endl;
            std::cout << "######################" << std::endl;

            curr.Send(task);
            sleep(1);
        }
    }

    // 回收所有子进程
    void CleanProcessPool()
    {
        for (auto &c : channels)
        {
            c.Close();  // 关闭写端 → 子进程 read 返回 0 → Worker 退出
            pid_t rid = ::waitpid(c.Id(), nullptr, 0);
            if (rid > 0)
                std::cout << "child " << rid << " wait ... success" << std::endl;
        }
    }

    void DebugPrint()
    {
        for (auto &c : channels)
            std::cout << c.Name() << std::endl;
    }

private:
    std::vector<Channel> channels;  // 每个子进程对应一个 Channel
    int processnum;                 // 子进程数量
    work_t work;                    // 子进程入口函数
};

#endif

Main.cc --- 入口

cpp 复制代码
#include "ProcessPool.hpp"
#include "Task.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " process-num" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return UsageError;
    }

    int num = std::stoi(argv[1]);
    ProcessPool *pp = new ProcessPool(num, Worker);

    // 1. 初始化进程池(创建管道 + fork)
    pp->InitProcessPool();

    // 2. 派发任务
    pp->DispatchTask();

    // 3. 清理进程池(关闭管道 + waitpid)
    pp->CleanProcessPool();

    delete pp;
    return 0;
}

Makefile

makefile 复制代码
BIN=processpool
CC=g++
FLAGS=-c -Wall -std=c++11
LDFLAGS=-o
SRC=$(wildcard *.cc)
OBJ=$(SRC:.cc=.o)

$(BIN):$(OBJ)
	$(CC) $(LDFLAGS) $@ $^
%.o:%.cc
	$(CC) $(FLAGS) $<

.PHONY:clean
clean:
	rm -f $(BIN) $(OBJ)

3.3 关键设计要点

① Channel 封装的是什么?

父进程向管道写端发送的是一个 int(任务编号),Channel 同时记录了写端 fd 和子进程 PID,这样父进程可以精准地"给某个子进程发任务"。

② 子进程为什么要关闭历史 wfd?

子进程 fork 后会继承父进程的整个 fd 表,包括之前给其他子进程创建的管道写端。必须关闭这些不属于自己的 fd,否则管道不会被真正关闭,其他子进程的 read 永远不会返回 0。

dup2(pipefd[0], 0) 的意义

把管道读端重定向为标准输入,之后子进程只需 read(0, ...) 就能拿到父进程发来的任务编号------不用关心管道具体的 fd 是什么,就像在等 stdin 输入一样。

④ CleanProcessPool 的关闭顺序 + 子进程必须关闭历史 fd(死锁 bug 分析)

先看 CleanProcessPool,它逐个关逐个收:

cpp 复制代码
void CleanProcessPool()
{
    for (auto &c : channels)
    {
        c.Close();                                   // ① 父进程关闭对某个子进程的写端
        pid_t rid = ::waitpid(c.Id(), nullptr, 0);   // ② 等待该子进程退出
    }
}

这个逻辑建立在一个前提上:① 关写端 → 子进程 read 返回 0 → 子进程退出 → ② waitpid 成功 。一旦 ① 关了写端但子进程的 read 没返回 0,waitpid 就永久阻塞。

这个前提被破坏的根源不在 CleanProcessPool 本身,而在 InitProcessPool

bug 场景复现

假设创建 2 个子进程。去掉子进程中关闭历史 wfd 的代码,看会发生什么。

第一轮循环(创建 child1):

复制代码
父进程: pipe1 创建完毕, fd[0]=3(读), fd[1]=4(写)
        fork → child1 继承父进程全部 fd(目前只有 pipe1 的读写端)
        父进程 close(3), channels = [Channel(4, child1)]
child1: 继承 fd[0]=3(读), fd[1]=4(写)
        close(4), dup2(3, 0), Worker() → read(0, ...) 阻塞等待

此时 pipe1 写端引用情况:

持有者 写端 fd 状态
父进程 4(在 channels0 中) 打开
child1 已关闭 ---
child2 还未创建 ---

第二轮循环(创建 child2):

复制代码
父进程: pipe2 创建完毕, fd[0]=5(读), fd[1]=6(写)
        注意:父进程 fd 表中还有 pipe1 的写端 fd=4 在 channels[0] 里!
        fork → child2 继承父进程全部 fd(包括 pipe1 写端 fd=4、pipe2 读写端 fd=5,6)
        父进程 close(5), channels = [Channel(4, child1), Channel(6, child2)]
child2: 继承 fd=4(pipe1写端!), fd=5(pipe2读端), fd=6(pipe2写端)
        close(6), dup2(5, 0), Worker() → read(0, ...) 阻塞等待

关键问题出现了。 如果 child2 不关闭继承来的 fd=4(pipe1 的写端),pipe1 写端引用情况变成:

pipe1 写端 fd=4 的持有者

持有者 下标 持有者 fd 表 状态
父进程 4 channels0.wFd 打开
child1 4 已关闭 ---
child2 4 继承来的! 打开

#mermaid-svg-AC7WVVnHQpsgLu8E{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AC7WVVnHQpsgLu8E .error-icon{fill:#552222;}#mermaid-svg-AC7WVVnHQpsgLu8E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AC7WVVnHQpsgLu8E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AC7WVVnHQpsgLu8E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AC7WVVnHQpsgLu8E .marker.cross{stroke:#333333;}#mermaid-svg-AC7WVVnHQpsgLu8E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AC7WVVnHQpsgLu8E p{margin:0;}#mermaid-svg-AC7WVVnHQpsgLu8E .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster-label text{fill:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster-label span{color:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster-label span p{background-color:transparent;}#mermaid-svg-AC7WVVnHQpsgLu8E .label text,#mermaid-svg-AC7WVVnHQpsgLu8E span{fill:#333;color:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E .node rect,#mermaid-svg-AC7WVVnHQpsgLu8E .node circle,#mermaid-svg-AC7WVVnHQpsgLu8E .node ellipse,#mermaid-svg-AC7WVVnHQpsgLu8E .node polygon,#mermaid-svg-AC7WVVnHQpsgLu8E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AC7WVVnHQpsgLu8E .rough-node .label text,#mermaid-svg-AC7WVVnHQpsgLu8E .node .label text,#mermaid-svg-AC7WVVnHQpsgLu8E .image-shape .label,#mermaid-svg-AC7WVVnHQpsgLu8E .icon-shape .label{text-anchor:middle;}#mermaid-svg-AC7WVVnHQpsgLu8E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AC7WVVnHQpsgLu8E .rough-node .label,#mermaid-svg-AC7WVVnHQpsgLu8E .node .label,#mermaid-svg-AC7WVVnHQpsgLu8E .image-shape .label,#mermaid-svg-AC7WVVnHQpsgLu8E .icon-shape .label{text-align:center;}#mermaid-svg-AC7WVVnHQpsgLu8E .node.clickable{cursor:pointer;}#mermaid-svg-AC7WVVnHQpsgLu8E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AC7WVVnHQpsgLu8E .arrowheadPath{fill:#333333;}#mermaid-svg-AC7WVVnHQpsgLu8E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AC7WVVnHQpsgLu8E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AC7WVVnHQpsgLu8E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AC7WVVnHQpsgLu8E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AC7WVVnHQpsgLu8E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AC7WVVnHQpsgLu8E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster text{fill:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E .cluster span{color:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AC7WVVnHQpsgLu8E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AC7WVVnHQpsgLu8E rect.text{fill:none;stroke-width:0;}#mermaid-svg-AC7WVVnHQpsgLu8E .icon-shape,#mermaid-svg-AC7WVVnHQpsgLu8E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AC7WVVnHQpsgLu8E .icon-shape p,#mermaid-svg-AC7WVVnHQpsgLu8E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AC7WVVnHQpsgLu8E .icon-shape .label rect,#mermaid-svg-AC7WVVnHQpsgLu8E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AC7WVVnHQpsgLu8E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AC7WVVnHQpsgLu8E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AC7WVVnHQpsgLu8E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Close()
未关闭
计数 ≠ 0

管道不关闭
父 fd4(channels0
pipe1 写端

引用计数
child2 fd4(继承)
child1 read(0)

永不返回 0

现在进入 CleanProcessPool

cpp 复制代码
// 第一轮:处理 child1
channels[0].Close();  // 父进程关闭 fd=4 → 引用计数 -1,但 child2 还持有,不为 0
                      // pipe1 不会真正关闭,child1 的 read(0) 不会返回 0
waitpid(child1, ...); // 永久阻塞!child1 永远不会退出

child1 在等管道 EOF,但 child2 手里还拿着 pipe1 的写端不放。 死锁。

CleanProcessPool 连第二轮都走不到------它卡在第一轮的 waitpid 上了。

修复

子进程 fork 后必须关闭所有不属于自己的、从父进程继承来的历史写端(即 channels 里已有的 wfd):

cpp 复制代码
if (id == 0)
{
    // 关闭所有历史 wfd!
    for (auto &c : channels)
    {
        c.Close();  // child2 关闭 pipe1 写端
    }
    // 现在 pipe1 写端只有父进程持有
    // 将来父进程 Close 后 pipe1 引用计数归零,child1 的 read 返回 0
    ...
}

关闭之后,pipe1 写端只有父进程一个人持有。父进程 Close → 引用计数归零 → child1 收到 EOF → 退出 → waitpid 成功。整个信号闭环才能正确闭合。

一句话总结: 逐个关逐个收的前提是"管道写端引用计数在父进程 Close 时归零"。子进程如果不关历史 wfd,引用计数永远 > 1,父进程关了自己那份也没用------先关哪边都不好使,死锁是必然的。

⑤ 进阶陷阱:栈对象生命周期与进程"殉情"现象

在编写进程池时,很多人会遇到一个奇怪的 Bug:如果将 ProcessPool 对象创建在栈上,子进程似乎"进不去"逻辑,什么都不打印。

cpp 复制代码
// ❌ 错误示范:栈对象
int main() {
    ProcessPool pp(num, Worker); 
    pp.InitProcessPool();
    return 0; // main 函数瞬间结束,pp 触发析构
}

现象分析:

  1. 父进程(Master) :执行完 InitProcessPool 建立完连接后,瞬间执行到 return 0
  2. RAII 机制生效 :作为栈对象,pp 的析构函数被调用,其内部持有的 _channels 销毁。
  3. 写端全关:父进程持有的所有管道写端 FD 被系统回收并关闭。
  4. 子进程(Worker) :刚 fork 出来正准备 read(0, ...),结果因为父进程关闭了写端,read 直接读到 EOF 返回 0。子进程执行 exit(0)

为什么 new 出来的对象"没问题"?

使用 ProcessPool *pp = new ... 且不写 delete 时,对象在堆上不会自动析构,FD 依然保持打开状态。这虽然导致了内存泄漏,但"歪打正着"地给了子进程运行的时间。

深度反思:

在 Master-Worker 架构中,Master 进程(及相关资源对象)的生命周期必须覆盖整个通信过程。如果 Master 提前结束,所有的 Worker 都会因为读取到管道 EOF 而被迫"殉情"退出。

修复方案:

要么在 main 结尾增加任务派发逻辑或 while(true) 保活,要么确保 Master 对象在整个程序运行期间有效。


四、 System V 共享内存 (Shared Memory)

管道和命名管道都是基于文件的 IPC,本质上在复用内核已有的文件子系统代码。这带来了两个限制:(1) 数据必须通过内核缓冲区中转,存在用户态 ↔ 内核态的拷贝开销;(2) 受文件读写语义约束,只能以字节流形式顺序存取。

System V IPC 由此诞生------它不再走文件那一套,而是直接操作物理内存,把内存块同时映射到多个进程的虚拟地址空间。这样进程 A 写入的数据,进程 B 在映射后的用户空间地址上直接就能读到,没有任何内核拷贝

4.1 原理:打破内核拷贝的壁垒

每一个进程都有自己的虚拟地址空间。共享内存的原理是:在物理内存中开辟一块内存块,通过页表映射,将其挂接到不同进程虚拟地址空间的"共享区"中。

这个"共享区"位于进程虚拟地址空间的堆栈之间 的区域(memory mapping segment),和 mmap 映射文件、动态库加载的区域是同一片。所以共享内存在地址空间布局上,和动态链接库是邻居------两者本质都是页表映射,只是映射的目标不同(库映射到磁盘文件,共享内存映射到匿名物理页)。
#mermaid-svg-wn6wWZCiVlotfb8Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wn6wWZCiVlotfb8Z .error-icon{fill:#552222;}#mermaid-svg-wn6wWZCiVlotfb8Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wn6wWZCiVlotfb8Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wn6wWZCiVlotfb8Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wn6wWZCiVlotfb8Z .marker.cross{stroke:#333333;}#mermaid-svg-wn6wWZCiVlotfb8Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wn6wWZCiVlotfb8Z p{margin:0;}#mermaid-svg-wn6wWZCiVlotfb8Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster-label text{fill:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster-label span{color:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster-label span p{background-color:transparent;}#mermaid-svg-wn6wWZCiVlotfb8Z .label text,#mermaid-svg-wn6wWZCiVlotfb8Z span{fill:#333;color:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z .node rect,#mermaid-svg-wn6wWZCiVlotfb8Z .node circle,#mermaid-svg-wn6wWZCiVlotfb8Z .node ellipse,#mermaid-svg-wn6wWZCiVlotfb8Z .node polygon,#mermaid-svg-wn6wWZCiVlotfb8Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wn6wWZCiVlotfb8Z .rough-node .label text,#mermaid-svg-wn6wWZCiVlotfb8Z .node .label text,#mermaid-svg-wn6wWZCiVlotfb8Z .image-shape .label,#mermaid-svg-wn6wWZCiVlotfb8Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-wn6wWZCiVlotfb8Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wn6wWZCiVlotfb8Z .rough-node .label,#mermaid-svg-wn6wWZCiVlotfb8Z .node .label,#mermaid-svg-wn6wWZCiVlotfb8Z .image-shape .label,#mermaid-svg-wn6wWZCiVlotfb8Z .icon-shape .label{text-align:center;}#mermaid-svg-wn6wWZCiVlotfb8Z .node.clickable{cursor:pointer;}#mermaid-svg-wn6wWZCiVlotfb8Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wn6wWZCiVlotfb8Z .arrowheadPath{fill:#333333;}#mermaid-svg-wn6wWZCiVlotfb8Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wn6wWZCiVlotfb8Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wn6wWZCiVlotfb8Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wn6wWZCiVlotfb8Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wn6wWZCiVlotfb8Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wn6wWZCiVlotfb8Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster text{fill:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z .cluster span{color:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wn6wWZCiVlotfb8Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wn6wWZCiVlotfb8Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-wn6wWZCiVlotfb8Z .icon-shape,#mermaid-svg-wn6wWZCiVlotfb8Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wn6wWZCiVlotfb8Z .icon-shape p,#mermaid-svg-wn6wWZCiVlotfb8Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wn6wWZCiVlotfb8Z .icon-shape .label rect,#mermaid-svg-wn6wWZCiVlotfb8Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wn6wWZCiVlotfb8Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wn6wWZCiVlotfb8Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wn6wWZCiVlotfb8Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-wn6wWZCiVlotfb8Z .task>*{fill:#ffeeee!important;stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .task span{fill:#ffeeee!important;stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .vma>*{stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .vma span{stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .pg>*{stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .pg span{stroke:red!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .phys>*{stroke:#0044dd!important;fill:#eef5ff!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .phys span{stroke:#0044dd!important;fill:#eef5ff!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .shareBlock>*{fill:#e6d9ff!important;stroke:purple!important;}#mermaid-svg-wn6wWZCiVlotfb8Z .shareBlock span{fill:#e6d9ff!important;stroke:purple!important;} 物理内存
挂载
挂载
虚拟地址空间 B
虚拟共享段
虚拟地址空间 A
虚拟共享段
task_struct A
页表A
共享物理块
task_struct B
页表B

理解: 共享内存可以看作是一个"简化版"的动态库映射。由于数据直接存储在物理内存中,进程 A 写入,进程 B 立刻可见,无需经过内核拷贝。


4.2 核心设计:为什么 key 由用户指定?

这是 System V IPC 的一个设计哲学。为了让两个毫不相关的进程看到同一份资源,必须有一个"共同的约定"。

先有鸡还是先有蛋的悖论:

如果 key 由操作系统自动生成并返回给创建者(进程 A),进程 A 如何把这个 key 传给进程 B?

  • 如果要传 key,进程 A 和 B 必须先能通信。
  • 但进程 A 和 B 正是因为不能通信,才需要创建共享内存。

解决方案:

用户(程序员)预先约定好一个路径名和项目 ID(pathname + proj_id),通过 ftok 函数将其转化成一个唯一的 key

  • 进程 A:用约定好的参数 ftok 生成 key,创建共享内存。
  • 进程 B:用相同 的参数 ftok 生成相同的 key,获取共享内存。
  • 本质: 这种方式把"寻址"逻辑从内核移交到了用户共识中,巧妙地避开了通信前提。

4.3 key vs shmid:fd 与 inode 的重演

在 System V IPC 中,有两个标识符,它们的角色和文件系统中的 fd / inode 完全对等:

  • key :内核层面的唯一标识。用于在系统中寻找、定位共享内存(类似于文件的 inode)。
  • shmid :用户层面的标识符。由 shmget 返回,后续所有对内存的操作(挂接、去关联、删除)都使用这个 id(类似于文件的 fd)。

用一个具体流程来理解:

复制代码
ftok("/tmp", 0x66) → key = 0x6603fa03           // 路径+项目ID → 唯一键值
shmget(key, 128, IPC_CREAT|0666) → shmid = 0     // 键值 → 整数句柄
shmat(shmid, NULL, 0) → addr = 0x7f1234560000    // 句柄 → 虚拟地址
...
shmdt(addr)                                      // 去关联(不删除)
shmctl(shmid, IPC_RMID, NULL)                    // 通过句柄删除

操作提示: ipcs -m 查看时能看到 key 和 shmid,ipcrm -m <shmid> 删除时只能用 shmid 不能用 key。


4.4 关键 API

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

// 生成 key
key_t ftok(const char *pathname, int proj_id);

// 创建/获取共享内存
int shmget(key_t key, size_t size, int shmflg);
// size:大小,必须是 4096 的整数倍(传 4097 会向上取整)
// shmflg:IPC_CREAT(获取) | IPC_EXCL(必须新建) | 权限位(如 0666)
// 返回值:成功返回 shmid,失败返回 -1

// 挂接(映射到进程虚拟地址空间)
void *shmat(int shmid, const void *shmaddr, int shmflg);
// shmaddr:传 NULL 让内核自动选择地址
// 返回值:共享内存段在进程中的起始虚拟地址,失败返回 (void*)-1

// 去关联
int shmdt(const void *shmaddr);

// 控制(删除/获取属性)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd=IPC_RMID:删除共享内存
// cmd=IPC_STAT:获取共享内存的属性,将内核中的数据结构拷贝到 buf 指向的结构体中

4.5 实战封装:Shm 类

Shm.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <cstdio>
#include <unistd.h>

const int gsize = 128; // 建议设为 4096 的整数倍

// 用户指明的 pathname + proj_id,等价于命名管道的文件路径
#define PATHNAME "/tmp"
#define PROJ_ID 0x66

class Shm {
public:
    Shm() : _size(gsize), _shmid(-1), _start_addr(nullptr) {}

    // 创建全新的共享内存(Server 用)
    void Create() { GetHelper(IPC_CREAT | IPC_EXCL | 0666); }

    // 获取已存在的共享内存(Client 用)
    void Get() { GetHelper(IPC_CREAT); }

    // 将共享内存映射到进程地址空间
    void Attach() {
        _start_addr = shmat(_shmid, nullptr, 0);
        if ((long long int)_start_addr == -1) exit(3);
    }

    // 去关联
    void Detach() { if (_start_addr) shmdt(_start_addr); }

    // 删除共享内存(System V IPC 资源随内核,需显式删除)
    void Delete() { shmctl(_shmid, IPC_RMID, nullptr); }

    // 获取并打印共享内存属性
    void PrintAttr() {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if (n < 0) {
            perror("shmctl");
            exit(4);
        }
        printf("key: 0x%x\n", ds.shm_perm.__key);
        printf("shm_nattch: %ld\n", ds.shm_nattch);
        printf("shm_segsz: 0x%lx\n", ds.shm_segsz);
    }

    void* Addr() { return _start_addr; }
    int Size() { return _size; }

    ~Shm() {}

private:
    key_t GetKey() { return ftok(PATHNAME, PROJ_ID); }

    void GetHelper(int shmflg) {
        // 1. 构建键值(pathname + proj_id → key)
        key_t k = GetKey();
        if (k < 0) { std::cerr << "GetKey error" << std::endl; exit(1); }

        // 2. 创建/获取共享内存
        _shmid = shmget(k, _size, shmflg);
        if (_shmid < 0) { perror("shmget"); exit(2); }
        printf("k=0x%x shmid=%d\n", k, _shmid);
    }

private:
    int _size;
    int _shmid;
    void *_start_addr;
};

Server.cc --- 读端

cpp 复制代码
#include "Shm.hpp"

int main() {
    // 1. 创建 + 挂接
    Shm *shm = new Shm();
    shm->Create();
    shm->Attach();

    // 2. 读取共享内存
    char* shm_start = (char*)shm->Addr();
    int size = shm->Size();
    shm->PrintAttr(); // 打印刚创建的共享内存属性
    while (true) {
        for (int i = 0; i < size; i++)
            std::cout << shm_start[i] << " ";
        std::cout << std::endl;
        sleep(1);
    }

    // 3. 去关联 + 删除
    shm->Detach();
    shm->Delete();
    delete shm;
    return 0;
}

Client.cc --- 写端

cpp 复制代码
#include "Shm.hpp"

int main() {
    // 1. 获取已存在的共享内存 + 挂接
    Shm *shm = new Shm();
    shm->Get();
    shm->Attach();

    // 2. 向共享内存写入
    char* shm_start = (char*)shm->Addr();
    int size = shm->Size();
    int offset = 0;
    while (true) {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);       // 读整行,避免缓冲区残留
        for (char ch : line) {
            shm_start[offset % size] = ch;  // 循环写入,防止越界
            offset++;
        }
        shm_start[offset % size] = '\0';    // 写入结束标记
        offset++;
    }

    // 3. 去关联
    shm->Detach();
    delete shm;
    return 0;
}

Makefile

makefile 复制代码
.PHONY:all
all:Client Server

Server:Server.cc
	g++ -o $@ $^ -std=c++11
Client:Client.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f Server Client

4.6 关键陷阱:权限与生命周期

① 权限 0 问题

如果在 shmget 时不加权限位(如 0666),创建的共享内存 perms 为 0。此时即便 shmid 正确,后续 shmat 挂接也会因为权限不足而失败。

shell 复制代码
root@ALiServer:SharedMemory# ./Server
k=0x6603fa03 shmid=0
root@ALiServer:SharedMemory# ./Server
shmget: File exists     ← 第二次用 IPC_EXCL 创建,因为已存在而失败

root@ALiServer:SharedMemory# ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x6603fa03 0          root         0         128        0
#                                    ↑ perms=0!挂接会失败

root@ALiServer:SharedMemory# ipcrm -m 0          ← 用 shmid 删除(不能用 key)
root@ALiServer:SharedMemory# ipcs -m             ← 确认已删除

② 生命周期随内核

与管道随进程不同,System V IPC 资源随内核。程序退出后,共享内存依然存在。用户不主动删除,IPC 资源会和操作系统一样一直存在,除非重启。

nattch 验证: 如果一个共享内存的 nattch(当前挂接数)> 0,即还有进程挂接着它,此时调用 shmctl(IPC_RMID) 会发生什么?内核会立即标记该段为删除状态,但物理内存直到 nattch 变为 0 才真正释放。 任何新进程无法再 shmat 这个 shmid,但已挂接的进程仍然可以正常读写,直到它们各自 shmdt

这就是 System V IPC 的释放语义:删除是"标记"而非"立即释放",内核直到引用计数归零才会回收物理资源。

③ key 与 shmid 的关系

维度 key shmid
作用域 内核中标识共享内存的唯一性 用户代码中访问共享内存
类比 inode number fd(文件描述符)
使用场景 ftok + shmget 定位资源 shmat/shmdt/shmctl 操作资源
删除 不可用于 ipcrm ipcrm -m [shmid]

4.7 共享内存的优缺点

优点(速度之王):

  • 写入共享内存是直接操作内存,没有系统调用切换成本。
  • 没有用户态 ↔ 内核态的数据搬运(与管道不同)。

缺点(缺乏同步):

  • 共享内存本身不提供任何互斥与同步机制。
  • Server 不知道 Client 是否写完,可能会读到残缺数据。
  • 需要结合信号量命名管道来实现访问控制。

进程间通信让不同进程看到了同一份资源,但解决问题的时候往往会带来新的问题------多执行流并发的问题。这就引入了同步和互斥


五、 System V 消息队列与信号量 (选学)

除了共享内存,System V IPC 还提供了另外两种机制:消息队列和信号量。虽然它们在现代开发中不如 POSIX 标准或网络套接字常用,但其底层设计思想依然极具学习价值。

5.1 消息队列 (Message Queue)

如果说共享内存是一块"无差别"的数据黑板,那么消息队列就是一个带有数据类型的链表

5.1.1 核心思想

操作系统在内核中维护一个队列结构。进程 A 可以向队列中放入数据块,进程 B 也可以从中取出数据块。

这带来了一个新问题:如果 A 发送了 "Hello",B 同时也发送了 "Hi",当 A 去读队列时,如何区分哪条消息是给自己的,哪条是别人发的?

为了解决这个问题,消息队列在设计时,要求每个数据块不仅包含数据本身 (mtext) ,还必须带有一个类型标识 (mtype)

c 复制代码
// 用户空间定义的消息结构体模板
struct msgbuf {
    long mtype;       // 消息类型(必须 > 0)
    char mtext[1];    // 消息数据(柔性数组或指定长度)
};

进程在调用接收接口时,可以指定只接收特定 mtype 的消息。这样,哪怕所有进程共享同一个队列,也可以通过类型过滤出属于自己的数据块。

5.1.2 关键操作与接口对比

System V IPC 的 API 具有高度的一致性。消息队列的接口形式与共享内存如出一辙:

操作 共享内存 (shm) 消息队列 (msg)
创建 / 获取 shmget(key, size, flg) msgget(key, msgflg)
控制 / 删除 shmctl(id, cmd, buf) msgctl(id, cmd, buf)
读操作 直接读取挂接后的内存 shmat msgrcv(id, msgp, size, type, flg)
写操作 直接写入挂接后的内存 shmat msgsnd(id, msgp, size, flg)
命令行查看 ipcs -m ipcs -q
命令行删除 ipcrm -m <shmid> ipcrm -q <msqid>

关键点: 消息队列解决了共享内存的无序问题,但其数据传输仍然需要将数据从用户态拷贝到内核的队列中,读取时再从内核拷贝回用户态,存在两次拷贝的开销。


5.2 信号量 (Semaphore)

在共享内存机制中,我们提到了一个致命缺陷:缺乏同步与互斥。为了解决多执行流并发导致的"数据不一致"问题,信号量应运而生。

5.2.1 并发中的四个核心概念

在深入信号量之前,必须先理清由于 IPC 通信而衍生出的四个基本概念:

  1. 共享资源:能被多个执行流(进程或线程)同时看到并访问的资源(如打开的文件、共享内存等)。
  2. 临界资源:被保护起来的、在同一时刻只允许有限个(通常是一个)执行流访问的共享资源。
  3. 临界区 :在代码中,真正访问临界资源的那部分代码称为临界区。其余不涉及共享资源访问的代码称为非临界区。
  4. 原子性:一个操作要么彻底做完,要么完全不做,没有中间状态。在汇编层面,如果一个操作对应单条汇编指令,通常是原子的;如果是多条指令组合,极易被打断。
5.2.2 信号量的本质:资源的预订机制

信号量的本质,是一个描述临界资源数量的计数器。

我们可以把共享资源想象成一个有 100 个座位的"放映厅"。为了防止超载,进放映厅前必须先买票 。只要买到了票,里面就必定有你的位置,哪怕你还没进去坐下。

这种"买票"行为,就是对座位的预订机制。信号量就是用来发放这些票的计数器。

申请访问资源,必须先申请信号量(P 操作),成功后才能访问资源;访问完毕后,释放信号量(V 操作)。
#mermaid-svg-gRn8dh6ncU1t5DZE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gRn8dh6ncU1t5DZE .error-icon{fill:#552222;}#mermaid-svg-gRn8dh6ncU1t5DZE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gRn8dh6ncU1t5DZE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gRn8dh6ncU1t5DZE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gRn8dh6ncU1t5DZE .marker.cross{stroke:#333333;}#mermaid-svg-gRn8dh6ncU1t5DZE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gRn8dh6ncU1t5DZE p{margin:0;}#mermaid-svg-gRn8dh6ncU1t5DZE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster-label text{fill:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster-label span{color:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster-label span p{background-color:transparent;}#mermaid-svg-gRn8dh6ncU1t5DZE .label text,#mermaid-svg-gRn8dh6ncU1t5DZE span{fill:#333;color:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE .node rect,#mermaid-svg-gRn8dh6ncU1t5DZE .node circle,#mermaid-svg-gRn8dh6ncU1t5DZE .node ellipse,#mermaid-svg-gRn8dh6ncU1t5DZE .node polygon,#mermaid-svg-gRn8dh6ncU1t5DZE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gRn8dh6ncU1t5DZE .rough-node .label text,#mermaid-svg-gRn8dh6ncU1t5DZE .node .label text,#mermaid-svg-gRn8dh6ncU1t5DZE .image-shape .label,#mermaid-svg-gRn8dh6ncU1t5DZE .icon-shape .label{text-anchor:middle;}#mermaid-svg-gRn8dh6ncU1t5DZE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gRn8dh6ncU1t5DZE .rough-node .label,#mermaid-svg-gRn8dh6ncU1t5DZE .node .label,#mermaid-svg-gRn8dh6ncU1t5DZE .image-shape .label,#mermaid-svg-gRn8dh6ncU1t5DZE .icon-shape .label{text-align:center;}#mermaid-svg-gRn8dh6ncU1t5DZE .node.clickable{cursor:pointer;}#mermaid-svg-gRn8dh6ncU1t5DZE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gRn8dh6ncU1t5DZE .arrowheadPath{fill:#333333;}#mermaid-svg-gRn8dh6ncU1t5DZE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gRn8dh6ncU1t5DZE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gRn8dh6ncU1t5DZE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gRn8dh6ncU1t5DZE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gRn8dh6ncU1t5DZE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gRn8dh6ncU1t5DZE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster text{fill:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE .cluster span{color:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gRn8dh6ncU1t5DZE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gRn8dh6ncU1t5DZE rect.text{fill:none;stroke-width:0;}#mermaid-svg-gRn8dh6ncU1t5DZE .icon-shape,#mermaid-svg-gRn8dh6ncU1t5DZE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gRn8dh6ncU1t5DZE .icon-shape p,#mermaid-svg-gRn8dh6ncU1t5DZE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gRn8dh6ncU1t5DZE .icon-shape .label rect,#mermaid-svg-gRn8dh6ncU1t5DZE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gRn8dh6ncU1t5DZE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gRn8dh6ncU1t5DZE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gRn8dh6ncU1t5DZE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Yes
No
被唤醒
申请信号量 (P 操作)
信号量 > 0 ?
计数器 -1

进入临界区
阻塞等待
执行临界区代码

(访问共享资源)
释放信号量 (V 操作)

计数器 +1

5.2.3 P/V 操作为什么必须是原子的?

如果用普通整数 int sem = 1 来充当信号量,sem-- 这个操作在底层至少分为三步汇编:

  1. 将内存中的 sem 值读入 CPU 寄存器。
  2. 在寄存器中对值进行 -1
  3. 将计算结果写回内存。

如果两个进程同时执行 sem--,在进程调度的切换下,极其容易发生数据覆盖,导致两个进程都以为自己拿到了那唯一的 "1"。

因此,信号量既然是为了保护共享资源,它自己本身也必须是一个绝对安全的共享资源。 操作系统在底层通过加锁或特殊硬件指令,保证了信号量的申请(P 操作)和释放(V 操作)是绝对原子的。

5.2.4 二元信号量与互斥

当一个信号量的初始值设定为 1 时,它被称为二元信号量

二元信号量的值只会在 1 和 0 之间切换。这就像一把锁:1 代表锁空闲,0 代表锁被占用。它能完美地实现互斥,确保同一时刻只有一个进程能进入临界区,这正是共享内存最缺乏的保护机制。


六、 内核如何管理 IPC 资源 (进阶)

我们发现 System V 的共享内存 (shm)、消息队列 (msg) 和信号量 (sem) 的接口形式与底层逻辑惊人地相似。这种相似并非巧合,而是 Linux 内核为了统一管理它们,在底层采用了面向对象 (多态) 的设计思想。

6.1 内核中的 IPC 结构

在内核中,每种 IPC 机制都有对应的描述结构体:shmid_kernelmsg_queuesem_array

有趣的是,这三个结构体的第一个成员完全一样 ,都是一个类型为 kern_ipc_perm 的权限结构体。

c 复制代码
// 所有 IPC 资源的公共基类 (抽象属性)
struct kern_ipc_perm {
    spinlock_t    lock;
    int           deleted;
    key_t         key;      // ftok 生成的全局唯一键值
    uid_t         uid;
    gid_t         gid;
    // ... 其他权限与属性位
};

// 消息队列的内核结构体 (派生类)
struct msg_queue {
    struct kern_ipc_perm q_perm;  // 必须是第一个成员!
    time_t q_stime;               // 最后发送时间
    time_t q_rtime;               // 最后接收时间
    // ... 其他队列专属属性
};

6.2 C 语言实现的多态:指针数组管理

Linux 内核通过一个名为 ipc_id_ary 的数组结构来集中管理同一类 IPC 资源:

c 复制代码
struct ipc_id_ary {
    int size;
    struct kern_ipc_perm *p[0];  // 柔性数组,存放基类指针
};

这里运用了非常经典的 C 语言多态技巧:

  1. 基类指针: 数组 p 中存放的全是 struct kern_ipc_perm * 类型的指针。
  2. 结构体地址对齐: 因为 q_permmsg_queue第一个成员 ,所以 msg_queue 结构体的首地址,在数值上等于 其内部成员 q_perm 的首地址。
  3. 强转访问子类: 当我们想访问消息队列的专属属性(如 q_stime)时,只需将基类指针强制类型转换为对应的子类指针即可:((struct msg_queue *)(p[0]))->q_stime

#mermaid-svg-aZ583aWHcUHp9676{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aZ583aWHcUHp9676 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aZ583aWHcUHp9676 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aZ583aWHcUHp9676 .error-icon{fill:#552222;}#mermaid-svg-aZ583aWHcUHp9676 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aZ583aWHcUHp9676 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aZ583aWHcUHp9676 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aZ583aWHcUHp9676 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aZ583aWHcUHp9676 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aZ583aWHcUHp9676 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aZ583aWHcUHp9676 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aZ583aWHcUHp9676 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aZ583aWHcUHp9676 .marker.cross{stroke:#333333;}#mermaid-svg-aZ583aWHcUHp9676 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aZ583aWHcUHp9676 p{margin:0;}#mermaid-svg-aZ583aWHcUHp9676 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aZ583aWHcUHp9676 .cluster-label text{fill:#333;}#mermaid-svg-aZ583aWHcUHp9676 .cluster-label span{color:#333;}#mermaid-svg-aZ583aWHcUHp9676 .cluster-label span p{background-color:transparent;}#mermaid-svg-aZ583aWHcUHp9676 .label text,#mermaid-svg-aZ583aWHcUHp9676 span{fill:#333;color:#333;}#mermaid-svg-aZ583aWHcUHp9676 .node rect,#mermaid-svg-aZ583aWHcUHp9676 .node circle,#mermaid-svg-aZ583aWHcUHp9676 .node ellipse,#mermaid-svg-aZ583aWHcUHp9676 .node polygon,#mermaid-svg-aZ583aWHcUHp9676 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aZ583aWHcUHp9676 .rough-node .label text,#mermaid-svg-aZ583aWHcUHp9676 .node .label text,#mermaid-svg-aZ583aWHcUHp9676 .image-shape .label,#mermaid-svg-aZ583aWHcUHp9676 .icon-shape .label{text-anchor:middle;}#mermaid-svg-aZ583aWHcUHp9676 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aZ583aWHcUHp9676 .rough-node .label,#mermaid-svg-aZ583aWHcUHp9676 .node .label,#mermaid-svg-aZ583aWHcUHp9676 .image-shape .label,#mermaid-svg-aZ583aWHcUHp9676 .icon-shape .label{text-align:center;}#mermaid-svg-aZ583aWHcUHp9676 .node.clickable{cursor:pointer;}#mermaid-svg-aZ583aWHcUHp9676 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aZ583aWHcUHp9676 .arrowheadPath{fill:#333333;}#mermaid-svg-aZ583aWHcUHp9676 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aZ583aWHcUHp9676 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aZ583aWHcUHp9676 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aZ583aWHcUHp9676 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aZ583aWHcUHp9676 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aZ583aWHcUHp9676 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aZ583aWHcUHp9676 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aZ583aWHcUHp9676 .cluster text{fill:#333;}#mermaid-svg-aZ583aWHcUHp9676 .cluster span{color:#333;}#mermaid-svg-aZ583aWHcUHp9676 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aZ583aWHcUHp9676 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aZ583aWHcUHp9676 rect.text{fill:none;stroke-width:0;}#mermaid-svg-aZ583aWHcUHp9676 .icon-shape,#mermaid-svg-aZ583aWHcUHp9676 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aZ583aWHcUHp9676 .icon-shape p,#mermaid-svg-aZ583aWHcUHp9676 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aZ583aWHcUHp9676 .icon-shape .label rect,#mermaid-svg-aZ583aWHcUHp9676 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aZ583aWHcUHp9676 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aZ583aWHcUHp9676 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aZ583aWHcUHp9676 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-aZ583aWHcUHp9676 .arr>*{fill:#e6f3ff!important;stroke:#0066cc!important;}#mermaid-svg-aZ583aWHcUHp9676 .arr span{fill:#e6f3ff!important;stroke:#0066cc!important;}#mermaid-svg-aZ583aWHcUHp9676 .base>*{fill:#ffe6e6!important;stroke:#cc0000!important;}#mermaid-svg-aZ583aWHcUHp9676 .base span{fill:#ffe6e6!important;stroke:#cc0000!important;}#mermaid-svg-aZ583aWHcUHp9676 .sub>*{fill:#f9f9f9!important;stroke:#666666!important;}#mermaid-svg-aZ583aWHcUHp9676 .sub span{fill:#f9f9f9!important;stroke:#666666!important;} shmid_kernel (子类)
msg_queue (子类)
ipc_id_ary.p (指针数组)
p0
p1
p2
首地址 q_perm

(kern_ipc_perm)
q_stime, q_rtime...
首地址 shm_perm

(kern_ipc_perm)
shm_file...

系统层面的分类管理:

虽然内核用同一种思路管理,但为了区分不同类型的 IPC,内核会维护三个独立的全局结构(如 msg_ids, sem_ids, shm_ids),分别用来指向存放相应 IPC 资源指针的柔性数组。用户层看到的 shmidmsqid,本质上就可以理解为这个指针数组的下标加上一定的偏移或序列号计算出来的。

为了更直观地理解,我们可以用一段通用的 C 语言代码来演示这种:"首部嵌套结构体实现属性多态" + "函数指针实现方法多态"

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

// ==================== 1. 定义基类 ====================
struct Base {
    int type;
    char name[20];
    // 函数指针:实现方法级多态(虚函数表概念的雏形)
    void (*print_info)(struct Base* self);
};

// ==================== 2. 定义派生类 A ====================
struct DerivedA {
    struct Base base;  // 必须是第一个成员!实现属性级多态
    int a_value;       // A 独有的属性
};

// 派生类 A 的专属方法
void print_derived_A(struct Base* self) {
    // 向下转型,获取自己真实的结构体
    struct DerivedA* pa = (struct DerivedA*)self;
    printf("[Type A] Name: %s, Special Value: %d\n", pa->base.name, pa->a_value);
}

// ==================== 3. 定义派生类 B ====================
struct DerivedB {
    struct Base base;  // 必须是第一个成员!
    double b_value;    // B 独有的属性
};

// 派生类 B 的专属方法
void print_derived_B(struct Base* self) {
    struct DerivedB* pb = (struct DerivedB*)self;
    printf("[Type B] Name: %s, Special Value: %.2f\n", pb->base.name, pb->b_value);
}

// 4. 测试与运行
int main() {
    // 初始化对象,并将函数指针绑定到专属的方法上
    struct DerivedA objA = {{1, "ObjectA", print_derived_A}, 100};
    struct DerivedB objB = {{2, "ObjectB", print_derived_B}, 3.14};

    // 定义基类指针数组,统一管理不同类型的派生类对象
    struct Base* array[2];
    array[0] = (struct Base*)&objA;  // 向上转型
    array[1] = (struct Base*)&objB;  // 向上转型

    // 遍历数组:多态调用!
    for (int i = 0; i < 2; i++) {
        // 调用统一的接口,但实际执行的是各自绑定的专属方法
        array[i]->print_info(array[i]);
    }

    return 0;
}

总结: 面向对象中类的本质就是"属性 + 方法"

  • 属性级多态:通过将基类结构体放在子类结构体的最开头,配合强制类型转换(指针强转)。
  • 方法级多态 :通过在结构体内嵌函数指针,在对象初始化时绑定不同的具体实现函数(类似 C++ 的虚函数表 vtable)。
    这种优雅的设计贯穿了整个 Linux 内核,无论是这里的 IPC 资源管理,还是文件系统中的 file_operations,都是这套思想的结晶。

6.3 拓展:共享内存与 mmap 的异同

在前文中我们提到,共享内存的挂接 (shmat) 与动态库的加载类似。在 Linux 中,另一种非常常见的内存映射技术是 mmap

mmap 的原理机制:

每个进程的 mm_struct (虚拟地址空间描述符) 中包含一个 vm_area_struct 链表,描述一块块独立的虚拟内存区域 (VMA)。

  • mmap 在映射普通磁盘文件时,会将 VMA 内部的 vm_file 指针指向打开的普通文件,从而将文件的页缓存(内核缓冲区)直接映射到进程的虚拟地址。
  • 匿名映射: 如果 mmap 时不指定文件 (传 MAP_ANONYMOUS 标志),内核会分配一段物理内存直接映射给进程,这常用于父子进程间的共享内存通信(类似于匿名管道适用于有血缘关系的进程)。

System V 共享内存底层的挂接 (shmat) 机制:

System V 的 shmid_kernel 结构体内部维护了一个特殊的虚拟文件 struct file * shm_file

当调用 shmat 时:

  1. 内核为进程创建一个新的 VMA。
  2. 这个 VMA 的 vm_private_data 被设置为指向对应的 shmid_kernel
  3. 最核心的是,这个 VMA 会与 shmid_kernel 中的 shm_file(代表位于内存文件系统 tmpfs/shmem 上的一个虚拟文件)建立映射关系。
  4. 于是,多个毫不相关的进程只要挂接了同一个 shmid,它们的虚拟内存最终都会通过这个底层的虚拟文件,页表映射到同一块物理内存上。
对比维度 System V 共享内存 (shmget) mmap (文件映射 / 匿名映射)
定位机制 ftok 生成全局唯一的 key 文件路径 (inode) / 无文件(匿名)
适用范围 任何进程之间 文件映射适用于任何进程;匿名映射仅限父子/亲缘进程
生命周期 随内核,必须主动 ipcrm 或系统重启 随进程 (munmap 或进程退出时解除),若有后备文件则数据持久化到磁盘
持久化 纯内存操作,掉电或删除即丢失 文件映射模式可以由内核定期通过 msync 刷入磁盘

总结:无论是 shmat 还是 mmap,其本质都是操纵虚拟地址空间中的 VMA 结构,通过修改页表将其指向物理内存页,从而彻底免除了内核数据拷贝的开销。