管道、消息队列与套接字
基于通用操作系统概念 · 核于 2026-08
速查
- 匿名管道(pipe):
pipe()返回读/写两个 fd,半双工字节流,只能父子进程间用(fork 继承 fd),写满/读空会阻塞,随进程消亡。 - 命名管道(FIFO):
mkfifo在文件系统建一条带路径的特殊文件,任意进程可 open,仍是半双工字节流——用于无亲缘关系的进程。 - 消息队列:内核维护带类型的结构化消息链表,
msgsnd/msgrcv收发,有消息边界(不像 pipe 字节流),可按类型筛选。 - 套接字(socket):最通用 IPC——本机用 UNIX domain socket(文件路径,比 TCP 快),跨机用 TCP/UDP(IP+端口),是网络通信根基。
- 信号(signal):异步软件中断,
kill(pid, sig)发送,目标进程被中断跳到处理函数——SIGINT(Ctrl+C)/SIGKILL(强杀)/SIGCHLD(子进程退出),只传编号不传数据。 - 管道 vs 消息队列:管道是无边界字节流(两次 write 可能被一次 read 合并读出);消息队列保留边界(一条一条取)。
- pipe 半双工:单向,要双向得开两条 pipe;FIFO 同理。
- 阻塞语义:管道/消息队列读写默认阻塞(写满等读、读空等写);socket 可设
O_NONBLOCK配合 select/epoll 做异步。 - 管道容量:Linux 默认 64KB 内核缓冲,写满后 write 阻塞直到被读走。
- SIGKILL/SIGSTOP 不可捕获:内核直接处理,进程无法自定义 handler(保证一定能杀掉进程)。
- 跨机能力:管道/FIFO/消息队列只本机;socket 与 RPC 才能跨机。
一、匿名管道:父子进程的半双工通道
匿名管道由 pipe() 系统调用创建,返回一对文件描述符 fd[0](读端)和 fd[1](写端)。它是半双工的——数据只能从 fd[1] 流向 fd[0]。匿名管道没有名字,只能通过 fork 让子进程继承 fd 来共享,所以只在有亲缘关系的进程间可用。
父进程
pipe(fds) → fds = {读端 r, 写端 w}
fork() → 子进程也拿到同样的 {r, w}
↓
父进程 close(r),写 w 子进程 close(w),读 r
"hello" ────────字节流──────→ read 出 "hello"
(内核缓冲,默认 64KB)- 半双工:单向。要双向通信就开两条 pipe(一条父→子,一条子→父)。
- 字节流无边界:父进程两次
write("ab", "cd"),子进程可能一次read读出"abcd"——管道不保留写入边界。 - 阻塞:写满(缓冲满)时 write 阻塞,读空时 read 阻塞;所有写端关闭后 read 返回 0(EOF)。
- 生命周期:所有 fd 关闭后管道消失,随进程消亡。
- shell 的
|:ls | grep就是用 pipe 实现——shell fork 两个进程,把 ls 的 stdout 接到 grep 的 stdin。
二、命名管道 FIFO:任意进程可用的管道
命名管道(FIFO)用 mkfifo 在文件系统里创建一条特殊文件(有路径名),任何有权限的进程都能 open 它来读写。它解决了匿名管道"只能父子用"的限制:
$ mkfifo /tmp/myfifo # 建一条命名管道(文件系统里有名字)
# 进程 A 进程 B
fd = open("/tmp/myfifo", O_WRONLY)
write(fd, data, n) ──────────→ fd = open("/tmp/myfifo", O_RDONLY)
read(fd, buf, n)- 任意进程:只要能访问该路径就能通信,无需亲缘关系。
- 仍是半双工字节流:和匿名管道一样,要双向开两条 FIFO。
- open 会阻塞:以只读打开会阻塞直到有进程以只写打开(反之亦然)——保证双方都在场。
- 持久:FIFO 文件本身留在文件系统,
unlink才删除(但管道内的数据仍随进程消亡,不持久化到磁盘)。
三、消息队列:有边界的结构化消息
消息队列是内核维护的消息链表。每条消息由类型(long)和数据组成。发送方 msgsnd 把消息挂到队尾,接收方 msgrcv 按类型筛选取出。与管道最大的区别是保留消息边界——发三条收三条,不会粘连。
c
struct my_msg {
long mtype; // 消息类型(必须 > 0)
char mtext[100]; // 数据
};
msgsnd(qid, &msg, len, IPC_NOWAIT); // 发
msgrcv(qid, &msg, len, type, 0); // 收,type 指定取哪类- 有边界:一条一条存取,不像 pipe 的字节流。
- 按类型筛选:
msgrcv可以只取mtype == 3的消息,实现优先级/分类。 - 无亲缘限制:任意进程用同一个 queue id(key)即可。
- 生命周期:消息队列由内核持有,进程退出后仍在,直到显式
msgctl(IPC_RMID)或系统重启(这点要小心,容易残留)。 - System V vs POSIX:两套 API(
msgsnd/msgrcvvsmq_send/mq_receive),POSIX 更现代但兼容性 System V 更广。
四、套接字:从本机到跨机的通用 IPC
套接字是最通用的 IPC,由 socket() 创建一个通信端点。它的强大在于双模:既可本机通信(UNIX domain socket),也可跨机通信(TCP/UDP)。
本机(UNIX domain socket): 跨机(TCP/UDP):
socket(AF_UNIX, ...) socket(AF_INET, ...)
bind("/tmp/sock") bind("10.0.0.1:8080")
不走协议栈,比 TCP 快 要经过 TCP/IP 协议栈- 本机首选:进程间同机通信,UNIX domain socket 比 TCP 快(不经过协议栈、不计算校验和),docker/MySQL/X11 都用它。
- 跨机:TCP(可靠字节流)/UDP(不可靠数据报),是所有网络应用的基础——HTTP、RPC、WebSocket 都跑在 socket 上。
- 全双工:socket 天生双向,不像 pipe 要开两条。
- 可阻塞/非阻塞:默认阻塞,设
O_NONBLOCK后配合select/poll/epoll可实现单线程管理大量连接(IO 多路复用,nginx/Redis 的核心)。 - 代价:跨机 socket 要经过协议栈(分包、校验、拥塞控制),是 IPC 里最慢的。
五、信号:异步通知机制
信号(signal)是一种异步通知——内核或进程用 kill(pid, sig) 给目标进程发一个信号编号,目标进程无论正在执行什么,都会被中断,跳转到预先注册的信号处理函数,处理完再返回原处。
| 信号 | 编号 | 含义 | 可捕获 |
|---|---|---|---|
| SIGINT | 2 | Ctrl+C 中断 | ✅ |
| SIGKILL | 9 | 强制杀进程 | ❌(内核直接处理) |
| SIGTERM | 15 | 请求终止(可被 handler 拦截做清理) | ✅ |
| SIGSTOP | 19 | 暂停进程 | ❌ |
| SIGCHLD | 17 | 子进程退出通知父进程 | ✅ |
| SIGSEGV | 11 | 段错误(非法内存访问) | ✅(但通常仍会终止) |
- 异步:信号可能在程序执行任意指令时到达(除了某些不可中断的内核操作)。
- 只传编号:标准信号不带数据,只表达"事件发生了"。
sigqueue可附带一个整数,但用得少。 - 不可捕获的信号:SIGKILL/SIGSTOP 无法被拦截/忽略——这是 OS 留的"最后手段",保证一定能控制进程。
- 不是好的数据通道:信号适合通知(如"配置变了请重载"),不适合传大量数据——频繁信号会打断逻辑、可重入性差。
下一步
数据传递类讲完后,下一步看共享内存与 RPC——最快的 IPC(共享内存)为何要配信号量、mmap 的用法、RPC 如何让跨机调用透明化,以及所有 IPC 机制的完整对比表。