Linux RT调度器之任务选核

调度器在任务创建、任务唤醒以及任务执行exec调用时都会为任务重新选择运行的cpu(负载均衡触发的选核不在这里讨论)。调度器框架将选核策略交给具体的调度类实现,框架只实现了一个兜底的策略,在调度类没有选出合适的cpu时由兜底策略为任务选择一个运行cpu。

这篇笔记分析了RT调度类是如何为RT任务完成选核的。代码使用的是Linux 5.10。

选核策略

调度框架在为RT任务选核时,会调用RT调度类的select_task_rq_rt()回调函数。

static int
select_task_rq_rt(struct task_struct *p, int cpu, int sd_flag, int flags)
{
    struct task_struct *curr;
    struct rq *rq;
    bool test;

    // 只处理任务唤醒和任务创建时的选核,其它场景直接返回cpu
    if (sd_flag != SD_BALANCE_WAKE && sd_flag != SD_BALANCE_FORK)
        goto out;

    rq = cpu_rq(cpu); // 任务p当前所在cpu运行队列

    rcu_read_lock();
    curr = READ_ONCE(rq->curr); // rq上正在运行的任务

    // 检查任务p是否可以抢占curr任务。test为true表示不可以抢占
    test = curr &&
        unlikely(rt_task(curr)) &&
        (curr->nr_cpus_allowed < 2 || curr->prio <= p->prio);

    // 任务p不可以抢占curr,或者当前cpu的能力无法满足任务要求时,
    // 尝试为任务p从其它cpu找寻找更合适的
    if (test || !rt_task_fits_capacity(p, cpu)) {
        // 寻找优先级最低的cpu作为target cpu
        int target = find_lowest_rq(p);
    
        // 当前cpu能力不满足任务需求时,要求target cpu的能力必须满足任务要求
        if (!test && target != -1 && !rt_task_fits_capacity(p, target))
            goto out_unlock;
    
        // 要求任务切换到target_cpu上可以立马抢占该cpu上正在运行的任务
        if (target != -1 &&
            p->prio < cpu_rq(target)->rt.highest_prio.curr)
            cpu = target;
    }
out_unlock:
    rcu_read_unlock();
out:
    return cpu;
}

可以看出RT调度类的选核原则:尽可能为任务p选择合适的cpu,如果找不到,就返回参数cpu,该cpu通常就是任务上一次运行时的cpu。

select_task_rq_rt()函数选核逻辑如下:

  1. 检查任务p是否可以就在当前cpu上,如果可以,选核结束;否则执行2。
  2. 从可用cpu列表中找到优先级最低的cpu作为target cpu,可用cpu列表的选择考虑了cache效率以及任务的亲和性设置。
  3. 如果target cpu上正在运行的任务优先级低于任务p的优先级,说明任务p放到该cpu上后可以立刻抢占运行,那么选择该target cpu。否则选核结束,依然返回参数cpu。

步骤1中任务p可以在当前cpu上需要同时满足下面两个条件:

  • 当前cpu上正在运行的任务记作curr,那么curr的优先级要低于任务p。如果curr是RT任务,它还必须可以在其它cpu上运行。
  • 当前cpu的能力可以满足任务p的利用率要求。

步骤2中的查找优先级最低的cpu涉及cpu优先级的概念,见下面单独分析。

任务能力匹配检查

如上,选核时要求cpu的能力能够满足任务的需求,这是通过rt_task_fits_capacity()函数实现的。

#ifdef CONFIG_UCLAMP_TASK
static inline bool rt_task_fits_capacity(struct task_struct *p, int cpu)
{
    unsigned int min_cap;
    unsigned int max_cap;
    unsigned int cpu_cap;

    // 同构系统所有cpu的能力都相同,忽略该检查
    if (!static_branch_unlikely(&sched_asym_cpucapacity))
        return true;

    // 任务p通过uclamp配置的最小和最大利用率
    min_cap = uclamp_eff_value(p, UCLAMP_MIN);
    max_cap = uclamp_eff_value(p, UCLAMP_MAX);

    // cpu的能力(最高运行频率)
    cpu_cap = capacity_orig_of(cpu);

    // cpu的能力超过任务的最小利用率就任务cpu能力是满足任务要求的
    return cpu_cap >= min(min_cap, max_cap);
}
#else
static inline bool rt_task_fits_capacity(struct task_struct *p, int cpu)
{
    return true;
}
#endif

可见,RT调度类仅仅是在开启uclamp,并且是异构系统时才会进行能力匹配检查,检查也是简单的检查cpu的最大能力是否满足任务的最小使用要求,并未考虑cpu的能力是否会被其它任务占用(如中断)。

查找target cpu

如上,选核时如果当前cpu不合适时,RT调度类会调用find_lowest_rq()函数为任务从其它cpu中找一个最合适的作为target cpu。

// 临时变量,用于保存优先级最低的cpu掩码
static DEFINE_PER_CPU(cpumask_var_t, local_cpu_mask);

static int find_lowest_rq(struct task_struct *task)
{
    struct sched_domain *sd;
    struct cpumask *lowest_mask = this_cpu_cpumask_var_ptr(local_cpu_mask);
    int this_cpu = smp_processor_id();
    int cpu      = task_cpu(task);
    int ret;

    // init_sched_rt_class()中会分配内存
    if (unlikely(!lowest_mask))
        return -1;

    // 任务p绑定到了一个核上,交给调度框架都兜底策略去选择
    if (task->nr_cpus_allowed == 1)
        return -1; /* No other targets possible */

    // 查找优先级最低的cpu,保存到lowest_mask中
    // 异构系统会额外要求cpu的能力要满足任务的要求
    if (static_branch_unlikely(&sched_asym_cpucapacity)) {
        ret = cpupri_find_fitness(&task_rq(task)->rd->cpupri,
            task, lowest_mask, rt_task_fits_capacity);
    } else {
        ret = cpupri_find(&task_rq(task)->rd->cpupri, task, lowest_mask);
    }
    // 没有合适的cpu,选核失败
    if (!ret)
        return -1; /* No targets found */
    // 经过上面的选择,lowest_mask中可能有多个cpu,下面的策略是从中选出一个作为target cpu

    // 任务当前所在cpu就在候选列表中,优先选择它
    if (cpumask_test_cpu(cpu, lowest_mask))
        return cpu;

    // 正在执行该选核流程的cpu如果不在候选列表中,将this_cpu设为-1后面的流程就不会选中它
    if (!cpumask_test_cpu(this_cpu, lowest_mask))
        this_cpu = -1; /* Skip this_cpu opt if not among lowest */

    // 从base->top的顺序遍历调度域,选择候选cpu列表和调度域cpu有交集的cpu
    rcu_read_lock();
    for_each_domain(cpu, sd) {
        if (sd->flags & SD_WAKE_AFFINE) {
            int best_cpu;
            // 优先原则执行该选核流程的cpu
            if (this_cpu != -1 &&
                cpumask_test_cpu(this_cpu, sched_domain_span(sd))) {
                rcu_read_unlock();
                return this_cpu;
            }
            // 选择候选cpu列表和调度域cpu交集中的第一个cpu
            best_cpu = cpumask_first_and(lowest_mask, sched_domain_span(sd));
            if (best_cpu < nr_cpu_ids) {
                rcu_read_unlock();
                return best_cpu;
            }
        }
    }
    rcu_read_unlock();

    // 上述策略都没有选中,但是执行该选核流程的cpu在候选列表中,就选择它了
    if (this_cpu != -1)
        return this_cpu;

    // 还是没有找到,从候选列表中随便选择一个
    cpu = cpumask_any(lowest_mask);
    if (cpu < nr_cpu_ids)
        return cpu;

    return -1;
}

find_lowest_cpu()的核心逻辑是先找到优先级最低且满足任务要求的cpu集合作为候选cpu列表(lowest_cpu),然后从候选cpu列表中按照如下策略选择一个target cpu:

  • 优先选择任务当前所在cpu,这样任务不需要迁核,cache很可能还是有效的。
  • 其次选择执行该选核流程的cpu,这样无需打扰其它cpu,抢占成本更低些。
  • 再其次从调度域中选择一个最底层的cpu,该策略还是考虑调度域层级越低,cache依然有效的可能性越大。
  • 最后,所有策略都失效了,从执行该选核流程的cpu和候选cpu列表中随便选择一个。

cpu优先级

RT调度类的特点就是保证绝对的优先级调度,即假设系统有N个cpu,那么总是保证优先级TopN的任务可以占用cpu资源。

为了实现上述目标,引入了cpu优先级的概念,cpu优先级由cpu上正在运行和正在等待运行的任务的优先级决定。这样每当RT任务选核时,直接去占用优先级最低的cpu即可。

cpu优先级的管理

cpu的优先级信息保存在root_domain中,在root_domain初始化时调用cpupri_init()函数完成cpu优先级相关的初始化。

struct root_domain {
...
    struct cpupri cpupri;
}

#define CPUPRI_NR_PRIORITIES    (MAX_RT_PRIO + 2) // 102

struct cpupri_vec {
    atomic_t count; // mask的weight
    cpumask_var_t mask; // 该优先级的cpu掩码
};

struct cpupri {
    struct cpupri_vec pri_to_cpu[CPUPRI_NR_PRIORITIES];
    // 初始化时分配内存,长度为nr_cpu_ids,初始值为-1,
    // 记录了每个cpu的优先级
    int *cpu_to_pri;
};
  • cp->pri_to_cpu向量表长度为102,分别对应102个cpu优先级等级,记录了每一级优先级上的cpu信息。任务优先级和cpu优先级有一个固定的映射关系,cpu的优先级由cpu上优先级最高的任务的优先级按照如下方式映射而来。
#define CPUPRI_INVALID        -1
#define CPUPRI_IDLE         0
#define CPUPRI_NORMAL         1

// 将140个等级的任务优先级task->prio映射为102个等级的cpu优先级
static int convert_prio(int prio)
{
    int cpupri;

    if (prio == CPUPRI_INVALID) // -1 -> -1
        cpupri = CPUPRI_INVALID;
    else if (prio == MAX_PRIO) // IDLE任务:140 -> 0
        cpupri = CPUPRI_IDLE;
    else if (prio >= MAX_RT_PRIO) // CFS任务:[100, 139] -> 1
        cpupri = CPUPRI_NORMAL;
    else // RT任务:[0, 99] -> [2, 101]
        cpupri = MAX_RT_PRIO - prio + 1;

    return cpupri;
}
  • cp->cpu_to_prio数组记录了每个cpu的优先级。

更新cpu优先级

从cpu优先级和任务优先级的转换关系中可以看出,cpu优先级主要取决于cpu运行队列中的RT任务,如果只有普通任务或者IDLE任务,cpu的优先级是固定值。所以只要在RT任务入队和出队时检查变化的任务是否影响了队列中任务的最高优先级,如果影响了,用新最高优先级调用cpupri_set()函数更新cpu优先级即可。

void cpupri_set(struct cpupri *cp, int cpu, int newpri)
{
    int *currpri = &cp->cpu_to_pri[cpu];
    int oldpri = *currpri; // 当前cpu优先级
    int do_mb = 0;

    newpri = convert_prio(newpri); // 将新的优先级转换为cpu优先级等级
    BUG_ON(newpri >= CPUPRI_NR_PRIORITIES);

    if (newpri == oldpri) // 优先级未发生变化
        return;

    // 将cpu信息记录到newpri对应的cp->pri_to_cpu向量中
    if (likely(newpri != CPUPRI_INVALID)) {
        struct cpupri_vec *vec = &cp->pri_to_cpu[newpri];
        
        cpumask_set_cpu(cpu, vec->mask);

        smp_mb__before_atomic();
        atomic_inc(&(vec)->count);
        do_mb = 1;
    }

    // 将cpu信息从oldpri对应的cp->pri_to_cpu向量中删除
    if (likely(oldpri != CPUPRI_INVALID)) {
        struct cpupri_vec *vec  = &cp->pri_to_cpu[oldpri];
    
        if (do_mb)
            smp_mb__after_atomic();

        atomic_dec(&(vec)->count);
        smp_mb__after_atomic();
        cpumask_clear_cpu(cpu, vec->mask);
    }

    *currpri = newpri; // 更新cpu_to_pri数组
}

查找优先级最低的cpu集合

有两个版本的接口可以用来为任务查询系统中优先级最低的cpu集合。

  • cpupri_find():不考虑cpu能力能否满足任务要求,从优先级低于任务优先级的cpu中找到最小的cpu集合。
  • cpupri_find_fitness():在cpupri_find()的基础上额外要求cpu能力必须满足任务要求。

cpupri_find()也是调用cpupri_find_fitness(),只是传入的fitness_fn参数为NULL。

int cpupri_find_fitness(struct cpupri *cp, struct task_struct *p,
        struct cpumask *lowest_mask,
        bool (*fitness_fn)(struct task_struct *p, int cpu))
{
    int task_pri = convert_prio(p->prio);
    int idx, cpu;

    BUG_ON(task_pri >= CPUPRI_NR_PRIORITIES);

    // 按照优先级由低到高的顺序遍历各优先级等级,第一个匹配到符号条件
    // 的cpu集合一定时优先级最低的。这里遍历到任务对应的优先级即可,
    // 因为任务也无法抢占比自己优先级更高的cpu上的任务,没有继续查找的必要
    for (idx = 0; idx < task_pri; idx++) {
        // 查找cp->pri_to_cpu[idx]中是否有符合条件的cpu,没有则继续遍历
        if (!__cpupri_find(cp, p, lowest_mask, idx))
            continue;
        // 如果无需检查能力是否匹配,则查找过程结束
        if (!lowest_mask || !fitness_fn)
            return 1;
    
        // 剔除掉那些能力不符合要求的cpu
        for_each_cpu(cpu, lowest_mask) {
            if (!fitness_fn(p, cpu))
                cpumask_clear_cpu(cpu, lowest_mask);
        }
    
        // 提出完后已经没有可用cpu了,继续查找
        if (cpumask_empty(lowest_mask))
            continue;
        // 查找成功
        return 1;
    }

    // 查找失败时,尝试去掉能力检查条件后重新查找一遍
    if (fitness_fn)
        return cpupri_find(cp, p, lowest_mask);

    return 0; // 返回0表示查找失败
}

// 查找指定优先级等级中是否有符合条件的cpu
static inline int __cpupri_find(struct cpupri *cp, struct task_struct *p,
                struct cpumask *lowest_mask, int idx)
{
    struct cpupri_vec *vec  = &cp->pri_to_cpu[idx];
    int skip = 0;

    // 没有处于该优先级的cpu,跳过
    if (!atomic_read(&(vec)->count))
        skip = 1;

    smp_rmb();

    /* Need to do the rmb for every iteration */
    if (skip)
        return 0;

    // 不满足任务的亲和性设置,跳过
    if (cpumask_any_and(p->cpus_ptr, vec->mask) >= nr_cpu_ids)
        return 0;

    if (lowest_mask) {
        // 和任务的亲和性取交集后保存在lowest_mask中
        cpumask_and(lowest_mask, p->cpus_ptr, vec->mask);
        // 再次检查是防止可能出现的竞争条件导致vec->mask在两次读取期间发生变化
        if (cpumask_empty(lowest_mask))
            return 0;
    }
    return 1; // 查找成功
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值