调度(任务切换)的发生要同时满足两个条件:能够调度( could)和需要调度(should)。
前者由 per-CPU 的 preempt_count 变量决定,即在 atomic 上下文不能执行调度,而后者取决于 TIF_NEED_RESCHED 标志位。该标志位被设定后,意味着当前有更值得(应该)运行的任务,当下一个调度时机出现时,就会检查这个标志位,并执行调度。

设置该标志位的时间点有几处,其中最典型的,一是任务唤醒的时候,二是周期性 tick 发生的时候。
唤醒抢占
任务被唤醒后,通过 select_task_rq() 函数,在 cpumask 限定的范围内,选择最适合运行的CPU(主要考虑下 CPU 亲和性和负载均衡)。

接着调用 check_preempt_curr(),看是否可以抢占这个 CPU 上当前正在执行的任务(参数 "p" 指向被唤醒任务,"rq" 代表抢占的 CPU)。

这里的 "for_each_class" 看起来只是一个链表遍历,但由于 sched class 的链表隐含了优先级的关系,所以被唤醒的任务、和当前正在运行的任务,谁所属的 "sched_class" 更早被遍历到,说明谁的优先级更高,高优先级的 class 自然可以抢占低优先级的 class(比如 RT 任务抢占 CFS 的任务)。
如果被唤醒任务和当前任务的 "sched_class" 相同,以 CFS 为例(check_preempt_curr 函数指针指向 check_preempt_wakeup),则应根据两者的 vruntime 值进行判断。谁的 vruntime 最小就运行谁,这是 CFS 的基本规则,但在这个规则的基础上,又有着一些灵活的处理。
因为任务切换是有代价的,如果被唤醒任务的 vruntime 比当前任务的 vruntime 小,但差别有限,可能就得不偿失。

为了避免任务过于频繁切换(over-schedule)带来的开销,要求两者 vruntime 的差值,需大于"sched_wakeup_granularity_ns" (就好像很多体育比赛,不是打到局分就结束,需要至少多 2 分才算赢下这一局)。

如果满足抢占条件,就通过 set_next_buddy() 函数,将被唤醒任务指定为下一次调度将运行的process。
Tick 抢占
由于主流的 Linux 版本默认没有使能 "nohz_full" 模式,即便 runqueue 上只有一个 runnable 的任务,tick 依然会发生,所以在tick到来时,需要检查下 runnable 的任务数量,大于 1 才存在切换的必要。

那怎么判断当前任务应该被抢占(让出CPU)呢?依据主要其是实际的运行时间(以"delta_exec"表示),以及其应该运行的时间(就是前面介绍过的 "ideal_runtime")。
如果运行时长已经超过了划定的份额,自然应该让出。而如果没有达到最低标准的 "sched_min_granularity",或者此时其 vruntime 值在 runqueue 中依然最小,则显然应该继续运行。

vruntime 在 runqueue 中不是最小,也不意味着就会被调度出去。同样为了避免 over-schedule,这里增加了一个时间缓冲,而拿来做缓冲阈值的,就是现成的 "ideal_runtime"。
很有意思的是,用来比较的等式左边的 "delta" 是虚拟时间,而右边的 "ideal_runtime" 是物理时间,smcdef 同学查阅了提交记录(http://www.wowotech.net/process_management/448.html),原来作者是想让 weight 值低的任务更容易被抢占,这种实现可以说是很机巧,也可以说是蛮晦涩。
标志位放哪里
这个 "need schedule" 的标志位,从其名字开头的 "TIF" 就不难猜到,它是一个 Thread Info Flag,按理应该放在 thread_info 结构体中,是 per-process 的。
本文的开头提过:调度的发生需同时检测 "preempt_count" 变量和 "TIF_NEED_RESCHED" 标志位,既然前者的 bit 位还没有被占满,那为何不把后者也放进去,这样判断调度条件的时候,只需读取一个变量,多方便啊。
但是,任务被唤醒后送到哪个 CPU 的 runqueue 上去是不确定的,一个 CPU(A) 上 "need schedule" 的标志位完全可能是被另一个 CPU(B) 设置的,如果合二为一,实际上就破坏了 preemption count 作为 per-CPU 变量的优势,会造成对这个变量的修改杂糅 atomic 操作和 non-atomic 操作。
怎么办呢,只能稍微麻烦一点,这个标志位还是放在 thread_info 中,当 CPU(B) 给 CPU(A) 上正在执行的任务设置了「需要切换」的标志位后,再通过 IPI(核间中断)的方式通知CPU(A):喂,你该切换了。

当 CPU(A) 收到这个通知后,自己把 "need schedule" 标志位从 thread_info 拷贝一份到 preemption count 中。最终的结果就是:既方便了调度条件的判断,也避免了 lock 的开销。

明代首辅张居正在《答罗近溪宛陵尹》中有言:“ 学问既知头脑,须窥实际。欲见实际,非至琐细、至猥俗、至纠纷处,不得稳贴”。




