跳转到内容

本站近期可能因网络攻击出现服务故障,导致无法联网阅读。建议用户安装 arch-wiki-docs-zh-cnCNRepo 或者 arch-wiki-docs-zh-twCNRepo 离线文档包备用,或者使用经由 Cloudflare CDN 的替代版本aw.lilydjwg.me

来自 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 放入您的内核命令行,导致整个系统崩溃。

另请参阅