Part1 链接:笔记_408_操作系统_02. 进程与线程(Part1)
文章目录
02. 进程与线程
2.3 同步和互斥
2.3.1 同步和互斥的基本概念
异步性:各并发执行的进程以各自独立的、不可预知的速度向前推进
同步:直接制约关系 互斥:间接制约关系
原则 → 前三个必须实现,最后一个不一定,如 Pearson
2.3.2 实现临界区互斥的基本方法
1. 软件实现方法
-
单标志法
-
算法:设置一个公用整型变量
turn
(谦让),用于指示被允许进入临界区的进程编号每个进程进入临界区的权限只能被另一个进程赋予
// P0进程 // P1进程 while(turn != 0); while(turn != 1); // 进入区 critical section; critical section; // 临界区 turn = 1; turn = 0; // 退出区 remainder section; remainder section; // 剩余区
-
优点:可以实现同一时刻最多只允许一个进程访问临界区
缺点:违背**“空闲让进”**原则(最大的问题)
-
-
双标志先检查法
-
算法:设置一个布尔型数组
flag[]
,数组中各个元素用来标记各进程想进入临界区的意愿// Pi进程 // Pj进程 while(flag[j]); while(flag[i]); // 进入区 flag[i] = true; flag[j] = true; // 进入区 critical section; critical section; // 临界区 flag[i] = false; flag[j] = false; // 退出区 remainder section; remainder section; // 剩余区
-
优点:不用交替进入,可连续使用
缺点:违背**“忙则等待”**原则(检查和上锁两个处理不是一气呵成的)
-
-
双标志后检查法
-
算法:
// Pi进程 // Pj进程 flag[i] = true; flag[j] = true; // 进入区 while(flag[j]); while(flag[i]); // 进入区 critical section; critical section; // 临界区 flag[i] = false; flag[j] = false; // 退出区 remainder section; remainder section; // 剩余区
-
优点:解决了“忙则等待”的问题
缺点:违背了**“空闲让进”和“有限等待”原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象**
-
-
Peterson算法
-
算法
// Pi进程 // Pj进程 flag[i] = true; flag[j] = true; // 进入区 turn = j; turn = i; // 进入区 while(flag[j] && turn = j); while(flag[i] && turn = i); // 进入区 critical section; critical section; // 临界区 flag[i] = false; flag[j] = false; // 退出区 remainder section; remainder section; // 剩余区
-
优点:用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,不会发生饥饿
缺点:违背了**“让权等待”**原则
-
2. 硬件实现方法
- 优点
- 适用于任意数目的进程,不管是单处理机还是多处理机
- 简单、容易验证其正确性
- 可以支持进程内有多个临界区(只需为每个临界区设置一个
bool
变量)
- 缺点
- 不能实现
让权等待
- 可能导致饥饿现象
- 不能实现
-
关中断指令:只对执行关中断指令的处理机有作用
-
TS 指令 / TSL指令
// bool共享变量 lock 表示当前临界区是否被加锁 // true 表示已加锁,false 表示未加锁 bool TestAndSet(bool *lock){ bool old; old = *lock; // old 用来存放 lock 原来的值 *lock = true; // 无论之前是否已上锁,都将 lock 设为 true return old; // 返回 lock 原来的值 } // 以下时使用 TSL 指令实现的互斥算法逻辑 while(TestAndSet(&lock)); // “上锁” 并 “检查” 临界区代码段··· lock = false; // “解锁” 剩余区代码段···
-
Swap 指令 / Exchange 指令 / XCHG 指令
Swap(bool *a,bool *b){ bool temp; temp = *a; *a = *b; *b = temp; } // 以下是用 swap 指令实现互斥的算法逻辑 bool old = true; while(old == true) swap (&lock, &old); 临界区代码段··· lock = false; 剩余区代码段···
2.3.3 互斥锁
acquire() {
while(!available); // 忙等
available = false; // 获得锁,available表示锁是否可用
}
release() {
available = true; // 释放锁
}
-
acquire()
或release()
必须是原子操作,所以互斥锁通常用硬件机制来实现 -
需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如 TSL指令、swap指令、单标志法
-
特性
-
需忙等,进程时间片用完才下处理机,违反**“让权等待”**
-
优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低
-
常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区
不太适用于单处理机系统,忙等的过程中不可能解锁
-
2.3.4 信号量
1. 信号量机制
信号量机制只能被
wait(S)
/ P操作和signal(S)
/ V操作访问
一个信号量对应一种资源,信号量的值 = 这种资源的剩余数量
整型信号量
int S = 1; // 初始化整型信号量S,表示当前系统中某种资源的数量
void wait(int S){ // wait()原语,相当于进入区
while(S <= 0); // 如果资源不够,就一直循环等待
S = S - 1; // 如果资源够,就占有一个资源
}
void signal(int S){ // signal()原语,相当于退出区
S = S + 1;
}
记录型信号量
// 记录型信号量的定义
typedef struct{
int value; // 剩余资源数
struct process *L; // 等待队列
}semaphore;
void wait(semaphore S){ // 某进程需要使用资源时,用wait原语申请
S.value--;
if(S.value < 0){ // <0 表示该资源已经分配完毕
add this process to S.L;
block(S.L); // 阻塞当前进程,挂到信号量S的等待队列(阻塞队列)
}
}
void signal(semaphore S){ // 进程使用完资源后,用signal原语释放
S.value++;
if(S.value <= 0){ // <=0 表示仍然有进程在等待该资源
remove a process P from S.L;
wakeup(S.L);
}
}
2. 用信号量实现互斥、同步、前驱关系
对不同的临界资源要设置不同的互斥信号量
除了互斥、同步问题外,还会考察有多个资源的问题,有多少资源就把信号量初值设为多少。申请资源时进行P操作,释放资源时进行 V 操作即可
2.3.6 经典同步问题
PV 操作题目的解题思路:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值
- 互斥信号量初值一般为1
- 同步信号量的初始值要看对应资源的初始值是多少
1. 生产者 - 消费者问题
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示非空闲缓冲区的数量
producer(){
while(1){
生产一个产品; // 理论上可以放入临界区
P(empty); // 实现互斥的P操作一定要在实现同步的P操作之后,否则可能导致死锁
P(mutex); // 互斥夹紧
将产品放入缓冲区;
V(mutex); // 两个V操作无先后顺序
V(full);
}
}
consumer(){
while(1){
P(full);
P(mutex);
从缓冲区取走一个产品;
V(mutex);
V(empty);
使用产品; // 理论上可以放入临界区
}
}
2. 多生产者 - 多消费者问题
- 在生产者-消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析
- 如果缓冲区大小大于1,就必须专门设置一个互斥信号量 mutex 来保证互斥访问缓冲区
3. 吸烟者问题 / 可生产多种产品的单生产者 - 多消费者
- 轮流的实现:整型变量
i
- 思考:如果“每次随机让一个吸烟者吸烟”?
4. 读者 - 写者问题
-
两类进程:写进程、读进程
互斥关系:写进程—写进程、写进程—读进程
- 要点
- 核心思想:设置了一个计数器
count
用来记录当前正在访问共享文件的读进程数 - “一气呵成” —— 互斥信号量
- 如何解决“写进程饥饿”问题的
- 绝大多数的考研PV操作大题都可以用之前介绍的几种生产者-消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决
- 核心思想:设置了一个计数器
5. 哲学家进餐问题
-
法一:至多允许 4 名哲学家同时进餐
semaphore chopstick[5] = {1,1,1,1,1}; semaphore max = 4; Pi(){ while(1){ P(max); P(chopstick[i]); // 拿左 P(chopstick[(i+1)%5]); // 拿右 V(max); 吃饭··· V(chopstick[i]); // 放左 V(chopstick[(i+1)%5]); // 放右 思考··· } }
-
法二:奇数号先拿左再拿右,偶数号先拿右再拿左
-
法三:各互斥的哲学家拿筷子
semaphore chopstick[5] = {1,1,1,1,1}; semaphore mutex = 1; Pi(){ while(1){ P(mutex); P(chopstick[i]); // 拿左 P(chopstick[(i+1)%5]); // 拿右 V(mutex); 吃饭··· V(chopstick[i]); // 放左 V(chopstick[(i+1)%5]); // 放右 思考··· } }
-
与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。关键在于解决进程死锁
-
哲学家问题关键点是限制并行,主要是三种思路
-
限制申请资源的顺序(不通用,不建议适用)
如:规定单号哲学家先取左筷子,双号先取右筷子
-
限制并发进程数(通用,但是并发度不高,不建议适用)
如:规定同一时间只能有一个哲学家就餐(禁止并行)
-
让进程一口气取得所有资源,再开始运行(通用,且并发度高,建议使用)
如:哲学家只有能有取得两个筷子的时候才会就餐
-
2.3.5 管程
引入管程的目的:解决临界区分散带来的管理和控制问题,无非是更方便地实现互斥和同步
管程的组成:+ 管程的名字
- 管程的基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据
- 每次仅允许一个进程在管程内执行某个内部过程
- 管程是语法范围,无法创建和撤销
- 条件变量
- 将阻塞原因定义为条件变量
- 每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程
- 对条件变量只能执行两种操作,即
wait
和signal
- 条件变量 vs 信号量
- 相似:条件变量的
wait
和signal
类似于信号量的 P/V 操作(但不同),可以实现进程的阻塞和唤醒 - 不同:
- 条件变量没有“值”,仅实现“排队等待”的功能(剩余资源数用共享数据结构记录)
- 信号量是“有值”的,反应了剩余资源数
- 相似:条件变量的
2.4 死锁
2.4.1 死锁的概念
检测与恢复、避免、预防:每种方法对死锁的处理从宽到严,同时系统并发性由大到小
-
死锁产生的原因:对不可剥夺资源的不合理分配
- 空间上:对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
- 时间上:进程推进顺序非法
- 信号量的使用不当也会造成死锁
-
发生死锁时一定有循环等待,但是发生循环等待时未必死锁
-
资源分配图含圈不一定死锁的原因:同类资源数大于1
-
如果系统中每类资源都只有一个,那循环等待(资源分配图含圈)就是死锁的充分必要条件了
-
-
预防死锁和避免死锁都属于事先预防策略
2.4.2 死锁预防
- 破坏不剥夺条件
- 常用于状态易于保存和恢复的资源,如 CPU的寄存器和内存资源
- 一般不能用于打印机之类的资源
- 破坏请求和保持条件:预先静态资源分配法
- 破坏循环等待条件:顺序资源分配法
2.4.3 死锁避免
-
安全序列:指如果系统按照这种序列分配资源,则每个进程都能顺利完成
-
只要能找出一个安全序列,系统就是安全状态(安全序列可能有多个)
-
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去
如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况
-
-
如果系统处于安全状态,就一定不会发生死锁
如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
银行家算法
-
数据结构:
长度为 m 的一维数组 Available // 表示还有多少可用资源 n*m 矩阵 Max // 表示各进程对资源的最大需求数 n*m 矩阵 Allocation // 表示已经给各进程分配了多少资源 Max – Allocation = Need 矩阵 // 表示各进程最多还需要多少资源 用长度为 m 的一位数组 Request // 表示进程此次申请的各种资源数
-
银行家算法步骤:
- 检查此次申请是否超过了之前声明的最大需求数
- 检查此时系统剩余的可用资源是否还能满足这次请求
- 试探着分配,更改各数据结构
- 用安全性算法检查此次分配是否会导致系统进入不安全状态
-
安全性算法步骤:
检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。
不断重复上述过程,看最终是否能让所有进程都加入安全序列。
导致不安全状态的请求得到满足后,则此刻系统并未立即进入死锁状态,因为这时所有的进程未提出新的资源申请,全部进程均未因资源请求没有得到满足而进入阻塞态。只有当进程提出资源申请且全部进程都进入阻塞态时,系统才处于死锁状态。
2.4.4 死锁检测与解除
-
检测死锁的算法:
- 在资源分配图中,找出既不阻塞又不是孤点的进程 P i P_i Pi,消去它所有的请求边和分配边,使之成为孤立的结点。
- 进程 P i P_i Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。根据 1. 中的方法进行一系列简化后,若能消去途中所有的边,则称该图是可完全简化的。
如果最终不能消除所有边,那么此时就是发生了死锁;
并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,最终还连着边的那些进程就是处于死锁状态的进程
死锁的解除
-
一旦检测出死锁的发生,就应该立即解除死锁
-
主要方法
-
资源剥夺法
-
挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程
【注意】解除死锁不采用:从非死锁进程处抢夺资源
-
但是应防止被挂起的进程长时间得不到资源而饥饿。
-
-
撤销进程法(或称终止进程法)
- 强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。
- 这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
-
进程回退法
让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
-