进程调度

来源:互联网 发布:北京好的婚纱摄影知乎 编辑:程序博客网 时间:2024/06/06 00:05

调度器的实现

概述

内核必须提供一种方法在各个进程之间尽可能共享CPU时间,还要考虑不同任务的优先级。

schedule()函数是理解调度操作的起点。

linux调度器的一个杰出特性是不需要时间片,调度的依据不再是时间片,而是依据进程就绪队列中等待时间的长短,在就绪队列中等待时间越长的进程得到调度的机会就越大。调度器的一般原理是,按照所能分配的计算能力,向系统中的每个进程提供最大的公平性。或者从另一个角度说,它试图确保没有进程被亏待。

就绪队列用一棵红黑树组织,还装配了虚拟时钟。

虚拟时钟的时间流逝速度慢于实际的时钟,精确的速度依赖于当前等待调度器挑选的进程的数目。假定队列上有n个进程,那么虚拟时钟将以实际时钟的1/n的速度运行。

假定虚拟时间由fair_clock给出,等待时间保存在wait_runtime,内核使用差值fair_clock-wait_runtime对红黑树的每个进程排序。

数据结构

有两种方法可以激活调度器。一种是直接的,比如进程打算睡眠或出于其他原因放弃CPU,另一种是通过周期性机制,以固定频率运行,不时检测是否有必要进行进程切换。

task_struct的成员

task_struct中有几个成员与调度有关


task_struct采用了3个成员来表示进程的优先级:prio,normal_prio表示动态优先级,static_prio表示进程的静态优先级。静态优先级是进程启动时分配的优先级。他可以用nice()和sched_setscheduler()系统调用修改,否则在进程运行期间会一直保持稳定。

normal_priority表示基于进程的静态优先级和调度策略计算出的优先级。调度器考虑的优先级保存在prio。

rt_priority表示实时进程的优先级。最低实时优先级为0,而最高优先级是99,值越大,优先级越高。

sched_class表示该进程所属的调度器类。

调度器不限于调度进程,还可以调度更大的实体。可以用于实现组调度。

这种一般性要求调度器不直接操作进程,而是处理可调度实体。一个实体由sched_entity的一个实例表示。

se在task_strcut中内嵌了一个sched_entity实例。

policy保存了对该进程应用的调度策略。linux支持5个可能的值。

SCHED_NORMAL用于普通进程,它们通过完全公平调度器处理。SCHED_BATCH和SCHED_IDLE也通过完全公平调度来处理。SCHED_BATCH用于非交互,CPU使用密集的批处理进程。调度决策对此类进程给予冷处理,它们不会抢占CFS处理的另一个进程,因此不会干扰交互式进程。

SCHED_RR和SCHED_FIFO用于实现软实时进程。SCHED_RR实现了一种循环方法,而SCHED_FIFO使用了先进先出机制。这些不是由CFS处理,而是由实时调度器类处理。

cpus_allowed是一个位域,在多处理器系统上使用,用来限制进程可以在哪些CPU上运行。

run_list和time_slice是循环实时调度器所需要的,但不用于完全公平调度。

调度器类

对各个调度类,都必须提供struct sched_class的一个实例。调度类之间的层次结构是平坦的,实时进程最重要,在完全公平进程之前处理,而完全公平进程则优于空闲进程,空闲进程只有CPU无事可做时才可处于活动状态。

  • enqueue_task向就绪队列添加一个新进程,在进程从睡眠状态变为可运行状态时,即可发生该操作。
  • dequeue_task提供逆向操作,将一个进程从就绪队列中删除。在进程从可运行状态切换到不可运行状态时,就会发生该操作。
  • 在进程想要自愿放弃对处理器的控制权时,可使用sched_yield系统调用。
  • check_preemt_curr,用一个新唤醒的进程来抢占当前进程。
  • pick_next_task用于选择下一个将要运行的进程,put_prev_task则在用另一个进程代替当前运行的进程之前调用,这些操作并不等价于将进程加入或撤销出就绪队列。
  • set_curr_task,改变进程的调度策略。


就绪队列

核心调度器用于管理活动进程进程的主要数据结构称之为就绪队列。各个CPU都有自身的就绪队列,各个活动进程只出现在一个就绪队列中,在多个CPU上同时运行一个进程是不可能的(但发源自同一个进程的不同线程可以在不同CPU上运行,CPU对进程和线程的调度并不做太大区分)

系统的所有就绪队列都在runqueues数组中,该数组的每个元素分别对应与系统中的一个CPU。在单处理器系统中,由于只需要一个就绪队列,数组只有一个元素。

调度实体

处理优先级

优先级的内核表示

内核可以使用nice命令设置进程的静态优先级,nice值在-20到19之间,值越低优先级越高。内核使用0到139表示内部优先级,值越低优先级越高。

计算优先级

考虑三种优先级动态优先级(tash_struct->prio),普通优先级(task_struct->normal_prio),静态优先级(task_struct->static_prio)。

static_prio是计算的起点,假定它已经设置好,内核计算其他优先级

p->prio=effective_prio(p);

辅助函数effective_prio执行了以下操作



普通优先级需要根据普通进程和实时进程进行不同的计算。这一次注意与effective_prio相比,实时进程的检测不再基于优先级数值,而是通过task_struct中设置的调度策略来检测。

__normal_priority直接返回静态优先级。

在新建进程wake_up_new_task唤醒时,或使用nice系统调用改变静态优先级时,则用上文给出的方法设置p->prio。

在进程分支子进程时,子进程的静态优先级继承自父进程,子进程的动态优先级设置为父进程的普通优先级。

计算负荷权重

进程的重要性不仅是由优先级指定的,而且还需要考虑保存在task_struct->se.load的负荷权重。set_load_weight负责根据进程类型及其静态计算优先级计算权重。

负荷权重包含在数据结构load_weight中:

一般概念是,进程每降低一个nice值,则多获得10%的CPU时间,每升高一个值,则放弃10%的CPU时间,内核将优先级转换为权重值。

对内核使用的范围[0,39]中的每个nice级别,该数组中都有一个对应的项,个数组之间的乘数因子为1.25

执行转换的代码也需要考虑实时进程。实时进程的权重是普通进程的两倍。另一方面,SCHED_IDLE进程的权重总是非常小

每次进程被加入到就绪队列中时,内核调用inc_nr_running,这确保了就绪队列能跟踪记录多少进程正在运行,而且还将进程的权重添加到就绪队列权重这中。

核心调度器

调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间。

周期性调度器

周期性调度器在scheduler_tick中实现,如果系统正在活动中,内核会按照频率Hz自动调用该函数。该函数有下面两个主要任务。

  • 管理内核中与整个系统和各个进程的调度相关的统计量。期间主要执行的操作是对各种计数器加1。
  • 激活负责当前进程的调度类的周期性调度方法。


函数的第一部分处理就绪队列时钟的更新。委托给_update_rq_clock()完成,本质上就是增加rq的时间戳。update_cpu_load负责更新就绪队列的cpu_load[]数组。本质上相当于将数组中先前存储的负荷值向后移动一个位置,将当前就绪队列的负荷记入数组中的第一个位置。如果当前进程应该被重新调度,那么调度器类方法会在task_struct中设置TIF_NEED_RESCHED标志,以表示该请求。

0 0
原创粉丝点击