static HOME_MASK: std::sync::OnceLock<Option<libc::cpu_set_t>> = std::sync::OnceLock::new();
pub fn capture_home_mask() {
let Some(home) = home_mask() else {
return;
};
let current = match current_mask() {
Ok(mask) => mask,
Err(e) => {
tracing::warn!(error = e, "cannot read the startup thread's CPU mask");
return;
}
};
let narrowed = intersection(¤t, &home);
if count(&narrowed) > 0
&& count(&narrowed) < count(¤t)
&& let Err(e) = set_mask(&narrowed, "home")
{
tracing::warn!(
error = e,
"cannot move the startup thread off the isolated CPUs; threads it spawns \
may start on one"
);
}
}
fn home_mask() -> Option<libc::cpu_set_t> {
*HOME_MASK.get_or_init(|| match current_mask() {
Ok(mask) => Some(home_from(&mask, &isolated_cpu_list())),
Err(e) => {
tracing::warn!(
error = e,
"cannot read the process CPU mask; unpinned threads get every housekeeping CPU"
);
None
}
})
}
fn home_from(startup: &libc::cpu_set_t, isolated: &str) -> libc::cpu_set_t {
match without_isolated(startup, isolated) {
Some(home) => home,
None => {
tracing::warn!(
isolated,
"every CPU this process may use is isolated; unpinned threads share \
isolated CPUs, which the scheduler does not balance — a thread placed \
beside a real-time pipeline thread there will not run"
);
*startup
}
}
}
fn without_isolated(mask: &libc::cpu_set_t, isolated: &str) -> Option<libc::cpu_set_t> {
let mut kept = *mask;
for cpu in cpus_of(mask) {
if cpu_list_contains(isolated, cpu) {
unsafe { libc::CPU_CLR(cpu, &mut kept) };
}
}
(count(&kept) > 0).then_some(kept)
}
fn intersection(a: &libc::cpu_set_t, b: &libc::cpu_set_t) -> libc::cpu_set_t {
let mut both = *a;
for cpu in cpus_of(a) {
if !unsafe { libc::CPU_ISSET(cpu, b) } {
unsafe { libc::CPU_CLR(cpu, &mut both) };
}
}
both
}
fn cpus_of(mask: &libc::cpu_set_t) -> impl Iterator<Item = usize> + '_ {
(0..libc::CPU_SETSIZE as usize).filter(move |&cpu| unsafe { libc::CPU_ISSET(cpu, mask) })
}
pub fn first_allowed_cpu() -> Option<usize> {
let mask = current_mask().ok()?;
cpus_of(&mask).next()
}
fn count(mask: &libc::cpu_set_t) -> usize {
cpus_of(mask).count()
}
fn current_mask() -> Result<libc::cpu_set_t, String> {
unsafe {
let mut mask: libc::cpu_set_t = std::mem::zeroed();
if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut mask) != 0 {
return Err(format!(
"sched_getaffinity failed: {}",
std::io::Error::last_os_error()
));
}
Ok(mask)
}
}
fn all_cpus() -> libc::cpu_set_t {
unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
for i in 0..libc::CPU_SETSIZE as usize {
libc::CPU_SET(i, &mut set);
}
set
}
}
fn set_mask(mask: &libc::cpu_set_t, what: &str) -> Result<(), String> {
let ret = unsafe { libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), mask) };
if ret != 0 {
return Err(format!(
"sched_setaffinity ({what}) failed: {}",
std::io::Error::last_os_error()
));
}
Ok(())
}
fn housekeeping_cpus() -> libc::cpu_set_t {
home_from(&all_cpus(), &isolated_cpu_list())
}
fn set_unpinned_mask() -> Result<(), String> {
let Some(home) = home_mask() else {
return set_mask(&housekeeping_cpus(), "unpin");
};
set_mask(&home, "unpin")
.or_else(|e| {
tracing::warn!(
error = e,
"the home CPU mask no longer applies; unpinning across every housekeeping CPU \
instead"
);
set_mask(&housekeeping_cpus(), "unpin")
})
.or_else(|_| set_mask(&all_cpus(), "unpin"))
}
fn set_other_policy(what: &str) -> Result<(), String> {
let param = libc::sched_param { sched_priority: 0 };
if unsafe { libc::sched_setscheduler(0, libc::SCHED_OTHER, ¶m) } != 0 {
return Err(format!(
"sched_setscheduler ({what}) failed: {}",
std::io::Error::last_os_error()
));
}
Ok(())
}
pub fn pin_to_core(core_id: usize) -> Result<usize, String> {
home_mask();
const MAX_CPUS: usize = 1024;
if core_id >= MAX_CPUS {
return Err(format!("core_id {core_id} exceeds maximum ({MAX_CPUS})"));
}
unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
libc::CPU_ZERO(&mut set);
libc::CPU_SET(core_id, &mut set);
let ret = libc::sched_setaffinity(
0, std::mem::size_of::<libc::cpu_set_t>(),
&set,
);
if ret != 0 {
return Err(format!(
"sched_setaffinity failed for core {core_id}: {}",
std::io::Error::last_os_error()
));
}
}
if core_id > 0 && core_is_isolated(core_id) {
set_realtime_fifo(1);
} else if core_id > 0 {
tracing::warn!(
core = core_id,
"core not isolated (no isolcpus); pinned affinity only, no SCHED_FIFO \
(real-time busy-spin on a shared core would starve co-located threads). \
Boot with isolcpus on the pipeline cores for lowest tail latency."
);
}
Ok(core_id)
}
fn core_is_isolated(core_id: usize) -> bool {
cpu_list_contains(&isolated_cpu_list(), core_id)
}
fn isolated_cpu_list() -> String {
match std::fs::read_to_string("/sys/devices/system/cpu/isolated") {
Ok(list) => list.trim().to_owned(),
Err(_) => String::new(),
}
}
fn cpu_list_contains(list: &str, core_id: usize) -> bool {
list.split(',').filter(|p| !p.is_empty()).any(|part| {
match part.split_once('-') {
Some((lo, hi)) => matches!(
(lo.parse::<usize>(), hi.parse::<usize>()),
(Ok(lo), Ok(hi)) if lo <= core_id && core_id <= hi
),
None => matches!(part.parse::<usize>(), Ok(n) if n == core_id),
}
})
}
fn set_realtime_fifo(priority: i32) {
unsafe {
let param = libc::sched_param {
sched_priority: priority,
};
let ret = libc::sched_setscheduler(0, libc::SCHED_FIFO, ¶m);
if ret != 0 {
tracing::warn!(
error = %std::io::Error::last_os_error(),
"SCHED_FIFO failed (run as root or grant CAP_SYS_NICE)"
);
}
}
}
pub fn pin_thread(name: &str, core: usize) {
if core == 0 {
match clear_affinity() {
Ok(()) => tracing::info!(thread = name, "thread left unpinned, on the home CPU mask"),
Err(e) => tracing::warn!(thread = name, error = e, "could not unpin thread"),
}
return;
}
match pin_to_core(core) {
Ok(c) => tracing::info!(core = c, thread = name, "pinned to core"),
Err(e) => tracing::warn!(thread = name, error = e, "core pinning failed"),
}
}
struct SchedContext {
mask: libc::cpu_set_t,
cpu: Option<usize>,
policy: libc::c_int,
priority: libc::c_int,
}
fn take_context() -> Result<SchedContext, String> {
unsafe {
let mut mask: libc::cpu_set_t = std::mem::zeroed();
if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut mask) != 0 {
return Err(format!(
"sched_getaffinity failed: {}",
std::io::Error::last_os_error()
));
}
let cpu = usize::try_from(libc::sched_getcpu()).ok();
let policy = libc::sched_getscheduler(0);
if policy < 0 {
return Err(format!(
"sched_getscheduler failed: {}",
std::io::Error::last_os_error()
));
}
let mut param: libc::sched_param = std::mem::zeroed();
if libc::sched_getparam(0, &mut param) != 0 {
return Err(format!(
"sched_getparam failed: {}",
std::io::Error::last_os_error()
));
}
Ok(SchedContext {
mask,
cpu,
policy,
priority: param.sched_priority,
})
}
}
fn restore_context(ctx: &SchedContext) -> Result<(), String> {
if let Some(cpu) = ctx.cpu.filter(|&cpu| {
cpu < libc::CPU_SETSIZE as usize && unsafe { libc::CPU_ISSET(cpu, &ctx.mask) }
}) {
let only = unsafe {
let mut only: libc::cpu_set_t = std::mem::zeroed();
libc::CPU_SET(cpu, &mut only);
only
};
if let Err(e) = set_mask(&only, "restore") {
tracing::warn!(
cpu,
error = e,
"could not move back to the CPU the thread was on"
);
}
}
unsafe {
let param = libc::sched_param {
sched_priority: ctx.priority,
};
if libc::sched_setscheduler(0, ctx.policy, ¶m) != 0 {
return Err(format!(
"sched_setscheduler (restore) failed: {}",
std::io::Error::last_os_error()
));
}
if libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &ctx.mask) != 0 {
return Err(format!(
"sched_setaffinity (restore) failed: {}",
std::io::Error::last_os_error()
));
}
Ok(())
}
}
fn prepare_child_context(core: usize) -> Result<(), String> {
home_mask();
const MAX_CPUS: usize = 1024;
if core >= MAX_CPUS {
return Err(format!("core {core} exceeds maximum ({MAX_CPUS})"));
}
set_other_policy("child prep")?;
if core == 0 {
return set_unpinned_mask();
}
let set = unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
libc::CPU_SET(core, &mut set);
set
};
set_mask(&set, "child prep")
}
pub fn with_child_context<R>(name: &str, core: usize, spawn: impl FnOnce() -> R) -> R {
let saved = match take_context() {
Ok(saved) => saved,
Err(e) => {
tracing::warn!(
thread = name,
error = e,
"cannot snapshot the spawning thread's scheduling context; the thread \
starts in it instead of its own"
);
return spawn();
}
};
if let Err(e) = prepare_child_context(core) {
tracing::warn!(
thread = name,
error = e,
"cannot prepare the thread's scheduling context"
);
}
let spawned = spawn();
if let Err(e) = restore_context(&saved) {
tracing::error!(
thread = name,
error = e,
"the spawning thread could not restore its own scheduling context"
);
}
spawned
}
const PLACED_START_BOUND: std::time::Duration = std::time::Duration::from_secs(10);
pub enum SpawnPlacedError<T> {
Spawn(std::io::Error),
NotStarted {
name: String,
core: usize,
handle: std::thread::JoinHandle<T>,
},
}
impl<T> SpawnPlacedError<T> {
pub fn keep_stuck(self) -> std::io::Result<std::thread::JoinHandle<T>> {
match self {
Self::Spawn(e) => Err(e),
Self::NotStarted { name, core, handle } => {
tracing::error!(
"{}; kept, in case it runs later",
not_started_message(&name, core)
);
Ok(handle)
}
}
}
}
fn not_started_message(name: &str, core: usize) -> String {
let bound = PLACED_START_BOUND.as_secs();
if core == 0 {
format!(
"thread {name} did not start within {bound}s on the home CPU mask; \
something there never yields the CPU"
)
} else {
format!(
"thread {name} did not start within {bound}s on core {core}; another \
thread is likely spinning there at real-time priority — check the \
--cores layout and what else is pinned to that core"
)
}
}
impl<T> std::fmt::Display for SpawnPlacedError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Spawn(e) => write!(f, "cannot create the thread: {e}"),
Self::NotStarted { name, core, .. } => f.write_str(¬_started_message(name, *core)),
}
}
}
impl<T> std::fmt::Debug for SpawnPlacedError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl<T> std::error::Error for SpawnPlacedError<T> {}
impl<T> From<SpawnPlacedError<T>> for std::io::Error {
fn from(e: SpawnPlacedError<T>) -> Self {
match e {
SpawnPlacedError::Spawn(e) => e,
not_started => {
std::io::Error::new(std::io::ErrorKind::TimedOut, not_started.to_string())
}
}
}
}
pub fn spawn_placed<T, F>(
name: &str,
core: usize,
f: F,
) -> Result<std::thread::JoinHandle<T>, SpawnPlacedError<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
spawn_checked_in(name, core, PLACED_START_BOUND, pin_thread, f)
}
fn spawn_checked_in<T, F>(
name: &str,
core: usize,
bound: std::time::Duration,
start: fn(&str, usize),
f: F,
) -> Result<std::thread::JoinHandle<T>, SpawnPlacedError<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let thread_name = name.to_owned();
let (started_tx, started_rx) = std::sync::mpsc::sync_channel::<()>(1);
let handle = with_child_context(name, core, || {
std::thread::Builder::new()
.name(thread_name.clone())
.spawn(move || {
start(&thread_name, core);
let _ = started_tx.send(());
f()
})
})
.map_err(SpawnPlacedError::Spawn)?;
match started_rx.recv_timeout(bound) {
Ok(()) => Ok(handle),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(SpawnPlacedError::NotStarted {
name: name.to_owned(),
core,
handle,
}),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Ok(handle),
}
}
pub fn clear_affinity() -> Result<(), String> {
home_mask();
set_other_policy("clear")?;
set_unpinned_mask()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
fn affinity_width() -> usize {
unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
assert_eq!(
libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut set),
0,
"sched_getaffinity failed: {}",
std::io::Error::last_os_error()
);
(0..libc::CPU_SETSIZE as usize)
.filter(|&c| libc::CPU_ISSET(c, &set))
.count()
}
}
#[test]
fn child_inherits_parent_affinity_and_can_clear_its_own() {
let full_width = affinity_width();
if full_width < 2 {
assert!(pin_to_core(0).is_ok());
return;
}
let widths = std::thread::spawn(|| {
pin_to_core(0).expect("core 0 always exists");
let parent_before = affinity_width();
let child = std::thread::spawn(|| {
let inherited = affinity_width();
clear_affinity().expect("clear affinity");
(inherited, affinity_width())
})
.join()
.expect("child thread");
(parent_before, child, affinity_width())
})
.join()
.expect("parent thread");
let (parent_before, (child_inherited, child_cleared), parent_after) = widths;
assert_eq!(parent_before, 1, "the parent pinned itself to one core");
assert_eq!(
child_inherited, 1,
"child must inherit the parent's single-core mask — if this ever \
stops holding, the disk thread's clear_affinity call is obsolete"
);
assert_eq!(
child_cleared, full_width,
"clear_affinity must put the calling thread on the home mask"
);
assert_eq!(
parent_after, 1,
"the child's clear_affinity must not touch the parent's pin"
);
}
fn affinity_cores() -> Vec<usize> {
cores(¤t_mask().expect("sched_getaffinity"))
}
fn cores(set: &libc::cpu_set_t) -> Vec<usize> {
cpus_of(set).collect()
}
#[test]
fn child_spawned_after_prepare_context_lands_on_its_own_core() {
if affinity_width() < 2 {
return; }
let (parent_before, child_cores, parent_after) = std::thread::spawn(|| {
pin_to_core(0).expect("core 0 always exists");
let parent_before = affinity_cores();
let saved = take_context().expect("snapshot context");
prepare_child_context(1).expect("prepare child context");
let child_cores = std::thread::spawn(affinity_cores)
.join()
.expect("child thread");
restore_context(&saved).expect("restore context");
(parent_before, child_cores, affinity_cores())
})
.join()
.expect("parent thread");
assert_eq!(parent_before, vec![0], "parent pinned itself to core 0");
assert_eq!(
child_cores,
vec![1],
"the child must start on the core it was prepared for — inheriting \
the parent's core is the deadlock this whole handover prevents"
);
assert_eq!(
parent_after, parent_before,
"the parent must get its own pin back after the spawn"
);
}
#[test]
fn an_unpinned_child_of_a_pinned_parent_gets_the_home_mask() {
capture_home_mask();
let home = cores(&home_mask().expect("the test thread's mask is readable"));
if home.len() < 2 {
return; }
let child_cores = std::thread::spawn(|| {
pin_to_core(0).expect("core 0 always exists");
let saved = take_context().expect("snapshot context");
prepare_child_context(0).expect("prepare unpinned child");
let child = std::thread::spawn(affinity_cores)
.join()
.expect("child thread");
restore_context(&saved).expect("restore context");
child
})
.join()
.expect("parent thread");
assert_eq!(
child_cores, home,
"an unpinned child must get the home mask, not the parent's pin"
);
}
#[test]
fn pin_thread_zero_puts_a_pinned_thread_back_on_the_home_mask() {
capture_home_mask();
let home = cores(&home_mask().expect("the test thread's mask is readable"));
if home.len() < 2 {
return; }
let (pinned, unpinned) = std::thread::spawn(|| {
pin_to_core(0).expect("core 0 always exists");
let pinned = affinity_cores();
pin_thread("unpinned-test", 0);
(pinned, affinity_cores())
})
.join()
.expect("thread");
assert_eq!(pinned, vec![0], "the thread pinned itself to core 0");
assert_eq!(
unpinned, home,
"pin_thread(_, 0) must put the thread back on the home mask"
);
}
#[test]
fn restore_context_round_trips_policy_and_priority() {
std::thread::spawn(|| {
let before = take_context().expect("snapshot");
let (policy_before, prio_before) = (before.policy, before.priority);
prepare_child_context(0).expect("prepare");
restore_context(&before).expect("restore");
let after = take_context().expect("snapshot again");
assert_eq!(after.policy, policy_before, "policy must round-trip");
assert_eq!(
after.priority, prio_before,
"priority must round-trip alongside the policy"
);
})
.join()
.expect("thread");
}
fn mask_of(cpus: &[usize]) -> libc::cpu_set_t {
let mut mask: libc::cpu_set_t = unsafe { std::mem::zeroed() };
for &cpu in cpus {
unsafe { libc::CPU_SET(cpu, &mut mask) };
}
mask
}
fn current_cpu() -> usize {
usize::try_from(unsafe { libc::sched_getcpu() }).expect("sched_getcpu")
}
fn current_policy() -> libc::c_int {
let policy = unsafe { libc::sched_getscheduler(0) };
assert!(policy >= 0, "sched_getscheduler failed");
policy
}
fn set_batch_policy() {
let param = libc::sched_param { sched_priority: 0 };
assert_eq!(
unsafe { libc::sched_setscheduler(0, libc::SCHED_BATCH, ¶m) },
0,
"sched_setscheduler(SCHED_BATCH) failed: {}",
std::io::Error::last_os_error()
);
}
#[test]
fn the_home_mask_leaves_out_isolated_cpus_unless_nothing_else_is_left() {
let startup = mask_of(&[0, 1, 2, 3, 8, 9]);
assert_eq!(cores(&home_from(&startup, "1-3,9-15")), vec![0, 8]);
assert_eq!(cores(&home_from(&startup, "")), vec![0, 1, 2, 3, 8, 9]);
assert_eq!(cores(&home_from(&startup, "4-7")), vec![0, 1, 2, 3, 8, 9]);
let isolated_only = mask_of(&[2, 3]);
assert!(without_isolated(&isolated_only, "1-7").is_none());
assert_eq!(cores(&home_from(&isolated_only, "1-7")), vec![2, 3]);
}
#[test]
fn restore_context_returns_the_thread_to_the_cpu_it_was_on() {
let [a, b, ..] = affinity_cores()[..] else {
return; };
let (cpu_after, mask_after) = std::thread::spawn(move || {
set_mask(&mask_of(&[a]), "test").expect("move to a");
set_mask(&mask_of(&[a, b]), "test").expect("widen to a and b");
let saved = take_context().expect("snapshot context");
prepare_child_context(b).expect("prepare child context");
std::thread::spawn(|| {}).join().expect("child thread");
restore_context(&saved).expect("restore context");
(current_cpu(), affinity_cores())
})
.join()
.expect("spawning thread");
assert_eq!(
cpu_after, a,
"the spawning thread must be back on its own CPU, not left on the child's"
);
assert_eq!(mask_after, vec![a, b], "and on its own mask");
}
#[test]
fn spawn_placed_starts_the_thread_in_its_own_context_and_puts_the_spawner_back() {
let available = affinity_cores();
let Some(&child_core) = available.iter().find(|&&cpu| cpu != 0) else {
return; };
let Some(&spawner_cpu) = available.iter().find(|&&cpu| cpu != child_core) else {
return; };
let ((child_policy, child_cores), spawner) = std::thread::spawn(move || {
set_mask(&mask_of(&[spawner_cpu]), "test").expect("move to the spawner's CPU");
set_mask(&mask_of(&[spawner_cpu, child_core]), "test").expect("widen");
set_batch_policy();
let child = spawn_placed("placed-test", child_core, || {
(current_policy(), affinity_cores())
})
.expect("spawn");
let spawner = (affinity_cores(), current_policy());
(child.join().expect("child thread"), spawner)
})
.join()
.expect("spawning thread");
assert_eq!(
child_policy,
libc::SCHED_OTHER,
"the child must start in its own context, not inherit the spawner's"
);
assert_eq!(child_cores, vec![child_core], "and run pinned to its core");
let mut spawner_mask = vec![spawner_cpu, child_core];
spawner_mask.sort_unstable();
assert_eq!(
spawner,
(spawner_mask, libc::SCHED_BATCH),
"the spawner must be left with its own mask and policy"
);
}
#[test]
fn spawn_placed_reports_a_thread_that_does_not_start_in_time() {
fn stuck(_: &str, _: usize) {
std::thread::sleep(std::time::Duration::from_millis(300));
}
let spawned = spawn_checked_in(
"stuck-test",
0,
std::time::Duration::from_millis(20),
stuck,
|| 7,
);
match spawned {
Err(SpawnPlacedError::NotStarted { name, core, handle }) => {
assert_eq!((name.as_str(), core), ("stuck-test", 0));
assert_eq!(handle.join().expect("thread"), 7, "it runs once it can");
}
Err(e) => panic!("expected NotStarted, got: {e}"),
Ok(_) => panic!("a thread that never checked in was reported started"),
}
}
#[test]
fn pin_to_core_0_succeeds() {
assert!(pin_to_core(0).is_ok());
}
#[test]
fn pin_to_invalid_core_fails() {
assert!(pin_to_core(99999).is_err());
}
#[test]
fn cpu_list_membership() {
assert!(cpu_list_contains("2-7", 2));
assert!(cpu_list_contains("2-7", 7));
assert!(cpu_list_contains("2-7", 5));
assert!(!cpu_list_contains("2-7", 1));
assert!(!cpu_list_contains("2-7", 8));
assert!(cpu_list_contains("1,3,5", 3));
assert!(!cpu_list_contains("1,3,5", 4));
assert!(cpu_list_contains("2-4,6-8", 7));
assert!(cpu_list_contains("2-4,6-8", 3));
assert!(!cpu_list_contains("2-4,6-8", 5));
assert!(cpu_list_contains("0,2-4,9", 9));
assert!(!cpu_list_contains("", 0));
assert!(!cpu_list_contains("", 2));
assert!(!cpu_list_contains("x,2-", 2));
assert!(!cpu_list_contains("foo", 0));
}
}