第二章 进程管理
2.1 进程与线程
2.1.1 进程的概念
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。为什么要引入进程的概念?在多道程序的环境下,允许多个程序并发执行,此时他们将失去封闭性(会共享一些变量),并具有间断性和不可再现的特点。此外,程序是静态的,程序的执行是动态的过程,需要一种结构来描述程序的执行。为此引入进程的概念,以便更好地描述和控制程序的并发执行,实现操作系统的并发性和共享性。进程具有如下5个特征。
- 动态性。进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生和消亡的。动态性是进程最基本的特征。
- 并发性。指多个进程实体同时存于内存中,能在一段时间内同时运行。
- 独立性。指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。
- 异步性。指进程按照各自独立的、不可预知速度向前推进。
- 结构性。每个进程都配置一个PCB对其描述。
2.1.2 进程的组织
进程由三部分组成,PCB、程序段和数据段。
- PCB。进程控制块,其包含程序执行过程中操作系统调度所需的信息,如进程标识符、进程优先级、资源分配清单等。在进程整个生命期中,系统总是通过PCB对进程进行控制的,也就是说,系统唯有通过进程的PCB才能感知该进程的存在。
- 程序段。程序段是能被进程调度程序调度到CPU执行的程序代码段。
- 数据段。数据段是进程对应的程序加工处理的原始数据,或者是程序执行产生的中间或最终结果。
2.1.3 进程的状态与切换
进程在系统中具有5种状态,如下所示。
- 运行态。进程正在处理机上执行。
- 就绪态。进程已经获得运行所需的所有资源,等待操作系统调度运行。
- 阻塞态。进程正在等待某一事件而暂停运行。
- 创建态。系统正在创建PCB,分配相关资源,准备将PCB放入就绪队列中。
- 结束态。进程正在从系统中消失,如进程正常结束或者是其他原因导致进程中断退出。
上面5种状态的切换如下图所示。
值得注意的是,进程从运行态变为阻塞态是主动行为,从阻塞态变为就绪态是被动行为。
2.1.4 进程的通信
进程的通信由三种方式。
- 共享存储。在通信的进程之间存在一块可以直接访问的共享空间,通过对该片空间进行读写操作便可实现进程之间的信息交换。
- 消息传递。进程之间的通信是通过格式化的消息来进行的。其包括直接通信和间接通信方式。
- 管道通信。管道是指连接一个读进程和一个写进程以实现它们之间通信的一个共享文件。显然,管道通信只能是半双工的。
2.1.5 线程
为了减小进程切换的时间开销,我们引入线程的概念,从将进程的执行归结到多个线程的执行。因此,处理机调度的基本单位变成了线程,但进程依然是资源分配的基本单位。线程由进程创建,隶属于创建它的进程,具有独立的地址空间。同一进程的线程共享进程的资源,并发地在处理机上执行。同时,线程的状态与切换和进程大致相同。属于同一进程的线程切换不会导致进程的切换,但不同进程的线程的切换则会导致进程的切换。
2.1.6 多线程模型
线程的实现方式有两种,用户线程和内核线程。
用户线程的管理(线程的创建、撤销和切换)的所有工作都又应用程序完成,内核意识不到线程的存在。内核线程的管理的所有工作由内核完成,应用程序只有一个到内核级线程编程的接口。有些系统同时支持用户线程和内核线程,由此产生三种模型。
- 多对一模型。多个用户线程映射到一个内核进程。线程管理在用户程序中实现,对系统透明,但一个线程阻塞会导致同属于一个进程的其他线程阻塞。
- 一对一模型。一个用户线程映射到一个内核进程。每个线程会被独立地调度,但开销较大。
- 多对多模型。 n n n个用户线程映射到 m m m个内核进程, n ≥ m n\ge m n≥m。多对多模型继承了上面两者的优点,但实现复杂。
2.2 处理机调度
2.2.1 调度的概念
由于进程的数量往往会多余处理机的数量,这就产生了进程争用处理机的情况。为了解决这一问题,进程调度就应运而生,旨在通过一定的算法从就绪队列中选择一个进程分配处理机执行,以实现进程的并发执行。
进程的调度分为三个层次:作业调度、中级调度和进程调度。三者的含义如下。
- 作业调度。又称为高级调度,其主要任务是从外存选择一个处于后备状态的作业,为其建立进程,使其具备被操作系统调度的权利,从而可以获得处理器执行。作业调度是内存和辅存之间的调度,对于每一个作业只调入一次、调出一次。
- 内存调度。又称为中级调度,其将处于阻塞态的进程从内存换出到辅存,将具备运行条件的就绪进程换入内存执行。内存调度的目的是提高内存的利用率和系统吞吐量。
- 进程调度。又称为初级调度,其主要任务是按照调度算法从就绪队列中选取一个进程,为其分配处理机执行。
总的来说,作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能活动的进程挂起。作业调度的次数少,内存调度的次数略多,进程调度的频率最高。进程调度是最基本的,不可或缺。
2.2.2 调度的条件
不能进行进程调度与切换的情况如下。
- 处理中断过程中。
- 进程在操作系统内核程序的临界区中。
- 其他需要屏蔽中断的原子操作过程中。
应当进行进程调度与切换的情况。
- 发生引起调度条件且当前进程无法继续进行下去的时候(如时间片完),可以马上进行调度与切换。若造作系统只在这种情况进行调度,则是非剥夺式调度。
- 中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行前。这种情况是剥夺式调度。
2.2.3 调度的基本方式
调度有两种基本方式,非剥夺式方式和剥夺式方式。非剥夺式方式实现简单,系统开销小,适用于大多数的批处理系统,但不能用于分时系统和大多数的实时系统。
2.2.4 调度的基本准则
基本准则有以下几种。
-
CPU利用率。调度算法需要尽量保证CPU保持忙的状态,使这一资源利用率最高。
-
系统吞吐量。指单位时间内CPU完成作业的数量。满足短作业的数量越多,系统的吞吐率越高。
-
周转时间。周转时间用公式表示如下。
周 转 时 间 = 作 业 完 成 时 间 − 作 业 提 交 时 间 周转时间=作业完成时间-作业提交时间 周转时间=作业完成时间−作业提交时间
带权周转时间的公式表示如下。
带 权 周 转 时 间 = 作 业 周 转 时 间 作 业 实 际 运 行 时 间 带权周转时间=\frac{作业周转时间}{作业实际运行时间} 带权周转时间=作业实际运行时间作业周转时间 -
等待时间。等待时间是指进程处于等待处理机的状态的时间之和。
-
响应时间。响应时间是指用户提交申请到系统首次分配处理机所经历的时间。
2.2.5 典型的调度算法
先到先服务算法(FCFS)
FCFS在每次作业调度时,从后备作业中选择一个最早进入该队列的作业,分配处理机执行,知道完成或者被阻塞后才释放处理机。因此,FCFS属于不可剥夺算法,其算法简单,效率低,对长作业有利,但对短作业不利,有利于CPU密集型作业,不利于I/O密集型作业。
短作业优先算法(SJF)
SJF在每次作业调度时,从后备作业中选择一个(估计)运行时间最短的作业,分配处理机执行。SJF对长作业不利,可能会产生“饥饿”现象,但其平均等待时间、平均周转时间最少。
优先级调度算法
优先级调度算法在每次作业调度时,从后备作业中选择一个优先级最高的作业,分配处理机执行。其具有非剥夺式和剥夺式优先级两种。优先级也有静态优先级和动态优先级之分。
高响应比优先调度算法
优先级调度算法在每次作业调度时,从后备作业中选择一个响应比最高的作业,分配处理机执行。响应比计算公式如下。
响
应
比
=
等
待
时
间
+
要
求
服
务
时
间
要
求
服
务
时
间
响应比=\frac{等待时间+要求服务时间}{要求服务时间}
响应比=要求服务时间等待时间+要求服务时间
此算法相当于FCFS和SJF的结合,但又会照顾到长作业,不会产生饥饿状态。
时间片轮转调度算法
时间片轮转算法主要适用于分时系统,其将到来的进程按照时间顺序排成队列,依次分配处理器执行,但每个进程最多执行一个固定的时间片之后就要被剥夺,将处理器让给下一进程,被剥夺的进程回到就绪队列中,等待调度。
多级反馈队列调度算法
多级反馈队列具有多个队列,具有不同优先级,优先级从1-n级队列依次降低。每个队列中进程的调度使用时间片轮转调度算法,当每级队列的进程的时间片完后,进入下一相邻的、优先级较低的队列。并且上一级的队列清空后才可以进行下一级队列的调度。当进程刚进入内存时,放入1级队列。由于优先级高的队列时间片小,优先级低的队列时间片大,对于长作业,在多个队列被调度后会被执行完成,不会产生饥饿现象。
2.3 进程同步
2.3.1 概念
在多道程序的环境下,进程是并发执行的,也就是说,进程之间以不可预知的速度向前推进。因此,为了协调进程之间的关系,我们引入了进程同步的概念,在本节中,需要区分以下概念。
- 临界资源。一次仅允许一个进程使用的资源被称为临界资源。
- 临界区。访问临界资源的代码。
- 同步。与同步相对的是异步,异步的意思是进程之间并发执行时互不干预,各自以不可预知的速度向前推进。因此,同步的含义是进程为了完成某种任务而需要协调各自的执行顺序而产生的制约关系。
- 互斥。互斥亦称间接制约关系,也就是两个进程不可同时访问同一临界资源。
同步机制还需要满足以下原则。
-
空闲让进。临界区空闲时,允许申请进入临界区的进程进入临界区。
-
忙则等待。当临界区被占据时,申请进入临界区的进程需要等待。
-
有限等待。进程在等待有限时间后必须进入临界区。
-
让权等待。当进程不能进入临界区时,应立即释放处理器,防止忙等待。
2.3.2 基本方法
Peterson算法(软件实现方法)
//p_i进程
flag[i] = true;
turn = j;
while(flag[j] && turn == j);
// 临界区
flag[i] = false;
//p_j进程
flag[j] = true;
turn = i;
while(flag[i] && turn == i);
// 临界区
flag[j] = false;
硬件实现方法
硬件实现方法有两种,中断屏蔽方法和硬件指令方法。
-
中断屏蔽法。中断屏蔽法在进程进入临界区前关中断,离开临界区后开中断。因为CPU只在发生中断时发生进程切换,因此,进入临界区关闭中断不会使其他进程再进入临界区,保证了临界资源的互斥访问。
-
硬件指令法。硬件指令法有两个,TestAndSet和Swap,这两个指令都是原子操作。其实现思路是:进程之间共享变量lock,当进程需要进入临界区时,使用硬件指令检查lock状态。若可进入临界区,则修改lock状态,进入临界区。若不可进入临界区,则反复检查lock状态,直到可以进入临界区为止。其代码如下。
bool TestAndSet(bool *lock) { bool old = *lock; *lock = true; return old; } bool swap(bool *lock, bool *key) { bool temp = *key; *key = *lock; *lock = temp; } // 使用TestAndSet的例子 while(TestAndSet(&lock)); // 临界区 lock = false; ----------------------------------- // 使用Swap的例子 key = true; while(key) Swap(&lock, &key); // 临界区 lock = false;
硬件指令法虽然可以实现进程互斥访问,但是在进程等待进入临界区时并未释放处理机,违背了让权等待的原则。
信号量
信号量是用来实现同步互斥的一种机制,其通过“P操作”和“V操作”来改变信号量中预先设定的临界数目,并能阻塞和唤醒等待进程以实现让权等待原则。信号量的定义如下。
typedef struct {
int value; // 临界资源数目
struct process *L; // 进程阻塞队列
} semaphore;
void P(semaphore S) {
--S.value;
if S.value <= 0,
put this process into S.L and block it;
}
void V(semaphore S) {
++ S.value;
if S.value <= 0,
wait up a process in S.L and remove it from S.L;
}
利用信号量解决同步互斥问题。
-
同步。初值为0。
-
互斥。初值为1。
管程
在信号量机制中,每个需要访问临界资源的进程必须自备同步互斥的PV操作,大量同步的操作给系统管理带来了麻烦,且容易因同步不当而造成系统死锁。于是,便诞生了一种新的进程同步工具——管程。
管程定义了一个数据结构和实现操作的一组过程所组成的资源管理程序。管程的特性保证了进程互斥,无需程序员自己实现互斥,从而降低了死锁发生的可能性。同时,管程提供了条件变量,可以让程序员灵活地实现进程同步。通常,一个进程被阻塞的原因可以有多个,因此在管程中设置了多个条件变量。每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程,对条件变量只能进行两种操作,即P和V。
2.3.3 经典同步问题
生产者-消费者问题
问题描述:一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。
解决方案:
semaphore mutex = 1;
semaphore empty = n;
semaphore full = 0;
producer() {
while(1) {
P(empty);
P(mutex);
// put into buffer
V(mutex);
V(full);
}
}
consumer() {
while(1) {
P(full);
P(mutex);
// remove frome buffer
V(empty);
V(mutex);
}
}
读者-写者问题
问题描述:有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若写进程和其他进程同时访问共享数据时则可能导致数据不一致的错误。因此要求
- 允许多个读者可以同时对文件执行读操作。
- 只允许一个写者往文件中写消息。
- 任一写者在完成写操作之前不允许其他读者或写者工作。
- 写者执行操作前,所有读者和写者应该退出。
解决方案:
int count = 0;
semaphore mutex = 1;
semaphore rw = 1;
reader() {
P(mutex);
if(count == 0) P(rw);
++count;
V(mutex);
// 读操作
P(mutex);
--count;
if(count == 0) V(rw);
V(mutex);
}
writer() {
P(rw);
// 写操作
V(rw);
}
写者公平方案:
int count = 0;
semaphore mutex = 1;
semaphore rw = 1;
semaphore w = 1;
reader() {
P(W);
P(mutex);
if(count == 0) P(rw);
++count;
V(mutex);
V(W);
// 读操作
P(mutex);
--count;
if(count == 0) V(rw);
V(mutex);
}
writer() {
P(W);
P(rw);
// 写操作
V(rw);
V(W);
}
哲学家进餐问题
问题描述:一张圆桌边上坐着5名哲学家,每两名哲学家的中间摆一个筷子,两根筷子之间是一碗米饭。哲学家们倾注毕生尽力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左右筷子。若筷子在他人手上,则必须等待。哲学家需要同时拿到两根筷子才可以进餐,吃完后,哲学家放下筷子继续思考。
解决方案:
semaphore mutex = 1;
semaphore chopsticks[5] = {1,1,1,1,1};
pi() {
while(1){
// 思考
P(mutex);
P(chopsticks[i]);
P(chopsticks[(i+1)%5]);
V(mutex);
// 吃饭
V(chopsticks[i]);
V(chopsticks[(i+1)%5]);
}
}
2.4 死锁
2.4.1 死锁的概念
死锁是指多个进程因竞争资源而造成的一种僵局,若无外力作用,这些进程会因互相等待而无法向前推进。
2.4.2 死锁产生的必要条件
产生死锁必须同时满足4个必要条件,只要其中任意一个条件不满足,死锁就不会产生。
- 互斥条件。一段时间内某个资源只能被一个进程所占有,若有其他进程请求该资源,则请求进程必须等待。
- 不剥夺条件。在进程被分配的资源未被使用完时,不能被其他进程强行夺走,只能主动释放。
- 请求并保持条件。进程已经保持了一个资源,但又提出了新的资源请求,而该资源被其他进程占有,此时请求的进程被阻塞,但对自己保持的资源不释放。
- 循环等待条件。存在一种资源循环等待链。但并不是说存在资源循环等待链就一定会造成死锁。
2.4.3 死锁的处理策略
死锁的处理策略有3个,死锁预防、死锁避免和死锁检测。
- 死锁预防。设置某些限制条件,破坏产生死锁的4个必要条件之一或几个。
- 死锁避免。在资源动态分配的过程中,用某种方法防止系统进入不安全状态,从而避免死锁。
- 死锁检测与解锁。允许进程在运行过程中发生死锁,但要求系统的检测结构及时地检测出死锁的发生,然后采取死锁解除措施。
2.4.4 死锁预防
死锁预防是从破坏4个必要条件之一入手。
- 破坏互斥条件。允许系统资源都能共享使用。
- 破坏不剥夺条件。当一个保持了某些不可剥夺资源的进程请求新的资源而得不到满足时,它必须释放已经保持的资源,待以后需要时再重新申请。
- 破坏请求并保持条件。进程再运行之前必须申请到所有资源,否则不投入运行。
- 破坏循环等待条件。采用顺序资源分配法。首先给系统的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源一次申请完。
2.4.5 死锁避免
死锁避免主要采用银行家算法,其主要思路如下。
进程再运行之前需要声明其资源最大需求量,在进程需要分配资源的某时刻,按以下步骤进行操作。
- 若进程请求数量资源大于现有资源数量,或者占用资源数量加上请求资源数量大于其声明的最大需求量时,拒绝分配。
- 在上面条件不满足的条件下,将资源尝试地分配给该进程。
- 执行安全性算法,查看是否可以找到一个安全序列,即能够找到某种资源推进顺序,使得每个进程在执行时的资源请求都能被满足。
- 若安全序列存在,则分配资源给进程。否则,拒绝分配。
2.4.6 死锁检测与解除
系统死锁可以使用资源分配图描述,其检测方法是不断去除图中资源可满足进程的边,直到无法去除为止。若最终得到的图中含有边,则存在死锁。
发现死锁后可以通过如下方法解决。
- 资源剥夺法。挂起某些死锁进程,抢占其资源,并将其分配给其他的死锁进程。
- 撤销进程法。强制撤销死锁进程并剥夺其资源。
- 进程回退法。让进程回退到恰好可以避免死锁的情况,并释放部分资源。