跳转至内容

cgroups

来自 ArchWiki
(重定向自 Control group)

控制组(或常称为 **cgroups**)是 Linux 内核提供的一项功能,用于管理、限制和审计进程组。与 nice(1) 命令或 /etc/security/limits.conf 等其他方法相比,cgroups 更具灵活性,因为它们可以针对进程的(子)集(可能具有不同的系统用户)进行操作。

可以使用各种工具访问控制组

  • 通过在 systemd 单元文件中使用指令来为服务和 slice 指定限制;
  • 直接访问 cgroup 文件系统;
  • 通过 cgcreatecgexeccgclassify 等工具(属于 libcgroupAURlibcgroup-gitAUR 软件包);
  • 使用“规则引擎守护进程”自动将某些用户/组/命令移至组(/etc/cgrules.confcgconfig.service)(属于 libcgroupAURlibcgroup-gitAUR 软件包);以及
  • 通过其他软件,例如 Linux Containers (LXC) 虚拟化。

对于 Arch Linux,systemd 是调用和配置 cgroups 的首选且最简单的方法,因为它是默认安装的一部分。

安装

确保您已 安装 以下软件包之一以实现自动 cgroup 处理

  • systemd - 用于控制 systemd 服务的资源。
  • libcgroupAUR, libcgroup-gitAUR - 一套独立的工具(cgcreate, cgclassify, 通过 cgconfig.conf 进行持久化)。

使用 systemd

层级结构

当前 cgroup 层级结构可以通过 systemctl statussystemd-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
注意 eBPF 技术上不是一个控制器,但那些使用它实现的 systemd 选项只能由 root 设置。

用户委托

要让用户控制 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/ 中。
注意 并非所有资源更改都会立即生效。例如,更改 TaskMax 只会在创建新进程时生效。

例如,切断所有用户会话的互联网访问

$ systemctl set-property user.slice IPAddressDeny=any

与 libcgroup 和 cgroup 虚拟文件系统

比 systemd 管理低一层的是 cgroup 虚拟文件系统。“libcgroup”提供了一个库和实用工具,使管理更容易,因此我们在这里也使用它们。

使用更低层的原因很简单:systemd 不为 cgroups 中的*每一个接口文件*提供接口,而且也不应期望它在未来任何时候都提供这些接口。从这些文件中读取以获取有关 cgroup 资源使用的额外见解是完全无害的。

在使用非 systemd 工具之前...

一个 cgroup *应该*只由一套程序写入,以避免竞争条件,“单一写入者规则”。这不受内核强制执行,但遵循此建议可以防止出现难以调试的问题。要设置 systemd 停止管理子 cgroup 的边界,请参阅 Delegate= 属性。否则,如果系统覆盖了您设置的内容,请不要感到惊讶。

创建临时组

警告 手动创建“临时”组并不会告知 systemd 委托其管理。此操作只能用于测试;在生产环境中,请使用 systemd 和适当的 Delegate= 设置(对所有内容为 Delegate=yes)来创建组。

cgroups 的一个强大之处在于可以即时创建“临时”组。甚至可以授予普通用户创建自定义组的权限。groupname 是 cgroup 名称

# cgcreate -a user -t user -g memory,cpu:groupname
注意 自 cgroup v2 起,“memory,cpu” 部分已无用。所有可用的控制器都将包含在内,因为它们都在同一个层级结构中。为了加快输入速度,请尝试仅使用 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 文件读取属性。

创建和移动进程

注意 在 cgroup v2 中,包含子组的 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
注意 在最后一个命令中,一次只能写入一个 PID,因此必须为每个要移动的进程重复此操作。

操作组属性

在创建 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 目录来查找更多可调参数或统计信息。

持久组配置

注意 systemd ≥ 205 提供了一种更好的方法来在单元文件中管理 cgroups。以下方法仍然有效,但不应在新设置中使用。

如果您希望在启动时创建 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 v258 之前,可以使用 内核参数 SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 systemd.unified_cgroup_hierarchy=0 来强制使用 cgroup-v1 启动(第一个参数在 v256 中添加 以增加使用 cgroup-v1 的难度)。然而,此功能现已移除。了解它仍然有价值,因为有些软件喜欢在您的内核命令行中放置 systemd.unified_cgroup_hierarchy=0 而不告知您,导致您的整个系统崩溃。

参见