概要
Chromium 是 Google 为了发展浏览器而发布的一款开源软件, Chrome 与 Chromium共享了大部分代码和功能(其实 Chrome 就是基于稳定版本的Chromium进行开发的)。大家都知道 Chrome 非常的高效, 稳定, 这其中少不了 Chromium 的功劳, 但是 Chromium 身为一个如此庞大的项目是怎样做到如此高效稳定的呢, 今天我们就从 Chromium 的多进程架构入手, 一步步看这个浏览器业界的标杆是如何运行的吧。
背景
构建一个永远不会崩溃的渲染引擎几乎是不可能的。同样也几乎不可能构建一个完全安全的渲染引擎。2006年左右的浏览器状态就像过去的单用户协作多任务操作系统一样。在这样的操作系统中,一个行为不良的应用程序可能会导致整个系统崩溃,就像一个行为不良的网页在浏览器中一样。只需要一个渲染引擎或插件错误就可以使整个浏览器和所有当前运行的标签崩溃。现代操作系统更加健壮,因为它们将应用程序放入相互隔离的单独进程中。一个应用程序的崩溃通常不会影响其他应用程序或操作系统的完整性,并且每个用户对其他用户数据的访问都受到限制。Chromium的架构旨在实现这种更加健壮的设计。
进程与线程

进程和线程是操作系统中非常重要的概念, 本文作为分析 Chromium 架构的文章不过多赘述, 仅做简要介绍, 想要完全搞明白他们的运行原理, 可以自行查阅相关资料。
简单来讲, 进程可以被认为是一个程序的运行时, 线程存在于进程中并且可以执行进程程序的任何部分。当你启动一个程序的时候, 就会创建一个进程, 程序可能会创建一些线程来协助工作(注意, 这不是必须的, 进程也可以自己工作)。操作系统会分配一"块"内存让进程去工作, 所有的应用信息都被保存在这块私有的内存空间中。当你关闭应用时, 进程也会将这块内存归还给操作系统。
进程可以让操作系统帮忙启动别的进程来运行不同的任务。当进程这么做的时候, 操作系统会分配另一"块"内存给新进程。如果两个进程间需要通信, 他们可以使用Inter Process Communication (IPC)来进行通信。许多程序都是被设计为以这种方式运行的, 如果一个进程没有响应了, 可以只重启这一个没有响应的进程而不影响运行其他功能的进程。
Chromium 架构
那么, Chromium 是怎样搭建自身的架构的呢? 如下图所示, 它可以是一个拥有许多不同线程的进程, 也可以是许多不同进程之间通过 IPC 进行通信的几个线程。

需要注意的是,这些不同的架构都是实现细节, 没有关于如何构建Web浏览器的标准规范。一个浏览器的方法可能与另一个完全不同。 Chromium 的做法是顶层是一个 Browser 进程,协调处理应用程序的不同部分的其他进程。对于 Renderer 进程,会创建多个进程,并分配给每个选项卡, 也就是说我们平时使用的每个tab都有自己单独的 Renderer 进程, 甚至在同一个 tab 内, 如果有其他域的 iframe , 那么这个 iframe 也会被分配单独的 Renderer 进程 (感兴趣的同学可以看看 Site Isolation 相关的内容)

不同进程分别是干嘛用的呢
Browser 进程
控制应用程序的"浏览器"部分,包括地址栏、书签、后退和前进按钮。还处理对Web浏览器来说透明的部分,如网络请求和文件访问。
Renderer 进程
控制 tab 展示网页部分的全部内容
Plugin 进程
控制所有网站用到的插件, 比如大名鼎鼎的 Flash
GPU 进程
将 GPU 任务与其他进程隔离处理是因为 GPU 处理来自多个应用程序的请求并在同一块屏幕上绘制它们, 为了方便管理, 将处理 GPU 相关的任务拆分到单独的进程。

还有像 Extension 进程和 Utility 进程这样的进程。如果您想查看 Chrome 中正在运行的进程数量,点击右上角三个点,选择更多工具,然后选择任务管理器。这样就可以显示当前正在运行的进程列表以及它们使用的 CPU/内存量。

多进程架构的收益
上文提到过 Chromium 使用多个渲染进程。举个最简单的例子,您可以想象每个选项卡都有自己的渲染器进程。假设您打开了3个选项卡,每个选项卡都由独立的渲染器进程运行。如果一个选项卡变得无响应,那么你可以关闭无响应的选项卡并继续保持其他选项卡处于活动状态。如果所有 tab 都在一个进程上运行, 当一个 tab 变得无响应时, 所有的 tab 都会无响应, 这并不是我们希望看到的。

将浏览器的工作分成多个进程的另一个好处是安全和沙盒(Sandbox)。由于操作系统提供了一种限制进程权限的方式,浏览器可以让某些进程无法使用某些功能。例如,Chromium 限制处理任意用户输入(User Input)的进程(如 Renderer 进程)对任意本地文件的访问。
由于每个进程拥有自己的私有内存空间,它们通常包含常见的基础功能的实例(例如 Chromium 的 JavaScript 引擎 V8)。这意味着会使用更多的内存,因为它们无法像同一进程内的线程那样共享, 相当于每个 Renderer 都需要分配一份基础功能的所占用的内存。为了节省内存,Chromium 对允许启动的进程数量设置了限制。该限制因设备的内存和 CPU 功率而异,但当 Chromium 达到限制时,他会对同一个域下的 tab 复用 Renderer 进程。
节约内存的利器 ------ 服务化
Chromium 将浏览器程序的每个部分作为服务运行,从而可以轻松地拆分成不同的进程或聚合成一个进程。
核心思想是,当 Chromium 在强大的硬件上运行时,它可以将每个服务拆分成不同的进程,从而提供更高的稳定性,但如果它在资源受限的设备上运行,则 Chromium 将服务合并成一个进程,节省内存占用。
深入 Chromium
上文从概念上介绍了一下 Chromium 多进程架构的组成和优势, 下面介绍一下这些进程中的主要对象及一些更深入的内容。

Renderer 进程
每个 Renderer 进程都有一个全局的 RenderProcess 对象,用于管理与 Browser 进程的通信并维护全局状态。Browser 为每个 Renderer 进程维护一个对应的 RenderProcessHost,用于管理 Renderer 的 Browser 状态和通信。Browser 和 Renderers 使用 Mojo 或 Chromium 的 IPC 系统进行通信。
Documents 和 Frames
每个 Renderer 进程都有一个或多个 RenderFrame 对象,对应包含内容的 document 的 frame。Browser 进程中的相应RenderFrameHost 管理与该document相关联的状态。每个 RenderFrame 都被赋予一个路由ID,用于区分同一 Renderer 中的多个document 或 frame。这些ID在一个 Renderer 内是唯一的,但在 Browser 内不是,因此识别一个具体的 frame 需要RenderProcessHost 和路由ID。从 Browser 到 Renderer 中特定 document 的通信是通过这些 RenderFrameHost 对象完成的,它们知道如何通过 Mojo 或 IPC 发送消息。
组件和接口
在 Renderer 进程中:
- RenderProcess 处理 Mojo 配置和IPC 与浏览器中相应的 RenderProcessHost 通信 。每个 Renderer 进程中恰好有一个 RenderProcess 对象。
- RenderFrame 对象通过 Mojo 与其在 Browser 进程中对应的 RenderFrameHost 和 Blink 层进行通信。该对象表示选项卡或 iframe 中一个 Web 文档的内容。
在 Browser 进程中:
- Browser 对象表示顶层浏览器窗口。
- RenderProcessHost 对象表示单个browser ↔ renderer IPC 连接的浏览器端。每个 Renderer 进程在 Browser 进程中有一个 RenderProcessHost。
- RenderFrameHost 对象封装了与 RenderFrame 的通信,而 RenderWidgetHost 则处理浏览器中 RenderWidget 的输入和绘制。
关于这些概念更加底层是如何工作的, 后续我会单独出文章来介绍。
共享 Renderer 进程
通常情况下,每个新窗口或标签页都会在一个新的进程中打开。浏览器将会生成一个新的进程,并指示其创建一个单独的 RenderFrame,该 RenderFrame 可能会在页面中创建更多的 iframe(可能在不同的进程中, 具体规则参见 Site Isolation)。有时候, 我们希望在标签页或窗口之间共享 Renderer 进程。举个例子,我们希望我们的应用程序可以使用 window.open 创建另一个窗口,如果该文档属于相同的域,则新文档必须共享相同的进程。当进程数太大时, Chromium 也会让新标签页复用已有进程。这些策略可以在 ProcessModels 中查看。
内存调度
使用独立进程运行 Renderer ,可以简单地将隐藏的 tab 视为较低优先级。通常,Windows 上最小化的进程会自动将它们的内存放入"可用内存"池中。在低内存情况下,Windows 会将这些内存交换到磁盘上,然后再交换出更高优先级的内存,从而帮助保持用户可见的程序更加及时响应。我们可以将同样的原则应用于隐藏的 tab。当 Renderer 进程不在用户界面中显示的时候,我们可以释放该进程的"working set"大小,提示系统将该内存交换到磁盘上(如果需要)。但是减少 workging set 大小也会降低用户在两个 tab 之间切换时的性能,所以 Chromium 会逐渐的释放这些内存。这意味着如果用户切换回最近使用过的 tab,那么该选项卡的内存相对于不常用的选项卡更有可能被分页。拥有足够内存运行所有程序的用户将完全不会注意到这个过程: 只有在需要时,Windows 才会实际回收这些数据,因此当有充足的内存时,没有性能损失。
这有助于在低内存情况下获得更优化的内存占用。与之相反,单进程浏览器会将所有选项卡的数据随机分布在其内存中,并且不可能如此干净地分离已使用和未使用的数据,从而浪费内存和性能。
参考文献
总结
以上就是本篇文章的所有内容。通过这边文章, 我们了解了 Chromium 是如何通过多进程架构来实现安全, 高效的浏览器, 也大概知道了进程间是如何协作的, 后续我会继续更新一些文章更加具体的介绍 Chromium 其他方面的"内幕"~