Cgroup之cpuset子系统
Contents
最近分析了内核cpuset的实现,发现在目前的常见的系统中应用不是很广泛。目前最火的docker也只是使用了其最简单的功能。本文对cpuset进行了简要总结,并总结了docker如何使用它。
注意:本文中引用的内核代码版本为
v5.2

什么是cpuset ?
man手册中对其的描述为:cpuset - confine processes to processor and memory node subsets,cpuset用于限制一组进程只运行在特定的cpu节点上和只在特定的mem节点上分配内存。
具体为什么需要cpuset,其和sched_setaffinity(2), mbind(2)和set_mempolicy(2)系统调用的实现关系,可以参考cpuset文档。
cpusets 是如何实现的?
由于linux kernel在实现cpusets之前,就有sched_setaffinity(2), mbind(2)和set_mempolicy(2)等系统调用完成类似的功能。cpusets扩展了这些机制。
刚开始,cpusets作为一个单独的实现,有一个文件系统类型为cpusets。后面随着cgroup的发展,cpusets又集成到了cgroup中,成了cgroup的一个子系统。
目前,作为cgroup的一个子系统,其遵循如下原则:
- 每个进程的
cpuset信息,保存在其task_struct中的cgroup`的数据结构中。 sched_setaffinity系统调用只被允许设置为进程的cpuset中的cpu。mbindandset_mempolicy系统调用只被允许设置为进程的cpuset中的mem。root cpuset包含系统上所有的cpu和mem。- 对于每一个
cpuset,可以定义其子cpuset,子cpuset包含的mem和cpu是其parent的子集。 - 可以查看某个
cpuset中的所有进程列表。
为了实现cpuset,只需在内核中添加一些hook,这些hook都不在性能关键路径中:
- 在
init/main.c, 在内核启动时,初始化root cpuset - 在
fork和exit接口中,attach和detach一个进程到对应的cpuset中 - 在
sched_setaffinity实现中,添加cpuset的过滤 - 在
mbindandset_mempolicy的实现中,添加cpuset的过滤 - 在
sched.c migrate_live_tasks()中,限制目标cpu为cpuset中所允许的 - 在
page_alloc.c,限制内存分配的节点为cpuset中所允许的内存节点上 - 在
vmscan.c,现在内存恢复到cpuset中所允许的内存节点上
另外,每一个进程对应的文件 /proc/<pid>/status中有四行信息描述其cpuset信息:
|
|
每一个进程对应的文件 /proc/<pid>/cpuset中展示了其对应的cgroup路径信息。
cpuset cgroup控制文件
每一个cpuset中,都会对应一组cgroup的控制文件,包括如下:
| 文件 | 说明 |
|---|---|
| cpuset.cpus | 限制一组进程所能使用的cpu, 供用户进行设置 |
| cpuset.mems | 限制一组进程所能使用的mem,供用户进行设置 |
| cpuset.effective_cpus | 显示实际进程可以使用的cpu |
| cpuset.effective_mems | 显示实际进程可以使用的mem |
| cpuset.memory_migrate | flag: 设置为1时,使能memory migration, 即内存结点改变后迁移内存,默认为0 |
| cpuset.cpu_exclusive | flag:设置为1时,说明独占指定cpus,默认为0 |
| cpuset.mem_exclusive | flag:设置为1时,说明独占指定mem,默认为0 |
| cpuset.mem_hardwall | flag:设置为1时,说明cpuset为Hardwall,默认为0 |
| cpuset.memory_pressure | 显示该cpuset中的内存压力,只读文件 |
| cpuset.memory_spread_page | flag:设置为1时,内核page cache将会均匀的分布在不同的节点上,默认值为0 |
| cpuset.memory_spread_slab | flag:设置为1时,内核slab cache将会均匀的分布在不同的节点上,默认值为0 |
| cpuset.sched_load_balance | flag:默认为值1,表示进程会在该cpuset中允许的cpu上进行负载均衡 |
| cpuset.sched_relax_domain_level | 可设置的值为-1到一个较小的整数值,只有sched_load_balance为1时生效,值越大,表示负载均衡时查找的cpu范围越大 |
另外,在root cpuset中除了上面提到的文件外,还包括控制文件:
| 文件 | 说明 |
|---|---|
| cpuset.memory_pressure_enabled | flag: 设置为1,表示计算每个cpuset的内存压力,此时memory_pressure的输出才有意义,该值默认为0 |
docker中cpuset的使用
docker中对cpuset的复杂功能都没有使用,即上面提到的各种flags都是用的默认值(即都未使能),docker只导出了两个接口:mems和cpus:
|
|
其实现请参考:runc代码
在实现中,新建cpuset时mems和cpus的值默认为其parent cpuset的mems和cpus的值。
cpuset 扩展功能
什么是 exclusive cpusets ?
如果一个cpuset是cpu 或者mem exclusive,那么没有其他cpuset(直接parent和其子cpuset除外)会和其共享相同的cpus和mems`。
什么是 memory_pressure ?
memory_pressure反应了cpuset中的这组进程尝试释放内存的频率,一般用户可以监控该文件,然后做出合理的反应。默认情况下,该功能是disable的。
什么是 memory spread ?
当进行内存分配时,默认从当前运行的cpu所在的node上分配内存(page cache or slab cache),当配置了memory_spread_page和memory_spread_slab后,分配内存时就会使用轮询算法。默认情况下,该功能是disable的。
代码如下: kernel/cgroup/cpuset.c(line 3449-3472)
|
|
什么是 sched_load_balance ?
该值默认为1,即打开调度CPU的负载均衡,这里指的是cpuset拥有的sched_domain,默认全局的CPU调度是本来就有负载均衡的。
简单说一下sched_domain的作用,其实就是划定了负载均衡的CPU范围,默认是有一个全局的sched_domain,对所有CPU做负载均衡的,现在再划分出一个sched_domain把CPU的某个子集作为负载均衡的单元。
对应到cpuset中,即将该cpuset所运行的cpu集合作为一个负载均衡在单元。
什么是 sched_relax_domain_level ?
当sched_load_balance使能后,该值代表寻找cpu的范围,该值有几个等级,越大越优先,表示迁移时搜索CPU的范围,这个主要开启了负载均衡选项的时候才有用。具体值代表的范围如下:
|
|
什么时候有用呢?
- 当进程在cpu间迁移时代价足够小
- searching cost 足够小,对我们没有影响
- 低延迟是相比cache miss更重要的场景