Skip to main content

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;