之所以要介绍 Chrome 浏览器的多进程架构,是因为这是理解浏览器行为的基础,也是本专栏的第一篇文章,希望大家能够打好基础
我们继续之前先来简要说下贯穿本文的两个基础概念 — 线程和进程
线程 VS 进程
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。
那什么又是进程呢?一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行其中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
总结来说,进程和线程之间的关系有以下 4 个特点。
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃
- 线程之间共享进程中的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
- 进程之间的内容相互隔离。进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。
单进程浏览器
其实早在 2007 年之前,市面上的浏览器都是单进程的。也就是说
网络、插件、JavaScript 运行环境、渲染引擎和页面等功能模块都是运行在同一个进程里。
如此多的功能模块运行在一个进程里,必定会导致浏览器不稳定、不流畅和不安全。下面我们就来分析下出现这些问题的原因。
问题 1:不稳定
早期浏览器需要借助于插件来实现诸如 Web 视频、Web 游戏等各种强大的功能,但是插件是最容易出问题的模块,并且还运行在浏览器进程之中,所以一个插件的意外崩溃会引起整个浏览器的崩溃。
除了插件之外,渲染引擎模块也是不稳定的,通常一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃。和插件一样,渲染引擎的崩溃也会导致整个浏览器的崩溃。
问题 2:不流畅
同一时刻只能有一个模块可以执行。如果有一个无限循环的脚本运行在一个单进程浏览器的页面里,它会独占整个线程,这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应。
问题 3:不安全
这里依然可以从插件和页面脚本两个方面来解释该原因。
插件可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题。
至于页面脚本,它可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题。
以上这些就是当时浏览器的缺点,不稳定,不流畅,而且不安全。下面我们来看下现代多进程浏览器
多进程浏览器
目前最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
下面我们来逐个分析下这几个进程的功能。
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程(在某些情况下,浏览器会让多个 Tab 页直接运行在同一个渲染进程中,下面详细说)。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
不过凡事都有两面性,虽然多进程架构提升了浏览器的稳定性、流畅性和安全性,但同样也带来了一些问题:
- 更高的资源占用。因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
- 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
对于上面这两个问题,Chrome 团队一直在寻求一种弹性方案,既可以解决资源占用高的问题,也可以解决复杂的体系架构的问题。
浏览上下文组
上文说到在某些情况下,浏览器会让多个 Tab 页直接运行在同一个渲染进程中。那么具体是指哪些情况呢?
如果从一个标签页 a 中打开了另一个新标签页 b,a 和 b 属于同一站点的话,那么 b 复用 a 的渲染进程。官方把这个默认策略叫 process-per-site-instance。
站点
我们将“同一站点”定义为根域名(例如 juejin.im)加上协议(例如 https:// 或者 http://),还包含了该根域名下的所有子域名和不同的端口,所有满足条件的标签页处于同一站点。
假如,现在我从掘金 (https://juejin.im/timeline) 的标签页中打开任意一篇新的文章 (https://juejin.im/post/5ec233… 标签页,由于这两个标签页属于同一站点 (相同协议、相同根域名),所以他们会共用同一个渲染进程。你可以看下面这张 Chrome 的任务管理器截图,他们共用了 11732 这个渲染进程:
不过如果我们分别打开这两个标签页,比如先打开掘金的标签页,然后再打开文章的标签页,这时候我们可以看到这两个标签页分别使用了两个不同的渲染进程。
既然都是同一站点,为什么从 A 标签页中打开 B 标签页,就会使用同一个渲染进程,而分别打开这两个标签页,又会分别使用不同的渲染进程?
标签页之间的连接
要搞清楚这个问题,我们要先来分析下浏览器标签页之间的连接关系。我们知道,浏览器标签页之间是可以通过 JavaScript 脚本来连接的,通常情况下有如下几种连接方式:
- 通过标签来和新标签建立连接,这种方式我们最熟悉,比如下面这种跳转链接
<a href="https://juejin.im/post/5ec23371f265da7bda41555e"> 跳转 </a>
通过这种方式跳转的链接他们的 window.opener 都会指向来源标签页,这样我们可以说,这两个标签页是有连接的。
- 通过 JavaScript 中的 window.open 方法来和新标签页建立连接
new_window = window.open("https://juejin.im/post/5ec23371f265da7bda41555e")
通过这种方式,可以在当前标签页中通过 new_window 来控制新标签页,还可以在新标签页中通过 window.opener 来控制当前标签页。所以我们也可以说,如果从 A 标签页中通过 window.open 的方式打开 B 标签页,那么 A 和 B 标签页也是有连接的。
其实通过上述两种方式打开的新标签页,不论这两个标签页是否属于同一站点,他们之间都能通过 opener 来建立连接,所以他们之间是有联系的。在 WhatWG 规范中,把这一类具有相互连接关系的标签页称为浏览上下文组 (browsing context group)。
Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中,这是因为如果一组标签页,既在同一个浏览上下文组中,又属于同一站点,那么它们可能需要在对方的标签页中执行脚本。因此,它们必须运行在同一渲染进程中。