本节重点:
进程间通信介绍
管道
消息队列
共享内存
信号量
一.进程间通信介绍
1.进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如
Debug
进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2.进程间通信发展
管道
System V
进程间通信
POSIX
进程间通信
3.进程间通信分类
(1)管道
匿名管道
pipe
命名管道
4.System V IPC
System V 消息队列
System V
共享内存
System V
信号量
5.POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
二.管道
1.什么是管道
管道是
Unix
中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个
“
管道
”

这张图展示的是 Linux 匿名管道(Anonymous Pipe) 的工作原理,以命令 who | wc -l 为例,直观呈现了管道如何连接两个进程实现数据传递。
管道创建当执行 who | wc -l 时,Shell 会在内核中创建一个匿名管道,它包含一个读端和一个写端。
进程关联
- who 进程:它的标准输出(stdout)被重定向到管道的写端,不再直接输出到终端。
- wc -l 进程:它的标准输入(stdin)被重定向到管道的读端,不再从终端读取输入。
数据传递
- who 命令执行后,会将当前登录用户的信息写入管道。
- wc -l 命令从管道读取数据,并统计输入的行数,最终将结果输出到终端。
关键特点
- 半双工通信:数据只能单向流动(从 who 到 wc -l)。
- 内核缓冲区:管道本质是内核中的一块环形缓冲区,数据在内核中临时存储,无需用户空间拷贝。
- 生命周期与进程绑定:匿名管道仅在创建它的进程及其子进程间有效,进程退出后管道会被自动销毁。
- 自动同步:读进程会阻塞等待数据写入,写进程会阻塞等待数据被读取,实现进程间的自动同步。
2.匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
关于pipe函数的详细讲解:
1. 函数基础信息
pipe() 是 Linux 系统调用,用于在内核中创建一个匿名管道(无名管道),实现有亲缘关系的进程(如父子进程)之间的单向通信。
| 函数原型 | int pipe(int pipefd[2]); |
| 所需头文件 | C:<unistd.h>;C++:<unistd.h>(兼容) |
| 函数归属 | 系统调用(内核态操作),属于 POSIX 标准 |
| 核心作用 | 创建匿名管道,返回两个文件描述符:读端和写端,用于进程间数据传递 |
| 适用场景 | 仅支持有亲缘关系的进程(父子、兄弟进程),不支持无关联进程通信 |
2. 参数详解
pipe() 的参数是一个长度为 2 的整型数组 pipefd,用于存储管道的两个文件描述符:
| pipefd[0] | 管道的读端 | 只能用 read() 读取数据,不能写 |
| pipefd[1] | 管道的写端 | 只能用 write() 写入数据,不能读 |
关键规则:
- 管道是半双工的:数据只能从写端(pipefd[1])写入,从读端(pipefd[0])读出,单向流动;
- 数组必须预先分配空间(如 int fd[2];),函数会自动填充两个文件描述符;
- 若参数为 NULL,函数会失败并设置 errno = EINVAL。
3. 返回值与错误处理
(1)返回值
- 成功:返回 0;
- 失败:返回 -1,并设置全局变量 errno 标识错误类型。
(2)常见错误码(errno)
| EMFILE | 进程打开的文件描述符达到上限 | 关闭无用的文件描述符,或提升进程文件描述符限制 |
| ENFILE | 系统打开的文件总数达到上限 | 清理系统中无用的文件 / 进程 |
| EINVAL | 参数无效(如 pipefd 为 NULL) | 确保传入长度为 2 的有效数组 |
4. 核心工作原理
(1)管道的本质
匿名管道是内核中的一块环形缓冲区(默认大小通常为 4KB 或 64KB,可通过 fcntl() 修改),数据写入后暂存于内核,读取后从缓冲区移除。
(2)关键特性
| 阻塞性 | ① 读端读取时,若管道为空 → 读进程阻塞,直到有数据写入;② 写端写入时,若管道满 → 写进程阻塞,直到数据被读取;③ 若写端关闭,读端读取完数据后会返回 0(表示 EOF);④ 若读端关闭,写端写入会触发 SIGPIPE 信号,进程默认终止 |
| 数据原子性 | 写入数据量 ≤ PIPE_BUF(管道缓冲区大小,通常 4096 字节)时,写入是原子的(不会被打断);超过则可能被拆分 |
| 生命周期 | 管道随进程生命周期结束而销毁:所有持有管道文件描述符的进程退出后,内核自动释放管道缓冲区 |

这张图也充分说明了管道本质是内核中的一块环形缓冲区.
因为匿名管道的实现需要有血缘关系的进程,因此需要使用fork函数来实现.
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
using namespace std;
int main() {
// 1. 创建管道
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
// 2. 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
// ========== 子进程(读数据)==========
if (pid == 0) {
// 3. 关闭子进程不需要的写端(核心!否则读端会一直阻塞)
close(pipefd[1]);
char buf[1024] = {0};
// 4. 从读端读取数据(阻塞等待)
ssize_t read_len = read(pipefd[0], buf, sizeof(buf)-1);
if (read_len > 0) {
cout << "[子进程] 读取到数据:" << buf << endl;
} else if (read_len == 0) {
cout << "[子进程] 管道写端已全部关闭" << endl;
} else {
perror("read failed");
}
// 5. 关闭读端
close(pipefd[0]);
return 0;
}
// ========== 父进程(写数据)==========
// 3. 关闭父进程不需要的读端
close(pipefd[0]);
// 4. 向写端写入数据
const char* msg = "Hello, this is parent process!";
ssize_t write_len = write(pipefd[1], msg, strlen(msg)+1); // +1 包含字符串结束符
if (write_len == -1) {
perror("write failed");
} else {
cout << "[父进程] 写入数据成功,长度:" << write_len << endl;
}
// 5. 关闭写端(触发子进程读端返回EOF)
close(pipefd[1]);
// 等待子进程执行完毕
wait(nullptr);
cout << "[父进程] 子进程已退出" << endl;
return 0;
}
5.用fork来解释共享管道原理(图加文字解析)

1. 左图:fork() 之后的初始状态
- 父进程调用 pipe() 创建管道,得到读端 fd[0] 和写端 fd[1]。
- 调用 fork() 创建子进程后,子进程会完整继承父进程的文件描述符表,因此也拥有 fd[0] 和 fd[1]。
- 此时父子进程都能同时读写管道,但这样无法形成单向通信,且会导致资源泄漏和阻塞问题。
2. 右图:关闭无用的文件描述符(关键一步)
为了实现单向通信,必须让父子进程各自关闭不需要的管道端:
- 父进程保留写端 fd[1],关闭读端 fd[0] → 只负责向管道写入数据。
- 子进程保留读端 fd[0],关闭写端 fd[1] → 只负责从管道读取数据。
- 这样就形成了父写子读的单向通信通道,避免了资源泄漏和不必要的阻塞。
为什么必须关闭无用端?
- 避免资源泄漏:文件描述符是有限资源,不关闭会导致进程打开的文件描述符数量超限。
- 防止错误阻塞:如果子进程不关闭写端,即使父进程关闭了写端,子进程的读端会一直阻塞(内核认为还有进程可能写入数据)。
- 触发 EOF 信号:当所有写端都关闭时,读端读取完剩余数据后会返回 0(表示 EOF),让读进程知道数据已传输完毕。
6.站在文件描述符角度–深度理解管道(图+文字)

为什么从3开始,因为0,1,2分别对应标准输入、标准输出和标准错误。并且是在进程启动时就默认打开的文件描述符。
1. 第一步:父进程创建管道
- 父进程调用 pipe(),内核创建一个管道,并返回两个文件描述符:
- fd[0] = 3:管道的读端
- fd[1] = 4:管道的写端
- 此时父进程的文件描述符表中,3 和 4 分别指向管道的读端和写端,进程可以同时读写。
2. 第二步:父进程 fork() 出子进程
- 子进程会完整继承父进程的文件描述符表,因此也拥有 fd[0] = 3 和 fd[1] = 4。
- 此时父子进程都能同时访问管道的读端和写端,但这会导致通信混乱和资源泄漏。
3. 第三步:关闭无用的文件描述符(关键)
- 父进程关闭读端 fd[0] = 3,只保留写端 fd[1] = 4 → 负责向管道写入数据。
- 子进程关闭写端 fd[1] = 4,只保留读端 fd[0] = 3 → 负责从管道读取数据。
- 这样就形成了父写子读的单向通信通道,避免了阻塞和资源泄漏。
核心意义
- 这是 Linux 中实现父子进程通信的标准流程,也是 | 管道符(如 who | wc -l)的底层实现原理。
- 关闭无用端是关键,它确保了数据单向流动,并且让内核能够正确识别管道的活跃状态(如写端全部关闭时,读端会返回 EOF)。
7.站在内核角度–管道本质(图+文)
1. 核心结构解析
-
进程 1/2 的 file 结构每个进程在打开管道时,都会创建一个 file 结构体实例,用来描述进程对管道的访问状态:
- f_mode:访问模式(读 / 写)
- f_pos:当前读写位置
- f_flags:文件状态标志(如 O_RDONLY、O_WRONLY)
- f_inode:指向管道的 inode 结构体,是两个进程共享管道的核心。
-
管道的 inode 结构这是管道的内核元数据,存储了管道的全局状态(如缓冲区大小、引用计数),是两个进程的 file 结构的共同指向,确保它们访问的是同一个管道。
-
数据页管道的实际数据存储区,是内核中的一块内存页(通常 4KB),进程 1 的写操作将数据写入这里,进程 2 的读操作从这里读取数据。
2. 通信流程
进程 1 写操作进程 1 通过其 file 结构找到管道的 inode,然后调用 write 将数据写入内核的数据页。
进程 2 读操作进程 2 通过其 file 结构找到同一个 inode,然后调用 read 从数据页中读取数据。
核心特点
- 数据直接在内核中传递,无需在用户空间之间拷贝,效率较高。
- 两个进程的 file 结构共享同一个 inode 和数据页,这是管道能实现进程间通信的本质。
3. 关键意义
这张图揭示了管道的底层实现:它并非一个真实的文件,而是内核中的一块内存缓冲区,通过 file 和 inode 结构让多个进程可以共享访问。这也解释了为什么匿名管道仅能在有亲缘关系的进程间使用 —— 因为它们需要继承这些内核数据结构。
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想".
例子
1.
在
minishell
中添加管道的实现:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <fcntl.h>
# define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
memset(command, 0x00, MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if (scanf("%[^\\n]%*c", command) == 0) {
getchar();
return -1;
}
return 0;
}
char **do_parse(char *buff)
{
int argc = 0;
static char *argv[32];
char *ptr = buff;
while(*ptr != '\\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
while((!isspace(*ptr)) && (*ptr) != '\\0') {
ptr++;
}
}else {
while(isspace(*ptr)) {
*ptr = '\\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_redirect(char *buff)
{
char *ptr = buff, *file = NULL;
int type = 0, fd, redirect_type = -1;
while(*ptr != '\\0') {
if (*ptr == '>') {
*ptr++ = '\\0';
redirect_type++;
if (*ptr == '>') {
*ptr++ = '\\0';
redirect_type++;
}
while(isspace(*ptr)) {
ptr++;
}
file = ptr;
while((!isspace(*ptr)) && *ptr != '\\0') {
ptr++;
}
*ptr = '\\0';
if (redirect_type == 0) {
fd = open(file, O_CREAT|O_TRUNC|O_WRONLY, 0664);
}else {
fd = open(file, O_CREAT|O_APPEND|O_WRONLY, 0664);
}
dup2(fd, 1);
}
ptr++;
}
return 0;
}
int do_command(char *buff)
{
int pipe_num = 0, i;
char *ptr = buff;
int pipefd[32][2] = {{-1}};
int pid = -1;
pipe_command[pipe_num] = ptr;
while(*ptr != '\\0') {
if (*ptr == '|') {
pipe_num++;
*ptr++ = '\\0';
pipe_command[pipe_num] = ptr;
continue;
}
ptr++;
}
pipe_command[pipe_num + 1] = NULL;
return pipe_num;
}
int do_pipe(int pipe_num)
{
int pid = 0, i;
int pipefd[10][2] = {{0}};
char **argv = {NULL};
for (i = 0; i <= pipe_num; i++) {
pipe(pipefd[i]);
}
for (i = 0; i <= pipe_num; i++) {
pid = fork();
if (pid == 0) {
do_redirect(pipe_command[i]);
argv = do_parse(pipe_command[i]);
if (i != 0) {
close(pipefd[i][1]);
dup2(pipefd[i][0], 0);
}
if (i != pipe_num) {
close(pipefd[i + 1][0]);
dup2(pipefd[i + 1][1], 1);
}
execvp(argv[0], argv);
}else {
close(pipefd[i][0]);
close(pipefd[i][1]);
waitpid(pid, NULL, 0);
}
}
return 0;
}
int main(int argc, char *argv[])
{
int num = 0;
while(1) {
if (do_face() < 0)
continue;
num = do_command(command);
do_pipe(num);
}
return 0;
}
8.管道读写规则
当没有数据可读时
O_NONBLOCK disable
:
read
调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable
:
read
调用返回
-1
,
errno
值为
EAGAIN
。
当管道满的时候
O_NONBLOCK disable
:
write
调用阻塞,直到有进程读走数据
O_NONBLOCK enable
:调用返回
-1
,
errno
值为
EAGAIN
如果所有管道写端对应的文件描述符被关闭,则
read
返回
0
如果所有管道读端对应的文件描述符被关闭,则
write
操作会产生信号
SIGPIPE,
进而可能导致
write
进程退出
当要写入的数据量不大于
PIPE_BUF
时,
linux
将保证写入的原子性。
当要写入的数据量大于
PIPE_BUF
时,
linux
将不再保证写入的原子性。
9.管道特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork
,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

三.命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用
FIFO
文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
1.创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
一、命令核心定位
mkfifo filename 是 Linux 系统中创建命名管道(Named Pipe/FIFO) 的命令,解决了匿名管道(pipe() 函数创建)仅能在亲缘进程间通信的限制,实现任意进程(无关联) 之间的双向 / 单向通信。
- 📌 命名管道本质:文件系统中可见的特殊文件(类型为 p),但不存储实际数据,仅作为进程间通信的 “桥梁”,数据仍存储在内核缓冲区。
- 📌 核心特性:有文件名、跨任意进程、生命周期与文件系统绑定(需手动删除)。
二、命令基础信息
| 命令格式 | mkfifo [选项] 文件名(最常用:mkfifo filename) |
| 核心作用 | 在文件系统中创建一个命名管道文件,供任意进程通过文件名访问管道 |
| 管道文件类型 | ls -l 查看时,权限位开头为 p(如 prw-rw-r–) |
| 所属分类 | POSIX 标准命令,所有 Linux/Unix 系统均支持 |
三、命令参数详解
1. 基础用法(无参数)
bash
运行
mkfifo myfifo # 创建名为 myfifo 的命名管道
- 执行后,文件系统中会生成一个名为 myfifo 的特殊文件,ls -l 查看示例:
plaintext
prw-rw-r– 1 user user 0 Jan 30 10:00 myfifo
- p:标识这是命名管道(FIFO)文件;
- 大小为 0:因为命名管道不存储实际数据,仅作为通信入口;
- 权限与普通文件一致(可通过 chmod 修改)。
2. 常用选项
| -m/–mode | 设置管道文件的权限(八进制) | mkfifo -m 0600 myfifo(仅当前用户可读可写) |
| -Z/–context | 设置 SELinux 安全上下文(仅 selinux 系统) | mkfifo -Z unconfined_u:object_r:user_tmp_t:s0 myfifo |
| –help | 查看帮助信息 | mkfifo –help |
| –version | 查看版本 | mkfifo –version |
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
一、mkfifo () 函数核心讲解
mkfifo() 是 Linux 系统调用,用于在内核中创建命名管道(FIFO)文件,是用户态代码创建命名管道的核心接口,对应命令行的 mkfifo filename。
1. 基础信息
| 函数原型 | int mkfifo(const char *pathname, mode_t mode); |
| 所需头文件 | <sys/types.h> + <sys/stat.h>(C/C++ 通用) |
| 返回值 | 成功返回0;失败返回-1,并设置errno |
| 核心作用 | 在指定路径创建命名管道文件,供任意进程通过文件名访问管道 |
2. 参数详解
| pathname | 管道文件的路径 + 名称(如 "./myfifo"、"/tmp/fifo_test"):✅ 相对路径 / 绝对路径均可;❌ 路径不存在会报错(ENOENT)。 |
| mode | 管道文件的权限(八进制数,如 0666、0644):✅ 最终权限 = mode & ~umask(与普通文件权限规则一致);✅ 常用 0666(所有用户可读可写)。 |
3. 常见错误码(errno)
| EEXIST | 管道文件已存在 | 先检查文件是否存在,或用 unlink() 删除后重建 |
| ENOENT | 路径不存在(如 /tmp/a/myfifo 中 a 目录不存在) | 先创建目录(mkdir()),再创建管道 |
| EACCES | 无权限创建文件 | 检查目录权限,提升权限(sudo) |
| ENOSPC | 文件系统空间不足 | 清理磁盘空间 |
4. 基础使用示例
cpp
运行
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <cstdio>
#include <cerrno>
using namespace std;
#define FIFO_PATH "./myfifo"
int main() {
// 创建命名管道,权限 0666
if (mkfifo(FIFO_PATH, 0666) == -1) {
if (errno == EEXIST) {
cout << "管道文件已存在,无需重复创建" << endl;
} else {
perror("mkfifo failed");
return 1;
}
} else {
cout << "管道文件创建成功:" << FIFO_PATH << endl;
}
return 0;
}
二、与 mkfifo () 配套的核心函数
命名管道创建后,需通过一系列函数完成 “打开→读写→关闭→删除” 的完整流程,以下是最常用的配套函数:
1. open ():打开命名管道(核心!)
命名管道创建后是 “空文件”,必须通过 open() 打开才能进行读写,这是与匿名管道的关键区别。
| 函数原型 | int open(const char *pathname, int flags); | |
| 核心参数(flags) | ① O_RDONLY:只读打开(读进程);② O_WRONLY:只写打开(写进程);③ O_NONBLOCK:非阻塞模式(可选);✅ 常用组合:`O_RDONLY | O_NONBLOCK`(非阻塞读) |
| 关键特性 | 🔴 默认阻塞模式:- 仅打开读端 → 阻塞,直到有进程打开写端;- 仅打开写端 → 阻塞,直到有进程打开读端;🔴 非阻塞模式:- 仅打开读端 → 立即返回(成功);- 仅打开写端 → 报错(ENXIO)。 |
示例:阻塞模式打开管道
cpp
运行
// 读进程:阻塞打开读端
int fd_read = open(FIFO_PATH, O_RDONLY);
if (fd_read == -1) { perror("open read failed"); return 1; }
// 写进程:阻塞打开写端
int fd_write = open(FIFO_PATH, O_WRONLY);
if (fd_write == -1) { perror("open write failed"); return 1; }
示例:非阻塞模式打开管道
cpp
运行
// 非阻塞打开读端(即使无写进程也立即返回)
int fd_read = open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
2. read ()/write ():读写管道数据
命名管道的读写与普通文件一致,但遵循管道的阻塞 / 原子性规则。
| read() | ssize_t read(int fd, void *buf, size_t count); | ① 阻塞模式:无数据则阻塞,有数据则读取;② 非阻塞模式:无数据返回 -1(errno=EAGAIN);③ 写端关闭:读取剩余数据后返回 0(EOF)。 |
| write() | ssize_t write(int fd, const void *buf, size_t count); | ① 阻塞模式:缓冲区满则阻塞,否则写入;② 非阻塞模式:缓冲区满返回 -1(errno=EAGAIN);③ 读端关闭:触发 SIGPIPE 信号(进程默认终止);④ 原子性:写入 ≤ PIPE_BUF(4KB)时原子写入。 |
示例:读写管道数据
cpp
运行
// 写进程:写入数据
const char* msg = "Hello FIFO!";
ssize_t write_len = write(fd_write, msg, strlen(msg)+1);
if (write_len == -1) { perror("write failed"); }
// 读进程:读取数据
char buf[1024] = {0};
ssize_t read_len = read(fd_read, buf, sizeof(buf)-1);
if (read_len > 0) {
cout << "读取到:" << buf << endl;
} else if (read_len == 0) {
cout << "写端已关闭" << endl;
}
3. close ():关闭管道文件描述符
完成读写后必须关闭文件描述符,释放资源,否则会导致管道引用计数不为 0,无法正常销毁。
| 函数原型 | int close(int fd); |
| 返回值 | 成功返回0;失败返回-1(errno=EBADF 表示 fd 无效) |
| 核心作用 | 关闭管道的读 / 写端,减少管道的引用计数;当所有端都关闭时,内核释放管道缓冲区。 |
示例:关闭文件描述符
cpp
运行
close(fd_read); // 关闭读端
close(fd_write); // 关闭写端
4. unlink ():删除命名管道文件
命名管道是文件系统中的实体文件,进程退出后不会自动销毁,需用 unlink() 手动删除(对应命令行 rm)。
| 函数原型 | int unlink(const char *pathname); |
| 返回值 | 成功返回0;失败返回-1(errno=ENOENT 表示文件不存在) |
| 核心作用 | 删除文件系统中的管道文件,即使有进程仍在使用该管道,也会先标记删除,待所有进程关闭后彻底清理。 |
示例:删除管道文件
if (unlink(FIFO_PATH) == -1) {
perror("unlink failed");
} else {
cout << "管道文件已删除" << endl;
}
三、完整实战案例:任意进程间通信
以下是两个无关联进程通过命名管道通信的完整代码,覆盖 mkfifo() + 所有配套函数:
1. 写进程(fifo_writer.cpp)
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <cerrno>
using namespace std;
#define FIFO_PATH "./myfifo"
int main() {
// 1. 创建命名管道(已存在则忽略)
if (mkfifo(FIFO_PATH, 0666) == -1) {
if (errno != EEXIST) {
perror("mkfifo failed");
return 1;
}
cout << "管道文件已存在,直接使用" << endl;
} else {
cout << "管道文件创建成功" << endl;
}
// 2. 阻塞打开写端(等待读进程连接)
cout << "等待读进程连接…" << endl;
int fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
cout << "读进程已连接,开始写入数据" << endl;
// 3. 写入数据
const char* msg = "Hello from FIFO writer (任意进程通信)!";
ssize_t write_len = write(fd, msg, strlen(msg)+1);
if (write_len == -1) {
perror("write failed");
} else {
cout << "成功写入 " << write_len << " 字节数据:" << msg << endl;
}
// 4. 关闭文件描述符
close(fd);
cout << "写端已关闭" << endl;
return 0;
}
2. 读进程(fifo_reader.cpp)
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
using namespace std;
#define FIFO_PATH "./myfifo"
int main() {
// 1. 阻塞打开读端(等待写进程连接)
cout << "等待写进程连接…" << endl;
int fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
cout << "写进程已连接,开始读取数据" << endl;
// 2. 读取数据
char buf[1024] = {0};
ssize_t read_len = read(fd, buf, sizeof(buf)-1);
if (read_len == -1) {
perror("read failed");
} else if (read_len == 0) {
cout << "写端已关闭,无数据可读" << endl;
} else {
cout << "成功读取 " << read_len << " 字节数据:" << buf << endl;
}
// 3. 关闭文件描述符
close(fd);
cout << "读端已关闭" << endl;
// 4. 删除管道文件(最后一个进程负责)
if (unlink(FIFO_PATH) == -1) {
perror("unlink failed");
} else {
cout << "管道文件已删除" << endl;
}
return 0;
}
编译运行
bash
运行
# 编译
g++ fifo_writer.cpp -o writer -std=c++11
g++ fifo_reader.cpp -o reader -std=c++11
# 终端1:运行写进程(阻塞等待读进程)
./writer
# 终端2:运行读进程(连接后读写数据)
./reader
输出结果
- 写进程终端:
plaintext
管道文件创建成功
等待读进程连接…
读进程已连接,开始写入数据
成功写入 41 字节数据:Hello from FIFO writer (任意进程通信)!
写端已关闭 - 读进程终端:
plaintext
等待写进程连接…
写进程已连接,开始读取数据
成功读取 41 字节数据:Hello from FIFO writer (任意进程通信)!
读端已关闭
管道文件已删除
四、配套函数使用注意事项
1. open () 阻塞 vs 非阻塞选择
- ✅ 推荐阻塞模式:适用于大多数场景,自动等待对方进程连接,无需手动轮询。
- ❌ 非阻塞模式:仅适用于 “无需等待对方” 的场景(如定时检查管道),需处理 EAGAIN 错误。
2. write () 避免触发 SIGPIPE
若读端已关闭,写进程调用 write() 会触发 SIGPIPE 信号,进程默认终止:
cpp
运行
// 可选:捕获 SIGPIPE 信号,避免进程终止
#include <signal.h>
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
3. unlink () 时机
- 建议由最后退出的进程调用 unlink(),避免提前删除导致其他进程无法访问管道。
- 即使调用 unlink() 后,已打开管道的进程仍可正常读写,直到所有进程关闭管道。
总结
- open():打开管道(阻塞 / 非阻塞,读写端需配对);
- read()/write():读写数据(遵循管道阻塞 / 原子性规则);
- close():关闭文件描述符,释放资源;
- unlink():删除管道文件,清理文件系统;
四.匿名管道与命名管道的区别
| 创建方式 | 1. 系统调用:pipe(int pipefd[2])2. 无命令行方式(仅代码中创建) | 1. 系统调用:mkfifo(const char *pathname, mode_t mode)2. 命令行:mkfifo filename |
| 标识 / 访问方式 | 无文件名,仅通过文件描述符(pipefd [0]/pipefd [1])访问 | 有文件系统中可见的文件名(如 ./myfifo),通过 open() 打开后用文件描述符访问 |
| 通信进程范围 | 仅支持有亲缘关系的进程(父子 / 兄弟进程,需继承文件描述符) | 支持任意进程(无关联进程也可,只要能访问管道文件路径) |
| 生命周期 | 随进程生命周期销毁:所有持有管道文件描述符的进程退出后,内核自动释放管道 | 随文件系统存在:管道文件会一直保留,需手动 rm/unlink() 删除,即使进程退出 |
| 文件系统可见性 | 不可见(仅存在于内核中,无文件实体) | 可见(ls -l 可查,文件类型为 p) |
| 打开方式 | 无需 open():pipe() 创建后直接得到文件描述符,fork 后继承 | 需显式 open():先创建管道文件,再用 open(pathname, flags) 打开得到文件描述符 |
| 阻塞特性(默认) | 读写默认阻塞:读空管道阻塞、写满管道阻塞 | 打开 / 读写默认阻塞:① 仅打开一端(读 / 写)会阻塞在 open();② 读写规则同匿名管道 |
| 核心适用场景 |
1. 父子 / 兄弟进程间临时通信2. Shell 管道符(如 `who|wc -l`)底层实现 |
1. 无关联进程间长期通信2. 跨程序 / 跨用户的进程通信(如服务端 – 客户端通信) |
| 数据存储 | 数据暂存内核缓冲区,读取后立即删除,无持久化 | 数据暂存内核缓冲区,管道文件本身大小始终为 0,无持久化 |
| 权限控制 | 无独立权限(继承创建进程的权限) | 有文件权限(创建时指定 mode,可通过 chmod 修改) |
| 典型代码流程 | 1. pipe() 创建管道2. fork() 创建子进程3. 关闭无用端4. 读写数据 | 1. mkfifo() 创建管道文件2. 进程 A open() 写端3. 进程 B open() 读端4. 读写数据5. unlink() 删除管道 |
关键差异深度解析
1. 最核心差异:通信进程范围
这是两者最本质的区别,决定了使用场景:
- 匿名管道:依赖 fork() 继承文件描述符,只能在父子 / 兄弟进程间通信(比如父进程创建管道后 fork 子进程,子进程继承 pipefd);
- 命名管道:通过文件系统的文件名标识,任何进程只要能访问该文件路径(有权限),就能打开并通信(比如进程 A 在终端 1 创建,进程 B 在终端 2 打开)。
2. 生命周期差异(易踩坑点)
- 匿名管道:开发中无需手动清理,进程退出后内核自动释放,不会残留资源;
- 命名管道:若忘记 rm/unlink(),管道文件会一直留在文件系统中,下次创建会报错 File exists,需手动清理。
3. 打开方式差异
- 匿名管道:pipe() 调用后直接得到可用的文件描述符,无需额外打开;
- 命名管道:mkfifo() 仅创建管道文件,必须通过 open() 打开(指定 O_RDONLY/O_WRONLY)才能读写,且默认情况下,仅打开一端会阻塞在 open() 调用(等待另一端打开)。
适用场景选择建议
| 父子进程间临时传递少量数据 | 匿名管道 | 无需创建文件,自动销毁,流程更简单 |
|
Shell 命令拼接(如 `ls grep`) |
匿名管道 | Shell 底层自动为管道符创建匿名管道,无需手动管理 |
| 两个无关联进程(如服务端 + 客户端)通信 | 命名管道 | 突破亲缘进程限制,通过文件名即可通信 |
| 跨终端 / 跨用户的进程通信 | 命名管道 | 只要有权限访问管道文件路径,任意进程都能参与通信 |
| 长期运行的进程间通信 | 命名管道 | 生命周期可控,进程重启后仍可通过文件名重新连接 |
总结
五.命名管道的打开规则
命名管道的打开规则是其与匿名管道最核心的区别之一,核心由 open() 函数的 flags 参数控制,分为阻塞模式(默认)和非阻塞模式(O_NONBLOCK)两类。
一、核心前提
在讲解打开规则前,先明确两个基础概念:
- O_RDONLY:只读打开(读进程);
- O_WRONLY:只写打开(写进程);
- O_NONBLOCK:非阻塞模式(可选,叠加在 O_RDONLY/O_WRONLY 上);
- ❌ 禁止使用 O_RDWR 打开:会破坏管道的半双工特性,且行为未定义(不同系统表现不同)。
二、默认模式(阻塞模式,无 O_NONBLOCK)
这是最常用的模式,也是最符合直觉的模式,核心规则:仅打开一端(读 / 写)时,open() 会阻塞,直到另一端被打开;两端都打开后,open() 才返回。
1. 场景拆解(阻塞模式)
| 1. 进程 A 以 O_RDONLY 打开 | open() 阻塞,直到有进程以 O_WRONLY 打开写端 | 以 O_WRONLY 打开后,进程 A 的 open() 立即返回,双方都得到文件描述符 |
| 2. 进程 A 以 O_WRONLY 打开 | open() 阻塞,直到有进程以 O_RDONLY 打开读端 | 以 O_RDONLY 打开后,进程 A 的 open() 立即返回,双方都得到文件描述符 |
| 3. 同时打开读写端 | 两个进程的 open() 都立即返回 |
2. 代码示例(阻塞模式)
// 读进程(阻塞打开读端,等待写进程)
int fd_read = open("./myfifo", O_RDONLY);
// 若没有写进程打开,会一直阻塞在这里,直到有写进程连接
// 写进程(阻塞打开写端,等待读进程)
int fd_write = open("./myfifo", O_WRONLY);
// 若没有读进程打开,会一直阻塞在这里,直到有读进程连接
3. 关键特性
- 阻塞发生在 open() 阶段,而非读写阶段;
- 只要读写两端都有进程打开,open() 就会成功返回,后续读写遵循管道的普通阻塞规则(读空阻塞、写满阻塞);
- 若一端进程退出(关闭文件描述符),另一端的 open() 不会重新阻塞,仅读写行为受影响。
三、非阻塞模式(O_NONBLOCK)
通过在 open() 的 flags 中添加 O_NONBLOCK,可以取消打开阶段的阻塞,核心规则:无论另一端是否打开,open() 都会立即返回(成功 / 失败)。
1. 场景拆解(非阻塞模式)
| 1. `O_RDONLY | O_NONBLOCK` | open() 立即成功返回(得到读端文件描述符) | open() 立即成功返回 |
| 2. `O_WRONLY | O_NONBLOCK` | open() 失败,返回 -1,errno = ENXIO | open() 立即成功返回 |
2. 代码示例(非阻塞模式)
// 非阻塞打开读端(即使无写进程也成功)
int fd_read = open("./myfifo", O_RDONLY | O_NONBLOCK);
if (fd_read == -1) {
perror("open read failed");
return 1;
}
// 非阻塞打开写端(无读进程则失败)
int fd_write = open("./myfifo", O_WRONLY | O_NONBLOCK);
if (fd_write == -1) {
if (errno == ENXIO) {
cout << "无读进程打开管道,写端打开失败" << endl;
}
return 1;
}
3. 关键特性
- 非阻塞模式仅影响 open() 阶段,读写阶段的阻塞规则仍适用(如读空管道时,非阻塞读会返回 -1,errno = EAGAIN);
- 写端非阻塞打开的 “苛刻性”:必须有读端已打开才能成功,否则直接失败(这是为了避免写进程向无读者的管道写数据,触发 SIGPIPE);
- 适用于 “无需等待对方进程” 的场景(如定时检查管道是否有读者 / 写者)。
四、特殊场景:O_RDWR 打开(禁止使用)
部分开发者会尝试用 O_RDWR 打开命名管道,这是强烈不推荐的,原因:
五、打开规则常见坑点与避坑指南
1. 阻塞打开时进程卡死
- 问题:仅启动读进程 / 写进程,open() 一直阻塞,进程看似 “卡死”;
- 避坑:确保读写进程配对启动,或使用非阻塞模式并处理失败场景。
2. 非阻塞写端打开失败
- 问题:用 O_WRONLY | O_NONBLOCK 打开时,返回 -1 且 errno=ENXIO;
- 原因:无读进程打开管道;
- 避坑:先检查读进程是否存在,或先启动读进程再启动写进程。
3. 多次打开同一端
- 问题:同一进程多次打开同一管道的读 / 写端,导致管道引用计数增加;
- 后果:关闭一端时,若引用计数不为 0,内核不会触发 EOF,读进程会一直阻塞;
- 避坑:一个进程仅打开一次管道的某一端,避免重复打开。
六、打开规则与读写规则的关联
命名管道的打开规则是 “前置条件”,读写规则是 “后续行为”,两者结合的完整逻辑:
- 打开成功 → 读空管道阻塞、写满管道阻塞;
- 写端全部关闭 → 读进程读取剩余数据后返回 0(EOF);
- 读端全部关闭 → 写进程写数据触发 SIGPIPE。
- 打开成功 → 读空管道返回 -1(EAGAIN)、写满管道返回 -1(EAGAIN);
- 其他读写规则与阻塞模式一致。
总结
例子1-用命名管道实现文件拷贝
读取文件,写入命名管道
:
1. 公共头文件(fifo_copy.h)
定义管道名称和缓冲区大小,方便两个进程共用:
#ifndef FIFO_COPY_H
#define FIFO_COPY_H
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <cerrno>
// 命名管道名称
#define FIFO_NAME "./file_copy_fifo"
// 缓冲区大小(建议等于PIPE_BUF,保证原子写入)
#define BUF_SIZE 4096
#endif // FIFO_COPY_H
2. 发送进程(fifo_sender.cpp)
负责读取源文件,将内容写入命名管道:
#include "fifo_copy.h"
int main(int argc, char *argv[]) {
// 1. 检查命令行参数
if (argc != 2) {
std::cerr << "用法:" << argv[0] << " <源文件路径>" << std::endl;
return 1;
}
const char* src_file = argv[1];
// 2. 创建命名管道(已存在则忽略)
if (mkfifo(FIFO_NAME, 0666) == -1) {
if (errno != EEXIST) {
perror("mkfifo failed");
return 1;
}
std::cout << "命名管道已存在,直接使用" << std::endl;
} else {
std::cout << "命名管道创建成功:" << FIFO_NAME << std::endl;
}
// 3. 打开源文件(只读)
int fd_src = open(src_file, O_RDONLY);
if (fd_src == -1) {
perror("open src file failed");
unlink(FIFO_NAME); // 清理管道
return 1;
}
std::cout << "源文件打开成功:" << src_file << std::endl;
// 4. 阻塞打开命名管道写端(等待接收进程连接)
std::cout << "等待接收进程连接…" << std::endl;
int fd_fifo = open(FIFO_NAME, O_WRONLY);
if (fd_fifo == -1) {
perror("open fifo write failed");
close(fd_src);
unlink(FIFO_NAME);
return 1;
}
std::cout << "接收进程已连接,开始拷贝文件…" << std::endl;
// 5. 逐段读取源文件并写入管道
char buf[BUF_SIZE] = {0};
ssize_t read_len, write_len;
while ((read_len = read(fd_src, buf, BUF_SIZE)) > 0) {
// 写入管道(确保全部数据写入)
write_len = write(fd_fifo, buf, read_len);
if (write_len == -1) {
perror("write fifo failed");
break;
}
if (write_len != read_len) {
std::cerr << "警告:部分数据未写入,已写 " << write_len << " / 应写 " << read_len << std::endl;
}
std::cout << "已拷贝 " << read_len << " 字节" << std::endl;
}
// 6. 检查读取结果
if (read_len == -1) {
perror("read src file failed");
} else {
std::cout << "文件拷贝完成!总写入管道数据:" << (ssize_t)(lseek(fd_src, 0, SEEK_END)) << " 字节" << std::endl;
}
// 7. 关闭文件描述符
close(fd_src);
close(fd_fifo);
std::cout << "发送进程:已关闭文件和管道" << std::endl;
return 0;
}
3. 接收进程(fifo_receiver.cpp)
负责从命名管道读取内容,写入目标文件:
#include "fifo_copy.h"
int main(int argc, char *argv[]) {
// 1. 检查命令行参数
if (argc != 2) {
std::cerr << "用法:" << argv[0] << " <目标文件路径>" << std::endl;
return 1;
}
const char* dst_file = argv[1];
// 2. 阻塞打开命名管道读端(等待发送进程连接)
std::cout << "等待发送进程连接…" << std::endl;
int fd_fifo = open(FIFO_NAME, O_RDONLY);
if (fd_fifo == -1) {
perror("open fifo read failed");
return 1;
}
std::cout << "发送进程已连接,准备接收数据…" << std::endl;
// 3. 打开/创建目标文件(只写,创建,截断)
int fd_dst = open(dst_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd_dst == -1) {
perror("open dst file failed");
close(fd_fifo);
unlink(FIFO_NAME);
return 1;
}
std::cout << "目标文件打开/创建成功:" << dst_file << std::endl;
// 4. 逐段从管道读取并写入目标文件
char buf[BUF_SIZE] = {0};
ssize_t read_len, write_len;
while ((read_len = read(fd_fifo, buf, BUF_SIZE)) > 0) {
// 写入目标文件
write_len = write(fd_dst, buf, read_len);
if (write_len == -1) {
perror("write dst file failed");
break;
}
if (write_len != read_len) {
std::cerr << "警告:部分数据未写入目标文件,已写 " << write_len << " / 应写 " << read_len << std::endl;
}
std::cout << "已接收 " << read_len << " 字节并写入目标文件" << std::endl;
}
// 5. 检查读取结果
if (read_len == -1) {
perror("read fifo failed");
} else if (read_len == 0) {
std::cout << "管道写端已关闭,接收完成!总写入目标文件数据:" << (ssize_t)(lseek(fd_dst, 0, SEEK_END)) << " 字节" << std::endl;
}
// 6. 关闭文件描述符
close(fd_dst);
close(fd_fifo);
std::cout << "接收进程:已关闭文件和管道" << std::endl;
// 7. 删除命名管道(最后退出的进程负责清理)
if (unlink(FIFO_NAME) == -1) {
perror("unlink fifo failed");
} else {
std::cout << "命名管道已删除:" << FIFO_NAME << std::endl;
}
return 0;
}
例子2-用命名管道实现server&client通信
1. 公共头文件(fifo_ipc.h)
定义管道名称、缓冲区大小和核心常量,供服务端 / 客户端共用:
#ifndef FIFO_IPC_H
#define FIFO_IPC_H
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <cerrno>
#include <string>
// 命名管道名称(区分服务端接收/回复管道)
#define SERVER_FIFO "./server_fifo" // Client → Server
#define CLIENT_FIFO "./client_fifo" // Server → Client
// 缓冲区大小(PIPE_BUF,保证原子写入)
#define BUF_SIZE 4096
// 退出指令
#define EXIT_CMD "exit"
// 封装错误处理函数
inline void handle_error(const std::string& msg) {
perror(msg.c_str());
exit(EXIT_FAILURE);
}
#endif // FIFO_IPC_H
2. 服务端代码(fifo_server.cpp)
持续监听客户端消息,处理后回复,支持多客户端(串行处理):
#include "fifo_ipc.h"
int main() {
// 1. 创建服务端接收管道(Client→Server)
if (mkfifo(SERVER_FIFO, 0666) == -1) {
if (errno != EEXIST) {
handle_error("mkfifo server_fifo failed");
}
std::cout << "[Server] 服务端管道已存在,复用该管道" << std::endl;
} else {
std::cout << "[Server] 服务端管道创建成功:" << SERVER_FIFO << std::endl;
}
// 2. 阻塞打开服务端管道读端(持续监听)
int fd_server = open(SERVER_FIFO, O_RDONLY);
if (fd_server == -1) {
handle_error("open server_fifo read failed");
}
std::cout << "[Server] 服务端已启动,等待客户端消息…" << std::endl;
char recv_buf[BUF_SIZE] = {0};
ssize_t recv_len;
// 3. 持续读取客户端消息
while (true) {
// 清空缓冲区
memset(recv_buf, 0, sizeof(recv_buf));
// 阻塞读取客户端消息(无消息则等待)
recv_len = read(fd_server, recv_buf, BUF_SIZE – 1);
if (recv_len == -1) {
handle_error("read server_fifo failed");
} else if (recv_len == 0) {
// 客户端关闭写端,继续等待下一个客户端
std::cout << "[Server] 客户端连接断开,等待新客户端…" << std::endl;
continue;
}
// 4. 处理客户端消息
std::string client_msg(recv_buf);
std::cout << "[Server] 收到客户端消息:" << client_msg << std::endl;
// 检测退出指令(服务端不退出,仅提示)
if (client_msg == EXIT_CMD) {
std::cout << "[Server] 客户端发起退出请求" << std::endl;
continue;
}
// 5. 准备回复消息
std::string reply_msg = "Server回复:已收到你的消息【" + client_msg + "】";
std::cout << "[Server] 准备发送回复:" << reply_msg << std::endl;
// 6. 打开客户端管道写端,发送回复
int fd_client = open(CLIENT_FIFO, O_WRONLY);
if (fd_client == -1) {
handle_error("open client_fifo write failed");
}
write(fd_client, reply_msg.c_str(), reply_msg.length() + 1); // +1 包含结束符
close(fd_client);
std::cout << "[Server] 回复已发送" << std::endl;
}
// 7. 清理资源(实际不会执行,需手动终止服务端)
close(fd_server);
unlink(SERVER_FIFO);
unlink(CLIENT_FIFO);
return 0;
}
3. 客户端代码(fifo_client.cpp)
主动连接服务端,发送消息并接收回复,支持手动输入多条消息:
#include "fifo_ipc.h"
int main() {
// 1. 创建客户端接收管道(Server→Client)
if (mkfifo(CLIENT_FIFO, 0666) == -1) {
if (errno != EEXIST) {
handle_error("mkfifo client_fifo failed");
}
std::cout << "[Client] 客户端管道已存在,复用该管道" << std::endl;
} else {
std::cout << "[Client] 客户端管道创建成功:" << CLIENT_FIFO << std::endl;
}
// 2. 打开服务端管道写端(发送消息用)
int fd_server = open(SERVER_FIFO, O_WRONLY);
if (fd_server == -1) {
handle_error("open server_fifo write failed");
}
std::cout << "[Client] 已连接服务端,可输入消息(输入exit退出):" << std::endl;
std::string input_msg;
char reply_buf[BUF_SIZE] = {0};
ssize_t reply_len;
// 3. 循环输入消息并发送
while (true) {
// 读取用户输入
std::cout << "[Client] 请输入消息:";
std::getline(std::cin, input_msg);
// 发送消息到服务端
write(fd_server, input_msg.c_str(), input_msg.length() + 1);
// 检测退出指令
if (input_msg == EXIT_CMD) {
std::cout << "[Client] 退出客户端" << std::endl;
break;
}
// 4. 打开客户端管道读端,接收服务端回复
int fd_client = open(CLIENT_FIFO, O_RDONLY);
if (fd_client == -1) {
handle_error("open client_fifo read failed");
}
memset(reply_buf, 0, sizeof(reply_buf));
reply_len = read(fd_client, reply_buf, BUF_SIZE – 1);
if (reply_len > 0) {
std::cout << "[Client] 收到服务端回复:" << reply_buf << std::endl;
}
close(fd_client);
}
// 5. 清理资源
close(fd_server);
unlink(CLIENT_FIFO); // 客户端退出时删除自身管道
std::cout << "[Client] 资源已清理,退出" << std::endl;
return 0;
}
六.system V共享内存
共享内存区是最快的
IPC
形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
1.共享内存示意图+文字解析

1. 进程地址空间布局(从低到高)
这是一个 32 位 Linux 系统的典型布局,从地址 0x00000000 到 0xC0000000 为用户空间,0xC0000000 以上为内核空间。
| 0x08048000 附近 | 文本段(Text) | 存放程序的可执行代码,只读。 |
| 0x08048000 以上 | 初始化数据段(Data) | 存放已初始化的全局变量和静态变量。 |
| 未初始化数据段(BSS) | 存放未初始化的全局变量和静态变量,程序启动时会被清零。 | |
| 堆(Heap) | 动态内存分配区域(malloc/new),向上增长。 | |
| 为扩展保留 | 预留区域,用于堆的扩展。 | |
| 0x40000000 附近 | 共享内存 / 内存映射区 | 存放共享内存、内存映射文件(mmap)和共享库。 |
| 栈(Stack) | 存放局部变量和函数调用栈帧,向下增长。 | |
| 0xC0000000 | argv/environ | 存放命令行参数和环境变量。 |
2. 共享内存的核心作用
图中灰色的 “共享内存、内存映射和共享库位于此处” 是关键,它解释了进程间通信的原理:
- 地址映射:进程 A 和进程 B 会将同一块物理内存,映射到各自地址空间的 “共享内存区”。
- 直接访问:两个进程可以直接读写这块内存,无需数据拷贝,是效率最高的 IPC(进程间通信)方式。
- 数据同步:一个进程写入的数据,另一个进程可以立即看到,实现了高效的实时通信。
3. 核心意义
这张图清晰地解释了为什么共享内存是最快的 IPC 方式:
- 数据直接在物理内存中共享,不需要像管道 / 消息队列那样在内核和用户空间之间来回拷贝。
- 共享内存区的地址空间是进程间可共享的,这也是它与堆、栈等私有区域的本质区别。
2.共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto – used by DIPC */
void *shm_unused3; /* unused */
};
3.共享内存函数(详细讲解)
一、shmget ():创建 / 获取共享内存段
1. 基础信息
| 函数原型 | int shmget(key_t key, size_t size, int shmflg); |
| 所需头文件 | <sys/ipc.h> + <sys/shm.h>(C/C++ 通用) |
| 返回值 | 成功返回共享内存标识符(shmid);失败返回-1,设置errno |
| 核心作用 | 创建新的共享内存段,或获取已存在的共享内存段的标识符 |
2. 参数详解
| key | 共享内存的唯一标识(键值):① IPC_PRIVATE(0):创建私有共享内存(仅亲缘进程可用);② 自定义键值(如 0x1234):通过 ftok() 生成唯一键值,供任意进程访问。 | |||
| size | 共享内存段的大小(字节):✅ 建议按系统页大小(getpagesize(),通常 4KB)对齐;❌ 最小为 1 字节,最大受系统限制(/proc/sys/kernel/shmmax)。 | |||
| shmflg | 标志位(权限 + 行为):① 权限位:如 0666(所有用户可读可写),与文件权限规则一致;② 行为位:- IPC_CREAT:不存在则创建,存在则获取;- IPC_EXCL:与 IPC_CREAT 配合,若已存在则报错(确保创建新段);✅ 常用组合:`IPC_CREAT | 0666、IPC_CREAT | IPC_EXCL | 0666`。 |
3. 常见错误码(errno)
| EEXIST | 已存在该共享内存段(使用 IPC_EXCL 时) | 改用 IPC_CREAT 或删除已有段(ipcrm -m shmid) |
| EINVAL | size 无效(过大 / 过小)或 shmid 无效 | 调整 size 为页大小整数倍,检查 shmid |
| ENOMEM | 系统内存不足 | 释放系统内存,减小 size |
| EACCES | 无权限访问 | 调整 shmflg 的权限位(如 0666) |
4. 基础使用示例
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <unistd.h>
using namespace std;
#define SHM_KEY 0x1234 // 自定义键值
#define SHM_SIZE 4096 // 共享内存大小(4KB,页对齐)
int main() {
// 创建共享内存(不存在则创建,存在则获取)
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget failed");
return 1;
}
cout << "共享内存创建/获取成功,shmid = " << shmid << endl;
return 0;
}
二、shmat ():将共享内存映射到进程地址空间
1. 基础信息
| 函数原型 | void *shmat(int shmid, const void *shmaddr, int shmflg); |
| 返回值 | 成功返回共享内存的虚拟地址;失败返回(void*)-1,设置errno |
| 核心作用 | 将内核中的共享内存段,映射到当前进程的用户地址空间(如图中 “共享内存区”) |
2. 参数详解
| shmid | shmget() 返回的共享内存标识符 |
| shmaddr | 指定映射的虚拟地址:① NULL(推荐):由系统自动分配合适的地址;② 非 NULL:指定地址(需对齐,不推荐,移植性差)。 |
| shmflg | 映射标志:① 0:默认,读写权限;② SHM_RDONLY:只读映射(仅能读取,不能写入);③ SHM_REMAP:替换已有映射(慎用)。 |
3. 关键特性
- 映射后,进程可以像操作普通内存一样(*ptr、memcpy)读写共享内存;
- 多个进程映射同一个 shmid,会指向同一块物理内存,实现数据共享;
- 映射地址属于进程私有,但指向的物理内存是共享的。
4. 代码示例
// 映射共享内存(系统自动分配地址,读写权限)
void* shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void*)-1) {
perror("shmat failed");
shmctl(shmid, IPC_RMID, NULL); // 清理共享内存
return 1;
}
cout << "共享内存映射成功,虚拟地址:" << shm_addr << endl;
三、shmdt ():解除共享内存映射
1. 基础信息
| 函数原型 | int shmdt(const void *shmaddr); |
| 返回值 | 成功返回0;失败返回-1,设置errno(EINVAL 表示地址无效) |
| 核心作用 | 将共享内存从进程地址空间中解除映射,不再访问该内存,但共享内存段本身仍存在 |
2. 参数详解
| shmaddr | shmat() 返回的共享内存虚拟地址(必须是有效的映射地址) |
3. 关键特性
- shmdt() 仅解除当前进程的映射,不会删除共享内存段;
- 进程退出时,内核会自动解除映射,但建议手动调用(规范);
- 解除映射后,该地址不可再访问,否则会触发段错误(SIGSEGV)。
4. 代码示例
// 解除共享内存映射
if (shmdt(shm_addr) == -1) {
perror("shmdt failed");
return 1;
}
cout << "共享内存映射已解除" << endl;
四、shmctl ():控制共享内存段(核心管理函数)
1. 基础信息
| 函数原型 | int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
| 返回值 | 成功返回0(或对应信息);失败返回-1,设置errno |
| 核心作用 | 对共享内存段进行管理(获取信息、修改权限、删除段) |
2. 参数详解
| shmid | shmget() 返回的共享内存标识符 |
| cmd | 控制命令(核心):① IPC_STAT:获取共享内存状态,存入 buf;② IPC_SET:修改共享内存属性(如权限),从 buf 读取;③ IPC_RMID:删除共享内存段(最常用);④ SHM_LOCK:锁定共享内存(不换出到交换区);⑤ SHM_UNLOCK:解锁共享内存。 |
| buf | 指向 struct shmid_ds 结构体的指针:① IPC_STAT/IPC_SET:需传入有效指针;② IPC_RMID:可传入 NULL(无需结构体)。 |
3. 核心结构体 shmid_ds(关键字段)
struct shmid_ds {
struct ipc_perm shm_perm; // 权限结构体(uid/gid/模式)
size_t shm_segsz; // 共享内存段大小(字节)
pid_t shm_lpid; // 最后操作该段的进程ID
pid_t shm_cpid; // 创建该段的进程ID
shmatt_t shm_nattch;// 当前映射的进程数
time_t shm_atime; // 最后映射时间
time_t shm_dtime; // 最后解除映射时间
time_t shm_ctime; // 最后修改时间
};
4. 常用场景示例
场景 1:删除共享内存段(最常用)
// 删除共享内存段(所有进程解除映射后,内核释放物理内存)
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
return 1;
}
cout << "共享内存段已删除" << endl;
场景 2:获取共享内存状态
struct shmid_ds shm_info;
// 获取共享内存信息
if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
perror("shmctl IPC_STAT failed");
return 1;
}
cout << "共享内存大小:" << shm_info.shm_segsz << " 字节" << endl;
cout << "当前映射进程数:" << shm_info.shm_nattch << endl;
cout << "创建进程ID:" << shm_info.shm_cpid << endl;
五、辅助函数:ftok ()(生成唯一键值)
1. 基础信息
| 函数原型 | key_t ftok(const char *pathname, int proj_id); |
| 返回值 | 成功返回唯一key_t键值;失败返回-1,设置errno |
| 核心作用 | 根据文件路径和项目 ID,生成唯一的key,供shmget()创建共享内存 |
2. 代码示例
// 生成唯一键值(pathname需是存在的文件,proj_id取1-255)
key_t key = ftok("./test.file", 1);
if (key == -1) {
perror("ftok failed");
return 1;
}
cout << "生成的键值:" << hex << key << endl;
六、共享内存完整实战案例(两个进程通信)
1. 写进程(shm_writer.cpp)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cstdio>
using namespace std;
#define SHM_KEY 0x1234
#define SHM_SIZE 4096
int main() {
// 1. 创建共享内存
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
perror("shmget failed (exist: use IPC_CREAT only)");
// 若已存在,尝试获取
shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget get failed");
return 1;
}
}
cout << "写进程:共享内存shmid = " << shmid << endl;
// 2. 映射共享内存
void* shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void*)-1) {
perror("shmat failed");
shmctl(shmid, IPC_RMID, NULL);
return 1;
}
cout << "写进程:映射地址 = " << shm_addr << endl;
// 3. 写入数据
const char* msg = "Hello from Shared Memory Writer!";
memcpy(shm_addr, msg, strlen(msg)+1); // +1 包含结束符
cout << "写进程:已写入数据:" << msg << endl;
// 4. 等待读进程读取(按回车继续)
cout << "写进程:按回车解除映射并删除共享内存…" << endl;
getchar();
// 5. 解除映射
if (shmdt(shm_addr) == -1) {
perror("shmdt failed");
return 1;
}
// 6. 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
return 1;
}
cout << "写进程:共享内存已删除" << endl;
return 0;
}
2. 读进程(shm_reader.cpp)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cstdio>
using namespace std;
#define SHM_KEY 0x1234
#define SHM_SIZE 4096
int main() {
// 1. 获取共享内存(仅获取,不创建)
int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget failed (no such shm)");
return 1;
}
cout << "读进程:共享内存shmid = " << shmid << endl;
// 2. 映射共享内存
void* shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void*)-1) {
perror("shmat failed");
return 1;
}
cout << "读进程:映射地址 = " << shm_addr << endl;
// 3. 读取数据
char buf[SHM_SIZE] = {0};
memcpy(buf, shm_addr, SHM_SIZE);
cout << "读进程:读取到数据:" << buf << endl;
// 4. 解除映射
if (shmdt(shm_addr) == -1) {
perror("shmdt failed");
return 1;
}
cout << "读进程:映射已解除" << endl;
return 0;
}
编译运行
# 编译
g++ shm_writer.cpp -o shm_writer -std=c++11
g++ shm_reader.cpp -o shm_reader -std=c++11
# 终端1:启动写进程(阻塞等待回车)
./shm_writer
# 终端2:启动读进程(读取数据)
./shm_reader
# 终端1:按回车,写进程删除共享内存
七、共享内存使用注意事项
1. 同步问题(核心坑点)
- 共享内存无内置同步机制,多个进程同时读写会导致数据错乱;
- ✅ 解决:配合信号量(Semaphore)/ 互斥锁(Mutex)实现同步。
2. 资源泄漏
- shmctl(IPC_RMID) 仅标记删除,需所有进程解除映射(shm_nattch=0)后,内核才释放内存;
- ✅ 避坑:确保最后一个进程调用 IPC_RMID,或用 ipcrm -m shmid 手动删除。
3. 查看 / 删除共享内存(命令行)
# 查看所有共享内存
ipcs -m
# 删除指定shmid的共享内存
ipcrm -m shmid
4. 权限问题
- 创建时权限位需合理(如 0666),否则其他进程无法访问;
- ✅ 调整权限:shmctl(shmid, IPC_SET, &shm_info)(修改 shm_perm.mode)。
网硕互联帮助中心



评论前必须登录!
注册