Linux中有两种调度器,一个是主调度器,一个是周期性调度器。主调度器函数就是schedule函数,当进程打算睡眠或者其他原因放弃CPU时,就会直接调用此函数。周期性调度器就是schedule_tick函数,它在时钟中断中调用,以固定的频率运行。
周期性调度器
内核会按照频率HZ自动调用schedule_tick函数。周期性调度器不负责进程的切换,只是更新调度相关的信息,以备主调度器使用。
在分析schedule_tick函数之前,先看下一些重要的数据结构。
数据结构
runqueue
unsigned long expired_timestamp; /*记录expired数组中最早用完时间片的时间*/ unsigned long long timestamp_last_tick; task_t *curr, *idle; prio_array_t *active, *expired, arrays[2]; int best_expired_prio; /*记录expired数组中最高优先级*/ |
◆active队列对应还有时间片的进程,expired队列对应时间片耗尽必须重新分配时间片的进程。array二元数组是两类队列的容器。
◆一个runqueue对应一个cpu,一个cpu上只能同时运行一个进程,一个runqueue里面只有一个curr。
prio_array
struct prio_array { unsigned int nr_active; /*本组中待处理进程的总数*/ unsigned long bitmap[BITMAP_SIZE]; /*用位图的方式表示某个优先级上有没有待处理的进程队列*/ struct list_head queue[MAX_PRIO]; /*与bitmap对应,存储所有待运行的进程*/ }; |
◆内核中进程有140种优先级,这里BITMAP_SIZE计算出来为5,unsigned long大小为32,这样32*5=160 > 140,每种优先级用1位表示,这样bitmap就足够表示内核的140种优先级,每个优先级上有没有待处理的进程。
函数分析
schedule_tick函数代码片段:
int cpu = smp_processor_id(); runqueue_t *rq = this_rq(); task_t *p = current; unsigned long long now = sched_clock();
update_cpu_clock(p, rq, now); rq->timestamp_last_tick = now;
if (p == rq->idle) { /*如果当前进程是idle进程*/ if (wake_priority_sleeper(rq)) goto out; rebalance_tick(cpu, rq, SCHED_IDLE); return; } |
if (rt_task(p)) { /*当前进程是实时进程*/ if ((p->policy == SCHED_RR) && !--p->time_slice) { p->time_slice = task_timeslice(p); /*重新计算时间片*/ p->first_time_slice = 0; set_tsk_need_resched(p); /* put it at the end of the queue: */ requeue_task(p, rq->active); } goto out_unlock; } |
如果当前进程是实时进程,调度策略为Round-Robin,且时间片已经用完了,则重新计算时间片。first_time_slice清零表明进程在退出时不会将剩余的时间片返还给父进程。设置TIF_NEED_RESCHED标志,并将进程插入active队列的末尾。
/*Normal进程*/ 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; if (!rq->expired_timestamp) /*为0表示第一个切换到expired队列中的进程*/ rq->expired_timestamp = jiffies; /*不是交换进程;或者expired队列中的进程等待了足够长的时间*/ 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); } } |
如果当前进程不是实时进程,且时间片已用完。将此进程从active队列中删除,并设置TIF_NEED_RESCHED标志,重新计算动态优先级和时间片。
●TASK_INTERACTIVE宏用来判断一个进程是否为交互式进程。基于nice值来判断,nice值越高,优先级越低,交互性也越低。
#define TASK_INTERACTIVE(p) ((p)->prio <= (p)->static_prio - DELTA(p)) |
如果一个进程的交互性越强,在其时间片用完之后,不会移到expired队列中,而是插入原来的队列中。这样交互式进程可以快速的响应用户。
●EXPIRED_STARVING宏用来判断expired队列中的进程是否等待了足够长的时间,以避免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)) |
条件1:当前时间(jiffies)减去expired队列中等待最久的时间(expired_timestamp)之差大于STARVATION_LIMIT * ((rq)->nr_running) + 1)。说明进程已经等待了足够长的时间。
条件2:当前进程的优先级比expired中最高的优先级要低。
上述两个条件只要有一个满足就说明expired队列中的进程等待了足够长的时间。
如果进程不是交互进程,或者expired队列中进程等待了足够长的时间,就会将其插入expired队列中,否则插入active队列。
如果当前进程不是实时进程,且时间片还没有用完。剩余的时间片过长,大于进程的时间片粒度,则将其插入active队列的末尾,这实际就是Round-Robin策略。
主调度器
主调度器函数为schedule函数,它主要是选择一个进程以替换当前正在运行的进程,当进程缺少自己申请的相应资源时,就会调用schedule函数让出处理器。
下面分析schedule函数代码实现:
if (likely(!current->exit_state)) { if (unlikely(in_atomic())) { printk(KERN_ERR "scheduling while atomic: " "%s/0x%08x/%d\n", current->comm, preempt_count(), current->pid); dump_stack(); } } |
如果当前进程正在运行,且处于原子态,那么就不能切换给其他进程。这也是为什么不能再中断中调用可能睡眠的函数的原因。
need_resched: preempt_disable(); /*关内核抢占*/ prev = current; need_resched_nonpreemptible: rq = this_rq(); /*idle进程里面调用了休眠函数,导致被调度*/ if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) { printk(KERN_ERR "bad: scheduling from the idle thread!\n"); dump_stack(); } /*计算prev运行时间*/ now = sched_clock(); if (likely((long long)(now - prev->timestamp) < NS_MAX_SLEEP_AVG)) { run_time = now - prev->timestamp; if (unlikely((long long)(now - prev->timestamp) < 0)) run_time = 0; } else run_time = NS_MAX_SLEEP_AVG; run_time /= (CURRENT_BONUS(prev) ? : 1);
spin_lock_irq(&rq->lock); /*关闭当前cpu中断,并获取spinlock锁*/ if (unlikely(prev->flags & PF_DEAD)) /*检查进程是否已终止*/ prev->state = EXIT_DEAD; |
上面代码部分首先关内核抢占,如果prev进程是idle进程,但不是运行状态,这说明idle进程里面调用了休眠函数,导致被调度了,因此会有相应的警告信息。
接着就是计算prev进程的运行时间,并将其值转换为纳秒。
最后检查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); /*从运行队列中移除进程*/ } } |
当prev进程是可中断的睡眠状态时,并且有挂起的信号需要处理,则设置prev的state字段为TASK_RUNNING,prev为可运行进程,保留在可运行队列中。否则将prev进程从运行队列中移除。
上面的部分是对prev进程也就是当前进程的处理,下面开始选择next进程,即使替换当前进程运行的一个进程。
cpu = smp_processor_id(); if (unlikely(!rq->nr_running)) { /*运行队列中没有可运行进程*/ go_idle: idle_balance(cpu, rq); /*从其他运行队列中迁移可运行进程到当前队列中*/ if (!rq->nr_running) { /*迁移失败*/ next = rq->idle; rq->expired_timestamp = 0; wake_sleeping_dependent(cpu, rq); /*调度可运行cpu中的可运行进程*/
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; /*跳转到go_idle重新尝试迁移进程*/ } |
如果运行对列中有可运行进程,不能简单的选择从当前队列中选择一个优先级高的进程来运行。如果这个cpu有一个逻辑兄弟cpu,且逻辑兄弟Cpu上运行着一个优先级要比选择的进程优先级高的进程,那么久不能打扰逻辑兄弟CPU上这个正在运行的进程(因为两个逻辑兄弟CPU共用一个mmu和一级cache),因此这个cpu上选择运行idle,也就是next选择为rq->idle。
上面的这两种情况为了正确的选择一个next进程。如果选择到了合适的next进程,就直接跳转到switch_tasks标签处执行进程切换。
array = rq->active; if (unlikely(!array->nr_active)) { /*可运行进程数组中没有待执行的进程*/ schedstat_inc(rq, sched_switch); rq->active = rq->expired; rq->expired = array; array = rq->active; /*交换active和expired域*/ rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; } |
如果运行队列的active进程组中没有待执行的进程,那么交换运行对列中的active域和expired域。
idx = sched_find_first_bit(array->bitmap); /*查找位图中首个可运行进程*/ queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list);
/*Normal进程,actived大于0说明是从TASK_INTERRUPTIBLE或者TASK_STOPPED状态唤醒的*/ if (!rt_task(next) && next->activated > 0) { unsigned long long delta = now - next->timestamp; if (unlikely((long long)(now - next->timestamp) < 0)) delta = 0;
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; |
调用sched_find_first_bit函数查找active进程数组中优先级最高的进程的ID,通过此ID获取对应的进程next。
task_t结构体的activated字段值意义:
Activated值 | 含义 |
-1 | 从TASK_UNINTERRUPTIBLE状态唤醒 |
0 | 初始化 |
1 | 被系统调用服务程序或内核线性唤醒 |
2 | 被中断或者被延迟函数唤醒 |
如果next进程是normal进程,并且是从TASK_INTERRUPTIBLE或者TASK_STOPPED状态唤醒的,为next进程增加平均睡眠时间。原则是被系统调用服务程序或者内核线程唤醒的进程加部分等待时间,被中断或者被延迟函数唤醒的进程加全部在runqueue中等待的时间。
完成上面的代码是在进程切换之前的一些准备工作,下面开始分析进程切换部分的代码。
(未完待续...)