Linux内核深度解析
上QQ阅读APP看书,第一时间看更新

2.8.8 带宽管理

本节介绍各种调度类管理进程占用的处理器带宽的方法。

1.限期调度类的带宽管理

每个限期进程有自己的带宽,不需要更高层次的带宽管理。

目前,内核把限期进程的运行时间统计到根实时任务组的运行时间里面了,限期进程共享实时进程的带宽。

    kernel/sched/deadline.c
    static void update_curr_dl(struct rq *rq)
    {
          …
          if (rt_bandwidth_enabled()) {
                struct rt_rq *rt_rq = &rq->rt;
                raw_spin_lock(&rt_rq->rt_runtime_lock);
                if (sched_rt_bandwidth_account(rt_rq))
                      rt_rq->rt_time += delta_exec;
                raw_spin_unlock(&rt_rq->rt_runtime_lock);
          }
    }

2.实时调度类的带宽管理

指定实时进程的带宽有以下两种方式。

(1)指定全局带宽:带宽包含的两个参数是周期和运行时间,即指定在每个周期内所有实时进程的运行时间总和。

默认的周期是1秒,默认的运行时间是0.95秒。可以借助文件“/proc/sys/kernel/sched_rt_period_us”设置周期,借助文件“/proc/sys/kernel/sched_rt_runtime_us”设置运行时间。

如果打开了配置宏CONFIG_RT_GROUP_SCHED,即支持实时任务组,那么全局带宽指定了所有实时任务组的总带宽。

(2)指定每个实时任务组的带宽:在每个指定的周期,允许一个实时任务组最多执行长时间。当实时任务组在一个周期用完了带宽时,这个任务组将会被节流,不允许继续运行,直到下一个周期。可以使用cgroup设置一个实时任务组的周期和运行时间,cgroup版本1的配置方法如下。

1)cpu.rt_period_us:周期,默认值是1秒。

2)cpu.rt_runtime_us:运行时间,默认值是0,把运行时间设置为非零值以后才允许把实时进程加入任务组,设置为−1表示没有带宽限制。


cgroup版本1的配置示例如下。

1)挂载cgroup文件系统,把CPU控制器关联到控制组层级树。

    mount -t cgroup -o cpu none /sys/fs/cgroup/cpu

2)创建一个任务组。

    cd /sys/fs/cgroup/cpu
    mkdir browser   # 创建"browser"任务组

3)把实时运行时间设置为10毫秒。

    echo 10000 > browser/cpu.rt_runtime_us

4)把一个实时进程加入任务组。

    echo <pid> > browser/cgroup.procs

cgroup版本2从内核4.15版本开始支持CPU控制器,暂时不支持实时进程。


一个处理器用完了实时运行时间,可以从其他处理器借用实时运行时间,称为实时运行时间共享,对应调度特性RT_RUNTIME_SHARE,默认开启。

    kernel/sched/features.h
    SCHED_FEAT(RT_RUNTIME_SHARE, true)

实时任务组的带宽存放在结构体task_group的成员rt_bandwidth中:

    kernel/sched/sched.h
    struct task_group {
          …
    #ifdef CONFIG_RT_GROUP_SCHED
          …
          struct rt_bandwidth rt_bandwidth;
    #endif
          …
    };

(1)节流。如图2.33所示,以下4种情况中,进程调度器调用函数update_curr_rt以更新当前进程的运行时间统计,然后检查实时进程的运行时间是否超过带宽限制。

图2.33 实时进程的带宽管理

1)dequeue_task_rt:把实时进程从运行队列中删除。

2)pick_next_task_rt:选择下一个实时进程。

3)put_prev_task_rt:把正在运行的实时进程放回运行队列。

4)task_tick_rt:周期调度。

函数update_curr_rt的主要代码如下:

    kernel/sched/rt.c
    1   static void update_curr_rt(struct rq *rq)
    2   {
    3    …
    4    if (! rt_bandwidth_enabled())
    5         return;
    6
    7    for_each_sched_rt_entity(rt_se) {
    8         struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
    9
    10        if (sched_rt_runtime(rt_rq) ! = RUNTIME_INF) {
    11              raw_spin_lock(&rt_rq->rt_runtime_lock);
    12              rt_rq->rt_time += delta_exec;
    13              if (sched_rt_runtime_exceeded(rt_rq))
    14                   resched_curr(rq);
    15              raw_spin_unlock(&rt_rq->rt_runtime_lock);
    16        }
    17   }
    18  }

第7行代码,从当前进程所属的任务组向上到根任务组,执行以下操作。

1)第12行代码,把任务组的运行时间加上增量。

2)第13行代码,调用函数sched_rt_runtime_exceeded以检查运行时间是否超过限额,处理如下。

❑ 如果运行时间超过限额,并且开启了实时运行时间共享的调度特性,那么尝试从其他处理器借用运行时间,直到运行时间限额等于周期为止。

❑ 如果运行时间超过限额,那么给任务组设置节流标志,把任务组从上一级实时运行队列中删除。

3)第14行代码,如果函数sched_rt_runtime_exceeded返回1,表示运行时间超过限额,那么给当前进程设置需要重新调度的标志。

(2)周期定时器。为每个任务组启动一个实时周期定时器,处理函数是sched_rt_period_timer,该函数把主要工作委托给函数do_sched_rt_period_timer,主要代码如下:

    kernel/sched/rt.c
    1   static int do_sched_rt_period_timer(struct rt_bandwidth *rt_b, int overrun)
    2   {
    3    …
    4    for_each_cpu(i, span) {
    5         int enqueue = 0;
    6         struct rt_rq *rt_rq = sched_rt_period_rt_rq(rt_b, i);
    7         struct rq *rq = rq_of_rt_rq(rt_rq);
    8
    9         raw_spin_lock(&rq->lock);
    10        if (rt_rq->rt_time) {/* rt_rq->rt_time是实时运行队列的已运行时间 */
    11              u64 runtime;
    12
    13              raw_spin_lock(&rt_rq->rt_runtime_lock);
    14              if (rt_rq->rt_throttled)
    15                   balance_runtime(rt_rq);
    16              runtime = rt_rq->rt_runtime;
    17              rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime);
    18              if (rt_rq->rt_throttled && rt_rq->rt_time < runtime) {
    19                   rt_rq->rt_throttled = 0;
    20                   enqueue = 1;
    21              }
    22              if (rt_rq->rt_time || rt_rq->rt_nr_running)
    23                   idle = 0;
    24              raw_spin_unlock(&rt_rq->rt_runtime_lock);
    25        } else if (rt_rq->rt_nr_running) {
    26              idle = 0;
    27              if (! rt_rq_throttled(rt_rq))
    28                   enqueue = 1;
    29        }
    30        if (rt_rq->rt_throttled)
    31              throttled = 1;
    32
    33        if (enqueue)
    34              sched_rt_rq_enqueue(rt_rq);
    35        raw_spin_unlock(&rq->lock);
    36   }
    37   …
    38  }

参数overrun:自从实时周期定时器上一次执行到这一次执行,经历了多少个实时周期,换句话说,实时周期定时器的执行被拖延了几个实时周期。

第4行代码,对于任务组在每个处理器上的实时运行队列,执行以下操作。

1)第10行代码,如果实时运行队列的已运行时间不是0,那么处理如下。

❑ 第14行和第15行代码,如果实时运行队列被节流,那么尝试从其他处理器借用实时运行时间。

❑ 第17行代码,把已运行时间减去两者的较小值:已运行时间、拖延的实时周期数量乘以实时运行时间限额的结果。

❑ 第18~21行代码,如果实时运行队列被节流,并且已运行时间小于实时运行时间限额,那么解除节流,把实时调度实体重新加入上一级实时运行队列。

2)第25~29行代码,如果实时运行队列的已运行时间是0,队列有实时进程,并且队列没有被节流,那么把实时调度实体重新加入上一级实时运行队列。

3.公平调度类的带宽管理

可以使用周期和限额指定一个公平任务组的带宽。在每个指定的周期内,允许一个任务组最多执行多长时间(即限额)。当任务组在一个周期内用完了带宽时,这个任务组将会被节流,不允许继续运行,直到下一个周期。

可以使用cgroup设置一个公平任务组的周期和限额,cgroup版本1的配置方法如下。

(1)cpu.cfs_quota_us:一个周期内的可用运行时间,默认值是−1,表示没有带宽限制。

(2)cpu.cfs_period_us:周期长度,默认值是100毫秒。


cgroup版本1的配置示例如下。

(1)挂载cgroup文件系统,把CPU控制器关联到控制组层级树。

    mount -t cgroup -o cpu none /sys/fs/cgroup/cpu

(2)创建一个任务组。

    cd /sys/fs/cgroup/cpu
    mkdir browser   # 创建"browser"任务组

(3)把限额设置为50毫秒。

    echo 50000 > browser/cpu.cfs_quota_us

(4)把一个普通进程加入任务组。

    echo <pid> > browser/cgroup.procs

cgroup版本2从内核4.15版本开始支持CPU控制器,配置公平任务组的周期和限额的方法是:向控制组的文件“cpu.max”写入“$MAX $PERIOD”,表示控制组在每个长度为$PERIOD微秒的周期内最多执行$MAX微秒。默认值是“max 100000”,参数$MAX是“max”表示没有限制。

cgroup版本2的配置示例如下。

(1)挂载cgroup2文件系统。

    mount -t cgroup2 none /sys/fs/cgroup

(2)在根控制组开启CPU控制器。

    cd /sys/fs/cgroup
    echo "+cpu" > cgroup.subtree_control

(3)创建一个任务组。

    mkdir browser    # 创建"browser"任务组

(4)把任务组browser的带宽配置为每个100毫秒周期最多执行50毫秒。

    echo "50000 100000" > browser/cpu.max

(5)把线程组加入任务组。

    echo <pid> > browser/cgroup.procs

公平任务组的带宽存放在结构体task_group的成员cfs_bandwidth中:

    kernel/sched/sched.h
    struct task_group {
          …
          struct cfs_bandwidth cfs_bandwidth;
    };

(1)节流:在以下两种情况下,调度器会检查公平运行队列是否用完运行时间。

1)put_prev_task_fair:调度器把当前正在运行的普通进程放回公平运行队列。

2)pick_next_task_fair:当前正在运行的进程属于公平调度类,调度器选择下一个普通进程。

如果公平运行队列用完了运行时间,那么先尝试向任务组请求分配运行时间;如果任务组没有可用运行时间分配,那么把公平运行队列节流。

公平带宽片是公平运行队列每次向任务组请求分配运行时间时任务组分配的运行时间数量,默认值是5毫秒,用户可以通过文件“/proc/sys/kernel/sched_cfs_bandwidth_slice_us”修改。


以函数put_prev_task_fair为例,执行流程如图2.34所示,针对从当前进程向上到根任务组的每级调度实体,处理如下。

图2.34 公平任务组的带宽管理

1)调用函数__account_cfs_rq_runtime,函数__account_cfs_rq_runtime所做的处理如下。

❑ 把调度实体所属的公平运行队列的剩余运行时间减去当前进程的运行时间。

❑ 如果公平运行队列的剩余运行时间小于或等于0,那么请求任务组分配运行时间。

2)调用函数check_cfs_rq_runtime,函数check_cfs_rq_runtime发现调度实体所属的公平运行队列的剩余运行时间小于或等于0,处理如下。

❑ 把公平运行队列对应的调度实体从上一级公平运行队列中删除。

❑ 给公平运行队列设置节流标志,添加到所属任务组的节流链表。

❑ 如果公平运行队列在所属任务组中被第一个节流,那么启动所属任务组的周期定时器。

(2)周期定时器:在每个周期的开始,重新填充任务组的带宽,把带宽分配给节流的公平运行队列。

如图2.35所示,周期定时器的处理函数是sched_cfs_period_timer,它把主要工作委托给函数do_sched_cfs_period_timer,主要代码如下:

图2.35 周期定时器

    kernel/sched/fair.c
    1   static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b, int overrun)
    2   {
    3    …
    4    throttled = ! list_empty(&cfs_b->throttled_cfs_rq);
    5    …
    6    __refill_cfs_bandwidth_runtime(cfs_b);
    7
    8    if (! throttled) {
    9         cfs_b->idle = 1;
    10        return 0;
    11   }
    12   …
    13
    14   while (throttled && cfs_b->runtime > 0) {
    15        runtime = cfs_b->runtime;
    16        raw_spin_unlock(&cfs_b->lock);
    17        runtime = distribute_cfs_runtime(cfs_b, runtime,
    18                               runtime_expires);
    19        raw_spin_lock(&cfs_b->lock);
    20
    21        throttled = ! list_empty(&cfs_b->throttled_cfs_rq);
    22        cfs_b->runtime -= min(runtime, cfs_b->runtime);
    23   }
    24   …
    25  }

第6行代码,调用函数__refill_cfs_bandwidth_runtime来重新填充任务组的带宽。

第14~23行代码,如果满足条件:“任务组有节流的公平运行队列,并且任务组的可用运行时间没分配完”,那么调用函数distribute_cfs_runtime以把任务组的可用运行时间分配给节流的公平运行队列。

注意实时任务组和公平任务组的带宽管理差别:实时任务组每个周期在每个处理器上的运行时间不超过限额,公平任务组每个周期在所有处理器上的运行时间总和不超过限额。

函数__refill_cfs_bandwidth_runtime负责重新填充任务组的带宽:“把可用运行时间设置成限额,把运行时间的到期时间设置成当前时间加上1个周期”,代码如下。

    kernel/sched/fair.c
    void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
    {
   u64 now;


   if (cfs_b->quota == RUNTIME_INF)
   return;


   now = sched_clock_cpu(smp_processor_id());
   cfs_b->runtime = cfs_b->quota;
   cfs_b->runtime_expires = now + ktime_to_ns(cfs_b->period);
   }

函数distribute_cfs_runtime负责把任务组的可用运行时间分配给节流的公平运行队列,代码如下:

    kernel/sched/fair.c
    1   static u64 distribute_cfs_runtime(struct cfs_bandwidth *cfs_b,
    2         u64 remaining, u64 expires)
    3   {
    4    struct cfs_rq *cfs_rq;
    5    u64 runtime;
    6    u64 starting_runtime = remaining;
    7
    8    rcu_read_lock();
    9    list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq,
    10                   throttled_list) {
    11        struct rq *rq = rq_of(cfs_rq);
    12
    13        raw_spin_lock(&rq->lock);
    14        if (! cfs_rq_throttled(cfs_rq))
    15              goto next;
    16
    17        /* cfs_rq->runtime_remaining是公平运行队列的剩余运行时间 */
    18        runtime = -cfs_rq->runtime_remaining + 1;
    19        if (runtime > remaining)
    20              runtime = remaining;
    21        remaining -= runtime;
    22
    23        cfs_rq->runtime_remaining += runtime;
    24        cfs_rq->runtime_expires = expires;
    25
    26        /* 上面检查过是否被节流 */
    27        if (cfs_rq->runtime_remaining > 0)
    28              unthrottle_cfs_rq(cfs_rq);
    29
    30   next:
    31        raw_spin_unlock(&rq->lock);
    32
    33        if (! remaining)
    34              break;
    35   }
    36   rcu_read_unlock();
    37
    38   return starting_runtime - remaining;
    39  }

第9行代码,针对每个节流的公平运行队列,执行下面的操作。

1)第18~20行代码,计算分配的运行时间,取“公平运行队列剩余运行时间的相反数 + 1”,如果大于任务组的可用运行时间,取后者。

注意:节流的公平运行队列的剩余运行时间是零或者负数。因为实际运行时间可能会超出分配的运行时间,所以剩余运行时间可能是负数。

2)第23行代码,把剩余运行时间加上分配的运行时间。

3)第24行代码,把运行时间的到期时间设置成任务组的运行时间的到期时间。

4)第27行和第28行代码,如果剩余运行时间大于0,那么对公平运行队列解除节流,加入上一级公平运行队列。

(3)取有余补不足:同一个任务组中,有些公平运行队列变成空的,可能没用完运行时间,另一些公平运行队列的运行时间不够用,被节流了。前者把没用完的(即富余的)运行时间归还给任务组,任务组把运行时间分配给后者,这称为“取有余补不足”,它是富余(slack)定时器的使命。

如图2.36所示,当最后一个调度实体退出公平运行队列的时候,如果公平运行队列没用完的运行时间大于1毫秒,将会把运行时间归还给所属任务组(如果没用完的运行时间太少,忽略不计,没必要归还)。

图2.36 公平运行队列归还运行时间

如果任务组的可用运行时间大于公平带宽片,并且任务组有节流的公平运行队列,那么启动富余定时器,等待5毫秒,期望收集更多的运行时间。

如果富余定时器和周期定时器的到期时间相距不到2毫秒,没必要启动富余定时器,直接让周期定时器分配运行时间。

如图2.37所示,富余定时器到期的时候,分两种情况。

图2.37 富余定时器

1)如果周期定时器的剩余时间大于或等于2毫秒,那么富余定时器把任务组的可用运行时间分配给节流的公平运行队列。

2)如果周期定时器正在执行回调函数,或者剩余时间小于2毫秒,那么富余定时器什么都不用做,让周期定时器来做。