前面两篇文章从原理角度分析了进程的调度,本文将从具体的源码出发,分析与进程进程调度密切相关的几个函数。
1.时间片的分配:task_timeslice()
正如我们所知的那样,进程的时间片与进程的静态优先级有直接的关系。从代码中可以看到,根据进程静态优先级static_prio与NICE_TO_PRIO(0)的大小关系,进程时间片的分配可以分为两条路线。以下代码如无特别说明均位于linux/kernel/sched.c下。
static unsigned int task_timeslice(task_t *p) { if (p->static_prio < NICE_TO_PRIO(0)) return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio); else return SCALE_PRIO(DEF_TIMESLICE, p->static_prio); }
NICE_TO_PRIO以及PRIO_TO_NICE宏的作用将进行nice值和进程静态优先级之间的转换。nice也用来表示进程的静态优先级,只不过它与静态优先级的大小范围不同,因此可以将nice看作是static_prio的缩影。
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20) #define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
目前我们已经知道普通进程的静态优先级大小范围是100到139,因此从上面的一些列宏可以得知,nice的取值范围为-20到19。
因此,NICE_TO_PRIO(0)取值为120,也就是说进程时间片分配的两条路线是根据静态优先级120进行划分的。从SCALE_PRIO宏的定义我们可以看到,该宏的作用是取两个数值(具体应该是时间片)的最大者。
#define MAX_PRIO (MAX_RT_PRIO + 40) #define USER_PRIO(p) ((p)-MAX_RT_PRIO) #define MAX_USER_PRIO (USER_PRIO(MAX_PRIO)) #define DEF_TIMESLICE (100 * HZ / 1000) #define MIN_TIMESLICE max(5 * HZ / 1000, 1) # define HZ 1000 //位于linux/include/asm-i386/param.h #define SCALE_PRIO(x, prio) \ max(x * (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)
从上面的宏定义可知,(MAX_USER_PRIO/2)为20。当进程静态优先级小于120时,x的值为DEF_TIMESLICE*4,具体即为400ms;否则x为100ms。因此对于SCALE_PRIO宏可以用下面的公式来表达:
静态优先级<120,基本时间片=max((140-静态优先级)*20, MIN_TIMESLICE)
静态优先级>=120,基本时间片=max((140-静态优先级)*5, MIN_TIMESLICE)
其中MIN_TIMESLICE为系统所规定的最小时间片大小。
2.对可运行队列的操作
在优先级数组结构prio_array中,数组queue用来表示系统中每种优先级进程所形成的可运行队列,而且过期进程和活动进程分别对应这样一个数组。
如果进程仍然处于活动进程队列中,即说明该进程的时间片未用完。当该进程的时间片用完时就需要离开活动进程队列并进入过期进程队列。可运行进程进入进程队列是通过enqueue_task函数完成的,而离开进程队列是通过dequeue_task函数完成的。
每个进程的task_struct结构中都有list_head类型的run_list字段,将进程从可运行队列中删除起始就是对双联表的操作,同时我们需要更新优先级数组结构中活动进程的数目nr_active。如果当前进程优先级所对应的可运行队列已空,那么还要清除优先级位图中该进程优先级所对应的那个位。
如果要进程某个可运行队列,所做的工作基本上跟删除相反。不过该函数首先通过sched_info_queued函数更新该进程最后进入可运行队列的时间戳,并且在最后更新该进程描述符中的array字段,该字段指向当前进程所在的优先级数组。
static void dequeue_task(struct task_struct *p, prio_array_t *array) { array->nr_active--; list_del(&p->run_list); if (list_empty(array->queue + p->prio)) __clear_bit(p->prio, array->bitmap); } static void enqueue_task(struct task_struct *p, prio_array_t *array) { sched_info_queued(p); list_add_tail(&p->run_list, array->queue + p->prio); __set_bit(p->prio, array->bitmap); array->nr_active++; p->array = array; }
3.schedule_tick()
schedule_tick函数用来更新进程的时间片,它被调用时本地中断被禁止,该函数的具体操作如下。
1.首先通过相应的函数和宏获得当前处理器的编号、当前可运行队列和当前进程描述符就,再通过sched_clock函数获得最近一次定时器中断的时间戳。如果array没有指向本地活动进程队列,则设置TIF_NEED_RESCHED标志,以便在稍候强制进程重新调度。
void scheduler_tick(void) { int cpu = smp_processor_id(); runqueue_t *rq = this_rq(); task_t *p = current; rq->timestamp_last_tick = sched_clock(); if (p == rq->idle) { if (wake_priority_sleeper(rq)) goto out; rebalance_tick(cpu, rq, SCHED_IDLE); return; } if (p->array != rq->active) { set_tsk_need_resched(p); goto out; }
2.由于实时进程和普通进程的调度方法不同,因此这两种进程对时间片的更新方式也有所不同,下面仅说明普通进程更新时间片的方式。如果当前进程是普通进程,则递减当前进程的时间片。
3.如果当前进程时间片用完,首先从当前活动进程集合中删除该进程,然后通过set_tsk_need_resched函数设置TIF_NEED_RESCHED标志。
接着通过effective_prio函数更新当前进程的动态优先级,在进程调度的基本原理中我们已经知道进程的动态优先级是以进程的静态优先级(static_prio)为基数,在通过bonus适当的对其惩罚或奖励。
static int effective_prio(task_t *p) { int bonus, 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; return prio; }
通过effective_prio函数,我们可以总结出进程动态优先级的计算规则:
动态优先级=max(100 , min(静态优先级 – bonus + 5) , 139)
再通过task_timeslice函数对当前进程重新分配时间片,因为我现在所处的分析环境是进程的时间片已经用完。然后将first_time_slice的值设置为0,说明当前进程的时间片可以用完。
上述过程的代码描述如下:
if (rt_task(p)) { /* *更新实时进程的时间片 */ } if (!--p->time_slice) { dequeue_task(p, rq->active); set_tsk_need_resched(p); p->prio = effective_prio(p); p->time_slice = task_timeslice(p); p->first_time_slice = 0;
4.运行队列结构中的expired_timestamp字段用来描述过期进程队列中最早进程被插入队列的时间,如果本地运行队列中该字段等于0,则说明当前过期进程集合为空。因此将当前的时钟节拍赋值给该字段。
由于当前进程的时间片已经用完,因此接下来应该判定是将当前进程插入活动进程集合还是过期进程集合。可能此时你会有疑惑:既然当前进程的时间片已经用完,就应该直接插入过期进程队列,为何还要进行判断插入那个进程集合?
正如基本原理中所说的,调度程序总偏向交互进程以提高系统的响应能力。因此当交互型进程使用完时间片后,调度程序总是重新填充时间片并把它留在活动进程集合中。但调度程序并不永远都偏向交互型程序,如果最早进入过期进程集合的进程已经等待了很长时间,或过期进程的静态优先级比交互进程的静态优先级高,此时调度程序就会将时间片用完的交互进程转移到过期进程集合中。EXPIRED_STARVING宏完成的工作就是判断上述两种情况,至少其一否和,该宏就产生值1。
如果说当前进程已经移入到过期进程集合中,还需根据当前进程的优先级更新运行队列结构中的best_expired_prio字段,该字段用于记录过期进程集合中最高的静态优先级。
如果当前进程是交互进程,而且不满足EXPIRED_STARVING宏,则直接将该交互进程继续插入活动进程集合中。
if (!rq->expired_timestamp) rq->expired_timestamp = jiffies; if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) { enqueue_task(p, rq->expired); if (p->static_prio < rq->best_expired_prio) rq->best_expired_prio = p->static_prio; } else enqueue_task(p, rq->active);
5.如果当前进程并未使用完时间片,则检查当前进程的剩余时间片是否太长。如果当前进程时间片过长的话,就将该进程的时间片分成若干个更小的时间段,这样可以防止拥有较长时间片的进程长时间霸占CPU。并且调度程序会将这样的进程放入与该进程优先级所对应的活动进程队列的末尾,稍候再次对其集成调度。
} else { if (TASK_INTERACTIVE(p) && !((task_timeslice(p) - p->time_slice) % TIMESLICE_GRANULARITY(p)) && (p->time_slice >= TIMESLICE_GRANULARITY(p)) && (p->array == rq->active)) { requeue_task(p, rq->active); set_tsk_need_resched(p); } }
至此,该函数分析完毕。