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}