IPC优化与历史总结

本文最后更新于 2026年6月14日 凌晨

功能实现阶段

最早期的 IPC 更多是为了解决功能问题,也就是让不同进程之间可以交换数据或信号。

经典结构包括:

  • Pipe
  • Signal
  • Message Queue
  • Shared Memory

这些模型现在依然被使用,只是不同模型适合不同场景。

最基础的通信路径可以抽象成:

1
2
3
4
5
Process A
↓
Kernel IPC Mechanism
↓
Process B

这一阶段的重点还不是极致性能,而是让隔离的进程能够安全通信。

MicroKernel 与 IPC

后来,Mach 提出:

Everything is Message

根据这个理念,MicroKernel 开始把很多传统内核服务拆到用户态。

原来在 Monolithic Kernel 中,路径通常是:

1
2
3
4
5
Application
↓
System Call
↓
Kernel Service

而在 MicroKernel 中,很多服务变成:

1
2
3
4
5
6
7
8
9
Application
↓
IPC
↓
User Space Server
↓
IPC
↓
Application

也就是说,原来一次内核内部函数调用,可能变成了跨地址空间的消息传递。

这个设计让系统结构更清晰,也更容易隔离不同服务。但早期测试时性能并不理想,核心原因是 IPC 太贵。

主要开销包括:

  • 上下文和地址空间切换
  • 数据复制
  • 调度器参与
  • Kernel Entry / Exit

因此,MicroKernel 的性能问题逐渐集中到了 IPC 性能上。

L4 的优化

后来 L4 的作者 Jochen Liedtke 发现,微内核的主要性能瓶颈就是 IPC,于是开始系统性优化 IPC。

优化目标主要是减少 Copy、减少 Scheduler 参与、减少不必要的 Queue 操作,并让常见 IPC 走 Fast Path。

区分 Short /Long Path

L4 发现,大部分 IPC 消息都很短,并且 Receiver 往往已经在等待。

如果这种场景仍然走完整路径,就会浪费很多调度和队列维护成本。

因此,L4 将 IPC 区分为:

1
2
3
4
5
6
7
Short IPC
↓
Fast Path

Long IPC
↓
Slow Path

Short IPC 用于处理最常见的小消息场景,尽可能跳过复杂逻辑;Long IPC 则处理更复杂、更少见的情况。

值的传递

对于小消息,L4 直接使用寄存器传递:

1
2
3
4
5
Sender Register
↓
Kernel Fast Path
↓
Receiver Register

这样可以避免把消息先复制到内核缓冲区,再从内核缓冲区复制到接收方。

对于大消息,则可以通过页表映射减少复制:

1
2
3
4
5
Sender Page
↓
Page Mapping
↓
Receiver Address Space

核心就是让数据尽量不要经过额外的内核缓冲区复制。

Scheduler 相关

如果内核已经知道 Receiver 正在等待,那么 IPC 时可以直接切到对应的 Server。

Fast Path 可以变成:

1
2
3
4
5
Sender
↓
Kernel
↓
Receiver

这样可以减少调度器参与。

此外,L4 还使用 Lazy Scheduling 减少等待队列修改。

Lazy Scheduling 的思路是:

1
2
3
4
5
6
7
Thread 状态变化
↓
先只修改 Thread State
↓
真正需要调度时
↓
再清理或更新 Queue

核心是把线程状态变化和调度队列维护解耦,让短时间重复进出的操作不必立刻反映到队列上。

但以上的优化还没有解决 Context Switch 的问题。

Shared Memory 与 Lock-Free Queue

即使单次 IPC 已经很快,频繁通信仍然会产生:

  • Kernel Entry
  • Context Switch
  • Scheduler 开销
  • Message Copy

因此,后续高性能 IPC 开始采用 Shared Memory + Notification。

数据不再通过每次 IPC 复制,而是放在共享内存中:

1
2
3
4
5
Sender
↓
Shared Ring Buffer
↓
Receiver

发送方和接收方通常只需要维护 head 和 tail。在 SPSC 等简单场景中,可以使用原子变量实现 Lock-Free Queue,从而减少锁竞争。

其核心变化是:

1
2
3
4
5
6
原来:
IPC = 数据传递 + 通知

后来:
Shared Memory = 数据传递
IPC / Interrupt / eventfd = 通知

Event-driven:从阻塞等待变成事件通知

传统阻塞式模型中,每个线程会等待自己的 I/O:

1
2
3
4
5
6
7
Thread
↓
read()
↓
Block
↓
Wakeup

大量连接通常意味着大量线程以及大量 Context Switch。

epoll 将其改成事件驱动模型:

1
2
3
4
5
注册感兴趣的 fd
↓
epoll_wait()
↓
内核返回 Ready fd

应用不需要为每个连接创建一个阻塞线程,而是可以使用少量线程处理大量事件。

这一阶段主要减少了:

  • 阻塞线程数量
  • 无效轮询
  • 线程上下文切换
  • 调度器压力

不过,epoll 只表示某个 I/O 已经 Ready。应用被唤醒后,通常仍然需要调用:

1
2
read();
write();

来真正执行 I/O。

io_uring:提交与完成解耦

io_uring 进一步把 I/O 的提交与完成拆开:

1
2
3
4
5
6
7
8
9
Application
↓
Submission Queue
↓
Kernel 执行 I/O
↓
Completion Queue
↓
Application

其中,SQ 是 Submission Queue,CQ 是 Completion Queue。用户态和内核共享这些 Ring Buffer。

应用可以批量提交多个请求:

1
2
3
4
5
Request 1
Request 2
Request 3
↓
一次提交

内核完成后,再批量写入 CQ。

其主要优化包括:

1
2
3
4
5
6
7
8
9
10
11
逐请求 System Call
↓
共享队列与批量提交

同步等待结果
↓
异步 Completion

每个请求单独唤醒
↓
批量处理完成事件

这与 Shared Memory + Notification 的模式非常相似:SQ / CQ 负责传递数据,系统调用或内核线程负责通知和处理。

Async Runtime:从线程调度转向任务调度

Tokio 等异步运行时将操作系统提供的 I/O 事件转化为 Future 和 Waker。

当 I/O 尚未完成时:

1
2
3
4
5
6
7
Future::poll()
↓
Poll::Pending
↓
保存 Waker
↓
任务暂停

当 epoll 或 io_uring 返回事件时:

1
2
3
4
5
6
7
I/O Event
↓
Waker::wake()
↓
任务重新进入 Runnable Queue
↓
Future 再次被 poll

因此,一个等待 I/O 的任务不需要一直占用一个操作系统线程。

原来的模型是:

1
一个并发任务 ≈ 一个阻塞线程

异步运行时中则变成:

1
2
3
大量 Future
↓
复用少量 Worker Thread

优化重点也从 Process / Thread Context Switch 转向 Future 状态切换和用户态任务调度。

不过,异步运行时并没有完全消灭调度,而是把一部分调度从内核移动到了用户态。

Embassy:中断直接唤醒 Future

在 MCU 或裸机环境中,通常没有:

  • epoll
  • io_uring
  • 完整的操作系统线程

Embassy 直接使用硬件中断作为异步事件源。

Linux 异步运行时中的路径通常是:

1
2
3
4
5
6
7
8
9
10
11
Hardware Interrupt
↓
Kernel
↓
epoll / io_uring
↓
Runtime
↓
Waker
↓
Future

Embassy 中则可以缩短成硬件中断直接唤醒 Future:

1
2
3
4
5
Hardware Interrupt
↓
Waker
↓
Future

因此,Embassy 减少了操作系统层次的参与,也适合静态内存和资源有限的嵌入式环境。

异步 IPC 优化主线

整体演进可以总结为:

1
2
3
4
5
6
7
8
9
10
11
12
13
L4 / seL4 优化单次 IPC Fast Path
↓
Shared Memory 减少数据 Copy
↓
Lock-Free Queue 减少 Lock 与竞争
↓
epoll 用 Ready Event 替代大量阻塞线程
↓
io_uring 用共享 SQ/CQ 解耦提交与完成
↓
Tokio 用 Future/Waker 替代一任务一线程
↓
Embassy 用硬件中断直接唤醒 Future

这些技术处于不同的系统层次,但优化目标基本一致:

1
2
3
4
5
6
7
减少 Copy
减少 Lock
减少 System Call
减少 Context Switch
减少 Scheduler 参与
减少无效 Wakeup
增加 Batch Processing

从 L4 到现代异步运行时,优化重点逐渐发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
最初:
让一次 IPC 更快

后来:
尽量减少 IPC 次数

再后来:
尽量避免阻塞线程和内核调度

最终:
使用共享内存、事件和状态机
完成异步的数据交换与任务执行

IPC优化与历史总结
https://chenxizhou233.github.io/posts/261f074f.html
作者
Xizhou Chen
发布于
2026年6月8日
许可协议