Linux调度中的任务优先级机制

发布时间 2023-10-12 18:59:30作者: ZouTaooo

前言

在阅读源码的过程中发现一个task_struct包含四个优先级相关的成员,priostatic_prionormal_priort_priority这几个优先级值有什么区别和联系呢?

struct task_struct {
    int				prio;
    int				static_prio;
    int				normal_prio;
    unsigned int    rt_priority;
}

内核中的优先级范围

+----+-------------------------+---------------+
| -1 |0                      99|100         139|   
+----+-------------------------+---------------+

内核中管理多任务的调度类有三个,分别是dl_sched_classrt_sched_classfair_sched_class。其中-1优先级用于所有的dl-task[0,99]rt-task的优先级,[100,139]normal-task的优先级。在内核中值越低其优先级越高。

static_prio

在内核中,static_prio用于normal-task,这个值与nice存在如下转化关系:static_prio = nice + 120。可以看到static_prionice值是绑定的。
内核中设置一个进程的优先级的入口是系统调用sys_setpriority,这个系统调用可以设置用户、进程组、进程的优先级。当设置某个进程的nice值时会进入到set_user_nice

void set_user_nice(struct task_struct *p, long nice)
{
    /* rt or deadline task */
    if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
        p->static_prio = NICE_TO_PRIO(nice);
        goto out_unlock;
    }
    /* cfs task */
    p->static_prio = NICE_TO_PRIO(nice);
    set_load_weight(p, true);
    p->prio = effective_prio(p);
}

如果设置一个deadline任务或者rt-taskstatic_prio时,该操作是允许的,但是并不会产生实质影响,因为rt_sched_classdl_sched_class并不看这个值。对于normal-task来说set_user_nice除了更新static_prio以外,set_load_weight还会更新normal-task以及cfs_rq的权重信息。

CFS中有一类特别的任务,这些任务的调度策略为SCHED_IDLE,这些任务的权重值与优先级无关,因此对于使用该策略的task修改其nice值没有意义。

static void set_load_weight(struct task_struct *p, bool update_load)
{
    int prio = p->static_prio - MAX_RT_PRIO;
    struct load_weight *load = &p->se.load;
    /* idle policy */
    if (idle_policy(p->policy)) {
        load->weight = scale_load(WEIGHT_IDLEPRIO);
        load->inv_weight = WMULT_IDLEPRIO;
        return;
    }
    /* normal cfs task */
    reweight_task(p, prio);
}

以上只是static_prio产生变化的一种场景,一个任务的生命周期内static_prio的变化的情况有两种:

  • 系统调用fork:在新任务初始化时,sched_fork会重置或者继承。
  • 系统调用sys_setpriority中,调用set_user_nice更新。

NOTE:系统调用sched_setscheduler支持修改任务的调度类的同时可以设置优先级,但是该参数是实时调度器使用的优先级,对于切换到CFS的场景,此时更新权重信息使用的是任务自身的static_prio。这样来看sys_setpriority是修改static_prio的唯一接口。

rt_priority

rt_priority是实时调度器使用的优先级,在设置一个rt-taskrt_priority时,输入值的有效范围为[0,99],与nice值不同的是rt_priority越大其rt-task的优先级就越大,但是内核为了让语义统一在使用时做了转化。real-prio = MAX_RT_PRIO - 1 - rt_priority = 99 - rt_priority,优先级越大其计算出的值越小。但是在外部使用时可以按照正常逻辑来设置优先级。

normal_prio

normal_prio的值与当前所属的调度策略有关,dl-tasknormal_prio固定-1rt-tasknormal_prio99 - rt_prioritynormal-tasknormal_priostatic_prionormal_prio实现了static_prio值和rt_priority优先级的统一,normal_prio值越小其优先级越高。

prio

prio是做调度决策时真正使用的优先级,在正常情况下与normal_prio一致。但是在一些特殊场景下需要在不修改static_priort_priority的情况下临时提高prio

典型的就是优先级反转问题,在以下两个场景需要临时提高prio

  • rt-task持有mutex A,另一个dl-task阻塞在mutex A上。
  • rt-task持有mutex A,另一个rt-task阻塞在mutex A上,并且该任务可以抢占当前运行中的进程。

在这里持锁的task称作p,阻塞的task称作pi。如果不做处理,某个中间优先级的task有可能会抢占p执行,导致因为锁阻塞的pi任务的实时性得不到保障。解决方法就是通过优先级继承让p持锁过程中priopiprio保持一致,让p能够尽快执行结束后释放mutex

总结

四类优先级的范围与用途一览:

类型 范围 用途 设置方法
static_prio [100,139] normal-task优先级 通过set_priority系统调用设置nice值转化为static_prio,将nice值从[-20,19]映射到[100,139]nice值越高优先级越低
rt_priority [0,99] rt-task优先级 通过sched_setscheduler系统调用设置, 输入范围为[0,99],值越高优先级越高
normal_prio [-1,139] 优先级的语义统一 normal_prio与调度类对应,fair_sched_class时与static_prio一致,rt_sched_class时将rt_priority[0,99]映射到[99,0]dl_sched_class时固定-1
prio [-1,139] 决策时真正使用的优先级 一般与normal_prio一致,特殊情况可以临时提高优先级

从优先级机制的设计上来看,真正的优先级prio会按顺序考虑优先级临时提高、调度类、调度类对应的优先级设置。临时提高的特殊情况下可以不考虑任务当前的调度类与优先级参数设置。普通情况下使用normal_prio,而normal_prio会根据当前的调度类和参数设置进行调整。

另外在API上,设置优先级的接口有两个:

  • set_priority可以改变static_prio的值,输入参数为nice
  • sched_setscheduler可以设置调度器以及rt_priority(如果调度类为rt_sched_class)。输入参数为rt_priority的值以及调度策略policy(可找到对应的调度器)。

相关的linux命令有:

  • renice可以调整normal-tasknice值。
  • chrt可以设置调度策略,如果是rt-task可以设置rt_priority
  • cat /proc/{pid}/sched 可以查看priopolicy