函数schedule()实现进程的调度。它的任务是从运行队列rq中找到一个进程,并随后将CPU分配给这个进程。schedule()可以采取主动调用或被动调用(可延迟的)的方式。
1 直接调用
如果current进程因缺乏资源而要立刻被阻塞,就主动调用调度程序。
a.把current进程插入适当的等待队列。
b.把current进程的状态改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
c.调用schedule()。
d.检查资源是否可用,如果不可用就转到b。
e.一但资源可用就从等待队列中删除当前进程current。
内核反复检查进程需要的资源是否可用,如果不可用,就调用schedule( )把CPU分配给其它进程,直到资源可用。这些步骤与wait_event( )所执行的步骤很相似。
许多反复执行长任务的设备驱动程序也直接调用调度程序。每次反复循环时,驱动程序都检查TIF_NEED_RESCHED标志,如果需要就调用schedule()自动放弃CPU。
2 被动调用
被动调用的方法是,把TIF_NEED_RESCHED标志设置为1(thread_info),在以后的某个时段调用调度程序schedule()。由于总是在恢复用户态进程的执行之前检查这个标志的值,所以schedule()将在不久之后的某个时间被明确地调用。
被动调用调度程序的典型例子,也是最重要的三个进程调度实务:
a 当 current 进程用完了它的CPU 时间片时,由scheduler_tick( )函数做延迟调用。
b 当一个被唤醒进程的优先权比当前进程的优先权高时,由try_to_wake_up( )函数做延迟调用。
c 当发出系统调用sched_setscheduler( )时。
下面,我们就来分析schedule函数到底做了些什么工作
Schedule函数的主要作用就是从就绪进程中选择一个优先级最高的进程来代替当前进程运行。
/*
* schedule() is the main scheduler function.
*/
asmlinkage void __sched schedule(void)
{
long *switch_count;
task_t *prev, *next;
runqueue_t *rq;
prio_array_t *array;
struct list_head *queue;
unsigned long long now;
unsigned long run_time;
int cpu, idx, new_prio;
/*
* Test if we are atomic. Since do_exit() needs to call into
* schedule() atomically, we ignore that path for now.
* Otherwise, whine if we are scheduling when we should not be.
*/
if (likely(!current->exit_state)) {
if (unlikely(in_atomic())) {
printk(KERN_ERR "scheduling while atomic: "
"%s/0x%08x/%d/n",
current->comm, preempt_count(), current->pid);
dump_stack();
}
}
profile_hit(SCHED_PROFILING, __builtin_return_address(0));
//先禁用内核抢占并初始化一些局部变量,把current返回的指针赋给prev,并把与本地CPU相对应的运行队列数据结构的地址赋给rq。
need_resched:
preempt_disable();
prev = current;
release_kernel_lock(prev);
need_resched_nonpreemptible:
rq = this_rq();
/*
* The idle thread is not allowed to schedule!
* Remove this check after it has been exercised a bit.
*/
if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) {
printk(KERN_ERR "bad: scheduling from the idle thread!/n");
dump_stack();
}
//调用sched_clock( )函数以读取TSC,并将它的值转换成纳秒,所获得的时间戳存放在局部变量now中。然后,schedule( )计算prev所用的时间片长度。通常使用限制在1秒(要转换成纳秒)的时间。run_time的值用来限制进程对CPU的使用。不过,鼓励进程有较长的平均睡眠时间:run_time /= (CURRENT_BONUS(prev) ? : 1);这是GCC对问号表达式的扩展
schedstat_inc(rq, sched_cnt);
now = sched_clock();
if (likely((long long)(now - prev->timestamp) < NS_MAX_SLEEP_AVG)) {
run_time = now - prev->timestamp;
if (unlikely((long long)(now - prev->timestamp) < 0))
run_time = 0;
} else
run_time = NS_MAX_SLEEP_AVG;
/*
* Tasks charged proportionately less run_time at high sleep_avg to
* delay them losing their interactive status
*/
run_time /= (CURRENT_BONUS(prev) ? : 1);
//关掉本地中断,并获得所要保护的运行队列的自旋锁
spin_lock_irq(&rq->lock);
//prev可能是一个正在被终止的进程,schedule( )检查PF_DEAD标志确认
if (unlikely(prev->flags & PF_DEAD))
prev->state = EXIT_DEAD;
//检查prev的状态,如果不是可运行状态,而且它没有在内核态被抢占,就应该从运行队列删除prev进程。不过,如果它是非阻塞挂起信号,而且状态为TASK_INTERRUPTIBLE,函数就把该进程的状态设置为TASK_RUNNING,并将它插入运行队列。这个操作与把处理器分配给prev是不同的,它只是给prev一次被选中执行的机会。
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
switch_count = &prev->nvcsw;
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev))))
prev->state = TASK_RUNNING;//再给一次运行的机会,当然不是马上
else {//没办法,我尽力了
if (prev->state == TASK_UNINTERRUPTIBLE)
rq->nr_uninterruptible++;
deactivate_task(prev, rq);
}
}
//检查运行队列中剩余的可运行进程数。如果有可运行的进程,schedule()就调用dependent_sleeper( )函数,在绝大多数情况下,该函数立即返回0。但是,如果内核支持超线程技术,函数检查要被选中执行的进程,其优先权是否比已经在相同物理CPU的某个逻辑CPU上运行的兄弟进程的优先权低,在这种特殊的情况下,schedule()拒绝选择低优先权的进程,而去执行swapper进程。如果运行队列中没有可运行的进程存在,函数就调用idle_balance( ),从另外一个运行队列迁移一些可运行进程到本地运行队列中,idle_balance( )与load_balance( )类似。
cpu = smp_processor_id();
if (unlikely(!rq->nr_running)) {
go_idle:
idle_balance(cpu, rq);
if (!rq->nr_running) {
next = rq->idle;
rq->expired_timestamp = 0;
wake_sleeping_dependent(cpu, rq);
/*
* wake_sleeping_dependent() might have released
* the runqueue, so break out if we got new
* tasks meanwhile:
*/
if (!rq->nr_running)
goto switch_tasks;
}
} else {
if (dependent_sleeper(cpu, rq)) {
next = rq->idle;
goto switch_tasks;
}
/*
* dependent_sleeper() releases and reacquires the runqueue
* lock, hence go into the idle loop if the rq went
* empty meanwhile:
*/
if (unlikely(!rq->nr_running))
goto go_idle;
}
//如果idle_balance( ) 没有成功地把进程迁移到本地运行队列中,schedule( )就调用wake_sleeping_dependent( )重新调度空闲CPU(即每个运行swapper进程的CPU)中的可运行进程,通常在内核支持超线程技术的时候可能会出现这种情况。然而,在单处理机系统中,或者当把进程迁移到本地运行队列的种种努力都失败的情况下,函数就选择swapper进程作为next进程并继续进行下一步骤。
//好了,乱七八糟的事干完了,该干正事了。现在可以确定队列中总会有任务让我来忙。检查这些可运行进程中是否至少有一个进程是活动的,如果没有,函数就交换运行队列数据结构的active和expired数组的内容,因此,所有的过期进程变为活动进程,而空集合准备接纳将要过期的进程。
array = rq->active;
if (unlikely(!array->nr_active)) {
/*
* Switch the active and expired arrays.
*/
schedstat_inc(rq, sched_switch);
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = MAX_PRIO;
}
//现在终于可以在active 数组的prio_array_t数据结构中搜索一个可运行进程了。首先,schedule()搜索活动进程集合位掩码的第一个非0位。当对应的优先权链表不为空时,就把位掩码的相应位置1。因此,第一个非0位的下标对应包含最佳运行进程的链表,随后,返回该链表的第一个进程描述符。uCOS也是这么干的
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, task_t, run_list);
//如果next是一个非实时进程而且它正在从TASK_INTERRUPTIBLE 或 TASK_STOPPED状态被唤醒,调度程序就把自从进程插入运行队列开始所经过的纳秒数加到进程的平均睡眠时间中。换而言之,进程的睡眠时间被增加了,以包含进程在运行队列中等待CPU所消耗的时间。实时进程是不需要用平均睡眠时间计算优先级的--------他就这么霸道,我也没办法。
if (!rt_task(next) && next->activated > 0) {
unsigned long long delta = now - next->timestamp;
if (unlikely((long long)(now - next->timestamp) < 0))
delta = 0;
//对于交互进程和高CPU占用的进程是要加以区别的,要不就太不公平了,对不?
if (next->activated == 1)
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
array = next->array;
new_prio = recalc_task_prio(next, next->timestamp + delta);
if (unlikely(next->prio != new_prio)) {
dequeue_task(next, array);
next->prio = new_prio;
enqueue_task(next, array);
} else
requeue_task(next, array);
}
next->activated = 0;
//要让next 进程投入运行了
switch_tasks:
if (next == rq->idle)
schedstat_inc(rq, sched_goidle);
//prefetch 宏把next进程描述符的第一部分字段的内容装入硬件高速缓存,正是这一点改善了schedule()的性能,因为对于后续指令的执行(不影响next),数据是并行移动的。
prefetch(next);
prefetch_stack(next);
//prev进程的TIF_NEED_RESCHED位要清零
clear_tsk_need_resched(prev);
rcu_qsctr_inc(task_cpu(prev));
update_cpu_clock(prev, rq, now);
//减少prev的平均睡眠时间,并把它补充给进程所使用的CPU时间片
prev->sleep_avg -= run_time;
if ((long)prev->sleep_avg <= 0)
prev->sleep_avg = 0;
prev->timestamp = prev->last_ran = now;
//prev!=next进程切换确实地发生了
sched_info_switch(prev, next);
if (likely(prev != next)) {
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
++*switch_count;
prepare_task_switch(rq, next);
// context_switch( )函数建立next的地址空间
prev = context_switch(rq, prev, next);
/好了,进程到此就换过来了,prev不再是原来的了,是现在系统里优先级最高的进程
//宏barrier( )产生一个代码优化屏障
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
} else //我本来就是第一名,那就不用换啦
spin_unlock_irq(&rq->lock);
//schedule( )在需要的时候重新获得大内核锁、重新启用内核抢占、并检查是否一些其他的进程已经设置了当前进程的TIF_NEED_RESCHED标志,如果是,整个schedule( )函数重新开始执行,否则,函数结束。
prev = current;
if (unlikely(reacquire_kernel_lock(prev) < 0))
goto need_resched_nonpreemptible;
preempt_enable_no_resched();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}
//最后schedule成功结束,感谢祖国、感谢党、感谢政府、感谢CCTV、感谢……。
1 直接调用
如果current进程因缺乏资源而要立刻被阻塞,就主动调用调度程序。
a.把current进程插入适当的等待队列。
b.把current进程的状态改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
c.调用schedule()。
d.检查资源是否可用,如果不可用就转到b。
e.一但资源可用就从等待队列中删除当前进程current。
内核反复检查进程需要的资源是否可用,如果不可用,就调用schedule( )把CPU分配给其它进程,直到资源可用。这些步骤与wait_event( )所执行的步骤很相似。
许多反复执行长任务的设备驱动程序也直接调用调度程序。每次反复循环时,驱动程序都检查TIF_NEED_RESCHED标志,如果需要就调用schedule()自动放弃CPU。
2 被动调用
被动调用的方法是,把TIF_NEED_RESCHED标志设置为1(thread_info),在以后的某个时段调用调度程序schedule()。由于总是在恢复用户态进程的执行之前检查这个标志的值,所以schedule()将在不久之后的某个时间被明确地调用。
被动调用调度程序的典型例子,也是最重要的三个进程调度实务:
a 当 current 进程用完了它的CPU 时间片时,由scheduler_tick( )函数做延迟调用。
b 当一个被唤醒进程的优先权比当前进程的优先权高时,由try_to_wake_up( )函数做延迟调用。
c 当发出系统调用sched_setscheduler( )时。
下面,我们就来分析schedule函数到底做了些什么工作
Schedule函数的主要作用就是从就绪进程中选择一个优先级最高的进程来代替当前进程运行。
/*
* schedule() is the main scheduler function.
*/
asmlinkage void __sched schedule(void)
{
long *switch_count;
task_t *prev, *next;
runqueue_t *rq;
prio_array_t *array;
struct list_head *queue;
unsigned long long now;
unsigned long run_time;
int cpu, idx, new_prio;
/*
* Test if we are atomic. Since do_exit() needs to call into
* schedule() atomically, we ignore that path for now.
* Otherwise, whine if we are scheduling when we should not be.
*/
if (likely(!current->exit_state)) {
if (unlikely(in_atomic())) {
printk(KERN_ERR "scheduling while atomic: "
"%s/0x%08x/%d/n",
current->comm, preempt_count(), current->pid);
dump_stack();
}
}
profile_hit(SCHED_PROFILING, __builtin_return_address(0));
//先禁用内核抢占并初始化一些局部变量,把current返回的指针赋给prev,并把与本地CPU相对应的运行队列数据结构的地址赋给rq。
need_resched:
preempt_disable();
prev = current;
release_kernel_lock(prev);
need_resched_nonpreemptible:
rq = this_rq();
/*
* The idle thread is not allowed to schedule!
* Remove this check after it has been exercised a bit.
*/
if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) {
printk(KERN_ERR "bad: scheduling from the idle thread!/n");
dump_stack();
}
//调用sched_clock( )函数以读取TSC,并将它的值转换成纳秒,所获得的时间戳存放在局部变量now中。然后,schedule( )计算prev所用的时间片长度。通常使用限制在1秒(要转换成纳秒)的时间。run_time的值用来限制进程对CPU的使用。不过,鼓励进程有较长的平均睡眠时间:run_time /= (CURRENT_BONUS(prev) ? : 1);这是GCC对问号表达式的扩展
schedstat_inc(rq, sched_cnt);
now = sched_clock();
if (likely((long long)(now - prev->timestamp) < NS_MAX_SLEEP_AVG)) {
run_time = now - prev->timestamp;
if (unlikely((long long)(now - prev->timestamp) < 0))
run_time = 0;
} else
run_time = NS_MAX_SLEEP_AVG;
/*
* Tasks charged proportionately less run_time at high sleep_avg to
* delay them losing their interactive status
*/
run_time /= (CURRENT_BONUS(prev) ? : 1);
//关掉本地中断,并获得所要保护的运行队列的自旋锁
spin_lock_irq(&rq->lock);
//prev可能是一个正在被终止的进程,schedule( )检查PF_DEAD标志确认
if (unlikely(prev->flags & PF_DEAD))
prev->state = EXIT_DEAD;
//检查prev的状态,如果不是可运行状态,而且它没有在内核态被抢占,就应该从运行队列删除prev进程。不过,如果它是非阻塞挂起信号,而且状态为TASK_INTERRUPTIBLE,函数就把该进程的状态设置为TASK_RUNNING,并将它插入运行队列。这个操作与把处理器分配给prev是不同的,它只是给prev一次被选中执行的机会。
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
switch_count = &prev->nvcsw;
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev))))
prev->state = TASK_RUNNING;//再给一次运行的机会,当然不是马上
else {//没办法,我尽力了
if (prev->state == TASK_UNINTERRUPTIBLE)
rq->nr_uninterruptible++;
deactivate_task(prev, rq);
}
}
//检查运行队列中剩余的可运行进程数。如果有可运行的进程,schedule()就调用dependent_sleeper( )函数,在绝大多数情况下,该函数立即返回0。但是,如果内核支持超线程技术,函数检查要被选中执行的进程,其优先权是否比已经在相同物理CPU的某个逻辑CPU上运行的兄弟进程的优先权低,在这种特殊的情况下,schedule()拒绝选择低优先权的进程,而去执行swapper进程。如果运行队列中没有可运行的进程存在,函数就调用idle_balance( ),从另外一个运行队列迁移一些可运行进程到本地运行队列中,idle_balance( )与load_balance( )类似。
cpu = smp_processor_id();
if (unlikely(!rq->nr_running)) {
go_idle:
idle_balance(cpu, rq);
if (!rq->nr_running) {
next = rq->idle;
rq->expired_timestamp = 0;
wake_sleeping_dependent(cpu, rq);
/*
* wake_sleeping_dependent() might have released
* the runqueue, so break out if we got new
* tasks meanwhile:
*/
if (!rq->nr_running)
goto switch_tasks;
}
} else {
if (dependent_sleeper(cpu, rq)) {
next = rq->idle;
goto switch_tasks;
}
/*
* dependent_sleeper() releases and reacquires the runqueue
* lock, hence go into the idle loop if the rq went
* empty meanwhile:
*/
if (unlikely(!rq->nr_running))
goto go_idle;
}
//如果idle_balance( ) 没有成功地把进程迁移到本地运行队列中,schedule( )就调用wake_sleeping_dependent( )重新调度空闲CPU(即每个运行swapper进程的CPU)中的可运行进程,通常在内核支持超线程技术的时候可能会出现这种情况。然而,在单处理机系统中,或者当把进程迁移到本地运行队列的种种努力都失败的情况下,函数就选择swapper进程作为next进程并继续进行下一步骤。
//好了,乱七八糟的事干完了,该干正事了。现在可以确定队列中总会有任务让我来忙。检查这些可运行进程中是否至少有一个进程是活动的,如果没有,函数就交换运行队列数据结构的active和expired数组的内容,因此,所有的过期进程变为活动进程,而空集合准备接纳将要过期的进程。
array = rq->active;
if (unlikely(!array->nr_active)) {
/*
* Switch the active and expired arrays.
*/
schedstat_inc(rq, sched_switch);
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = MAX_PRIO;
}
//现在终于可以在active 数组的prio_array_t数据结构中搜索一个可运行进程了。首先,schedule()搜索活动进程集合位掩码的第一个非0位。当对应的优先权链表不为空时,就把位掩码的相应位置1。因此,第一个非0位的下标对应包含最佳运行进程的链表,随后,返回该链表的第一个进程描述符。uCOS也是这么干的
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, task_t, run_list);
//如果next是一个非实时进程而且它正在从TASK_INTERRUPTIBLE 或 TASK_STOPPED状态被唤醒,调度程序就把自从进程插入运行队列开始所经过的纳秒数加到进程的平均睡眠时间中。换而言之,进程的睡眠时间被增加了,以包含进程在运行队列中等待CPU所消耗的时间。实时进程是不需要用平均睡眠时间计算优先级的--------他就这么霸道,我也没办法。
if (!rt_task(next) && next->activated > 0) {
unsigned long long delta = now - next->timestamp;
if (unlikely((long long)(now - next->timestamp) < 0))
delta = 0;
//对于交互进程和高CPU占用的进程是要加以区别的,要不就太不公平了,对不?
if (next->activated == 1)
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
array = next->array;
new_prio = recalc_task_prio(next, next->timestamp + delta);
if (unlikely(next->prio != new_prio)) {
dequeue_task(next, array);
next->prio = new_prio;
enqueue_task(next, array);
} else
requeue_task(next, array);
}
next->activated = 0;
//要让next 进程投入运行了
switch_tasks:
if (next == rq->idle)
schedstat_inc(rq, sched_goidle);
//prefetch 宏把next进程描述符的第一部分字段的内容装入硬件高速缓存,正是这一点改善了schedule()的性能,因为对于后续指令的执行(不影响next),数据是并行移动的。
prefetch(next);
prefetch_stack(next);
//prev进程的TIF_NEED_RESCHED位要清零
clear_tsk_need_resched(prev);
rcu_qsctr_inc(task_cpu(prev));
update_cpu_clock(prev, rq, now);
//减少prev的平均睡眠时间,并把它补充给进程所使用的CPU时间片
prev->sleep_avg -= run_time;
if ((long)prev->sleep_avg <= 0)
prev->sleep_avg = 0;
prev->timestamp = prev->last_ran = now;
//prev!=next进程切换确实地发生了
sched_info_switch(prev, next);
if (likely(prev != next)) {
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
++*switch_count;
prepare_task_switch(rq, next);
// context_switch( )函数建立next的地址空间
prev = context_switch(rq, prev, next);
/好了,进程到此就换过来了,prev不再是原来的了,是现在系统里优先级最高的进程
//宏barrier( )产生一个代码优化屏障
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
} else //我本来就是第一名,那就不用换啦
spin_unlock_irq(&rq->lock);
//schedule( )在需要的时候重新获得大内核锁、重新启用内核抢占、并检查是否一些其他的进程已经设置了当前进程的TIF_NEED_RESCHED标志,如果是,整个schedule( )函数重新开始执行,否则,函数结束。
prev = current;
if (unlikely(reacquire_kernel_lock(prev) < 0))
goto need_resched_nonpreemptible;
preempt_enable_no_resched();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}
//最后schedule成功结束,感谢祖国、感谢党、感谢政府、感谢CCTV、感谢……。