anjia

多进程架构 - The Chromium Projects

原文链接: dev.chromium.org

本文档介绍 Chromium 的顶级架构。

问题

构建一个从不崩溃或挂掉的渲染引擎几乎是不可能的,构建一个完全安全的渲染引擎也几乎是不可能的。

从某种意义上说,2006 年左右的 web 浏览器就像是过去的单用户多任务的操作系统。在那种老旧的操作系统里,一个异常的应用程序会让整个系统挂掉。而一个异常的 web 页面也可以让整个浏览器崩溃掉,造成崩溃的原因可能仅仅就是一个浏览器或插件的 bug,便摧毁了整个浏览器和当前正在运行的所有标签页。

现代操作系统越来越健壮了,因为它们把应用程序放在了彼此独立的进程中。一个应用程序的崩溃通常不会损害其他应用程序,也不会破坏操作系统的完整性。它也会限制用户访问其他用户的数据。

架构概览

浏览器的一个标签页对应一个独立的进程,这样做是为了保护整个应用程序不受渲染引擎 bug 和故障的影响。我们会限制不同渲染引擎进程之间的彼此访问,也会限制渲染引擎进程对系统其他部分的访问。这样一来,web 浏览器也就有了内存保护和访问控制的机制。

我们把运行UI、管理标签页和插件进程的主进程称为“浏览器进程”或“浏览器”,把特定标签页进程称为“渲染进程”或“渲染器”。渲染进程使用开源布局引擎 Blink 来解释和布局 HTML。

译者注:为了突出“进程”的概念,译文中统一使用“浏览器进程”和“渲染进程”。在日常表述中,用“浏览器”和“渲染器”更多些。

管理渲染进程

每个渲染进程都有一个全局对象RenderProcess,用来管理与父浏览器进程的通信,同时维护着一份全局状态。浏览器进程为每个渲染进程维护一个RenderProcessHost对象,用来管理浏览器状态和与渲染进程的通信。浏览器进程和渲染进程使用 Chromium 的 IPC 系统进行通信。

管理视图

每个渲染进程都有一个或多个由RenderProcess管理的RenderView对象,它们与内容标签页相对应。相应的,浏览器进程里的RenderProcessHost管理着RenderViewHost,它们和渲染进程中的视图相对应。每个视图都有一个视图 ID,用来区分同一个渲染进程里的多个视图。视图 ID 在所在的渲染进程中唯一,但在浏览器进程中不唯一。所以,要标识一个视图就需要RenderProcessHost和视图 ID。从浏览器进程到特定内容标签页的通信是由RenderViewHost完成负责的,它知道如何通过RenderProcessHost将消息发送到RenderProcess,再到RenderView

组件和接口

在渲染进程中:

  • RenderProcess处理和浏览器进程中的RenderProcessHost的 IPC。每个渲染进程只有一个RenderProcess对象,它处理所有浏览器进程↔渲染进程的通信。
  • RenderView对象负责和浏览器进程中的RenderViewHost的通信(通过 RenderProcess),也负责和内嵌 WebKit 层进行通信。该对象表示web 标签或弹出窗口的网页内容。

在浏览器进程中:

  • Browser对象表示顶级浏览器窗口
  • RenderProcessHost对象表示浏览器进程↔渲染进程的 IPC 连接的浏览器端。每个渲染进程对应浏览器进程中的唯一RenderProcessHost
  • RenderViewHost对象封装了与远程RenderView的通信,RenderWidgetHost 处理 RenderWidget 的输入和绘制

译者注:RenderView 继承自 RenderWidget,RenderViewHost 继承自 RenderWidgetHost

有关此嵌入工作的更详细信息,可参阅 Chromium 如何显示 web 页面的设计文档。

共享渲染进程

通常,每个新窗口/新标签页都会在新进程中打开。浏览器进程会创建一个新的进程,并为它创建一个单独的RenderView

有时候,有需要/必要在标签页/窗口之间共享渲染进程。比如,Web 应用程序使用 window.open 打开一个期望与之进行同步通信的新窗口,在这种情况下,当创建新窗口/标签页的时候,我们需要复用已打开窗口的进程。如果进程总数太大,或者用户已经把导航到该域的进程打开了,我们也有相应的策略,可以将新标签页分配给已有进程。这些策略在过程模型中有描述。

检测崩溃或异常的渲染进程

每个到浏览器进程的 IPC 连接都会监听进程句柄。如果这些句柄收到了信号,那说明渲染进程已经崩溃了,标签页收到了崩溃通知。目前,我们会显示一个哭脸标签页,以通知用户渲染器已经崩溃。你可以按 reload 按钮重新加载此页面,也可以在此页签中打开一个新导航。当发生这种情况时,我们会创建一个新进程。

沙箱渲染进程

在独立进程中运行渲染进程,我们就可以通过沙箱来限制它对系统资源的访问了。比如,我们通过父浏览器进程确保渲染进程只能访问网络,通过主机操作系统的内置权限来限制它对文件系统的访问。

除了限制渲染进程访问文件系统和网络之外,我们还可以限制它访问用户显示及相关对象。我们在用户不可见的单独 Windows “桌面”上运行每个渲染进程,这样可以防止受影响的渲染进程打开新窗口或捕获按键。

释放内存

渲染进程在单独的进程中运行,这让隐藏标签页拥有较低优先级变得很简单。通常,Windows 的最小化进程是将其内存自动放到“可用内存”池中。在内存不足的情况下,Windows 会把该段内存转移到磁盘上(而不是转移更高优的内存),从而保证用户可见程序的响应。我们可以对隐藏标签页使用相同的策略。当渲染进程没有高级别标签页的时候,我们可以释放该进程的“工作集”,作为给系统的提示,以便在必要时将该段内存交换到磁盘。因为我们发现,当用户在两个标签页之间切换时,工作集大小的减少也会降低标签页切换的性能,所以我们逐渐释放此内存。这意味着如果用户切换回最近使用的标签页,则那个标签页的内存会比最近较少使用标签页更有可能被换入。当有足够的内存来运行所有程序的时候,用户就根本不会注意到这个过程:Windows 只有在需要的时候才会实际收回这些数据。所以当内存充足时,也不会有性能问题。

这有助于我们在低内存情况下获得更优的内存占用。较少使用的后台标签页所对应的内存可以完全交换出来,而前台标签页的数据可以完全加载到内存中。相比之下,如果是单进程的浏览器,它是将所有标签页的数据随机分布在内存中,并且不能清晰地区分出已使用数据和未使用数据,进而浪费内存,影响性能。

插件和扩展

Firefox 风格的 NPAPI 插件在它们自己的进程里运行,与渲染进程不在一块。这在插件架构中有详细描述。

站点隔离项目旨在为渲染进程提供更多的隔离,此项目的一个早期版本可以在隔离进程中运行 Chrome 的 HTML/JavaScript 内容扩展。