跳转到内容

控制组

来自 Arch Linux 中文维基

控制组 (或者通常被简写为 cgroups) 是一项 Linux 内核提供的特性, 用于管理,限制和审核一组进程。Cgroups 可以操作一个进程的集合或者子集(例如,集合中由不同用户启动的进程),这使得 cgroups 和其它类似工具,如 nice(1) 命令和 /etc/security/limits.conf 相比更为灵活。

可以使用以下方式使用控制组:

  • systemd 单元文件中使用指令来制定服务和切片的限制;
  • 通过直接访问 cgroup 文件系统;
  • 通过 cgcreatecgexeccgclassifylibcgroupAURlibcgroup-gitAUR 包的一部分) 等工具;
  • 使用“规则应用守护程序”来自动移动特定的用户/组/命令到另一个组中(/etc/cgrules.confcgconfig.service)(libcgroupAURlibcgroup-gitAUR 包的一部分);以及
  • 通过其它软件例如 Linux 容器 (LXC) 虚拟化。

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

安装

[编辑 | 编辑源代码]

确保你已经安装了这些用于自动处理 cgroups 的包中的至少一个:

  • systemd —— 用于控制 systemd 服务的资源使用。
  • libcgroupAURlibcgroup-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 组可以在 /proc/PID/cgroup 找到。

例如,找到 shell 进程的 cgroup:

$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/session-3.scope

查看控制组的系统资源使用情况

[编辑 | 编辑源代码]

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        -        -

自定义控制组

[编辑 | 编辑源代码]

systemd.slice(5) systemd 单元文件可以用于自定义一个 cgroup 配置。单元文件必须放在 systemd 目录下,例如 /etc/systemd/system/。可以指定的资源控制选项文档可以在 systemd.resource-control(5) 找到。

这是一个只允许使用 CPU 的 30% 的切片单元例子:

/etc/systemd/system/my.slice
[Slice]
CPUQuota=30%

记得 daemon-reload 来应用 .slice 文件的更改。

在 Systemd 服务中使用

[编辑 | 编辑源代码]

单元文件

[编辑 | 编辑源代码]

资源可以直接在服务定义或者 drop-in 文件中指定:

[Service]
MemoryMax=1G 

这个例子将内存使用限制在 1 GB。

使用切片将单元分组

[编辑 | 编辑源代码]

一个服务可以在指定切片下运行:

[Service]
Slice=my.slice

以 root 用户的身份使用

[编辑 | 编辑源代码]

systemd-run 可以用于在特定切片下运行命令。

# systemd-run --slice=my.slice command

--uid=username 选项可以以特定用户的身份运行命令。

# systemd-run --uid=username --slice=my.slice command

--shell 选项可以在指定切片下启动一个 shell。

以非特权用户的身份使用

[编辑 | 编辑源代码]

非特权用户可以在特定条件下将提供给他们的服务分成若干 cgroups。

必须使用 Cgroups v2 才能允许非 root 用户管理 cgroup 资源。

控制器种类

[编辑 | 编辑源代码]

并非所有系统资源都可以由用户控制。

Controller Can be controlled by user Options
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 文件来完成。

加入你的 UID 是 1000:

/etc/systemd/system/user@1000.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io

重启并确认用户会话下的切片有了 CPU 和 IO 控制器。

$ cat /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers
cpuset cpu io memory pids

用户定义的切片

[编辑 | 编辑源代码]

用户切片文件可以放置在 ~/.config/systemd/user/

可以这样在特定切片下运行命令:

$ systemd-run --user --slice=my.slice command

你也可以在切片里运行你的登陆 shell:

$ systemd-run --user --slice=my.slice --shell

运行时调整

[编辑 | 编辑源代码]

cgroups 资源可以在运行时使用 systemctl set-property 命令进行调整。选项语法与 systemd.resource-control(5) 中相同。

警告: 除非传递了 --runntime 选项,否则调整将永久性生效。系统范围的调整保存在 /etc/systemd/systemd/system.control/,用户范围内的保存在 .config/systemd/user.control/
注意:并非所有资源更改都会立即生效。例如,更改 TaskMax 只会在生成新进程时生效。

例如,切断所有用户会话的 Internet 访问:

$ systemctl set-property user.slice IPAddressDeny=any

与 libcgroup 和 cgroup 虚拟文件系统一起使用

[编辑 | 编辑源代码]

与使用 systemd 管理相比,cgroup 虚拟文件系统要更更接近底层。"libgroup" 提供了一个库和一些使管理更容易的实用程序,因此我们也将在这里使用它们。

使用更接近底层的方式的原因很简单:systemd 不为 cgroups 中的“每个接口文件”提供接口,也不应该期望它在未来的任何时间点提供它们。从它们中读取以获取有关 cgroup 资源使用的其他信息是完全无害的。

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

[编辑 | 编辑源代码]

一个 cgroup 应该只由一组程序来写入,以避免竟态条件,即“单一写入规则”。这不是由内核强制执行的,但遵循此建议可以防止难以调试的问题发生。为了让 systemd 停止管理某些子控制组,请参阅 Delegate= 属性。否则,systemd 可能覆盖你设置的内容。

创建专用组

[编辑 | 编辑源代码]
警告:手动创建“专用”组不会使其被 systemd 管理。除了测试用途之外,不应该这样做;在生产环境中,应当使用 systemd 创建具有适当 Delegate= 设置的组(要委派所有权限,设置 Delegate=yes)。

cgroups 允许你创建“专用”组。您甚至可以授予创建自定义组的权限给常规用户。 groupname 是 cgroup 名称:

# cgcreate -a user -t user -g memory,cpu:groupname
注意:自 cgroup v2 以来,“memory,cpu”部分是无用的。所有可用的控制器都将包含在内,不会有任何提醒,因为它们都处于相同的层次中。要加快打字速度,请使用 cpu\*

Now all the tunables in the group groupname are writable by your user:

现在,您的用户可以调整 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

使用控制组

[编辑 | 编辑源代码]

正如前文所提到的,在任何时候,只“应该”有一个程序写入 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,内核提供了在 cgroups 之间移动进程的另外两种方法。这两个是等价的:

$ echo 0::/groupname/foo > /proc/13244/cgroup
$ echo 13244 > /sys/fs/cgroup/groupname/foo/cgroup.procs
注意:在最后一个命令中,一次只能写入一个 PID,因此必须对需要移动的每个进程重复此操作。

管理组属性

[编辑 | 编辑源代码]

一个新的子目录 /sys/fs/cgroup/group/foo 将在 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 的更好方法。以下内容仍然有效,但不应用于新设置。

如果您希望在引导时创建 cgroup,则可以在 /etc/cgconfig.conf 中定义它们。这会导致在引导时启动一个服务以配置您的 cgroups。请参阅有关此文件语法的相关手册页;我们将不会说明如何使用真正已弃用的机制。

例子

[编辑 | 编辑源代码]

限制进程使用的内存和 CPU

[编辑 | 编辑源代码]

下面的示例显示一个 cgroup,它将指定的命令使用的内存限制为 2GB。

$ systemd-run --scope -p MemoryMax=2G --user command

下面的示例显示一个命令使用的 CPU 限制为一个 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 文档中找到。

有关命令和配置文件,请参阅相关手册页,例如 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 放入您的内核命令行,导致整个系统崩溃。

另请参阅

[编辑 | 编辑源代码]