seize/raw/membarrier.rs
1//! Memory barriers optimized for RCU, inspired by <https://github.com/jeehoonkang/membarrier-rs>.
2//!
3//! # Semantics
4//!
5//! There is a total order over all memory barriers provided by this module:
6//! - Light store barriers, created by a pair of [`light_store`] and
7//! [`light_barrier`].
8//! - Light load barriers, created by a pair of [`light_barrier`] and
9//! [`light_load`].
10//! - Sequentially consistent barriers, or cumulative light barriers.
11//! - Heavy barriers, created by [`heavy`].
12//!
13//! If thread A issues barrier X and thread B issues barrier Y and X occurs
14//! before Y in the total order, X is ordered before Y with respect to coherence
15//! only if either X or Y is a heavy barrier. In other words, there is no way to
16//! establish an ordering between light barriers without the presence of a heavy
17//! barrier.
18#![allow(dead_code)]
19
20#[cfg(all(target_os = "linux", feature = "fast-barrier", not(miri)))]
21pub use linux::*;
22
23#[cfg(all(target_os = "windows", feature = "fast-barrier", not(miri)))]
24pub use windows::*;
25
26#[cfg(any(
27 not(feature = "fast-barrier"),
28 not(any(target_os = "windows", target_os = "linux")),
29 miri
30))]
31pub use default::*;
32
33#[cfg(any(
34 not(feature = "fast-barrier"),
35 not(any(target_os = "windows", target_os = "linux")),
36 miri
37))]
38mod default {
39 use core::sync::atomic::{fence, Ordering};
40
41 pub fn detect() {}
42
43 /// The ordering for a store operation that synchronizes with heavy
44 /// barriers.
45 ///
46 /// Must be followed by a light barrier.
47 #[inline]
48 pub fn light_store() -> Ordering {
49 // Synchronize with `SeqCst` heavy barriers.
50 Ordering::SeqCst
51 }
52
53 /// Issues a light memory barrier for a preceding store or subsequent load
54 /// operation.
55 #[inline]
56 pub fn light_barrier() {
57 // This is a no-op due to strong loads and stores.
58 }
59
60 /// The ordering for a load operation that synchronizes with heavy barriers.
61 #[inline]
62 pub fn light_load() -> Ordering {
63 // Participate in the total order established by light and heavy `SeqCst`
64 // barriers.
65 Ordering::SeqCst
66 }
67
68 /// Issues a heavy memory barrier for slow path that synchronizes with light
69 /// stores.
70 #[inline]
71 pub fn heavy() {
72 // Synchronize with `SeqCst` light stores.
73 fence(Ordering::SeqCst);
74 }
75}
76
77#[cfg(all(target_os = "linux", feature = "fast-barrier", not(miri)))]
78mod linux {
79 use std::sync::atomic::{self, AtomicU8, Ordering};
80
81 /// The ordering for a store operation that synchronizes with heavy
82 /// barriers.
83 ///
84 /// Must be followed by a light barrier.
85 #[inline]
86 pub fn light_store() -> Ordering {
87 match STRATEGY.load(Ordering::Relaxed) {
88 FALLBACK => Ordering::SeqCst,
89 _ => Ordering::Relaxed,
90 }
91 }
92
93 /// Issues a light memory barrier for a preceding store or subsequent load
94 /// operation.
95 #[inline]
96 pub fn light_barrier() {
97 atomic::compiler_fence(atomic::Ordering::SeqCst)
98 }
99
100 /// The ordering for a load operation that synchronizes with heavy barriers.
101 #[inline]
102 pub fn light_load() -> Ordering {
103 // There is no difference between `Acquire` and `SeqCst` loads on most
104 // platforms, so checking the strategy is not worth it.
105 Ordering::SeqCst
106 }
107
108 /// Issues a heavy memory barrier for slow path.
109 #[inline]
110 pub fn heavy() {
111 // Issue a private expedited membarrier using the `sys_membarrier()` system
112 // call, if supported; otherwise, fall back to `mprotect()`-based
113 // process-wide memory barrier.
114 match STRATEGY.load(Ordering::Relaxed) {
115 MEMBARRIER => membarrier::barrier(),
116 MPROTECT => mprotect::barrier(),
117 _ => atomic::fence(atomic::Ordering::SeqCst),
118 }
119 }
120
121 /// Use the `membarrier` system call.
122 const MEMBARRIER: u8 = 0;
123
124 /// Use the `mprotect`-based trick.
125 const MPROTECT: u8 = 1;
126
127 /// Use `SeqCst` fences.
128 const FALLBACK: u8 = 2;
129
130 /// The right strategy to use on the current machine.
131 static STRATEGY: AtomicU8 = AtomicU8::new(FALLBACK);
132
133 /// Perform runtime detection for a membarrier strategy.
134 pub fn detect() {
135 if membarrier::is_supported() {
136 STRATEGY.store(MEMBARRIER, Ordering::Relaxed);
137 } else if mprotect::is_supported() {
138 STRATEGY.store(MPROTECT, Ordering::Relaxed);
139 }
140 }
141
142 macro_rules! fatal_assert {
143 ($cond:expr) => {
144 if !$cond {
145 #[allow(unused_unsafe)]
146 unsafe {
147 libc::abort();
148 }
149 }
150 };
151 }
152
153 mod membarrier {
154 /// Commands for the membarrier system call.
155 ///
156 /// # Caveat
157 ///
158 /// We're defining it here because, unfortunately, the `libc` crate
159 /// currently doesn't expose `membarrier_cmd` for us. You can
160 /// find the numbers in the [Linux source code](https://github.com/torvalds/linux/blob/master/include/uapi/linux/membarrier.h).
161 ///
162 /// This enum should really be `#[repr(libc::c_int)]`, but Rust
163 /// currently doesn't allow it.
164 #[repr(i32)]
165 #[allow(dead_code, non_camel_case_types)]
166 enum membarrier_cmd {
167 MEMBARRIER_CMD_QUERY = 0,
168 MEMBARRIER_CMD_GLOBAL = (1 << 0),
169 MEMBARRIER_CMD_GLOBAL_EXPEDITED = (1 << 1),
170 MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED = (1 << 2),
171 MEMBARRIER_CMD_PRIVATE_EXPEDITED = (1 << 3),
172 MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED = (1 << 4),
173 MEMBARRIER_CMD_PRIVATE_EXPEDITED_SYNC_CORE = (1 << 5),
174 MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED_SYNC_CORE = (1 << 6),
175 }
176
177 /// Call the `sys_membarrier` system call.
178 #[inline]
179 fn sys_membarrier(cmd: membarrier_cmd) -> libc::c_long {
180 unsafe { libc::syscall(libc::SYS_membarrier, cmd as libc::c_int, 0 as libc::c_int) }
181 }
182
183 /// Returns `true` if the `sys_membarrier` call is available.
184 pub fn is_supported() -> bool {
185 // Queries which membarrier commands are supported. Checks if private expedited
186 // membarrier is supported.
187 let ret = sys_membarrier(membarrier_cmd::MEMBARRIER_CMD_QUERY);
188 if ret < 0
189 || ret & membarrier_cmd::MEMBARRIER_CMD_PRIVATE_EXPEDITED as libc::c_long == 0
190 || ret & membarrier_cmd::MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED as libc::c_long
191 == 0
192 {
193 return false;
194 }
195
196 // Registers the current process as a user of private expedited membarrier.
197 if sys_membarrier(membarrier_cmd::MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED) < 0 {
198 return false;
199 }
200
201 true
202 }
203
204 /// Executes a heavy `sys_membarrier`-based barrier.
205 #[inline]
206 pub fn barrier() {
207 fatal_assert!(sys_membarrier(membarrier_cmd::MEMBARRIER_CMD_PRIVATE_EXPEDITED) >= 0);
208 }
209 }
210
211 mod mprotect {
212 use std::cell::UnsafeCell;
213 use std::mem::MaybeUninit;
214 use std::ptr;
215 use std::sync::{atomic, OnceLock};
216
217 struct Barrier {
218 lock: UnsafeCell<libc::pthread_mutex_t>,
219 page: u64,
220 page_size: libc::size_t,
221 }
222
223 unsafe impl Sync for Barrier {}
224
225 impl Barrier {
226 /// Issues a process-wide barrier by changing access protections of
227 /// a single mmap-ed page. This method is not as fast as
228 /// the `sys_membarrier()` call, but works very
229 /// similarly.
230 #[inline]
231 fn barrier(&self) {
232 let page = self.page as *mut libc::c_void;
233
234 unsafe {
235 // Lock the mutex.
236 fatal_assert!(libc::pthread_mutex_lock(self.lock.get()) == 0);
237
238 // Set the page access protections to read + write.
239 fatal_assert!(
240 libc::mprotect(page, self.page_size, libc::PROT_READ | libc::PROT_WRITE,)
241 == 0
242 );
243
244 // Ensure that the page is dirty before we change the protection so that we
245 // prevent the OS from skipping the global TLB flush.
246 let atomic_usize = &*(page as *const atomic::AtomicUsize);
247 atomic_usize.fetch_add(1, atomic::Ordering::SeqCst);
248
249 // Set the page access protections to none.
250 //
251 // Changing a page protection from read + write to none causes the OS to issue
252 // an interrupt to flush TLBs on all processors. This also results in flushing
253 // the processor buffers.
254 fatal_assert!(libc::mprotect(page, self.page_size, libc::PROT_NONE) == 0);
255
256 // Unlock the mutex.
257 fatal_assert!(libc::pthread_mutex_unlock(self.lock.get()) == 0);
258 }
259 }
260 }
261
262 /// An alternative solution to `sys_membarrier` that works on older
263 /// Linux kernels and x86/x86-64 systems.
264 static BARRIER: OnceLock<Barrier> = OnceLock::new();
265
266 /// Returns `true` if the `mprotect`-based trick is supported.
267 pub fn is_supported() -> bool {
268 cfg!(target_arch = "x86") || cfg!(target_arch = "x86_64")
269 }
270
271 /// Executes a heavy `mprotect`-based barrier.
272 #[inline]
273 pub fn barrier() {
274 let barrier = BARRIER.get_or_init(|| {
275 unsafe {
276 // Find out the page size on the current system.
277 let page_size = libc::sysconf(libc::_SC_PAGESIZE);
278 fatal_assert!(page_size > 0);
279 let page_size = page_size as libc::size_t;
280
281 // Create a dummy page.
282 let page = libc::mmap(
283 ptr::null_mut(),
284 page_size,
285 libc::PROT_NONE,
286 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
287 -1 as libc::c_int,
288 0 as libc::off_t,
289 );
290 fatal_assert!(page != libc::MAP_FAILED);
291 fatal_assert!(page as libc::size_t % page_size == 0);
292
293 // Locking the page ensures that it stays in memory during the two mprotect
294 // calls in `Barrier::barrier()`. If the page was unmapped between those calls,
295 // they would not have the expected effect of generating IPI.
296 libc::mlock(page, page_size as libc::size_t);
297
298 // Initialize the mutex.
299 let lock = UnsafeCell::new(libc::PTHREAD_MUTEX_INITIALIZER);
300 let mut attr = MaybeUninit::<libc::pthread_mutexattr_t>::uninit();
301 fatal_assert!(libc::pthread_mutexattr_init(attr.as_mut_ptr()) == 0);
302 let mut attr = attr.assume_init();
303 fatal_assert!(
304 libc::pthread_mutexattr_settype(&mut attr, libc::PTHREAD_MUTEX_NORMAL) == 0
305 );
306 fatal_assert!(libc::pthread_mutex_init(lock.get(), &attr) == 0);
307 fatal_assert!(libc::pthread_mutexattr_destroy(&mut attr) == 0);
308
309 let page = page as u64;
310
311 Barrier {
312 lock,
313 page,
314 page_size,
315 }
316 }
317 });
318
319 barrier.barrier();
320 }
321 }
322}
323
324#[cfg(all(target_os = "windows", feature = "fast-barrier", not(miri)))]
325mod windows {
326 use core::sync::atomic::{self, Ordering};
327 use windows_sys;
328
329 pub fn detect() {}
330
331 /// The ordering for a store operation that synchronizes with heavy
332 /// barriers.
333 ///
334 /// Must be followed by a light barrier.
335 #[inline]
336 pub fn light_store() -> Ordering {
337 Ordering::Relaxed
338 }
339
340 /// Issues a light memory barrier for a preceding store or subsequent load
341 /// operation.
342 #[inline]
343 pub fn light_barrier() {
344 atomic::compiler_fence(atomic::Ordering::SeqCst)
345 }
346
347 /// The ordering for a load operation that synchronizes with heavy barriers.
348 #[inline]
349 pub fn light_load() -> Ordering {
350 Ordering::Relaxed
351 }
352
353 /// Issues a heavy memory barrier for slow path that synchronizes with light
354 /// stores.
355 #[inline]
356 pub fn heavy() {
357 // Invoke the `FlushProcessWriteBuffers()` system call.
358 unsafe { windows_sys::Win32::System::Threading::FlushProcessWriteBuffers() }
359 }
360}