此章以80x86体系结构为例。假定系统采用统一内存访问模型,系统时钟设定为1ms。
传统Unix 操作系统的调度策略须实现几个互相冲突的目标:
Linux 的调度基于分时 ( time sharing ) 技术:多个进程以“时间多路复用 ”方式运行,因为CPU的时间被分成 “片 ( slice) " , 给每个可运行进程分配一 片。如果当前运行进程的时间片或时限(quantum ) 到期时,该进程还没有运行完毕,进程切换就可以发生。
调度策略根据进程优先级对其分类。在Linux中,进程优先级是动态的。调度程序跟踪进程运行并周期性调整期优先级。
进程分类
传统上把进程分为以下两类:
另一饭分类法把进程分为以下三类:
两种分类法相互独立。Linux中,调度算法可确认所有实时程序的身份,但无法区分交互式程序和批处理程序。Linux2.6调度陈旭实现了基于进程过去行为的启发式算法,以区分交互是进程和批处理进程。
表7-1列举可改变调度优先级的系统调用
系统调用 | 说明 |
---|---|
nice() | 改变一个将通进程的静态优先级 |
getpriority() | 获得一组将通进程的最大静态优先级 |
setpriority() | 设置一组普通进程的静态优先级 |
sched_getscheduler() | 获得一个迸程的调度策略 |
sched_setscheduler() | 设置一个进程的调度策略和实时优先级 |
sched_getparam() | 获得一个进程的实时优先级 |
sched_setpararn() | 设置一个进程的实时优先级 |
sched_yield() | 自愿放弃处理器而不阻塞 |
sched_get_ priority_min() | 获得一种镶略的最小实时优先级 |
sched_get_ priority_max() | 获得-种锁赂的般大实时优先级 |
sched_rr_get_intcrval() | 获得时间片轮转策略的时间片值 |
sched_setaffinity() | 设置进程的CPU 亲和力掩码 |
sched_getaffinity() | 获得进程的CPU 亲和力掩码 |
Linux进程是抢占式的。
注意:
时间片不能太长也不能太短。
对时间片大小的选择始终是一种折衷。Linux 采取单凭经验的方撞,即选择尽可能长、同时能保持良好响应时间的时间片。
Linux2.6调度算法相比早期Linux版本的优点:
每个Linux进程总按照下面调度类型被调度:
每个普通进程都有它自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间调度的程度。内核用从100 (最高优先级)到139 (最低优先级)的数表示普通进程的静态优先级。
新进程继承其父进程的静态优先级。不过,通过把某些“nice 值”传递给系统调用nice()和setpriority()(参见本章稍后“与调度相关的系统调用” 一节),用户可改变自己拥有的进程的静态优先级。
静态优先级决定了进程基本事件片,公式如下:
基本时间片(单位 m s ) = { ( 140 − 静态优先级 ) × 20 若静态优先级 < 120 ( 140 − 静态优先级 ) × 20 若静态优先级 ≥ 120 (1) 基本时间片(单位ms)= \begin{cases} (140-静态优先级) \times 20 & 若静态优先级<120\\ (140-静态优先级) \times 20 & 若静态优先级\geq120 \end{cases} \tag{1} 基本时间片(单位ms)={(140−静态优先级)×20(140−静态优先级)×20若静态优先级<120若静态优先级≥120(1)
可见,静态优先级较高的进程获得更长CPU时间。表7-2说明拥有最高、默认和最低静态优先级的普通进程,其静态优先级、基本时间片及nice值(表中还列出了交互式的δ值和睡暇时间的极限值,在本章稍后给予说明)。
说明 | 静态优先级 | nice值 | 基本时间片 | 交互式的δ值 | 睡眠时间的极限值 |
---|---|---|---|---|---|
最高静态优先级 | 100 | -20 | 800ms | -3 | 299ms |
高静态优先级 | 110 | -10 | 600ms | -1 | 499ms |
缺省静态优先级 | 120 | 0 | 100ms | +2 | 799ms |
低静态优先级 | 130 | +10 | 50ms | +4 | 999ms |
最低静态优先级 | 139 | +19 | 5ms | +6 | 1199ms |
动态优先级取值100(最高优先级)~139(最低优先级)。动态优先级是调度程序选择新进程来运行时使用的数。其与静态优先级关系如下:
动态优先级 = m a x ( 100 , m i n ( 静态优先级 − b o n u s + 5 , 139 ) ) (2) 动态优先级=max(100, min(静态优先级 - bonus + 5, 139)) \tag{2} 动态优先级=max(100,min(静态优先级−bonus+5,139))(2)
bonus取值0~10,取值依赖于进程平均睡眠时间。
平均睡眠时间是进程在睡眠状态消耗的平均纳秒数。进程运行中平均睡眠时间递减,最终平均睡眠时间用于小于1s。表7-3说明平均睡眠时间和bonus的关系(表中时间片粒度在稍后讨论)。
平均睡眠时间 | bonus | 粒度 |
---|---|---|
[0,100ms) | 0 | 5120 |
[100ms, 200ms) | 1 | 2560 |
[200ms, 300ms) | 2 | 1280 |
[300ms, 400ms) | 3 | 640 |
[400ms, 500ms) | 4 | 320 |
[500ms, 600ms) | 5 | 160 |
[600ms, 700ms) | 6 | 80 |
[700ms, 800ms) | 7 | 40 |
[800ms, 900ms) | 8 | 20 |
[900ms, 1000ms) | 9 | 10 |
1s | 10 | 10 |
调度程序使用平均睡眠时间区分交互式进程和批处理进程。满足以下公式,被视为交互式进程:
动态优先级 ≤ 3 × 静态优先级 / 4 + 28 (3) 动态优先级\leq3\times静态优先级/4+28 \tag{3} 动态优先级≤3×静态优先级/4+28(3)
相当于
b o n u s − 5 ≥ 静态优先级 / 4 − 28 bonus-5\geq静态优先级/4-28 bonus−5≥静态优先级/4−28
”静态优先级/4-28“为交互式的δ,见表7-2。
为了避免进程饥饿,当一个进程用完他的时间片是,应被还没用完时间片的低优先级进程取代。为了实现此机制,调度程序维持两个不想交的可运行进程的集合。
调度程序试图提升交互是进程的性能。原则如下:
实时进程与实时优先级相关,取值1(最高优先级)~99(最低优先级)。调度程序总让优先级高的进程运行,实时进程总是被当成活动进程。
若多个可运行的实时进程具有相同最高优先级,调度程序选择第一个出现在与本地CPU运行队列响应链表的进程(参见第三章“TAS K_RUNNI NG状态的进程链表”)。只有下述事件之一发生时,实时进程会被另一进程取代:
当系统调用nice()和setpriority()用于基于时间片轮转的实时进程时,不改变实时进程的优先级而会改变其基本时间片的长度。实际上.基于时间片轮转的实时进程的基本时间片的长度与实时进程的优先级无关,而依赖于进程的静态优先级, 关系见参见“普通进程的调度”一节中的公式(1)。
进程链表链接所有的进程描述符,而运行队列链表链接所有的可运行进程(也就是处于TASK_RUNNING 状态的进程)的进程描述符,swapper 进程(idle进程)除外。
系统中的每个CPU都有它自己的运行队列,所有的runqueue结构存放在runqueues 每CPU变量中。
宏this_rq()产生本地CPU运行队列的地趾,而宏cpu_rq(n)产生索引为n的CPU的运行队列的地址。
表7-4 列出了runqueue数据结构所包括的字段。
类型 | 名称 | 说明 |
---|---|---|
spinlock_t | lock | 保护进程链表的自旋锁 |
unsigned long | nr_running | 运行队列链表中可运行进程的数量 |
unsigned long | cpu_load | 基于运行队列中进程的平均数量的CPU负载因子 |
unsigned long | nr_switches | CPU执行进程切换的次数 |
unsigned long | nr_uninterruptible | 先前在运行队列链表中而现在睡眠在 TASK_UNINTERRUPTIBLE状态的 进程的数量(对所有运行队列来说, 只有这些字段的总数才是有意义的) |
unsigned long | expired_timestamp | 过期队列中最老的进程被插入队列的时间 |
unsigned long long | timestamp_last_tick | 最近一次定时器中断的时间戳的值 |
task_t * | curr | 当前正在运行进程的进程描述符指针 (对本地CPU,它与current相同) |
task_t * | idle | 当前CPU(this CPU)上swapper进程的进程描述符指针 |
struct mm_struct * | prev_mm | 在进程切换期间用来存放被替换进程的内存描述符的地址 |
prio_array_t * | active | 指向活动进程链表的指针 |
prio_array_t * | expired | 指向过期进程链表的指针 |
prio_array_t[2] | arrays | 活动进程和过期进程的两个集合 |
int | best_expired_prio | 过期进程中静态优先级最高的进程(权值最小) |
atomic_t | nr_iowait | 先前在运行队列的链表中而现在正等 待磁盘I/O操作结束的进程的数量 |
struct sched_domain * | sd | 指向当前CPU的基本调度域 |
int | active_balance | 如果要把一些进程从本地运行队列迁 移到另外的运行队列(平衡运行队 列),就设置这个标志 |
int | push_cpu | 未使用 |
task_t * | migration_thread | 迁移内核线程的进程描述符指针 |
struct list_head | migration_queue | 从运行队列中被删除的进程的链表 |
下图7-1表示runqueue中最重要的字段,即与可运行进程的链表相关的字段。prio_array_t参见第三章的表3-2。
调度程序通过交换active和expired字段内容以实现活动进程与过期进程的周期性切换。
表7-5列举与调度程序相关的进程描述符字段
类型 | 名称 | 说明 |
---|---|---|
unsigned long | thread_info->flags | 存放TIF_NEED_RESCHED 标志,如果必须调用调度 程序,则设置该标志(见第四章“从中断和异常返回” 一节) |
unsigned int | thread_info->cpu | 可运行进程所在运行队列的CPU逻辑号 |
unsigned long | state | 进程的当前状态(见第三章“进程状态”一节) |
int | prio | 进程的动态优先级 |
int | static_prio | 进程的静态优先级 |
struct list_head | run_list | 指向进程所属的运行队列链表中的下一个和前一个元素 |
prio_array_t * | array | 指向包含进程的运行队列的集合prio_array_t |
unsigned long | sleep_avg | 进程的平均睡眠时间 |
unsigned long long | timestamp | 进程最近插入运行队列的时间,或涉及本进程的最近一次 进程切换的时间 |
unsigned long long | last_ran | 最近一次替换本进程的进程切换时间 |
int | activated | 进程被唤醒时所使用的条件代码 |
unsigned long | policy | 进程的调度类型(SCHED_NORMAL、SCHED_RR或 SCHED_FIFO) |
cpumask_t | cpus_allowed | 能执行进程的CPU的位掩码 |
unsigned int | time_slice | 在进程的时间片中还剩余的时钟节拍数 |
unsigned int | first_time_slice | 如果进程肯定不会用完其时间片,就把该标志设置为1 |
unsigned long | rt_priority | 进程的实时优先级 |
当新进程被创建的时候,由于copy_process()调用的函数sched_fork()用下述方法设置current 进程(父进程)和p进程(子进程) 的time_slice 字段:
p->time_slice = (current->time_slice + 1) >> 1;
current->time_slice >>= 1;
父进程剩余的节拍数被划分成两等份: 一份给父进程,另一份给子进程。这样做是为了避免用户通过下述方法获得无限的CPU时间:父进程创建一个运行相同代码的子进程,并随后杀死自己,通过适当地调节创建的速度, 子进程就可以总是在父进程过期之前获得新的时间片。因为内核不奖赏创建,所以这种编程技巧不起作用。
如果父进程的时间片只剩下一个时钟节拍,则划分操作强行把current->time_slice置为0,从而耗尽父进程的时时间片。这种情况下,copy_process()把current->time_slice重新置为1 ,然后调用scheduler_tick()递减该字段(见下一节)。
函数copy_process()也初始化子进程描述符中与进程调度相关的几个字段:
p->first_time_slice = 1;
p->timestamp = sched_clock();
因为子进程没有用完它的时间片(如果一个进程在它的第一个时间片内终止或执行新的程序,就把子进程的剩余时问奖励给父进程),所以first_time_slice标志被置为1。用函数sched_clock()所产生的时间戳的值初始化timestamp字段:实际上,函数sched_clock()返回被转化成纳秒的64位寄存器TSC[见第六章“时间戳计数器(TSC)”一节]的内容。
调度程序依赖的最重要函数如下
函数名 | 函数说明 |
---|---|
scheduler_tick() | 维持当前最新的time_slice计数器 |
try_to_wake_up() | 唤醒睡眠进程 |
recalc_task_prio() | 更新进程的动态优先级 |
schedule() | 选择要被执行的新进程 |
load_balance() | 维持多处理器系统中运行队列的平衡 |
第六章“更新本地CPU统计数”一节中说明:每次时钟节拍到来时,scheduler_tick()如何被调用以执行与调度相关的操作的。它执行步骤如下:
若current进程是SCHED_FIFO的实时进程
scheduler_tick()什么都不做。因为,current进程不可能被比起优先级低或相等的进程抢占。
若current进程是SCHED_RR的实时进程
scheduler_tick()递减其时间片计数器并检查时间片是否用完;若发现时间片用完,执行以下操作已达到抢占当前进程的目的。如下:
if (current->policy == SCHED_RR && !--current->time_slice) {
current->time_slice = task_timeslice(current); //检查进程进程优先级,并根据“普通进程的调度”一节中的公式(1)返回相应的基本时间片
current->first_time_slice = O; //该标识被fork()系统调用服务例程中的copy_process()设置,并在进程的第一个时间片刚一用完时立刻清0
set_tsk_need_resched(current); //设置进程TIF_NEED_RESCHED标志
// 以下两步把进程描述符移到与当前进程优先级相应的运行队列活动链表的尾部
list_del(¤t->run_list);
list_add_tail(¤t->run_list,
this_rq()->active->queue + current->prio);
}
若current进程是SCHED_NORMAL的普通进程
scheduler_tick()执行以下操作:
current->prio = effective_prio(current); //读current的static_prio和sleep_avg字段,根据前面“普通进程的调度”一节的公式(2)计算进程的动态优先级
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
if (!this_rq()->expired_timestamp)
this_ rq()->expired_timestamp = jiffies;
// TASK_INTERACTIVE宏返回1条件:前面“普通进程的调度” 一节的公式(3)识别进程是一个交互式进程
// EXPIRED_STARVING宏返回1条件:运行队列中的第一个过期进程的等待时间已经超过1000 个时钟节拍乘以运行队列中的可运行进程数加1
// 或当前进程的静态优先级大于一个过期进程的静态优先级
if (!TASK_INTERACTIVE (current) || EXPIRED_STARVING(this_rq()) {
enqueue_task(current, this_rq()->expired);
if (current->static_prio < this_rq()->best_expired_prio)
this_rq()->best_expired_prio = current ->static_prio;
} else
enqueue_task(current, this_rq()->active) ;
// 宏TIMESLICE_GRANULARITY产生“系统中CPU的数量”与“成比例的常量”的乘积给当前进程的bonus(见表7-3)。
//具有高静态优先级的交互式进程,其时间片被分成大小为TIMESLICE_GRANULAR ITY的几个片段,以使这些进程不会独占CPU.
if (TASK_INTERACTIVE(p) &&
!((task_timeslice(p) - p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
(p->time_slice >= TIMESLICE_GRANULARITY(p)) &&
(p->array == rq->activce)) (
list_del(¤t->run_list);
llst_add_tail(¤t->run_llst,
this_rq()->active->queue + current->prio);
set_tsk_need_resched(p);
}
try_to_wake_up()函数通过把进程状态设置为TASK _RUNNING,并把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程。参数有:
流程如下:
调用函数task_rq_lock()禁用本地中断,并获得最后执行进程的CPU(它可能不同于本地CPU)所拥有的运行队列rq的锁。CPU的逻辑号存储在p->thread_info->cpu字段。
检查进程的状态p->state是否属于被当作参数传递给函数的状态掩码state,如果不是,就跳转到第9 步终止函数。
如果p->array字段不等于NULL,那么进程已经属于某个运行队列,因此跳转到第8 步。
多处理器系统中,该函数检查要被唤醒的进程是否应该从最近运行的CPU的运行队列迁移到另外一个CPU的运行队列。实际上,函数就是根据一些启发式规则选择一个目标运行队列。例如:
执行完这一步, 函数已经确定了目标CPU和对应的目标运行队列rq,前者将执行被唤醒的进程,后者就是进程插入的队列。
如果进程处于TASK_UNINTERRUPTIBLE状态,函数递减目标运行队列的nr_uninterruptible字段,并把进程描述符的p->activated字段设置为-1。参见后面的“recalc_task_prio()函数”一节对activated 字段的说明。
调用activate_task()函数,它依次执行下面的子步骤:
now = (sched_clock() - this_rq()->timestamp_last_tick)
+ rq->timest.amp_last_tick;
enqueue_task(p, rq->active);
rq ->nr_running++;
如果目标CPU不是本地CPU,或者没有设置sync标志,就检查可运行的新进程的动态优先级是否比rq 运行队列中当前进程的动态优先级高(p->prio < rq->curr->prio);如果是,就调用resched_task()抢占rq->curr。
把进程的p->state 字段设置为TASK_RUNNING状态。
调用task_rq_unlock()来打开rq运行队列的锁并打开本地中断。
返回1(成功唤醒进程)或0(进程没有被唤醒)。
函数更新进程的平均睡眠时间和动态优先级。接收参数如下:
流程如下:
p->prio = effective_prio(p);//函数在本章前面“scheduler_tick()函数"一节讨论过
函数实现调度程序。其任务是从运行队列的连表中找到一个进程,并随后将CPU分配给这个进程。
若current进程不能获得必要的资源而要立即被阻塞,可直接调用调度程序。步骤如下:
内核例程反复检查进程需要的资源是否可用,若不可用,调用schedule()把CPU分配给其他进程。当调度程序再次把CPU分给此进程时,要重新检查资源的可用性。
许多执行长迭代任务的设备驱动程序也直接调用调度程序。每次迭代循环时,驱动程序检查TIF_NEED_RESCHED标志,若需要调用schedule()自动放弃CPU。
可以把current的进程TIF_NEED_RESCHED 标志设置为1,而以延迟方式调用调度程序 。由于总在恢复用户态进程的执行之前检查这个标志的值(见第四章从“中断和异常返回”一 节),所以schedule()将在不久之后的某个时间被明确地调用 。
以下是延迟调用调度程序的典型例子:
函数的任务之一是用另外一个进程来替换当前正在执行的进程。因此,该函数的关键结果是设置next变量,使它指向被选中取代current的进程。如果系统中没有优先级高于current进程的可运行进程,那么最终next与current相等,不发生任何进程切换。步骤如下:
schedule()函数在一开始先禁用内核抢占,并初始化一些局部变量:
need_resched:
preempt_disable();
prev = current;
rq = this_rq();
schedule()保证prev不占用大内核锁(参见第五章“大内核锁”一节):
// schedule()不改变lock_depth字段的值;
// 当prev恢复执行的时候,如果该字段的值不为负数,则prev重新获得kernel_flag自旋锁。
// 因此,通过进程切换会自动释放和重新获得大内核锁。
if (prev->lock_depth >= O)
up(&kernel_sem);
调用sched_lock()读取TSC,并其转候成纳秒,所获得的时间戳存放在局部变量now 中。然后,schedule()计算prev所用的CPU时间片长度:
now = sched_clock();
run_time = now - prev->timestamp;
if (run_time > 1000000000)
run_time = 1000000000; // 通常使用限制在1s的时间
run_time的值用来限制进程对CPU的使用。不过,鼓励进程有较长的平均睡眠时间:
run_time /= (CURRENT_BONUS(prev) ? : 1); // CURRENT_BONUS返回0~10之间的值,它与进程平均睡眠时间成正比
开始寻找可运行进程之前, schedule()必须关掉本地中断,并获得所要保护的运行队列的自旋锁:
spin_lock_irq(&rq->lock);
正如在第三章“进程终止”一节中所描述的,prev可能是一个正在被终止的进程。为确认这个事实,schedule()检查PF_DEAD标志:
if (prev->flags & PF_DEAD)
prev->state = EXIT_DEAD;
接下来schedule()检查prev的状态。
// 如果prev不是可运行状态,
// 而且它没有在内核态被抢占(见第四章“从中断和异常返回”一节),
// 则从运行队列删除prev进程。
if (prev->state != TASK_RUNNING &&
!(preempt_count() & PREEMPT_ACTIVE)) {
// 如果prev是非阻塞挂起信号,而且状态为TASK_INTERRUPTIBLE,
// 函数就把该进程的状态设置为TASK_RUNNING,并将它插入运行队列。
// 这个操作与把处理器分配给prev是不同的,它只是给prev一次披选中执行的机会。
if (prev->state == TASK_INTERRUPTIBLE && signal_pending(prev))
prev->state = TASK_RUNNING;
else {
if (prev->state == TASK_UNINTERRUPTIBLE)
rq->nr_uninterruptible++;
deactivate_task(prev, rq) ;
}
}
函数deactivate_task()从运行队列中删除出进程:
rq->nr_running--;
dequeue_task(p, p->array);
p->array = NULL;
schecule()检查运行队列中剩余的可运行进程数。
若运行队列中有可运行的进程,流程如下:
if (rq->nr_running) {
// dependent_sleeper()函数,绝大多数情况下立即返回0。
// 如果内核支持超线程技术(见本章稍后“多处理器系统中运行队列的平衡” 一节),
// 函数检查要被选中执行的进程,其优先级是否比已在相同物理CPU的某个逻辑CPU上运行的兄弟进程的优先级低;
// 在这种特殊的情况下,schedule()拒绝选择低优先级的进程,而去执行swapper进程。
if (dependent_sleeper(smp_processor_id(), rq)) {
next = rq->idle ;
goto switch_tasks;
}
}
若运行队列中没有可运行的进程,流程如下:
if (!rq->nr_running) {
// idle_balance()从另外一个运行队列迁移一些可运行进程到本地运行队列中,在稍后的“load_balance()函数”一节中对它进行说明
idle_balance(smp_processor_id(), rq);
if (!rq->nr_running) { //如果idle_balance()没有成功地把进程迁移到本地运行队列中
next = rq->idle;
rq->expired_timestamp = 0;
// wake_sleeping_dependent()重新调度空闲CPU(每个运行swapper进程的CPU)中的可运行进程
wake_sleeping_dependent(smp_processor_id(), rq);
// 在单处理器系统中,或者当把进程迁移到本地运行队列的种种努力都失败的情况下,
// 函数就选择swapper进程作为next进程并继续进行下一步骤
if (!rq->nr_running)
goto switch_tasks;
}
}
假设schedule()函数已经肯定运行队列中有一些可运行的进程,现在它必须检查这些可运行进程中是否至少有一个进程是活动的。流程如下:
array = rq->active;
// 检查这些可运行进程中是否至少有一个进程是活动的
if (!array->nr_active) { // 如果没有,函数就交换运行队列数据结构的active和expired字段的内容
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = O;
rq->best_expired_prio = 140;
}
现在可以在活动的prio_array_t数据结构中搜索一个可运行进程(参见第三章“标识一个进程”一节)。
// 搜索活动进程集合位掩码的第一个非0位。
// 当对应的优先级链表不为空时,就把位掩码的相应位置1。
// 第一个非0位的下标对应包含最佳运行进程的链表。
idx = sched_find_first_bit(array->bitmap); // 函数基于bsfl汇编语言指令的,它返回32位字中被设置为1的最低位的位下标。
// 返回该链表的第一个进程描述符
next = list_entry(array->queue[idx].next, task_t, run_list); // next 现在存放将取代prev的进程描述符指针
schedule()函数检查next->activated字段,该字段的编码值表示进程在被唤醒时的状态。表7-6列举进程描述符中activated字段的含义。
值 | 说明 |
---|---|
0 | 进程处于TASK_RUNNING状态 |
1 | 进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,而且正在被系统 调用服务例程或内核线程唤醒 |
2 | 进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,而且正在被中断 处理程序或可延迟函数唤醒 |
-1 | 进程处于TASK_UNINTERRUPTIBLE状态而且正在被唤醒 |
检查流程如下:
// 如果next是一个普通进程,而且它正在从TASK_INTERRUPTIBLE或TASK_STOPPED状态被唤醒
// 调度程序就把自从进程插入运行队列开始所经过的纳秒数加到进程的平均睡眠时间中
// 换言之,进程的睡眠时间被增加了,以包含进程在运行队列中等待CPU所消耗的时间
if (next->prio >= 100 && next->activated > 0) {
// 由于交互式进程更可能被异步事件(按键等)而不是同步事件唤醒。
// 若进程被中断处理程序和可延迟函数唤醒,调度程序会增加全部运行队列的等待时间;
// 若进程被系统调用服务例程和内核线程唤醒,调度程序只增加等待部分的时间。
unsigned long long delta = now - next->timestamp;
if (next->activated == 1)
delta = (delta * 38) / 128;
array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta);
enqueue_task(next, array);
}
next->activated = 0;
现在schedule()要让next进程投入运行。流程如下:
内核将立刻访问next进程的thread_info数据结构,其地址存放在next进程描述符的接近顶部的位置。
switch_tasks:
prefetch(next); // prefetch宏提示CPU控制单元把next 的进程描述符第一部分字段的内容装入硬件高速缓存。
// 这一点改善了schedule()的性能,因为对于后续指令的执行,数据是并行移动的
在替代prev之前,调度程序应该完成一些管理的工作:
clear_tsk_need_resched(prev); // 清除prev的TIF_NEED_RESCHED标志,以防以延迟方式调用schedule()
rcu_qsctr_inc(prev->thread_info->cpu); // 函数记录CPU正在经历静止状态[参见第五章“读-拷贝-更新(RCU)” 一节]
schedule()必须减少prev的平均睡眠时间,并把它补充给进程所使用的CPU时间片,随后更新进程时间戳:
prev->sleep_avg -= run_time;
if ((long)prev->sleep_avg <= 0)
prev->sleep_avg = O;
prev->timestamp = prev->last_ran = now;
若prev和next是同一进程,函数不做进程切换。
if (prev == next) {
spin_unlock_irq(&rq->lock);
goto finish_schedule;
}
若prev和next是不同的进程,进程切换确实发生了。
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
prev = context_switch(rq, prev, next); // 函数建立next的地址空间
如将在第九章“内核钱程的内存描述符”中将要看到的,进程描述符的active_mm字段指向进程所使用的内存描述符,而mm字段指向进程所拥有的内存描述符。对于一般的进程,这两个字段有相同的地址,但是,内核线程没有它自己的地址空间,而且它的mm字段总是被设置为NULL。
context_switch()函数实现以下切换流程:
if (!next->mm) {
next->active_mm = prev->active_mm;
atomic_inc(&prev->active_mm->mm_count);
enter_lazy_tlb(prev- >active_mm, next);
}
if (next->mm)
switch_mm(prev->active_mm, next->mm, next);
if (!prev->mm) {
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
context_switch()终于可以调用switch_to()执行prev和next之间的进程切换了(见第三章“执行进程切换” 一节):
switch_to(prev, next , prev);
return prev;
schedule()函数中在switch_to宏调用之后紧接着的指令并不由next进程立即执行,而是稍后当调度程序又选择prev执行时由prev执行。然而,在那个时刻,prev局部变量并不指向我们开始描述schedule()时所替换出去的原来那个进程,而是指向prev被调度时由prev替换出的原来那个进程(如果你被搞糊涂了,请回到第三章阅读“执行进程切换”一节)。
barrier(); // 产生一个代码优化屏障(见第五章“优化和内存屏障”一节)
finish_task_switch(prev); // 说明详见下段代码
// 如果prev是一个内核线程,那么运行队列的prev_mm字段存放借给prev的内存描述符的地址
mm = this_rq()->prev_mm;
this_rq()->prev_mm = NULL;
prev_task_flags = prev->flags;
spin_unlock_irq(&this_rq()->lock); // 释放运行队列的自旋锁并打开本地中断
if (mm)
mmdrop(mm); // 第九章将要看到mmdrop()减少内存描述符的使用计数器
// 如果该计数器等于O了(可能是因为prev是一个僵死进程,
// 函数还要释放与页表相关的所有描述符和虚拟存储区
if (prev_task_flags & PF_DEAD) // 检查prev是否是一个正在从系统中被删除的僵死任务(见第三章”进程终止”一节)
put_task_struct(prev); // 若是,释放进程描述符引用计数器,并撤消所有其余对该进程的引用(见第三章“进程删除”一节)。
finish_schedule:
prev = current;
if (prev->lock_depth >= 0)
__reacquire_kernel_lock(); // 在需要的时候重新获得大内核锁
preempt_enable_no_resched(); // 重新启用内核抢占
if (test_bit(TIF_NEED_RESCHED, ¤t_thread_info()->flags)) // 检查是否一些其他的进程已经设置了当前进程的TIF_NEED_RESCHED标志
goto need_resched; // 如果是,整个schedule()函数量新开始执行
return; // 否则,函数结束
多处理器机器具有很多不同的风格,且调度程序的实现随硬件特征的不同而不同。关注下面3种多处理器机器:
由上节可见:
针对上述设计:
针对上述缺点,内核周期性地检查运行队列的工作量是否平衡,并在需要的时候,把一些进程从一个运行队列迁移到另一个运行队列。但是,为了从多处理器系统获得最佳性能,负载平衡算法应考虑系统CPU的拓扑结构。
从内核2.6.7版本开始,Linux 提出一种基于“调度域”慨念的复杂的运行队列平衡算法。而调度域这一慨念使得这种算法能够容易适应各种已有的多处理器体系结构(甚至那些基于新近出现的“多核”微处理器的体系结构)。
调度域(scheduling domain)实际上是一个CPU集合,它们的工作量应当自内核保持平衡。调度域采取分层的组织形式,工作量的平衡采用如下有效方式来实现。
图7-2 给出了3个调度域分层实例,对应3 种主要的多处理器机器体系结构。
调度域相关数据结构说明如下:
为了保持系统中运行队列的平衡,每次经过一次时钟节拍,scheduler_tick()就调用rebalance_tick()函数。
参数如下:
函数流程如下:
load_balance()函数检查是否调度域处于严重的不平衡状态。更确切地说,它检查是否可以通过把最繁忙的组中的一些进程迁移到本地CPU的运行队列来减轻不平衡的状况。如果是,函数尝试实现这个迁移。
参数如下:
函数流程如下:
获取this_rq->lock自旋锁。
调用find_busiest_group()函数分析调度域中各组的工作量,返回最繁忙组的sched_group描述符的地址。
这个过程试图过滤掉统计工作量中的波动。
如果find_busiest_group()在调度域中没有找到既不包括本地CPU又非常繁忙的组,就释放this_rq->lock自旋锁,调整调度域描述符的参数,以延迟本地CPU下一次对load_balance()的调度。然后函数终止。
调用find_busiest_queue()函数以查找在第2步中找到的组中最繁忙的CPU,函数返回相应运行队列的描述符地址busiest。
获取5中最繁忙CPU的自旋锁busiest->lock自旋锁。为了避免死锁,这一操作必须非常小心:首先释放this_rq->lock,然后通过增加CPU下标获得这两个锁。
调用move_tasks()函数,尝试从最繁忙的运行队列中把一些进程迁移到本地运行队列this_rq中(见下节)。
如果函数move_task()没有成功地把某些进程迁移到本地运行队列,那么调度域还是不平衡。执行如下操作:
释放busiest->lock和this_rq->lock自旋锁。
函数结束。
move_tasks()函数把进程从源运行队列迁移到本地运行队列。
参数如下:
函数流程如下:
首先分析busiest运行队列的过期进程,从优先级高的进程开始。
扫描完所有过期进程后,扫描busiest运行队列的活动进程。
对所有的候选进程调用can_migrate_task(),如果下列条件都满足,则can_migrate_task()返回1:
如果can_migrate_task()返回1,move_tasks()就调用pull_task()函数把候选进程迁移到本地运行队列中。pull_task()流程如下:
作为一般原则,总是允许用户降低其进程的优先级。然而,如果他们想修改属于其他某一用户进程的优先级,或者如果他们想增加自己进程的优先级,那么,他们必须拥有超级用户的特权。
nice()系统调用允许进程改变它们的基本优先级。包含在increment参数中的整数值用来修改进程描述符的nice字段。
sys_nice()服务例程处理nice()系统调用。说明如下:
nice()系统调用只维持向后兼容,它已经披下面描述的setpriority()系统调用取代。
nice()系统调用只影响调用它的进程,而另外两个系统调用getpriority()和setpriority()则作用于给定组中所有进程的基本优先级。
内核对这两个系统调用的实现是通过sys_getpriority()和sys_setpriority()服务例程完成的。这两个服务例程本质上作用于一组相同的参数:
第十将看到,只有当出现了某些错误时,系统调用才返回一个负值。因此,getpriority()不返回-20 ~ +19之间正常的nice值,而是1 ~ 40之间的一个非负值。
sched_getaffinity()和sched_setaffinity()系统调用分别返回和设置CPU进程亲和力掩码,即允许执行进程的CPU的位掩码。该掩码存放在进程描述符的cpus_allowed字段中。
后续介绍的一组系统调用允许进程改变自己的调度规则,尤其是可以变为实时进程。进程为了修改任何进程(包括自己)的描述符的rt_priority和policy 字段,必须具有CAP_SYS_NICE权能。
sched_ yield()系统调用允许进程在不被挂起的情况下自愿放弃CPU,进程仍然处于TASK_RUNNING状态,但调度程序把它放在运行队列的过期进程集合中(如果进程是普通进程),或放在运行队列链表的末尾(如果进程是实时进程)。随后调用schedule()函数。在这种方式下,具有相同动态优先级的其他进程将有机会运行。这个调用主要由SCHED_FIFO实时迸程使用。
sched_ get_priority_min()和sched_ get_priority_max()系统调用分别返回最小和最大实时静态优先级的值,这个值由policy参数所标识的调度策略来使用。
如果current是实时进程,则sys_sched_get_priority_min()服务例程返回1,否则返回0。
如果current是实时进程,则sys_sched_get_priority_max()服务例程返回99(最高优先级),否则返回0。
sched_rr_get_interval()系统调把参数pid表示的实时进程的轮转时间片写入用户态地址空间的一个结构中。如果pid等于0,系统调用就写当前进程的时间片。
sys_sched_rr_get_interval()服务例程同样调用find_process_by_pid()检索与pid相关的进程描述符。然后,把所选中进程的基本时间片转换为秒数和纳秒数,并把它们拷贝到用户态的结构中。通常,FIFO实时进程的时间片等于0。