进程与线程
【考点笔记1】进程与线程的比较
进程 | 线程 | |
---|---|---|
映像组成 | 由程序段、相关数据段和PCB组成 | 共享其隶属进程的进程映像,仅拥有线程ID、寄存器集合和堆栈等 |
并发性 | 没有引入线程的系统中,进程是独立运行的基本单位 | 线程是独立运行的基本单位,一个进程可以拥有一个或多个线程 |
资源分配 | 进程是资源分配和拥有的基本单位 | 线程自己基本不拥有系统资源,但它可以访问所属进程所拥有的的全部资源 |
调度 | 在没有引入线程的操作系统中,进程是独立调度和分派的基本单位 | 引入线程后的操作系统中,线程是独立调度和分派的基本单位 |
通信 | PV操作;共享存储; 消息传递;管道通信 | 同一进程的各线程直接读写进程数据段。 不同进程的线程之间通信属于进程间通信。 |
系统开销 | 进程切换时涉及当前CPU环境的保存以及新进程CPU环境的设置,系统开销大 | 线程切换时只需保存和设置少量寄存器内容,开销很小 |
地址空间 | 进程的地址空间之间互相独立 | 同一进程的各线程间共享进程的地址空间 |
进程状态与进程控制
【考点笔记2】进程状态转换
【考点笔记3】进程的阻塞与唤醒
进程阻塞 | 进程唤醒 | |
---|---|---|
引发事件 | 请求系统服务。 启动某种操作。 新数据尚未到达 无新工作可做 | 引发阻塞的事件完成 |
特点 | 阻塞:运行状态->阻塞状态 进程通过调用阻塞原语block把自己阻塞。进程的阻塞是进程自身的一种主动行为。 | 唤醒:阻塞状态->就绪状态 由相关事件的进程调用weakup原语将阻塞进程唤醒,是被动完成的。 |
进程阻塞的引发事件可总结为:需要等待某个事件的发生时引发阻塞
进程唤醒的引发事件可总结为:等待的事件发生时唤醒进程
处理机调度
【考点笔记4】调度的原则
指标 | 定义 | 原则 |
---|---|---|
CPU利用率 | CPU的工作时间在整个系统工作时间中所占的比例 | 高 |
系统吞吐量 | 系统吞吐量表示单位时间内CPU完成作业的数量 | 高 |
周转时间 | 周转时间=作业完成时间-作业提交时间 | 短 |
平均周转时间 | 平均周转时间=(作业1周转时间+···+作业n周转时间)/n | 短 |
带权周转时间 | 带权周转时间=作业周转时间/作业实际运行时间 | 小 |
平均带权周转时间 | 平均带权周转时间=(作业1的带权周转时间+···)/n | 小 |
等待时间 | 进程处于等待处理机状态的时间之和 | 短 |
响应时间 | 从用户提交请求到系统首次产生响应所用的时间 | 短 |
【考点笔记5】调度的基本概念
调度从作业提交到完成可以分为:作业调度、中级调度、进程调度三个层次:
【考点笔记6】进程调度的优先级
根据进程创建后其优先级是否可以改变,可以将进程优先级分为以下两种:
1)静态优先级。在进程投入运行前就确定了一个优先级,并且之后一直不变。
2)动态优先级。在进程运行过程中,根据进程情况的变化动态的调整优先级。
通常(静态优先级):
系统进程优先级 高于 用户进程;
前台进程优先级 高于 后台进程;
I/O型进程(或称I/O繁忙型进程)优先级 高于 计算型进程(CPU繁忙行进程)
【考点笔记7】典型的调度算法
算法 |
|
|
---|---|---|
先来先服务调度算法 | 作业:每次从后背作业队列中选择最先进入该队列的作业调入内存。 进程:每次从就绪队列中选择最先进入该队列的进程,将处理器分配给它,使之投入运行。 | 1.算法简单,但效率低; 2.有利于长作业,不利于短作业; 3.有利于CPU繁忙型作业而不利于I/O繁忙型作业。 |
短作业/进程优先调度算法 | 作业:每次从后背队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存。 进程:每次从就绪队列中选择一个估计运行时间最短的进程,将处理器分配给它,使之投入运行。 | 1.对长作业不利; 2.估计运行时间可能不准确; 3.平均等待时间、平均周转时间最少 |
高响应比优先调度算法 | 先计算后备队列中每个作业的响应比,从中选出响应比最高的作业投入内存。 响 应 比 R P = 等 待 时 间 + 要 求 服 务 时 间 要 求 服 务 时 间 响应比R_P=\frac{等待时间+要求服务时间}{要求服务时间} 响应比RP=要求服务时间等待时间+要求服务时间 综合考虑了作业的等待时间和估计运行时间 | 1.有利于短作业; 2.长作业不至于产生饥饿; 3.等待时间越长,优先级越高 |
优先级调度算法 | 作业:每次从后备队列中选择优先级最高的一个或几个作业,将它们调入内存。 进程:每次从就绪队列中选择优先级最高的进程,将处理器分配给它,使之投入运行。 | 优先级高的先运行。优先级分为静态优先级:创建时确定且不变。动态优先级:创建时确定并动态改变。 |
时间片轮转调度算法 | 所有就绪进程按照先来先服务排队,总是选择就绪队列中第一个进程运行一个时间片。然后释放处理器给下一个就绪进程,而被剥夺处理器的进程返回到就绪队列的末尾重新排队。 | 所有用户在给定时间内得到响应。时间片大:系统开销小但交互性差。时间片小:有利于短作业,但系统开销大。 |
多级反馈队列调度算法(适用于分时系统进程调度) | 1.设置多个就绪队列,优先级越高的队列进程的运行时间片就越小。优先级按第1~n级队列递减。 2.当一个新进程进入内存后,首先将它放入第1级队列的末尾,按FCFS原则排队等待调度。 当轮到位于第i级队列的某一进程执行时,若它在该时间片内完成,便可撤离系统,否则 **第1~(n-1)级队列:将该进程转入下一级队列的末尾,等待下次调度运行。 **第n级队列:采用时间片轮转的方式运行。 3.仅当第1~(i-1)级队列均为空时,才会调度第i级队列中的进程运行。 4.第i级队列中的进程运行时,若又有新进程进入第1~(i-1)中的任一队列,则把正在运行的进程放回第i级队列的末尾,把处理器分配给新进程。 | 1.终端型作业用户:短作业优先 2.短批处理作业用户:周转时间较短 3.长批处理作业用户:不会长期得不到处理 |
进程同步与互斥
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:
原则 |
|
---|---|
空闲让进 | 临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区 |
忙则等待 | 当已有进程进入临界区时,其他试图进入临界区的进程必须等待 |
有限等待 | 对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿) |
让权等待 | 当进程不能进入临界区时,应立即释放处理机,防止进程忙等 |
互斥的四种实现方式——单标志法、双标志先检查法、双标志后检查法、Peterson算法。
【考点笔记8】临界区互斥的软件实现方法
对两个进程P0、P1互斥访问临界区的软件实现方法:
单标志法
特点:
- 确保两个进程互斥访问临界区。
- 两个进程必须交替进入临界区。
- 违反“空闲让进”原则
设置一个公共整型变量turn,用于指示被允许进入临界区的进程编号,即若turn==0,则允许P0进程进入临界区
P0进程:
while(turn!=0); //进入区--检查自己是否拥有访问临界区的权限
critical section; //临界区--将临界区的访问权让给另一个进程
turn = 1; //退出区
remainder section; //剩余区
P1进程:
while(turn!=1); //进入区--检查自己是否拥有访问临界区的权限
critical section; //临界区--将临界区的访问权让给另一个进程
turn = 0; //退出区
remainder section; //剩余区
双标志先检查法
特点:
- 进程不必交替进入,可连续使用
- 两进程可能同时进入临界区,无法实现互斥访问,违背“忙则等待”
设置了一个数组flag[2],表示进程是否进入临界区。若flag[i]值为FALSE,表示Pi进程未进入临界区;值为TRUE,表示Pi进程进入临界区(i=0或1)。
补充说明:flag[i]表示Pi进程想要进入临界区的意愿。初始时flag[0]=flase,flag[1]=false
P0进程:
while(flag[1]); //进入区--检查对方是否想进入临界区,如果对方想进入,自己就循环等待
flag[0]=TRUE; //进入区--上锁
critical section; //临界区
flag[0]=FALSE; //退出区
remainder section; //剩余区
P1进程:
while(flag[0]); //进入区--检查对方是否想进入临界区,如果对方想进入,自己就循环等待
flag[0]=TRUE; //进入区--上锁
critical section; //临界区
flag[0]=FALSE; //退出区
remainder section; //剩余区
双标志后检查法
特点:
- 确保进程互斥访问临界区
- 存在两个进程都进不了临界区的“饥饿”现象,违背“空闲让进”、“有限等待”
设置flag[2],采用先设置自己标志后,再检测对应状态标志,若对方标志位为TRUE,则进程等待;否则进入临界区。
P0进程:
flag[0]=TRUE; //进入区--上锁
while(flag[1]); //进入区--检查
critical section; //临界区
flag[0]=FALSE; //退出区
remainder section; //剩余区
P1进程:
flag[1]=TRUE; //进入区--上锁
while(flag[0]); //进入区--检查
critical section; //临界区
flag[1]=FALSE; //退出区
remainder section; //剩余区
皮特森算法
特点:
- 确保进程互斥访问临界区
- 不会出现“饥饿”现象
- 不满足“让权等待”原则,暂时不能进入临界区的进程还会占用处理机
(在while循环处一直轮询,看是否可以进入临界区)
设置变量turn(表示一种“谦让”,优先让哪个进程进入临界区)以指示不允许进入临界区的进程编号,每个进程在先设置自己的flag标志后再设置turn标志,不允许另一个进程进入。这时,再同时检测另一个进程flag标志和不允许进入标志turn。若对方flag标志位为TRUE且turn显示不允许对方进入临界区,则进程进入临界区;否则等待。
P0进程:
flag[0]=TRUE; //进入区--表明自己想进入临界区的意愿
turn =1; //进入区--表示可以优先让对方进入
while(flag[1]&&turn==1);//进入区--若对方也想进入,且最后使自己做出了“谦让动作”,则自己等待
critical section;
flag[0]=FALSE;
remainder section;
P0进程:
flag[0]=TRUE; //进入区--表明自己想进入临界区的意愿
turn =1; //进入区--表示可以优先让对方进入
while(flag[1]&&turn==1);//进入区--若对方也想进入,且最后使自己做出了“谦让动作”,则自己等待
critical section;
flag[0]=FALSE;
remainder section;
【考点笔记9】信号量机制
整型信号量: 一个用于表示资源个数的整型量S。当进程发现S<=0时,就会不断测试。因此进程处于忙等状态,未遵循“让权等待”原则。
记录型信号量 :遵循了让权“让权等待”原则,其数据结构描述如下。
typedef struct{
int value; //代表资源数目
struct process *L; //链接等待资源的进程
}semaphore;
信号量的P、V操作的实质是“加减”操作,P、V操作又是原语操作,所以,P操作是对信号量(只能对信号量,不能对普通变量)进行减“1”操作,然后判断是否小于零,“是”则阻塞等待,“否”则什么都不做。而V操作是对信号量加“1”操作,然后判断是否小于等于零,“是”则唤醒进程,“否”则什么也不做。
记录型信号量S中value值的意义:
- S.value的初值表示系统中某类资源的总数
- S.value<0表示当前系统中已经没有可用的该类资源
- S.value<0时,其绝对值表示等待该资源而阻塞的进程的个数
生产者-消费者问题
生产者、消费者共享一个初始为空、大小为n的缓冲区。
只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。
只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。
缓冲区是临街资源,各进程必须互斥地访问。
semaphore mutex = 1;//互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n;//同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲区的数量
producer(){
while(1){
生产一个产品;
P(empty); //消耗一个空缓冲区
P(mutex);
把产品放入缓冲区;
V(mutex);
V(full); //增加一个产品
}
}
consumer(){
while(1){
P(full); //消耗一个产品(非空缓冲区)
P(mutex);
从缓冲区中取出一个产品;
V(mutex);
V(empty); //增加一个空闲缓冲区
}
}
注: 实现互斥的P操作一定要在实现同步的P操作之后,否则可能会发生死锁。
读者-写者问题
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时,则可能导致数据不一致的错误。因此要求:
1.允许多个读者可以同时对文件执行读操作
2.只允许一个写者往文件中写信息
3.任一写者在完成写操作之前不允许其他读者或写者工作
4.写者执行写操作前,应让已有的读者和写者完全退出
semaphore rw=1;//用于实现对文件的互斥访问,表示当前是否有进程在访问共享文件
int count=0;//记录当前有几个人读进程在访问文件
semaphore mutex=1;//用于保证对count变量的互斥访问
writer(){
while(1){
P(rw); //写之前“加锁”
写文件...
V(rw); //写之前“解锁”
}
}
reader(){
while(1){
P(mutext); //各读进程互斥访问count
if(count==0)
P(rw); //第一个读进程负责“加锁”
count++; //访问文件的读进程数+1
V(mutex);
读文件...
P(mutex); //各读进程互斥访问count
count--; //访问文件的读进程数-1
if(count==0)
V(rw); //最后一个读进程负责“解锁”
V(mutex);
}
}
**潜在的问题:只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”,因此,这种算法中,读进程是优先的。
实现相对公平的 读者-写者问题
写者不会饥饿,是相对公平的 先来先服务 原则。也叫“读写公平法”
//互斥信号量初值为1
semaphore rw=1;//用于实现对文件的互斥访问
int count = 0;//记录当前有几个读进程在访问文件
semaphore mutex=1;//用于保证对count变量的互斥访问
semaphore w = 1;//用于实现“写优先”
writer(){
while(1){
P(w);
P(rw);
写文件...
V(rw);
V(w);
}
}
reader(){
while(1){
P(w);
P(mutex);
if(count==0)
P(rw);
count++;
V(mutex);
V(w);
读文件...
P(mutex);
count--;
if(count==0)
V(rw);
V(mutex);
}
}
读者-写者问题总结:
其核心思想在于设置了一个计数器count,用来记录当前正在访问的共享文件的读进程数。我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。
另外,对于count变量的检查和复制不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
最后,还要认真体会我们是如何解决“写进程饥饿”问题的
哲学家进餐问题
一张圆桌上坐着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];
思考...
}
}
1.mutex保证了同一时刻最多只有一个哲学家正在“拿筷子”。也就是说,最多只有一个哲学家处于“请求和保持”的状态(占有了一支筷子的同时申请另一支筷子)。别的哲学家要么手里没有筷子,要么有两支筷子。手里有两只筷子的哲学家一定可以顺利运行结束,并释放筷子,唤醒处于“请求和保持”状态的哲学家。
2.哲学家进餐问题的关键在于解决进程死锁。
这些进程知己恩只存在互斥关系,但与之前的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。
解决哲学家进餐问题死锁的方案:(方法2为代码所描述的方法)
1.最多只允许四个哲学家同时进餐
2.仅当一个哲学家左右两边的筷子同时可用时,他才可以拿起筷子
3.将哲学家编号,要求奇数号的哲学家先拿左边筷子,偶数号哲学家先拿右边筷子
死锁
【考点笔记10】死锁条件
产生死锁必须同时满足一下四个条件,只要其中任一个条件不成立,死锁就不会发生。
条件 | 解释 |
---|---|
互斥条件 | 在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求资源,则请求进程只能等待 |
不剥夺条件 | 进程获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放 |
请求和保持条件 | 进程已经保持了至少一种资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己获得的资源保持不放 |
循环等待条件 | 存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求 |
1.预防死锁:破坏思索的四个必要条件中的一个或几个
2.避免死锁:用某种方式防止系统进入不安全状态,从而避免死锁(银行家算法)
3.死锁的检测和解除:允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后才去某种措施解除死锁。(资源分配图)
【考点笔记11】资源分配图
顶点结合可以分为两部分:
P=(P1,P2,···,Pn)系统内所有进程组成的集合
R=(r1,r2,···,rn) 系统内所以资源组成的集合
<Pi,ri>: Pi指向ri - -> Pi请求一个ri资源
<ri,Pi>: ri指向Pi - ->ri类资源中的一个资源已分配给进程Pi
当前状态如下:
- 进程P1已占用两个r1资源,而且正在申请获得一个r2资源。
- 进程P2已占用一个r2资源和一个r2资源,而且正在申请获得一个r1资源
资源分配图的简化
- 在分配图中,找出一个既不阻塞又非孤立的进程节点Pi(即从进程集合中找到一个存在连接的边,且资源申请数量小于系统中已有空闲资源数量的进程)。因进程Pi获得了所需要的全部资源,它能继续运行直到完成,然后释放其占有的所有资源(这相当于消去Pi的所有申请边与分配边,使之成为孤立的结点)
- 进程Pi释放资源后,可以唤醒因等待这些资源而阻塞的进程,原来阻塞的进程可能变为非阻塞进程,根据第一步中的简化方法,消去申请边和分配边。
- 重复前两步的简化过程后,若能校区图中所有边,使所有进程成为孤立结点,则称图式可完全简化的;若遇到任何过程均不能使该图完全简化,则称该图是不可完全简化的。
可以证明的是:不同简化顺序将得到相同的不可简化图。系统状态S为死锁状态的条件是:当且仅当S状态的资源分配图是不可完全简化的,该定理称为***死锁定理***。
【考点笔记12】银行家算法
死锁的避免