一级目录
进程创建
进程在创建的时候,操作系统会将磁盘上的程序和数据加载进内存进程的内存结构大致如下
我们可以通过fork()调用创建一个新的进程,子进程创建之初,基本拷贝了父进程的运行环境,包括后续的代码,唯一不同的是fork返回值,返回为O的是子进程,父进程会返回子进程的pid。我们可以根据返回值让父子进程走到不同逻辑的代码段上。
printf ( "hello world (pid:%d) \n",(int) getpid( ) ) ;
int rc - fork (o) :
if (rc < 0) {
fprintf (stderr,"fork failed\n" );
exit (1);
}lse if (rc -= 0){
printf ( "hello,I am child (pid:'d) \n",(int) getpid() ):else {
printf ( "hello, I am parent of id (pid:id) ln”,
rc,(int) getpid ( ) ) ;
进程状态
创建态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
就绪态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
运行态:进程处于就绪状态,进入就绪队列,被调度后,进程进入执行状态
阻塞态:正在执行的进程由于某些事件((Vo请求,申请缓存区失败)而暂时无法运行,进程受到阻塞,进入阻塞队列。在满足请求时进入就绪状态等待系统调用
终止态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行。
用户态内核态切换
凡是涉及到O读写、内存分配等硬件资源的操作时,往往不能直接操作,而是通过一种叫系统调用的过程,让程序从用户态陷入到内核态运行。
这是因为在CPU的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
这一类指令,我们称为特权指令,在执行特权指令,我们需要从用户态切换到内核态进行系统调用。而用户态到内核态的切换是有一定的开销的,具体来说有以下几点
1.保留用户态现场(上下文、寄存器、用户栈等)
2.复制用户态参数,用户栈切到内核栈,进入内核态·额外的检查(因为内核代码对用户不信任)
3.执行内核态代码
4.复制内核态代码执行结果,回到用户态
5.恢复用户态现场(上下文、寄存器、用户栈等)
上下文切换
CPU切换
就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
系统调用
在执行特权指令时,需要从用户态切换成内核态。就发生了CPU上下文切换,整个过程是这样的:1、保存CPU寄存器里原来用户态的指令位
2、为了执行内核态代码,CPU寄存器需要更新为内核态指令的新位置。3、跳转到内核态运行内核任务。
4、当系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次CPU上下文切换。(用户态-内核态-用户态)。
系统调用的切换,也叫特权模式切换。
进程上下文切换
首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和CPU寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
虚拟内存,就是逻辑内存到实际内存的一种映射关系,也可以理解为是页表
线程上下文切换
相同进程间的线程,是共享页表(虚拟内存)的。所以线程间的上下文切换,省去了虚拟内存的切换,线程有自己私有的栈,的寄存器。切换这些即可。
进程调度
操作系统其实也是一个程序,如果一个程序在cpu上运行,那么就意味着操作系统没有运行。那么操作系统如果重新获得cpu的控制权?
实际上这个问题很早就有了答案,通过时钟中断。有了时钟中断,操作系统重新获得了控制权,就可以做它想做的任何事,比如切换进程。
回顾上文的图,就绪队列中那么多等待运行的进程,操作系统改如何调度,能让它们看起来像并行一样的运行,是本节需要讨论的问题。
调度指标:
周转时间=任务完成时间-到达时间
响应时间=首次运行时间-到达时间
基于周转时间:
先进先出-FIFO·
最短任务优先-SJF
最短完成时间优先-STCF
基于响应时间:
时间片轮转-RR
以上的算法各有优缺点,所以一个综合的调度算法出现了多级反馈队列-MLFQ,该算法将进程分为了多个优先级的队列。
多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。他有以下规则
就绪队列1的优先级大于就绪队列2
1.如果在相同的就绪队列上,轮转运行
2.工作放入系统时,会在最高优先级(最上层队列)
3.工作用完整个时间片后,降低优先级(移入下一层队列)4.如果在时间片内主动释放cpu,优先级不变
5.一段时间后,所有队列都重新加入最高优先级(避免进程饥饿)
其实根据作业完成的过程中是否会被打断,又分为非抢占式和抢占式调度。VM采用的是抢占式调度策略。
多处理器调度
前面我们讨论的进程调度都基于单cpu单核的情况下,然而在现实中,单个cpu会有多核的处理器,多处理器的调度更加复杂。
为了加速cpu的运行,cpu都配备了高速缓存,缓存中有的数据就不需要去内存中读取了。
有了缓存就涉及到缓存一致性的问题
通过监控内存访问,在基于总线的系统中,通过总线窥探(bus snooping)的方式监听链接所有的缓存的内存的总线,如果cpu发现他们缓存中的数据被更新,就会作废本地缓存,或者重新更新本地缓存。
在进行上下文切换的情况下,我们还需要考虑不同cpu中,缓存的亲和度问题。比如我们会更偏向同一进程,在相同的cpu中调度和切换,避免高速缓存的命中率下降。还有同一进程中的线程,也尽量在相同cpu下调度。
进程通信
共享存储:
1,什么是共享存储?
操作系统提供了一个共享空间可以被进程1,2互斥的访问2,分类
1))基于数据结构的共享:比如共享空间只能存放一个长度为10的数组。这种共享方式速度慢,限制多,是一种低级的通信方式
2)基于存储区的共享:在内存中画出一块共享存储区,数据的形式,存放的位置都由进程控制,而不是操作系统。相比之下它的速度更快,是一种高级通信方式
消息传递:
1,什么是消息传递?
进程中的数据交换以“格式化消息“为单位,进程通过操作系统提供的”发送消息、接收消息“两个源语进行数据交换
2,格式化消息的结构:
1)消息头:包括发送进程的ID,接收进程的ID,消息类型,消息长度等格式化信息2)消息体:记录被发送的信息
3,分类:
1)直接通信方式:比如进程1,给进程2发信息,就把信息挂到进程2的信息缓存队列上
2)间接通信方式:进程1把消息放到一个中间实体上,等待需要它的进程2取走
管道通信:
1,什么是管道:
是指用于连接读写进程的一个共享文件,是在内存中开辟了一个固定大小的缓冲区2,特点:
1)管道只能采用半双工通信,在一段时间只能单向传输,如果要实现双向传输,就需要设置两个管道2)各个进程需要互斥的访问管道
3)数据以字符流的形式写入管道,当管道写满时,写进程将被堵塞,等待数据被取走。当数据全部被取出后,管道变空,此时写进程的系统调用将被堵塞。
4)如果没写满,就不允许读,没有读空,就不允许写
5)速度一旦读出,就从管道中被抛弃,这就意味着读进程只能有一个,否则可能会发生读错数据的情况
进程同步
上文的进程通信,我们需要保证进程间互斥的访问共享存储。以及在多进程并发的情况下,我们需要做到进程间的互斥以及同步,控制进程执行的顺序,这要怎么实现呢?
可以通过信号量来实现进程同步
(1)信号量就是一个变量,表示系统中某种资源的数量
(2)用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作
(3)一对原语是: wait(S)原语和signal(S)原语,S是信号量(将此对原语理解为函数)(4)通常将wait(S)称为P操作,写为P(S);通常将signal(S)称为V操作,写为V(s)
我们也可以通过操作系统提供的互斥量Mutex实现互斥。java synchronize底层用的就是互斥量。信号量和互斥量的区别
1.互斥量用于线程的互斥,信号量用于线程的同步。2.互斥量值只能为0/1,信号量值可以为非负整数。
3.互斥量的加锁和解锁必须由同一线程分别对应使用(解铃还须系铃人),信号量可以由一个线程释放,
另一个线程得到。
4.信号量也可以实现互斥效果
死锁
形成死锁的四大必要条件:
1、互斥条件:
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2、不可剥夺条件:
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3、请求与保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4、循环等待条件:
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl,P2... pn),其中Pi等待的资源被P(i+1)占有 (i=0,1....-1),Pn等待的资源被PO占有,死锁,死锁条件,死锁策略,死锁检测,死锁恢复
处理死锁的方法
预防死锁∶通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
检测死锁︰允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
预防死锁
破坏“互斥”条件:
就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。
破坏“占有并等待”条件:
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:一次申请所需的全部资源,即“一次性分配”。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。即“先释放后申请”。破坏“不可抢占”条件:
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:占有某些资源的同时再请求被拒绝,则该进程必须释放已占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:设置进程优先级,优先级高的可以抢占资源。破坏“循环等待”条件:
将系统中的所有资源统─编号,所有进程必须按照资源编号顺序提出申请。
避免死锁
加锁顺序:线程按照一定的顺序加锁。
加锁时限∶线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
检测死锁
一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
可了解相关的死锁检测算法
接触死锁
资源剥夺法∶挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
撤销进程法∶强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点