初探 Linux Cgroups:资源控制的奇妙世界
Cgroups 是 linux 內核提供的功能,由于牽涉的概念比較多,所以不太容易理解。本文試圖通過簡單的描述和 Demo 幫助大家理解 Cgroups 。
1. 什么是 Cgroups
Cgroups 是 Linux 下的一種將進程按組進行管理的機制,它提供了對一組進程及將來子進程的資源限制控制和統計的能力。
這些資源包括 CPU、內存、存儲、網絡等。通過 Cgroups 可以方便地限制某個進程的資源占用,并且可以實時地監控進程的監控與統計信息
Cgroups 分 v1 和 v2 兩個版本:
-
v1 實現較早,功能比較多,但是由于它里面的功能都是零零散散的實現的,所以規劃的不是很好,導致了一些使用和維護上的不便。
-
v2 的出現就是為了解決 v1 的問題,在最新的 4.5 內核中,Cgroups v2 聲稱已經可以用于生產環境了,但它所支持的功能還很有限。
v1 和 v2 可以混合使用,但是這樣會更復雜,所以一般沒人會這樣用。
1. 三部分組件
Cgroups 主要包括下面幾部分:
- cgroups 本身:cgroup 是對進程分組管理的一種機制,一個 cgroup 包含一組進程,并可以在這個 cgroup 上增加 Linux subsystem 的各種參數配置,將一組進程和一組 subsystem 的系統參數關聯起來。
-
subsystem: 一個 subsystem 就是一個內核模塊,他被關聯到一顆 cgroup 樹之后,就會在樹的每個節點(進程組)上做具體的操作。subsystem 經常被稱作"resource controller",因為它主要被用來調度或者限制每個進程組的資源,但是這個說法不完全準確,因為有時我們將進程分組只是為了做一些監控,觀察一下他們的狀態,比如 perf_event subsystem。到目前為止,Linux 支持 12 種 subsystem,比如限制 CPU 的使用時間,限制使用的內存,統計 CPU 的使用情況,凍結和恢復一組進程等,后續會對它們一一進行介紹。
-
hierarchy:一個 hierarchy 可以理解為一棵 cgroup 樹,樹的每個節點就是一個進程組,每棵樹都會與零到多個 subsystem 關聯。在一顆樹里面,會包含 Linux 系統中的所有進程,但每個進程只能屬于一個節點(進程組)。系統中可以有很多顆 cgroup 樹,每棵樹都和不同的 subsystem 關聯,一個進程可以屬于多顆樹,即一個進程可以屬于多個進程組,只是這些進程組和不同的 subsystem 關聯。目前 Linux 支持 12 種 subsystem,如果不考慮不與任何 subsystem 關聯的情況(systemd 就屬于這種情況),Linux 里面最多可以建 12 顆 cgroup 樹,每棵樹關聯一個 subsystem,當然也可以只建一棵樹,然后讓這棵樹關聯所有的 subsystem。當一顆 cgroup 樹不和任何 subsystem 關聯的時候,意味著這棵樹只是將進程進行分組,至于要在分組的基礎上做些什么,將由應用程序自己決定,systemd 就是一個這樣的例子。
3 個部分間的關系
- 系統在創建了新的 hierarchy 之后,系統中所有的進程都會加入這個 hierarchy 的 cgroup 根節點,這個 cgroup 根節點是 hierarchy 默認創建的。
- 一個 subsystem 只能附加到 一 個 hierarchy 上面。
- 一個 hierarchy 可以附加多個 subsystem 。
- 一個進程可以作為多個 cgroup 的成員,但是這些 cgroup 必須在不同的 hierarchy 中。
- 一個進程 fork 出子進程時,子進程是和父進程在同一個 cgroup 中的,也可以根據需要將其移動到其他 cgroup 中。
個人理解:
- cgroup 用于對進程進行分組。
- hierarchy 則根據繼承關系,將多個 cgroup 組成一棵樹。
- subsystem 則負責資源限制的工作,將 subsystem 和 hierarchy 綁定后,該 hierarchy 上的所有 cgroup 下的進程都會被 subsystem 給限制。
- 子 cgroup 會繼承父 cgroup 的 subsystem,但是子 cgroup 卻可以自定義自己的配置
- 因此:使用時可以直接在某個已存在的 hierarchy 下創建子 cgroup 或者直接創建一個新的 hierarchy 。
注:后續的 cgroup 樹就指的是 hierarchy,cgroup 則指 hierarchy 上的節點。
2. 具體架構
看完上面的描述,可能還是搞不清具體的關系,下面幾幅圖比較清晰的展示了 cgroup 中幾部分組件的關系。
這部分內容參考:美團技術團隊
hierarchy、cgroup、subsystem 3 者的關系:
比如上圖表示兩個 hierarchiy,每一個 hierarchiy 中是一顆樹形結構,樹的每一個節點是一個 cgroup (比如 cpu_cgrp, memory_cgrp)。
-
第一個 hierarchiy attach 了 cpu 子系統和 cpuacct 子系統, 因此當前 hierarchiy 中的 cgroup 就可以對 cpu 的資源進行限制,并且對進程的 cpu 使用情況進行統計。
-
第二個 hierarchiy attach 了 memory 子系統,因此當前 hierarchiy 中的 cgroup 就可以對 memory 的資源進行限制。
在每一個 hierarchiy 中,每一個節點(cgroup)可以設置對資源不同的限制權重(即自定義配置)。比如上圖中 cgrp1 組中的進程可以使用 60%的 cpu 時間片,而 cgrp2 組中的進程可以使用 20%的 cpu 時間片。
cgroups 和 進程間的關系:
上面這個圖從整體結構上描述了進程與 cgroups 之間的關系。最下面的P代表一個進程。
-
每一個進程的描述符中有一個指針指向了一個輔助數據結構
css_set(cgroups subsystem set)。 指向某一個css_set的進程會被加入到當前css_set的進程鏈表中。一個進程只能隸屬于一個css_set,一個css_set可以包含多個進程,隸屬于同一css_set的進程受到同一個css_set所關聯的資源限制。 -
上圖中的”M×N Linkage”說明的是
css_set通過輔助數據結構可以與 cgroups 節點進行多對多的關聯。但是 cgroups 的實現不允許css_set同時關聯同一個 cgroups 層級結構下多個節點。 這是因為 cgroups 對同一種資源不允許有多個限制配置。 -
一個
css_set關聯多個 cgroups 層級結構的節點時,表明需要對當前css_set下的進程進行多種資源的控制。而一個 cgroups 節點關聯多個css_set時,表明多個css_set下的進程列表受到同一份資源的相同限制。
一個節點的控制列表中的所有進程都會受到當前節點的資源限制。同時某一個進程也可以被加入到不同的 cgroups 層級結構的節點中,因為不同的 cgroups 層級結構可以負責不同的系統資源。所以說進程和 cgroup 結構體是一個多對多的關系。
2. 如何使用 Cgroups
注:本文所有操作在 Ubuntu20.04 下進行。
cgroup 相關的所有操作都是基于內核中的 cgroup virtual filesystem,使用 cgroup 很簡單,掛載這個文件系統就可以了。
一般情況下都是掛載到/sys/fs/cgroup 目錄下,當然掛載到其它任何目錄都沒關系。
cgroups 以文件的方式提供應用接口,我們可以通過 mount 命令來查看 cgroups 默認的掛載點:
[root@iZ2zefmrr626i66omb40ryZ ~]# mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
- 第一行的 tmpfs 說明 /sys/fs/cgroup 目錄下的文件都是存在于內存中的臨時文件。
- 第二行的掛載點 /sys/fs/cgroup/systemd 用于 systemd 系統對 cgroups 的支持。
- 其余的掛載點則是內核支持的各個子系統的根級層級結構。
需要注意的是,在使用 systemd 系統的操作系統中,/sys/fs/cgroup 目錄都是由 systemd 在系統啟動的過程中掛載的,并且掛載為只讀的類型。換句話說,系統是不建議我們在 /sys/fs/cgroup 目錄下創建新的目錄并掛載其它子系統的。這一點與之前的操作系統不太一樣。
查看 subsystem 列表
可以通過查看/proc/cgroups(since Linux 2.6.24)知道當前系統支持哪些 subsystem,下面是一個例子:
DESKTOP-9K4GB6E# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 11 1 1
cpu 3 64 1
cpuacct 3 64 1
blkio 8 64 1
memory 9 104 1
devices 5 64 1
freezer 10 4 1
net_cls 6 1 1
perf_event 7 1 1
net_prio 6 1 1
hugetlb 4 1 1
pids 2 68 1
從左到右,字段的含義分別是:
- subsys_name:subsystem 的名字
- hierarchy:subsystem 所關聯到的 cgroup 樹的 ID,如果多個 subsystem 關聯到同一顆 cgroup 樹,那么他們的這個字段將一樣,比如這里的 cpu 和 cpuacct 就一樣,表示他們綁定到了同一顆樹。如果出現下面的情況,這個字段將為 0:
- 當前 subsystem 沒有和任何 cgroup 樹綁定
- 當前 subsystem 已經和 cgroup v2 的樹綁定
- 當前 subsystem 沒有被內核開啟
- num_cgroups:subsystem 所關聯的 cgroup 樹中進程組的個數,也即樹上節點的個數
- enabled:1 表示開啟,0 表示沒有被開啟(可以通過設置內核的啟動參數“cgroup_disable”來控制 subsystem 的開啟).
hierarchy 相關操作
掛載
Linux 中,用戶可以使用 mount 命令掛載 cgroups 文件系統:
語法為: mount -t cgroup -o subsystems name /cgroup/name
- 其中 subsystems 表示需要掛載的 cgroups 子系統
- /cgroup/name 表示掛載點
這條命令同在內核中創建了一個 hierarchy 以及一個默認的 root cgroup。
示例:
掛載一個和 cpuset subsystem 關聯的 hierarchy 到 ./cg1 目錄
# 首先肯定是創建對應目錄
mkdir cg1
# 具體掛載操作--參數含義如下
# -t cgroup 表示操作的是 cgroup 類型,
# -o cpuset 表示要關聯 cpuset subsystem,可以寫0個或多個,0個則是關聯全部subsystem,
# cg1 為 cgroup 的名字,
# ./cg1 為掛載目標目錄。
mount -t cgroup -o cpuset cg1 ./cg1
# 掛載一顆和所有subsystem關聯的cgroup樹到cg1目錄
mkdir cg1
mount -t cgroup cg1 ./cg1
#掛載一顆與cpu和cpuacct subsystem關聯的cgroup樹到 cg1 目錄
mkdir cg1
mount -t cgroup -o cpu,cpuacct cg1 ./cg1
# 掛載一棵cgroup樹,但不關聯任何subsystem,這systemd所用到的方式
mkdir cg1
mount -t cgroup -o none,name=cg1 cg1 ./cg1
卸載
作為文件系統,同樣是使用umount 命令卸載。
# 指定路徑來卸載,而不是名字。
$ umount /path/to/your/hierarchy
例如
umount /sys/fs/cgroup/hierarchy
cgroup 相關操作
創建 cgroup 比較簡單,直接在 hierarchy 或 cgroup 目錄下創建子目錄(mkdir)即可。
刪除則是刪除對應目錄(rmdir)。
注:不能直接遞歸刪除對應目錄,因為目錄中的文件是虛擬的,遞歸刪除時會報錯。
也可以借助 libcgroup 工具來創建或刪除。
使用 libcgroup 工具前,請先安裝 libcgroup 和 libcgroup-tools 數據包
redhat 系統安裝:
$ yum install libcgroup
$ yum install libcgroup-tools
ubuntu 系統安裝:
$ apt-get install cgroup-bin
# 如果提示cgroup-bin找不到,可以用 cgroup-tools 替換
$ apt-get install cgroup-tools
具體語法:
# controllers就是subsystem
# path可以用相對路徑或者絕對路徑
$ cgdelete controllers:path
例如:
cgdelete cpu:./mycgroup
3. 演示
分別演示以下直接在某個已存在的 hierarchy 下創建子 cgroup 或者直接創建一個新的 hierarchy 兩種方式。
1. 新 hierarchy 方式
創建 hierarchy
首先,要創建并掛載一個 hierarchy。
# 創建一個目錄作為掛載點
lixd ~ $ mkdir cgroup-test
# 創建一個不掛載任何subsystem的hierarchy,由于 name=cgroup-test 的 cgroup 不存在,所以這里會由hierarchy默認創建出來
? lixd ~ $ sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
lixd ~ $ cd cgroup-test
lixd ~/cgroup-test $ ls
# 可以發現多了幾個文件
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
這些文件就是 hierarchy 中 cgroup 根節點的配置項。具體含義如下:
-
cgroup.clone_ children, cpuset 的 subsystem 會讀取這個配置文件,如果這個值是 1 (默認是 0),子 cgroup 才會繼承父 cgroup 的 cpuset 的配置。
-
cgroup.procs 是樹中當前節點 cgroup 中的進程組 ID,現在的位置是在根節點,這個文件中會有現在系統中所有進程組的 ID。
-
notify_on_release 和 release agent 會一起使用。 notify_on_release 標識當這個 cgroup 最后一個進程退出的時候是否執行了 release_agent; release_agent 則是一個路徑,通常用作進程退出之后自動清理掉不再使用的 cgroup。
-
tasks 標識該 cgroup 下面的進程 ID,如果把一個進程 ID 寫到 tasks 文件中,便會將相應的進程加入到這個 cgroup 中。
創建子 cgroup
然后,從剛創建好的 hierarchy 上 cgroup 根節點中擴展出兩個子 cgroup:
# 創建子cgroup cgroup-1
lixd ~/cgroup-test $ sudo mkdir cgroup-1
# 創建子cgroup cgroup-1
lixd ~/cgroup-test $ sudo mkdir cgroup-2
lixd ~/cgroup-test $ tree
.
├── cgroup-1
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup-2
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks
可以看到,在一個 cgroup 的目錄下創建文件夾時,Kernel 會把文件夾標記為這個 cgroup 的子 cgroup,它們會繼承父 cgroup 的屬性。
在 cgroup 中添加和移動進程
一個進程在一個 Cgroups 的 hierarchy 中,只能在一個 cgroup 節點上存在,系統的所有進程都會默認在根節點上存在。
想要將進程移動到其他 cgroup 節點,只需要將進程 ID 寫到目標 cgroup 節點的 tasks 文件中即可。
將當前 shell 所在進程添加到 tasks:
cgroup-test#cd cgroup-1
# 需要 root 權限
cgroup-1# echo $$ >> tasks
cgroup-1# cat tasks
7575
cgroup-1# cat /proc/7575/cgroup
14:name=cgroup-test:/cgroup-1 # 可以看到該進程已經被加入到cgroup中了
13:rdma:/
12:pids:/
11:hugetlb:/
10:net_prio:/
9:perf_event:/
8:net_cls:/
7:freezer:/
6:devices:/
5:blkio:/a
4:cpuacct:/
3:cpu:/
2:cpuset:/
1:memory:/
0::/
通過 subsystem 限制 cgroup 中的進程
在上面創建 hierarchy 的時候,這個 hierarchy 并沒有關聯到任何的 subsystem ,所以沒辦法通過那個 hierarchy 中的 cgroup 節點限制進程的資源占用。
即 只能在創建 hierarchy 時指定要關聯哪些 subsystem,創建后就無法修改。
其實系統默認已經為每個 subsystem 創建了一個默認的 hierarchy,比如 memory 的 hierarchy。
2. 子 cgroup 方式
在很多使用 systemd 的系統中,systemd 已經幫我們將各個 subsystem 和 cgroup 樹關聯并掛載好了:
DESKTOP-9K4GB6E# mount |grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup-test on /home/lixd/cgroup-test type cgroup (rw,relatime,name=cgroup-test)
因此我們可以直接在對應 cgroup 樹下創建子 cgroup 即可。
直接進到 /sys/fs/cgroup/cpu 目錄創建 cgroup-cpu 子目錄即可:
DESKTOP-9K4GB6E# cd /sys/fs/cgroup/cpu
DESKTOP-9K4GB6E# mkdir cgroup-cpu
DESKTOP-9K4GB6E# cd cgroup-cpu
DESKTOP-9K4GB6E# ls
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
簡單跑個程序測試一下,執行下面這條命令
DESKTOP-9K4GB6E# while : ; do : ; done &
[1] 12887
顯然,它執行了一個死循環,可以把計算機的 CPU 吃到 100%,根據它的輸出,我們可以看到這個腳本在后臺運行的進程號(PID)是 12887。
查看一下 CPU 占用:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12887 root 25 5 14912 1912 0 R 100.0 0.0 0:33.31 zsh
果然這個 PID=12887 的進程占用了差不多 100% 的 CPU。
結下來我們就通過 Cgroups 對其進行限制,這里就用前面創建的 cgroup-cpu 控制組。
我們可以通過查看 container 目錄下的文件,看到 container 控制組里的 CPU quota 還沒有任何限制(即:-1),CPU period 則是默認的 100 ms(100000 us):
DESKTOP-9K4GB6E# cat /sys/fs/cgroup/cpu/cgroup-cpu/cpu.cfs_quota_us
-1
DESKTOP-9K4GB6E# cat /sys/fs/cgroup/cpu/cgroup-cpu/cpu.cfs_period_us
100000
接下來,我們可以通過修改這些文件的內容來設置限制。比如,向 container 組里的 cfs_quota 文件寫入 20 ms(20000 us):
$ echo 20000 > /sys/fs/cgroup/cpu/cgroup-cpu/cpu.cfs_quota_us
這樣意味著在每 100 ms 的時間里,被該控制組限制的進程只能使用 20 ms 的 CPU 時間,也就是說這個進程只能使用到 20% 的 CPU 帶寬。
接下來,我們把被限制的進程的 PID 寫入 container 組里的 tasks 文件,上面的設置就會對該進程生效了:
$ echo 12887 > /sys/fs/cgroup/cpu/cgroup-cpu/tasks
使用 top 指令查看一下
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12887 root 25 5 14912 1912 0 R 20.3 0.0 2:51.05 zsh
果然 CPU 被限制到了 20%。
4. 小結
Cgroups 是 Linux 下的一種將進程按組進行管理的機制,它提供了對一組進程及將來子進程的資源限制控制和統計的能力。
cgroups 分為以下三個部分:
-
cgroup 本身:對進程進行分組
-
hierarchy:將 cgroup 形成樹形結構
-
subsystem:真正起到限制作用的部組件
使用步驟:
- 1)創建 cgroup
- 2)配置 subsystem 參數
- 3)將進程加入到該 cgroup
5. 參考
cgroups(7) — Linux manual page
Control groups series by Neil Brown
美團技術團隊---Linux 資源管理之 cgroups 簡介
Red Hat---資源管理指南
Linux Cgroup 系列(01):Cgroup 概述
總結
以上是生活随笔為你收集整理的初探 Linux Cgroups:资源控制的奇妙世界的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设计模式(二十)状态
- 下一篇: STM32CubeMX教程10 RTC