本专栏主要讲解浏览器底层工作的原理,鉴于自己在极客时间上面学习李兵的《浏览器工作原理与实践》,同时自己将学习资料整理了一下,再加上自己的一些理解,从而写的一份浏览器原理的专栏文章笔记。相信你打开这篇文章目的就是为了搞懂浏览器到底是怎么工作的,理解浏览器的执行原理,不仅对你面试有帮助,还能针对前端性能优化作出相应的处理,如果对你学习有帮助,请赐给小编一个赞!!。废话不多说,直接开卷。
基于现在最流行的浏览器之一,本文主要以chrome浏览器为主进行讲解,我们首先来看一下,当我们在浏览器中打开一个页面的时候,看看浏览器任务管理器中都有哪些进程。如下图:
看到这里或许你会有点疑惑,为什么我只打开一个页面,需要启动这么多线程,在回答这个问题之前,首先,你要了解什么是进程和线程。
进程和线程
进程:描述了CPU在运行指令
及加载
和保存上下文
所需要的时间,放在应用上来说就是代表一个程序。是操作系统分配资源的最小单位
。
一个进程就是一个程序的运行实例。详细的解释其实就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据
和一个执行任务的主线程
,我们将这样的一个运行环境叫做进程
线程:描述了执行一段指令
所需要的时间,线程是进程中的更小单位。是CPU调度的最小单位。
它是由进程启动和管理的。
这里需要注意的是:线程是不能够脱离进程而独立存在的,但是每一个线程在进程中都是独立存在的。
并行处理
在计算机中的并行处理就是在"同一时间点"处理多个任务,比如我们需要计算下面的三个表达式的值,并显示出结果。
A = 1 + 2
B = 20 / 5
C = 7 * 8
假设这里的的执行是单线程,我们以任务的形式进行划分,分别是以下的几个过程:
任务1: 计算 1 + 2
任务2: 计算 20 / 5
任务3: 计算 7 * 8
任务4: 显示最后的计算结果
假设这里使用多线程的话,可以将这里的过程划分为2个,首先,执行计算的部分,也就是任务1、任务2、任务3,然后再执行任务4的显示计算的结果。
这样,一对比下来,执行相同的操作,单线程需要4步,多线程只需要2步,你是否发现采用多线程比单线程的效率是不是更高了?因此,使用并行处理能够大大提升性能
。
为了能够更加直观的理解,下面画出了两个情况的对比图:
可以从上图可以看出,线程是依附于进程的,而进程中使用多线程并行处理能够提升运算效率
。这也验证了上面所说线程概念的一点。
进程 VS 线程
总的来说,进程和线程之间的关系有以下四个特点:
1、进程中的任意一线程出错,都会导致整个进程的崩溃。
基于上面的例子,我们可以模拟一下这个特点的场景:
A = 1 + 2
B = 20 / 5 --> 20 / 0
C = 7 * 8
当我计算B中的值时,将分母从5变成0的时候,当线程执行到B的时候,此时就会报错,这样就会导致整个进程的崩溃,当然,另外的两个线程执行的结果也不会显示出来。
2、线程之间共享进程中的数据
线程之间可以对进程的公共数据进行读写操作。如下图所示:
从上图可以看出,线程1、线程2、线程3分别将执行的结果写道A、B、C中,然后线程2又去A、B、C、中读取数据,用来展示执行的结果。
3、当一个进行关闭之后,操作系统会回收进程所占用的内存
当一个进程退出时,操作系统会回收该进程所申请的内存资源,即使其中任意线程因为操作不当而导致内存泄漏,当进程退出时,这些内存也会被正确的回收。
比如之前的IE浏览器,支持很多插件,而这些插件又很容易导致内存泄漏,这意味着只要浏览器还在开着,内存的占用就可能越来越多,但是当关闭浏览器进程时,这些内存就会被系统回收掉。
4、进程之间的内容相互隔离
进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免了A进程将数据写入到B进程中的情况。正因为进程之间的数据是相互隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其它进程的。如果进程之间需要进行数据的通信,这时候,就需要用进程间通信的机制了(IPC)
有了上面的特点,下面列举一下两者之间的区别:
1、进程可以看做独立应用,线程不能,并且线程必须依附于进程,一个进程可以有多个线程。
2、进程是资源分配的最小单位,线程是CPU调度的最小单位。
3、进程之间通信需要借助(IPC),线程间可以直接共享同一进程的资源
4、进程的切换要比线程的切换开销要更大
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O等,其开销远大于创建或撤销线程。同理,在进行进程切换时,涉及当前进程CPU环境
还有各种状态的保存
及新调度进程状态
的设置,而线程切换时只需要保存和设置少量的寄存器内容
,开销比较小。
既然说到了进程,那就不得不提到死锁,下面简单说一下什么是死锁,为什么产生死锁,怎么避免死锁?
死锁
死锁 :所谓的死锁,是指多个进程在运行过程中因争夺资源而陷入的一种僵局,当进程处于这种僵持状态时,若无外力的作用,它们将无法再向前推进。
操作系统中将资源分为两类:
可剥夺资源
是指某进程获得资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺资源;
不可剥夺资源
当系统把这类资源分配给某个进程后,再不能强行收回,只能在进程结束之后自行的释放,如磁带机、打印机等。
产生死锁的原因
(1)竞争资源:
竞争不可剥夺资源(如系统中只有一台打印机、可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
竞争临时资源(如临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁。
(2)进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程在向前推进,便可能发生死锁。(如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞,当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞)
产生死锁的必要条件
1、互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一进程所占用。(如:有两个进程或线程A和B,其中A需要占有资源R1,而B需要占有资源R2,那么只有当A占有资源R1时,B才能占有资源R2,否则B将被阻塞,无法继续执行)
2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。(当一个线程或进程需要使用某个资源时,必须请求该资源,并且在获得该资源之前,他将一直占用该资源)
3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。(如:在一个系统中,有两个进程或线程A和B,其中A拥有资源A和B,而B需要资源A和B才能继续执行,如果A不释放资源A,那么B就无法获得资源A和B,因此系统就陷入死锁状态)
4、环路等待条件:指系统中有多个资源相互等待对方释放某个资源的状态,而这些资源又必须互相制约,使得没有一个资源可以被释放。(如:一个系统中有两个进程或线程,他们分别需要使用两个不同的资源A和B。并且他们都在等待对方释放资源A或B,那么系统就陷入死锁状态,因为无法分配资源给任何一进程或线程)
预防死锁方法
1、资源一次性分配:一次性分配所有资源,这样就不会再有请求了。(破坏请求条件)
2、只要有一个资源得不到分配,也不给这个进程分配其他资源。(破坏请求保持条件)
3、可剥夺资源:即当某进程获得了部分资源,但得不到其他资源,则释放已占有的资源。(破坏不可剥夺条件)
4、资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反。(破坏环路等待条件)
单进程浏览器的时代
了解了进程和线程之后,我们再来看一下单进程浏览器的架构,在早期的浏览器里面,浏览器的所有功能模块都是运行在同一个进程里的。
这里的功能模块包括网络、插件、JS引擎、渲染引擎、页面
等等,早期浏览器都是单进程的浏览器。单进程浏览器的架构如下图所示:
如此多的功能模块只运行在一个进程里面,导致了单进程浏览器出现不稳定、不流畅、不安全的因素。下面我们来分析一下,为什么会出现这些因素。
问题1:不稳定
我们已经知道,单进程浏览器会将全部的功能模块都在一个进程中运行,这里就涉及到了前面我们所说的进程和线程之间的特点,在同一个进程中的任意一线程出错,都会导致整个进程崩溃。
早期浏览器是需要借助各种插件来实现Web视频、Web游戏等各种功能,但是这些插件是最容易出错的模块。
除了插件之外,渲染引擎模块也是不稳定的,通常一些复杂化的JavaScript代码就有可能会引起渲染引擎模块的出错。和插件一样,渲染引擎的出错也会导致整个浏览器的崩溃。
问题2:不流畅
从上面的单进程浏览器架构图可以看出,所有页面的渲染模块、JS引擎和插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行。
试想一下执行一个JavaScript这个无限循环的代码
function freeze() {
while (1) {
console.log('qf')
}
}
freeze();
如果这段代码在单进程浏览器中,你可以结合上面所说的并行处理的例子思考一下,当执行时,他会独占整一个页面线程,这样就会导致其它功能运行在该线程的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中
,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应、变得卡顿。
除了JavaScript代码的执行
和插件
会导致单进程浏览器变得卡顿之外,页面的内存泄漏
也是单进程变慢的一个重要原因。通常浏览器的内核都是非常复杂的,运行一个复杂点的页面再关闭页面,会存在内存不能够完全回收的情况,这样就会导致使用时间越长,内存占用就会越高,浏览器会变得越慢。
问题3:不安全
这里可以从插件以及页面脚本两个方面来进行解释: 插件是可以使用C/C++等其它语言进行编写的,通过插件是可以获取操作系统的任意资源,当你运行一个插件的时候,也就意味着这个插件可以完全操作你的电脑。如果是恶意的插件,那么就会释放病毒、窃取你的用户信息,会引起安全问题。(插件直接对接操作系统)
至于页面脚本,它可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,也会引发安全问题。
多进程浏览器的时代
现在多进程浏览器已经解决了上面的问题了,那么我们正式来聊聊是如何解决的。
早期多进程架构
早期多进程架构,以2008年Chrome浏览器发布的进程架构为图例: 从图中我们可以看出,Chrome的页面是运行在单独的渲染进程中的,同时页面里的插件也是单独的运行再插件进程之中的,而进程之间的通信是通过IPC机制通信的(虚线部分)
解决问题 1 不稳定
我们知道每个进程都是独立的、相互隔离的
,所以当一个页面或者插件崩溃时,仅仅是影响到当前页面进程或者是插件进程,并不会影响到浏览器和其它页面,这就完美的解决了页面或者插件的崩溃导致整个浏览器也会崩溃,这就解决了不稳定的问题。
解决问题 2 不流畅
同样,JavaScript引擎也是运行在渲染进程中的,所以即使JavaScript代码阻塞了渲染进程,影响的只是当前渲染进程渲染的页面,而不会影响浏览器和其它页面的
,因为其它页面的JavaScript代码是运行在它们各自的渲染进程中的。所以,我们在Chrome中运行死循环的代码时,没有响应的仅仅是当前页面。
对于内存泄漏的问题,同样的,当关闭页面的时候,对应的的渲染进程也会被关闭,之后该进程所占用的内存也会被系统回收
,这样就轻松解决浏览器页面的内存泄漏问题。
解决问题 3 不安全
我们从图中可以看到,当前的渲染进程会有一个沙箱进行包裹着,不仅是渲染进程有沙箱进行包裹,所有的进程外面都有一层沙箱包裹
,你可以将沙箱看成是操作系统给进程上的一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能读取一些敏感的信息。例如,你的硬盘中的文档和桌面,Chrome把插件进程和渲染进程锁在了沙箱里面,这样即使在渲染进程和插件进行中执行了恶意代码,那么恶意代码也无法突破沙箱去获得系统的权限。
目前多进程架构
了解了早前多进程浏览器架构之后,我们现在来看一下目前多进程的架构。以下是最新的Chrome进程架构,如下图:
从图中可以看出,最新的Chrome浏览器包括:
1个浏览器主进程(Browser Process)、1个网络进程(NewWork Process)、1个GPU进程(GPU Process)、多个渲染进程(Render Process)、多个插件进程(Plugin Process)。
下面分析一下这五大进程的功能:
浏览器主进程(Browser Process)
主要负责页面显示、交互、子进程管理、同时提供存储功能。
渲染进程(Render Process)
负责将html、css、JavaScript转为用户可以操作交互的页面,排版引擎和JavaScript引擎V8都是运行在该进程中。每新开一个tab页面都会新增一个渲染进程,出于安全考虑,该进程需要使用沙箱进行包裹。
渲染进程包含五个线程:
GUI 渲染线程
负责渲染浏览器页面,解析html、css、构建DOM树、构建CssStyle、才构建渲染树和绘制页面;当界面需要重绘或重排时,该线程就会执行。
注意
:GUI渲染线程和JS引擎线程是互斥的
,也就是说JS引擎执行时GUI渲染线程会被挂起,GUI的更新会被保存在任务队列中等待JS引擎空闲的时候去执行。
JS 引擎线程
负责处理JavaScript脚本程序,解析JavaScript脚本,运行代码,JS引擎线程一直等待任务队列中任务的到来,然后再去任务队列中获取任务执行,一个tab页面都只有一个JS引擎线程在运行JS程序。
注意
:GUI渲染线程与JS引擎线程互斥
,如果JS执行的时候过长,那么就会造成页面的渲染不流畅,也就是会导致页面渲染阻塞。
事件触发线程
包含核心的事件循环(Event Loop)、事件源(点击事件等)、事件队列
等,这里需要注意的是,事件触发线程是属于浏览器而不是JS引擎
。当JS引擎执行代码块,会将对应的任务添加到事件触发线程中,当符合触发条件时,该线程会把事件添加到待处的任务队列中,等待JS引擎的处理。
定时器触发线程
包含setTimeout、setInterval
所在的线程。这里需要注意,浏览器定时器计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果线程处于阻塞的状态,那么就会影响到计数的准确性,所以需要单独的线程来计数。等到指定的时间之后,会将回调函数添加到任务队列中,等待JS引擎空闲时再去执行,所以定时器的任务不一定能够准时的执行,定时器只是在指定的时间点将任务添加到任务事件队列中。
注意:W3C在html标准中规定,定时器的时间不能小于4ms,如果小于4ms,则默认是4ms。
异步http请求线程
处理异步请求的线程,不会阻塞浏览器的执行,而是在请求响应返回时继续执行后续的代码。检测状态变更时,是否有设置回调函数,如果有,则添加到任务队列中,等待JS引擎执行。
GPU进程(GPU Process)
一开始CPU进程只是为了用来实现3D CSS的效果,随后chrome浏览器都是用这个进程来对页面进行绘制,且这个进程会加速网页的渲染和图形处理,从而提高网页的性能和用户体验。
今年4.26号正式在chrome中接入WebGPU
(替代WebGL),浏览器游戏这将会是一个大的趋势。需要了解的小伙伴可以自行查阅资料。
网络进程(NewWork Process)
负责页面网络资源的加载,处理页面浏览器请求和响应
的进程。
插件进程(Plugin Process)
负责插件的运行,用来扩展浏览器的功能,因为插件任意出错,所以需要通过一个单独的进程来隔离,避免进程的崩溃而不会造成对页面的影响。
相信你看到这里,你就明白了为什么,浏览器仅仅打开了一个页面,为什么会有四个进程了,因为打开1个页面至少需要1个浏览器主进程、1个网络进程、一个GPU进程、1个渲染进程,如果此时打开的页面有插件的话,还有一个插件进程。
虽然多进程提升了浏览器的稳定性、流畅性、安全性,但是,同时也会带来一些问题:
更高的资源占用。因为每个进程都会包含公共基础结构的副本(如JavaScript运行环境),这就意味着浏览器会消耗更多的内存资源。
更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求。
为了解决上面所说的问题,Chrome团队现在在使用“面向服务的架构(SOA)”的思想来设计新的Chrome架构。将各个模块重构成独立的服务(Service)。每个服务都可以在独立的进程中运行、访问服务必须使用定义好的接口和协议,通过IPC进行通信,从而构建一个更内聚、低耦合、易于维护和扩展
的系统。
异步http请求线程与网络进程
看到这里不知道你是否有疑惑,为什么在渲染进程中存在一个异步http请求线程,这里的请求数据不应该是在网络进程中的吗?怎么出现在渲染进程里面了?我也有这种疑惑,相关的资料也没说明白具体的原因,按照我自己的想法写了一下,如果有大佬知道的话,可以在评论区说一下。
异步http请求线程
通常是在主线程中启动一个异步请求处理线程,该线程负责处理http请求。异步http请求线程通常不会阻塞主线程,而是通过事件或消息队列等方式来管理请求处理过程中的状态变化。可以避免线程池压力过大的问题。
网络进程
使用多线程或者多进程的方式来处理网络请求,网络进程通常使用套接字来接收和处理网络数据包,并将数据包转发到相应的进程进行处理。
1、如果存在网络进程中,在获取网络数据的时候需要将数据转发到渲染进程,这里就涉及到了进程之间的通信,开销大
。
2、异步http请求线程是请求处理完毕之后再异步通知主线程
,主线程可以继续执行其他任务。而网络进程是在请求接收到时就开始处理,直到处理完成或者出错时才通知主线程。
3、异步http请求线程与网络进程不同,它不是用于处理网络请求的主要进程,而是用于处理http请求的异步线程。在网络进程处理网络请求时,异步http请求线程可以处理其它请求,以提高并发处理能力。同时,异步http请求线程不会阻塞渲染进程,而是继续执行其它任务
,以便在响应用户请求时能够快速响应。
SOA 架构
SOA 是一种面向服务的架构,基于分布式架构,他将不同的业务功能按服务进行拆分,并通过这些服务之间定义好的接口和协议联系起来。 参照这里的图解,你可以将其替换成进程的情况,每一个进程都会有对应的Service,并且这些Service是互不影响的。
这就说明了为什么文章开头里面的图会有NetWork Service、Storage Service(缓存)等,也就说明现在Chrome已经向SOA进行靠拢了。下面是 Chrome 的 SOA 进程图。
最后
本文主要讲述浏览器的进化史,从早期的单进程浏览器演变到多进程浏览器,并分析了单进程浏览器与多进程浏览器的之间存在的优劣势,以及目前Chrome浏览器的架构模式。同时,文章中也有对进程和线程有部分的讲解。希望对你学习浏览器原理有所帮助!
基于上面说的异步http请求线程,为什么在渲染进程,而不在网络进程,希望有大佬能帮我解答一下!!