操作系统 -- CPU的调度策略 CPU Scheduling
进程状态
//pesudo code
Activate(pid) {
p = Get PCB(pid);
if (p -> Status.Type == ‘ready_s’) {
p -> Status.Type = ‘ready_a’;
Scheduler();
}
else {
p -> Status.Type = ‘blocked_a’;
}
}
Suspend(pid) {
p = Get PCB(pid);
s = p -> Status.Type;
if ((s == ‘blocked_a’) || (s == ‘blocked_s’)) {
p -> Status.Type = ‘blocked_s’;
}
else {
p -> Status.Type = ‘ready_s’;
}
if (s == ‘running’) {
cpu = p -> Processor ID;
p -> CPU State = Interrupt(cpu);
Scheduler();
}
}
Create(s0, m0, pi, pid) {
p = Get New PCB();
pid = Get New PID();
p -> ID = pid;
p -> CPU State = s0;
p -> Memory = m0;
p -> Priority = pi;
p -> Status.Type = ‘ready_s’;
p -> Status.List = RL;
p -> Creation Tree.Parent = self;
p -> Creation Tree.Child = NULL;
insert(self -> Creation Tree.Child, p);
insert(RL, p);
Scheduler();
}
Destroy(pid) {
p = Get PCB(pid);
KillTree(p);
Scheduler();
}
KillTree(p) {
for (each q in p -> CreationTree.Child) {
KillTree(q);
}
if (p -> Status.Type == ‘running’) {
cpu = p -> Processor ID;
Interrupt(cpu);
}
Remove(p -> Status.List, p);
Release all(p -> Memory);
Release all(p -> Other Resources);
Close all(p -> Open Files);
Delete PCB(p);
}
preemptive and non-preemptive
- preemptive: 目前正在运行的进程可以被打断,将优先级更高的进程分配给CPU
- non-preemptive: 一个进程一旦开始就不能被打断,只能等待自己结束或者进入block状态
- preemptive的代价会高一些,因为需要context switch
- non-preemptive的代价低,但是不太使用real-time和time-shared systems
- 很多系统会两种策略都采用,操作系统中关键的部分比如内核可以采用non-preemptive,而用户进程可以采用preemptive
Scheduler解决的三个问题
什么时候切换进程 — When
怎么将进程和CPU绑定 — How
Context Switch
- 当操作系统切换进程时, 被切换掉的进程的状态会被保存进对应的PCB,然后加载需要执行的进程的PCB
这个过程就是context switch
怎么选择需要执行的 进程 — Who
调度策略需要满足什么要求
- 尽快结束任务: 如编译代码, Matlab运算等任务,需要尽快完成
- 用户操作尽快响应: 如word, 浏览器等, 不需要这些软件结束, 但是需要尽快响应用户的操作
- 系统内耗时间少: 进程切换次数多会导致系统内耗大
不同的任务类型
- 前台任务关注响应时间,后台任务关注周转时间
- 前台任务一般是I/O约束性任务, 因为要和用户交互会有大概率读取存储
- 后台任务一般是CPU约束性任务, 因为不需要交互,会长时间占用CPU进行计算
- I/O约束性任务 > CPU约束性任务,先运行I/O约束性任务可以让CPU和I/O并行,(如果先执行CPU约束性,则会导致I/O长时间处于闲置状态)
优先级 Priority
- 大多数调度策略采用优先级的概念来决定下一个需要运行的进程
- 每个进程用一个数字代表优先级 (越小的数字代表越高的优先级或者越低的优先级)
- 一个进程的优先级通过 priority (p ) 计算
- 优先级分为 static priority和dynamic priority
- static priority: 优先级在一个进程的生命周期中不会改变
- dynamic priority: 每次scheduler被唤起时重新计算优先级
- scheduler 需要保证每个CPU上运行的进程比所有在ready状态的进程优先级要高
Priority Levels
- 所有的进程被分为不同的priority levels, 通过priority queue实现
- Hash + Linked List实现
- Heap
如何评估一个调度算法
Throughput
- Number of processes executed / unit time
CPU Utilization
- Busy time / total time
Waiting Time
- Time in ready queue
Response Time
- From request to run to first response
Burst Time
- Time taken by process on CPU (CPU time)
Arrival Time
- The time when a process enter ready state
Exit Time
- The time when a process complete execution
Total time
- Total time = CPU time + I/O time
Turnaround Time
- The time required for a process to complete
- request to run ------ finish execution
- Turn-around time = exit time - arrival time = burst time + waiting time (CPU time + non-CPU time)
Time slice (quantum)
- time period that a process is allowed to have CPU each time
调度算法
FIFO – First Come, First Served
- 相当于银行办理业务,按顺序处理, 如果同一时间到达则随机选择
- Non-preemptive
- Example formula: priority = 1 / arrival time
- 问题是: 如果前面有耗时长的用户,会导致后面耗时时间短的用户长时间等待, 增加平均耗时
Short Job First
- 首先处理耗时短的用户
- Example formula: priority = -T
- Non-Preemptive
- 问题:如果一直有短耗时用户,则长耗时用户得不到处理导致饥饿
- 问题: 比如任务A总共需要10ms执行,目前还剩3ms. 任务B需要5ms. 此时应该执行任务A而不是任务B
Shortest Remaining Time
- dynamic version of SJF
- Preemptive
- 给剩余时间最短的任务更高的优先级
- Example formula: - remaining time
- 优点是turnaround time比较短,缺点是还是会有饥饿问题
Example of SJF and SRT
HRRN (Highest Response Ratio Next)
- CPU time越短优先级越高,但是wait time越长也会增加优先级
- formula: priority = wait time / CPU time
- non-preemptive
Round Robin (RR)
- 给所有的进程分配一个time slice
- time slice到时后就切换下一个
- 所有进程拥有相同优先级
- time slice ----> infinity RR = FCFS
- time slice ----> 0 系统在不断的context switch,效率很低
Multilevel Feedback Queue
- 用hash table不同的priority level, ,每一层对应一个queue
- 每一层设定一个max time,当一个进程的burst time超过max time时就会被降低优先级移动到下一层
- 每一层的max time是上一层的max time的两倍
- 最后一层可以采用FIFO或者RR
- 只有当目前层的queue为空时才能处理下一层
- 当一个进程刚进入时会被分配到优先级最高的一层
- 这样可以保证burst time长的进程的优先级在不断的变换,系统可以优先处理burst time短的进程
在这里插入图片描述
优先级反转 Priority Inversion
- 正常情况下优先级高的进程应该比优先级低的进程先执行
- 但是也有可能发生优先级低的进程先于优先级高的进程执行,这个就是Priority Inversion
- 通常发生在竞争critical section的时候
- 下图中P low先执行并且拿到mutex锁,然后开始执行critical section (cs)
- 在Plow执行critical section的时候,P high进来了,因为拥有更高的优先级,P low被打断
- P high同时需要对同一个cs进行操作,但是没有拿到锁(锁在P low那里), 所有进入了阻塞状态
- P low继续执行cs, 此时又有P medium进来, 优先级高于 P low 同时不需要对cs操作
- 所以P medium开始执行,等待P medium执行完毕后 P low执行剩下的cs
- 最后P high拿到锁,开始执行
- 问题是 P medium的优先级比P high低, 但是却先于P high执行
- 本来P low可以快速执行完毕然后执行P high,但是却被P medium打断了
- 火星探路者号(Mars Pathfinder)就由于优先级反转而导致系统不断的重置, 信息无法传回地面
Solution 1: Priority Ceiling
- 在一个进程拿到锁之后,将此进程的优先级提到最高
Solution 2: Priority Inheritance
- 在有更高优先级的进程来竞争锁时, 将P low的优先级提到和P high一样
Example of 3 tasks
- P low, P medium, P high 都竞争同一个锁
- P low先开始执行cs
- 当P medium来时没有竞争到锁,进入block,P low优先级从low被提到medium
- 然后当P high来时也没有竞争到锁,进入block,P low优先级从medium被提到high
- P low执行完cs, P high开始执行cs和其他代码
- P high执行完毕,P medium开始执行cs和其他代码
- P medium执行完毕,P low执行剩下的代码,然后结束
Linux的Schedule函数
使用一个counter
- 起到Timer的作用,不断的 count down
- 起到了优先级的作用
代码实现
- 给每个任务分配一个counter
- 每次调度时遍历所有在就绪状态的任务, 选择运行counter最大的任务
- 如果找不到counter最大的任务(比如都为0或者没有就绪状态的任务),则执行进程0,也就是scheduler
并且则重置所有任务的counter: 每个任务的counter右移一位(相当于除2) 然后加上目前的counter值- 这样的重置方法可以保证从阻塞状态恢复的任务的优先级一定比只用CPU的任务的优先级高, 因为只用CPU的任务counter等于0时,从阻塞状态恢复的任务的counter是大于0的
//Pesudo code
while (1) {
c = -1;
next = 0;
//loop all Ready Task, 每一个task有一个counter
if (counter > c) {
c = counter;
next = i //(task indxe)
}
if (c) break;
//loop all processes
counter = counter / 2 + priority
}
switch_to(next)
实际代码: