- 调度器概述
由于现在的计算机系统运行的任务的个数远远超过处理器核心的个数,因此导致了各任务在共享处理器、寄存器资源,为了实现处理器时间在各个任务之间公平的分配,实现程序并行运行的假象,操作系统内核需要进程调度器来尽量公平的在各个进程之间分配运行时间。受到现实问题的影响,调度器实现变得很复杂:
- 需要在各进程间尽量公平的分配处理器时间
- 由于更重要的进程要比次重要的进程分配更多的处理器时间,因此需要时间优先级调度,差异化进程
- 进程的切换次数不能太频繁,否则导致处理器的效率降低,将时间消耗在进程切换上
- 两次进程切换的时间又不能太长,否则导致某些进程相应缓慢
linux通用的的调度器框架包括主调度器和周期性调度器,调度器类是实现了不同调度策略的实例,such as CFS、实时调度器,调度器类判断接下来要执行哪个进程。主调度器使用特定调度器类的选择进程,然后负责同底层CPU交互。下面来看看调度器相关的数据结构。
2. 数据结构
先来看看熟悉的struct task_struct结构和调度器相关的成员:
int on_rq;
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
- on_rq:表示进程是否在就绪队列上面
- prio,static_prio, normal_prio ,rt_priority和进程的优先级相关,prio,normal_prio是进程的动态优先级,由于内核有时候可能需要临时提高进程的优先级,因此增加了prio变量,such as 为了防止优先级反转rt_mutex提高当前持有锁进程的优先级就是设置prio变量然后引起重调度,normal_prio是用于调度器计算进程的weight用的,在CFS中可以看到,normal_prio越高,weight越大,表示当前进程所占的权重较大,就可以获得跟多的处理器时间。rt_priority是进程的实时优先级,在普通进程中没有用到。
- se 就是调度实体,是调度器作用的对象,因此task_struct中嵌入这个对象就可以被调度器调度啦
- sched_class 这个是调度器具体实现的接口,主要包含就绪队列的入队和出队操作(对于CFS来将就绪队列是红黑树),还有就是从就绪队列中选择下一个要执行的进程,周期性调度的底层操作,以及修改进程优先级的和内核抢占等操作。
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 nr_migrations;
#ifdef CONFIG_SCHEDSTATS
struct sched_statistics statistics;
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
};
- load就是由进程优先级计算而来的表示进程权重的值
- run_node使得进程可以串在就绪队列上,CFS的就绪队列为一颗红黑树
- on_rq指示进程是否在就绪队列上,就绪队列上的进程表示进程可以运行,等待获得处理器时间,当进程被调度执行时进程从就绪队列上删除并且将on_rq设为0
- exec_start这是个动态更新的值,在进程被调度执行时更新为当前时间,表示此次调度开始执行的时间
- sum_exec_runtime表示总的在处理器上执行的时间,由于进程调度不能太频繁,内核保证每个进程都会执行一段时间才允许被抢占,sum_exec_runtime-prev_exec_runtime就表示此次调度执行的时间。
- vruntime是进程的在完全公平的优先级调度的情况下进程运行的时间,CFS调度中最重要的一个值了,CFS的就绪队列的黑红树的键值就是它了,每次调度就找vruntime最小的进程执行,应该是位于黑红树的最左边的进程。
- cfs_rq这个就是CFS的就绪队列,就绪队列是每一个处理器都有一个
3. 调度框架
先看看周期性调度,想想也可以差不多想出来周期性调度要干什么的,每一个cpu的时钟周期都触发一次进程调度,由上面可以看到进程需要维护调度的当前时间,因此这个函数需要更新进程的当前调度时间,然后就是调用特定调度器类的周期调度函数就ok啦,大体的框框应该是这样子,具体细节还有不少,看看代码:
/*
* This function gets called by the timer code, with HZ frequency.
* We call it with interrupts disabled.
*/
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
sched_clock_tick(); /*处理硬件时钟的一些地方,和我们不相关*/
raw_spin_lock(&rq->lock);
update_rq_clock(rq); /*更新就绪队列的时间*/
update_cpu_load_active(rq);
curr->sched_class->task_tick(rq, curr, 0); /*调用调度器类的底层函数,这个函数会设置进程重调度标志TIF_NEED_RESCHED表示需要重新调度,然后内核会在适当的时机(比如系统调用结束重返用户空间之前)调度,因此周期性调度并不执行真正的调度任务,只是设置一个重调度请求的标志而已*/
raw_spin_unlock(&rq->lock);
perf_event_task_tick();
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq, cpu);
#endif
}
周期性调度还是比较简单,没有涉及到处理真正的调度任务,下面看看主调器,也就是它来响应周期性调度器的TIF_NEED_RESCHED请求执行调度任务的。
在看主调度器之前先看看内核抢占,2.5版本的内核之前,在内核太运行的程序是不能被抢占,只能等内核运行完成调度器才能调度其他的程序运行,这会造成很大的系统延时,在2.5中加入了内核抢占,在非重要的区域内核是可以被抢占,每个进程都维护了一个抢占计数器,preempt_count,preempt_count为0时表示可以抢占,大于0是表示不能抢占,当需要禁止抢占的时候就调用inc_preempt_count将preempt_count加1。当前可以被抢占的时候且已经被抢占的时候将preempt_count加PREEMPT_ACTIVE表示这个进程是被内核抢占的,为了避免其他的inc_preempt_count调用影响此标志位,PREEMPT_ACTIVE = 0x1<<30
主调度器的任务就比较复杂了:
/*
* __schedule() is the main scheduler function.
*/
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); /*停止内核抢占,关键区域*/
cpu = smp_processor_id();
rq = cpu_rq(cpu); /*获得当前cpu的就绪队列*/
rcu_note_context_switch(cpu);
prev = rq->curr; /*在cpu上运行的当前进程,也就是准备被调度离开cpu的进程*/
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
raw_spin_lock_irq(&rq->lock);
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {/*获取当前的进程内核抢占计数,如果设置PREEMPT_ACTIVE表示被抢占,为了使得被抢占的进程可以快速恢复执行,不会执行下面的使进程停止活动的操作*/
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP); /*是进程停止活动*/
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*//*这个跟内核线程相关*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
pre_schedule(rq, prev);/*在CFS中无操作*/
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
put_prev_task(rq, prev); /*将让出处理器的进程加入到就绪队列中,并且统计就绪队列相关数据*/
next = pick_next_task(rq); /*选择下一个要执行的进程,这两个操作的主体都是在具体的调度器类中实现,而不是在linux调度器框架中实现,因此实际实现将在CFS和实时调度中说明*/
clear_tsk_need_resched(prev); /*由于调度已经完成,需要清除TIF_NEED_RESCHED标志位*/
rq->skip_clock_update = 0;
if (likely(prev != next)) {/*在调度器找不到需要运行的进程时才会相等*/
rq->nr_switches++;
rq->curr = next;
++*switch_count;
context_switch(rq, prev, next);/* unlocks the rq *//*处理底层的上下文切换的操作*/
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id(); /*由于切换了新的进程运行,新进程可能运行在不同的cpu上面*/
rq = cpu_rq(cpu); /*同样的理由需要更新就绪队列*/
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
preempt_enable_no_resched();
if (need_resched()) /*如果新的被切换上来的进程设置了TIF_NEED_RESCHED标志,则又重新调度,这个是可能的,当这个进程设置了TIF_NEED_RESCHED标志位之后被高优先级的进程抢占了就会发生这种情况*/
goto need_resched;
}
调度器的整体框架基本上就是这样了,这个调度器框架调度具体的调度类CFS和实时调度的相关操作调度进程。好晚了,睡觉。。。。