深入浏览器原理系列(4):多进程架构
问题
构建一个从不崩溃或挂起的渲染引擎几乎是不可能的。构建一个完全安全的渲染引擎几乎是不可能的。
在某些方面,2006年左右的web浏览器状态就像过去的单用户、协同多任务操作系统。正如操作系统中行为不当的应用程序可能导致整个系统瘫痪一样,web浏览器中的行为不当的web页面也可能导致整个系统瘫痪。只需一个浏览器或插件漏洞,整个浏览器和当前运行的所有选项卡就会关闭。
现代操作系统更加健壮,因为它们将应用程序放置在彼此隔离的独立进程中。一个应用程序的崩溃通常不会损害其他应用程序或操作系统的完整性,而且每个用户对其他用户数据的访问受到限制。
架构一览
我们为浏览器标签页使用单独的进程,以保护整个应用程序不受渲染引擎的bug和小故障的影响。我们还限制了每个渲染引擎进程对其他进程和系统其余部分的访问。在某些方面,这给web浏览带来了内存保护和访问控制给操作系统带来的好处。
我们将运行UI并管理标签和插件进程的主进程称为“浏览器进程”或“浏览器”。类似地,标签页相关的进程称为“渲染进程”或“渲染器”。渲染器使用Blink开源布局引擎来解释和布局HTML页面。在这里插入图片描述
管理渲染进程
每个渲染进程都有一个全局的RenderProcess
对象,它管理与父亲进程-浏览器进程的通信并维护全局状态。
浏览器为每个渲染进程维护一个相应的RenderProcessHost
,它管理浏览器的状态和同渲染器之间的通信。
浏览器和渲染器使用Chromium的IPC
系统进行通信
管理视图Views
每个渲染进程都有一个或多个由 RenderProcess
管理的 RenderView
对象,这些对象与content的标签页相对应。
相应的RenderProcessHost
维护一个与渲染器中每个视图相对应的RenderViewHost
。
每个视图都有一个视图ID,该ID用于区分同一渲染器中的多个视图。这些ID在同一个渲染器进程中是唯一的,但在浏览器进程中不是唯一的,因此标识一个视图需要一个RenderProcessHost
和一个视图ID
。
通过这些RenderViewHost
对象可以完成从浏览器进程到特定content的标签页的通信,这些对象知道如何通过其RenderProcessHost
将消息发送到RenderProcess
以及继续发送到RenderView
组件和接口
渲染器进程中:
RenderProcess使用浏览器进程中相应的RenderProcessHost处理IPC消息。每个渲染器进程只有一个RenderProcess对象。这就是所有浏览器进程<->渲染器进程之间通信的方式。
RenderView对象与浏览器进程中相应的RenderViewHost通信(通过RenderProcess),以及我们的WebKit嵌入层。此对象表示一个标签页或弹出窗口中的web页面的内容
浏览器进程中:
Browser对象表示一个顶级浏览器窗口。
RenderProcessHost对象表示单个 浏览器进程<–>渲染器进程 IPC通信连接中的浏览器进程端。每个渲染器进程在浏览器进程中都有一个RenderProcessHost
RenderViewHost对象封装了与远程RenderView的通信,RenderWidgetHost处理浏览器进程中RenderWidget的输入和绘制
有关此嵌入的工作方式的更多详细信息,请参阅Chromium如何显示网页设计文档
共享渲染进程
通常,每个新窗口或标签页都会在新进程中打开。浏览器将产生一个新进程,并指示它创建一个RenderView。
有时有必要或有需要的在标签页或窗口之间共享渲染进程。Web应用程序会打开一个新窗口,希望与之进行同步通信,例如,使用JavaScript中的window.open。在这种情况下,当我们创建一个新的窗口或标签页时,我们需要重新使用打开窗口的进程。如果进程总数太大,或者用户已经打开一个导航到该域domain的进程,我们也有策略将新的标签页分配到现有进程中去。这些策略在进程模型中进行了描述
崩溃或错误行为的渲染器检测
一个浏览器进程的每个IPC连接都会监视该进程的句柄。如果这些句柄被通知,渲染进程已经崩溃,标签页被通知崩溃。现在,我们会显示一个“sad标签”屏幕,通知用户渲染器崩溃了。可以通过按reload按钮或启动一个新的导航来重新加载页面。当这种情况发生时,我们注意到没有进程,并创建一个新的进程
沙盒渲染器
鉴于渲染器在单独的进程中运行,我们可以通过沙盒限制其对系统资源的访问。例如,我们可以确保渲染器对网络的唯一访问是通过其父进程-浏览器进程进行的。同样,我们可以使用主机操作系统的内置权限来限制渲染器进程对文件系统的访问。
除了限制渲染器对文件系统和网络的访问之外,我们还可以限制对渲染器对用户的显示和相关对象的访问。我们在用户看不见的单独Windows“桌面”上运行每个渲染过程。这样可以防止受损的渲染器打开新窗口或捕获击键。
回馈记忆
假如渲染器在单独的进程中运行,那么将隐藏的标签页视为较低优先级变得很简单。
通常,Windows上最小化的进程会将其内存自动放入“可用内存”池中。在低内存情况下,Windows将在交换出更高优先级的内存之前将该最小化进程的内存交换到磁盘,这有助于使用户可见的程序保持更高的响应速度。
我们可以将相同的原理应用于隐藏的标签页。当渲染进程没有顶级标签页时,我们可以释放该进程的“工作集”大小,以提示系统在必要时先将该内存换出到磁盘。
因为我们发现减小工作集大小,当用户在两个标签页之间切换时也会降低标签页切换性能,所以我们逐渐释放此内存。这意味着,如果用户切换回最近使用的标签页,则与最近很少使用的标签页相比,该标签页的内存更有可能被分页。
具有足够内存来运行所有程序的用户根本不会注意到此过程:Windows仅会在需要时才真正回收这些数据,因此,如果有足够的内存,则不会影响性能。
这有助于我们在低内存情况下获得更好的内存占用。与很少使用的背景标签相关联的内存可以完全换出,而前景标签的数据可以完全加载到内存中。相比之下,单进程浏览器会将所有标签页的数据随机分布在其内存中,并且不可能如此干净地分离使用和未使用的数据,从而浪费内存和性能。
插件和扩展
Firefox样式的NPAPI插件在其自身的进程中运行,与渲染器分开。插件架构中对此进行了详细描述。
“网站隔离”项目旨在提供渲染器之间的更多隔离,该项目的早期交付成果包括在隔离的进程中运行Chrome的HTML /JavaScript内容扩展。