Skip to content

管道、消息队列与套接字

基于通用操作系统概念 · 核于 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/msgrcv vs mq_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) 给目标进程发一个信号编号,目标进程无论正在执行什么,都会被中断,跳转到预先注册的信号处理函数,处理完再返回原处。

信号编号含义可捕获
SIGINT2Ctrl+C 中断
SIGKILL9强制杀进程❌(内核直接处理)
SIGTERM15请求终止(可被 handler 拦截做清理)
SIGSTOP19暂停进程
SIGCHLD17子进程退出通知父进程
SIGSEGV11段错误(非法内存访问)✅(但通常仍会终止)
  • 异步:信号可能在程序执行任意指令时到达(除了某些不可中断的内核操作)。
  • 只传编号:标准信号不带数据,只表达"事件发生了"。sigqueue 可附带一个整数,但用得少。
  • 不可捕获的信号:SIGKILL/SIGSTOP 无法被拦截/忽略——这是 OS 留的"最后手段",保证一定能控制进程。
  • 不是好的数据通道:信号适合通知(如"配置变了请重载"),不适合传大量数据——频繁信号会打断逻辑、可重入性差。

下一步

数据传递类讲完后,下一步看共享内存与 RPC——最快的 IPC(共享内存)为何要配信号量、mmap 的用法、RPC 如何让跨机调用透明化,以及所有 IPC 机制的完整对比表。