目录
sched_getaffinity和sched_setaffinity
我们这里,准备讨论一下进程调度这个话题,这是对进程那一章节的继续补充!
调度策略
Linux的调度基于分时技术!多个进程以时间多路复用的方式进行运行。因为CPU的时间被分成了片分配给每个进程。当然单处理器在任何给定时刻只能运行一个进程,如果当前进程的时间片或时限到期时,进程没有运行完毕,那么进程切换就可以!
发生分时机依赖于定时中断,因此对进程是透明的,不需要在程序中插入额外的代码来进行切换。
在Linux中进程的优先级是动态进行分配的,调度程序跟踪器进程正在做什么,动态的调整它们的优先级!传统上把进程分为io受限或CPU受限,前者频繁的使用io设备,并且花费等很多时间等待io操作完成。后者则需要大量的CPU时间的数值计算这样的应用程序。
另一种则把进程分为三类:
-
交互式进程:这类进程经常与用户进行交互,因此需要花很多时间等待键盘和鼠标操作
-
批处理进程:这些进程不必与用户进行一种交互,经常在后台进行运行。这样的进程不必很快响应,因此经常受到调度程序的慢怠
-
实时进程:这些进程有很强的调度需求,这样的进程绝不会被低优先级的进程阻塞。他们应该有一个很短的响应时间。
一些API:
系统调用 | 说明 |
---|---|
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状态,内核检查它的动态优先级是否大于当前正在运行的进程的优先级!如果是,进程切换发生。
时间片的选择应该如何
时间片的长短对系统性能很关键,不可太长也不可太短,如果太短,由进程切换引起的系统额外开销会变得很高;如果太长,系统看起来的响应非常的差。所以对时间片大小的选择始终是一种折中 。
调度算法
每次进程切换时,内核扫描可运行进程链表,计算进程的优先级,再选择"最佳"进程来运行。在固定的时间内(与可运行的进程数量无关)选中要运行的进程。很好处理了与处理器数量的比例关系,每个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
总结:
优先级越高获得的时间片越大
睡眠时间越长,动态优先级在静态优先级基础上越高(值越小)。
活动与过期进程
当然,我们想到即使是不具有较高静态优先级的普通进程获得较大的CPU时间片,也不应该使得静态优先级较低的进程无法运行!(活动队列一直被高优先级的进程抢占!)为了避免进程饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的低优先级进程取代!
为了实现这种机制调度程序分了两个不相交的可运行进程的集合:
-
活动进程:这些进程还没有用完他们的时间片因此引起他们的运行运行
-
过期进程:这些可运行进程已经完它们的时间片,因此禁止被运行直到所有活动进程都过期
实时进程的调度
每个实时进程都与一个实时优先级相关!实时优先级是一个从1到99的值!调用程序总是让优先级高的进程运行,换句话说实时进程运行的过程中禁止低优先级进程的运行!与普通进程相反,实时进程总是可以被当成活动进程!
如果几个可运行的实时进程具有相同的优先级那么调度,进程选择第一个出现在本地CPU的运行队列的相应的列表的进程!与进程只有在以下列这些事情发生的时候实时进程才会被另外一个进程取代:
-
进程被另一个具有较高优先级对实时进程抢占
-
实时进程执行了阻塞操作并且进入了睡眠进程
-
停止或者被杀死进程
-
通过系统调用sched_yield自愿放弃CPU
-
进程正是基于时间片轮转的实时进程,而且用完了它的时间片
调度程序所使用的数据结构
数据结构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
:
把转换为纳秒的
TSC
当前值存入本地运行队列的timestamp_last_tick
。这个时间戳是从sched_clock
获得的。检查当前进程是否是本地
CPU
的swapper
进程。如是,如本地运行队列除了
swapper
外,还包括一个可运行的进程,就设置当前进程的TIF_NEED_RESCHED
。如内核支持超线程技术,则只要一个逻辑CPU
运行队列中的所有进程 都有比 另一个逻辑CPU
上已经在执行的进程 有低得多的优先级(两个逻辑CPU
对应同一个物理CPU
),前一逻辑CPU
就可能空闲。超线程下,将2
个进程安排在两个不同物理cpu
,相比在同一物理cpu
的多个逻辑cpu
可以更好并发,然后跳到第七步!检查
current->array
是否指向本地运行队列的活动链表。如不是,设置TIF_NEED_RESCHED
。跳到7
获得
this_rq()->lock
递减当前进程的时间片计数器。检查是否已用完时间片。由于进程的调度类型不同,这一步操作也有很大差别。稍后讨论。
释放
this_rq()->lock
调
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(¤t->run_list); list_add_tail(¤t->run_list, this_rq()->active->queue+current->prio); }
如函数确定时间片用完了,就执行操作以抢占当前进程。
-
调
task_timeslice
重填进程的时间片计数器 -
scheduler_tick
调set_tsk_need_resched
设置TIF_NEED_RESCHED
-
把进程描述符移到与当前进程优先级相应的运行队列活动链表尾部。
更新普通进程的时间片
如当前进程是普通进程,scheduler_tick
:
-
递减
current->time_slice
-
如时间片用完
-
调
dequeue_task
从可运行进程的this_rq()->active
集合中删除current
指向的进程 -
调
set_tsk_need_resched
设置TIF_NEED_RESCHED
-
更新
current
指向的进程的动态优先级,current->prio = effective_prio(current);
-
重填进程的时间片
current->time_slice = task_timeslice(current); current->first_time_slice = 0;
-
如果本地运行队列的
expired_timestamp
等于0
,就把当前时钟节拍值赋给expired_timestamp
-
把当前进程插入活动进程集合或过期进程集合
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
。
-
-
如时间片没用完。检查当前进程的剩余时间片是否太长。
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(¤t->run_list); list_add_tail(¤t->run_list, this_rq()->active->queue+current->prio); set_tsk_need_resched(p); }
基本上,具有高静态优先级的交互式进程,其时间片被分成大小为TIMESLICE_GRANULARITY
的几个片段,每次用完一个片段,就重新调度一次。以便时间片太长下,其他活动进程有机会得到执行。
try_to_wake_up
把进程状态置为TASK_RUNNING
,把进程插入本地CPU
的运行队列来唤醒睡眠或停止的进程。参数:
被唤醒进程的描述符指针
可被唤醒的进程状态掩码
一个标志,用来禁止被唤醒的进程抢占本地
CPU
上正运行的进程
操作:
调
task_rq_lock
禁用本地中断。获得最后执行进程的cpu
所拥有的运行队列rq
锁。检查进程状态
p->state
是否属于被当作参数传递给函数的状态掩码。如不是,跳到9
。如
p->array
不等于NULL
。跳到8
在多处理器系统中,函数检查要被唤醒的进程是否应该从最近运行的
CPU
的运行队列迁移到另外一个CPU
的运行队列。实际上,函数根据一些启发式规则选择一个目标运行队列。
如系统中某些
CPU
空闲,就选择空闲CPU
的运行队列。按优先选择当前正执行进程的CPU
和本地CPU
这种顺序。如先前执行进程的CPU
的
工作量远小于本地CPU
的工作量,就选择先前的运行队列作为目标如进程最近被执行过,就选择老的运行队列作为目标(可能仍用这个进程的数据填充硬件高速缓存)
如把进程移到本地
CPU
以缓解CPU
之间的不平衡,目标就是本地运行队列此时,已经确定了目标
CPU
和对应的目标运行队列rq
。如进程处于
TASK_UNINTERRUPTIBLE
,递减目标运行队列的nr_uninterruptible
,把进程描述符的p->activated
置为-1
。调
active_task
调
sched_clock
获取以纳秒为单位的当前时间戳。如目标CPU
不是本地CPU
,就补偿本地时钟中断的偏差。从而得到准确的目标cpu
上的时间戳。now=(sched_clock()-this_rq()->timestamp_last_tick)+rq->timestamp_last_tick;调
recalc_task_prio
,把进程描述符的指针和上一步计算出的时间戳传递给它。重新计算平均睡眠时间,动态优先级。调整
p->activated
,以便反映从中断唤醒,从非中断唤醒,不可中断睡眠进程唤醒。据
6.1
.算出的时间戳设置p->timestamp
把进程描述符插入活动进程集合
enqueue_task(p, rq->active); rq->nr_running++;. 如目标
CPU
不是本地CPU
,或没设置sync
。就检查可运行的新进程的动态优先级是否比rq
运行队列中当前进程动态优先级高。如是,就让目标cpu
及时发生新的调度。把进程的
p->state
置为TASK_RUNNING
调
task_rq_unlock
打开rq
运行队列的锁并打开本地中断返回
1
或0
recalc_task_prio
更新进程的平均睡眠时间,动态优先级.接收进程描述符指针p
,和由sched_clock
计算出的当前时间戳。操作:
-
把
min
(now - p->timestamp,
1 0 9 10^9109)的结果赋给局部变量sleep_time
。这样计算出来的是进程的睡眠时间。p->timestamp
包含导致进程进入睡眠状态的进程切换的时间戳。sleep_time
中存放的是从进程最后一次执行开始,进程消耗在睡眠状态的纳秒数。睡眠时间长时,sleep_time
就等于1s
-
如
sleep_time
不大于0
,跳到8
。 -
若进程不是内核线程,进程不是从
TASK_UNINTERRUPTIBLE
被唤醒,进程连续睡眠的时间超过给定的睡眠时间极限。都满足,函数把p->sleep_avg
设置为相当于900
个时钟节拍的值。(用最大平均睡眠时间减去一个标准进程的基本时间片长度获得一个经验值)跳8
。睡眠时间极限,进程静态优先级。这些经验规则的目的是保证已经睡眠了很长时间的进程,获得一个预先确定且足够长的平均睡眠时间,以使这些进程能尽快获得服务。
-
执行
CURRENT_BONUS
计算进程原来的平均睡眠时间的bonus
值。如(10 - bonus)
大于0
,函数用这个值与sleep_time
相乘。因为要把sleep_time
加到进程的平均睡眠时间上,所以当前平均睡眠时间越短,它增加的就越快。 -
如进程处于
TASK_UNINTERRUPTIBLE
且不是内核线程,执行下述:-
检查平均睡眠时间
p->sleep_avg
是否大于或等于进程的睡眠时间极限。如是,把局部变量sleep_time
重新置为0
,因此不用调整平均睡眠时间,跳6
。 -
如
sleep_time+p->sleep_avg
的和大于或等于睡眠时间极限,就把p->sleep_avg
置为睡眠时间极限并把sleep_time
置为0
。通过对进程平均睡眠时间的轻微限制,函数不会对睡眠时间很长的批处理进程给予过多奖赏。
-
-
把
sleep_time
加到进程的平均睡眠时间上。 -
检查
p->sleep_avg
是否超过1000
个时钟节拍(以纳秒为单位),如是,函数就把它减到1000
个时钟节拍(以纳秒为单位)。 -
更新进程的动态优先级:
p->prio=effective_prio(p);
函数依据p
的静态优先级,sleep_avg
按前面介绍的规则计算动态优先级。
schedule
从运行队列的链表找到一个进程,随后将CPU
分配给这个进程。schedule
可由几个内核控制路径调用,可采取直接调用或延迟调用的方式。
从使用角度介绍schedule使用场景–直接调用
如current
进程因不能获得必须的资源而要立刻被阻塞,就直接调调度程序。此时,要阻塞进程的内核路径按下述步骤:
-
把
current
进程插入适当的等待队列 -
把
current
进程状态改为TASK_INTERRUPTIBLE
或TASK_UNINTERRUPTIBLE
-
调
schedule
-
检查资源是否可用。如不可用就跳到
2
。 -
一旦资源可用,就从等待队列中删除
current
。
从使用角度介绍schedule使用场景–延迟调用
也可把current
进程的TIF_NEED_RESCHED
标志设置为1
,而以延迟方式调用调度程序。 由于总是在恢复用户态进程的执行前检查这个标志的值,所以schedule
将在不久后的某个时间被明确地调用。
以下是延迟调用调度程序的典型例子:
-
当
current
进程用完了它的CPU
时间片时,由schedule_tick
完成schedule
的延迟调用。 -
当一个被唤醒进程的优先级比当前进程的优先级高,由
try_to_wake_up
完成schedule
的延迟调用 -
当发出系统调用
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, ¤t_thread_info()->flags)) goto need_resched; return;
schedule
在需要时重新获得大内核锁,重新启用内核抢占。并检查是否一些其他的进程已设置了当前进程的TIF_NEED_RESCHED
。如是,整个schedule
重新执行。如否,结束
多处理器系统中运行队列的平衡
(1). 标准的多处理器体系结构 这些机器所共有的RAM
芯片集被所有CPU
共享 (2). 超线程 当前线程在访问内存的间隙,处理器可使用它的机器周期去执行另一个线程。一个物理CPU
包含多个逻辑CPU
。 (3). 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
。 (1). 2-CPU
的SMP
基本域(0
级): 有两个组,每组一个CPU
(2). 2-CPU
,有超线程的SMP
一级域: 有两个组,每组一个物理CPU
基本域(0
级): 有两个组,每组一个逻辑CPU
(3). 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
,该标志可取下面值: (1). SCHED_IDLE
CPU
当前空闲,即current
是swapped
进程 (2). 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
的运行队列来减轻不平衡的状况。如是,函数尝试实现这个迁移。它接收四个参数: (1). this_cpu
本地CPU
的下标 (2). this_rq
本地运行队列的描述符的地址 (3). sd
指向被检查的调度域的描述符 (4). 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
个参数: (1). this_rq
(2). this_cpu
(3). busiest
(4). max_nr_move
(5). sd
(6). 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
:指定进程组的值。
a.
PRIO_PROCESS
根据进程的ID
选择进程 b.PRIO_PGRP
根据组ID
选择进程 c.PRIO_USER
根据用户ID
选择进程 d.who
用pid,pgrp
或uid
字段的值(取决于which
的值)选择进程。 如who
是0
,把它的值置为current
进程相应字段的值。 e.niceval
新的基本优先级值。取值范围在`-20~+19
sched_getaffinity和sched_setaffinity
返回和设置CPU
进程亲和力掩码、即允许执行进程的CPU
的位掩码。该掩码放在进程描述符的cpus_allowed
字段。
与实时进程相关的系统调用
进程为了修改任何进程的描述符的rt_priority
和policy
,必须具有CAP_SYS_NICE
权能。
调用 | 说明 |
---|---|
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_getparam 为pid所表示的进程检索调度参数,如果pid是0,则current进程的参数被检索!正如所期望的,相应的sys_sched_getparam() 服务例程找到与pid相关的进程描述指针,把它的rt_priority 字段放在了字类型为sched_param 的局部变量中,并且调用copy to user把它拷贝到进程地址空间由param参数指定的地址 |
sched_setparam | 这个系统调用类似于sched_setscheduler ,它与后者的不同在于不让调用者设置policy字段的值!相应的sys_sched_set_prama服务例程几乎与do_sched_setscheduler 相同的参数调用 |
sched_yield | 允许进程在不被挂起的情况下自愿放弃CPU ,进程仍处于TASK_RUNNING ,但调度程序把它放在运行队列的过期进程集合中,或放在运行队列链表的末尾。随后调schedule 。这种方式下,有相同动态优先级的其他进程将有机会运行。 |
sched_get_priority_min | current 是实时进程,则sys_sched_get_priority_min 返回1 。否则,返回0 ;如current 是实时进程,则sys_sched_get_priority_max 返回99 ,否则,返回0 ; |
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 表示的实时进程的轮转时间片写入用户态地址空间的一个结构中。 |