进程 & 线程
前言:之前是对于进程和线程的概念一点也不理解,公司内部分享的内容还介绍了这些东西所以我想先把进程和线程的概念弄清楚一下。
ps:如果感觉进程和线程的概念理解的不是很清楚也可以找后端给讲一下,重点还是放在理解
ps:本文针对基于webkit内核的Chrome浏览器进行分析
进程(process)和线程(thread)
进程是一个工厂,工厂有它的独立资源 | -> | 系统分配的内存(独立的一块内存) |
工厂之间相互独立 | -> | 进程之间相互独立 |
线程是工厂中的工人,多个工人协作完成任务 | -> | 多个线程在进程中协作完成任务 |
工厂内有一个或多个工人 | -> | 一个进程由一个或多个线程组成 |
工人之间共享空间 | -> | 同一个进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等) |
官方概念:
进程是CPU资源分配的最小单位(是能拥有资源和独立运行环境的最小单位),线程是CPU调度的最小单位(是进程的一个执行单位)(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
tips:
不同的进程之间也可以通信,不过代价很大。现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)
架构
Web浏览器的架构,可以实现为一个进程包含多个线程,也可以实现为很多进程包含少数线程通过IPC通信。如何实现,浏览器并没有统一的标准。(Chromium架构设计的灵活性,使用者可以通过简单的设置来随意改变它的进程模型方式。)Chrome最新的架构:最上层是浏览器进程,负责协调承担各项工作的其他进程,比如实用程序进程、渲染器进程、GPU进程、插件进程等。
多进程的浏览器
浏览器是多进程的,有一个主控进程,以及每一个tab页面都会新开一个进程(某些情况多个tab会合并进程),可以在Chrome的任务管理器中查看
主要包含的进程
1)Browser Process:浏览器的主进程(负责协调、主控),只有一个。作用有:
(1)负责浏览器界面显示(UI thread),与用户交互。如前进,后退等
(2)是所有其他类型进程的祖先,负责各个页面的管理,创建和销毁其他进程
(3)将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
(4)网络资源的管理,下载。不可见的一些操作,请求,文件访问等
2)Renderer Process:(内核,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为:
(1)负责一个网页tab的所有工作:页面渲染,脚本执行,事件处理等。
tips:注意:在这里浏览器应该也有自己的优化机制,有时候打开多个空白tab页后,可以在Chrome任务管理器中看到,有些进程被合并了
由于安全(Renderer进程是没有权限去获取资源的)和效率上的考虑,Renderer进程的资源获取实际上是通过进程间通信将任务交给Browser进程来完成,
Browser进程有权限从网络和本地获取资源。
3)Plugin Process:每种类型的插件对应一个进程,仅当使用该插件时才创建
4)GPU Process :最多一个,用于3D绘制等
5)Utility Process : 一些公用实用进程,如扩展程序
浏览器为什么是多进程?
最简单的情况下,可以想像一个标签页就是一个渲染器进程,比如3个标签页就是3个渲染器进程。这时候,如果有一个渲染器崩溃了,只要把它关掉即可, 不会影响 其他标签页。如果所有标签页都运行在一个进程中,那只要有一个标签页卡住,所有标签页都会卡住。
多进程优势:
- 避免单个page crash(崩溃) 影响整个浏览器
- 避免第三方插件 crash 影响整个浏览器
- 多进程充分利用多核优势
- 方便使用沙箱模式隔离插件等进程,提高浏览器稳定性
沙箱模型:将网页的运行限制在一个特定的环境中,也就是一个沙箱中,使它只能访问有限的功能。那么,即使网页工作的渲染引擎被攻击,它也不能够获取渲染引擎工作的主机系统中的任何权限,这一思想就是沙箱模型。
缺点:
- 更高的资源占用。因为每个进程都会包含公共基础结构的副本(如 Java 运行环境),这就意味着浏览器会消耗更多的内存资源。有点空间换时间的意思
- 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
多线程的浏览器
每个进程内部,都有很多的线程(可以并行处理任务),对于Browser进程,Chromium的官方说法告诉我们:多线程的主要目的就是为了保持用户界面的高响应度,保证UI线程(Browser进程中的主线程)不会被任何其他费时的(如本地文件读写、socket读写、数据库操作等)操作阻碍从而影响了对用户操作的响应。而在Renderer进程中,Chromium则不让其他操作阻止渲染线程的快速执行。每一个tab页面可以看做是浏览器内核进程,然后这个进程是多线程的,主要常驻线程:
1)GUI渲染线程
(1)负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
(2)当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
(3)注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2)JS引擎线程
(1)也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
(2)JS引擎线程负责解析Javascript脚本,运行代码。
(3)JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
(4)同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3)事件触发线程
(1)归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
(2)当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
(3)当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
(4)注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4)定时触发器线程
(1)setInterval与setTimeout所在线程
(2)浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
(3)因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
5)http网络请求线程
(1)将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行
(2)网络请求都是单独的线程
(3)每次网络请求时都需要开辟单独的线程进行,譬如如果URL解析到http协议,就会新建一个网络线程去处理资源下载。
(4)因此浏览器会根据解析出得协议,开辟一个网络线程,前往请求资源
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。总结来说,进程和线程之间的关系有以下 4 个特点。
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
- 线程之间共享进程中的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。
- 进程之间的内容相互隔离。
导航:从输入URL到获得HTML响应称为导航,导航涉及浏览器进程与线程间为显示网页而通信。一切从用户在浏览器中输入一个URL开始。输入URL之后,浏览器会通过互联网获取数据并显示网页。从请求网页到浏览器准备渲染网页的过程,叫做导航。标签页外面的一切都由浏览器进程处理。浏览器进程中有线程(UI线程)负责绘制浏览器的按钮和地址栏,有线程(网络线程)负责处理网络请求并从互联网接收数据,有线程(存储线程)负责访问文件和存储数据
具体分为几个步骤:
- 处理输入:UI线程会判断用户输入的是查询字符串还是URL。因为Chrome的地址栏同时也是搜索框。
- 开始导航:如果输入的是URL,UI线程会通知网络线程发起网络调用,获取网站内容。此时标签页左端显示旋转图标,网络线程进行DNS查询、建立TLS连接(对于HTTPS)。网络线程可能收到服务器的重定向头部,如HTTP 301。此时网络线程会跟UI线程沟通,告诉它服务器要求重定向。然后,再发起对另一个URL的请求
- 读取响应:服务器返回的响应体到来之后,网络线程会检查接收到的前几个字节。响应的Content-Type头部应该包含数据类型。如果响应是HTML文件,那下一步就是把数据交给渲染器进程。但如果是一个zip文件或其他文件,那就意味着是一个下载请求,需要把数据传给下载管理器。
- 联系渲染器进程:网络线程确认浏览器可以导航到用户请求的网站,于是会通知UI线程数据已经准备好了。UI线程会联系渲染器进程渲染网页由于网络请求可能要花几百毫秒才能拿到响应,这里还会应用一个优化策略。第二步UI线程要求网络线程发送请求后,已经知道可能要导航到哪个网站去了。因此在发送网络请求的同时,UI线程会提前联系或并行启动一个渲染器进程。这样在网络线程收到数据后,就已经有渲染器进程原地待命了。如果发生了重定向,这个待命进程可能用不上,而是换作其他进程去处理。
- 提交导航:数据和渲染器进程都有了,就可以通过IPC从浏览器进程向渲染器进程提交导航。渲染器进程也会同时接收到不间断的HTML数据流。当浏览器进程收到渲染器进程的确认消息后,导航完成,文档加载阶段开始。此时,地址栏会更新,安全指示图标和网站设置UI也会反映新页面的信息。当前标签页面的会话历史会更新,后退/前进按钮起作用。为便于标签页/会话在关闭标签页或窗口后恢复,会话历史会写入磁盘。
- 初始加载完成:提交导航之后,渲染器进程将负责加载资源和渲染页面(具体细节后面介绍)。而在“完成”渲染后(在所有iframe中的onload事件触发且执行完成后),渲染器进程会通过IPC给浏览器进程发送一个消息。此时,UI线程停止标签页上的旋转图标。初始加载完成后,客户端JavaScript仍然可能加载额外资源并重新渲染页面。如果此时用户在地址又输入了其他URL,浏览器进程还会重复上述步骤,导航到新站点。
渲染
渲染是渲染器进程内部的工作,标签页中的一切都由渲染器进程负责处理,渲染器进程的核心任务是把HTML、CSS和JavaScript转换成用户可以交互的网页。其中主线程负责运行大多数客户端JavaScript代码,少量代码可能会由工作线程处理(如果用到了Web Worker或Service Worker)。合成器(compositor)线程和栅格化(raster)线程负责高效、平滑地渲染页面。
简单梳理下浏览器渲染流程
浏览器输入url,浏览器主进程接管,开一个下载线程,然后进行http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,随后将内容通过RendererHost接口转交给Renderer进程。
浏览器渲染流程开始
- 主线程解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)网站都会用到图片、CSS和JavaScript等外部资源。浏览器需要从缓存或网络加载这些文件。主线程可以在解析并构建DOM的过程中发现一个加载一个,但这样效率太低。为此,Chrome会在解析同时并发运行“预加载扫描器”,当发现HTML文档中有 <img>或<link>时,预加载扫描器会将请求提交给浏览器进程中的网络线程。
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算主线程会遍历DOM元素及其计算样式,然后构造一棵布局树,这棵树的每个节点将带有坐标和大小信息。(与DOM树的结构类似,但只包含页面中可见元素的信息。)
- 绘制render树(paint),绘制页面像素信息有了DOM、样式和布局,仍然不足以渲染页面。还要解决先画什么后画什么,即绘制顺序的问题。比如,z-index影响元素叠放,如果有这个属性,直接进行绘制会出问题在这一步,主线程会遍历布局树并创建绘制记录。绘制记录是对绘制过程的注解,比如“先画背景,然后画文本,最后画矩形”。如果用过<canvas>,应该更容易理解这一点。渲染是一个流水线作业(pipeline):前一道工序的输出就是下一道工序的输入。这意味着如果布局树有变化,则相应的绘制记录也要重新生成。
- 合成:浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。在知道了文档结构、每个元素的样式、页面的几何关系,以及绘制顺序的前提下。把上述信息转换为屏幕上的像素叫做栅格化。合成(composite)是将页面不同部分先分层并分别栅格化,然后再通过独立的合成器线程合成页面分层?为了确定哪个元素应该在哪一层,主线程会遍历布局树并创建分层树(这一部分在开发工具的“性能”面板中叫“Update Layer Tree”)。合成线程会收集这些块的信息(称为绘制四边形),创建合成帧渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了由此,顺便可以得出DOMContentLoaded事件与load事件的先后
tips:在总结浏览器缓存的时候,提到的MSG总问地址栏输入URL浏览器都做了什么?到现在为止,是不是能回答很多点了
DNS解析、CND服务、浏览器的线程/进程、tcp/ip连接、请求资源时的浏览器缓存、以及上面提到的渲染的步骤。
以自己现在的努力程度,还没有资格和别人拼天赋