跳至內容

控制组

出自 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 放入您的內核命令行,導致整個系統崩潰。

另請參閱

[編輯 | 編輯原始碼]