#[cfg(any(feature = "jpeg", feature = "rknpu", feature = "rga"))]
pub mod dmabuf;
pub mod epoll;
#[cfg(test)]
mod epoll_axtest;
mod epoll_file;
mod epoll_topology;
pub mod event;
mod fs;
pub mod inotify;
pub mod io_uring;
#[cfg(feature = "sg2002")]
pub mod ion;
pub mod memfd;
mod mount_table;
mod net;
pub mod netlink;
mod nsfd;
mod packet;
mod pidfd;
mod pipe;
pub mod signalfd;
pub mod timerfd;
mod wext;
use alloc::{
borrow::Cow,
sync::{Arc, Weak},
};
use core::{
cell::UnsafeCell,
ffi::c_int,
ops::{Deref, DerefMut},
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use ax_fs_ng::vfs::{FileBackend, FileFlags, OpenOptions, current_fs_context};
use ax_io::prelude::*;
use ax_std::os::arceos::task::thread::ThreadState;
use axfs_ng_vfs::DeviceId;
use axpoll::Pollable;
use downcast_rs::{DowncastSync, impl_downcast};
use flatten_objects::FlattenObjects;
use linux_raw_sys::general::{
O_ACCMODE, O_PATH, O_RDONLY, O_RDWR, O_WRONLY, RLIMIT_NOFILE, STATX_ATTR_MOUNT_ROOT,
STATX_BASIC_STATS, stat, statx, statx_timestamp,
};
pub(crate) use self::mount_table::{MountTableFile, notify_mount_namespace_changed};
#[cfg(feature = "qperf-metrics")]
pub(crate) use self::pipe::qperf_metrics_snapshot as pipe_qperf_metrics_snapshot;
pub use self::{
fs::{
Directory, File, ResolveAtResult, metadata_to_kstat, resolve_at, resolve_at_checked,
resolve_fd, with_fs,
},
io_uring::IoUring,
net::Socket,
nsfd::NsFd,
packet::{PacketSocket, SockAddrLl},
pidfd::PidFd,
pipe::Pipe,
};
use crate::{
StarryError, StarryResult,
pseudofs::DeviceMmap,
sync::RwLock,
task::{AX_FILE_LIMIT, PidIdentityId, current_user_task, tasks},
};
#[derive(Debug, Clone, Copy)]
pub struct Kstat {
pub dev: u64,
pub ino: u64,
pub nlink: u32,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub blksize: u32,
pub blocks: u64,
pub rdev: DeviceId,
pub atime: Duration,
pub mtime: Duration,
pub ctime: Duration,
}
impl Default for Kstat {
fn default() -> Self {
Self {
dev: 0,
ino: 1,
nlink: 1,
mode: 0,
uid: 1,
gid: 1,
size: 0,
blksize: 4096,
blocks: 0,
rdev: DeviceId::default(),
atime: Duration::default(),
mtime: Duration::default(),
ctime: Duration::default(),
}
}
}
impl From<Kstat> for stat {
fn from(value: Kstat) -> Self {
let mut stat: stat = unsafe { core::mem::zeroed() };
stat.st_dev = value.dev as _;
stat.st_ino = value.ino as _;
stat.st_nlink = value.nlink as _;
stat.st_mode = value.mode as _;
stat.st_uid = value.uid as _;
stat.st_gid = value.gid as _;
stat.st_size = value.size as _;
stat.st_blksize = value.blksize as _;
stat.st_blocks = value.blocks as _;
stat.st_rdev = value.rdev.0 as _;
stat.st_atime = value.atime.as_secs() as _;
stat.st_atime_nsec = value.atime.subsec_nanos() as _;
stat.st_mtime = value.mtime.as_secs() as _;
stat.st_mtime_nsec = value.mtime.subsec_nanos() as _;
stat.st_ctime = value.ctime.as_secs() as _;
stat.st_ctime_nsec = value.ctime.subsec_nanos() as _;
stat
}
}
impl From<Kstat> for statx {
fn from(value: Kstat) -> Self {
let mut statx: statx = unsafe { core::mem::zeroed() };
statx.stx_mask = STATX_BASIC_STATS;
statx.stx_attributes_mask = STATX_ATTR_MOUNT_ROOT as u64;
statx.stx_blksize = value.blksize as _;
statx.stx_nlink = value.nlink as _;
statx.stx_uid = value.uid as _;
statx.stx_gid = value.gid as _;
statx.stx_mode = value.mode as _;
statx.stx_ino = value.ino as _;
statx.stx_size = value.size as _;
statx.stx_blocks = value.blocks as _;
statx.stx_rdev_major = value.rdev.major();
statx.stx_rdev_minor = value.rdev.minor();
fn time_to_statx(time: &Duration) -> statx_timestamp {
statx_timestamp {
tv_sec: time.as_secs() as _,
tv_nsec: time.subsec_nanos() as _,
__reserved: 0,
}
}
statx.stx_atime = time_to_statx(&value.atime);
statx.stx_ctime = time_to_statx(&value.ctime);
statx.stx_mtime = time_to_statx(&value.mtime);
statx.stx_dev_major = (value.dev >> 32) as _;
statx.stx_dev_minor = value.dev as _;
statx
}
}
pub trait WriteBuf: Write + IoBufMut {}
impl<T: Write + IoBufMut> WriteBuf for T {}
pub type IoDst<'a> = dyn WriteBuf + 'a;
pub trait ReadBuf: Read + IoBuf {}
impl<T: Read + IoBuf> ReadBuf for T {}
pub type IoSrc<'a> = dyn ReadBuf + 'a;
#[allow(dead_code)]
pub trait FileLike: Pollable + DowncastSync {
fn supports_epoll(&self) -> bool {
true
}
fn validate_write_len(&self, _len: usize) -> StarryResult {
Ok(())
}
fn read(&self, _dst: &mut IoDst) -> StarryResult<usize> {
Err(StarryError::InvalidInput)
}
fn write(&self, _src: &mut IoSrc) -> StarryResult<usize> {
Err(StarryError::InvalidInput)
}
fn stat(&self) -> StarryResult<Kstat> {
Ok(Kstat::default())
}
fn path(&self) -> Cow<'_, str>;
fn file_mmap(&self) -> StarryResult<(FileBackend, FileFlags)> {
Err(StarryError::NoSuchDevice)
}
fn device_mmap(&self, _offset: u64, _length: u64) -> StarryResult<DeviceMmap> {
Ok(DeviceMmap::None)
}
fn ioctl(
&self,
_current: &crate::task::UserTaskRef,
_cmd: u32,
_arg: usize,
) -> StarryResult<usize> {
Err(StarryError::NotATty)
}
fn open_flags(&self) -> u32 {
0
}
fn nonblocking(&self) -> bool {
false
}
fn set_nonblocking(&self, _nonblocking: bool) -> StarryResult {
Ok(())
}
fn async_mode(&self) -> bool {
false
}
fn supports_async_mode(&self) -> bool {
false
}
fn set_async_mode(&self, _async_mode: bool) -> StarryResult {
Err(StarryError::NotATty)
}
fn owner(&self) -> StarryResult<i32> {
Err(StarryError::NotATty)
}
fn set_owner(&self, _owner: i32) -> StarryResult {
Err(StarryError::NotATty)
}
fn inode_key(&self) -> Option<(u64, u64)> {
None
}
fn append(&self) -> bool {
false
}
fn set_append(&self, _append: bool) -> StarryResult {
Ok(())
}
fn on_close(&self, _owner: PidIdentityId) {}
fn from_fd(fd: c_int) -> StarryResult<Arc<Self>>
where
Self: Sized + 'static,
{
get_file_like(fd)?
.downcast_arc()
.map_err(|_| StarryError::InvalidInput)
}
fn add_to_fd_table(self, cloexec: bool) -> StarryResult<c_int>
where
Self: Sized + 'static,
{
add_file_like(Arc::new(self), cloexec)
}
}
impl_downcast!(sync FileLike);
#[derive(Clone)]
pub struct FileDescriptor {
pub inner: Arc<dyn FileLike>,
pub cloexec: bool,
}
enum FileSlot {
Reserved,
Installed(FileDescriptor),
}
impl FileSlot {
fn into_descriptor(self) -> Option<FileDescriptor> {
match self {
Self::Installed(descriptor) => Some(descriptor),
Self::Reserved => None,
}
}
}
pub struct FileTable {
entries: FlattenObjects<FileSlot, AX_FILE_LIMIT>,
generation: Arc<AtomicUsize>,
}
impl FileTable {
pub fn new() -> Self {
Self {
entries: FlattenObjects::new(),
generation: Arc::new(AtomicUsize::new(0)),
}
}
#[inline]
fn generation(&self) -> Arc<AtomicUsize> {
Arc::clone(&self.generation)
}
#[inline]
fn changed(&self) {
self.generation.fetch_add(1, Ordering::Release);
}
pub fn count(&self) -> usize {
self.entries.count()
}
pub fn get(&self, fd: usize) -> Option<&FileDescriptor> {
match self.entries.get(fd)? {
FileSlot::Installed(descriptor) => Some(descriptor),
FileSlot::Reserved => None,
}
}
pub fn add(&mut self, descriptor: FileDescriptor) -> Result<usize, FileDescriptor> {
let result = self
.entries
.add(FileSlot::Installed(descriptor))
.map_err(|slot| {
slot.into_descriptor()
.expect("inserting an installed descriptor")
});
if result.is_ok() {
self.changed();
}
result
}
pub fn add_at(
&mut self,
fd: usize,
descriptor: FileDescriptor,
) -> Result<usize, FileDescriptor> {
let result = self
.entries
.add_at(fd, FileSlot::Installed(descriptor))
.map_err(|slot| {
slot.into_descriptor()
.expect("inserting an installed descriptor")
});
if result.is_ok() {
self.changed();
}
result
}
pub fn remove(&mut self, fd: usize) -> Option<FileDescriptor> {
self.get(fd)?;
let descriptor = self.entries.remove(fd)?.into_descriptor();
self.changed();
descriptor
}
pub fn ids(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
self.entries.ids().filter(|fd| self.get(*fd).is_some())
}
pub fn last_id(&self) -> Option<usize> {
self.ids().next_back()
}
pub(crate) fn is_reserved(&self, fd: usize) -> bool {
matches!(self.entries.get(fd), Some(FileSlot::Reserved))
}
pub(crate) fn set_cloexec(&mut self, fd: usize, cloexec: bool) -> StarryResult {
let Some(FileSlot::Installed(descriptor)) = self.entries.get_mut(fd) else {
return Err(StarryError::BadFileDescriptor);
};
if descriptor.cloexec != cloexec {
descriptor.cloexec = cloexec;
self.changed();
}
Ok(())
}
fn reserve(&mut self) -> Option<usize> {
let fd = self.entries.add(FileSlot::Reserved).ok()?;
self.changed();
Some(fd)
}
fn install_reserved(
&mut self,
fd: usize,
descriptor: FileDescriptor,
) -> Result<(), FileDescriptor> {
let Some(slot @ FileSlot::Reserved) = self.entries.get_mut(fd) else {
return Err(descriptor);
};
*slot = FileSlot::Installed(descriptor);
self.changed();
Ok(())
}
fn release_reserved(&mut self, fd: usize) {
assert!(
self.is_reserved(fd),
"releasing an unreserved file descriptor"
);
self.entries.remove(fd);
self.changed();
}
}
impl Clone for FileTable {
fn clone(&self) -> Self {
let mut entries = FlattenObjects::new();
for fd in self.ids() {
let descriptor = self.get(fd).expect("installed file iterator").clone();
assert!(entries.add_at(fd, FileSlot::Installed(descriptor)).is_ok());
}
Self {
entries,
generation: Arc::new(AtomicUsize::new(self.generation.load(Ordering::Acquire))),
}
}
}
impl Default for FileTable {
fn default() -> Self {
Self::new()
}
}
const FILE_LOOKUP_CACHE_SLOTS: usize = 2;
struct FileLookupCacheEntry {
table: usize,
fd: c_int,
generation: usize,
file: Option<Weak<dyn FileLike>>,
}
impl FileLookupCacheEntry {
const fn new() -> Self {
Self {
table: 0,
fd: -1,
generation: 0,
file: None,
}
}
}
struct FileLookupCache {
entries: [FileLookupCacheEntry; FILE_LOOKUP_CACHE_SLOTS],
}
impl FileLookupCache {
const fn new() -> Self {
Self {
entries: [const { FileLookupCacheEntry::new() }; FILE_LOOKUP_CACHE_SLOTS],
}
}
}
pub(crate) struct FileTableScope {
table: Arc<RwLock<FileTable>>,
generation: Arc<AtomicUsize>,
cache: UnsafeCell<FileLookupCache>,
}
unsafe impl Sync for FileTableScope {}
unsafe impl Send for FileTableScope {}
impl FileTableScope {
fn new() -> Self {
let table = Arc::new(RwLock::new(FileTable::new()));
let generation = table.read().generation();
Self {
table,
generation,
cache: UnsafeCell::new(FileLookupCache::new()),
}
}
fn from_table(table: Arc<RwLock<FileTable>>) -> Self {
let generation = table.read().generation();
Self {
table,
generation,
cache: UnsafeCell::new(FileLookupCache::new()),
}
}
fn lookup(&self, fd: c_int) -> StarryResult<Arc<dyn FileLike>> {
let table_key = Arc::as_ptr(&self.table) as usize;
let generation = self.generation.load(Ordering::Acquire);
let cache = unsafe { &mut *self.cache.get() };
for entry in &cache.entries {
if entry.table == table_key
&& entry.fd == fd
&& entry.generation == generation
&& let Some(file) = entry.file.as_ref().and_then(Weak::upgrade)
{
return Ok(file);
}
}
#[cfg(all(test, axtest))]
FD_TABLE_LOOKUP_READ_LOCKS.fetch_add(1, Ordering::Relaxed);
let table = unsafe { self.table.read_raw() };
let file = table
.get(fd as usize)
.map(|fd| Arc::clone(&fd.inner))
.ok_or(StarryError::BadFileDescriptor)?;
let generation = self.generation.load(Ordering::Acquire);
cache.entries.rotate_right(1);
cache.entries[0] = FileLookupCacheEntry {
table: table_key,
fd,
generation,
file: Some(Arc::downgrade(&file)),
};
Ok(file)
}
}
impl Clone for FileTableScope {
fn clone(&self) -> Self {
Self {
table: Arc::clone(&self.table),
generation: Arc::clone(&self.generation),
cache: UnsafeCell::new(FileLookupCache::new()),
}
}
}
pub(crate) fn new_file_table_scope(table: Arc<RwLock<FileTable>>) -> FileTableScope {
FileTableScope::from_table(table)
}
pub(crate) fn clone_file_table_scope(table: &Arc<RwLock<FileTable>>) -> FileTableScope {
FileTableScope::from_table(Arc::new(RwLock::new(table.read().clone())))
}
impl Deref for FileTableScope {
type Target = Arc<RwLock<FileTable>>;
fn deref(&self) -> &Self::Target {
&self.table
}
}
impl DerefMut for FileTableScope {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.table
}
}
scope_local::scope_local! {
pub static FD_TABLE: FileTableScope = FileTableScope::new();
}
pub fn current_fd_table() -> Arc<RwLock<FileTable>> {
FD_TABLE.clone_current().table
}
#[cfg(all(test, axtest))]
static FD_TABLE_LOOKUP_READ_LOCKS: AtomicUsize = AtomicUsize::new(0);
pub struct PreparedFileDescriptor {
table: Arc<RwLock<FileTable>>,
fd: usize,
state: DescriptorPreparation,
}
enum DescriptorPreparation {
Reserved,
Ready(FileDescriptor),
Installed,
}
impl PreparedFileDescriptor {
fn prepare_in(
table: Arc<RwLock<FileTable>>,
create: impl FnOnce() -> StarryResult<FileDescriptor>,
max_entries: usize,
) -> StarryResult<Self> {
let mut prepared = Self::reserve_in(table, max_entries)?;
prepared.state = DescriptorPreparation::Ready(create()?);
Ok(prepared)
}
fn reserve_in(table: Arc<RwLock<FileTable>>, max_entries: usize) -> StarryResult<Self> {
let fd = {
let mut table = table.write();
if table.count() >= max_entries {
return Err(StarryError::TooManyOpenFiles);
}
table.reserve().ok_or(StarryError::TooManyOpenFiles)?
};
Ok(Self {
table,
fd,
state: DescriptorPreparation::Reserved,
})
}
pub const fn fd(&self) -> c_int {
self.fd as c_int
}
pub fn install(mut self) {
let DescriptorPreparation::Ready(descriptor) =
core::mem::replace(&mut self.state, DescriptorPreparation::Installed)
else {
panic!("prepared descriptor installed without a file");
};
let install = self.table.write().install_reserved(self.fd, descriptor);
if let Err(descriptor) = install {
drop(descriptor);
panic!("prepared file descriptor lost its reservation before install");
}
}
}
impl Drop for PreparedFileDescriptor {
fn drop(&mut self) {
let state = core::mem::replace(&mut self.state, DescriptorPreparation::Installed);
if !matches!(state, DescriptorPreparation::Installed) {
self.table.write().release_reserved(self.fd);
}
drop(state);
}
}
pub fn prepare_file_like(
create: impl FnOnce() -> StarryResult<Arc<dyn FileLike>>,
cloexec: bool,
) -> StarryResult<PreparedFileDescriptor> {
let max_nofile = current_user_task()
.as_thread()
.proc_data
.rlimit_current(RLIMIT_NOFILE);
let table = current_fd_table();
PreparedFileDescriptor::prepare_in(
table,
|| {
Ok(FileDescriptor {
inner: create()?,
cloexec,
})
},
max_nofile as usize,
)
}
pub(crate) fn prepare_file_pair(
copy_out: impl FnOnce([c_int; 2]) -> StarryResult<()>,
create: impl FnOnce() -> StarryResult<[Arc<dyn FileLike>; 2]>,
cloexec: bool,
) -> StarryResult<[PreparedFileDescriptor; 2]> {
let max_nofile = current_user_task()
.as_thread()
.proc_data
.rlimit_current(RLIMIT_NOFILE) as usize;
let table = current_fd_table();
let mut first = PreparedFileDescriptor::reserve_in(table.clone(), max_nofile)?;
let mut second = PreparedFileDescriptor::reserve_in(table, max_nofile)?;
copy_out([first.fd(), second.fd()])?;
let [first_file, second_file] = create()?;
first.state = DescriptorPreparation::Ready(FileDescriptor {
inner: first_file,
cloexec,
});
second.state = DescriptorPreparation::Ready(FileDescriptor {
inner: second_file,
cloexec,
});
Ok([first, second])
}
pub fn get_file_like(fd: c_int) -> StarryResult<Arc<dyn FileLike>> {
FD_TABLE.with(|fd_table| fd_table.lookup(fd))
}
pub fn fd_is_path(fd: c_int) -> bool {
get_file_like(fd)
.map(|f| f.open_flags() & O_PATH != 0)
.unwrap_or(false)
}
pub fn add_file_like(f: Arc<dyn FileLike>, cloexec: bool) -> crate::StarryResult<c_int> {
let max_nofile = current_user_task()
.as_thread()
.proc_data
.rlimit_current(RLIMIT_NOFILE);
let fd_table = current_fd_table();
let mut table = fd_table.write();
if table.count() as u64 >= max_nofile {
return Err(StarryError::TooManyOpenFiles);
}
let fd = FileDescriptor { inner: f, cloexec };
Ok(table.add(fd).map_err(|_| StarryError::TooManyOpenFiles)? as c_int)
}
pub fn close_file_like(fd: c_int) -> StarryResult {
let removed = current_fd_table().write().remove(fd as usize);
if let Some(f) = removed {
debug!("close_file_like <= count: {}", Arc::strong_count(&f.inner));
release_locks_on_close(f);
return Ok(());
}
Err(StarryError::BadFileDescriptor)
}
fn fd_tables_contain_file(file: &Arc<dyn FileLike>) -> bool {
tasks().into_iter().any(|task| {
if task.state() == ThreadState::Exited {
return false;
}
let thread = task.as_thread();
let scoped_fd_table = thread.clone_scope_item(&FD_TABLE);
let table = scoped_fd_table.read();
table
.ids()
.any(|id| table.get(id).is_some_and(|fd| Arc::ptr_eq(&fd.inner, file)))
})
}
fn notify_close_write(fd: &FileDescriptor) {
let access = fd.inner.open_flags() & O_ACCMODE;
if (access == O_WRONLY || access == O_RDWR) && fd.inner.is::<File>() {
let path = fd.inner.path();
inotify::notify_close_write_path(path.as_ref());
}
}
pub fn release_locks_on_close(fd: FileDescriptor) {
let key = fd.inner.inode_key();
let owner = current_user_task().as_thread().proc_data.identity().id();
fd.inner.on_close(owner);
notify_close_write(&fd);
if let Some(k) = key {
crate::syscall::release_inode_posix_locks(owner, k);
if !fd_tables_contain_file(&fd.inner) {
crate::syscall::release_flock_lock(k, &fd.inner);
}
}
drop(fd);
if let Some(k) = key {
crate::syscall::wake_lock_waiters(k);
crate::syscall::wake_flock_waiters(k);
}
}
pub fn close_all_fds() {
let fd_table = current_fd_table();
let mut table = fd_table.write();
if Arc::strong_count(&fd_table) > 2 {
return;
}
let ids: alloc::vec::Vec<usize> = table.ids().collect();
let mut removed = alloc::vec::Vec::with_capacity(ids.len());
for id in ids {
match table.remove(id) {
Some(fd) => removed.push(fd),
None => warn!("close_all_fds: fd {id} disappeared during close sweep"),
}
}
drop(table);
for fd in removed {
release_locks_on_close(fd);
}
}
pub fn add_stdio(fd_table: &mut FileTable) -> StarryResult<()> {
assert_eq!(fd_table.count(), 0);
let fs_context = current_fs_context();
let cx = fs_context.lock();
let open = |options: &mut OpenOptions, flags| {
StarryResult::Ok(Arc::new(File::new(
options.open(&cx, "/dev/console")?.into_file()?,
flags,
)))
};
let tty_in = open(OpenOptions::new().read(true).write(false), O_RDONLY as _)?;
let tty_out = open(OpenOptions::new().read(false).write(true), O_WRONLY as _)?;
fd_table
.add(FileDescriptor {
inner: tty_in,
cloexec: false,
})
.map_err(|_| StarryError::TooManyOpenFiles)?;
fd_table
.add(FileDescriptor {
inner: tty_out.clone(),
cloexec: false,
})
.map_err(|_| StarryError::TooManyOpenFiles)?;
fd_table
.add(FileDescriptor {
inner: tty_out,
cloexec: false,
})
.map_err(|_| StarryError::TooManyOpenFiles)?;
Ok(())
}
#[cfg(all(test, axtest))]
fn prepared_descriptor_stays_hidden_until_install_for_test() -> bool {
fn descriptor() -> FileDescriptor {
let (read_end, _write_end) = Pipe::new();
FileDescriptor {
inner: Arc::new(read_end),
cloexec: true,
}
}
let table = Arc::new(RwLock::new(FileTable::new()));
let prepared =
PreparedFileDescriptor::prepare_in(table.clone(), || Ok(descriptor()), AX_FILE_LIMIT)
.unwrap();
let reserved_fd = prepared.fd;
let hidden = table.read().get(reserved_fd).is_none();
let counted_against_limit =
PreparedFileDescriptor::prepare_in(table.clone(), || Ok(descriptor()), 1).is_err();
let installed_descriptor = descriptor();
let Ok(installed_fd) = table.write().add(installed_descriptor) else {
return false;
};
let allocation_skipped_reservation = installed_fd != reserved_fd;
let cloned = table.read().clone();
let clone_excluded_reservation = cloned.get(reserved_fd).is_none()
&& cloned.get(installed_fd).is_some()
&& cloned.count() == 1;
drop(prepared);
let reused_descriptor = descriptor();
let Ok(reused_fd) = table.write().add(reused_descriptor) else {
return false;
};
let rollback_released_number = reused_fd == reserved_fd;
let install_table = Arc::new(RwLock::new(FileTable::new()));
let prepared = PreparedFileDescriptor::prepare_in(
install_table.clone(),
|| Ok(descriptor()),
AX_FILE_LIMIT,
)
.unwrap();
let installed_fd = prepared.fd;
prepared.install();
let install_made_visible = install_table.read().get(installed_fd).is_some();
hidden
&& counted_against_limit
&& allocation_skipped_reservation
&& clone_excluded_reservation
&& rollback_released_number
&& install_made_visible
}
#[cfg(all(test, axtest))]
fn stable_descriptor_lookup_avoids_repeated_read_lock_for_test() -> bool {
let (read_end, _write_end) = Pipe::new();
let scope = FileTableScope::new();
assert!(
scope
.table
.write()
.add(FileDescriptor {
inner: Arc::new(read_end),
cloexec: false,
})
.is_ok()
);
let before = FD_TABLE_LOOKUP_READ_LOCKS.load(Ordering::Relaxed);
scope.lookup(0).unwrap();
scope.lookup(0).unwrap();
FD_TABLE_LOOKUP_READ_LOCKS.load(Ordering::Relaxed) - before == 1
}
#[cfg(all(test, axtest))]
fn alternating_descriptor_lookup_avoids_cache_thrashing_for_test() -> bool {
let (first_read, _first_write) = Pipe::new();
let (second_read, _second_write) = Pipe::new();
let scope = FileTableScope::new();
assert!(
scope
.table
.write()
.add(FileDescriptor {
inner: Arc::new(first_read),
cloexec: false,
})
.is_ok()
);
assert!(
scope
.table
.write()
.add(FileDescriptor {
inner: Arc::new(second_read),
cloexec: false,
})
.is_ok()
);
let before = FD_TABLE_LOOKUP_READ_LOCKS.load(Ordering::Relaxed);
scope.lookup(0).unwrap();
scope.lookup(1).unwrap();
scope.lookup(0).unwrap();
scope.lookup(1).unwrap();
FD_TABLE_LOOKUP_READ_LOCKS.load(Ordering::Relaxed) - before == 2
}
#[cfg(all(test, axtest))]
fn descriptor_lookup_invalidates_after_reuse_for_test() -> bool {
let scope = FileTableScope::new();
let (first_read, _first_write) = Pipe::new();
let (second_read, _second_write) = Pipe::new();
assert!(
scope
.table
.write()
.add(FileDescriptor {
inner: Arc::new(first_read),
cloexec: false,
})
.is_ok()
);
let first = scope.lookup(0).ok();
assert!(scope.table.write().remove(0).is_some());
assert!(
scope
.table
.write()
.add(FileDescriptor {
inner: Arc::new(second_read),
cloexec: false,
})
.is_ok()
);
let second = scope.lookup(0).ok();
match (first, second) {
(Some(first), Some(second)) => !Arc::ptr_eq(&first, &second),
_ => false,
}
}
#[cfg(all(test, axtest))]
fn cloned_table_scope_invalidates_after_fd_reuse_for_test() -> bool {
let parent = FileTableScope::new();
let (parent_read, _parent_write) = Pipe::new();
assert!(
parent
.table
.write()
.add(FileDescriptor {
inner: Arc::new(parent_read),
cloexec: false,
})
.is_ok()
);
let child = clone_file_table_scope(&parent.table);
let first = child.lookup(0).ok();
assert!(child.table.write().remove(0).is_some());
let (child_read, _child_write) = Pipe::new();
assert!(
child
.table
.write()
.add(FileDescriptor {
inner: Arc::new(child_read),
cloexec: false,
})
.is_ok()
);
let second = child.lookup(0).ok();
match (first, second) {
(Some(first), Some(second)) => !Arc::ptr_eq(&first, &second),
_ => false,
}
}
#[cfg(all(test, axtest))]
mod tests {
#[axtest::axtest]
fn descriptor_reservation_precedes_fallible_file_creation() {
use super::*;
let table = Arc::new(RwLock::new(FileTable::new()));
let result = PreparedFileDescriptor::prepare_in(
table.clone(),
|| {
let guard = table
.try_write()
.expect("file creation must run outside the table lock");
assert_eq!(guard.count(), 1, "reserve the fd before creating its file");
assert!(guard.is_reserved(0));
assert!(guard.get(0).is_none());
Err(StarryError::NoMemory)
},
AX_FILE_LIMIT,
);
assert!(matches!(result, Err(StarryError::NoMemory)));
assert_eq!(
table.read().count(),
0,
"failed creation must release its reservation"
);
assert_eq!(table.write().reserve(), Some(0));
let result = PreparedFileDescriptor::prepare_in(
table.clone(),
|| panic!("fd exhaustion must precede file allocation"),
1,
);
assert!(matches!(result, Err(StarryError::TooManyOpenFiles)));
table.write().release_reserved(0);
}
#[axtest::axtest]
fn prepared_descriptor_stays_hidden_until_install() {
assert!(super::prepared_descriptor_stays_hidden_until_install_for_test());
}
#[axtest::axtest]
fn stable_descriptor_lookup_avoids_repeated_shared_read_lock() {
assert!(
super::stable_descriptor_lookup_avoids_repeated_read_lock_for_test(),
"a stable descriptor lookup must not update one shared reader-count cache line twice"
);
}
#[axtest::axtest]
fn alternating_descriptor_lookup_avoids_cache_thrashing() {
assert!(
super::alternating_descriptor_lookup_avoids_cache_thrashing_for_test(),
"two stable descriptors must not evict each other from the task-local lookup cache"
);
}
#[axtest::axtest]
fn descriptor_lookup_invalidates_after_fd_reuse() {
assert!(
super::descriptor_lookup_invalidates_after_reuse_for_test(),
"fd reuse must invalidate the task-local lookup cache"
);
}
#[axtest::axtest]
fn cloned_table_scope_invalidates_after_fd_reuse() {
assert!(
super::cloned_table_scope_invalidates_after_fd_reuse_for_test(),
"a private fd-table clone must bind its lookup cache to the cloned table generation"
);
}
}