深入理解Linux内核--进程调度

本章讨论进程调度,主要关心什么时候进行进程切换,选择哪个进程来运行。调度策略,从理论上介绍Linux进程调度选择。调度算法,为实现策略的数据结构和算法。相关系统调用,影响进程调度的系统调用。为叙述简单,仍以80x86为例。假定系统采用统一内存访问模型,系统时钟设定为1ms。

调度策略

目标:
1.进程响应时间尽可能快
2.后台作业吞吐量尽可能高
3.尽可能避免进程饥饿
4.低优先级,高优先级进程的需要尽可能调和

调度策略:决定什么时候以怎样方式选择一个新进程运行。Linux的调度基于分时技术:多个进程以"时间多路复用"方式运行。CPU时间分成片,每个可运行进程分配一片。如当前运行进程的时间片或时限到期,进程还没结束,进程切换就可以发生。

分时以来定时中断。调度策略也是根据进程的优先级对它们进行分类。每个进程都与一个值关联,这个值表示把进程如何适当地分配给CPU。Linux中,进程的优先级是动态的。调度程序跟踪进程在做什么,周期性地调整它们的优先级。此方式下, 较长时间间隔内没使用CPU的进程,通过动态地增加它们的优先级来提升它们。对已经在CPU上运行了较长时间的进程,通过减少它们的优先级来处罚它们。谈及有关调度问题时,传统上把进程分类为"I/O受限"或"CPU受限"。另一种分类把进程区分为三类:

交互式进程
	花比较多时间等待键盘和鼠标操作。如shell,文本编辑程序,图形应用程序。
批处理进程
	后台运行。如程序设计语言的编译程序,数据库搜索引擎及科学计算。
实时进程
	绝不会被低优先级进程阻塞,应该有一个短的响应时间。
	如视频和音频应用程序,机器人控制程序,从物理传感器上收集数据的程序。

Linux 2.6调度程序实现了基于进程过去行为的启发式算法,以确定进程应被当作交互式进程还是批处理进程。

系统调用 说明
nice 改变一个普通进程的静态优先级
getpriority 获得一组普通进程的最大静态优先级
setpriority 设置一组普通进程的静态优先级
sched_getscheduler 获得一个进程的调度策略
sched_setscheduler 设置一个进程的调度策略和实时优先级
sched_getparam 获得一个进程的实时优先级
sched_setparam 设置一个进程的实时优先级
sched_yield 自愿放弃处理器而不阻塞
sched_get_priority_min 获得一种策略的最小实时优先级
sched_get_priority_max 获得一种策略的最大实时优先级
sched_rr_get_interval 获得时间片轮转策略的时间片值
sched_setaffinity 设置进程的CPU亲和力掩码
sched_getaffinity 获得进程的CPU亲和力掩码

进程的抢占

Linux的进程是抢占式的。如进程进入TASK_RUNNING,内核检查它的动态优先级是否大于当前正运行进程的优先级。如是,current的执行被中断,且调度程序选择另一个进程运行。

进程在它的时间片到期时也可被抢占。此时,当前进程thread_info中的TIF_NEED_RESCHED被设置,以便中断处理程序终止时调度程序被调用。

Linux 2,6内核是抢占式的,意味着进程无论是处于内核态还是用户态都可能被抢占。中断返回时处理的调度就是抢占。关闭核心抢占,就是中断返回时,即使有TIF_NEED_RESCHED被设置,也不执行调度。

一个时间片必须持续多长

如果太短,由进程切换引起的额外开销就变得非常高。如平均时间片太长,进程看起来就不再是并发执行。交互式进程相对有较高的优先级,因此,不管时间片多长,它们都会很快地抢占批处理进程。对时间片大小的选择始终是一种折衷。Linux采取单凭经验的方法,即选择尽可能长,同时保持良好响应时间的。

调度算法

每次进程切换时,内核扫描可运行进程链表,计算进程的优先级,再选择"最佳"进程来运行。

Linux 2.6调度算法,在固定的时间内(与可运行的进程数量无关)选中要运行的进程。很好处理了与处理器数量的比例关系,每个CPU都拥有自己的可运行进程队列。新算法较好解决了区分交互式进程,批处理进程的问题。每个Linux进程总是按下面的调度类型被调度:

SCHED_FIFO
	当调度程序把CPU分配给进程时候,它把该进程描述符保留在运行队列链表的当前位置。
	如没其他可运行的更高优先级实时进程,进程就继续用CPU。想用多久用多久。
SCHED_RR
	时间片轮转的实时进程。调度程序把CPU分配给进程时候,把该进程的描述符放在运行队列链表的末尾。
	保证对所有具有相同优先级的SCHED_RR实时进程公平地分配CPU时间。
SCHED_NORMAL
	普通的分时进程。

普通进程的调度

每个普通进程有它自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程和其他普通进程间调度的程度。内核用100(高)到139(低)的数表示普通进程的静态优先级。值越大静态优先级越低。新进程总是继承其父进程的静态优先级。通过把某些"nice值"传递给系统调用nice和setpriority,用户可改变自己拥有的进程的静态优先级。

基本时间片

静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间片长度。静态优先级和基本时间片的关系用下列公式确定:

基本时间片(ms)
若静态优先级 < 120
	(140 - 静态优先级) * 20
若静态优先级 >= 120
	(140 - 静态优先级) * 5
说明 静态优先级 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(低)。动态优先级是调度程序在选择新进程来运行时候使用的数。它与静态优先级的关系用下面的经验公式表示:

动态优先级=max(100, min(静态优先级 - bonus + 5, 139))

bonus是范围0~10的值,值小于5表示降低动态优先级以示惩罚,值大于5表示增加动态优先级以示奖赏。bonus值依赖于进程过去的情况,更准确些,与进程的平均睡眠时间相关。粗略讲,平均睡眠时间是进程在睡眠状态所消耗的平均纳秒数。如,在TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态所计算出的平均睡眠时间是不同的。且,进程在运行过程中平均睡眠时间递减,平均睡眠时间永远不会大于1s。

平均睡眠时间 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
	或者
	bonus - 5 >= 静态优先级 / 4 - 28

静态优先级 / 4 - 28称为交互式的p

总结:
1.优先级越高获得的时间片越大
2.睡眠时间越长,动态优先级在静态优先级基础上越高(值越小)。

活动和过期进程

即使具有较高静态优先级的普通进程获得了较大的CPU时间片,也不应使静态优先级较低的进程无法运行。当一个进程用完它的时间片时,应被还没用完时间片的较低优先级进程取代。

活动进程
	这些进程还没用完它们的时间片
过期进程
	这些可运行进程已用完了它们的时间片。被禁止运行,直到所有活动进程都过期。

用完其时间片的活动批处理进程总是变成过期进程。用完其时间片的交互式进程通常仍是活动进程:调度程序重填其时间片并把它留在活动进程集合中。但,如最老的过期进程已等待了很长时间,或过期进程比交互式进程的静态优先级高,调度程序就把用完时间片的交互式进程移到过期进程集合。

总结:
1.用完其时间片的批处理进程总是变成过期进程
2.用完其时间片的交互式进程,视情况决定是活动进程还是过期进程。
3.优先级越高越容易被视为交互式进程。相同优先级下平均睡眠时间越长,越容易被视为交互式进程。

实时进程的调度

每个实时进程都与一个实时优先级相关。实时优先级是一个范围从1(高)~99(低)的值。调度程序总是让优先级高的进程运行。实时进程总是被当成活动进程。可通过sched_setparam和sched_setscheduler改变实时进程的优先级。

如几个可运行的实时进程具有相同的最高优先级,则调度程序选择第一个出现在与本地CPU的运行队列相应链表中的进程。只有在下述事件之一发生时,实时进程才会被另外一个进程取代:
1.进程被另外一个具有更高实时优先级的实时进程抢占。
2.进程执行了阻塞操作并进入睡眠(处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE)
3.进程停止(处于TASK_STOPPED或TASK_TRACED)或被杀死(处于EXIT_ZOMBIE或EXIT_DEAD)
4.进程通过系统调用调用sched_yield自愿放弃CPU。
5.进程是基于时间片轮转的实时进程,且用完了它的时间片。
当系统调用nice和setpriority用于基于时间片轮转的实时进程时,不改变实时进程的优先级,而会改变其基本时间片的长度。实际上,基于时间片轮转的实时进程的基本时间片的长度与实时进程的优先级无关,而依赖于进程的静态优先级。

调度程序所使用的数据结构

数据结构runqueue

系统中的每个CPU都有它自己的运行队列,所有的runqueue结构存放在runqueues每CPU变量中。宏this_rq产生本地CPU运行队列的地址,宏cpu_rq(n)产生索引为n的CPU的运行队列的地址。

类型 名称 说明
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 当前正运行进程的描述符指针
task_t* idle 当前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 从运行队列中被删除的进程的链表

系统中每个可运行进程属于且只属于一个运行队列。只要可运行进程保持在同一个运行队列中,它就只可能在拥有该运行队列的CPU上执行。可运行进程会从一个运行队列迁移到另一个运行队列。

运行队列的arrays字段是一个包含两个prio_array_t结构的数组。每个数据结构都表示一个可运行进程的集合,并包括140个双向链表头(每个链表对应一个可能的进程优先级),一个优先级位图,一个集合中所包含的进程数量的计数器。

arrays中两个数据结构的作用会发生周期性的变化:活动进程突然变成过期进程,过期进程变成活动进程。调度程序简单地交换运行队列的active和expired字段的内容以完成变化。

进程描述符

类型 名称 说明
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 如进程肯定不会用完其时间片,就设置
unsigned long rt_priority 进程的实时优先级

进程被创建时

p->time_slice = (current->time_slice+1)>>1;
current->time_slice>>=1;

父进程剩余的节拍数被划分成两等份。一份给父进程,一份给子进程。子进程在首个时间片内终止或执行新程序,剩余时间奖励给父进程。

调度程序所使用的函数

scheduler_tick
	维持最新的time_slice计数器
try_to_wake_up
	唤醒睡眠进程
recalc_task_prio
	更新进程的动态优先级
schedule
	选择要被执行的新进程
load_balance
	维持多处理器系统中运行队列的平衡

scheduler_tick

每次时钟节拍到来时,scheduler_tick:
1.把转换为纳秒的TSC当前值存入本地运行队列的timestamp_last_tick。这个时间戳是从sched_clock获得的。
2.检查当前进程是否是本地CPU的swapper进程。如是,
2.1.如本地运行队列除了swapper外,还包括一个可运行的进程,就设置当前进程的TIF_NEED_RESCHED。如内核支持超线程技术,则只要一个逻辑CPU运行队列中的所有进程 都有比 另一个逻辑CPU上已经在执行的进程 有低得多的优先级(两个逻辑CPU对应同一个物理CPU),前一逻辑CPU就可能空闲。超线程下,将2个进程安排在两个不同物理cpu,相比在同一物理cpu的多个逻辑cpu可以更好并发。
2.2.跳到7
3.检查current->array是否指向本地运行队列的活动链表。如不是,设置TIF_NEED_RESCHED。跳到7
4.获得this_rq()->lock
5.递减当前进程的时间片计数器。检查是否已用完时间片。由于进程的调度类型不同,这一步操作也有很大差别。稍后讨论。
6.释放this_rq()->lock
7.调rebalance_tick。保证不同CPU的运行队列包含数量基本相同的可运行进程。

更新实时进程的时间片

如当前进程是FIFO的实时进程,scheduler_tick什么也不做。维持当前进程的最新时间片计数器没意义。如current表示基于时间片轮转的实时进程,scheduler_tick就递减它的时间片计数器并检查时间片是否被用完

if(current->policy == SCHED_RR && !--current->time_slice)
{
	current->time_slice = task_timeslice(current);
	current->first_time_slice = 0;
	set_tsk_need_resched(current);
	list_del(&current->run_list);
	list_add_tail(&current->run_list, this_rq()->active->queue+current->prio);
}

如函数确定时间片用完了,就执行操作以抢占当前进程。
1.调task_timeslice重填进程的时间片计数器
2.scheduler_tick调set_tsk_need_resched设置TIF_NEED_RESCHED
3.把进程描述符移到与当前进程优先级相应的运行队列活动链表尾部。

更新普通进程的时间片

如当前进程是普通进程,scheduler_tick:
1.递减current->time_slice
2.如时间片用完
2.1.调dequeue_task从可运行进程的this_rq()->active集合中删除current指向的进程
2.2.调set_tsk_need_resched设置TIF_NEED_RESCHED
2.3.更新current指向的进程的动态优先级,current->prio = effective_prio(current);
2.4.重填进程的时间片
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
2.5.如本地运行队列的expired_timestamp等于0,就把当前时钟节拍值赋给expired_timestamp
2.6.把当前进程插入活动进程集合或过期进程集合

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);

宏EXPIRED_STARVING检查运行队列中的第一个过期进程的等待时间是否已经超过1000个时钟节拍乘以运行队列中可运行进程数加1。如是,产生1。如当前进程的静态优先级大于一个过期进程的静态优先级。也产生1。
3.如时间片没用完。检查当前进程的剩余时间片是否太长。

if(TASK_INTERACTIVE(p) 
	&& !((task_timeslice(p)-p->time_slice % TIMESLICE_GRANULARITY(p)) 
	&& (p->time_slice >= TIMESLICE_GRANULARITY(p))
	&& (p->array == rq->active))
{
	list_del(&current->run_list);
	list_add_tail(&current->run_list, this_rq()->active->queue+current->prio);
	set_tsk_need_resched(p);
}

基本上,具有高静态优先级的交互式进程,其时间片被分成大小为TIMESLICE_GRANULARITY的几个片段,每次用完一个片段,就重新调度一次。以便时间片太长下,其他活动进程有机会得到执行。

try_to_wake_up

把进程状态置为TASK_RUNNING,把进程插入本地CPU的运行队列来唤醒睡眠或停止的进程。参数:
1.被唤醒进程的描述符指针
2.可被唤醒的进程状态掩码
3.一个标志,用来禁止被唤醒的进程抢占本地CPU上正运行的进程
操作:
1.调task_rq_lock禁用本地中断。获得最后执行进程的cpu所拥有的运行队列rq锁。
2.检查进程状态p->state是否属于被当作参数传递给函数的状态掩码。如不是,跳到9。
3.如p->array不等于NULL。跳到8
4.在多处理器系统中,函数检查要被唤醒的进程是否应该从最近运行的CPU的运行队列迁移到另外一个CPU的运行队列。实际上,函数根据一些启发式规则选择一个目标运行队列。
4.1.如系统中某些CPU空闲,就选择空闲CPU的运行队列。按优先选择当前正执行进程的CPU和本地CPU这种顺序。
4.2.如先前执行进程的CPU的工作量远小于本地CPU的工作量,就选择先前的运行队列作为目标
4.3.如进程最近被执行过,就选择老的运行队列作为目标(可能仍用这个进程的数据填充硬件高速缓存)
4.4.如把进程移到本地CPU以缓解CPU之间的不平衡,目标就是本地运行队列

此时,已经确定了目标CPU和对应的目标运行队列rq。
5.如进程处于TASK_UNINTERRUPTIBLE,递减目标运行队列的nr_uninterruptible,把进程描述符的p->activated置为-1。
6.调active_task
6.1.调sched_clock获取以纳秒为单位的当前时间戳。如目标CPU不是本地CPU,就补偿本地时钟中断的偏差。从而得到准确的目标cpu上的时间戳。

now=(sched_clock()-this_rq()->timestamp_last_tick)+rq->timestamp_last_tick;

6.2.调recalc_task_prio,把进程描述符的指针和上一步计算出的时间戳传递给它。重新计算平均睡眠时间,动态优先级。
6.3.调整p->activated,以便反映从中断唤醒,从非中断唤醒,不可中断睡眠进程唤醒。
6.4.据6.1.算出的时间戳设置p->timestamp
6.5.把进程描述符插入活动进程集合
enqueue_task(p, rq->active);
rq->nr_running++;
7.如目标CPU不是本地CPU,或没设置sync。就检查可运行的新进程的动态优先级是否比rq运行队列中当前进程动态优先级高。如是,就让目标cpu及时发生新的调度。
8.把进程的p->state置为TASK_RUNNING
9.调task_rq_unlock打开rq运行队列的锁并打开本地中断
10.返回1或0

recalc_task_prio

更新进程的平均睡眠时间,动态优先级,接收进程描述符指针p,和由sched_clock计算出的当前时间戳。操作:
1.把min(now - p->timestamp, 10^9)的结果赋给局部变量sleep_time。这样计算出来的是进程的睡眠时间。p->timestamp包含导致进程进入睡眠状态的进程切换的时间戳。sleep_time中存放的是从进程最后一次执行开始,进程消耗在睡眠状态的纳秒数。睡眠时间长时,sleep_time就等于1s
2.如sleep_time不大于0,跳到8。
3.若进程不是内核线程,进程不是从TASK_UNINTERRUPTIBLE被唤醒,进程连续睡眠的时间超过给定的睡眠时间极限。都满足,函数把p->sleep_avg设置为相当于 900个时钟节拍的值。(用最大平均睡眠时间减去一个标准进程的基本时间片长度获得一个经验值)跳8。

睡眠时间极限,进程静态优先级。这些经验规则的目的是保证已经睡眠了很长时间的进程,获得一个预先确定且足够长的平均睡眠时间,以使这些进程能尽快获得服务。
4.执行CURRENT_BONUS计算进程原来的平均睡眠时间的bonus值。如(10 - bonus)大于0,函数用这个值与sleep_time相乘。因为要把sleep_time加到进程的平均睡眠时间上,所以当前平均睡眠时间越短,它增加的就越快。
5.如进程处于TASK_UNINTERRUPTIBLE且不是内核线程,执行下述:
5.1.检查平均睡眠时间p->sleep_avg是否大于或等于进程的睡眠时间极限。如是,把局部变量sleep_time重新置为0,因此不用调整平均睡眠时间,跳6。
5.2.如sleep_time+p->sleep_avg的和大于或等于睡眠时间极限,就把p->sleep_avg置为睡眠时间极限并把sleep_time置为0。通过对进程平均睡眠时间的轻微限制,函数不会对睡眠时间很长的批处理进程给予过多奖赏。
6.把sleep_time加到进程的平均睡眠时间上。
7.检查p->sleep_avg是否超过1000个时钟节拍(以纳秒为单位),如是,函数就把它减到1000个时钟节拍(以纳秒为单位)。
8.更新进程的动态优先级:p->prio=effective_prio§;函数依据p的静态优先级,sleep_avg按前面介绍的规则计算动态优先级。

schedule

从运行队列的链表找到一个进程,随后将CPU分配给这个进程。schedule可由几个内核控制路径调用,可采取直接调用或延迟调用的方式。

从使用角度介绍schedule使用场景–直接调用

如current进程因不能获得必须的资源而要立刻被阻塞,就直接调调度程序。此时,要阻塞进程的内核路径按下述步骤:

1.把current进程插入适当的等待队列
2.把current进程状态改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
3.调schedule
4.检查资源是否可用。如不可用就跳到2。
5.一旦资源可用,就从等待队列中删除current。

从使用角度介绍schedule使用场景–延迟调用

也可把current进程的TIF_NEED_RESCHED标志设置为1,而以延迟方式调用调度程序。由于总是在恢复用户态进程的执行前检查这个标志的值,所以schedule将在不久后的某个时间被明确地调用。

以下是延迟调用调度程序的典型例子:
1.当current进程用完了它的CPU时间片时,由schedule_tick完成schedule的延迟调用。
2.当一个被唤醒进程的优先级比当前进程的优先级高,由try_to_wake_up完成schedule的延迟调用
3.当发出系统调用sched_setscheduler时。

进程切换之前schedule所执行的操作

schedule任务之一是用另外一个进程来替换当前正执行的进程。该函数的关键结果是设置一个叫next的变量,使它指向被选中的进程,该进程将取代current进程。如系统中没优先级高于current进程的可运行进程,则最终next与current相等,不发生任何进程切换。

need_resched:
	preempt_disable();
	prev = current;
	rq = thi_rq();

下一步,schedule要保证prev不占用大内核锁

if(prev->lock_depth >= 0)
	up(&kernel_sem);

注意,schedule不改变lock_depth;进程切换会自动释放和重新获取大内核锁。
调sched_clock以读取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之间的值,它与进程的平均睡眠时间是成比例的。这样 平均睡眠时间越长,有效的run_time就越小。开始寻找可运行进程前,schedule需关掉本地中断,并获得所要保护的运行队列的自旋锁。

spin_lock_irq(&rq->lock);

prev可能是一个正被终止的进程。为确认这个事实,schedule检查PF_DEAD标志:

if(prev->flags & PF_DEAD)
	prev->state = EXIT_DEAD;

schedule检查prev的状态。

// 进程状态不是可运行&允许内核抢占
if(prev->state != TASK_RUNNING && !(preempt_count() & PREEMPT_ACTIVE))
{
	// 进程状态是可中断休眠&存在待处理信号
	if(prev->state == TASK_INTERRUPTIBLE && signal_pending(prev))
		prev->state = TASK_RUNNING;// 恢复进程状态为可运行
	else
	{
		if(prev->state == TASK_UNINTERRUPTIBLE)
			rq->nr_uninterruptible++;
		// 将进程从其所在的进程链表移除。这样进程被移除在调度目标考虑范围外。
		deactive_task(prev, rq);
	}
}

现在schedule检查运行队列中剩余的可运行进程数。如有可运行的进程,schedule就调dependent_sleeper。绝大多数情况下,该函数立即返回0。但,如内核支持超线程技术,函数检查要被选中执行的进程,其优先级是否比已经在相同物理CPU的某个逻辑CPU上运行的兄弟进程的优先级低;这种特殊情况下,schedule拒绝选中低优先级进程,执行swapper。这样是为了避免低优先级进程抢占同一物理cpu内的共享资源。

if(rq->nr_running)
{
	if(dependent_sleeper(smp_processor_id(), rq))
	{
		next = rq_idle;
		goto switch_tasks;
	}
}

如运行队列中没可运行的进程存在,就调idle_balance。从另外一个运行队列迁移一些可运行进程到本地运行队列中。idle_balance与load_balance类似。

if(!rq->nr_running)
{
	idle_balance(smp_processor_id(), rq);
	if(!rq->nr_running){
		next = rq_idle;
		rq->expired_timestamp = 0;
		wake_sleeping_dependent(smp_processor_id(), rq);
		if(!rq->nr_running)
			goto switch_tasks;
	}
}

如idle_balance没成功把进程迁移到本地运行队列, wake_sleeping_dependent是检查兄弟进程正在运行空闲进程,且存在可运行进程下,设置其调度标志。在单处理器系统,或把进程迁移到本地运行队列的种种努力都失败情况下,函数选择swapper作为next并继续执行下一步骤。

// 走到这里,是可以继续在运行队列中选择目标进程
array = rq->active;
if(!array->nr_active)
{
	// 交换活动队列,过期队列
	rq->active = rq->expired;
	rq->expired = array;
	array = rq->active;
	rq->expired_timestamp = 0;
	rq->best_expired_prio = 140;
}

schedule搜索活动进程集合位掩码的第一个非0位。当对应的优先级链表不为空时,就把位掩码相应位置1。第一个非0位的下标对应包含最佳运行进程的链表。

idx = sched_find_first_bit(array->bitmap);
next  = list_entry(array->queue[idx].next, task_t, run_list);

函数sched_find_first_bit基于bsfl汇编语言指令的,它返回32位字中被设置为1的最低位的位下标。局部变量next现在存放将取代prev的进程描述符指针。schedule检查next->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, task);
	recalc_task_prio(next, next->timestamp + delta);// 内部会用参数2 - 参数1的timestamp字段计算平均睡眠时间
}
enqueue_task(next, array);

调度程序把被中断处理程序和可延迟函数所唤醒的进程与被系统调用服务例程和内核线程所唤醒的进程区分开来。前一种,调度程序增加全部运行队列等待时间。后一种,它只增加等待时间的部分。交互式进程更可能被异步事件而不是同步事件唤醒。

schedule完成进程切换时操作

到这里已经完成了目标进程的选择。

switch_tasks:
	prefetch(next);

prefetch提示CPU控制单元把next的进程描述符第一部分字段的内容装入硬件高速缓存。替代prev之前,调度程序应完成一些管理工作

clear_tsk_need_resched(prev);
rcu_qsctr_inc(prev->thread_info->cpu);

clear_tsk_need_resched清除prev的TIF_NEED_RESCHED标志。函数记录CPU正在经历静止状态。schedule还必须基于进程所使用的CPU时间片减少prev的平均睡眠时间

prev->sleep_avg -= run_time;
if((long)prev->sleep_avg <= 0)
	prev->sleep_avg = 0;
// 记录进程失去cpu的时间点
// 这样,便能在后续用于记录睡眠了多长时间
prev->timestamp = prev->last_ran = now;

prev和next很可能是同一个进程:如在当前运行队列中没优先级较高或相等的其他活动进程时,会发生这种情况。

if(prev == next)
{
	spin_unlock_irq(&rq->lock);
	goto finish_schedule;
}

prev和next是不同的进程,进程切换发生。

// 目标进程记录开始获得cpu的时间。这样后续就可用来统计运行时间。
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
prev = context_switch(rq, prev, next);

context_switch

context_switch建立next的地址空间,进程描述符的active_mm指向进程所使用的内存描述符,mm指向进程所拥有的内存描述符。

内核线程没自己的地址空间,且它的mm置为NULL。context_switch确保,如next是一个内核线程,使用prev的地址空间。

// 表明next是一个内核线程
if(!next->mm)
{
	// 保持之前active_mm 
	next->active_mm == prev->active_mm;
	atomic_inc(&prev->active_mm->mm_count);
	// 进入懒惰TLB
	enter_lazy_tlb(prev->active_mm, next);
}

如果next是内核线程,schedule把进程设置为懒惰TLB模式。如next是一个普通进程,context_switch用next的地址空间替换prev的。

// 表明next是一个普通进程
if(next->mm)
	switch_mm(prev->active_mm, next->mm, next);// 完成页表切换

如prev是内核线程或正退出的进程,context_switch就把指向prev内存描述符的指针保存到运行队列的prev_mm,重新设置prev->active_mm:

// 之前是内核线程
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所执行的操作

稍后调度程序又选择prev执行时由prev执行。然而,那个时刻,prev局部变量并不指向我们开始描述schedule时所替换出去的原来那个进程,而是指向prev被调度时由prev替换出的原来那个进程。

barrier();
finish_task_switch(prev);
finish_task_switch函数:
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);
if(prev_task_flags & PF_DEAD)
	put_task_struct(prev);

如prev是一个内核线程,则运行队列的prev_mm字段存放借给prev的内存描述符的地址。mmdrop减少内存描述符的使用计数器;如计数器等于0,函数还需释放与页表相关的所有描述符和虚拟存储区。finish_task_switch函数还要释放运行队列的自旋锁并打开本地中断。检查prev是否是一个正在从系统中被删除的僵死任务。如是,就调put_task_struct以释放进程描述符引用计数器,并撤销所有其余对该进程的引用。

finish_schedule:
	prev = current;
	if(prev->lock_depth >= 0)
		__reacquire_kernel_lock();
	preempt_enable_no_resched();
	if(test_bit(TIF_NEED_RESCHED, &current_thread_info()->flags))
		goto need_resched;
	return;

schedule在需要时重新获得大内核锁,重新启用内核抢占。并检查是否一些其他的进程已设置了当前进程的TIF_NEED_RESCHED。如是,整个schedule重新执行。如否,结束。

多处理器系统中运行队列的平衡

标准的多处理器体系结构
	这些机器所共有的RAM芯片集被所有CPU共享
超线程
	当前线程在访问内存的间隙,处理器可使用它的机器周期去执行另一个线程。一个物理CPU包含多个逻辑CPU。
NUMA
	把CPU和RAM以本地"节点"为单位分组。通常一个节点包括一个CPU和几个RAM芯片。
	内存仲裁器(一个使系统中的CPU以串行方式访问RAM的专用电路)是典型的多处理器系统的性能瓶颈。
	在NUMA体系结构中,当CPU访问与它同在一个节点中的"本地"RAM芯片时,几乎没有竞争,因此访问通常很快。
	另一方面,访问其所属节点外的"远程"RAM芯片就非常慢。

这些基本的多处理器系统类型经常被组合使用。如,内核把一个包括两个不同超线程CPU的主板看作四个逻辑CPU。schedule从本地CPU的运行队列挑选新进程运行。一个指定的CPU只能执行其相应的运行队列中的可运行进程。一个可运行进程总是存放在某一个运行队列中:任何一个可运行进程都不可能同时出现在两个或多个运行队列。

某些情况下,把可运行进程限制在一个指定的CPU上可能引起严重的性能损失。如考虑频繁使用CPU的大量批处理进程:如它们绝大多数都在同一个运行队列中,则系统中的一个CPU将会超负荷。而其他一些CPU几乎处于空闲状态。故,内核周期性地检查运行队列的工作量是否平衡,并在需要的时候,把一些进程从一个运行队列迁移到另一个运行队列。但为了从多处理系统获得最佳性能,负载平衡算法应考虑系统中CPU的拓扑结构。从内核2.6.7开始,Linux提出一种基于"调度域"概念的复杂的运行队列平衡算法。有了调度域概念,使得这种算法很容易适应各种已有的多处理器体系结构。

调度域

调度域实际上是一个CPU集合,它们的工作量应由内核保持平衡。一般,调度域采取分层的组织形式:最上层的调度域(通常包括系统中的所有CPU)包括多个子调度域,每个子调度域包括一个CPU子集。正是调度域的这种分层结构,使工作量的平衡能以如下有效方式来实现。

每个调度域被依次划分成一个或多个组,每个组代表调度域的一个CPU子集。工作量的平衡总是在调度域的组之间来完成。只有在调度域的某个组的总工作量远远低于同一个调度域的另一个组的工作量时,才把进程从一个CPU迁移到另一个CPU。

2-CPU的SMP
	基本域(0级):
	有两个组
	每组一个CPU
2-CPU,有超线程的SMP
	一级域:
	有两个组,
	每组一个物理CPU
	基本域(0级):
	有两个组,
	每组一个逻辑CPU
8-CPU的NUMA(每个节点有四个CPU)
	一级域:
	有两个组
	每组一个节点
	基本域(0级):
	有四个组
	每组1个CPU

每个调度域由一个sched_domain表示。调度域中的每个组由sched_group表示。每个sched_domain包括一个groups字段,它指向组描述符链表中的第一个元素。此外,sched_domain结构的parent指向父调度域的描述符。系统中所有物理CPU的sched_domain都存放在每CPU变量phys_domains中。

如内核不支持超线程技术,这些域就在域层次结构的最底层,运行队列描述符的sd字段指向它们,即它们是基本调度域。相反,如内核支持超线程技术,则底层调度域存放在每CPU变量cpu_domains中。

rebalance_tick

为保持系统中运行队列的平衡,每经过一次时钟节拍,scheduler_tick就调用rebalance_tick。它接收参数有:本地CPU的下标this_cpu,本地运行队列的地址this_rq,一个标志idle,该标志可取下面值:

SCHED_IDLE
	CPU当前空闲,即current是swapped进程
NOT_IDLE
	CPU当前不空闲,即current不是swapper进程

rebalance_tick先确定运行队列中的进程数,更新运行队列的平均工作量,为完成此工作。函数要访问运行队列描述符的nr_running和cpu_load。最后,rebalance_tick开始在所有调度域上的循环,其路径是从基本域(本地运行队列描述符的sd字段所引用的域)到最上层的域。每次循环中,函数确定是否已到调用函数load_balance的时间,从而在调度域上执行重新平衡的操作。

由存放在sched_domain描述符中的参数和idle值决定调用load_balance的频率。如idle等于SCHED_IDLE,则运行队列为空。rebalance_tick就以很高的频率调load_balance。大概每一到两个节拍处理一次对应于逻辑和物理CPU的调度域。如idle等于NOT_IDLE,rebalance_tick就以很低的频率调度load_balance。大概每10ms处理一次逻辑CPU对应的调度域,每100ms处理一次物理CPU对应的调度域。

load_balance

检查是否调度域处于严重的不平衡状态。它检查是否可通过把最繁忙的组中的一些进程迁移到本地CPU的运行队列来减轻不平衡的状况。如是,函数尝试实现这个迁移。它接收四个参数:

this_cpu
	本地CPU的下标
this_rq
	本地运行队列的描述符的地址
sd
	指向被检查的调度域的描述符
idle
	取值为SCHED_IDLE(本地CPU空闲)或NOT_IDLE

函数执行下面的操作:
1,获得this_rq->lock
2.调find_busiest_group分析调度域中各组的工作量。
函数返回最繁忙组的sched_group描述符的地址,假设这个组不包括本地CPU,此时,函数还返回为了恢复平衡而被迁移到本地运行队列的进程数。另一方面,如最繁忙的组包括本地CPU或所有组本来就是平衡的,函数返回NULL。
3.如find_busiest_group在调度域中没找到既不包括本地CPU又非常繁忙的组,就释放this_rq->lock,调整调度域描述符的参数,以延迟本地CPU下一次对load_balance的调度,函数终止。
4.调find_busiest_queue以查找2中找到的组中最繁忙的CPU,函数返回相应运行队列的描述符地址busiest。

5.获取另一自旋锁,即busiest->lock。为避免死锁,先释放this_rq->lock,通过增加CPU下标获得这两个锁
6.调move_tasks,尝试从最繁忙运行队列中把一些进程迁移到本地运行队列this_rq
7.如move_tasks没成功。则调度域还是不平衡。把busiest->active_balance置为1,唤醒migration内核线程,它的描述符存在busiest->migration_thread。

Migration顺着调度域的链搜索-从最繁忙运行队列的基本域到最上层域,寻找空闲CPU。如找到,该内核线程就调move_tasks把一个进程迁移到空闲运行队列。
8.释放busiest->lock和this_rq->lock
9.结束。

move_tasks

把进程从源运行队列迁移到本地运行队列。接收6个参数:
this_rq
this_cpu
busiest
max_nr_move
sd
idle

函数先分析busiest运行队列的过期进程,从优先级高的进程开始,扫描完所有过期进程后,扫描busiest运行队列的活动进程。对所有的候选进程调can_migrate_task。如下列条件都满足,则can_migrate_task返回1:
1.进程当前没在远程CPU上执行
2.本地CPU包含在进程描述符的cpus_allowed位掩码
3.至少满足下列条件之一
3.1.本地CPU空闲。如内核支持超线程,则所有本地物理芯片中的逻辑CPU必须空闲。
3.2.内核在平衡调度域时因反复进行进程迁移都不成功陷入困境。
3.3.被迁移的进程不是"高速缓存命中"的(最近不曾在远程CPU上执行,可设想远程CPU上的硬件高速缓存中没该进程的数据)

如can_migrate_task返回1,move_tasks调pull_task把候选进程迁移到本地运行队列。pull_tasks执行dequeue_task从远程队列删除进程,执行enqueue_task把进程插入本地运行队列。如刚被迁移的进程比当前进程拥有更高的动态优先级,就调resched_task抢占本地CPU的当前进程。

与调度相关的系统调用

nice

允许进程改变它们的基本优先级,负增加下,调capable核实进程是否有CAP_SYS_NICE。且,函数调security_task_setnice安全钩。

getpriority和setpriority

nice只影响调用它的进程,getpriority和setpriority作用于给定组中所有进程的基本优先级。getpriority返回20减去组中所有进程之中最低nice字段的值;setpriority把给定组中所有进程的基本优先级都设置为一个给定的值。内核对这两个系统调用的实现是通过sys_getpriority和sys_setpriority完成的。

which
	指定进程组的值。
	PRIO_PROCESS
		根据进程的ID选择进程
	PRIO_PGRP
		根据组ID选择进程
	PRIO_USER
		根据用户ID选择进程
who
	用pid,pgrp或uid字段的值(取决于which的值)选择进程。
	如who是0,把它的值置为current进程相应字段的值。
niceval
	新的基本优先级值。取值范围在-20~+19

sched_getaffinity和sched_setaffinity

返回和设置CPU进程亲和力掩码、即允许执行进程的CPU的位掩码。该掩码放在进程描述符的cpus_allowed字段。

与实时进程相关的系统调用

进程为了修改任何进程的描述符的rt_priority和policy,必须具有CAP_SYS_NICE权能。

sched_getscheduler和sched_setscheduler

sched_getscheduler查询由pid所表示的进程当前所用的调度策略。如pid等于0,将检索调用进程的策略。如成功,这个系统调用为进程返回策略:SCHED_FIFO,SCHED_RR或SCHED_NORMAL。相应的sys_sched_getscheduler调find_task_by_pid,后一函数确定给pid所对应的进程描述符,并返回其policy字段值。

sched_setscheduler既设置调度策略,也设置由pid所表示进程的相关参数。如pid等于0,调用进程的调度程序参数将被设置。相应的sys_sched_setscheduler简单地调do_sched_setscheduler。后者检查由policy指定的调度策略和由参数param->sched_priority指定的新优先级是否有效。还检查进程是否有CAP_SYS_NICE。或进程的拥有者是否有超级用户权限。

如每个条件都满足,就把进程从它的运行队列中删除;更新进程的静态优先级,实时优先级,动态优先级;把进程插回到运行队列;在需要的情况下,调resched_task抢占运行队列的当前进程。

sched_getparam和sched_setparam

sched_yield

允许进程在不被挂起的情况下自愿放弃CPU,进程仍处于TASK_RUNNING,但调度程序把它放在运行队列的过期进程集合中,或放在运行队列链表的末尾。随后调schedule。这种方式下,有相同动态优先级的其他进程将有机会运行。

sched_get_priority_min和sched_get_priority_max

如current是实时进程,则sys_sched_get_priority_min返回1。否则,返回0;如current是实时进程,则sys_sched_get_priority_max返回99,否则,返回0;

sched_rr_get_interval

把参数pid表示的实时进程的轮转时间片写入用户态地址空间的一个结构中。

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/131908901