linux进程调度介绍

一、Linux新老版本调度器对比

在 2.6 版本的内核之前,当很多任务都处于活动状态时,调度器有很明显的限制。这是由于调度器是使用一个复杂度为 O(n) 的算法实现的。在这种调度器中,调度任务所花费的时间是一个系统中任务个数的函数。换而言之,活动的任务越多,调度任务所花费的时间越长。在任务负载非常重时,处理器会因调度消耗掉大量的时间,用于任务本身的时间就非常少了。因此,这个算法缺乏可伸缩性。

在对称多处理系统(SMP)中,2.6 版本之前的调度器对所有的处理器都使用一个运行队列。这意味着一个任务可以在任何处理器上进行调度 —— 这对于负载均衡来说是好事,但是对于内存缓存来说却是个灾难。例如,假设一个任务正在CPU-1 上执行,其数据在这个处理器的缓存中。如果这个任务被调度到 CPU-2 上执行,那么数据就需要先在 CPU-1 使其无效,并将其放到 CPU-2 的缓存中。

以前的调度器还使用了一个运行队列锁;因此在 SMP 系统中,选择一个任务执行就会阻碍其他处理器操作这个运行队列。结果是空闲处理器只能等待这个处理器释放出运行队列锁,这样会造成效率的降低。(Linux内核学习笔记:SMP、UMA、NUMA

最后,在早期的内核中,抢占是不可能的;这意味着如果有一个低优先级的任务在执行,高优先级的任务只能等待它完成。

1.1Linux2.6 调度器简介

2.6版本的调度器是由 Ingo Molnar 设计并实现的。Ingo 从 1995 年开始就一直参与Linux 内核的开发。他编写这个新调度器的动机是为唤醒、上下文切换和定时器中断开销建立一个完全 O(1) 的调度器。触发对新调度器的需求的一个问题是 Java™ 虚拟机(JVM)的使用。Java编程模型使用了很多执行线程,在 O(n) 调度器中这会产生很多调度负载。O(1) 调度器在这种高负载的情况下并不会受到太多影响,因此 JVM 可以有效地执行。

2.6版本的调度器解决了以前调度器中发现的 3 个主要问题(O(n) 和 SMP 可伸缩性的问题),还解决了其他一些问题。现在我们将开始探索一下 2.6 版本的调度器的基本设计。

1.1.1主要的调度结构

首先我们来回顾一下 2.6 版本的调度器结构。每个 CPU 都有一个运行队列,其中包含了 140 个优先级列表,它们是按照先进先出的顺序进行服务的。被调度执行的任务都会被添加到各自运行队列优先级列表的末尾。每个任务都有一个时间片,这取决于系统允许执行这个任务多长时间。运行队列的前 100 个优先级列表保留给实时任务使用,后 40 个用于用户任务(参见图 1)。我们稍后将来看一下为什么这种区别非常重要。

图 1. Linux 2.6 调度器的运行队列结构



除了 CPU 的运行队列(称为活动运行队列(active runqueue))之外,还有一个过期运行队列。当活动运行队列中的一个任务用光自己的时间片之后,它就被移动到过期运行队列(expired runqueue 中。在移动过程中,会对其时间片重新进行计算(因此会体现其优先级的作用;稍后会更详细地介绍)。如果活动运行队列中已经没有某个给定优先级的任务了,那么指向活动运行队列和过期运行队列的指针就会交换,这样就可以让过期优先级列表变成活动优先级的列表。

调度器的工作非常简单:它在优先级最高的队列中选择一个任务来执行。为了使这个过程的效率更高,内核使用了一个位图来定义给定优先级列表上何时存在任务。因此,在大部分体系架构上,会使用一条 find-first-bit-set 指令在 5 个 32 位的字(140 个优先级)中哪一位的优先级最高。查找一个任务来执行所需要的时间并不依赖于活动任务的个数,而是依赖于优先级的数量。这使得 2.6 版本的调度器成为一个复杂度为 O(1) 的过程,因为调度时间既是固定的,而且也不会受到活动任务个数的影响。

尽管优先级调度在 SMP 系统上也可以工作,但是它这种大锁体系架构意味着当一个 CPU 选择一个任务进行分发调度时,运行队列会被这个 CPU 加锁,其他 CPU 只能等待。2.6版本的调度器不是使用一个锁进行调度;相反,它对每个运行队列都有一个锁。这样允许所有的 CPU 都可以对任务进行调度,而不会与其他 CPU 产生竞争。另外,由于每个处理器都有一个运行队列,因此任务通常都是与 CPU 密切相关的,可以更好地利用 CPU 的热缓存。

Linux2.6 版本调度器的另外一个优点是它允许抢占。这意味着当高优先级的任务准备运行时低优先级的任务就不能执行了。调度器会抢占低优先级的进程,并将这个进程放回其优先级列表中,然后重新进行调度。

似乎 2.6 版本调度器的 O(1) 特性和抢占特性还不够,这个调度器还提供了动态任务优先级和 SMP 负载均衡功能。

1.1.2动态任务优先级

为了防止任务独占 CPU 从而会饿死其他需要访问 CPU 的任务,Linux 2.6 版本的调度器可以动态修改任务的优先级。这是通过惩罚 CPU 绑定的任务而奖励 I/O 绑定的任务实现的。I/O 绑定的任务通常使用 CPU 来设置 I/O,然后就睡眠等待I/O 操作完成。这种行为为其他任务提供了 CPU 的访问能力。

由于 I/O 绑定型的任务对于 CPU 访问来说是无私的,因此其优先级减少(奖励)最多 5 个优先级。CPU 绑定的任务会通过将其优先级增加最多 5 个优先级进行惩罚。

任务到底是 I/O 绑定的还是 CPU 绑定的,这是根据交互性原则确定的。任务的交互性指标是根据任务执行所花费的时间与睡眠所花费的时间的对比程度进行计算的。注意,由于 I/O 任务先对 I/O 进行调度,然后再进行睡眠,因此 I/O 绑定的任务会在睡眠和等待 I/O 操作完成上面花费更多的时间。这会提高其交互性指标。有一点值得注意,优先级的调整只会对用户任务进行,对于实时任务来说并不会对其优先级进行调整。

1.1.3SMP负载均衡

在 SMP 系统中创建任务时,这些任务都被放到一个给定的 CPU 运行队列中。通常来说,我们无法知道一个任务何时是短期存在的,何时需要长期运行。因此,最初任务到 CPU 的分配可能并不理想。

为了在 CPU 之间维护任务负载的均衡,任务可以重新进行分发:将任务从负载重的 CPU 上移动到负载轻的 CPU 上。Linux 2.6 版本的调度器使用负载均衡(load balancing提供了这种功能。每隔 200ms,处理器都会检查 CPU 的负载是否不均衡;如果不均衡,处理器就会在 CPU 之间进行一次任务均衡操作。

这个过程的一点负面影响是新 CPU 的缓存对于迁移过来的任务来说是冷的(需要将数据读入缓存中)。

记住 CPU 缓存是一个本地(片上)内存,提供了比系统内存更快的访问能力。如果一个任务是在某个 CPU 上执行的,与这个任务有关的数据都会被放到这个 CPU 的本地缓存中,这就称为热的。如果对于某个任务来说,CPU 的本地缓存中没有任何数据,那么这个缓存就称为冷的。

不幸的是,保持 CPU 繁忙会出现 CPU 缓存对于迁移过来的任务为冷的情况。

综上所述2.6版继承和发扬2.4版调度器的特点:

(1)  交互式作业优先

(2)  轻载条件下调度/唤醒的高性能

(3)  公平共享
(4)  基于优先级调度

(5)  搞CPU使用率

(6)  SMP高效亲和

(7)  实时调度和cpu绑定等调度手段

在此基础之上的新特征:

(1)  O(1)调度算法,调度器开销恒定(与当前系统负载无关),实时性能更好

(2)  高可扩展性,锁粒度大幅度减小

(3)  新设计的SMP亲和方法

(4)  优化计算密度型的批处理作业调度

(5)  重载条件下调度器工作更平滑

(6)  子进程先于父进程运行等其他改进

(7)  增加了对可抢占内核的支持

二、代码分析

2.1主要文件

include/linux/sched.h

2.2文件代码

   分析远代码可以用下载linux2.6的源代码使用软件source insight进行分析,也可以在http://lxr.oss.org.cn查看linux的源代码(网站资料比较丰富,各个版本的代码都有,比较source insight利于查找不同的代码,但效率较低)

   task_struct包含有进程的描述信息、控制信息以及资源信息,是进程的静态描述。2.6 版的内核仍然用 task_struct 来表征进程,尽管对线程进行了优化,但线程的内核表示仍然与进程相同。随着调度器的改进,task_struct 的内容也有了改进,交互式进程优先支持、内核抢占支持等新特性,在task_struct 中都有所体现。在 task_struct 中,有的属性是新增加的,有的属性的值的含义发生了变化,而有的属性仅仅是改了一下名字。可称为进程控制块(TCB),主要包含进程标识符、优先级、堆栈空间、进程状态

task_struct源代码定义在kernel/include/Linux/sched.h中

struct task_struct 

{

volatile longstate;    /* -1 unrunnable, 0 runnable, >0 stopped */    

structthread_info *thread_info;

atomic_tusage;

unsigned longflags;    /* per process flags, defined below */

unsigned longptrace;

int lock_depth;        /* Lock depth */

int prio, static_prio;

structlist_head run_list;

prio_array_t *array;

unsigned long sleep_avg;

long interactive_credit;

unsigned long longtimestamp;

int activated;

unsigned long policy;

cpumask_t cpus_allowed;

unsigned int time_slice, first_time_slice;

structlist_head tasks;

/* ptrace_list/ptrace_children formsthe list of my children

 * that were stolen by a ptracer.

 */

structlist_head ptrace_children;

structlist_head ptrace_list;

structmm_struct *mm, *active_mm;

2.2.1核心结构 task_struct中的state

进程的状态仍然用 state 表示

#define TASK_RUNNING                   0
#define TASK_INTERRUPTIBLE       1
#define TASK_UNINTERRUPTIBLE  2
#define TASK_STOPPED                   4
#define TASK_ZOMBIE                      8
#define TASK_DEAD                         16

Linux2.4 task_struct中的state

#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE     1
#define TASK_UNINTERRUPTIBLE   2
#define TASK_ZOMBIE        4
#define TASK_DEAD          8

2.6新增加了两种状态:TRACED、DEAD。

新增加的TASK_DEAD指的是已经退出且不需要父进程来回收的进程。TASK_TRACED供调试使用。

原有的几个state:

TASK_ZOMBIE一个已经终止的但仍保留有任务的进程(已经死了,户口未注销)。

TASK_RUNNING就绪态(准确的说你应该是task_runable)

TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE不同深度的睡眠态

TASK_STOPPED描述一个已经停止的进程,当进程收到一个特殊信号或被时候ptrace系统调用的进程监控,并将监控权交给监控进程。Linux2.4版,内核在创建进程是,为每个进程配两个连续的物理页面(8KB)它的顶端(低地址部分)用来存储进程的task_struct结构(约1KB),剩下的约7KB就是进程的系统空间堆栈,内核可以通过栈寄存器指针ESP快速地方位该进程。

2.2.2thread_info

在linux2.6中。这两个页面顶端存放的不再是进程的整个task_struct结构,而是task_中的thread_info,task_struct的大部分信息保存在栈外,通过thread_info的task指针可以方便地访问到。

thread_info是描述一个描述任务的重要结构体,下面将看到thread_info的数据结构定义(/include/asm-386/thread_.h)

struct thread_info 
{
struct task_struct     *task;     /* main task structure */
struct exec_domain      *exec_domain;   /* execution domain */
unsigned long      flags;     /* low level flags */
unsigned long     status;    /* thread-synchronous flags */
__u32         cpu;            /* current CPU */
 __s32        preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t   addr_limit;  /* thread address space:  0-0xBFFFFFFF for user-thead 0-0xFFFFFFFF for kernel-thread  */
struct restart_block  restart_block;
 unsigned long      previous_esp;   /* ESP of the previous stack in case                                                   of nested (IRQ) stacks
__u8     supervisor_stack[0];
};

一些主要的环境信息如下:

task指针指向其对应的任务控制块

preempt_count是用来表示内核能否被抢占的使能成员,如果它大于0.表示内核不能被抢占:如果等于0,则表示内核处于安全状态(即没有加锁),可以抢占。

Flags里面有一个TIF_NEED_RESCHED位,如果此标志位为1.则表示应该尽快启动调度器。

2.2.3timestamp

  进程发生调度事件的时间(单位是nanosecond,见下)。包括以下几类:

  • 被唤醒的时间(在 activate_task() 中设置);
  • 被切换下来的时间(schedule());
  • 被切换上去的时间(schedule());
  • 负载平衡相关的赋值(见"调度器相关的负载平衡")。

从这个值与当前时间的差值中可以分别获得"在就绪队列中等待运行的时长"、"运行时长"等与优先级计算相关的信息(见"优化了的优先级计算方法")。

2.2.4static_prio

prio是进程的动态优先级,相当于 2.4 中 goodness() 的计算结果,在 0~MAX_PRIO-1 之间取值(MAX_PRIO 定义为 140),其中 0~MAX_RT_PRIO-1 (MAX_RT_PRIO 定义为100)属于实时进程范围,MAX_RT_PRIO~MX_PRIO-1 属于非实时进程。数值越大,表示进程优先级越小。是调度器选择候选进程next的主要依据,static_则是进程的静态优先级,应该是进程开始时从父进程继承来的。nice值沿用 Linux 的传统,在 -20 到 19 之间变动,数值越大,进程的优先级越小。nice 是用户可维护的,但仅影响非实时进程的优先级。2.6 内核中不再存储 nice 值,而代之以 static_prio。进程初始时间片的大小仅决定于进程的静态优先级,这一点不论是实时进程还是非实时进程都一样,不过实时进程的 static_prio 不参与优先级计算。

nice 与 static_prio 之间的关系如下:static_prio = MAX_RT_PRIO + nice + 20

内核定义了两个宏用来完成这一转换:PRIO_TO_NICE()、NICE_TO_PRIO()。

#defineNS_TO_JIFFIES(TIME)     ((TIME) / (1000000000 / HZ))

#define JIFFIES_TO_NS(TIME)     ((TIME) * (1000000000 / HZ))

两种时间单位 :

系统的时间是以 nanosecond(十亿分之一秒)为单位的,但这一数值粒度过细,大部分核心应用仅能取得它的绝对值,感知不到它的精度。

时间相关的核心应用通常围绕时钟中断进行,在 Linux 2.6 中,系统时钟每 1 毫秒中断一次(时钟频率,用 HZ 宏表示,定义为 1000,即每秒中断 1000 次,--2.4 中定义为100,很多应用程序也仍然沿用 100 的时钟频率),这个时间单位称为一个 jiffie。很多核心应用都是以 jiffies 作为时间单位,例如进程的运行时间片。 

jiffies 与绝对时间之间的转换公式如下: nanosecond=jiffies*1000000 

 核心用两个宏来完成两种时间单位的互换:JIFFIES_TO_NS()、NS_TO_JIFFIES(),很多时间宏也有两种形式,例如 NS_MAX_SLEEP_AVG 和 MAX_SLEEP_AVG。

2.2.5 activated

activated表示进程因什么原因进入就绪态,这一原因会影响到调度优先级的计算。activated 有四个值:

  • -1,进程从 TASK_UNINTERRUPTIBLE 状态被唤醒;
  • 0,缺省值,进程原本就处于就绪态;
  • 1,进程从 TASK_INTERRUPTIBLE 状态被唤醒,且不在中断上下文中;
  • 2,进程从 TASK_INTERRUPTIBLE 状态被唤醒,且在中断上下文中。 
    activated 初值为 0,在两个地方修改,一是在 schedule() 中,被恢复为 0,另一个就是 activate_task(),这个函数由 try_to_wake_up() 函数调用,用于激活休眠进程:
  • 如果是中断服务程序调用的 activate_task(),也就是说进程由中断激活,则该进程最有可能是交互式的,因此,置 activated=2;否则置activated=1。
  • 如果进程是从 TASK_UNINTERRUPTIBLE 状态中被唤醒的,则 activated=-1(在try_to_wake_up()函数中)。 

2.2.6sleep_avg

进 程的平均等待时间(以 nanosecond 为单位),在 0 到 NS_MAX_SLEEP_AVG 之间取值,初值为 0,相当于进程等待时间与运行时间的差值。sleep_avg 所代表的含义比较丰富,既可用于评价该进程的"交互程度",又可用于表示该进程需要运行的紧迫性。这个值是动态优先级计算的关键因子,sleep_avg 越大,计算出来的进程优先级也越高(数值越小),后面将会介绍具体函数。

2.2.7interactive_credit

这个变量记录了本进程的"交互程度",在 -CREDIT_LIMIT 到 CREDIT_LIMIT+1 之间取值。进程被创建出来时,初值为 0,而后根据不同的条件加 1 减 1,一旦超过CREDIT_LIMIT(只可能等于 CREDIT_LIMIT+1),它就不会再降下来,表示进程已经通过了"交互式"测试,被认为是交互式进程了。

2.2.8time_slice

进程的时间片余额,相当于 2.4 的 counter,但不再直接影响进程的动态优先级。进 程的time_slice 值代表进程的运行时间片剩余大小,在进程创建时与父进程平分时间片,在运行过程中递减,一旦归0,则按 static_prio 值重新赋予上述基准值,并请求调度。时间片的递减和重置在时钟中断中进行(sched_tick()),除此之外,time_slice 值的变化主要在创建进程和进程退出过程中:

a)  进程创建 

和 2.4 类似,为了防止进程通过反复 fork 来偷取时间片,子进程被创建时并不分配自己的时间片,而是与父进程平分父进程的剩余时间片。也就是说,fork 结束后,两者时间片之和与原先父进程的时间片相等。

b) 进程退出 

进程退出时(sched_exit()),根据 first_time_slice 的值判断自己是否从未重新分配过时间片,如果是,则将自己的剩余时间片返还给父进程(保证不超过 MAX_TIMESLICE)。这个动作使进程不会因创建短期子进程而受到惩罚(与不至于因创建子进程而受到"奖励"相对应)。如果进程已经用完了从父进 程那分得的时间片,就没有必要返还了(这一点在 2.4 中没有考虑)。

在 2.4 中,进程剩余时间片是除 nice 值以外对动态优先级影响最大的因素,并且休眠次数多的进程,它的时间片会不断叠加,从而算出的优先级也更大,调度器正是用这种方式来体现对交互式进程的优先策略。但实际上休眠次数多并不表示该进程就是交互式的,只能说明它是 IO 密集型的,因此,这种方法精度很低,有时因为误将频繁访问磁盘的数据库应用当作交互式进程,反而造成真正的用户终端响应迟缓。

2.6的调度器以时间片是否耗尽为标准将就绪进程分成active、expired 两大类,分别对应不同的就绪队列,前者相对于后者拥有绝对的调度优先权--仅当active 进程时间片都耗尽,expired进程才有机会运行。但在 active 中挑选进程时,调度器不再将进程剩余时间片作为影响调度优先级的一个因素,并且为了满足内核可剥夺的要求,时间片太长的非实时交互式进程还会被人为地分成好几段(每一段称为一个运行粒度,定义见下)运行,每一段运行结束后,它都从 cpu 上被剥夺下来,放置到对应的 active 就绪队列的末尾,为其他具有同等优先级的进程提供运行的机会。

这一操作在 schedule_tick() 对时间片递减之后进行。此时,即使进程的时间片没耗完,只要该进程同时满足以下四个条件,它就会被强制从 cpu 上剥夺下来,重新入队等候下一次调度:

  • 进程当前在 active 就绪队列中;
  • 该进程是交互式进程(TASK_INTERACTIVE()返回真,见"更精确的交互式进程优先",nice 大于 12 时,该宏返回恒假);
  • 该进程已经耗掉的时间片(时间片基准值减去剩余时间片)正好是运行粒度的整数倍;
  • 剩余时间片不小于运行粒度

运行粒度的定义 运行粒度 TIMESLICE_GRANULARITY 被定义为与进程的 sleep_avg 和系统总 CPU 数相关的宏。因为 sleep_avg 实际上代表着进程的非运行时间与运行时间的差值,与交互程度判断关系密切,所以,运行粒度的定义说明了内核的以下两个调度策略:

  • 进程交互程度越高,运行粒度越小,这是交互式进程的运行特点所允许的;与之对应,CPU-bound 的进程为了避免 Cache 刷新,不应该分片;
  • 系统 CPU 数越多,运行粒度越大。

2.2.9 新的数据结构 runqueue

2.4 的就绪队列是一个简单的以 runqueue_head 为头的双向链表,在 2.6 中,就绪队列定义为一个复杂得多的数据结构 struct runqueue,并且,尤为关键的是,每一个 CPU 都将维护一个自己的就绪队列,这将大大减小竞争(这在前面已经介绍,同时也是调度器的最主要的部分,放在最后详细介绍)。

O(1)算法中很多关键技术都与 runqueue 有关

1) prio_array_t*active, *expired, arrays[2]

runqueue 中最关键的数据结构。每个 CPU 的就绪队列按时间片是否用完分为两部分,分别通过 active 指针和 expired 指针访问,active 指向时间片没用完、当前可被调度的就绪进程,expired 指向时间片已用完的就绪进程。每一类就绪进程都用一个 structprio_array 的结构表示:

struct prio_array 

{
int nr_active;  /* 本进程组中的进程数 */
struct list_head queue[MAX_PRIO];
/* 以优先级为索引的 HASH 表,见下 */
unsigned long bitmap[BITMAP_SIZE];
/* 加速以上 HASH 表访问的位图,见下 */
};

在 2.4 版的内核里,查找最佳候选就绪进程的过程是在调度器 schedule() 中进行的,每一次调度都要进行一次(在 for 循环中调用 goodness()),这种查找过程与当前就绪进程的个数相关,因此,查找所耗费的时间是 O(n) 级的,n 是当前就绪进程个数。正因为如此,调度动作的执行时间就和当前系统负载相关,无法给定一个上限,这与实时性的要求相违背。

在新的 O(1) 调度中,这一查找过程分解为 n 步,每一步所耗费的时间都是 O(1) 量级的。

prio_array 中包含一个就绪队列数组,数组的索引是进程的优先级(,相同优先级的进程放置在相应数组元素的链表 queue 中。调度时直接给出就绪队列 active 中具有最高优先级的链表中的第一项作为候选进程,而优先级的计算过程则分布到各个进程的执行过程中进行。

为了加速寻找存在就绪进程的链表,2.6 核心又建立了一个位映射数组来对应每一个优先级链表,如果该优先级链表非空,则对应位为 1,否则为 0。核心还要求每个体系结构都构造一个sched_find_first_bit() 函数来执行这一搜索操作,快速定位第一个非空的就绪进程链表。

采用这种将集中计算过程分散进行的算法,保证了调度器运行的时间上限,同时在内存中保留更加丰富的信息的做法也加速了候选进程的定位过程。这一变化简单而又高效,是 2.6 内核中的亮点之一。

arrays 二元数组是两类就绪队列的容器,active 和 expired 分别指向其中一个。active 中的进程一旦用完了自己的时间片,就被转移到 expired 中,并设置好新的初始时间片;而当 active 为空时,则表示当前所有进程的时间片都消耗完了,此时,active 和 expired 进行一次对调,重新开始下一轮的时间片递减过程。

回忆一下 2.4 调度系统,进程时间片的计算是比较耗时的,在早期内核版本中,一旦时间片耗尽,就在时钟中断中重新计算时间片,后来为了提高效率,减小时钟中断的处理时 间,2.4 调度系统在所有就绪进程的时间片都耗完以后在调度器中一次性重算。这又是一个 O(n) 量级的过程。为了保证 O(1) 的调度器执行时间,2.6 的时间片计算在各个进程耗尽时间片时单独进行,而通过以上所述简单的对调来完成时间片的轮转。这又是 2.6 调度系统的一个亮点。

2) spinlock_tlock

runqueue 的自旋锁,当需要对 runqueue 进行操作时,仍然应该锁定,但这个锁定操作只影响一个 CPU 上的就绪队列,因此,竞争发生的概率要小多了。

3) task_t *curr

本 CPU 正在运行的进程。

4) tast_t *idle

指向本 CPU 的 idle 进程,相当于 2.4 中init_tasks[this_cpu()] 的作用。

5) intbest_expired_prio

记录 expired 就绪进程组中的最高优先级(数值最小)。该变量在进程进入 expired 队列的时候保存(schedule_tick()),用途见"expired_timestamp"的解释)。

6) unsigned longexpired_timestamp

当 新一轮的时间片递减开始后,这一变量记录着最早发生的进程耗完时间片事件的时间(jiffies 的绝对值,在 schedule_tick() 中赋),它用来表征 expired 中就绪进程的最长等待时间。它的使用体现在 EXPIRED_STARVING(rq) 宏上。

上 面已经提到,每个 CPU 上维护了两个就绪队列,active 和 expired。一般情况下,时间片结束的进程应该从 active 队列转移到 expired 队列中(schedule_tick()),但如果该进程是交互式进程,调度器就会让其保持在 active 队列上以提高它的响应速度。这种措施不应该让其他就绪进程等待过长时间,也就是说,如果 expired 队列中的进程已经等待了足够长时间了,即使是交互式进程也应该转移到 expired 队列上来,排空 active。这个阀值就体现在EXPIRED_STARVING(rq) 上:在expired_timestamp 和 STARVATION_LIMIT 都不等于 0 的前提下,如果以下两个条件都满足,则EXPIRED_STARVING() 返回真:

  • (当前绝对时间 - expired_timestamp) >= (STARVATION_LIMIT * 队列中所有就绪进程总数 + 1),也就是说 expired 队列中至少有一个进程已经等待了足够长的时间;
  • 正在运行的进程的静态优先级比 expired 队列中最高优先级要低(best_expired_prio,数值要大),此时当然应该尽快排空 active 切换到expired 上来。

7) structmm_struct *prev_mm

保 存进程切换后被调度下来的进程(称之为 prev)的 active_mm 结构指针。因为在 2.6 中 prev 的 active_mm 是在进程切换完成之后释放的(mmdrop()),而此时 prev 的 active_mm 项可能为 NULL,所以有必要在 runqueue 中预先保留。

8) unsigned longnr_running

本 CPU 上的就绪进程数,该数值是 active 和 expired 两个队列中进程数的总和,是说明本 CPU 负载情况的重要参数。

9) unsigned longnr_switches

记录了本 CPU 上自调度器运行以来发生的进程切换的次数。

10) unsignedlong nr_uninterruptible

记录本 CPU 尚处于TASK_UNINTERRUPTIBLE 状态的进程数,和负载信息有关。

11) atomic_tnr_iowait

记录本 CPU 因等待 IO 而处于休眠状态的进程数。

12) unsignedlong timestamp_last_tick

本就绪队列最近一次发生调度事件的时间,在负载平衡的时候会用到。

13) intprev_cpu_load[NR_CPUS]

记录进行负载平衡时各个 CPU 上的负载状态(此时就绪队列中的 nr_running 值),以便分析负载情况。

14) atomic_t*node_nr_running; int prev_node_load[MAX_NUMNODES]

这两个属性仅在 NUMA 体系结构下有效,记录各个 NUMA 节点上的就绪进程数和上一次负载平衡操作时的负载情况。

15) task_t*migration_thread

指向本 CPU 的迁移进程。每个 CPU 都有一个核心线程用于执行进程迁移操作。

16) structlist_head migration_queue

需要进行迁移的进程列表。

(以上各个功能将在具体的调度函数recalc_task_prio中详细说明)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值