进程间通信重要知识点
一、无名管道:核心概念与基础用法
1. 什么是无名管道?
无名管道(也叫匿名管道,Pipe)是 Linux 中最基础的进程间通信(IPC)方式,本质是内核中的一块内存缓冲区,类比成一条带两个端口的 “水管”:
- 一端是读端(
fd[0]),只能读取数据 - 一端是写端(
fd[1]),只能写入数据 - 管道是半双工通信:同一时间只能单向传输数据(要么父写子读,要么子写父读)
- 数据是字节流形式,写入的数据会被内核按顺序缓存,读取时按先进先出(FIFO)的顺序取出
2. 核心限制(新手必记)
- 只能用于有亲缘关系的进程(父子进程、兄弟进程)通信,因为只有
fork()后子进程能继承父进程的文件描述符,从而拿到管道的读写端 - 管道是 “一次性” 的,数据被读取后就会从内核缓冲区中移除,无法重复读取
- 管道的生命周期随进程:所有持有管道文件描述符的进程都关闭后,内核会自动释放管道资源,无需手动清理
3.基础 API 与流程
// 1. 创建管道 int pipe(int fd[2]); // 参数:存放读写端文件描述符的数组,成功返回0,失败返回-1 // fd[0] → 读端,fd[1] → 写端 // 2. 核心通信流程(父写子读为例) // 父进程:创建管道 → fork子进程 → 关闭读端(fd[0]) → write写数据 → 关闭写端(fd[1]) → wait回收子进程 // 子进程:继承管道fd → 关闭写端(fd[1]) → read读数据 → 关闭读端(fd[0]) → exit退出二、无名管道:通信细节与阻塞特性
1. 读写端的关闭原则(关键!)
管道通信必须遵循 **“单方向单端口”** 原则,否则会出现阻塞或异常:
- 读进程:必须关闭写端(
fd[1]),否则read可能永远阻塞 - 写进程:必须关闭读端(
fd[0]),否则write会一直等待读端读取数据,甚至阻塞 - 原理:管道的内核缓冲区会根据两端的文件描述符状态判断是否还有进程持有端口,从而决定读写的行为
2. 读写的阻塞 / 非阻塞特性
| 场景 | 行为 |
|---|---|
| 管道为空,读进程读数据 | 读进程阻塞,直到有数据写入管道 |
| 管道写端全部关闭,读进程读数据 | read立即返回 0,表示读到文件末尾(EOF) |
| 管道写满,写进程写数据 | 写进程阻塞,直到读进程读取数据释放缓冲区空间 |
| 管道读端全部关闭,写进程写数据 | 写进程会收到SIGPIPE信号,默认直接退出进程 |
3. 进阶场景:双向通信
管道是半双工的,无法直接双向通信。如果需要父子进程互相发送数据,必须创建两个管道:
- 管道 A:父写子读(父进程写端、子进程读端)
- 管道 B:子写父读(子进程写端、父进程读端)
- 两个管道的文件描述符需要分别管理,避免混淆端口
三、无名管道:常见问题与工程实践
1. 常见错误与避坑指南
| 错误场景 | 问题原因 | 解决方案 |
|---|---|---|
read一直阻塞,不返回 | 读进程没有关闭写端,内核认为还有进程可能写入数据 | 读进程必须关闭写端,确保写端全部关闭后,read会读到 EOF 返回 0 |
| 写进程突然崩溃退出 | 读端已经全部关闭,写进程写入时收到SIGPIPE信号 | 写进程处理SIGPIPE信号,或提前判断读端状态 |
| 子进程变成僵尸进程 | 父进程没有调用wait()/waitpid()回收子进程 | 父进程必须等待子进程退出,或设置信号处理回收僵尸进程 |
| 数据丢失或读取不全 | 单次write的数据超过管道的原子写大小(默认 4096 字节) | 分多次写入数据,或使用循环读取直到read返回 0 |
2. 工程实践技巧
- 循环读写:处理大体积数据时,使用循环read直到返回 0,循环write直到数据全部写入
- 错误处理:每次调用pipe/fork/read/write都必须判断返回值,并用perror()打印错误信息
- 端口管理:通信完成后必须关闭所有管道文件描述符,避免文件描述符泄漏
- 进程回收:父进程必须调用wait(NULL)或waitpid()回收子进程,防止产生僵尸进程
四、有名管道基础概念与本质
- 定义:也叫命名管道、FIFO(先进先出),是一种特殊的文件类型(文件类型标记为p),存在于文件系统中,具有可见的路径名。
- 本质:和无名管道底层一样是内核中的环形缓冲区,数据不存磁盘,仅在内存中传输;但通过文件系统的路径名实现了跨进程的可见性。
- 核心作用:实现无亲缘关系进程间的半双工通信,任意有权限的进程都可通过路径名打开管道读写。
五、有名管道核心特点
- 文件系统可见:拥有独立路径名,可通过ls -l查看,生命周期独立于创建进程(除非显式删除)。
- 半双工通信:数据单向流动;如需双向通信,需创建两个独立的 FIFO 管道。
- 先进先出(FIFO):数据按写入顺序读取,无随机访问,不支持lseek()定位。
- 阻塞特性(默认):
以只读模式打开管道:阻塞直到有进程以写模式打开。
以只写模式打开管道:阻塞直到有进程以读模式打开。
管道为空时读操作阻塞,管道满时写操作阻塞。
- 写入原子性:单次写入不超过 PIPE_BUF(通常 4096 字节)时,数据不会被其他写进程打断,保证数据完整性。
- 权限控制:创建时可指定权限,支持chmod修改,只有有权限的进程才能打开。
六、有名管道创建与使用流程
1. 创建有名管道
命令行方式:
mkfifo /tmp/my_pipe # 创建名为my_pipe的管道文件C 语言函数方式:
#include <sys/types.h> #include <sys/stat.h> // 函数原型:创建FIFO文件 int mkfifo(const char *pathname, mode_t mode); // 示例:创建管道,权限为0664(所有者可读写,组和其他可读) if (mkfifo("/tmp/my_pipe", 0664) == -1) { perror("mkfifo failed"); return 1; }参数说明:pathname是管道路径,mode是文件权限(如 0664)。
注意:管道已存在时调用会失败,需提前用access()或stat()检查。
2. 打开管道
使用标准文件操作open(),可指定阻塞 / 非阻塞模式:
// 读进程:以只读模式打开(默认阻塞) int fd_r = open("/tmp/my_pipe", O_RDONLY); // 写进程:以只写模式打开(默认阻塞) int fd_w = open("/tmp/my_pipe", O_WRONLY); // 非阻塞模式:添加O_NONBLOCK标志 int fd_r_nonblock = open("/tmp/my_pipe", O_RDONLY | O_NONBLOCK);3. 读写数据
和普通文件一样使用read()和write():
// 写进程示例:写入数据 char buf[] = "Hello, FIFO!"; write(fd_w, buf, sizeof(buf)); // 读进程示例:读取数据 char recv_buf[100]; read(fd_r, recv_buf, sizeof(recv_buf)); printf("收到数据:%s\n", recv_buf);4. 关闭与删除
- 通信结束后,用close()关闭文件描述符。
- 管道文件不会自动删除,需手动调用unlink("/tmp/my_pipe")删除。
七、阻塞 / 非阻塞模式对比
| 模式 | 打开行为 | 读操作 | 写操作 | 适用场景 |
|---|---|---|---|---|
| 阻塞(默认) | 读 / 写端未打开时阻塞,直到另一端打开 | 管道为空时阻塞 | 管道满时阻塞 | 简单同步通信,保证数据可靠传输 |
| 非阻塞(O_NONBLOCK) | 读端打开时无写端会立即成功;写端打开时无读端会失败 | 管道为空时立即返回 - 1(errno=EAGAIN) | 管道满时立即返回 - 1(errno=EAGAIN) | 异步通信,需循环轮询读写 |
八、有名管道 vs 无名管道
| 特性 | 有名管道(FIFO) | 无名管道(Pipe) |
| 通信范围 | 任意进程(无亲缘关系限制) | 仅父子 / 兄弟进程 |
| 文件系统 | 有可见路径名,永久存在 | 无实体文件,随进程销毁 |
| 半双工 | 支持,可通过两个 FIFO 实现双向 | 半双工,单向通信 |
| 打开方式 | open()打开路径名 | pipe()创建,继承文件描述符 |
| 适用场景 | 不同程序间的进程通信 | 同一程序内父子进程通信 |
九、常见问题与注意事项
- 管道破裂(SIGPIPE 信号):写进程向读端已关闭的管道写数据时,内核会发送
SIGPIPE信号,进程默认会终止。可通过signal()捕获该信号处理。 - 多进程读写:多个写进程同时写入时,单次写入不超过 PIPE_BUF 的数据不会被打断,超过则可能出现数据交叉。
- 非阻塞模式下的错误处理:非阻塞读 / 写失败时,需检查
errno是否为EAGAIN,表示暂时无数据 / 缓冲区满,可稍后重试。 - 权限问题:创建管道时的
mode会受进程 umask 影响,实际权限为mode & ~umask,如需明确权限可手动设置。
十、简单代码示例
写进程(writer.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #define FIFO_PATH "/tmp/test_fifo" int main() { // 1. 创建管道(不存在时创建,存在则忽略错误) if (mkfifo(FIFO_PATH, 0664) == -1 && errno != EEXIST) { perror("mkfifo failed"); exit(EXIT_FAILURE); } // 2. 以只写模式打开管道 int fd = open(FIFO_PATH, O_WRONLY); if (fd == -1) { perror("open failed"); exit(EXIT_FAILURE); } // 3. 写入数据 char msg[] = "Hello from writer!"; write(fd, msg, strlen(msg) + 1); // +1 包含'\0' close(fd); printf("数据已写入管道\n"); return 0; }读进程(reader.c)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #define FIFO_PATH "/tmp/test_fifo" int main() { // 1. 以只读模式打开管道(阻塞等待写端打开) int fd = open(FIFO_PATH, O_RDONLY); if (fd == -1) { perror("open failed"); exit(EXIT_FAILURE); } // 2. 读取数据 char buf[100]; read(fd, buf, sizeof(buf)); printf("收到数据:%s\n", buf); close(fd); unlink(FIFO_PATH); // 删除管道文件 return 0; }💡 补充:使用场景与优缺点
优点
实现简单,基于文件接口,和普通文件操作一致,学习成本低。
支持任意进程间通信,不受亲缘关系限制。
自带同步机制(阻塞模式),无需额外实现互斥锁。
缺点
半双工通信,双向通信需两个管道。
数据无持久化,进程退出或管道关闭后数据丢失。
不支持多进程复杂同步,大规模并发场景性能一般。
十一、信号基本概念
1.本质
内核发给进程的一个事件通知,进程收到后必须暂停当前执行,去处理该事件。
2.特点
- 异步:进程不知道信号何时到来
- 轻量级:不传递复杂数据,只传递 “事件编号”
- 每个信号有固定编号和默认行为
3.信号来源
- 键盘:如 Ctrl+C(SIGINT)、Ctrl+\(SIGQUIT)
- 硬件异常:除零、非法内存访问
- 内核 / 其他进程:通过 kill、raise、alarm 发送
- 软件事件:定时器超时、子进程退出
十二、常用信号(必须记住)
| 信号名 | 编号 | 说明 | 默认动作 |
|---|---|---|---|
SIGINT | 2 | 终端中断Ctrl+C | 终止进程 |
SIGQUIT | 3 | 终端退出Ctrl+\ | 终止 + core 转储 |
SIGKILL | 9 | 强制杀死进程(不可捕获、不可忽略) | 立即终止 |
SIGALRM | 14 | 闹钟信号(alarm()) | 终止进程 |
SIGUSR1 | 10 | 用户自定义信号 1 | 终止进程 |
SIGUSR2 | 12 | 用户自定义信号 2 | 终止进程 |
SIGCHLD | 17 | 子进程退出 / 暂停时发给父进程 | 忽略 |
SIGPIPE | 13 | 向无读端的管道写数据 | 终止进程 |
SIGSTOP | 19 | 暂停进程(不可捕获、不可忽略) | 暂停进程 |
重点:9 号 SIGKILL、19 号 SIGSTOP 不能被捕获、忽略、阻塞。
十三、进程对信号的三种处理方式
- 默认动作(Default)
终止、暂停、忽略、core dump 等。
- 忽略(Ignore)
信号来了直接丢弃,不做处理。
注意:SIGKILL / SIGSTOP 不能忽略。
- 捕获(Catch / 自定义处理)
进程注册一个信号处理函数,信号到来时自动调用该函数。
十四、信号相关核心函数
1. signal () — 注册信号处理函数
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);handler可以是:
- SIG_IGN:忽略
- SIG_DFL:恢复默认
- 自定义函数名:捕获处理
示例:
// 捕获 SIGINT,执行自己的函数 signal(SIGINT, handler_func);2. kill () — 进程发信号
int kill(pid_t pid, int signum);- pid > 0:发给指定 PID
- pid = 0:发给同组所有进程
- pid = -1:发给所有有权限发送的进程
- pid < -1:发给组 ID 为 -pid 的进程组
3. raise () — 自己给自己发信号
int raise(int signum); // 等价于 kill(getpid(), signum);4. alarm () — 定时发送 SIGALRM
unsigned int alarm(unsigned int seconds);- 秒数后内核向当前进程发
SIGALRM - 再次调用会覆盖旧定时器
5. pause () — 挂起等待信号
int pause(void);- 进程阻塞,直到收到任意信号
- 信号处理完后,pause 返回 -1,errno=EINTR
十五、信号生命周期
- 产生:内核 / 进程发送信号
- 递达(Delivery):信号到达进程并被处理
- 未决(Pending):信号已产生但还没处理(被阻塞时)
- 阻塞(Block / 信号屏蔽字):暂时不让信号递达,先存为未决
十六、信号集与阻塞(重点)
1. 信号集操作函数
int sigemptyset(sigset_t *set); // 清空 int sigfillset(sigset_t *set); // 全部置1 int sigaddset(sigset_t *set, int signo);// 添加信号 int sigdelset(sigset_t *set, int signo);// 删除信号 int sigismember(const sigset_t *set, int signo);// 判断是否在集合2. sigprocmask () — 屏蔽 / 解除屏蔽信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how:
SIG_BLOCK:屏蔽 set 中的信号(叠加)SIG_UNBLOCK:解除屏蔽SIG_SETMASK:直接设置为 set
3. sigpending () — 获取未决信号集
int sigpending(sigset_t *set);十七、信号处理的重要特性
- 信号会打断阻塞的系统调用如 read、wait、pause 等,被信号打断后返回 -1,errno=EINTR。
- 信号处理函数要保证可重入不能调用不可重入函数:malloc、printf、非线程安全的全局变量等。
- 普通信号不排队同一信号多次触发,只记录一次,不会排队。
- 实时信号(34 以上)支持排队考试 / 面试一般只考标准信号(1~31)。
十八、典型面试问答
- 信号能不能传递大量数据?不能。信号只传事件,不传数据。要传数据用管道、消息队列、共享内存。
- 哪些信号不能捕获和忽略?SIGKILL(9)、SIGSTOP(19)。
- 子进程退出时父进程收到什么信号?SIGCHLD,默认忽略。
- 向没有读端的管道写数据会产生什么信号?SIGPIPE,默认终止进程。
