use std::os::fd::{AsRawFd, OwnedFd, RawFd};
#[derive(Default)]
struct Slot {
held: bool,
entries: u32,
own: Option<RawFd>,
}
pub(crate) struct Files {
table: Vec<Slot>,
copies: Vec<Slot>,
fixed: u32,
free: Vec<u32>,
spare: Vec<u32>,
}
impl Files {
pub(crate) fn new(fixed: u32) -> Self {
Self {
table: Vec::new(),
copies: Vec::new(),
fixed,
free: Vec::new(),
spare: Vec::new(),
}
}
fn slot(&self, slot: u32) -> Option<&Slot> {
match slot.checked_sub(self.fixed) {
None => self.table.get(slot as usize),
Some(copy) => self.copies.get(copy as usize),
}
}
fn slot_mut(&mut self, slot: u32) -> Option<&mut Slot> {
match slot.checked_sub(self.fixed) {
None => self.table.get_mut(slot as usize),
Some(copy) => self.copies.get_mut(copy as usize),
}
}
pub(crate) fn take(&mut self) -> Option<u32> {
let slot = match self.free.pop() {
Some(slot) => slot,
None if self.table.len() < self.fixed as usize => {
self.table.push(Slot::default());
(self.table.len() - 1) as u32
}
None => return None,
};
self.table[slot as usize].held = true;
Some(slot)
}
pub(crate) fn take_own(&mut self, fd: RawFd) -> u32 {
let slot = self.spare.pop().unwrap_or_else(|| {
self.copies.push(Slot::default());
self.fixed + (self.copies.len() - 1) as u32
});
if let Some(s) = self.slot_mut(slot) {
s.held = true;
s.own = Some(fd);
}
slot
}
pub(crate) fn is_fixed(&self, slot: u32) -> bool {
slot < self.fixed
}
pub(crate) fn number_of(&self, slot: u32) -> Option<RawFd> {
self.slot(slot).and_then(|s| s.own)
}
pub(crate) fn is_named(&self, slot: u32) -> bool {
self.slot(slot).is_some_and(|s| s.entries > 0)
}
#[cfg(test)]
pub(crate) fn named(&mut self, slot: u32) {
self.name(slot);
}
#[inline]
pub(crate) fn name(&mut self, slot: u32) -> Option<RawFd> {
let s = self.slot_mut(slot)?;
s.entries += 1;
s.own
}
pub(crate) fn done(&mut self, slot: u32) -> bool {
match self.slot_mut(slot) {
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.slot_mut(slot) {
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 {
if let Some(s) = self.slot_mut(slot) {
s.own = None;
}
self.spare.push(slot);
}
true
}
#[cfg(test)]
pub(crate) fn in_use(&self) -> usize {
self.table.len() + self.copies.len() - self.free.len() - self.spare.len()
}
#[cfg(test)]
pub(crate) fn own_in_use(&self) -> usize {
self.copies.len() - 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_slot_names_the_sources_own_descriptor_until_its_last_entry() {
let mut files = Files::new(1);
let fixed = files.take().unwrap();
assert!(files.take().is_none());
assert_eq!(files.number_of(fixed), None);
let own = descriptor();
let number = own.as_raw_fd();
let a = files.take_own(number);
assert!(!files.is_fixed(a));
assert_eq!(files.number_of(a), Some(number));
files.named(a);
assert!(!files.release(a));
assert!(files.is_named(a));
let other = descriptor();
let b = files.take_own(other.as_raw_fd());
assert_ne!(a, b);
assert!(files.done(a));
assert_eq!(files.number_of(a), None);
assert_eq!(files.in_use(), 2);
let c = files.take_own(number);
assert_eq!(a, c);
assert!(files.release(c));
assert!(files.release(b));
drop((own, other));
}
}