Chromium 多进程架构

1,980 阅读7分钟

本篇文章是浏览器架构原理的扩展篇,主要分享浏览器多进程架构的设计理念。

Multi-process:多进程

本文主要核心内容来源于Multi-process Architecture

设计动机

2006年前后的浏览器更多的是采用单进程架构。这样做有很多好处,例如

  • 实现/部署简单快速
  • 内存占用少
  • 通信高效,比 IPC/socket 等通信方式简单便捷

但是于此同时,单进程架构有巨大的痛点:任何内部模块的崩溃「Crash」都会导致整个进程崩溃。因此,在单进程模式之下,其中一个页面/插件发生了错误,整个浏览器和所有当前运行的页面都无法正常运行。这也是早期的计算机容易崩溃的原因之一。

而我们几乎不可能构建一个永远不会崩溃或挂起的渲染引擎。

随着内存条变得越来越便宜,健壮性成为了现代浏览器的追求目标。

Chromium 的多进程架构将应用程序「网页」放在彼此隔离开的单独进程中。一个应用程序的崩溃不会损害其他程序和整个浏览器的完整性。

概述

Chromium 使用多个进程来保护整个应用程序不受渲染引擎或其他组件中的错误和故障的影响。它还限制每个渲染引擎进程访问其他进程或其他系统能力。这样的思路对内存保护和系统控制带来了好处。

例如,我们不能在网页中进行文件操作

在 Chromium 多进程架构中,有一个主线程用于运行UI并且管理其他进程,称之为 Browser process 或者 Browser.

处理 web 页面内容的进程称之为 renderer process 或者 renderer

renderer 使用 Blink 开源引擎来解释和布局 HTML

image.png

多进程架构虽然有效的解决了隔离问题,但是随之带来的一个巨大的问题就是内存消耗巨大,因此在此基础之上,chromium 也设计了许多策略来尽可能的减少内存的消耗。

多进程架构图

管理 renderer progresses

每个 renderer process 都有一个全局 RenderProcess 对象,用于管理与主进程 Browser process 的通信,并维护自身的全局状态。主进程为每个 renderer 维护相应的 RenderProcessHost,用于管理 browser process 的状态,和与 renderer 的通信。

browser processrenderer process 之间通信使用 Mojo 或者 chromium 传统的 IPC 系统。

现在通信都优先使用 Mojo 来实现,过去使用的 IPC 正在被逐步放弃,许多老代码仍然使用 IPC,不过 IPC 的底层也使用 Mojo 进行了重构

管理 frames 和 documents

这里的 frames 的概念类似于我们熟悉的 iframe 标签。

每个 renderer process 都会有至少一个 RenderFrame 对象,与包含 document 对象的 frame 相对应。在主进程 Browser process 中,有对应的 RenderFrameHost 用于管理该文档的状态。

每个 RenderFrame 会被分配到一个 routing ID, 用于区分在同一个 renderer 中的多个 document 或者 frame. 这些 ID 在同一个 renderer 中是唯一的,但是在整个浏览器中不一定,因此,我们需要结合 RenderProcessHostrouting ID 才能识别出唯一 frame

browser process 到一个 renderer process 中特定 document 的通信是通过 RenderFrameHost 来完成的,这些对象知道如何通过 Mojo 和 IPC 发送消息。

Components and interfaces

renderer process 中:

  • 每个 renderer 都有一个全局的 RenderProcess 对象,通过 Mojo/IPC 与主进程中对应的 RenderProcessHost 通信
  • 每个 renderer 会有多个 RenderFrame 对象通过 Mojo/IPC 与主进程中对应的 RenderFrameHost 通信。「因为每个页面出了自身意外,还可以内置多个 iframe」

Browser process 中:

  • Browser 对象表示顶层浏览器窗口
  • 为每个 renderer process 维护对应的 RenderProcessHost 对象,它表示主进程到渲染进程的单一 IPC 连接:browser ↔ renderer
  • RenderFrameHost 对象封装与 RenderFrame 的通信,RenderWidgetHost 处理浏览器中 RenderWidget 的输入和绘制

For more detailed information on how this embedding works, see the How Chromium displays web pages design document.

共享 renderer process

通常情况下,一个新的窗口/选项卡会在一个新的进程中打开。主进程会创建一个新的进程,并指示它创建一个RenderFrame,这可能在页面中创建更多的 iframe.

但是有的时候希望能够在 窗口/tab 之间共享 render process

例如我们在网页中使用 window.open 打开一个新页面,如果新的页面属于同一来源,那么这两个页面就会共享进程

除此之外,如果用户在使用浏览器时,同时打开了过多的 tab,此时如果每个页面都享有独立的进程的话,那么内存的消耗就会变得非常巨大。因此 chromium 此时会有针对性的共享策略来应对这种情况

检测崩溃的 renderer

每一个 Mojo/IPC 连接都会监听进程对应的句柄。如果这些句柄被执行,则表示 renderer process 已经崩溃,那么所有受到影响的 tab 和 frame 都会接收到通知。Chromium 显示一个 “sad tab” or "sad frame" 来告诉用户这个页面已经崩溃。用户可以刷新页面或者输入一个新的地址重新加载页面,chromium 会注意到没有 renderer process 并创建一个新的 renderer

隔离沙箱

因为 renderer 在一个单独的进程中运行,因此 chromium 可以通过沙箱环境限制其对系统资源的访问。例如,chromium 可以确保所有的 renderer process 都只能通过它提供的网络服务来访问网络资源

同样,chromium 也可以限制 rednerer process 对系统文件系统进行访问,这些限制性极大的缩小了网页能完成的任务,也是网页不具备很多本地能力的核心原因

内存优化策略

虽然前面我们已经提到过有共享策略来优化内存的使用。不过多进程的模式依然对内存的消耗是巨大的。因此 chromium 还提供了别的方式来优化内存 Giving back memory

在 windows 操作系统中,最小化到后台的进程会自动将内存放入一个「可用内存池」中,在内存不足时,windows 会在换出优先级较高的内存之前,将此内存交换到磁盘。也就是说,windows 会利用磁盘的存储空间让内存的使用压力变小

这样的策略同样可以运用于浏览器中。

当一个 renderer 隐藏到后台时,我们可以释放该进程的 work set 大小,以提示系统在必要时首先将改内存交换到磁盘。

因为我们发现,当用户在两个 tab 之间进行切换时,如果马上就减小 work set 的大小会降低 tab 的切换性能,因此先标记并在必要时候释放内存是一个更好的选择

所以,如果你的电脑内存足够大不需要考虑这个问题,系统只会在内存不够时运用这个空间交换策略,这有助于我们在低内存的情况下获得更优的内存使用。

内存不足时,很少使用 tab 可以完全置换为磁盘空间,当前处于前台的 tab 可以完全加载到内存中以获得最佳的运行速度。

而单进程架构则无法使用这种内存优化策略。单进程浏览器会将所有 tab 的数据随机分配在内存中,无法干净的区分使用的和未使用的数据,这对于内存和性能而言都是一种浪费

其他进程策略

chromium 也将许多其他组件拆分成单独的进程,有时是在特定平台使用的兼容策略。

例如,现在 chromium 拥有单独的 GPU 进程,网络服务,存储服务。沙箱工具进程也可以用于一些小任务或者高风险的任务。

可以在浏览器的任务管理器中查看

关注我的公众号添加我微信好友
这波能反杀icanmeetu