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

发送方和接收方通常只需要维护 headtail。在 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

任务暂停

epollio_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日
许可协议