2. IPC基础
在Linux与Android系统中,进程之间天然是隔离的:每个进程拥有独立的虚拟地址空间、独立的文件描述符表和独立的权限上下文。一个进程无法直接读写另一个进程的变量,也无法直接调用另一个进程的函数。所谓进程间通信(IPC,Inter-Process Communication),就是操作系统为进程提供的、用于交换数据与同步行为的受控通道。理解IPC,是理解Binder驱动存在意义的前提。
一、IPC要解决的四个基本问题
无论哪种IPC机制,都必须回答以下问题:
- 寻址:发送方如何唯一标识接收方?是文件名、key值、端口号,还是句柄?
- 数据传输:数据是经过内核缓冲区中转,还是通过映射内存直接共享?
- 同步与通知:接收方如何知道数据到达?是阻塞等待、轮询,还是事件唤醒?
- 权限与安全:内核如何校验发送方是否有权向该接收方发送数据?
这四点决定了IPC机制的性能上限与适用场景。下面这张图概括了四类常见IPC机制的数据流向与内核参与程度。
二、四类IPC机制的原理与特点
1. 管道(Pipe / FIFO)
管道是最古老的IPC形式。匿名管道由pipe()创建,只能用于有亲缘关系的进程;命名管道FIFO由mkfifo()创建,以文件路径标识,可用于任意进程。数据写入内核环形缓冲区,读端按字节流顺序读取。管道是半双工的,且缓冲区满时写阻塞、空时读阻塞,天然完成同步。
# 命令行演示:匿名管道
ls -l | grep ".so"
# 命名管道
mkfifo /tmp/myfifo
echo "hello" > /tmp/myfifo &
cat /tmp/myfifo
2. 消息队列(Message Queue)
System V消息队列与POSIX消息队列都允许进程以“消息”为单位收发数据,消息带有类型字段,接收方可按类型选择性读取。相比管道,它保留了消息边界,不会出现“粘包”,但每条消息仍需两次拷贝,且队列容量受内核参数限制。
3. 共享内存(Shared Memory)
共享内存通过shmget/shmat或mmap将同一块物理内存映射到多个进程的地址空间。一旦映射完成,读写就像访问本地内存一样,没有内核拷贝,是性能最高的IPC方式。代价是内核不再介入数据同步,必须配合信号量、文件锁或原子操作来避免竞态。
4. Socket
Socket最初为网络通信设计,但通过AF_UNIX域也可用于本机进程通信。它提供面向连接(TCP/流式)与无连接(UDP/数据报)两种语义,可跨主机,是唯一能同时覆盖本机与网络的IPC机制。代价是协议栈处理开销大,数据要经过发送缓冲、接收缓冲多次拷贝。
三、优缺点对比表
| 机制 | 寻址方式 | 数据边界 | 拷贝次数 | 同步 | 跨主机 | 主要缺点 |
|---|---|---|---|---|---|---|
| 管道/FIFO | 文件描述符/路径 | 字节流 | 2 | 内核阻塞 | 否 | 半双工、容量小、无消息边界 |
| 消息队列 | key / 队列名 | 有消息边界 | 2 | 内核阻塞 | 否 | 容量受限、大消息效率低 |
| 共享内存 | key / 映射地址 | 无 | 0 | 需用户态同步 | 否 | 竞态风险高、生命周期管理复杂 |
| Socket | IP:端口 / 路径 | 流式或数据报 | 2~4 | 内核阻塞 | 是 | 开销大、延迟高于共享内存 |
四、典型场景与选型思路
假设要开发一个日志采集服务:采集进程持续产生日志,分析进程需要实时消费。若两者是父子进程且数据量小,管道足够;若需要按日志级别分类读取,消息队列更合适;若日志量极大且要求微秒级延迟,共享内存加环形缓冲是首选;若分析进程可能部署在另一台机器,则必须使用Socket。
五、常见误区与复盘
- 误区一:共享内存一定最快。零拷贝只省去数据搬运,若同步逻辑设计不当,锁竞争和缓存一致性开销可能反超消息队列。
- 误区二:管道只能用于父子进程。匿名管道确实如此,但FIFO以路径标识,任意进程均可打开。
- 误区三:Socket只能走网络。
AF_UNIX域Socket在本机通信时绕过网络层,性能远高于TCP回环。 - 误区四:消息队列不会丢消息。队列满时发送方可能阻塞或返回错误,必须处理
EAGAIN与背压。