Linux内核(2.6)进程调度算法

1.1      进程状态

在Sched.h(include\linux)中定义了进程的状态。

/*

*Task state bitmask. NOTE! These bits are also

*encoded in fs/proc/array.c: get_task_state().

*

* Wehave two separate sets of flags: task->state

* isabout runnability, while task->exit_state are

*about the task exiting. Confusing, but this way

*modifying one set can‘t modify the other one by

*mistake.

*/

#define TASK_RUNNING           0

#define TASK_INTERRUPTIBLE       1

#define TASK_UNINTERRUPTIBLE  2

#define __TASK_STOPPED        4

#define __TASK_TRACED          8

/* in tsk->exit_state */

#define EXIT_ZOMBIE        16

#define EXIT_DEAD           32

/* in tsk->state again */

#define TASK_DEAD          64

#define TASK_WAKEKILL         128

#define TASK_WAKING            256

其实我们只要关心这几个就行了

#define TASK_RUNNING           0

#define TASK_INTERRUPTIBLE       1

#define TASK_UNINTERRUPTIBLE  2

1.1.1       TASK_RUNNING

运行态:或者是当前正在运行,或者是在一个等待运行的队列里。运行态的进程可以分为3种情况:内核运行态、用户运行态、就绪态。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, intel cpu提供Ring0-Ring3三种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。Ring3状态不能访问Ring0的地址空间,包括代码和数据。Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能
随意操作内核地址空间,具有一定的安全保护作用。

用户态切换到内核态的3种方式

A)    系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

B) 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

C) 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

1.1.2       TASK_INTERRUPTIBLE

可中断的睡眠态。进程被阻塞,它在等待某些条件来执行。

1.1.3       TASK_UNINTERRUPTIBLE

不可中断的睡眠态。这个状态只能被wake_up()唤醒。

1.1.4       TASK_STOPPED

停止态:这种情况发生在进程收到SIGSTOP, SIGTSTP, SIGTTIN或SIGTTOU信号,或者在它被调试的时候收到任何信号的时候。可向其发送SIGCONT信号让进程转换到可运行状态。

1.2      进程调度算法

1.2.1       抢占型多任务

调度器背后的思想是为了最好的利用处理器资源。假设系统中可运行的进程的数量大于系统处理器的数量,那么必须决定哪个进程需要优先运行,这其实是选出最优先的k个进程的问题,k就是处理器的数目。而调度器就是解决这个问题的。

世界上的多任务操作系统分为两种风格:合作型多任务和抢占型多任务。Linux,像所有Unix和大多数现代操作系统一样,提供抢占型多任务。调度器一旦决定运行一个进程,它就会毫无情面的挂起一个当前正在运行的进程。这样的动作称为抢占。一个进程在抢占之前运行的时间是预设好的,叫做时间片。管理时间片是调度算法的一部分。

而在合作型多任务中,任务切换只能依靠一个任务去主动放弃CPU。这种主动放弃叫做yield----就像pthread中的一样。目前的系统很少有采用这种调度策略的。

从kernel 2.5开始,调度器的算法做了很大的调整,时间复杂度降到了O(1)。下面我们来看它的设计和实现

1.2.2       时间片

时间片是决定了一个任务在没有被抢占的理想情况下可以运行多久。时间片的长度很难确定:时间片太长会导致系统交互性能不好,时间片太短会导致进程切换频繁,浪费系统资源。而且还需要考虑进程的目的,比如偏于IO交互的进程(比如响应触摸屏操作的进程)和偏于处理器运算的进程(比如视频解码进程)设计目的是不一样的,UNIX系统认为,偏于IO的进程需要快速响应,这种策略也影响了时间片的分配策略。

另外调度系统需要满足两个自相矛盾的目标:最小化的进程响应时间,最大化的系统工作效率。前者倾向于IO消耗型进程,后者倾向于CPU消耗型进程。

进程调度算法是基于优先级的,而优先级基于进程对处理器时间的需求和他们自己的价值。高优先级的进程会优先得到调度,而相同优先级的进程会排队循环的得到调度。优先级和时间片的关系也是比较微妙,有的系统是正相关的,比如Linux。也就是说,优先级越高,时间片越大。

1.2.3       优先级

Linux的进程分普通进程和实时进程,而实时进程又分SCHED_FIFO与SCHED_RR,它们只有静态优先级,范围从0到99,而普通进程的优先级是从100到139,所以实时进程比普通进程的优先级高。

#define SCHED_NORMAL    0      //非实时进程,基于优先级的轮回法(Round Robin)

#define SCHED_FIFO      1      //实时进程,先进先出

#define SCHED_RR        2      //实时进程,基于优先级的轮回法(Round Robin)

对相同优先级的任务,SCHED_RR是分配给每个任务一个特定的时间片,然后轮转依次执行;而SCHED_FIFO则是让一个任务执行完再调度下一个任务,而顺序就是按照创建的先后。当一个FIFO进程变得可以运行时,它会持续的跑直到阻塞或自己放弃了CPU,只有高优先级的实时进程可以抢占它。

SCHED_RR和SCHED_FIFO唯一不同的地方是拥有了时间片。当时间片用完时,不管这个线程优先级有多高,都不会在运行,而是进入就绪队列,等待下一个时间片到来。可以这么说,因为有了时间片,RR进程多了一些可以被中断的机会。

SCHED_NORMAL就是普通进程,它不仅有静态优先级,还有动态优先级。

在定义进程的时候就定义了它的静态优先级,这是用nice值换算出来的。nice值从-20到19,它决定了优先级和时间片,19是最低的而-20最高。所以我们也可以认为nice值就是静态优先级。普通进程的静态优先级范围从100(最高优先级)到139(最低优先级)。

NICE值和静态优先级的关系:

/*

* Convertuser-nice values [ -20 ... 0 ... 19 ]

* tostatic priority [ MAX_RT_PRIO..MAX_PRIO-1 ],

* andback.

*/

#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)

#define PRIO_TO_NICE(prio)  ((prio) - MAX_RT_PRIO - 20)

#define TASK_NICE(p)            PRIO_TO_NICE((p)->static_prio)

动态优先级是由静态优先级和“bonus”一起算出来的,下面这个宏计算某进程的bonus:

#define CURRENT_BONUS(p) \

(NS_TO_JIFFIES((p)->sleep_avg)* MAX_BONUS / \

MAX_SLEEP_AVG)

Bonus是用sleep_avg来算出来的,它随着进程的睡眠而增长,随着进程的运行而减少,可以认为Bonus是平均睡眠时间的一种表达形式,IO倾向的进程会得到调度程序的奖励, 即Bonus为正,CPU倾向的进程会得到调度程序的处罚,即Bonus为负。

而计算动态优先级的函数是

static int effective_prio(task_t *p)

{

intbonus, prio;

if(rt_task(p))

return p->prio;

bonus= CURRENT_BONUS(p) - MAX_BONUS / 2;

prio= p->static_prio - bonus;

if(prio < MAX_RT_PRIO)

prio= MAX_RT_PRIO;

if(prio > MAX_PRIO-1)

prio= MAX_PRIO-1;

returnprio;

}

通常说的优先级指的是动态优先级。

下面的函数用来重新计算时间片,注意是用静态优先级计算的,实时进程的时间片相对普通进程要大一些。所有进程的时间片和优先级成正比,比如-20对应800ms,0对应100ms,19对应5ms,反正无论一个进程优先级多低,它都会有时间片资源的。

#define SCALE_PRIO(x, prio) \

max(x* (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)

static unsigned int task_timeslice(task_t*p)

{

if(p->static_prio < NICE_TO_PRIO(0))

returnSCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);

else

returnSCALE_PRIO(DEF_TIMESLICE, p->static_prio);

}

1.2.4       进程调度

/*

*This is the main, per-CPU runqueue data structure.

*

*Locking rule: those places that want to lock multiple runqueues

*(such as the load balancing or the thread migration code), lock

* acquireoperations must be ordered by ascending &runqueue.

*/

struct runqueue {

spinlock_tlock;

/*

* nr_running and cpu_load should be in thesame cacheline because

* remote CPUs use both these fields when doingload calculation.

*/

unsignedlong nr_running;

#ifdef CONFIG_SMP

unsignedlong cpu_load;

#endif

unsignedlong long nr_switches;

/*

* This is part of a global counter where onlythe total sum

* over all CPUs matters. A task can increasethis counter on

* one CPU and if it got migrated afterwards itmay decrease

* it on another CPU. Always updated under therunqueue lock:

*/

unsignedlong nr_uninterruptible;

unsignedlong expired_timestamp;

unsignedlong long timestamp_last_tick;

task_t*curr, *idle;

structmm_struct *prev_mm;

prio_array_t*active, *expired, arrays[2];

intbest_expired_prio;

atomic_tnr_iowait;

#ifdef CONFIG_SMP

structsched_domain *sd;

/*For active balancing */

intactive_balance;

intpush_cpu;

task_t*migration_thread;

structlist_head migration_queue;

#endif

};

注意里面定义的prio_array_t *active, *expired, arrays[2]; 它定义了活跃的和过期的两个进程数组,分别对应处于TASK_RUNNING状态的有时间片的进程和消耗尽时间片的进程

typedef struct prio_array prio_array_t;

struct prio_array {

unsignedint nr_active;

unsignedlong bitmap[BITMAP_SIZE];

structlist_head queue[MAX_PRIO];

};

注意,BITMAP_SIZE是5,也就是5*32=160个bit,MAX_PRIO是140。而queue里包含了每一种优先级进程组成的链表,一共有140个。Sched.c (kernel)中有一个数据结构是runqueues,每个CPU都有一个runqueue,为了避免死锁,试图锁住很多runqueue的代码需要按照相同的顺序去加锁和解锁,比如采用递增的顺序。例如:

/* to lock ... */

if (rq1 < rq2) {

spin_lock(&rq1->lock);

spin_lock(&rq2->lock);

}else {

spin_lock(&rq2->lock);

spin_lock(&rq1->lock);

}

/* manipulate both runqueues ... */

/* to unlock ... */

spin_unlock(&rq1->lock);

spin_unlock(&rq2->lock);

O(1)算法的实现在于对bitmap的操作,初始情况下所有bit为0,当一个进程状态变为TASK_RUNNING时,active数组中的bitmap对应的位被设置为1。因此寻找哪个优先级的进程可运行的问题就转化为了寻找bitmap中第一个置为1的bit的位置的问题。这个问题显然是个O(1)的问题,函数为sched_find_first_bit。找到这个优先级后,再找到这个优先级对应的进程队列,按照“roundrobin”的方式去找到当前需要运行哪个进程。这种方式是个术语,其实就是指优先级相同的进程公平的获得运行的机会。这段代码是:

idx =sched_find_first_bit(array->bitmap);

queue = array->queue + idx;

next = list_entry(queue->next, task_t,run_list);

queue->next是采用迭代器的方式返回的链表的下一个元素。

时间片算法。很多操作系统会在所有能运行的进程时间片都达到0的时候,统一的,一次性的重新计算时间片。而Linux的算法是一旦某进程时间片耗尽,在送到耗尽数组之前,重算它的时间片。这样,耗尽数组中的进程其实都是有一个新的时间片的,这样,当活跃数组中的进程数达到0时,直接交换active和expire指针即可。 这段代码在schedule函数里:

if (unlikely(!array->nr_active)) {

/*

* Switch the active and expired arrays.

*/

schedstat_inc(rq,sched_switch);

rq->active= rq->expired;

rq->expired= array;

……

}

这个交换保证整个调度算法O(1)的重要部分。

总结:Linux的调度策略其实是:优先选择处于运行态且有时间片的进程中优先级最高的那个。

1.2.5       scheduler_tick

函数scheduler_tick()会被时钟中断调到, 它更新当前进程的time_slice,并根据time_slice的使用情况(剩余还是耗尽),来做进一步处理。另外,在fork调用中,当改变父进程的时间片时,也会调到这个函数。

如果是实时进程,先判断是否是RR进程,若是,递减它的时间片。如果时间片已经耗尽,则根据静态优先级重新计算时间片,然后仍然把它塞到活跃数组尾部,如此一来它还有可能被调度到。如果是FIFO进程

如果是普通进程,需要递减时间片,更新它的动态优先级,根据静态优先级重填时间片,然后判断此进程是否是一个交互性质的进程。若是,还加入到活跃数组中。

1.3      抢占(preemption)

这篇文章写得很好:http://blog.csdn.net/sailor_8318/article/details/2870184

Linux为了增加自身的实时性,在2.6版本中支持了内核抢占。当一个进程进入running状态后,内核会检查它的优先级是否大于当前运行的进程的优先级,若是,在一定时间内切换,不管当前进程运行在内核态还是用户态。

上面讲的是策略,那么,下面看看具体在什么时候切换。

内核提供了一个标志位need_resched来表示是否需要切换,这个标志位在如下情况下被设置:

1.  scheduler_tick()检查到一个进程耗尽了它的时间片

2.  try_to_wake_up()唤醒一个进程

一旦回到用户态,或者从中断恢复时,内核会去检查这个标志位,若被设置则调用schedule()。从中断可以恢复到用户态,也可以恢复到内核态。

注意,有几种情况Linux内核不应该被抢占,

1.   内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。

2.   内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。

3.   内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。

4.   内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。

5.   内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占。

为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占锁。这一变量被设置在进程的PCB结构task_struct中。每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_count就减1,同时进行可抢占的判断与调度。

由于在某些情况下无法进行内核抢占,所以我们说Linux是软实时性的,也就是一般保证实时,但极少情况下可能无法做到。

时间: 2024-08-28 14:33:12

Linux内核(2.6)进程调度算法的相关文章

linux内核学习:进程管理

进程状态 TASK_RUNNING 可运行或正在运行 TASK_INTERRUPTIBLE 进程被阻塞,但可以被信号唤醒 TASK_UNINTERRUPTIBLE 进程被阻塞,且不可以被信号唤醒 TASK_STOPPED 进程已停止,且不能再投入运行 TASK_ZOMBIE 所谓的僵死进程,进程描述符仍然保留 关键函数和结构 task_struct thread_info current clone fork exec wait exit linux内核学习:进程管理,布布扣,bubuko.co

【转载】linux内核笔记之进程地址空间

原文:linux内核笔记之进程地址空间 进程的地址空间由允许进程使用的全部线性地址组成,在32位系统中为0~3GB,每个进程看到的线性地址集合是不同的. 内核通过线性区的资源(数据结构)来表示线性地址区间,线性区是由起始线性地址,长度和一些访问权限来描述的.线性区的大小为页框的整数倍,起始地址为4096的整数倍. 下图展示了x86 Linux 进程的地址空间组织结构: 正文段 .text ,这是CPU执行的机器指令部分.通常正文段是共享的,而且是只读的,以防止程序修改其自身的指令. 数据段 .d

Linux内核剖析 之 进程地址空间(二)

//接前一章,本节主要介绍线性区以及相关线性区的操作. 线性区 Linux通过类型为vm_area_struct的对象实现线性区. vm_area_struct: struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_e

linux内核中与进程相关的数据结构(基于linux-mainline-rc4)

1.进程描述符    struct task_struct {  volatile long state; ....... struct list_head tasks; ....... struct mm_struct *mm, *active_mm; ....... struct vm_area_struct *vmacache[VMACACHE_SIZE]; ...... pid_t pid; pid_t tgid; .......   }所在文件:include/linux/sched.

Linux内核剖析 之 进程简介

1.概念 1.1  什么是进程? 进程是程序执行的一个实例,可以看作充分描述程序已经执行到何种程度的数据结构的汇集. 从内核观点看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体. 我们熟悉的fork()库函数,它有两种用法: (1).一个父进程希望复制自己,使父子进程执行不同的代码段,常用于网络服务程序. (2).一个进程要执行一个不同的程序,fork()后立即exec(),如shell. 1.2  什么是线程? 有时候,一个进程希望有多个执行流,如一款麻将游戏,三个由电脑控制的人

Linux内核工程导论——进程

进程 进程调度 概要 linux是个多进程的环境,不但用户空间可以有多个进程,而且内核内部也可以有内核进程.linux内核中线程与进程没有区别,因此叫线程和进程都是一样的.调度器调度的是CPU资源,按照特定的规则分配给特定的进程.然后占有CPU资源的资源去申请或使用硬件或资源.因此这里面涉及到的几个问题: 对于调度器来说: l  调度程序在运行时,如何确定哪一个程序将被调度来使用CPU资源? n  如何不让任何一个进程饥饿? n  如何更快的定位和响应交互式进程? l  单个CPU只有一个流水线

Linux内核创建新进程的过程

作者:xujianguo  原创作品转载请注明出处,<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ——————————————————————————————————————————————————————-———— 实验目的:  使用gdb跟踪分析一个fork系统调用内核处理函数sys_clone ,验证您对Linux系统创建一个新进程的理解; 分析fork函数对应的内核处理过程sys_clone,理解创建

Linux内核的idle进程分析

1. idle是什么 简单的说idle是一个进程,其pid号为 0.其前身是系统创建的第一个进程.也是唯一一个没有通过fork()产生的进程. 在smp系统中,每一个处理器单元有独立的一个执行队列,而每一个执行队列上又有一个idle进程,即有多少处理器单元.就有多少idle进程. 系统的空暇时间,事实上就是指idle进程的"执行时间".既然是idle是进程.那我们来看看idle是怎样被创建,又详细做了哪些事情? 2. idle的创建 我们知道系统是从BIOS加电自检,载入MBR中的引导

linux内核分析之进程fork操作

最近线上遇见了奇怪的现象,redis在做rdb持久化的时候,后台曝出can not allocat memory的错误 具体错误参见redis模块中的错误分析 这里主要说明一下fork(),以及copy-on-write操作 linux内核在2.6以后对fork操作加入copy-on-write,提高系统的可靠性和高性能 首先说明的是fork(),只是一次资源的确认(这里由vm.overcommit参数决定), 网上很多对overcommit参数的说均misunderstanding 简单来讲,

Linux内核调试技术——进程上下文R状态死锁监测

前一篇博文介绍了内核监测D状态死锁的hung task机制,本文介绍另一种死锁状态的监测手段--R状态死锁监测.R状态死锁指的是某一任务一直处于TASK_RUNNING态且一直占用着CPU,从而导致其他进程得不到调度而饿死的情况.一般情况下,R状态死锁较可能是由于程序出现死循环导致的,可以出现在内核态的进程上下文中(内核配置为非抢占式,soft lockup),也可以出现在中断上下文中的中断处理程序中(hard lockup).异常的程序一直运行,CPU无法调度到其他的任务运行,对于单CPU的设