Skip to content

浏览器的进程与线程

浏览器的进程与线程

本文的浏览器在没有特殊指明的情况下都是指的Chrome浏览器,因为它足够具有代表性。

什么是进程和线程

进程

学术上来说,进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位。

通俗来说,进程就是应用程序运行的载体,也可以理解为一个应用程序的具体实例。

例如Windows系统中你可以在任务管理器中看到具体进程信息:

image-20220120161721795

线程

在早期的操作系统中并没有线程的概念,进程就是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。

任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换或者通信开销较大,已经无法满足越来越复杂的程序的要求了。

于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。

一个进程中可以有多个线程,多个线程可以共享他们所属进程的数据。

进程和线程的联系

img

  1. 进程是操作系统分配资源的最小单位,它能够独立运行。但是线程不能够独立运行,线程必须存在在进程之中;

  2. 一个进程中可以有多个线程,线程之间共享它们所属进程的数据。而进程之间的数据一般来说却是相互隔离的(可以通过IPC进行进程间通信,但是开销比较大);

  3. 只要单独的某一个线程执行出错,就会导致所属的整个进程崩溃;

  4. 进程关闭之后,进程之前运行时所占用的内存都会被操作系统回收。而线程却不会,如果线程中因为内存泄漏造成的一些内存无法释放,即使该线程关闭之后,该内存依旧会被占据;

单进程浏览器

顾名思义,单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件JS 运行环境、渲染引擎和页面等。

其实早在 2007 年之前,市面上浏览器都是单进程的。单进程浏览器的架构如下图所示:

Chrome-单进程浏览器

各种不同的线程放在一个进程中,这样带来了很多问题。

阻塞

由单进程浏览器示意图可以看出,页面渲染,JS执行环境等线程都在同一个名叫页面线程的线程中,这多个线程中只要有一个线程阻塞了,都会导致该线程中的其它模块无法运行下去,比如当JS中执行一个长耗时的代码时:

for (let i = 0; i < 100 * 100 * 100 * 100; i++) {
// ...
}

那么此时页面线程中的页面渲染,插件等等模块的执行都会卡住;

并且,由于不同的Tab页都运行在同一个进程中,那就意味着,只要一个页面卡住了,其他页面全部会卡住,这将是灾难性的。

系统资源占据过大

如果线程中造成内存泄漏,比如页面线程JS执行过程中的闭包所导致的内存泄漏,或者其他语言编写的插件中所带来的内存泄漏,这些占据的系统资源是都不会随着某个页面被关闭而被正常回收的。

因为从始至终这个这个进程都没有关闭过。

随着出现内存泄漏的情况逐渐变多,该浏览器进程所占据的系统资源会越来越多,你的页面访问会越来越卡。

不稳定

通过前文我们得知,只要进程的其中一个线程崩溃,那么当前整个进程都会直接崩溃。那么如果遇到以下的一些情况,整个浏览器的不同Tab页全部会挂掉。

  1. 单进程时代的浏览器通过插件扩展各种功能,其中的某一个插件运行出现问题;
  2. 某个页面中JS执行出现了异常;

不安全

先提一嘴安全沙箱大概是什么:

可以把安全沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据。

而由于线程是无法单独运行的,他必须依赖一个进程,因此无法让线程单独运行在安全沙箱中。

那就意味着,单进程浏览器中的页面线程中的JS或者插件模块能够突破系统权限去一些恶意的事,比如某个插件利用浏览器漏洞去给你的电脑装一些病毒,盗取你的密码之类的,这些显然都是不安全的。

也许你会想,那为什么不直接将整个浏览器进程直接放入安全沙箱中,但是需要知道的是,浏览器是具有下载,打开文件,访问网络等的功能,如果直接将整个浏览器直接装到安全沙箱中,那就会丧失这些功能。

多进程浏览器

早期多进程架构

下图是 2008Chrome 发布时的进程架构:

img

从图中可以看出,Chrome 的页面是运行在单独的渲染进程中的,同时页面里的插件也是运行在单独的插件进程之中,而进程之间是通过 IPC 机制进行通信(如图中虚线部分)。

  1. 我们先看看如何解决阻塞和不稳定的问题:

    由于进程是相互隔离的,所以当一个页面,插件崩溃或者阻塞时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面;

  2. 接下来再来看看系统资源占据过大是如何解决的:

    因为当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收;

  3. 最后我们再来看看安全问题是怎么解决的:

    采用多进程架构的额外好处是可以使用安全沙箱,我们可以给你需要安全限制的进程直接加上安全沙箱即可。

    比如我们可以给插件进程或者渲染进程加上安全沙箱,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限;

近期多进程架构

下图是近期 Chrome 的进程架构:

img

从图中可以看出,近期的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。

image-20220118221939761

备注:当前是2022年,对应Chrome的版本为 97.0.4692.71(正式版本)(64 位)。因此会发现里面多出一些Service,这是最新的面向服务的架构。而我们所描述的版本比这个要老一些,单纯就是多进程架构,因此会发现一些出入。

下面我们来逐个分析这几个进程的功能:

  1. 浏览器进程(单个):

    • 负责浏览器界面显示,与用户交互。如前进,后退等;
    • 子进程管理,负责各个页面的管理,创建和销毁其他进程;
    • 将渲染进程得到的内存中的Bitmap,绘制到用户界面上;
    • 网络资源的管理,下载等;
  2. GPU 进程(单个):

    其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程;

  3. 网络进程(单个):

    主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程;

  4. 渲染进程(可以为多个):

    出于安全考虑,渲染进程都是运行在沙箱模式下,渲染进程的核心任务是将 HTMLCSSJS 转换为用户可以与之交互的网页,排版引擎 BlinkJS 引擎 V8 都是运行在该进程中。

    默认情况下,Chrome会尽可能为每个 Tab 标签页甚至是页面里面的每一个iframe都分配一个单独的渲染进程。但是也有一些特殊的情况,关于具体细节,在下面的章节中细说;

  5. 插件进程(可以为多个):

    主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。一般来说每运行一个不同的插件,就会开启一个进程来运行它;

渲染进程

进程合并策略

上文我们知道,Chrome每打开一个Tab页默认情况下都会创建一个单独的渲染进程。

但是Chrome实际上是存在一种进程合并的策略,用来降低内存占用,方便不同Tab页共享同一个渲染进程的数据。

如下图:

image-20220118220617691

理想情况

比如如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程,官方把这个默认策略叫process-per-site-instance

那么什么是同一站点呢?

我们将同一站点定义为根域名(例如,baidu.com)加上协议(例如,https:// 或者http://)相同,比如下面这三个:

https://time.geekbang.org

https://www.geekbang.org

https://www.geekbang.org:8080

都是属于同一站点,因为它们的协议都是https,而根域名也都是geekbang.org

实际情况

上述合并策略只是理想情况下,实际上不同Tab页的渲染进程的合并与否,有很多种情况:

比如:

  1. 如果你直接从地址栏输入地址的,即便同一站点,也会使用不同的渲染进程;

  2. 如果代码设置了一些安全属性,即便同一站,也会使用不同的渲染进程;

渲染进程中的多线程

渲染进程多线程示意图

  1. GUI渲染线程

    • 负责渲染浏览器界面,布局和绘制等。解析HTMLCSS,构建DOM Tree, CSSOM Tree最终组合得到Render Tree
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会重新执行;
    • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行;
  2. JS引擎线程

    我们常规说的JS为单线程指的就是这个JS引擎线程是单线程的。

    • 也称为JS内核,负责处理JS脚本程序(例如V8引擎);

    • JS引擎线程负责解析JS脚本,运行代码;

    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Render进程)中无论什么时候都只有一个JS线程在运行JS程序;

    • GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞;

      互斥的原因就是JS可能会对DOM或者CSS进行操作,也就是说JS可能会影响DOM Tree, CSSOM Tree, Render Tree的结构,因此GUI线程不能同时和JS线程同时进行;

  3. 事件触发线程(不要将其理解为专门处理click, enter之类的DOM事件的线程)

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助);

    • JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他进程或者线程,如鼠标点击、ajax异步请求等),会将对应任务添加到事件线程中。

      当对应的事件符合触发条件被触发时(比如xhr请求得到回调,或者定时器时间到了),该线程会把事件添加到待处理队列(先进先出)的队尾,等待JS引擎的处理;

    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时,也就是同步执行栈为空时才会去执行),具体可以参考事件循环;

  4. 定时触发器线程

    • 处理setIntervalsetTimeout的线程;
    • 浏览器定时计数器并不是由JS引擎计数的,(因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确);
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行);
    • 注意,W3CHTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。但是实际上由于浏览器性能或者实现机制不同等,实现往往没有这么精确;
  5. 异步网络请求线程

    img

    • xhr在连接后是通过浏览器新开一个线程请求,fecth请求(应该也会开启);
    • 该线程会通过IO线程去通知浏览器的网络进程,然后由网络进程向服务器发送请求;
    • 当检测到状态变更时(比如服务器有响应时),网络进程会通过IO线程告知异步网络请求线程,然后再通过异步网络请求线程告知事件触发线程,将其回调函数放入事件队列中,等待JS线程执行;
  6. 合成线程(compositing

    • GUI线程它解析出来的DOM Tree, CSSOM Tree, Render Tree一些渲染信息(比如图层等信息)最终会汇总得到一个绘制列表,里面包含具体该如何绘制页面等信息:

    img

    • 然后该绘制列表会传递给合成线程,然后合成线程会根据绘制信息将图层分成图块,并在栅格化(是指将图块转换为位图)线程池中将图块转换成位图,图块是栅格化执行的最小单位;

    img

    • 渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的;

    img

    • 通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作;

      具体形式你可以参考下图:

    img

    • 从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中;

    • 一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——DrawQuad,然后将该命令提交给浏览器进程;

    • 浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上;

参考

Chrome架构:仅仅打开了1个页面,为什么有4个进程?