themis/topology/cpu/binding.rs
1//! Confining the calling thread to one logical processor.
2//!
3//! Unlike the best-effort node binding a consumer may layer on top, this
4//! primitive reports every failure as a typed [`BindError`], so a scheduler can
5//! refuse to start a worker whose binding did not take effect.
6
7use core::fmt;
8
9#[cfg(all(target_os = "linux", not(miri)))]
10mod linux;
11#[cfg(all(windows, not(miri)))]
12mod windows;
13
14/// Why the calling thread could not be confined to a logical processor.
15///
16/// The variants partition the failure by who can act on it: the target has no
17/// backend ([`Self::Unsupported`]), the processor id is not addressable
18/// ([`Self::OutOfRange`]), or the operating system refused the request
19/// ([`Self::Os`]). In every case the thread keeps its previous affinity.
20#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21#[non_exhaustive]
22pub enum BindError {
23 /// This target has no thread-binding backend, or the crate runs under an
24 /// interpreter that cannot issue the call.
25 Unsupported,
26 /// The processor id is beyond what the target's affinity interface can
27 /// name.
28 OutOfRange {
29 /// The requested flattened logical processor id.
30 processor: u32,
31 },
32 /// The operating system rejected the request, for example because the
33 /// processor is offline or outside the process's allowed set.
34 Os {
35 /// The raw operating-system error code: `GetLastError` on Windows,
36 /// `errno` on Linux.
37 code: i32,
38 },
39}
40
41impl fmt::Display for BindError {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match *self {
44 Self::Unsupported => {
45 formatter.write_str("thread binding is unsupported on this target")
46 }
47 Self::OutOfRange { processor } => write!(
48 formatter,
49 "logical processor {processor} is outside the range this target can bind"
50 ),
51 Self::Os { code } => write!(
52 formatter,
53 "the operating system refused the thread binding (error code {code})"
54 ),
55 }
56 }
57}
58
59impl std::error::Error for BindError {}
60
61/// Confines the calling thread to exactly one logical processor.
62///
63/// `processor` uses the flattened numbering of [`crate::CpuTopology`]: on
64/// Windows `group * 64 + bit`, on Linux the kernel's cpu number. The binding
65/// lasts for the thread's life. There is no guard and no restore, because a
66/// worker thread pins once at startup; a second call rebinds the thread.
67///
68/// The call succeeds only when the operating system accepted the new
69/// affinity, so a caller that publishes a worker-to-processor assignment after
70/// `Ok(())` publishes a fact, and a caller that receives `Err` may refuse to
71/// start the worker.
72///
73/// # Errors
74///
75/// - [`BindError::Unsupported`] when the target has no backend.
76/// - [`BindError::OutOfRange`] when the processor id cannot be named by the
77/// target's affinity interface: a Windows processor group beyond `u16`, or a
78/// Linux id at or past the crate's processor-id bound of 32 768.
79/// - [`BindError::Os`] with the raw error code when the operating system
80/// refuses, for example `EINVAL` on Linux for a processor that is offline or
81/// outside the process's cpuset.
82///
83/// # Examples
84///
85/// A processor id no target can name is refused, and the calling thread is
86/// left untouched:
87///
88/// ```
89/// use themis::{bind_current_thread, BindError};
90///
91/// assert!(matches!(
92/// bind_current_thread(u32::MAX),
93/// Err(BindError::OutOfRange { processor: u32::MAX } | BindError::Unsupported)
94/// ));
95/// ```
96///
97/// A worker pins itself once at startup and refuses to run unbound. Which
98/// processors the host permits is environmental, so the example is not run:
99///
100/// ```no_run
101/// use themis::bind_current_thread;
102///
103/// let worker = std::thread::spawn(|| bind_current_thread(0));
104/// worker.join().expect("worker panicked")?;
105/// # Ok::<(), themis::BindError>(())
106/// ```
107pub fn bind_current_thread(processor: u32) -> Result<(), BindError> {
108 #[cfg(all(target_os = "linux", not(miri)))]
109 {
110 linux::bind_current_thread(processor)
111 }
112 #[cfg(all(windows, not(miri)))]
113 {
114 windows::bind_current_thread(processor)
115 }
116 #[cfg(not(any(all(target_os = "linux", not(miri)), all(windows, not(miri)))))]
117 {
118 let _ = processor;
119 Err(BindError::Unsupported)
120 }
121}
122
123/// Reads the error the failed call just left in the thread's error slot.
124///
125/// Must run immediately after the failing call, before any other system call
126/// on the thread can overwrite it.
127#[cfg(all(any(target_os = "linux", windows), not(miri)))]
128fn last_os_error() -> BindError {
129 BindError::Os {
130 code: std::io::Error::last_os_error()
131 .raw_os_error()
132 .expect("invariant: last_os_error is built from a raw OS code"),
133 }
134}
135
136#[cfg(test)]
137mod tests;