调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间:
好文参考:https://blog.csdn.net/janneoevans/article/details/8125106
1.主调度器
在内核中的许多地方,如果要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主
调度器函数(schedule)。在从系统调用返回之后,内核也会检查当前进程是否设置了重调度标志
TIF_NEED_RESCHED,例如,前述的scheduler_tick通过调度类就会设置该标志。如果是这样,则内核会调用
schedule。该函数假定当前活动进程一定会被另一个进程取代。网友相关理解:
主调度器就负责将CPU使用权限从一个进程切换到另一个进程。。上图中,三种不同颜色的长条分别表示CPU分配给进程A、B、C的一小段执行时间,执行顺序是:A,B,C。竖直的虚线表示当前时间,也就是说;A已经在CPU上执行完CPU分配给它的时间,马上轮到B执行了。这时主调度器shedule就负责完成相关处理工作然后将CPU的使用权交给进程B。
总之,主调度器的工作就是完成进程间的切换。
主调度器基本流程:
看下具体代码实现:
schedule函数的实现:
asmlinkage void __sched schedule(void)
{
/* 获取当前的进程 */
struct task_struct *tsk = current;
/* 避免死锁 */
sched_submit_work(tsk);
__schedule();
}
sched_submit_work函数的实现:
static inline void sched_submit_work(struct task_struct *tsk)
{
/* 检测tsk->state是否为0 (runnable), 若为运行态时则返回,
tsk_is_pi_blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return*/
if (!tsk->state || tsk_is_pi_blocked(tsk))
return;
/*
* If we are going to sleep and we have plugged IO queued,
* make sure to submit it to avoid deadlocks.
*/
if (blk_needs_flush_plug(tsk)) /* 然后检测是否需要刷新plug队列,用来避免死锁 */
blk_schedule_flush_plug(tsk);
}
__schedule函数的实现:
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); /* 关闭内核抢占 */
/* ==1==
找到当前cpu上的就绪队列rq
并将正在运行的进程curr保存到prev中 */
cpu = smp_processor_id();
rq = cpu_rq(cpu);
/* 更新全局状态,
* 标识当前CPU发生上下文的切换 */
rcu_note_context_switch(cpu);
prev = rq->curr;
/* 如果禁止内核抢占,而又调用了cond_resched就会出错
* 这里就是用来捕获该错误的 */
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
/*
* Make sure that signal_pending_state()->signal_pending() below
* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*/
smp_mb__before_spinlock();
/* 锁住该队列 */
raw_spin_lock_irq(&rq->lock);
/* 切换次数记录, 默认认为非主动调度计数(抢占) */
switch_count = &prev->nivcsw;
/*
* scheduler检查prev的状态state和内核抢占标志
* 如果prev是不可运行的, 并且在内核态没有被抢占
*
* 此时当前进程不是处于运行态, 并且不是被抢占
* 此时不能只检查抢占计数
* 因为可能某个进程(如网卡轮询)直接调用了schedule
* 如果不判断prev->stat就可能误认为task进程为RUNNING状态
* 到达这里,有两种可能,一种是主动schedule, 另外一种是被抢占
* 被抢占有两种情况, 一种是时间片到点, 一种是时间片没到点
* 时间片到点后, 主要是置当前进程的need_resched标志
* 接下来在时钟中断结束后, 会preempt_schedule_irq抢占调度
*
* 那么我们正常做的是应该将进程prev从就绪队列rq中删除,
* 但是如果当前进程prev有非阻塞等待信号,
* 并且它的状态是TASK_INTERRUPTIBLE
* 我们就不应该从就绪队列总删除它
* 而是配置其状态为TASK_RUNNING, 并且把他留在rq中
/* 如果内核态没有被抢占, 并且内核抢占有效
即是否同时满足以下条件:
1 该进程处于停止状态
2 该进程没有在内核态被抢占 */
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
/* 如果当前进程有非阻塞等待信号,并且它的状态是TASK_INTERRUPTIBLE */
if (unlikely(signal_pending_state(prev->state, prev))) {
/* 将当前进程的状态设为:TASK_RUNNING */
prev->state = TASK_RUNNING;
} else {/* 否则需要将prev进程从就绪队列中删除*/
/* 将当前进程从runqueue(运行队列)中删除 */
deactivate_task(rq, prev, DEQUEUE_SLEEP);
/* 标识当前进程不在runqueue中 */
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
/* 如果不是被抢占的,就累加主动切换次数 */
switch_count = &prev->nvcsw;
}
pre_schedule(rq, prev);
//当前CPU运行队列为NULL的时候
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);//进行CPU负载平衡
/*CPU负载平衡有两种方式:pull和push,即空闲CPU从其他忙的CPU队列中拉一个进程到当前CPU队列;
或者忙的CPU队列将一个进程推送到空闲的CPU队列中。idle_balance干的则是pull的事情*/
/*put_prev_task在另一个进程代替当前运行的进程之前调用,它负责向进程提供或撤销CPU。
但在不同进程之间切换,仍然需要执行一个底层的上下文切换*/
put_prev_task(rq, prev);
//选择下一个即将运行的进程
next = pick_next_task(rq);
/*清除pre的TIF_NEED_RESCHED(该进程应该或者想要调度器选择另一个进程替换本进程执行)标志*/
clear_tsk_need_resched(prev);
rq->skip_clock_update = 0;
/* 如果prev和next非同一个进程 */
if (likely(prev != next)) {
rq->nr_switches++;/* 队列切换次数更新 */
rq->curr = next; /* 将next标记为队列的curr进程 */
++*switch_count; /* 进程切换次数更新 */
#ifdef CONFIG_IDLESTATS
if(cpu_accurate_status_enable) {
cpu_accurate_schedule_begin();
context_switch(rq, prev, next); /* unlocks the rq */
cpu_accurate_schedule_end();
}
else
context_switch(rq, prev, next); /* unlocks the rq */
#else
/* 进程之间上下文切换 */
context_switch(rq, prev, next); /* unlocks the rq */
#endif
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else /* 如果prev和next为同一进程,则不进行进程切换 */
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
sched_preempt_enable_no_resched(); /* 开启内核抢占 */
/*检测当前进程的重调度位是否设置,并跳转到如上所述的标号,重新开始搜索一个新
进程*/
if (need_resched())
goto need_resched;
}
STACK_FRAME_NON_STANDARD(__schedule); /* switch_to() */
2. 周期调度器
同样参照上图:周期性调度不关注进程切换,而是把A在CPU上执行的过程放大后观察细节。
在A享用它得到的CPU时间的过程中,系统会定时调用周期性调度器(即定时执行周期性调度函数scheduler_tick())。
在此版本的内核中,这个周期为10ms(这个10ms是这样得来的:内中定义了一个宏变量:HZ=100,它表示每秒钟周期性
调度器执行的次数,那么时间间隔t=1/HZ=1/100s=10ms。
周期性调度器是用中断实现的:系统定时产生一个中断,然后在中断过程中执行scheduler_tick()函数,执行完毕后将CPU使用权限还给A
(有可能不会还给A了,细节后续在讨论),下一个时间点到了,系统会再次产生中断,然后去执行scheduler_tick()函数。(中断过程对进程
A是透明的,所以A是一个傻子,它以为自己连续享用了自己得到的CPU时间段,其实它中途被scheduler_tick()中断过很多次)。
周期性调度器在scheduler_tick中实现。如果系统正在活动中,内核会按照频率HZ自动调用该
函数。如果没有进程在等待调度,那么在计算机电力供应不足的情况下,也可以关闭该调度器以减少
电能消耗。 该函数有下面两个主要任务。
(1) 管理内核中与整个系统和各个进程的调度相关的统计量。其间执行的主要操作是对各种计数器加1。
a) 更新时钟(rq->clock),这个任务由__update_rq_clock()完成。
b) 更新rq结构体的的cpu_load[ ]数组。它保留了5(由 CPU_LOAD_IDX_MAX指定)个历史load值,update_cpu_load(rq)则是将每个元素往后移,
将最老的值从数组中移出,将当前的load添加进去。
c) 接下来更新进程的vruntime,sum_exec_runtime等参数。这任务由该进程指向的调度类所指向的task_tick()函数完成。看调度类(sched_class)结构体
的定义,你会发现task_tick只是一个函数指针。如果当前进程是普通进程,它对应的sched_class的task_tick()函数指针是指向task_tick_fair()这个函数的,如果
是空闲(idle)进程,它对应的sched_class的task_tick()指向的是task_tick_idle()函数,如果是实时进程则是task_tick_rt()。
(2) 激活负责当前进程的调度类的周期性调度方法。
看看scheduler_tick具体代码实现:
void scheduler_tick(void)
{
/* 1. 获取当前cpu上的全局就绪队列rq和当前运行的进程curr */
/* 1.1 在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0 */
int cpu = smp_processor_id();
/* 1.2 获取cpu的全局就绪队列rq, 每个CPU都有一个就绪队列rq */
struct rq *rq = cpu_rq(cpu);
/* 1.3 获取就绪队列上正在运行的进程curr */
struct task_struct *curr = rq->curr;
sched_clock_tick();
/* 2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度 */
/* 加锁 */
raw_spin_lock(&rq->lock);
/* 2.1 更新rq的当前时间戳(包括任务时间戳).即使rq->clock变为当前时间戳 */
update_rq_clock(rq);
/* 2.2 更新rq的负载信息, 即就绪队列的cpu_load[]数据
* 本质是讲数组中先前存储的负荷值向后移动一个位置,
* 将当前负荷记入数组的第一个位置 */
update_cpu_load_active(rq);
/* 2.3 执行当前运行进程所在调度类的task_tick函数进行周期性调度 */
curr->sched_class->task_tick(rq, curr, 0);
/* 解锁 */
raw_spin_unlock(&rq->lock);
/* 与perf计数事件相关 */
perf_event_task_tick();
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq, cpu);
#endif
rq_last_tick_reset(rq);
}
如果当前进程应该被重新调度,那么调度器类方法会在task_struct中设置TIF_NEED_RESCHED
标志,以表示该请求,而内核会在接下来的适当时机完成该请求。
举例:如果当前进程是普通进程(调度类采用公平调度),那么周期性调度器的执行流程如下(其中省略了判断语句和其他的细节):
看看task_tick_fair的实现;
/*
* scheduler tick hitting a task of our scheduling class:
*/
//task_tick_fair()负责更新普通进程的统计信息。
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
struct cfs_rq *cfs_rq;
/* 获取到当前进程curr所在的调度实体 */
struct sched_entity *se = &curr->se;
/* for_each_sched_entity
* 在不支持组调度条件下, 只循环一次
* 在组调度的条件下, 调度实体存在层次关系,
* 更新子调度实体的同时必须更新父调度实体
* for_each_sched_entity函数便是依次往上遍历,直至它的最高层祖先。
* #definefor_each_sched_entity(se) \
for (; se; se = se->parent)*/
for_each_sched_entity(se) {
/* 获取当当前运行的进程所在的CFS就绪队列 */
cfs_rq = cfs_rq_of(se);
/* entity_tick完成周期性调度,负责二个工作:
1.调用update_curr()函数更新相关统计量,
2.调用check_preempt_tick(),检查进程本次被获得CPU使用权执行的时间是否超过了它对应的ideal_runtime值,
如果超过了,则将当前进程的TIF_NEED_RESCHED标志位置位。 */
entity_tick(cfs_rq, se, queued);----------------->1
}
if (numabalancing_enabled)
task_tick_numa(rq, curr);
update_rq_runnable_avg(rq, 1);
}
------------------->1再看看entity_tick的代码实现:
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
/*
* Update run-time statistics of the 'current'.
调用update_curr()函数更新相关统计量
*/
update_curr(cfs_rq);-------->1
/*
* Ensure that runnable average is periodically updated.
*/
update_entity_load_avg(curr, 1);
update_cfs_rq_blocked_load(cfs_rq, 1);
update_cfs_shares(cfs_rq);
if (cfs_rq->nr_running > 1)
check_preempt_tick(cfs_rq, curr);-------------------->2
}
----------->1看看update_curr的代码实现:
/*
* Update the current task's runtime statistics.
*/
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
/*是计算周期性调度器上次执行时到周期性这次执行之间,进程实际执行的CPU时间(如果周期性调度器每1ms执行一次,
delta_exec就表示没1ms内进程消耗的CPU时间,这个在前面讲了),它是一个实际运行时间。
*/
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
//更新exec_start
curr->exec_start = now;
schedstat_set(curr->statistics->exec_max,
max(delta_exec, curr->statistics->exec_max));
//更新当前进程的实际运行时间sum_exec_runtime
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
/* calc_delta_fair:更新当前进程的虚拟时间vruntime
*/
curr->vruntime += calc_delta_fair(delta_exec, curr);
//更新cfs_rq->min_vruntime。
update_min_vruntime(cfs_rq);
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
------------->2》check_preempt_tick的代码实现:
/*
* Preempt the current task with a newly woken task if needed:
*/
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
/* 计算curr的理论上应该运行的时间 */
ideal_runtime = sched_slice(cfs_rq, curr);
/* 计算curr的实际运行时间
* sum_exec_runtime: 进程执行的总时间
* prev_sum_exec_runtime:进程在切换进CPU时的sum_exec_runtime值 */
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
/* 如果实际运行时间比理论上应该运行的时间长
* 说明curr进程已经运行了足够长的时间
* 应该调度新的进程抢占CPU了 */
if (delta_exec > ideal_runtime) {
/*resched_task发出重调度请求. 这会在task_struct中设置TIF_NEED_RESCHED标志,
核心调度器会在下一个适当的时机发起重调度.*/
resched_curr(rq_of(cfs_rq));
/*
* The current task ran long enough, ensure it doesn't get
* re-elected due to buddy favours.
*/
clear_buddies(cfs_rq, curr);
return;
}
}
实时进程的情形:
实时进程的调度不涉及虚拟运行时间,所以比更新统计量的工作普通进程要简单很多。该函数首先调用update_curr_rt()函数
更新当前进程的统计信息。然后根据判断条件进行必要的操作。
实时进程有两种调度策略,FIFO(先进先出)和RR(时间片轮转)。FIFO策略很简单:得到CPU使用权的进程可以执行任意长
时间,直到它主动放弃CPU。RR策略呢,则是给每个进程分配一个时间片,当前进程的时间片消耗完毕则切换至下一个进程。
所以,接下来的代码,判断调度策略是不是RR,如果不是RR则无事可做。如果是RR,则将其时间片减一,如果时间片不为零,该进程
可以继续执行,那么什么都不需要做。如果时间片为0,则重新给它分配时间片(长度由DEF_TIMESLICE指定),如果可运行进程大于一个,
就调用requeue_task_rt()将当前进程放到实时就绪队列的末尾,并将TIF_NEED_RESCHED标志位置为,提示系统需要进行进程切换。
如果当前进程是实时进程,周期性调度器的执行流程如下: