进程和线程
# 进程和线程
# 介绍一下进程和线程特别重要
概念:从本质来说,进程和线程都是cpu
工作时间片的描述
- 进程描述了
cpu
在运行指令及加载和保存上下文所需的时间,放在应用上来说就是一个程序 - 线程是进程更小的单位,描述了执行一段指令所需的时间
进程是资源分配的最小单位,而线程是cpu
调度的最小单位
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间
一个标准的线程由线程ID
、当前指令指针(PC
)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成
虚拟地址空间
如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。
进程和线程之间的关系有以下四个特点:
(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。
(2)线程之间共享进程中的数据。
(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A
写入数据到进程 B
的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
Chrome浏览器的架构图: 从图中可以看出,最新的
Chrome
浏览器包括:
1 个浏览器主进程
1 个
GPU
进程1 个网络进程
多个渲染进程
多个插件进程
这些进程的功能:
浏览器进程:主要负责界面显示、用户交互、子进程管理,还包括地址栏、书签、后退和前进按钮,同时提供存储,网络请求和文件访问等功能。
渲染进程:核心任务是将
HTML
、CSS
和JavaScript
转换为用户可以与之交互的网页,排版引擎Blink
和JavaScript
引擎V8
都是运行在该进程中,默认情况下,Chrome
会为每个Tab
标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。GPU
进程:其实,GPU
的使用初衷是为了实现3D CSS
的效果,只是随后网页、Chrome
的UI
界面都选择采用GPU
来绘制,这使得GPU
成为浏览器普遍的需求。最后,Chrome
在其多进程架构上也引入了GPU
进程。网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
- 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如
JavaScript
运行环境),这就意味着浏览器会消耗更多的内存资源。 - 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
# 进程和线程的区别特别重要
进程可以看做独立应用,线程不能
资源:进程是
cpu
资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是cpu
调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。进程切换比线程切换的开销要大。线程是
CPU
调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、
I/O
等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程CPU
环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小。并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
# 浏览器的渲染进程的线程有哪些重要
GUI
渲染线程 负责渲染浏览器页面,解析html
、css
,构建dom
树、cssom
树、render
树和绘制页面,当界面需要重绘或者某些操作引发的回流,该线程就会执行GUI
渲染线程和JS
引擎线程是互斥的,当JS
引擎执行时GUI
线程会被挂起,GUI
更新会被保存在一个队列等到JS
引擎空闲时候立即被执行
JS
引擎线程 也称为JS
内核,负责处理js
,解析js
并运行,JS
引擎线程一直等待任务队列中任务的到来,然后处理,一个Tab
页中无论什么时候都只有一个JS
引擎线程在运行JS
程序- 由于
GUI
和js
引擎的互斥,如果JS
执行时间过长,会导致页面渲染不连贯,导致页面渲染加载阻塞
- 由于
事件触发线程 用来控制事件循环,当
js
引擎执行到setTimeout
(或者鼠标点击,ajax
请求),会将对应任务加入到事件触发线程,当对应的事件符合触发条件的时候,该线程会把事件添加到待处理队尾,等待JS
引擎线程处理- 由于
JS
是单线程,所以待处理队列的事件都得排队等待
- 由于
定时器触发线程 即
setInterval
与setTimeout
所在线程,浏览器定时计数器并不是由js
引擎计数的,因为JS
引擎是单线程,如果处于阻塞就会影响计时的准确性,因此使用单独的线程来计时并且触发定时器,到时就添加进事件队列里面,等待JS
引擎空闲的时候执行,所以不一定准确,定时器只是将指定时间点的任务添加进事件队列W3C
规定 定时器定时不能小于4ms
如果小于4ms
默认为4ms
异步
HTTP
请求线程XMLHttpRequest
连接后通过浏览器新开一个线程请求,当他检测到状态变更的时候如果设有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS
引擎空闲后执行
# 进程之前的通信方法重要
管道通信
管道是最基本的进程间通信机制,管道是操作系统在内核中开辟的一段缓冲区,进程1
可以将需要交互的数据拷贝到缓存区中供进程2
读取
特点
- 单向通信
- 只能血缘关系的进程进行通信
- 依赖于文件系统
- 生命周期跟进程一样
- 面向字节流的服务
- 管道内部提供同步机制
匿名管道和命名管道的区别
匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
命名管道:可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。
消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
- 使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。
管道通信和消息队列的区别在于:后者少了打开和关闭管道方面的复杂性。但他有和命名管道相同处是每个数据块有一个最大长度的限制,而管道满时的阻塞问题。消息队列的优势在于,它独立于发送和接收进程而存在
共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC
方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1
,然后 a
进程来访问内存1
的时候,我们就把信号量的值设为 0
,然后进程b
也要来访问内存1
的时候,看到信号量的值为 0
就知道已经有进程在访问内存1
了,这个时候进程 b
就会访问不了内存1
。所以说,信号量也是进程之间的一种通信方式。
套接字通信
上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket
这家伙就派上用场了,例如我们平时通过浏览器发起一个 http
请求,然后服务器给你返回对应的数据,这种就是采用 Socket
的通信方式了。
信号通信
信号(Signals
)是Unix
系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
# 共享内存比管道和消息队列效率高的原因
共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc()
函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。 因为系统内核没有对访问共享内存进行同步,您必须提供自己的同步措施。例如,在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等。解决这些问题的常用方法是通过使用信号量进行同步。
共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。
# 僵尸进程和孤儿进程是什么
- 孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被
init
进程(进程号为1
)所收养,并由init
进程对它们完成状态收集工作。 - 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
# 死锁产生的原因和解决办法重要
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
系统中的资源可以分为两类:
- 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,
CPU
和主存均属于可剥夺性资源; - 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁的原因:
- 竞争资源
- 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程
P1
使用,假定P1
已占用了打印机,若P2
继续要求打印机打印将阻塞) - 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
- 进程间推进顺序非法
若P1
保持了资源R1
,P2
保持了资源R2
,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1
运行到P1:Request(R2)
时,将因R2
已被P2
占用而阻塞;当P2
运行到P2:Request(R1)
时,也将因R1
已被P1
占用而阻塞,于是发生进程死锁
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
占有且等待条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
非抢占条件:不能强行抢占进程中已占有的资源,只能在使用完时由自己释放。
循环等待条件:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。
预防死锁的方法:
资源一次性分配 :一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏占有且等待条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏非抢占条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待条件)
# 如何实现浏览器多个标签页之间的通信
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:
使用
websocket
协议,因为websocket
协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。使用
ShareWorker
的方式,shareWorker
会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。使用
localStorage
的方式,我们可以在一个标签页对localStorage
的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候localStorage
对象就是充当的中介者的角色。使用
postMessage
方法,如果我们能够获得对应标签页的引用,就可以使用postMessage
方法,进行通信。
# 对service Worker
的理解
Service Worker
是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker
的话,传输协议必须为 HTTPS
。因为 Service Worker
中涉及到请求拦截,所以必须使用HTTPS
协议来保障安全。
Service Worker
实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker
,然后监听到 install
事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
打开页面,可以在开发者工具中的 Application
看到 Service Worker
已经启动了: 在
Cache
中也可以发现所需的文件已被缓存: