前言

作为一个平台,现代浏览器是专门设计用来快速、高效、安全地交付Web应用的。事实上,在其表面之下,现代浏览器完全是一个囊括数百个组件的操作系统,包括进程管理、安全沙箱、分层的优化缓存、JavaScript 虚拟机、图形渲染和GPU 管道、存储系统、传感器、音频与视频、网络机制等等
显然,浏览器乃至运行在其中的应用的性能,取决于若干组件:解析、布局、HTMl与CSS的样式计算、JavaScript 执行速度、渲染管道,当然还有网络相关各层协议的配合。其中每个组件的角色都很重要,所以保证各个组件之间合理高效地互相配合工作也是浏览器至关重要的工作。

进程和线程

进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握,可以看下阮一峰老师的文章
进程:当我们启动某个程序时 操作系统会给该程序创建一块内存(当程序关闭时,该内存空间就会被回收), 用来存放代码、运行中的数据和一个执行任务的主线程,这样的一个运行环境就叫进程
线程:线程是处理具体任务的,多线程可以并行处理任务。但是线程是不能单独存在的,它是有进程来启动和管理的

进程和线程的关系特点:

  • 进程与进程之间完全隔离,互不干扰,一个进程崩溃不会影响其他进程,避免一个进程出错影响整个程序
  • 进程与进程之间需要传递某些数据的话,就需要通过进程通信管道IPC 来传递
  • 一个进程中可以并发多个线程,每个线程并行执行不同的任务
  • 一个进程中的任意一个线程执行出错,会导致这个进程崩溃
  • 同一进程下的线程之间可以直接通信和共享数据
  • 当一个进程关闭之后,操作系统会回收该进程的内存空间

image.png
从图中可以看到,线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率
早期浏览器(单进程浏览器)
image.png
2007 年以前浏览器并不是多进程的结构,而是单进程的结构,一个进程中包含了网络、JS运行环境、渲染引擎、页面、插件等,这也导致单进程的结构引发了很多问题

  • 一是不稳定, 其中一个线程卡死,可能会导致整个程序出问题,比如打开多个标签页,其中一个标签页卡死可能会导致整个浏览器无法正常运行
  • 二是不安全,浏览器一个进程里是可以共享数据的,那JS线程岂不是可以随意访问浏览器进程内的所有数据,这显然不合理
  • 三是不流畅, 一个进程需要负责太多事情,会导致运行效率问题

所以为了解决这些问题,才发展出了多进程结构

Chrome 打开一个页面有多少进程?分别是哪些

image.png
浏览器从关闭到启动,然后打开一个页面至少需要: 1个浏览器进程,1个GPU 进程,1个网络进程,1个渲染进程,一共4个进程;
后续如果再打开新的标签页: 浏览器进程、GPU 进程、网络进程是共享的,不会重新启动, 然后默认情况下会为每一个标签页配置一个渲染进程,但是也有例外,比如从A 页面里面打开一个新的页面B,而A页面和 B页面又属于同一个站点的话,A和B就共用一个渲染进程,其他情况就为B创建一个新的渲染进程
所以,最新的Chrome浏览器包括:1个浏览器主进程、1个GPU进程、1个网络进程、多个渲染进程和多个插件进程

  • 浏览器进程:负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能
  • GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而是用GPU的初衷是为了实现3D CSS 的效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU 成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程
  • 网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面,后面才独立出来,成为一个单独的进程
  • 插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响
  • 渲染进程:负责控制现实tab标签页内的所有内容,核心任务是将HTML、CSS、JS 转为用户可以与之交互的网页,排版引擎Blink和JS引擎V8都是运行在该进程中,默认情况下Chrome 会为每个Tab标签页创建一个渲染进程,出于安全考虑,渲染进程都是运行在沙箱模式下

我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成所以接下来看下渲染进程中的线程

渲染进程中的线程

  • GUI 渲染线程:负责渲染页面,解析HTML和CSS、构建DOM树、CSSOM树、渲染树和绘制页面
  • JS 引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。它和GUI渲染进程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧
  • 计时器线程:指setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
  • 异步http请求线程:XMLHttpRequest 连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行
  • 事件触发线程:主要用来控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理

    浏览器的进程模型

    Chrome为例,有四种 进程模型,分别是

  • Process-per-site-instance:默认模式。 访问不同站点创建新的进程,在旧页面中打开的新页面,且新页面与旧页面属于同一站点的话会共用一个进程

  • Process-per-site:同一站点使用同一进程
  • Process-per-tab:每一个标签页都创建新的进程
  • Single Process:单进程模式

    线程模型中的线程都是干嘛的?

  • MessagePumpForIO:处理进程间通信的线程,在Chrome中,这类线程都叫做IO线程

  • MessagePumpForUI:处理UI的线程用的
  • MessagePumpDefault:一般的线程用到的

每一个Chrome的线程,入口函数都差不多,都是启动一个消息循环,等待并执行任务

进程间通信的方式

  • 管道通信: 就是操作系统在内核中开辟一段缓冲区,进程1可以将需要交互的数据拷贝到这个缓冲区里,进程2就可以读取了
  • 消息队列通信:消息队列就是用户可以添加和读取消息的列表,消息队列里提供了一种从一个进程向另一个进程发送数据块的方法,不过和管道通信一样每个数据块有最大长度限制
  • 共享内存通信:就是映射一段能被其他进程访问的内存,又一个进程创建,但多个进程都可以访问,共享进程最快的是 IPC 方式
  • 信号量通信:比如信号量初始值是1,进程1来访问一块内存的时候,就把信号量设为0,然后进程2也来访问的时候看到信号量为0,就知道有其他进程在访问,就不访问了
  • socket:其他的都是同一台主机之间的进程通信,而在不同主机的进程通信就要用到socket的通信方式了,比如发起http请求,服务器返回数据

多标签之间怎么通信

  • localStorage:在一个标签页监听localStorage的变化,然后当另一个标签页修改的时候,可以通过监听获取新数据
  • WebSocket:因为webSocket可以实现实时服务器推送,所以服务器就可以来当这个中介者。标签页通过向服务器发送数据,然后服务器再向其他标签推送转发
  • ShareWorker:会在页面的生命周期内创建一个唯一的线程,并开启多个页面也只会使用同一个线程,标签页共享一个线程
  • postMessage:
    1. // 发送方
    2. window.parent().pastMessage('发送的数据','http://接收的址')
    3. // 接收方
    4. window.addEventListener('message',(e)=>{ let data = e.data })

    僵尸进程和孤儿进程

    孤儿进程: 顾名思义,就是没爹的孩子。 父进程退出了,而它的一个或多个进程还在运行,那么这些子进程都会成为孤儿进程。这些孤儿都将被 init 进程收养 ,并负责这些孤儿的以后
    僵尸进程:就是子进程比父进程先结束,而父进程有没有释放子进程占用的资源,那么子进程的描述还留在系统中,这种进程就是僵尸进程。