浏览器的进程与线程
浏览器的进程与线程
本文的浏览器在没有特殊指明的情况下都是指的
Chrome
浏览器,因为它足够具有代表性。
什么是进程和线程
进程
学术上来说,进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位。
通俗来说,进程就是应用程序运行的载体,也可以理解为一个应用程序的具体实例。
例如Windows
系统中你可以在任务管理器中看到具体进程信息:
线程
在早期的操作系统中并没有线程的概念,进程就是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。
任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU
的要求越来越高,进程之间的切换或者通信开销较大,已经无法满足越来越复杂的程序的要求了。
于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。
一个进程中可以有多个线程,多个线程可以共享他们所属进程的数据。
进程和线程的联系
-
进程是操作系统分配资源的最小单位,它能够独立运行。但是线程不能够独立运行,线程必须存在在进程之中;
-
一个进程中可以有多个线程,线程之间共享它们所属进程的数据。而进程之间的数据一般来说却是相互隔离的(可以通过
IPC
进行进程间通信,但是开销比较大); -
只要单独的某一个线程执行出错,就会导致所属的整个进程崩溃;
-
进程关闭之后,进程之前运行时所占用的内存都会被操作系统回收。而线程却不会,如果线程中因为内存泄漏造成的一些内存无法释放,即使该线程关闭之后,该内存依旧会被占据;
单进程浏览器
顾名思义,单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件JS
运行环境、渲染引擎和页面等。
其实早在 2007
年之前,市面上浏览器都是单进程的。单进程浏览器的架构如下图所示:
各种不同的线程放在一个进程中,这样带来了很多问题。
阻塞
由单进程浏览器示意图可以看出,页面渲染,JS
执行环境等线程都在同一个名叫页面线程的线程中,这多个线程中只要有一个线程阻塞了,都会导致该线程中的其它模块无法运行下去,比如当JS
中执行一个长耗时的代码时:
那么此时页面线程中的页面渲染,插件等等模块的执行都会卡住;
并且,由于不同的Tab
页都运行在同一个进程中,那就意味着,只要一个页面卡住了,其他页面全部会卡住,这将是灾难性的。
系统资源占据过大
如果线程中造成内存泄漏,比如页面线程JS
执行过程中的闭包所导致的内存泄漏,或者其他语言编写的插件中所带来的内存泄漏,这些占据的系统资源是都不会随着某个页面被关闭而被正常回收的。
因为从始至终这个这个进程都没有关闭过。
随着出现内存泄漏的情况逐渐变多,该浏览器进程所占据的系统资源会越来越多,你的页面访问会越来越卡。
不稳定
通过前文我们得知,只要进程的其中一个线程崩溃,那么当前整个进程都会直接崩溃。那么如果遇到以下的一些情况,整个浏览器的不同Tab
页全部会挂掉。
- 单进程时代的浏览器通过插件扩展各种功能,其中的某一个插件运行出现问题;
- 某个页面中
JS
执行出现了异常; - …
不安全
先提一嘴安全沙箱大概是什么:
可以把安全沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据。
而由于线程是无法单独运行的,他必须依赖一个进程,因此无法让线程单独运行在安全沙箱中。
那就意味着,单进程浏览器中的页面线程中的JS
或者插件模块能够突破系统权限去一些恶意的事,比如某个插件利用浏览器漏洞去给你的电脑装一些病毒,盗取你的密码之类的,这些显然都是不安全的。
也许你会想,那为什么不直接将整个浏览器进程直接放入安全沙箱中,但是需要知道的是,浏览器是具有下载,打开文件,访问网络等的功能,如果直接将整个浏览器直接装到安全沙箱中,那就会丧失这些功能。
多进程浏览器
早期多进程架构
下图是 2008
年 Chrome
发布时的进程架构:
从图中可以看出,Chrome
的页面是运行在单独的渲染进程中的,同时页面里的插件也是运行在单独的插件进程之中,而进程之间是通过 IPC
机制进行通信(如图中虚线部分)。
-
我们先看看如何解决阻塞和不稳定的问题:
由于进程是相互隔离的,所以当一个页面,插件崩溃或者阻塞时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面;
-
接下来再来看看系统资源占据过大是如何解决的:
因为当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收;
-
最后我们再来看看安全问题是怎么解决的:
采用多进程架构的额外好处是可以使用安全沙箱,我们可以给你需要安全限制的进程直接加上安全沙箱即可。
比如我们可以给插件进程或者渲染进程加上安全沙箱,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限;
近期多进程架构
下图是近期 Chrome
的进程架构:
从图中可以看出,近期的 Chrome
浏览器包括:1 个浏览器(Browser
)主进程、1 个 GPU
进程、1 个网络(NetWork
)进程、多个渲染进程和多个插件进程。
备注:当前是2022
年,对应Chrome
的版本为 97.0.4692.71
(正式版本)(64
位)。因此会发现里面多出一些Service
,这是最新的面向服务的架构。而我们所描述的版本比这个要老一些,单纯就是多进程架构,因此会发现一些出入。
下面我们来逐个分析这几个进程的功能:
-
浏览器进程(单个):
- 负责浏览器界面显示,与用户交互。如前进,后退等;
- 子进程管理,负责各个页面的管理,创建和销毁其他进程;
- 将渲染进程得到的内存中的
Bitmap
,绘制到用户界面上; - 网络资源的管理,下载等;
-
GPU
进程(单个):其实,
Chrome
刚开始发布的时候是没有GPU
进程的。而GPU
的使用初衷是为了实现3D CSS
的效果,只是随后网页、Chrome UI
界面都选择采用GPU
来绘制,这使得GPU
成为浏览器普遍的需求。最后,Chrome
在其多进程架构上也引入了GPU
进程; -
网络进程(单个):
主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程;
-
渲染进程(可以为多个):
出于安全考虑,渲染进程都是运行在沙箱模式下,渲染进程的核心任务是将
HTML
、CSS
和JS
转换为用户可以与之交互的网页,排版引擎Blink
和JS
引擎V8
都是运行在该进程中。默认情况下,
Chrome
会尽可能为每个Tab
标签页甚至是页面里面的每一个iframe
都分配一个单独的渲染进程。但是也有一些特殊的情况,关于具体细节,在下面的章节中细说; -
插件进程(可以为多个):
主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。一般来说每运行一个不同的插件,就会开启一个进程来运行它;
渲染进程
进程合并策略
上文我们知道,Chrome
每打开一个Tab
页默认情况下都会创建一个单独的渲染进程。
但是Chrome
实际上是存在一种进程合并的策略,用来降低内存占用,方便不同Tab
页共享同一个渲染进程的数据。
如下图:
理想情况
比如如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程,官方把这个默认策略叫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
页的渲染进程的合并与否,有很多种情况:
比如:
-
如果你直接从地址栏输入地址的,即便同一站点,也会使用不同的渲染进程;
-
如果代码设置了一些安全属性,即便同一站,也会使用不同的渲染进程;
-
…
渲染进程中的多线程
-
GUI
渲染线程- 负责渲染浏览器界面,布局和绘制等。解析
HTML
,CSS
,构建DOM Tree, CSSOM Tree
最终组合得到Render Tree
; - 当界面需要重绘(
Repaint
)或由于某种操作引发回流(Reflow
)时,该线程就会重新执行; GUI
渲染线程与JS
引擎线程是互斥的,当JS
引擎执行时GUI
线程会被挂起(相当于被冻结了),GUI
更新会被保存在一个队列中等到JS
引擎空闲时立即被执行;
- 负责渲染浏览器界面,布局和绘制等。解析
-
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
线程同时进行;
-
-
事件触发线程(不要将其理解为专门处理
click, enter
之类的DOM
事件的线程)-
归属于浏览器而不是
JS
引擎,用来控制事件循环(可以理解,JS
引擎自己都忙不过来,需要浏览器另开线程协助); -
当
JS
引擎执行代码块如setTimeOut
时(也可来自浏览器内核的其他进程或者线程,如鼠标点击、ajax
异步请求等),会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时(比如
xhr
请求得到回调,或者定时器时间到了),该线程会把事件添加到待处理队列(先进先出)的队尾,等待JS
引擎的处理; -
注意,由于
JS
的单线程关系,所以这些待处理队列中的事件都得排队等待JS
引擎处理(当JS
引擎空闲时,也就是同步执行栈为空时才会去执行),具体可以参考事件循环;
-
-
定时触发器线程
- 处理
setInterval
与setTimeout
的线程; - 浏览器定时计数器并不是由
JS
引擎计数的,(因为JS
引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确); - 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待
JS
引擎空闲后执行); - 注意,
W3C
在HTML
标准中规定,规定要求setTimeout
中低于4ms
的时间间隔算为4ms
。但是实际上由于浏览器性能或者实现机制不同等,实现往往没有这么精确;
- 处理
-
异步网络请求线程
- 在
xhr
在连接后是通过浏览器新开一个线程请求,fecth
请求(应该也会开启); - 该线程会通过
IO
线程去通知浏览器的网络进程,然后由网络进程向服务器发送请求; - 当检测到状态变更时(比如服务器有响应时),网络进程会通过
IO
线程告知异步网络请求线程,然后再通过异步网络请求线程告知事件触发线程,将其回调函数放入事件队列中,等待JS
线程执行;
- 在
-
合成线程(
compositing
)GUI
线程它解析出来的DOM Tree, CSSOM Tree, Render Tree
一些渲染信息(比如图层等信息)最终会汇总得到一个绘制列表,里面包含具体该如何绘制页面等信息:
- 然后该绘制列表会传递给合成线程,然后合成线程会根据绘制信息将图层分成图块,并在栅格化(是指将图块转换为位图)线程池中将图块转换成位图,图块是栅格化执行的最小单位;
- 渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的;
-
通常,栅格化过程都会使用
GPU
来加速生成,使用GPU
生成位图的过程叫快速栅格化,或者GPU
栅格化,生成的位图被保存在GPU
内存中。相信你还记得,GPU
操作是运行在GPU
进程中,如果栅格化操作使用了GPU
,那么最终生成位图的操作是在GPU
中完成的,这就涉及到了跨进程操作;具体形式你可以参考下图:
-
从图中可以看出,渲染进程把生成图块的指令发送给
GPU
,然后在GPU
中执行生成图块的位图,并保存在GPU
的内存中; -
一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——
DrawQuad
,然后将该命令提交给浏览器进程; -
浏览器进程里面有一个叫
viz
的组件,用来接收合成线程发过来的DrawQuad
命令,然后根据DrawQuad
命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上;