文章目录:
一、进程、线程、协程、管程
(一)进程
【1.引入进程:】
传统的程序无法并发,为了实现多个程序并发运行,引入了进程的概念。
【2.PCB:】
因为并发程序运行时需要时CPU时间片轮转,需要保存一些现场信息,以便下次恢复CPU现场继续执行,因此我们需要一种数据结构标识一个运行中的程序,称为PCB进程控制块,保存了进程的各类信息。
【3.进程间切换:】
- 虚拟地址空间的切换,最耗时,从task_struct中获取到页表的物理地址,建立映射。
- 进程内核栈的切换。
- 进程用户堆栈的切换和寄存器的保存。
【4.进程基本概念:】
- 进程是操作系统对一个正在运行的程序的一种抽象结构。
- 进程是指在操作系统中能独立运行并作为资源分配的基本单位。
- 进程是由一组机器指令,数据和堆栈等组成的能独立运行的活动实体。
- 操作系统可以同时运行多个进程,多个进程直接可以并发执行和交换信息。
- 进程的运行和管理需要一定的资源,如PCB,CPU等。
- 进程极大的提高了资源利用率和系统吞吐量
(二)线程
【1.引入线程:】
建立进程、管理进程和进程并发之间的切换都会带来时间效率上的开销,开销较大,但是切换过程效率较低。
为了提高效率,操作系统引入了线程的概念,进程是操作系统进程资源和调用的基本单位,线程的引入分离了这种职责,线程是进程的实际运作单位,是执行调度的基本单位,同时拥有部分资源,这样,我们一个进程里面可以创建多个线程实现并发,节省了进程切换的开销。
【2.线程间切换:】
线程共享进程的地址空间和内核栈,所以线程切换时,只会保存线程的栈信息和少量寄存器即可,因此线程之间切换的开销远小于进程。
【3.线程基本概念:】
- 线程实际上是进程内部的一条执行序列,即执行流。执行序列是指一组有序指令假数据的集合,以函数单位。
- 在Linux系统中,没有线程的概念,内核看到了比传统进程更加轻量化地PCB,故线程是一种轻量级的进程。
- Linux 内核的实现中,并没有单独的线程概念,线程仅仅被视为与进程共享资源的特殊进程,clone 系统调用中传入 CLONE_VM,即共享父进程地址空间。
- 线程在进程内部运行,共享进程的地址空间,每一个线程都拥有一个独立的计数器,栈,寄存器。
- 进程调度的对象是线程,而不是进程。
- 一个进程至少有一个执行线程。
- PCB管理进程,Linux上没有真正意义上地线程控制块TCB,而是用进程控制块来模拟线程TCB。
【4.线程的优点:】
- 创建线程需要的资源比进程小得多。
- 线程切换的开销小,不必进行虚地址空间切等切换,只需要进行栈,寄存器保存即可。
- 线程占用资源少,并发高。
- 可以实现计算密集型应用,可以将计算分解到多个线程中实现。
- 可以实现I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。
【5.线程的缺点:】
- 性能方面:如果有大量的线程,操作系统需要在它们之间来回切换,会影响性能。
- 安全方面:线程的共享性大,很容易引发线程安全问题,对共享资源的控制如果不当,那么就会出现死锁等安全问题。
- 控制方面:线程是不可控的,所以必须对线程进行同步控制,实现线程同步。
- 编程方面:实现多个线程比单线程困难,编程难度大。
(三)进程&&线程
线程和进程的区别,总结如下:
- 根本区别:进程是系统进行资源分配的基本单位; 线程是任务调度和执行的基本单位。
- 内存层面:操作系统每创建一个进程,都会给这个进程分配不同的地址空间,来存储程序所占用的资源,进程相互独立存在; 线程共享进程的地址空间,操作系统制位它分配很小一部分内存,TLS(在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为线程局部静态变量),线程局部存储,用来存储线程独有的资源,整体上它是共享进程的资源,所以栈独立,其他空间共享。
- 开销方面:进程切换的开销很大,需要地址空间的切换,进程内核栈的切换,进程用户堆栈以及寄存器的切换; 线程可以看做是轻量级进程,共享进程地址空间,它的切换仅仅是线程栈和PC寄存器的保存切换,因此线程切换的开销小。
- 包含关系:一个进程至少有一个线程,用于执行程序,称为主线程,或者单线程程序,因此,线程是包含在进程中的。
- 通信方面:进程之间的通信需要通过进程间通信(IPC),而同一个进程的个线程之间可以直接通过传递地址或者全局变量的方式传递变量。
- 安全方面:进程之间不存在安全问题;而同一个进程的线程因为共享性大,所以存在安全问题。
我们列表总结:
进程 | 线程 | |
---|---|---|
本质 | 系统进行分配资源的最小单位 | CPU调度的最小单位 |
包含关系 | 一个进程包含多个线程 | 一个线程只属于一个进程 |
开销 | 大 | 小 |
切换效率 | 低 | 高 |
存在形式 | 相互独立 | 栈独立,其他空间共享 |
通信 | 使用IPC,借助外部手段 | 内部直接通过共享内存通信 |
安全问题 | 不存在 | 存在 |
(四)多进程
我们可以通过fork创建子进程,exec替换进程这种方式构建多个线程,即多个程序在运行,我们称为多线程。
【1.多进程的优点:】
- 每个进程相互独立,不影响主程序的稳定性,子进程奔溃也没关系。
- 通过增加CPU,可以实现并行,扩充性能。
- 多进程间使用同步机制少,所以性能较高。
- 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。
【2.多进程的缺点:】
- 逻辑控制复杂,需要创建,替换进程两个步骤,和主程序交互。
- 进程间通信不方便,需要跨进程边界,可以通信小数据,一旦密集运算,大数据,那么开销很大。
(五)多线程
多线程就是指一个进程中同时有多个线程正在执行。
多线程是异步的,多个线程同时执行不代表真正的同时运行,而是一种并发形式,系统不断在各个线程之间来回切换,切换速度快,所以给我们一种多线程一起运行的错觉
【1.多线程的优点:】
- 多线程可以同时执行多个操作,并发性高。
- 无需跨进程边界,可以提高程序的效率。
- 所有线程可以直接共享内存和变量进行通信。
- 线程消耗的资源比进程少。
【2.多线程的缺点:】
- 线程之间的同步和加锁控制麻烦。
- 一个线程的崩溃,如挂起,终止等操作可能影响整个程序的稳定性。
- 线程能够提高的性能有限,随着线程的增多,会耗费大量的系统资源,内存。
- 大量线程之间的切换影响性能。
- 对线程控制不当会引发线程安全问题。
(六)多进程&&多线程
下面的例子从知乎-pansz上转载的,非常通俗地解释了这几个概念:桌子就是进程,菜就是临界资源。
- 单进程单线程::一个人在一个桌子上吃菜
- 单进程多线程:多个人在同一个桌子上吃菜
- 多进程单线程:多个人每个人在自己的桌子上吃菜
多线程的问题是:多个人在一个桌子上同时吃一道菜的时候容易发生争抢,如两个人同时夹着一道菜,这就是临界资源会发生冲突争夺。
多进程的问题是:坐在不同的桌子上,说话不方面,即通信不方便,需要借助外力去说话,如声音要放大,移动板凳,找个人传话等。
【对于windows系统来说】,【开桌子】的开销很大,所以windows鼓励大家在一个桌子上吃菜,因此windows多线程重点是学习多线程同步控制和安全控制。
【对于Linux系统来说】,【开桌子】的开销很小,所以Linux鼓励大家尽量每个人都开自己的桌子吃菜,因此Linux多线程的学习重点是进程间通讯的方式。
【多线程和多线程的区别:】
- 多进程数据是分开的,共享复杂,需要用 IPC,同步简单;多线程共享进程数据:共享简单,同步复杂。
- 多进程创建销毁、切换复杂,速度慢 ;多线程创建销毁、切换简单,速度快。
- 多进程占用内存多, CPU 利用率低;多线程占用内存少, CPU 利用率高。
- 多进程编程简单,调试简单;多线程编程复杂,调试复杂。
- 多进程间不会相互影响 ;多线程一个线程挂掉将导致整个进程挂掉。
- 多进程适应于多核、多机分布;多线程适用于多核
我们总结为一张表:
多进程 | 多线程 | 总结 | |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 数据共享简单,用全局变量即可,同步复杂 | 根据情况选择 |
内存、CPU | 占用内存多,切换复杂,CPC利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程较好 |
创建销毁,切换 | 创建销毁,切换复杂,速度慢 | 创建销毁 | 切换简单 |
编程 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程较好 |
可靠性 | 进程间不会相互影响 | 一个线程出现问题对整个进程有影响 | 进程较好 |
分布式 | 适用于多核,多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程较好 |
(七)协程
- 协程是用户模式下的轻量级线程,操作系统内核对协程一无所知。
- 协程的调度完全由应用程序来控制,操作系统不管这部分的调度。
- 一个线程可以包含一个或多个协程。
- 协程拥有自己的寄存器上下问和栈,协程调度切换时,将寄存器上下文和栈保存起来,在切换回来时先恢复保存的上下文和栈。
- 协程能保留上一次调用时的状态。
- Windows下协程又称为纤程。
- 应用:python,Java,Go等语言。
(八)线程&&协程
区别:
- 线程是由CPU调度,而协程是由用户调度。
- 线程存在安全问题,协程比线程较安全。
- 线程使用同步机制,协程使用异步机制。
- 协程完全由程序控制,而线程的阻塞状态由操作系统内核控制切换,所以协程性能提升。
- 协程的开销远远小于线程。
用一张图来标识进程,线程,协程之间的关系:
(九)管程
管程:由存储共享资源的数据结构,对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成的一个操作系统的资源管理模块。
简单来说管程就是用来管理进程的。所谓的管程实际上是定义的一种数据结构和控制进程的一些操作的集合。
二、锁的实现
为了保证临界区代码的安全,操作系统引入了锁机制。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。
因为锁机制的一个特点是他的同步原语都是原子操作硬件已经为我们提供了一些原子操作,如下:
- 忙等待:中断禁止和启用
- 内存加载和存入
- 非忙等待:测试与设置指令
他们都是硬件步骤,中间没有办法插入别的操作。在这些硬件原子操作之上,我们便可以构建软件原子操作——锁、睡眠与叫醒、信号量等。
三、锁的类型
在使用锁时需要明确几个问题:
- 锁的所有权问题,一般谁加锁谁就负责解锁。
- 锁的作用就是对临界区资源的读写操作的安全限制
- 锁是否可以被多个使用者使用(互不影响的使用者对资源的占用)
- 多个临界区资源锁的循环问题(死锁场景)
(一)互斥锁mutex
互斥锁是线程同步中常用的一种锁,一般操作步骤为:
- 使用互斥锁时在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作,谁加锁谁释放;
- 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
互斥锁无法获取锁时将阻塞睡眠,需要系统来唤醒,解锁时有多个线程阻塞,那么所有该锁上的线程都变成就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待,对其他线程而言就是虚假唤醒。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
(二)读写锁rwlock
读写锁也叫共享-互斥锁:读模式共享和写模式互斥。
本质上这种非常合理,因为在数据没有被写的前提下,多个使用者读取时完全不需要加锁的。读写锁有读加锁状态、写加锁状态和不加锁状态三种状态,当读写锁在写加锁模式下,任何试图对这个锁进行加锁的线程都会被阻塞,直到写进程对其解锁。
读写锁写优先。读写锁适合对数据结构读的次数远大于写的情况下使用
(三)自旋锁spinlock
自旋锁的主要特征是线程在想要获得临界区执行权限时,如果临界区已经被加锁,那么自旋锁并不会阻塞睡眠,等待系统来主动唤醒,而是原地忙轮询资源是否被释放加锁,自旋就是自我旋转。
自旋锁优点:
- 避免了系统的唤醒,自己来执行轮询,即轮询忙等待。
- 如果在临界区的资源代码非常短且是原子的,那么使用起来是非常方便的,避免了各种上下文切换,开销非常小,因此在内核的一些数据结构中自旋锁被广泛的使用。
自旋锁缺点:
- 在单核cpu下不起作用:被自旋锁保护的临界区代码执行时不能进行挂起状态。会造成死锁。
- 自旋锁的初衷是在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个线程获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
四、死锁
多个进程争夺资源过程中形成的一种僵局状态。当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。两个及以上个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁,这些线程都陷入了无限的等待中。
(一)死锁产生的原因
产生死锁的原因可归结为:竞争资源;进程间推进顺序非法
- 竞争资源
- 竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
- 竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁。
- 进程间推进顺序非法
两个线程都各自保持了资源,系统处在了不安全的状态,因为两个线程向前推进,便可能发生死锁。
(二)死锁产生的必要条件
- 互斥条件: 同一时间只能一个进程使用这个资源。
- 请求与保持请求: 一个进程因请求资源而阻塞时,对已获资源保持不变。
- 不剥夺条件: 不能强制剥夺进程的资源。
- 循环等待: 若干进程形成头尾相连的循环等待资源关系。
(三)处理死锁的方法
1. 预防:
- 破坏请求条件: 资源一次性分配,一次性分配所有资源,这样就不会再有请求了。
- 破坏保持条件: 只要有一个资源得不到分配,也不给这个进程分配其他的资源。
- 破坏不可剥夺条件: 可剥夺资源,即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源。
- 破坏循环等待:用资源有序分配算法,系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反,这样可以及时检测死锁发生,为解除死锁创造条件。
2. 避免:
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。
由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
银行家算法步骤:
- 首先需要定义状态和安全状态的概念。
- 系统的状态是当前给进程分配的资源情况。
- 因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。
- 安全状态是指至少有一个资源分配序列不会导致死锁。
- 当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
3. 检测:
此时资源已经分配,所以我们可以为每个进程和每个资源指定一个唯一的号码,建立资源分配表和进程等待表,用资源分配图化简法来检测死锁。
4. 解除:
当发现有进程死锁后,应立即把它从死锁状态中解脱出来,常采用的方法有:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
(四)四个算法
- 银行家算法:避免死锁(资源动态分配的过程中,用某种方法去阻止它进入不安全状态);不那么严格的限制产生死锁的必要条件的存在,而是在系统运行过程中小心的避免死锁的最终发生。每次先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。
- 资源有序分配法 :预防死锁(破坏死锁四个必要条件之一);系统中所有的资源都按某种规则统一编号,所有分配请求必须使用上升的次序进行,当遵守上升次序的规则时,若资源可以,则予以分配,否则等待。这样破坏了循环等待资源条件。
- 资源分配图化简法:检测死锁(及时检测死锁发生,为解除死锁创造条件),如果经过化简后,节点都不能化简为孤立节点,则代表形成死锁。
- 撤销进程法:解除死锁(常用方法是撤销或挂起一些进程,回收资源,分配给已经阻塞的,让它运行)。