进程管理
一、进程与线程
1.1 进程的概念与特征
- 进程的概念
为了更好的控制程序之间的并发执行,OS引入了进程的概念。
进程又称进程映像的运行过程,进程映像包含程序段、相关数据、PCB。
PCB(Process Control Block,进程控制块)是一个描述进程基本情况和运行状态的数据结构。
PCB是进程存在的唯一标志,OS分配资源和调度的唯一单位。创建进程就是创建进程中的PCB,撤销进程就是撤销进程中的PCB。
- 进程的特征
a)动态性:进程是进程映像一次运行过程,有着自己的生命周期,动态的产生、变化、消亡。
b)并发性:多个进程映像(进程实体)存入内存之中,并发的执行,以提高资源利用率。
c)独立性:进程是独立运行、独立获取资源、独立接受调度的基本单位。
d)异步性:由于进程相互制约,会使得进程具有制约性,各个进程走走停停,以不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此OS要配备进程同步机制,让进程之间有合理的前后运行关系。
e)结构性:进程是进程实体运行的过程,而进程实体是由程序段、数据段、进程控制块三部分组成。
1.2 进程的状态与转换
进程是一个动态的过程,就自然有不同的状态。
1)创建态:申请PCB,向其填一些控制信息和管理信息,然后分配能运行所需要的前置资源,然后就将进程转换为就绪态
2)就绪态:进程获得了除处理机外的一切所需资源,然后排在就绪队列后面,等待获取处理机。
3)运行态:就绪队列拿出就绪进程,让其在处理机上运行,运行结束则转到结束态,若是进程时间片用完则将进程转化为就绪态。在单核CPU中,每一时刻只有一个进程处于运行态。
4)阻塞态:当一个进程在运行过程中想获取其它资源如I/O,进程暂停,让出CPU,将自己阻塞,当其获得了除CPU之外的一切资源时,又转为就绪态。
5)结束态:正常结束或其它原因中断退出运行,OS先将进程状态置为结束态,然后再进一步处理资源释放和回收等工作。
1.3 进程控制
进程控制的代码是原语,不允许中断。进程控制主要是进程的创建、进程的撤销、实现进程之间的转换。
- 进程的创建
子进程与父进程的关系:父进程可以创建一个子进程,子进程可以继承父进程的所有资源。当子进程撤销时,将其所用的资源归还给父进程,而父进程撤销时,要将子进程一起撤销。
创建一个新的进程4步骤如下:
1)分配唯一的进程标识号,申请PCB(PCB是有限的,所以可能申请失败)
2)为进程分配除CPU之外的资源,分配程序、数据、用户栈所需的内存资源。
注:资源不足时,进程不会创建失败,而是处于等待资源的状态。
3)初始化PCB
4)若进程就绪队列未满,则将其插入就绪队列。
- 进程的终止
进程结束情况:正常结束、异常结束、外界干预。
OS终止进程有5步:
1)根据终止进程标识符,检索PCB,从中读取该进程的状态。
2)若此时发现该进程处于执行状态,则马上终止,让出CPU。
3)若此时发现该进程有子进程,马上终止。
4)把该进程所拥有的资源归还给父进程或者OS。
5)将该PCB从所在数据结构中删除。
- 进程的阻塞和唤醒
正在执行的进程,若请求系统其它资源失败、等待某种操作的完成、新数据尚未到达或无新工作可做等,由系统自动执行阻塞原语(Block),使自己从运行态转变成阻塞态。阻塞原语执行过程如下3步:
1)根据标识号找PCB
2)若该进程在运行,则保护现场,然后转为阻塞态。
3)将该PCB插入等待事件相应的数据结构,然后让出CPU。
唤醒原语如下三步:
1)从该事件的等待队列找到PCB。
2)移除该PCB,将其转为就绪态。
3)将该PCB插入就绪队列,等待CPU的调度。
Block原语与Wakeup原语是一对相反操作,Block原语由阻塞进程自己调用的,而Wakeup原语是由一个与被唤醒进程合作或被其它相关的进程调用实现的。
- 进程切换
进程切换和上面的其它操作都是在操作系统内核的支持下运行的。
进程切换时指处理机从一个进程的运行转到另一个进程上运行。切换过程如下6步:
1)保存处理机上下文,包括程序计数器和其它寄存器。
2)更新PCB信息
3)把进程的PCB移入就绪或是阻塞队列。
4)选择另一个进程执行,并更新其PCB。
5)更新内存管理的数据结构
6)恢复处理机上下文。
1.4 进程的组织
进程是进程映像运行的过程,进程映像由程序段、数据段、PCB组成。
- 进程控制块PCB
建立好PCB之后,该结构常驻内存,是进程存在的唯一标志。
进程描述信息 | 进程控制和管理信息 | 资源分配清单 | 处理机相关信息 |
---|---|---|---|
进程标识符(PID) | 进程当前状态 | 代码段指针 | 通用寄存器值 |
用户标识符(UID) | 进程优先级 | 数据段指针 | 地址寄存器址 |
代码运行入口地址 | 堆栈段指针 | 控制寄存器值 | |
程序的外存地址 | 文件描述符 | 标注寄存器值 | |
进入内存时间 | 键盘 | 状态字 | |
处理机占用时间 | 鼠标 | ||
信号量使用 |
1.5 进程的通信
进程通信是指进程之间的信息交换,通过这种信息交换以达到进程之间的相互合作。通信分为两类:
1)PV操作,低级通信方式。
2)较高的效率传输大量数据的通信方式,高级通信方式,有如下3种。
- 共享存储
进程空间都是相互独立的,不能直接访问其它进程空间中的信息。此时系统给出一块空间,各个进程通过对上面的数据进行读写来实现通信。此时需要用到互斥机制来保证数据的正确性。
注:共享空间可以是共享的数据结构,也可以是基于存储区的共享。
- 消息传递
系统定义一个消息格式Message,并提供发送消息和接收消息两个原语进行数据交换。而这种通信分为两种如下:
1)直接通信方式:发送进程把发的消息直接挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
2)间接通信方式:发送进程把消息发送到某个中间实体,接受进程从中间实体取得消息。我们也称中间实体为信箱。
- 管道通信
用一个文件来连接读进程和写进程,方便它们以字符流形式大量传输信息。我们又称管道为pipe文件。pipe文件与普通的文件的区别是:
1)限制管道大小。不会像文件那样不加检验的增长。当写满时,write()将调用阻塞原语阻塞自己。
2)读进程也可能工作得比写进程快,当读空是,read()将阻塞自己,而不是像文件那样返回文件结束的问题。
注:管道算法共享存储的发展,它可以一边读一边写,只是当满或没的时候有相应的阻塞。这也决定管道是半双工通信。
1.6线程概念和多线程模型
- 线程的基本概念
线程最直接的理解就是“轻量级进程”,它不拥有系统资源,但是CPU执行的基本单元。它的出现是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
线程由线程ID、程序计数器、寄存器集合和堆栈组成。切换的两个线程如果来自同一个进程,则只需要很少的时空开销。
- 线程与进程的比较
1)调度与拥有资源
线程是独立调度的基本单位,进程是拥有资源的基本单位。
2)并发性
不仅进程之间可以执行并发,而且多个线程之间也可以执行并发,从而使操作系统具有更好的并发性,提高了系统的吞吐量。
3)系统开销
进程创建等的开销大于线程,进程切换的开销比同一进程中线程切换的开销大。进程通信的开销比同一进程中多个线程通信的开销大。
4)地址空间和其它资源
进程的地址空间之间相互独立,同一进程的各线程间共享进程的资源,某进程内的线程对于其它进程不可见。
- 线程的属性
1)线程是一个轻型实体,它不拥有系统资源,每一个线程有一个线程控制块。
2)不同线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,OS创建不同的线程。
3)同一进程的各线程共享该进程所有资源。
4)CPU的执行单位。
5)线程跟进程一样,有自己的生命周期,在其期间也有各种状态的转换。
- 线程的实现方式
线程分为内核级线程(内核支持的线程)和用户级线程。
1)用户级线程的各种操作由代码实现(用线程库区派生线程),内核感受不到这些线程的存在。
2)内核级线程的各种操作由内核完成
- 多线程模型
1)多对一模型:将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。
2)一对一模型:将每个用户级线程映射到一个内核级线程。
3)多对多模型:将n个用户级线程映射到m个内核级线程上,m<=n
二、处理机调度
2.1 调度的概念
- 调度的基本概念
有多个进程争夺一个CPU,如何让它们有序的并发,就要按照一定的算法(公平、高效)选择进程执行的顺序来让CPU执行,保证系统的并发和利用率。这是多道程序设计的基础,也是操作系统的核心。
- 调度的层次
1)作业调度
又称高级调度,主要就是按照一定的原则将外存上处于后备状态的作业中,给它们分配资源,让它们有资格去争夺CPU。
2)中级调度
又称内存调度,其作用是提高内存利用率,把暂时不能运行的作业掉至外存外,将其挂起。
3)进程调度
按照一定规则,把CPU分配给其它进程。进程调度的频率极高。
- 三级调度的联系
1)作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的程序挂起。
2)作业调度次数少,中级调度略多,进程调度频率最高。
3)进程调度是最基本的,不可或缺。
2.2 调度的时机、切换与过程
- 不能进行进程的调度与切换的情况
1)在处理中断过程中
2)进程在操作系统内核程序临界区中- 应该进行进程切换的情况
1)发生引起调度条件且当前进程无法继续运行下去是。
2)中断处理结束或自陷处理结束后,恢复原来进程的执行现场,根据PC来继续执行。
2.3 进程调度方式
1)非剥夺调度方式,非抢占方式,除非自己让出CPU,否则别想。
2)剥夺调度方式,除了自己可以把CPU让出来,系统可以决定是否把CPU让出来。
2.4 调度的基本准则
1)CPU利用率
CPU是计算机中最昂贵的资源,要让CPU“忙起来”。
2)系统吞吐量
单位时间内CPU完成作业的数量
3)周转时间
周转时间=作业完成时间-作业提交时间
4)等待时间
进程等待处理机状态的时间之和。
5)响应时间
用户从提交作业到系统首次响应所用的时间。
2.5 典型的调度算法
为了满足以上的准则,以下介绍一些经典的调度算法。
- 先来先服务(FCFS)调度算法
算法实现简单,对长作业有利,有利于CPU繁忙型作业,不利于I/O繁忙性作业。效率低- 短作业优先(SJF)调度算法
根据FCFS对短作业长时间无法响应的缺点,提出SJF。但是SJF会导致长作业长时间得不到执行,甚至直接“饿死”。第二个就是没有考虑作业的时间紧迫度。- 优先级调度算法
按照不同的标准可以分为
1)非剥夺式优先级调度算法2)剥夺式
1)静态优先级2)动态优先级
优先级的设置可以按照如下原则
1)系统进程>用户进程2)交互型进程>非交互型进程3)I/O型进程>计算型进程- 高响应比优先调度算法
结合上面的优缺点,考虑到短作业,又考虑到长作业不会“饥饿”。
响应比=(等待时间+要求服务时间)/ 要求服务时间- 时间片轮转调度算法
顾名思义,每个进程分配一个时间片,用完就把CPU分配给另一个进程。- 多级反馈队列调度算法
时间片轮转法和优先级调度算法的综合发展,动态的调整时间片和优先级。
思想如下
1)设置多个就绪队列,每个队列的优先级逐渐降低
2)每个队列的时间片大小各不相同
3)第一队列的进程时间片用完之后就放在下一队列,如此循环。
4)只有当前面的所有队列的进程的时间片都用完了,才能进行下面队列中进程的执行。
三、进程同步
3.1 进程同步的基本概念
1+23=?对于这个问题,计算机先算(1+2)这个进程,还是先算(23)这个进程?这里就有一个执行的先后顺序,这就要考虑到进程之间的同步问题。
- 临界资源
很多进程想同时访问的资源就叫临界资源。对于临界资源的访问,就必须互斥的进行,在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,可把临界资源的访问过程分成4个部分:
1)进入区:检查是否可进入,设置标志防止其它进程进入临界区
2)临界区:进程中访问临界资源的那段代码,又称临界段。
3)退出区:将正在访问临界区的标志清除
4)剩余区:代码中的其它部分
- 同步
又称其为直接制约关系,它们需要相互协调。- 互斥
互斥也称间接制约关系,当一个进程进入临界区域时,另一个进程必须等待。为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
1)空闲让进
2)忙则等待
3)有限等待
4)让权等待
注:后面所讲的内容都是围绕着这四个标准去发展演进的。
3.2 实现临界区互斥的基本方法
- 软件实现方法
核心思想为设置标志来代表临界区的资源是否正在被访问。
1)单标志法
P0进程
while(turn != 0);//进入区
critical section;//临界区
turn = 1;//退出区
remainder section;//剩余区
P1进程
while(turn != 1);
critical section;
turn = 0;
remainder section;
算法一违背了“空闲让进”
2)双标志法先检查
Pi进程
while(flag[j]);第一步
flag[i] = TRUE;第三步
critical section;
flag[i] = FALSE;
remainder section;
Pj进程
while(flag[i]);第二步
flag[j] = TRUE;第四步
critical section;
flag[j] = FALSE;
remainder section;
若上面按照第一二三四步完成,则会违背“忙则等待”。
3)双标志法后检查
Pi进程
flag[i] = TRUE;
while(falg[j]);
critical section;
flag[i] = FALSE;
remainder section;
Pj进程
flag[j] = TRUE;
while(falg[i]);
critical section;
flag[j] = FALSE;
remainder section;
可能出现相互谦让,导致谁也进入不了临界区,导致“饥饿”现象。
4)Peterson’s Algorithm
Pi进程
flag[i] = TRUE;turn = j;
while(falg[j] && turn == j);
critical section;
flag[i] = FALSE;
remainder section;
Pj进程
flag[j] = TRUE;turn = i;
while(falg[i] && turn ==i);
critical section;
flag[j] = FALSE;
remainder section;
- 硬件实现方法
1)中断屏蔽方法
关中断;临界区;开中断;
2)硬件指令方法
a)TestAndSet指令:原子操作。
boolean TestAndSet(boolean *lock){
boolean old;
old = *lock;
*lock = true;
return old;
}
while TesAndSet(&lock);
进程的临界区代码段;
lock = FALSE;
进程的其它代码;
b)Swap指令:该指令的功能是交换两个字的内容
Swap(boolean *a,boolean *b){
boolean temp;
temp = *a;
*a = *b;
*b = temp;
}
注:它们是由硬件逻辑实现,不会被中断,上面的代码只是描述其功能。
3.3 信号量
- 整型信号量
把资源数目用一个整型变量S表示,wait和signal操作描述如下:
//wait()操作我们称其为P操作
//signal()操作我们称其为V操作
wait(S){
while(S<=0);
S = S - 1;
}
signal(S){
S = S + 1;
}
注:当S<0时,wait操作一直反复测试,没有遵循“让权等待”。
2. 记录型信号量
在上面的基础上增加一个进程链表L,实现让权等待,将等待的进程插入进程链表L。
typedef struct{
int value;
struct process *L;
}semaphore;
void wait(semaphore S){
S.value--;
if(S.value < 0){//这里用的就不是while()
add this process to S.L;
block(S.L);//通过自我阻塞来实现让权等待
}
}
void signal(semaphore){
S.value++;
if(S.value <= 0){
remove a process P from S.L;
wakeup(P);
}
}
- 利用信号量实现同步
semaphore S = 0;//设为0是因为这是同步而不是互斥,P2进程一开始就不能操作x,必须先让P1完成。
P1(){
do(x);
V(S);
}
P2(){
P(S);
do(x);
}
//注:自己的阻塞与别人的唤醒
- 利用信号量实现进程互斥
semaphore S = 1;//初始化信号量为1,这个时候是去多个进程去抢。
P1(){
P(S);
进程P1的临界区;
V(S);
}
P2(){
P(S);
进程P2的临界区;
V(S)
}
- 利用信号量实现前驱关系
有同步的地方就设置一个记录型信号量,前驱关系就通过同步的方式表达出来。
3.4 管程
在上面的信号量机制中,要访问临界资源的进程都必须自备同步的PV操作,大量分散的同步操作给系统带来了麻烦,而且容易因同步操作不当带来死锁,此时系统提供一种类似P V操作的方法来实现互斥,我们称其为管程。
- 管程的定义
它统一管理了各类资源,用少量的信息来表示资源,我们称为对资源定义的数据结构,在这组数据结构上定义一组操作来实现互斥。数据结构加这组操作就叫管程。
特点
1)管程把对共享资源的操作封装起来,想类一样。
2)每次仅允许一个进程进入管程,从而实现进程互斥。- 条件变量
当一个进程进入管程,但是它又要申请其它共享资源,此时它并不释放管程。而当这个时候,应该把该进程阻塞,所以设置条件变量condition,当因为一种条件阻塞时,就将其插入该条件而阻塞的队列里。
3.5 经典同步问题
- 生产者-消费者问题:缓冲区是临界资源
- 读者-写者问题:运行读读,但是不允许读写,写写同时进行。
- 哲学家进餐问题:n个人,每个人旁边有一根筷子,一共n个,如果每个人都拿起一个筷子,则大家都吃不成饭。
- 吸烟者问题:同步+互斥问题
详细解答
四、死锁
4.1 死锁的概念
- 死锁的定义
一条单行道,两边都有小轿车,一个想过去,一个想过来。现实生活中的死锁。
计算机中,进程P1获取资源1并保持资源1,进程P2获取资源2并保存资源1,此时P1请求资源2,P2请求资源1。- 死锁产生的原因
1)系统资源竞争且不可剥夺
2)进程推荐顺序非法
3)信号量使用不当也会造成死锁- 死锁产生的必要条件
1)互斥条件
2)不剥夺条件
3)请求并保存条件
4)循环等待条件
注:这四个条件必须同时满足才会产生死锁,所以死锁预防和解除也是靠这四个。
4.2 死锁的处理策略
破坏死锁产生的四个必要条件,或允许产生死锁,然后再去解除。
- 死锁预防
破坏四个条件之一- 避免死锁
用算法防止系统进入不安全状态,来避免死锁,不安全状态就是进程无论怎么分配资源,如果不发生其它进程终止的情况,一定会导致死锁。- 死锁的检测及解除
允许发生死锁,然后采取措施解除死锁。
注:死锁预防实现简单,但是利用率低。而避免死锁需要用算法来判断系统是否进入不安全状态,实现起来较为复杂。
4.3 死锁预防
防止死锁的发生只需要破坏死锁产生的4个必要条件之一即可。
- 破坏户次条件:不太可行
- 破坏不剥夺条件:保存CPU现场即可,一般不能用于打印机之类的资源。
- 破坏请求并保存条件:一次性申请完要使用的全部资源,这样实现起来简单,但效率极低。
- 破坏循环等待条件:首先给系统中的资源编号,规定每一个进程必须按编号递增的顺序请求资源,同类资源一次性申请完
4.4 死锁避免
- 系统安全状态
在保证资源都能分配够的情况下预先找出一种进程推进的序列,若能找到,就认为系统处于安全状态,否则系统处于不安全状态。- 银行家算法(资源就像钱,申请资源就像贷款一样,系统就是银行家)
进程先申明自己对某个共享资源最大需求量是多少(声明这个的原因是好提前判断系统是否处于安全状态),当进程申请某类资源时,判断自己已拥有资源+想申请资源是否大于最大需求资源(针对一种共享资源)。若大于直接拒绝,否则判断系统如果分配了该资源,此时的系统是否处于安全状态,若处于则分配,否则也要推迟分配。
1)数据结构描述
a)可利用资源Available[j] = K表示系统中有Rj类资源K个
b)最大需求矩阵Max:nxm矩阵,n个进程,m类资源。Max[i,j]=K表示进程i需要Rj类资源的最大数目为K。
c)分配矩阵Allocation:nxm矩阵,Allocation[i,j]=K表示进程 i 当前已分得Rj类资源的数目为K。
d)需求矩阵Need:nxm矩阵Need[i,j]=K表示进程 i 还需要Rj类资源的数目为K
2)银行家算法描述
若进程请求某类资源,先分配,再判断系统是否处于安全状态,若处于,则实际分配给该进程,否则撤销分配。
3)安全性算法
用Available向量去匹配Need矩阵,若资源够就分配,然后加上Allocation矩阵,循环上面继续去匹配,直到所有进程都进入一个安全序列,则系统是安全的。否则不安全。
- 银行家算法举例
详细例子
4.5 死锁检测和解除
- 资源分配图
长方形代表一类资源,小圆圈代表该资源的个数。
- 死锁定理
简化资源分配图,找出既不阻塞又不孤点的进程,然后去掉它的边,循环前面的步骤,若资源分配图能够被完全简化,则不会有死锁。
有死锁的条件是当且仅当此时刻的资源分配图是不可完全简化的,该条件为死锁定理。- 死锁解除
1)资源剥夺法:需注意防止被挂起的进程长时间得不到资源而处于资源匮乏状态。
2)撤销进程法:撤销到进程可以推进为止,撤销可以按照进程的优先级和撤销所需要的代价大小来进行。
3)进程回退法:让一个或多个进程回退到组一回避死锁的地步,进程回退时自愿释放资源而非被剥夺。要求系统保持进程的历史信息,设置还原点。