4、选择下一个进程
选择下一个将要运行的进程由pick_next_task_fair执行。代码流程图在图2-21给出。
kernel/sched_fair.c
如果nr_running计数器为0,即当前队列上没有可运行进程,则无事可做,函数可以立即返回。否则将具体工作委托给pick_next_entity。
kernel/sched_fair.c
kernel/sched_fair.c
如果红黑树中最左边的进程可用,可以使用辅助函数first_fair立即确定,然后用__pick_next_entity从红黑树中提取出sched_entity实例。使用container_of机制完成的,因为红黑树管理的结点是rb_node的实例,而rb_node即嵌入在sched_entity中。
kernel/sched_fair.c
现在已经选择了进程,但还需要完成一些工作,才能将其标记为运行进程。这是通过set_next_entity处理的。
kernel/sched_fair.c
当前执行进程不保存在就绪队列上,使用__dequeue_entity将其从红黑树中移除。如果当前进程是最左边的结点,则将leftmost指针设置到下一个最左边的进程。注意在例子中,进程确实已经在就绪队列上,但set_next_entity可能从不同地方调用,所以情况会有所不同。
尽管该进程不再包含在红黑树中,但进程和就绪队列之间的关联没有丢失,因为curr标记了当前运行的进程:
kernel/sched_fair.c
因为该进程是当前活动进程,在CPU上花费的实际时间将记入sum_exec_runtime,因此内核会在prev_sum_exec_runtime保存此前的设置。要注意进程中的sum_exec_runtime没有重置。因此差值sum_exec_runtime - prev_sum_exec_runtime确实表示了在CPU上执行花费的实际时间。
5、处理周期性调度器
在处理周期调度时前述的差值很重要。形式上由函数task_tick_fair负责,但实际工作由entity_tick完成。图2-22给出了代码流程图。
kernel/sched_fair.c
kernel/sched_fair.c
首先,使用update_curr更新统计量。如果队列的nr_running计数器表明队列上可运行的进程少于两个,则实际上无事可做。如果某个进程应该被抢占,那么至少需要有另一个进程能够抢占它。如果进程数目不少于两个,则由check_preempt_tick作出决策:
kernel/sched_fair.c
函数check_preempt_tick的目的在于,确保没有哪个进程能够比延迟周期中确定的份额运行得更长。该份额对应的实际时间长度在sched_slice中计算,进程在CPU上已经运行的实际时间间隔由sum_exec_runtime-prev_sum_exec_runtime给出。因此抢占决策很容易作出:如果进程运行时间比期望的时间间隔长,那么通过resched_task发出重调度请求。这会在task_struct中设置TIF_NEED_RESCHED标志,核心调度器会在下一个适当时机发起重调度。
6、唤醒抢占
当在try_to_wake_up和wake_up_new_task中唤醒进程时,内核使用check_preempt_curr看看是否新进程可以抢占当前运行的进程。注意该过程不涉及核心调度器!对完全公平调度器处理的进程,则由check_preempt_wakeup函数执行该检测。
新唤醒的进程不必一定由完全公平调度器处理。如果新进程是一个实时进程,则会立即请求重调度,因为实时进程总是会抢占CFS进程:
kernel/sched_fair.c
最便于处理的情况是SCHED_BATCH进程,根据定义它们不抢占其他进程。
当运行进程被新进程抢占时,内核确保被抢占者至少已经运行了某一最小时间限额。该最小值保存在sysctl_sched_wakeup_granularity。回想可知其默认值设置为4毫秒。这指的是实际时间,因此在必要的情况下内核首先需要将其转换为虚拟时间:
kernel/sched_fair.c
如果新进程的虚拟运行时间,加上最小时间限额,仍然小于当前执行进程的虚拟运行时间(由其调度实体se表示),则请求重调度:
kernel/sched_fair.c
增加的时间“缓冲”确保了进程不至于切换得太频繁,避免了花费过多的时间用于上下文切换,而非实际工作。
7、处理新进程
对完全公平调度器需要考虑的最后一个操作是创建新进程时调用的挂钩函数:task_new_fair。该函数的行为可使用参数sysctl_sched_child_runs_first控制。该参数用于判断新建子进程是否应该在父进程之前运行。这通常是有益的,特别是在子进程随后会执行exec系统调用的情况下。该参数的默认设置是1,但可以通过/proc/sys/kernel/sched_child_runs_first修改。
该函数先用update_curr进行通常的统计量更新,然后调用place_entity:
kernel/sched_fair.c
在这种情况下,调用place_entity时的initial参数设置为1,以便用sched_vslice_add计算初始的vruntime。这实际上确定了进程在延迟周期中所占的时间份额,只是转换为虚拟时间。这是调度器最初向进程欠下的债务。
如果父进程的虚拟运行时间(由curr表示)小于子进程的虚拟运行时间,则意味着父进程将在子进程之前调度运行。回想可知虚拟运算时间比较小,则在红黑树中的位置比较靠左。如果子进程应该在父进程之前运行,则二者的虚拟运算时间需要换过来。
然后子进程按常规加入就绪队列,并请求重调度。