Skip to main content
 首页 » 操作系统

Linux 调度器之CFS的buddy

2022年07月19日151txw1958

一、buddy简介

buddy 是 cfs_rq 中的三个 sched_entity,在cfs线程间抢占,线程主动放弃cpu,对某些线程进行特殊照顾扮演重要角色。

1. buddy 成员位置

//fair.c 
struct cfs_rq { 
    ... 
    struct sched_entity    *next; //和last差不多,只不过优先级没有next高 
    struct sched_entity    *last; //优先调度 
    struct sched_entity    *skip; //用于跳过一些任务 
    ... 
};

2. buddy 相关函数

static void set_next_buddy(struct sched_entity *se) 
{ 
    ... 
    cfs_rq_of(se)->next = se; 
    ... 
} 
static void set_last_buddy(struct sched_entity *se) 
{ 
    ... 
    cfs_rq_of(se)->last = se; 
    ... 
} 
static void set_skip_buddy(struct sched_entity *se) 
{ 
    ... 
    cfs_rq_of(se)->skip = se; 
    ... 
} 
 
static void clear_buddies(struct cfs_rq *cfs_rq, struct sched_entity *se) 
{ 
    ... 
    if (cfs_rq->last == se) 
        cfs_rq_of(se)->last = NULL; 
    if (cfs_rq->next == se) 
        cfs_rq_of(se)->next = NULL; 
    if (cfs_rq->skip == se) 
        cfs_rq_of(se)->skip = NULL; 
    ... 
}

clear_buddies是是所有buddy共用的,相当于只清理自己这一个调度实体,其调用流程:

 可以看出这些buddy存在的生命周期。

二、next buddy分析

1. 设置位置

static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags) //fair.c 
{ 
    struct cfs_rq *cfs_rq; 
    struct sched_entity *se = &p->se; 
    int task_sleep = flags & DEQUEUE_SLEEP; 
    se = parent_entity(se); //没有使能组调度,这里返回NULL 
    ... 
    /*若没有开启对CFS的带宽控制的话,传参flags中有 DEQUEUE_SLEEP 标志就会设置,若没有使能CFS组调度,恒不会设置*/ 
    if (task_sleep && se && !throttled_hierarchy(cfs_rq)) 
        set_next_buddy(se); 
    ... 
}

(1) dequeue_task_fair

DEQUEUE_SLEEP传参时机:

a. __schedule --> deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK) --> dequeue_task //正常进程切换的时候,flag中会包含DEQUEUE_SLEEP,任务切换时偏向于将由于sleep而休眠退出的任务设置到next buddy上。

b. throttle_cfs_rq --> dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP); //throttle cfs时也会传DEQUEUE_SLEEP

c. dequeue_task_fair --> flags |= DEQUEUE_SLEEP //若是使能了组调度,一个任务组中只有第一个dequeue的任务可能不传DEQUEUE_SLEEP标志,其它的都会传这个标志,单只可能对第一个任务设置next buddy.

dequeue_task_fair调用路径:

 

(2) check_preempt_wakeup

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags) //fair.c 
{ 
    struct sched_entity *se = &curr->se, *pse = &p->se; 
    struct cfs_rq *cfs_rq = task_cfs_rq(curr); //因为有组调度,这个和rq->cfs还不一定是同一个 
    int scale = cfs_rq->nr_running >= sched_nr_latency; //8个runnable的线程 
    int next_buddy_marked = 0; 
 
    ... 
    /* 如果开启了 NEXT_BUDDY feature,且此cfs_rq上有超过8个线程待运行,且不是fork线程后进行唤醒的,则将新换新的进程设置到next buddy 
    fork系统调用 -> _do_fork -> wake_up_new_task -> check_preempt_curr(rq, p, WF_FORK); 
    */ 
    if (sched_feat(NEXT_BUDDY) && scale && !(wake_flags & WF_FORK)) { 
        set_next_buddy(pse); 
        next_buddy_marked = 1; 
    } 
    ... 
  /*当前任务的虚拟时间减去任务p的虚拟时间大于抢占阈值,就将任务p设置为next_buddy,以便在 pick_next_entity 时抢占当前线程*/ 
    if (wakeup_preempt_entity(se, pse) == 1) { //1.当前进程的虚拟时间比新进程多运行2ms在新进程上对应的虚拟时间 
        /* Bias pick_next to pick the sched entity that is triggering this preemption.*/ 
        //和前面的设置是互斥的 
        if (!next_buddy_marked) 
            set_next_buddy(pse); 
        goto preempt; //2.触发抢占 
    } 
    ... 
preempt: 
    /* 设置 TIF_NEED_RESCHED 标志,在下一个抢占点到来时进行抢占 */ 
    resched_curr(rq); 
 
    ... 
 
    //LAST_BUDDY默认设置的 
    //函数的最后,此cfs_rq上有大于8个runnable的任务且是任务(非任务组),要被强占的当前任务被设置为 last_buffy 
    if (sched_feat(LAST_BUDDY) && scale && entity_is_task(se)) 
        set_last_buddy(se); 
}

check_preempt_wakeup函数是next buddy主要作用的位置,注意,第一个位置判断了 sched_feat(NEXT_BUDDY),第二个使用位置没有判断,第一个位置可以通过关闭feature关掉(一般是关掉的),第二个位置关不掉。注:NEXT_BUDDY 默认是disable的,LAST_BUDDY 默认是使能的。

check_preempt_wakeup的调用路径:

 

(3) yield_to_task_fair

static bool yield_to_task_fair(struct rq *rq, struct task_struct *p, bool preempt) 
{ 
    struct sched_entity *se = &p->se; 
    set_next_buddy(se); 
    ... 
}

yield_to_task_fair的调用路径:

注:yield_task 和 yield_to_task 不是同一个,它两个都是 fair_sched_class 调度类的两个回调函数,但是前者将当前task设置为cfs_rq->skip,后者将p设置到cfs_rq->next.

2. 使用位置

(1) pick_next_entity

static struct sched_entity * pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr) 
{ 
    struct sched_entity *left = __pick_first_entity(cfs_rq); 
    struct sched_entity *se; 
 
    /*若cfs_rq上没有ready的任务,或则curr的虚拟时间比rbtree最左侧的还小*/ 
    if (!left || (curr && entity_before(curr, left))) 
        left = curr; 
 
    se = left; /* ideally we run the leftmost entity */ 
 
    /* Avoid running the skip buddy, if running something else can be done without getting too unfair. */ 
    /* 如果有其它可运行的,就避免运行这个skip buddy */ 
    if (cfs_rq->skip == se) { 
        struct sched_entity *second; 
        if (se == curr) { 
            second = __pick_first_entity(cfs_rq); //重新选一个来运行 
        } else { 
            second = __pick_next_entity(se); // se是最左侧的,这里选第二左的 
            /* 如果虚拟时间第二小的不存在 或 curr比选出来的这个虚拟时间还小,那么继续选curr */ 
            if (!second || (curr && entity_before(curr, second))) 
                second = curr; 
        } 
 
        /* 只有当选出来的second的虚拟时间比left还小2ms对应的虚拟时间以上,才取second*/ 
        if (second && wakeup_preempt_entity(second, left) < 1) 
            se = second; 
    } 
 
    /* 
     * Prefer last buddy, try to return the CPU to a preempted task. 
     */ 
    /* 若是cfs_rq->last也比left的虚拟时间也小这么多,优先选cfs_rq->last。也就是看能否选被强占的当前任务 */ 
    if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) 
        se = cfs_rq->last; 
 
    /* 
     * Someone really wants this to run. If it's not unfair, run it. 
     */ 
    /* 若是next的虚拟时间比rbtree最左侧的的任务还小,那么就选next,这个是最高优先级。也就是看能否选抢占当前任务的任务运行 */ 
    if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) 
        se = cfs_rq->next;  //这里选择了next_buddy 
 
    /*将此se从cfs_rq中清理掉,无论是否选择它,都将其清理掉,因此一次设置只起一次作用*/ 
    clear_buddies(cfs_rq, se); 
 
    return se; 
}

注:在 pick_next_entity 中会先使用,这里 next_buddy 比 last_buddy 的优先级高。然后 clear_buddies,也就是说这里的 yield_task_fair 只能选中它时放弃一次cpu,下次再选中它就正常执行了。

pick_next_entity 的调用路径:

 (2) task_hot

static int task_hot(struct task_struct *p, struct lb_env *env) 
{ 
    s64 delta; 
    ... 
    /* 
     * Buddy candidates are cache hot: 
     */ 
    /* CACHE_HOT_BUDDY默认为true */ 
    if (sched_feat(CACHE_HOT_BUDDY) && env->dst_rq->nr_running && 
            (&p->se == cfs_rq_of(&p->se)->next || &p->se == cfs_rq_of(&p->se)->last)) 
        return 1; 
 
    //默认0.5ms 
    if (sysctl_sched_migration_cost == -1) 
        return 1; 
    if (sysctl_sched_migration_cost == 0) 
        return 0; 
 
    //上次开始运行到现在的时间差值小于0.5ms就认为还是task_hot的,返回真 
    delta = rq_clock_task(env->src_rq) - p->se.exec_start; 
    //若delta小于,那就返回1,否则返回0 
    return delta < (s64)sysctl_sched_migration_cost; 
}

task_hot 的调用路径:

迁移时,尽量跳过 task_hot 的任务,也就是会尽量跳过设置在 cfs_rq->next 和 cfs_rq->last 的任务。

三、last buddy

1. 设置位置

(1) check_preempt_wakeup

见上面对next buddy的分析

2. 使用位置

(1) pick_next_entity

见上面对next buddy的分析

(2) task_hot

见上面对next buddy的分析

注:last buddy 和next buddy作用类似,只是优先级没有next buddy高。

四、skip buddy

1. 设置位置

(1) yield_task_fair

static void yield_task_fair(struct rq *rq) 
{ 
    struct sched_entity *se = &curr->se; 
    ... 
    set_skip_buddy(se); 
}

yield_task_fair 的调用流程:

 2. 使用位置

(1) pick_next_entity

见上面对next buddy的分析

注:Linux5.4




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