Skip to main content

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}