diff --git a/docs/src/kernel/linux-compatibility.md b/docs/src/kernel/linux-compatibility.md index 8267a999c..72a565eb0 100644 --- a/docs/src/kernel/linux-compatibility.md +++ b/docs/src/kernel/linux-compatibility.md @@ -93,7 +93,7 @@ provided by Linux on x86-64 architecture. | 70 | msgrcv | ❌ | | 71 | msgctl | ❌ | | 72 | fcntl | ✅ | -| 73 | flock | ❌ | +| 73 | flock | ✅ | | 74 | fsync | ✅ | | 75 | fdatasync | ✅ | | 76 | truncate | ✅ | diff --git a/kernel/aster-nix/src/fs/utils/flock.rs b/kernel/aster-nix/src/fs/utils/flock.rs new file mode 100644 index 000000000..203193be5 --- /dev/null +++ b/kernel/aster-nix/src/fs/utils/flock.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MPL-2.0 +use alloc::fmt; +use core::ptr; + +use ostd::sync::WaitQueue; + +use crate::{ + fs::{file_handle::FileLike, inode_handle::InodeHandle}, + prelude::*, +}; + +/// Represents a file lock (FLOCK) with an owner and type. +#[derive(Debug, Clone)] +struct Flock { + /// Owner of the lock, which is an opened file descriptor. + owner: Weak, + /// Type of the lock, either shared or exclusive. + type_: FlockType, +} + +/// Represents a Flock item that can be held in a list of file locks. +/// Each FlockItem contains a lock and a wait queue for threads that are blocked by the lock. +pub struct FlockItem { + lock: Flock, + /// A wait queue for any threads that are blocked by this lock. + waitqueue: Arc, +} + +impl FlockItem { + /// Creates a new FlockItem with the specified owner and lock type. + pub fn new(owner: &Arc, type_: FlockType) -> Self { + Self { + lock: Flock { + owner: Arc::downgrade(owner), + type_, + }, + waitqueue: Arc::new(WaitQueue::new()), + } + } + + /// Returns the owner of the lock if it exists. + pub fn owner(&self) -> Option> { + Weak::upgrade(&self.lock.owner) + } + + /// Checks if this lock has the same owner as another lock. + pub fn same_owner_with(&self, other: &Self) -> bool { + self.lock.owner.ptr_eq(&other.lock.owner) + } + + /// Returns true if this lock conflicts with another lock. + /// Two locks conflict if they have different owners and at least one of them is an exclusive lock. + pub fn conflict_with(&self, other: &Self) -> bool { + if self.same_owner_with(other) { + return false; + } + if self.lock.type_ == FlockType::ExclusiveLock + || other.lock.type_ == FlockType::ExclusiveLock + { + return true; + } + false + } + + /// Waits until the lock can be acquired. + pub fn wait(&self) { + let cond = || None::<()>; + self.waitqueue.wait_until(cond); + } + + /// Wakes all threads that are waiting for this lock. + pub fn wake_all(&self) { + self.waitqueue.wake_all(); + } +} + +impl Clone for FlockItem { + fn clone(&self) -> Self { + Self { + lock: self.lock.clone(), + waitqueue: self.waitqueue.clone(), + } + } +} + +/// When a FlockItem is dropped, it wakes all threads that are waiting for the lock. +impl Drop for FlockItem { + fn drop(&mut self) { + self.waitqueue.wake_all(); + } +} + +impl Debug for FlockItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Flock") + .field("owner", &self.lock.owner.as_ptr()) + .field("type_", &self.lock.type_) + .finish() + } +} + +/// Represents a list of non-POSIX file advisory locks (FLOCK). +/// The list is used to manage file locks and resolve conflicts between them. +pub struct FlockList { + inner: RwMutex>, +} + +impl FlockList { + /// Creates a new FlockList. + pub fn new() -> Self { + Self { + inner: RwMutex::new(VecDeque::new()), + } + } + + /// Attempts to set a lock on the file. + /// If no conflicting locks exist, the lock is set and the function returns Ok(()). + /// If is_nonblocking is true and a conflicting lock exists, the function returns EAGAIN. + /// Otherwise, the function waits until the lock can be acquired. + pub fn set_lock(&self, mut req_lock: FlockItem, is_nonblocking: bool) -> Result<()> { + debug!( + "set_lock with Flock: {:?}, is_nonblocking: {}", + req_lock, is_nonblocking + ); + loop { + let conflict_lock; + { + let mut list = self.inner.write(); + if let Some(existing_lock) = list.iter().find(|l| req_lock.conflict_with(l)) { + if is_nonblocking { + return_errno_with_message!(Errno::EAGAIN, "the file is locked"); + } + conflict_lock = existing_lock.clone(); + } else { + match list.iter().position(|l| req_lock.same_owner_with(l)) { + Some(idx) => { + core::mem::swap(&mut req_lock, &mut list[idx]); + } + None => { + list.push_front(req_lock); + } + } + return Ok(()); + } + } + conflict_lock.wait(); + } + } + + /// Unlocks the specified owner, waking any waiting threads. + /// If the owner is no longer valid, the lock is removed from the list. + /// If the owner is valid, the lock is removed from the list and all threads waiting for the lock are woken. + /// The function does nothing if the owner is not found in the list. + /// The function is called when the file is closed or the lock is released. + pub fn unlock(&self, req_owner: &InodeHandle) { + debug!( + "unlock with owner: {:?}", + req_owner as *const InodeHandle + ); + let mut list = self.inner.write(); + list.retain(|lock| { + if let Some(owner) = lock.owner() { + if ptr::eq( + Arc::as_ptr(&owner) as *const InodeHandle, + req_owner as *const InodeHandle, + ) { + lock.wake_all(); // Wake all threads waiting for this lock. + false // Remove lock from the list. + } else { + true // Keep lock in the list. + } + } else { + false // Remove lock if the owner is no longer valid. + } + }); + } +} + +impl Default for FlockList { + fn default() -> Self { + Self::new() + } +} + +/// Represents the type of a Flock - either shared or exclusive. +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u16)] +pub enum FlockType { + /// Represents a shared lock. + SharedLock = 0, + /// Represents an exclusive lock. + ExclusiveLock = 1, +} diff --git a/kernel/aster-nix/src/syscall/arch/x86.rs b/kernel/aster-nix/src/syscall/arch/x86.rs index fddd71107..b28877d4e 100644 --- a/kernel/aster-nix/src/syscall/arch/x86.rs +++ b/kernel/aster-nix/src/syscall/arch/x86.rs @@ -25,6 +25,7 @@ use crate::syscall::{ exit_group::sys_exit_group, fallocate::sys_fallocate, fcntl::sys_fcntl, + flock::sys_flock, fork::sys_fork, fsync::{sys_fdatasync, sys_fsync}, futex::sys_futex, @@ -190,6 +191,7 @@ impl_syscall_nums_and_dispatch_fn! { SYS_KILL = 62 => sys_kill(args[..2]); SYS_UNAME = 63 => sys_uname(args[..1]); SYS_FCNTL = 72 => sys_fcntl(args[..3]); + SYS_FLOCK = 73 => sys_flock(args[..2]); SYS_FSYNC = 74 => sys_fsync(args[..1]); SYS_FDATASYNC = 75 => sys_fdatasync(args[..1]); SYS_TRUNCATE = 76 => sys_truncate(args[..2]); diff --git a/kernel/aster-nix/src/syscall/flock.rs b/kernel/aster-nix/src/syscall/flock.rs new file mode 100644 index 000000000..ab0d3c59d --- /dev/null +++ b/kernel/aster-nix/src/syscall/flock.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MPL-2.0 + +use super::SyscallReturn; +use crate::{ + fs::{ + file_table::FileDesc, + inode_handle::InodeHandle, + utils::{FlockItem, FlockType}, + }, + prelude::*, +}; + +pub fn sys_flock(fd: FileDesc, ops: i32, ctx: &Context) -> Result { + debug!("flock: fd: {}, ops: {:?}", fd, ops); + + let file = { + let current = ctx.process; + let file_table = current.file_table().lock(); + file_table.get_file(fd)?.clone() + }; + let inode_file = file + .downcast_ref::() + .ok_or(Error::with_message(Errno::EBADF, "not inode"))?; + let ops: FlockOps = FlockOps::from_i32(ops)?; + if ops.contains(FlockOps::LOCK_UN) { + inode_file.unlock_flock(); + } else { + let is_nonblocking = ops.contains(FlockOps::LOCK_NB); + let flock = { + let type_ = FlockType::from(ops); + FlockItem::new(&file, type_) + }; + inode_file.set_flock(flock, is_nonblocking)?; + } + Ok(SyscallReturn::Return(0)) +} + +impl From for FlockType { + fn from(ops: FlockOps) -> Self { + if ops.contains(FlockOps::LOCK_EX) { + Self::ExclusiveLock + } else if ops.contains(FlockOps::LOCK_SH) { + Self::SharedLock + } else { + panic!("invalid flockops"); + } + } +} + +bitflags! { + struct FlockOps: i32 { + /// Shared lock + const LOCK_SH = 1; + /// Exclusive lock + const LOCK_EX = 2; + // Or'd with one of the above to prevent blocking + const LOCK_NB = 4; + // Remove lock + const LOCK_UN = 8; + } +} + +impl FlockOps { + fn from_i32(bits: i32) -> Result { + if let Some(ops) = Self::from_bits(bits) { + if ops.contains(Self::LOCK_SH) { + if ops.contains(Self::LOCK_EX) || ops.contains(Self::LOCK_UN) { + return_errno_with_message!(Errno::EINVAL, "invalid operation"); + } + } else if ops.contains(Self::LOCK_EX) { + if ops.contains(Self::LOCK_UN) { + return_errno_with_message!(Errno::EINVAL, "invalid operation"); + } + } else if !ops.contains(Self::LOCK_UN) { + return_errno_with_message!(Errno::EINVAL, "invalid operation"); + } + Ok(ops) + } else { + return_errno_with_message!(Errno::EINVAL, "invalid operation"); + } + } +} diff --git a/kernel/aster-nix/src/syscall/mod.rs b/kernel/aster-nix/src/syscall/mod.rs index 42d68c229..cd0b40e4f 100644 --- a/kernel/aster-nix/src/syscall/mod.rs +++ b/kernel/aster-nix/src/syscall/mod.rs @@ -33,6 +33,7 @@ mod exit; mod exit_group; mod fallocate; mod fcntl; +mod flock; mod fork; mod fsync; mod futex; diff --git a/test/syscall_test/Makefile b/test/syscall_test/Makefile index 898cafc28..4e8680a6c 100644 --- a/test/syscall_test/Makefile +++ b/test/syscall_test/Makefile @@ -16,6 +16,7 @@ TESTS ?= \ epoll_test \ eventfd_test \ fcntl_test \ + flock_test \ fsync_test \ getdents_test \ link_test \ diff --git a/test/syscall_test/blocklists/flock_test b/test/syscall_test/blocklists/flock_test new file mode 100644 index 000000000..07a60eb01 --- /dev/null +++ b/test/syscall_test/blocklists/flock_test @@ -0,0 +1,9 @@ +FlockTest.TestSharedLockFailExclusiveHolderBlocking_NoRandomSave +FlockTest.TestExclusiveLockFailExclusiveHolderBlocking_NoRandomSave +FlockTest.BlockingLockFirstSharedSecondExclusive_NoRandomSave +FlockTest.BlockingLockFirstExclusiveSecondShared_NoRandomSave +FlockTest.BlockingLockFirstExclusiveSecondExclusive_NoRandomSave +FlockTestNoFixture.FlockSymlink +FlockTestNoFixture.FlockProc +FlockTestNoFixture.FlockPipe +FlockTestNoFixture.FlockSocket \ No newline at end of file