区分读和写,处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。
注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写优先于读,当有线程因为等待写锁而进入睡眠时,则后续读者也必须等待
应用场景
适用于读取数据的频率远远大于写数据的频率的场合。
扩展
加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。
互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源
区别:
当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。
互斥锁加锁失败后,线程会释放 CPU ,给其他线程;那这个开销成本是什么呢?会有两次线程上下文切换的成本:当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,
RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高
信号量:semaphore,是用于线程间同步的,当一个线程完成操作后就通过信号量通知其它线程,然后别的线程就可以继续进行某些操作了。
信号量和互斥锁的区别:semaphore
信号量是用于线程间同步的,而互斥锁是用于线程的互斥的
互斥量的获取和释放都是在同一线程中完成的,pthread_mutex_lock(),pthread_mutex_unlock()。而信号量的获得和释放是在不同的线程的操作为sem_wait(),sempost();
互斥量的值只能为0和1,而信号量只要value>0,其它线程就可以sem_wait成功,成功后信号量value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value加1。因此信号量的值可以为非负整数
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
进程推进顺序不合理 :进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。
死锁只有在这四个条件同时满足时出现。
(1)死锁预防:通过确保死锁的一个必要条件不会满足,保证不会发生死锁
(2)死锁检测:允许死锁的发生,但是可以通过系统设置的检测结构及时的检测出死锁的发生,采取一些措施,将死锁清除掉
(3)死锁避免:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
(4)死锁解除:与死锁检测相配套的一种措施。当检测到系统中已发生死锁,需将进程从死锁状态中解脱出来。
死锁预防 :核心思想是,死锁只有在四个条件同时满足时出现。预防死锁就是至少破坏这四个条件其中一项,即破坏“禁止抢占”、破坏“持有等待”、破坏“资源互斥”和破坏“循环等待”。
*进程启动拒绝:如果一个进程的请求会导致死锁,则不启动该进程。
*资源分配拒绝:如果一个进程增加的资源请求会导致死锁,则不允许此分配
死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现;而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。
1.在资源分配图中,找出既不阻塞又不是孤点的进程。消去它所有的请求边和分配边,使之称为孤立的结点
2.进程所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。若能消去途中所有的边,则称该图是可完全简化的。
*两种常用的死锁解除方法:
1.资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
2.撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能己经运行了很长时间,己经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
3.进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
如何决定“对谁动手”
1.进程优先级
2.己执行多长时间
3.还要多久能完成
4.进程己经使用了多少资源
5.进程是交互式的还是批处理式的
进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位一般由程序、数据集合和进程控制块三部分组成
线程(thread)是操作系统能够进行运算调度的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行
一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存,线程之间可以共享对象、资源,如果有冲突或需要协同,还可以随时沟通以解决冲突或保持同步
区别
本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。它与数据机构中的堆是两回事,分配方式类似于链表。
全局区(静态区)(static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。程序结束后由系统释放。
文字常量区:常量字符串就是放在这里。程序结束后由系统释放。
程序代码区:存放函数体的二进制代码
内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
区别:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用, 系统已经不能再分配出你所需要的空间;内存泄露是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但是内存泄漏次数多了就会导致内存溢出。
进程是资源分配的基本单位;线程是系统调度和分派的基本单位。
属于同一进程的线程,堆是共享的,栈是私有的。
属于同一进程的所有线程都具有相同的地址空间。
多进程的优点:
①编程相对容易;通常不需要考虑锁(占用相同资源才会考虑)和同步资源的问题。
②更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
③有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。
多线程的优点:
①创建速度快,方便高效的数据共享
共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术。
②较轻的上下文切换开销 - 不用切换地址空间,不用更改寄存器,不用刷新TLB。
③提供非均质的服务。如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。
多线程相比于多进程占用内存少、CPU利用率高,创建销毁,切换都比较简单,速度很快。多进程相比于多线程共享数据复杂,需要进行进程间通信,但是同步简单,多线程因为数据共享简单,导致同步复杂。多进程编程调试都比多线程简单。进程之间互相不影响,一个线程挂掉将导致整个进程挂掉。多进程适合多核,多机分布,多线程适合多核分布。
多线程模型适用于I/O密集型的工作场景,因此I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。
多进程模型,适用于CPU密集型。同时,多进程模型也适用于多机分布式场景中,易于多机扩展。
①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
②需要进行大量计算的优先使用线程(CPU频繁切换)
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
③强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
④可能要扩展到多机分布的用进程,多核分布的用线程
进程间通信的方式有:1、无名管道( pipe );2、高级管道(popen);3、有名管道 (named pipe);4、消息队列( message queue );5、信号量( semophore );7、共享内存( shared memory );8、套接字( socket )。
1、无名管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2、高级管道(popen):
将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
3、有名管道 (named pipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4、消息队列( message queue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5、信号量( semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6、信号 ( sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
7、共享内存( shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是非常快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
8、套接字( socket ) :
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
多线程并发:多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行。
C++多线程并发: (简单情况下)实现C++多线程并发程序的思路如下:将任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。
9.计算机内存管理的方式
3.1、内存管理基础 | 王道考研操作系统知识点整理
简单的分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,比如块式管理。非连续分配管理方式允许一个程序内存分散,比如页式管理、段式管理和段页式管理。
1. 块式管理:远古时代的计算机操作系统的内存管理方式,将内存分为几个固定大小的块,每个块只包含一个进程,如果程序运行需要内存,操作系统就给它分配一块,如果程序运行只需要很小的空间,则分配的这块内存很大一部分就浪费了,这些在每个块中未被利用的空间,我们称为碎片。
2. 页式管理:把主存分为大小相等且固定的一页一页的形式,页比较小,相对于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
3. 段式管理:页式管理虽然提高了内存利用率,但是其中的页没有任何实际意义,段式管理把主存分为一段一段 ,每一段的空间又比一页的空间小很多。但是段有实际意义,段式管理通过段表对应逻辑地址和物理地址。
4. 段页式管理:段页式管理机制结合了段式管理和页式管理的优点,就是把主存分为若干段,每个段又分为若干页,也就是说段页式管理机制中段和段之间以及段的内部都是离散的。
等待态(阻塞):等待某个事件的完成;
2.就绪态:等待系统分配处理器以便运行;
3.运行态:占有处理器正在运行。
运行态→等待态 往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
等待态→就绪态 则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态 不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态 系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态
10.进程调度方法
调度算法是指:根据系统的资源分配策略所规定的资源分配算法
1 先来先服务调度算法(队列)(FCFS)First Come First Served
2 短作业(进程)优先调度算法(优先队列)(SJF)Shortest Job First
3 优先权调度算法 (PSA)priority-scheduling algorithm
4.高响应比优先调度算法 (HRRN) HighestResponseRatioNext
使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
5 基于时间片的轮转调度算法(RoundRobin)
6 多级反馈队列调度算法(Feedback)
10.进程和线程的上下文切换
上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态。
线程上下文切换和进程上下文切换的区别:
1 线程切换发生在CPU保存线程当前状态并切换到同一进程的另一个线程时。 进程切换发生在操作系统的调度程序保存正在运行的 Program 的当前状态(包括 PCB 的状态)并切换到另一个程序时。
2 线程切换帮助CPU同时处理多个线程。 进程切换 涉及加载新程序的执行状态。
3 线程切换不涉及内存地址空间的切换,所有处理器帐户保持保存的内存地址。 PCS 涉及内存地址空间的切换。处理器帐户的所有内存地址都会被刷新。
4 线程切换处理器的缓存和平移后备缓冲区保留它们的状态。 进程切换处理器的缓存和 TLB 被刷新。
5 线程切换虽然涉及寄存器和堆栈指针的切换,但它不承担改变地址空间的成本,因此效率更高。 进程切换涉及更改地址空间的沉重成本。因此效率较低。
6 线程切换 更快更便宜。 进程切换 相对较慢且成本较高。
协程(Coroutine)是一种轻量级的用户级线程,实现的是非抢占式的调度,即由当前协程切换到其他协程由当前协程来控制。是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处继续运行。 一个线程内的多个协程的运行是串行的,这点和多进程(多线程)在多核CPU上执行时是不同的。 多进程(多线程)在多核CPU上是可以并行的。当线程内的某一个协程运行时,其它协程必须挂起。
由于协程切换是在线程内完成的,涉及到的资源比较少。不像内核级线程(进程)切换那样,上下文的内容比较多,切换代价较大。协程本身是非常轻巧的,可以简单理解为只是切换了寄存器和协程栈的内容。这样代价就非常小。
线程池其实是一种池化的技术的实现,池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。
使用线程池的好处
一、内核态、用户态概念
内核态:也叫内核空间,是内核进程/线程所在的区域。主要负责运行系统、硬件交互。
用户态:也叫用户空间,是用户进程/线程所在的区域。主要用于执行用户程序。
二、内核态和用户态的区别
内核态:运行的代码不受任何限制,CPU可以执行任何指令。
用户态:运行的代码需要受到CPU的很多检查,不能直接访问内核数据和程序,也就是说不可以像内核态线程一样访问任何有效地址。
操作系统在执行用户程序时,主要工作在用户态,只有在其执行没有权限完成的任务时才会切换到内核态。
三、为什么要区分内核态和用户态
保护机制。防止用户进程误操作或者是恶意破坏系统。内核态类似于C++的私有成员,只能在类内访问,用户态类似于公有成员,可以随意访问。
线程安全的实现方式
C++的线程安全的方法有以下几种:
虚拟内存是干嘛的
虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片。还有部分暂时存储在外部磁盘存储器上(Swap),在需要时进行数据交换。
它允许操作系统将物理内存和磁盘存储器组合起来使用,使得应用程序能够使用比实际可用内存更多的内存空间。虚拟内存的核心思想是将内存分成大小相同的页面(page),并将其映射到磁盘上的一个交换文件(swap file)中。
当应用程序访问一个尚未分配的页面时,操作系统会将其加载到物理内存中。当物理内存不足时,操作系统会将一部分页面置换到交换文件中,以便腾出物理内存供新的页面使用。这个过程称为页面置换(page swapping)。
常用的页面置换算法(如最近最少使用算法、先进先出算法、时钟算法等)、虚拟内存的优化方法(如预读取、页面合并等)
优点:可以弥补物理内存大小的不足;一定程度的提高反映速度;减少对物理内存的读取从而保护内存延长内存使用寿命;
缺点:占用一定的物理硬盘空间;加大了对硬盘的读写;设置不得当会影响整机稳定性与速度