本文转载自微信公众号《Linux内核航海者》,作者Linux内核航海者。转载本文请联系Linux内核航海者公众号。1.开启环境:处理器架构:arm64内核源码:linux-5.11ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope本文步入Linux内核进程管理的核心部分,打开内核的黑框调度程序,并查看Linux内核如何调度进程。实际上,进程调度器主要做了两件事:选择下一个进程,然后进行上下文切换。而什么时候调用主调度器对进程进行调度就是调度时序的问题,而调度时序在之前的内核抢占文章中已经有详细的讲解,这里不再赘述,但是本篇的调度时序文章重点实际上是调用主调度器的时机。本文分析的内核源码主要集中在:kernel/sched/core.ckernel/sched/fair.c2。调用时序关于调度时序,网上有各种文章。内核抢占那篇文章之前已经详细讲解过了,在这篇文章中,我们从源码注释给出了依据(再次声明:本文中的调度时机重点是什么时候调用主调度器,而不是设置调度器的时机)reschedulingflag,我们知道在前面的解释中都可以称之为调度时序)。先说什么是主调度器。其实还有一个和主调度器并行的叫做periodicscheduler的东西(后面会解释,主要用于时钟中断滴答,抢占处理器的控制权),它们都是内核中的函数,分别是在正确的时间调用。主要的调度函数如下:kernel/sched/core.c__schedule()内核的很多路径都会包裹这个函数,主要分为主动调度和抢占式调度场景。内核源码中的主要调度函数也给出了调度时序的注释。我们基于这个来看一下:kernel/sched/core.c/**__schedule()是主要的调度器函数。**驱动调度器从而进入该函数的主要手段有:**1.显式阻塞:互斥锁、信号量、等待队列等。**2.TIF_NEED_RESCHEDflagischeckedoninterruptanduserspacereturn*paths。例如,seearch/x86/entry_64.S。**为了驱动任务之间的抢占,调度程序设置了flagintimer*interrupthandlerscheduler_tick()。**3。任务到运行队列,就是这样。**现在,如果添加到运行队列的新任务抢占当前*任务,则唤醒设置TIF_NEED_RESCHED和调度()在最近的可能情况下被*调用:**-如果内核是可抢占的(CONFIG_PREEMPTION=y):**-insyscallorexceptioncontext,attenextoutmost*preempt_enable()尽快()*spin_unlock()!)**-在IRQ上下文中,从中断处理程序返回到*可抢占上下文**-如果内核不可抢占(CONFIG_PREEMPTION未设置)*则在下一个:*-cond_resched()call*-explicitschedule()call*-returnfromsyscallorexceptiontouser-space*-returnfrominterrupt-handlertouser-space**WARNING:mustbecalledwithpreemptiondisabled!*/staticvoid__schednotrace__schedule(boolpreempt)我们对注解进行解释,让大家深入理解调度时机(基本上就是照原样翻译,用颜色标出)1.显式阻塞场景:包括互斥量、信号量、等待队列等。这种场景主要是等待一些资源,主动让出处理器调用主调度器。如果发现互斥量被其他内核路径持有,它就会休眠,等待互斥量被释放来唤醒我。2.检查中断和用户空间返回路径上的TIF_NEED_RESCHED标志。例如,arch/x86/entry_64.S。为了驱动任务之间的抢占,调度程序在定时器中断处理程序scheduler_tick()中设置标志。解释如下:这里其实是指重新调度标志(TIF_NEED_RESCHED)的设置和检查。1)Reschedulingflag设置情况:比如根据具体情况设置scheduler_tick周期调度器,根据具体情况设置唤醒路径等。当前场景不直接调用主调度器,而是调用当最近的调度点到达时主调度器。2)Reschedulingflagcheck情况:是真正调用主调度器,后面的场景都会涉及到,这里不再赘述。3.唤醒实际上并不导致进入schedule()。他们将任务添加到运行队列,仅此而已。现在,如果添加到运行队列的新任务抢占当前任务,则唤醒设置TIF_NEED_RESCHED,并在最接近的可能情况下调用schedule():1)如果内核是可抢占的(CONFIG_PREEMPTION=y)-在系统调用时或在异常上下文中,最外层的preempt_enable()。(这可能和wake_up()的spin_unlock()一样快!)-从中??断处理程序返回到IRQ上下文中的抢占式上下文是评论中简洁的几句话,但其含义需要深入理解。首先要知道内核抢占是指内核态的任务被其他任务抢占的情况(不管是不是抢占内核,用户态的任务都可以被抢占,内核态的任务是否可以被抢占Preemption由是否开启内核抢占决定),当然内核态的任务可以是内核线程,也可以是通过系统调用请求内核服务的用户任务。Case1:这是重新启用内核抢占的情况,即当抢占计数器为0时,检查重新调度标志(TIF_NEED_RESCHED),如果设置了,则调用主调度器并放弃处理器(这是抢占式调度)。Case2:中断返回内核态时,检查重调度标志(TIF_NEED_RESCHED)。如果已设置且抢占计数器为0,则调用主调度器并放弃处理器(这就是抢占式调度)。注:内核抢占请参考之前发表的文章。2)如果内核是不可抢占的(CONFIG_PREEMPTION=y)cond_resched()从系统调用或异常中调用显式的schedule()调用从中断处理程序返回用户空间解释如下:cond_resched()是为非抢占内核的一些耗时内核处理路径添加主动抢占点(抢占计数器是否为0且当前任务是否设置了重调度标志),则调用主调度器进行抢占调度,并低-执行延迟处理。显式schedule()调用,这是处理器主动放弃的场景,比如一些睡眠场景,比如用户任务调用sleep。系统调用或异常返回用户空间会判断当前进程是否设置了重新调度标志(TIF_NEED_RESCHED),如果设置了则调用主调度器并放弃处理器。当中断处理程序返回到用户空间时,会判断当前进程是否设置了重新调度标志(TIF_NEED_RESCHED),如果设置了,则调用主调度器,放弃处理器。其实还有一种场景是调用主调度器让出处理器,即进程退出时,这里不再赘述。总结如下:1.主动调度:睡眠场景,比如睡觉。显式阻塞场景,例如互斥量、信号量、等待队列、完成等。任务退出时,调用do_exit释放进程资源,最后调用主调度器一次内核回到用户空间的可抢占内核(增加一些抢占点)重新使能内核抢占中断,返回内核态参考.3.1正常场景中断返回用户态:arch/arm64/kernel/entry.Sel0_irq->ret_to_user->work_pending->do_notify_resume->if(thread_flags&_TIF_NEED_RESCHED){//arch/arm64/kernel/signal.cschedule();->__schedule(false);//kernel/sched/core.cfalse表示主动调度异常返回用户态场景:arch/arm64/kernel/entry.Sel0_sync->ret_to_user...任务退出场景:kernel/exit.cdo_exit->do_task_dead->__schedule(false);//kernel/sched/core.cfalse表示主动调度显式阻塞场景(例如mutex):kernel/locking/mutex.cmutex_lock->__mutex_lock_slowpath->__mutex_lock->__mutex_lock_common->schedule_preempt_disabled->schedule();->__schedule(false);//kernel/sched/core.cfalse表示主动调度3.2支持内核抢占场景中断并返回内核态场景arch/arm64/kernel/entry.Sel1_irq#ifdefCONFIG_PREEMPTION->arm64_preempt_schedule_irq->preempt_schedule_irq();->__schedule(true);//kernel/sched/core.ctrue表示抢占式调度#endif内核抢占开启场景preempt_enable->if(unlikely(preempt_count_dec_and_test()))\//抢占计数器减一0__preempt_schedule();\->preempt_schedule//kernel/sched/core.c-&Gt;__schedule(true)//调用主调度器进行抢占式调度注意:一般来说,异常/中断返回,返回的是处理器的异常状态,可能是用户态也可能是内核态,但是你会看到很多ofinformationwritten都是用户空间/内核空间,不准确,但是我们认为要表达一个意思,思路清晰就可以了。相关的。下面看具体实现:kernel/sched/core.c__schedule->next=pick_next_task(rq,prev,&rf);->if(likely(prev->sched_class<=&fair_sched_class&&|rq->nr_running==rq->cfs.h_nr_running)){p=pick_next_task_fair(rq,prev,rf);if(不太可能(p==RETRY_TASK))gotorestart;/*Assumesfair_sched_class->next==idle_sched_class*/if(!p){put_prev_task(rq,prev);p=pick_next_task_idle(rq);}returnp;}for_each_class(class){p=class->pick_next_task(rq);if(p)returnp;}这里是优化的,当当前进程的调度类是公平的scheduling类或者idle调度类,cpu运行队列的进程数等于cfs运行队列进程数,说明运行队列进程都是普通进程,那么直接调用fair调度类的pick_next_task_fair即可选择下一个进程(选择红黑树最左边的进程),如果没有找到,则当前进程调度类为空闲调度类,直接调用pick_next_task_idle选择空闲进程。否则遍历调度类,调用其pick_next_task方法从高优先级调度类中选择下一个进程。下面以公平调度类为例,看看如何选择下一个进程:调用流程如下(这里不考虑分组调度):pick_next_task->pick_next_task_fair//kernel/sched/fair.c->if(prev)put_prev_task(rq,prev);se=pick_next_entity(cfs_rq,NULL);set_next_entity(cfs_rq,se);先看put_prev_task:put_prev_task->prev->sched_class->put_prev_task(rq,prev);->put_prev_task_fair->put_prev_entity(cfs_rq,se);->/*Put'current'backintothetree.*/__enqueue_entity(cfs_rq,prev));cfs_rq->curr=NULL;这里会调用__enqueue_entity将之前的进程重新加入到cfs队列的红黑树中。然后将cfs_rq->curr设置为空。查看pick_next_entity:pick_next_entity->left=__pick_first_entity(cfs_rq);->left=rb_first_cached(&cfs_rq->tasks_timeline);会选择cfs队列红黑树最左边的进程。最后看set_next_entity:set_next_entity->__dequeue_entity(cfs_rq,se);->cfs_rq->curr=se;这里调用__dequeue_entity将下一个选中的进程从cfs队列的红黑树中删除,然后将cfs队列的curr指向进程的调度实体。选择下一个进程总结如下:如果运行队列中只有公平进程,则选择公平调度类的pick_next_task_fair选择进程。如果当前进程是空闲进程,不存在公平进程,则调用pick_next_task_idle选择空闲进程。如果运行队列中除fair进程外还有其他进程,则调用具体调度类的pick_next_task从高优先级到低优先级选择进程。对于公平调度类,选择下一个进程的主要过程如下:1)调用put_prev_task方法将上一个进程添加到cfs队列的红黑树中。2)调用pick_next_entity选择红黑树最左边的进程作为下一个进程。3)从红黑树中删除下一个进程,cfs队列的curr指向进程的调度实体。一般的调度类选择顺序是:stop_sched_class->dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class例如:当前运行的队列是cfs的一个普通进程,某时刻一个中断唤醒了一个rt进程,然后在最近的调度点到达时,会调用主调度器选择rt进程作为下一个进程。做完以上工作后,在红黑树中选择下一个进程时,将不再选择当前cpu上运行的进程,当前进程调度实体由cfs队列的curr记录(currof正在运行的队列还将记录当前进程)。下面是公平调度类选择下一个进程的示意图(其中A为上一个进程,即当前进程,即上一个进程,B为下一个进程):
