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

2.8.5 任务分组

1.任务分组的意义

我们先看以下两种场景。

(1)执行“make -j10”(选项“-j10”表示同时执行10条命令),编译Linux内核,同时运行视频播放器,如果给每个进程平均分配CPU时间,会导致视频播放很卡。

(2)用户1启动100个进程,用户2启动1个进程,如果给每个进程平均分配CPU时间,用户2的进程只能得到不到1%的CPU时间,用户2的体验很差。

怎么解决呢?把进程分组。对于第一种场景,把编译Linux内核的所有进程放在一个任务组中,把视频播放器放在另一个任务组中,给两个任务组分别分配50%的CPU时间。对于第二种场景,给用户1和用户2分别创建一个任务组,给两个任务组分别分配50%的CPU时间。

2.任务分组的方式

Linux内核支持以下任务分组方式。

❑ 自动组,配置宏是CONFIG_SCHED_AUTOGROUP。

❑ CPU控制组,即控制组(cgroup)的CPU控制器。需要打开配置宏CONFIG_CGROUPS和CONFIG_CGROUP_SCHED,如果公平调度类要支持任务组,打开配置宏CONFIG_FAIR_GROUP_SCHED;如果实时调度类要支持任务组,打开配置宏CONFIG_RT_GROUP_SCHED。

(1)自动组:创建会话时创建一个自动组,会话里面的所有进程是自动组的成员。启动一个终端窗口时就会创建一个会话。

在运行过程中可以通过文件“/proc/sys/kernel/sched_autogroup_enabled”开启或者关闭该功能,默认值是1。

实现自动组的源文件是“kernel/sched/auto_group.c”。

(2)CPU控制组:可以使用cgroup创建任务组和把进程加入任务组。cgroup已经从版本1(cgroup v1)演进到版本2(cgroup v2),版本1可以创建多个控制组层级树,版本2只有一个控制组层级树。

使用cgroup版本1的CPU控制器配置的方法如下。

1)在目录“/sys/fs/cgroup”下挂载tmpfs文件系统。

    mount -t tmpfs cgroup_root /sys/fs/cgroup

2)在目录“/sys/fs/cgroup”下创建子目录“cpu”。

    mkdir /sys/fs/cgroup/cpu

3)在目录“/sys/fs/cgroup/cpu”下挂载cgroup文件系统,把CPU控制器关联到控制组层级树。

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

4)创建两个任务组。

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

5)指定两个任务组的权重。

    echo 2048 > multimedia/cpu.shares
    echo 1024 > browser/cpu.shares

6)把线程加入任务组。

    echo <pid1> > browser/tasks
    echo <pid2> > multimedia/tasks

7)也可以把线程组加入任务组,指定线程组中的任意一个线程的标识符,就会把线程组的所有线程加入任务组。

    echo <pid1> > browser/cgroup.procs
    echo <pid2> > multimedia/cgroup.procs

cgroup版本2从内核4.15版本开始支持CPU控制器。使用cgroup版本2的CPU控制器配置的方法如下。

1)在目录“/sys/fs/cgroup”下挂载tmpfs文件系统。

    mount -t tmpfs cgroup_root /sys/fs/cgroup

2)在目录“/sys/fs/cgroup”下挂载cgroup2文件系统。

    mount -t cgroup2  none /sys/fs/cgroup

3)在根控制组开启CPU控制器。

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

4)创建两个任务组。

    mkdir multimedia   # 创建"multimedia"任务组
    mkdir browser      # 创建"browser"任务组

5)指定两个任务组的权重。

    echo 2048 > multimedia/cpu.weight
    echo 1024 > browser/cpu.weight

6)把线程组加入控制组。

    echo <pid1> > browser/cgroup.procs
    echo <pid2> > multimedia/cgroup.procs

7)把线程加入控制组。控制组默认只支持线程组,如果想把线程加入控制组,必须先把控制组的类型设置成线程化的控制组,方法是写字符串“threaded”到文件“cgroup.type”中。在线程化的控制组中,如果写文件“cgroup.procs”,将会把线程组中的所有线程加入控制组。

    echo threaded > browser/cgroup.type
    echo <pid1> > browser/cgroup.threads
    echo threaded > multimedia/cgroup.type
    echo <pid2> > multimedia/cgroup.threads

3.数据结构

任务组的结构体是task_group。默认的任务组是根任务组(全局变量root_task_group),默认情况下所有进程属于根任务组。

引入任务组以后,因为调度器的调度对象不仅仅是进程,所以内核抽象出调度实体,调度器的调度对象是调度实体,调度实体是进程或者任务组。

如表2.11所示,进程描述符中嵌入了公平、实时和限期3种调度实体,成员sched_class指向进程所属的调度类,进程可以更换调度类,并且使用调度类对应的调度实体。

表2.11 进程描述符的调度类和调度实体

如图2.25所示,任务组在每个处理器上有公平调度实体、公平运行队列、实时调度实体和实时运行队列,根任务组比较特殊:没有公平调度实体和实时调度实体。

图2.25 任务组

(1)任务组的下级公平调度实体加入任务组的公平运行队列,任务组的公平调度实体加入上级任务组的公平运行队列。

(2)任务组的下级实时调度实体加入任务组的实时运行队列,任务组的实时调度实体加入上级任务组的实时运行队列。

为什么任务组在每个处理器上有一个公平调度实体和一个公平运行队列呢?因为任务组包含多个进程,每个进程可能在不同的处理器上运行。同理,任务组在每个处理器上也有一个实时调度实体和一个实时运行队列。

在每个处理器上,计算任务组的公平调度实体的权重的方法如下(参考源文件“kernel/sched/fair.c”中的函数update_cfs_shares)。

(1)公平调度实体的权重 = 任务组的权重 × 负载比例

(2)公平调度实体的负载比例 = 公平运行队列的权重/(任务组的平均负载 − 公平运行队列的平均负载 + 公平运行队列的权重)

(3)公平运行队列的权重 = 公平运行队列中所有调度实体的权重总和

(4)任务组的平均负载 = 所有公平运行队列的平均负载的总和

为什么负载比例不是公平运行队列的平均负载除以任务组的平均负载?公平运行队列的权重是实时负载,而公平运行队列的平均负载是上一次计算的负载值,更新被延迟了,我们使用实时负载计算权重。

在每个处理器上,任务组的实时调度实体的调度优先级,取实时运行队列中所有实时调度实体的最高调度优先级。

用数据结构描述任务组的调度实体和运行队列,如图2.26所示。

图2.26 任务组的调度实体和运行队列

如图2.27所示,根任务组没有公平调度实体和实时调度实体,公平运行队列指针指向运行队列中嵌入的公平运行队列,实时运行队列指针指向运行队列中嵌入的实时运行队列。

图2.27 根任务组的调度实体和运行队列

假设普通进程p在处理器n上运行,它属于任务组g,数据结构如图2.28所示。

图2.28 普通进程p属于任务组g

(1)成员depth是调度实体在调度树中的深度,任务组g的深度是d,进程p的深度是(d+1)。

(2)成员parent指向调度树中的父亲,进程p的父亲是任务组g。

(3)成员cfs_rq指向调度实体所属的公平运行队列。进程p所属的公平运行队列是任务组g在处理器n上拥有的公平运行队列。

(4)成员my_q指向调度实体拥有的公平运行队列,任务组拥有公平运行队列,进程没有公平运行队列。任务组g在每个处理器上有一个公平调度实体和一个公平运行队列,处理器n上的调度实体的成员my_q指向处理器n上的公平运行队列。