OS04 抽象-进程间通信
type
status
date
slug
summary
tags
category
icon
password
抽象:IPC、管道和套接字
在操作系统中,进程间通信(Inter-Process Communication, IPC)和网络通信是两个核心概念。它们的共同点在于:都被封装成文件 I/O 的形式,使得开发者可以使用熟悉的
read()
、write()
、close()
等系统调用进行操作。这种统一的抽象极大简化了编程模型,体现了 Unix “一切皆文件”的设计哲学。进程间通信(IPC)
进程是操作系统对运行中程序的抽象。为了保障安全性和隔离性,每个进程拥有独立的地址空间,默认无法直接访问其他进程的内存。然而,现实中的任务往往需要多个进程协同完成(例如:一个进程生成数据,另一个进程处理数据),这就催生了对 IPC 机制的需求。
使用普通文件进行通信虽然可行,但存在明显缺陷:
- 文件是持久化的,每次读写都涉及磁盘 I/O,开销大;
- 若通信内容是瞬态(transient)的(如临时消息传递),则无需持久保存,使用文件反而低效。
因此,操作系统在内存中提供了一种轻量级的通信机制——管道(Pipe),它通过内核维护的缓冲区实现进程间的数据传递,避免了磁盘访问,显著提升了性能。
管道(Pipe)
管道是一种单向的 IPC 机制,基于先进先出(FIFO)的缓冲区实现。其核心特性如下:
- 缓冲区管理:
- 当写入端尝试向已满的缓冲区写入时,写入进程会被阻塞,直到有空间可用;
- 当读取端尝试从空缓冲区读取时,读取进程会被阻塞,直到有数据到达。
- 系统调用接口:
- 使用
int pipe(int fd[2])
创建管道; fd[0]
是读端,fd[1]
是写端;- 数据从
fd[1]
写入,从fd[0]
读出; - 管道有固定大小(通常为 64KB,具体取决于系统);
- 使用完毕后,必须显式关闭两个文件描述符,否则可能导致资源泄漏或死锁。
- 关闭语义:
- 当所有写端关闭后,后续对读端的
read()
将返回 0(即 EOF); - 当所有读端关闭后,向写端的
write()
会触发SIGPIPE
信号(默认终止进程)。
以下是一个典型的父子进程通过管道通信的示例:
注意:实践中应始终关闭不需要的文件描述符,以避免资源浪费和意外行为。
管道的通信协议
管道本身只提供字节流传输,不包含结构化语义。因此,通信双方必须约定协议,包括:
- 语法(Syntax)
- 数据的格式(如:定长消息、以
\\n
分隔的文本、JSON 等); - 消息的顺序(如:先发长度,再发内容)。
- 语义(Semantics)
- 收到特定消息后应执行的操作;
- 错误处理机制(如超时、校验失败等)。
这类协议常通过状态机建模。在更复杂的场景中(如跨语言、跨平台调用),还需解决数据表示差异(如字节序、对齐方式),这正是远程过程调用(RPC)要解决的问题——它在底层通信之上封装了函数调用的语义。
网络通信
网络通信扩展了 IPC 的概念,使不同机器上的进程也能协同工作。主流模型包括:
- 客户端-服务器(Client-Server):服务器长期运行,客户端按需连接;
- 对等网络(Peer-to-Peer, P2P):所有节点既是客户端也是服务器。
本节重点讨论基于 TCP 的可靠通信。
套接字(Socket)
套接字是网络通信的核心抽象,首次在 4.2BSD 中引入。它将网络端点(endpoint)视为一种特殊的“文件”,支持
read()
、write()
等操作,但不支持随机访问(如 lseek()
无效)。关键特性:
- 每个套接字关联两个内核队列:
- 发送队列:
write()
将数据放入此队列,由协议栈异步发送; - 接收队列:
read()
从此队列取出已到达的数据。
- 套接字屏蔽了底层网络细节,提供统一的 I/O 接口。
由于网络传输的是无结构的字节流,应用层需自行处理:
- 分块(如按消息边界解析);
- 数据翻译(如序列化/反序列化);
- 可借助 RPC 框架(如 gRPC)自动完成这些工作。
TCP 套接字
TCP(Transmission Control Protocol)提供面向连接、可靠、保序的字节流服务,适用于对数据完整性要求高的场景。
TCP 的核心假设
- 所有写入的数据最终都会完整送达接收方;
- 数据按发送顺序被接收。
命名与寻址
TCP 使用 IP 地址 + 端口号 唯一标识一个通信端点:
组件 | 说明 |
主机名 | 如 example.com ,通过 DNS 解析为 IP |
IP 地址 | IPv4(如 192.168.1.1 )或 IPv6(如 2001:db8::1 ) |
端口号 | 16 位整数,范围 0–65535:<br>• 0–1023:知名端口(如 HTTP=80, SSH=22)<br>• 1024–49151:注册端口(用户程序可申请)<br>• 49152–65535:动态/私有端口(由 OS 自动分配) |
连接标识
一个 TCP 连接由五元组唯一确定:
服务器与客户端的角色
服务器端
服务器需管理两类套接字:
- 监听套接字(Listening Socket)
- 通过
socket()
创建,bind()
绑定到特定端口,listen()
开始监听; - 不可用于数据传输,仅用于接受新连接;
- 调用
accept()
时,内核会为每个新连接创建一个新的连接套接字。
- 连接套接字(Connection Socket)
- 由
accept()
返回; - 用于与特定客户端进行
read()
/write()
通信。
客户端
- 端口通常由操作系统自动分配(从动态端口范围中选取);
- 通过
connect()
主动连接服务器。
典型调用流程
- 客户端:
socket()
→ connect()
→ write()
/read()
→ close()
- 服务器:
socket()
→ bind()
→ listen()
→ accept()
→ write()
/read()
→ close()
并发处理
为支持多客户端,服务器需并发处理连接。常见方案包括:
- 多进程:
fork()
为每个连接创建子进程;
- 多线程:为每个连接创建新线程;
- 线程池:预创建一组工作线程,从连接队列中领取任务(更高效,避免频繁创建/销毁线程)。
现代高性能服务器(如 Nginx)常结合 I/O 多路复用(如 epoll)与线程池,实现高并发、低延迟的服务。
Prev
OS03 抽象-文件与I/O
Next
OS05 并发与同步
Loading...