浏览器的架构演进

274 阅读8分钟

浏览器的架构演进

进程与线程

对于进程与线程,简单来讲线程就是执行任务的单元,而进程则是用来管理和启动线程的。

我们可以这样理解,一个进程就是一个程序的运行实例。当我们启动一个程序时,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程.

此处简单介绍一下单线程与多线程,在理解单线程与多线程之前,我们先理解一下什么叫并行处理。

计算机中的并行处理就是同一时刻处理多个任务,比如我们要计算下面这三个表达式的值,并显示出结果。

A = 1+2
B = 20/5
C = 7*8

在编写代码的时候,我们可以把这个过程拆分为四个任务:

  • 任务 1 是计算 A=1+2;
  • 任务 2 是计算 B=20/5;
  • 任务 3 是计算 C=7*8;
  • 任务 4 是显示最后计算的结果。

正常情况下程序可以使用单线程来处理,也就是分四步按照顺序分别执行这四个任务。

如果采用多线程,我们只需分“两步走”:第一步,使用三个线程同时执行前三个任务;第二步,再执行第四个显示任务。

通过对比分析,你会发现用单线程执行需要四步,而使用多线程只需要两步。因此,使用并行处理能大大提升性能。

为了更好的理解,请参照下面这张图。

image.png

如果还是不能很好理解,可以参考下面的示例。

如下的函数中我们使用了await,所以此次执行是顺序执行的,当第一个接口的信息返回后,才会去发起第二个请求, 这样整体的请求时间是三次的请求时间之和。

async request() {
    const a = await axios.get('xxx');
    const b = await axios.get('yyy');
    const c = await axios.get('zzz');
}

在举一个多线程的例子,如下代码中我们使用Promise.all去同时发起了三次请求,此时网络进程会开启三个请求线程来处理对应的任务,这样整体的请求时间就是三次中耗时最长的接口的时间。

const a = axios.get('xxx');
const b = axios.get('yyy');
const c = axios.get('zzz');
Promise.all([a,b,c]);

总结下来,进程和线程之间的关系有如下4点。

  • 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
A = 1+2
B = 20/0
C = 7*8

我把上述三个表达式稍作修改,在计算 B 的值的时候,我把表达式的分母改成 0,当线程执行到 B = 20/0 时,由于分母为 0,线程会执行出错,这样就会导致整个进程的崩溃,当然另外两个线程执行的结果也没有了。

  • 线程之间共享进程中的数据。 image.png

从上图可以看出,线程 1、线程 2、线程 3 分别把执行的结果写入 A、B、C 中,然后线程 2 继续从 A、B、C 中读取数据,用来显示执行结果。

  • 当一个进程关闭之后,操作系统会回收进程所占用的内存。 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。

  • 进程之间的内容相互隔离。 进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。

单进程浏览器时代

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。其实早在 2007 年之前,市面上浏览器都是单进程的。单进程浏览器的架构如下图所示:

image.png

单进程浏览器的缺点

  • 不稳定 早期的浏览器会借助于插件来实现视频(flash?)等各种强大的功能,上面我们已经说到了进程中的任意一线程执行出错,都会导致整个进程的崩溃。 ,那么插件的崩溃会导致整个浏览器的崩溃。试想一下,打开了多个tab页签,写了一下午的邮件,因为某个插件的bug,导致浏览器崩溃,而网页版的邮箱也没有提供暂存功能,是不是很崩溃。

  • 不流畅 所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行。那如果有一个地方代码执行过长,甚至产生了死循环,就会引起其他后续流程的执行。

  • 不安全 这里可以从插件和页面脚本两个方面来分析。插件可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题。

至于页面脚本,它可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题。

目前多进程架构

image.png

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

下面分析一下现在的多进程架构是如何解决单进程架构的缺点的。

  • 不稳定。由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面,这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃,也就是不稳定的问题。
  • 不流畅。同样,JavaScript 也是运行在渲染进程中的,所以即使 JavaScript 阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面,因为其他页面的脚本是运行在它们自己的渲染进程中的。所以当我们再在 Chrome 中运行上面那个死循环的脚本时,没有响应的仅仅是当前的页面。对于内存泄漏的解决方法那就更简单了,因为当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收,这样就轻松解决了浏览器页面的内存泄漏问题。
  • 不安全。采用多进程架构的额外好处是可以使用安全沙箱,你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据,例如你的文档和桌面。Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。

读史使人明智,读诗使人灵秀,数学使人周密,科学使人深刻,伦理使人庄重,逻辑修辞之学使人善辩。

摘自:浏览器工作原理与实践