假定系统使用统一内存访问模型,并且系统时钟设置为1ms。
统一内存访问模型(UMA):指所有的处理器一致的共享全部物理内存。与之相对的是非统一内存访问(NUMA),这种架构下,CPU访问本地内存比访问共享内存快的多。
7.1:调度策略
进程被分成两类,一类是普通进程,另一类是实时进程。普通进程又分为交互式进程和批处理进程。
交互式进程:这些进程经常和用户交互,要花很多时间键盘和鼠标的操作。当收到输入的时候,进程必须马上被唤醒。
批处理进程:这些进程不需要和用户交互,经常在后台运行。这样的进程不必很快被响应。
实时进程:这些进程有很强的调度需求。体现在优先级小于100。
7.2:调度算法
调度算法根据进程是普通进程还是实时进程有很大的区别。并且,在进程结构体中,通过policy字段,来标识进程的调度类型:
policy标志 | 进程类型 | 备注 |
SCHED_FIFO | 先进先出的实时进程 | 当调度程序把CPU分给这个进程的时候,将进程描述符放在运行队列的当前位置。 |
SCHED_RR | 时间片轮转的实时进程 | 当调度程序把CPU分给这个进程的时候,将进程描述符放在运行队列的末尾位置。这样能够保证同等优先机的实时进程公平的分配时间片。 |
SCHED_NORMAL | 普通的分时进程 |
7.2.1:普通进程的调度
每个普通进程都有静态优先级,从100(最高静态优先级)~139(最低静态优先级)。子进程会继承其父进程的静态优先级,当然也可以通过系统调用改变进程的优先级。
7.2.1.1:基本时间片
静态优先级决定了每次分配给进程的基本时间片。静态优先级越高,其基本时间片也就越长。基本时间片使用task结构体中的time_slice成员描述。
7.2.1.2:动态优先级和平均睡眠时间
动态优先级与静态优先级有关。它是根据某个公式从静态优先级算出来的。动态优先级的范围也是100~139。动态优先级是调度程序选用新的进程来执行的时候使用的值。
根据函数的实现可以明确普通进程优先级的确认方式:
static int effective_prio(task_t *p) { int bonus, prio; if (rt_task(p)) //判断是否是实时进程,依据就是优先级是否小于100 return p->prio; bonus = CURRENT_BONUS(p) - MAX_BONUS / 2; //根据进程的平均睡眠时间来计算bonus。 prio = p->static_prio - bonus; if (prio < MAX_RT_PRIO) prio = MAX_RT_PRIO; if (prio > MAX_PRIO-1) prio = MAX_PRIO-1; //依然限制普通分时进程的优先级在100~139之间。这时候算出的优先级是动态优先级。 return prio; } |
7.2.1.3:活动和过期进程
对于普通分时进程而言,高优先级的进程获得了较大的时间片。在他执行完成后,又会重新给他赋时间片。这样的后果就是,低优先级的任务永远都不会被执行。因此,调度器维持了两个正交的可运行进程队列。分别是活动进程和过期进程。
prio_array_t *active | 包含了140项链表头的数组的数组指针,里面存放的是活动进程 |
prio_array_t *expired | 包含了140项链表头的数组的数组指针,里面存放的是过期进程 |
活动进程指的是还没有用完他的时间片的进程。过期进程指的是已经用完了这次分给他的时间片的进程。
调度器在活动进程数组不为空的情况下调用活动进程。在没有活动进程的时候调用过期进程。
7.2.2:实时进程的调度
实时进程的优先级是0~99。因此实时进程总是先于普通进程执行。此外,实时进程总是被当做是活动进程(也就是他的时间片用完后,不会被放到过期进程的队列中)。
只有在以下情况中,实时进程才会被另一个进程取代:
1:进程被另一个更高优先级的实时进程抢占
2:进程执行了阻塞操作并且进入睡眠。
3:进程停止或者被杀死
4:进程调度策略为时间片轮转(SCHED_RR),并且用完了他的时间片。
总之,这些都反映在调度程序中。
7.3:调度程序所使用的数据结构
在前面进程的介绍是已知,我们总共有五个链表链接进程描述符。进程链表连接了所有的进程描述符,而运行队列链表链接了所有可运行的进程描述符(idle进程除外)。
7.3.1:数据结构runqueue
这是一个每CPU变量,在这里进行定义
static DEFINE_PER_CPU(struct runqueue, runqueues); |
每个成员都是struct runqueue类型,结构体定义如下:
struct runqueue { spinlock_t lock; //保护进程链表的自旋锁 unsigned long nr_running; //链表中可运行进程的数目 unsigned long cpu_load; //运行队列的CPU负载因子,在函数rebalance_tick中更新 unsigned long long nr_switches; unsigned long nr_uninterruptible; //之前在运行队列,但是现在睡眠在TASK_UNINTERRUPTIBLE状态的进程数量。TASK_INTERRUPTIBLE是正常的阻塞睡眠进程,可以被异步信号唤醒。TASK_UNINTERRUPTIBLE是不可中断的睡眠进程,不能被异步信号唤醒 unsigned long expired_timestamp; unsigned long long timestamp_last_tick; //上一次时钟中断时候的纳秒值 task_t *curr; //当前运行队列中正在运行的进程的PCB task_t *idle; //当前CPU的swapper进程的PCB struct mm_struct *prev_mm; prio_array_t *active; //指向活动进程链表的指针,也就是下方的arrays[0] prio_array_t *expired; //指向过期进程链表的指针,也就是下方的arrays[1] prio_array_t arrays[2]; //两个结构体,prio_array_t结构体在之前已经讲过,包含了140个链表,分别链接者优先级不同的进程。这两个结构体分别代表了过期进程和活动进程。调度程序会在调度时,改变被调度任务在arrays中的位置。 int best_expired_prio; atomic_t nr_iowait; struct sched_domain *sd; /* For active balancing */ int active_balance; int push_cpu; task_t *migration_thread; struct list_head migration_queue; }; |
arrays,active和expired的关系如图所示,active和expired分别指向arrays的两个成员。arrays的每个成员类型如下:
struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; //包含了140个链表头,链接不同优先级的进程 }; |
7.3.2:进程描述符
进程描述符中也有一些字段用于调度。
thread_info->flags | 用于存放TIF_NEED_RESCHED,表示必须进行调度 |
thread_info->cpu | 进程所在运行队列的CPU逻辑号 |
prio_array_t *array | 指向前面所说的prio_array_t arrays[2]中的一个 |
time_slice | 进程的时间片中还剩余的时钟节拍数 |
run_list | 双向链表,链表头就是array中的queue |
在创建子进程的时候,会初始化子进程中和调度相关的字段。函数如下:sched_fork
void fastcall sched_fork(task_t *p) { p->state = TASK_RUNNING; //这里将任务状态设置为running,但是还没有将这个任务添加到就绪队列中。也就导致现在还不能通过调度器唤醒这个任务。 INIT_LIST_HEAD(&p->run_list); p->array = NULL; spin_lock_init(&p->switch_lock); #ifdef CONFIG_PREEMPT p->thread_info->preempt_count = 1; #endif local_irq_disable(); p->time_slice = (current->time_slice + 1) >> 1; //子进程的时间片是父进程时间片的一半 p->first_time_slice = 1; //这个标志的作用是,表示如果子进程还没有用完自己的时间片就退出的话,会将时间片返还给父进程 current->time_slice >>= 1; //父进程时间片也减半,这样才能保证调度公平 p->timestamp = sched_clock(); //系统启动以来的纳秒数 if (unlikely(!current->time_slice)) { //如果父进程已经没有时间可用,进入这里 current->time_slice = 1; preempt_disable(); scheduler_tick(); local_irq_enable(); preempt_enable(); } else local_irq_enable(); } |
7.4:调度程序所使用的函数
7.4.1:scheduler_tick函数
函数作用是维持当前任务的time_slice计数器。函数实现如下:
//这个函数会在时钟中断处理函数中调用(关中断);或者是在创建进程的时候调用 void scheduler_tick(void) { int cpu = smp_processor_id(); runqueue_t *rq = this_rq(); //拿本地CPU的就绪队列(每个CPU都有一个就绪队列) task_t *p = current; rq->timestamp_last_tick = sched_clock(); if (p == rq->idle) { //如果当前任务是idle任务 if (wake_priority_sleeper(rq)) //这个函数的作用就是,如果当前就绪队列中还有其他任务,就设置当前任务(也就是idle_task)的调度标志,在中断结束后会进行调度。是否有其他任务是通过rq->nr_running的值判断的。 goto out; return; } /* Task might have expired already, but not scheduled off yet */ if (p->array != rq->active) {//进程没有指向准备队列的active成员,说明进程已经过期(这时候p->array==NULL,但是还是在执行),但是还没有被替换。设置任务的调度标志(TIF_NEED_RESCHED)。产生的原因是,将任务移出运行队列的函数deactivate_task中没有调度点? set_tsk_need_resched(p); goto out; } spin_lock(&rq->lock); if (rt_task(p)) { //这里判断进程的优先级是否小于100。Linux认为优先级小于100的任务是实时任务。 *///根据进程的调度类型有普通进程,先进先出进程和时间片轮转进程 if ((p->policy == SCHED_RR) && !--p->time_slice) {//如果进程的调度类型是时间片,并且现在进程的时间片已经用完了 p->time_slice = task_timeslice(p); //根据进程的静态优先级,重新设置进程的时间片数量 p->first_time_slice = 0; //这个标志在创建任务的时候设置为1(copy_process),在进程的第一个时间片用完的时候清0。 set_tsk_need_resched(p); //设置调度标志,这样的话,进程会被优先级相同或者更高的任务抢占 requeue_task(p, rq->active); //将这个任务移动到相应的优先级等待队列的最后 } goto out_unlock; } //上面的操作就是针对实时进程的操作。可知,如果是policy不为SCHED_RR的实时进程,会一直执行下去,直到这个进程因为其他原因放弃CPU。也就是说,这种实时进程不会因为时间片用完而退出。 只有policy为SCHED_RR的实时进程,会在时间片用完之后,重新设置任务的时间片。然后设置调度标志。不过这种实时任务也不会变成过期任务,而是将他放入运行队列的active的末尾。 可见这些内容和之前介绍的实时任务的调度策略相同。 if (!--p->time_slice) { //这种情况下,任务不是实时任务。还是递减时间片 dequeue_task(p, rq->active); //进程时间片用完,将这个进程从active队列中删除。这是链表操作。这时候task的run_list成员没有加入任何队列中,在下面才被插入到过期进程链表中。 set_tsk_need_resched(p); p->prio = effective_prio(p); p->time_slice = task_timeslice(p); //重新写任务的时间片 p->first_time_slice = 0; if (!rq->expired_timestamp) //如果调度器发现,只有过期进程而没有活动进程的时候,就会将指向过期进程的指针和指向活动进程的指针相互交换(rq->active与rq->expired互换)。这时候会将expired_timestamp这个值设置为0. rq->expired_timestamp = jiffies; // jiffies为当前tick数 if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) { enqueue_task(p, rq->expired); //将进程插入到队列的过期进程链表中 } else enqueue_task(p, rq->active); //将进程插入到队列的活动进程链表中 } else { //非实时任务,并且其时间片没有用完 if (……) { requeue_task(p, rq->active); //一些条件下,任务没有用完他的时间片,也会设置调度标志并且将任务移动到队尾。以防止有些任务的时间片过长 set_tsk_need_resched(p); } } out_unlock: spin_unlock(&rq->lock); out: rebalance_tick(cpu, rq, NOT_IDLE); } |
总之,排除这些和时间相关的算法代码可以看到,这个函数做的主要事情就是,判断当前任务的时间片是否还有剩余。有的话继续执行,不然的话,就设置调度标志TIF_NEED_RESCHED。并且,在这里就将任务移出活动进程链表,移入过期进程链表,但是此时并没有发生任务切换,还是这个任务在执行,任务切换要到schedule函数时才会发生。
结合之前的时钟中断的处理流程,可以知道,时钟中断的处理例程中,除了一些监视热点等不重要操作外,重要操作只有设置和时间相关的全局变量,以及根据当前任务是否还有时间片,设置调度标志。
7.4.2:try_to_wake_up函数
try_to_wake_up函数将睡眠或者停止的进程设置为TASK_RUNNING状态,并且将进程插入到本地CPU的运行队列中。代码如下所示:
static int try_to_wake_up(task_t * p, unsigned int state, int sync) //sync的作用:用于禁止被唤醒的进程抢占本地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); //函数的作用包含:关中断,拿就绪队列的锁,以及拿就绪队列。就绪队列就是之前任务在的队列。 schedstat_inc(rq, ttwu_cnt); //增加就绪队列中ttwu_cnt的值,表示try_to_wake_up_count old_state = p->state; if (!(old_state & state)) //如果要被唤醒的进程状态不在掩码中,那么不唤醒这个进程 goto out; if (p->array) //如果进程的就绪队列指针不为空,说明已经被加到其他CPU的就绪队列中 goto out_running; cpu = task_cpu(p); //进程之前所在的CPU this_cpu = smp_processor_id(); #ifdef CONFIG_SMP //在多处理器系统中,函数要检查被唤醒的进程是否要从最近运行的CPU迁移到另外的CPU的运行队列上去。和负载平衡相关。这部分代码不讨论 #endif /* CONFIG_SMP */ 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; } |
对task_rq_lock进行分析:
static runqueue_t *task_rq_lock(task_t *p, unsigned long *flags) __acquires(rq->lock) { struct runqueue *rq; repeat_lock_task: local_irq_save(*flags); rq = task_rq(p); //这里拿到任务所在的就绪队列 spin_lock(&rq->lock); if (unlikely(rq != task_rq(p))) { //如果这时候任务所在的就绪队列和之前不一致,那么就要重新拿一遍就绪队列 spin_unlock_irqrestore(&rq->lock, *flags); goto repeat_lock_task; } return rq; } |
对函数activate_task进行分析。在此之前首先介绍进程描述符中activated字段的含义。
值 | 说明 |
0 | 进程处于TASK_RUNNING状态 |
1 | 进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,并且正在被系统调用服务例程或者内核线程唤醒 |
2 | 进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,并且正在被中断处理程序或者可延迟函数唤醒 |
-1 | 进程处于TASK_UNINTERRUPTIBLE状态而且正在被唤醒 |
static void activate_task(task_t *p, runqueue_t *rq, int local)//把进程p加到就绪队列rq中,参数local表示当前CPU是否是进程p之前使用的CPU { unsigned long long now; now = sched_clock(); //获取当前时间(单位为纳秒) #ifdef CONFIG_SMP //对称多处理,每个核的功能一致 #endif recalc_task_prio(p, now); if (!p->activated) { if (in_interrupt()) p->activated = 2; else { p->activated = 1; } } p->timestamp = now; //可知timestamp字段表明了任务被放入就绪队列的时间 __activate_task(p, rq); //将任务添加到就绪队列的active队列 } |
7.4.3:recalc_task_prio()函数
函数recalc_task_prio用于计算进程的平均睡眠事件和动态优先级。函数实现如下:
static void recalc_task_prio(task_t *p, unsigned long long now) { unsigned long long __sleep_time = now - p->timestamp; //now是当前时间,timestamp是任务上一次切出的时间。因此得到任务的睡眠时间 unsigned long sleep_time; if (__sleep_time > NS_MAX_SLEEP_AVG) sleep_time = NS_MAX_SLEEP_AVG; else sleep_time = (unsigned long)__sleep_time; if (likely(sleep_time > 0)) { if (p->mm && p->activated != -1 && // 当任务从TASK_UNINTERRUPTIBLE状态下被唤醒时,activated位设置为-1 sleep_time > INTERACTIVE_SLEEP(p)) { //这个条件表示,进程不是内核线程(内核线程的mm结构体是NULL),进程不是从TASK_UNINTERRUPTIBLE被唤醒,进程连续睡眠时间超过极限 p->sleep_avg = JIFFIES_TO_NS(MAX_SLEEP_AVG - DEF_TIMESLICE); } else { sleep_time *= (MAX_BONUS - CURRENT_BONUS(p)) ? : 1; if (p->activated == -1 && p->mm) { if (p->sleep_avg >= INTERACTIVE_SLEEP(p)) sleep_time = 0; else if (p->sleep_avg + sleep_time >= INTERACTIVE_SLEEP(p)) { p->sleep_avg = INTERACTIVE_SLEEP(p); sleep_time = 0; } } p->sleep_avg += sleep_time; if (p->sleep_avg > NS_MAX_SLEEP_AVG) p->sleep_avg = NS_MAX_SLEEP_AVG; } } p->prio = effective_prio(p); //更新进程的动态优先级 } |
7.4.4:schedule()函数
asmlinkage void __sched schedule(void) //asmlinkage表示所有的参数都从栈上取 { long *switch_count; task_t *prev, *next; runqueue_t *rq; prio_array_t *array; struct list_head *queue; unsigned long long now; unsigned long run_time; int cpu, idx; need_resched: preempt_disable(); //关抢占 prev = current; release_kernel_lock(prev); //释放大内核锁 need_resched_nonpreemptible: rq = this_rq(); schedstat_inc(rq, sched_cnt); now = sched_clock(); //获取当前的时间戳,单位为纳秒 if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG)) run_time = now - prev->timestamp; //获得任务的执行时间 else run_time = NS_MAX_SLEEP_AVG; /* * Tasks charged proportionately less run_time at high sleep_avg to * delay them losing their interactive status */ run_time /= (CURRENT_BONUS(prev) ? : 1); spin_lock_irq(&rq->lock); //关中断,获取就绪队列的锁 if (unlikely(prev->flags & PF_DEAD)) prev->state = EXIT_DEAD; switch_count = &prev->nivcsw; if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { switch_count = &prev->nvcsw; if (unlikely((prev->state & TASK_INTERRUPTIBLE) && unlikely(signal_pending(prev)))) prev->state = TASK_RUNNING; else { if (prev->state == TASK_UNINTERRUPTIBLE) rq->nr_uninterruptible++; deactivate_task(prev, rq); //将任务从运行队列中移出 } } cpu = smp_processor_id(); if (unlikely(!rq->nr_running)) { //如果运行队列中没有可运行的任务,那么需要使用负载平衡技术,从其他CPU中选择任务 go_idle: idle_balance(cpu, rq); if (!rq->nr_running) { next = rq->idle; rq->expired_timestamp = 0; wake_sleeping_dependent(cpu, rq); /* * wake_sleeping_dependent() might have released * the runqueue, so break out if we got new * tasks meanwhile: */ if (!rq->nr_running) goto switch_tasks; } } else { if (dependent_sleeper(cpu, rq)) { next = rq->idle; goto switch_tasks; } if (unlikely(!rq->nr_running)) goto go_idle; } array = rq->active; if (unlikely(!array->nr_active)) { //如果active队列中没有任务,那么说明所有的任务都从active变成了expired状态 schedstat_inc(rq, sched_switch); rq->active = rq->expired; rq->expired = array; array = rq->active; rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; } else schedstat_inc(rq, sched_noswitch); idx = sched_find_first_bit(array->bitmap); //找到最高优先级对应的数组 queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list); //找到下一个任务 if (!rt_task(next) && next->activated > 0) { unsigned long long delta = now - next->timestamp; if (next->activated == 1) delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128; array = next->array; dequeue_task(next, array); recalc_task_prio(next, next->timestamp + delta); enqueue_task(next, array); } next->activated = 0; switch_tasks: if (next == rq->idle) schedstat_inc(rq, sched_goidle); prefetch(next); clear_tsk_need_resched(prev); rcu_qsctr_inc(task_cpu(prev)); prev->sleep_avg -= run_time; if ((long)prev->sleep_avg <= 0) prev->sleep_avg = 0; prev->timestamp = prev->last_ran = now; //更新进程时间。应该是进程每次切入切出的时间都会记在这个变量中 sched_info_switch(prev, next); if (likely(prev != next)) { next->timestamp = now; rq->nr_switches++; rq->curr = next; ++*switch_count; prepare_arch_switch(rq, next); prev = context_switch(rq, prev, next); //发生进程切换 barrier(); //prev再次执行这里的时候,是下一个任务又切换到prev的时候。但是这时候上下文是perv切换到next的时候 finish_task_switch(prev); } else spin_unlock_irq(&rq->lock); prev = current; if (unlikely(reacquire_kernel_lock(prev) < 0)) goto need_resched_nonpreemptible; preempt_enable_no_resched(); if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) goto need_resched; } |
7.5:多处理系统中运行队列的平衡
内核为了充分利用CPU,需要实现不同CPU之间的负载平衡,在需要的时候,将一些进程从一个CPU的运行队列转移到其他CPU的运行队列上。负载平衡算法需要考虑到CPU的拓扑结构。因此,Linux提出了调度域的概念。
7.5.1:调度域
7.5.2:rebalance_tick函数
static void rebalance_tick(int this_cpu, runqueue_t *this_rq, enum idle_type idle) { unsigned long old_load, this_load; unsigned long j = jiffies + CPU_OFFSET(this_cpu); // jiffies每次时钟中断的时候增加1 struct sched_domain *sd; /* Update our load */ old_load = this_rq->cpu_load; this_load = this_rq->nr_running * SCHED_LOAD_SCALE; /* * Round up the averaging division if load is increasing. This * prevents us from getting stuck on 9 if the load is 10, for * example. */ if (this_load > old_load) old_load++; this_rq->cpu_load = (old_load + this_load) / 2; for_each_domain(this_cpu, sd) { unsigned long interval; if (!(sd->flags & SD_LOAD_BALANCE)) continue; interval = sd->balance_interval; if (idle != SCHED_IDLE) interval *= sd->busy_factor; /* scale ms to jiffies */ interval = msecs_to_jiffies(interval); if (unlikely(!interval)) interval = 1; if (j - sd->last_balance >= interval) { if (load_balance(this_cpu, this_rq, sd, idle)) { /* We've pulled tasks over so no longer idle */ idle = NOT_IDLE; } sd->last_balance += interval; } } } |