Compare commits

..

49 Commits

Author SHA1 Message Date
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
56 changed files with 6296 additions and 1436 deletions

View File

@@ -1,7 +1,7 @@
name: BVT
on: [pull_request]
env:
RUST_VERSION: 1.52
RUST_VERSION: 1.85.1
jobs:
build:
name: Build

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.3.2"
version = "0.4.0"
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,14 +13,19 @@ readme = "README.md"
[dependencies]
log = "0.4"
regex = "1.1"
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.8.1", 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

@@ -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,
};
@@ -181,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>> {
@@ -430,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,
}
}
@@ -828,9 +829,9 @@ 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

View File

@@ -6,10 +6,11 @@
//! This module handles cgroup operations. Start here!
use crate::error::ErrorKind::*;
use crate::error::*;
use crate::fs::error::ErrorKind::*;
use crate::fs::error::*;
use crate::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem};
use crate::fs::hierarchies::V1;
use crate::fs::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem};
use std::collections::HashMap;
use std::convert::From;
@@ -50,7 +51,7 @@ impl Clone for Cgroup {
fn clone(&self) -> Self {
Cgroup {
subsystems: self.subsystems.clone(),
hier: crate::hierarchies::auto(),
hier: crate::fs::hierarchies::auto(),
path: self.path.clone(),
specified_controllers: None,
}
@@ -61,7 +62,7 @@ impl Default for Cgroup {
fn default() -> Self {
Cgroup {
subsystems: Vec::new(),
hier: crate::hierarchies::auto(),
hier: crate::fs::hierarchies::auto(),
path: "".to_string(),
specified_controllers: None,
}
@@ -73,8 +74,13 @@ impl Cgroup {
self.hier.v2()
}
/// Return the path the cgroup is located at.
pub fn path(&self) -> &str {
&self.path
}
/// Create this control group.
fn create(&self) -> Result<()> {
pub fn create(&self) -> Result<()> {
if self.hier.v2() {
create_v2_cgroup(self.hier.root(), &self.path, &self.specified_controllers)
} else {
@@ -492,6 +498,13 @@ impl Cgroup {
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";
@@ -509,7 +522,7 @@ fn supported_controllers() -> Vec<String> {
let ret = fs::read_to_string(p.as_str());
ret.unwrap_or_default()
.split(' ')
.map(|x| x.to_string())
.map(|x| x.trim().to_string())
.collect::<Vec<String>>()
}
@@ -576,19 +589,47 @@ pub fn get_cgroups_relative_paths_by_pid(pid: u32) -> Result<HashMap<String, Str
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))?;
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());
// 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)
@@ -60,7 +60,7 @@
//! .build(h).unwrap();
//! ```
use crate::{
use crate::fs::{
BlkIoDeviceResource, BlkIoDeviceThrottleResource, Cgroup, DeviceResource, Error, Hierarchy,
HugePageResource, MaxValue, NetworkPriority, Resources,
};
@@ -250,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,
}
}

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

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,
};
@@ -254,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,
}
}
@@ -591,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![
@@ -602,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,
}
}
@@ -238,7 +239,13 @@ 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("devices.allow".to_string(), final_str), e)
Error::with_cause(
WriteFailed(
self.get_path().join("devices.allow").display().to_string(),
final_str,
),
e,
)
})
})
}
@@ -271,7 +278,13 @@ 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("devices.deny".to_string(), final_str), e)
Error::with_cause(
WriteFailed(
self.get_path().join("devices.deny").display().to_string(),
final_str,
),
e,
)
})
})
}
@@ -282,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)
}
}
})
},
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

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

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,14 +73,13 @@ 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_name = "freezer.state";

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::{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
@@ -218,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,
)));
}
@@ -275,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
})
@@ -309,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> {
@@ -364,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,
}
@@ -181,7 +181,6 @@ impl HugeTlbController {
}
pub const HUGEPAGESIZE_DIR: &str = "/sys/kernel/mm/hugepages";
use regex::Regex;
use std::collections::HashMap;
use std::fs;
@@ -263,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 {
@@ -305,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,
}
}
@@ -573,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,
@@ -593,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(),
@@ -727,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)
@@ -736,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),
}
}
@@ -844,13 +849,16 @@ impl MemController {
/// Set the memory usage limit of the control group, in bytes.
pub fn set_limit(&self, limit: i64) -> Result<()> {
let mut file_name = "memory.limit_in_bytes";
let mut limit_str = limit.to_string();
if self.v2 {
file_name = "memory.max";
if limit == -1 {
limit_str = "max".to_string();
}
}
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)
})
file.write_all(limit_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e))
})
}
@@ -881,13 +889,16 @@ impl MemController {
/// Set the memory+swap limit of the control group, in bytes.
pub fn set_memswap_limit(&self, limit: i64) -> Result<()> {
let mut file_name = "memory.memsw.limit_in_bytes";
let mut limit_str = limit.to_string();
if self.v2 {
file_name = "memory.swap.max";
if limit == -1 {
limit_str = "max".to_string();
}
}
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)
})
file.write_all(limit_str.as_ref())
.map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e))
})
}
@@ -991,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,
};

1013
src/fs/mod.rs Normal file

File diff suppressed because it is too large Load Diff

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

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, _>>>()
})
}

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

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,6 +81,11 @@ 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| {

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

1048
src/lib.rs

File diff suppressed because it is too large Load Diff

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

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

@@ -0,0 +1,582 @@
// 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,
systemd_version: usize,
) -> Result<()> {
if let Some(cpus) = linux_cpu.cpus().as_ref() {
let (id, value) = cpuset::cpus(cpus, systemd_version)?;
props.push((id, value.into()));
}
if let Some(mems) = linux_cpu.mems().as_ref() {
let (id, value) = cpuset::mems(mems, systemd_version)?;
props.push((id, value.into()));
}
Ok(())
}
fn set_cpu(
&self,
props: &mut Vec<Property>,
linux_cpu: &LinuxCpu,
systemd_version: usize,
) -> 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, systemd_version)?;
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![];
let systemd_version = self.systemd_client.systemd_version()?;
if let Some(linux_cpu) = resources.cpu() {
self.set_cpuset(&mut props, linux_cpu, systemd_version)?;
self.set_cpu(&mut props, linux_cpu, systemd_version)?;
}
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";

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

@@ -0,0 +1,35 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::systemd::error::{Error, Result};
use crate::systemd::{
CPU_QUOTA_PERIOD_US, CPU_QUOTA_PER_SEC_US, CPU_SHARES, CPU_SYSTEMD_VERSION, 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, systemd_version: usize) -> Result<(&'static str, u64)> {
if systemd_version < CPU_SYSTEMD_VERSION {
return Err(Error::ObsoleteSystemd);
}
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))
}

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

@@ -0,0 +1,106 @@
// 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, CPUSET_SYSTEMD_VERSION};
const BYTE_IN_BITS: usize = 8;
/// Returns the property for cpuset CPUs.
pub fn cpus(cpus: &str, systemd_version: usize) -> Result<(&'static str, Vec<u8>)> {
if systemd_version < CPUSET_SYSTEMD_VERSION {
return Err(Error::ObsoleteSystemd);
}
let mask = convert_list_to_mask(cpus)?;
Ok((ALLOWED_CPUS, mask))
}
/// Returns the property for cpuset memory nodes.
pub fn mems(mems: &str, systemd_version: usize) -> Result<(&'static str, Vec<u8>)> {
if systemd_version < CPUSET_SYSTEMD_VERSION {
return Err(Error::ObsoleteSystemd);
}
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
```

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

@@ -0,0 +1,572 @@
// 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(())
}
/// Get the systemd version.
pub fn systemd_version(&self) -> Result<usize> {
let sys_proxy = systemd_manager_proxy()?;
// Parse 249 from "249.11-0ubuntu3.16"
let version = sys_proxy.version()?;
let version = version
.split('.')
.next()
.and_then(|v| v.parse::<usize>().ok())
.ok_or(Error::CorruptedSystemdVersion(version))?;
Ok(version)
}
/// 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, systemd_version};
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_systemd_version() {
skip_if_no_systemd!();
let unit = test_unit();
let props = PropertiesBuilder::default_cgroup(TEST_SLICE, &unit).build();
let cgroup = SystemdClient::new(&unit, props).unwrap();
let version = cgroup.systemd_version().unwrap();
let expected_version = systemd_version().unwrap();
assert_eq!(version, expected_version, "Systemd version mismatch");
}
#[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();
}
}

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

@@ -0,0 +1,18 @@
// 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),
#[error("corrupted systemd version: {0}")]
CorruptedSystemdVersion(String),
}

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

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

@@ -0,0 +1,18 @@
// 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("obsolete systemd, please upgrade your systemd")]
ObsoleteSystemd,
#[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))
}

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

@@ -0,0 +1,25 @@
// 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";
pub const CPU_SYSTEMD_VERSION: usize = 242;
pub const CPUSET_SYSTEMD_VERSION: usize = 244;

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,19 +5,19 @@
//
//! 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)
@@ -36,7 +36,7 @@ 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)
@@ -61,7 +61,7 @@ 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))
@@ -81,7 +81,7 @@ 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])
@@ -108,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;
@@ -130,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;
@@ -153,7 +153,7 @@ 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)

View File

@@ -5,20 +5,22 @@
//
//! Simple unit tests about the control groups system.
use cgroups_rs::cgroup::{
CGROUP_MODE_DOMAIN, CGROUP_MODE_DOMAIN_INVALID, CGROUP_MODE_DOMAIN_THREADED,
CGROUP_MODE_THREADED,
};
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_procs_iterator_cgroup() {
let h = cgroups_rs::hierarchies::auto();
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_procs_iterator_cgroup")).unwrap();
{
@@ -42,10 +44,10 @@ fn test_procs_iterator_cgroup() {
#[test]
fn test_tasks_iterator_cgroup_v1() {
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 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();
{
@@ -69,23 +71,23 @@ fn test_tasks_iterator_cgroup_v1() {
#[test]
fn test_tasks_iterator_cgroup_threaded_mode() {
if !cgroups_rs::hierarchies::is_cgroup2_unified_mode() {
if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() {
return;
}
let pid = libc::pid_t::from(nix::unistd::getpid()) as u64;
let cg = Cgroup::new(
cgroups_rs::hierarchies::auto(),
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::hierarchies::auto(),
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::hierarchies::auto(),
cgroups_rs::fs::hierarchies::auto(),
String::from("test_tasks_iterator_cgroup_threaded_mode/threaded_sub2"),
Some(vec![String::from("cpuset"), String::from("cpu")]),
)
@@ -163,10 +165,10 @@ fn test_tasks_iterator_cgroup_threaded_mode() {
#[test]
fn test_kill_cgroup() {
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 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.
@@ -199,17 +201,17 @@ fn test_kill_cgroup() {
}
}
};
assert!(!status.is_none());
assert!(status.is_some());
}
cg.delete().unwrap();
}
#[test]
fn test_cgroup_with_relative_paths() {
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 h = cgroups_rs::fs::hierarchies::auto();
let cgroup_root = h.root();
let cgroup_name = "test_cgroup_with_relative_paths";
@@ -247,10 +249,10 @@ 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 h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("test_v2")).unwrap();
let mem_controller: &MemController = cg.controller_of().unwrap();

View File

@@ -4,12 +4,12 @@
//
//! 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 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,16 +3,16 @@
//
// 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 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,7 +26,7 @@ fn test_cpuset_memory_pressure_root_cg() {
#[test]
fn test_cpuset_set_cpus() {
let h = cgroups_rs::hierarchies::auto();
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,7 +63,7 @@ fn test_cpuset_set_cpus() {
#[test]
fn test_cpuset_set_cpus_add_task() {
let h = cgroups_rs::hierarchies::auto();
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();

View File

@@ -6,17 +6,17 @@
//! 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 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,19 +4,19 @@
//
//! 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 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();

View File

@@ -4,13 +4,12 @@
//
//! 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 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,7 +34,7 @@ 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;
}
@@ -50,7 +49,7 @@ 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;
}

View File

@@ -5,18 +5,15 @@
//
//! 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 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();
@@ -30,7 +27,7 @@ fn create_and_delete_cgroup() {
#[test]
fn test_pids_current_is_zero() {
let h = cgroups_rs::hierarchies::auto();
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();
@@ -42,7 +39,7 @@ fn test_pids_current_is_zero() {
#[test]
fn test_pids_events_is_zero() {
let h = cgroups_rs::hierarchies::auto();
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();
@@ -55,7 +52,7 @@ fn test_pids_events_is_zero() {
#[test]
fn test_pid_events_is_not_zero() {
let h = cgroups_rs::hierarchies::auto();
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();

View File

@@ -5,12 +5,12 @@
//
//! 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 h = cgroups_rs::fs::hierarchies::auto();
let cg = Cgroup::new(h, String::from("pid_resources")).unwrap();
{
let res = Resources {