Compare commits

...

93 Commits

Author SHA1 Message Date
Bin Liu
7a7fd32e4f Merge pull request #168 from kata-containers/release-0.5.1
release: v0.5.1
2026-07-14 18:04:33 +08:00
Xuewei Niu
3961c4b0dd release: v0.5.1
Changes since v0.5.0:

* 7cd1cda Fix race condition in create_v2_cgroup
* 0d10296 ci: bump Rust toolchain to 1.90.0
* a32f96e build: Bump oci-spec to 0.10.0
* 3eeab4e docs: improve README formatting and badges

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2026-07-14 04:51:40 -05:00
Xuewei Niu
fdcf75b27b Merge pull request #166 from burgerdev/fix-cgroup-create-race
Fix race condition in create_v2_cgroup
2026-07-14 17:00:41 +08:00
Markus Rudy
7cd1cda42f Fix race condition in create_v2_cgroup
There used to be a race condition while creating the cgroup hierarchy:
if a parent directory was created after .exists() but before
.create_dir, the function failed unnecessarily.

This commit changes the function to use create_dir_all, which is the
atomic variant of the above pattern, and fixes some surrounding
docstrings on the way.

Signed-off-by: Markus Rudy <mr@edgeless.systems>
2026-07-13 14:31:03 +02:00
Tim Zhang
0236d210d5 Merge pull request #167 from zvonkok/bump-oci-spec
build: Bump oci-spec to 0.10.0
2026-07-10 12:02:56 +08:00
Tim Zhang
0d1029646c ci: bump Rust toolchain to 1.90.0
New zbus and zvariant releases require a newer Rust compiler.

Use Rust 1.90.0 in CI so dependency resolution can complete.

Signed-off-by: Tim Zhang <tim@hyper.sh>
2026-07-10 12:00:01 +08:00
Zvonko Kaiser
a32f96e473 build: Bump oci-spec to 0.10.0
cgroups-rs exposes oci-spec types in its public API (e.g.
Manager::set() takes &LinuxResources), so the version pinned here
dictates which oci-spec version every consumer must use: 0.x lines
are semver-incompatible, and a consumer on a newer oci-spec gets
"expected LinuxResources, found LinuxResources" type mismatches.

Kata Containers is moving its workspace to oci-spec 0.10.0 and
cannot call into cgroups-rs until this crate follows. The 0.8 to
0.10 changes are additive for everything cgroups-rs touches (no
code changes needed): the crate builds warning-free with the oci
feature and the unit tests pass (the only failures are the
pre-existing fs manager tests that need root to write cgroupfs).

Signed-off-by: Zvonko Kaiser <zkaiser@nvidia.com>
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:18:34 +00:00
Tim Zhang
acad18f4a5 Merge pull request #162 from gaius-qi/feature/readme
docs: update README with badges and formatting
2026-02-03 14:38:00 +08:00
Gaius
3eeab4ef1c docs: improve README formatting and badges
- Add crates.io badge
- Simplify header and description
- Fix markdown heading levels (# -> ##)
- Fix code block language annotation spacing
- Remove redundant blank lines

Signed-off-by: Gaius <gaius.qi@gmail.com>
2026-02-02 22:26:06 +08:00
Alex Lyn
b99114bf40 Merge pull request #161 from Tim-Zhang/release-0.5.0
release: v0.5.0
2025-11-21 10:41:36 +08:00
Tim Zhang
f44ae5d4da release: v0.5.0
Bump major(minor actually) version for incompatible changes in #154.

Changelog:
- #152
- #154
- #159

Signed-off-by: Tim Zhang <tim@hyper.sh>
2025-11-21 10:31:47 +08:00
Tim Zhang
cf4c62dd34 Merge pull request #159 from Lu-yq/fix-verify-path
cgroups-rs: fix verify_path() logic
2025-11-21 10:24:26 +08:00
Lu-yq
c1a99fff50 cgroups-rs: fix verify_path() logic
remove the verify_path() logic because the
semantics change of controller.base.

Signed-off-by: Lu-yq <443471302@qq.com>
2025-11-21 01:26:26 +08:00
Xuewei Niu
6a11b32b64 Merge pull request #152 from jokemanfire/dev
fix(Controller): Errors are hidden by default
2025-11-20 19:18:36 +08:00
jokemanfire
a30729bcf1 fix(Controller): Errors are hidden by defaul
Fixed an error hidden during the creation of cgroup,
which resulted in subsequent error propagation

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2025-11-20 19:09:55 +08:00
Tim Zhang
69e3897a6d Merge pull request #154 from justxuewei/systemd-version
systemd: Do not check systemd version
2025-08-08 11:19:32 +08:00
Xuewei Niu
c49bee218f systemd: Do not check systemd version
The systemd version is purely informational and should not be parsed, as
documented in the [1]. In practice, systemd version has different formats
on OpenShift and Ubuntu.

This commit skips the version check and allows all operations. The errors
will be thrown from dbus when performing unsupported operations on obsolete
versions of systemd.

1: https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-08-07 15:05:53 +08:00
Fupan Li
5d74a1dfc9 Merge pull request #150 from Tim-Zhang/release-v0.4.0
release: v0.4.0
2025-07-22 18:19:27 +08:00
Tim Zhang
daa71aca91 release: v0.4.0
Changelog:
- #148
- #149

Signed-off-by: Tim Zhang <tim@hyper.sh>
2025-07-22 18:11:47 +08:00
Fupan Li
fefbdc8665 Merge pull request #148 from kata-containers/manager
Introduce FsManager and SystemdManager
2025-07-22 17:37:55 +08:00
Xuewei Niu
c774e9e123 manager: Add unit test cases
The tests cover the methods of `conv` mod, `FsManager`, and
`SystemdManager`. Since we have to manipulate the cgroups during testing,
the tests related to this part are set to be run in sequence.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Xuewei Niu
d69517847b systemd: Add unit test cases
The tests cover `SystemdClient` and methods from `utils` mods.

Please note that the `SystemdClient` tests do not run well in parallel, but
work well in sequence. Please run them with `--test-threads=1`.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Xuewei Niu
0c26caecfc manager: Introduce SystemdManager
Systemd manager takes cgroups path in the format of
"parent:scope_prefix:name" to create and manipulate cgroups through
systemd. It does value conversions for resources defined in the Linux
resources from the OCI spec, such as CPU quota, period, etc.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Xuewei Niu
4a68c762e1 systemd: Add support for managing cgroups through systemd
`SystemdCgroup` takes a `parent`, which is the name of a slice, and a
`unit`, which is the name of a slice or a scope unit, and provides methods
to start, kill the unit, as well as set properties for the unit.

The mods, `cpu`, `memory`, `cpuset`, and `pids`, are designed to generate
properties quickly. It hides the difference between cgroups v1 and v2, and
does simple checks for the systemd version and arguments.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Xuewei Niu
1250cbe182 manager: Introduce FsManager
`Manager` is a trait to unify the interface of cgroups. It is designed for
OCI containers. Its `set()` takes Linux resources of the OCI spec to set
cgroups.

The `FsManager`, the concrete implementation of `Manager`, manipulates
cgroups through cgroupfs, and supports both cgroups v1 and v2.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Xuewei Niu
b8031f1a21 fs: Move all files to fs subdirectory
Prepare to implement FsManager, which should be done in the following-up
commits. Apart from that, no code is not modified.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-18 15:17:12 +08:00
Fabiano Fidêncio
6e273cd2fb Merge pull request #149 from kata-containers/bump-rust
Bump Rust version to 1.85.1
2025-07-09 09:37:32 +02:00
Xuewei Niu
bce4b0bb3b Fix the warning about config file
The cargo complaints that ".cargo/config" is deprecated in favor of
".cargo/config.toml". Therefore, the file is renamed to it to make cargo
happy.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-08 23:56:57 +08:00
Xuewei Niu
f5a67c632e Fix the issues that makes cargo clippy unhappy
Most of the issues are small, but `fold()`. The usage of `fold()` in these
cases confuses me a lot, so that I don't follow the sugguestions from
clippy to replace `fold()` with `try_fold()`. Instead, I replace them with
`map() + collect()`.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-08 23:56:55 +08:00
Xuewei Niu
c8febbc67d .github: Bump Rust version to 1.85.1
Rust 1.69.0, released in 2023, lags significantly behind the version used
by Kata Containers. Therefore, we bump it to 1.85.1.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2025-07-08 17:07:52 +08:00
Fupan Li
2536e33696 Merge pull request #143 from oguzbilgener/no-regex
refactor: Parse HugeTLB size without Regex
2025-06-12 10:23:34 +08:00
Oguz Bilgener
694627e2fc refactor: Parse HugeTLB size without Regex
This removes the only place the regex crate is used, which could lead to significant binary size savings in projects that don't use the regex craate.

Signed-off-by: Oguz Bilgener <oguz@bilgener.me>
2025-05-29 11:05:53 -04:00
Fupan Li
de9625ff57 Merge pull request #146 from fidencio/topic/release-v0.3.5
release: v0.3.5
2025-05-29 16:33:54 +08:00
Fupan Li
b6b65f79d1 Merge pull request #141 from michelhe/remove-outdated-docs
hierarchies: Remove outdated documentation
2025-05-29 16:31:55 +08:00
Fupan Li
547fb08c03 Merge pull request #140 from jokemanfire/dev
fix bug: if cgroup path has ":"
2025-05-29 16:31:14 +08:00
Fupan Li
ec9f3547ed Merge pull request #135 from zzzzzzzzzy9/pull-branch
rdma: read rdma.max file
2025-05-29 16:30:09 +08:00
Michel Heily
82a6aa491a hierarchies: Remove outdated documentation
The documentation in the hierarchies module is outdated, this project
does support cgroups2 and the unified hierarchy :)

Signed-off-by: Michel Heily <michelheily@gmail.com>
2025-05-29 10:21:27 +02:00
zzzzzzzzzy9
65c36214b7 rdma: read rdma.max file
This is a simple one, just support rdma.max.

Signed-off-by: zzzzzzzzzy9 <zhang.yu58@zte.com.cn>
Signed-off-by: Fabiano Fidêncio <fidencio@northflank.com>
2025-05-29 10:12:10 +02:00
Fabiano Fidêncio
e0d0b8f4bc release: v0.3.5
There are several fixes since the last release, and itś been a few years
waiting for a new one.

Signed-off-by: Fabiano Fidêncio <fidencio@northflank.com>
2025-05-29 08:53:03 +02:00
Fabiano Fidêncio
db822470e5 Merge pull request #116 from xueruini/fix-mem-panic
Use default memory stats instead of panic.
2025-05-29 08:14:51 +02:00
Fabiano Fidêncio
362373b3ec memory: Fix test_procs_iterator_cgroup test
Otherwise it simply breaks.

Signed-off-by: Alex Man <alexman@stripe.com>
Signed-off-by: Fabiano Fidêncio <fidencio@northflank.com>
2025-05-28 23:02:04 +02:00
Ruini Xue
eadbf53140 memory: Use default memory stats instead of panic.
When containers are terminated, cgroup v2 memory metrics under
/sys/fs/cgroup may disappear.  Previously, kata-agent assumed these
metrics always exist, leading to panics as reported in
kata-containers#138.

This commit returns default value (0) when memory metric files are
missing.  This behaviour aligns with cgroup v1, which also defaults to 0
memory metric files are missing:
- Memory.limit_in_bytes which maps to m.max
  https://github.com/kata-containers/cgroups-rs/blob/main/src/memory.rs#L635
- Memory.soft_limit_in_bytes which maps to m.low
  https://github.com/kata-containers/cgroups-rs/blob/main/src/memory.rs#L661
- MemSwap.fail_cnt:
  https://github.com/kata-containers/cgroups-rs/blob/main/src/memory.rs#L631

Signed-off-by: Ruini Xue <ruini.xue@gmail.com>
Signed-off-by: Alex Man <alexman@stripe.com>
Signed-off-by: Fabiano Fidêncio <fidencio@northflank.com>
2025-05-28 23:02:04 +02:00
Fabiano Fidêncio
b3c57840ee lib: Ignore dead_code warning
I'm not fmiliar with this crate, so let's just ignore the warning for
now instead of removing the code.

Signed-off-by: Fabiano Fidêncio <fidencio@northflank.com>
2025-05-28 23:02:04 +02:00
Fabiano Fidêncio
b10e52d85f Merge pull request #133 from nxfs/controllers-parsing
controllers parsing is resilient to trailing newline
2025-05-28 22:44:03 +02:00
jokemanfire
7d4d4579a3 fix bug: if cgroup path has ":"
This bug is occur in cgroup path "which has :"

Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
2024-11-02 20:23:48 +08:00
Tim Zhang
eb3e37a4bc Merge pull request #132 from aa624545345/collect-cgroup-path
Correct path of cgroup when running a container in a container
2024-05-13 17:38:52 +08:00
乔琛 10307740
4005ad844d cgroup: collect path of cgroup when running a container in a container
Path of cgroup is wrong when running a container in a container. Use
the root path of mountinfo fetched from /proc/$(shim_pid)/mountinfo
to trim the path obtained from /proc/self/mountinfo.

Fixes: #131

Signed-off-by: 乔琛 10307740 <qiao.chen@zte.com.cn>
2024-04-16 16:40:05 +08:00
Fernand Sieber
ef3497646f controllers parsing is resilient to trailing newline
In older kernel versions (tested with 5.10.208), the cgroup.controllers
files ends with a newline. This newline is not trimmed and as such the
application later can incorrectly consider that the last controller is not
supported
2024-03-20 13:55:56 +02:00
Peng Tao
69ef63a0ef Merge pull request #130 from Tim-Zhang/release-0.3.4
release: v0.3.4
2023-10-25 15:35:45 +08:00
Tim Zhang
346844ca72 release: v0.3.4
To include #126, #127, #129

Signed-off-by: Tim Zhang <tim@hyper.sh>
2023-10-25 14:48:38 +08:00
Tim Zhang
4f1fe13d91 Merge pull request #127 from nrxus/cgroup2-identify-musl
make musl cgroupv2 identifying same as other target_envs
2023-10-25 14:42:03 +08:00
Andrés Medina
17a6c6b842 fix misidentification of cgroup v2 in musl targets
When compiling for a musl target, use the same CGROUP2_SUPER_MAGIC
constant that we use for other linux targets

Signed-off-by: Andrés Medina <andresnrx@gmail.com>
2023-10-24 15:52:35 -07:00
Tim Zhang
3c4b724433 Merge pull request #129 from mzweilz/mzweilz/main
Support setting max memory limit in cgroup v2
2023-10-23 20:00:55 +08:00
Tim Zhang
01885adb99 Merge pull request #126 from HeRaNO/use-peak
memory: use `peak` in `max_usage_in_bytes`
2023-10-23 20:00:25 +08:00
mengze
ce5f5f638e Support setting max memory limit in cgroup v2
When setting memory.max/memory.swap.max in cgroup v2, Linux uses "max"
instead of "-1" to indicate no restriction on memory usage. The
set_limit and set_memswap_limit functions of the memory controller
accept i64 as a parameter. In cgroupv2, if the parameter is -1, "max"
should be written into the cgroup file.

Fixes #128

Signed-off-by: mengze <mengze@linux.alibaba.com>
2023-10-23 19:14:16 +08:00
HeRaNO
be837166e9 memory: use peak in max_usage_in_bytes
Kernel 5.19 introduces `memory.peak` and kernel 6.5 introduces
`memory.swap.peak` to expose the memory and swap usage watermark.
This change use these values to fill `max_usage_in_bytes`.

Signed-off-by: HeRaNO <heran55@126.com>
2023-09-12 12:14:11 +08:00
Tim Zhang
8d29c194e3 Merge pull request #122 from justxuewei/release-033
release: v0.3.3
2023-08-03 15:18:13 +08:00
Xuewei Niu
8a82ad0ac2 release: v0.3.3
The included patches are

- 89edba0f85: gha: Bump Rust version to 1.69.0
- 66a93b1c3d: devices: Throw an error if device resources are invalid
- 55505e0b3e: Minor changes for cgroup and devices subsystem
- 0b6b229a38: add .path() method

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2023-08-03 15:11:31 +08:00
Bin Liu
369f3bebed Merge pull request #120 from fprasx/main
add .path() method
2023-08-03 15:10:08 +08:00
Felix Prasanna
0b6b229a38 add .path() method
Allows the user to re-retrieve the path to the cgroup.

Signed-off-by: Felix Prasanna <felix@neon.tech>
2023-08-01 10:14:45 -04:00
Tim Zhang
f55bdb1775 Merge pull request #121 from justxuewei/devices
Minor changes for cgroup and devices subsystem
2023-08-01 19:46:06 +08:00
Xuewei Niu
55505e0b3e Minor changes for cgroup and devices subsystem
The changes include:

- Expose `create()` and add `exists()` for `Cgroup`: The changes
are allowed to load cgroup and test if the cgroup exists. If not exists,
performing the `create()` directly to avoid performing `new()`.
- Make path of devices cgroup error more details: The origin path is
either `devices.allow` or `devices.deny`. It not shows which cgroup it
belongs to.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2023-08-01 19:36:55 +08:00
Fupan Li
df347c1db8 Merge pull request #118 from justxuewei/devices
devices: Throw an error if device resources are invalid
2023-08-01 09:51:34 +08:00
Xuewei Niu
66a93b1c3d devices: Throw an error if device resources are invalid
The cgroup-rs should throw errors while setting devices cgroup if the rule
is invalid. For example, if a cgroup has permissions of some devices. Then
we set a `a *:* rwm` to its parent's `devices.deny`. An error should be
thrown to make users realize that it is a invalid rule.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2023-07-31 15:18:19 +08:00
Tim Zhang
ca66292f5f Merge pull request #119 from justxuewei/ga/rust1690
gha: Bump Rust version to 1.69.0
2023-07-31 15:17:24 +08:00
Xuewei Niu
89edba0f85 gha: Bump Rust version to 1.69.0
Keep Rust version the same as kata-containers repo 's version.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
2023-07-31 15:01:17 +08:00
Tim Zhang
1b61c07b69 Merge pull request #114 from Tim-Zhang/release-0.3.2
release: v0.3.2
2023-03-15 15:53:10 +08:00
Tim Zhang
45e1f0c274 release: v0.3.2
To include patches #104, #113.

Signed-off-by: Tim Zhang <tim@hyper.sh>
2023-03-15 10:50:12 +08:00
Bin Liu
41b5f9c25c Merge pull request #113 from gkurz/fix-hugetlb-limit-in-bytes
Fix HugeTlbController::limit_in_bytes() for v2
2023-03-14 13:32:38 +08:00
Greg Kurz
93a59571e3 Fix HugeTlbController::limit_in_bytes() for v2
With Cgroups v2, the file to use is "max", not "limit_in_bytes".

Fixes #112

Signed-off-by: Greg Kurz <groug@kaod.org>
2023-03-10 13:37:41 +01:00
Tim Zhang
257012f2bb Merge pull request #104 from yaoyinnan/103/fix/add-task
Determine cgroup mode in add_task()
2023-02-09 11:39:29 +08:00
yaoyinnan
3dd0735324 Add UT for add_task().
Add UT for add_task() for cgroup v1 and v2.

Fixes: #103

Signed-off-by: yaoyinnan <yaoyinnan@foxmail.com>
2023-02-07 22:40:33 +08:00
yaoyinnan
6b338cf997 Determine cgroup mode in add_task()
Determine the cgroup mode in add_task() to avoid the wrong operation of the caller writing threads to cgroup.threads in non-thread mode.

Fixes: #103

Signed-off-by: yaoyinnan <yaoyinnan@foxmail.com>
2023-02-07 22:40:06 +08:00
Fupan Li
225be2cdbb Merge pull request #111 from Tim-Zhang/release-0.3.1
release: v0.3.1
2023-02-07 16:34:03 +08:00
Tim Zhang
51779d6915 release: v0.3.1
To include patches #105, #108, #110.

Signed-off-by: Tim Zhang <tim@hyper.sh>
2023-02-07 16:22:48 +08:00
Bin Liu
5ea28f076c Merge pull request #105 from amitlevy/patch-1
Fixed documentation for cpu quota and period
2023-02-07 15:10:48 +08:00
Fupan Li
aa74f34a91 Merge pull request #110 from liubin/fix/add-discard-to-blkio
blkio: add discard field to IoService
2023-02-06 14:59:08 +08:00
Amit Levy
328428ace4 Fixed documentation for cpu quota and period
nanoseconds -> microseonds

Noticed while debugging, also fits Red Hat documentation
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-cpu
, and I read the implementation to confirm

Signed-off-by: Amit Levy <amitlevy49@gmail.com>
2023-02-06 11:40:42 +08:00
Chao Wu
c8bb7e1c7e Merge pull request #108 from jongwu/error_cause
error: replace cause method with source
2023-02-05 23:05:04 +08:00
bin liu
25a1340123 blkio: add discard field to IoService
Some system has the `Discard` field in io service data, current the
library can't handle it correctly.

With this commit the blkio can get metrics whether it has the discard field.

Fixes: #109

Signed-off-by: bin liu <liubin0329@gmail.com>
2023-02-02 22:38:46 +08:00
Jianyong Wu
4203075f19 error: replace cause method with source
cause method for std::error::Error is depricated by rust, source method
is recommended.

Fixes: #107
Signed-off-by: Jianyong Wu <jianyong.wu@arm.com>
2023-02-02 14:00:11 +08:00
Fupan Li
a45ecf0884 Merge pull request #102 from Tim-Zhang/0.3.0
release: v0.3.0
2023-01-11 20:37:19 +08:00
Tim Zhang
2f60f213cc release: v0.3.0
Bump the major version for API changed in #91.

Signed-off-by: Tim Zhang <tim@hyper.sh>
2023-01-11 20:08:37 +08:00
Tim Zhang
91146f0ea3 Merge pull request #101 from Tim-Zhang/release-0.2.12
release: v0.2.12
2023-01-11 11:30:48 +08:00
Tim Zhang
e845665b3a release: v0.2.12
To include patches #91, #94, #98, #99

Signed-off-by: Tim Zhang <tim@hyper.sh>
2023-01-11 11:22:27 +08:00
Tim Zhang
55034f5b05 Merge pull request #97 from dcantah/cg2-kill
cgroup2: Add Kill method
2023-01-11 11:18:48 +08:00
Bin Liu
e2c2618707 Merge pull request #100 from dcantah/fix-clippy
Lint: Fix clippy errors
2023-01-10 14:36:47 +08:00
Danny Canter
07878325c3 cgroup2: Add Kill method
Fixes: #98

Add a Kill method for cgroups v2. This uses the new functionality in
kernels 5.14+ where writing to a cgroup.kill file will send a SIGKILL
to every process running in the cgroup. This would be useful for
kata to avoid freezing+manually sending SIGKILL+thawing process it
does currently to emulate runc's behavior.

Signed-off-by: Danny Canter <danny@dcantah.dev>
2023-01-09 21:56:47 -08:00
Danny Canter
88fb33113d Lint: Fix clippy errors
Fixes: #99

Signed-off-by: Danny Canter <danny@dcantah.dev>
2023-01-08 02:31:56 -08:00
Bin Liu
1211754b62 Merge pull request #91 from yaoyinnan/90/ftr/threaded-mode
cgroup: support to set threaded mode in cgroup v2
2023-01-05 11:57:07 +08:00
yaoyinnan
c9d02afe33 cgroup: support to set threaded mode in cgroup v2
Support to set threaded mode in cgroup v2. The premise of switching to threaded mode is that only the cgroup of cpuset, cpu and pids is supported.

Fixes: #90

Signed-off-by: yaoyinnan <yaoyinnan@foxmail.com>
2022-12-30 12:46:50 +08:00
Tim Zhang
0348f0a95e Merge pull request #94 from liubin/fix/93-include-path-in-error-message
error: introduce thiserror to add path to error message
2022-11-28 10:42:47 +08:00
bin liu
d387c6edc7 error: introduce thiserror to add path to error message
For some cgroup file operations, when failed, add the path
and the value (for write operation) to the error message.

Fixes: #93

Signed-off-by: bin liu <liubin0329@gmail.com>
2022-11-18 17:46:02 +08:00
58 changed files with 7488 additions and 1968 deletions

View File

@@ -1,7 +1,7 @@
name: BVT
on: [pull_request]
env:
RUST_VERSION: 1.52
RUST_VERSION: 1.90.0
jobs:
build:
name: Build
@@ -35,4 +35,3 @@ jobs:
- run: rustup install ${{ env.RUST_VERSION }} && rustup default ${{ env.RUST_VERSION }}
- run: make test

4
.gitignore vendored
View File

@@ -8,7 +8,3 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
/target
**/*.rs.bk
Cargo.lock

View File

@@ -5,7 +5,7 @@ repository = "https://github.com/kata-containers/cgroups-rs"
keywords = ["linux", "cgroup", "containers", "isolation"]
categories = ["os", "api-bindings", "os::unix-apis"]
license = "MIT OR Apache-2.0"
version = "0.2.11"
version = "0.5.1"
authors = ["The Kata Containers community <kata-dev@lists.katacontainers.io>", "Levente Kurusa <lkurusa@acm.org>", "Sam Wilson <tecywiz121@hotmail.com>"]
edition = "2018"
homepage = "https://github.com/kata-containers/cgroups-rs"
@@ -13,13 +13,19 @@ readme = "README.md"
[dependencies]
log = "0.4"
regex = "1.1"
nix = { version = "0.24", default-features = false, features = ["event", "fs", "process"] }
nix = { version = "0.25.0", default-features = false, features = ["event", "fs", "process"] }
libc = "0.2"
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "1"
oci-spec = { version = "0.10.0", optional = true }
zbus = "5.8"
bit-vec = "0.6"
[dev-dependencies]
libc = "0.2.76"
rand = "0.8"
nix = "0.25"
[features]
default = []
oci = ["oci-spec"]

View File

@@ -19,9 +19,37 @@ build: debug
# Tests and linters
#
.PHONY: test
test:
cargo test -- --color always --nocapture
# Tests that manipulate cgroups should run in sequence, so that
# `--test-threads=1` is used.
test: test-systemd test-fs-manager test-systemd-manager
cargo test --all-features -- --color always \
--nocapture \
--skip systemd::dbus::client::tests \
--skip manager::fs::tests \
--skip manager::systemd::tests
.PHONY: test-systemd
# Tests that manipulate cgroups should run in sequence, so that
# `--test-threads=1` is used.
test-systemd:
cargo test --package cgroups-rs --lib \
-- systemd::dbus::client::tests \
--color always --nocapture \
--test-threads=1
.PHONY: test-fs-manager
# See test-systemd
test-fs-manager:
cargo test --all-features --package cgroups-rs \
--lib -- manager::fs::tests \
--color always --nocapture --test-threads=1
.PHONY: test-systemd-manager
# See test-systemd
test-systemd-manager:
cargo test --all-features --package cgroups-rs \
--lib -- manager::systemd::tests \
--color always --nocapture --test-threads=1
.PHONY: check
check: fmt clippy

View File

@@ -1,15 +1,14 @@
# cgroups-rs ![Build](https://travis-ci.org/kata-containers/cgroups-rs.svg?branch=master)
Native Rust library for managing control groups under Linux
# cgroups-rs
Both v1 and v2 of cgroups are supported.
[![Crate](https://img.shields.io/crates/v/cgroups-rs.svg)](https://crates.io/crates/cgroups-rs)
# Examples
Native Rust library for managing Linux control groups. Supports both cgroups v1 and v2.
## Create a control group using the builder pattern
``` rust
## Examples
### Create a control group using the builder pattern
```rust
use cgroups_rs::*;
use cgroups_rs::cgroup_builder::*;
@@ -44,7 +43,7 @@ cg.delete();
// major version change.
```
# Disclaimer
## Disclaimer
This crate is licensed under:

View File

@@ -1,386 +0,0 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2020 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
//! This module handles cgroup operations. Start here!
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem};
use std::collections::HashMap;
use std::convert::From;
use std::fs;
use std::path::{Path, PathBuf};
/// A control group is the central structure to this crate.
///
///
/// # What are control groups?
///
/// Lifting over from the Linux kernel sources:
///
/// > Control Groups provide a mechanism for aggregating/partitioning sets of
/// > tasks, and all their future children, into hierarchical groups with
/// > specialized behaviour.
///
/// This crate is an attempt at providing a Rust-native way of managing these cgroups.
#[derive(Debug)]
pub struct Cgroup {
/// The list of subsystems that control this cgroup
subsystems: Vec<Subsystem>,
/// The hierarchy.
hier: Box<dyn Hierarchy>,
path: String,
}
impl Clone for Cgroup {
fn clone(&self) -> Self {
Cgroup {
subsystems: self.subsystems.clone(),
path: self.path.clone(),
hier: crate::hierarchies::auto(),
}
}
}
impl Default for Cgroup {
fn default() -> Self {
Cgroup {
subsystems: Vec::new(),
hier: crate::hierarchies::auto(),
path: "".to_string(),
}
}
}
impl Cgroup {
/// Create this control group.
fn create(&self) {
if self.hier.v2() {
let _ret = create_v2_cgroup(self.hier.root(), &self.path);
} else {
for subsystem in &self.subsystems {
subsystem.to_controller().create();
}
}
}
pub fn v2(&self) -> bool {
self.hier.v2()
}
/// Create a new control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group that can be used to manipulate it.
pub fn new<P: AsRef<Path>>(hier: Box<dyn Hierarchy>, path: P) -> Cgroup {
let cg = Cgroup::load(hier, path);
cg.create();
cg
}
/// Create a new control group in the hierarchy `hier`, with name `path` and `relative_paths`
///
/// Returns a handle to the control group that can be used to manipulate it.
///
/// Note that this method is only meaningful for cgroup v1, call it is equivalent to call `new` in the v2 mode
pub fn new_with_relative_paths<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
relative_paths: HashMap<String, String>,
) -> Cgroup {
let cg = Cgroup::load_with_relative_paths(hier, path, relative_paths);
cg.create();
cg
}
/// Create a handle for a control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group (that possibly does not exist until `create()` has
/// been called on the cgroup.
pub fn load<P: AsRef<Path>>(hier: Box<dyn Hierarchy>, path: P) -> Cgroup {
let path = path.as_ref();
let mut subsystems = hier.subsystems();
if path.as_os_str() != "" {
subsystems = subsystems
.into_iter()
.map(|x| x.enter(path))
.collect::<Vec<_>>();
}
Cgroup {
path: path.to_str().unwrap().to_string(),
subsystems,
hier,
}
}
/// Create a handle for a control group in the hierarchy `hier`, with name `path` and `relative_paths`
///
/// Returns a handle to the control group (that possibly does not exist until `create()` has
/// been called on the cgroup.
///
/// Note that this method is only meaningful for cgroup v1, call it is equivalent to call `load` in the v2 mode
pub fn load_with_relative_paths<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
relative_paths: HashMap<String, String>,
) -> Cgroup {
// relative_paths only valid for cgroup v1
if hier.v2() {
return Self::load(hier, path);
}
let path = path.as_ref();
let mut subsystems = hier.subsystems();
if path.as_os_str() != "" {
subsystems = subsystems
.into_iter()
.map(|x| {
let cn = x.controller_name();
if relative_paths.contains_key(&cn) {
let rp = relative_paths.get(&cn).unwrap();
let valid_path = rp.trim_start_matches('/').to_string();
let mut p = PathBuf::from(valid_path);
p.push(path);
x.enter(p.as_ref())
} else {
x.enter(path)
}
})
.collect::<Vec<_>>();
}
Cgroup {
subsystems,
hier,
path: path.to_str().unwrap().to_string(),
}
}
/// The list of subsystems that this control group supports.
pub fn subsystems(&self) -> &Vec<Subsystem> {
&self.subsystems
}
/// Deletes the control group.
///
/// Note that this function makes no effort in cleaning up the descendant and the underlying
/// system call will fail if there are any descendants. Thus, one should check whether it was
/// actually removed, and remove the descendants first if not. In the future, this behavior
/// will change.
pub fn delete(&self) -> Result<()> {
if self.v2() {
if !self.path.is_empty() {
let mut p = self.hier.root();
p.push(self.path.clone());
return fs::remove_dir(p).map_err(|e| Error::with_cause(RemoveFailed, e));
}
return Ok(());
}
self.subsystems.iter().try_for_each(|sub| match sub {
Subsystem::Pid(pidc) => pidc.delete(),
Subsystem::Mem(c) => c.delete(),
Subsystem::CpuSet(c) => c.delete(),
Subsystem::CpuAcct(c) => c.delete(),
Subsystem::Cpu(c) => c.delete(),
Subsystem::Devices(c) => c.delete(),
Subsystem::Freezer(c) => c.delete(),
Subsystem::NetCls(c) => c.delete(),
Subsystem::BlkIo(c) => c.delete(),
Subsystem::PerfEvent(c) => c.delete(),
Subsystem::NetPrio(c) => c.delete(),
Subsystem::HugeTlb(c) => c.delete(),
Subsystem::Rdma(c) => c.delete(),
Subsystem::Systemd(c) => c.delete(),
})
}
/// Apply a set of resource limits to the control group.
pub fn apply(&self, res: &Resources) -> Result<()> {
self.subsystems
.iter()
.try_fold((), |_, e| e.to_controller().apply(res))
}
/// Retrieve a container based on type inference.
///
/// ## Example:
///
/// ```text
/// let pids: &PidController = control_group.controller_of()
/// .expect("No pids controller attached!");
/// let cpu: &CpuController = control_group.controller_of()
/// .expect("No cpu controller attached!");
/// ```
pub fn controller_of<'a, T>(&'a self) -> Option<&'a T>
where
&'a T: From<&'a Subsystem>,
T: Controller + ControllIdentifier,
{
for i in &self.subsystems {
if i.to_controller().control_type() == T::controller_type() {
// N.B.:
// https://play.rust-lang.org/?gist=978b2846bacebdaa00be62374f4f4334&version=stable&mode=debug&edition=2015
return Some(i.into());
}
}
None
}
/// Removes a task from the control group.
///
/// Note that this means that the task will be moved back to the root control group in the
/// hierarchy and any rules applied to that control group will _still_ apply to the task.
pub fn remove_task(&self, pid: CgroupPid) {
let _ = self.hier.root_control_group().add_task(pid);
}
/// Attach a task to the control group.
pub fn add_task(&self, pid: CgroupPid) -> Result<()> {
if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
c.add_task(&pid)
} else {
Ok(())
}
} else {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().add_task(&pid))
}
}
/// Attach a task to the control group by thread group id.
pub fn add_task_by_tgid(&self, pid: CgroupPid) -> Result<()> {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().add_task_by_tgid(&pid))
}
/// Set notify_on_release to the control group.
pub fn set_notify_on_release(&self, enable: bool) -> Result<()> {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().set_notify_on_release(enable))
}
/// Set release_agent
pub fn set_release_agent(&self, path: &str) -> Result<()> {
self.hier
.root_control_group()
.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().set_release_agent(path))
}
/// Returns an Iterator that can be used to iterate over the tasks that are currently in the
/// control group.
pub fn tasks(&self) -> Vec<CgroupPid> {
// Collect the tasks from all subsystems
let mut v = if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
c.tasks()
} else {
vec![]
}
} else {
self.subsystems()
.iter()
.map(|x| x.to_controller().tasks())
.fold(vec![], |mut acc, mut x| {
acc.append(&mut x);
acc
})
};
v.sort();
v.dedup();
v
}
}
pub const UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
fn enable_controllers(controllers: &[String], path: &Path) {
let f = path.join("cgroup.subtree_control");
for c in controllers {
let body = format!("+{}", c);
let _rest = fs::write(f.as_path(), body.as_bytes());
}
}
fn supported_controllers() -> Vec<String> {
let p = format!("{}/{}", UNIFIED_MOUNTPOINT, "cgroup.controllers");
let ret = fs::read_to_string(p.as_str());
ret.unwrap_or_default()
.split(' ')
.map(|x| x.to_string())
.collect::<Vec<String>>()
}
fn create_v2_cgroup(root: PathBuf, path: &str) -> Result<()> {
// controler list ["memory", "cpu"]
let controllers = supported_controllers();
let mut fp = root;
// enable for root
enable_controllers(&controllers, &fp);
// path: "a/b/c"
let elements = path.split('/').collect::<Vec<&str>>();
let last_index = elements.len() - 1;
for (i, ele) in elements.iter().enumerate() {
// ROOT/a
fp.push(ele);
// create dir, need not check if is a file or directory
if !fp.exists() {
if let Err(e) = std::fs::create_dir(fp.clone()) {
return Err(Error::with_cause(ErrorKind::FsError, e));
}
}
if i < last_index {
// enable controllers for substree
enable_controllers(&controllers, &fp);
}
}
Ok(())
}
pub fn get_cgroups_relative_paths() -> Result<HashMap<String, String>> {
let path = "/proc/self/cgroup".to_string();
get_cgroups_relative_paths_by_path(path)
}
pub fn get_cgroups_relative_paths_by_pid(pid: u32) -> Result<HashMap<String, String>> {
let path = format!("/proc/{}/cgroup", pid);
get_cgroups_relative_paths_by_path(path)
}
fn get_cgroups_relative_paths_by_path(path: String) -> Result<HashMap<String, String>> {
let mut m = HashMap::new();
let content = fs::read_to_string(path).map_err(|e| Error::with_cause(ReadFailed, e))?;
for l in content.lines() {
let fl: Vec<&str> = l.split(':').collect();
if fl.len() != 3 {
continue;
}
let keys: Vec<&str> = fl[1].split(',').collect();
for key in &keys {
m.insert(key.to_string(), fl[2].to_string());
}
}
Ok(m)
}

View File

@@ -11,11 +11,11 @@
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::{read_string_from, read_u64_from};
use crate::{
use crate::fs::{read_string_from, read_u64_from};
use crate::fs::{
BlkIoResources, ControllIdentifier, ControllerInternal, Controllers, CustomizedAttribute,
Resources, Subsystem,
};
@@ -43,7 +43,7 @@ pub struct BlkIoData {
pub data: u64,
}
#[derive(Eq, PartialEq, Debug)]
#[derive(Eq, PartialEq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Per-device activity from the control group.
pub struct IoService {
@@ -59,6 +59,8 @@ pub struct IoService {
pub sync: u64,
/// How many items were asynchronously transferred.
pub r#async: u64,
/// How many items were discarded.
pub discard: u64,
/// Total number of items transferred.
pub total: u64,
}
@@ -87,44 +89,62 @@ pub struct IoStat {
}
fn parse_io_service(s: String) -> Result<Vec<IoService>> {
s.lines()
let mut io_services = Vec::<IoService>::new();
let mut io_service = IoService::default();
let lines = s
.lines()
.filter(|x| x.split_whitespace().count() == 3)
.map(|x| {
let mut spl = x.split_whitespace();
(spl.next().unwrap(), spl.next().unwrap(), spl.next().unwrap())
(
spl.next().unwrap(),
spl.next().unwrap(),
spl.next().unwrap(),
)
})
.map(|(a, b, c)| {
let mut spl = a.split(':');
(spl.next().unwrap(), spl.next().unwrap(), b, c)
})
.collect::<Vec<_>>()
.chunks(5)
.map(|x| {
match x {
[(major, minor, "Read", read_val), (_, _, "Write", write_val),
(_, _, "Sync", sync_val), (_, _, "Async", async_val),
(_, _, "Total", total_val)] =>
Some(IoService {
major: major.parse::<i16>().unwrap(),
minor: minor.parse::<i16>().unwrap(),
read: read_val.parse::<u64>().unwrap(),
write: write_val.parse::<u64>().unwrap(),
sync: sync_val.parse::<u64>().unwrap(),
r#async: async_val.parse::<u64>().unwrap(),
total: total_val.parse::<u64>().unwrap(),
}),
_ => None,
}
})
.fold(Ok(Vec::new()), |acc, x| {
if acc.is_err() || x.is_none() {
Err(Error::new(ParseError))
} else {
let mut acc = acc.unwrap();
acc.push(x.unwrap());
Ok(acc)
}
(
spl.next().unwrap().parse::<i16>(),
spl.next().unwrap().parse::<i16>(),
b,
c,
)
})
.collect::<Vec<_>>();
for (major_num, minor_num, op, val) in lines.iter() {
let major = *major_num.as_ref().map_err(|_| Error::new(ParseError))?;
let minor = *minor_num.as_ref().map_err(|_| Error::new(ParseError))?;
if (major != io_service.major || minor != io_service.minor) && io_service.major != 0 {
// new block device
io_services.push(io_service);
io_service = IoService::default();
}
io_service.major = major;
io_service.minor = minor;
let val = val.parse::<u64>().map_err(|_| Error::new(ParseError))?;
match *op {
"Read" => io_service.read = val,
"Write" => io_service.write = val,
"Sync" => io_service.sync = val,
"Async" => io_service.r#async = val,
"Discard" => io_service.discard = val,
"Total" => io_service.total = val,
_ => {}
}
}
if io_service.major != 0 {
io_services.push(io_service);
}
Ok(io_services)
}
fn get_value(s: &str) -> String {
@@ -161,13 +181,14 @@ fn parse_io_stat(s: String) -> Vec<IoStat> {
fn parse_io_service_total(s: String) -> Result<u64> {
s.lines()
.filter(|x| x.split_whitespace().count() == 2)
.fold(Err(Error::new(ParseError)), |_, x| {
match x.split_whitespace().collect::<Vec<_>>().as_slice() {
["Total", val] => val.parse::<u64>().map_err(|_| Error::new(ParseError)),
_ => Err(Error::new(ParseError)),
.find_map(|line| {
let mut parts = line.split_whitespace();
match (parts.next(), parts.next(), parts.next()) {
(Some("Total"), Some(val), None) => val.parse::<u64>().ok(),
_ => None,
}
})
.ok_or_else(|| Error::new(ParseError))
}
fn parse_blkio_data(s: String) -> Result<Vec<BlkIoData>> {
@@ -410,10 +431,10 @@ impl<'a> From<&'a Subsystem> for &'a BlkIoController {
impl BlkIoController {
/// Constructs a new `BlkIoController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
@@ -665,8 +686,12 @@ impl BlkIoController {
pub fn set_leaf_weight(&self, w: u64) -> Result<()> {
self.open_path("blkio.leaf_weight", true)
.and_then(|mut file| {
file.write_all(w.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(w.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("blkio.leaf_weight".to_string(), w.to_string()),
e,
)
})
})
}
@@ -675,7 +700,15 @@ impl BlkIoController {
self.open_path("blkio.leaf_weight_device", true)
.and_then(|mut file| {
file.write_all(format!("{}:{} {}", major, minor, weight).as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| {
Error::with_cause(
WriteFailed(
"blkio.leaf_weight_device".to_string(),
format!("{}:{} {}", major, minor, weight),
),
e,
)
})
})
}
@@ -683,95 +716,112 @@ impl BlkIoController {
pub fn reset_stats(&self) -> Result<()> {
self.open_path("blkio.reset_stats", true)
.and_then(|mut file| {
file.write_all("1".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("1".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("blkio.reset_stats".to_string(), "1".to_string()),
e,
)
})
})
}
/// Throttle the bytes per second rate of read operation affecting the block device
/// `major:minor` to `bps`.
pub fn throttle_read_bps_for_device(&self, major: u64, minor: u64, bps: u64) -> Result<()> {
let mut file = "blkio.throttle.read_bps_device";
let mut file_name = "blkio.throttle.read_bps_device";
let mut content = format!("{}:{} {}", major, minor, bps);
if self.v2 {
file = "io.max";
file_name = "io.max";
content = format!("{}:{} rbps={}", major, minor, bps);
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), content.to_string()), e)
})
})
}
/// Throttle the I/O operations per second rate of read operation affecting the block device
/// `major:minor` to `bps`.
pub fn throttle_read_iops_for_device(&self, major: u64, minor: u64, iops: u64) -> Result<()> {
let mut file = "blkio.throttle.read_iops_device";
let mut file_name = "blkio.throttle.read_iops_device";
let mut content = format!("{}:{} {}", major, minor, iops);
if self.v2 {
file = "io.max";
file_name = "io.max";
content = format!("{}:{} riops={}", major, minor, iops);
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), content.to_string()), e)
})
})
}
/// Throttle the bytes per second rate of write operation affecting the block device
/// `major:minor` to `bps`.
pub fn throttle_write_bps_for_device(&self, major: u64, minor: u64, bps: u64) -> Result<()> {
let mut file = "blkio.throttle.write_bps_device";
let mut file_name = "blkio.throttle.write_bps_device";
let mut content = format!("{}:{} {}", major, minor, bps);
if self.v2 {
file = "io.max";
file_name = "io.max";
content = format!("{}:{} wbps={}", major, minor, bps);
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), content.to_string()), e)
})
})
}
/// Throttle the I/O operations per second rate of write operation affecting the block device
/// `major:minor` to `bps`.
pub fn throttle_write_iops_for_device(&self, major: u64, minor: u64, iops: u64) -> Result<()> {
let mut file = "blkio.throttle.write_iops_device";
let mut file_name = "blkio.throttle.write_iops_device";
let mut content = format!("{}:{} {}", major, minor, iops);
if self.v2 {
file = "io.max";
file_name = "io.max";
content = format!("{}:{} wiops={}", major, minor, iops);
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), content.to_string()), e)
})
})
}
/// Set the weight of the control group's tasks.
pub fn set_weight(&self, w: u64) -> Result<()> {
// Attation: may not find in high kernel version.
let mut file = "blkio.weight";
let mut file_name = "blkio.weight";
if self.v2 {
file = "io.bfq.weight";
file_name = "io.bfq.weight";
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(w.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(w.to_string().as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), w.to_string()), e)
})
})
}
/// Same as `set_weight()`, but settable per each block device.
pub fn set_weight_for_device(&self, major: u64, minor: u64, weight: u64) -> Result<()> {
let mut file = "blkio.weight_device";
let mut file_name = "blkio.weight_device";
if self.v2 {
// Attation: there is no weight for device in runc
// https://github.com/opencontainers/runc/blob/46be7b612e2533c494e6a251111de46d8e286ed5/libcontainer/cgroups/fs2/io.go#L30
// may depends on IO schedulers https://wiki.ubuntu.com/Kernel/Reference/IOSchedulers
file = "io.bfq.weight";
file_name = "io.bfq.weight";
}
self.open_path(file, true).and_then(|mut file| {
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(format!("{}:{} {}", major, minor, weight).as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| {
Error::with_cause(
WriteFailed(
file_name.to_string(),
format!("{}:{} {}", major, minor, weight),
),
e,
)
})
})
}
}
@@ -779,15 +829,16 @@ impl BlkIoController {
impl CustomizedAttribute for BlkIoController {}
#[cfg(test)]
mod test {
use crate::blkio::{parse_blkio_data, BlkIoData};
use crate::blkio::{parse_io_service, parse_io_service_total, IoService};
use crate::error::*;
use crate::fs::blkio::{parse_blkio_data, BlkIoData};
use crate::fs::blkio::{parse_io_service, parse_io_service_total, IoService};
use crate::fs::error::*;
static TEST_VALUE: &str = "\
8:32 Read 4280320
8:32 Write 0
8:32 Sync 4280320
8:32 Async 0
8:32 Discard 1
8:32 Total 4280320
8:48 Read 5705479168
8:48 Write 56096055296
@@ -804,28 +855,6 @@ mod test {
8:0 Sync 7192576
8:0 Async 0
8:0 Total 7192576
Total 61823067136
";
static TEST_WRONG_VALUE: &str = "\
8:32 Read 4280320
8:32 Write 0
8:32 Async 0
8:32 Total 4280320 8:48 Read 5705479168
8:48 Write 56096055296
8:48 Sync 11213923328
8:48 Async 50587611136
8:48 Total 61801534464
8:16 Read 10059776
8:16 Write 0
8:16 Sync 10059776
8:16 Async 0
8:16 Total 10059776
8:0 Read 7192576
8:0 Write 0
8:0 Sync 7192576
8:0 Async 0
8:0 Total 7192576
Total 61823067136
";
@@ -855,6 +884,7 @@ Total 61823067136
write: 0,
sync: 4280320,
r#async: 0,
discard: 1,
total: 4280320,
},
IoService {
@@ -864,6 +894,7 @@ Total 61823067136
write: 56096055296,
sync: 11213923328,
r#async: 50587611136,
discard: 0,
total: 61801534464,
},
IoService {
@@ -873,6 +904,7 @@ Total 61823067136
write: 0,
sync: 10059776,
r#async: 0,
discard: 0,
total: 10059776,
},
IoService {
@@ -882,12 +914,34 @@ Total 61823067136
write: 0,
sync: 7192576,
r#async: 0,
discard: 0,
total: 7192576,
}
]
);
let err = parse_io_service(TEST_WRONG_VALUE.to_string()).unwrap_err();
assert_eq!(err.kind(), &ErrorKind::ParseError,);
let invalid_values = vec![
"\
8:32 Read 4280320
8:32 Write a
8:32 Async 1
",
"\
8:32 Read 4280320
b:32 Write 1
8:32 Async 1
",
"\
8:32 Read 4280320
8:32 Write 1
8:c Async 1
",
];
for value in invalid_values {
let err = parse_io_service(value.to_string()).unwrap_err();
assert_eq!(err.kind(), &ErrorKind::ParseError,);
}
}
#[test]

636
src/fs/cgroup.rs Normal file
View File

@@ -0,0 +1,636 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2020 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
//! This module handles cgroup operations. Start here!
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::hierarchies::V1;
use crate::fs::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem};
use std::collections::HashMap;
use std::convert::From;
use std::fs;
use std::path::{Path, PathBuf};
pub const CGROUP_MODE_DOMAIN: &str = "domain";
pub const CGROUP_MODE_DOMAIN_THREADED: &str = "domain threaded";
pub const CGROUP_MODE_DOMAIN_INVALID: &str = "domain invalid";
pub const CGROUP_MODE_THREADED: &str = "threaded";
/// A control group is the central structure to this crate.
///
///
/// # What are control groups?
///
/// Lifting over from the Linux kernel sources:
///
/// > Control Groups provide a mechanism for aggregating/partitioning sets of
/// > tasks, and all their future children, into hierarchical groups with
/// > specialized behaviour.
///
/// This crate is an attempt at providing a Rust-native way of managing these cgroups.
#[derive(Debug)]
pub struct Cgroup {
/// The list of subsystems that control this cgroup
subsystems: Vec<Subsystem>,
/// The hierarchy.
hier: Box<dyn Hierarchy>,
path: String,
/// List of controllers specifically enabled in the control group.
specified_controllers: Option<Vec<String>>,
}
impl Clone for Cgroup {
fn clone(&self) -> Self {
Cgroup {
subsystems: self.subsystems.clone(),
hier: crate::fs::hierarchies::auto(),
path: self.path.clone(),
specified_controllers: None,
}
}
}
impl Default for Cgroup {
fn default() -> Self {
Cgroup {
subsystems: Vec::new(),
hier: crate::fs::hierarchies::auto(),
path: "".to_string(),
specified_controllers: None,
}
}
}
impl Cgroup {
pub fn v2(&self) -> bool {
self.hier.v2()
}
/// Return the path the cgroup is located at.
pub fn path(&self) -> &str {
&self.path
}
/// Create this control group.
pub fn create(&self) -> Result<()> {
if self.hier.v2() {
create_v2_cgroup(self.hier.root(), &self.path, &self.specified_controllers)
} else {
self.subsystems
.iter()
.try_for_each(|subsystem| subsystem.to_controller().create())?;
Ok(())
}
}
/// Create a new control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group that can be used to manipulate it.
pub fn new<P: AsRef<Path>>(hier: Box<dyn Hierarchy>, path: P) -> Result<Cgroup> {
let cg = Cgroup::load(hier, path);
cg.create()?;
Ok(cg)
}
/// Create a new control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group that can be used to manipulate it.
pub fn new_with_specified_controllers<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
specified_controllers: Option<Vec<String>>,
) -> Result<Cgroup> {
let cg = if let Some(sc) = specified_controllers {
Cgroup::load_with_specified_controllers(hier, path, sc)
} else {
Cgroup::load(hier, path)
};
cg.create()?;
Ok(cg)
}
/// Create a new control group in the hierarchy `hier`, with name `path` and `relative_paths`
///
/// Returns a handle to the control group that can be used to manipulate it.
///
/// Note that this method is only meaningful for cgroup v1, call it is equivalent to call `new` in the v2 mode.
pub fn new_with_relative_paths<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
relative_paths: HashMap<String, String>,
) -> Result<Cgroup> {
let cg = Cgroup::load_with_relative_paths(hier, path, relative_paths);
cg.create()?;
Ok(cg)
}
/// Create a handle for a control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group (that possibly does not exist until `create()` has
/// been called on the cgroup.
pub fn load<P: AsRef<Path>>(hier: Box<dyn Hierarchy>, path: P) -> Cgroup {
let path = path.as_ref();
let mut subsystems = hier.subsystems();
if path.as_os_str() != "" {
subsystems = subsystems
.into_iter()
.map(|x| x.enter(path))
.collect::<Vec<_>>();
}
Cgroup {
path: path.to_str().unwrap().to_string(),
subsystems,
hier,
specified_controllers: None,
}
}
/// Create a handle for a specified control group in the hierarchy `hier`, with name `path`.
///
/// Returns a handle to the control group (that possibly does not exist until `create()` has
/// been called on the cgroup.
pub fn load_with_specified_controllers<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
specified_controllers: Vec<String>,
) -> Cgroup {
let path = path.as_ref();
let mut subsystems = hier.subsystems();
if path.as_os_str() != "" {
subsystems = subsystems
.into_iter()
.filter(|x| specified_controllers.contains(&x.controller_name()))
.map(|x| x.enter(path))
.collect::<Vec<_>>();
}
Cgroup {
path: path.to_str().unwrap().to_string(),
subsystems,
hier,
specified_controllers: Some(specified_controllers),
}
}
/// Create a handle for a control group in the hierarchy `hier`, with name `path` and `relative_paths`
///
/// Returns a handle to the control group (that possibly does not exist until `create()` has
/// been called on the cgroup.
///
/// Note that this method is only meaningful for cgroup v1, call it is equivalent to call `load` in the v2 mode
pub fn load_with_relative_paths<P: AsRef<Path>>(
hier: Box<dyn Hierarchy>,
path: P,
relative_paths: HashMap<String, String>,
) -> Cgroup {
// relative_paths only valid for cgroup v1
if hier.v2() {
return Self::load(hier, path);
}
let path = path.as_ref();
let mut subsystems = hier.subsystems();
if path.as_os_str() != "" {
subsystems = subsystems
.into_iter()
.map(|x| {
let cn = x.controller_name();
if relative_paths.contains_key(&cn) {
let rp = relative_paths.get(&cn).unwrap();
let valid_path = rp.trim_start_matches('/').to_string();
let mut p = PathBuf::from(valid_path);
p.push(path);
x.enter(p.as_ref())
} else {
x.enter(path)
}
})
.collect::<Vec<_>>();
}
Cgroup {
subsystems,
hier,
path: path.to_str().unwrap().to_string(),
specified_controllers: None,
}
}
/// The list of subsystems that this control group supports.
pub fn subsystems(&self) -> &Vec<Subsystem> {
&self.subsystems
}
/// Deletes the control group.
///
/// Note that this function makes no effort in cleaning up the descendant and the underlying
/// system call will fail if there are any descendants. Thus, one should check whether it was
/// actually removed, and remove the descendants first if not. In the future, this behavior
/// will change.
pub fn delete(&self) -> Result<()> {
if self.v2() {
if !self.path.is_empty() {
let mut p = self.hier.root();
p.push(self.path.clone());
return fs::remove_dir(p).map_err(|e| Error::with_cause(RemoveFailed, e));
}
return Ok(());
}
self.subsystems.iter().try_for_each(|sub| match sub {
Subsystem::Pid(pidc) => pidc.delete(),
Subsystem::Mem(c) => c.delete(),
Subsystem::CpuSet(c) => c.delete(),
Subsystem::CpuAcct(c) => c.delete(),
Subsystem::Cpu(c) => c.delete(),
Subsystem::Devices(c) => c.delete(),
Subsystem::Freezer(c) => c.delete(),
Subsystem::NetCls(c) => c.delete(),
Subsystem::BlkIo(c) => c.delete(),
Subsystem::PerfEvent(c) => c.delete(),
Subsystem::NetPrio(c) => c.delete(),
Subsystem::HugeTlb(c) => c.delete(),
Subsystem::Rdma(c) => c.delete(),
Subsystem::Systemd(c) => c.delete(),
})
}
/// Apply a set of resource limits to the control group.
pub fn apply(&self, res: &Resources) -> Result<()> {
self.subsystems
.iter()
.try_fold((), |_, e| e.to_controller().apply(res))
}
/// Retrieve a container based on type inference.
///
/// ## Example:
///
/// ```text
/// let pids: &PidController = control_group.controller_of()
/// .expect("No pids controller attached!");
/// let cpu: &CpuController = control_group.controller_of()
/// .expect("No cpu controller attached!");
/// ```
pub fn controller_of<'a, T>(&'a self) -> Option<&'a T>
where
&'a T: From<&'a Subsystem>,
T: Controller + ControllIdentifier,
{
for i in &self.subsystems {
if i.to_controller().control_type() == T::controller_type() {
// N.B.:
// https://play.rust-lang.org/?gist=978b2846bacebdaa00be62374f4f4334&version=stable&mode=debug&edition=2015
return Some(i.into());
}
}
None
}
/// Removes tasks from the control group by thread group id.
///
/// Note that this means that the task will be moved back to the root control group in the
/// hierarchy and any rules applied to that control group will _still_ apply to the proc.
pub fn remove_task_by_tgid(&self, tgid: CgroupPid) -> Result<()> {
self.hier.root_control_group().add_task_by_tgid(tgid)
}
/// Removes a task from the control group.
///
/// Note that this means that the task will be moved back to the root control group in the
/// hierarchy and any rules applied to that control group will _still_ apply to the task.
pub fn remove_task(&self, tid: CgroupPid) -> Result<()> {
self.hier.root_control_group().add_task(tid)
}
/// Moves tasks to the parent control group by thread group id.
pub fn move_task_to_parent_by_tgid(&self, tgid: CgroupPid) -> Result<()> {
self.hier
.parent_control_group(&self.path)
.add_task_by_tgid(tgid)
}
/// Moves a task to the parent control group.
pub fn move_task_to_parent(&self, tid: CgroupPid) -> Result<()> {
self.hier.parent_control_group(&self.path).add_task(tid)
}
/// Return a handle to the parent control group in the hierarchy.
pub fn parent_control_group(&self) -> Cgroup {
self.hier.parent_control_group(&self.path)
}
/// Kill every process in the control group. Only supported for v2 cgroups and on
/// kernels 5.14+. This will fail with InvalidOperation if the 'cgroup.kill' file does
/// not exist.
pub fn kill(&self) -> Result<()> {
if !self.v2() {
return Err(Error::new(CgroupVersion));
}
let val = "1";
let file_name = "cgroup.kill";
let p = self.hier.root().join(self.path.clone()).join(file_name);
// If cgroup.kill doesn't exist they're not on 5.14+ so lets
// surface some error the caller can check against.
if !p.exists() {
return Err(Error::new(InvalidOperation));
}
fs::write(p, val)
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), val.to_string()), e))
}
/// Attach a task to the control group.
pub fn add_task(&self, tid: CgroupPid) -> Result<()> {
if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
let cgroup_type = self.get_cgroup_type()?;
// In cgroup v2, writing to the cgroup.threads file is only supported in thread mode.
if cgroup_type == *CGROUP_MODE_DOMAIN_THREADED
|| cgroup_type == *CGROUP_MODE_THREADED
{
// It is used to move the threads of a process into a cgroup in thread mode.
c.add_task(&tid)
} else {
// When the cgroup type is domain or domain invalid,
// cgroup.threads cannot be written.
Err(Error::new(CgroupMode))
}
} else {
Err(Error::new(SubsystemsEmpty))
}
} else {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().add_task(&tid))
}
}
/// Attach tasks to the control group by thread group id.
pub fn add_task_by_tgid(&self, tgid: CgroupPid) -> Result<()> {
if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
// It is used to move a thread of the process to a cgroup,
// and other threads of the process will also move together.
c.add_task_by_tgid(&tgid)
} else {
Err(Error::new(SubsystemsEmpty))
}
} else {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().add_task_by_tgid(&tgid))
}
}
/// set cgroup.type
pub fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()> {
if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
c.set_cgroup_type(cgroup_type)
} else {
Err(Error::new(SubsystemsEmpty))
}
} else {
Err(Error::new(CgroupVersion))
}
}
/// get cgroup.type
pub fn get_cgroup_type(&self) -> Result<String> {
if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
let cgroup_type = c.get_cgroup_type()?;
Ok(cgroup_type)
} else {
Err(Error::new(SubsystemsEmpty))
}
} else {
Err(Error::new(CgroupVersion))
}
}
/// Set notify_on_release to the control group.
pub fn set_notify_on_release(&self, enable: bool) -> Result<()> {
self.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().set_notify_on_release(enable))
}
/// Set release_agent
pub fn set_release_agent(&self, path: &str) -> Result<()> {
self.hier
.root_control_group()
.subsystems()
.iter()
.try_for_each(|sub| sub.to_controller().set_release_agent(path))
}
/// Returns an Iterator that can be used to iterate over the procs that are currently in the
/// control group.
pub fn procs(&self) -> Vec<CgroupPid> {
// Collect the procs from all subsystems
let mut v = if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
c.procs()
} else {
vec![]
}
} else {
self.subsystems()
.iter()
.map(|x| x.to_controller().procs())
.fold(vec![], |mut acc, mut x| {
acc.append(&mut x);
acc
})
};
v.sort();
v.dedup();
v
}
/// Returns an Iterator that can be used to iterate over the tasks that are currently in the
/// control group.
pub fn tasks(&self) -> Vec<CgroupPid> {
// Collect the tasks from all subsystems
let mut v = if self.v2() {
let subsystems = self.subsystems();
if !subsystems.is_empty() {
let c = subsystems[0].to_controller();
c.tasks()
} else {
vec![]
}
} else {
self.subsystems()
.iter()
.map(|x| x.to_controller().tasks())
.fold(vec![], |mut acc, mut x| {
acc.append(&mut x);
acc
})
};
v.sort();
v.dedup();
v
}
/// Checks if the cgroup exists.
///
/// Returns true if at least one subsystem exists.
pub fn exists(&self) -> bool {
self.subsystems().iter().any(|e| e.to_controller().exists())
}
}
pub const UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
fn enable_controllers(controllers: &[String], path: &Path) {
let f = path.join("cgroup.subtree_control");
for c in controllers {
let body = format!("+{}", c);
let _rest = fs::write(f.as_path(), body.as_bytes());
}
}
fn supported_controllers() -> Vec<String> {
let p = format!("{}/{}", UNIFIED_MOUNTPOINT, "cgroup.controllers");
let ret = fs::read_to_string(p.as_str());
ret.unwrap_or_default()
.split(' ')
.map(|x| x.trim().to_string())
.collect::<Vec<String>>()
}
fn create_v2_cgroup(
root: PathBuf,
path: &str,
specified_controllers: &Option<Vec<String>>,
) -> Result<()> {
// controler list ["memory", "cpu"]
let controllers = if let Some(s_controllers) = specified_controllers.clone() {
if verify_supported_controllers(s_controllers.as_ref()) {
s_controllers
} else {
return Err(Error::new(ErrorKind::SpecifiedControllers));
}
} else {
supported_controllers()
};
let mut fp = root;
// enable for root
enable_controllers(&controllers, &fp);
// path: "a/b/c"
let elements = path.split('/').collect::<Vec<&str>>();
let last_index = elements.len() - 1;
// Build up the directory hierarchy element by element, enabling the controllers for all
// parents along the way.
for (i, ele) in elements.iter().enumerate() {
// ROOT/a
fp.push(ele);
// create dir if necessary
if let Err(e) = std::fs::create_dir_all(fp.clone()) {
return Err(Error::with_cause(ErrorKind::FsError, e));
}
if i < last_index {
// enable controllers for subtree
enable_controllers(&controllers, &fp);
}
}
Ok(())
}
pub fn verify_supported_controllers(controllers: &[String]) -> bool {
let sc = supported_controllers();
for controller in controllers.iter() {
if !sc.contains(controller) {
return false;
}
}
true
}
pub fn get_cgroups_relative_paths() -> Result<HashMap<String, String>> {
let path = "/proc/self/cgroup".to_string();
get_cgroups_relative_paths_by_path(path)
}
pub fn get_cgroups_relative_paths_by_pid(pid: u32) -> Result<HashMap<String, String>> {
let path = format!("/proc/{}/cgroup", pid);
get_cgroups_relative_paths_by_path(path)
}
fn get_cgroup_destination(mut mount_root: String, pidpath: String) -> String {
if mount_root == "/" {
mount_root = String::from("");
}
pidpath.trim_start_matches(&mount_root).to_string()
}
pub fn existing_path(paths: HashMap<String, String>) -> Result<HashMap<String, String>> {
let mount_roots_v1 = V1::new();
let mut mount_roots_subsystems_map = HashMap::new();
for s in mount_roots_v1.subsystems().iter() {
let controller_name = s.controller_name();
let path_from_cgroup = paths
.get(&controller_name)
.ok_or(Error::new(Common(format!(
"controller {} found in mountinfo, but not found in cgroup.",
controller_name
))))?;
let path_from_mountinfo = s.to_controller().base().to_string_lossy().to_string();
let des_path = get_cgroup_destination(path_from_mountinfo, path_from_cgroup.to_owned());
mount_roots_subsystems_map.insert(controller_name, des_path);
}
Ok(mount_roots_subsystems_map)
}
fn get_cgroups_relative_paths_by_path(path: String) -> Result<HashMap<String, String>> {
let mut m = HashMap::new();
let content =
fs::read_to_string(path.clone()).map_err(|e| Error::with_cause(ReadFailed(path), e))?;
// cgroup path may have ":" , likes
// "2:cpu,cpuacct:/system.slice/containerd.service/test.slice:cri-containerd:96b37a2edf84351487f42039e137427f1812f678850675fac214caf597ee5e4a"
for line in content.lines() {
if let Some((first_value_part, remaining_path)) =
line.split_once(':').unwrap_or_default().1.split_once(':')
{
let keys: Vec<&str> = first_value_part.split(',').collect();
keys.iter().for_each(|key| {
m.insert(key.to_string(), remaining_path.to_string());
});
}
}
Ok(m)
}

View File

@@ -16,10 +16,10 @@
//! by a call to `build()`.
//!
//! ```rust,no_run
//! # use cgroups_rs::*;
//! # use cgroups_rs::devices::*;
//! # use cgroups_rs::cgroup_builder::*;
//! let h = cgroups_rs::hierarchies::auto();
//! # use cgroups_rs::fs::*;
//! # use cgroups_rs::fs::devices::*;
//! # use cgroups_rs::fs::cgroup_builder::*;
//! let h = cgroups_rs::fs::hierarchies::auto();
//! let cgroup: Cgroup = CgroupBuilder::new("hello")
//! .memory()
//! .kernel_memory_limit(1024 * 1024)
@@ -57,11 +57,11 @@
//! .read(6, 1, 10)
//! .write(11, 1, 100)
//! .done()
//! .build(h);
//! .build(h).unwrap();
//! ```
use crate::{
BlkIoDeviceResource, BlkIoDeviceThrottleResource, Cgroup, DeviceResource, Hierarchy,
use crate::fs::{
BlkIoDeviceResource, BlkIoDeviceThrottleResource, Cgroup, DeviceResource, Error, Hierarchy,
HugePageResource, MaxValue, NetworkPriority, Resources,
};
@@ -80,6 +80,8 @@ pub struct CgroupBuilder {
name: String,
/// Internal, unsupported field: use the associated builders instead.
resources: Resources,
/// List of controllers specifically enabled in the control group.
specified_controllers: Option<Vec<String>>,
}
impl CgroupBuilder {
@@ -90,6 +92,7 @@ impl CgroupBuilder {
CgroupBuilder {
name: name.to_owned(),
resources: Resources::default(),
specified_controllers: None,
}
}
@@ -134,10 +137,22 @@ impl CgroupBuilder {
}
/// Finalize the control group, consuming the builder and creating the control group.
pub fn build(self, hier: Box<dyn Hierarchy>) -> Cgroup {
let cg = Cgroup::new(hier, self.name);
let _ret = cg.apply(&self.resources);
cg
pub fn build(self, hier: Box<dyn Hierarchy>) -> Result<Cgroup, Error> {
if let Some(controllers) = self.specified_controllers {
let cg = Cgroup::new_with_specified_controllers(hier, self.name, Some(controllers))?;
cg.apply(&self.resources)?;
Ok(cg)
} else {
let cg = Cgroup::new(hier, self.name)?;
cg.apply(&self.resources)?;
Ok(cg)
}
}
/// Specifically enable some controllers in the control group.
pub fn set_specified_controllers(mut self, specified_controllers: Vec<String>) -> Self {
self.specified_controllers = Some(specified_controllers);
self
}
}
@@ -235,9 +250,9 @@ impl DeviceResourceBuilder {
mut self,
major: i64,
minor: i64,
devtype: crate::devices::DeviceType,
devtype: crate::fs::devices::DeviceType,
allow: bool,
access: Vec<crate::devices::DevicePermissions>,
access: Vec<crate::fs::devices::DevicePermissions>,
) -> DeviceResourceBuilder {
self.cgroup.resources.devices.devices.push(DeviceResource {
allow,

View File

@@ -13,11 +13,11 @@ use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::{parse_max_value, read_i64_from, read_u64_from};
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::{parse_max_value, read_i64_from, read_u64_from};
use crate::{
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, CpuResources, CustomizedAttribute,
MaxValue, Resources, Subsystem,
};
@@ -59,7 +59,6 @@ impl ControllerInternal for CpuController {
fn get_path(&self) -> &PathBuf {
&self.path
}
fn get_path_mut(&mut self) -> &mut PathBuf {
&mut self.path
}
@@ -113,10 +112,10 @@ impl<'a> From<&'a Subsystem> for &'a CpuController {
impl CpuController {
/// Contructs a new `CpuController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
@@ -131,7 +130,7 @@ impl CpuController {
let res = file.read_to_string(&mut s);
match res {
Ok(_) => Ok(s),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
Err(e) => Err(Error::with_cause(ReadFailed("cpu.stat".to_string()), e)),
}
})
.unwrap_or_default(),
@@ -145,14 +144,15 @@ impl CpuController {
/// `shares` to `200` ensures that control group `B` receives twice as much as CPU bandwidth.
/// (Assuming both `A` and `B` are of the same parent)
pub fn set_shares(&self, shares: u64) -> Result<()> {
let mut file = "cpu.shares";
let mut file_name = "cpu.shares";
if self.v2 {
file = "cpu.weight";
file_name = "cpu.weight";
}
// NOTE: .CpuShares is not used here. Conversion is the caller's responsibility.
self.open_path(file, true).and_then(|mut file| {
file.write_all(shares.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(shares.to_string().as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), shares.to_string()), e)
})
})
}
@@ -174,8 +174,12 @@ impl CpuController {
}
self.open_path("cpu.cfs_period_us", true)
.and_then(|mut file| {
file.write_all(us.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(us.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("cpu.cfs_period_us".to_string(), us.to_string()),
e,
)
})
})
}
@@ -200,8 +204,12 @@ impl CpuController {
}
self.open_path("cpu.cfs_quota_us", true)
.and_then(|mut file| {
file.write_all(us.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(us.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("cpu.cfs_quota_us".to_string(), us.to_string()),
e,
)
})
})
}
@@ -262,23 +270,31 @@ impl CpuController {
let line = format!("{} {}", new_quota, new_period);
self.open_path("cpu.max", true).and_then(|mut file| {
file.write_all(line.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| Error::with_cause(WriteFailed("cpu.max".to_string(), line), e))
})
}
pub fn set_rt_runtime(&self, us: i64) -> Result<()> {
self.open_path("cpu.rt_runtime_us", true)
.and_then(|mut file| {
file.write_all(us.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(us.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("cpu.rt_runtime_us".to_string(), us.to_string()),
e,
)
})
})
}
pub fn set_rt_period_us(&self, us: u64) -> Result<()> {
self.open_path("cpu.rt_period_us", true)
.and_then(|mut file| {
file.write_all(us.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(us.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("cpu.rt_period_us".to_string(), us.to_string()),
e,
)
})
})
}
}
@@ -288,7 +304,7 @@ impl CustomizedAttribute for CpuController {}
fn parse_cfs_quota_and_period(mut file: File) -> Result<CfsQuotaAndPeriod> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed, e))?;
.map_err(|e| Error::with_cause(ReadFailed("cpu.max".to_string()), e))?;
let fields = content.trim().split(' ').collect::<Vec<&str>>();
if fields.len() != 2 {

View File

@@ -10,11 +10,11 @@
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::{read_string_from, read_u64_from};
use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::fs::{read_string_from, read_u64_from};
use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
/// A controller that allows controlling the `cpuacct` subsystem of a Cgroup.
///
@@ -100,10 +100,10 @@ impl<'a> From<&'a Subsystem> for &'a CpuAcctController {
impl CpuAcctController {
/// Contructs a new `CpuAcctController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
@@ -148,8 +148,9 @@ impl CpuAcctController {
/// Reset the statistics the kernel has gathered about the control group.
pub fn reset(&self) -> Result<()> {
self.open_path("cpuacct.usage", true).and_then(|mut file| {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(WriteFailed("cpuacct.usage".to_string(), "0".to_string()), e)
})
})
}
}

View File

@@ -13,11 +13,11 @@ use log::*;
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::{read_string_from, read_u64_from};
use crate::{
use crate::fs::{read_string_from, read_u64_from};
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, CpuResources, Resources, Subsystem,
};
@@ -144,7 +144,12 @@ fn find_no_empty_parent(from: &str, file: &str) -> Result<(String, Vec<PathBuf>)
let current_value =
match ::std::fs::read_to_string(current_path.clone().join(file).to_str().unwrap()) {
Ok(cpus) => String::from(cpus.trim()),
Err(e) => return Err(Error::with_cause(ReadFailed, e)),
Err(e) => {
return Err(Error::with_cause(
ReadFailed(current_path.display().to_string()),
e,
))
}
};
if !current_value.is_empty() {
@@ -177,7 +182,12 @@ fn copy_from_parent(current: &str, file: &str) -> Result<()> {
pb.push(file);
match ::std::fs::write(pb.to_str().unwrap(), value.as_bytes()) {
Ok(_) => (),
Err(e) => return Err(Error::with_cause(WriteFailed, e)),
Err(e) => {
return Err(Error::with_cause(
WriteFailed(pb.display().to_string(), pb.display().to_string()),
e,
))
}
}
}
@@ -244,10 +254,10 @@ fn parse_range(s: String) -> Result<Vec<(u64, u64)>> {
impl CpuSetController {
/// Contructs a new `CpuSetController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
@@ -347,11 +357,19 @@ impl CpuSetController {
self.open_path("cpuset.cpu_exclusive", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.cpu_exclusive".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.cpu_exclusive".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -362,11 +380,19 @@ impl CpuSetController {
self.open_path("cpuset.mem_exclusive", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.mem_exclusive".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.mem_exclusive".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -377,8 +403,9 @@ impl CpuSetController {
/// be represented via dashes.
pub fn set_cpus(&self, cpus: &str) -> Result<()> {
self.open_path("cpuset.cpus", true).and_then(|mut file| {
file.write_all(cpus.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(cpus.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed("cpuset.cpus".to_string(), cpus.to_string()), e)
})
})
}
@@ -387,8 +414,9 @@ impl CpuSetController {
/// Syntax is the same as with `set_cpus()`.
pub fn set_mems(&self, mems: &str) -> Result<()> {
self.open_path("cpuset.mems", true).and_then(|mut file| {
file.write_all(mems.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(mems.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed("cpuset.mems".to_string(), mems.to_string()), e)
})
})
}
@@ -401,11 +429,19 @@ impl CpuSetController {
self.open_path("cpuset.mem_hardwall", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.mem_hardwall".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.mem_hardwall".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -416,11 +452,19 @@ impl CpuSetController {
self.open_path("cpuset.sched_load_balance", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.sched_load_balance".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.sched_load_balance".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -431,8 +475,12 @@ impl CpuSetController {
pub fn set_rebalance_relax_domain_level(&self, i: i64) -> Result<()> {
self.open_path("cpuset.sched_relax_domain_level", true)
.and_then(|mut file| {
file.write_all(i.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(i.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.sched_relax_domain_level".to_string(), i.to_string()),
e,
)
})
})
}
@@ -442,11 +490,19 @@ impl CpuSetController {
self.open_path("cpuset.memory_migrate", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_migrate".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_migrate".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -457,11 +513,19 @@ impl CpuSetController {
self.open_path("cpuset.memory_spread_page", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_spread_page".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_spread_page".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -472,11 +536,19 @@ impl CpuSetController {
self.open_path("cpuset.memory_spread_slab", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_spread_slab".to_string(), "1".to_string()),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed("cpuset.memory_spread_slab".to_string(), "0".to_string()),
e,
)
})
}
})
}
@@ -493,11 +565,25 @@ impl CpuSetController {
self.open_path("cpuset.memory_pressure_enabled", true)
.and_then(|mut file| {
if b {
file.write_all(b"1")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"1").map_err(|e| {
Error::with_cause(
WriteFailed(
"cpuset.memory_pressure_enabled".to_string(),
"1".to_string(),
),
e,
)
})
} else {
file.write_all(b"0")
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(b"0").map_err(|e| {
Error::with_cause(
WriteFailed(
"cpuset.memory_pressure_enabled".to_string(),
"0".to_string(),
),
e,
)
})
}
})
}
@@ -505,7 +591,7 @@ impl CpuSetController {
#[cfg(test)]
mod tests {
use crate::cpuset;
use crate::fs::cpuset;
#[test]
fn test_parse_range() {
let test_cases = vec![
@@ -516,7 +602,7 @@ mod tests {
"1,2,3,4".to_string(),
"1-5,6-7,8-9".to_string(),
];
let expecteds = vec![
let expecteds = [
vec![(1, 1), (2, 2), (4, 6), (9, 9)],
vec![],
vec![(1, 1)],

View File

@@ -12,10 +12,10 @@ use std::path::PathBuf;
use log::*;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::{
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, DeviceResource, DeviceResources,
Resources, Subsystem,
};
@@ -46,6 +46,7 @@ pub enum DeviceType {
Block,
}
#[allow(clippy::derivable_impls)]
impl Default for DeviceType {
fn default() -> Self {
DeviceType::All
@@ -170,9 +171,9 @@ impl ControllerInternal for DevicesController {
for i in &res.devices {
if i.allow {
let _ = self.allow_device(i.devtype, i.major, i.minor, &i.access);
self.allow_device(i.devtype, i.major, i.minor, &i.access)?;
} else {
let _ = self.deny_device(i.devtype, i.major, i.minor, &i.access);
self.deny_device(i.devtype, i.major, i.minor, &i.access)?;
}
}
@@ -203,10 +204,10 @@ impl<'a> From<&'a Subsystem> for &'a DevicesController {
impl DevicesController {
/// Constructs a new `DevicesController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
@@ -237,8 +238,15 @@ impl DevicesController {
};
let final_str = format!("{} {}:{} {}", devtype.to_char(), major, minor, perms);
self.open_path("devices.allow", true).and_then(|mut file| {
file.write_all(final_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(final_str.as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed(
self.get_path().join("devices.allow").display().to_string(),
final_str,
),
e,
)
})
})
}
@@ -269,8 +277,15 @@ impl DevicesController {
};
let final_str = format!("{} {}:{} {}", devtype.to_char(), major, minor, perms);
self.open_path("devices.deny", true).and_then(|mut file| {
file.write_all(final_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(final_str.as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed(
self.get_path().join("devices.deny").display().to_string(),
final_str,
),
e,
)
})
})
}
@@ -280,43 +295,48 @@ impl DevicesController {
let mut s = String::new();
let res = file.read_to_string(&mut s);
match res {
Ok(_) => {
s.lines().fold(Ok(Vec::new()), |acc, line| {
let ls = line.to_string().split(|c| c == ' ' || c == ':').map(|x| x.to_string()).collect::<Vec<String>>();
if acc.is_err() || ls.len() != 4 {
error!("allowed_devices: acc: {:?}, ls: {:?}", acc, ls);
Err(Error::new(ParseError))
} else {
let devtype = DeviceType::from_char(ls[0].chars().next());
let mut major = ls[1].parse::<i64>();
let mut minor = ls[2].parse::<i64>();
if major.is_err() && ls[1] == "*" {
major = Ok(-1);
}
if minor.is_err() && ls[2] == "*" {
minor = Ok(-1);
}
if devtype.is_none() || major.is_err() || minor.is_err() || !DevicePermissions::is_valid(&ls[3]) {
error!("allowed_devices: acc: {:?}, ls: {:?}, devtype: {:?}, major {:?} minor {:?} ls3 {:?}",
acc, ls, devtype, major, minor, &ls[3]);
Err(Error::new(ParseError))
} else {
let access = DevicePermissions::from_str(&ls[3])?;
let mut acc = acc.unwrap();
acc.push(DeviceResource {
allow: true,
devtype: devtype.unwrap(),
major: major.unwrap(),
minor: minor.unwrap(),
access,
});
Ok(acc)
}
}
})
},
Err(e) => Err(Error::with_cause(ReadFailed, e)),
Ok(_) => s
.lines()
.map(|line| parse_device_line(line, true))
.collect(),
Err(e) => Err(Error::with_cause(ReadFailed("devices.list".to_string()), e)),
}
})
}
}
fn parse_device_number(s: &str) -> Result<i64> {
if s == "*" {
Ok(-1)
} else {
s.parse::<i64>().map_err(|_| Error::new(ParseError))
}
}
fn parse_device_line(line: &str, allow: bool) -> Result<DeviceResource> {
let parts: Vec<&str> = line.split([' ', ':']).collect();
if parts.len() != 4 {
error!("allowed_devices: invalid line format: {:?}", line);
return Err(Error::new(ParseError));
}
let devtype = DeviceType::from_char(parts[0].chars().next()).ok_or_else(|| {
error!("allowed_devices: invalid device type: {:?}", parts[0]);
Error::new(ParseError)
})?;
let major = parse_device_number(parts[1]).inspect_err(|_| {
error!("allowed_devices: invalid major number: {:?}", parts[1]);
})?;
let minor = parse_device_number(parts[2]).inspect_err(|_| {
error!("allowed_devices: invalid minor number: {:?}", parts[2]);
})?;
let access = DevicePermissions::from_str(parts[3])?;
Ok(DeviceResource {
allow,
devtype,
major,
minor,
access,
})
}

View File

@@ -8,40 +8,67 @@ use std::error::Error as StdError;
use std::fmt;
/// The different types of errors that can occur while manipulating control groups.
#[derive(Debug, Eq, PartialEq)]
#[derive(thiserror::Error, Debug, Eq, PartialEq)]
pub enum ErrorKind {
#[error("fs error")]
FsError,
#[error("common error: {0}")]
Common(String),
/// An error occured while writing to a control group file.
WriteFailed,
#[error("unable to write to a control group file {0}, value {1}")]
WriteFailed(String, String),
/// An error occured while trying to read from a control group file.
ReadFailed,
#[error("unable to read a control group file {0}")]
ReadFailed(String),
/// An error occured while trying to remove a control group.
#[error("unable to remove a control group")]
RemoveFailed,
/// An error occured while trying to parse a value from a control group file.
///
/// In the future, there will be some information attached to this field.
#[error("unable to parse control group file")]
ParseError,
/// You tried to do something invalid.
///
/// This could be because you tried to set a value in a control group that is not a root
/// control group. Or, when using unified hierarchy, you tried to add a task in a leaf node.
#[error("the requested operation is invalid")]
InvalidOperation,
/// The path of the control group was invalid.
///
/// This could be caused by trying to escape the control group filesystem via a string of "..".
/// This crate checks against this and operations will fail with this error.
#[error("the given path is invalid")]
InvalidPath,
#[error("invalid bytes size")]
InvalidBytesSize,
/// The specified controller is not in the list of supported controllers.
#[error("specified controller is not in the list of supported controllers")]
SpecifiedControllers,
/// Using method in wrong cgroup version.
#[error("using method in wrong cgroup version")]
CgroupVersion,
/// Using method in wrong cgroup mode.
#[error("using method in wrong cgroup mode.")]
CgroupMode,
/// Subsystems is empty.
#[error("subsystems is empty")]
SubsystemsEmpty,
/// An unknown error has occured.
#[error("an unknown error")]
Other,
}
@@ -53,29 +80,16 @@ pub struct Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let msg = match &self.kind {
ErrorKind::FsError => "fs error".to_string(),
ErrorKind::Common(s) => s.clone(),
ErrorKind::WriteFailed => "unable to write to a control group file".to_string(),
ErrorKind::ReadFailed => "unable to read a control group file".to_string(),
ErrorKind::RemoveFailed => "unable to remove a control group".to_string(),
ErrorKind::ParseError => "unable to parse control group file".to_string(),
ErrorKind::InvalidOperation => "the requested operation is invalid".to_string(),
ErrorKind::InvalidPath => "the given path is invalid".to_string(),
ErrorKind::InvalidBytesSize => "invalid bytes size".to_string(),
ErrorKind::Other => "an unknown error".to_string(),
};
if let Some(cause) = &self.cause {
write!(f, "{} caused by: {:?}", msg, cause)
write!(f, "{} caused by: {:?}", &self.kind, cause)
} else {
write!(f, "{}", msg)
write!(f, "{}", &self.kind)
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
#[allow(clippy::manual_map)]
match self.cause {
Some(ref x) => Some(&**x),

View File

@@ -12,8 +12,8 @@ use std::path::Path;
use std::sync::mpsc::{self, Receiver};
use std::thread;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
// notify_on_oom returns channel on which you can expect event about OOM,
// if process died without OOM this channel will be closed.
@@ -46,10 +46,11 @@ fn register_memory_event(
arg: &str,
) -> Result<Receiver<String>> {
let path = cg_dir.join(event_name);
let event_file = File::open(path).map_err(|e| Error::with_cause(ReadFailed, e))?;
let event_file = File::open(path.clone())
.map_err(|e| Error::with_cause(ReadFailed(path.display().to_string()), e))?;
let eventfd =
eventfd(0, EfdFlags::EFD_CLOEXEC).map_err(|e| Error::with_cause(ReadFailed, e))?;
let eventfd = eventfd(0, EfdFlags::EFD_CLOEXEC)
.map_err(|e| Error::with_cause(ReadFailed("eventfd".to_string()), e))?;
let event_control_path = cg_dir.join("cgroup.event_control");
let data = if arg.is_empty() {
@@ -59,7 +60,12 @@ fn register_memory_event(
};
// write to file and set mode to 0700(FIXME)
fs::write(&event_control_path, data).map_err(|e| Error::with_cause(WriteFailed, e))?;
fs::write(&event_control_path, data.clone()).map_err(|e| {
Error::with_cause(
WriteFailed(event_control_path.display().to_string(), data),
e,
)
})?;
let mut eventfd_file = unsafe { File::from_raw_fd(eventfd) };

View File

@@ -11,10 +11,10 @@
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::FreezerState;
/// A controller that allows controlling the `freezer` subsystem of a Cgroup.
///
@@ -31,17 +31,6 @@ pub struct FreezerController {
v2: bool,
}
/// The current state of the control group
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FreezerState {
/// The processes in the control group are _not_ frozen.
Thawed,
/// The processes in the control group are in the processes of being frozen.
Freezing,
/// The processes in the control group are frozen.
Frozen,
}
impl ControllerInternal for FreezerController {
fn control_type(&self) -> Controllers {
Controllers::Freezer
@@ -84,50 +73,49 @@ impl<'a> From<&'a Subsystem> for &'a FreezerController {
impl FreezerController {
/// Contructs a new `FreezerController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
/// Freezes the processes in the control group.
pub fn freeze(&self) -> Result<()> {
let mut file = "freezer.state";
let mut file_name = "freezer.state";
let mut content = "FROZEN".to_string();
if self.v2 {
file = "cgroup.freeze";
file_name = "cgroup.freeze";
content = "1".to_string();
}
self.open_path(file, true).and_then(|mut file| {
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), content), e))
})
}
/// Thaws, that is, unfreezes the processes in the control group.
pub fn thaw(&self) -> Result<()> {
let mut file = "freezer.state";
let mut file_name = "freezer.state";
let mut content = "THAWED".to_string();
if self.v2 {
file = "cgroup.freeze";
file_name = "cgroup.freeze";
content = "0".to_string();
}
self.open_path(file, true).and_then(|mut file| {
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(content.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), content), e))
})
}
/// Retrieve the state of processes in the control group.
pub fn state(&self) -> Result<FreezerState> {
let mut file = "freezer.state";
let mut file_name = "freezer.state";
if self.v2 {
file = "cgroup.freeze";
file_name = "cgroup.freeze";
}
self.open_path(file, false).and_then(|mut file| {
self.open_path(file_name, false).and_then(|mut file| {
let mut s = String::new();
let res = file.read_to_string(&mut s);
match res {
@@ -139,7 +127,7 @@ impl FreezerController {
"FREEZING" => Ok(FreezerState::Freezing),
_ => Err(Error::new(ParseError)),
},
Err(e) => Err(Error::with_cause(ReadFailed, e)),
Err(e) => Err(Error::with_cause(ReadFailed(file_name.to_string()), e)),
}
})
}

View File

@@ -5,38 +5,37 @@
//
//! This module represents the various control group hierarchies the Linux kernel supports.
//!
//! Currently, we only support the cgroupv1 hierarchy, but in the future we will add support for
//! the Unified Hierarchy.
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use crate::blkio::BlkIoController;
use crate::cpu::CpuController;
use crate::cpuacct::CpuAcctController;
use crate::cpuset::CpuSetController;
use crate::devices::DevicesController;
use crate::freezer::FreezerController;
use crate::hugetlb::HugeTlbController;
use crate::memory::MemController;
use crate::net_cls::NetClsController;
use crate::net_prio::NetPrioController;
use crate::perf_event::PerfEventController;
use crate::pid::PidController;
use crate::rdma::RdmaController;
use crate::systemd::SystemdController;
use crate::{Controllers, Hierarchy, Subsystem};
use crate::fs::blkio::BlkIoController;
use crate::fs::cpu::CpuController;
use crate::fs::cpuacct::CpuAcctController;
use crate::fs::cpuset::CpuSetController;
use crate::fs::devices::DevicesController;
use crate::fs::freezer::FreezerController;
use crate::fs::hugetlb::HugeTlbController;
use crate::fs::memory::MemController;
use crate::fs::net_cls::NetClsController;
use crate::fs::net_prio::NetPrioController;
use crate::fs::perf_event::PerfEventController;
use crate::fs::pid::PidController;
use crate::fs::rdma::RdmaController;
use crate::fs::systemd::SystemdController;
use crate::fs::{Controllers, Hierarchy, Subsystem};
use crate::cgroup::Cgroup;
use crate::fs::cgroup::Cgroup;
/// Process mounts information.
///
/// See `proc(5)` for format details.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Mountinfo {
/// Mount root directory of the file system.
pub mount_root: PathBuf,
/// Mount pathname relative to the process's root.
pub mount_point: PathBuf,
/// Filesystem type (main type with optional sub-type).
@@ -57,6 +56,7 @@ pub(crate) fn parse_mountinfo_for_line(line: &str) -> Option<Mountinfo> {
return None;
}
let mount_point = PathBuf::from(s0_values[4]);
let mount_root = PathBuf::from(s0_values[3]);
let fs_type_values: Vec<_> = s1_values[0].trim().split('.').collect();
let fs_type = match fs_type_values.len() {
1 => (fs_type_values[0].to_string(), None),
@@ -69,6 +69,7 @@ pub(crate) fn parse_mountinfo_for_line(line: &str) -> Option<Mountinfo> {
let super_opts: Vec<String> = s1_values[2].trim().split(',').map(String::from).collect();
Some(Mountinfo {
mount_root,
mount_point,
fs_type,
super_opts,
@@ -123,47 +124,53 @@ impl Hierarchy for V1 {
// The cgroup writeback feature requires cooperation between memcgs and blkcgs
// To avoid exceptions, we should add_task for blkcg before memcg(push BlkIo before Mem)
// For more Information: https://www.alibabacloud.com/help/doc-detail/155509.htm
if let Some(root) = self.get_mount_point(Controllers::BlkIo) {
subs.push(Subsystem::BlkIo(BlkIoController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::BlkIo) {
subs.push(Subsystem::BlkIo(BlkIoController::new(point, root, false)));
}
if let Some(root) = self.get_mount_point(Controllers::Mem) {
subs.push(Subsystem::Mem(MemController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::Mem) {
subs.push(Subsystem::Mem(MemController::new(point, root, false)));
}
if let Some(root) = self.get_mount_point(Controllers::Pids) {
subs.push(Subsystem::Pid(PidController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::Pids) {
subs.push(Subsystem::Pid(PidController::new(point, root, false)));
}
if let Some(root) = self.get_mount_point(Controllers::CpuSet) {
subs.push(Subsystem::CpuSet(CpuSetController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::CpuSet) {
subs.push(Subsystem::CpuSet(CpuSetController::new(point, root, false)));
}
if let Some(root) = self.get_mount_point(Controllers::CpuAcct) {
subs.push(Subsystem::CpuAcct(CpuAcctController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::CpuAcct) {
subs.push(Subsystem::CpuAcct(CpuAcctController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::Cpu) {
subs.push(Subsystem::Cpu(CpuController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::Cpu) {
subs.push(Subsystem::Cpu(CpuController::new(point, root, false)));
}
if let Some(root) = self.get_mount_point(Controllers::Devices) {
subs.push(Subsystem::Devices(DevicesController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::Devices) {
subs.push(Subsystem::Devices(DevicesController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::Freezer) {
subs.push(Subsystem::Freezer(FreezerController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::Freezer) {
subs.push(Subsystem::Freezer(FreezerController::new(
point, root, false,
)));
}
if let Some(root) = self.get_mount_point(Controllers::NetCls) {
subs.push(Subsystem::NetCls(NetClsController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::NetCls) {
subs.push(Subsystem::NetCls(NetClsController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::PerfEvent) {
subs.push(Subsystem::PerfEvent(PerfEventController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::PerfEvent) {
subs.push(Subsystem::PerfEvent(PerfEventController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::NetPrio) {
subs.push(Subsystem::NetPrio(NetPrioController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::NetPrio) {
subs.push(Subsystem::NetPrio(NetPrioController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::HugeTlb) {
subs.push(Subsystem::HugeTlb(HugeTlbController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::HugeTlb) {
subs.push(Subsystem::HugeTlb(HugeTlbController::new(
point, root, false,
)));
}
if let Some(root) = self.get_mount_point(Controllers::Rdma) {
subs.push(Subsystem::Rdma(RdmaController::new(root)));
if let Some((point, root)) = self.get_mount_point(Controllers::Rdma) {
subs.push(Subsystem::Rdma(RdmaController::new(point, root)));
}
if let Some(root) = self.get_mount_point(Controllers::Systemd) {
subs.push(Subsystem::Systemd(SystemdController::new(root, false)));
if let Some((point, root)) = self.get_mount_point(Controllers::Systemd) {
subs.push(Subsystem::Systemd(SystemdController::new(
point, root, false,
)));
}
subs
@@ -173,6 +180,12 @@ impl Hierarchy for V1 {
Cgroup::load(auto(), "")
}
fn parent_control_group(&self, path: &str) -> Cgroup {
let path = Path::new(path);
let parent_path = path.parent().unwrap().to_string_lossy().to_string();
Cgroup::load(auto(), parent_path)
}
fn root(&self) -> PathBuf {
self.mountinfo
.iter()
@@ -212,29 +225,51 @@ impl Hierarchy for V2 {
for s in controller_list {
match s {
"cpu" => {
subs.push(Subsystem::Cpu(CpuController::new(self.root(), true)));
subs.push(Subsystem::Cpu(CpuController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"io" => {
subs.push(Subsystem::BlkIo(BlkIoController::new(self.root(), true)));
subs.push(Subsystem::BlkIo(BlkIoController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"cpuset" => {
subs.push(Subsystem::CpuSet(CpuSetController::new(self.root(), true)));
subs.push(Subsystem::CpuSet(CpuSetController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"memory" => {
subs.push(Subsystem::Mem(MemController::new(self.root(), true)));
subs.push(Subsystem::Mem(MemController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"pids" => {
subs.push(Subsystem::Pid(PidController::new(self.root(), true)));
subs.push(Subsystem::Pid(PidController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"freezer" => {
subs.push(Subsystem::Freezer(FreezerController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
"hugetlb" => {
subs.push(Subsystem::HugeTlb(HugeTlbController::new(
self.root(),
PathBuf::from(""),
true,
)));
}
@@ -249,6 +284,12 @@ impl Hierarchy for V2 {
Cgroup::load(auto(), "")
}
fn parent_control_group(&self, path: &str) -> Cgroup {
let path = Path::new(path);
let parent_path = path.parent().unwrap().to_string_lossy().to_string();
Cgroup::load(auto(), parent_path)
}
fn root(&self) -> PathBuf {
PathBuf::from(self.root.clone())
}
@@ -263,10 +304,10 @@ impl V1 {
}
}
pub fn get_mount_point(&self, controller: Controllers) -> Option<PathBuf> {
pub fn get_mount_point(&self, controller: Controllers) -> Option<(PathBuf, PathBuf)> {
self.mountinfo.iter().find_map(|m| {
if m.fs_type.0 == "cgroup" && m.super_opts.contains(&controller.to_string()) {
return Some(m.mount_point.clone());
return Some((m.mount_point.to_owned(), m.mount_root.to_owned()));
}
None
})
@@ -297,43 +338,16 @@ impl Default for V2 {
pub const UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
#[cfg(any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "android"
))]
pub fn is_cgroup2_unified_mode() -> bool {
use nix::sys::statfs;
let path = std::path::Path::new(UNIFIED_MOUNTPOINT);
let fs_stat = statfs::statfs(path);
if fs_stat.is_err() {
return false;
}
let fs_stat = match statfs::statfs(path) {
Ok(fs_stat) => fs_stat,
Err(_) => return false,
};
// FIXME notwork, nix will not compile CGROUP2_SUPER_MAGIC because not(target_env = "musl")
fs_stat.unwrap().filesystem_type() == statfs::CGROUP2_SUPER_MAGIC
}
pub const INIT_CGROUP_PATHS: &str = "/proc/1/cgroup";
#[cfg(all(target_os = "linux", target_env = "musl"))]
pub fn is_cgroup2_unified_mode() -> bool {
let lines = fs::read_to_string(INIT_CGROUP_PATHS);
if lines.is_err() {
return false;
}
for line in lines.unwrap().lines() {
let fields: Vec<&str> = line.split(':').collect();
if fields.len() != 3 {
continue;
}
if fields[0] != "0" {
return false;
}
}
true
fs_stat.filesystem_type() == statfs::CGROUP2_SUPER_MAGIC
}
pub fn auto() -> Box<dyn Hierarchy> {
@@ -352,19 +366,19 @@ mod tests {
fn test_parse_mount() {
let mountinfo = vec![
("29 26 0:26 / /sys/fs/cgroup/cpuset,cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,cpuset,cpu,cpuacct",
Mountinfo{mount_point: PathBuf::from("/sys/fs/cgroup/cpuset,cpu,cpuacct"), fs_type: ("cgroup".to_string(), None), super_opts: vec![
Mountinfo{mount_root: PathBuf::from("/"), mount_point: PathBuf::from("/sys/fs/cgroup/cpuset,cpu,cpuacct"), fs_type: ("cgroup".to_string(), None), super_opts: vec![
"rw".to_string(),
"cpuset".to_string(),
"cpu".to_string(),
"cpuacct".to_string(),
]}),
("121 1731 0:42 / /shm rw,nosuid,nodev,noexec,relatime shared:68 master:66 - tmpfs shm rw,size=65536k",
Mountinfo{mount_point: PathBuf::from("/shm"), fs_type: ("tmpfs".to_string(), None), super_opts: vec![
Mountinfo{mount_root: PathBuf::from("/"), mount_point: PathBuf::from("/shm"), fs_type: ("tmpfs".to_string(), None), super_opts: vec![
"rw".to_string(),
"size=65536k".to_string(),
]}),
("121 1731 0:42 / /shm rw,nosuid,nodev,noexec,relatime shared:68 master:66 - tmpfs.123 shm rw,size=65536k",
Mountinfo{mount_point: PathBuf::from("/shm"), fs_type: ("tmpfs".to_string(), Some("123".to_string())), super_opts: vec![
Mountinfo{mount_root: PathBuf::from("/"), mount_point: PathBuf::from("/shm"), fs_type: ("tmpfs".to_string(), Some("123".to_string())), super_opts: vec![
"rw".to_string(),
"size=65536k".to_string(),
]}),

View File

@@ -12,11 +12,11 @@ use log::warn;
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::{flat_keyed_to_vec, read_u64_from};
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::{flat_keyed_to_vec, read_u64_from};
use crate::{
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, HugePageResources, Resources, Subsystem,
};
@@ -88,11 +88,11 @@ impl<'a> From<&'a Subsystem> for &'a HugeTlbController {
impl HugeTlbController {
/// Constructs a new `HugeTlbController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
let sizes = get_hugepage_sizes();
Self {
base: root.clone(),
path: root,
base: root,
path: point,
sizes,
v2,
}
@@ -138,8 +138,11 @@ impl HugeTlbController {
/// Get the limit (in bytes) of how much memory can be backed by hugepages of a certain size
/// (`hugetlb_size`).
pub fn limit_in_bytes(&self, hugetlb_size: &str) -> Result<u64> {
self.open_path(&format!("hugetlb.{}.limit_in_bytes", hugetlb_size), false)
.and_then(read_u64_from)
let mut file_name = format!("hugetlb.{}.limit_in_bytes", hugetlb_size);
if self.v2 {
file_name = format!("hugetlb.{}.max", hugetlb_size);
}
self.open_path(&file_name, false).and_then(read_u64_from)
}
/// Get the current usage of memory that is backed by hugepages of a certain size
@@ -165,19 +168,19 @@ impl HugeTlbController {
/// Set the limit (in bytes) of how much memory can be backed by hugepages of a certain size
/// (`hugetlb_size`).
pub fn set_limit_in_bytes(&self, hugetlb_size: &str, limit: u64) -> Result<()> {
let mut file = format!("hugetlb.{}.limit_in_bytes", hugetlb_size);
let mut file_name = format!("hugetlb.{}.limit_in_bytes", hugetlb_size);
if self.v2 {
file = format!("hugetlb.{}.max", hugetlb_size);
file_name = format!("hugetlb.{}.max", hugetlb_size);
}
self.open_path(&file, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(&file_name, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), limit.to_string()), e)
})
})
}
}
pub const HUGEPAGESIZE_DIR: &str = "/sys/kernel/mm/hugepages";
use regex::Regex;
use std::collections::HashMap;
use std::fs;
@@ -259,37 +262,46 @@ pub fn get_decimal_abbrs() -> Vec<String> {
}
fn parse_size(s: &str, m: &HashMap<String, u128>) -> Result<u128> {
let re = Regex::new(r"(?P<num>\d+)(?P<mul>[kKmMgGtTpP]?)[bB]?$");
// Remove leading/trailing whitespace.
let s = s.trim();
if re.is_err() {
// Remove an optional trailing 'b' or 'B'
let s = if let Some(stripped) = s.strip_suffix('b').or_else(|| s.strip_suffix('B')) {
stripped
} else {
s
};
// Ensure that the string is not empty after stripping.
if s.is_empty() {
return Err(Error::new(InvalidBytesSize));
}
let caps = re.unwrap().captures(s).unwrap();
let num = caps.name("num");
let size: u128 = if let Some(num) = num {
let n = num.as_str().trim().parse::<u128>();
if n.is_err() {
return Err(Error::new(InvalidBytesSize));
}
n.unwrap()
} else {
// The last character should be the multiplier letter.
let last_char = s.chars().last().unwrap();
if !"kKmMgGtTpP".contains(last_char) {
return Err(Error::new(InvalidBytesSize));
};
}
let q = caps.name("mul");
let mul: u128 = if let Some(q) = q {
let t = m.get(q.as_str());
if let Some(t) = t {
*t
} else {
return Err(Error::new(InvalidBytesSize));
}
} else {
// The numeric part is everything before the multiplier letter.
let num_part = &s[..s.len() - last_char.len_utf8()];
if num_part.trim().is_empty() {
return Err(Error::new(InvalidBytesSize));
};
}
Ok(size * mul)
// Parse the numeric part into a u128.
let number: u128 = num_part
.trim()
.parse()
.map_err(|_| Error::new(InvalidBytesSize))?;
// Look up the multiplier in the provided HashMap.
let multiplier_key = last_char.to_string();
let multiplier = m
.get(&multiplier_key)
.ok_or_else(|| Error::new(InvalidBytesSize))?;
Ok(number * multiplier)
}
fn custom_size(mut size: f64, base: f64, m: &[String]) -> String {
@@ -301,3 +313,60 @@ fn custom_size(mut size: f64, base: f64, m: &[String]) -> String {
format!("{}{}", size, m[i].as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binary_size_valid() {
let m = get_binary_size_map();
// Valid inputs must include a multiplier letter.
assert_eq!(parse_size("1k", &m).unwrap(), KiB);
assert_eq!(parse_size("2m", &m).unwrap(), 2 * MiB);
assert_eq!(parse_size("3g", &m).unwrap(), 3 * GiB);
assert_eq!(parse_size("4t", &m).unwrap(), 4 * TiB);
assert_eq!(parse_size("5p", &m).unwrap(), 5 * PiB);
}
#[test]
fn test_decimal_size_valid() {
let m = get_decimal_size_map();
assert_eq!(parse_size("1k", &m).unwrap(), KB);
assert_eq!(parse_size("2m", &m).unwrap(), 2 * MB);
assert_eq!(parse_size("3g", &m).unwrap(), 3 * GB);
assert_eq!(parse_size("4t", &m).unwrap(), 4 * TB);
assert_eq!(parse_size("5p", &m).unwrap(), 5 * PB);
}
#[test]
fn test_trailing_b_suffix() {
let m = get_binary_size_map();
// Trailing 'b' or 'B' should be accepted.
assert_eq!(parse_size("1kb", &m).unwrap(), KiB);
assert_eq!(parse_size("2mB", &m).unwrap(), 2 * MiB);
}
#[test]
fn test_invalid_inputs() {
let m = get_binary_size_map();
// Missing multiplier letter results in error.
assert!(parse_size("1", &m).is_err());
// Invalid multiplier letter.
assert!(parse_size("10x", &m).is_err());
// Non-numeric input.
assert!(parse_size("abc", &m).is_err());
// Only multiplier letter with no number.
assert!(parse_size("k", &m).is_err());
// Number with an invalid trailing character.
assert!(parse_size("123z", &m).is_err());
}
#[test]
fn test_uppercase_multiplier_fails() {
let m = get_binary_size_map();
// Although the regex matches uppercase letters, the provided map only contains lowercase keys.
// Therefore, "1K" does not match any key and should produce an error.
assert!(parse_size("1K", &m).is_err());
}
}

View File

@@ -14,14 +14,14 @@ use std::io::Write;
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::events;
use crate::{read_i64_from, read_string_from, read_u64_from};
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::events;
use crate::fs::{read_i64_from, read_string_from, read_u64_from};
use crate::flat_keyed_to_hashmap;
use crate::fs::flat_keyed_to_hashmap;
use crate::{
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, CustomizedAttribute, MaxValue,
MemoryResources, Resources, Subsystem,
};
@@ -142,9 +142,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
let hier_unevict_line = ls.next().unwrap_or_default();
Ok(NumaStat {
total_pages: total_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
total_pages: total_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0),
total_pages_per_node: {
@@ -157,9 +155,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
})
.collect()
},
file_pages: file_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
file_pages: file_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0),
file_pages_per_node: {
@@ -172,9 +168,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
})
.collect()
},
anon_pages: anon_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
anon_pages: anon_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0),
anon_pages_per_node: {
@@ -187,9 +181,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
})
.collect()
},
unevictable_pages: unevict_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
unevictable_pages: unevict_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0),
unevictable_pages_per_node: {
@@ -204,9 +196,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
},
hierarchical_total_pages: {
if !hier_total_line.is_empty() {
hier_total_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
hier_total_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0)
} else {
@@ -229,9 +219,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
},
hierarchical_file_pages: {
if !hier_file_line.is_empty() {
hier_file_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
hier_file_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0)
} else {
@@ -254,9 +242,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
},
hierarchical_anon_pages: {
if !hier_anon_line.is_empty() {
hier_anon_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
hier_anon_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0)
} else {
@@ -279,9 +265,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
},
hierarchical_unevictable_pages: {
if !hier_unevict_line.is_empty() {
hier_unevict_line
.split(|x| x == ' ' || x == '=')
.collect::<Vec<_>>()[1]
hier_unevict_line.split([' ', '=']).collect::<Vec<_>>()[1]
.parse::<u64>()
.unwrap_or(0)
} else {
@@ -540,10 +524,10 @@ impl ControllerInternal for MemController {
impl MemController {
/// Contructs a new `MemController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
@@ -562,8 +546,9 @@ impl MemController {
if let Some(v) = v {
let v = v.to_string();
self.open_path(f, true).and_then(|mut file| {
file.write_all(v.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(v.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(f.to_string(), format!("{:?}", v)), e)
})
})?;
}
}
@@ -572,18 +557,33 @@ impl MemController {
// for v2
pub fn get_mem(&self) -> Result<SetMemory> {
let mut m: SetMemory = Default::default();
self.get_max_value("memory.high")
.map(|x| m.high = Some(x))?;
self.get_max_value("memory.low").map(|x| m.low = Some(x))?;
self.get_max_value("memory.max").map(|x| m.max = Some(x))?;
self.get_max_value("memory.min").map(|x| m.min = Some(x))?;
let m = SetMemory {
high: self
.get_max_value("memory.high")
.map_or(Some(MaxValue::default()), Some),
low: self
.get_max_value("memory.low")
.map_or(Some(MaxValue::Value(0)), Some),
max: self
.get_max_value("memory.max")
.map_or(Some(MaxValue::default()), Some),
min: self
.get_max_value("memory.min")
.map_or(Some(MaxValue::Value(0)), Some),
};
Ok(m)
}
fn memory_stat_v2(&self) -> Memory {
let set = self.get_mem().unwrap();
// NOTE: get_mem() always returns T, but let's
// still do `unwrap_or` for safety.
let set = self.get_mem().unwrap_or(SetMemory {
low: Some(MaxValue::Value(0)),
high: Some(MaxValue::default()),
max: Some(MaxValue::default()),
min: Some(MaxValue::Value(0)),
});
Memory {
fail_cnt: 0,
@@ -592,7 +592,10 @@ impl MemController {
.open_path("memory.current", false)
.and_then(read_u64_from)
.unwrap_or(0),
max_usage_in_bytes: 0,
max_usage_in_bytes: self
.open_path("memory.peak", false)
.and_then(read_u64_from)
.unwrap_or(0),
move_charge_at_immigrate: 0,
numa_stat: NumaStat::default(),
oom_control: OomControl::default(),
@@ -726,7 +729,7 @@ impl MemController {
.open_path("memory.swap.events", false)
.and_then(flat_keyed_to_hashmap)
.map(|x| *x.get("fail").unwrap_or(&0) as u64)
.unwrap(),
.unwrap_or(0),
limit_in_bytes: self
.open_path("memory.swap.max", false)
.and_then(read_i64_from)
@@ -735,7 +738,10 @@ impl MemController {
.open_path("memory.swap.current", false)
.and_then(read_u64_from)
.unwrap_or(0),
max_usage_in_bytes: 0,
max_usage_in_bytes: self
.open_path("memory.swap.peak", false)
.and_then(read_u64_from)
.unwrap_or(0),
}
}
@@ -769,8 +775,12 @@ impl MemController {
/// Reset the fail counter
pub fn reset_fail_count(&self) -> Result<()> {
self.open_path("memory.failcnt", true).and_then(|mut file| {
file.write_all("0".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("0".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.failcnt".to_string(), "0".to_string()),
e,
)
})
})
}
@@ -783,8 +793,12 @@ impl MemController {
self.open_path("memory.kmem.failcnt", true)
.and_then(|mut file| {
file.write_all("0".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("0".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.kmem.failcnt".to_string(), "0".to_string()),
e,
)
})
})
}
@@ -797,8 +811,12 @@ impl MemController {
self.open_path("memory.kmem.tcp.failcnt", true)
.and_then(|mut file| {
file.write_all("0".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("0".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.kmem.tcp.failcnt".to_string(), "0".to_string()),
e,
)
})
})
}
@@ -806,8 +824,12 @@ impl MemController {
pub fn reset_memswap_fail_count(&self) -> Result<()> {
self.open_path("memory.memsw.failcnt", true)
.and_then(|mut file| {
file.write_all("0".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("0".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.memsw.failcnt".to_string(), "0".to_string()),
e,
)
})
})
}
@@ -815,20 +837,28 @@ impl MemController {
pub fn reset_max_usage(&self) -> Result<()> {
self.open_path("memory.max_usage_in_bytes", true)
.and_then(|mut file| {
file.write_all("0".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("0".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.max_usage_in_bytes".to_string(), "0".to_string()),
e,
)
})
})
}
/// Set the memory usage limit of the control group, in bytes.
pub fn set_limit(&self, limit: i64) -> Result<()> {
let mut file = "memory.limit_in_bytes";
let mut file_name = "memory.limit_in_bytes";
let mut limit_str = limit.to_string();
if self.v2 {
file = "memory.max";
file_name = "memory.max";
if limit == -1 {
limit_str = "max".to_string();
}
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(limit_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e))
})
}
@@ -848,20 +878,27 @@ impl MemController {
warn!("memory.kmem.limit_in_bytes is unsupported by the kernel");
Ok(())
}
Err(e) => Err(Error::with_cause(WriteFailed, e)),
Err(e) => Err(Error::with_cause(
WriteFailed("memory.kmem.limit_in_bytes".to_string(), limit.to_string()),
e,
)),
}
})
}
/// Set the memory+swap limit of the control group, in bytes.
pub fn set_memswap_limit(&self, limit: i64) -> Result<()> {
let mut file = "memory.memsw.limit_in_bytes";
let mut file_name = "memory.memsw.limit_in_bytes";
let mut limit_str = limit.to_string();
if self.v2 {
file = "memory.swap.max";
file_name = "memory.swap.max";
if limit == -1 {
limit_str = "max".to_string();
}
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(limit_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e))
})
}
@@ -874,8 +911,15 @@ impl MemController {
self.open_path("memory.kmem.tcp.limit_in_bytes", true)
.and_then(|mut file| {
file.write_all(limit.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(limit.to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed(
"memory.kmem.tcp.limit_in_bytes".to_string(),
limit.to_string(),
),
e,
)
})
})
}
@@ -884,13 +928,14 @@ impl MemController {
/// This limit is enforced when the system is nearing OOM conditions. Contrast this with the
/// hard limit, which is _always_ enforced.
pub fn set_soft_limit(&self, limit: i64) -> Result<()> {
let mut file = "memory.soft_limit_in_bytes";
let mut file_name = "memory.soft_limit_in_bytes";
if self.v2 {
file = "memory.low"
file_name = "memory.low"
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(limit.to_string().as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), limit.to_string()), e)
})
})
}
@@ -899,22 +944,27 @@ impl MemController {
///
/// Note that a value of zero does not imply that the process will not be swapped out.
pub fn set_swappiness(&self, swp: u64) -> Result<()> {
let mut file = "memory.swappiness";
let mut file_name = "memory.swappiness";
if self.v2 {
file = "memory.swap.max"
file_name = "memory.swap.max"
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(swp.to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(swp.to_string().as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(file_name.to_string(), swp.to_string()), e)
})
})
}
pub fn disable_oom_killer(&self) -> Result<()> {
self.open_path("memory.oom_control", true)
.and_then(|mut file| {
file.write_all("1".to_string().as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all("1".to_string().as_ref()).map_err(|e| {
Error::with_cause(
WriteFailed("memory.oom_control".to_string(), "1".to_string()),
e,
)
})
})
}
@@ -952,7 +1002,7 @@ impl<'a> From<&'a Subsystem> for &'a MemController {
#[cfg(test)]
mod tests {
use crate::memory::{
use crate::fs::memory::{
parse_memory_stat, parse_numa_stat, parse_oom_control, MemoryStat, NumaStat, OomControl,
};

995
src/fs/mod.rs Normal file
View File

@@ -0,0 +1,995 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2020 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
#![allow(clippy::unnecessary_unwrap)]
use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
macro_rules! update_and_test {
($self: ident, $set_func:ident, $value:expr, $get_func:ident) => {
if let Some(v) = $value {
$self.$set_func(v)?;
if $self.$get_func()? != v {
return Err(Error::new(Other));
}
}
};
}
macro_rules! update {
($self: ident, $set_func:ident, $value:expr) => {
if let Some(v) = $value {
let _ = $self.$set_func(v);
}
};
}
pub mod blkio;
pub mod cgroup;
pub mod cgroup_builder;
pub mod cpu;
pub mod cpuacct;
pub mod cpuset;
pub mod devices;
pub mod error;
pub mod events;
pub mod freezer;
pub mod hierarchies;
pub mod hugetlb;
pub mod memory;
pub mod net_cls;
pub mod net_prio;
pub mod perf_event;
pub mod pid;
pub mod rdma;
pub mod systemd;
use crate::fs::blkio::BlkIoController;
use crate::fs::cpu::CpuController;
use crate::fs::cpuacct::CpuAcctController;
use crate::fs::cpuset::CpuSetController;
use crate::fs::devices::DevicesController;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::fs::freezer::FreezerController;
use crate::fs::hugetlb::HugeTlbController;
use crate::fs::memory::MemController;
use crate::fs::net_cls::NetClsController;
use crate::fs::net_prio::NetPrioController;
use crate::fs::perf_event::PerfEventController;
use crate::fs::pid::PidController;
use crate::fs::rdma::RdmaController;
use crate::fs::systemd::SystemdController;
#[doc(inline)]
pub use crate::fs::cgroup::Cgroup;
/// Contains all the subsystems that are available in this crate.
#[derive(Debug, Clone)]
pub enum Subsystem {
/// Controller for the `Pid` subsystem, see `PidController` for more information.
Pid(PidController),
/// Controller for the `Mem` subsystem, see `MemController` for more information.
Mem(MemController),
/// Controller for the `CpuSet subsystem, see `CpuSetController` for more information.
CpuSet(CpuSetController),
/// Controller for the `CpuAcct` subsystem, see `CpuAcctController` for more information.
CpuAcct(CpuAcctController),
/// Controller for the `Cpu` subsystem, see `CpuController` for more information.
Cpu(CpuController),
/// Controller for the `Devices` subsystem, see `DevicesController` for more information.
Devices(DevicesController),
/// Controller for the `Freezer` subsystem, see `FreezerController` for more information.
Freezer(FreezerController),
/// Controller for the `NetCls` subsystem, see `NetClsController` for more information.
NetCls(NetClsController),
/// Controller for the `BlkIo` subsystem, see `BlkIoController` for more information.
BlkIo(BlkIoController),
/// Controller for the `PerfEvent` subsystem, see `PerfEventController` for more information.
PerfEvent(PerfEventController),
/// Controller for the `NetPrio` subsystem, see `NetPrioController` for more information.
NetPrio(NetPrioController),
/// Controller for the `HugeTlb` subsystem, see `HugeTlbController` for more information.
HugeTlb(HugeTlbController),
/// Controller for the `Rdma` subsystem, see `RdmaController` for more information.
Rdma(RdmaController),
/// Controller for the `Systemd` subsystem, see `SystemdController` for more information.
Systemd(SystemdController),
}
#[doc(hidden)]
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum Controllers {
Pids,
Mem,
CpuSet,
CpuAcct,
Cpu,
Devices,
Freezer,
NetCls,
BlkIo,
PerfEvent,
NetPrio,
HugeTlb,
Rdma,
Systemd,
}
impl fmt::Display for Controllers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Controllers::Pids => write!(f, "pids"),
Controllers::Mem => write!(f, "memory"),
Controllers::CpuSet => write!(f, "cpuset"),
Controllers::CpuAcct => write!(f, "cpuacct"),
Controllers::Cpu => write!(f, "cpu"),
Controllers::Devices => write!(f, "devices"),
Controllers::Freezer => write!(f, "freezer"),
Controllers::NetCls => write!(f, "net_cls"),
Controllers::BlkIo => write!(f, "blkio"),
Controllers::PerfEvent => write!(f, "perf_event"),
Controllers::NetPrio => write!(f, "net_prio"),
Controllers::HugeTlb => write!(f, "hugetlb"),
Controllers::Rdma => write!(f, "rdma"),
Controllers::Systemd => write!(f, "name=systemd"),
}
}
}
mod sealed {
use super::*;
pub trait ControllerInternal {
fn apply(&self, res: &Resources) -> Result<()>;
// meta stuff
fn control_type(&self) -> Controllers;
fn get_path(&self) -> &PathBuf;
fn get_path_mut(&mut self) -> &mut PathBuf;
fn get_base(&self) -> &PathBuf;
/// Hooks running after controller crated, if have
fn post_create(&self) {}
fn is_v2(&self) -> bool {
false
}
fn open_path(&self, p: &str, w: bool) -> Result<File> {
let mut path = self.get_path().clone();
path.push(p);
if w {
match File::create(&path) {
Err(e) => Err(Error::with_cause(
ErrorKind::WriteFailed(
path.display().to_string(),
"[CREATE FILE]".to_string(),
),
e,
)),
Ok(file) => Ok(file),
}
} else {
match File::open(&path) {
Err(e) => Err(Error::with_cause(
ErrorKind::ReadFailed(path.display().to_string()),
e,
)),
Ok(file) => Ok(file),
}
}
}
fn get_max_value(&self, f: &str) -> Result<MaxValue> {
self.open_path(f, false).and_then(|mut file| {
let mut string = String::new();
let res = file.read_to_string(&mut string);
match res {
Ok(_) => parse_max_value(&string),
Err(e) => Err(Error::with_cause(ReadFailed(f.to_string()), e)),
}
})
}
#[doc(hidden)]
fn path_exists(&self, p: &str) -> bool {
std::path::Path::new(p).exists()
}
}
pub trait CustomizedAttribute: ControllerInternal {
fn set(&self, key: &str, value: &str) -> Result<()> {
self.open_path(key, true).and_then(|mut file| {
file.write_all(value.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed(key.to_string(), value.to_string()), e)
})
})
}
#[allow(dead_code)]
fn get(&self, key: &str) -> Result<String> {
self.open_path(key, false).and_then(|mut file: File| {
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => Ok(string.trim().to_owned()),
Err(e) => Err(Error::with_cause(ReadFailed(key.to_string()), e)),
}
})
}
}
}
pub(crate) use crate::fs::sealed::{ControllerInternal, CustomizedAttribute};
use crate::CgroupPid;
/// A Controller is a subsystem attached to the control group.
///
/// Implementors are able to control certain aspects of a control group.
pub trait Controller {
#[doc(hidden)]
fn control_type(&self) -> Controllers;
/// The file system path to the controller.
fn path(&self) -> &Path;
/// Root path of the file system to the controller.
fn base(&self) -> &Path;
/// Apply a set of resources to the Controller, invoking its internal functions to pass the
/// kernel the information.
fn apply(&self, res: &Resources) -> Result<()>;
/// Create this controller
fn create(&self) -> Result<()>;
/// Does this controller already exist?
fn exists(&self) -> bool;
/// Set notify_on_release
fn set_notify_on_release(&self, enable: bool) -> Result<()>;
/// Set release_agent
fn set_release_agent(&self, path: &str) -> Result<()>;
/// Delete the controller.
fn delete(&self) -> Result<()>;
/// Attach a task to this controller.
fn add_task(&self, pid: &CgroupPid) -> Result<()>;
/// Attach a task to this controller.
fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()>;
/// set cgroup type.
fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()>;
/// get cgroup type.
fn get_cgroup_type(&self) -> Result<String>;
/// Get the list of tasks that this controller has.
fn tasks(&self) -> Vec<CgroupPid>;
/// Get the list of procs that this controller has.
fn procs(&self) -> Vec<CgroupPid>;
fn v2(&self) -> bool;
}
impl<T> Controller for T
where
T: ControllerInternal,
{
fn control_type(&self) -> Controllers {
ControllerInternal::control_type(self)
}
fn path(&self) -> &Path {
self.get_path()
}
fn base(&self) -> &Path {
self.get_base()
}
/// Apply a set of resources to the Controller, invoking its internal functions to pass the
/// kernel the information.
fn apply(&self, res: &Resources) -> Result<()> {
ControllerInternal::apply(self, res)
}
/// Create this controller
fn create(&self) -> Result<()> {
std::fs::create_dir_all(self.get_path())
.map_err(|err| Error::with_cause(ErrorKind::FsError, err))?;
self.post_create();
Ok(())
}
/// Set notify_on_release
fn set_notify_on_release(&self, enable: bool) -> Result<()> {
if self.is_v2() {
return Err(Error::new(ErrorKind::CgroupVersion));
}
self.open_path("notify_on_release", true)
.and_then(|mut file| {
write!(file, "{}", enable as i32).map_err(|e| {
Error::with_cause(
ErrorKind::WriteFailed("notify_on_release".to_string(), enable.to_string()),
e,
)
})
})
}
/// Set release_agent
fn set_release_agent(&self, path: &str) -> Result<()> {
if self.is_v2() {
return Err(Error::new(ErrorKind::CgroupVersion));
}
self.open_path("release_agent", true).and_then(|mut file| {
file.write_all(path.as_bytes()).map_err(|e| {
Error::with_cause(
ErrorKind::WriteFailed("release_agent".to_string(), path.to_string()),
e,
)
})
})
}
/// Does this controller already exist?
fn exists(&self) -> bool {
self.get_path().exists()
}
/// Delete the controller.
fn delete(&self) -> Result<()> {
if !self.get_path().exists() {
return Ok(());
}
// Compatible with runC for remove dir operation
// https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L272
//
// We trying to remove all paths five times with increasing delay between tries.
// If after all there are not removed cgroups - appropriate error will be
// returned.
let mut delay = std::time::Duration::from_millis(10);
let cgroup_path = self.get_path();
for _i in 0..4 {
if let Ok(()) = remove_dir(cgroup_path) {
return Ok(());
}
std::thread::sleep(delay);
delay *= 2;
}
remove_dir(cgroup_path)
}
/// Attach a task to this controller.
fn add_task(&self, pid: &CgroupPid) -> Result<()> {
let mut file_name = "tasks";
if self.is_v2() {
file_name = "cgroup.threads";
}
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(pid.pid.to_string().as_ref()).map_err(|e| {
Error::with_cause(
ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()),
e,
)
})
})
}
/// Attach a task to this controller by thread group id.
fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()> {
let file_name = "cgroup.procs";
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(pid.pid.to_string().as_ref()).map_err(|e| {
Error::with_cause(
ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()),
e,
)
})
})
}
/// Get the list of procs that this controller has.
fn procs(&self) -> Vec<CgroupPid> {
let file_name = "cgroup.procs";
self.open_path(file_name, false)
.map(|file| {
let bf = BufReader::new(file);
let mut v = Vec::new();
for line in bf.lines() {
match line {
Ok(line) => {
let n = line.trim().parse().unwrap_or(0u64);
v.push(n);
}
Err(_) => break,
}
}
v.into_iter().map(CgroupPid::from).collect()
})
.unwrap_or_default()
}
/// Get the list of tasks that this controller has.
fn tasks(&self) -> Vec<CgroupPid> {
let mut file_name = "tasks";
if self.is_v2() {
file_name = "cgroup.threads";
}
self.open_path(file_name, false)
.map(|file| {
let bf = BufReader::new(file);
let mut v = Vec::new();
for line in bf.lines() {
match line {
Ok(line) => {
let n = line.trim().parse().unwrap_or(0u64);
v.push(n);
}
Err(_) => break,
}
}
v.into_iter().map(CgroupPid::from).collect()
})
.unwrap_or_default()
}
/// set cgroup.type
fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()> {
if !self.is_v2() {
return Err(Error::new(ErrorKind::CgroupVersion));
}
let file_name = "cgroup.type";
self.open_path(file_name, true).and_then(|mut file| {
file.write_all(cgroup_type.as_bytes()).map_err(|e| {
Error::with_cause(
ErrorKind::WriteFailed(file_name.to_string(), cgroup_type.to_string()),
e,
)
})
})
}
/// get cgroup.type
fn get_cgroup_type(&self) -> Result<String> {
if !self.is_v2() {
return Err(Error::new(ErrorKind::CgroupVersion));
}
let file_name = "cgroup.type";
self.open_path(file_name, false).and_then(|mut file: File| {
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => Ok(string.trim().to_owned()),
Err(e) => Err(Error::with_cause(
ErrorKind::ReadFailed(file_name.to_string()),
e,
)),
}
})
}
fn v2(&self) -> bool {
self.is_v2()
}
}
// remove_dir aims to remove cgroup path. It does so recursively,
// by removing any subdirectories (sub-cgroups) first.
fn remove_dir(dir: &Path) -> Result<()> {
// try the fast path first.
if fs::remove_dir(dir).is_ok() {
return Ok(());
}
if dir.exists() && dir.is_dir() {
for entry in fs::read_dir(dir)
.map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))?
{
let entry =
entry.map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))?;
let path = entry.path();
if path.is_dir() {
remove_dir(&path)?;
}
}
fs::remove_dir(dir).map_err(|e| Error::with_cause(RemoveFailed, e))?;
}
Ok(())
}
#[doc(hidden)]
pub trait ControllIdentifier {
fn controller_type() -> Controllers;
}
/// Control group hierarchy (right now, only V1 is supported, but in the future Unified will be
/// implemented as well).
pub trait Hierarchy: std::fmt::Debug + Send + Sync {
/// Returns what subsystems are supported by the hierarchy.
fn subsystems(&self) -> Vec<Subsystem>;
/// Returns the root directory of the hierarchy.
fn root(&self) -> PathBuf;
/// Return a handle to the root control group in the hierarchy.
fn root_control_group(&self) -> Cgroup;
/// Return a handle to the parent control group in the hierarchy.
fn parent_control_group(&self, path: &str) -> Cgroup;
fn v2(&self) -> bool;
}
/// Resource limits for the memory subsystem.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MemoryResources {
/// How much memory (in bytes) can the kernel consume.
pub kernel_memory_limit: Option<i64>,
/// Upper limit of memory usage of the control group's tasks.
pub memory_hard_limit: Option<i64>,
/// How much memory the tasks in the control group can use when the system is under memory
/// pressure.
pub memory_soft_limit: Option<i64>,
/// How much of the kernel's memory (in bytes) can be used for TCP-related buffers.
pub kernel_tcp_memory_limit: Option<i64>,
/// How much memory and swap together can the tasks in the control group use.
pub memory_swap_limit: Option<i64>,
/// Controls the tendency of the kernel to swap out parts of the address space of the tasks to
/// disk. Lower value implies less likely.
///
/// Note, however, that a value of zero does not mean the process is never swapped out. Use the
/// traditional `mlock(2)` system call for that purpose.
pub swappiness: Option<u64>,
/// Customized key-value attributes
///
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::fs::Resources::default();
/// resource.memory.attrs.insert("memory.numa_balancing".to_string(), "true".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// Resources limits on the number of processes.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PidResources {
/// The maximum number of processes that can exist in the control group.
///
/// Note that attaching processes to the control group will still succeed _even_ if the limit
/// would be violated, however forks/clones inside the control group will have with `EAGAIN` if
/// they would violate the limit set here.
pub maximum_number_of_processes: Option<MaxValue>,
}
/// Resources limits about how the tasks can use the CPU.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CpuResources {
// cpuset
/// A comma-separated list of CPU IDs where the task in the control group can run. Dashes
/// between numbers indicate ranges.
pub cpus: Option<String>,
/// Same syntax as the `cpus` field of this structure, but applies to memory nodes instead of
/// processors.
pub mems: Option<String>,
// cpu
/// Weight of how much of the total CPU time should this control group get. Note that this is
/// hierarchical, so this is weighted against the siblings of this control group.
pub shares: Option<u64>,
/// In one `period`, how much can the tasks run in microseconds.
pub quota: Option<i64>,
/// Period of time in microseconds.
pub period: Option<u64>,
/// This is currently a no-operation.
pub realtime_runtime: Option<i64>,
/// This is currently a no-operation.
pub realtime_period: Option<u64>,
/// Customized key-value attributes
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::fs::Resources::default();
/// resource.cpu.attrs.insert("cpu.cfs_init_buffer_us".to_string(), "10".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// A device resource that can be allowed or denied access to.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceResource {
/// If true, access to the device is allowed, otherwise it's denied.
pub allow: bool,
/// `'c'` for character device, `'b'` for block device; or `'a'` for all devices.
pub devtype: crate::fs::devices::DeviceType,
/// The major number of the device.
pub major: i64,
/// The minor number of the device.
pub minor: i64,
/// Sequence of `'r'`, `'w'` or `'m'`, each denoting read, write or mknod permissions.
pub access: Vec<crate::fs::devices::DevicePermissions>,
}
/// Limit the usage of devices for the control group's tasks.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceResources {
/// For each device in the list, the limits in the structure are applied.
pub devices: Vec<DeviceResource>,
}
/// Assigned priority for a network device.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NetworkPriority {
/// The name (as visible in `ifconfig`) of the interface.
pub name: String,
/// Assigned priority.
pub priority: u64,
}
/// Collections of limits and tags that can be imposed on packets emitted by the tasks in the
/// control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NetworkResources {
/// The networking class identifier to attach to the packets.
///
/// This can then later be used in iptables and such to have special rules.
pub class_id: Option<u64>,
/// Priority of the egress traffic for each interface.
pub priorities: Vec<NetworkPriority>,
}
/// A hugepage type and its consumption limit for the control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HugePageResource {
/// The size of the hugepage, i.e. `2MB`, `1GB`, etc.
pub size: String,
/// The amount of bytes (of memory consumed by the tasks) that are allowed to be backed by
/// hugepages.
pub limit: u64,
}
/// Provides the ability to set consumption limit on each type of hugepages.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HugePageResources {
/// Set a limit of consumption for each hugepages type.
pub limits: Vec<HugePageResource>,
}
/// Weight for a particular block device.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoDeviceResource {
/// The major number of the device.
pub major: u64,
/// The minor number of the device.
pub minor: u64,
/// The weight of the device against the descendant nodes.
pub weight: Option<u16>,
/// The weight of the device against the sibling nodes.
pub leaf_weight: Option<u16>,
}
/// Provides the ability to throttle a device (both byte/sec, and IO op/s)
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoDeviceThrottleResource {
/// The major number of the device.
pub major: u64,
/// The minor number of the device.
pub minor: u64,
/// The rate.
pub rate: u64,
}
/// General block I/O resource limits.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoResources {
/// The weight of the control group against descendant nodes.
pub weight: Option<u16>,
/// The weight of the control group against sibling nodes.
pub leaf_weight: Option<u16>,
/// For each device, a separate weight (both normal and leaf) can be provided.
pub weight_device: Vec<BlkIoDeviceResource>,
/// Throttled read bytes/second can be provided for each device.
pub throttle_read_bps_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled read IO operations per second can be provided for each device.
pub throttle_read_iops_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled written bytes/second can be provided for each device.
pub throttle_write_bps_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled write IO operations per second can be provided for each device.
pub throttle_write_iops_device: Vec<BlkIoDeviceThrottleResource>,
/// Customized key-value attributes
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::fs::Resources::default();
/// resource.blkio.attrs.insert("io.cost.weight".to_string(), "10".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// The resource limits and constraints that will be set on the control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Resources {
/// Memory usage related limits.
pub memory: MemoryResources,
/// Process identifier related limits.
pub pid: PidResources,
/// CPU related limits.
pub cpu: CpuResources,
/// Device related limits.
pub devices: DeviceResources,
/// Network related tags and limits.
pub network: NetworkResources,
/// Hugepages consumption related limits.
pub hugepages: HugePageResources,
/// Block device I/O related limits.
pub blkio: BlkIoResources,
}
impl Subsystem {
fn enter(self, path: &Path) -> Self {
match self {
Subsystem::Pid(mut cont) => Subsystem::Pid({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Mem(mut cont) => Subsystem::Mem({
cont.get_path_mut().push(path);
cont
}),
Subsystem::CpuSet(mut cont) => Subsystem::CpuSet({
cont.get_path_mut().push(path);
cont
}),
Subsystem::CpuAcct(mut cont) => Subsystem::CpuAcct({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Cpu(mut cont) => Subsystem::Cpu({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Devices(mut cont) => Subsystem::Devices({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Freezer(mut cont) => Subsystem::Freezer({
cont.get_path_mut().push(path);
cont
}),
Subsystem::NetCls(mut cont) => Subsystem::NetCls({
cont.get_path_mut().push(path);
cont
}),
Subsystem::BlkIo(mut cont) => Subsystem::BlkIo({
cont.get_path_mut().push(path);
cont
}),
Subsystem::PerfEvent(mut cont) => Subsystem::PerfEvent({
cont.get_path_mut().push(path);
cont
}),
Subsystem::NetPrio(mut cont) => Subsystem::NetPrio({
cont.get_path_mut().push(path);
cont
}),
Subsystem::HugeTlb(mut cont) => Subsystem::HugeTlb({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Rdma(mut cont) => Subsystem::Rdma({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Systemd(mut cont) => Subsystem::Systemd({
cont.get_path_mut().push(path);
cont
}),
}
}
pub fn to_controller(&self) -> &dyn Controller {
match self {
Subsystem::Pid(cont) => cont,
Subsystem::Mem(cont) => cont,
Subsystem::CpuSet(cont) => cont,
Subsystem::CpuAcct(cont) => cont,
Subsystem::Cpu(cont) => cont,
Subsystem::Devices(cont) => cont,
Subsystem::Freezer(cont) => cont,
Subsystem::NetCls(cont) => cont,
Subsystem::BlkIo(cont) => cont,
Subsystem::PerfEvent(cont) => cont,
Subsystem::NetPrio(cont) => cont,
Subsystem::HugeTlb(cont) => cont,
Subsystem::Rdma(cont) => cont,
Subsystem::Systemd(cont) => cont,
}
}
pub fn controller_name(&self) -> String {
self.to_controller().control_type().to_string()
}
}
/// The values for `memory.hight` or `pids.max`
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MaxValue {
/// This value is returned when the text is `"max"`.
Max,
/// When the value is a numerical value, they are returned via this enum field.
Value(i64),
}
#[allow(clippy::derivable_impls)]
impl Default for MaxValue {
fn default() -> Self {
MaxValue::Max
}
}
impl MaxValue {
#[allow(clippy::should_implement_trait, clippy::wrong_self_convention)]
fn to_i64(&self) -> i64 {
match self {
MaxValue::Max => -1,
MaxValue::Value(num) => *num,
}
}
}
impl fmt::Display for MaxValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MaxValue::Max => write!(f, "max"),
MaxValue::Value(num) => write!(f, "{}", num),
}
}
}
pub fn parse_max_value(s: &str) -> Result<MaxValue> {
if s.trim() == "max" {
return Ok(MaxValue::Max);
}
match s.trim().parse() {
Ok(val) => Ok(MaxValue::Value(val)),
Err(e) => Err(Error::with_cause(ParseError, e)),
}
}
// Flat keyed
// KEY0 VAL0\n
// KEY1 VAL1\n
pub fn flat_keyed_to_vec(mut file: File) -> Result<Vec<(String, i64)>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?;
let mut v = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.len() == 2 {
if let Ok(i) = parts[1].parse::<i64>() {
v.push((parts[0].to_string(), i));
}
}
}
Ok(v)
}
// Flat keyed
// KEY0 VAL0\n
// KEY1 VAL1\n
pub fn flat_keyed_to_hashmap(mut file: File) -> Result<HashMap<String, i64>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?;
let mut h = HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.len() == 2 {
if let Ok(i) = parts[1].parse::<i64>() {
h.insert(parts[0].to_string(), i);
}
}
}
Ok(h)
}
// Nested keyed
// KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01...
// KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11...
pub fn nested_keyed_to_hashmap(mut file: File) -> Result<HashMap<String, HashMap<String, i64>>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?;
let mut h = HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.is_empty() {
continue;
}
let mut th = HashMap::new();
for item in parts[1..].iter() {
let fields: Vec<&str> = item.split('=').collect();
if fields.len() == 2 {
if let Ok(i) = fields[1].parse::<i64>() {
th.insert(fields[0].to_string(), i);
}
}
}
h.insert(parts[0].to_string(), th);
}
Ok(h)
}
fn read_from<T>(mut file: File) -> Result<T>
where
T: FromStr,
<T as FromStr>::Err: 'static + Send + Sync + std::error::Error,
{
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => string
.trim()
.parse::<T>()
.map_err(|e| Error::with_cause(ParseError, e)),
Err(e) => Err(Error::with_cause(
ReadFailed("FIXME: can't get path in fn read_from".to_string()),
e,
)),
}
}
fn read_string_from(mut file: File) -> Result<String> {
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => Ok(string.trim().to_string()),
Err(e) => Err(Error::with_cause(
ReadFailed("FIXME: can't get path in fn read_string_from".to_string()),
e,
)),
}
}
/// read and parse an u64 data
fn read_u64_from(file: File) -> Result<u64> {
read_from::<u64>(file)
}
/// read and parse an i64 data
fn read_i64_from(file: File) -> Result<i64> {
read_from::<i64>(file)
}

View File

@@ -10,11 +10,11 @@
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::read_u64_from;
use crate::{
use crate::fs::read_u64_from;
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, NetworkResources, Resources, Subsystem,
};
@@ -76,10 +76,10 @@ impl<'a> From<&'a Subsystem> for &'a NetClsController {
impl NetClsController {
/// Constructs a new `NetClsController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
@@ -88,8 +88,9 @@ impl NetClsController {
self.open_path("net_cls.classid", true)
.and_then(|mut file| {
let s = format!("{:#08X}", class);
file.write_all(s.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(s.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed("net_cls.classid".to_string(), s), e)
})
})
}

View File

@@ -11,11 +11,11 @@ use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::read_u64_from;
use crate::{
use crate::fs::read_u64_from;
use crate::fs::{
ControllIdentifier, ControllerInternal, Controllers, NetworkResources, Resources, Subsystem,
};
@@ -79,10 +79,10 @@ impl<'a> From<&'a Subsystem> for &'a NetPrioController {
impl NetPrioController {
/// Constructs a new `NetPrioController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
@@ -94,36 +94,26 @@ impl NetPrioController {
}
/// A map of priorities for each network interface.
#[allow(clippy::iter_nth_zero, clippy::unnecessary_unwrap)]
pub fn ifpriomap(&self) -> Result<HashMap<String, u64>> {
self.open_path("net_prio.ifpriomap", false)
.and_then(|file| {
let bf = BufReader::new(file);
bf.lines().fold(Ok(HashMap::new()), |acc, line| {
if acc.is_err() {
acc
} else {
let mut acc = acc.unwrap();
let l = line.unwrap();
let mut sp = l.split_whitespace();
bf.lines()
.map(|line| {
let line = line.map_err(|_| Error::new(ParseError))?;
let mut parts = line.split_whitespace();
let ifname = sp.nth(0);
let ifprio = sp.nth(1);
if ifname.is_none() || ifprio.is_none() {
Err(Error::new(ParseError))
} else {
let ifname = ifname.unwrap();
let ifprio = ifprio.unwrap().trim().parse();
match ifprio {
Err(e) => Err(Error::with_cause(ParseError, e)),
Ok(_) => {
acc.insert(ifname.to_string(), ifprio.unwrap());
Ok(acc)
}
}
}
}
})
let ifname = parts.next().ok_or(Error::new(ParseError))?;
let ifprio_str = parts.next().ok_or(Error::new(ParseError))?;
let ifprio = ifprio_str
.trim()
.parse()
.map_err(|e| Error::with_cause(ParseError, e))?;
Ok((ifname.to_string(), ifprio))
})
.collect::<Result<HashMap<String, _>>>()
})
}
@@ -132,7 +122,15 @@ impl NetPrioController {
self.open_path("net_prio.ifpriomap", true)
.and_then(|mut file| {
file.write_all(format!("{} {}", eif, prio).as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
.map_err(|e| {
Error::with_cause(
WriteFailed(
"net_prio.ifpriomap".to_string(),
format!("{} {}", eif, prio),
),
e,
)
})
})
}
}

View File

@@ -9,9 +9,9 @@
//! [tools/perf/Documentation/perf-record.txt](https://raw.githubusercontent.com/torvalds/linux/master/tools/perf/Documentation/perf-record.txt)
use std::path::PathBuf;
use crate::error::*;
use crate::fs::error::*;
use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
/// A controller that allows controlling the `perf_event` subsystem of a Cgroup.
///
@@ -65,10 +65,10 @@ impl<'a> From<&'a Subsystem> for &'a PerfEventController {
impl PerfEventController {
/// Constructs a new `PerfEventController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
}

View File

@@ -11,11 +11,11 @@
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::read_u64_from;
use crate::{
use crate::fs::read_u64_from;
use crate::fs::{
parse_max_value, ControllIdentifier, ControllerInternal, Controllers, MaxValue, PidResources,
Resources, Subsystem,
};
@@ -92,10 +92,10 @@ impl<'a> From<&'a Subsystem> for &'a PidController {
impl PidController {
/// Constructors a new `PidController` instance, with `root` serving as the controller's root
/// directory.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
v2,
}
}
@@ -112,7 +112,7 @@ impl PidController {
},
None => Err(Error::new(ParseError)),
},
Err(e) => Err(Error::with_cause(ReadFailed, e)),
Err(e) => Err(Error::with_cause(ReadFailed("pids.events".to_string()), e)),
}
})
}
@@ -130,7 +130,7 @@ impl PidController {
let res = file.read_to_string(&mut string);
match res {
Ok(_) => parse_max_value(&string),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
Err(e) => Err(Error::with_cause(ReadFailed("pids.max".to_string()), e)),
}
})
}
@@ -145,7 +145,10 @@ impl PidController {
let string_to_write = max_pid.to_string();
match file.write_all(string_to_write.as_ref()) {
Ok(_) => Ok(()),
Err(e) => Err(Error::with_cause(WriteFailed, e)),
Err(e) => Err(Error::with_cause(
WriteFailed("pids.max".to_string(), format!("{:?}", max_pid)),
e,
)),
}
})
}

View File

@@ -10,11 +10,11 @@
use std::io::Write;
use std::path::PathBuf;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::read_string_from;
use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::fs::read_string_from;
use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
/// A controller that allows controlling the `rdma` subsystem of a Cgroup.
///
@@ -68,10 +68,10 @@ impl<'a> From<&'a Subsystem> for &'a RdmaController {
impl RdmaController {
/// Constructs a new `RdmaController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf) -> Self {
pub fn new(point: PathBuf, root: PathBuf) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
}
}
@@ -81,11 +81,17 @@ impl RdmaController {
.and_then(read_string_from)
}
/// Returns the max usage of RDMA/IB specific resources.
pub fn max(&self) -> Result<String> {
self.open_path("rdma.max", false).and_then(read_string_from)
}
/// Set a maximum usage for each RDMA/IB resource.
pub fn set_max(&self, max: &str) -> Result<()> {
self.open_path("rdma.max", true).and_then(|mut file| {
file.write_all(max.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
file.write_all(max.as_ref()).map_err(|e| {
Error::with_cause(WriteFailed("rdma.max".to_string(), max.to_string()), e)
})
})
}
}

View File

@@ -7,9 +7,9 @@
//!
use std::path::PathBuf;
use crate::error::*;
use crate::fs::error::*;
use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem};
/// A controller that allows controlling the `systemd` subsystem of a Cgroup.
///
@@ -62,10 +62,10 @@ impl<'a> From<&'a Subsystem> for &'a SystemdController {
impl SystemdController {
/// Constructs a new `SystemdController` with `root` serving as the root of the control group.
pub fn new(root: PathBuf, v2: bool) -> Self {
pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self {
Self {
base: root.clone(),
path: root,
base: root,
path: point,
_v2: v2,
}
}

View File

@@ -1,668 +1,37 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2020 Ant Group
// Copyright (c) 2020-2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
#![allow(clippy::unnecessary_unwrap)]
use log::*;
use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
macro_rules! update_and_test {
($self: ident, $set_func:ident, $value:expr, $get_func:ident) => {
if let Some(v) = $value {
$self.$set_func(v)?;
if $self.$get_func()? != v {
return Err(Error::new(Other));
}
}
};
}
macro_rules! update {
($self: ident, $set_func:ident, $value:expr) => {
if let Some(v) = $value {
let _ = $self.$set_func(v);
}
};
}
pub mod blkio;
pub mod cgroup;
pub mod cgroup_builder;
pub mod cpu;
pub mod cpuacct;
pub mod cpuset;
pub mod devices;
pub mod error;
pub mod events;
pub mod freezer;
pub mod hierarchies;
pub mod hugetlb;
pub mod memory;
pub mod net_cls;
pub mod net_prio;
pub mod perf_event;
pub mod pid;
pub mod rdma;
pub mod fs;
#[cfg(feature = "oci")]
pub mod manager;
#[cfg(feature = "oci")]
pub use manager::{FsManager, Manager, SystemdManager};
pub mod stats;
pub use stats::CgroupStats;
pub mod systemd;
use crate::blkio::BlkIoController;
use crate::cpu::CpuController;
use crate::cpuacct::CpuAcctController;
use crate::cpuset::CpuSetController;
use crate::devices::DevicesController;
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::freezer::FreezerController;
use crate::hugetlb::HugeTlbController;
use crate::memory::MemController;
use crate::net_cls::NetClsController;
use crate::net_prio::NetPrioController;
use crate::perf_event::PerfEventController;
use crate::pid::PidController;
use crate::rdma::RdmaController;
use crate::systemd::SystemdController;
/// The maximum value for CPU shares in cgroups v1
pub const CPU_SHARES_V1_MAX: u64 = 262144;
/// The maximum value for CPU weight in cgroups v2
pub const CPU_WEIGHT_V2_MAX: u64 = 10000;
#[doc(inline)]
pub use crate::cgroup::Cgroup;
/// Contains all the subsystems that are available in this crate.
#[derive(Debug, Clone)]
pub enum Subsystem {
/// Controller for the `Pid` subsystem, see `PidController` for more information.
Pid(PidController),
/// Controller for the `Mem` subsystem, see `MemController` for more information.
Mem(MemController),
/// Controller for the `CpuSet subsystem, see `CpuSetController` for more information.
CpuSet(CpuSetController),
/// Controller for the `CpuAcct` subsystem, see `CpuAcctController` for more information.
CpuAcct(CpuAcctController),
/// Controller for the `Cpu` subsystem, see `CpuController` for more information.
Cpu(CpuController),
/// Controller for the `Devices` subsystem, see `DevicesController` for more information.
Devices(DevicesController),
/// Controller for the `Freezer` subsystem, see `FreezerController` for more information.
Freezer(FreezerController),
/// Controller for the `NetCls` subsystem, see `NetClsController` for more information.
NetCls(NetClsController),
/// Controller for the `BlkIo` subsystem, see `BlkIoController` for more information.
BlkIo(BlkIoController),
/// Controller for the `PerfEvent` subsystem, see `PerfEventController` for more information.
PerfEvent(PerfEventController),
/// Controller for the `NetPrio` subsystem, see `NetPrioController` for more information.
NetPrio(NetPrioController),
/// Controller for the `HugeTlb` subsystem, see `HugeTlbController` for more information.
HugeTlb(HugeTlbController),
/// Controller for the `Rdma` subsystem, see `RdmaController` for more information.
Rdma(RdmaController),
/// Controller for the `Systemd` subsystem, see `SystemdController` for more information.
Systemd(SystemdController),
}
#[doc(hidden)]
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum Controllers {
Pids,
Mem,
CpuSet,
CpuAcct,
Cpu,
Devices,
Freezer,
NetCls,
BlkIo,
PerfEvent,
NetPrio,
HugeTlb,
Rdma,
Systemd,
}
impl fmt::Display for Controllers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Controllers::Pids => write!(f, "pids"),
Controllers::Mem => write!(f, "memory"),
Controllers::CpuSet => write!(f, "cpuset"),
Controllers::CpuAcct => write!(f, "cpuacct"),
Controllers::Cpu => write!(f, "cpu"),
Controllers::Devices => write!(f, "devices"),
Controllers::Freezer => write!(f, "freezer"),
Controllers::NetCls => write!(f, "net_cls"),
Controllers::BlkIo => write!(f, "blkio"),
Controllers::PerfEvent => write!(f, "perf_event"),
Controllers::NetPrio => write!(f, "net_prio"),
Controllers::HugeTlb => write!(f, "hugetlb"),
Controllers::Rdma => write!(f, "rdma"),
Controllers::Systemd => write!(f, "name=systemd"),
}
}
}
mod sealed {
use super::*;
pub trait ControllerInternal {
fn apply(&self, res: &Resources) -> Result<()>;
// meta stuff
fn control_type(&self) -> Controllers;
fn get_path(&self) -> &PathBuf;
fn get_path_mut(&mut self) -> &mut PathBuf;
fn get_base(&self) -> &PathBuf;
/// Hooks running after controller crated, if have
fn post_create(&self) {}
fn is_v2(&self) -> bool {
false
}
fn verify_path(&self) -> Result<()> {
if self.get_path().starts_with(self.get_base()) {
Ok(())
} else {
Err(Error::new(ErrorKind::InvalidPath))
}
}
fn open_path(&self, p: &str, w: bool) -> Result<File> {
let mut path = self.get_path().clone();
path.push(p);
self.verify_path()?;
if w {
match File::create(&path) {
Err(e) => Err(Error::with_cause(ErrorKind::WriteFailed, e)),
Ok(file) => Ok(file),
}
} else {
match File::open(&path) {
Err(e) => Err(Error::with_cause(ErrorKind::ReadFailed, e)),
Ok(file) => Ok(file),
}
}
}
fn get_max_value(&self, f: &str) -> Result<MaxValue> {
self.open_path(f, false).and_then(|mut file| {
let mut string = String::new();
let res = file.read_to_string(&mut string);
match res {
Ok(_) => parse_max_value(&string),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
}
})
}
#[doc(hidden)]
fn path_exists(&self, p: &str) -> bool {
if self.verify_path().is_err() {
return false;
}
std::path::Path::new(p).exists()
}
}
pub trait CustomizedAttribute: ControllerInternal {
fn set(&self, key: &str, value: &str) -> Result<()> {
self.open_path(key, true).and_then(|mut file| {
file.write_all(value.as_ref())
.map_err(|e| Error::with_cause(WriteFailed, e))
})
}
fn get(&self, key: &str) -> Result<String> {
self.open_path(key, false).and_then(|mut file: File| {
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => Ok(string.trim().to_owned()),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
}
})
}
}
}
pub(crate) use crate::sealed::{ControllerInternal, CustomizedAttribute};
/// A Controller is a subsystem attached to the control group.
///
/// Implementors are able to control certain aspects of a control group.
pub trait Controller {
#[doc(hidden)]
fn control_type(&self) -> Controllers;
/// The file system path to the controller.
fn path(&self) -> &Path;
/// Apply a set of resources to the Controller, invoking its internal functions to pass the
/// kernel the information.
fn apply(&self, res: &Resources) -> Result<()>;
/// Create this controller
fn create(&self);
/// Does this controller already exist?
fn exists(&self) -> bool;
/// Set notify_on_release
fn set_notify_on_release(&self, enable: bool) -> Result<()>;
/// Set release_agent
fn set_release_agent(&self, path: &str) -> Result<()>;
/// Delete the controller.
fn delete(&self) -> Result<()>;
/// Attach a task to this controller.
fn add_task(&self, pid: &CgroupPid) -> Result<()>;
/// Attach a task to this controller.
fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()>;
/// Get the list of tasks that this controller has.
fn tasks(&self) -> Vec<CgroupPid>;
fn v2(&self) -> bool;
}
impl<T> Controller for T
where
T: ControllerInternal,
{
fn control_type(&self) -> Controllers {
ControllerInternal::control_type(self)
}
fn path(&self) -> &Path {
self.get_path()
}
/// Apply a set of resources to the Controller, invoking its internal functions to pass the
/// kernel the information.
fn apply(&self, res: &Resources) -> Result<()> {
ControllerInternal::apply(self, res)
}
/// Create this controller
fn create(&self) {
self.verify_path()
.unwrap_or_else(|_| panic!("path should be valid: {:?}", self.path()));
match ::std::fs::create_dir_all(self.get_path()) {
Ok(_) => self.post_create(),
Err(e) => warn!("error create_dir: {:?} error: {:?}", self.get_path(), e),
}
}
/// Set notify_on_release
fn set_notify_on_release(&self, enable: bool) -> Result<()> {
self.open_path("notify_on_release", true)
.and_then(|mut file| {
write!(file, "{}", enable as i32)
.map_err(|e| Error::with_cause(ErrorKind::WriteFailed, e))
})
}
/// Set release_agent
fn set_release_agent(&self, path: &str) -> Result<()> {
self.open_path("release_agent", true).and_then(|mut file| {
file.write_all(path.as_bytes())
.map_err(|e| Error::with_cause(ErrorKind::WriteFailed, e))
})
}
/// Does this controller already exist?
fn exists(&self) -> bool {
self.get_path().exists()
}
/// Delete the controller.
fn delete(&self) -> Result<()> {
if !self.get_path().exists() {
return Ok(());
}
// Compatible with runC for remove dir operation
// https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L272
//
// We trying to remove all paths five times with increasing delay between tries.
// If after all there are not removed cgroups - appropriate error will be
// returned.
let mut delay = std::time::Duration::from_millis(10);
let cgroup_path = self.get_path();
for _i in 0..4 {
if let Ok(()) = remove_dir(cgroup_path) {
return Ok(());
}
std::thread::sleep(delay);
delay *= 2;
}
remove_dir(cgroup_path)
}
/// Attach a task to this controller.
fn add_task(&self, pid: &CgroupPid) -> Result<()> {
let mut file = "tasks";
if self.is_v2() {
file = "cgroup.procs";
}
self.open_path(file, true).and_then(|mut file| {
file.write_all(pid.pid.to_string().as_ref())
.map_err(|e| Error::with_cause(ErrorKind::WriteFailed, e))
})
}
/// Attach a task to this controller by thread group id.
fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()> {
self.open_path("cgroup.procs", true).and_then(|mut file| {
file.write_all(pid.pid.to_string().as_ref())
.map_err(|e| Error::with_cause(ErrorKind::WriteFailed, e))
})
}
/// Get the list of tasks that this controller has.
fn tasks(&self) -> Vec<CgroupPid> {
let mut file = "tasks";
if self.is_v2() {
file = "cgroup.procs";
}
self.open_path(file, false)
.map(|file| {
let bf = BufReader::new(file);
let mut v = Vec::new();
for line in bf.lines() {
match line {
Ok(line) => {
let n = line.trim().parse().unwrap_or(0u64);
v.push(n);
}
Err(_) => break,
}
}
v.into_iter().map(CgroupPid::from).collect()
})
.unwrap_or_default()
}
fn v2(&self) -> bool {
self.is_v2()
}
}
// remove_dir aims to remove cgroup path. It does so recursively,
// by removing any subdirectories (sub-cgroups) first.
fn remove_dir(dir: &Path) -> Result<()> {
// try the fast path first.
if fs::remove_dir(dir).is_ok() {
return Ok(());
}
if dir.exists() && dir.is_dir() {
for entry in fs::read_dir(dir).map_err(|e| Error::with_cause(ReadFailed, e))? {
let entry = entry.map_err(|e| Error::with_cause(ReadFailed, e))?;
let path = entry.path();
if path.is_dir() {
remove_dir(&path)?;
}
}
fs::remove_dir(dir).map_err(|e| Error::with_cause(RemoveFailed, e))?;
}
Ok(())
}
#[doc(hidden)]
pub trait ControllIdentifier {
fn controller_type() -> Controllers;
}
/// Control group hierarchy (right now, only V1 is supported, but in the future Unified will be
/// implemented as well).
pub trait Hierarchy: std::fmt::Debug + Send + Sync {
/// Returns what subsystems are supported by the hierarchy.
fn subsystems(&self) -> Vec<Subsystem>;
/// Returns the root directory of the hierarchy.
fn root(&self) -> PathBuf;
/// Return a handle to the root control group in the hierarchy.
fn root_control_group(&self) -> Cgroup;
fn v2(&self) -> bool;
}
/// Resource limits for the memory subsystem.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
/// The current state of the control group
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MemoryResources {
/// How much memory (in bytes) can the kernel consume.
pub kernel_memory_limit: Option<i64>,
/// Upper limit of memory usage of the control group's tasks.
pub memory_hard_limit: Option<i64>,
/// How much memory the tasks in the control group can use when the system is under memory
/// pressure.
pub memory_soft_limit: Option<i64>,
/// How much of the kernel's memory (in bytes) can be used for TCP-related buffers.
pub kernel_tcp_memory_limit: Option<i64>,
/// How much memory and swap together can the tasks in the control group use.
pub memory_swap_limit: Option<i64>,
/// Controls the tendency of the kernel to swap out parts of the address space of the tasks to
/// disk. Lower value implies less likely.
///
/// Note, however, that a value of zero does not mean the process is never swapped out. Use the
/// traditional `mlock(2)` system call for that purpose.
pub swappiness: Option<u64>,
/// Customized key-value attributes
///
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::Resources::default();
/// resource.memory.attrs.insert("memory.numa_balancing".to_string(), "true".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// Resources limits on the number of processes.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PidResources {
/// The maximum number of processes that can exist in the control group.
///
/// Note that attaching processes to the control group will still succeed _even_ if the limit
/// would be violated, however forks/clones inside the control group will have with `EAGAIN` if
/// they would violate the limit set here.
pub maximum_number_of_processes: Option<MaxValue>,
}
/// Resources limits about how the tasks can use the CPU.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CpuResources {
// cpuset
/// A comma-separated list of CPU IDs where the task in the control group can run. Dashes
/// between numbers indicate ranges.
pub cpus: Option<String>,
/// Same syntax as the `cpus` field of this structure, but applies to memory nodes instead of
/// processors.
pub mems: Option<String>,
// cpu
/// Weight of how much of the total CPU time should this control group get. Note that this is
/// hierarchical, so this is weighted against the siblings of this control group.
pub shares: Option<u64>,
/// In one `period`, how much can the tasks run in nanoseconds.
pub quota: Option<i64>,
/// Period of time in nanoseconds.
pub period: Option<u64>,
/// This is currently a no-operation.
pub realtime_runtime: Option<i64>,
/// This is currently a no-operation.
pub realtime_period: Option<u64>,
/// Customized key-value attributes
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::Resources::default();
/// resource.cpu.attrs.insert("cpu.cfs_init_buffer_us".to_string(), "10".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// A device resource that can be allowed or denied access to.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceResource {
/// If true, access to the device is allowed, otherwise it's denied.
pub allow: bool,
/// `'c'` for character device, `'b'` for block device; or `'a'` for all devices.
pub devtype: crate::devices::DeviceType,
/// The major number of the device.
pub major: i64,
/// The minor number of the device.
pub minor: i64,
/// Sequence of `'r'`, `'w'` or `'m'`, each denoting read, write or mknod permissions.
pub access: Vec<crate::devices::DevicePermissions>,
}
/// Limit the usage of devices for the control group's tasks.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceResources {
/// For each device in the list, the limits in the structure are applied.
pub devices: Vec<DeviceResource>,
}
/// Assigned priority for a network device.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NetworkPriority {
/// The name (as visible in `ifconfig`) of the interface.
pub name: String,
/// Assigned priority.
pub priority: u64,
}
/// Collections of limits and tags that can be imposed on packets emitted by the tasks in the
/// control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NetworkResources {
/// The networking class identifier to attach to the packets.
///
/// This can then later be used in iptables and such to have special rules.
pub class_id: Option<u64>,
/// Priority of the egress traffic for each interface.
pub priorities: Vec<NetworkPriority>,
}
/// A hugepage type and its consumption limit for the control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HugePageResource {
/// The size of the hugepage, i.e. `2MB`, `1GB`, etc.
pub size: String,
/// The amount of bytes (of memory consumed by the tasks) that are allowed to be backed by
/// hugepages.
pub limit: u64,
}
/// Provides the ability to set consumption limit on each type of hugepages.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HugePageResources {
/// Set a limit of consumption for each hugepages type.
pub limits: Vec<HugePageResource>,
}
/// Weight for a particular block device.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoDeviceResource {
/// The major number of the device.
pub major: u64,
/// The minor number of the device.
pub minor: u64,
/// The weight of the device against the descendant nodes.
pub weight: Option<u16>,
/// The weight of the device against the sibling nodes.
pub leaf_weight: Option<u16>,
}
/// Provides the ability to throttle a device (both byte/sec, and IO op/s)
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoDeviceThrottleResource {
/// The major number of the device.
pub major: u64,
/// The minor number of the device.
pub minor: u64,
/// The rate.
pub rate: u64,
}
/// General block I/O resource limits.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BlkIoResources {
/// The weight of the control group against descendant nodes.
pub weight: Option<u16>,
/// The weight of the control group against sibling nodes.
pub leaf_weight: Option<u16>,
/// For each device, a separate weight (both normal and leaf) can be provided.
pub weight_device: Vec<BlkIoDeviceResource>,
/// Throttled read bytes/second can be provided for each device.
pub throttle_read_bps_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled read IO operations per second can be provided for each device.
pub throttle_read_iops_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled written bytes/second can be provided for each device.
pub throttle_write_bps_device: Vec<BlkIoDeviceThrottleResource>,
/// Throttled write IO operations per second can be provided for each device.
pub throttle_write_iops_device: Vec<BlkIoDeviceThrottleResource>,
/// Customized key-value attributes
/// # Usage:
/// ```
/// let resource = &mut cgroups_rs::Resources::default();
/// resource.blkio.attrs.insert("io.cost.weight".to_string(), "10".to_string());
/// // apply here
/// ```
pub attrs: HashMap<String, String>,
}
/// The resource limits and constraints that will be set on the control group.
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Resources {
/// Memory usage related limits.
pub memory: MemoryResources,
/// Process identifier related limits.
pub pid: PidResources,
/// CPU related limits.
pub cpu: CpuResources,
/// Device related limits.
pub devices: DeviceResources,
/// Network related tags and limits.
pub network: NetworkResources,
/// Hugepages consumption related limits.
pub hugepages: HugePageResources,
/// Block device I/O related limits.
pub blkio: BlkIoResources,
pub enum FreezerState {
/// The processes in the control group are _not_ frozen.
Thawed,
/// The processes in the control group are in the processes of being frozen.
Freezing,
/// The processes in the control group are frozen.
Frozen,
}
/// A structure representing a `pid`. Currently implementations exist for `u64` and
/// `std::process::Child`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct CgroupPid {
/// The process identifier
pub pid: u64,
@@ -674,241 +43,40 @@ impl From<u64> for CgroupPid {
}
}
impl<'a> From<&'a std::process::Child> for CgroupPid {
impl From<&std::process::Child> for CgroupPid {
fn from(u: &std::process::Child) -> CgroupPid {
CgroupPid { pid: u.id() as u64 }
}
}
impl Subsystem {
fn enter(self, path: &Path) -> Self {
match self {
Subsystem::Pid(mut cont) => Subsystem::Pid({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Mem(mut cont) => Subsystem::Mem({
cont.get_path_mut().push(path);
cont
}),
Subsystem::CpuSet(mut cont) => Subsystem::CpuSet({
cont.get_path_mut().push(path);
cont
}),
Subsystem::CpuAcct(mut cont) => Subsystem::CpuAcct({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Cpu(mut cont) => Subsystem::Cpu({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Devices(mut cont) => Subsystem::Devices({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Freezer(mut cont) => Subsystem::Freezer({
cont.get_path_mut().push(path);
cont
}),
Subsystem::NetCls(mut cont) => Subsystem::NetCls({
cont.get_path_mut().push(path);
cont
}),
Subsystem::BlkIo(mut cont) => Subsystem::BlkIo({
cont.get_path_mut().push(path);
cont
}),
Subsystem::PerfEvent(mut cont) => Subsystem::PerfEvent({
cont.get_path_mut().push(path);
cont
}),
Subsystem::NetPrio(mut cont) => Subsystem::NetPrio({
cont.get_path_mut().push(path);
cont
}),
Subsystem::HugeTlb(mut cont) => Subsystem::HugeTlb({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Rdma(mut cont) => Subsystem::Rdma({
cont.get_path_mut().push(path);
cont
}),
Subsystem::Systemd(mut cont) => Subsystem::Systemd({
cont.get_path_mut().push(path);
cont
}),
#[cfg(test)]
pub mod tests {
use std::fs;
use std::process::{Child, Command, Stdio};
/// Start a mock subprocess that will sleep forever
pub fn spawn_sleep_inf() -> Child {
let child = Command::new("sleep")
.arg("infinity")
.spawn()
.expect("Failed to start mock subprocess");
child
}
pub fn spawn_yes() -> Child {
let devnull = fs::File::create("/dev/null").expect("cannot open /dev/null");
let child = Command::new("yes")
.stdout(Stdio::from(devnull))
.spawn()
.expect("Failed to start mock subprocess");
child
}
pub fn systemd_version() -> Option<String> {
let output = Command::new("systemd").arg("--version").output().ok()?; // Return None if command execution fails
if !output.status.success() {
return None;
}
}
pub fn to_controller(&self) -> &dyn Controller {
match self {
Subsystem::Pid(cont) => cont,
Subsystem::Mem(cont) => cont,
Subsystem::CpuSet(cont) => cont,
Subsystem::CpuAcct(cont) => cont,
Subsystem::Cpu(cont) => cont,
Subsystem::Devices(cont) => cont,
Subsystem::Freezer(cont) => cont,
Subsystem::NetCls(cont) => cont,
Subsystem::BlkIo(cont) => cont,
Subsystem::PerfEvent(cont) => cont,
Subsystem::NetPrio(cont) => cont,
Subsystem::HugeTlb(cont) => cont,
Subsystem::Rdma(cont) => cont,
Subsystem::Systemd(cont) => cont,
}
}
pub fn controller_name(&self) -> String {
self.to_controller().control_type().to_string()
Some(String::from_utf8_lossy(&output.stdout).to_string())
}
}
/// The values for `memory.hight` or `pids.max`
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MaxValue {
/// This value is returned when the text is `"max"`.
Max,
/// When the value is a numerical value, they are returned via this enum field.
Value(i64),
}
impl Default for MaxValue {
fn default() -> Self {
MaxValue::Max
}
}
impl MaxValue {
#[allow(clippy::should_implement_trait, clippy::wrong_self_convention)]
fn to_i64(&self) -> i64 {
match self {
MaxValue::Max => -1,
MaxValue::Value(num) => *num,
}
}
}
impl fmt::Display for MaxValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MaxValue::Max => write!(f, "max"),
MaxValue::Value(num) => write!(f, "{}", num),
}
}
}
pub fn parse_max_value(s: &str) -> Result<MaxValue> {
if s.trim() == "max" {
return Ok(MaxValue::Max);
}
match s.trim().parse() {
Ok(val) => Ok(MaxValue::Value(val)),
Err(e) => Err(Error::with_cause(ParseError, e)),
}
}
// Flat keyed
// KEY0 VAL0\n
// KEY1 VAL1\n
pub fn flat_keyed_to_vec(mut file: File) -> Result<Vec<(String, i64)>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed, e))?;
let mut v = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.len() == 2 {
if let Ok(i) = parts[1].parse::<i64>() {
v.push((parts[0].to_string(), i));
}
}
}
Ok(v)
}
// Flat keyed
// KEY0 VAL0\n
// KEY1 VAL1\n
pub fn flat_keyed_to_hashmap(mut file: File) -> Result<HashMap<String, i64>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed, e))?;
let mut h = HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.len() == 2 {
if let Ok(i) = parts[1].parse::<i64>() {
h.insert(parts[0].to_string(), i);
}
}
}
Ok(h)
}
// Nested keyed
// KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01...
// KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11...
pub fn nested_keyed_to_hashmap(mut file: File) -> Result<HashMap<String, HashMap<String, i64>>> {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| Error::with_cause(ReadFailed, e))?;
let mut h = HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.split(' ').collect();
if parts.is_empty() {
continue;
}
let mut th = HashMap::new();
for item in parts[1..].iter() {
let fields: Vec<&str> = item.split('=').collect();
if fields.len() == 2 {
if let Ok(i) = fields[1].parse::<i64>() {
th.insert(fields[0].to_string(), i);
}
}
}
h.insert(parts[0].to_string(), th);
}
Ok(h)
}
fn read_from<T>(mut file: File) -> Result<T>
where
T: FromStr,
<T as FromStr>::Err: 'static + Send + Sync + std::error::Error,
{
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => string
.trim()
.parse::<T>()
.map_err(|e| Error::with_cause(ParseError, e)),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
}
}
fn read_string_from(mut file: File) -> Result<String> {
let mut string = String::new();
match file.read_to_string(&mut string) {
Ok(_) => Ok(string.trim().to_string()),
Err(e) => Err(Error::with_cause(ReadFailed, e)),
}
}
/// read and parse an u64 data
fn read_u64_from(file: File) -> Result<u64> {
read_from::<u64>(file)
}
/// read and parse an i64 data
fn read_i64_from(file: File) -> Result<i64> {
read_from::<i64>(file)
}

113
src/manager/conv.rs Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::manager::error::{Error, Result};
use crate::{CPU_SHARES_V1_MAX, CPU_WEIGHT_V2_MAX};
// Converts CPU shares, used by cgroup v1, to CPU weight, used by cgroup
// v2.
//
// Cgroup v1 CPU shares has a range of [2^1...2^18], i.e. [2...262144],
// and the default value is 1024.
//
// Cgroup v2 CPU weight has a range of [10^0...10^4], i.e. [1...10000],
// and the default value is 100.
pub(crate) fn cpu_shares_to_cgroup_v2(shares: u64) -> u64 {
if shares == 0 {
return 0;
}
if shares <= 2 {
return 1;
}
if shares >= CPU_SHARES_V1_MAX {
return CPU_WEIGHT_V2_MAX;
}
(((shares - 2) * 9999) / 262142) + 1
}
// ConvertMemorySwapToCgroupV2Value converts MemorySwap value from OCI spec
// for use by cgroup v2 drivers. A conversion is needed since
// Resources.MemorySwap is defined as memory+swap combined, while in cgroup
// v2 swap is a separate value.
pub(crate) fn memory_swap_to_cgroup_v2(memswap_limit: i64, mem_limit: i64) -> Result<i64> {
// For compatibility with cgroup1 controller, set swap to unlimited in
// case the memory is set to unlimited, and swap is not explicitly set,
// treating the request as "set both memory and swap to unlimited".
if mem_limit == -1 && memswap_limit == 0 {
return Ok(-1);
}
// -1 is "max", 0 is "unset", so treat as is
if memswap_limit == -1 || memswap_limit == 0 {
return Ok(memswap_limit);
}
// Unlimited memory, so treat swap as is.
if mem_limit == -1 {
return Ok(memswap_limit);
}
// Unset or unknown memory, can't calculate swap.
if mem_limit == 0 {
return Err(Error::InvalidLinuxResource);
}
// Does not make sense to subtract a negative value.
if mem_limit < 0 {
return Err(Error::InvalidLinuxResource);
}
// Sanity check.
if memswap_limit < mem_limit {
return Err(Error::InvalidLinuxResource);
}
Ok(memswap_limit - mem_limit)
}
#[cfg(test)]
mod tests {
use crate::manager::conv::*;
#[test]
fn test_cpu_shares_to_cgroup_v2() {
assert_eq!(cpu_shares_to_cgroup_v2(0), 0);
assert_eq!(cpu_shares_to_cgroup_v2(1), 1);
assert_eq!(cpu_shares_to_cgroup_v2(2), 1);
assert_eq!(cpu_shares_to_cgroup_v2(100), 4);
assert_eq!(
cpu_shares_to_cgroup_v2(CPU_SHARES_V1_MAX),
CPU_WEIGHT_V2_MAX
);
assert_eq!(
cpu_shares_to_cgroup_v2(CPU_SHARES_V1_MAX - 1),
CPU_WEIGHT_V2_MAX - 1
);
assert_eq!(cpu_shares_to_cgroup_v2(u64::MAX), CPU_WEIGHT_V2_MAX);
}
#[test]
fn test_memory_swap_to_cgroup_v2() {
// memory no limit and swap is 0, treat it as no limit
assert_eq!(memory_swap_to_cgroup_v2(0, -1).unwrap(), -1);
// -1 is "max", 0 is "unset", so treat as is
assert_eq!(memory_swap_to_cgroup_v2(-1, 0).unwrap(), -1);
assert_eq!(memory_swap_to_cgroup_v2(0, 0).unwrap(), 0);
// Now swap cannot be 0 or -1
// Unlimited memory, so treat swap as is.
assert_eq!(memory_swap_to_cgroup_v2(100, -1).unwrap(), 100);
// Unset or unknown memory, can't calculate swap.
assert!(memory_swap_to_cgroup_v2(100, 0).is_err());
// Does not make sense to subtract a negative value.
assert!(memory_swap_to_cgroup_v2(100, -2).is_err());
// Swap + mem < mem
assert!(memory_swap_to_cgroup_v2(50, 100).is_err());
// Real swap
assert_eq!(memory_swap_to_cgroup_v2(200, 100).unwrap(), 100);
}
}

28
src/manager/error.rs Normal file
View File

@@ -0,0 +1,28 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::fs::error::Error as CgroupfsError;
use crate::systemd::dbus::error::Error as SystemdDbusError;
use crate::systemd::error::Error as SystemdCgroupError;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("invalid argument")]
InvalidArgument,
#[error("invalid linux resource")]
InvalidLinuxResource,
#[error("cgroupfs error: {0}")]
Cgroupfs(#[from] CgroupfsError),
#[error("systemd cgroup error: {0}")]
SystemdCgroup(#[from] SystemdCgroupError),
#[error("systemd dbus error: {0}")]
SystemdDbus(#[from] SystemdDbusError),
}

1420
src/manager/fs.rs Normal file

File diff suppressed because it is too large Load Diff

113
src/manager/mod.rs Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
mod error;
use std::collections::HashMap;
pub use error::{Error, Result};
mod fs;
pub use fs::FsManager;
mod systemd;
pub use systemd::SystemdManager;
mod conv;
use oci_spec::runtime::LinuxResources;
use crate::systemd::SLICE_SUFFIX;
use crate::{CgroupPid, CgroupStats, FreezerState};
/// Check if the cgroups path is a systemd cgroup.
pub fn is_systemd_cgroup(cgroups_path: &str) -> bool {
let parts: Vec<&str> = cgroups_path.split(':').collect();
parts.len() == 3 && parts[0].ends_with(SLICE_SUFFIX)
}
/// Manage cgroups designed for OCI containers.
pub trait Manager: Send + Sync {
/// Add a process specified by its tgid.
fn add_proc(&mut self, tgid: CgroupPid) -> Result<()>;
/// Add a thread specified by its pid.
fn add_thread(&mut self, pid: CgroupPid) -> Result<()>;
/// Get the list of pids joint to the cgroups.
fn pids(&self) -> Result<Vec<CgroupPid>>;
/// Set the freezer cgroup to the specified state.
fn freeze(&self, state: FreezerState) -> Result<()>;
/// Remove the cgroups.
fn destroy(&mut self) -> Result<()>;
/// Set the resources to the cgroups.
fn set(&mut self, resources: &LinuxResources) -> Result<()>;
/// Get the cgroup path.
///
/// # Arguments
///
/// - `subsystem`: cgroup subsystem, for cgroup v1 the value should not
/// be empty, while for cgroup v2 the only valid value is `None`.
fn cgroup_path(&self, subsystem: Option<&str>) -> Result<String>;
/// Enable CPUs, topdown from root in cgroup hierarchy, this would be
/// useful for CPU hotplug in the guest.
///
/// The caller should update cgroup resources manually, in particular
/// cpuset, after this, in order to use the new CPUs (or avoid using
/// offline CPUs).
///
/// # Arguments
///
/// - `cpus`: online CPUs in the same format with `cat
/// /sys/devices/system/cpu/online`, e.g. "0-3,6-7".
fn enable_cpus_topdown(&self, cpus: &str) -> Result<()>;
/// Get cgroup stats.
fn stats(&self) -> CgroupStats;
/// Get the mappings of subsystems to their relative path. The full
/// path would be something like "{mountpoint}/{relative_path}". The
/// mappings of mountpoints see "mounts()".
fn paths(&self) -> &HashMap<String, String>;
/// Get the mappings of subsystems to their mountpoints. The full
/// path would be something like "{mountpoint}/{relative_path}". The
/// mappings of relative paths see "paths()".
fn mounts(&self) -> &HashMap<String, String>;
/// Indicate whether the cgroup manager is using systemd.
fn systemd(&self) -> bool;
/// Indicate whether the cgroup manager is using cgroup v2.
fn v2(&self) -> bool;
}
#[cfg(test)]
mod tests {
pub const MEMORY_512M: i64 = 512 * 1024 * 1024; // 512 MiB
pub const MEMORY_1G: i64 = 1024 * 1024 * 1024; // 1 GiB
pub const MEMORY_2G: i64 = 2 * 1024 * 1024 * 1024; // 2 GiB
#[macro_export]
macro_rules! skip_if_cgroups_v1 {
() => {
if !$crate::fs::hierarchies::is_cgroup2_unified_mode() {
eprintln!("Skipping test in cgroups v1 mode");
return;
}
};
}
#[macro_export]
macro_rules! skip_if_cgroups_v2 {
() => {
if $crate::fs::hierarchies::is_cgroup2_unified_mode() {
eprintln!("Skipping test in cgroups v2 mode");
return;
}
};
}
}

570
src/manager/systemd.rs Normal file
View File

@@ -0,0 +1,570 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use std::collections::HashMap;
use oci_spec::runtime::{LinuxCpu, LinuxMemory, LinuxPids, LinuxResources};
use zbus::zvariant::Value as ZbusValue;
use crate::manager::conv;
use crate::manager::error::{Error, Result};
use crate::manager::fs::{join_path, FsManager};
use crate::systemd::props::PropertiesBuilder;
use crate::systemd::utils::expand_slice;
use crate::systemd::{
cpu, cpuset, memory, pids, Property, SystemdClient, DEFAULT_SLICE, SCOPE_SUFFIX, SLICE_SUFFIX,
TIMEOUT_STOP_USEC,
};
use crate::{CgroupPid, CgroupStats, FreezerState, Manager};
/// Default kernel value for cpu quota period is 100000 us (100 ms), same
/// for v1 [1] and v2 [2].
///
/// 1: https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html
/// 2: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
const DEFAULT_CPU_QUOTA_PERIOD: u64 = 100_000; // 100ms
pub struct SystemdManager<'a> {
/// The name of slice
slice: String,
/// The name of unit
unit: String,
/// Systemd client
systemd_client: SystemdClient<'a>,
/// Cgroupfs manager
fs_manager: FsManager,
}
impl SystemdManager<'_> {
fn parse_slice_and_unit(path: &str) -> Result<(String, String)> {
let parts: Vec<&str> = path.split(':').collect();
if parts.len() != 3 {
return Err(Error::InvalidArgument);
}
let slice = if parts[0].is_empty() {
DEFAULT_SLICE.to_string()
} else {
parts[0].to_string()
};
let unit = new_unit_name(parts[1], parts[2]);
Ok((slice, unit))
}
/// Create a new `SystemdManager` from a cgroup path.
///
/// # Arguments
///
/// * `path` - A string slice that holds the cgroup path in the format
/// "parent:scope_prefix:name".
pub fn new(path: &str) -> Result<Self> {
let (slice, unit) = Self::parse_slice_and_unit(path)?;
let props = PropertiesBuilder::default_cgroup(&slice, &unit).build();
let slice_base = expand_slice(&slice)?;
let fs_base = join_path(&slice_base, &unit);
let fs_manager = FsManager::new(&fs_base)?;
let cgroup = SystemdClient::new(&unit, props)?;
Ok(Self {
slice,
unit,
fs_manager,
systemd_client: cgroup,
})
}
}
impl SystemdManager<'_> {
/// Get the slice name.
pub fn slice(&self) -> &str {
&self.slice
}
/// Get the unit name.
pub fn unit(&self) -> &str {
&self.unit
}
fn set_cpuset(&self, props: &mut Vec<Property>, linux_cpu: &LinuxCpu) -> Result<()> {
if let Some(cpus) = linux_cpu.cpus().as_ref() {
let (id, value) = cpuset::cpus(cpus)?;
props.push((id, value.into()));
}
if let Some(mems) = linux_cpu.mems().as_ref() {
let (id, value) = cpuset::mems(mems)?;
props.push((id, value.into()));
}
Ok(())
}
fn set_cpu(&self, props: &mut Vec<Property>, linux_cpu: &LinuxCpu) -> Result<()> {
if let Some(shares) = linux_cpu.shares() {
let shares = if self.v2() {
conv::cpu_shares_to_cgroup_v2(shares)
} else {
shares
};
let (id, value) = cpu::shares(shares, self.v2())?;
props.push((id, value.into()));
}
let period = linux_cpu.period().unwrap_or(0);
let quota = linux_cpu.quota().unwrap_or(0);
if period != 0 {
let (id, value) = cpu::period(period)?;
props.push((id, value.into()));
}
if period != 0 || quota != 0 {
// Corresponds to USEC_INFINITY in systemd
let mut quota_systemd = u64::MAX;
let mut period = period;
if quota > 0 {
if period == 0 {
period = DEFAULT_CPU_QUOTA_PERIOD;
}
// systemd converts CPUQuotaPerSecUSec (microseconds per
// CPU second) to CPUQuota (integer percentage of CPU)
// internally. This means that if a fractional percent of
// CPU is indicated by Resources.CpuQuota, we need to round
// up to the nearest 10ms (1% of a second) such that child
// cgroups can set the cpu.cfs_quota_us they expect.
quota_systemd = ((quota as u64) * s_to_us(1)) / period;
if quota_systemd % ms_to_us(10) != 0 {
quota_systemd = (quota_systemd / ms_to_us(10) + 1) * ms_to_us(10);
}
}
let (id, value) = cpu::quota(quota_systemd)?;
props.push((id, value.into()));
}
Ok(())
}
fn set_memory(&self, props: &mut Vec<Property>, linux_memory: &LinuxMemory) -> Result<()> {
let v2 = self.v2();
let mem_limit = linux_memory.limit().unwrap_or(0);
if mem_limit != 0 {
let (id, value) = memory::limit(mem_limit, v2)?;
props.push((id, value.into()));
}
let reservation = linux_memory.reservation().unwrap_or(0);
if reservation != 0 && v2 {
let (id, value) = memory::low(reservation, v2)?;
props.push((id, value.into()));
}
let memswap_limit = linux_memory.swap().unwrap_or(0);
if memswap_limit != 0 && v2 {
let memswap_limit = conv::memory_swap_to_cgroup_v2(memswap_limit, mem_limit)?;
let (id, value) = memory::swap(memswap_limit, v2)?;
props.push((id, value.into()));
}
Ok(())
}
fn set_pids(&self, props: &mut Vec<Property>, linux_pids: &LinuxPids) -> Result<()> {
let limit = linux_pids.limit();
if limit == -1 || limit > 0 {
let (id, value) = pids::max(limit)?;
props.push((id, value.into()));
}
Ok(())
}
/// The systemd sends SIGTERM to processes in the unit on stop. Once a
/// timeout occurs, SIGKILL will be sent to the processes.
///
/// The item could be retrieved by:
///
/// ```bash
/// $ systemctl show <unit> -p TimeoutStopUSec
/// ```
pub fn set_term_timeout(&mut self, timeout_in_sec: u64) -> Result<()> {
let timeout_in_usec = timeout_in_sec * 1_000_000;
let prop = (TIMEOUT_STOP_USEC, ZbusValue::U64(timeout_in_usec));
self.systemd_client.set_properties(&[prop])?;
Ok(())
}
}
impl Manager for SystemdManager<'_> {
fn add_proc(&mut self, pid: CgroupPid) -> Result<()> {
if !self.systemd_client.exists() {
self.systemd_client.set_pid_prop(pid)?;
self.systemd_client.start()?;
// The fs_manager was created in load mode, which doesn't create
// the cgroups. So we create them here.
self.fs_manager.create_cgroups()?;
return Ok(());
}
let subcgroup = self.fs_manager.subcgroup();
self.systemd_client.add_process(pid, subcgroup)?;
Ok(())
}
/// `add_thread()` is the same as `add_proc()`, as systemd doesn't
/// expose an API to add a thread directly. As a result, the whole
/// threads belonging to one process will be added to this cgroup.
fn add_thread(&mut self, pid: CgroupPid) -> Result<()> {
self.add_proc(pid)
}
fn cgroup_path(&self, subsystem: Option<&str>) -> Result<String> {
self.fs_manager.cgroup_path(subsystem)
}
/// Destroy the cgroup and stop the transient unit.
///
/// Please note that if the current manager is in the cgroup, the
/// manager will be killed with SIGTERM signal. If you do not intend
/// that, please ignore the signal and do cleanup things immediately.
/// Systemd will forcibly terminate the process with SIGKILL after a
/// while.
fn destroy(&mut self) -> Result<()> {
self.systemd_client.stop()?;
Ok(())
}
fn enable_cpus_topdown(&self, cpus: &str) -> Result<()> {
self.fs_manager.enable_cpus_topdown(cpus)
}
fn freeze(&self, state: FreezerState) -> Result<()> {
match state {
FreezerState::Thawed => self.systemd_client.thaw()?,
FreezerState::Frozen => self.systemd_client.freeze()?,
FreezerState::Freezing => return Err(Error::InvalidArgument),
}
Ok(())
}
fn pids(&self) -> Result<Vec<CgroupPid>> {
self.fs_manager.pids()
}
fn set(&mut self, resources: &LinuxResources) -> Result<()> {
let mut props = vec![];
if let Some(linux_cpu) = resources.cpu() {
self.set_cpuset(&mut props, linux_cpu)?;
self.set_cpu(&mut props, linux_cpu)?;
}
if let Some(linux_memory) = resources.memory() {
self.set_memory(&mut props, linux_memory)?;
}
if let Some(linux_pids) = resources.pids() {
self.set_pids(&mut props, linux_pids)?;
}
self.systemd_client.set_properties(&props)?;
Ok(())
}
fn stats(&self) -> CgroupStats {
self.fs_manager.stats()
}
fn paths(&self) -> &HashMap<String, String> {
self.fs_manager.paths()
}
fn mounts(&self) -> &HashMap<String, String> {
self.fs_manager.mounts()
}
fn systemd(&self) -> bool {
true
}
fn v2(&self) -> bool {
self.fs_manager.v2()
}
}
fn new_unit_name(scope_prefix: &str, name: &str) -> String {
// By default, we create a scope unless the user explicitly asks
// for a slice.
if !name.ends_with(SLICE_SUFFIX) {
if scope_prefix.is_empty() {
// {name}.scope
return format!("{}{}", name, SCOPE_SUFFIX);
}
// {scope_prefix}-{name}.scope
return format!("{}-{}{}", scope_prefix, name, SCOPE_SUFFIX);
}
name.to_string()
}
#[inline]
/// Convert milliseconds to microseconds.
fn ms_to_us(ms: u64) -> u64 {
ms * 1_000
}
#[inline]
/// Convert seconds to microseconds.
fn s_to_us(s: u64) -> u64 {
s * 1_000_000
}
#[cfg(test)]
mod tests {
//! Tests for the `SystemdManager` implementation of the `Manager`
//! trait.
//!
//! Don't run tests in parallel, use `--test-threads=1`!
//!
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use oci_spec::runtime::{LinuxCpuBuilder, LinuxMemoryBuilder, LinuxResourcesBuilder};
use rand::distributions::Alphanumeric;
use rand::Rng;
use crate::fs::cpu::CpuController;
use crate::fs::memory::MemController;
use crate::fs::{ControllIdentifier, Controller, Subsystem};
use crate::manager::systemd::*;
use crate::manager::tests::{MEMORY_1G, MEMORY_2G, MEMORY_512M};
use crate::tests::spawn_sleep_inf;
use crate::{skip_if_cgroups_v1, skip_if_cgroups_v2, skip_if_no_systemd};
fn new_cgroups_path() -> (String, String, String) {
let rand_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(5)
.map(char::from)
.collect();
(
"cgroupsrs-test.slice".to_string(),
"cri".to_string(),
format!("pod{}", rand_string),
)
}
fn new_systemd_manager<'a>() -> SystemdManager<'a> {
let (slice, scope_prefix, name) = new_cgroups_path();
SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap()
}
fn run_set_resources_failed(resources: LinuxResources) {
let mut child = spawn_sleep_inf();
let mut manager = new_systemd_manager();
manager
.add_proc(CgroupPid {
pid: child.id() as u64,
})
.unwrap();
assert!(manager.set(&resources).is_err());
manager.destroy().unwrap();
child.wait().unwrap();
}
fn run_set_resources<F>(linux_resources: LinuxResources, test_fn: F)
where
F: FnOnce(&mut SystemdManager),
{
let mut manager = new_systemd_manager();
let mut child = spawn_sleep_inf();
manager
.add_proc(CgroupPid {
pid: child.id() as u64,
})
.unwrap();
manager.set(&linux_resources).unwrap();
test_fn(&mut manager);
manager.destroy().unwrap();
child.wait().unwrap();
}
#[test]
fn test_new_unit_name() {
assert_eq!(new_unit_name("test", "unit"), "test-unit.scope");
assert_eq!(new_unit_name("test", "unit.slice"), "unit.slice");
assert_eq!(new_unit_name("", "unit"), "unit.scope");
assert_eq!(new_unit_name("", "unit.slice"), "unit.slice");
assert_eq!(new_unit_name("prefix", "unit"), "prefix-unit.scope");
}
#[test]
fn test_slice_and_unit() {
skip_if_no_systemd!();
let (slice, scope_prefix, name) = new_cgroups_path();
let manager = SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap();
assert_eq!(manager.slice(), "cgroupsrs-test.slice");
assert_eq!(manager.unit(), format!("{scope_prefix}-{name}.scope"));
}
#[test]
fn test_destory() {
skip_if_no_systemd!();
let (slice, scope_prefix, name) = new_cgroups_path();
let mut manager =
SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap();
let cgroup_path = manager.cgroup_path(Some("memory")).unwrap();
// Before starting the unit, no cgroup should exist.
assert!(!Path::new(&cgroup_path).exists());
let mut child = spawn_sleep_inf();
manager
.add_proc(CgroupPid {
pid: child.id() as u64,
})
.unwrap();
// Now cgroup should exist.
assert!(Path::new(&cgroup_path).exists());
manager.destroy().unwrap();
// This process should be killed.
child.wait().unwrap();
// No cgroup should exist after destroy, retry 5 times at 1-second
// intervals.
for _ in 0..5 {
if !Path::new(&cgroup_path).exists() {
break;
}
sleep(Duration::from_secs(1));
}
assert!(!Path::new(&cgroup_path).exists());
// Unit should be stopped.
assert!(!manager.systemd_client.exists());
}
fn controller<'a, T>(fs_manager: &'a FsManager) -> &'a T
where
&'a T: From<&'a Subsystem>,
T: Controller + ControllIdentifier,
{
let controller: &T = fs_manager.cgroup().controller_of().unwrap();
controller
}
#[test]
fn test_set_cpu() {
skip_if_no_systemd!();
// 1024 shares, every 100ms allows to use 1 CPU
let linux_cpu = LinuxCpuBuilder::default()
.shares(1024u64)
.quota(100000i64)
.period(100000u64)
.quota(100000i64)
.build()
.unwrap();
let linux_resources = LinuxResourcesBuilder::default()
.cpu(linux_cpu)
.build()
.unwrap();
run_set_resources(linux_resources, |manager| {
let controller: &CpuController = controller(&manager.fs_manager);
let shares = controller.shares().unwrap();
let period = controller.cfs_period().unwrap();
let quota = controller.cfs_quota().unwrap();
if manager.v2() {
assert_eq!(shares, conv::cpu_shares_to_cgroup_v2(1024));
} else {
assert_eq!(shares, 1024);
}
assert_eq!(period, 100000);
assert_eq!(quota, 100000);
})
}
#[test]
fn test_set_memory_v2() {
skip_if_no_systemd!();
skip_if_cgroups_v1!();
// Expected failure: swap < limit
let linux_memory = LinuxMemoryBuilder::default()
.limit(MEMORY_1G)
.swap(MEMORY_512M)
.build()
.unwrap();
let linux_resources = LinuxResourcesBuilder::default()
.memory(linux_memory)
.build()
.unwrap();
run_set_resources_failed(linux_resources);
// Expected success
let linux_memory = LinuxMemoryBuilder::default()
.limit(MEMORY_512M)
.swap(MEMORY_1G)
.reservation(MEMORY_2G)
.build()
.unwrap();
let linux_resources = LinuxResourcesBuilder::default()
.memory(linux_memory)
.build()
.unwrap();
run_set_resources(linux_resources, |manager| {
let controller: &MemController = controller(&manager.fs_manager);
let memory_stat = controller.memory_stat();
let memory_swap_stat = controller.memswap();
assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M);
assert_eq!(memory_swap_stat.limit_in_bytes, MEMORY_512M);
assert_eq!(memory_stat.soft_limit_in_bytes, MEMORY_2G);
});
}
#[test]
fn test_set_memory_v1() {
skip_if_no_systemd!();
skip_if_cgroups_v2!();
// Expected success
let linux_memory = LinuxMemoryBuilder::default()
.limit(MEMORY_512M)
.build()
.unwrap();
let linux_resources = LinuxResourcesBuilder::default()
.memory(linux_memory)
.build()
.unwrap();
run_set_resources(linux_resources, |manager| {
let controller: &MemController = controller(&manager.fs_manager);
let memory_stat = controller.memory_stat();
assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M);
});
}
}

156
src/stats.rs Normal file
View File

@@ -0,0 +1,156 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct CgroupStats {
pub cpu: CpuCgroupStats,
pub memory: MemoryCgroupStats,
pub pids: PidsCgroupStats,
pub blkio: BlkioCgroupStats,
pub hugetlb: HugeTlbCgroupStats,
}
#[derive(Debug, Default)]
pub struct CpuCgroupStats {
pub cpu_acct: Option<CpuAcctStats>,
pub cpu_throttling: Option<CpuThrottlingStats>,
}
#[derive(Debug, Default)]
pub struct CpuAcctStats {
/// Usage in userspace, read from `cpuacct.stat` from the line starting
/// with `user`. Set 0 if no data.
pub user_usage: u64,
/// Usage in kernelspace, read from `cpuacct.stat` from the line
/// starting with `system`. Set 0 if no data.
pub system_usage: u64,
/// Total usage, read from `cpuacct.usage`. Set 0 if no data.
pub total_usage: u64,
/// Per-CPU usage, read from `cpuacct.usage_percpu`.
pub usage_percpu: Vec<u64>,
}
#[derive(Debug, Default)]
pub struct CpuThrottlingStats {
/// Periods, read from `cpu.stat` from the line starting with
/// `nr_periods`. Set 0 if no data.
pub periods: u64,
/// Throttled periods, read from `cpu.stat` from the line starting with
/// `nr_throttled`. Set 0 if no data.
pub throttled_periods: u64,
/// Throttled time, read from `cpu.stat` from the line starting with
/// `throttled_time`. Set 0 if no data.
pub throttled_time: u64,
}
#[derive(Debug, Default)]
pub struct MemoryCgroupStats {
pub memory: Option<MemoryStats>,
pub memory_swap: Option<MemoryStats>,
pub kernel_memory: Option<MemoryStats>,
/// Use hierarchy, read from `memory.use_hierarchy` in cgroups v1. Only
/// available in cgroups v1.
pub use_hierarchy: bool,
// The following data is read from `memory.stat`, see also
// `crate::fs::memory::MemoryStat::stat`.
pub cache: u64,
pub rss: u64,
pub rss_huge: u64,
pub shmem: u64,
pub mapped_file: u64,
pub dirty: u64,
pub writeback: u64,
pub swap: u64,
pub pgpgin: u64,
pub pgpgout: u64,
pub pgfault: u64,
pub pgmajfault: u64,
pub inactive_anon: u64,
pub active_anon: u64,
pub inactive_file: u64,
pub active_file: u64,
pub unevictable: u64,
pub hierarchical_memory_limit: i64,
pub hierarchical_memsw_limit: i64,
pub total_cache: u64,
pub total_rss: u64,
pub total_rss_huge: u64,
pub total_shmem: u64,
pub total_mapped_file: u64,
pub total_dirty: u64,
pub total_writeback: u64,
pub total_swap: u64,
pub total_pgpgin: u64,
pub total_pgpgout: u64,
pub total_pgfault: u64,
pub total_pgmajfault: u64,
pub total_inactive_anon: u64,
pub total_active_anon: u64,
pub total_inactive_file: u64,
pub total_active_file: u64,
pub total_unevictable: u64,
}
#[derive(Debug, Default)]
pub struct MemoryStats {
/// Memory [swap] usage, read from `memory[.memsw].usage_in_bytes` in
/// cgroups v1 and `memory[.swap].current` in cgroups v2.
pub usage: u64,
/// Maximum memory [swap] usage observed by cgroups, read from
/// `memory[.memsw].max_usage_in_bytes` in cgroups v1 and
/// `memory[.swap].peak` in cgroups v2.
pub max_usage: u64,
/// Memory [swap] limit, read from `memory[.memsw].limit_in_bytes` in
/// cgroups v1 and `memory[.swap].max` in cgroups v2.
pub limit: i64,
/// Failure count, read from `memory[.memsw].failcnt`. Only available in
/// cgroups v1.
pub fail_cnt: u64,
}
#[derive(Debug, Default)]
pub struct PidsCgroupStats {
/// Current number of processes in the cgroup, read from `pids.current`.
pub current: u64,
/// Maximum number of processes in the cgroup, read from `pids.limit`.
pub limit: i64,
}
#[derive(Debug, Default)]
pub struct BlkioCgroupStats {
pub io_service_bytes_recursive: Vec<BlkioStat>,
pub io_serviced_recursive: Vec<BlkioStat>,
pub io_queued_recursive: Vec<BlkioStat>,
pub io_service_time_recursive: Vec<BlkioStat>,
pub io_wait_time_recursive: Vec<BlkioStat>,
pub io_merged_recursive: Vec<BlkioStat>,
pub io_time_recursive: Vec<BlkioStat>,
pub sectors_recursive: Vec<BlkioStat>,
}
#[derive(Debug, Default)]
pub struct BlkioStat {
pub major: u64,
pub minor: u64,
pub op: String,
pub value: u64,
}
/// A structure representing the statistics of the `hugetlb` subsystem of a
/// Cgroup. The key is the huge page size, and the value is the statistics
/// for that size.
pub type HugeTlbCgroupStats = HashMap<String, HugeTlbStat>;
#[derive(Debug, Default)]
pub struct HugeTlbStat {
pub usage: u64,
pub max_usage: u64,
pub fail_cnt: u64,
}

68
src/systemd/consts.rs Normal file
View File

@@ -0,0 +1,68 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
/// Who enum: all
pub const WHO_ENUM_ALL: &str = "all";
/// Unit mode: replace
pub const UNIT_MODE_REPLACE: &str = "replace";
/// No such unit error
pub const NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit";
/// Default description for transient units.
pub const DEFAULT_DESCRIPTION: &str = "cgroups-rs transient unit";
/// Turn on CPU usage accounting for this unit.
pub const CPU_ACCOUNTING: &str = "CPUAccounting";
/// This setting controls the memory controller in the unified hierarchy.
/// Added in version 208.
pub const MEMORY_ACCOUNTING: &str = "MemoryAccounting";
/// This setting controls the pids controller in the unified hierarchy.
pub const TASKS_ACCOUNTING: &str = "TasksAccounting";
/// This setting controls the io controller in the unified hierarchy.
/// Added in version 230.
pub const IO_ACCOUNTING: &str = "IOAccounting";
/// This setting controls the block IO controller in the legacy hierarchy.
/// Deprecated in version 252.
pub const BLOCK_IO_ACCOUNTING: &str = "BlockIOAccounting";
/// Description of the unit.
pub const DESCRIPTION: &str = "Description";
/// PIDs
pub const PIDS: &str = "PIDs";
/// Default dependencies for this unit.
pub const DEFAULT_DEPENDENCIES: &str = "DefaultDependencies";
/// Wants, expressing a weak dependency on other units.
pub const WANTS: &str = "Wants";
/// Slice, used to assign a unit to a specific slice.
pub const SLICE: &str = "Slice";
/// Turns on delegation of further resource control partitioning to
/// processes of the unit.
pub const DELEGATE: &str = "Delegate";
/// Timeout for stopping the unit in microseconds.
pub const TIMEOUT_STOP_USEC: &str = "TimeoutStopUSec";
/// CPU shares in the legacy hierarchy.
pub const CPU_SHARES: &str = "CPUShares";
/// CPU shares in the unified hierarchy.
pub const CPU_WEIGHT: &str = "CPUWeight";
/// CPU quota period us.
pub const CPU_QUOTA_PERIOD_US: &str = "CPUQuotaPeriodUSec";
/// CPU quota us
pub const CPU_QUOTA_PER_SEC_US: &str = "CPUQuotaPerSecUSec";
/// Allowed CPUs
pub const ALLOWED_CPUS: &str = "AllowedCPUs";
/// Allowed memory nodes
pub const ALLOWED_MEMORY_NODES: &str = "AllowedMemoryNodes";
/// Memory limit in the legacy hierarchy.
pub const MEMORY_LIMIT: &str = "MemoryLimit";
/// Memory limit in the unified hierarchy.
pub const MEMORY_MAX: &str = "MemoryMax";
/// Memory low
pub const MEMORY_LOW: &str = "MemoryLow";
/// Memory swap max
pub const MEMORY_SWAP_MAX: &str = "MemorySwapMax";
/// Tasks max
pub const TASKS_MAX: &str = "TasksMax";

29
src/systemd/cpu.rs Normal file
View File

@@ -0,0 +1,29 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::systemd::error::Result;
use crate::systemd::{CPU_QUOTA_PERIOD_US, CPU_QUOTA_PER_SEC_US, CPU_SHARES, CPU_WEIGHT};
/// Returns the property for CPU shares.
///
/// Please note that if the shares is obtained from OCI runtime spec, it
/// MUST be converted, see [1] and `convert_shares_to_v2()`.
///
/// 1: https://github.com/containers/crun/blob/main/crun.1.md#cgroup-v2
pub fn shares(shares: u64, v2: bool) -> Result<(&'static str, u64)> {
let id = if v2 { CPU_WEIGHT } else { CPU_SHARES };
Ok((id, shares))
}
/// Returns the property for CPU period.
pub fn period(period: u64) -> Result<(&'static str, u64)> {
Ok((CPU_QUOTA_PERIOD_US, period))
}
/// Return the property for CPU quota.
pub fn quota(quota: u64) -> Result<(&'static str, u64)> {
Ok((CPU_QUOTA_PER_SEC_US, quota))
}

96
src/systemd/cpuset.rs Normal file
View File

@@ -0,0 +1,96 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use bit_vec::BitVec;
use crate::systemd::error::{Error, Result};
use crate::systemd::{ALLOWED_CPUS, ALLOWED_MEMORY_NODES};
const BYTE_IN_BITS: usize = 8;
/// Returns the property for cpuset CPUs.
pub fn cpus(cpus: &str) -> Result<(&'static str, Vec<u8>)> {
let mask = convert_list_to_mask(cpus)?;
Ok((ALLOWED_CPUS, mask))
}
/// Returns the property for cpuset memory nodes.
pub fn mems(mems: &str) -> Result<(&'static str, Vec<u8>)> {
let mask = convert_list_to_mask(mems)?;
Ok((ALLOWED_MEMORY_NODES, mask))
}
/// Convert cpuset cpus/mems from the string in comma-separated list format
/// to bitmask restored in `Vec<u8>`, see [1].
///
/// 1: https://man7.org/linux/man-pages/man7/cpuset.7.html
///
/// # Arguments
///
/// * `list` - A string slice that holds the list of CPUs in the format
/// "0-3,5,7".
fn convert_list_to_mask(list: &str) -> Result<Vec<u8>> {
let mut bit_vec = BitVec::from_elem(8, false);
let local_idx =
|index: usize| -> usize { index / BYTE_IN_BITS * BYTE_IN_BITS + 7 - index % BYTE_IN_BITS };
for part1 in list.split(',') {
let range: Vec<&str> = part1.split('-').collect();
match range.len() {
// x-
1 => {
let left: usize = range[0].parse().map_err(|_| Error::InvalidArgument)?;
while left >= bit_vec.len() {
bit_vec.grow(BYTE_IN_BITS, false);
}
bit_vec.set(local_idx(left), true);
}
// x-y
2 => {
let left: usize = range[0].parse().map_err(|_| Error::InvalidArgument)?;
let right: usize = range[1].parse().map_err(|_| Error::InvalidArgument)?;
while right >= bit_vec.len() {
bit_vec.grow(BYTE_IN_BITS, false);
}
for index in left..=right {
bit_vec.set(local_idx(index), true);
}
}
_ => {
return Err(Error::InvalidArgument);
}
}
}
let mut mask = bit_vec.to_bytes();
mask.reverse();
Ok(mask)
}
#[cfg(test)]
mod tests {
use crate::systemd::cpuset::convert_list_to_mask;
#[test]
fn test_convert_list_to_mask() {
let mask = convert_list_to_mask("2-4").unwrap();
assert_eq!(vec![0b00011100_u8], mask);
let mask = convert_list_to_mask("1,7").unwrap();
assert_eq!(vec![0b10000010_u8], mask);
let mask = convert_list_to_mask("0-4,9").unwrap();
assert_eq!(vec![0b00000010_u8, 0b00011111_u8], mask);
assert!(convert_list_to_mask("1-3-4").is_err());
assert!(convert_list_to_mask("1-3,,").is_err());
}
}

View File

@@ -0,0 +1,17 @@
# Systemd Dbus
How to generate `xxx_proxy.rs` files
```shell
# install zbus-xmlgen if not
$ cargo install zbus-xmlgen
# generate interface in XML format
$ busctl introspect --xml-interface \
org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager > /tmp/systemd1-manager.xml
# generate Rust code from XML
$ zbus-xmlgen file /tmp/systemd1-manager.xml \
--output src/systemd/dbus/systemd_manager_proxy.rs
$ rm -rf /tmp/systemd1-manager.xml
```

544
src/systemd/dbus/client.rs Normal file
View File

@@ -0,0 +1,544 @@
// Copyright 2021-2023 Kata Contributors
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use zbus::zvariant::Value;
use zbus::{Error as ZbusError, Result as ZbusResult};
use crate::systemd::dbus::error::{Error, Result};
use crate::systemd::dbus::proxy::systemd_manager_proxy;
use crate::systemd::{Property, NO_SUCH_UNIT, PIDS, UNIT_MODE_REPLACE};
use crate::CgroupPid;
pub struct SystemdClient<'a> {
/// The name of the systemd unit (slice or scope)
unit: String,
props: Vec<Property<'a>>,
}
impl<'a> SystemdClient<'a> {
pub fn new(unit: &str, props: Vec<Property<'a>>) -> Result<Self> {
Ok(Self {
unit: unit.to_string(),
props,
})
}
}
impl SystemdClient<'_> {
/// Set the pid to the PIDs property of the unit.
///
/// Append a process ID to the PIDs property of the unit. If not
/// exists, one property will be created.
pub fn set_pid_prop(&mut self, pid: CgroupPid) -> Result<()> {
if self.exists() {
return Ok(());
}
for prop in self.props.iter_mut() {
if prop.0 == PIDS {
// If PIDS is already set, we append the new pid to the existing list.
if let Value::Array(arr) = &mut prop.1 {
arr.append(pid.pid.into())
.map_err(|_| Error::InvalidProperties)?;
return Ok(());
}
// Invalid type of PIDs
return Err(Error::InvalidProperties);
}
}
// If PIDS is not set, we create a new property.
self.props
.push((PIDS, Value::Array(vec![pid.pid as u32].into())));
Ok(())
}
/// Start a slice or a scope unit controlled and supervised by systemd.
///
/// For more information, see:
/// https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
/// https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html
/// https://www.freedesktop.org/software/systemd/man/latest/systemd.scope.html
pub fn start(&self) -> Result<()> {
// PIDs property must be present
if !self.props.iter().any(|(k, _)| k == &PIDS) {
return Err(Error::InvalidProperties);
}
let sys_proxy = systemd_manager_proxy()?;
let props_borrowed: Vec<(&str, &zbus::zvariant::Value)> =
self.props.iter().map(|(k, v)| (*k, v)).collect();
let props_borrowed: Vec<&(&str, &Value)> = props_borrowed.iter().collect();
sys_proxy.start_transient_unit(&self.unit, UNIT_MODE_REPLACE, &props_borrowed, &[])?;
Ok(())
}
/// Stop the current transient unit, the processes will be killed on
/// unit stop, see [1].
///
/// 1. https://www.freedesktop.org/software/systemd/man/latest/systemd.kill.html#KillMode=
pub fn stop(&self) -> Result<()> {
let sys_proxy = systemd_manager_proxy()?;
let ret = sys_proxy.stop_unit(&self.unit, UNIT_MODE_REPLACE);
ignore_no_such_unit(ret)?;
// If we stop the unit and it still exists, it may be in a failed
// state, so we will try to reset it.
if self.exists() {
let ret = sys_proxy.reset_failed_unit(&self.unit);
ignore_no_such_unit(ret)?;
}
Ok(())
}
/// Set properties for the unit through dbus `SetUnitProperties`.
pub fn set_properties(&mut self, properties: &[Property<'static>]) -> Result<()> {
for prop in properties {
let new = prop.1.try_clone().map_err(|_| Error::InvalidProperties)?;
// Try to update the value first, if fails, append it.
if let Some(existing) = self.props.iter_mut().find(|p| p.0 == prop.0) {
existing.1 = new;
} else {
self.props.push((prop.0, new));
}
}
// The unit must exist before setting properties.
if !self.exists() {
return Ok(());
}
let sys_proxy = systemd_manager_proxy()?;
let props_borrowed: Vec<(&str, &Value)> = properties.iter().map(|(k, v)| (*k, v)).collect();
let props_borrowed: Vec<&(&str, &Value)> = props_borrowed.iter().collect();
sys_proxy.set_unit_properties(&self.unit, true, &props_borrowed)?;
Ok(())
}
/// Freeze the unit through dbus `FreezeUnit`.
pub fn freeze(&self) -> Result<()> {
let sys_proxy = systemd_manager_proxy()?;
sys_proxy.freeze_unit(&self.unit)?;
Ok(())
}
/// Thaw the frozen unit through dbus `ThawUnit`.
pub fn thaw(&self) -> Result<()> {
let sys_proxy = systemd_manager_proxy()?;
sys_proxy.thaw_unit(&self.unit)?;
Ok(())
}
/// Check if the unit exists.
pub fn exists(&self) -> bool {
let sys_proxy = match systemd_manager_proxy() {
Ok(proxy) => proxy,
_ => return false,
};
sys_proxy
.get_unit(&self.unit)
.map(|_| true)
.unwrap_or_default()
}
/// Add a process (tgid) to the unit through dbus
/// `AttachProcessesToUnit`.
pub fn add_process(&self, pid: CgroupPid, subcgroup: &str) -> Result<()> {
let sys_proxy = systemd_manager_proxy()?;
sys_proxy.attach_processes_to_unit(&self.unit, subcgroup, &[pid.pid as u32])?;
Ok(())
}
}
fn ignore_no_such_unit<T>(result: ZbusResult<T>) -> ZbusResult<bool> {
if let Err(ZbusError::MethodError(err_name, _, _)) = &result {
if err_name.as_str() == NO_SUCH_UNIT {
return Ok(true);
}
}
result.map(|_| false)
}
#[cfg(test)]
pub mod tests {
//! Unit tests for the SystemdClient
//!
//! Not sure why the tests are going to fail if we run them in
//! parallel. Everything goes smoothly in serial.
//!
//! $ cargo test --package cgroups-rs --lib \
//! -- systemd::dbus::client::tests \
//! --show-output --test-threads=1
use std::fs;
use std::path::Path;
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
use rand::distributions::Alphanumeric;
use rand::Rng;
use crate::fs::hierarchies;
use crate::systemd::dbus::client::*;
use crate::systemd::props::PropertiesBuilder;
use crate::systemd::utils::expand_slice;
use crate::systemd::{DEFAULT_DESCRIPTION, DESCRIPTION, PIDS};
use crate::tests::{spawn_sleep_inf, spawn_yes};
const TEST_SLICE: &str = "cgroupsrs-test.slice";
fn test_unit() -> String {
let rand_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(5)
.map(char::from)
.collect();
format!("cri-pod{}.scope", rand_string)
}
#[macro_export]
macro_rules! skip_if_no_systemd {
() => {
if $crate::tests::systemd_version().is_none() {
eprintln!("Test skipped, no systemd?");
return;
}
};
}
fn systemd_show(unit: &str) -> String {
let output = Command::new("systemctl")
.arg("show")
.arg(unit)
.output()
.expect("Failed to execute systemctl show command");
String::from_utf8_lossy(&output.stdout).to_string()
}
fn start_default_cgroup(pid: CgroupPid, unit: &'_ str) -> SystemdClient<'_> {
let mut props = PropertiesBuilder::default_cgroup(TEST_SLICE, unit).build();
props.push((PIDS, Value::Array(vec![pid.pid as u32].into())));
let cgroup = SystemdClient::new(unit, props).unwrap();
// Stop the unit if it exists.
cgroup.stop().unwrap();
// Write the current process to the cgroup.
cgroup.start().unwrap();
cgroup.add_process(pid, "/").unwrap();
cgroup
}
fn stop_cgroup(cgroup: &SystemdClient) {
cgroup.stop().unwrap();
}
#[test]
fn test_start() {
skip_if_no_systemd!();
let v2 = hierarchies::is_cgroup2_unified_mode();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let base = expand_slice(TEST_SLICE).unwrap();
// Check if the cgroup exists in the filesystem
let full_base = if v2 {
format!("/sys/fs/cgroup/{}", base)
} else {
format!("/sys/fs/cgroup/memory/{}", base)
};
assert!(
Path::new(&full_base).exists(),
"Cgroup base path does not exist: {}",
full_base
);
// PIDs
let cgroup_procs_path = format!("{}/{}/cgroup.procs", full_base, &unit);
for i in 0..5 {
let content = fs::read_to_string(&cgroup_procs_path);
if let Ok(content) = &content {
if content.contains(&child.id().to_string()) {
break;
}
}
// Retry attempts exhausted, resulting in failure
if i == 4 {
let content = content.as_ref().unwrap();
assert!(
content.contains(&child.id().to_string()),
"Cgroup procs does not contain the child process ID"
);
}
// Wait 500ms before next retrying
sleep(Duration::from_millis(500));
}
// Check the unit from "systemctl show <unit>"
let output = systemd_show(&cgroup.unit);
// Slice
assert!(
output
.lines()
.any(|line| line == format!("Slice={}", TEST_SLICE)),
"Slice not found"
);
// Delegate
assert!(
output.lines().any(|line| line == "Delegate=yes"),
"Delegate not set"
);
// DelegateControllers
// controllers: cpu cpuacct cpuset io blkio memory devices pids
let controllers = output
.lines()
.find(|line| line.starts_with("DelegateControllers="))
.map(|line| line.trim_start_matches("DelegateControllers="))
.unwrap();
let controllers = controllers.split(' ').collect::<Vec<&str>>();
assert!(
controllers.contains(&"cpu"),
"DelegateControllers cpu not set"
);
assert!(
controllers.contains(&"cpuset"),
"DelegateControllers cpuset not set"
);
if v2 {
assert!(
controllers.contains(&"io"),
"DelegateControllers io not set"
);
} else {
assert!(
controllers.contains(&"blkio"),
"DelegateControllers blkio not set"
);
}
assert!(
controllers.contains(&"memory"),
"DelegateControllers memory not set"
);
assert!(
controllers.contains(&"pids"),
"DelegateControllers pids not set"
);
// CPUAccounting
assert!(
output.lines().any(|line| line == "CPUAccounting=yes"),
"CPUAccounting not set"
);
// IOAccounting for v2, and BlockIOAccounting for v1
if v2 {
assert!(
output.lines().any(|line| line == "IOAccounting=yes"),
"IOAccounting not set"
);
} else {
assert!(
output.lines().any(|line| line == "BlockIOAccounting=yes"),
"BlockIOAccounting not set"
);
}
// MemoryAccounting
assert!(
output.lines().any(|line| line == "MemoryAccounting=yes"),
"MemoryAccounting not set"
);
// TasksAccounting
assert!(
output.lines().any(|line| line == "TasksAccounting=yes"),
"TasksAccounting not set"
);
// ActiveState
assert!(
output.lines().any(|line| line == "ActiveState=active"),
"Unit is not active"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_stop() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
// Check ActiveState: expected to be "active"
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line == "ActiveState=active"),
"Unit is not active"
);
stop_cgroup(&cgroup);
// Check ActiveState: expected to be "inactive"
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line == "ActiveState=inactive"),
"Unit is not inactive"
);
child.wait().unwrap();
}
#[test]
fn test_set_properties() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let mut cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line
== format!(
"Description={} {}:{}",
DEFAULT_DESCRIPTION, TEST_SLICE, unit
)),
"Initial description not set correctly"
);
let properties = [(
DESCRIPTION,
Value::Str("kata-container1 description".into()),
)];
cgroup.set_properties(&properties).unwrap();
assert!(cgroup.props.iter().any(|(k, v)| {
k == &DESCRIPTION && v == &Value::Str("kata-container1 description".into())
}));
let output = systemd_show(&cgroup.unit);
assert!(
output
.lines()
.any(|line| line == "Description=kata-container1 description"),
"Updated description not set correctly"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_freeze_and_thaw() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_yes();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
// Freeze the unit
cgroup.freeze().unwrap();
let pid = child.id() as u64;
let stat_path = format!("/proc/{}/stat", pid);
let content = fs::read_to_string(&stat_path).unwrap();
// The process state is the third field, e.g.:
// 1234 (bash) S 1233 ...
// ^
let mut content_iter = content.split_whitespace();
assert_eq!(
content_iter.nth(2).unwrap(),
"S",
"Process should be in 'S' (sleeping) state after freezing"
);
// Thaw the unit
cgroup.thaw().unwrap();
// No more S now
let content = fs::read_to_string(&stat_path).unwrap();
let mut content_iter = content.split_whitespace();
assert_ne!(
content_iter.nth(2).unwrap(),
"S",
"Process should not be in 'S' (sleeping) state after thawing"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_exists() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
assert!(cgroup.exists(), "Cgroup should exist after starting");
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_add_process() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let mut child1 = spawn_sleep_inf();
let pid1 = CgroupPid::from(child1.id() as u64);
cgroup.add_process(pid1, "/").unwrap();
let cgroup_procs_path = format!(
"/sys/fs/cgroup/{}/{}/cgroup.procs",
expand_slice(TEST_SLICE).unwrap(),
unit
);
for i in 0..5 {
let content = fs::read_to_string(&cgroup_procs_path);
if let Ok(content) = content {
assert!(
content.contains(&child1.id().to_string()),
"Cgroup procs does not contain the child1 process ID"
);
break;
}
// Retry attempts exhausted, resulting in failure
if i == 4 {
content.unwrap();
}
// Wait 500ms before next retrying
sleep(Duration::from_millis(500));
}
stop_cgroup(&cgroup);
child.wait().unwrap();
child1.wait().unwrap();
}
}

15
src/systemd/dbus/error.rs Normal file
View File

@@ -0,0 +1,15 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0
//
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid properties")]
InvalidProperties,
#[error("dbus error: {0}")]
Dbus(#[from] zbus::Error),
}

18
src/systemd/dbus/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
// Copyright (c) 2018 Levente Kurusa
// Copyright (c) 2020-2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
//! Systemd D-Bus interface for managing cgroups and units.
//!
//! References:
//! https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html
//! https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
//! https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
mod client;
pub mod error;
mod systemd_manager_proxy;
pub use client::SystemdClient;
mod proxy;

16
src/systemd/dbus/proxy.rs Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use zbus::blocking::Connection;
use zbus::Result;
use crate::systemd::dbus::systemd_manager_proxy::ManagerProxyBlocking as SystemManager;
pub(crate) fn systemd_manager_proxy<'a>() -> Result<SystemManager<'a>> {
let connection = Connection::system()?;
let proxy = SystemManager::new(&connection)?;
Ok(proxy)
}

File diff suppressed because it is too large Load Diff

15
src/systemd/error.rs Normal file
View File

@@ -0,0 +1,15 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid argument")]
InvalidArgument,
#[error("resource not supported by cgroups v1")]
CgroupsV1NotSupported,
}

32
src/systemd/memory.rs Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::systemd::error::{Error, Result};
use crate::systemd::{MEMORY_LIMIT, MEMORY_LOW, MEMORY_MAX, MEMORY_SWAP_MAX};
/// Returns the property for memory limit.
pub fn limit(limit: i64, v2: bool) -> Result<(&'static str, u64)> {
let id = if v2 { MEMORY_MAX } else { MEMORY_LIMIT };
Ok((id, limit as u64))
}
/// Returns the property for memory limit.
pub fn low(low: i64, v2: bool) -> Result<(&'static str, u64)> {
if !v2 {
return Err(Error::CgroupsV1NotSupported);
}
Ok((MEMORY_LOW, low as u64))
}
/// Returns the property for memory swap.
pub fn swap(swap: i64, v2: bool) -> Result<(&'static str, u64)> {
if !v2 {
return Err(Error::CgroupsV1NotSupported);
}
Ok((MEMORY_SWAP_MAX, swap as u64))
}

22
src/systemd/mod.rs Normal file
View File

@@ -0,0 +1,22 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
pub mod cpu;
pub mod cpuset;
pub mod dbus;
pub use dbus::SystemdClient;
mod consts;
pub use consts::*;
pub mod error;
pub mod memory;
pub mod pids;
pub mod props;
pub use props::Property;
pub mod utils;
pub const DEFAULT_SLICE: &str = "system.slice";
pub const SLICE_SUFFIX: &str = ".slice";
pub const SCOPE_SUFFIX: &str = ".scope";

11
src/systemd/pids.rs Normal file
View File

@@ -0,0 +1,11 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::systemd::error::Result;
use crate::systemd::TASKS_MAX;
pub fn max(max: i64) -> Result<(&'static str, u64)> {
Ok((TASKS_MAX, max as u64))
}

170
src/systemd/props.rs Normal file
View File

@@ -0,0 +1,170 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use zbus::zvariant::Value as ZbusValue;
use crate::fs::hierarchies;
use crate::systemd::utils::is_slice_unit;
use crate::systemd::{
BLOCK_IO_ACCOUNTING, CPU_ACCOUNTING, DEFAULT_DEPENDENCIES, DEFAULT_DESCRIPTION, DELEGATE,
DESCRIPTION, IO_ACCOUNTING, MEMORY_ACCOUNTING, PIDS, SLICE, TASKS_ACCOUNTING,
TIMEOUT_STOP_USEC, WANTS,
};
pub type Property<'a> = (&'a str, ZbusValue<'a>);
#[derive(Debug, Clone, Default)]
pub struct PropertiesBuilder {
cpu_accounting: Option<bool>,
// MemoryAccount is for cgroup v2 as documented in dbus. However,
// "github.com/opencontainer/runc" uses it for all. Shall we follow the
// same way?
memory_accounting: Option<bool>,
task_accounting: Option<bool>,
// Use IO_ACCOUNTING for cgroup v2 and BLOCK_IO_ACCOUNTING for cgroup v1.
io_accounting: Option<bool>,
default_dependencies: Option<bool>,
description: Option<String>,
wants: Option<String>,
slice: Option<String>,
delegate: Option<bool>,
pids: Option<Vec<u32>>,
timeout_stop_usec: Option<u64>,
}
impl PropertiesBuilder {
pub fn default_cgroup(slice: &str, unit: &str) -> Self {
let mut builder = Self::default()
.cpu_accounting(true)
.memory_accounting(true)
.task_accounting(true)
.io_accounting(true)
.default_dependencies(false)
.description(format!("{} {}:{}", DEFAULT_DESCRIPTION, slice, unit));
if is_slice_unit(unit) {
// If we create a slice, the parent is defined via a Wants=.
builder = builder.wants(slice.to_string());
} else {
// Otherwise it's a scope, which we put into a Slice=.
builder = builder.slice(slice.to_string());
// Assume scopes always support delegation (supported since systemd v218).
builder = builder.delegate(true);
}
builder
}
pub fn cpu_accounting(mut self, enabled: bool) -> Self {
self.cpu_accounting = Some(enabled);
self
}
pub fn memory_accounting(mut self, enabled: bool) -> Self {
self.memory_accounting = Some(enabled);
self
}
pub fn task_accounting(mut self, enabled: bool) -> Self {
self.task_accounting = Some(enabled);
self
}
pub fn io_accounting(mut self, enabled: bool) -> Self {
self.io_accounting = Some(enabled);
self
}
pub fn default_dependencies(mut self, enabled: bool) -> Self {
self.default_dependencies = Some(enabled);
self
}
pub fn description(mut self, desc: String) -> Self {
self.description = Some(desc);
self
}
pub fn wants(mut self, wants: String) -> Self {
self.wants = Some(wants);
self
}
pub fn slice(mut self, slice: String) -> Self {
self.slice = Some(slice);
self
}
pub fn delegate(mut self, enabled: bool) -> Self {
self.delegate = Some(enabled);
self
}
pub fn pids(mut self, pids: Vec<u32>) -> Self {
self.pids = Some(pids);
self
}
pub fn timeout_stop_usec(mut self, timeout: u64) -> Self {
self.timeout_stop_usec = Some(timeout);
self
}
pub fn build(self) -> Vec<Property<'static>> {
let mut props = vec![];
if let Some(cpu_accounting) = self.cpu_accounting {
props.push((CPU_ACCOUNTING, ZbusValue::Bool(cpu_accounting)));
}
if let Some(memory_accounting) = self.memory_accounting {
props.push((MEMORY_ACCOUNTING, ZbusValue::Bool(memory_accounting)));
}
if let Some(task_accounting) = self.task_accounting {
props.push((TASKS_ACCOUNTING, ZbusValue::Bool(task_accounting)));
}
if let Some(io_accounting) = self.io_accounting {
if hierarchies::is_cgroup2_unified_mode() {
props.push((IO_ACCOUNTING, ZbusValue::Bool(io_accounting)));
} else {
props.push((BLOCK_IO_ACCOUNTING, ZbusValue::Bool(io_accounting)));
}
}
if let Some(default_dependencies) = self.default_dependencies {
props.push((DEFAULT_DEPENDENCIES, ZbusValue::Bool(default_dependencies)));
}
if let Some(description) = self.description {
props.push((DESCRIPTION, ZbusValue::Str(description.into())));
} else {
props.push((DESCRIPTION, ZbusValue::Str(DEFAULT_DESCRIPTION.into())));
}
if let Some(wants) = self.wants {
props.push((WANTS, ZbusValue::Str(wants.into())));
}
if let Some(slice) = self.slice {
props.push((SLICE, ZbusValue::Str(slice.into())));
}
if let Some(delegate) = self.delegate {
props.push((DELEGATE, ZbusValue::Bool(delegate)));
}
if let Some(pids) = self.pids {
props.push((PIDS, ZbusValue::Array(pids.into())));
}
if let Some(timeout) = self.timeout_stop_usec {
props.push((TIMEOUT_STOP_USEC, ZbusValue::U64(timeout)));
}
props
}
}

101
src/systemd/utils.rs Normal file
View File

@@ -0,0 +1,101 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::systemd::error::{Error, Result};
use crate::systemd::{SCOPE_SUFFIX, SLICE_SUFFIX};
/// Check if a systemd unit name is a slice unit.
pub fn is_slice_unit(name: &str) -> bool {
name.ends_with(SLICE_SUFFIX)
}
/// Check if a systemd unit name is a scope unit.
pub fn is_scope_unit(name: &str) -> bool {
name.ends_with(SCOPE_SUFFIX)
}
/// Expand a slice name to a full path in the filesystem.
///
/// # Arguments
///
/// * `slice` - A string slice that holds the slice name in the format
/// "xxx-yyy-zzz.slice".
///
/// # Returns
///
/// A string that represents the full path of the slice in the filesystem.
/// In the above case, the value would be
/// "xxx.slice/xxx-yyy.slice/xxx-yyy-zzz.slice".
pub fn expand_slice(slice: &str) -> Result<String> {
// Name has to end with ".slice", but can't be just ".slice".
if !slice.ends_with(SLICE_SUFFIX) || slice.len() < SLICE_SUFFIX.len() {
return Err(Error::InvalidArgument);
}
// Path-separators are not allowed.
if slice.contains('/') {
return Err(Error::InvalidArgument);
}
let name = slice.trim_end_matches(SLICE_SUFFIX);
// If input was -.slice, we should just return root now
if name == "-" {
return Ok("".to_string());
}
let mut slice_path = String::new();
let mut prefix = String::new();
for sub_slice in name.split('-') {
if sub_slice.is_empty() {
return Err(Error::InvalidArgument);
}
slice_path = format!("{}/{}{}{}", slice_path, prefix, sub_slice, SLICE_SUFFIX);
prefix = format!("{}{}-", prefix, sub_slice);
}
// We need a relative path, so remove the first slash.
slice_path.remove(0);
Ok(slice_path)
}
#[cfg(test)]
mod tests {
use crate::systemd::utils::*;
#[test]
fn test_is_slice_unit() {
assert!(is_slice_unit("test.slice"));
assert!(!is_slice_unit("test.scope"));
}
#[test]
fn test_is_scope_unit() {
assert!(is_scope_unit("test.scope"));
assert!(!is_scope_unit("test.slice"));
}
#[test]
fn test_expand_slice() {
assert_eq!(expand_slice("test.slice").unwrap(), "test.slice");
assert_eq!(
expand_slice("test-1.slice").unwrap(),
"test.slice/test-1.slice"
);
assert_eq!(
expand_slice("test-1-test-2.slice").unwrap(),
"test.slice/test-1.slice/test-1-test.slice/test-1-test-2.slice"
);
assert_eq!(
expand_slice("slice-slice.slice").unwrap(),
"slice.slice/slice-slice.slice"
);
assert_eq!(expand_slice("-.slice").unwrap(), "");
assert!(expand_slice("invalid/slice").is_err());
assert!(expand_slice("invalid-slice").is_err());
}
}

View File

@@ -5,24 +5,25 @@
//
//! Some simple tests covering the builder pattern for control groups.
use cgroups_rs::blkio::*;
use cgroups_rs::cgroup_builder::*;
use cgroups_rs::cpu::*;
use cgroups_rs::devices::*;
use cgroups_rs::hugetlb::*;
use cgroups_rs::memory::*;
use cgroups_rs::net_cls::*;
use cgroups_rs::pid::*;
use cgroups_rs::*;
use cgroups_rs::fs::blkio::*;
use cgroups_rs::fs::cgroup_builder::*;
use cgroups_rs::fs::cpu::*;
use cgroups_rs::fs::devices::*;
use cgroups_rs::fs::hugetlb::*;
use cgroups_rs::fs::memory::*;
use cgroups_rs::fs::net_cls::*;
use cgroups_rs::fs::pid::*;
use cgroups_rs::fs::*;
#[test]
pub fn test_cpu_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
let cg: Cgroup = CgroupBuilder::new("test_cpu_res_build")
.cpu()
.shares(85)
.done()
.build(h);
.build(h)
.unwrap();
{
let cpu: &CpuController = cg.controller_of().unwrap();
@@ -35,14 +36,15 @@ pub fn test_cpu_res_build() {
#[test]
pub fn test_memory_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
let cg: Cgroup = CgroupBuilder::new("test_memory_res_build")
.memory()
.kernel_memory_limit(128 * 1024 * 1024)
.swappiness(70)
.memory_hard_limit(1024 * 1024 * 1024)
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &MemController = cg.controller_of().unwrap();
@@ -59,12 +61,13 @@ pub fn test_memory_res_build() {
#[test]
pub fn test_pid_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
let cg: Cgroup = CgroupBuilder::new("test_pid_res_build")
.pid()
.maximum_number_of_processes(MaxValue::Value(123))
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &PidController = cg.controller_of().unwrap();
@@ -78,12 +81,13 @@ pub fn test_pid_res_build() {
#[test]
#[ignore] // ignore this test for now, not sure why my kernel doesn't like it
pub fn test_devices_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
let cg: Cgroup = CgroupBuilder::new("test_devices_res_build")
.devices()
.device(1, 6, DeviceType::Char, true, vec![DevicePermissions::Read])
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &DevicesController = cg.controller_of().unwrap();
@@ -104,7 +108,7 @@ pub fn test_devices_res_build() {
#[test]
pub fn test_network_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
if h.v2() {
// FIXME add cases for v2
return;
@@ -113,7 +117,8 @@ pub fn test_network_res_build() {
.network()
.class_id(1337)
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &NetClsController = cg.controller_of().unwrap();
@@ -125,7 +130,7 @@ pub fn test_network_res_build() {
#[test]
pub fn test_hugepages_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
if h.v2() {
// FIXME add cases for v2
return;
@@ -134,7 +139,8 @@ pub fn test_hugepages_res_build() {
.hugepages()
.limit("2MB".to_string(), 4 * 2 * 1024 * 1024)
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &HugeTlbController = cg.controller_of().unwrap();
@@ -147,12 +153,13 @@ pub fn test_hugepages_res_build() {
#[test]
#[ignore] // high version kernel not support `blkio.weight`
pub fn test_blkio_res_build() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
let cg: Cgroup = CgroupBuilder::new("test_blkio_res_build")
.blkio()
.weight(100)
.done()
.build(h);
.build(h)
.unwrap();
{
let c: &BlkIoController = cg.controller_of().unwrap();

View File

@@ -5,29 +5,62 @@
//
//! Simple unit tests about the control groups system.
use cgroups_rs::memory::MemController;
use cgroups_rs::Controller;
use cgroups_rs::{Cgroup, CgroupPid, Subsystem};
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
use cgroups_rs::fs::cgroup::{
CGROUP_MODE_DOMAIN, CGROUP_MODE_DOMAIN_INVALID, CGROUP_MODE_DOMAIN_THREADED,
CGROUP_MODE_THREADED,
};
use cgroups_rs::fs::memory::MemController;
use cgroups_rs::fs::Controller;
use cgroups_rs::fs::{Cgroup, Subsystem};
use cgroups_rs::CgroupPid;
#[test]
fn test_tasks_iterator() {
let h = cgroups_rs::hierarchies::auto();
fn test_procs_iterator_cgroup() {
let h = cgroups_rs::fs::hierarchies::auto();
let pid = libc::pid_t::from(nix::unistd::getpid()) as u64;
let cg = Cgroup::new(h, String::from("test_tasks_iterator"));
let cg = Cgroup::new(h, String::from("test_procs_iterator_cgroup")).unwrap();
{
// Add a task to the control group.
cg.add_task_by_tgid(CgroupPid::from(pid)).unwrap();
let mut procs = cg.procs().into_iter();
// Verify that the task is indeed in the xcontrol group
assert_eq!(procs.next(), Some(CgroupPid::from(pid)));
assert_eq!(procs.next(), None);
// Now, try removing it.
cg.remove_task_by_tgid(CgroupPid::from(pid)).unwrap();
procs = cg.procs().into_iter();
// Verify that it was indeed removed.
assert_eq!(procs.next(), None);
}
cg.delete().unwrap();
}
#[test]
fn test_tasks_iterator_cgroup_v1() {
if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::fs::hierarchies::auto();
let pid = libc::pid_t::from(nix::unistd::getpid()) as u64;
let cg = Cgroup::new(h, String::from("test_tasks_iterator_cgroup_v1")).unwrap();
{
// Add a task to the control group.
cg.add_task(CgroupPid::from(pid)).unwrap();
use std::{thread, time};
thread::sleep(time::Duration::from_millis(100));
let mut tasks = cg.tasks().into_iter();
// Verify that the task is indeed in the control group
// Verify that the task is indeed in the xcontrol group
assert_eq!(tasks.next(), Some(CgroupPid::from(pid)));
assert_eq!(tasks.next(), None);
// Now, try removing it.
cg.remove_task(CgroupPid::from(pid));
cg.remove_task(CgroupPid::from(pid)).unwrap();
tasks = cg.tasks().into_iter();
// Verify that it was indeed removed.
@@ -37,11 +70,148 @@ fn test_tasks_iterator() {
}
#[test]
fn test_cgroup_with_relative_paths() {
if cgroups_rs::hierarchies::is_cgroup2_unified_mode() {
fn test_tasks_iterator_cgroup_threaded_mode() {
if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::hierarchies::auto();
let pid = libc::pid_t::from(nix::unistd::getpid()) as u64;
let cg = Cgroup::new(
cgroups_rs::fs::hierarchies::auto(),
String::from("test_tasks_iterator_cgroup_threaded_mode"),
)
.unwrap();
let cg_threaded_sub1 = Cgroup::new_with_specified_controllers(
cgroups_rs::fs::hierarchies::auto(),
String::from("test_tasks_iterator_cgroup_threaded_mode/threaded_sub1"),
Some(vec![String::from("cpuset"), String::from("cpu")]),
)
.unwrap();
let cg_threaded_sub2 = Cgroup::new_with_specified_controllers(
cgroups_rs::fs::hierarchies::auto(),
String::from("test_tasks_iterator_cgroup_threaded_mode/threaded_sub2"),
Some(vec![String::from("cpuset"), String::from("cpu")]),
)
.unwrap();
{
// Verify that cgroup type of the control group is domain mode.
assert_eq!(cg.get_cgroup_type().unwrap(), CGROUP_MODE_DOMAIN);
// Set cgroup type of the sub-control group is thread mode.
cg_threaded_sub1
.set_cgroup_type(CGROUP_MODE_THREADED)
.unwrap();
// Verify that cgroup type of the sub-control group is thread mode.
assert_eq!(
cg_threaded_sub1.get_cgroup_type().unwrap(),
CGROUP_MODE_THREADED
);
// Verify that the cgroup type of the sub-control group that does
// not set the cgroup type is domain invalid mode.
assert_eq!(
cg_threaded_sub2.get_cgroup_type().unwrap(),
CGROUP_MODE_DOMAIN_INVALID
);
// Verify whether the cgroup type of the parent control group of
// the control group whose cgroup type is set to thread mode is
// domain thread mode.
assert_eq!(cg.get_cgroup_type().unwrap(), CGROUP_MODE_DOMAIN_THREADED);
// Set cgroup type of the sub-control group is thread mode.
cg_threaded_sub2
.set_cgroup_type(CGROUP_MODE_THREADED)
.unwrap();
// Verify that cgroup type of the sub-control group is thread mode.
assert_eq!(
cg_threaded_sub2.get_cgroup_type().unwrap(),
CGROUP_MODE_THREADED
);
// Add a proc to the control group.
cg.add_task_by_tgid(CgroupPid::from(pid)).unwrap();
let mut procs = cg.procs().into_iter();
// Verify that the task is indeed in the x control group
assert_eq!(procs.next(), Some(CgroupPid::from(pid)));
assert_eq!(procs.next(), None);
// Add a task to the sub control group.
cg_threaded_sub1.add_task(CgroupPid::from(pid)).unwrap();
let mut tasks = cg_threaded_sub1.tasks().into_iter();
// Verify that the task is indeed in the xcontrol group
assert_eq!(tasks.next(), Some(CgroupPid::from(pid)));
assert_eq!(tasks.next(), None);
// Now, try move it to parent.
cg_threaded_sub1
.move_task_to_parent(CgroupPid::from(pid))
.unwrap();
tasks = cg_threaded_sub1.tasks().into_iter();
// Verify that it was indeed removed.
assert_eq!(tasks.next(), None);
// Now, try removing it.
cg.remove_task_by_tgid(CgroupPid::from(pid)).unwrap();
procs = cg.procs().into_iter();
// Verify that it was indeed removed.
assert_eq!(procs.next(), None);
}
cg_threaded_sub1.delete().unwrap();
cg_threaded_sub2.delete().unwrap();
cg.delete().unwrap();
}
#[test]
fn test_kill_cgroup() {
if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_kill_cgroup")).unwrap();
{
// Spawn a proc, don't want to getpid(2) here.
let mut child = Command::new("sleep").arg("infinity").spawn().unwrap();
cg.add_task_by_tgid(CgroupPid::from(child.id() as u64))
.unwrap();
let cg_procs = cg.procs();
assert_eq!(cg_procs.len(), 1_usize);
// Now kill and wait on the proc.
cg.kill().unwrap();
let mut tries = 0;
let status: Option<std::process::ExitStatus> = loop {
match child.try_wait() {
Ok(Some(status)) => {
break Some(status);
}
Ok(None) => {
if tries > 3 {
break None;
}
sleep(Duration::from_millis(100));
tries += 1;
}
Err(e) => {
child.kill().unwrap();
panic!("error attempting to wait: {}", e);
}
}
};
assert!(status.is_some());
}
cg.delete().unwrap();
}
#[test]
fn test_cgroup_with_relative_paths() {
if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::fs::hierarchies::auto();
let cgroup_root = h.root();
let cgroup_name = "test_cgroup_with_relative_paths";
@@ -79,11 +249,11 @@ fn test_cgroup_with_relative_paths() {
#[test]
fn test_cgroup_v2() {
if !cgroups_rs::hierarchies::is_cgroup2_unified_mode() {
if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_v2"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_v2")).unwrap();
let mem_controller: &MemController = cg.controller_of().unwrap();
let (mem, swp, rev) = (4 * 1024 * 1000, 2 * 1024 * 1000, 1024 * 1000);

View File

@@ -4,13 +4,13 @@
//
//! Simple unit tests about the CPU control groups system.
use cgroups_rs::cpu::CpuController;
use cgroups_rs::Cgroup;
use cgroups_rs::fs::cpu::CpuController;
use cgroups_rs::fs::Cgroup;
#[test]
fn test_cfs_quota_and_periods() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cfs_quota_and_periods"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cfs_quota_and_periods")).unwrap();
let cpu_controller: &CpuController = cg.controller_of().unwrap();

View File

@@ -3,17 +3,17 @@
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use cgroups_rs::cpuset::CpuSetController;
use cgroups_rs::error::ErrorKind;
use cgroups_rs::{Cgroup, CgroupPid};
use std::fs;
use cgroups_rs::fs::cpuset::CpuSetController;
use cgroups_rs::fs::error::ErrorKind;
use cgroups_rs::fs::Cgroup;
use cgroups_rs::CgroupPid;
#[test]
fn test_cpuset_memory_pressure_root_cg() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_memory_pressure_root_cg"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_memory_pressure_root_cg")).unwrap();
{
let cpuset: &CpuSetController = cg.controller_of().unwrap();
@@ -26,8 +26,8 @@ fn test_cpuset_memory_pressure_root_cg() {
#[test]
fn test_cpuset_set_cpus() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus")).unwrap();
{
let cpuset: &CpuSetController = cg.controller_of().unwrap();
@@ -63,8 +63,8 @@ fn test_cpuset_set_cpus() {
#[test]
fn test_cpuset_set_cpus_add_task() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus_add_task/sub-dir"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus_add_task/sub-dir")).unwrap();
let cpuset: &CpuSetController = cg.controller_of().unwrap();
let set = cpuset.cpuset();
@@ -77,13 +77,13 @@ fn test_cpuset_set_cpus_add_task() {
// Add a task to the control group.
let pid_i = libc::pid_t::from(nix::unistd::getpid()) as u64;
let _ = cg.add_task(CgroupPid::from(pid_i));
let _ = cg.add_task_by_tgid(CgroupPid::from(pid_i));
let tasks = cg.tasks();
assert!(!tasks.is_empty());
println!("tasks after added: {:?}", tasks);
// remove task
let _ = cg.remove_task(CgroupPid::from(pid_i));
cg.remove_task_by_tgid(CgroupPid::from(pid_i)).unwrap();
let tasks = cg.tasks();
println!("tasks after deleted: {:?}", tasks);
assert_eq!(0, tasks.len());

View File

@@ -6,18 +6,18 @@
//! Integration tests about the devices subsystem
use cgroups_rs::devices::{DevicePermissions, DeviceType, DevicesController};
use cgroups_rs::{Cgroup, DeviceResource};
use cgroups_rs::fs::devices::{DevicePermissions, DeviceType, DevicesController};
use cgroups_rs::fs::{Cgroup, DeviceResource};
#[test]
fn test_devices_parsing() {
// now only v2
if cgroups_rs::hierarchies::is_cgroup2_unified_mode() {
if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_devices_parsing"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_devices_parsing")).unwrap();
{
let devices: &DevicesController = cg.controller_of().unwrap();

View File

@@ -4,20 +4,20 @@
//
//! Integration tests about the hugetlb subsystem
use cgroups_rs::error::*;
use cgroups_rs::hugetlb::{self, HugeTlbController};
use cgroups_rs::Cgroup;
use cgroups_rs::fs::error::*;
use cgroups_rs::fs::hugetlb::{self, HugeTlbController};
use cgroups_rs::fs::Cgroup;
use std::fs;
#[test]
fn test_hugetlb_sizes() {
// now only v2
if cgroups_rs::hierarchies::is_cgroup2_unified_mode() {
if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_hugetlb_sizes"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_hugetlb_sizes")).unwrap();
{
let hugetlb_controller: &HugeTlbController = cg.controller_of().unwrap();
let _ = hugetlb_controller.get_sizes();

View File

@@ -4,14 +4,13 @@
//
//! Integration tests about the hugetlb subsystem
use cgroups_rs::memory::{MemController, SetMemory};
use cgroups_rs::Controller;
use cgroups_rs::{Cgroup, MaxValue};
use cgroups_rs::fs::memory::{MemController, SetMemory};
use cgroups_rs::fs::{Cgroup, Controller, MaxValue};
#[test]
fn test_disable_oom_killer() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_disable_oom_killer"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_disable_oom_killer")).unwrap();
{
let mem_controller: &MemController = cg.controller_of().unwrap();
@@ -35,12 +34,12 @@ fn test_disable_oom_killer() {
#[test]
fn set_kmem_limit_v1() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
if h.v2() {
return;
}
let cg = Cgroup::new(h, String::from("set_kmem_limit_v1"));
let cg = Cgroup::new(h, String::from("set_kmem_limit_v1")).unwrap();
{
let mem_controller: &MemController = cg.controller_of().unwrap();
mem_controller.set_kmem_limit(1).unwrap();
@@ -50,12 +49,12 @@ fn set_kmem_limit_v1() {
#[test]
fn set_mem_v2() {
let h = cgroups_rs::hierarchies::auto();
let h = cgroups_rs::fs::hierarchies::auto();
if !h.v2() {
return;
}
let cg = Cgroup::new(h, String::from("set_mem_v2"));
let cg = Cgroup::new(h, String::from("set_mem_v2")).unwrap();
{
let mem_controller: &MemController = cg.controller_of().unwrap();

View File

@@ -5,19 +5,16 @@
//
//! Integration tests about the pids subsystem
use cgroups_rs::pid::PidController;
use cgroups_rs::Controller;
use cgroups_rs::{Cgroup, MaxValue};
use cgroups_rs::fs::pid::PidController;
use cgroups_rs::fs::{Cgroup, Controller, MaxValue};
use libc::pid_t;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{fork, ForkResult};
use libc::pid_t;
#[test]
fn create_and_delete_cgroup() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("create_and_delete_cgroup"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("create_and_delete_cgroup")).unwrap();
{
let pidcontroller: &PidController = cg.controller_of().unwrap();
pidcontroller.set_pid_max(MaxValue::Value(1337)).unwrap();
@@ -30,8 +27,8 @@ fn create_and_delete_cgroup() {
#[test]
fn test_pids_current_is_zero() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pids_current_is_zero"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pids_current_is_zero")).unwrap();
{
let pidcontroller: &PidController = cg.controller_of().unwrap();
let current = pidcontroller.get_pid_current();
@@ -42,8 +39,8 @@ fn test_pids_current_is_zero() {
#[test]
fn test_pids_events_is_zero() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pids_events_is_zero"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pids_events_is_zero")).unwrap();
{
let pidcontroller: &PidController = cg.controller_of().unwrap();
let events = pidcontroller.get_pid_events();
@@ -55,8 +52,8 @@ fn test_pids_events_is_zero() {
#[test]
fn test_pid_events_is_not_zero() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pid_events_is_not_zero"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_pid_events_is_not_zero")).unwrap();
{
let pids: &PidController = cg.controller_of().unwrap();
let before = pids.get_pid_events();
@@ -65,7 +62,7 @@ fn test_pid_events_is_not_zero() {
match unsafe { fork() } {
Ok(ForkResult::Parent { child, .. }) => {
// move the process into the control group
let _ = pids.add_task(&(pid_t::from(child) as u64).into());
let _ = pids.add_task_by_tgid(&(pid_t::from(child) as u64).into());
println!("added task to cg: {:?}", child);

View File

@@ -5,13 +5,13 @@
//
//! Integration test about setting resources using `apply()`
use cgroups_rs::pid::PidController;
use cgroups_rs::{Cgroup, MaxValue, PidResources, Resources};
use cgroups_rs::fs::pid::PidController;
use cgroups_rs::fs::{Cgroup, MaxValue, PidResources, Resources};
#[test]
fn pid_resources() {
let h = cgroups_rs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("pid_resources"));
let h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("pid_resources")).unwrap();
{
let res = Resources {
pid: PidResources {