Chromium 如何实现 IPC
2025-08-06
Chromium 最具标识的架构特点,便是:多进程架构。
浏览器进程、渲染进程、GPU 进程等,各司其职,同时借助操作系统提供的进程隔离性(Isolation),确保浏览器的健壮与安全。
每个进程都有独立的虚拟内存空间,数据共享要借助 IPC(进程间通信,Inter-process Communication)。
这很符合 Golang 哲学:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。
作为经典面试题,常见的 IPC 有以下几种:
- 信号(Signal)
- 信号量(Semaphores)
- 管道(Pipe)
- 共享内存(Share Memory)
- 消息队列(Message Queue)
- 套接字(Socket)
应用在工程上需要一定程度的封装(Encapsulation)。Chromium 构建的 IPC 框架,就是 Mojo。
Mojo 的应用范围相对较窄,只在 Chromium 中用到,因此非 Chromium 开发者不必花太多时间钻研。简单了解其产生背景,实现,及设计哲学即可。
1. 背景
在 Chromium 发展早期,使用的是一套叫 IPC::Channel 的 IPC 框架。
这套方案存在以下三个问题:
- 性能不佳,序列化和反序列化的效率低下
- 缺乏类型系统,容易出错
- 缺少跨平台和跨语言的支持
Mojo 的诞生,就是为了解决这些问题。它更现代、类型安全、且方便扩展。
2. 使用
在 Chromium 代码库中,经常会看到 .mojom
为后缀的文件,这便是 Mojo 的核心:接口定义语言(Interface Definition Language, IDL)🔗。
类似 Protocol Buffers 的 .proto
文件,它可以定义接口、结构体、枚举和类型,并生成对应的跨平台、跨语言的代码。
举个例子:
module example.mojom;
interface Greeter { Greet(string name) => (string message);};
这里定义了一个 Greeter
接口,指定传参和返回值。
基于这份 mojom 定义,Mojo 可将其翻译为 C++(或 JS、Java、Rust 等)可调用的代码,并生成进程间通信所需要的序列化、路由方法等。
Chromium 的 GN 构建系统内置了一套 mojom 的处理方案。可以这样在 BUILD.gn 中声明:
mojom("interfaces") { sources = [ "example.mojom" ]}
生成以下文件:
example.mojom.h
– 接口定义的 C++ 头文件example.mojom.cc
– 消息的打包、解包实现example.mojom-shared.h/cc
– 公共类型定义example.mojom-forward.h
– 前向声明
这些代码中包含客户端 Proxy、服务端 Stub 以及中间的数据传输结构体。
如果开发者想使用,只需引入头文件,调用相关方法,即可与其他进程通信,就像是调用本地方法一般。Mojo 屏蔽掉了内部的复杂性。
3. 实现
Mojo 的实现,可以分为 编译 + Runtime 两个部分。
通过 mojom_bindings_generator🔗 工具链将 mojom 文件翻译成接口文件后,真正负责通信的是 Runtime。
核心组件如下:
组件 | 作用 |
---|---|
mojo::MessagePipe | 双向、全双工、异步的消息管道(进程内外均可用) |
mojo::Connector | 把 MessagePipe 和接口连接起来(路由) |
mojo::InterfacePtr | 客户端接口,负责发送请求 |
mojo::Binding / mojo::Receiver | 服务端绑定,用于接收请求 |
mojo::PendingReceiver/Remote | Modern Mojo 使用的延迟绑定结构 |
mojo::Dispatcher | 管理句柄(Handle)和其生命周期 |
看一下完整的调用过程。
greeter_remote_->Greet("Alice", callback);
-
编译生成的
GreeterProxy::Greet()
被调用void GreeterProxy::Greet(const std::string& name, GreetCallback callback) {mojo::Message message;SerializeParams(name, &message);receiver_->AcceptWithResponder(message, std::move(callback));}- 参数被序列化为
mojo::Message
对象 receiver_
是绑定到MessagePipe
的通信接口
- 参数被序列化为
-
消息通过
MessagePipe
传输mojo::MessagePipe
是一个轻量的、全双工的、异步的消息传输通道,消息通过 Socket、共享内存、平台句柄(如 Windows HANDLE)等方式传输。 -
服务端的
Stub::AcceptWithResponder()
被调用Mojo 运行时将消息路由到绑定的
GreeterStub
。bool GreeterStub::AcceptWithResponder(Message* message, Responder* responder) {std::string name;DeserializeParams(message, &name);impl_->Greet(name, BindResponder(responder));}- 参数反序列化为
std::string
- 调用真正实现类
impl_
的Greet()
方法
- 参数反序列化为
-
响应给客户端并调用
callback.Run("Hello, Alice");消息被封装成
mojo::Message
并写入MessagePipe
,再由客户端 Proxy 解包,调用用户的 Callback。
Mojo Core 是 IPC 的关键:
- 一个用户态 C++ 库,Chromium 在启动时会初始化(位于
mojo/core/
) - 每个进程有一个全局
NodeController
实例,管理连接 - 通过平台 Channel(如 Unix Socket、Windows Named Pipe)将
MessagePipe
映射到对端进程 - 句柄(如文件描述符)通过平台特定方法传递(如
sendmsg
)
4. 对比 Protocol Buffers
既然都是 IDL,就不得不提及 Protocol Buffers。
mojom 和 Protocol Buffers 既有诸多相似,又有很多差异。
首先,它们都是接口定义语言,都支持类型、嵌套,都有代码生成器,可生成跨平台、跨语言代码,而且都实现了异步调用。
两者最大的不同之处在于应用场景:
- mojom 是用于模块/进程间通信
- protobuf 则是通用的序列化协议,主要用于网络通信、持久化等
更详细的区别:
项目 | mojom | protobuf |
---|---|---|
设计目标 | Chromium IPC(跨线程、跨进程通信) | 高效序列化 & 跨系统 RPC(gRPC) |
适用范围 | Chromium 内部 | 广泛应用于各种系统、服务 |
是否必须运行时支持 | 是,必须依赖 Mojo runtime | 否,可嵌入到任意项目 |
支持的调用模式 | 单一方向(客户端-服务端),无服务发现 | 支持服务注册、发现(gRPC) |
序列化机制 | 自定义 mojo message 序列化 | Protocol Buffers 序列化协议 |
传输通道 | MessagePipe(socket、shared memory) | TCP/HTTP/QUIC 等 |
二进制兼容性 | 较差(字段增删会破坏 IPC) | 很强(支持字段编号,前向兼容) |
版本控制 | 基本不支持(需谨慎修改) | 支持良好版本升级和字段变更 |
可持久化 | ❌ 仅用于 IPC,不能存磁盘 | ✅ 可存为二进制或文本配置 |
因此,除了在 Chromium 中会使用 mojom 外,其他几乎所有场景,protobuf 都是更优的选择。
5. 总结
Mojo 看起来有些复杂,其实目标很清晰:简化并优化进程间通信。
简单,一方面意味着使用门槛低,从旧的方案迁移会更容易,另一方面意味着不易出错,即便出错也方便调试。
当然,简单只是表象,背后的封装和实现十分复杂。
这就是这个世界的常态。
你看到的所有现象,看似直白,运作机制也许相当繁琐。
小到一个函数、App,大到企业、国家,莫不如是。
我们要做的,就是从看到,到看懂。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2025/chromium-ipc-101/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2025-08-07T02:13:26+08:00