浏览器架构变迁

1,780 阅读13分钟

进程和线程

  • 进程是操作系统资源分配的基本单位;
  • 线程是操作系统调度的基本单位;
  • 同一个进程内的线程可以共享分配给进程的内存资源。进程间的通信通过 IPC(Inter-Process Communication)实现;

上面的几句话是老生常谈了,下面的解释对我来说更友好:

  • 一个最最基础的事实:CPU 太快了,寄存器仅仅能够追的上他的脚步,RAM 和别的挂在各总线上的设备完全是望其项背。
  • 一个必须知道的事实:执行一段程序代码,当得到 CPU 的时候,相关的资源必须已经就位了(显卡、GPS等),然后 CPU 开始执行。这里除了 CPU 以外所有的构成了这个程序的执行环境,也叫程序上下文。当这个程序执行完了,或者分配给他的 CPU 时间用完了,那它就要被切换出去,等待下一次 CPU 的临幸。在被切换出去的最后一步工作就是保存程序上下文,因为这个是下次他被 CPU 临幸的运行环境,必须保存。
  • 在 CPU 看来所有的任务都是一个一个轮流执行的,具体的轮流方法就是:先加载程序 A 的上下文,然后开始执行 A,保存程序 A 的上下文,调入下一个要执行的程序 B 的程序上下文,然后开始执行 B,保存程序B的上下文。
  • 进程和线程都是一个时间段的描述,是 CPU 工作时间段的描述,不过是颗粒大小不同。
  • IPC 是一个统称,指可用于不同进程之间通信的手段。Chromium 最新使用的是 mojo。

单进程架构

  • 早在 2007 年之前,市面上浏览器都是单进程的。 单进程架构

  • 浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 引擎、渲染引擎等。

  • 缺点:不稳定、不流畅、不安全。

    不稳定:

    • 插件:早期浏览器需要借助插件来实现诸如 Web 视频、Web 游戏等各种强大的功能,比如:各种 flash 插件。但是插件是最容易出问题的模块,并且还和其他模块运行在同一个进程中,所以一个插件的意外崩溃会引起整个浏览器的崩溃。
    • 渲染引擎:渲染引擎也是不稳定的,通常一些复杂的 JS 代码就有可能引起渲染引擎的崩溃,和插件一样,渲染引擎的崩溃也会导致整个浏览器的崩溃。

    不流畅:

    • 一个线程:所有页面的渲染引擎、JS 引擎以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行。假如:有一个无限循环的脚本,当其执行时,它会独占整个线程,这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应,变卡顿。
    • 内存泄漏:通常浏览器的内核都是非常复杂的,运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢。

    不安全:

    • 插件:Chrome 是由 C/C++ 程序编写的,插件也可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可能释放病毒、窃取你的账号密码,引发安全性问题。
    • 页面脚本:页面脚本可以通过浏览器的漏洞来获取系统权限,可能也会窃取你的账号密码,同样也会引发安全问题。

早期的 Chrome 架构

  • 2008 年 Chrome 发布时的多进程架构 早期的 Chrome 架构
  • 和单进程架构对比有哪些变化?
    • 单独的浏览器主进程。
    • 单独的渲染进程。
    • 单独的插件进程。
    • 安全沙箱。
    • 进程间通过 IPC 通信。
  • 解决了哪些问题?
    • 很大程度地解决了 不稳定、不流畅、不安全 的问题。

      不稳定:在单进程架构下,插件和渲染引擎是造成不稳定的重要因素。但是在多进程架构下,它们都运行在各自的进程中,所以即使它们奔溃也不会影响到浏览器和其他页面。

      不流畅:在单进程架构下,所有页面的渲染引擎、JS引擎、插件运行在同一个线程中,一旦某个页面的 JS 运行时间长就会造成其他页面的卡顿。但是在多进程架构下,每个页面都有自己的渲染进程、插件也独立成了一个进程,所以某个页面 JS 的长时间运行也就不会造成其他页面的卡顿了。还有内存泄漏的问题:当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收,这样就轻松解决了浏览器页面的内存泄漏问题。

      不安全:采用多进程架构的额外好处是可以使用安全沙箱,安全沙箱的最小运行单位是进程。你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据。这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。

目前 Chrome 多进程架构

  • 目前 Chrome 多进程架构 目前 Chrome 多进程架构
  • 和早期 Chrome 多进程架构对比有哪些变化?
    • 多了一个 GPU 进程:Chrome 刚开始发布的时候是没有 GPU 进程的,而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

    • 网络从浏览器主进程中独立成了一个网络进程。

进程职责

  • 浏览器进程(Browser Process)
    • 负责包括地址栏,书签栏,前进后退按钮等部分的工作;
    • 界面显示、用户交互、子进程管理,文件访问等功能;
  • 渲染进程(Renderer Process)
    • 核心任务是将 HTML、CSS、JavaScript 转换为用户可与之交互的网页,排版引擎 Blink 和 JavaScript V8 引擎都运行在该进程中;

    • 默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程;进程模型

      Google 在宣传的时候一直都说 Chrome 是 one tab one process 的模式,这只是为了宣传起来方便。实际上,Chrome 支持的进程模型远比宣传丰富,Chrome 支持以下几种进程模型:

      • Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列与原来同域的网站都属于一个进程。这是 Chrome 的默认模式。
      • Process-per-site:同域名范畴的网站放在一个进程,比如 www.google.comwww.google.com/bookmarks 就属于一个域名内(google 有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。用命令行 --process-per-site 开启。
      • Process-per-tab:一组脚本相关的页面属于同一个进程。用 --process-per-tab 开启。
      • Single Process:传统浏览器的模式,没有多进程只有多线程,用 --single-process 开启。
    • 出于安全的考虑,渲染进程都是运行在安全沙箱中;

  • GPU 进程
    • 负责 3D CSS 渲染;
    • 加速页面渲染;
  • 网络进程(Network Process)
    • 负责页面的网络资源加载;
    • 之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程;
  • 插件进程(Plugin Process)
    • 负责插件的运行;
    • 因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响;部分系统支持运行在安全沙箱模式中;

仅仅打开 1 个页面,Chrome 开启了多少个进程?

答案是不确定的,不同版本的 Chrome 会有不同的结果,以下是我本地 Chrome v83 的参考结果。

  • 1 个浏览器进程;
  • 1 个渲染进程;
  • 1 个网络进程;
  • 1 个 audio 进程;
  • 1 个 GPU 进程;
  • 可能有 N 个插件进程,N >= 0(如果打开的页面有运行插件的话);

优缺点

  • 优点
    • 很大程度上解决了不稳定、不流畅、不安全的问题:多进程架构通过多进程以及沙箱模式提升了浏览器的稳定性、流畅性和安全性。
  • 缺点
    • 更高的资源占用:每个渲染进程都会包含公共基础结构的副本(如 JavaScript 引擎),这就意味着浏览器会消耗更多的内存资源。
    • 更复杂的体系架构:从一开始,Chrome 开发团队就有意识地不去过度设计代码和体系结构,导致代码库被简单地推动来完成许多其原本不适合的功能;第二个原因是,在急于添加新功能和支持新平台的过程中,Chrome 开发团队没有停下来重新考虑分层。

Chrome 面向服务的架构

为了解决复杂的体系架构导致的各模块之间耦合性高、扩展性差的问题,在 2016 年,Chrome 官方团队使用“面向服务的架构”(Services-Oriented Architecture,简称SOA)的思想设计了新的 Chrome 架构。 也就是说 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展,从而构建一个更内聚、低耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。

  • 多进程架构下各个层级的依赖关系 多进程架构下各个层级的依赖关系

    • Application 层的 Chrome 依赖 Platform 层的 Content,这里面包括 File、Network、UI 等模块,而 Content 又依赖 Framework 层的 Blink 渲染网页。

    Framework 层: 1. Blink、V8 都属于 Framework 层。 2. 以 Blink 为例,Blink 是 Web 平台的渲染引擎。从代码结构的角度来看,"Blink" 一般意味着 "//third_party/blink/" 目录下的代码。从项目的角度来看,"Blink" 一般意味着实现 Web 平台特性的项目。实现这些 Web 特性的代码分布在 "//third_party/blink/", "//content/renderer/","//content/browser/" 和其它地方。 3. Blink 通过 Blink public APIs 将 //third_party/blink/ 的功能暴露给 Platform 层。

    Platform 层:
      1. 它里面的 Content 提供了 Framework 层的多种操作系统,包括 Windows,Linux,Mac OS X 和 Android 的具体实现,在其上可以建构出一个完整的浏览器应用。
      2. Content 通过 Content public APIs 将 Framework 层的功能暴露给上层应用。
    

    Application 层:

      基于 Content 层实现的各种应用,例如:Chrome / Android WebView / Chrome OS,这些都统称为 embedders。
    
  • 面向服务架构下各个层级的依赖关系 面向服务架构下各个层级的依赖关系

    • Chrome 最终把 UI、文件、设备、网络等原属于 platform 的模块重构为基础服务,类似操作系统底层服务。与多进程架构的对比是 Framework 与 Platform 之间的依赖关系完全倒置了!
    • 目前 Chrome 正处在老的架构向服务化架构过渡阶段。比如:我本地 Chrome 版本是 v83,这个版本中已经把 Network & Audio(starting from Chrome v76) 从浏览器进程中独立出来,成了一个独立的进程。

如何实现面向服务的架构?

  • 使用新的 IPC 系统 -> Mojo;
  • 将 UI、文件、设备、网络等基础模块重构成独立的服务(Service),每个服务都可以在独立的进程中运行;
  • 使用定义好的接口访问服务,通过 Mojo 来通信;

什么是 Mojo?

  • Mojo 是一个新的 IPC 系统,它为每一个接口创建了一个独立的消息管道,确保不同接口的 IPC 是独立的;可以简单理解成一个消息管道对应一个接口,这个消息管道只能发送其对应接口描述的消息;
  • 一个消息管道(message pipe)是由一对端点(endpoints)构成,每个端点都有一个传入消息的队列,在一个端点上写一条消息可以有效地将该消息排队到另一个(对等)端点上,因此消息队列是双向的;
  • 老的 IPC 系统,是基于在两个进程之间的一个大的命名管道(IPC::Channel)实现的,进程间所有的 IPC 消息都是通过这一个管道根据先进先出的顺序去传递;
  • 和老的 IPC 系统相比,Mojo 快了 24-34%,上下文切换次数少了 1/3;Mojo Performance

如何使用 Mojo 通信 ?

  • 定义接口以及编译规则;

  • 在 Remote 端创建消息管道、发送消息、发送 receiver 到浏览器进程;

  • 在 Receiver 端实现接口;

    例子:使用 Mojo 从渲染进程发送消息到浏览器进程,浏览器进程返回一个随机整数给渲染进程。

    • 新建一个 .mojom 文件,定义接口;
    • 新建一个 BUILD.gn 文件,定义对应的编译规则,生成不同语言的 bindings,如 C++ / Java bindings;
    • 在 Remote 端创建消息管道、发送消息;
    • 发送 receiver 到浏览器进程;
    • 在 Receiver 端实现接口;

什么是服务?

  • 一个 service 是一个独立的代码库,它实现了一个或多个相关的特性或行为,并且与外部代码的交互仅通过 Mojo 接口连接进行的,通常由浏览器进程代理。
  • 每个服务定义并实现一个主 Mojo 接口,浏览器可以使用该接口来管理服务实例。

如何定义一个 service?

  • 新建一个 .mojom 文件,里面定义接口;
  • 新建一个 BUILD.gn 文件,里面定义对应的编译规则,用于生成不同语言的 bindings,如 C++ bindings / Java bindings;
  • 实现定义的接口;
  • 链接服务,使其可以作为一个单独的进程运行;
  • 启动服务;

services 所在目录

通常,基础服务定义在 //services 目录中,模块特定的服务定义在模块的 services 目录中,例如://chrome/services。

优点

  • 进一步提升了稳定性、流畅性、安全性;

  • 进行实验性功能:面向服务的架构会产生可重用和解耦的组件,Chrome 小组和其他小组可以通过组合这些组件、尝试新功能,而无需修改 Chrome。

  • 灵活的弹性架构:在性能强大的设备上会以多进程的方式运行基础服务,在资源受限的设备上,Chrome 会将基础服务整合到浏览器进程中,从而节省内存占用;

    • 性能强大的设备 性能强大的设备
    • 资源受限的设备 资源受限的设备

总结

总结

参考