59、Linux系统中cgroups v2的深入解析与应用

Linux系统中cgroups v2的深入解析与应用

1. cgroup控制器概述

cgroup控制器是内核的底层组件,负责在cgroup层次结构(一个cgroup及其后代)内分配特定资源,如CPU周期、内存和I/O带宽等。可以将其视为特定cgroup层次结构的“资源限制器”。

以下是现代Linux系统中常见的cgroups v2控制器及其功能和可用的内核版本:
| Cgroups v2控制器名称 | 启用时控制(或约束、调节)的内容 | 自内核版本 |
| — | — | — |
| cpu | CPU带宽(周期) | 4.15 |
| cpuset | CPU亲和性和内存节点放置(对大型NUMA系统特别有用) | 5.0 |
| memory | 内存(RAM)使用 | 4.5 |
| io | I/O资源的分配 | 4.5 |
| pids | cgroup中进程数量的硬限制 | 4.5 |
| devices | 设备文件的创建和访问(仅通过cgroup BPF程序) | 4.15 |
| rdma | 远程直接内存访问(RDMA)资源的分配和核算 | 4.11 |
| hugetlb | 每个cgroup的HugeTLB(大页面)使用限制 | 5.6 |
| misc | 各种;详见https://www.kernel.org/doc/html/v6.1/admin-guide/cgroup-v2.html#misc | 5.13 |

例如,PIDS控制器可通过限制从cgroup或其后代派生的进程数量来防止fork炸弹攻击。

2. 内核cgroups与用户空间的交互

内核cgroups通过专门构建的合成或伪文件系统向用户空间暴露,即cgroup文件系统,通常挂载在 /sys/fs/cgroup 。在cgroups v2中,文件系统类型为 cgroup2 ,可以使用 mount | grep cgroup 命令查看。

要查看系统中启用的控制器,可以使用以下命令:

cat /sys/fs/cgroup/cgroup.controllers

输出示例:

cpuset cpu io memory hugetlb pids rdma misc

需要注意的是, /proc/cgroups 仅与cgroups v1兼容,不适用于cgroups v2。

在cgroups v2中,所有控制器都挂载在单个层次结构中,而cgroups v1允许多个控制器挂载在多个层次结构或组下。现代的init框架 systemd 同时使用v1和v2 cgroups,并在启动时自动挂载cgroups v2文件系统。

3. 探索cgroups v2层次结构

3.1 确认挂载位置

使用以下命令确认cgroups v2层次结构的挂载位置:

mount | grep cgroup2

输出示例:

cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

可以看到,它通常挂载在 /sys/fs/cgroup

3.2 旧发行版的处理

如果使用的是较旧的发行版(如Ubuntu 18.04),可能会存在混合的cgroups v1和v2,导致cgroup2中没有控制器。要仅使用v2版本并使所有配置的控制器可见,需要在启动时传递内核命令行参数 cgroup_no_v1=all ,然后重启系统并重新检查。而较新的发行版(如Ubuntu 22.04或Fedora 38)则无需此操作。

3.3 根cgroup下的内容

在根cgroup位置 /sys/fs/cgroup 下,可以看到许多文件和文件夹,这些都是通过sysfs挂载在RAM中的易失性伪文件对象:
- 常规文件 :如 cgroup.controllers cpu.pressure 等,是cgroup2接口文件,可进一步分为核心接口和控制器接口。所有以 cgroup.* 开头的文件是核心接口文件, cpu.* 是CPU控制器的接口文件, memory.* 是内存控制器的接口文件等。
- 文件夹 :代表控制组或cgroups,但并非所有cgroups都受到约束。默认情况下,这些cgroups是由 systemd 在启动时设置的。

4. 启用或禁用控制器

4.1 cgroup.controllers文件

cgroup.controllers 文件列出了cgroup可用的控制器。对于根cgroup,它显示的是内核配置的控制器。默认情况下,现代发行版通常包含 cpuset cpu io memory hugetlb pids rdma misc

需要注意的是,控制器出现在此列表中并不意味着它在cgroup层次结构中已启用,默认情况下所有控制器都是禁用的。

4.2 启用和禁用控制器的方法

要启用控制器,将字符串 +<controller-name> 写入 cgroup.subtree_control 伪文件;要禁用控制器,则写入 -<controller-name> 。例如,要启用CPU和I/O控制器,但禁用内存控制器(对当前cgroup及其后代生效),可以使用以下命令(需以root权限执行):

echo "+cpu +io -memory" > cgroup.subtree_control

cgroup.subtree_control 文件列出了从该cgroup到其子代的资源分配中启用的控制器。

4.3 其他核心cgroup接口文件

  • cgroup.events :只读文件,有两个可能的值:
  • populated :0或1。如果为1,表示cgroup或其后代包含活动进程;否则为0,表示为空或未填充的cgroup。
  • frozen :0或1。如果为1,表示cgroup被冻结;否则为0,表示正常。
    例如,查看 init.scope cgroup的 cgroup.events 文件:
cat /sys/fs/cgroup/init.scope/cgroup.events

输出示例:

populated 1
frozen 0

由于 populated 为1,说明该cgroup值得进一步研究。

  • cgroup.kill :只写文件,写入1会导致cgroup树及其所有后代死亡,目标cgroup树中的进程将收到 SIGKILL 信号。

5. cgroups层次结构中的cgroups

5.1 init.scope cgroup

init.scope cgroup为例,查看其内部的文件和接口:

cd /sys/fs/cgroup/
ls init.scope/

部分输出示例:

cgroup.controllers cgroup.threads io.latency    memory.high memory.swap.current
cgroup.events      cgroup.type    io.max        memory.low  memory.swap.events
cgroup.freeze      cpu.idle       io.pressure   memory.max  memory.swap.high
cgroup.kill        cpu.max        io.prio.class memory.min  memory.swap.max

查看 init.scope cgroup中的 cgroup.procs 文件,它显示了属于该cgroup的进程的PID列表:

cat init.scope/cgroup.procs

输出示例:

1

这表明 init.scope cgroup只包含PID为1的init进程(即 systemd 本身)。

要将进程迁移到给定的cgroup,可以将其PID写入目标cgroup的 cgroup.procs 文件。写入者需要具有相应的权限,root用户可以执行此操作,非root用户在权限匹配的情况下也可以。

5.2 查看CPU控制器接口

使用以下命令查看 init.scope cgroup中CPU控制器的接口和值:

grep . init.scope/cpu.*

可以方便地查看所有CPU控制器接口及其当前值。

5.3 嵌套cgroups

cgroups可以嵌套,一个cgroup可以包含其他cgroups。例如,查看 system.slice cgroup下的子文件夹(即子cgroups):

find /sys/fs/cgroup/system.slice/ -maxdepth 1 -type d

每个cgroup下都有类似的伪文件,代表核心和控制器接口,用于限制系统资源,并且可能包含更多的cgroups。

5.4 使用systemd-cgls查看cgroup层次结构

使用 systemd-cgls 工具可以查看cgroup层次结构。例如,查看 init.scope cgroup下的进程:

systemd-cgls /sys/fs/cgroup/init.scope

输出示例:

Directory /sys/fs/cgroup/init.scope:
└─1 /usr/lib/systemd/systemd --switched-root --system –deserialize=32

查看 system.slice cgroup下的进程:

systemd-cgls /sys/fs/cgroup/system.slice

输出示例:

Directory /sys/fs/cgroup/system.slice:
├─abrt-journal-core.service
│ └─1386 /usr/bin/abrt-dump-journal-core -D -T -f -e
├─bolt.service
│ └─1425 /usr/libexec/boltd
├─low-memory-monitor.service
│ └─1296 /usr/libexec/low-memory-monitor
├─systemd-udevd.service …
│ └─udev
│   ├─   688 /usr/lib/systemd/systemd-udevd
│   ├─610731 (udev-worker)
│   ├─610732 (udev-worker)

需要注意的是,只有当cgroup或其父cgroup中的 cgroup.subtree_control 文件启用了相关控制器时,cgroup的资源限制功能才会生效。

6. systemd与cgroups

6.1 systemd的作用

手动管理cgroups可能是一项艰巨的任务,而强大的 systemd init框架可以在启动时创建和管理cgroups,自动利用其功能为用户和应用程序带来好处。

6.2 可视化工具

有多种工具可以帮助可视化系统中定义的cgroups(以及切片和作用域),包括 ps systemd-cgls systemctl systemd-cgtop

6.3 切片和作用域

systemd 通过定义和使用切片(slice)和作用域(scope)来逻辑地组织进程。切片用于表示属于特定用户的所有进程,或一组通过该单元管理资源的应用程序。作用域是对切片的进一步逻辑划分。

例如,所有在终端窗口中运行的进程通常会被 systemd 分组到一个 session-<number>.scope cgroup中。每个登录系统的用户会在 user.slice 节点下显示为一个“切片”,他们运行的应用程序将显示在该cgroup下。

6.4 更改默认设置的方法

如果想更改 systemd 设置cgroups的默认方式,有三种主要方法:
1. 手动编辑服务单元文件。
2. 使用 systemctl set-property 子命令进行编辑。
3. 在 systemd 目录结构中使用所谓的“drop-in”文件。

例如,查看 /usr/lib/systemd/system/user@.service 文件:

cat /usr/lib/systemd/system/user@.service

部分内容示例:

[Unit]
Description=User Manager for UID %i
Documentation=man:user@.service(5)
After=user-runtime-dir@%i.service dbus.service systemd-oomd.service
Requires=user-runtime-dir@%i.service
IgnoreOnIsolate=yes
[Service]
User=%i
PAMName=systemd-user
Type=notify-reload
ExecStart=/usr/lib/systemd/systemd --user
Slice=user-%i.slice
KillMode=mixed
Delegate=pids memory cpu
TasksMax=infinity

7. 使用systemctl和systemd-cgtop可视化cgroups

7.1 使用systemctl

systemd 定义了多种单元类型,其中 slice scope 与cgroups相关。可以使用 systemctl 命令查看它们:

systemctl list-units --type=slice,scope

7.2 使用systemd-cgtop

systemd-cgtop 工具可以可视化cgroups层次结构,并在运行时观察哪些cgroups(以及其中的切片/服务)占用了大部分资源。默认情况下,输出按CPU负载排序。

以下是 systemd-cgtop 的一些常用选项:
| 选项 | 描述 |
| — | — |
| -h --help | 显示帮助信息 |
| --version | 显示软件包版本 |
| -p --order=path | 按路径排序 |
| -t --order=tasks | 按任务/进程数量排序 |
| -c --order=cpu | 按CPU负载排序(默认) |
| -m --order=memory | 按内存负载排序 |
| -i --order=io | 按I/O负载排序 |
| -r --raw | 提供原始(非人类可读)数字 |
| --cpu=percentage | 以百分比显示CPU使用率(默认) |
| --cpu=time | 以时间显示CPU使用率 |
| -P | 计算用户空间进程而不是任务(不包括内核) |
| -k | 计算所有进程而不是任务(包括内核) |
| --recursive=BOOL | 递归计算进程数量 |
| -d --delay=DELAY | 更新之间的延迟 |
| -n --iterations=N | 运行N次迭代后退出 |
| -1 | --iterations=1 的快捷方式 |
| -b --batch | 以批处理模式运行,不接受输入 |
| --depth=DEPTH | 最大遍历深度(默认:3) |
| -M --machine= | 显示容器 |

需要注意的是,仅运行 systemd-cgtop 工具是不够的,如果cgroup上未启用资源核算(即 CPUAccounting=1 MemoryAccounting=1 BlockIOAccounting=1 未启用),它将无法提供有关资源使用情况的信息。

8. 查看进程所属的cgroup

使用 ps 命令并添加 -o cgroup 选项可以查看进程所属的cgroup:

ps fw -eo pid,user,cgroup,args

输出示例:

   PID USER     CGROUP                      COMMAND
      2 root     -                                           [kthreadd]
      3 root     -                                            \_ [rcu_gp]
      4 root     -                                            \_ [rcu_par_gp]
      5 root     -                                            \_ [slub_flushwq]
         1 root      0::/init.scope                    /usr/lib/systemd/systemd --switched-root --system [...]
   1096 root     0::/system.slice/systemd-jo /usr/lib/systemd/systemd-journald
   1109 root     0::/system.slice/systemd-ud /usr/lib/systemd/systemd-udevd
   1228 systemd+ 0::/system.slice/systemd-oo /usr/lib/systemd/systemd-oomd
   1229 systemd+ 0::/system.slice/systemd-re /usr/lib/systemd/systemd-resolved

可以看到,内核线程不属于任何cgroup,而其他进程则被分配到相应的cgroup中。

当用户启动新进程时, systemd 会将其放置在适当的切片和作用域中,从而纳入相应的cgroup进行管理。例如,运行 vim 编辑文件后,使用 ps 命令可以看到 vim 进程已成为用户切片的一部分:

ps fw -eo pid,user,cgroup,args | grep "[v]im"

输出示例:

1378003 kaiwan   0::/user.slice/user-1000.sl              \_ vim cgroupsv2_explore

如果系统未运行 systemd ,则需要手动管理cgroups。除了 systemd ,还有其他工具可用于cgroup管理,如 cg* 工具套件(通过 cgroup-tools/libcgroup 软件包安装,包含 cgcreate cgexec cgclassify 等工具)。

9. 内核命名空间简介

容器技术主要基于Linux内核中的两个关键技术:cgroups和命名空间。可以将容器视为轻量级的虚拟机。内核命名空间是实现容器概念的重要结构,通过它,内核可以对资源进行分区,使不同命名空间中的进程看到不同的值。

常见的内核命名空间及其提供的功能如下:
| 命名空间 | 提供的功能 |
| — | — |
| Mount | 每个挂载命名空间都有自己的文件系统布局(因此 /proc 在不同挂载命名空间中的内容不同) |
| PID | 进程隔离(每个命名空间可以有自己的PID 1进程) |
| Network | 网络隔离 |
| UTS | 域名和主机名隔离 |
| IPC | IPC资源(共享内存、消息队列和信号量)可以隔离 |
| User | 用户ID隔离(允许不同命名空间中的进程具有相同的UID/GID) |

在Linux中, clone() 系统调用的 CLONE_NEW* 标志可用于在内核中创建新命名空间的进程。其他与命名空间相关的系统调用包括 setns() unshare() ioctl_ns()

10. 总结与操作流程回顾

10.1 关键操作总结

为了更好地理解和运用上述知识,我们将关键操作总结如下:
| 操作目的 | 操作命令及说明 |
| — | — |
| 查看系统启用的控制器 | cat /sys/fs/cgroup/cgroup.controllers ,输出为以空格分隔的可用控制器列表 |
| 确认cgroups v2挂载位置 | mount | grep cgroup2 ,通常挂载在 /sys/fs/cgroup |
| 启用或禁用控制器 | 以root权限执行 echo "+<controller-name> +<controller-name> -<controller-name>" > cgroup.subtree_control ,如 echo "+cpu +io -memory" > cgroup.subtree_control |
| 查看cgroup内进程PID列表 | cat <cgroup-name>/cgroup.procs ,如 cat init.scope/cgroup.procs |
| 迁移进程到指定cgroup | 将进程PID写入目标cgroup的 cgroup.procs 文件,需相应权限 |
| 查看CPU控制器接口值 | grep . <cgroup-name>/cpu.* ,如 grep . init.scope/cpu.* |
| 查看cgroup层次结构 | systemd-cgls <cgroup-path> ,如 systemd-cgls /sys/fs/cgroup/init.scope |
| 查看进程所属cgroup | ps fw -eo pid,user,cgroup,args |

10.2 操作流程示例

下面是一个简单的操作流程示例,展示如何查看和管理cgroups:

graph LR
    A[开始] --> B[查看系统启用的控制器]
    B --> C[确认cgroups v2挂载位置]
    C --> D{是否为旧发行版}
    D -- 是 --> E[传递内核参数并重启]
    D -- 否 --> F[查看根cgroup下内容]
    E --> F
    F --> G[启用或禁用控制器]
    G --> H[查看cgroup内进程PID列表]
    H --> I[迁移进程到指定cgroup]
    I --> J[查看CPU控制器接口值]
    J --> K[查看cgroup层次结构]
    K --> L[查看进程所属cgroup]
    L --> M[结束]

11. 实际应用场景分析

11.1 资源限制场景

在多用户或多应用的服务器环境中,为了避免某个用户或应用占用过多资源影响其他服务的正常运行,可以使用cgroups进行资源限制。例如,通过启用 cpu memory 控制器,对特定用户或应用所在的cgroup进行CPU和内存使用限制。

# 启用CPU和内存控制器
echo "+cpu +memory" > /sys/fs/cgroup/myapp/cgroup.subtree_control
# 设置CPU最大带宽
echo "100000 100000" > /sys/fs/cgroup/myapp/cpu.max
# 设置内存最大使用量
echo "512M" > /sys/fs/cgroup/myapp/memory.max

11.2 容器化场景

在容器化应用部署中,cgroups和命名空间是实现容器隔离和资源管理的关键技术。通过 systemd 或其他容器管理工具,可以为每个容器创建独立的cgroup,对其CPU、内存、I/O等资源进行精确控制。例如,使用 docker 创建容器时,会自动为容器分配一个cgroup,并根据配置对资源进行限制。

# 创建一个限制CPU和内存使用的容器
docker run -it --cpus=0.5 --memory=256M ubuntu:latest /bin/bash

11.3 性能监控场景

使用 systemd-cgtop 工具可以实时监控cgroups的资源使用情况,帮助管理员及时发现资源使用异常的cgroup和进程。例如,通过按CPU负载排序查看哪些cgroup占用了大量CPU资源。

systemd-cgtop -c

12. 注意事项与技巧

12.1 权限问题

在进行cgroups操作时,很多操作需要root权限。例如,写入 cgroup.subtree_control 文件、迁移进程到指定cgroup等操作,普通用户需要相应权限才能执行。可以使用 sudo 命令以root权限执行操作。

12.2 正则表达式的使用

在使用 grep 命令过滤进程时,使用正则表达式可以避免显示 grep 命令本身。例如,使用 grep "[v]im" 而不是 grep "vim" 来查找 vim 进程。

12.3 资源核算问题

使用 systemd-cgtop 工具时,需要确保cgroup上启用了资源核算(即 CPUAccounting=1 MemoryAccounting=1 BlockIOAccounting=1 ),否则无法获取准确的资源使用信息。可以在 systemd 服务单元文件中添加相应配置来启用资源核算。

[Service]
CPUAccounting=yes
MemoryAccounting=yes
BlockIOAccounting=yes

13. 未来发展趋势

随着云计算、容器化和微服务等技术的不断发展,cgroups和命名空间在系统资源管理和应用隔离方面的作用将越来越重要。未来,可能会出现更强大的cgroup管理工具和更精细的资源分配策略,以满足不断增长的复杂应用场景需求。同时,内核命名空间的功能也可能会进一步扩展,提供更完善的隔离和安全机制。

14. 总结

本文深入介绍了Linux系统中cgroups v2的相关知识,包括cgroup控制器、层次结构、启用和禁用控制器的方法、与 systemd 的结合使用、内核命名空间以及可视化工具等内容。通过实际操作命令和示例,展示了如何查看、管理和监控cgroups。同时,分析了cgroups在不同场景下的应用和注意事项。希望读者通过本文的学习,能够更好地理解和运用cgroups技术,提高系统资源管理的效率和安全性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值