Skip to main content
 首页 » 操作系统

Linux 调度器之p->on_rq 和 se->on_rq 分析

2022年07月19日27linjiqin

基于MTK linux-4.14

看代码过程中发现 put_prev_entity() 中判断 prev 的 se->on_rq 为真还执行 enqueue 操作,感到疑惑,追踪一下代码进行分析。

1. 相关代码段

__schedule(bool preempt) 
{ 
    /* 非抢占且非running状态,表示 prev 任务自己因为休眠主动放弃cpu的 */ 
    if (!preempt && prev->state) { 
        deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); //只有非抢占状态下才有执行,若是被抢占的,没有执行 
            dequeue_entity() { 
                if (se != cfs_rq->curr) //此调用路径不成立,正在running的任务是已经dequeue的,在选prev时pick_next_task_fair()中已经dequeue过了。 
                    __dequeue_entity(cfs_rq, se); 
                se->on_rq = 0; //也就是说睡眠导致被切换的任务其 se->on_rq 才会被设置为0 ###### 
            } 
        prev->on_rq = 0; //也就是说睡眠导致被切换的任务其 task->on_rq 才会被设置为0 ###### 
    } 
    ... 
    next = pick_next_task(rq, prev, &rf); 
     
    /* 下面就直接 switch 了,没有on_rq相关内容了 */ 
    if (likely(prev != next)) { 
        ... 
        rq->curr = next; //switch 的前一时刻才对 rq->curr 进行更新 ###### 
        rq = context_switch(rq, prev, next, &rf); 
    } 
}

dequeue_entity 中将 se->on_rq = 0,其常规调用路径如下,而抢占切换路劲下是没有对prev任务调用 deactivate_task 的。

__schedule 
    deactivate_task 
        dequeue_task //调度类的这个回调 
            dequeue_task_fair //几乎是唯一调用路径 
                dequeue_entity 
                    se->on_rq = 0;

若是在CFS任务之间切换,pick_next_task 调用的就是 pick_next_task_fair:

static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) 
{ 
    struct sched_entity *curr = cfs_rq->curr; 
 
    /* 
     * 由于我们在没有执行 put_prev_entity() 的情况下到达这里(这个函数的下面才执行), 
     * 我们还需要考虑 cfs_rq->curr。 如果它仍然是一个runnable的实体,update_curr() 
     * 将更新它的 vruntime,否则忘记我们见过它。 
     */ 
    if (curr) { 
        if (curr->on_rq) //此调度实体还在cfs_rq队列上,此路径下,对应的是prev->se,由上可知若是被抢占的才为真。 
            update_curr(cfs_rq); 
        else 
            curr = NULL; 
        ... 
    } 
    ... 
    se = pick_next_entity(cfs_rq, curr); //只是选出来一个se,并没有执行任何dequeue的操作 
    p = task_of(se); 
     
    if (prev != p) { 
        struct sched_entity *pse = &prev->se; 
        ... 
        put_prev_entity(cfs_rq, pse); //这里面将cfs_rq->curr = NULL 
        set_next_entity(cfs_rq, se); //这里面将cfs_rq->curr = se 
    } 
    ... 
} 
 
//将prev任务放回队列 
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev) 
{ 
    if (prev->on_rq) //为真表示是被抢占的 
        update_curr(cfs_rq); //此路径下应该是和上面update_curr重复了。。。 
    ... 
    /* prev若是被抢占的,条件才成立。只有被抢占的,才需要执行挂回去的操作(若是sleep触发的切换就不用挂回去了)*/ 
    if (prev->on_rq) { //se是on_rq状态了还要 enqueue! 
        /* Put 'current' back into the tree. */ 
        __enqueue_entity(cfs_rq, prev); //将被抢占的任务重新放回cfs队列。放回去之后其on_rq状态与其实际就在cfs_rq上就匹配上了 
        ... 
    } 
    cfs_rq->curr = NULL; //更新cfs_rq->curr 
} 
 
//从cfs_rq中选出一个任务来运行 
static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) //fair.c 
{ 
    /* 'current' is not kept within the tree.  
     * 这个注释应该就是说这个判断是为了避免对curr重复dequeue。 
     */ 
    if (se->on_rq) { 
        ... 
        __dequeue_entity(cfs_rq, se); //虽然dequeue了,但是 se->on_rq 并没有清0 ###### 
    } 
    cfs_rq->curr = se; //更新cfs_rq->curr 
    ... 
}

2. se->on_rq分析结论

准确来说,当任务被执行 set_next_entity() 选出去运行,其 se->on_rq 就不能正确表示其在 cfs_rq 上挂载状态了,直到其由于休眠被执行 deactivate_task() 而被切走,或由于被抢占执行 put_prev_entity() 将其重新放回队列中,其 se->on_rq 状态才与其是否的确挂在 cfs_rq 上相吻合。可以理解为 se->on_rq 表示 running+runnable 的调度实体。


3. 对于任务的 p->on_rq,选出来作为 next 时默认是不为0的,因为在任务入队列的路径中都是有赋值的:

(1) 被唤醒

    try_to_wake_up //core.c 
        //p->on_rq == 0 的情况 
        ttwu_queue //core.c 
            ttwu_do_activate //core.c 
                ttwu_activate //core.c 
                    enqueue_task 
                    p->on_rq = TASK_ON_RQ_QUEUED; 
        //p->on_rq !=0 的情况 
        if (p->on_rq && ttwu_remote(p, wake_flags)) //ttwu_remote的唯一调用位置,作用主要是将p->state=TASK_RUNNING 
            goto stat; //只是统计一些信息就成功返回了

看 ttwu_remote() 函数的注释,它也是将 p->on_rq !=0 当做是被抢占的情况了.

(2) 迁移过来

attach_one_task //fair.c 
attach_tasks //fair.c 
    attach_task 
        activate_task 
            enqueue_entity 
        p->on_rq = TASK_ON_RQ_QUEUED;

(3) 被抢占而插入队列

(4) 还有一种情况,当任务被迁移走时,其 p->on_rq 也是不等于0的

active_load_balance_cpu_stop //fair.c 
    detach_one_task //fair.c 
load_balance //fair.c 
    detach_tasks //fair.c 
        detach_task 
            p->on_rq = TASK_ON_RQ_MIGRATING; 
            deactivate_task 
                dequeue_entity

4. p->on_rq分析结论

对于 p->on_rq,若是由于睡眠被切走的才为0,其它情况下都不为0。应该是主要用来判断其是否是由于睡眠而被切走的,只有在睡眠状态下为0。一部分内核代码中通过判断 p->on_rq 不等于0来选择 runnable 的任务。也有一部分代码和 p->state 配合使用来断言特定状态下的任务,例如:

void set_task_cpu(struct task_struct *p, unsigned int new_cpu) //kernel/sched/core.c 
{ 
    /* 
     * We should never call set_task_cpu() on a blocked task, 
     * ttwu() will sort out the placement. 
     */ 
    WARN_ON_ONCE(p->state != TASK_RUNNING && p->state != TASK_WAKING && !p->on_rq); 
    ... 
}

5. 若是没有使能 CONFIG_FAIR_GROUP_SCHED,那么 cfs_rq->curr 基本上等同于 rq->curr。

6. 在 scheduler_tick() 中debug当前正在执行的线程的on_rq状态如下:

//绝大多数情况下current不为idle时,curr->se.on_rq=1 是为1的 
  video_decod-16367   [006] d.h.   846.624385: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=1, curr_u_max_active=1, curr_u_min_active=1 
//若current是idle的话,curr->se.on_rq就是0 
       <idle>-0       [005] dNh1   846.628360: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=0, curr_u_max_active=0, curr_u_min_active=0 
       <idle>-0       [004] d.h1   846.684357: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=0, curr_u_max_active=0, curr_u_min_active=0 
//但是也有少部分,即使current不为idle,curr->se.on_rq也为0 
TimerDispatch-1055    [000] d.h.   859.216354: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=0, curr_u_max_active=1, curr_u_min_active=1 
        <...>-5757    [001] d.h.   860.568374: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=0, curr_u_max_active=1, curr_u_min_active=1 
mali-cpu-comman-1069  [005] d.h2   860.888526: scheduler_tick: curr->on_cpu=1, curr->on_rq=1, curr->se.on_rq=0, curr_u_max_active=1, curr_u_min_active=1

TODO: current不为idle,且 curr->se.on_rq 为0的,是不是刚迁移过来的?

7. 实测验证:任务被选出来 running 的时候是没有从等待队列 dequeue 出来的,sleep 的时候才会从等待队列中 dequeue 出来。即使被抢占而进入 runnable 状态也是没有从等待队列中 dequeue 出来的。


本文参考链接:https://www.cnblogs.com/hellokitty2/p/15459829.html