内容参考摘录自 《Linux内核设计与实现 第三版》
CFS调度实现的相关代码位于/kernel/sched/fair.c中,接下来重点关注4个部分
- 时间记账
- 进程选择
- 调度器入口
- 睡眠和唤醒
一、时间记账
所有的调度都必须对进程运行做时间记账。当每次系统时钟节拍发生时,时间片都会被减少一个节拍周期。当一个进程的时间片被减少到0时,它就会被另一个尚未减到0的时间片可运行进程抢占。
1.调度器实体结构
CFS不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保每个进程只在公平分配给它的处理器时间内运行。CFS使用调度器实体结构
(路径:
)
调度器实体结构实se的成员变量,嵌入在进程描述符struct task_struct内。
2.虚拟实时
vruntime变量存放进程的虚拟运行时间,该运行时间(花在运行上的时间和)的计算是经过了所有可运行进程总数的标准化(或者说是被加权的)。虚拟时间是以ns为单位的,所以vruntime和定时器节拍不再相关。虚拟运行时间可以帮助我们逼近CFS模型所追求的“理想多任务处理器”。优先级相同的所有进程的虚拟运行时间都是相同的——所有任务都将接收到相等的处理器份额。但是因为处理器无法实现完美的多任务,它必须依次运行每个任务。因此CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久。
二、进程选择
若存在一个完美的多任务处理器,所有可运行进程的vruntime值将一致。实际上完美的多任务处理器不存在,因此CFS试图利用一个简单的规则去均衡进程的虚拟运行时间:当CFS需要选择下一个进程时,它会挑一个具有最小vruntime的进程。这其实就是CFS调度算法的核心:选择具有最小vruntime的任务。
CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime值得进程。
选择过程
1.挑选下一个任务
假设,一个红黑树存储了系统中所有的可运行进程,其中节点的键值便是可运行进程的虚拟时间。CFS调度器选取待运行的下一个进程,是所有进程中vruntime最小的那个,它对应的便是树中最左侧的叶子节点。<从树的根节点沿着左边的子节点向下找,一直找到叶子节点,便可以找到vruntime值最小的那个进程。>
2.向树中加入进程
CFS将进程加入rbtree中是通过在进程变为可运行状态(被唤醒)或者是通过fork()调用第一次创建进程时。
3.从树中删除进程
删除动作发生在进程堵塞或终止时。
三、调度器入口
进程调度的主要入口点是函数schedule()。它是内核其它部分用于调用进程调度器的入口:选择哪个进程可以运行,何时将其投入运行。Schedule()通常都需要和一个具体的调度类相关联,也就是说,它会找到一个最高优先级的调度类——后者需要有自己可运行队列,然后问后者谁才是下一个该运行的进程。期间会调用pick_next_task(),pick_next_task()会以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程。
四、睡眠和唤醒
休眠(被阻塞)的进程处于一个特殊的不可执行状态。如果没有这种状态,调度程序就可能选出一个本不愿意被执行的进程,更糟糕的是,休眠必须以轮询的方式实现了。进程休眠有多种原因,但肯定都是为了等待一些事件。<例子:1>休眠最常见的一个原因就是文件I/O——如进程对一个文件执行了read操作,而这需要从磁盘里读取。2>进程在获取键盘输入的时候需要等待。>无论那种情况,内核的操作都相同:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。唤醒进程刚好相反,进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
【注】进程运行队列,每个CPU都有一个,而睡眠队列有很多,用户可以自己创建。
1.等待队列
休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单的链表。内核用wake_queue_head_t来代表等待队列。等待队列可以通过DECLARE_WAITQUEUE()静态创建,也可以由init_waitqueue_head()动态创建。进场把自己放入等待队列中并设置成不可执行状态。当与等待队列相关的事情发生的时候,队列上的进程会被唤醒。
2.唤醒
唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用函数try_to_wake_up(),该函数负责将进程设置为TASK_RUNNING状态,调用enqueue_task()将此进程放入红黑树中,如果被唤醒的进程优先级比当前正在执行的进程的优先级高,还要设置need_resched标志。