文章目录
一 、CPU调度
在一堆就绪队列中,选择哪个进程执行。如何设计调度算法呢?对于用户来说等待时间越短越好。要让进程满意,做到以下三点:
- 周转时间短(从任务进入到任务结束),尽快结束任务
- 响应时间快(从操作发生到响应):用户操作尽快响应
- 吞吐量大(完成的任务量):系统内耗的时间少
调度算法总原则:系统专注于任务执行,又能合理调配任务。需要折中,需要综合。
不同类型的任务有其特点和关注点。任务按照主体不同,分为以下两类:
- 前台任务和后台任务
关注点不同,前台关注响应时间,后台关注周转时间。 - IO约束型任务和CPU约束型任务,IO约束型任务大多都是前台任务
一个矛盾点:吞吐量和响应时间之间存在矛盾。
要考虑各个任务的特点,折中和综合让操作系统调度变得很复杂,下面介绍常见的CPU调度算法
1、FCFS(First Come,First Served,先来先服务)——最公平
2、SJF(短作业优先)—— 平均周转时间最短
- 最小化等待时间:SJF算法选择执行时间最短的作业,这意味着较短的作业将更早地得到执行。因此,对于长时间的作业,其他作业需要等待的时间会相对较长,而短作业可以更快地完成。这样可以最小化作业的等待时间,从而减少整体的平均周转时间。
- 最大化CPU利用率:由于SJF算法选择执行时间最短的作业,CPU的利用率相对较高。较短的作业会更快地释放CPU资源,使其他作业有更多机会获得CPU时间。这样可以提高CPU的利用率,从而减少作业的等待时间和平均周转时间。
平均周转时间(Average Turnaround Time)是衡量作业或进程调度算法性能的指标之一,它表示从作业提交到作业完成所经过的平均时间。计算平均周转时间的方法如下:
1.对于每个作业或进程,计算其周转时间(Turnaround Time):周转时间 = 完成时间 - 提交时间(提交时间不等于开始时间,提交了不一定立马开始)
2.对所有作业或进程的周转时间进行求和。
3.平均周转时间 = 总的周转时间 / 作业或进程的数量.
3、RR(按时间片轮转调度)
时间片大:响应时间长;时间片小:吞吐量小,根据实际情况折衷设置
4、优先级调度
一个调度算法如何让多种类型的任务满意?一个直观的想法是定义前台任务和后台任务两队列,前台RR,后台SJF,只有前台任务没有时才调度后台任务。
存在的问题:
- 固定优先级可能会导致后台任务饥饿,前台任务优先级高于后台任务,前台一直有任务来,一直执行,导致后台任务无法执行。
- 优先级变化,后台任务优先级提高,后台任务往往是CPU很长的任务,比如编译很大的C文件,导致很长时间不释放CPU,导致前台任务响应时间短。
调度算法应该具有一定的学习能力
5、schedule()函数
schedule会根据counter切换进程,counter既承担了时间片的作用,又承担了优先级的作用,在以下两种情况修改counter:
- 每次进程调度,counter都会变化,假设counter=p,counter>>1+ p。
- 每次时钟中断,都会执行counter–。
counter的作用:
- 保证了响应时间的界,在响应时间不断变化的情况下。
- 经过IO以后,counter就会变大;IO时间越长,counter越大,照顾了IO进程,也照顾了前台进程。
- 后台进程一直按照counter轮转,近似SJF调度。
- 每个进程只用维护一个counter变量,简单又高效。
二、进程同步与信号量
进程合作:多个进程共同完成一个任务。每个进程的语句不能随便执行,有些语句需要等待一个信号才能执行。
假设现在有司机进程和售票员进程,这两个进程要共同合作完成车辆的运行,如果司机不理售票,门没关好就启动车辆,容易引发事故。司机要等售票员关好门后启动车辆,将门关闭比作信号,司机看到了信号,启动车辆。
让“进程走走停停”来保证多进程的合理有序。进程同步(Process Synchronization)是指协调和控制进程或线程之间的执行顺序,以保证它们正确地共享和操作共享资源。
在生产者-消费者问题中,不仅要知道缓冲区空闲的个数,还需要知道睡眠的进程数量。信号counter的含义不能反映出正在睡眠的进程数量。等待信号和发信号对应睡眠和唤醒,因此还需要另一个量来决定要不要发信号,用信号量来记录一些信息,根据这个信息决定是唤醒还是睡眠。
信号量的实现当信号量的值小于0时,表示有进程或线程正在等待资源,资源当前不可用。当资源变为可用时,等待的进程或线程将被唤醒并获得资源的访问权限。
用信号量解决生产者-消费者问题
semaphore full = 0;
semaphore empty =BUFFER_SIZE;
semaphore mutex = 1; // 互斥信号量,一次只有一个进程操作缓冲区
P函数表示睡眠,V函数表示唤醒
在C语言中,P函数的原型是:
#include<semaphore.h>
int sem_wait(sem_t *sem);
sem_wait函数的作用是将信号量的值减一(如果大于零),或者阻塞线程(如果信号量的值为零)。如果信号量的值大于零,则减一操作会立即返回,线程可以继续执行。如果信号量的值为零,则线程将进入阻塞状态,直到有其他线程增加信号量的值
在C语言中,V函数的原型是:
#include<semaphore.h>
int sem_post(sem_t *sem);
sem_post函数的作用是将信号量的值加一,但并不会阻塞当前线程的执行。如果有其他线程正在等待该信号量,则其中一个等待的线程会被唤醒,从而可以继续执行。
三、信号量临界区保护
信号量用来实现进程同步,但是当多个进程共同修改信号量时,会导致信号量语义发生错误,这种错误和调度顺序有关,所以要对信号量进行保护。
临界区:一次只允许一个进程进入的该进程的那一段代码。读写信号量的代码一定是临界区
临界区代码的保护原则
- 基本原则:互斥进入:如果一个进程在临界区中执行,则其他进程不允许进入。
- 这些进程间的约束关系称为互斥。
- 好的临界区保护原则
- 有空让进:若干进程要求进入空闲临界区时,应尽快使一些进程进入临界区。
- 有限等待:从进程发出进入请求到允许进入,不能无限等待。
1、Peterson算法
Peterson算法实现了两个进程之间的互斥操作,确保同一时间只有一个进程可以进入临界区执行操作,从而避免竞态条件的发生,结合了标记和轮转两种思想(turn负责轮换,flag负责做标记)。
Peterson算法代码示例
Peterson算法正确性
需要注意的是,Peterson算法是用于两个进程的情况,并且在某些特定的硬件和操作系统环境下可能存在问题。
2、面包店算法
用面包店算法,保护多个进程同步之间的信号量。面包店算法仍然是标记法加轮转法的结合,只是需给进程标号。
- 如何轮转:每个进程都获得一个序号,序号最小的进入。
- 如何标记:进程离开时序号为0,不为0的序号被标记。
面包店算法的正确性
3、关中断
cli(); #关中断
临界区
sti();#开中断
这种方式在多CPU情况下失效。
4、硬件原子指令
while(TestAndSet(&lock));
临界区
lock = false;
剩余区
TestAndSet(boolean &x)
{
boolean rv = x;
x = true;
return rv'
}
四、死锁处理
死锁是多线程或多进程并发程序中的一种常见问题,指的是两个或多个进程(或线程)互相等待对方释放资源,导致它们都无法继续执行的状态。
1、死锁成因
-
资源互斥使用,一旦占有别人无法使用。
-
进程占有了一些资源,又不释放,再去申请其他资源。
-
各自占有的资源和互相申请的资源形成环路等待
2、死锁的4个必要条件
- 互斥使用,资源的固有特性
- 不可抢占,资源只能自愿放弃
- 请求和保持,进程必须占有资源,再去申请
- 循环等待,在资源分配图中存在一个环路
3、死锁处理方法概述
- 死锁预防,破坏死锁出现的条件
- 死锁避免,检测到煤气超标时,自动切断电路(银行家算法)
- 死锁检测+恢复,检测死锁出现时,让一些进程回滚,让出资源
- 死锁忽略,就好像没有死锁样(一般用这个方法,死锁了就重启)
五、条件变量与信号量的区别
下图分别是使用信号量和条件变量实现生产者-消费者问题的生产者代码示例:
- 信号量是一个计数器,用于控制对共享资源的访问。它可以用于线程间的同步和互斥,通过对信号量的原子操作来实现。
- 条件变量是基于互斥锁(mutex)和等待队列的机制。它允许线程通过等待操作阻塞自己,并在条件满足时被唤醒。其他线程可以通过条件变量的信号(pthread_cond_signal)或广播(如pthread_cond_broadcast)唤醒等待的线程。被唤醒的线程会重新尝试获取互斥锁,继续执行。
自旋
自旋也是一种线程同步的技术,和条件变量类似,只是在自旋中,线程在等待某个条件满足时不会阻塞自己,二是通过忙等待的方式持续检查条件是否满足。