hwlocality/interop/
linux.rs

1//! Linux-specific helpers
2
3#[cfg(doc)]
4use crate::cpu::binding::CpuBindingFlags;
5use crate::{
6    cpu::cpuset::CpuSet,
7    errors::{self, HybridError, RawHwlocError},
8    path::{self, PathError},
9    topology::Topology,
10};
11#[allow(unused)]
12#[cfg(test)]
13use similar_asserts::assert_eq;
14use std::{ops::Deref, path::Path};
15
16// This file is rustdoc-visible so we must provide a substitute for
17// linux-specific libc entities when people run rustdoc on Windows.
18#[cfg(target_os = "linux")]
19use libc::pid_t;
20#[cfg(all(doc, not(target_os = "linux")))]
21#[allow(non_camel_case_types)]
22struct pid_t;
23
24/// # Linux-specific helpers
25///
26/// This includes helpers for manipulating Linux kernel cpumap files, and hwloc
27/// equivalents of the Linux `sched_setaffinity` and `sched_getaffinity` system
28/// calls.
29//
30// --- Implementation details ---
31//
32// Upstream docs: https://hwloc.readthedocs.io/en/stable/group__hwlocality__linux.html
33impl Topology {
34    /// Bind a thread `tid` on cpus given in `set`
35    ///
36    /// `set` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`.
37    ///
38    /// The behavior is exactly the same as the Linux `sched_setaffinity` system
39    /// call, but uses a hwloc [`CpuSet`].
40    ///
41    /// This is equivalent to calling [`bind_process_cpu()`] with the [`THREAD`]
42    /// binding flag.
43    ///
44    /// [`bind_process_cpu()`]: Topology::bind_process_cpu()
45    /// [`THREAD`]: CpuBindingFlags::THREAD
46    #[allow(clippy::missing_errors_doc)]
47    #[doc(alias = "hwloc_linux_set_tid_cpubind")]
48    pub fn bind_tid_cpu(
49        &self,
50        tid: pid_t,
51        set: impl Deref<Target = CpuSet>,
52    ) -> Result<(), RawHwlocError> {
53        /// Polymorphized version of this function (avoids generics code bloat)
54        fn polymorphized(self_: &Topology, tid: pid_t, set: &CpuSet) -> Result<(), RawHwlocError> {
55            // SAFETY: - Topology is trusted to contain a valid ptr (type invariant)
56            //         - Bitmap is trusted to contain a valid ptr (type invariant)
57            //         - hwloc ops are trusted not to modify *const parameters
58            //         - TID cannot be validated (think TOCTOU), but hwloc should be
59            //           able to handle an invalid TID
60            errors::call_hwloc_zero_or_minus1("hwloc_linux_set_tid_cpubind", || unsafe {
61                hwlocality_sys::hwloc_linux_set_tid_cpubind(self_.as_ptr(), tid, set.as_ptr())
62            })
63        }
64        polymorphized(self, tid, &set)
65    }
66
67    /// Current binding of thread `tid`.
68    ///
69    /// Returns the [`CpuSet`] of PUs which the thread was last bound to.
70    ///
71    /// The behavior is exactly the same as the Linux `sched_getaffinity` system
72    /// call, but uses a hwloc [`CpuSet`].
73    ///
74    /// This is equivalent to calling [`process_cpu_binding()`] with the
75    /// [`THREAD`] binding flag.
76    ///
77    /// [`process_cpu_binding()`]: Topology::process_cpu_binding()
78    /// [`THREAD`]: CpuBindingFlags::THREAD
79    #[allow(clippy::missing_errors_doc)]
80    #[doc(alias = "hwloc_linux_get_tid_cpubind")]
81    pub fn tid_cpu_binding(&self, tid: pid_t) -> Result<CpuSet, RawHwlocError> {
82        let mut set = CpuSet::new();
83        // SAFETY: - Topology is trusted to contain a valid ptr (type invariant)
84        //         - Bitmap is trusted to contain a valid ptr (type invariant)
85        //         - hwloc ops are trusted not to modify *const parameters
86        //         - hwloc ops are trusted to keep *mut parameters in a
87        //           valid state unless stated otherwise
88        //         - TID cannot be validated (think TOCTOU), but hwloc should be
89        //           able to handle an invalid TID
90        errors::call_hwloc_zero_or_minus1("hwloc_linux_get_tid_cpubind", || unsafe {
91            hwlocality_sys::hwloc_linux_get_tid_cpubind(self.as_ptr(), tid, set.as_mut_ptr())
92        })
93        .map(|()| set)
94    }
95
96    /// Last physical CPU where thread `tid` ran.
97    ///
98    /// Indicates the PU which the thread last ran on, as a singleton [`CpuSet`].
99    ///
100    /// This is equivalent to calling [`last_process_cpu_location()`] with the
101    /// [`THREAD`] binding flag.
102    ///
103    /// [`last_process_cpu_location()`]: Topology::last_process_cpu_location()
104    /// [`THREAD`]: CpuBindingFlags::THREAD
105    #[allow(clippy::missing_errors_doc)]
106    #[doc(alias = "hwloc_linux_get_tid_last_cpu_location")]
107    pub fn last_tid_cpu_location(&self, tid: pid_t) -> Result<CpuSet, RawHwlocError> {
108        let mut set = CpuSet::new();
109        // SAFETY: - Topology is trusted to contain a valid ptr (type invariant)
110        //         - Bitmap is trusted to contain a valid ptr (type invariant)
111        //         - hwloc ops are trusted not to modify *const parameters
112        //         - hwloc ops are trusted to keep *mut parameters in a
113        //           valid state unless stated otherwise
114        //         - TID cannot be validated (think TOCTOU), but hwloc should be
115        //           able to handle an invalid TID
116        errors::call_hwloc_zero_or_minus1("hwloc_linux_get_tid_last_cpu_location", || unsafe {
117            hwlocality_sys::hwloc_linux_get_tid_last_cpu_location(
118                self.as_ptr(),
119                tid,
120                set.as_mut_ptr(),
121            )
122        })
123        .map(|()| set)
124    }
125
126    /// Convert a linux kernel cpumask file path into a hwloc bitmap set.
127    ///
128    /// Might be used when reading CPU sets from sysfs attributes such as
129    /// `topology` and `caches` for processors, or `local_cpus` for devices.
130    ///
131    /// Note that this function ignores the [HWLOC_FSROOT environment
132    /// variable](https://hwloc.readthedocs.io/en/stable/envvar.html).
133    ///
134    /// # Errors
135    ///
136    /// - [`ContainsNul`] if `path` contains NUL chars.
137    /// - [`NotUnicode`] if `path` contains non-Unicode data
138    ///
139    /// # Example
140    ///
141    #[cfg_attr(target_os = "linux", doc = "```rust")]
142    #[cfg_attr(not(target_os = "linux"), doc = "```rust,ignore")]
143    /// # use hwlocality::{topology::Topology, object::types::ObjectType};
144    /// #
145    /// # let topology = Topology::test_instance();
146    /// #
147    /// use eyre::eyre;
148    ///
149    /// // Read cpuset of first core via Linux sysfs
150    /// let core_set = topology
151    ///     .read_path_as_cpumask("/sys/devices/system/cpu/cpu0/topology/core_cpus")?;
152    ///
153    /// // Check that the hwloc version is consistent
154    /// assert_eq!(
155    ///     core_set,
156    ///     topology
157    ///         .objects_with_type(ObjectType::Core)
158    ///         .next()
159    ///         .ok_or_else(|| eyre!("Linux system should have CPU cores"))?
160    ///         .cpuset()
161    ///         .ok_or_else(|| eyre!("CPU cores should have cpusets"))?
162    /// );
163    /// #
164    /// # Ok::<(), eyre::Report>(())
165    /// ```
166    ///
167    /// [`ContainsNul`]: PathError::ContainsNul
168    /// [`NotUnicode`]: PathError::NotUnicode
169    #[doc(alias = "hwloc_linux_read_path_as_cpumask")]
170    pub fn read_path_as_cpumask(
171        &self,
172        path: impl AsRef<Path>,
173    ) -> Result<CpuSet, HybridError<PathError>> {
174        /// Polymorphized version of this function (avoids generics code bloat)
175        fn polymorphized(path: &Path) -> Result<CpuSet, HybridError<PathError>> {
176            let path = path::make_hwloc_path(path)?;
177            let mut set = CpuSet::new();
178            // SAFETY: - Path is trusted to contain a valid C string (type invariant)
179            //         - Bitmap is trusted to contain a valid ptr (type invariant)
180            //         - hwloc ops are trusted not to modify *const parameters
181            //         - hwloc ops are trusted to keep *mut parameters in a
182            //           valid state unless stated otherwise
183            errors::call_hwloc_zero_or_minus1("hwloc_linux_read_path_as_cpumask", || unsafe {
184                hwlocality_sys::hwloc_linux_read_path_as_cpumask(path.borrow(), set.as_mut_ptr())
185            })
186            .map(|()| set)
187            .map_err(HybridError::Hwloc)
188        }
189        polymorphized(path.as_ref())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::{
197        cpu::binding::CpuBindingFlags, object::types::ObjectType, strategies::topology_related_set,
198    };
199    use proptest::prelude::*;
200    #[allow(unused)]
201    use similar_asserts::assert_eq;
202
203    #[test]
204    fn read_path_as_cpumask() {
205        let topology = Topology::test_instance();
206
207        // Read cpuset of first core via Linux sysfs
208        let core_set = topology
209            .read_path_as_cpumask("/sys/devices/system/cpu/cpu0/topology/core_cpus")
210            .unwrap();
211
212        // Compare with hwloc result
213        assert_eq!(
214            core_set,
215            topology
216                .objects_with_type(ObjectType::Core)
217                .next()
218                .unwrap()
219                .cpuset()
220                .unwrap()
221        );
222    }
223
224    #[test]
225    fn initial_tid_cpubind() {
226        // SAFETY: Should always be safe to call
227        let my_tid = unsafe { libc::gettid() };
228        let topology = Topology::test_instance();
229
230        let my_cpu_binding = topology
231            .process_cpu_binding(my_tid.try_into().unwrap(), CpuBindingFlags::THREAD)
232            .unwrap();
233        assert_eq!(topology.tid_cpu_binding(my_tid).unwrap(), my_cpu_binding);
234
235        let last_cpu_location = topology.last_tid_cpu_location(my_tid).unwrap();
236        assert_eq!(last_cpu_location.weight(), Some(1));
237        assert!(my_cpu_binding.includes(&last_cpu_location));
238    }
239
240    proptest! {
241        #[test]
242        fn bind_tid_cpu(
243            set in topology_related_set(Topology::allowed_cpuset)
244        ) {
245            // SAFETY: Should always be safe to call
246            let my_tid = unsafe { libc::gettid() };
247            let topology = Topology::test_instance();
248            let initial = topology.tid_cpu_binding(my_tid).unwrap();
249
250            let result = topology.bind_tid_cpu(my_tid, &set);
251            if result.is_err() {
252                prop_assert!(!initial.includes(&set) || set.is_empty());
253                return Ok(());
254            }
255
256            // Linux can enforce a tighter binding than requested
257            let actual_binding = topology.tid_cpu_binding(my_tid).unwrap();
258            prop_assert!(
259                actual_binding == set
260                || set.includes(&actual_binding),
261                "actual binding {actual_binding} doesn't match request {set}"
262            );
263            prop_assert!(set.includes(&topology.last_tid_cpu_location(my_tid).unwrap()));
264
265            topology.bind_tid_cpu(my_tid, &initial).unwrap();
266        }
267    }
268}