date: 2014-11-02 13:16
在《进程的调度与切换》一节中,我们提到,强制调度的两个条件:
-
调度时机:系统调用返回到用户空间前夕,以及中断或者异常服务程序返回到用户空间前夕。可能会有人担心如下两种情况:其一如果进程“躲在”“安全地带”内核空间中不出来,调度器岂不只能干着急?好在内核的设计与实现避免了这个问题。其二:如果进程在用户空间运行,既不调用系统调用函数,也没有中断与异常发生,岂不是也无法进行强转调度?别忘了,系统的时钟中断在默默地坚守着哩。
-
必要条件:当前进程的need_resched字段必须非0。该字段必须由内核去设置,为了让调度器有效运转起来,内核必须“瞅准时机”“见缝插针”地设置该字段。设置的时机包括:
- 其一:在时钟中断服务程序中,当发现当前进程连续运行太长时间时;
- 其二:当唤醒一个睡眠中的进程,发现被唤醒的进程比当前进程更有资格运行时;
- 其三:一个进程通过系统调用改变调度政策(sched_setscheduler)或表示礼让(sched_yield)时。这种情况实际上应该被视为主动的、自愿的调度,因为这些系统调用在返回用户空间时会引起立即调度(而上面两种情况只是设置好了need_resched字段,至于何时能有“调度时机”还是个未知数)。
1 时钟中断
时钟中断服务程序do_timer_interrupt()中调用do_timer(),对单CPU结构后者调用update_process_times()来调整当期进程与时间相关的一些运行参数,代码在<kernel/timer.c>中:
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;
update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter <= 0) {
p->counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
2 wake_up_process()唤醒一个进程
<kerne./sched.c>
/*
* Wake up a process. Put it on the run-queue if it's not
* already there. The "current" process is always on the
* run-queue (except when the actual re-schedule is in
* progress), and as such you're allowed to do the simpler
* "current->state = TASK_RUNNING" to mark yourself runnable
* without the overhead of this.
*/
inline void wake_up_process(struct task_struct * p)
{
unsigned long flags;
/*
* We want the common case fall through straight, thus the goto.
*/
spin_lock_irqsave(&runqueue_lock, flags);
p->state = TASK_RUNNING;
if (task_on_runqueue(p))
goto out;
add_to_runqueue(p);
reschedule_idle(p);
out:
spin_unlock_irqrestore(&runqueue_lock, flags);
}
可见,唤醒一个进程只是将它的状态改为TASK_RUNNING然后加入可运行队列。
reschedule_idle()判断被唤醒的进程是否比当前进程更有资格运行,如果是,则设置need_resched字段,代码如下:
/*
* the 'goodness value' of replacing a process on a given CPU.
* positive value means 'replace', zero or negative means 'dont'.
*/
static inline int preemption_goodness(struct task_struct * prev, struct task_struct * p, int cpu)
{
return goodness(p, cpu, prev->active_mm) –
goodness(prev, cpu, prev->active_mm);
}
static void reschedule_idle(struct task_struct * p)
{
#ifdef CONFIG_SMP
...
#else /* UP */
int this_cpu = smp_processor_id();
struct task_struct *tsk;
tsk = cpu_curr(this_cpu);
if (preemption_goodness(tsk, p, this_cpu) > 1)
tsk->need_resched = 1;
#endif
}
3 改变调度政策和“礼让”
用户登录到系统后,第一个进程的使用调度政策为SCHED_OTHER,即无实时要求的交互式引用。其后,通过fork创建子进程时,则将此调度政策遗传给子进程。但在子进程中可以通过sched_setscheduler()来改变调度政策或者调用sched_setparam()函数来改变实时调度政策的优先级。它们的原型为:
int sched_setscheduler(pid_t pid, int policy, struct sched_param *);
int sched_setparam(pid_t pid, struct sched_param *);
结构体sched_param表示调度参数,只有一个成员sched_priority表示实时优先级,取值范围为[0, 99]。对于SCHED_OTHER政策来说,该值必须为0。
struct sched_param {
int sched_priority;
};
这两个系统调用内核中都是调用setscheduler(),代码在<linux/sched.c>中,代码比较简单,这里不赘述。这里说明一点:如果pid所代表的进程在可执行队列中,那么这两个系统调用会将它们挪到可执行队列的队首,使得再下次调度时占优,然后将当前进程的need_resched字段置1,立即启动一次调度。
至于sched_yield(),使当前进程为其他进程“让路”,但当前进程并没有睡眠,也没有改变当前进程在可执行队列中的位置。sched_yield()只是通过设置当前进程的need_resched字段来启动一次调度。注意,只有在当前进程的调度政策为SCHED_OTHER时,才会在调度政策上设置SCHED_YIELD标志,使得下一次调度将不会再调度该进程。但在下一次调度之后,在__schedule_tail函数中会清除SCHED_YIELD标志,还进程“自由之身”。
与主动调度不同,强制调度在适当的时机将当前进程的need_resched字段置1,然后“眼巴巴”地等待调度时间的到来。也就是发现有调度的必要到调度真正发生有一个延迟,叫做调度延迟(dispatch latency)。
来源:oschina
链接:https://my.oschina.net/u/3857782/blog/1857561