Insidelookatmodernwebbrowser是介绍浏览器实现原理的系列文章。共有4篇文章。本次精读介绍第一篇文章。这篇文章虽然是2018年写的,但现在仍然值得学习,因为浏览器实现很复杂,从细节上学习容易迷路,缺乏整体感。不过本文是从宏观层面出发,几乎不涉及代码实现。是一个深思熟虑的描述,非常适合培养浏览器的整体框架思维。原文有大量的图解和动画,便于加深对知识的理解,所以也推荐直接阅读原文。概述文章从CPU、GPU和操作系统开始,因为这些是浏览器运行的基础。CPU、GPU、操作系统、应用关系CPU是中央处理器,几乎可以处理所有的计算。以前的CPU是单核的,现在笔记本电脑大多是多核的,专业服务器甚至有100核之多。CPU的计算能力很强,但它只能处理一件事。GPU本来就是为图像处理设计的,也就是主要处理像素点,所以对简单的东西有大量的并行处理能力,很适合做矩阵运算。矩阵运算是计算机图形学的基础,因此在可视化领域应用广泛。CPU和GPU都是计算机硬件,都提供了一些汇编语言调用的接口;而操作系统使用C语言(如linux)来管理基于它们的硬件,包括进程调度、内存分配、用户Kernel模式切换等;应用程序运行在操作系统之上,因此应用程序不直接与硬件打交道,而是通过操作系统间接地对硬件进行操作。为什么应用程序不能直接操作硬件?这有很大的安全隐患,因为硬件没有任何抽象和安全措施,也就是说理论上一个网页可以直接访问你的任意内存地址,在你打开网页的时候通过js程序读取你的聊天记录,甚至读取历史输入的银行卡密码进行转账操作。显然,浏览器作为应用程序运行在操作系统之上。进程和线程为了让程序运行的更安全,操作系统创建了进程和线程的概念(linux实现了同一套进程和线程),进程可以分配独立的内存空间,同时可以创建多个线程进程工作时,线程共享内存空间。由于线程之间共享内存空间,它们可以不通信也可以通信,但是内存地址相互隔离的进程之间也需要通信,这就需要通过IPC(InterProcessCommunication)进行通信。进程之间是相互独立的,即一个进程挂了,不会影响其他进程,但可以在一个进程中创建一个新的进程并与之通信,所以浏览器采用这种策略来整合UI、网络、渲染、插件、存储等模块与进程无关,任意挂机后均可重新唤醒。浏览器架构浏览器可以拆分成很多独立的模块,例如:浏览器模块(Browser):负责协调整个浏览器的行为,调用各个模块。网络模块(Network):负责网络I/O。存储模块(Storage):负责本地I/O。用户界面模块(UI):负责浏览器向用户提供的界面模块。GPU模块:负责绘图。渲染模块(Renderer):负责渲染网页。设备模块(Device):负责与本地各种设备进行交互。插件模块(Plugin):负责处理各种浏览器插件。基于这些模块,浏览器有两种可用的架构设计,一种是少进程,一种是多进程。更少的进程是指将这些模块放在一个或有限数量的进程中,即每个模块有一个线程。这样做的好处是最大程度共享内存空间,对设备的要求较低,但问题是只需要一个线程。挂了会导致整个浏览器挂掉,所以稳定性很差。多进程是指为每个模块创建一个进程(尽可能多),模块之间通过IPC通信,所以任何一个模块挂掉都不会影响其他模块,但缺点是占用内存大,比如浏览器的js解析和执行引擎V8在这种架构下需要复制多个实例运行在每个进程中。Chrome多进程架构的优势Chrome试图为每个标签创建一个单独的进程,这样我们就可以在某个标签没有响应的时候从容地关闭它,而其他标签不会受到影响。不仅选项卡之间,选项卡内的iframe之间也会创建单独的进程,这样做是为了保护网站的安全。Servicing-单/多进程弹性架构Chrome并不满足于采用一种架构,而是在不同的环境中切换不同的架构。Chrome将各个功能模块化后,可以自由决定当前哪些模块放在一个进程中,哪些模块作为独立进程启动,即可以决定在运行时使用哪种进程架构。这样做的好处是可以在资源有限的机器上开启单进程模式,尽可能节省内存开销。事实上,这就是移动应用程序所做的;而在资源丰富、核数充足的机器上使用独立进程,在消耗更多资源的同时,获得了更好的稳定性。iframe独占进程站点隔离,将不同iframe包裹在不同进程的同一个tab中,保证iframe之间的资源独占性和安全性。这个功能是2018.7才更新的,因为后台有很多复杂的工作要处理,比如开发者工具的调试,网页的全局搜索功能,不能受进程隔离的影响。Chrome必须让每个进程独立地响应这些。操作,最后聚合在一起,让用户感觉不到流程之间的隔阂。精读本文从浏览器如何基于操作系统提供的进程和线程的概念构建自己的应用程序出发,从硬件、操作系统和软件的分层出发,介绍浏览器如何划分模块和分配进程或线程给他们。这些模块如何工作背后的思考非常有价值。从宏观上看,要设计出安全、稳定、高性能、可扩展的浏览器,首先需要明确划分各个功能模块,定义各个模块的通信关系,制定一套每个业务场景中用于协作的模块。过程。浏览器的主从架构类似于应用程序的主从模式。浏览器的Browser模块可以看作是主模块,它本身就是用来协调其他模块的运行,维护其他模块的正常工作的。当其他模块失去响应时等待或重新唤醒,或者在模块销毁时进行内存回收。每个从模块也有明确的分工。例如,当浏览器打到URL地址时,首先会通过UI模块响应用户的输入,判断输入的是否是URL地址,因为输入的可能是其他非法参数,也可能是一些查询或设置命令。.如果输入的确实是URL地址,验证通过后,会通知Network网络模块发送请求,UI模块就不再关心请求是如何处理的。Network模块也相对独立,只处理请求的发送和接收。如果接收到接收到的HTML网页,就会交给Renderer模块进行渲染。有了这些分工明确的相对独立的模块,将这些模块作为线程或进程来管理,就不会影响它们的业务逻辑。唯一影响的是内存是否共享,一个模块crash会不会影响其他模块,所以基于这个架构,判断设备类型,采用单进程或多进程模式变得容易很多,并且这种流程弹性架构本身不需要侵入各个模块的业务逻辑,是一个独立的机制。作为一个非常复杂的应用程序,浏览器要想对其进行持续维护,就必须对每个功能点进行适当的设计,使模块之间具有高内聚和低耦合,这样任何修改都不会牵一发而动全身。tab和iframe进程隔离微前端的沙箱隔离方案也很流行。在这里您可以将其与浏览器选项卡/iframe隔离进行比较。大部分基于js运行时的沙箱解决方案都是因为iframe的慢而诞生的。一般是根据with改变沙箱代码的上下文,修改访问的全局对象引用。但是基于js原型链的特点,为了阻止原型链溯源到主应用代码,一般会使用代理来阻止对mock变量的访问。还有一些解决方案是通过创建一个空的iframe来获取document变量传递给沙箱,在一定程度上实现了访问隔离,在iframe销毁时添加到document中的监控也会被销毁,这很容易控制。还有一些更彻底的尝试,把js代码丢给webworker运行,通过mocks模拟worker运行时缺失的domAPI。对比这些方案,可以发现只有最后一种worker方案是最彻底的,因为浏览器创建的worker进程是完全资源隔离的。如果要和浏览器主线程通信,只能使用postMessage,虽然有一些基于ArrayBuffer的内存共享方案,但是因为支持的数据类型是针对性的,不会有安全问题。回到浏览器开发者的角度,为什么iframe隔离费了好大的功夫拆分了多个进程,最后费了好大的劲才把它们重新拼接起来,还原出相对无缝的体验?事实上,浏览器制造商可以利用上面提到的js运行时能力来修改API语法并创建一个逻辑沙箱环境。我认为本质原因是浏览器要实现的沙箱必须是进程级别的,也就是内存访问权限的绝对隔离,因为逻辑级别的隔离可能会随着各个浏览器厂商的实现而有所不同,或者是API本身的逻辑漏洞,导致了未授权情况的出现,所以如果需要构建一个完全安全的沙箱,最好使用浏览器提供的API,新建一个进程来处理沙箱代码。总结本文介绍了浏览器是如何基于操作系统进行宏架构设计的。主要讲一件事,就是进程和线程模型的灵活运用。同时,在tabs和iframe的设计中也要考虑安全需求,必要时使用流程。由于浏览器自身模块之间不存在安全问题,进程模型可以灵活切换。讨论地址为:Jingdu《深入了解现代浏览器一》·Issue#374·dt-fe/weekly想参与讨论的请戳这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号
