Linux内核深入学习 - 进程调度

目录

调度策略

一些API:

进程抢占

时间片的选择应该如何

调度算法

普通进程的调度

基本时间片

动态优先级和平均睡眠时间

活动与过期进程

实时进程的调度

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

数据结构runqueue

进程描述符

调度程序所使用的函数

scheduler_tick

更新实时进程的时间片

更新普通进程的时间片

try_to_wake_up

recalc_task_prio

schedule

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

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

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

schedule完成进程切换时操作

context_switch

进程切换后schedule所执行的操作

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

调度域

rebalance_tick

load_balance

move_tasks

与调度相关的系统调用

nice

getpriority和setpriority

sched_getaffinity和sched_setaffinity

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

Reference


我们这里,准备讨论一下进程调度这个话题,这是对进程那一章节的继续补充!

调度策略

Linux的调度基于分时技术!多个进程以时间多路复用的方式进行运行。因为CPU的时间被分成了片分配给每个进程。当然单处理器在任何给定时刻只能运行一个进程,如果当前进程的时间片或时限到期时,进程没有运行完毕,那么进程切换就可以!

发生分时机依赖于定时中断,因此对进程是透明的,不需要在程序中插入额外的代码来进行切换。

在Linux中进程的优先级是动态进行分配的,调度程序跟踪器进程正在做什么,动态的调整它们的优先级!传统上把进程分为io受限或CPU受限,前者频繁的使用io设备,并且花费等很多时间等待io操作完成。后者则需要大量的CPU时间的数值计算这样的应用程序。

另一种则把进程分为三类:

  1. 交互式进程:这类进程经常与用户进行交互,因此需要花很多时间等待键盘和鼠标操作

  2. 批处理进程:这些进程不必与用户进行一种交互,经常在后台进行运行。这样的进程不必很快响应,因此经常受到调度程序的慢怠

  3. 实时进程:这些进程有很强的调度需求,这样的进程绝不会被低优先级的进程阻塞。他们应该有一个很短的响应时间。

一些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"值传递给系统调用nicesetpriority,用户可改变自己拥有的进程的静态优先级

基本时间片

静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间片长度。静态优先级和基本时间片的关系用下列公式确定: 基本时间片(ms): 若静态优先级 < 120(140 - 静态优先级) * 20 若静态优先级 >= 120(140 - 静态优先级) * 5

说明静态优先级nice值基本时间片交互式@值睡眠时间极限值
最高静态优先级100-20800ms-3299ms
高静态优先级110-10600ms-1499ms
缺省静态优先级1200100ms+2799ms
低静态优先级130+1050ms+4999ms
最低静态优先级139+195ms+61199ms

动态优先级和平均睡眠时间

普通进程除了静态优先级,还有动态优先级,其值的范围是100(高)~139(低)。动态优先级是调度程序在选择新进程来运行时候使用的数。它与静态优先级的关系用下面的经验公式表示: 动态优先级=max(100, min(静态优先级 - bonus + 5, 139))

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

平均睡眠时间bonus粒度
[0,100ms)05120
[100ms,200ms)12560
[200ms,300ms)21280
[300ms,400ms)3640
[400ms,500ms)4320
[500ms,600ms)5160
[600ms,700ms)680
[700ms,800ms)740
[800ms,900ms)820
[900ms,1000ms)910
1s1010

平均睡眠时间也被调度程序用来确定一个给定进程是交互式进程,还是批处理进程。对于交互式进程的计算方式是:动态优先级 <= 3 * 静态优先级 / 4 + 28bonus - 5 >= 静态优先级 / 4 - 28

总结:

  1. 优先级越高获得的时间片越大

  2. 睡眠时间越长,动态优先级在静态优先级基础上越高(值越小)。

活动与过期进程

当然,我们想到即使是不具有较高静态优先级的普通进程获得较大的CPU时间片,也不应该使得静态优先级较低的进程无法运行!(活动队列一直被高优先级的进程抢占!)为了避免进程饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的低优先级进程取代!

为了实现这种机制调度程序分了两个不相交的可运行进程的集合:

  • 活动进程:这些进程还没有用完他们的时间片因此引起他们的运行运行

  • 过期进程:这些可运行进程已经完它们的时间片,因此禁止被运行直到所有活动进程都过期

实时进程的调度

每个实时进程都与一个实时优先级相关!实时优先级是一个从1到99的值!调用程序总是让优先级高的进程运行,换句话说实时进程运行的过程中禁止低优先级进程的运行!与普通进程相反,实时进程总是可以被当成活动进程!

如果几个可运行的实时进程具有相同的优先级那么调度,进程选择第一个出现在本地CPU的运行队列的相应的列表的进程!与进程只有在以下列这些事情发生的时候实时进程才会被另外一个进程取代:

  • 进程被另一个具有较高优先级对实时进程抢占

  • 实时进程执行了阻塞操作并且进入了睡眠进程

  • 停止或者被杀死进程

  • 通过系统调用sched_yield自愿放弃CPU

  • 进程正是基于时间片轮转的实时进程,而且用完了它的时间片

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

数据结构runqueue

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

类型名称说明
spinlock_tlock保护进程链表的自旋锁
unsigned longnr_running运行队列链表中可运行进程的数量
unsigned longcpu_load基于运行队列中进程的平均数量的CPU负载因子
unsigned longnr_switchesCPU执行进程切换的次数
unsigned longnr_uninterruptible先前在运行队列链表,现在睡眠在TASK_UNINTERRUPTIBLE状态的进程的数量
unsigned longexpired_timestamp过期队列中最老的进程被插入队列的时间
unsigned long longtimestamp_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活动进程和过期进程的两个集合
intbest_expired_prio过期进程中静态优先级最高的进程
atomic_tnr_iowait先前在运行队列链表中,现在正等待磁盘I/O操作结束的进程的数量
struct sched_domain*sd指向当前CPU的基本调度域
intactive_balance如要把一些进程从本地运行队列迁移到另外的运行队列,就设置
intpush_cpu未使用
task_t*migration_thread迁移内核线程的进程描述符指针
struct list_headmigration_queue从运行队列中被删除的进程的链表

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

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

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

进程描述符

类型名称说明
unsigned longthread_info->flags存放TIF_NEED_RESCHED,如必须调调度程序,则设置
unsigned intthread_info->cpu可运行进程所在运行队列的CPU逻辑号
unsigned longstate进程的当前状态
intprio进程的动态优先级
intstatic_prio进程的静态优先级
struct list_headrun_list指向进程所属的运行队列链表中的下一个和前一个元素。链表节点。
prio_array_t*array指向包含进程的运行队列的集合prio_array_t
unsigned longsleep_avg进程的平均睡眠时间
unsigned long longtimestamp进程最近插入运行队列时间,或涉及本进程的最近一次进程切换的时间
unsigned long longlast_ran最近一次替换本进程的进程切换时间
intactivated进程被唤醒时使用的条件代码
unsigned longpolicy进程的调度类型(SCHED_NORMAL,SCHED_RR,SCHED_FIFO)
cpumask_tcpus_allowed能执行进程的CPU的位掩码
unsigned inttime_slice在进程的时间片中还剩余的时钟节拍数
unsigned intfirst_time_slice如进程肯定不会用完其时间片,就设置
unsigned longrt_priority进程的实时优先级

进程被创建时

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

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

调度程序所使用的函数

  1. scheduler_tick:维持最新的time_slice计数器

  2. try_to_wake_up:唤醒睡眠进程

  3. recalc_task_prio更新进程的动态优先级

  4. schedule选择要被执行的新进程

  5. load_balance:维持多处理器系统中运行队列的平衡

scheduler_tick

每次时钟节拍到来时,scheduler_tick

  1. 把转换为纳秒的TSC当前值存入本地运行队列的timestamp_last_tick。这个时间戳是从sched_clock获得的。

  2. 检查当前进程是否是本地CPUswapper进程。如是,

    如本地运行队列除了swapper外,还包括一个可运行的进程,就设置当前进程的TIF_NEED_RESCHED。如内核支持超线程技术,则只要一个逻辑CPU运行队列中的所有进程 都有比 另一个逻辑CPU上已经在执行的进程 有低得多的优先级(两个逻辑CPU对应同一个物理CPU),前一逻辑CPU就可能空闲。超线程下,将2个进程安排在两个不同物理cpu,相比在同一物理cpu的多个逻辑cpu可以更好并发,然后跳到第七步!

  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_tickset_tsk_need_resched设置TIF_NEED_RESCHED

  3. 把进程描述符移到与当前进程优先级相应的运行队列活动链表尾部。

更新普通进程的时间片

如当前进程是普通进程,scheduler_tick

  1. 递减current->time_slice

  2. 如时间片用完

    1. dequeue_task从可运行进程的this_rq()->active集合中删除current指向的进程

    2. set_tsk_need_resched设置TIF_NEED_RESCHED

    3. 更新current指向的进程的动态优先级,current->prio = effective_prio(current);

    4. 重填进程的时间片

      current->time_slice = task_timeslice(current);
      current->first_time_slice = 0;
    5. 如果本地运行队列的expired_timestamp等于0,就把当前时钟节拍值赋给expired_timestamp

    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的运行队列。实际上,函数根据一些启发式规则选择一个目标运行队列。

    1. 如系统中某些CPU空闲,就选择空闲CPU的运行队列。按优先选择当前正执行进程的CPU和本地CPU这种顺序。

    2. 如先前执行进程的CPU工作量远小于本地CPU的工作量,就选择先前的运行队列作为目标

    3. 如进程最近被执行过,就选择老的运行队列作为目标(可能仍用这个进程的数据填充硬件高速缓存)

    4. 如把进程移到本地CPU以缓解CPU之间的不平衡,目标就是本地运行队列

    此时,已经确定了目标CPU和对应的目标运行队列rq

  5. 如进程处于TASK_UNINTERRUPTIBLE,递减目标运行队列的nr_uninterruptible,把进程描述符的p->activated置为-1

  6. active_task

    1. sched_clock获取以纳秒为单位的当前时间戳。如目标CPU不是本地CPU,就补偿本地时钟中断的偏差。从而得到准确的目标cpu上的时间戳。

      now=(sched_clock()-this_rq()->timestamp_last_tick)+rq->timestamp_last_tick;
    2. recalc_task_prio,把进程描述符的指针和上一步计算出的时间戳传递给它。重新计算平均睡眠时间,动态优先级。

    3. 调整p->activated,以便反映从中断唤醒,从非中断唤醒,不可中断睡眠进程唤醒。

    4. 6.1.算出的时间戳设置p->timestamp

    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. 返回10

recalc_task_prio

更新进程的平均睡眠时间,动态优先级.接收进程描述符指针p,和由sched_clock计算出的当前时间戳。操作:

  1. min(now - p->timestamp, 1 0 9 10^9109)的结果赋给局部变量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且不是内核线程,执行下述:

    1. 检查平均睡眠时间p->sleep_avg是否大于或等于进程的睡眠时间极限。如是,把局部变量sleep_time重新置为0,因此不用调整平均睡眠时间,跳6

    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);函数依据p的静态优先级,sleep_avg按前面介绍的规则计算动态优先级。

schedule

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

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

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

  1. current进程插入适当的等待队列

  2. current进程状态改为TASK_INTERRUPTIBLETASK_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进程的可运行进程,则最终nextcurrent相等,不发生任何进程切换。

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,将它的值换成纳秒。获得的时间戳存放在局部变量nowschedule计算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_balanceload_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_INTERRUPTIBLETASK_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清除prevTIF_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;

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

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

prevnext是不同的进程,进程切换发生。

// 目标进程记录开始获得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置为NULLcontext_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_switchnext的地址空间替换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执行prevnext之间的进程切换了。

// 设置寄存器,栈,执行流程切换
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重新执行。如否,结束

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

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

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

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

调度域

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

每个调度域被依次划分成一个或多个组,每个组代表调度域的一个CPU子集。工作量的平衡总是在调度域的组之间来完成。只有在调度域的某个组的总工作量远远低于同一个调度域的另一个组的工作量时,才把进程从一个CPU迁移到另一个CPU。 (1). 2-CPUSMP 基本域(0级): 有两个组,每组一个CPU (2). 2-CPU,有超线程的SMP 一级域: 有两个组,每组一个物理CPU 基本域(0级): 有两个组,每组一个逻辑CPU (3). 8-CPUNUMA(每个节点有四个CPU) 一级域: 有两个组,每组一个节点 基本域(0级): 有四个组,每组1个CPU

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

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

rebalance_tick

为保持系统中运行队列的平衡,每经过一次时钟节拍,scheduler_tick就调用rebalance_tick。它接收参数有:本地CPU的下标this_cpu,本地运行队列的地址this_rq,一个标志idle,该标志可取下面值: (1). SCHED_IDLE CPU当前空闲,即currentswapped进程 (2). NOT_IDLE CPU当前不空闲,即current不是swapper进程

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

由存放在sched_domain描述符中的参数和idle值决定调用load_balance的频率。如idle等于SCHED_IDLE,则运行队列为空。rebalance_tick就以很高的频率调load_balance。大概每一到两个节拍处理一次对应于逻辑和物理CPU的调度域。如idle等于NOT_IDLErebalance_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->lockthis_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返回1move_taskspull_task把候选进程迁移到本地运行队列。pull_tasks执行dequeue_task从远程队列删除进程,执行enqueue_task把进程插入本地运行队列。如刚被迁移的进程比当前进程拥有更高的动态优先级,就调resched_task抢占本地CPU的当前进程。

与调度相关的系统调用

nice

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

getpriority和setpriority

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

a. PRIO_PROCESS 根据进程的ID选择进程 b. PRIO_PGRP 根据组ID选择进程 c. PRIO_USER 根据用户ID选择进程 d. whopid,pgrpuid字段的值(取决于which的值)选择进程。 如who0,把它的值置为current进程相应字段的值。 e. niceval 新的基本优先级值。取值范围在`-20~+19

sched_getaffinity和sched_setaffinity

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

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

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

调用说明
sched_getscheduler查询由pid所表示的进程当前所用的调度策略。如pid等于0,将检索调用进程的策略。如成功,这个系统调用为进程返回策略:SCHED_FIFOSCHED_RRSCHED_NORMAL。相应的sys_sched_getschedulerfind_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_mincurrent是实时进程,则sys_sched_get_priority_min返回1。否则,返回0;如current是实时进程,则sys_sched_get_priority_max返回99,否则,返回0
sched_get_priority_maxcurrent是实时进程,则sys_sched_get_priority_min返回1。否则,返回0;如current是实时进程,则sys_sched_get_priority_max返回99,否则,返回0
sched_rr_get_interval把参数pid表示的实时进程的轮转时间片写入用户态地址空间的一个结构中。

Reference

深入理解Linux内核-进程-进程调度_sched_setscheduler-CSDN博客

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值