Skip to main content

subetha_cxc/
hugepages.rs

1//! `hugepages`: Linux-only hugepage-backed mmap helper.
2//!
3//! Allocates an anonymous mmap region backed by 2MB hugepages
4//! (MAP_HUGETLB | MAP_HUGE_2MB). Useful for large rings where
5//! TLB pressure measurably hurts: a 16MB region fits in 8
6//! hugepages vs 4096 4KB pages.
7//!
8//! Falls back gracefully (returns Err) if the kernel does not have
9//! hugepages reserved; callers handle the fallback by switching to
10//! standard 4KB anon mmap.
11
12#![cfg(target_os = "linux")]
13
14use std::io;
15use std::os::unix::io::AsRawFd;
16use std::path::{Path, PathBuf};
17use std::ptr;
18
19/// 2MB hugepage size in bytes.
20pub const HUGEPAGE_2MB: usize = 2 * 1024 * 1024;
21
22/// 1GB hugepage size in bytes.
23pub const HUGEPAGE_1GB: usize = 1024 * 1024 * 1024;
24
25/// Which hugepage size to request from the kernel.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HugepageSize {
28    /// 2 MB hugepages (most widely available).
29    Mb2,
30    /// 1 GB hugepages (require CONFIG_HUGETLBFS + reserved
31    /// gigabyte pages at boot).
32    Gb1,
33}
34
35impl HugepageSize {
36    fn bytes(self) -> usize {
37        match self {
38            Self::Mb2 => HUGEPAGE_2MB,
39            Self::Gb1 => HUGEPAGE_1GB,
40        }
41    }
42
43    fn mmap_flag(self) -> libc::c_int {
44        match self {
45            // MAP_HUGE_2MB = 21 << MAP_HUGE_SHIFT
46            // MAP_HUGE_1GB = 30 << MAP_HUGE_SHIFT
47            // MAP_HUGE_SHIFT = 26
48            Self::Mb2 => libc::MAP_HUGETLB | (21 << 26),
49            Self::Gb1 => libc::MAP_HUGETLB | (30 << 26),
50        }
51    }
52}
53
54/// A hugepage-backed anonymous mmap region.
55pub struct HugepageRegion {
56    ptr: *mut u8,
57    len: usize,
58}
59
60unsafe impl Send for HugepageRegion {}
61unsafe impl Sync for HugepageRegion {}
62
63impl HugepageRegion {
64    /// Allocate `pages` hugepages of the requested size.
65    pub fn allocate(pages: usize, size: HugepageSize) -> io::Result<Self> {
66        assert!(pages > 0);
67        let len = pages * size.bytes();
68        let prot = libc::PROT_READ | libc::PROT_WRITE;
69        let flags = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | size.mmap_flag();
70        let raw = unsafe { libc::mmap(ptr::null_mut(), len, prot, flags, -1, 0) };
71        if raw == libc::MAP_FAILED {
72            return Err(io::Error::last_os_error());
73        }
74        Ok(Self { ptr: raw as *mut u8, len })
75    }
76
77    pub fn as_mut_slice(&mut self) -> &mut [u8] {
78        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
79    }
80
81    pub fn as_slice(&self) -> &[u8] {
82        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
83    }
84
85    pub fn len(&self) -> usize { self.len }
86    pub fn is_empty(&self) -> bool { self.len == 0 }
87}
88
89impl Drop for HugepageRegion {
90    fn drop(&mut self) {
91        unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
92    }
93}
94
95impl crate::spsc_ring::RegionOwner for HugepageRegion {
96    fn region_ptr(&mut self) -> *mut u8 {
97        self.as_mut_slice().as_mut_ptr()
98    }
99    fn region_len(&self) -> usize {
100        self.len()
101    }
102}
103
104/// A CROSS-PROCESS hugepage-backed region: a file on a `hugetlbfs` mount,
105/// mmap'd `MAP_SHARED`. Unrelated processes open the same path and mmap it,
106/// so a ring laid out in the region is shared through hugepage physical
107/// memory. This is the Linux analogue of the Windows large-page
108/// `LargePageSection` (named, openable by a second process); the
109/// anonymous [`HugepageRegion`] above is in-process / fork-shared only.
110///
111/// The file's mmap is automatically hugepage-backed because it lives on a
112/// `hugetlbfs` filesystem (mount one with
113/// `mount -t hugetlbfs nodev <dir>`); the length must be a multiple of
114/// the mount's hugepage size.
115pub struct SharedHugepageRegion {
116    _file: std::fs::File,
117    ptr: *mut u8,
118    len: usize,
119    path: PathBuf,
120    owner: bool,
121}
122
123unsafe impl Send for SharedHugepageRegion {}
124unsafe impl Sync for SharedHugepageRegion {}
125
126impl SharedHugepageRegion {
127    fn map(file: &std::fs::File, len: usize) -> io::Result<*mut u8> {
128        let raw = unsafe {
129            libc::mmap(
130                ptr::null_mut(),
131                len,
132                libc::PROT_READ | libc::PROT_WRITE,
133                libc::MAP_SHARED,
134                file.as_raw_fd(),
135                0,
136            )
137        };
138        if raw == libc::MAP_FAILED {
139            Err(io::Error::last_os_error())
140        } else {
141            Ok(raw as *mut u8)
142        }
143    }
144
145    /// Obtain a hugetlbfs file of `pages` hugepages and map it.
146    /// Initializes the file if the path does not yet exist, and
147    /// otherwise attaches to it with its contents in place. The
148    /// region carries no header, so the page count is checked against
149    /// the file's exact byte size. The elected creator owns the path
150    /// and unlinks it on drop; attachers do not. `path` must be on a
151    /// hugetlbfs mount. [`reset`](Self::reset) reinitializes.
152    pub fn create(
153        path: impl AsRef<Path>,
154        pages: usize,
155        size: HugepageSize,
156    ) -> io::Result<Self> {
157        assert!(pages > 0);
158        let path = path.as_ref().to_path_buf();
159        let len = pages * size.bytes();
160        match std::fs::OpenOptions::new()
161            .read(true)
162            .write(true)
163            .create_new(true)
164            .open(&path)
165        {
166            Ok(file) => {
167                // hugetlbfs requires the length be a multiple of its
168                // hugepage size.
169                file.set_len(len as u64)?;
170                let ptr = Self::map(&file, len)?;
171                Ok(Self { _file: file, ptr, len, path, owner: true })
172            }
173            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
174                let file = std::fs::OpenOptions::new()
175                    .read(true)
176                    .write(true)
177                    .open(&path)?;
178                // The size doubles as the ready signal: the creator's
179                // set_len publishes it, so a mid-init file reads short.
180                let deadline = std::time::Instant::now() + crate::mmf_attach::INIT_WAIT;
181                loop {
182                    let actual = file.metadata()?.len();
183                    if actual == len as u64 {
184                        break;
185                    }
186                    if actual > len as u64 {
187                        return Err(io::Error::new(
188                            io::ErrorKind::InvalidData,
189                            "hugetlbfs region size does not match the requested pages",
190                        ));
191                    }
192                    if std::time::Instant::now() >= deadline {
193                        return Err(io::Error::new(
194                            io::ErrorKind::TimedOut,
195                            "the region's creator did not finish initializing it",
196                        ));
197                    }
198                    std::thread::yield_now();
199                }
200                let ptr = Self::map(&file, len)?;
201                Ok(Self { _file: file, ptr, len, path, owner: false })
202            }
203            Err(e) => Err(e),
204        }
205    }
206
207    /// Truncate the hugetlbfs file at `path` to a zeroed region of
208    /// `pages` hugepages and map it, discarding contents live peers
209    /// hold. The caller becomes the owner and unlinks the path on
210    /// drop.
211    pub fn reset(
212        path: impl AsRef<Path>,
213        pages: usize,
214        size: HugepageSize,
215    ) -> io::Result<Self> {
216        assert!(pages > 0);
217        let path = path.as_ref().to_path_buf();
218        let len = pages * size.bytes();
219        let file = std::fs::OpenOptions::new()
220            .read(true)
221            .write(true)
222            .create(true)
223            .truncate(true)
224            .open(&path)?;
225        file.set_len(len as u64)?;
226        let ptr = Self::map(&file, len)?;
227        Ok(Self { _file: file, ptr, len, path, owner: true })
228    }
229
230    /// Open an existing hugetlbfs region (a second process) and map it.
231    /// The exact byte size is the page-count record, same as attach.
232    pub fn open(
233        path: impl AsRef<Path>,
234        pages: usize,
235        size: HugepageSize,
236    ) -> io::Result<Self> {
237        let path = path.as_ref().to_path_buf();
238        let len = pages * size.bytes();
239        let file = std::fs::OpenOptions::new()
240            .read(true)
241            .write(true)
242            .open(&path)?;
243        if file.metadata()?.len() != len as u64 {
244            return Err(io::Error::new(
245                io::ErrorKind::InvalidData,
246                "hugetlbfs region size does not match the requested pages",
247            ));
248        }
249        let ptr = Self::map(&file, len)?;
250        Ok(Self { _file: file, ptr, len, path, owner: false })
251    }
252
253    pub fn as_mut_slice(&mut self) -> &mut [u8] {
254        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
255    }
256
257    pub fn as_slice(&self) -> &[u8] {
258        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
259    }
260
261    pub fn len(&self) -> usize { self.len }
262    pub fn is_empty(&self) -> bool { self.len == 0 }
263}
264
265impl Drop for SharedHugepageRegion {
266    fn drop(&mut self) {
267        unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
268        // The creator unlinks the backing file; an opener leaves it.
269        if self.owner {
270            std::fs::remove_file(&self.path).ok();
271        }
272    }
273}
274
275impl crate::spsc_ring::RegionOwner for SharedHugepageRegion {
276    fn region_ptr(&mut self) -> *mut u8 {
277        self.as_mut_slice().as_mut_ptr()
278    }
279    fn region_len(&self) -> usize {
280        self.len()
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    /// The default hugetlbfs mount on most distros. The test skips
289    /// cleanly when it is absent / unwritable / has no reserved pages,
290    /// so it is safe to run on any host; on a host with hugepages it
291    /// proves two independent maps of the same hugetlbfs file see each
292    /// other's writes (the cross-process sharing mechanism, exercised
293    /// in-process).
294    fn hugetlbfs_dir() -> Option<PathBuf> {
295        let candidate = Path::new("/dev/hugepages");
296        // A cheap writability probe: try to create + remove a temp file.
297        let probe = candidate.join(format!("subetha_probe_{}", std::process::id()));
298        match std::fs::File::create(&probe) {
299            Ok(_) => {
300                std::fs::remove_file(&probe).ok();
301                Some(candidate.to_path_buf())
302            }
303            Err(_) => None,
304        }
305    }
306
307    #[test]
308    fn shared_hugepage_region_two_maps_share_memory() {
309        let Some(dir) = hugetlbfs_dir() else {
310            eprintln!("skipping: no writable hugetlbfs mount at /dev/hugepages");
311            return;
312        };
313        let path = dir.join(format!("subetha_shr_{}", std::process::id()));
314
315        // Creating may still fail if no pages are reserved; skip if so.
316        let mut a = match SharedHugepageRegion::create(&path, 1, HugepageSize::Mb2) {
317            Ok(r) => r,
318            Err(e) => {
319                eprintln!("skipping: hugetlbfs create failed ({e}); reserve pages");
320                return;
321            }
322        };
323        assert_eq!(a.len(), HUGEPAGE_2MB);
324
325        // A second independent map of the SAME file.
326        let mut b = SharedHugepageRegion::open(&path, 1, HugepageSize::Mb2)
327            .expect("open second map of the same hugetlbfs file");
328
329        // Write through A, read through B: same physical hugepage.
330        a.as_mut_slice()[0] = 0xAB;
331        a.as_mut_slice()[HUGEPAGE_2MB - 1] = 0xCD;
332        assert_eq!(b.as_slice()[0], 0xAB, "B must see A's write at offset 0");
333        assert_eq!(
334            b.as_slice()[HUGEPAGE_2MB - 1], 0xCD,
335            "B must see A's write at the last byte",
336        );
337
338        // And the reverse direction.
339        b.as_mut_slice()[42] = 0x7E;
340        assert_eq!(a.as_slice()[42], 0x7E, "A must see B's write");
341    }
342
343    /// A second create attaches with contents in place and without
344    /// taking ownership of the path; reset is what strips them. Skips
345    /// cleanly like the test above when hugepages are unavailable.
346    #[test]
347    fn second_create_attaches_and_keeps_contents() {
348        let Some(dir) = hugetlbfs_dir() else {
349            eprintln!("skipping: no writable hugetlbfs mount at /dev/hugepages");
350            return;
351        };
352        let path = dir.join(format!("subetha_attach_{}", std::process::id()));
353
354        let mut a = match SharedHugepageRegion::create(&path, 2, HugepageSize::Mb2) {
355            Ok(r) => r,
356            Err(e) => {
357                eprintln!("skipping: hugetlbfs create failed ({e}); reserve pages");
358                return;
359            }
360        };
361        a.as_mut_slice()[7] = 0x5A;
362
363        let b = SharedHugepageRegion::create(&path, 2, HugepageSize::Mb2)
364            .expect("second create attaches");
365        assert_eq!(b.as_slice()[7], 0x5A, "attach lost region contents");
366        assert!(
367            SharedHugepageRegion::create(&path, 1, HugepageSize::Mb2).is_err(),
368            "a different page count must be refused",
369        );
370
371        // The attacher's drop must not unlink the path the creator owns.
372        drop(b);
373        assert!(path.exists(), "attacher drop unlinked the creator's path");
374
375        let fresh = SharedHugepageRegion::reset(&path, 2, HugepageSize::Mb2)
376            .expect("reset");
377        assert_eq!(fresh.as_slice()[7], 0, "reset kept region contents");
378        drop(fresh);
379        drop(a);
380    }
381}