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>
This commit is contained in:
Xuewei Niu
2025-07-11 20:55:23 +08:00
parent b8031f1a21
commit 1250cbe182
9 changed files with 1456 additions and 32 deletions

View File

@@ -17,9 +17,11 @@ nix = { version = "0.25.0", default-features = false, features = ["event", "fs",
libc = "0.2"
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "1"
oci-spec = { version = "0.8.1", optional = true }
[dev-dependencies]
libc = "0.2.76"
[features]
default = []
oci = ["oci-spec"]

View File

@@ -13,8 +13,8 @@ use std::path::PathBuf;
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

View File

@@ -245,6 +245,7 @@ mod sealed {
}
pub(crate) use crate::fs::sealed::{ControllerInternal, CustomizedAttribute};
use crate::CgroupPid;
/// A Controller is a subsystem attached to the control group.
///
@@ -771,26 +772,6 @@ pub struct Resources {
pub blkio: BlkIoResources,
}
/// A structure representing a `pid`. Currently implementations exist for `u64` and
/// `std::process::Child`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CgroupPid {
/// The process identifier
pub pid: u64,
}
impl From<u64> for CgroupPid {
fn from(u: u64) -> CgroupPid {
CgroupPid { pid: u }
}
}
impl From<&std::process::Child> for CgroupPid {
fn from(u: &std::process::Child) -> CgroupPid {
CgroupPid { pid: u.id() as u64 }
}
}
impl Subsystem {
fn enter(self, path: &Path) -> Self {
match self {

View File

@@ -5,3 +5,45 @@
//
pub mod fs;
#[cfg(feature = "oci")]
pub mod manager;
#[cfg(feature = "oci")]
pub use manager::{FsManager, Manager};
pub mod stats;
pub use stats::CgroupStats;
/// The maximum value for CPU shares in cgroups v1
pub const CPU_SHARES_V1_MAX: u64 = 262144;
/// The maximum value for CPU weight in cgroups v2
pub const CPU_WEIGHT_V2_MAX: u64 = 10000;
/// 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,
}
/// A structure representing a `pid`. Currently implementations exist for `u64` and
/// `std::process::Child`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct CgroupPid {
/// The process identifier
pub pid: u64,
}
impl From<u64> for CgroupPid {
fn from(u: u64) -> CgroupPid {
CgroupPid { pid: u }
}
}
impl From<&std::process::Child> for CgroupPid {
fn from(u: &std::process::Child) -> CgroupPid {
CgroupPid { pid: u.id() as u64 }
}
}

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

@@ -0,0 +1,69 @@
// 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)
}

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

@@ -0,0 +1,20 @@
// Copyright (c) 2025 Ant Group
//
// SPDX-License-Identifier: Apache-2.0 or MIT
//
use crate::fs::error::Error as CgroupfsError;
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),
}

1088
src/manager/fs.rs Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,77 @@
// 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 conv;
use oci_spec::runtime::LinuxResources;
use crate::{CgroupPid, CgroupStats, FreezerState};
/// 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;
}

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