cgroups
控制组(或常称为 **cgroups**)是 Linux 内核提供的一项功能,用于管理、限制和审计进程组。与 nice(1) 命令或 /etc/security/limits.conf 等其他方法相比,cgroups 更具灵活性,因为它们可以针对进程的(子)集(可能具有不同的系统用户)进行操作。
可以使用各种工具访问控制组
- 通过在 systemd 单元文件中使用指令来为服务和 slice 指定限制;
- 直接访问
cgroup文件系统; - 通过
cgcreate、cgexec和cgclassify等工具(属于 libcgroupAUR 和 libcgroup-gitAUR 软件包); - 使用“规则引擎守护进程”自动将某些用户/组/命令移至组(
/etc/cgrules.conf和cgconfig.service)(属于 libcgroupAUR 和 libcgroup-gitAUR 软件包);以及 - 通过其他软件,例如 Linux Containers (LXC) 虚拟化。
对于 Arch Linux,systemd 是调用和配置 cgroups 的首选且最简单的方法,因为它是默认安装的一部分。
安装
确保您已 安装 以下软件包之一以实现自动 cgroup 处理
- systemd - 用于控制 systemd 服务的资源。
- libcgroupAUR, libcgroup-gitAUR - 一套独立的工具(
cgcreate,cgclassify, 通过cgconfig.conf进行持久化)。
使用 systemd
层级结构
当前 cgroup 层级结构可以通过 systemctl status 或 systemd-cgls 命令查看。
$ systemctl status
● myarchlinux
State: running
Jobs: 0 queued
Failed: 0 units
Since: Wed 2019-12-04 22:16:28 UTC; 1 day 4h ago
CGroup: /
├─user.slice
│ └─user-1000.slice
│ ├─user@1000.service
│ │ ├─gnome-shell-wayland.service
│ │ │ ├─ 1129 /usr/bin/gnome-shell
│ │ ├─gnome-terminal-server.service
│ │ │ ├─33519 /usr/lib/gnome-terminal-server
│ │ │ ├─37298 fish
│ │ │ └─39239 systemctl status
│ │ ├─init.scope
│ │ │ ├─1066 /usr/lib/systemd/systemd --user
│ │ │ └─1067 (sd-pam)
│ └─session-2.scope
│ ├─1053 gdm-session-worker [pam/gdm-password]
│ ├─1078 /usr/bin/gnome-keyring-daemon --daemonize --login
│ ├─1082 /usr/lib/gdm-wayland-session /usr/bin/gnome-session
│ ├─1086 /usr/lib/gnome-session-binary
│ └─3514 /usr/bin/ssh-agent -D -a /run/user/1000/keyring/.ssh
├─init.scope
│ └─1 /sbin/init
└─system.slice
├─systemd-udevd.service
│ └─285 /usr/lib/systemd/systemd-udevd
├─systemd-journald.service
│ └─272 /usr/lib/systemd/systemd-journald
├─NetworkManager.service
│ └─656 /usr/bin/NetworkManager --no-daemon
├─gdm.service
│ └─668 /usr/bin/gdm
└─systemd-logind.service
└─654 /usr/lib/systemd/systemd-logind
查找进程的 cgroup
进程的 cgroup 名称可以在 /proc/PID/cgroup 中找到。
例如,shell 的 cgroup
$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/session-3.scope
cgroup 资源使用
可以使用 systemd-cgtop 命令查看资源使用情况
$ systemd-cgtop
Control Group Tasks %CPU Memory Input/s Output/s user.slice 540 152,8 3.3G - - user.slice/user-1000.slice 540 152,8 3.3G - - user.slice/u…000.slice/session-1.scope 425 149,5 3.1G - - system.slice 37 - 215.6M - -
自定义 cgroup
systemd.slice(5) systemd 单元文件可用于定义自定义 cgroup 配置。它们必须放置在 systemd 目录中,例如 /etc/systemd/system/。可以分配的资源控制选项记录在 systemd.resource-control(5) 中。
这是一个只允许使用一个 CPU 30% 的 slice 单元示例
/etc/systemd/system/my.slice
[Slice] CPUQuota=30%
请记住,要执行 daemon-reload 以使任何新的或更改的 .slice 文件生效。
作为服务
服务单元文件
资源可以直接在服务定义中指定,或者作为 drop-in file 指定
[Service] MemoryMax=1G
此示例将服务限制为 1 GB。
将单元分组到 slice 下
服务可以指定在哪个 slice 中运行
[Service] Slice=my.slice
作为 root
systemd-run 可用于在特定 slice 中运行命令。
# systemd-run --slice=my.slice command
可以使用 --uid=username 选项以特定用户身份启动命令。
# systemd-run --uid=username --slice=my.slice command
可以使用 --shell 选项在 slice 中启动命令 shell。
作为非特权用户
如果满足某些条件,非特权用户可以将分配给他们的资源划分为新的 cgroup。
必须使用 Cgroups v2 才能允许非 root 用户管理 cgroup 资源。
控制器类型
并非所有资源都可以由用户控制。
| 控制器 | 可由用户控制 | 选项 |
|---|---|---|
| cpu | 需要委托 | CPUAccounting, CPUWeight, CPUQuota, AllowedCPUs, AllowedMemoryNodes |
| io | 需要委托 | IOWeight, IOReadBandwidthMax, IOWriteBandwidthMax, IODeviceLatencyTargetSec |
| memory | 是 | MemoryLow, MemoryHigh, MemoryMax, MemorySwapMax |
| pids | 是 | TasksMax |
| rdma | 否 | ? |
| eBPF | 否 | IPAddressDeny, DeviceAllow, DevicePolicy |
用户委托
要让用户控制 CPU 和 IO 资源,需要将这些资源委托出去。这可以通过 drop-in file 完成。
例如,如果您的用户 ID 是 1000
/etc/systemd/system/user@1000.service.d/delegate.conf
[Service] Delegate=cpu cpuset io
重启并验证您的用户会话所在的 slice 是否具有 CPU 和 IO 控制器
$ cat /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers
cpuset cpu io memory pids
用户定义 slice
用户 slice 文件可以放置在 ~/.config/systemd/user/ 中。
在特定 slice 下运行命令
$ systemd-run --user --slice=my.slice command
您也可以在 slice 中运行您的登录 shell
$ systemd-run --user --slice=my.slice --shell
运行时调整
cgroups 资源可以在运行时使用 systemctl set-property 命令进行调整。选项语法与 systemd.resource-control(5) 相同。
--runtime 选项。调整会保存在系统范围选项的 /etc/systemd/system.control/ 中,用户选项保存在 .config/systemd/user.control/ 中。例如,切断所有用户会话的互联网访问
$ systemctl set-property user.slice IPAddressDeny=any
与 libcgroup 和 cgroup 虚拟文件系统
比 systemd 管理低一层的是 cgroup 虚拟文件系统。“libcgroup”提供了一个库和实用工具,使管理更容易,因此我们在这里也使用它们。
使用更低层的原因很简单:systemd 不为 cgroups 中的*每一个接口文件*提供接口,而且也不应期望它在未来任何时候都提供这些接口。从这些文件中读取以获取有关 cgroup 资源使用的额外见解是完全无害的。
在使用非 systemd 工具之前...
一个 cgroup *应该*只由一套程序写入,以避免竞争条件,“单一写入者规则”。这不受内核强制执行,但遵循此建议可以防止出现难以调试的问题。要设置 systemd 停止管理子 cgroup 的边界,请参阅 Delegate= 属性。否则,如果系统覆盖了您设置的内容,请不要感到惊讶。
创建临时组
Delegate= 设置(对所有内容为 Delegate=yes)来创建组。cgroups 的一个强大之处在于可以即时创建“临时”组。甚至可以授予普通用户创建自定义组的权限。groupname 是 cgroup 名称
# cgcreate -a user -t user -g memory,cpu:groupname
cpu 或 \*。现在,组 groupname 中的所有可调参数都可以由您的用户写入
$ ls -l /sys/fs/cgroup/groupname
total 0 -r--r--r-- 1 root root 0 Jun 20 19:38 cgroup.controllers -r--r--r-- 1 root root 0 Jun 20 19:38 cgroup.events -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.freeze --w------- 1 root root 0 Jun 20 19:38 cgroup.kill -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.max.depth -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.max.descendants -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.pressure -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.procs -r--r--r-- 1 root root 0 Jun 20 19:38 cgroup.stat -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.subtree_control -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.threads -rw-r--r-- 1 root root 0 Jun 20 19:38 cgroup.type -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.idle -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.max -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.max.burst -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.pressure -r--r--r-- 1 root root 0 Jun 20 19:38 cpu.stat -r--r--r-- 1 root root 0 Jun 20 19:38 cpu.stat.local -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.uclamp.max -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.uclamp.min -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.weight -rw-r--r-- 1 root root 0 Jun 20 19:38 cpu.weight.nice -rw-r--r-- 1 root root 0 Jun 20 19:38 io.pressure -rw-r--r-- 1 root root 0 Jun 20 19:38 irq.pressure -r--r--r-- 1 root root 0 Jun 20 19:38 memory.current -r--r--r-- 1 root root 0 Jun 20 19:38 memory.events -r--r--r-- 1 root root 0 Jun 20 19:38 memory.events.local -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.high -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.low -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.max -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.min -r--r--r-- 1 root root 0 Jun 20 19:38 memory.numa_stat -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.oom.group -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.peak -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.pressure --w------- 1 root root 0 Jun 20 19:38 memory.reclaim -r--r--r-- 1 root root 0 Jun 20 19:38 memory.stat -r--r--r-- 1 root root 0 Jun 20 19:38 memory.swap.current -r--r--r-- 1 root root 0 Jun 20 19:38 memory.swap.events -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.swap.high -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.swap.max -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.swap.peak -r--r--r-- 1 root root 0 Jun 20 19:38 memory.zswap.current -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.zswap.max -rw-r--r-- 1 root root 0 Jun 20 19:38 memory.zswap.writeback -r--r--r-- 1 root root 0 Jun 20 19:38 pids.current -r--r--r-- 1 root root 0 Jun 20 19:38 pids.events -r--r--r-- 1 root root 0 Jun 20 19:38 pids.events.local -rw-r--r-- 1 root root 0 Jun 20 19:38 pids.max -r--r--r-- 1 root root 0 Jun 20 19:38 pids.peak
cgroups 是层级化的,所以您可以创建任意数量的子组。如果一个普通用户想创建一个名为 foo 的新子组
$ cgcreate -g cpu:groupname/foo
使用 cgroups
如前所述,任何时候*应该*只有一个东西写入 cgroup。这不影响非写入操作,包括在组内创建新进程、将进程移动到组或从 cgroup 文件读取属性。
创建和移动进程
libcgroup 包含一个简单的工具,用于在 cgroup 中运行新进程。如果一个普通用户想在之前的 groupname/foo 下运行一个 bash shell
$ cgexec -g cpu:groupname/foo bash
在 shell 内部,我们可以用以下命令确认它属于哪个 cgroup
$ cat /proc/self/cgroup
0::/groupname/foo
这使用了 /proc/$PID/cgroup,这是每个进程都有的一个文件。手动写入该文件也会导致 cgroup 发生变化。
将所有 'bash' 命令移至此组
$ pidof bash
13244 13266
$ cgclassify -g cpu:groupname/foo `pidof bash`
$ cat /proc/13244/cgroup
0::/groupname/foo
内部(即不使用 cgclassify)内核提供了两种在 cgroup 之间移动进程的方法。这两种方法是等效的
$ echo 0::/groupname/foo > /proc/13244/cgroup $ echo 13244 > /sys/fs/cgroup/groupname/foo/cgroup.procs
操作组属性
在创建 groupname/foo 时,会在 /sys/fs/cgroup/groupname/foo 下创建一个新的子目录。这些文件可以被读取和写入以更改组的属性。(再次强调,除非进行了委托,否则不建议写入!)
让我们看看我们组中所有进程占用了多少内存
$ cat /sys/fs/cgroup/groupname/foo/memory.current
1536000
要限制所有进程的 RAM(非交换空间)使用,请运行以下命令
$ echo 10000000 > /sys/fs/cgroup/groupname/foo/memory.max
要更改此组的 CPU 优先级(默认为 100)
$ echo 10 > /sys/fs/cgroup/groupname/foo/cpu.weight
您可以通过列出 cgroup 目录来查找更多可调参数或统计信息。
持久组配置
如果您希望在启动时创建 cgroups,则可以在 /etc/cgconfig.conf 中定义它们。这会使一个在启动时启动的服务配置您的 cgroups。有关此文件语法的说明,请参阅相关手册页;我们不会对如何使用一个真正已弃用的机制进行指导。
示例
限制命令的内存或 CPU 使用
以下示例显示了一个将给定命令限制为 2GB 内存的cgroup。
$ systemd-run --scope -p MemoryMax=2G --user command
以下示例显示了一个命令被限制为只使用一个 CPU 核心的 20%。
$ systemd-run --scope -p CPUQuota="20%" --user command
Matlab
在 MATLAB 中进行大量计算可能会导致系统崩溃,因为 Matlab 没有防止其占用系统所有内存或 CPU 的保护机制。以下示例显示了一个将 Matlab 限制为前 6 个 CPU 核心和 5 GB 内存的cgroup。
使用 systemd
~/.config/systemd/user/matlab.slice
[Slice] AllowedCPUs=0-5 MemoryHigh=6G
像这样启动 Matlab(请确保使用正确的路径)
$ systemd-run --user --slice=matlab.slice /opt/MATLAB/2012b/bin/matlab -desktop
文档
- 有关控制器以及某些开关和可调参数含义的信息,请参阅内核文档 v2(或安装 linux-docs 并查看
/usr/src/linux/Documentation/cgroup) - Linux 手册页:cgroups(7)
- 您可以在 Red Hat Enterprise Linux 文档中找到详细完整的资源管理指南:Red Hat Enterprise Linux documentation。
有关命令和配置文件,请参阅相关手册页,例如 cgcreate(1) 或 cgrules.conf(5)
历史说明:cgroup v1
在我们当前的 cgroup v2 之前,有一个早期版本叫做 v1。V1 提供了许多额外的灵活性,包括非统一层级结构和线程粒度管理。事后看来,这是一个糟糕的主意(请参阅 v2 的理由)。
- 即使可以存在多个层级结构,并且进程可以绑定到多个层级结构,但一个控制器只能在一个层级结构中使用。这使得多个层级结构实际上毫无意义,通常的设置是将每个控制器绑定到一个层级结构(例如
/sys/fs/cgroup/memory/),然后将每个进程绑定到多个层级结构。反过来,这使得cgcreate等工具对于同步进程在多个层级结构中的成员资格至关重要。 - 线程粒度管理导致 cgroup 被滥用为进程自我管理的一种方式。正确的方法是通过系统调用来实现,而不是通过为支持此用法而出现的复杂接口。自我管理需要笨拙的字符串管理,并且本质上容易发生竞争条件。
为了避免进一步的混乱,cgroup v2 在移除功能的基础上,还具备 两个关键设计规则:
- 如果一个 cgroup 具有子 cgroup,则它不能附加进程(根 cgroup 除外)。这在 v2 中是强制执行的,有助于使单一写入者规则(如下)可行。
- 每个 cgroup 在同一时间应该只有一个进程管理它(单一写入者规则)。这没有被强制执行,但在大多数情况下应该遵守,以避免软件之间因对组进行何种操作而产生冲突。
- 在 systemd 系统上,根 cgroup 由 systemd 管理,因此任何不由 systemd 进行的更改都将违反此规则(或,由于未强制执行,因此是建议),除非在周围的服务或作用域单元上设置了
Delegate=来告知 systemd 不要干预其中内容。
- 在 systemd 系统上,根 cgroup 由 systemd 管理,因此任何不由 systemd 进行的更改都将违反此规则(或,由于未强制执行,因此是建议),除非在周围的服务或作用域单元上设置了
在 systemd v258 之前,可以使用 内核参数 SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 systemd.unified_cgroup_hierarchy=0 来强制使用 cgroup-v1 启动(第一个参数在 v256 中添加 以增加使用 cgroup-v1 的难度)。然而,此功能现已移除。了解它仍然有价值,因为有些软件喜欢在您的内核命令行中放置 systemd.unified_cgroup_hierarchy=0 而不告知您,导致您的整个系统崩溃。