调度策略
决定什么时候以怎么样的方式选择一个新的进程运行的这组规则就是所谓的进程调度。
linux调度基于分时(time sharing)技术,多个进程以“时间多路复用”方式运行。CPU的时间被分成片,给每个可运行进程分配一片。
分时依赖于定时中断。
linux中,进程的优先级是动态的,调度程序跟踪进程正在做什么,并周期性的调整优先级。
传统上把进程分类为“I/O受限”或“CPU受限”。前者频繁使用I/O设备,花费很多时间等待I/O操作的完成;后者需要大量CPU时间的数值计算应用程序。
另一种分类方法把进程分为三类:
交互式进程(interactive process):与用户进行交互,典型的交互程序是shell,文本编辑器及图形应用程序。
批处理进程(batch process):不与用户交互,经常在后台运行。受到调度程序的慢待。典型的有程序设计语言的编译程序、数据库搜索引擎及科学计算。
实时进程(real-time process):有很强的调度需要。这些进程绝不会被低优先级进程阻塞,有一个短的响应时间。
进程的抢占
linux进程是抢占式的,如果进程进入TASK_RUNNING状态,内核检查它的动态优先级是否大于当前进程的优先级,如果是,current执行被中断,执行新的进程。被抢占的进程并没有挂起,仍然处于TASK_RUNNING状态,只不过不再使用CPU。
一个时间片必须持续多长?
通常认为长的时间片会降低交互式应用程序的响应时间,这往往是错误的。交互式进程优先级较高,不管时间片多长,都会很快抢占批处理进程。
linux采取单凭经验的方法,即选择尽可能长、同时能保持良好响应时间的一个时间片。
调度算法
linux2.6调度算法,通过设计,解决了与可运行进程数量的比例关系。因为它在固定时间内,选中要运行的进程。同时也很好的处理了与处理器数量的比例关系,因为每个CPU都有自己的可运行进程队列。并且解决了区分交互式进程和批处理进程的问题。
总是至少有一个可运行进程,即swapper进程,PID等于0。每个linux进程总是按照下面的调度类型被调度:
SCHED_FIFO:先进先出的实时进程。
SCHED_RR:时间片轮转的实时进程。当调度程序把cpu分配给进程的时候,把该进程的描述符放在运行队列链表的末尾,这样,对所有具有相同优先级的SCHED_RR实时进程公平的分配CPU时间。
SCHE_NORMAL:普通的分时进程。
普通进程的调度
每个普通进程都有自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间调度的程度。内核从100(最高)到139(最低)表示普通进程的静态优先级。新进程总是继承父进程的静态优先级。
基本时间片
静态优先级与基本时间片的关系如下:
基本时间片(ms) = (140 - 静态优先级) * 20, 如果静态优先级 < 120
基本时间片(ms) = (140 - 静态优先级) * 5, 如果静态优先级 >= 120
这样看来,优先级较高的进程,获得更长的CPU时间片。
动态优先级
动态优先级范围是100(最高)~ 139(最低)。动态优先级是调度程序在选择新进程来运行的时候使用的数。与静态优先级的关系如下:
动态优先级 = max(100, min(静态优先级 - bonus + 5, 139))
bonus范围是0 ~ 10,值小于5表示降低动态优先级以惩罚,大于5表示增加动态优先级奖赏。bonus的值与进程的平均睡眠时间有关。
平均睡眠时间是进程在睡眠状态所消耗的平均纳秒数。
如果进程满足下面的公式,被看作是交互式进程:
动态优先级 <= 3 * 静态优先级/4 + 28
这个公式相当于:bonus-5 >= 静态优先级/4 - 28
活动和活期进程
即使具有较高静态优先级的普通进程获得较大的CPU时间片,也不应该让静态优先级较低的进程无法进行。为了避免进程饥饿,调度程序维持了两个不相交的可运行进程的集合。
活动进程:没有用完他们的时间片,允许他们运行。
过期进程:已经用完时间片,因此被禁止运行,直到所有活动进程都过期。
实时进程的调度
每个实时进程都与一个实时优先级相关,这个优先级范围是从1 ~ 99的值。调度程序总是让优先级高的进程运行。用户可以通过sched_setparam()和sched_setscheduler()改变进程的实时优先级。
只有下述事件之一发生时,实时进程才会被另外一个进程取代:
- 进程被另外一个具有更高实时优先级的实时进程抢占
- 进程执行了阻塞操作并进入睡眠。
- 进程停止或被杀死
- 进程通过系统调用sched_yield自愿放弃CPU。
- 进程是基于时间片轮转的实时进程,而且用完了它的时间片。
运行队列链表链接所有的可运行进程的进程描述符,swapper进程除外。
数据结构runqueue
runqueue是linux2.6调度程序最重要的数据结构。每个CPU都有自己的runqueue,所有的runqueue数据结构存放在runqueue每CPU变量中。this_rq产生本地CPU运行队列的地址,cpu_rq产生相应CPU的运行队列地址。
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_SMP
unsigned long cpu_load;//基于运行队列中进程的平均数量的CPU负载因子
#endif
unsigned long long nr_switches;//CPU执行进程切换的次数
/*
* 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;//先前在运行队列链表中,而现在睡眠在TASK_UNINTERRUPTIBLE状态的进程的数量
unsigned long expired_timestamp;//过期队列中最老的进程被插入队列的时间
unsigned long long timestamp_last_tick;//最近一次定时器中断的时间戳的值
task_t *curr, *idle;//curr是当前正在运行进程的描述符指针,当前CPU上swapper进程的描述符指针
struct mm_struct *prev_mm;//进程切换期间存放被替换进程的内存描述符的地址
prio_array_t *active, *expired, arrays[2];//active指向活动进程链表的指针,expired指向过期进程链表的指针,arrays这两个进程的集合
int best_expired_prio;//过期进程中静态优先级最高的进程
atomic_t nr_iowait;//先前在运行队列的链表中,而现在正等待磁盘I/O操作结束的进程的数量
#ifdef CONFIG_SMP
struct sched_domain *sd;//指向当前CPU的基本调度域
/* 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
};
runqueue数据结构中最重要的字段是与可运行进程的链表相关的 字段。每个可运行进程属于且只属于一个运行队列。expired和active分别指向arrays数组中的其中一个成员。
进程描述符
每个进程描述符都包括几个与调度相关的字段。
unsigned thread_info->flags;//存放TIF_NEED_RESCHED标志,如果必须调用调度程序,设置该标志
unsigned int thread_info->cpu;//可运行进程所在运行队列的CPU逻辑号
unsigned long state;//进程的当前状态
int prio;//进程的动态优先级
int static_prio;//进程的静态优先级
struct list_head run_list;//指向进程所属的运行队列链表中的下一个和前一个元素
prio_array_t *array;//指向包含进程的运行队列的集合
unsigned long sleep_avg;//进程的平均睡眠时间
unsigned long long timestamp;//进程最近插入运行队列的时间
unsigned long long last_ran;//最后一次替换本进程的进程切换时间
int activated;//进程被唤醒时所用的条件代码。
unsigned long policy;//进程的调度类型
cpumask_t cpus_allowed;//能执行进程的CPU的位掩码
unsigned int time_slice;//进程的时间片中还剩余的时钟节拍数
unsigned int first_time_slite;//如果进程肯定不会用完其时间片,设置该标志位
unsinged long rt_priority;//进程的实时优先级
创建新进程时,在copy_process中调用sched_fork函数,设置current(父)和p进程(子)的time_slice字段:
p->time_slice = (current->time_slice + 1) >> 1;
current->time_slice >>= 1;
也就是把父进程的剩余的节拍数分成两等分。这样就避免了用户获得无限的CPU时间。一个进程不能创建多个后代来霸占资源。
如果父进程的时间只剩下一个时钟节拍,则强制把父进程的时钟节拍设为0,从而耗尽父进程的时间片。
copy_process初始化子进程描述符中与进程调度相关的几个字段:
p->first_time_slice = 1;
p->timestamp = sched_clock();
函数sched_clock返回被转化成纳秒的64位寄存器TSC的内容。
调度程序所使用的函数
调度程序依靠几个函数完成调度工作,重要的是:
scheduler_tick():维持当前最新的time_slice计数器。
try_to_wake_up():唤醒睡眠进程
recalc_task_prio():更新进程的动态优先级
schedule():选择要执行的新进程。
load_balance():维持多处理器中运行队列的平衡。
schdeduler_tick()函数
void scheduler_tick(void)
{
int cpu = smp_processor_id();
runqueue_t *rq = this_rq();
task_t *p = current;
rq->timestamp_last_tick = sched_clock();//把转换为纳秒的TSC的当前值保存
if (p == rq->idle) {//检查当前进程是否是swapper进程
if (wake_priority_sleeper(rq))//如果本地运行队列除了swapper进程,还包括另外一个可运行的进程,就设置TIF_NEED_RESCHED标志,强迫进行重新调度
goto out;
rebalance_tick(cpu, rq, SCHED_IDLE);
return;
}
if (p->array != rq->active) {//检查当前的array是否指向本地运行队列的活动链表。如果不是,进程已经过期,设置TIF_NEED_RESCHED,强制进行重新调度。
set_tsk_need_resched(p);
goto out;
}
spin_lock(&rq->lock);//获取this_rq->lock自旋锁
if (rt_task(p)) {//更新实时进程的时间片
if ((p->policy == SCHED_RR) && !--p->time_slice) {
p->time_slice = task_timeslice(p);//重新填写进程的时间片计数器,检查静态优先级,并根据公式返回相应的基本时间片
p->first_time_slice = 0;//字段清0
set_tsk_need_resched(p);//设置TIF_NEED_RESCHED标志,该标志强制调用schedule函数,以便current指向另外一个同优先级的实时进程
requeue_task(p, rq->active);//把进程描述符移到与当前进程优先级相应的运行队列活动链表的尾部
}
goto out_unlock;
}
if (!--p->time_slice) {//更新普通进程的时间片
dequeue_task(p, rq->active);//从可运行进程的active集合中删除current指向的进程
set_tsk_need_resched(p);//设置TIF_NEED_RESCHED标志
p->prio = effective_prio(p);//更新current指向的进程的动态优先级
p->time_slice = task_timeslice(p);//重填进程的时间片
p->first_time_slice = 0;
if (!rq->expired_timestamp)//如果expired_timestamp字段等于0
rq->expired_timestamp = jiffies;//把当前时钟节拍的值赋给expired_timestamp
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);
} 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);
}
}
out_unlock:
spin_unlock(&rq->lock);//释放lock自旋锁
out:
rebalance_tick(cpu, rq, NOT_IDLE);//保证不同CPU的运行队列包含数量基本相同的可运行进程。
}
try_to_wake_up函数
static int try_to_wake_up(task_t * p, unsigned int state, int sync)//把进程设置为TASK_RUNNING,把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程
{
int cpu, this_cpu, success = 0;
unsigned long flags;
long old_state;
runqueue_t *rq;
#ifdef CONFIG_SMP
unsigned long load, this_load;
struct sched_domain *sd;
int new_cpu;
#endif
rq = task_rq_lock(p, &flags);//禁用本地中断,获得最后执行进程的CPU所拥有的
schedstat_inc(rq, ttwu_cnt);
old_state = p->state;
if (!(old_state & state))//检查进程的状态是否属于传入的参数state,如果不是,结束
goto out;
if (p->array)//如果不为NULL,进程已经属于某个运行队列
goto out_running;
cpu = task_cpu(p);
this_cpu = smp_processor_id();
#ifdef CONFIG_SMP
if (unlikely(task_running(rq, p)))
goto out_activate;
new_cpu = cpu;
if (cpu == this_cpu || unlikely(!cpu_isset(this_cpu, p->cpus_allowed)))
goto out_set_cpu;
load = source_load(cpu);
this_load = target_load(this_cpu);
if (sync)
this_load -= SCHED_LOAD_SCALE;
if (load < SCHED_LOAD_SCALE/2 && this_load > SCHED_LOAD_SCALE/2)
goto out_set_cpu;
new_cpu = this_cpu;
for_each_domain(this_cpu, sd) {
unsigned int imbalance;
imbalance = sd->imbalance_pct + (sd->imbalance_pct - 100) / 2;
if ((sd->flags & SD_WAKE_AFFINE) &&
!task_hot(p, rq->timestamp_last_tick, sd)) {
if (cpu_isset(cpu, sd->span)) {
schedstat_inc(sd, ttwu_wake_affine);
goto out_set_cpu;
}
} else if ((sd->flags & SD_WAKE_BALANCE) &&
imbalance*this_load <= 100*load) {
if (cpu_isset(cpu, sd->span)) {
schedstat_inc(sd, ttwu_wake_balance);
goto out_set_cpu;
}
}
}
new_cpu = cpu;
out_set_cpu:
schedstat_inc(rq, ttwu_attempts);
new_cpu = wake_idle(new_cpu, p);
if (new_cpu != cpu) {
schedstat_inc(rq, ttwu_moved);
set_task_cpu(p, new_cpu);
task_rq_unlock(rq, &flags);
rq = task_rq_lock(p, &flags);
old_state = p->state;
if (!(old_state & state))
goto out;
if (p->array)
goto out_running;
this_cpu = smp_processor_id();
cpu = task_cpu(p);
}
out_activate:
#endif
if (old_state == TASK_UNINTERRUPTIBLE) {
rq->nr_uninterruptible--;
p->activated = -1;
}
activate_task(p, rq, cpu == this_cpu);
if (!sync || cpu != this_cpu) {
if (TASK_PREEMPTS_CURR(p, rq))
resched_task(rq->curr);
}
success = 1;
out_running:
p->state = TASK_RUNNING;
out:
task_rq_unlock(rq, &flags);
return success;
}
static void activate_task(task_t *p, runqueue_t *rq, int local)
{
unsigned long long now;
now = sched_clock();
#ifdef CONFIG_SMP
if (!local) {
runqueue_t *this_rq = this_rq();
now = (now - this_rq->timestamp_last_tick)
+ rq->timestamp_last_tick;
}
#endif
recalc_task_prio(p, now);
if (!p->activated) {
if (in_interrupt())
p->activated = 2;
else {
p->activated = 1;
}
}
p->timestamp = now;
__activate_task(p, rq);
}