Skip to main content

agave_cpu_utils/
affinity.rs

1//! Core CPU affinity operations.
2
3use std::{io, mem, ops::Deref};
4
5const CPU_SETSIZE: usize = libc::CPU_SETSIZE as usize;
6
7/// Identifies a logical CPU (hardware thread) by its kernel-assigned ID.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct CpuId(usize);
10
11impl CpuId {
12    pub fn new(cpu: usize) -> io::Result<Self> {
13        if cpu < CPU_SETSIZE {
14            Ok(Self(cpu))
15        } else {
16            Err(io::Error::from_raw_os_error(libc::EINVAL))
17        }
18    }
19}
20
21impl Deref for CpuId {
22    type Target = usize;
23
24    fn deref(&self) -> &Self::Target {
25        &self.0
26    }
27}
28
29/// Set CPU affinity for a thread.
30///
31/// Restricts the thread to run only on the specified CPUs.
32///
33/// # Arguments
34/// * `thread_id` - Thread ID to set affinity for. `None` means the calling thread.
35/// * `cpus` - CPU IDs to bind the thread to. Can be any iterable collection.
36///
37/// # Examples
38///
39/// ```no_run
40/// # use agave_cpu_utils::*;
41/// # fn main() -> std::io::Result<()> {
42/// // Pin current thread to CPU 0
43/// set_cpu_affinity(None, [CpuId::new(0)?])?;
44///
45/// // Pin current thread to multiple CPUs
46/// set_cpu_affinity(None, [CpuId::new(0)?, CpuId::new(1)?, CpuId::new(2)?])?;
47/// # Ok(())
48/// # }
49/// ```
50///
51/// # Errors
52///
53/// Returns [`io::ErrorKind::InvalidInput`] if any CPU ID is too large for `cpu_set_t`.
54/// Returns [`io::Error`] if the system call fails, including offline CPUs or CPUs
55/// disallowed by the task's hard cpuset/cgroup.
56pub fn set_cpu_affinity(
57    thread_id: Option<libc::pid_t>,
58    cpus: impl IntoIterator<Item = CpuId>,
59) -> io::Result<()> {
60    // safety: cpu_set_t is a POD type, zero-initialization is standard
61    let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() };
62
63    for cpu in cpus {
64        // safety: CpuId values are constructed only after validating cpu < CPU_SETSIZE.
65        unsafe { libc::CPU_SET(*cpu, &mut cpu_set) };
66    }
67
68    let tid = thread_id.unwrap_or(0);
69
70    // safety: sched_setaffinity is safe with valid parameters
71    let result =
72        unsafe { libc::sched_setaffinity(tid, mem::size_of::<libc::cpu_set_t>(), &cpu_set) };
73
74    if result != 0 {
75        return Err(io::Error::last_os_error());
76    }
77
78    Ok(())
79}
80
81/// Get the CPU affinity mask for a thread.
82///
83/// Returns a sorted vector of CPU IDs that the thread is allowed to run on.
84///
85/// # Arguments
86/// * `thread_id` - Thread ID to query. `None` means the calling thread.
87///
88/// # Examples
89///
90/// ```no_run
91/// # use agave_cpu_utils::*;
92/// # fn main() -> std::io::Result<()> {
93/// let cpus = cpu_affinity(None)?;
94/// println!("Thread can run on CPUs: {:?}", cpus);
95/// # Ok(())
96/// # }
97/// ```
98///
99/// # Errors
100///
101/// Returns [`io::Error`] if the system call fails.
102pub fn cpu_affinity(thread_id: Option<libc::pid_t>) -> io::Result<Vec<CpuId>> {
103    // safety: cpu_set_t is a POD type, zero-initialization is standard
104    let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() };
105
106    let tid = thread_id.unwrap_or(0);
107
108    // safety: sched_getaffinity is safe with valid parameters
109    let result =
110        unsafe { libc::sched_getaffinity(tid, mem::size_of::<libc::cpu_set_t>(), &mut cpu_set) };
111
112    if result != 0 {
113        return Err(io::Error::last_os_error());
114    }
115
116    let mut cpus = Vec::new();
117    for cpu in 0..CPU_SETSIZE {
118        // safety: cpu < CPU_SETSIZE by construction
119        if unsafe { libc::CPU_ISSET(cpu, &cpu_set) } {
120            cpus.push(CpuId::new(cpu)?);
121        }
122    }
123
124    Ok(cpus)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_cpu_id_validation() {
133        let result = CpuId::new(CPU_SETSIZE);
134        assert_eq!(result.unwrap_err().raw_os_error(), Some(libc::EINVAL));
135    }
136
137    #[test]
138    fn test_cpu_affinity_returns_sorted() {
139        let cpus = cpu_affinity(None).expect("failed to query current CPU affinity");
140        assert!(
141            cpus.windows(2).all(|window| *window[0] <= *window[1]),
142            "cpu_affinity should return sorted CPU list"
143        );
144    }
145}