天天看点

__schedule的一些小细节

(代码主要参考5.10)

1. __schedule的参数preempt

static void __sched notrace __schedule(bool preempt)

preempt

是一个bool的类型的值。

__schedule

中有这样的一段代码,(有删减):

switch_count = &prev->nivcsw;
​
prev_state = prev->state;
if (!preempt && prev_state) {
    if (signal_pending_state(prev_state, prev)) {
        prev_state = TASK_RUNNING;
    } else {
        ......;
        deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
        ......;
    }
    switch_count = &prev->nvcsw;
}
​
......;
if (likely(prev != next)) {
    ......;
    ++switch_context;
}      

preempt

代表是否自愿上下文切换。如果是自愿(非抢占进行调度),则为false;如果是非自愿(抢占进行调度),则为true。

struct task_struct

有两个成员

nvcsw

nivcsw

nvcsw nivcsw
Number of Voluntary Context Switches(自愿上下文切换的计数) Number of InVoluntary Context Switches(非自愿上下文切换计数)

当一个进程非自愿上下文切换的时候,即被抢占的时候,会少判断一些内容;

而当一个进程自愿上下文切换的时候,即主动放弃CPU的时候,要进行一些判断,会决定prev的状态,是否出队,以及负载均衡的一些操作,这里就不详细描述了。

至于哪些函数,会触发调度

__schedule

,它们分别是抢占还是非抢占呢?5.10中如下所示:

function preempt
do_task_dead false
schedule false
schedule_idle false
preempt_schedule_comm true
preempt_schedule_notrace true
preempt_schedule_irq true

这些函数留给后续分析吧。

2. pick_next_task的两条路径

pick_next_task

函数在

__schedule

中调用,挑选下一个要执行的进程。

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *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);
        ......; 
    }
restart:
    ......;
    for_each_class(class) {---长路径
        p = class->pick_next_task(rq);
        ......;
    }
}      

是走长路径、还是短路径呢?判断条件为:当前进程的调度类是否为cfs或者idle以及运行队列的进程数量是否与cfs运行队列的进程数量相等。

cfs_rq

中除了

h_nr_running

外,还有一个

nr_running

,以及

rq

中也存在一个

nr_running

,它们分别代表什么?

成员 解释
rq的nr_running 代表运行队列的进程个数
cfs_rq的nr_running 开启组调度的话,代表组调度最上层的group个数
cfs_rq的h_nr_running 代表cfs_rq中的进程个数

3. context_switch的四种情况

挑选出下一个要执行的进程next后,要使用

context_switch

进行地址空间的切换。

static __always_inline stuct rq *
context_switch(struct rq *rq, struct task_struct *prev,
        struct task_struct *next, struct rq_flags *rf)
{
    ......;
    if (!next->mm) {
        enter_lazy_tlb(prev->active);
        
        next->active_mm = prev->active_mm;
        if (prev->mm)
            mmgrab(prev->active_mm);
        else
            prev->active_mm = NULL;
    } else {
        membarrier_switch_mm(rq, prev->active_mm, next->mm);
​
        switch_mm_irqs_off(prev->active_mm, next->mm, next);
​
        if (!prev->mm) {
            rq->prev_mm = prev->active_mm;
            prev->active_mm = NULL;
        }
    }
    ......;
    switch_to(prev, next, prev);
    barrier();
​
    return finish_task_switch(prev);
}      

所谓的四种情况,其实就是prev和next分别是user线程还是kernel线程的组合情况。

prev next 操作
kernel kernel tlb lazy模式,next借用prev的active_mm,prev的active_mm清空
user kernel tlb lazy模式,next借用prev的active_mm,prev的mm_count增加计数
kernel user 地址空间切换,rq记录prev_mm,将prev->active_mm清空
user user 进程地址空间切换

Q1:什么是tlb lazy模式?

tlb是什么?是一个虚拟地址转换成物理地址的快速转换表,常用于cache寻址中。

通常CPU都是进程切换一次,进行一次flush(后面有其他不用全部flush的方法,不详细描述了)。

而内核空间是所有进程通用的,故可以不用flush tlb,这就是tlb lazy模式。

Q2:

mm

active_mm

区别?

mm

的存在与否用于判定该进程是属于user还是kernel;

active_mm

则为实际使用的地址空间,kernel线程总是借用user线程的地址空间。

可以看到,每次kernel线程被切换出去后,它的

active_mm

就会被清空,因为是借用的;而每次user线程切换kernel的时候,还会增加一个计数值,用于表示该user线程的地址空间被借用了。

4. switch_to的三个参数

switch_to

的工作主要是切换内核栈,它的具体实现就不在这里分析。

不同的体系架构下也不一样,例如,X86的实现主要使用将当前寄存器的一些值压到prev的内核栈,将内核栈顶指针保存到每个进程相关联的

thread_info

,然后切换到next的内核栈,并出栈,将其栈中内容填充到寄存器,以恢复现场。

switch_to

三个参数,其中两个prev的考虑:

设想场景如下:a切换到b,b切换到c,c切换到a。

a压栈时内容:prev为a,next为b;

b压栈时内容:prev为b,next为c;

c压栈时内容:prev为c,next为a。

c切换a,出栈之后呢?

prev为a,next为b

可以看到完全没有c的事了。我们必须得留下c存在过的痕迹。

故这里使用三个参数,其中两个prev,用来留下最新的prev的痕迹。

5. finish_task_switch与context_switch的联动

为什么一定要留下最新的prev的痕迹呢?你有没有想过?

finish_task_switch

的参数就是prev,就是因为它使用到了prev,所以才得留下它。

finsih_task_switch

其实涉及到的东西蛮多,计算vtime,perf追踪点等等。

但就在进程调度过程中,还有一个细节没有处理,还记得是什么吗,参见

finish_task_switch

的部分代码:

struct mm_struct *mm = rq->prev_mm;
​
......;
​
if (mm) {
    membarrier_mm_sync_core_before_usermode(mm);
    mmdrop(mm);
​
}      

如果从user到kernel,那么得给借用的

mm

增加一个计数;

但是什么时候减去呢?

一旦从kernel到user,

rq

就记录下prev使用的

active_mm

,在

finish_task_switch

中减去这个计数。

有道词典

static void __s ...

详细X

静态孔隙__sched notrace __schedule (bool抢占)