进程调度

Posted 贺二公子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程调度相关的知识,希望对你有一定的参考价值。

文章目录

第7章 进程调度

此章以80x86体系结构为例。假定系统采用统一内存访问模型,系统时钟设定为1ms。

7.1. 调度策略

传统Unix 操作系统的调度策略须实现几个互相冲突的目标:

  • 进程响应时间尽可能快;
  • 后台作业的吞吐量尽可能高;
  • 尽可能避免进程的饥饿现象;
  • 低优先级和高优先级进程的需要尽可能调和等等。

Linux 的调度基于分时 ( time sharing ) 技术:多个进程以“时间多路复用 ”方式运行,因为CPU的时间被分成 “片 ( slice) " , 给每个可运行进程分配一 片。如果当前运行进程的时间片或时限(quantum ) 到期时,该进程还没有运行完毕,进程切换就可以发生。

调度策略根据进程优先级对其分类。在Linux中,进程优先级是动态的。调度程序跟踪进程运行并周期性调整期优先级。

进程分类

传统上把进程分为以下两类:

  • “I/O受限(I/O-bound)”:频繁使用I/O设备,花费很多时间等待I/O操作完成;
  • “CPU受限(CPU-bound)”:需大量CPU时间的数值计算。

另一饭分类法把进程分为以下三类:

  • 交互式进程(interactive process):与用户交互,花大量时间等待键盘、鼠标操作。得到输入后,需快速唤醒,平均延迟须在50~150ms间。典型交互式进程:命令shell、文本编辑程序、图形应用程序。
  • 批处理程序(batch process):无用户交互,后台运行,不必快速响应。典型批处理进程:编译程序,数据库搜索引擎、科学计算。
  • 实时进程(real-time process):有很强的调度需要。进程不会被低优先级进程阻塞,要求有短且变化小的响应时间。典型实时进程:音视频应用程序、机器人控制程序、传感器采集程序。

两种分类法相互独立。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 亲和力掩码

7.1.1. 进程的抢占

Linux进程是抢占式的。

  • 若进程A进入TASK_RUNNING状态,内核检查其动态优先级是否大于当前正在运行的进程。如是,正在运行的进程被中断,调用调度函数选择进程A。
  • 进程在时间片到期时也快被抢占。此时,进程thread_info结构中的TIF_NEED_RESCHED标志被置位,以便时钟中断处理程序终止时调度程序被调用。

注意:

  • 被抢占进程没有被挂起,仍处于TASK_RUNNING状态,只是不再使用CPU。
  • Linux2.6内核是抢占式的,意味着进程无论出于内核态还是用户态,都可能被抢占,详见第五章“内核抢占”一节。

7.1.2. 一个时间片必须持续多长?

时间片不能太长也不能太短。

  • 时间片太短:进程切换引起的系统额外开销非常高。
  • 时间片太长:进程看起来不是并发执行。

对时间片大小的选择始终是一种折衷。Linux 采取单凭经验的方撞,即选择尽可能长、同时能保持良好响应时间的时间片。

7.2. 调度算法

Linux2.6调度算法相比早期Linux版本的优点:

  • 固定时间内选中要运行进程,与可运行进程数量无关。
  • 很好地处理与处理器数量地比例关系,因为每个CPU都有自己的可运行进程队列。
  • 解决了区分交互式进程和批处理进程的问题。

每个Linux进程总按照下面调度类型被调度:

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

7.2.1. 普通进程的调度

每个普通进程都有它自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间调度的程度。内核用从100 (最高优先级)到139 (最低优先级)的数表示普通进程的静态优先级。
新进程继承其父进程的静态优先级。不过,通过把某些“nice 值”传递给系统调用nice()和setpriority()(参见本章稍后“与调度相关的系统调用” 一节),用户可改变自己拥有的进程的静态优先级。

基本时间片

静态优先级决定了进程基本事件片,公式如下:
基本时间片(单位 m s ) = ( 140 − 静态优先级 ) × 20 若静态优先级 < 120 ( 140 − 静态优先级 ) × 20 若静态优先级 ≥ 120 (1) 基本时间片(单位ms)= \\begincases (140-静态优先级) \\times 20 & 若静态优先级<120\\\\ (140-静态优先级) \\times 20 & 若静态优先级\\geq120 \\endcases \\tag1 基本时间片(单位ms=(140静态优先级)×20(140静态优先级)×20若静态优先级<120若静态优先级120(1)
可见,静态优先级较高的进程获得更长CPU时间。表7-2说明拥有最高、默认和最低静态优先级的普通进程,其静态优先级、基本时间片及nice值(表中还列出了交互式的δ值和睡暇时间的极限值,在本章稍后给予说明)。

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

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

动态优先级取值100(最高优先级)~139(最低优先级)。动态优先级是调度程序选择新进程来运行时使用的数。其与静态优先级关系如下:
动态优先级 = m a x ( 100 , m i n ( 静态优先级 − b o n u s + 5 , 139 ) ) (2) 动态优先级=max(100, min(静态优先级 - bonus + 5, 139)) \\tag2 动态优先级=max(100,min(静态优先级bonus+5,139))(2)
bonus取值0~10,取值依赖于进程平均睡眠时间。

  • bonus<5:降低动态优先级;
  • bonus>5: 增加动态优先级。

平均睡眠时间是进程在睡眠状态消耗的平均纳秒数。进程运行中平均睡眠时间递减,最终平均睡眠时间用于小于1s。表7-3说明平均睡眠时间和bonus的关系(表中时间片粒度在稍后讨论)。

平均睡眠时间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 + 28 (3) 动态优先级\\leq3\\times静态优先级/4+28 \\tag3 动态优先级3×静态优先级/4+28(3)
相当于
b o n u s − 5 ≥ 静态优先级 / 4 − 28 bonus-5\\geq静态优先级/4-28 bonus5静态优先级/428
”静态优先级/4-28“为交互式的δ,见表7-2。

活动和过期进程

为了避免进程饥饿,当一个进程用完他的时间片是,应被还没用完时间片的低优先级进程取代。为了实现此机制,调度程序维持两个不想交的可运行进程的集合。

  • 活动进程:没用完时间片,允许运行。
  • 过期进程:已用完时间片,被禁止运行,直至所有进程都过期。

调度程序试图提升交互是进程的性能。原则如下:

  1. 用完时间片的活动批处理进程变为过期进程。
  2. 用完时间片的交互式进程通常仍是活动进程,调度程序重填其时间片并留在活动进程集合中。
  3. 若最老的过期进程已等待很长时间,或过期进程比交互式进程的静态优先级高,将用完时间片的交互式进程移到过期进程集合中。
  4. 结果,活动进程集合变为空,过期进程有机会运行。

7.2.2. 实时进程的调度

实时进程与实时优先级相关,取值1(最高优先级)~99(最低优先级)。调度程序总让优先级高的进程运行,实时进程总是被当成活动进程。
若多个可运行的实时进程具有相同最高优先级,调度程序选择第一个出现在与本地CPU运行队列响应链表的进程(参见第三章“TAS K_RUNNI NG状态的进程链表”)。只有下述事件之一发生时,实时进程会被另一进程取代:

  • 进程被另一个有更高实时优先级的实时进程抢占;
  • 进程执行了阻塞操作并进入睡眠(TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE);
  • 进程停止(TASK_STOPPED或TASK_TRACED)或被杀死(EXIT_ZOMBIE或EXIE_DEAD);
  • 进程调用sched_yield()自愿放弃CPU。
  • 进程是基于时间片轮转的实时进程(SCHED_RR),且用完时间片。

当系统调用nice()和setpriority()用于基于时间片轮转的实时进程时,不改变实时进程的优先级而会改变其基本时间片的长度。实际上.基于时间片轮转的实时进程的基本时间片的长度与实时进程的优先级无关,而依赖于进程的静态优先级, 关系见参见“普通进程的调度”一节中的公式(1)。

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

进程链表链接所有的进程描述符,而运行队列链表链接所有的可运行进程(也就是处于TASK_RUNNING 状态的进程)的进程描述符,swapper 进程(idle进程)除外。

7.3.1. 数据结构runqueue

系统中的每个CPU都有它自己的运行队列,所有的runqueue结构存放在runqueues 每CPU变量中。
宏this_rq()产生本地CPU运行队列的地趾,而宏cpu_rq(n)产生索引为n的CPU的运行队列的地址。
表7-4 列出了runqueue数据结构所包括的字段。

类型名称说明
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当前正在运行进程的进程描述符指针
(对本地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活动进程和过期进程的两个集合
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上执行。
  • 可运行进程会从一个运行队列迁移到另一个运行队列。

下图7-1表示runqueue中最重要的字段,即与可运行进程的链表相关的字段。prio_array_t参见第三章的表3-2。

  • runqueue中active字段指向arrays中两个prio_array_t数据结构之一:包含活动进程的可运行进程的集合;
  • runqueue中expired字段指向数组中的另一个prio_array_t数据结构:包含过期进程的可运行进程的集合。

调度程序通过交换active和expired字段内容以实现活动进程与过期进程的周期性切换。

7.3.2. 进程描述符

表7-5列举与调度程序相关的进程描述符字段

类型名称说明
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如果进程肯定不会用完其时间片,就把该标志设置为1
unsigned longrt_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)”一节]的内容。

7.4. 调度程序所使用的函数

调度程序依赖的最重要函数如下

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

7.4.1. scheduler_tick()函数

第六章“更新本地CPU统计数”一节中说明:每次时钟节拍到来时,scheduler_tick()如何被调用以执行与调度相关的操作的。它执行步骤如下:

  1. 把转换为纳秒的TSC的当前值存入本地运行队列的timestamp_last_tick 字段。此时间戳从函数sched_clock()获得。
  2. 检查当前进程是否是本地CPU的swapper进程,如果是,执行下面的子步骤:
    • 如果本地运行队列除了swapper 进程外,还包括另外一个可运行的进程,就设置当前进程的TIF_NEED_RESCHED字段,以强迫进行重新调度。本章稍后“schedule()函数一节”将看到,如果内核支持超线程技术(见本章稍后“多处理器系统中运行队列的平衡”一节),那么,只要一个逻辑CPU运行队列中的所有进程都有比另一个逻辑CPU(两个逻辑CPU对应同一个物理CPU)上已经在执行的进程有低得多的优先级,前一个逻辑CPU就可能空闲,即使它的运行队列中有可运行的进程。
    • 跳转到第7 步(不必更新swapper进程的时间片计数器)。
  3. 检查current->array是否指向本地运行队列的活动链表。如果不是,说明进程已经过期但还没有被替换:设置TIF_NEED_RESCHED标志,以强制进行重新调度并跳转到第7步。
  4. 获得this_rq()->lock自旋锁。
  5. 递减当前进程的时间片计数器,并检查是否已经用完时间片。不同调度类型不同,函数所执行的这一步操作有很大的差别,后续讨论。
  6. 释放this_rq()->lock自旋锁。
  7. 调用rebalance_tick()函数,该函数应该保证不同CPU的运行队列包含数量基本相同的可运行进程。详见后续“多处理器系统中运行队列的平衡”一节。

更新实时进程的时间片

若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(&current->run_list);
	list_add_tail(&current->run_list,
				  this_rq()->active->queue + current->prio);

更新普通进程的时间片

若current进程是SCHED_NORMAL的普通进程
scheduler_tick()执行以下操作:

  1. 递减时间片计数器(current->time_slice)。
  2. 检查时间片计数器。若时间片用完,执行下列操作:
    • 调用dequeue_task()从可运行进程的this_rq()->active集合中删除current指向的进程。
    • 调用set_tsk_need_resched()设置TIF_NEED_RESCHED 标志。
    • 更新current 指向的进程的动态优先级:
      current->prio = effective_prio(current); //读current的static_prio和sleep_avg字段,根据前面“普通进程的调度”一节的公式(2)计算进程的动态优先级
      
    • 重填进程的时间片:
      current->time_slice = task_timeslice(current);
      current->first_time_slice = 0;  
      
    • 如果本地运行队列数据结构的expired_timestamp字段等于0(即过期进程集合为空),就把当前时钟节拍的值赋给expired_timestamp:
      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) ;
      
  3. 若时间片没有用完(current->time_slice≠0),检查当前进程的剩余时间片是否太长:
    // 宏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(&current->run_list);
    	llst_add_tail(&current->run_llst,
    			      this_rq()->active->queue + current->prio);
    	set_tsk_need_resched(p);
    
    

7.4.2. try_to_wake_up()函数

try_to_wake_up()函数通过把进程状态设置为TASK _RUNNING,并把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程。参数有:

  • 被唤醒进程的描述符指针( p )
  • 可以被唤醒进程状态掩码( state )
  • 标志( sync ),用来禁止被唤醒的进程抢占本地CPU上正运行的进程

流程如下:

  1. 调用函数task_rq_lock()禁用本地中断,并获得最后执行进程的CPU(它可能不同于本地CPU)所拥有的运行队列rq的锁。CPU的逻辑号存储在p->thread_info->cpu字段。

  2. 检查进程的状态p->state是否属于被当作参数传递给函数的状态掩码state,如果不是,就跳转到第9 步终止函数。

  3. 如果p->array字段不等于NULL,那么进程已经属于某个运行队列,因此跳转到第8 步。

  4. 多处理器系统中,该函数检查要被唤醒的进程是否应该从最近运行的CPU的运行队列迁移到另外一个CPU的运行队列。实际上,函数就是根据一些启发式规则选择一个目标运行队列。例如:

    • 如果系统中某些CPU空闲,就选择空闲CPU的运行队列作为目标。按照优先选择先前正在执行进程的CPU和本地CPU这种顺序来进行。
    • 如果先前执行进程的CPU的工作量远小于本地CPU的工作量,就选择先前的运行队列作为目标。
    • 如果进程最近被执行过,就选择老的运行队列作为目标(可能仍然用这个进程的数据填充硬件高速缓存)。
    • 如果把进程移到本地CPU以缓解CPU之间的不平衡,目标就是本地运行队列(见本章稍后“多处理器系统中运行队列的平衡”一节)。

    执行完这一步, 函数已经确定了目标CPU和对应的目标运行队列rq,前者将执行被唤醒的进程,后者就是进程插入的队列。

  5. 如果进程处于TASK_UNINTERRUPTIBLE状态,函数递减目标运行队列的nr_uninterruptible字段,并把进程描述符的p->activated字段设置为-1。参见后面的“recalc_task_prio()函数”一节对activated 字段的说明。

  6. 调用activate_task()函数,它依次执行下面的子步骤:

    • 调用sched_clock()获取以纳秒为单位的当前时间戳。如果目标CPU不是本地CPU,就要补偿本地时钟中断的偏差,这是通过使用本地CPU和目标CPU上最近一次发生时钟中断的相对时间戳来达到的。
      now = (sched_clock() - this_rq()->timestamp_last_tick)
      	  + rq->timest.amp_last_tick;
      
    • 调用recalc_task_prio(),把进程描述符的指针和上一步计算出的时间戳传递给它,详见下节。
    • 根据后续表7-6设置p->activated字段的值。
    • 使用第6a步中计算的时间戳设置p->timestamp字段。
    • 把进程描述符插入活动进程集合:
      enqueue_task(p, rq->active);
      rq ->nr_running++;
      
  7. 如果目标CPU不是本地CPU,或者没有设置sync标志,就检查可运行的新进程的动态优先级是否比rq 运行队列中当前进程的动态优先级高(p->prio < rq->curr->prio);如果是,就调用resched_task()抢占rq->curr。

    • 单处理器系统中,后面的函数只是执行set_tsk_need_resched()设置rq->curr进程的TIF_NEED_RESCHED标志。
    • 多处理器系统中,resched_task()也检查TIF_NEED_RESCHED的旧值是否为0、目标CPU与本地CPU是否不同、rq->curr进程的TIF_POLLING_NRFLAG标志是否清0(目标CPU没有轮询进程TIF_NEEP_RESCHED标志的值)。如果是, resched_task()调用smp_send_reschedule()产生IPI,并强制目标CPU重新调度(参见第4章“处理器间中断处理”一节)。
  8. 把进程的p->state 字段设置为TASK_RUNNING状态。

  9. 调用task_rq_unlock()来打开rq运行队列的锁并打开本地中断。

  10. 返回1(成功唤醒进程)或0(进程没有被唤醒)。

7.4.3. recalc_task_prio()函数

函数更新进程的平均睡眠时间和动态优先级。接收参数如下:

  • 进程描述符的指针p
  • 函数sched_clock()计算出的当前时间戳now

流程如下:

  1. 把min(now - p->timestamp, 109)的结果赋给局部变量sleep_time。sleep_time存放的是从进程最后一次执行开始,进程消耗在睡眠状态的纳秒数(时长<1s)。
  2. 如果sleep_time 不大于0 ,就不用更新进程的平均睡眠时间,直接跳转到第8 步。
  3. 检查进程是否不是内核钱程、进程是否从TASK_UNINTERRUPTIBLE状态(p->activated 字段等于-1 ,见前节第5 步)被唤醒、进程连续睡眠的时间是否超过给定的睡眠时间极限。如果这三个条件都满足,函数把p->sleep_avg字段设置为相当于900个时钟节拍的值(用最大平均睡眠时间减去一个标准进程的基本时间片长度获得的一个经验值)。然后,跳转到第8 步。
    睡眠时间极限依赖于进程的静态优先级,表7-2说明了它的一些典型值。简而言之,此经验规则的目的是保证已经在不可中断模式上(通常是等待磁盘I/O的操作)睡眠了很长时间的进程获得一个预先确定而且足够长的平均睡眠时间,以使这些进程既能尽快获得服务,又不会因睡眠时间太长而引起其他进程的饥饿。
  4. 执行CURRENT_BONUS宏计算进程原来的平均睡眠时间的bonus值(见表7 -3)。如果(10-bonus)大于0,函数用这个值与sleep_time相乘。因为将要把sleep_time加到进程的平均睡眠肘间上(见下面的第6 步),所以当前平均睡眠时间越短,它增加的就越快。
  5. 如果进程处于TASK_UNINTERRUPTIBLE状态而且不是内核线程, 执行下述子步骤:
    • 检查平均睡眠时间p->sleep_avg是否大子或等于进程的睡眠时间极限(见前表7-2)。如果是,把局部变量sleep_time重新置为0,因此不用调整平均睡眠时间,而直接跳转到第6 步。
    • 如果sleep_time + p->sleep_avg的和大于或等于睡眠时间极限,就把p->sleep_avg字段置为睡眠时间极限并把sleep_time设置为0。
      通过对进程平均睡眠时间的轻微限制,函数不会对睡眠时间很长的批处理进程给予过多的奖赏。
  6. 把sleep_time 加到进程的平均睡眠时间上(p->sleep_avg)。
  7. 检查p->sleep_avg是否超过1000 个时钟节拍(ns),如果是,函数把它减到1000个时钟节拍(ns)。
  8. 更新进程的动态优先级:
    p->prio = effective_prio(p);//函数在本章前面“scheduler_tick()函数"一节讨论过
    

7.4.4. schedule()函数

函数实现调度程序。其任务是从运行队列的连表中找到一个进程,并随后将CPU分配给这个进程。

直接调用

若current进程不能获得必要的资源而要立即被阻塞,可直接调用调度程序。步骤如下:

  1. current进程插入适当的等待队列。
  2. 把current进程状态改为TASK_INTERRUPTIBLE或TASE_UNINTERRUPTIBLE。
  3. 调用schedule()。
  4. 检查资源是否可用,不可用则转到第2步。
  5. 一旦资源可用,从等待队列删除current进程。

内核例程反复检查进程需要的资源是否可用,若不可用,调用schedule()把CPU分配给其他进程。当调度程序再次把CPU分给此进程时,要重新检查资源的可用性。
许多执行长迭代任务的设备驱动程序也直接调用调度程序。每次迭代循环时,驱动程序检查TIF_NEED_RESCHED标志,若需要调用schedule()自动放弃CPU。

延迟调用

可以把current的进程TIF_NEED_RESCHED 标志设置为1,而以延迟方式调用调度程序 。由于总在恢复用户态进程的执行之前检查这个标志的值(见第四章从“中断和异常返回”一 节),所以schedule()将在不久之后的某个时间被明确地调用 。
以下是延迟调用调度程序的典型例子:

  • current进程用完了它的CPU时间片时,由scheduler_tick()函数完成schedule()的延迟调用。
  • 当一个被唤醒进程的优先级比当前进程的优先级高时,由try_to_wake_up()函数完成schedule()的延迟调用 。
  • 当发出系统调用sched_setscheduler()时(见本章稍后“与调度相关的系统调用”一节)。

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

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

  1. schedule()函数在一开始先禁用内核抢占,并初始化一些局部变量:

    need_resched:以上是关于进程调度的主要内容,如果未能解决你的问题,请参考以下文章

    芹菜节拍进程在启动时分配大量内存

    top状态细分,进程状态

    RT-Thread快速入门-时钟管理

    《Linux设计与实现》学习笔记——定时器和时间管理

    踩准时钟节拍玩转时间转换,鸿蒙轻内核时间管理有妙招

    踩准时钟节拍玩转时间转换,鸿蒙轻内核时间管理有妙招