CFS调度流程分析
主要以CFS调度器为例,介绍一下进程调度的一般流程。首先是调度的时机,基本调度器会根据TIF_NEED_RESHED标记判断是否需要进行调度,所以这个标记的设置就是调度的开始。有以下几种情况都会设置这个标记。例如周期性调度会触发中断设置调度标记,唤醒或者新建一个进程也会进行设置,负载均衡需要迁移进程时也会进行标记,还有修改NICE值导致权重变化的时候也需要设置标记。需要注意的是,设置这个标记不代表立刻开始调度,具体什么时候被调度到还是要看具体的策略和整个就绪队列的情况。
进程创建中的调度初始化sched_fork()
fork()---->kernel_clone()---->copy_process()---->sched_fork()。针对sched_fork()函数,
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
p->state = TASK_NEW;
p->prio = current->normal_prio;
p->sched_class = &fair_sched_class; /* 1 */
if (p->sched_class->task_fork)
p->sched_class->task_fork(p); /* 2 */
return 0;
}
以CFS为例,为task选择调度类。fair_sched_class是CFS调度类。
调用调度类中的task_fork函数。task_fork方法会调用到task_fork_fair。
传递的参数p就是创建的task_struct。
sched_fork函数进行调度相关的初始化比如设置优先级,调度类,就绪队列等,把进程设置为TASK_NEW,无法运行也无法唤醒。task_fork_fair主要用于设置新进程的vruntime,从而也能确定子任务的调度实体在红黑树中的位置。
新进程加入就绪队列wake_up_new_task
fork()---->kernel_clone()---->copy_process()---->wake_up_new_task
经过copy_process的大部分初始化工作完成之后,就可以唤醒新进程准备运行。也就是将新进程加入就绪队列准备调度。
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
p->state = TASK_RUNNING;
#ifdef CONFIG_SMP
p->recent_used_cpu = task_cpu(p);
/*
通过调用调度类中select_task_rq方法选择最空闲的cpu
*/
__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
rq = __task_rq_lock(p, &rf);
//通过调用调度类中enqueue_task方法, 将进程加入就绪队列
activate_task(rq, p, ENQUEUE_NOCLOCK);
p->on_rq = TASK_ON_RQ_QUEUED;
/*既然新进程已经准备就绪,那么此时需要检查新进程是否满足抢占当前正在运行进程的条件,
如果满足抢占条件需要设置TIF_NEED_RESCHED标志位
*/
check_preempt_curr(rq, p, WF_FORK);
}
检测抢占
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
const struct sched_class *class;
if (p->sched_class == rq->curr->sched_class) {
rq->curr->sched_class->check_preempt_curr(rq, p, flags); /* 1 */
} else {
for_each_class(class) { /* 2 */
if (class == rq->curr->sched_class)
break;
if (class == p->sched_class) {
resched_curr(rq);
break;
}
}
}
}
- 唤醒的进程和当前的进程同属于一个调度类,直接调用调度类的check_preempt_curr方法检查抢占条件。毕竟调度器自己管理的进程,自己最清楚是否适合抢占当前进程。
- 如果唤醒的进程和当前进程不属于一个调度类,就需要比较调度类的优先级。例如,当期进程是CFS调度类,唤醒的进程是RT调度类,自然实时进程是需要抢占当前进程的,因为优先级更高。
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
struct sched_entity *se = &curr->se, *pse = &p->se;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
if (wakeup_preempt_entity(se, pse) == 1) /* 1 */
goto preempt;
return;
preempt:
resched_curr(rq); /* 2 */
}
- 检查唤醒的进程是否满足抢占当前进程的条件。
满足抢占的条件就是,唤醒的进程的虚拟时间首先要比正在运行进程的虚拟时间小,并且差值还要大于一定的值才行(这个值是sysctl_sched_wakeup_granularity,称作唤醒抢占粒度)。这样做的目的是避免抢占过于频繁,导致大量上下文切换影响系统性能。
- 如果可以抢占当前进程,设置TIF_NEED_RESCHED flag。
选择下一个合适进程运行
当进程被设置TIF_NEED_RESCHED flag后会在某一时刻触发系统发生调度或者进程调用schedule()函数主动放弃cpu使用权,触发系统调度。
schedule()的核心函数是__schedule()
__schedule进程调度
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
unsigned long *switch_count; /*记录当前进程切换的次数*/
struct rq *rq;
cpu = smp_processor_id(); /*获取所在的cpu*/
rq = cpu_rq(cpu); /*切换到该cpu的rq*/
prev = rq->curr;
update_rq_clock(rq); /*更新rq时钟值*/
/*非0表示处于非running状态,主动调用schedule前会设置此状态*/
prev_state = READ_ONCE(prev->__state);
if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) /*发生非抢占调度,比如主动调出*/
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); /*将当前进程摘除出就绪队列*/
next = pick_next_task(rq, prev, &rf); /*选择要运行的进程(如果当前进程还在运行,在这里摘除)*/
clear_tsk_need_resched(prev); /*清除当前进程的TIF_NEED_RESCHED标记*/
clear_preempt_need_resched();
if (likely(prev != next)) { //不等于说明rq上还有可运行进程
rq->nr_switches++; /*更新队列切换次数*/
++*switch_count; /*更新进程切换次数*/
rq = context_switch(rq, prev, next, &rf); /*切换进程上下文*/
} else
__balance_callbacks(rq); /*当前rq没有可运行进程,进行负载平衡*/
}
- 针对主动放弃cpu进入睡眠的进程,需要从对应的就绪队列上删除该进程。如果
- 选择下个合适的进程开始运行。
- 清除TIF_NEED_RESCHED flag。
- 上下文切换,从prev进程切换到next进程