<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} a:link, span.MsoHyperlink {color:blue; text-decoration:underline; text-underline:single;} a:visited, span.MsoHyperlinkFollowed {color:purple; text-decoration:underline; text-underline:single;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:595.3pt 841.9pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->
优先权只是调度算法考虑的一个方面
进程调度依据
调度程序运行时,要在所有可运行状态的进程中选择最值得运行的进程投入运行。选择进程的依据是什么呢?在每个进程的 task_struct 结构中有以下四 项: policy 、 priority 、 counter 、 rt_priority 。这四项是选择进程的依据。其中, policy 是进程的调度策略,用来区分 实时进程和普通进程,实时进程优先于普通进程运行; priority 是进程 ( 包括实时和普通 ) 的静态优先级; counter 是进程剩余的时间片,它的起始 值就是 priority 的值;由于 counter 在后面计算一个处于可运行状态的进程值得运行的程度 goodness 时起重要作用,因此, counter 也可以看作是进程的动态优先级。 rt_priority 是实时进程特有的,用于实时进程间的选择。
Linux 用 函数 goodness() 来衡量一个处于可运行状态的进程值得运行的程度。该函数综合了以上提到的四项,还结合了一些其他的因素,给每个处于可运行状态的 进程赋予一个权值 (weight) ,调度程序以这个权值作为选择进程的唯一依据。关于 goodness() 的情况在后面将会详细分析。
进程调度策略
调度程序运行时 , 要在所有处于可运行状态的进程之中选择最值得运行的进程投入运行。选择进程的依据是什么呢 ? 在每个进程的 task_struct 结构中有这么四项:
policy, priority , counter, rt_priority
这四项就是调度程序选择进程的依据 . 其中 ,policy 是进程的调度策略 , 用来区分两种进程 - 实时和普通; priority 是进程 ( 实时和普通 ) 的优先 级; counter 是进程剩余的时间片 , 它的大小完全由 priority 决定 ;rt_priority 是实时优先级 , 这是实时进程所特有的,用于实时进程间的选择。
首先, Linux 根据 policy 从整体上区分实时进程和普通进程,因为实时进程和普通进程度调度是不同的,它们两者之间,实时进程应该先于普通进程而运行,然后,对于同 一类型的不同进程,采用不同的标准来选择进程:
对于普通进程, Linux 采用动态优先调度,选择进程的依据就是进程 counter 的大小。进程创建时,优先级 priority 被赋一个初值,一般为 0 ~ 70 之间的数字,这个数字同时也是计数器 counter 的初值,就是说进程创建时两者是相等的。字面上看, priority 是 " 优先级 " 、 counter 是 " 计数器 " 的意思,然而实际上,它们表达的是同一个意思 - 进程的 " 时间片 " 。 Priority 代表分配给该进程的时间片, counter 表示该进程剩余的时间片。在进程运行过程中, counter 不断减少,而 priority 保持不变,以便在 counter 变为 0 的时候(该进程用完了所分 配的时间片)对 counter 重新赋值。当一个普通进程的时间片用完以后,并不马上用 priority 对 counter 进行赋值,只有所有处于可运行状态 的普通进程的时间片 (p->counter==0) 都用完了以后,才用 priority 对 counter 重新赋值,这个普通进程才有了再次被调度的 机会。这说明,普通进程运行过程中, counter 的减小给了其它进程得以运行的机会,直至 counter 减为 0 时才完全放弃对 CPU 的使用,这就相对于优先级在动态变 化,所以称之为动态优先调度。至于时间片这个概念,和其他不同 操作系统 一样的, Linux 的时间单位也是 " 时钟滴答 " ,只是不同操作系统对一个时钟滴答的定义不同而已 ( Linux 为 10ms )。进程的时间片就是指多少个时钟滴答,比如,若 priority 为 20 ,则分配给该进程的时间片就为 20 个时钟滴答,也就是 20*10ms=200ms 。 Linux 中某个进程的调度策略 (policy) 、优先级 (priority) 等可以作为参数由用户自己决定,具有相当的灵 活性。内核创建新进程时分配给进程的时间片缺省为 200ms( 更准确的,应为 210ms) ,用户可以通过系统调用改变它。
对于实时进程, Linux 采用了两种调度策略,即 FIFO( 先来先服务调度 ) 和 RR (时间片轮转调度)。因为实时进程具有一定程度的紧迫性,所以衡量一个 实时进程是否应该运行, Linux 采用了一个比较固定的标准。实时进程的 counter 只是用来表示该进程的剩余时间片,并不作为衡量它是否值得运行的标 准。实时进程的 counter 只是用来表示该进程的剩余时间片,并不作为衡量它是否值得运行的标准,这和普通进程是有区别的。上面已经看到,每个进程有两 个优先级,实时优先级就是用来衡量实时进程是否值得运行的。
这一切看来比较麻烦,但实际上 Linux 中的实现相当简单。 Linux 用函数 goodness() 来衡量一个处于可运行状态的进程值得运行的程度。该函数 综合了上面提到的各个方面,给每个处于可运行状态的进程赋予一个权值 (weight) ,调度程序以这个权值作为选择进程的唯一依据。
Linux 根据 policy 的值将进程总体上分为实时进程和普通进程,提供了三种调度算法:一种传统的 Unix 调度程序和两个由 POSIX.1b( 原名为 POSIX.4) 操作系统标准所规定的 " 实时 " 调度程序。但这种实时只是软实时,不满足诸如中断等待时间等硬实时要求,只是保证 了当实时进程需要时一定只把 CPU 分配给实时进程。
非实时进程有两种优先级,一种是静态优先级,另一种是动态优先级。实时进程又增加了第三种优先级,实时优先级。优先级是一些简单的整数,为了决定应该允许 哪一个进程使用 CPU 的资源,用优先级代表相对权值 - 优先级越高,它得到 CPU 时间的机会也就越大。
? 静态优先级 (priority)- 不随时间而改变,只能由用户进行修改。它指明了在被迫和其他进程竞争 CPU 之前,该进程所应该被允许的时间片的最大值 (但很可能的,在该时间片耗尽之前,进程就被迫交出了 CPU )。
? 动态优先级 (counter)- 只要进程拥有 CPU ,它就随着时间不断减小;当它小于 0 时,标记进程重新调度。它指明了在这个时间片中所剩余的时间量。
? 实时优先级 (rt_priority)- 指明这个进程自动把 CPU 交给哪一个其他进程;较高权值的进程总是优先于较低权值的进程。如果一个进程不是实时进 程,其优先级就是 0 ,所以实时进程总是优先于非实时进程的(但实际上,实时进程也会主动放弃 CPU )。
当 policy 分别为以下值时:
1) SCHED_OTHER :这是普通的用户进程,进程的缺省类型,采用动态优先调度策略,选择进程的依据主要是根据进程 goodness 值的大小。这种进程 在运行时,可以被高 goodness 值的进程抢先。
2) SCHED_FIFO :这是一种实时进程,遵守 POSIX1.b 标准的 FIFO( 先入先出 ) 调度规则。它会一直运行,直到有一个进程因 I/O 阻塞,或者主 动释放 CPU ,或者是 CPU 被另一个具有更高 rt_priority 的实时进程抢先。在 Linux 实现中, SCHED_FIFO 进程仍然拥有时间片 - 只有 当时间片用完时它们才被迫释放 CPU 。因此,如同 POSIX1.b 一样,这样的进程就象没有时间片 ( 不是采用分时 ) 一样运行。 Linux 中进程仍然保持对 其时间片的记录(不修改 counter )主要是为了实现的方便,同时避免在调度代码的关键路径上出现条件判断语句 if (!(current->policy&SCHED_FIFO)){...}- 要知道,其他大量非 FIFO 进程都需要记录时间片,这种多余 的检测只会浪费 CPU 资源。(一种优化措施,不该将执行时间占 10% 的代码的运行时间减少到 50% ;而是将执行时间占 90% 的代码的运行时间减少到 95% 。 0.9+0.1*0.5=0.95>0.1+0.9*0.9=0.91 )
3) SCHED_RR :这也是一种实时进程,遵守 POSIX1.b 标准的 RR( 循环 round-robin) 调度规则。除了时间片有些不同外,这种策略与 SCHED_FIFO 类似。当 SCHED_RR 进程的时间片用完后,就被放到 SCHED_FIFO 和 SCHED_RR 队列的末尾。
只要系统中有一个实时进程在运行,则任何 SCHED_OTHER 进程都不能在任何 CPU 运行。每个实时进程有一个 rt_priority ,因此,可以按照 rt_priority 在所有 SCHED_RR 进程之间分配 CPU 。其作用与 SCHED_OTHER 进程的 priority 作用一样。只有 root 用户能 够用系统调用 sched_setscheduler ,来改变当前进程的类型 (sys_nice,sys_setpriority) 。
此外,内核还定义了 SCHED_YIELD ,这并不是一种调度策略,而是截取调度策略的一个附加位。如同前面说明的一样,如果有其他进程需要 CPU ,它就 提示调度程序释放 CPU 。特别要注意的就是这甚至会引起实时进程把 CPU 释放给非实时进程。
主要的进程调度的函数分析
真正执行调度的函数是 schedule(void), 它选择一个最合适的进程执行,并且真正进行上下文切换,使得选中的进程得以执行。而 reschedule_idle(struct task_struct *p) 的作用是为进程选择一个合适的 CPU 来执行,如果它选中了某个 CPU ,则将该 CPU 上当前运行进程的 need_resched 标志置为 1, 然后向它 发出一个重新调度的处理机间中断,使得选中的 CPU 能够在中断处理返回时执行 schedule 函数,真正调度进程 p 在 CPU 上执行。在 schedule() 和 reschedule_idle() 中调用了 goodness() 函数。 goodness() 函数用来衡量一个处于可运行状态的进 程值得运行的程度。此外,在 schedule() 函数中还调用了 schedule_tail() 函数 ; 在 reschedule_idle() 函数中还调用 了 reschedule_idle_slow() 。这些函数的实现对理解 SMP 的调度非常重要,下面一一分析这些函数。先给出每个函数的主要流程图,然后 给出源代码,并加注释。
goodness() 函数分析
goodness() 函数计算一个处于可运行状态的进程值得运行的程度。一个任务的 goodness 是以下因素的函数:正在运行的任务、想要运行的任务、 当前的 CPU 。 goodness 返回下面两类值中的一个: 1000 以下或者 1000 以上。 1000 或者 1000 以上的值只能赋给 " 实时 " 进程,从 0 到 999 的值只能赋给普通进程。实际上,在单 处 理器 情况下,普通进程的 goodness 值只使用这个范围底部的一部分,从 0 到 41 。在 SMP 情况下, SMP 模式会优先照顾等待同一个处理器的进 程。不过,不管是 UP 还是 SMP ,实时进程的 goodness 值的范围是从 1001 到 1099 。
goodness() 函数其实是不会返回 -1000 的,也不会返回其他负值。由于 idle 进程的 counter 值为负,所以如果使用 idle 进程作为参数 调用 goodness ,就会返回负值,但这是不会发生的。
goodness() 是个简单的函数,但是它是 linux 调度程序不可缺少的部分。运行队列中的每个进程每次执行 schedule 时都要调度它,因此它的 执行速度必须很快。
// 在 /kernel/sched.c 中
static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
{ int weight;
if (p->policy != SCHED_OTHER) {/* 如果是实时进程,则 */
weight = 1000 + p->rt_priority;
goto out;
}
/* 将 counter 的值赋给 weight ,这就给了进程一个大概的权值, counter 中的值表示进程在一个时间片内,剩下要运行的时间 .*/
weight = p->counter;
if (!weight) /* weight==0, 表示该进程的时间片已经用完,则直接转到标号 out*/
goto out;
#ifdef __SMP__
/* 在 SMP 情况下,如果进程将要运行的 CPU 与进程上次运行的 CPU 是一样的,则最有利,因此,假如进程上次运行的 CPU 与当前 CPU 一致的话,权值加 上 PROC_CHANGE_PENALTY ,这个宏定义为 20 。 */
if (p->processor == this_cpu)
weight += PROC_CHANGE_PENALTY;
#endif
if (p->mm == this_mm) /* 进程 p 与当前运行进程,是同一个进程的不同线程,或者是共享地址空间的不同进程,优先选择,权值加 1*/
weight += 1;
weight += p->priority; /* 权值加上进程的优先级 */
out:
return weight; /* 返回值作为进程调度的唯一依据,谁的权值大,就调度谁运行 */
}
schedule() 函数分析
schedule() 函数的作用是,选择一个合适的进程在 CPU 上执行,它仅仅根据 'goodness' 来工作。对于 SMP 情况,除了计算每个进程的加权 平均运行时间外,其他与 SMP 相关的部分主要由 goodness() 函数来体现。
流程:
① 将 prev 和 next 设置为 schedule 最感兴趣的两个进程:其中一个是在调用 schedule 时正在运行的进程 (prev) ,另外一个应该是接着 就給予 CPU 的进程( next )。注意: prev 和 next 可能是相同的 -schedule 可以重新调度已经获得 cpu 的进程 .
② 中断处理程序运行 " 下半部分 ".
③ 内核实时系统部分的实现,循环调度程序( SCHED_RR )通过移动 " 耗尽的 "RR 进程 - 已经用完其时间片的进程 - 到队列末尾,这样具有相同优先级的其 他 RR 进程就可以获得 CPU 了。同时,这补充了耗尽进程的时间片。
④ 由于代码的其他部分已经决定了进程必须被移进或移出 TASK_RUNNING 状态,所以会经常使用 schedule ,例如,如果进程正在等待的 硬件 条件已经发生,所以如果必要,这个 switch 会改变进程的状态。如果进程已经处于 TASK_RUNNING 状态,它就无需处理了。如果它是可以中断的(等待信号),并且信号已经到达了进程,就返回 TASK_RUNNING 状态。在所以其他情况下(例如,进程已经处于 TASK_UNINTERRUPTIBLE 状态了),应该从运行队列中将进程移走。
⑤ 将 p 初始化为运行队列的第一个任务; p 会遍历队列中的所有任务。
⑥ c 记录了运行队列中所有进程最好的 "goodness"- 具有最好 "goodness" 的进程是最易获得 CPU 的进程。 goodness 的值越高越好。
⑦ 遍历执行任务链表,跟踪具有最好 goodness 的进程。
⑧ 这个循环中只考虑了唯一一个可以调度的进程。在 SMP 模式下,只有任务不在 cpu 上运行时,即 can_schedule 宏返回为真时,才会考虑该任务。 在 UP 情况下, can_schedule 宏返回恒为真 .
⑨ 如果循环结束后,得到 c 的值为 0 。说明运行队列中的所有进程的 goodness 值都为 0 。 goodness 的值为 0, 意味着进程已经用完它的时间片,或 者它已经明确说明要释放 CPU 。在这种情况下, schedule 要重新计算进程的 counter ;新 counter 的值是原来值的一半加上进程的静态优先 级( priortiy ),除非进程已经释放 CPU ,否则原来 counter 的值为 0 。因此, schedule 通常只是把 counter 初始化为静态优先 级。(中断处理程序和由另一个处理器引起的分支在 schedule 搜寻 goodness 最大值时都将增加此循环中的计数器,因此由于这个原因计数器可能不 会为 0 。显然,这很罕见。)在 counter 的值计算完成后,重新开始执行这个循环,找具有最大 goodness 的任务。
⑩ 如果 schedule 已经选择了一个不同于前面正在执行的进程来调度,那么就必须挂起原来的进程并允许新的进程运行。这时调用 switch_to 来进行 切换。
代码摘自 linux/kernel/sched.c :
asmlinkage void schedule(void)
{
struct schedule_data * sched_data;
struct task_struct *prev, *next, *p;
int this_cpu, c;
if (tq_scheduler) /*tq_scheduler 是一个特殊的队列,只在调度程序运行时得到执行,其目的是为了进行扩充,用来支持系统中多于 32 个的任务队列 */
goto handle_tq_scheduler;
tq_scheduler_back:
prev = current;
this_cpu = prev->processor;
if (in_interrupt()) /* 判断 schedule 是否在中断处理中执行,如果是在中断中执行,就说明发生了错误,会引发 OOPS 错误 */
goto scheduling_in_interrupt;
release_kernel_lock(prev, this_cpu); /* 释放全局内核锁,并开 this_cpu 的中断,主要是在进行进程切换前释放内核锁,否则,若不释放,切换后的进程也要获得内核锁,就会发生死锁 */
/* 检测是否有中断下半部需要处理 */
if (bh_mask & bh_active)
goto handle_bh;
handle_bh_back:
/* 对于 aligned_data[this_cpu].schedule_data 的读写不需要用锁保护,因为,每个 CPU 上任意时刻只有一个进程运 行 */
sched_data = & aligned_data[this_cpu].schedule_data;/* 取得本地 cpu 上的调度数据 */
spin_lock_irq(&runqueue_lock);/* 要开始操作要运行进程队列,不允许打断,故锁住运行队列,并且同时关中断 */
/* 将一个时间片用完的 SCHED_RR 进程放到队列的末尾 */
if (prev->policy == SCHED_RR)
goto move_rr_last;
move_rr_back:
switch (prev->state) {
case TASK_INTERRUPTIBLE: /* 此状态表明进程可以被信号中断 */
if (signal_pending(prev)) {/* 如果该进程有未处理的信号 */
prev->state = TASK_RUNNING;
break;
}
default:
del_from_runqueue(prev);
case TASK_RUNNING:
}
prev->need_resched = 0;
repeat_schedule: /* 真正找合适的进程 */
p = init_task.next_run;
/* Default process to select.. */
next = idle_task(this_cpu); /* 缺省选择空闲进程 */
c = -1000;
if (prev->state == TASK_RUNNING)
goto still_running;
still_running_back:
while (p != &init_task) {
if (can_schedule(p)) { /* 进程 p 不占用 cpu*/
int weight = goodness(prev, p, this_cpu);
if (weight > c)
c = weight, next = p;
}
p = p->next_run;
}
/*c 中存放最大的 goodness 值 */
if (!c) /* 如果 goodness ( prev,p,this_cpu )函数返回的是 0, 表示进程 p 剩余的运行时间为 0, 则要重新计算 */
goto recalculate;
/* 到这一点,已经确定了要调度执行的进程 */
/* 记录调度选择的情况 */
sched_data->curr = next;
#ifdef __SMP__
next->has_cpu = 1;
next->processor = this_cpu;
#endif
spin_unlock_irq(&runqueue_lock); /* 对运行队列数据结构操作完成,释放运行队列锁,并打开中断 */
if (prev == next) /* 如果选中的进程和原来运行的进程是同一个 */
goto same_process;
#ifdef __SMP__
/* 计算该进程在其生命周期里占有 cpu 的平均时间: avg_slice 。这是加权平均,进程近期的活动远比很久以前的活动权值大。这个值将在 reschedule_idle 中用来决定是否将进程调入到另一个 CPU 中。因此,在 UP 情况下,它不需要而且也不会被计算。 */
{
cycles_t t, this_slice;
t = get_cycles();
this_slice = t - sched_data->last_schedule;
sched_data->last_schedule = t;
/* 计算进程的平均运行时间 */
prev->avg_slice = (this_slice*1 + prev->avg_slice*1)/2;
}
#endif /* __SMP__ */
kstat.context_switch++;
get_mmu_context(next);
switch_to(prev, next, prev); /* 切换到选中的进程执行 */
__schedule_tail(prev); /* 该函数调用的作用是:考虑将当前被切换下来的进程,放到别的 CPU 上运行 */
same_process:
reacquire_kernel_lock(current); /* 重新获得内核锁 */
return;
recalculate:
{
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock); /*tasklist 可以允许多个读者读,但当有一个写者时,不运行有其他读者或写者 */
for_each_task(p) /* 对于每个重新计算 p->counter*/
p->counter = (p->counter >> 1) + p->priority;
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
goto repeat_schedule;
}
still_running:
c = prev_goodness(prev, prev, this_cpu);
next = prev;
goto still_running_back;
handle_bh:/* 处理中断下半部 */
do_bottom_half();
goto handle_bh_back;
handle_tq_scheduler:/* 处理 tq_scheduler 中的任务 */
run_task_queue(&tq_scheduler);
goto tq_scheduler_back;
move_rr_last:/* 将一个时间片用完的进程放到运行队列的末尾 */
if (!prev->counter) {
prev->counter = prev->priority;
move_last_runqueue(prev);
}
goto move_rr_back;
scheduling_in_interrupt: /* 处理在中断中调度的情况,产生一个错误 */
printk("Scheduling in interrupt/n");
*(int *)0 = 0; /* 向一个非法地址写 , 产生错译 */
return;
}
reschedule_idle() 函数分析
当已经不在运行队列中的进程被唤醒时, wake_up_process(struct task_struct * p) 将调用 reschedule_idle ,进程是作为 p 而被传递进 reschedule_idle 中的。这个函数试图把新近唤醒的进程在一个最合适的 CPU 上运行。
reschedule_idle() 函数主要针对 SMP 系统,不过,在该函数中调用了 reschedule_idle_slow() 中,在 reschedule_idle_slow() 中,存在一些针对 UP 的代码。
在 reschedule_idle() 中使用 goodness() ,目的是用来预测在我们发送到的 CPU 上运行 schedule() 的效果。通过预测未来 执行 schedule() 的效果,可以在任务被唤醒时,选择最好的 CPU 去运行。 reschedule_idle() 的最后一个目标是:让选中的 CPU 上 调用 schedule() ,使得被唤醒的任务能在该 CPU 上重新调度运行。这是如何做的的呢?如果选择到的最好 CPU 不是当前 CPU ,就可以通过 CPU 间 消息传递方法(在 i386 中,是 SMP-IPI 中断),向该 CPU 发送一个重新调度的事件。如果就是当前 CPU ,就可以直接将当前运行进程的 need_resched 标志置为 1 ,在随后的时钟中断结束时引发调度。
以下代码摘自 linux/kernel/sched.c
static void reschedule_idle(struct task_struct * p)
{
/* 这个函数代码分两部分。第一部分:快速判断是否需要找一个合适的 CPU 让进程 p 运行,如果不需要直接返回;这部分代码,只是在 SMP 情况下才会执行。 第二部分代码调用 reschedule_idle_slow(), 真正去找一个合适的 CPU 让进程 p 执行。 */
#ifdef __SMP__
int cpu = smp_processor_id();
/* 检查是否值得找一个合适的 CPU 让 p 执行 , 如果不值得,则直接返回 */
if ((p->processor == cpu) && related(cpu_curr(cpu), p))
return;
#endif /* __SMP__ */
reschedule_idle_slow(p);
}
宏 related(p1,p2) 的分析:
其中宏 related(p1,p2) 定义为:
摘自: kernel/sched.c
#define related(p1,p2) (((p1)->lock_depth >= 0) && (p2)->lock_depth >= 0) && (((p2)->policy== SCHED_OTHER) && ((p1)->avg_slice < cacheflush_time))
当以下三种条件同时满足时,该宏返回为真:
① (p1)->lock_depth >= 0) && (p2)->lock_depth >= 0) 为真,表明进程 p1 和 p2 都在控制着,或想要控制内核锁,说明这两个进程存在相互依赖关系,则不管这两个进程生存于何处,都不可能同时运行;
② 进程 p1 的的平均运行时间小于清空本地高速缓存的时间( cacheflush_time ), cacheflush_time 在系统启动时被赋值为 cpu_hz/1024*cachesize/5000 ,其中 cpu_hz 为 CPU 的时钟频率, cachesize 为高速缓存的大小;
③ 进程 p2 是一个普通进程;
从总体上来说,宏 related(p1,p2) 主要检测进程 p1 和 p2 是否存在相互依赖关系。如果依赖关系存在,则不管这两个进程生存于何处,都不可能同 时运行。宏 related(p1,p2) 返回真,表明没有必要找另一个合适的 CPU 让进程 p2 执行。
reschedule_idle_slow() 函数分析
reschedule_idle_slow(struct task_struct * p) 的目标是试图找出一个合适的 CPU 来运行进程 p 。
SMP 情况下流程:
① 先检查 p 进程上一次运行的 cpu 是否空闲,如果空闲,这是最好的 cpu ,直接返回;
② 找一个合适的 cpu ,查看 SMP 中的每个 CPU 上运行的进程,与 p 进程相比的抢先 goodness ,把具有最高的抢先 goodness 值的进程记录在 target_task 中,该进程运行的 cpu 为最合适的 CPU 。但是,如果在检查的过程中,发现某个 cpu 上运行的进程与进程 p 是相关的,即 related 宏返回为真,表明没有必要找一个 CPU 让进程运行,直接转到标号 out_no_target 执行;
③ 如 target_task 为空,说明没有找到合适的 cpu ,直接转到标号 out_no_target 执行。
④ 如果 target_task 不为空,则说明找到了合适的 cpu ,因此将 target_task->need_resched 置为 1, 如果运行 target_task 的 cpu 不是 reschedule_idle_slow 运行的 cpu ,则向运行 target_task 的 cpu 发送一个中断,让它 重新调度;
⑤ out_no_target 后的语句,就直接返回;
UP 情况下流程:
检查 p 能否抢先 cpu 上运行的进程,如果能抢先(即 preemtion_goodness(task,p,this_cpu)>0 ),则将 cpu 上 当前进程的 need_resched 域置为 1 ,引发调度。
以下代码摘自: linux/kernel/sched.c
static inline void reschedule_idle_slow(struct task_struct * p)
{
#ifdef __SMP__
/* 在 SMP 中,尽力找一个最合适的 CPU 让进程 p 执行。这个合适的 CPU 可能是空闲 CPU ,但也有可能不是空闲 CPU 。只要进程 p 的 goodness 值 比 CPU 上运行的当前进程的 goodness 值要高,就可以抢先在该 CPU 上运行的进程。当然,在选择合适的 CPU 时,是选择进程 p 与该 CPU 上运行进程 的 goodness 的值相差最大的 CPU 。 */
int this_cpu = smp_processor_id(), target_cpu;
struct task_struct *tsk, *target_tsk;
int cpu, best_cpu, weight, best_weight, i;
unsigned long flags;
best_weight = 0;
/* 要读写运行队列,必须先获得运行队列自旋锁,并且确保关中断 */
spin_lock_irqsave(&runqueue_lock, flags);
best_cpu = p->processor;
target_tsk = idle_task(best_cpu); /*idle_task ()得到当该 cpu 的空闲进程 */
if (cpu_curr(best_cpu) == target_tsk) /* 进程上一次运行的 cpu 是空闲的 */
goto send_now;
target_tsk = NULL;
for (i = 0; i < smp_num_cpus; i++) {
cpu = cpu_logical_map(i);
tsk = cpu_curr(cpu);
if (related(tsk, p)) /* 如果 tak 和 p 相依赖,则这两个进程几乎不可能同时运行,因此,不需要找在空闲 cpu 让 p 执行 */
goto out_no_target;
/* 计算 p 抢先 tsk 的 goodness,preemption_goodness(), 实际上是计算两个进程的 goodness() 的差值 */
weight = preemption_goodness(tsk, p, cpu);
if (weight > best_weight) {
best_weight = weight;
target_tsk = tsk;
}
}
/* 以上 for 循环执行完后, target_task 应该是最合适的 cpu 上运行的进程 */
if (!target_tsk) /* 如果没有合适的 cpu*/
goto out_no_target;
send_now:
target_cpu = target_tsk->processor;
target_tsk->need_resched = 1;
/* 释放运行队列锁,并恢复中断标志,注意:不一定是开中断,恢复执行 spin_lock_irqsave 前的状态 */
spin_unlock_irqrestore(&runqueue_lock, flags);
if (target_cpu != this_cpu) /* 如果不是是当前调度程序运行的 cpu ,则 */
smp_send_reschedule(target_cpu);/* 向选择的 CPU 发送一个 IPI, 使得该 CPU 上能进行调度 */
return;
out_no_target:
/* 没有找到合适的 CPU ,释放运行队列锁,并恢复中断标志 */
spin_unlock_irqrestore(&runqueue_lock, flags);
return;
#else
/* 处理单 CPU 的情况,因为只有一个 CPU ,因此不需要选择了,只需要看是否值得抢先当前正在运行的进程就可以了。 */
int this_cpu = smp_processor_id();
struct task_struct *tsk;
tsk = current;
if (preemption_goodness(tsk, p, this_cpu) > 0)
tsk->need_resched = 1;
#endif
}
这里涉及处理机间 通信 / 中 断,我们将在第三部分详细介绍,这里只讨论与处理机调度有关的部分。 Intel 多处理规范 MP 的核心就是高级可编程中断控制器( Advanced Programmable Interrupt Controllers , APIC )的使用。 CPU 通过彼此发送中断来完成它们之间的通信。通过给中断附加动作,不同的 CPU 可以在某种程度广彼此进行控 制。每个 CPU 有自己的 APIC (成为那个 CPU 的本地 APIC ),并且还有一个 I / O APIC 来处理由 I / O 设备引起的中断。在普通的多处理器系统中, I / O APIC 中断控制器芯片组的作用。
void smp_send_reschedule(int cpu)
{
send_IPI_single(cpu, RESCHEDULE_VECTOR);
}
这个函数只有一行,它仅仅是给其 ID 以参数形式给出的目标 CPU 发送一个中断。函数用 CPU ID 和 RESCHEDULE _ VECTOR 向量调用 send _ IPI _ single 函数。 RESCHEDULE_VECTOR 与其他 CPU 中断向量是一起 在 arch/i386/kernel/irq.h 中被定义。
#define RESCHEDULE_VECTOR 0x30
#define INVALIDATE_TLB_VECTOR 0x31
#define STOP_CPU_VECTOR 0x40
#define LOCAL_TIMER_VECTOR 0x41
#define CALL_FUNCTION_VECTOR 0x50
static inline void send_IPI_single(int dest, int vector)
send _ IPI _ single 函数发送一个 IPI-- 那是 Intel 对处理器问中断( IntERP rocessor interruPt )的称呼 -- 给指定的目的 CPU 。在这一行,内核以相当低级的方式与发送 CPU 的本地 APIC 对话。
{
unsigned long cfg;
#if FORCE_APIC_SERIALIZATION
unsigned long flags;
__save_flags(flags);
__cli();
#endif
cfg = __prepare_ICR2(dest);
得到中断命令寄存器( ICR )上半部分的内容 -- 本地 APIC 就是通过这个寄存器进行编程的 -- 不过它的目的信息段要被设置为 dest 。尽 管._ prepare _ ICRZ 里使用了 "2" , CPU 实际上只有一个 ICR ,而不是两个。但是它是一个 64 位寄存器,内核更愿意把它看作是两个 32 位寄 存器 -- 在内核代码里, "ICR" 表示这个寄存器的低端 32 位,所以 "ICR2" 就表示高端 32 位。我们想要设置的目的信息段就在高端 32 位,即 ICR2 里。
apic_write(APIC_ICR2, cfg);
把修改过的信息写回 ICR 。现在 ICR 知道了目的 CPU 。
cfg = __prepare_ICR(0, vector);
调用_ prepare _ ICR 来设置我们想要发送给目的 CPU 的中断向量(注意没有什么措施能够保证目的 CPU 不是当前 CPU--ICR 完全能够发送一个 IPI 给它自己的 CPU 。尽管这样,没有任何理由要这样做)。
apic_write(APIC_ICR, cfg);
通过往 ICR 里写入新的配置来发送中断。
#if FORCE_APIC_SERIALIZATION
__restore_flags(flags);
#endif
}
schedule_tail() 函数分析
顾名思义,该函数的作用是执行 schedule() 函数的一些扫尾工作。在 SMP 情况下,如果失去 CPU 的进程 , 其状态是 TASK_RUNNING, 并且 不是空闲进程,则调用 reschedule_idle() ,找一个合适的 CPU 去运行。并且将失去 CPU 的进程的 has_cpu 域置为 0 。并且为保证处理 器一致性,调用 wmb() 来 has_cpu=0 这个操作不会被提前执行。
代码摘自: linux/kernel/sched.c :
static inline void __schedule_tail (struct task_struct *prev)
{
#ifdef __SMP__
if ((prev->state == TASK_RUNNING) &&(prev != idle_task(smp_processor_id())))
reschedule_idle(prev);
wmb(); /* 根据处理器一致性( PO ) , 保证读、写操作的顺序 */
prev->has_cpu = 0;
#endif /* __SMP__ */
}
此外, 源码 是最好的文档,看看 kernel/sched.c
#ifndef __alpha__
/*
* This has been replaced by sys_setpriority. Maybe it should be
* moved into the arch dependent tree for those ports that require
* it for backward compatibility?
*/
asmlinkage int sys_nice(int increment)
{
unsigned long newprio;
int increase = 0;
/*
* Setpriority might change our priority at the same moment.
* We don't have to worry. Conceptually one call occurs first
* and we have a single winner.
*/
newprio = increment;
if (increment < 0) {
if (!capable(CAP_SYS_NICE))
return -EPERM;
newprio = -increment;
increase = 1;
}
if (newprio > 40)
newprio = 40;
/*
* do a "normalization" of the priority (traditionally
* Unix nice values are -20 to 20; Linux doesn't really
* use that kind of thing, but uses the length of the
* timeslice instead (default 210 ms). The rounding is
* why we want to avoid negative values.
*/
newprio = (newprio * DEF_PRIORITY + 10) / 20;
increment = newprio;
if (increase)
increment = -increment;
/*
* Current->priority can change between this point
* and the assignment. We are assigning not doing add/subs
* so thats ok. Conceptually a process might just instantaneously
* read the value we stomp over. I don't think that is an issue
* unless posix makes it one. If so we can loop on changes
* to current->priority.
*/
newprio = current->priority - increment;
if ((signed) newprio < 1)
newprio = 1;
if (newprio > DEF_PRIORITY*2)
newprio = DEF_PRIORITY*2;
current->priority = newprio;
return 0;
)
#endif