use std::os::fd::{AsRawFd, OwnedFd, RawFd};
#[derive(Default)]
struct Slot {
held: bool,
entries: u32,
copy: Option<OwnedFd>,
}
pub(crate) struct Files {
slots: Vec<Slot>,
fixed: u32,
free: Vec<u32>,
spare: Vec<u32>,
}
impl Files {
pub(crate) fn new(fixed: u32) -> Self {
Self {
slots: (0..fixed).map(|_| Slot::default()).collect(),
fixed,
free: (0..fixed).rev().collect(),
spare: Vec::new(),
}
}
pub(crate) fn take(&mut self) -> Option<u32> {
let slot = self.free.pop()?;
self.slots[slot as usize].held = true;
Some(slot)
}
pub(crate) fn take_copy(&mut self, copy: OwnedFd) -> u32 {
let slot = self.spare.pop().unwrap_or_else(|| {
self.slots.push(Slot::default());
(self.slots.len() - 1) as u32
});
let s = &mut self.slots[slot as usize];
s.held = true;
s.copy = Some(copy);
slot
}
pub(crate) fn is_fixed(&self, slot: u32) -> bool {
slot < self.fixed
}
pub(crate) fn copy_of(&self, slot: u32) -> Option<RawFd> {
self.slots
.get(slot as usize)
.and_then(|s| s.copy.as_ref())
.map(AsRawFd::as_raw_fd)
}
pub(crate) fn named(&mut self, slot: u32) {
if let Some(s) = self.slots.get_mut(slot as usize) {
s.entries += 1;
}
}
pub(crate) fn done(&mut self, slot: u32) -> bool {
match self.slots.get_mut(slot as usize) {
Some(s) if s.entries > 0 => {
s.entries -= 1;
!s.held && s.entries == 0 && self.free_slot(slot)
}
_ => false,
}
}
pub(crate) fn release(&mut self, slot: u32) -> bool {
match self.slots.get_mut(slot as usize) {
Some(s) if s.held => {
s.held = false;
s.entries == 0 && self.free_slot(slot)
}
_ => false,
}
}
fn free_slot(&mut self, slot: u32) -> bool {
if self.is_fixed(slot) {
self.free.push(slot);
} else {
self.slots[slot as usize].copy = None;
self.spare.push(slot);
}
true
}
#[cfg(test)]
pub(crate) fn in_use(&self) -> usize {
self.slots.len() - self.free.len() - self.spare.len()
}
#[cfg(test)]
pub(crate) fn copies_in_use(&self) -> usize {
self.slots.len() - self.fixed as usize - self.spare.len()
}
}
pub(crate) fn release_at_once() -> bool {
static AT_ONCE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*AT_ONCE.get_or_init(|| trial().unwrap_or(false))
}
fn trial() -> std::io::Result<bool> {
use std::os::fd::FromRawFd;
use super::ring::{Ring, RingConfig, Sqe, sys::IOSQE_FIXED_FILE};
let pipe = || -> std::io::Result<(OwnedFd, OwnedFd)> {
let mut fds = [-1; 2];
if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
};
let (watched_read, watched_write) = pipe()?;
let (busy_read, busy_write) = pipe()?;
let mut ring = Ring::new(&RingConfig::new(4))?;
ring.register_files_sparse(2)?;
ring.update_file(0, watched_write.as_raw_fd())?;
ring.update_file(1, busy_read.as_raw_fd())?;
let mut byte = [0u8; 1];
let read = Sqe::read(1, byte.as_mut_ptr(), 1, u64::MAX)
.flags(IOSQE_FIXED_FILE)
.user_data(1);
ring.push(&read)
.map_err(|_| std::io::Error::other("no room in a new ring"))?;
ring.submit()?;
ring.update_file(0, -1)?;
drop(watched_write);
ring.submit()?;
let mut poll = libc::pollfd {
fd: watched_read.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let polled = unsafe { libc::poll(&mut poll, 1, 0) };
let released = polled == 1 && poll.revents & libc::POLLHUP != 0;
unsafe { libc::write(busy_write.as_raw_fd(), [0u8].as_ptr().cast(), 1) };
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
ring.submit_and_wait(1, Some(std::time::Duration::from_millis(100)))?;
if ring.completions().any(|cqe| cqe.user_data == 1) {
break;
}
if std::time::Instant::now() >= deadline {
core::mem::forget(ring);
return Ok(false);
}
}
Ok(released)
}
#[cfg(test)]
mod tests {
use std::os::fd::{AsRawFd, OwnedFd};
use super::Files;
fn descriptor() -> OwnedFd {
std::fs::File::open("/dev/null").unwrap().into()
}
#[test]
fn the_release_trial_answers_as_the_kernel_behaves() {
let mut name: libc::utsname = unsafe { core::mem::zeroed() };
assert_eq!(unsafe { libc::uname(&mut name) }, 0);
let release = unsafe { core::ffi::CStr::from_ptr(name.release.as_ptr()) }
.to_string_lossy()
.into_owned();
let mut parts = release.split(|c: char| !c.is_ascii_digit());
let major: u32 = parts.next().and_then(|p| p.parse().ok()).unwrap();
let minor: u32 = parts.next().and_then(|p| p.parse().ok()).unwrap();
if super::super::ring::Ring::new(&super::super::ring::RingConfig::new(4)).is_err() {
eprintln!("skipping: io_uring unavailable");
return;
}
assert_eq!(
super::release_at_once(),
(major, minor) >= (6, 13),
"kernel {release}"
);
}
#[test]
fn a_released_slot_waits_for_its_last_entry() {
let mut files = Files::new(2);
let a = files.take().unwrap();
files.named(a);
files.named(a);
assert!(!files.release(a));
assert!(!files.done(a));
let b = files.take().unwrap();
assert_ne!(a, b);
assert!(files.take().is_none(), "a still-named slot was handed out");
assert!(files.done(a), "the last entry frees the slot");
assert_eq!(files.take(), Some(a));
}
#[test]
fn a_slot_with_no_entries_is_free_at_its_release() {
let mut files = Files::new(1);
let a = files.take().unwrap();
assert_eq!(files.in_use(), 1);
assert!(files.release(a));
assert_eq!(files.in_use(), 0);
assert_eq!(files.take(), Some(a));
}
#[test]
fn past_a_full_table_a_copy_is_named_until_its_last_entry() {
let mut files = Files::new(1);
let fixed = files.take().unwrap();
assert!(files.take().is_none());
assert_eq!(files.copy_of(fixed), None);
let copy = descriptor();
let number = copy.as_raw_fd();
let a = files.take_copy(copy);
assert!(!files.is_fixed(a));
assert_eq!(files.copy_of(a), Some(number));
files.named(a);
assert!(!files.release(a));
assert!(unsafe { libc::fcntl(number, libc::F_GETFD) } >= 0);
assert!(files.done(a));
assert_eq!(files.copy_of(a), None);
assert_eq!(files.in_use(), 1);
let b = files.take_copy(descriptor());
assert_eq!(a, b);
assert!(files.release(b));
}
}