LKD_chapter4_进程调度

     调度程序是内核的组成部分,它负责选择下一个要运行的进程。进程调度程序可看作在可运行态进程之间分配有限处理器时间资源的内核子系统。在一组处于可执行状态的进程中选择一个来执行,是调度程序所需要完成的基本工作。

  多任务系统可以划分为两类:非抢占式多任务和抢占式多任务。

  在抢占式多任务模式下,由调度程序来决定什么时候停止一个进程的运行以便其它进程能够得到执行的机会,这个强制的挂起动作叫做抢占。进程在被抢占之前能够运行的时间是预先设置的,叫做时间片。Linux进程调度采用动态方法计算时间片。

  在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的操作叫让步。

策略

I/O消耗性和处理器消耗型的进程

    I/O消耗型的进程大部分时间用来提交I/O请求或等待I/O请求。常处于可运行状态、运行时间很短,大部分时间在等待I/O时阻塞。

    处理器消耗型进程大部分时间用在代码执行。除非被抢占,否则它们一直不停的运行,从系统响应速度考虑,处理器应该降低它们的运行频率,延长其运行时间更合适。

   调度策略需要在两个矛盾的目标中寻址平衡:进程的响应速度(时间短)和最大系统利用率(高吞吐量)。

   Linux为了保证交互式应用,对进程的响应做了优化,更倾向于优先调度I/O消耗的进程。

进程优先级

    基于进程优先级调度是根据进程的价值和其对处理器时间的需求对进程分级的思想。优先级高的先运行,优先级低的后运行,相同优先级的进程按轮转方式进行调度。调度程序总是选择时间片未用尽且优先级最高的进程运行。用户可以通过设置进程的优先级来影响系统的调度。

    Linux基于以上思想,实现了基于动态优先级调度的方法。一开始设置进程的基本优先级,然后允许调度程序根据需要来加减优先级。

    Linux内核提供了两组独立的优先级范围。第一种nice值,范围从-20到19,默认值是0。nice值越大优先级越低。nice值用来决定分配给进程时间片的长短,nice值为-20可以获得的时间片最长,nice值为19的进程获取的时间片可能最短。

    第二种范围是实时优先级,其值是可以配置的。默认情况下它的变化范围从0到99。任何实时进程的优先级都高于普通的进程。

时间片

         时间片表明进程被抢占前所能持续运行的时间。默认的时间片很短(20ms)。Linux调度程序提交交互式程序的优先级,让它们运行的更频繁。提供长的默认时间给交互进程。同时根据进程的优先级动态调整给它的时间片。

   注意,进程并不一定非要一次用完它所有的时间片。进程可以通过重复调度用完时间片

   当一个进程的时间片耗尽时,认为进程到期。没有时间片的进程不会再投入运行,除非等到其他所有的进程都耗完时间片。

进程抢占

    Linux系统是抢占式的,当一个进程进入TASK_RUNNING状态,内核会检查它的优先级是否高于当前执行的进程,如果是这样,调度程序被唤醒,抢占当前运行的进程并运行新的可运行进程。当一个进程的时间片为0时,它会被抢占。


Linux调度算法

调度算法实现了以下目标

·充分实现O(1)调度,不管有多少进程,新的调度程序采用的每个算法都能在恒定时间内完成。

·全面实现SMP可扩展性。每个处理器有自己的锁和自己的可执行队列。

·强化SMP亲和力。先关一组人物分配给一个cpu进行连续的执行。只有在需要平衡任务队列大小的时候才在cpu之间迁移进程。

·加强交互性能。即使在系统处于相当的负载情况下,也能保证系统的响应。并立即调度交互进程。

·保证公平。在合理设定的时间范围内,没有进程会处于饥饿状态。也没有进程能够失去公平的得到大量时间片。

可执行队列

   调度程序中最基本的数据结构运行队列(runqueue)。可执行队列是给定处理器上的可执行进程的链表,每个处理器一个。每个可投入运行的进程都唯一的归属于一个可执行队列。

struct runqueue {

spinlock_t lock;  /* 需要对 runqueue 进行操作时,仍然应该锁定*/

unsigned long nr_running; /*本 CPU 上的就绪进程数,该数值是 active 和 expired 两个队列中进程数的总和*/

unsigned long long nr_switches;  /*cpu执行进程切换的次数*/

unsigned long nr_uninterruptible; /*本 CPU 尚处于 TASK_UNINTERRUPTIBLE 状态的进程数*/

unsigned long expired_timestamp; /*表征 expired 中就绪进程的最长等待时间*/

unsigned long long timestamp_last_tick; /*本就绪队列最近一次发生调度事件的时间*/

task_t *curr, *idle; /*本 CPU 正在运行的进程,指向本 CPU 的 idle 进程*/

struct mm_struct *prev_mm; /*保存进程切换后被调度下来的进程(称之为 prev)的 active_mm 结构指针*/

prio_array_t *active, *expired, arrays[2];  /*活动运行集合,过期运行集合*/

int best_expired_prio; /*记录 expired 就绪进程组中的最高优先级(数值最小)*/

atomic_t nr_iowait; /*本 CPU 因等待 IO 而处于休眠状态的进程数*/

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

struct list_head migration_queue; ?*需要进行迁移的进程列表*/

}

#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)   给定处理器运行队列的当前进程

想锁住运行队列恰巧有一个特定任务在运行采用下面的函数完成

unsigned long flags;

runqueue_t *rq;

rq =task_rq_lock(task,&flags)

/*对任务队列rq进行操作*/

task_rq_unlock(rq, &flags);

 

锁住当前运行队列采用下面的函数

runqueue_t *rq = this_rq_lock();

/*对任务队列rq进行操作*/

rq_unlock(rq);

为了避免死锁,要锁住多个运行队列的代码必需按照同样的顺序获取这些锁:按照可执行队列地址从低到高的顺序。

double_rq_lock(rq_src, rq_dest);

/*对任务队列rq_src, rq_dest进行操作*/

double_rq_unlock(rq_src, rq_dest);

优先级数组

    每个运行队列都有两个优先级数组,一个活跃的和一个过期的。

struct prio_array {

unsigned int nr_active; /*任务数目*/

unsigned long bitmap[BITMAP_SIZE]; /*任务位图*/

struct list_head queue[MAX_PRIO]; /*优先级队列*/

};

    MAX_PRIO是系统拥有的优先级个数。默认值是140。每个优先级都有一个struct list_head结构体,每个链表都包含该处理器队列上相应优先级的全部可运行进程。为了保证优先级位图的快速查找,系统提供了函数sched_find_first_bit()来查找第一个设置的位。计数器nr_active保存了该优先级数组内可执行进程的数目。

 

重新计算时间片

    Linux为每个处理器维护两个优先级数组:活动数组和过期数组。活动数组内可执行队列上的进程都还有时间片剩余。而过期数组内可执行队列上的进程都耗尽了时间片。当一个进程耗尽时间片时,它会被移到过期数组中,在此动作之前时间片已经重新计算好。现在只需要在活动和过期数组之间来回切换就可以了。实际操作在schedule()函数中如下:

array = rq->active;

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

 /*如果没有活动进程,将过期集合与可运行集合交换*/

rq->active = rq->expired;

rq->expired = array;

}

schedule()函数

    schedule()函数选定下一个进程并切换到它去执行。内核代码需要休眠时,直接调用它。如果有进程被抢占,也会唤醒该函数执行。schedule()函数独立于每个处理器运行。每个cpu对下一次该运行那个进程作出自己的判断。

task_t *prev, *next;

prio_array_t *array;

struct list_head *queue;

prev = current;

array = rq->active;

idx = sched_find_first_bit(array->bitmap); /*检查活动集合的最佳运行队列进程的链表*/

queue = array->queue + idx;  /*选中需要运行优先级进程链表的头部*/

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

    如果prev和current不等,使用函数context_switch()负责从prev切换到next。上面的算法使得系统中的进程数目对代码运行的时间没有影响,所使用的时间恒定。

计算优先级和时间片

    进程拥有一个初始优先级nice值,该数值的变化范围从-20到+19,默认值为0。19为优先级最低,-20最高。进程task_struct的静态优先级static_prio域存放这个值,用户制定后,不能改变。调度程序要用的动态优先级存放在prio域里。动态优先级是通过一个关于静态优先级和进程的交互性函数关系计算出来的。

    effective_prio()函数返回一个进程的动态优先级,这个值以nice值为基数,再加上-5到+5之间的进程交互性的奖励和惩罚。调度程序判断一个进程的交互性强不强,使用在task_struct的sleep_avg域来记录一个进程用于休眠和用于执行时间的相对值。它的范围在0到MAX_SLEEP_AVG,它的默认值为10毫秒。当一个进程从休眠状态恢复到执行状态时,sleep_avg会根据它的休眠时间的长度而增长,直到达到最大值MAX_SLEEP_AVG。进程每运行一个时钟节拍,sleep_avg就做相应的减少,直到0为止。由于奖励和罚分都加在作为基数的nice值上,用户还是可以通过改变进程nice值对调度程序施加影响。

    在一个进程创建的时候,新创建的子进程和父进程均分父进程的时间片。防止用户通过不断创建新进程来不停撺掇时间片。一个任务的时间片用完以后,根据任务的静态优先级重新计算时间片。task_timeslice()返回一个给定任务新的时间片。优先级最高的进程(nice等于-20)能获得最大时间片长度800毫秒(MAX_TIMESLICE),优先级最低的进程(nice等于+19)获得最短时间片5毫秒(MIN_TIMESLICE)或一个时钟滴答。默认优先级(nice等于0)的进程得到的时间片长度为100毫秒。

 

    调度程序还支持另外一种机制支持交互进程:一个进程的交互性非常强,那么它的时间片用完后,它会被再放置到活动数组而不是过期数组中,但是该进程不会立即执行,它会和优先级相同的进程轮流着被调度和执行。对于一般的进程时间片用完直接放入到过期数组中。当活动数组中没有剩余进程的时候,两个数组就会交换。

#define TASK_INTERACTIVE(p) \

((p)->prio <= (p)->static_prio - DELTA(p))  查看进程是不是交互进程

EXPIRED_STARVING(rq) :在 expired_timestamp 和 STARVATION_LIMIT 都不等于 0 的前提下,如果以下两个条件都满足,则 EXPIRED_STARVING() 返回真:

1、(当前绝对时间 - expired_timestamp) >= (STARVATION_LIMIT * 队列中所有就绪进程总数 + 1),也就是说 expired 队列中至少有一个进程已经等待了足够长的时间;

2、正在运行的进程的静态优先级比 expired 队列中最高优先级要低(best_expired_prio,数值要大),此时当然应该尽快排空 active 切换到expired 上来。

#define EXPIRED_STARVING(rq) \

((STARVATION_LIMIT && ((rq)->expired_timestamp && \

(jiffies - (rq)->expired_timestamp >= \

STARVATION_LIMIT * ((rq)->nr_running) + 1))) || \

((rq)->curr->static_prio > (rq)->best_expired_prio))

/*当前进程非交互进程或者在过期队列处于饥饿状态将当前进程插入到过期队列否则插入到活动队列中*/

if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {

enqueue_task(p, rq->expired);

} else

enqueue_task(p, rq->active);

睡眠和唤醒

    休眠(被阻塞)进程处于一个特殊的不可执行状态。进程把自己标记位休眠状态,把自己从可执行队列中移出,放入到等待队列中,然后调用schedule函数选择一个其他可执行进程。唤醒过程相反,进程被设置成可执行状态,然后从等待队列中移动到可执行队列中。

    TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的区别在于处于TASK_UNINTERRUPTIBLE状态的进程会忽略信号,而TASK_INTERRUPTIBLE状态的进程在接收到一个信号会被提前唤醒并相应信号。两种状态的进程位于同一个等待队列上等待某些事件,不能够运行。

DECLARE_WAITQUEUE(wait, tsk);

add_wait_queue(q, &wait);

while (!condition) { /*等待的条件*/

set_current_state(TASK_INTERRUPTIBLE); /*or TASK_UNINTERRUPTIBLE*/

if(signal_pending(current))

/*处理信号*/

schedule();

}

set_current_state(TASK_RUNNING);

remove_wait_queue(&ctx->wait, &wait);

    唤醒操作通过wake_up()进行,它会唤醒指定队列上的所有进程。调用try_to_wake_up()函数。休眠需要注意,存在虚假的唤醒,所以需要一个循环来保证它等待的条件为真。

 

负载平衡程序

   负载平衡程序负责保证可执行队列之间的负载处于均衡状态。负载均衡程序会将当前处理器的可执行队列和系统中的其他可执行队列作比较。发现它不均衡,就会把相相对繁忙的队列中的进程抽到当前的可执行队列中来。理想情况下,每个队列上的进程数目应该相等。

   负载均衡程序由load_balance()来实现。在schedule()执行的时候,只要当前可执行队列为空,它就会被调用。此外会被定时器调用,系统空闲时每隔1毫秒调用一次,或在其他情况下每隔200ms调用一次。

   负载均衡程序调用要锁住当前处理器的可运行队列并且屏蔽中断,以避免可执行队列被并发的访问。schedule()调用load_balance()的时候,当前执行队列为空,只需要找到一个进程插入到这个队列里。在定时器里调用,需要解决所有运行队列间所有的失衡,使它们大致平衡。

 

抢占和上下文切换

    上下文切换:从一个执行进程切换到另一个可执行进程,由函数context_switch函数负责完成。完成下面基本工作:

·调用函数switch_mm()函数,负责将虚拟内存从上一个进程映射切换到新进程中。

·调用函数switch_to()函数,从上一个处理器状态切换到新进程的处理器状态。包括保存和恢复栈信息以及寄存器信息。

    内核提供一个need_resched标志标明是否需要重新执行一次调度。当某个进程的时间片消耗完的时候,schedule_tick就会设置这个标志。当一个高优先级的进程进入可执行状态的时候,try_to_wake_up()也会设置这个标志。

 

    在2.6版内核中,使用thread_info结构体中,一个特殊标志变量中的一位来表示need_resched。

用户抢占

用户抢占发生在以下情况:

·从系统调用返回到用户空间。

·从中断处理程序返回到用户空间。

内核抢占

    Linux内核完全支持内核抢占。2.6版内核中,只要重新调度是安全的,那么内核就可以在任何时候抢占正在执行的进程。

    只要没有持有锁,内核就可以进行抢占。锁是非抢占区的标志。为了支持内核抢占,在thread_info结构中有preempt_count计数器,初始值为0,每当使用锁的时候数值加1,释放锁的时候数值减1。当数值为0时,内核是可抢占的。

    内核中进程被阻塞或显示调用了schedule函数,内核抢占也会显示的发生。

    内核抢占发生在下面情况:

·当中断处理程序正在执行,且返回到内核空间之前。

·当内核代码再一次具有可抢占性的时候。

·当内核任务显示调用schedule

·当内核中的任务阻塞

实时

    Linux提供了两种实时策略:SCHED_FIFO和SCHED_RR。普通的,非实时的调度策略是SCHED_NORMAL。

    SCHED_FIFO是先入先出的调度算法,不使用时间片。SCHED_FIFO级进程比任何SCHED_NORMAL级的进程都要先调度。一旦一个SCHED_FIFO级进程处于可执行状态时,就会一直执行,直到它自己受阻塞或显示的释放处理器为止。只有具有较高优先级的SCHED_FIFO和SCHED_RR任务才能抢占SCHED_FIFO任务。两个或多个SCHED_FIFO进程,它们会轮流执行,只要SCHED_FIFO级进程在执行,其他级别较低的进程只能等待它结束后才能有机会执行。

    SCHED_RR级的进程在耗尽事先分配给它的时间片后就不能再执行,在同一优先级的其他实时进程被轮转调度。时间片只用来重新调度同一优先级的进程。对于SCHED_FIFO进程,高优先级总是立即抢占低优先级,但是低优先级进程决不能抢占SCHED_RR任务,即使它的时间片耗尽。

    两种实时算法实现都是基于静态优先级。内核不为实时进程计算动态优先级。保证给定优先级的进程总是能够抢占比它优先级低的进程。

    实时优先级范围0到MAX_RT_PRIO减1(0~99)。SCHED_NORMAL级进程优先级在MAX_RT_PRIO到MAX_RT_PRIO+40(100~139)。Nice值从-20到+19直接对应从100到139的实时优先级范围。

    Linux实时调度算法是一种软实时工作方式,内核调度进程,尽力使进程在它限定的时间到来前执行,但内核不保证总能满足这些进程的要求。硬实时系统可以保证在一定条件下,满足任何调度的要求。

与调度相关的系统调用

与调度策略和优先级相关的系统调用

int sched_setscheduler(struct task_struct *p, int policy, struct sched_param *param)

int sched_getscheduler(struct task_struct *p, int policy, struct sched_param *param)

    设置和获取进程的调度策略和实时优先级,修改task_struct结构中的policy和rt_priority的值。

asmlinkage long sys_nice(int increment)

    将一个普通进程的进程优先级增加一个给定的量。设置进程task_struct的static_prio和prio的值。

asmlinkage long sys_sched_setparam(pid_t pid, struct sched_param __user *param)

asmlinkage long sys_sched_getparam(pid_t pid, struct sched_param __user *param)

    设置或获取进程的实时优先级

asmlinkage long sys_sched_get_priority_max(int policy)

asmlinkage long sys_sched_get_priority_min(int policy)

    返回给定调度策略的最大和最小优先级。实时调度策略优先级最大值MAX_USER_RT_PRIO减1,最小值等于1。

与处理器绑定相关的系统调用

long sched_getaffinity(pid_t pid, cpumask_t *mask)

long sched_setaffinity(pid_t pid, cpumask_t new_mask)

    Linux提供处理器绑定机制。强制亲和性保存在进程task_struct的cpus_allowed这个位掩码中,该掩码每一位对应一个系统可用的处理器。

    进程创建时,继承父进程的掩码值。当处理器绑定关系发生变化时,内核采用“移植线程”把任务推到合法的处理器上,最后加载平衡器只把任务拉倒允许的处理器上。

放弃处理器

asmlinkage long sys_sched_yield(void)

    Linux提供让进程显示的将处理器时间让给其他等待执行的进程。它通过将进程从活动队列移到过期队列中实现的,并将其放在优先级队列的最后面。由于实时进程不会过期,所以属于例外,它们只会被移到到其优先级队列的最后面(不会放到过期队列中)。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值