L14 CPU调度策略
FIFO:谁先进入,谁先调度
Priority:任务短可以适当优先调度
周转时间: 从开始申请执行任务,到执行任务完成
响应时间: 从开始申请执行任务到开始执行任务
相互影响和矛盾:响应时间小 ------> 切换次数多 ------> 系统内耗大 ------> 吞吐量小
调度需要折中和综合
FCFS:先来先服务 first come first serverd 公平
SJF:短作业优先,周转时间最小,但是响应时间长
RR: round robin 按照时间片轮转调度,保证响应时间,适用于前台程序,IO操作多的
- 时间片大:响应时间太长;时间片小:吞吐量小
直观想法:定义前台任务和后台任务,前台RR,后台SJF ,只有前台任务没有时才调度后台任务。
前台优先级高于后台优先级,若只按照优先级固定调度,可能会产生饥饿,某些任务调度不到
L15 schedule()分析
/*
* 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
* 环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
* 处理代码。
* 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
* 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
*/
void
schedule (void)
{
int i, next, c;
struct task_struct **p; // 任务结构指针的指针。
/* check alarm, wake up any interruptible tasks that have got a signal */
/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */
// 从任务数组中最后一个任务开始检测alarm。
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
{
// 如果任务的alarm 时间已经过期(alarm<jiffies),则在信号位图中置SIGALRM 信号,然后清alarm。
// jiffies 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h 第139 行。
if ((*p)->alarm && (*p)->alarm < jiffies)
{
(*p)->signal |= (1 << (SIGALRM - 1));
(*p)->alarm = 0;
}
// 如果信号位图中除被阻塞的信号外还有其它信号,并且任务处于可中断状态,则置任务为就绪状态。
// 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL 和SIGSTOP 不能被阻塞。
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state == TASK_INTERRUPTIBLE)
(*p)->state = TASK_RUNNING; //置为就绪(可执行)状态。
}
/* this is the scheduler proper: */
/* 这里是调度程序的主要部分 */
while (1)
{
c = -1;
next = 0;
i = NR_TASKS;//设置末尾,从后往前移动
p = &task[NR_TASKS];
// 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个就绪
// 状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,next 就
// 指向哪个的任务号。
while (--i)
{
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;//找到最大的counter时间片
}
// 如果比较得出有counter 值大于0 的结果,则退出124 行开始的循环,执行任务切换(141 行)。
if (c)
break;
// 否则就根据每个任务的优先权值,更新每一个任务的counter 值,然后回到125 行重新比较。
// counter 值的计算方式为counter = counter /2 + priority。[右边counter=0??]
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;//修改counter,恢复时间片
}
switch_to (next); // 切换到任务号为next 的任务,并运行之。
}
counter即承担着时间片作用,也承担着优先级的作用,counter代表的优先级可以动态调整,I/O正是前台进程的特征,前台进程优先,counter的操作((*p)->counter >> 1)保证了响应时间的界。
L16 进程同步与信号量
合理有序的推进多进程,共同完成一个任务
核心:阻塞,唤醒
生产者消费者模型:counter 反应缓冲区中空闲的个数,还需要知道有多少个进程睡眠(信号量)
P (semaphore s); //消费资源,睡眠 -1
V (semaphore s); //生产资源,唤醒 +1
L17 信号量临界区保护
临界区保护信号量,信号量实现进程间的同步
临界区:一次只允许一个进程进入
为什么需要临界区?
- 两个生产者因竞争资源会产生随机的执行顺序,造成信号量的修改不当,信号量语义错误,所以需要保护信号量。
临界区代码的保护原则:
- 互斥进入:如果一个进程在临界区中执行,则其他进程不允许进入(基本原则)
- 有空让进:若干进程要求进入空闲临界区时,应尽快使一个进程进入临界区
- 有限等待:从进程发出进入请求到允许进入,不能无限等待
软件方法:
1)轮换法 有空让进效果不好!!!
2)标记法 可能会造成无限制空转等待
3)非对称标记 结合了标记和轮转两种思想
两个进程:Peterson算法
多个进程:面包店算法
硬件方法:
1)单cpu实现临界区保护的方法:开关中断
cli = clear interuption ; //关中断
sti = set interuption; //开中断
原理是,在进入区阻止调度schedule的发生,而调度是因为中断,所以就是关闭中断
但在多CPU上不行,因为无法控制关闭所有cpu上的中断。
2)硬件原子指令
其实是用 mutex锁信号量 来保护信号量,为了解决mutex仍然需要保护的问题
使用硬件级原子操作,不能被打断不能切出去进行调度
L18 进入睡眠的方式
- 用 if 方式进入睡眠:
- 用 while 方式进入睡眠:
开关中断做信号量的保护,根据信号量的值是否睡眠,若不睡眠,加锁修改信号量
睡眠的实质,就是把自己写到阻塞队列上+改变自己的状态(也就是圈圈里的内容) ,然后再调用schedule切出去执行其他进程。
事实上,圈圈里的内容形成了一个睡眠队列:
tmp是局部变量,存储在当前进程的内核栈中,所以可以通过当前的task_struct找到内核栈中的tmp;tmp指向的是下一个task_struct,又可以通过下一个task_struct找到tmp,指向下下一个……由此形成队列。实际上把自己作为了队首,然后用tmp记录了队列里的下一个pcb,便于在唤醒的时候能够唤醒队列里的下一个进程。
利用中断唤醒wake_up阻塞队列,进程被唤醒后要从当时进入sleep的后面一句继续执行:
也就是if(tmp) tmp->state=0;
因为tmp指向睡眠队列中的下一个进程,所以会继续唤醒队列中下一个进程。
while的好处:
- while把所有阻塞进程一次性全部唤醒,让schedule来决定switch_to谁,这样可以不按照先来先后,可以按优先级。if的话只是将一个进程唤醒。
- 并且优先级最高的执行后,其他被唤醒的进程会继续上锁,不会一连串执行:
- 被唤醒后还要判断一遍while(),schedule切换到优先级最高的进程,执行bh->b_lock=1,而其他被唤醒的在进行while(bh->b_lock)判断时,就执行sleep_on了。
L19 死锁处理
死锁:多个进程由于互相等待对方持有的资源而造成谁都无法执行的情况
- 必要条件(怎么样造成死锁:形成了资源等待环路!!!)
- 死锁处理方法
-
死锁预防:破坏死锁出现的条件
一次性申请所有需要资源,不会占有资源后再去申请其他资源
对资源进行排序,按照顺序申请资源,不会出现环路等待
缺点:资源浪费,利用率低。 -
死锁避免 :检测每个资源请求,假装分配,看看进程组是否会造成死锁,如果造成死锁就拒绝如果找到了安全序列,就可以这样分配。
银行家算法
但是这样的算法时间复杂度太高,每次请求资源都算一次,效率太低 -
死锁检测+恢复:发现问题再处理
等出现问题了,有一些进程因为死锁而停住了,再处理,选择一个进程进行回滚,然后再用银行家算法来算是否能找到安全序列,如果不行,再回滚,直到所有程序都能执行。
但是回滚是个大问题!!!已经写入磁盘,还得退回来,那就很麻烦了。 -
死锁忽略:
windows,linux个人版都不做死锁处理,直接忽略,大不了重启就好了,小概率事件,代价可以接受