进程调度
- 1 策略
- I/O消耗型和处理器消耗型的进程
- 进程优先级
- 时间片
- 进程抢占
- 2 Linux调度算法
- 可执行队列
- 优先级数组
- 重新计算时间片
- schedule()
- 计算优先级和时间片
- 睡眠和唤醒
- 负载平衡程序
- 3 抢占和上下文切换
- 用户抢占
- 内核抢占
- 4 实时
- 5 与调度相关的系统调用
- 与调度策略和优先级相关的系统调用
- 与处理器绑定有关的系统调用
- 放弃处理器时间
- 6 调度程序小结
调度程序是内核的组成部分,它负责选择下一个要运行的进程。多任务操作系统就是能 同时并发地执行 多个进程的操作系统。多任务系统可以分为两类:非抢占式多任务和抢占式多任务。 Linux提供抢占式的多任务模式。在此模式下,由调度程序来决定什么时候 停止一个进程的运行以便其他进程能够得到执行。这个 强制停止动作叫做抢占,注意这是强制停止,不是进程自己停止。进程在被抢占之前能够运行的时间是预先设置好的,而且有一个专门的名字,叫进程的时间片。时间片实际上就是分配给每个 可运行进程的处理器时间段。
在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直运行。进程主动挂起自己的操作称为让步。Unix从一开始就是抢占式的多任务。
1 策略
策略决定调度程序在何时让什么进程运行
I/O消耗型和处理器消耗型的进程
I/O消耗型进程大部分时间用来提交I/O请求或是等待I/O请求。因此,这样的进程经常处于可运行状态,但通常都是运行短短的一会儿,因为I/O操作总会阻塞。
处理器消耗型把时间大多用在执行代码上。对于这类处理器消耗型的进程,调度策略是尽量降低它们的运行频率,对它们而言,延迟其运行时间会更合适些。
I/O消耗型是尽量提高其运行频率,降低运行时间;处理器消耗型是降低其运行频率,提高其运行时间。
Linux为了保证交互时应用,对进程的响应做了优化(缩短响应时间),更倾向于优先调度I/O消耗型进程。
进程优先级
调度算法中最基本的一类就是基于优先级的调度。优先级高的进程先运行,低的后运行,相同优先级的进程按轮转方式进行调度(一个接一个,重复进行)。在包括Linux在内的某些系统中,优先级高的进程使用的时间片也较长。调度程序总是选择时间片未用尽而且优先级最高的进程运行。
Linux根据以上思想实现了一种基于动态优先级的调度方法。一开始,该方法先设置基本的优先级,然后允许调度程序根据需要来加减优先级。
Linux内核提供了两组独立的优先级范围。第一种是nice值,范围是-20到+19,默认值是0。nice值越大优先级越低。另外nice值也用来决定分配给进程的时间片的长短,nice值为-20的进程可能获得的时间片最长,nice值为19的进程获得时间片可能最短。"可能"是因为时间片的长短还受其他因素影响。nice值是所有Unix系统都使用到的标准优先级范围。
第二个是实时优先级,其值是可配置的,默认情况下它的变化范围是从0到99,任何实时进程的优先级都高于普通的进程。Linux提供了对POSIX实时优先级的支持。
时间片
时间片是一个数值,它表明进程在被抢占前还能运行的时间。调度策略必须规定一个默认的时间片。
Linux调度程序提高交互式程序的优先级,让它们运行更频繁。于是,程序提供提供了较长默认时间片给交互程序。如下图
当一个进程的时间片耗尽时,就认为进程到期了。没有时间片的进程不会再投入运行,除非等他其他所有的进程都耗尽了它们的时间片,系统会重算所有进程的时间片,然后继续运行。
进程抢占
Linux系统是抢占式的。当一个进程进入TASK_RUNNING状态,内核会检查它的优先级是否高于当前正在执行的进程。如果高于,调度程序会被唤醒,抢占当前正在运行的进程并运行高优先级的进程。此外,当一个进程的时间片变为0,调度程序被唤醒以选择一个新的进程。
2 Linux调度算法
Linux的调度程序定义于kernel/sched.c中。调度程序的算法和相关支持代码大部分都在2.5版内核的开发版中被重写了。因此,现在的程序与以前版本内核的调度程序区别很大,几乎是全新的。设计新的调度程序是为了实现以下目标:
- 充分实现0(1)调度。不管有多少进程,新调度程序采用的每个算法都能在恒定时间内完成。
- 全面实现SMP的可拓展性。每个处理器都拥有自己的锁和自己的可执行队列
- 强化SMP的可拓展性。尽量将相关的一组任务分配给一个CPU进行连续的执行。只有在需要平衡任务队列的大小时才在CPU之间移动进程
- 加强交互性能。即使在系统处于相当负载的情况下,也能保证系统的响应,并立即调度交互式进程。
- 保证公平。在合理设定的时间范围内,没有进程会处于饥饿状态。
可执行队列
调度程序中最基本的数据结构是运行队列。可执行队列定义在kernel/sched.c中。由结构runqueue表示。可执行队列是给定处理器上的可执行进程的链表,每个处理器一个。每个可投入运行的进程都唯一的归属于一个可执行队列。此外,可执行队列中还包含每个处理器的调度信息。
/** 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* acquire operations must be ordered by ascending &runqueue.*/
struct runqueue {spinlock_t lock; /* 保护运行队列的自旋锁 *//** nr_running and cpu_load should be in the same cacheline because* remote CPUs use both these fields when doing load calculation.*/unsigned long nr_running; /* 可运行任务的数目 */
#ifdef CONFIG_SMPunsigned long cpu_load;
#endifunsigned long long nr_switches; /* 上下文切换数目 *//** This is part of a global counter where only the total sum* over all CPUs matters. A task can increase this counter on* one CPU and if it got migrated afterwards it may decrease* it on another CPU. Always updated under the runqueue lock:*/unsigned long nr_uninterruptible; /* 处于不可中断睡眠状态的任务数目 */unsigned long expired_timestamp; /* 队列最后被换出时间 */unsigned long long timestamp_last_tick; /* 最后一个调度程序的节拍 */task_t *curr, *idle; /* 当前运行任务、该处理器的空任务 */struct mm_struct *prev_mm; /* 最后运行任务的mm_struct结构体 */prio_array_t *active, *expired, arrays[2];/* 活动优先级队列、超时优先级队列、实际优先级数组 */int best_expired_prio;atomic_t nr_iowait; /* 等待I/O操作的任务数目 */#ifdef CONFIG_SMPstruct sched_domain *sd;/* For active balancing */int active_balance;int push_cpu;task_t *migration_thread; /* 移出线程 */struct list_head migration_queue; /* 移出队列 */
#endif#ifdef CONFIG_SCHEDSTATS/* latency stats */struct sched_info rq_sched_info;/* sys_sched_yield() stats */unsigned long yld_exp_empty;unsigned long yld_act_empty;unsigned long yld_both_empty;unsigned long yld_cnt;/* schedule() stats */unsigned long sched_noswitch;unsigned long sched_switch;unsigned long sched_cnt;unsigned long sched_goidle;/* pull_task() stats */unsigned long pt_gained[MAX_IDLE_TYPES];unsigned long pt_lost[MAX_IDLE_TYPES];/* active_load_balance() stats */unsigned long alb_cnt;unsigned long alb_lost;unsigned long alb_gained;unsigned long alb_failed;/* try_to_wake_up() stats */unsigned long ttwu_cnt;unsigned long ttwu_attempts;unsigned long ttwu_moved;/* wake_up_new_task() stats */unsigned long wunt_cnt;unsigned long wunt_moved;/* sched_migrate_task() stats */unsigned long smt_cnt;/* sched_balance_exec() stats */unsigned long sbe_cnt;
#endif
};
由于可执行队列是调度程序的核心数据结构体,所以有一组宏定义用于获取与给定处理器或进程相关的可执行队列:
- cup_rq(process):用于返回给定处理器上的可执行队列的指针
- this_rq():返回当前处理器的可执行队列
- task_rq(task):返回给定进程所在的队列指针
在对可执行队列操作之前,应该先锁住可执行队列,用来防止队列被其他代码改动。此时需要用到task_rq_lock()和task_re_unlock()函数:
struct runqueue *rq;
unsigned long flags;rq = task_rq_lock(task,&flags); /* task是用到可执行队列的任务 */.../* 对任务的队列rq进行操作 */...
task_rq_unlock(rq,&flags);
或者可以用this_rq_lock()来锁住当前的可执行队列,用rq_unlock(struct runqueue *rq)释放给定队列上的锁。
为了避免死锁,要锁住多个运行队列的代码必须按照同样的顺序获取这些锁:可按照执行队列地址从低到高的顺序
/* 锁定 */
if (rq1 == rq2)spin_lock(&rq1->lock)
else {if(re1 <rq2){spin_lock(&rq1->lock);spin_lock(&rq2->lock);}else{spin_lock(&rq2->lock);spin_lock(&rq1->lock);}
}/* 操作两个运行队列 *//* 释放锁 */
spin_unlock(&rq1->lock);
if(rq1 != rq2)spin_unlock(&rq2=>lock);
这些步骤能通过double_rq_lock()和double_rq_unlock()自动完成。上面的步骤会被简化为:
double_rq_lock(rq1,rq2);/* 操作两个运行队列 */double_rq_unlock(rq1,rq2);
优先级数组
每个运行队列都有两个优先级数组,一个活跃的和一个过期的。优先级数组在kernel/sched.c文件中被定义,它是prio_array类型的结构体。
struct prio_array {unsigned int nr_active; /* 任务数目 */unsigned long bitmap[BITMAP_SIZE]; /* 优先级位图 */struct list_head queue[MAX_PRIO]; /* 优先级队列 */
};
优先级数组是一种能够提供0(1)级算法复杂度的数据结构。优先级数组使可运行处理器的每一种优先级都包含一个相应的队列,而这些队列包含对应优先级上的可执行进程链表。优先级还拥有一个优先机位图,当需要查找当前系统内拥有最高优先级的可执行进程时,它可以提高效率。
MAX_PRIO定义了系统拥有的优先级个数。默认是140,BITMAP_SIZE是优先级位图数组的大小,类型为unsigned long长整数型,32位,每一位代表一个优先级,140个优先级需要5个长整型,bitmap就正好含有五个数组项,总共160位。一开始,所有的位都被置为0,当某个拥有一定优先级的进程开始执行时,位图中相应的位就会被置为1.比如,如果一个优先级为7的进程变为可执行状态,第7位就被置为1。这样,查找系统中最高的优先级就变成了查找位图中被设置的第一个位。Linux提供了sched_find_first_bit()函数,来快速查找位图被设置的第一个位。
每个优先级还包含一个加做struct list_head的队列,每个链表与一个给定的优先级相对应,每个链表都包含该处理器队列上相应优先级的全部可运行进程。所以要找到下一个要运行的任务非常简单,就像在链表中选择下一个元素一样,对于给定的优先级,按轮转方式调度任务。
nr_active是计数器,表示该优先级数组内可执行进程的数目。
重新计算时间片
新的Linux为每个处理器维护两个优先级数组:活动数组和过期数组。活动数组内的可执行队列上的进程都还有时间片剩余,而过期数组内的可执行队列上的进程都用完了时间片。当一个进程的时间片用完时,它会从活动数组移至到过期数组,在移动前,会重新计算时间片。
schedule()
选定下一个进程并让该进程执行是通过schedule()函数实现的。当内核代码想要休眠时,会直接调用该函数,另外,如果有那个进程将被抢占,那么该函数也会被唤起执行。
首先,该函数在活动优先级数组中找到第一个被设置的位。该位对应着优先级最高的可执行进程。然后,调度程序选择这个级别链表里的头一个进程。这就是系统中优先级最高的可执行程序,也是马上会被调度执行的进程。
计算优先级和时间片
进程拥有一个初始的优先级,叫做nice值。该数值变化范围是-20到+19,默认值是0。进程task_struct的static_prio变量存放着这个值。而调度程序要用到的动态优先级存放在prio变量里。动态优先级通过一个关于静态优先级和进程交互性的函数关系计算而来。
effective_prio()函数可以返回一个进程的动态优先级。这个函数以nice值为基数,再加上-5到+5之间的进程交互性的奖励或罚分。交互性很强的进程可以认为是I/O消耗型,加上的值为负数,优先级会变高,很弱可以认为是处理器消耗型,加上的值为正数,优先级会变高。调度程序根据进程休眠的时间长短来推断进程是I/O消耗型还是处理器消耗型。
为了支持这种推断机制,Linux用一个变量记录一个进程用于休眠和用于执行的时间。该变量是task_struct的sleep_avg,它的范围是0到MAX_SLEEP_AVG,默认值是10毫秒。当一个进程从休眠状态恢复到执行状态时,sleep_arg会根据它休眠时间的长短而增加,直到达到MAX_SLEEP_AVG为至。相反,进程每运行一个时钟节拍,sleep_avg就做相应的递减,到0为止。
sleep_avg的计算不仅仅根据休眠时间的长短,而且运行时间的长短也要被计算进去。如果一个进程开始占用大量的处理器时间,那么,它很快就会失去增加得到的优先级提升。由于奖励和罚分都加在作为基数的nice值上,所以用户还是可以通过改变进程的nice值来对调度程序施加影响。
在一个进程创建的时候,新建的子进程和父进程均分父进程剩余的进程时间片。这样分配是为了防止不断创建新进程来获取时间片。当一个任务的时间用完之后,可以用task_timeslice()为给定任务计算一个新的时间。时间片的计算只需要把优先级按比例缩放。进程的优先级越高,它每次执行得到的时间片越长。
调度程序还提供了另外一种机制来支持交换进程:如果一个进程的交互性非常强,那么当它的时间片用完后,它会被再放置到活动数组而不是过期数组中。
一般进程在用完它们的时间片后,都会被移至过期数组,当活动数组没有剩余进程的时候,这两个数组就会被交换,活动数组变成过期数组,过期数组替代活动数组。这种操作提供了时间复杂度为0(1)的时间片重新计算。将 交互式强的进程重新插入到活动数组,该进程不会被立即执行,它会和优先级相同的进程轮流被调度和执行。将交互性非常强的进程插入到活动数组的逻辑过程在scheduler_tick ()中实现,该函数会被中断定时器调用。
睡眠和唤醒
休眠(被阻塞)的进程处于一个特殊的不可执行状态,为了等待一些事件的发生。
休眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPIBLE。它们唯一区别是处于TASK_UNINTERRUPIBLE的进程会忽略信号,而处于TASK_INTERRUPTIBLE状态的进程如果接收到一个信号会被提前唤醒并响应该信号。两种状态的进程位于同一个等待队列上,等待某些事件。
等待队列是由等待某些事件发生的进程组成的简单链表。内核用wake_queue_head_t来代表等待队列。等待队列可以通过DECLARE_WAITQUEUE()静态创建,也可以用init_waitqueue_head()动态创建。进程把自己放入等待队列中并设置成不可执行状态。当与等待队列相关的事件发生的时候,队列上的进程会被唤醒。
进程通过执行下面几个步骤将自己加入到一个等待队列中:
- 调用DECLRE_WAITQUEUE()创建一个等待队列的项
- 调用add_wait_queue()把自己将入到等待队列中。该队列会在进程等待的条件满足时唤醒它。
- 将进程的状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
- 如果状态被置为TASK_INTERRUPTIBLE,则信号满足会唤醒进程。
- 检查条件是否为真,如果为真的话,就没必要休眠了。如果不为真,调用schedule()
- 当进程被唤醒的时候,它会再次检查条件是否为真,如果是,他就退出循环,如果不是,它再次调用schedule()并一直重复这步操作。
- 当条件满足后,进程将自己设置为TASK_RUNNING并调用remove_wait_queue()把自己移出等待队列。
唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用函数try_to_wake_up(),该函数负责将进程设置为TASK_RUNNING状态,调用activate_task()将此进程放入可执行队列,如果被唤醒的进程优先级比当前正在执行的进程的优先级高,还要设置need_resched标志。
关于休眠有一点要注意,存在虚假的唤醒。有时候进程被唤醒并不是因为它所等待的条件达成了,所以才需要一个循环处理来保证它等待的条件真正达成。
负载平衡程序
Linux的调度程序为对称多处理系统的每个处理器准备了单独的可执行队列和锁。每个处理器都有一个自己的进程链表,而它只对属于自己的这些进程进行调度操作。当可执行队列间出现负载不均衡的情况时,比如一个处理器的队伍上有五个进程,而另外一个处理器的队列上只有一个进程,这时候就需负载平衡程序解决,它负责保证可执行队列之间的负载处于均衡状态。负责平衡程序会拿当前处理器的可执行队列和系统中的其他可执行队列做比较。如果它发生了不均衡,就会把相对繁忙的队列中的进程抽到当前的可执行队列中来,使它们大致持平。
负载平衡程序由kernel/sched.c中的函数load_balance()来实现。它有两种调用方法。在schedule()执行的时候,只要当前的可执行队列为空,它就会被调用。此外,它还会被定时器调用:系统空闲时每隔一毫秒调用一次或者在其他情况下每隔200毫秒调用一次。在单处理器系统中,load_balance不会被调用,它甚至不会被编译进内核。因为那里面只有一个可执行队列,因此根本没有平衡负载的必要。
负载平衡程序调用时需要锁住当前处理器的可执行队列并且屏蔽中断,以避免可执行队列被并发地访问
load_balance()操作步骤:
/** Check this_cpu to ensure it is balanced within domain. Attempt to move* tasks if there is an imbalance.** Called with this_rq unlocked.*/
static int load_balance(int this_cpu, runqueue_t *this_rq,struct sched_domain *sd, enum idle_type idle)
{struct sched_group *group;runqueue_t *busiest;unsigned long imbalance;int nr_moved;spin_lock(&this_rq->lock);schedstat_inc(sd, lb_cnt[idle]);group = find_busiest_group(sd, this_cpu, &imbalance, idle);if (!group) {schedstat_inc(sd, lb_nobusyg[idle]);goto out_balanced;}busiest = find_busiest_queue(group);if (!busiest) {schedstat_inc(sd, lb_nobusyq[idle]);goto out_balanced;}/** This should be "impossible", but since load* balancing is inherently racy and statistical,* it could happen in theory.*/if (unlikely(busiest == this_rq)) {WARN_ON(1);goto out_balanced;}schedstat_add(sd, lb_imbalance[idle], imbalance);nr_moved = 0;if (busiest->nr_running > 1) {/** Attempt to move tasks. If find_busiest_group has found* an imbalance but busiest->nr_running <= 1, the group is* still unbalanced. nr_moved simply stays zero, so it is* correctly treated as an imbalance.*/double_lock_balance(this_rq, busiest);nr_moved = move_tasks(this_rq, this_cpu, busiest,imbalance, sd, idle);spin_unlock(&busiest->lock);}spin_unlock(&this_rq->lock);if (!nr_moved) {schedstat_inc(sd, lb_failed[idle]);sd->nr_balance_failed++;if (unlikely(sd->nr_balance_failed > sd->cache_nice_tries+2)) {int wake = 0;spin_lock(&busiest->lock);if (!busiest->active_balance) {busiest->active_balance = 1;busiest->push_cpu = this_cpu;wake = 1;}spin_unlock(&busiest->lock);if (wake)wake_up_process(busiest->migration_thread);/** We've kicked active balancing, reset the failure* counter.*/sd->nr_balance_failed = sd->cache_nice_tries;}/** We were unbalanced, but unsuccessful in move_tasks(),* so bump the balance_interval to lessen the lock contention.*/if (sd->balance_interval < sd->max_interval)sd->balance_interval++;} else {sd->nr_balance_failed = 0;/* We were unbalanced, so reset the balancing interval */sd->balance_interval = sd->min_interval;}return nr_moved;out_balanced:spin_unlock(&this_rq->lock);/* tune up the balancing interval */if (sd->balance_interval < sd->max_interval)sd->balance_interval *= 2;return 0;
}
- 首先,load_balance调用find_busiest_queue(),找到最繁忙的可执行队列。如果没有那个可执行队列中进程的数目比当前队列中的数目多个25%或25%以上,find_busiest_queue()返回NULL,并且load_balance()也返回。否则,最繁忙的可执行队列就被返回。
- 其次,load_balance()从最繁忙的运行队列中选择一个优先级数组以便抽取进程,
- 接着,load_balance()寻找到含有进程并且优先级最高的链表,因为把优先级高的进程平均分散开来才是最重要的。
- 分析找到的所有这些优先级相同的进程,选择一个不是正在执行,也不会因为处理器相关性不可移动,并且不在高速缓存中的进程,如果有进程满足这些条件,调用pull_task()将其从最繁忙的队列中抽取到当前队列。
- 只要可执行队列之间仍然不均衡,就重复上面步骤,继续冲繁忙的队列中抽取进程到当前队列。这终将消除不平衡,此时,解除对当前运行队列的锁定,从load_balance返回。
3 抢占和上下文切换
上下文切换,也就是从一个可执行进程切换到另一个可执行进程,由定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。它完成了两项基本的工作:
- 调用定义在include/asm-具体架构/mmu_context.h中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中
- 调用定义在include/asm-具体架构/system.h中的switch_to(),该函数负责从上一个进程的处理器切换到新进程的处理器状态。
内核必须知道什么时候调用schedule()。内核提供了一个need_resched标志来表明是否需要重新执行一次调度。
每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中)。在2.2以前的内核版本中,该标志是一个全局变量。2.2到2.4版本内核中它在task_struct中。而在2.6中,它被移到thread_info结构体中,用一个特别的标志变量中的一位来表示。
用户抢占
用户抢占就是一个运行在用户空间的进程被另一个进程抢占执行。
内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间时,它知道自己是安全的,因为它可以去继续去执行当前进程,那么它当然可以选择一个新的进程去执行,所以,内核无论是在从中断处理程序还是在系统调用后返回,都会检查need_resched标志。
简而言之,用户抢占在以下情况时发生:
- 从系统调用返回用户空间
- 从中断处理程序返回用户空间
内核抢占
内核抢占就是指一个在内核态运行的进程, 可能在执行内核函数期间被另一个进程取代。
在2.6版本的内核中,内核引入了抢占能力。现在,只要重新调度是安全的,那么内核就可以在任何时间抢占正在执行的任务。
什么时候重新调度才是安全的呢?只要没有持有锁,内核就可以进行抢占。为了支持内核抢占,每个进程的thread_info结构引入了preempt_count计数器。该计数器初始值为0,每当使用锁的时候数值加1,释放锁的时候数值减一。当数值为0的时候,内核就可执行抢占。从中断返回内核空间的时候,内核会检查need_resched和preempt_count的值。如果need_resched被设置,并且preempt_count为0的话,这说明有一个更为重要的任务需要执行并且可以安全抢占,此时,调度程序就会被调用。
如果内核中的进程被阻塞了,或它显示地调用了schedule(),内核抢占也会显式的发生。这种形式的内核抢占从来都是受支持的,因为无需保证内核可以安全地被抢占。
内核抢占发生在:
- 当从中断处理程序正在执行且返回内核空间之前
- 当内核代码再一次具有可抢占性的时候
- 如果内核中的任务显示的调用schedule()
- 如果内核中的任务阻塞
4 实时
Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。而普通的,非实时的调度策略是SCHED_NORMAL。SCHED_FIFO实现了一种简单的、先进先出的调度算法,它不使用时间片。SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。 一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己受阻塞或显式释放处理器或者由被其他进程抢占,只有较高优先级的SCHED_FIFO或SCHED_RR任务才能抢占SCHED_FIFO任务。
SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给他的时间后就不能接着执行了。也就说,SCHED_RR是有时间片的。当SCHED_RR任务耗尽它的时间片,在同一优先级的其他实时进程被轮流调度。
这两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能确保给定优先级别的实时进程总能抢占优先级比它低的进程。
实时优先级范围是从0到MAX_RT_PRIO减1.默认情况下,MAX_RT_PRIO为100。SCHED_NORMAL级进程的nice值共享了这个取值空间,它的取值范围是从MAX_RT_PRIO到(MAX_RT_PRIO+40)。也就说,在默认情况下,nice值从-20到+19直接对应的是从100到139的实时优先级范围。
5 与调度相关的系统调用
Linux提供了一族系统调用,用于管理与调度程序相关的参数。这些系统调用可以用来操作和处理进程优先级、调度策略及处理器,同时还提供了显式的将处理器交给其他进程的机制。我们在man帮助文档上可以查找
与调度策略和优先级相关的系统调用
sched_setscheduler()和sched_getscheduler()分别用于设置和获取进程的调度策略和实时优先级,它们的作用是读取或改写进程描述符task_struct的polucy和rt_priority的值。
sched_setparam()和sched_getparam()分别用于设置和获取进程的实时优先级。这两个系统调用获取封装在sched_param特殊结构体中的rt_priority。sched_get_prioruty_max()和sched_get_priority_min()分别用于返回给定调度策略的最大和最小优先级。
对于一个普通的进程,nice()函数可以将给定进程的静态优先级增加一个给定的量。只有超级用户才能调用它时使用负值,从而提高进程的优先级。nice()函数会调用内核的set_user_nice()函数,这个函数会设置进程的task_struct的static_prio和prio值。
与处理器绑定有关的系统调用
Linux调度程序提供强制的处理器绑定机制。也就是说,允许用户强制指定“这个进程无论如何都必须在这些处理器上运行”。这种强制的亲和性保存在进程task_struct的cpus_allowed这个位掩码标志中。该掩码标志的每一位对应一个系统可用的处理器。默认情况下,所有的位都被设置,进程可以在系统中所有可用的处理器上执行。用户可以用sched_setaffinity函数设置一个不同一个或几个位组合的位掩码。而调用sched_getaffinity函数则返回当前的cpus_allowed位掩码。
子进程继承父进程进程描述符的cpus_allowed。如果父进程运行在指定的处理器上,子进程也会运行在该处理器上。
放弃处理器时间
Linux通过sched_yield函数系统调用,来将处理器让给其他等待执行的进程。它是通过将进程从活动队列移到过期队列实现的。由于实时进程不会过期,所以属于例外,实时进程只被移动到其优先级队列的最后面(不会放到过期队列中)。
内核代码为了方便,可以直接调用yield(),它先要确定给定进程确实处于可执行状态,然后再调用sched_yield函数。用户空间的应用程序直接使用sched_yield()系统调用就可以了。
6 调度程序小结
本章介绍了进程调度所遵循的基本原理、具体实现、调度算法以及目前Linux内核使用的借口。