use std::collections::VecDeque;
use std::num::NonZeroUsize;
use std::ptr::NonNull;
use nix::sys::mman::{MapFlags, ProtFlags};
use crate::error::Error;
pub(crate) struct Umem {
base: NonNull<u8>,
size: usize,
frame_size: usize,
#[allow(dead_code)]
frame_count: usize,
free_list: VecDeque<u64>,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct UmemOptions {
pub(crate) hugepages: bool,
pub(crate) numa_node: Option<u32>,
}
const HUGEPAGE_2MB: usize = 2 * 1024 * 1024;
impl Umem {
#[allow(dead_code)]
pub(crate) fn new(frame_size: usize, frame_count: usize) -> Result<Self, Error> {
Self::new_with_options(frame_size, frame_count, &UmemOptions::default())
}
pub(crate) fn new_with_options(
frame_size: usize,
frame_count: usize,
opts: &UmemOptions,
) -> Result<Self, Error> {
let size = frame_size
.checked_mul(frame_count)
.ok_or_else(|| Error::Config("umem size overflow".into()))?;
if size == 0 {
return Err(Error::Config("umem size is 0".into()));
}
let alloc_size = if opts.hugepages {
size.div_ceil(HUGEPAGE_2MB) * HUGEPAGE_2MB
} else {
size
};
let nz_size =
NonZeroUsize::new(alloc_size).ok_or_else(|| Error::Config("umem size is 0".into()))?;
let base_flags = MapFlags::MAP_PRIVATE | MapFlags::MAP_POPULATE;
let regular = |len: usize| -> Result<NonNull<libc::c_void>, Error> {
let nz = NonZeroUsize::new(len).unwrap();
unsafe {
nix::sys::mman::mmap_anonymous(
None,
nz,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
base_flags,
)
.map_err(|e| Error::Mmap(e.into()))
}
};
let (base, mapped_len) = if opts.hugepages {
match unsafe {
nix::sys::mman::mmap_anonymous(
None,
nz_size,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
base_flags | MapFlags::MAP_HUGETLB,
)
} {
Ok(b) => (b, alloc_size),
Err(e) => {
tracing::warn!(error = %e, "UMEM hugepage mmap failed (no hugepages reserved?); falling back to regular pages");
(regular(size)?, size)
}
}
} else {
(regular(size)?, size)
};
if let Some(node) = opts.numa_node {
bind_numa(base.as_ptr() as usize, mapped_len, node);
}
let free_list: VecDeque<u64> = (0..frame_count).map(|i| (i * frame_size) as u64).collect();
Ok(Self {
base: base.cast(),
size: mapped_len,
frame_size,
frame_count,
free_list,
})
}
#[inline]
pub(crate) fn alloc_frame(&mut self) -> Option<u64> {
self.free_list.pop_front()
}
#[inline]
pub(crate) fn free_frame(&mut self, addr: u64) {
self.free_list.push_back(addr);
}
#[inline]
pub(crate) fn free_frames(&mut self, addrs: &[u64]) {
self.free_list.extend(addrs);
}
#[inline]
pub(crate) fn available(&self) -> usize {
self.free_list.len()
}
#[inline]
pub(crate) fn frame_size(&self) -> usize {
self.frame_size
}
#[inline]
pub(crate) fn data_checked(&self, addr: u64, len: usize) -> Option<&[u8]> {
if len > self.frame_size {
return None;
}
let end = (addr as usize).checked_add(len)?;
if end > self.size {
return None;
}
let ptr = self.base.as_ptr().map_addr(|a| a + addr as usize);
Some(unsafe { std::slice::from_raw_parts(ptr, len) })
}
#[inline]
pub(crate) fn data_mut_checked(&mut self, addr: u64, len: usize) -> Option<&mut [u8]> {
if len > self.frame_size {
return None;
}
let end = (addr as usize).checked_add(len)?;
if end > self.size {
return None;
}
let ptr = self.base.as_ptr().map_addr(|a| a + addr as usize);
Some(unsafe { std::slice::from_raw_parts_mut(ptr, len) })
}
pub(crate) fn as_reg(&self) -> libc::xdp_umem_reg {
libc::xdp_umem_reg {
addr: self.base.as_ptr() as u64,
len: self.size as u64,
chunk_size: self.frame_size as u32,
headroom: 0,
flags: 0,
tx_metadata_len: 0,
}
}
}
fn bind_numa(addr: usize, len: usize, node: u32) {
if node >= 64 {
tracing::warn!(
node,
"NUMA node out of range for the 64-bit nodemask; skipping mbind"
);
return;
}
let nodemask: u64 = 1u64 << node;
let maxnode: libc::c_ulong = 64;
let rc = unsafe {
libc::syscall(
libc::SYS_mbind,
addr as *mut libc::c_void,
len as libc::c_ulong,
libc::MPOL_BIND as libc::c_int,
&nodemask as *const u64,
maxnode,
0 as libc::c_uint,
)
};
if rc != 0 {
let e = std::io::Error::last_os_error();
tracing::warn!(error = %e, node, "mbind(UMEM, MPOL_BIND) failed; UMEM not NUMA-bound");
}
}
impl Drop for Umem {
fn drop(&mut self) {
let _ = unsafe { nix::sys::mman::munmap(self.base.cast(), self.size) };
}
}
unsafe impl Send for Umem {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_creates_frames() {
let umem = Umem::new(4096, 16).unwrap();
assert_eq!(umem.available(), 16);
assert_eq!(umem.frame_size(), 4096);
}
#[test]
fn hugepage_request_falls_back_when_unavailable() {
let opts = UmemOptions {
hugepages: true,
numa_node: None,
};
let umem = Umem::new_with_options(4096, 64, &opts).expect("hugepage UMEM or fallback");
assert_eq!(umem.available(), 64);
assert_eq!(umem.frame_size(), 4096);
}
#[test]
fn numa_bind_is_best_effort() {
let opts = UmemOptions {
hugepages: false,
numa_node: Some(0),
};
let umem = Umem::new_with_options(4096, 16, &opts).expect("numa-bound UMEM or warn");
assert_eq!(umem.available(), 16);
}
#[test]
fn alloc_returns_sequential_offsets() {
let mut umem = Umem::new(4096, 4).unwrap();
assert_eq!(umem.alloc_frame(), Some(0));
assert_eq!(umem.alloc_frame(), Some(4096));
assert_eq!(umem.alloc_frame(), Some(8192));
assert_eq!(umem.alloc_frame(), Some(12288));
}
#[test]
fn exhaustion_returns_none() {
let mut umem = Umem::new(4096, 2).unwrap();
assert!(umem.alloc_frame().is_some());
assert!(umem.alloc_frame().is_some());
assert_eq!(umem.alloc_frame(), None);
}
#[test]
fn free_recycles_fifo() {
let mut umem = Umem::new(4096, 2).unwrap();
let a = umem.alloc_frame().unwrap();
let b = umem.alloc_frame().unwrap();
assert_eq!(umem.alloc_frame(), None);
umem.free_frame(a);
umem.free_frame(b);
assert_eq!(umem.available(), 2);
assert_eq!(umem.alloc_frame(), Some(a));
assert_eq!(umem.alloc_frame(), Some(b));
}
#[test]
fn as_reg_correct() {
let umem = Umem::new(4096, 16).unwrap();
let reg = umem.as_reg();
assert_eq!(reg.len, (4096 * 16) as u64);
assert_eq!(reg.chunk_size, 4096);
assert_eq!(reg.headroom, 0);
assert_eq!(reg.flags, 0);
assert_eq!(reg.tx_metadata_len, 0);
assert_ne!(reg.addr, 0);
}
#[test]
fn data_read_write() {
let mut umem = Umem::new(4096, 2).unwrap();
let addr = umem.alloc_frame().unwrap();
let buf = umem.data_mut_checked(addr, 4).unwrap();
buf.copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let data = umem.data_checked(addr, 4).unwrap();
assert_eq!(data, &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn data_checked_rejects_oversize_len() {
let umem = Umem::new(4096, 4).unwrap();
assert!(umem.data_checked(0, 4096).is_some());
assert!(umem.data_checked(0, 4097).is_none());
}
#[test]
fn data_checked_rejects_past_umem_end() {
let umem = Umem::new(4096, 4).unwrap(); assert!(umem.data_checked(16384 - 200, 200).is_some());
assert!(umem.data_checked(16384 - 100, 200).is_none());
}
#[test]
fn data_checked_rejects_overflow_addr() {
let umem = Umem::new(4096, 4).unwrap();
assert!(umem.data_checked(u64::MAX, 1).is_none());
}
#[test]
fn zero_size_rejected() {
assert!(Umem::new(0, 16).is_err());
assert!(Umem::new(4096, 0).is_err());
}
#[test]
fn overflow_rejected() {
let result = Umem::new(usize::MAX, 2);
assert!(result.is_err());
}
}