浏览器进程架构的演化

3,746 阅读24分钟

前言

  • 曾经你用 IE6或IE7 或者 firefox 的时候有遇到一个插件崩溃,而你打开的一系列页面全部崩溃的场景么?😰
  • 曾经你遇到过打开浏览器或某些页面总是弹出很多你不想打开的恶意窗,要一个一个手动叉掉的情况吗?😤
  • 曾经在你打开某个页面由于某种原因整个浏览器就卡住,连关闭按钮都点不动的时候,你是只能强制关掉整个浏览器么?🤔

如果上述情况你都遇见过,那我们今天就有得聊了。如果没遇到也没关系,今天的内容也会让你拓展一下视野,当你真正遇到的时候,不至于一头雾水。

今天的主题是《浏览器进程架构的演化》,可能你会问了,什么是浏览器进程架构。其实很简单,架构指的是一个软件的各个方面的设计,那浏览器进程架构你就可以理解为 浏览器是怎么设计其进程的操作和管理方式的

我们会将各种浏览器放到一起来聊一聊这些 浏览器在历史发展过程中,其进程架构做了哪些调整?为什么这样调整?解决了哪些问题? 相信看完这篇文章,能让你明白开头提出的三个问题是怎么回事儿,对你以后遇到的其它问题也给出一个思考方向。

好了,既然要说进程架构的演化,那你真的了解了进程是怎么回事儿么?与线程之间有啥关系?我们来简单的过一遍

了解进程和线程

我们一般使用电脑的时候都会打开多个程序同时运行,比如同时打开了音乐程序放着歌,又打开了文档程序写记录,还开了一个下载程序下载电影。但 CPU其实在某一个特定时刻是只能执行一个程序的(先不考虑多核),那么我们的电脑又是如何同时运行多个程序的呢?

答案就是 进程。进程是操作系统中的概念,操作系统在面对同时处理多个程序的时候,将应用程序抽象为进程来运行。这样一来,操作系统就可以根据一定的规则快速的将CPU执行时间这一宝贵的资源分配给不同的进程去使用,因为切换分配的速度很快,所以看起来就像是多个应用程序同时在运行一样。

但是只有进程还不够,往往一个应用程序整体在执行的时候会存在多个子任务的情况,就像一个在线音乐程序在运行的时候,需要同时运行网络加载的子程序加载音乐流,还要运行音乐数据流的编解码子程序,还会运行音乐界面的UI程序等。因而操作系统又在进程里面划分出了 线程 的概念。有了线程,一个应用程序就可以同时管理自己的多个子任务了。从这里我们还要更新一个概念,CPU在某个特定时刻其实只能执行某个线程

我们再来整体过一遍一个程序的运行。当你运行一个应用程序的时候,操作系统会把你的应用程序封装为一个进程来运行。因为程序运行是需要内存的,所以在分配线程的时候也会同时给你分配一块儿对应的内存,用来存储程序代码、数据和文件资源等。当你的进程需要执行子任务的时候,就可以创建新的线程来执行。

进程模型

这里我们需要了解几个特性:

  • 线程共享进程资源。进程是操作系统分配资源的基本单位,所以进程所需要的系统资源都是操作系统给的,而里面的线程要用资源的时候只能共享进程所拥有的资源。这种资源包括内存空间,也包括操作系统的权限。
  • 一个线程崩溃,整个进程跟着崩。 其实很好理解,一个程序运行过程中,如果某个线程出错了,因为内存是共享的,那如果产生了错误的数据,整个进程最终执行结果也很可能是错的。所以操作系统就直接全部干掉了。
  • 进程之间相互隔离,通过IPC进行通信。 虽然我们一直说一个进程就是一个应用程序,但有的时候一个应用程序也有可能会启动子进程,进程之间的数据呢又是隔离开的,各自为战。要同步某些数据内容,就可以通过某种进程间通信(IPC)的手段来进行。(IPC是一个统称,指可用于不同进程之间通信的手段,这里无需细究)

早期的单进程浏览器

早期的浏览器包括IE7及之前的IE浏览器,Firefox浏览器都是使用的单进程的浏览器架构,也就是说整个浏览器程序都是在一个进程中运行的。不同类型的浏览器的实际进程架构肯定是有一定的差异的,为了描述方便,我们都简化为下面的这张图来说明一下单进程浏览器存在的问题。

单进程浏览器进程架构

在这样的进程架构里,整个进程除了要运行浏览器窗口,下载资源,其中的页面线程还要同时承担了页面渲染、JS执行以及各种插件的运行。这样的一个进程运行起来在我们现在看来其实是有点刀尖上跳舞的感觉。因为这样来运行包含很多页面甚至很多插件的和功能的浏览器,会及其的不稳定,不流畅和不安全

先来看看为啥不稳定。我们回到最开始提的问题,曾经你有用IE6、7或者Firefox的时候,一个插件或页面崩的时候,整个页面崩的情况吗?

现在听起来是不是有点熟悉,可能你心里已经有答案了吧。我们刚才谈到,线程崩溃,整个进程也会全部崩掉。在单进程架构的浏览器里,我们的页面渲染引擎、插件程序,还有像IE里有很多动态链接库的程序都是可能出错的❌,那么不管这些程序在进程中的哪个线程里面运行,其实只要有一个出了个错,那么整个浏览器就挂掉了。这就是我们说的不稳定

再来看看不流畅。我们已经知道CPU其实在某个时间点只能执行某个进程中的某一条线程。那么当我们一个页面线程中既包含了页面的渲染又包含了JS的执行,还有各种插件执行的时候。假设我们JS代码中写了一个死循环的任务,那可想而知,整个浏览器中的其他任务就都没法执行下去了——也就卡住了。那么即使我们不那么暴躁,只是我们的JS或者插件程序需要一直运行一些东西,当你页面正在用JS执行动画的时候,CPU突然被插件进程抢过去执行其他任务了,那你的动画效果怕不是卡得你心慌 😫

最后说一下不安全的问题,这就要用到另一条我们刚提过的特性了——线程共享进程资源。在windows上用过杀毒软件和各种安全卫士软件的应该对 “恶意插件” 这个词不陌生吧。插件这种东西本来是用来便捷的扩展浏览器功能特性的,比如我们最常见的Adobe Flash Player这个插件,以前几乎人手必备。当浏览器和插件的程序在一个页面里面运行的时候,因为进程的内存是被共享的,因而插件就能获取到浏览器运行过程中的数据,以及拥有和浏览器同等的系统权限。那么当你的系统感染了恶意插件,在浏览器运行的过程中,这个插件就可以记录你输入到网页的密码,给你弹出各种窗口,打开多个网页等等。现在你知道曾经你手动一个一个叉掉的那些无故打开的页面是怎么来的吧,同时你也应该了解到当初QQ号被盗的一种可能的原因了吧。

这就是早期的单进程浏览器存在的问题,不稳定、不流畅和不安全。而这一切都是由于把所有的东西像揉面团一样的放到一起所产生的问题,职责是混淆不清的、权限是一给全给的、还有 “一人放火全家遭殃” 的风险。

向多进程架构演化

可能是由于早期的网页功能特性都比较简单,不像现在这样需要非常丰富的功能特性(比如:Canvas、WebGL、Webworker 这些东西),那个时候常规来说单进程就足够了。曾经前端的开发工作还没那么复杂的时候,页面很多都是后端直接就写了。现在有了前端开发工程师这样的一个职位,而且有了更加系统和全面的前端开发工作流,前端的重要性和复杂性都在不断提升。除了单进程浏览器自身存在的一些问题,面对时代的变革,前端的崛起,浏览器若还停留在原地肯定是行不通的,必然无法支撑新的技术的发展,何况单进程本身带来的问题也是需要解决的。

所谓多进程浏览器当然就是将浏览器的各种不同类别的任务拆分出来,放到多个不同的进程中去执行。这里会用到一个关键的安全技术,就是Sandox。

Sandbox(沙盒)可以看做就是一个被限制了权限的进程,一个稍微严格的定义是这样的

沙箱技术按照安全策略来限制程序对系统资源的使用,进而防止其对系统进行破坏,其有效性依赖于所使用的安全策略的有效性

也就是说沙盒限制了多少权限是根据你的安全策略来的,它能防止系统被恶意破坏,提供了一定的安全性,这也是我们上面提到的 “不安全” 这一个问题的解决方案的一环。具体的沙盒的实现方法呢在不同的操作系统上都是有差异的,毕竟进程是操作系统提供的,这不是我们的重点,就此打住。

接着我们一起来看一下Chrome、IE 和 Firefox这三款浏览器是如何演化自身到多进程的一个架构方案的?

Chrome的多进程架构

Google Chrome浏览器最早是在2008年发布的,比起IE这种老牌浏览器,现在看来算是后起之秀。不过这个后起之秀并非只是突兀的来抢占浏览器市场份额的,这一点从 Chrome一开始就是基于多进程架构 可见端倪。

Chrome多进程架构

在Chrome的多进程架构中,包含这样几种进程

  • 浏览器主进程。整个浏览器的主要进程,其他几个进程都是这个进程的子进程,由它来管理和调配;同时你所看到的浏览器的整个窗口,包含地址输入栏,书签栏这些东西也都是它来展示的;
  • 渲染进程。一般来说一个Tab标签页面一个渲染进程(对于不一般的情况可以了解Site Isolation策略);每个渲染进程中会运行Blink布局引擎,V8 JavaScript执行引擎等,单独服务于一个Tab标签页;运行在沙盒中无法访问系统资源。
  • 插件进程。一个插件单独存在于一个进程当中,同时为了安全性,运行在沙盒中限制其权限。
  • 网络进程。发起网络请求访问。
  • GPU进程。处理GPU渲染方面的任务。

可以看得出,chrome这样的多进程设计,将原本揉到一起的各种任务根据职责的不同分拆了出去,极大的减轻了单进程的执行负担。(其实,最早的chrome进程设计其实没有单独的网络进程和GPU进程的,都是放到浏览器主进程中的)

当你打开浏览器的时候,默认先启动浏览器主进程,展示了整个浏览器窗口和地址栏等一系列的基础UI界面。然后主进程启动其他的各项子进程。当你附带要运行插件的时候,就给对应的插件分配一个进程,如果没有就不分配。当你打开了一个Tab标签页的时候,又会根据这个标签页创建一个渲染进程,用于标签页内页面的渲染和脚本的执行。当然,如果你的页面使用了WebGL或者CSS3动画之类的需要GPU做渲染的东西,Chrome也给了一个统一的CPU进程来维护这些渲染任务。你的html页面或者js脚本是需要从服务器下载到本地才能被渲染进程使用的,那我们就单独有个网络进程来处理这些和网络交互相关的任务。因此,一个基本的多进程架构就这样呈现了出来。

其中我们需要注意到几点:

  1. 其他各个进程都是由浏览器主进程启动和管理的,进程间通过IPC进行数据通信。
  2. 渲染进程和插件进程可能不止一个,根据要渲染的页面和插件数而定。

解决问题了么

那这样解决了我们单进程浏览器中出现的问题么?当然是解决了。

首先我们看不稳定的问题,不稳定是由于插件或者渲染引擎之类的出错导致的,现在不管是插件还是页面的渲染工作都是单独在各自的线程中运行的,如果某个插件出错了,那最多是那个插件不能用,其他页面的浏览和其他的插件都会照常运行。如果是某个渲染进程出错,那也只是那个页面看不了了而已,其他页面也不会受到影响,更别说把整个浏览器搞崩溃。说到这里,你是不是又想起了曾经某个页面显示“崩溃了”,然后你关闭那个Tab标签然后重新打开一下就好了;又或者曾经浏览器告诉你你的Flash插件崩溃了,你想看有些视频网站看不了,但是网页还是正常的,重启浏览器之后,Flash又恢复了。🤓

然后再是不流畅。在页面、插件和浏览器三种各自分别有各自的线程的情况下,插件的执行不流畅只会影响到插件自身,页面的无论渲染还是js的执行,不流畅那也只是那一个页面。若是放在单进程浏览器时代,我们说的不流畅那就是不管任何页面还是任何一个插件引起的,会导致整个浏览器窗口、所有页面、所有插件的不流畅。多进程的方式,让这种“共患难”的模式不复存在。🤣

最后再谈不安全。多进程对安全问题的处理主要靠两方面:1.渲染进程和插件进程运行在沙盒环境中;2.相互隔离的进程数据不共享。拆分成多线程的形式让我们能够对每个单独的页面和每个单独的插件加上沙盒限制,剥除了渲染进程的文件系统权限,网络访问权限等;对于插件进程,也无法对某个页面全权操控,或者对浏览器主进程数据任意访问和修改,插件自身能做什么也严格受到限制。如果他们需要某种操作,比如网络请求或者文件访问怎么办呢?通过IPC告知浏览器主进程,主进程来决定给不给你权限以及给你哪些权限。是不是又想起了在chrome中打开某个地图网站,浏览器问你“是否允许网站获取你的位置信息”,又或者 “是否允许网站使用摄像头” 😇

做一个小实验

现在你可以跟着一起打开Chrome 【设置】=> 【更多工具】=> 【任务管理器】看看你的Chrome运行了哪些进程。

Chrome任务管理器

这里我直接打开Chrome浏览器,然后打开了两个Tab,一个是百度一个是Google。我们可以看到这里的任务管理器里显示了我们的浏览器运行了哪些进程,包括进程的名称,内存占用大小,CPU使用率,网络和进程ID等信息(实际你可以在这个标头点击鼠标右键列出更多参考信息)。和我们刚才说的差不多,我们无论如何得有一个浏览器主进程,然后有一个GPU进程渲染一些图形相关的东西,还有一个Network 的网络进程,另外两个标签页分别就是两个渲染进程。这个时候我想模拟一下当某个渲染进程崩溃会出现啥,点击百度这这个标签页,然后点击右下角的结束进程。你是不是也和我一样,出现了下面的页面呢。

页面进程崩溃

你应该也注意到了整个浏览器仍然是稳定运行的,Google的那个页面也是正常的,崩掉的只有百度的这一个页面而已。如果你的浏览器里运行了插件,你也可以试着关掉插件进程试试。

这个小实验是否对 “曾经在你打开某个页面由于某种原因整个浏览器就卡住,连关闭按钮都点不动的时候,你是只能强制关掉整个浏览器么?” 这个问题于你有所启发呢?

站点隔离(Site Isolation)

对于Chrome的多进程架构还有一个必然需要谈到的东西,就是站点隔离 Site Isolation 策略。我们之前说的是一个标签页一个渲染进程,但实际情况下如果真的每个便签页都是一个渲染进程的话,那还是有点浪费进程资源的。另一个方面有个很特殊的问题就是如果只是一个标签页一个渲染进程的话,那如果存在一个标签页,里面有一个iframe引用了另一个页面,那这两个不同网站的页面就会在同一个进程中去渲染和执行脚本。这样来看,Tab的进程隔离并不能对站点做隔离。而且由于已知存在的CPU级别的漏洞Spectre和Meltdown(它们可以让程序访问到不属于当前进程的数据),从安全性上来说,Chrome也必须对“一个标签页一个渲染进程”的策略做调整。

这里的调整策略就是站点隔离策略。所谓站点隔离,就是指同一个域下的内容,会放在同一个渲染进程中进行渲染。对于刚才我们提到的一个Tab标签页中存在iframe引入其他网页的情况,标签页自身肯定是一个渲染进程,但对于内部的iframe,如果iframe是和标签页属于同一域,那就共用渲染进程,否则会给这个iframe一个单独的渲染进程。比如下面这张图中的 a.comb.comc.com 的页面就是单独各自都有一个渲染进程。而如果 b.com你将其换成 a.com 下的某个其他页面,那他们就会使用同一个渲染进程了。

site isolation

其实站点隔离最重要的作用还是对于安全性的要求,其次才是可以节省那么一点内存。

这里给各位提供一个测试这个特性的网站: csreis.github.io/tests/cross… 。 在Chrome中打开,同时打开上面我们实验的时候打开过的【任务管理器】,点击测试网站的按钮,观察观察会发生什么。(注:属于同一个进程的时候,只有表示页面的信息行,没有进程ID )。😎

这里其实站点隔离策略也是适用于从一个页面里打开的新的Tab页的,比如我先打开得到招聘页的网站 www.igetget.com/join/work 点击右上角 “了解我们”,这个时候会打开一个新的Tab,然后再去【任务管理器】里面查看,你会发现,新打开的Tab页是没有进程ID的,当你点击这个进程的时候,它会选中这两个便签页行,其实也就是说它们是用的同一个进程。(注意,这里我们新打开的Tab和原始页面属于同一个域)

site isolation of tab

IE的多进程架构

IE浏览器从IE8开始其进程架构就变成多进程的了。下图就是IE8的进程架构示意图。看起来是不是有点慌乱,我们来理一下。

IE8的多进程架构

在说IE的多线程之前,我们需要先简单了解一下DLL这个东西,DLL叫做动态链接库,可以看做是某些个特定功能实现的代码库,在windows上编程要调用一些windows系统的功能的时候,就可以直接通过调用一些DLL库给我们提供对应的功能。

IE的主要进程其实就两类:

  1. 框架进程。
  2. Tab进程。

框架进程就是我们图中最上方的那个红色区域表示的东西,它呢其实主要就是一个UI进程,底层使用了 BrowseUI.dll 这个DLL库,用来构建用户界面,包括工具栏、菜单一类的东西。框架进程可以创建多个Tab进程,Tab进程与框架进程之间的通信是用了ALPC(高级本地调用)的机制,这种机制是windows内核中常用的,感兴趣可以自己再去了解。

Tab进程是用来渲染页面,执行JS代码(当然这里可能是JScript和VBscript,对于IE8至少不是标准的Ecmascript),以及执行一个IE插件程序。Tab进程里会调用到有关历史记录的 ShDocVw.dll 库,有关HTML解析、DOM生成和操作和JScript脚本的 MSHTML.dll库,有关网络和缓存的 Winlnet.dll 以及再上面包裹了一层的更安全的提供下载资源的 URLMon.dll 库等。可能你会想 **Tab进程就是说一个Tab页一个进程么?**实际上不是的,考虑到创建一个进程的开销比较大,在windows上创建一个新的Tab进程也需要载入这么一堆的动态链接库。IE8的Tab进程数目是有一定的限制策略的,当Tab进程达到最大限度的时候,新打开的网页会复用之前创建的Tab进程来处理。(另:这个策略存在于windows注册表中的 TabProcGrowth 键值,你可以Google一下这个配置信息看看改变它会如何改变Tab进程的使用策略)

Firefox的多进程架构

早期的firefox也是单进程的,当他们也发现把所有Tab页的HTML渲染和JavaScript执行,以及浏览器窗口的UI都放到一个进程里是一个非常糟糕的设计的时候,也开始在进程架构上做出了调整。然后Mozilla启动了一个叫做 Electrolysis (也叫做 e10s )的计划准备逐步将进程架构往多进程上去迁移,这个时间节点是在2016年。于是经历了Firefox一共9个版本的迭代,从Firefox 48 到 56,逐步的完善了多进程架构。

对于浏览器主进程、GPU进程、扩展程序进程来说,相信各位已经很了解了。也就不多废话了。

主要来看Tab进程,Tab进程和Chrome的渲染进程类似,也是用来渲染页面,执行Javascript代码的。不过对于Firefox的Tab进程来说,可以看出Firefox的Tab进程和IE的Tab进程有个相似点就是:有多个Tab进程,但都不一定是一个页面一个Tab进程,一个Tab进程可能会负责多个页面的渲染。作为对比,Chrome是以一个页面一个渲染进程,加上站点隔离的策略来进行的。所以我们可想而知,一般情况下,Chrome所需要的内存消耗应该也会更多,毕竟不像Firefox和IE一样对页面渲染所用的进程做最大值的限制,站点隔离的策略也只是优化了那么一些。我在下面附上一张内存消耗对比图,各位可以自行看一下。

browser memory usage

总结

对于早期的单进程浏览器来说,页面渲染、JS执行、插件运行、还有浏览器主程序的运行都放在单个的一个进程里,对于浏览器来说,无法应用更好的安全特性,而且很容易一崩全崩,即使是正常运行也会出现一些不流畅的问题。于是各个浏览器厂商最终都在向多进程浏览器做转变,Chrome浏览器在最开始发布的时候就是采用了多进程的架构,IE是从IE8开始做的调整,而Firefox则是开启 Electrolysis 计划,在2016年前后逐步将Firefox迁移到了多进程的架构模式。

在迁移的过程中,各个浏览器有个相同点是各个浏览器都将浏览器的主进程,也就是用来运行浏览器窗口的进程单独抽离成一个来运行,以此为基础创建各个去进行页面渲染的渲染进程等子进程,并统一管理。IE8的多进程架构比较简单,且强依赖于windows系统的各种动态链接库;Chrome呢比较大方的给到每个页面一个渲染进程,同时也用站点隔离的策略做好了优化,对插件进程也是比较大方的给每个插件都有单独的进程,这样执行的各个程序从进程层面隔离开来,相互影响降到很小的程度,唯一的问题就是进程分配太大方,内存占用也上去了;Firefox在多进程架构的设计上,给了一个专门运行插件进程,用来渲染页面的Tab进程和IE8的Tab进程有个相同点在于都是对Tab进程有个最大值的限制的。

另外Chrome最新的进程架构也在往SOA,也就是面向服务的架构的方向转型。实现的方式就是将网络、设备、UI、媒体一类的程序抽象为服务,统一放入基础服务层中,供浏览器主进程、插件进程和渲染进程调用。利于节省资源以及拥有更良好可扩展性,降低现有多进程架构中耦合性太高的问题。这块儿可以各位自行了解,文末给出的参考资料中也有涉及。

相信各位对于浏览器的进程架构至少也略知一二了,希望对你的日常开发或对浏览器的使用上有一定的帮助。

参考资料

  1. Internet Explorer Wikipedia: en.wikipedia.org/wiki/Intern…
  2. Internet Exploere Architecture: docs.microsoft.com/en-us/previ…
  3. Modern Multi-Process Browser Architecture: helgeklein.com/blog/2019/0…
  4. Inside look at mordern web browser (part 1): developers.google.com/web/updates…
  5. Multi-process Architecture: dev.chromium.org/developers/…
  6. Multiprocess Firefox: developer.mozilla.org/en-US/docs/…
  7. Multi-Process Firefox: everything you need to know: (www.ghacks.net/2016/07/22/…)[www.ghacks.net/2016/07/22/…]
  8. 极客时间《浏览器工作原理与实战》-- Chrome架构: 仅仅打开了1个页面,为什么有4个进程?
  9. Servicification: www.chromium.org/servicifica…
  10. Meltdown/Spectre: developers.google.com/web/updates…