1#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct CpuMask {
25 words: Vec<u64>,
26}
27
28impl CpuMask {
29 pub fn empty(cores: usize) -> Self {
31 Self {
32 words: vec![0; cores.div_ceil(64).max(1)],
33 }
34 }
35
36 pub fn all(cores: usize) -> Self {
38 let mut mask = Self::empty(cores);
39 for core in 0..cores {
40 mask.set(core, true);
41 }
42 mask
43 }
44
45 pub fn get(&self, core: usize) -> bool {
46 let (word, bit) = (core / 64, core % 64);
47 self.words.get(word).is_some_and(|w| w & (1 << bit) != 0)
48 }
49
50 pub fn set(&mut self, core: usize, on: bool) {
51 let (word, bit) = (core / 64, core % 64);
52 if word >= self.words.len() {
53 self.words.resize(word + 1, 0);
54 }
55 if on {
56 self.words[word] |= 1 << bit;
57 } else {
58 self.words[word] &= !(1 << bit);
59 }
60 }
61
62 pub fn toggle(&mut self, core: usize) {
63 let on = self.get(core);
64 self.set(core, !on);
65 }
66
67 pub fn to_bytes(&self) -> Vec<u8> {
69 self.words
70 .iter()
71 .flat_map(|w| w.to_ne_bytes())
72 .collect()
73 }
74
75 pub fn from_bytes(bytes: &[u8], cores: usize) -> Self {
77 let mut mask = Self::empty(cores);
78 for core in 0..cores {
79 let byte = core / 8;
80 if byte < bytes.len() && bytes[byte] & (1 << (core % 8)) != 0 {
81 mask.set(core, true);
82 }
83 }
84 mask
85 }
86
87 pub fn count(&self) -> usize {
88 self.words.iter().map(|w| w.count_ones() as usize).sum()
89 }
90
91 pub fn is_empty(&self) -> bool {
92 self.count() == 0
93 }
94}
95
96pub fn supported() -> bool {
98 cfg!(any(target_os = "linux", target_os = "freebsd"))
99}
100
101pub fn read(pid: u32, cores: usize) -> Result<Option<CpuMask>, String> {
106 imp::read(pid, cores)
107}
108
109pub fn write(pid: u32, mask: &CpuMask) -> Result<(), String> {
111 if mask.is_empty() {
112 return Err("refusing to set an empty CPU mask".to_string());
115 }
116 imp::write(pid, mask)
117}
118
119#[cfg(target_os = "linux")]
120mod imp {
121 use super::CpuMask;
122 use std::io;
123
124 pub fn read(pid: u32, cores: usize) -> Result<Option<CpuMask>, String> {
125 let mut bytes = vec![0u8; cores.div_ceil(64).max(1) * 8];
126 let rc = unsafe {
128 libc::sched_getaffinity(
129 pid as libc::pid_t,
130 bytes.len(),
131 bytes.as_mut_ptr().cast(),
132 )
133 };
134 if rc != 0 {
135 return Err(io::Error::last_os_error().to_string());
136 }
137 Ok(Some(CpuMask::from_bytes(&bytes, cores)))
138 }
139
140 pub fn write(pid: u32, mask: &CpuMask) -> Result<(), String> {
141 let bytes = mask.to_bytes();
142 let rc = unsafe {
144 libc::sched_setaffinity(
145 pid as libc::pid_t,
146 bytes.len(),
147 bytes.as_ptr().cast(),
148 )
149 };
150 if rc == 0 {
151 Ok(())
152 } else {
153 Err(io::Error::last_os_error().to_string())
154 }
155 }
156}
157
158#[cfg(target_os = "freebsd")]
165mod imp {
166 use super::CpuMask;
167
168 pub fn read(pid: u32, cores: usize) -> Result<Option<CpuMask>, String> {
169 let mut set = std::mem::MaybeUninit::<libc::cpuset_t>::zeroed();
170 let rc = unsafe {
173 libc::cpuset_getaffinity(
174 libc::CPU_LEVEL_WHICH,
175 libc::CPU_WHICH_PID,
176 pid as libc::id_t,
177 std::mem::size_of::<libc::cpuset_t>(),
178 set.as_mut_ptr(),
179 )
180 };
181 if rc != 0 {
182 return Err(std::io::Error::last_os_error().to_string());
183 }
184 let set = unsafe { set.assume_init() };
186 let bytes = unsafe {
194 std::slice::from_raw_parts(
195 (&set as *const libc::cpuset_t).cast::<u8>(),
196 std::mem::size_of::<libc::cpuset_t>(),
197 )
198 };
199 Ok(Some(CpuMask::from_bytes(bytes, cores)))
200 }
201
202 pub fn write(pid: u32, mask: &CpuMask) -> Result<(), String> {
203 let mut set = std::mem::MaybeUninit::<libc::cpuset_t>::zeroed();
204 let ptr = set.as_mut_ptr();
206 unsafe {
207 std::ptr::write_bytes(ptr.cast::<u8>(), 0, std::mem::size_of::<libc::cpuset_t>());
208 let bytes =
209 std::slice::from_raw_parts_mut(ptr.cast::<u8>(), std::mem::size_of::<libc::cpuset_t>());
210 for (i, b) in mask.to_bytes().into_iter().enumerate() {
211 if let Some(slot) = bytes.get_mut(i) {
212 *slot = b;
213 }
214 }
215 }
216 let rc = unsafe {
218 libc::cpuset_setaffinity(
219 libc::CPU_LEVEL_WHICH,
220 libc::CPU_WHICH_PID,
221 pid as libc::id_t,
222 std::mem::size_of::<libc::cpuset_t>(),
223 set.as_ptr(),
224 )
225 };
226 if rc == 0 {
227 Ok(())
228 } else {
229 Err(std::io::Error::last_os_error().to_string())
230 }
231 }
232}
233
234#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
236mod imp {
237 use super::CpuMask;
238
239 pub fn read(_pid: u32, _cores: usize) -> Result<Option<CpuMask>, String> {
240 Ok(None)
243 }
244
245 pub fn write(_pid: u32, _mask: &CpuMask) -> Result<(), String> {
246 Err(
247 "this platform has no per-process CPU affinity mask; \
248 macOS only offers a scheduler hint"
249 .to_string(),
250 )
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn bits_round_trip() {
260 let mut m = CpuMask::empty(16);
261 assert_eq!(m.count(), 0);
262 m.set(0, true);
263 m.set(15, true);
264 assert!(m.get(0) && m.get(15));
265 assert!(!m.get(7));
266 assert_eq!(m.count(), 2);
267
268 m.toggle(15);
269 assert!(!m.get(15));
270 m.toggle(15);
271 assert!(m.get(15));
272 }
273
274 #[test]
275 fn all_covers_every_core() {
276 for cores in [1usize, 8, 64, 65, 128] {
277 let m = CpuMask::all(cores);
278 assert_eq!(m.count(), cores, "cores={cores}");
279 assert!(m.get(cores - 1));
280 }
281 }
282
283 #[test]
284 fn a_mask_beyond_one_word_works() {
285 let mut m = CpuMask::all(100);
286 assert_eq!(m.count(), 100);
287 m.set(99, false);
288 assert_eq!(m.count(), 99);
289 assert!(m.get(64));
290 }
291
292 #[test]
293 fn bytes_round_trip() {
294 let m = CpuMask::all(40);
295 let back = CpuMask::from_bytes(&m.to_bytes(), 40);
296 assert_eq!(m, back);
297 }
298
299 #[test]
300 fn a_short_byte_buffer_is_not_trusted() {
301 let back = CpuMask::from_bytes(&[0b0000_0101], 64);
303 assert!(back.get(0));
304 assert!(back.get(2));
305 assert!(!back.get(8), "bits past the buffer must be clear, not garbage");
306 }
307
308 #[test]
309 fn an_empty_mask_is_rejected() {
310 let err = write(std::process::id(), &CpuMask::empty(4));
311 assert!(err.is_err(), "an empty mask must never reach the kernel");
312 }
313
314 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
315 #[test]
316 fn reading_our_own_affinity_works_and_is_never_empty() {
317 let cores = std::thread::available_parallelism()
318 .map(|n| n.get())
319 .unwrap_or(1);
320 let mask = read(std::process::id(), cores).expect("no error").expect("a mask");
321 assert!(!mask.is_empty(), "a running process has at least one CPU");
322 assert!(mask.count() <= cores);
323 }
324
325 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
326 #[test]
327 fn setting_our_own_affinity_round_trips() {
328 let cores = std::thread::available_parallelism()
329 .map(|n| n.get())
330 .unwrap_or(1);
331 let original = read(std::process::id(), cores).unwrap().unwrap();
332
333 let mut wanted = CpuMask::empty(cores);
335 wanted.set(0, true);
336 if write(std::process::id(), &wanted).is_ok() {
337 let got = read(std::process::id(), cores).unwrap().unwrap();
338 assert!(got.get(0), "the mask should now contain CPU 0");
339 assert_eq!(got.count(), 1);
340 }
341
342 let _ = write(std::process::id(), &original);
344 }
345
346 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
347 #[test]
348 fn platforms_without_a_mask_say_so() {
349 assert!(!supported());
350 assert_eq!(read(std::process::id(), 8), Ok(None));
351 let err = write(std::process::id(), &CpuMask::all(8)).unwrap_err();
352 assert!(err.contains("no per-process CPU affinity"), "got {err:?}");
353 }
354}