2.进程调度

 

 

 

可以用两种方法激活调度。一种是直接的,比如进程打算睡眠或出于其他原因放弃CPU;另一种是通过周期性机制,以固定的频率运行,不时检测是否有必要进行进程切换。在下文中我将这两个组件称为通用调度器(generic scheduler)或核心调度器(core scheduler)。
<sched.h> 
struct task_struct { 
... 
  int prio, static_prio, normal_prio; 
  unsigned int rt_priority; 
  struct list_head run_list; 
  const struct sched_class *sched_class; 
  struct sched_entity se; 
  unsigned int policy; 
  cpumask_t cpus_allowed; 
  unsigned int time_slice; 
... 
}
prio和normal_prio表示动态优先级,static_prio表示进程的静态优先级。静态优先级是进程启动时分配的优先级。它可以用nice和sched_setscheduler系统调用修改,否则在进程运行期间会一直保持恒定。
normal_priority表示基于进程的静态优先级和调度策略计算出的优先级(实时进程基于调度策略,非实时等于静态优先级)。因此,即使普通进程和实时进程具有相同的静态优先级,其普通优先级也是不同的。进程分支时,子进程会继承普通优先级。
但调度器考虑的优先级则保存在prio。由于在某些情况下内核需要暂时提高进程的优先级,因此需要第3个成员来表示。由于这些改变不是持久的,因此静态和普通优先级不受影响。
rt_priority表示实时进程的优先级。最低的实时优先级为0,而最高的优先级是99。值越大,表明优先级越高。这里使用的惯例不同于nice值。(prio越小,优先级越高)
sched_class表示该进程所属的调度器类。
调度器不限于调度进程,还可以处理更大的实体。故用sched_entity表示。
policy保存了对该进程应用的调度策略。Linux支持5个可能的值。
SCHED_NORMAL用于普通进程,我们主要讲述此类进程。它们通过完全公平调度器来处理。SCHED_BATCH和SCHED_IDLE也通过完全公平调度器来处理,不过可用于次要的进程。SCHED_BATCH用于非交互、CPU使用密集的批处理进程。
SCHED_RR和SCHED_FIFO用于实现软实时进程。
 
调度器类
<sched.h> 
struct sched_class { 
  const struct sched_class *next; 
  void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup); 
  void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep); 
  void (*yield_task) (struct rq *rq); 
  void (*check_preempt_curr) (struct rq *rq, struct task_struct *p); 
  struct task_struct * (*pick_next_task) (struct rq *rq); 
  void (*put_prev_task) (struct rq *rq, struct task_struct *p); 
  void (*set_curr_task) (struct rq *rq); 
  void (*task_tick) (struct rq *rq, struct task_struct *p); 
  void (*task_new) (struct rq *rq, struct task_struct *p); 
};
在必要的情况下,会调用check_preempt_curr,用一个新唤醒的进程来抢占当前进程。例如,在用wake_up_new_task唤醒新进程时,会调用该函数。

task_tick在每次激活周期性调度器时,由周期性调度器调用。
 
SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE映射到fair_sched_class,而SCHED_RR和SCHED_FIFO与rt_sched_class关联。fair_sched_class和rt_sched_class都是struct sched_class的实例,分别表示完全公平调度器和实时调度器。
check_preempt_curr在非实时是check_preempt_wakeup,在实时是check_preempt_curr_rt,check_preempt_curr函数在try_to_wake_up或wake_up_new_task中唤醒,
pick_next_task在非实时是直接取自cfs_rq的最左红黑树结点,在实时则是从0遍历位图
task_new只在非实时上找到
task_tick只在周期性调度器被调度,非实时是task_tick_fair,用于检测自身是否运行足够长时间,task_tick_rt则是根据实时类型,若是FIFO直接返回,若是RR,判断是否时间片已经用完,没用完则返回。

就绪队列

kernel/sched.c 
struct rq { 
  unsigned long nr_running; 
#define CPU_LOAD_IDX_MAX 5 
  unsigned long cpu_load[CPU_LOAD_IDX_MAX]; 
... 
  struct load_weight load; 
  struct cfs_rq cfs; 
  struct rt_rq rt; 
  struct task_struct *curr, *idle; 
  u64 clock; 
... 
};
 load提供了就绪队列当前负荷的度量。队列的负荷本质上与队列上当前活动进程的数目成正比,其中的各个进程又有优先级作为权重。
 cpu_load用于跟踪此前的负荷状态。
 cfs和rt是嵌入的子就绪队列,分别用于完全公平调度器和实时调度器。
 clock和prev_raw_clock用于实现就绪队列自身的时钟。每次调用周期性调度器时,都会更新clock的值。另外内核还提供了标准函数update_rq_clock,可在操作就绪队列的调度器中多处调用,例如,在用wakeup_new_task唤醒新进程时。
系统的所有就绪队列都在runqueues数组中,该数组的每个元素分别对应于系统中的一个CPU。在单处理器系统中,由于只需要一个就绪队列,数组只有一个元素。
 
内核也定义了一些便利的宏,其含义很明显。
kernel/sched.c 
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) 
#define this_rq() (&__get_cpu_var(runqueues)) 
#define task_rq(p) cpu_rq(task_cpu(p)) 
#define cpu_curr(cpu) (cpu_rq(cpu)->curr)
调度实体
由于调度器可以操作比进程更一般的实体,因此需要一个适当的数据结构来描述此类实体。
<sched.h> 
struct sched_entity { 
  struct load_weight load; /* 用于负载均衡 */ 
  struct rb_node run_node; 
  unsigned int on_rq; 
  u64 exec_start; 
  u64 sum_exec_runtime; 
  u64 vruntime; 
  u64 prev_sum_exec_runtime; 
... 
}
 load指定了权重,决定了各个实体占队列总负荷的比例。

 on_rq表示该实体当前是否在就绪队列上接受调度

新进程加入就绪队列时,或者周期性调度器中
会计算当前时间和exec_start之间的差值,exec_start则更新到当前时间。差值则被加到sum_exec_runtime。
在进程执行期间虚拟时钟上流逝的时间数量由vruntime统计。
在进程被撤销CPU时,其当前sum_exec_runtime值保存到prev_exec_runtime。此后,在进程抢占时又需要该数据。但请注意,在prev_exec_runtime中保存sum_exec_runtime的值,并不意味着重置sum_exec_runtime!原值保存下来,而sum_exec_runtime则持续单调增长。
 
处理优先级
p->prio = effective_prio(p);
static int effective_prio(struct task_struct *p) 
{ 
  p->normal_prio = normal_prio(p); 
/* 
* 如果是实时进程或已经提高到实时优先级,则保持优先级不变。否则,返回普通优先级:
*/ 
  if (!rt_prio(p->prio)) 
    return p->normal_prio; 
  return p->prio; 
}

static inline int normal_prio(struct task_struct *p){
  int prio; 
  if (task_has_rt_policy(p)) 
    prio = MAX_RT_PRIO-1 -p->rt_priority; 
  else 
    prio = __normal_prio(p); 
  return prio;
}
static inline int __normal_prio(struct task_struct *p) 
{ 
  return p->static_prio; 
}
为什么内核在effective_prio中检测实时进程是基于优先级数值,而非task_has_rt_policy?对于临时提高至实时优先级的非实时进程来说,这是必要的,这种情况可能发生在使用实时互斥量(RT-Mutex)时。
在新建进程用wake_up_new_task唤醒时,或使用nice系统调用改变静态优先级时,则用上文给出的方法设置p->prio。
请注意,在进程分支出子进程时,子进程的静态优先级继承自父进程。子进程的动态优先级,即task_struct->prio,则设置为父进程的普通优先级。这确保了实时互斥量引起的优先级提高不会传递到子进程。

 

 

程序的优先级范围为[0,139],有效的实时优先级(RT priority)范围为[0,99],SCHED_NORMAL和SCHED_BATCH这两个非实时任务的优先级为[100,139]。[100,139]这个区间的优先级又称为静态优先级(static priority)。之所以称为静态优先级是因为它不会随着时间而改变,内核不会修改它,只能通过系统调用nice去修改。静态优先级用进程描述符中的static_prio表示。优先级的值越低,表示具有更高的优先级,0的优先级最高。

 

计算负荷权重
进程的重要性不仅是由优先级指定的,而且还需要考虑保存在task_struct->se.load的负荷权重。set_load_weight负责根据进程类型及其静态优先级计算负荷权重。
<sched.h> 
struct load_weight { 
  unsigned long weight, inv_weight; 
};

kernel/sched.c 
static const int prio_to_weight[40] = { 
  /* -20 */ 88761, 71755, 56483, 46273, 36291, 
  /* -15 */ 29154, 23254, 18705, 14949, 11916, 
  /* -10 */ 9548, 7620, 6100, 4904, 3906, 
  /* -5 */ 3121, 2501, 1991, 1586, 1277, 
  /* 0 */ 1024, 820, 655, 526, 423, 
  /* 5 */ 335, 272, 215, 172, 137, 
  /* 10 */ 110, 87, 70, 56, 45, 
  /* 15 */ 36, 29, 23, 18, 15, 
};

kernel/sched.c 
#define WEIGHT_IDLEPRIO 2 
#define WMULT_IDLEPRIO (1 << 31) 
static void set_load_weight(struct task_struct *p) 
{ 
  if (task_has_rt_policy(p)) { 
    p->se.load.weight = prio_to_weight[0] * 2; //只要是实时进程获得的权重即为2倍
    p->se.load.inv_weight = prio_to_wmult[0] >> 1; 
    return; 
  } 
/*
* SCHED_IDLE进程得到的权重最小:
*/ 
  if (p->policy == SCHED_IDLE) { 
    p->se.load.weight = WEIGHT_IDLEPRIO; 
    p->se.load.inv_weight = WMULT_IDLEPRIO; 
    return; 
  } 
  p->se.load.weight = prio_to_weight[p->static_prio -MAX_RT_PRIO]; 与static有关
  p->se.load.inv_weight = prio_to_wmult[p->static_prio -MAX_RT_PRIO]; 
}
每次进程被加到就绪队列时,内核会调用inc_nr_running。这不仅确保就绪队列能够跟踪记录有多少进程在运行,而且还将进程的权重添加到就绪队列的权重中
调度器
1. 周期性调度器
周期性调度器在scheduler_tick中实现。如果系统正在活动中,内核会按照频率HZ自动调用该函数。
由于调度器的模块化结构,主体工程实际上比较简单,因为主要的工作可以完全委托给特定调度器类的方法:
kernel/sched.c 
if (curr != rq->idle) 
  curr->sched_class->task_tick(rq, curr); 
}
task_tick的实现方式取决于底层的调度器类。例如,完全公平调度器会在该方法中检测是否进程已经运行太长时间,以避免过长的延迟,如果当前进程应该被重新调度,那么调度器类方法会在task_struct中设置TIF_NEED_RESCHED标志,以表示该请求,而内核会在接下来的适当时机完成该请求。 
2. 主调度器
kernel/sched.c 
asmlinkage void __sched schedule(void) 
{ 
  struct task_struct *prev, *next; 
  struct rq *rq; 
  int cpu; 
  need_resched: 
  cpu = smp_processor_id(); 
  rq = cpu_rq(cpu);  
  prev = rq->curr;   //首先确定当前就绪队列,并在prev中保存一个指向(仍然)活动进程的task_struct的指针
...
  __update_rq_clock(rq); 
  clear_tsk_need_resched(prev); //类似于周期性调度器,内核也利用该时机来更新就绪队列的时钟,并清除当前运行进程task_struct中的重调度标志TIF_NEED_RESCHED。
...
  if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&unlikely(signal_pending(prev)))) { 
    prev->state = TASK_RUNNING; 
  } else { 
    deactivate_task(rq, prev, 1); 
  } 
...
  prev->sched_class->put_prev_task(rq, prev); 
  next = pick_next_task(rq, prev); //优先调度实时进程
if (likely(rq->nr_running == rq->cfs.nr_running)) {
        p = fair_sched_class.pick_next_task(rq);
        if (likely(p))
            return p;
    }

 


...
  if (likely(prev != next)) { //不见得必然选择一个新进程。也可能其他进程都在睡眠,当前只有一个进程能够运行,这样它自然就被留在CPU上。
    rq->curr = next; 
    context_switch(rq, prev, next); 
  } 
...
下列代码检测当前进程的重调度位是否设置,并跳转到如上所述的标号,重新开始搜索一个新进程:
  if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) 
    goto need_resched; 
}
上述代码片段可能在两个不同的上下文中执行。在没有执行上下文切换时,它在schedule函数的末尾直接执行。但如果已经执行了上下文切换,当前进程会正好在这以前停止运行,新进程已经接管了CPU。但稍后在前一进程被再次选择运行时,它会刚好在这一点上恢复执行。在这种情况下,由于prev不会指向正确的进程,所以需要通过current和test_thread_flag找到当前线程。
原因:
switch_to3个参数:prev,next和last。last的作用:A切换到B时,prev=A,next=B, 经过一定时间后,A被重新调度到CPU上执行时,A需要知道从哪个进程切换过来的,需要从last参数得到。
4. 上下文切换
kernel/sched.c 
static inline void context_switch(struct rq *rq, struct task_struct *prev, 
struct task_struct *next) 
{ 
  struct mm_struct *mm, *oldmm; 
  prepare_task_switch(rq, prev, next); 
  mm = next->mm; 
  oldmm = prev->active_mm; 
..
内核线程没有自身的用户空间内存上下文,可能在某个随机进程地址空间的上部执行。其task_struct->mm为NULL。从当前进程“借来”的地址空间记录在active_mm中。
enter_lazy_tlb通知底层体系结构不需要切换虚拟地址空间的用户空间部分。这种加速上下文切换的技术称之为惰性TLB。
  if (unlikely(!mm)) { 
    next->active_mm = oldmm; 
    atomic_inc(&oldmm->mm_count); 
    enter_lazy_tlb(oldmm, next); 
} else 
  switch_mm(oldmm, mm, next);该工作的细节取决于处理器,主要包括加载页表、刷出地址转换后备缓冲器(部分或全部)、向内存管理单元(MMU)提供新的信息。
...
如果前一进程是内核线程(即prev->mm为NULL),则其active_mm指针必须重置为NULL,以断开与借用的地址空间的联系
  if (unlikely(!prev->mm)) { 
    prev->active_mm = NULL; 
    rq->prev_mm = oldmm; 
  } 
...
/* 这里我们只是切换寄存器状态和栈。 */ 
  switch_to(prev, next, prev); //编译器指令
  barrier(); 
/* 
* this_rq必须重新计算,因为在调用schedule()之后prev可能已经移动到其他CPU,
* 因此其栈帧上的rq可能是无效的。
*/ 
  finish_task_switch(this_rq(), prev); 
}
finish_task_switch完成一些清理工作,使得能够正确地释放锁。finish_task_switch的有趣之处在于,调度过程可能选择了一个新进程,而清理则是针对此前的活动进程。请注意,这不是发起上下文切换的那个进程,而是系统中随机的某个其他进程
完全公平调度类
kernel/sched_fair.c 
static const struct sched_class fair_sched_class = { 
  .next = &idle_sched_class, 
  .enqueue_task = enqueue_task_fair, 
  .dequeue_task = dequeue_task_fair, 
  .yield_task = yield_task_fair, 
  .check_preempt_curr = check_preempt_wakeup, 
  .pick_next_task = pick_next_task_fair, 
  .put_prev_task = put_prev_task_fair,
... 
  .set_curr_task = set_curr_task_fair, 
  .task_tick = task_tick_fair, 
  .task_new = task_new_fair, 
};
所有与虚拟时钟有关的计算都在update_curr中执行,该函数在系统中各个不同地方调用,包括周期性调度器之内。

 

static void update_curr(struct cfs_rq *cfs_rq) 
{ 
  struct sched_entity *curr = cfs_rq->curr; 
  u64 now = rq_of(cfs_rq)->clock; //获取主调度器就绪队列的实际时钟值
  unsigned long delta_exec; 
  if (unlikely(!curr)) 
    return; 
  ...
  delta_exec = (unsigned long)(now -curr->exec_start); //计算当前和上一次更新负荷统计量时两次的时间差,并将其余的工作委托给__update_curr。
  __update_curr(cfs_rq, curr, delta_exec); 
  curr->exec_start = now; 
}

static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec) 
{ 
  unsigned long delta_exec_weighted; 
  u64 vruntime; 
  curr->sum_exec_runtime += delta_exec; //物理时间的更新
...
  delta_exec_weighted = delta_exec;
  if (unlikely(curr->load.weight != NICE_0_LOAD)) { 
    delta_exec_weighted = calc_delta_fair(delta_exec_weighted, &curr->load); 
  } 
  curr->vruntime += delta_exec_weighted; 
...
calc_delta_fair所作的就是根据下列公式计算: (越重要的进程会有越高的优先级(即,越低的nice值),会得到更大的权重,因此累加的虚拟运行时间会小一些)

 

kernel/sched_fair.c 
/* 
* 跟踪树中最左边的结点的vruntime,维护cfs_rq->min_vruntime的单调递增性
*/ 
//first_fair是一个辅助函数,检测树是否有最左边的结点,即是否有进程在树上等待调度。 if (first_fair(cfs_rq)) {   vruntime = min_vruntime(curr->vruntime,   __pick_next_entity(cfs_rq)->vruntime); } else   vruntime = curr->vruntime;   cfs_rq->min_vruntime =   max_vruntime(cfs_rq->min_vruntime, vruntime); }
 
红黑树的排序过程是根据下列键进行的: 
static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
  return se->vruntime -cfs_rq->min_vruntime;
键值较小的结点,排序位置就更靠左,因此会被更快地调度。
(1) 在进程运行时,其vruntime稳定地增加,它在红黑树中总是向右移动的。因为越重要的进程vruntime增加越慢,因此它们向右移动的速度也越慢,这样其被调度的机会要大于次要进程,这刚好是我们需要的。
(2) 如果进程进入睡眠,则其vruntime保持不变。因为每个队列min_vruntime同时会增加(回想一下,它是单调的!),那么睡眠进程醒来后,在红黑树中的位置会更靠左,因为其键值变得更小了。① 
延迟跟踪 
sysctl_sched_latency可通过/proc/sys/kernel/sched_latency_ns控制,默认值为20 000 000纳秒或20毫秒,即保证每个可运行的进程都应该至少运行一次的某个时间间隔。
sched_nr_latency,控制在一个延迟周期中处理的最大活动进程数目。如果活动进程的数目超出该上限,则延迟周期也成比例地线性扩展。
 
分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和   (公式1)
vruntime = 实际运行时间 * 1024 / 进程权重 (公式2)
vruntime =调度周期 * 1024 / 所有进程总权重 
 CFS的思想就是让每个进程的vruntime互相追赶,而由于每个进程的vruntime增加速度不同,权重越大的增加的越慢,这样就能获得更多的CPU执行时间了
加入队列

 

 

kernel/sched_fair.c 
static void 
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) 
{ 
  u64 vruntime; 
  vruntime = cfs_rq->min_vruntime; 
  if (initial) 
    vruntime += sched_vslice_add(cfs_rq, se); //加上公式中的vruntime
  if (!initial) { 
    vruntime -= sysctl_sched_latency; //减去基准虚拟实践
    vruntime = max_vruntime(se->vruntime, vruntime); 
  } 
  se->vruntime = vruntime; 
}
只有在新进程加入到队列时才会设置
initial,即fork创建时

选择下一个进程

 

kernel/sched_fair.c 
static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) 
{ 
/* 树中不保存“当前”进程。 */ 
if (se->on_rq) { 
   __dequeue_entity(cfs_rq, se); 
}
...
当前执行进程不保存在就绪队列上,因此使用__dequeue_entity将其从树中移除。
尽管该进程不再包含在红黑树中,但进程和就绪队列之间的关联没有丢失,因为curr标记了当前运行的进程:
kernel/sched_fair.c
  cfs_rq->curr = se;
  se->prev_sum_exec_runtime = se->sum_exec_runtime;
处理周期性调度器

 

 

 

首先,一如既往地使用update_curr更新统计量。如果队列的nr_running计数器表明队列上可运行的进程少于两个,则实际上无事可做。如果某个进程应该被抢占,那么至少需要有另一个进程能够抢占它。如果进程数目不少于两个,则由check_preempt_tick作出决策:
kernel/sched_fair.c 
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) 
{ 
  unsigned long ideal_runtime, delta_exec; 
  ideal_runtime = sched_slice(cfs_rq, curr); 
  delta_exec = curr->sum_exec_runtime -curr->prev_sum_exec_runtime; 
//进程对应
额对应的实际时间长度在sched_slice中计算,如上文上述,进程在CPU上已经运行的实际时间间隔由sum_exec_runtime-prev_sum_exec_runtime给出
  if (delta_exec > ideal_runtime) 
    resched_task(rq_of(cfs_rq)->curr); //这会在task_struct中设置TIF_NEED_RESCHED标志,核心调度器会在下一个适当时机发起重调度。
}
唤醒抢占
当在try_to_wake_up和wake_up_new_task中唤醒进程时,内核使用check_preempt_curr看看是否新进程可以抢占当前运行的进程。请注意该过程不涉及核心调度器!对完全公平调度器处理的进程,则由check_preempt_wakeup函数执行该检测。
kernel/sched_fair.c 
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p) 
{ 
  struct task_struct *curr = rq->curr; 
  struct cfs_rq *cfs_rq = task_cfs_rq(curr); 
  struct sched_entity *se = &curr->se, *pse = &p->se; 
  unsigned long gran; 
新唤醒的进程不必一定由完全公平调度器处理。如果新进程是一个实时进程,则会立即请求重调度,因为实时进程总是会抢占CFS进程
  if (unlikely(rt_prio(p->prio))) { 
    update_rq_clock(rq); 
    update_curr(cfs_rq); 
    resched_task(curr); 
    return; 
  } 
...
当运行进程被新进程抢占时,内核确保被抢占者至少已经运行了某一最小时间限额。
  gran = sysctl_sched_wakeup_granularity; 
  if (unlikely(se->load.weight != NICE_0_LOAD)) 
    gran = calc_delta_fair(gran, &se->load); 
...
如果新进程的虚拟运行时间,加上最小时间限额,仍然小于当前执行进程的虚拟运行时间(由其调度实体se表示),则请求重调度:
  if (pse->vruntime + gran < se->vruntime) 
    resched_task(curr); 
}

处理新进程

static void task_new_fair(struct rq *rq, struct task_struct *p)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(p);
    struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
    int this_cpu = smp_processor_id();

    sched_info_queued(p);

    update_curr(cfs_rq);
在这种情况下,调用place_entity时的initial参数设置为1,以便用sched_vslice_add计算初始的vruntime。
    place_entity(cfs_rq, se, 1);

    /* 'curr' will be NULL if the child belongs to a different group */
sysctl_sched_child_runs_first控制新建子进程是否应该在父进程之前运行,默认为1
    if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) &&
            curr && curr->vruntime < se->vruntime) {
        /*
         * Upon rescheduling, sched_class::put_prev_task() will place
         * 'current' within the tree based on its new key value.
         */
如果父进程的虚拟运行时间(由curr表示)小于子进程的虚拟运行时间,则意味着父进程将在子进程之前调度运行。回想一下前文的内容,可知虚拟运算时间比较小,则在红黑树中的位置比较靠左。如果子进程应该在父进程之前运行,则二者的虚拟运算时间需要换过来。
        swap(curr->vruntime, se->vruntime);
    }
然后子进程按常规加入就绪队列,并请求重调度。 
    enqueue_task_fair(rq, p, 0);
    resched_task(rq->curr);
}
实时调度类
kernel/sched.c 
struct rt_prio_array { 
  DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* 包含1比特用于间隔符 */ 
  struct list_head queue[MAX_RT_PRIO]; 
}; 
struct rt_rq { 
  struct rt_prio_array active; 
};
实时调度器类中对应于update_cur的是update_curr_rt,该函数将当前进程在CPU上执行花费的时间记录在sum_exec_runtime中。所有计算的单位都是实际时间,不需要虚拟时间。这样就简化了很多。
具有相同优先级的所有实时进程都保存在一个链表中,表头为active.queue[prio],而active.bitmap位图中的每个比特位对应于一个链表,凡包含了进程的链表,对应的比特位则置位。如果链表中没有进程,则对应的比特位不置位。
调度器操作
两个比较有趣的操作分别是,如何选择下一个将要执行的进程,以及如何处理抢占。首先考虑pick_next_task_rt,该函数放置选择下一个将执行的进程。其代码流程图在图2-24给出。
sched_find_first_bit是一个标准函数,可以找到active.bitmap中第一个置位的比特位,这意味着高的实时优先级(对应于较低的内核优先级值),因此在较低的实时优先级之前处理。
周期调度的实现同样简单。SCHED_FIFO进程最容易处理。它们可以运行任意长的时间,而且必须使用yield系统调用将控制权显式传递给另一个进程:
kernel/sched.c 
static void task_tick_rt(struct rq *rq, struct task_struct *p) 
{ 
  update_curr_rt(rq); 
/* 
* 循环进程需要一种特殊形式的时间片管理。
* 先进先出进程没有时间片。
*/ 
SCHED_FIFO进程最容易处理。它们可以运行任意长的时间,而且必须使用yield系统调用将控制权显式传递给另一个进程   if (p->policy != SCHED_RR)     return; ...
如果当前进程是循环进程,则减少其时间片。在尚未超出时间段时,没什么可作的
  if (--p->time_slice) 
    return; 
  p->time_slice = DEF_TIMESLICE; 
/* 
* 如果不是队列上的唯一成员,则重新排队到末尾。
*/ 
  if (p->run_list.prev != p->run_list.next) { 
    requeue_task_rt(rq, p); 
如果该进程不是链表中唯一的进程,则重新排队到末尾。通过用set_tsk_need_resched设置TIF_NEED_RESCHED标志,照常请求重调度
    set_tsk_need_resched(p); 
  } 
}

 

为将进程转换为实时进程,必须使用sched_setscheduler系统调用。这里不详细讨论该函数了,因为它只执行了下列简单任务。
 使用deactivate_task将进程从当前队列移除。
 在task_struct中设置实时优先级和调度类。
 重新激活进程。
如果进程此前不在任何就绪队列上,那么只需要设置调度类和新的优先级数值。停止进程活动和重激活则是不必要的。
要注意,只有具有root权限(或等价于CAP_SYS_NICE)的进程执行了sched_setscheduler系统调用,才能修改调度器类或优先级。否则,下列规则适用。
 调度类只能从SCHED_NORMAL改为SCHED_BATCH,或反过来。改为SCHED_FIFO是不可能的。
 只有目标进程的UID或EUID与调用者进程的EUID相同时,才能修改目标进程的优先级。此外,优先级只能降低,不能提升。

问题:是否可以用获得的时间片减去已运行时间作为键值呢?

 1.非实时进程优先级

2.主调用器

3.周期性调度器(实时非实时)

4.唤醒新进程(实时非实时)

5.处理新进程(非实时)

 

posted @ 2022-04-26 22:48  while(true);;  阅读(94)  评论(0编辑  收藏  举报