PidsLimits

Struct PidsLimits 

Source
#[non_exhaustive]
pub struct PidsLimits { pub max: Option<u64>, }
Expand description

Pids controller limits (pids.max). None is the default (inherit from parent — typically "max", no ceiling).

Per the kernel’s pids_max_write, existing tasks are NOT killed when the limit lands below the current task count; only future fork() / clone() calls are blocked once the cgroup’s task count meets the limit. Useful for fork-bomb / task-count-ceiling tests.

§Per-WorkType thread-budget guidance

pids.max counts every task (process AND thread) inside the cgroup. Sizing the limit below the workload’s natural task budget produces silent fork failures that surface as WorkloadConfig-level workers refusing to start.

Most variants spawn exactly one task per worker — their worker_main dispatch arm neither spawns helper threads nor forks children. Two exceptions run internal helper threads inside the worker process: Schbench (message_threads message threads, each spawning worker_threads worker threads, plus a control thread) and Taobench (client_threads client threads + slow_threads dispatcher threads); their per-worker task counts are config/CPU-sized, not 1. Per-worker budget therefore depends on CloneMode (whether each worker is a process or a thread sharing the parent’s tgid), the variant’s internal helper-thread topology, and whether the variant transiently forks short-lived children inside its own loop. The columns below capture all three:

VariantSteady-state tasksTransient peak
SpinWait, YieldHeavy, Mixed1/worker
Bursty, IdleChurn1/worker
IoSyncWrite, IoRandRead, IoConvoy1/worker
CachePressure, CacheYield, CachePipe1/worker
PageFaultChurn1/worker
AffinityChurn, PolicyChurn, NiceSweep1/worker
NumaWorkingSetSweep, NumaMigrationChurn, CgroupChurn1/worker
Sequence1/worker
AluHot, SmtSiblingSpin, IpcVariance1/worker
PipeIo, FutexPingPong, AsymmetricWaker, SignalStorm1/worker
FutexFanOut, FanOutCompute1/worker
ThunderingHerd, MutexContention, WakeChain1/worker
PriorityInversion, ProducerConsumerImbalance1/worker
RtStarvation, PreemptStorm, EpollStorm1/worker
CrossAffinityChurn, TimerLatency, NetTraffic, IrqWake1/worker
ForkExit1/worker+1/worker (waitpid’d before next iter)
CgroupAttachStorm1/worker+1/worker (forked child per iter, _exits + auto-reaped)
Schbench, Taobench>1/worker (internal helper threads, config/CPU-sized)
Custom1/workerdepends on user closure (see below)

CloneMode::Fork (the default): each worker is a separate process placed in the cgroup. The cgroup’s task count for one WorkSpec is exactly num_workers; for ForkExit the instantaneous peak is 2 × num_workers (each parent forks one child, waitpid’s, repeats).

CloneMode::Thread: every worker is a thread sharing the test runner’s tgid. The pids controller counts each thread as a task, so the cgroup’s task count for one WorkSpec is num_workers + 1 (workers + the parent task). ForkExit is rejected at spawn time under Thread mode (see WorkType::ForkExit).

Custom: the framework runs the user closure in a single task per worker (1/worker, identical to every other variant). Any fork/clone the closure issues inside its loop adds to the cgroup’s task count for as long as the resulting child lives; pids.max must reserve headroom equal to the closure’s peak child count per worker. Under CloneMode::Fork the framework reaps closure-spawned descendants at teardown via killpg(worker_pid, SIGKILL) against the worker’s per-process group, so transient children are bounded by the closure itself. Under CloneMode::Thread the worker shares the test runner’s pgid and killpg-based cleanup is unavailable, so the closure owns whatever helpers it spawns and must reap them explicitly before returning the WorkerReport.

Sizing rule: pids.max ≥ Σ(steady-state + transient) for every WorkSpec in the cgroup, plus headroom for cgroup.procs migration scratch tasks and any payload-binary helper processes the test attaches via CgroupDef::workload (e.g. stress-ng spawns one task per --cpu N). Tests with composed WorkSpec groups must sum across every group — the framework does NOT auto-derive a budget from the work spec.

§Parent-cgroup hierarchical charging

pids.max is a per-cgroup ceiling, but every fork/clone charges every ancestor up to (but not including) the unified-hierarchy root. The kernel’s pids_can_fork calls pids_try_charge, which loops for (p = pids; parent_pids(p); p = parent_pids(p)) and charges each level (kernel/cgroup/pids.c) — root is NOT charged per the loop’s parent_pids(p) termination condition. EAGAIN propagates from the FIRST level (leaf-to-root traversal order) whose post-charge counter exceeds its limit, so a child cgroup with pids.max = 1024 still hits EAGAIN when a parent two levels up sits at its own ceiling.

Sizing rule for nested test trees: the effective limit is min(pids.max) along the path from the test cgroup up to the pids-controlled root, NOT just the value set on the test cgroup itself. When ktstr runs under a delegated parent slice (systemd user.slice, container runtime cgroup, ktstr’s own build sandbox), inspect the parent’s pids.max before sizing the test cgroup — a generous test-cgroup setting is silently shadowed by a tighter ancestor.

§pids.max(0) is rejected at apply_setup, not type-level

Some(0) would silently halt every fork/clone inside the cgroup, including the worker spawn itself for CloneMode::Fork and the ForkExit per-iteration child fork. The kernel accepts the value (it’s a legitimate pids_max_write input), so apply_setup adds the bail at scenario-setup time; promoting it to a type-level invariant (e.g. NonZeroU64) would force every numeric literal through a non-const constructor and ripple into every test fixture. The runtime bail keeps the surface ergonomic while still surfacing the foot-cannon at construction time (before any worker spawns).

Set via CgroupDef::pids_max or CgroupDef::pids_unlimited. Construct directly only when copying a PidsLimits across CgroupDefs — the builder methods are the preferred entry point because they route the per-knob value through the framework’s validation seam at apply_setup.

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§max: Option<u64>

pids.max task-count ceiling. None writes the literal string "max" (the kernel’s PIDS_MAX_STR sentinel for unlimited). Some(n) writes the decimal n. The kernel rejects negative or >= PIDS_MAX (PID_MAX_LIMIT + 1, typically ~4M on 64-bit) values with EINVAL; the framework’s apply_setup rejects Some(0) before the syscall (a 0 limit silently halts every fork or clone inside the cgroup, blocking both worker spawn under CloneMode::Fork and ForkExit’s per-iteration child fork).

Trait Implementations§

Source§

impl Clone for PidsLimits

Source§

fn clone(&self) -> PidsLimits

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PidsLimits

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for PidsLimits

Source§

fn default() -> PidsLimits

Returns the “default value” for a type. Read more
Source§

impl PartialEq for PidsLimits

Source§

fn eq(&self, other: &PidsLimits) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Eq for PidsLimits

Source§

impl StructuralPartialEq for PidsLimits

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<T> MaybeSend for T
where T: Send,

§

impl<T> MaybeSend for T
where T: Send,