Parcourir la source

feat(filesystem): 实现truncate系统调用 (#1308)

* feat(filesystem): 实现truncate系统调用

- 添加VFS层的统一vfs_truncate封装,包含文件类型和只读挂载检查
- 实现truncate系统调用处理,支持路径解析和符号链接跟随
- 修复FAT文件系统resize方法,确保页缓存和元数据同步更新
- 添加全面的用户空间测试用例,覆盖各种边界条件和错误情况
- 优化文件截断流程,统一通过VFS封装处理类型和只读检查

Signed-off-by: longjin <longjin@DragonOS.org>

* fix(page_cache): 修复新文件截断时的错误处理逻辑

Signed-off-by: longjin <longjin@DragonOS.org>

* style(filesystem): 移除vcore.rs中的调试日志和注释

Signed-off-by: longjin <longjin@DragonOS.org>

---------

Signed-off-by: longjin <longjin@DragonOS.org>
LoGin il y a 3 semaines
Parent
commit
84a5869169

+ 4 - 1
kernel/src/filesystem/fat/fs.rs

@@ -1655,8 +1655,9 @@ impl IndexNode for LockedFATInode {
         Ok(())
     }
     fn resize(&self, len: usize) -> Result<(), SystemError> {
+        // 先调整页缓存大小,但不要提前返回;后续仍需同步到底层文件并更新元数据
         if let Some(page_cache) = self.page_cache() {
-            return page_cache.lock_irqsave().resize(len);
+            page_cache.lock_irqsave().resize(len)?;
         }
 
         let mut guard: SpinLockGuard<FATInode> = self.0.lock();
@@ -1690,7 +1691,9 @@ impl IndexNode for LockedFATInode {
                         file.truncate(fs, len as u64)?;
                     }
                 }
+                // 同步元数据:从文件对象获取最新大小,并确保一致
                 guard.synchronize_metadata();
+                guard.metadata.size = len as i64;
                 return Ok(());
             }
             FATDirEntry::Dir(_) => return Err(SystemError::ENOSYS),

+ 2 - 2
kernel/src/filesystem/page_cache.rs

@@ -301,9 +301,9 @@ impl InnerPageCache {
                 unsafe {
                     page.write_irqsave().truncate(last_len);
                 };
-            } else {
-                return Err(SystemError::EIO);
             }
+            // 对于新文件,最后一页不存在是正常的,不需要返回错误
+            // 只有当文件需要截断到更小的尺寸时,才需要处理最后一页
         }
 
         Ok(())

+ 2 - 2
kernel/src/filesystem/vfs/file.rs

@@ -481,8 +481,8 @@ impl File {
         // 如果文件不可写,返回错误
         self.writeable()?;
 
-        // 调用inode的truncate方法
-        self.inode.resize(len)?;
+        // 统一通过 VFS 封装,复用类型/只读检查
+        crate::filesystem::vfs::vcore::vfs_truncate(self.inode(), len)?;
         return Ok(());
     }
 

+ 1 - 0
kernel/src/filesystem/vfs/syscall/mod.rs

@@ -40,6 +40,7 @@ mod sys_select;
 mod sys_statfs;
 mod sys_statx;
 mod sys_symlinkat;
+mod sys_truncate;
 mod sys_unlinkat;
 mod sys_utimensat;
 mod sys_write;

+ 65 - 0
kernel/src/filesystem/vfs/syscall/sys_truncate.rs

@@ -0,0 +1,65 @@
+use crate::arch::syscall::nr::SYS_TRUNCATE;
+use crate::{
+    arch::interrupt::TrapFrame,
+    filesystem::vfs::{
+        fcntl::AtFlags, utils::user_path_at, IndexNode, MAX_PATHLEN, VFS_MAX_FOLLOW_SYMLINK_TIMES,
+    },
+    process::ProcessManager,
+    syscall::{
+        table::{FormattedSyscallParam, Syscall},
+        user_access::check_and_clone_cstr,
+    },
+};
+
+use alloc::sync::Arc;
+use alloc::vec::Vec;
+use system_error::SystemError;
+
+use crate::filesystem::vfs::vcore::vfs_truncate;
+
+/// # truncate(path, length)
+///
+/// 基于路径调整文件大小:
+/// - 跟随符号链接定位最终 inode。
+/// - 目录返回 EISDIR;非普通文件返回 EINVAL。
+/// - 只读挂载点返回 EROFS。
+/// - 调用 inode.resize(length)。
+pub struct SysTruncateHandle;
+
+impl Syscall for SysTruncateHandle {
+    fn num_args(&self) -> usize {
+        2
+    }
+
+    fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result<usize, SystemError> {
+        let path_ptr = args[0] as *const u8;
+        let length = args[1];
+
+        // 复制并校验用户态路径
+        let path = check_and_clone_cstr(path_ptr, Some(MAX_PATHLEN))?;
+        let path = path.to_str().map_err(|_| SystemError::EINVAL)?;
+
+        // 解析起始 inode 与剩余路径
+        let (begin_inode, remain_path) = user_path_at(
+            &ProcessManager::current_pcb(),
+            AtFlags::AT_FDCWD.bits(),
+            path,
+        )?;
+
+        // 跟随符号链接解析最终目标 inode
+        let target: Arc<dyn IndexNode> = begin_inode
+            .lookup_follow_symlink(remain_path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES)?;
+
+        vfs_truncate(target, length)?;
+        Ok(0)
+    }
+
+    fn entry_format(&self, args: &[usize]) -> Vec<FormattedSyscallParam> {
+        vec![
+            FormattedSyscallParam::new("path", format!("{:#x}", args[0])),
+            FormattedSyscallParam::new("length", format!("{:#x}", args[1])),
+        ]
+    }
+}
+
+syscall_table_macros::declare_syscall!(SYS_TRUNCATE, SysTruncateHandle);

+ 30 - 2
kernel/src/filesystem/vfs/vcore.rs

@@ -4,6 +4,7 @@ use alloc::sync::Arc;
 use log::{error, info};
 use system_error::SystemError;
 
+use crate::libs::casting::DowncastArc;
 use crate::{
     define_event_trace,
     driver::base::block::{gendisk::GenDisk, manager::block_dev_manager},
@@ -188,7 +189,6 @@ pub fn do_mkdir_at(
     mode: FileMode,
 ) -> Result<Arc<dyn IndexNode>, SystemError> {
     trace_do_mkdir_at(path, mode);
-    // debug!("Call do mkdir at");
     let (mut current_inode, path) =
         user_path_at(&ProcessManager::current_pcb(), dirfd, path.trim())?;
     let (name, parent) = rsplit_path(&path);
@@ -196,7 +196,6 @@ pub fn do_mkdir_at(
         current_inode =
             current_inode.lookup_follow_symlink(parent, VFS_MAX_FOLLOW_SYMLINK_TIMES)?;
     }
-    // debug!("mkdir at {:?}", current_inode.metadata()?.inode_id);
     return current_inode.mkdir(name, ModeType::from_bits_truncate(mode.bits()));
 }
 
@@ -280,3 +279,32 @@ pub(super) fn do_file_lookup_at(
     let follow_final = lookup_flags.contains(LookUpFlags::FOLLOW);
     return inode.lookup_follow_symlink2(&path, VFS_MAX_FOLLOW_SYMLINK_TIMES, follow_final);
 }
+
+/// 统一的 VFS 截断封装:对 inode 进行基本检查并调用 resize
+/// - 目录返回 EISDIR
+/// - 非普通文件返回 EINVAL
+/// - 只读挂载返回 EROFS
+#[inline(never)]
+pub fn vfs_truncate(inode: Arc<dyn IndexNode>, len: usize) -> Result<(), SystemError> {
+    let md = inode.metadata()?;
+
+    if md.file_type == FileType::Dir {
+        return Err(SystemError::EISDIR);
+    }
+    if md.file_type != FileType::File {
+        return Err(SystemError::EINVAL);
+    }
+
+    // 只读挂载检查:若当前 fs 是 MountFS 且带 RDONLY 标志,拒绝写
+    let fs = inode.fs();
+    if let Some(mfs) = fs.clone().downcast_arc::<MountFS>() {
+        let mount_flags = mfs.mount_flags();
+        if mount_flags.contains(crate::filesystem::vfs::mount::MountFlags::RDONLY) {
+            return Err(SystemError::EROFS);
+        }
+    }
+
+    let result = inode.resize(len);
+
+    result
+}

+ 275 - 0
user/apps/c_unitest/test_truncate.c

@@ -0,0 +1,275 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define TEST_FILE "/tmp/test_truncate.txt"
+#define TEST_DIR "/tmp/test_truncate_dir"
+#define TEST_SYMLINK "/tmp/test_truncate_symlink"
+#define TEST_RO_MOUNT "/tmp/test_ro_mount"
+
+// 测试辅助函数
+static void test_assert(int condition, const char *message) {
+    if (!condition) {
+        printf("FAIL: %s\n", message);
+        exit(1);
+    }
+}
+
+static void test_success(const char *message) {
+    printf("PASS: %s\n", message);
+}
+
+// 测试正常文件截断
+static void test_normal_truncate() {
+    printf("\n=== 测试正常文件截断 ===\n");
+    
+    // 创建测试文件
+    FILE *fp = fopen(TEST_FILE, "w");
+    test_assert(fp != NULL, "创建测试文件");
+    fprintf(fp, "Hello, World! This is a test file.");
+    fclose(fp);
+    
+    // 检查初始文件大小
+    struct stat st;
+    test_assert(stat(TEST_FILE, &st) == 0, "获取文件状态");
+    printf("初始文件大小: %ld bytes\n", st.st_size);
+    
+    // 截断到较小大小
+    printf("DEBUG: 调用 truncate(%s, 10)\n", TEST_FILE);
+    int result = truncate(TEST_FILE, 10);
+    printf("DEBUG: truncate 返回值: %d, errno: %d\n", result, errno);
+    
+    test_assert(result == 0, "截断到10字节");
+    test_assert(stat(TEST_FILE, &st) == 0, "获取截断后文件状态");
+    printf("DEBUG: 截断后文件大小: %ld bytes (期望: 10)\n", st.st_size);
+    
+    if (st.st_size == 10) {
+        test_success("截断到较小大小");
+    } else {
+        printf("FAIL: 文件大小应为10字节,实际为%ld字节\n", st.st_size);
+        // 继续测试其他情况
+    }
+    
+    // 截断到较大大小
+    printf("DEBUG: 调用 truncate(%s, 100)\n", TEST_FILE);
+    result = truncate(TEST_FILE, 100);
+    printf("DEBUG: truncate 返回值: %d, errno: %d\n", result, errno);
+    
+    test_assert(result == 0, "截断到100字节");
+    test_assert(stat(TEST_FILE, &st) == 0, "获取截断后文件状态");
+    printf("DEBUG: 截断后文件大小: %ld bytes (期望: 100)\n", st.st_size);
+    
+    if (st.st_size == 100) {
+        test_success("截断到较大大小");
+    } else {
+        printf("FAIL: 文件大小应为100字节,实际为%ld字节\n", st.st_size);
+    }
+    
+    // 截断到0
+    printf("DEBUG: 调用 truncate(%s, 0)\n", TEST_FILE);
+    result = truncate(TEST_FILE, 0);
+    printf("DEBUG: truncate 返回值: %d, errno: %d\n", result, errno);
+    
+    test_assert(result == 0, "截断到0字节");
+    test_assert(stat(TEST_FILE, &st) == 0, "获取截断后文件状态");
+    printf("DEBUG: 截断后文件大小: %ld bytes (期望: 0)\n", st.st_size);
+    
+    if (st.st_size == 0) {
+        test_success("截断到0字节");
+    } else {
+        printf("FAIL: 文件大小应为0字节,实际为%ld字节\n", st.st_size);
+    }
+    
+    // 清理
+    unlink(TEST_FILE);
+}
+
+// 测试目录截断(应返回EISDIR)
+static void test_directory_truncate() {
+    printf("\n=== 测试目录截断 ===\n");
+    
+    // 创建测试目录
+    test_assert(mkdir(TEST_DIR, 0755) == 0, "创建测试目录");
+    
+    // 尝试截断目录
+    int result = truncate(TEST_DIR, 10);
+    test_assert(result == -1, "截断目录应失败");
+    test_assert(errno == EISDIR, "错误码应为EISDIR");
+    test_success("目录截断正确返回EISDIR");
+    
+    // 清理
+    rmdir(TEST_DIR);
+}
+
+// 测试符号链接截断
+static void test_symlink_truncate() {
+    printf("\n=== 测试符号链接截断 ===\n");
+    
+    // 创建目标文件
+    FILE *fp = fopen(TEST_FILE, "w");
+    test_assert(fp != NULL, "创建目标文件");
+    fprintf(fp, "Target file content");
+    fclose(fp);
+    
+    // 创建符号链接
+    test_assert(symlink(TEST_FILE, TEST_SYMLINK) == 0, "创建符号链接");
+    
+    // 截断符号链接(应跟随到目标文件)
+    test_assert(truncate(TEST_SYMLINK, 5) == 0, "截断符号链接");
+    
+    // 检查目标文件大小
+    struct stat st;
+    test_assert(stat(TEST_FILE, &st) == 0, "获取目标文件状态");
+    test_assert(st.st_size == 5, "目标文件大小应为5字节");
+    test_success("符号链接截断正确跟随到目标");
+    
+    // 清理
+    unlink(TEST_SYMLINK);
+    unlink(TEST_FILE);
+}
+
+// 测试不存在的文件
+static void test_nonexistent_file() {
+    printf("\n=== 测试不存在文件 ===\n");
+    
+    int result = truncate("/tmp/nonexistent_file", 10);
+    test_assert(result == -1, "截断不存在文件应失败");
+    test_assert(errno == ENOENT, "错误码应为ENOENT");
+    test_success("不存在文件正确返回ENOENT");
+}
+
+// 测试只读挂载点截断
+static void test_readonly_mount() {
+    printf("\n=== 测试只读挂载点截断 ===\n");
+    
+    // 创建挂载点目录
+    test_assert(mkdir(TEST_RO_MOUNT, 0755) == 0, "创建挂载点目录");
+    
+    // 尝试以只读方式挂载(如果支持的话)
+    // 注意:这里可能需要根据实际文件系统支持情况调整
+    if (mount("", TEST_RO_MOUNT, "ramfs", MS_RDONLY, NULL) == 0) {
+        // 在挂载点创建文件
+        char test_path[256];
+        snprintf(test_path, sizeof(test_path), "%s/test_file", TEST_RO_MOUNT);
+        
+        FILE *fp = fopen(test_path, "w");
+        if (fp != NULL) {
+            fprintf(fp, "Test content");
+            fclose(fp);
+            
+            // 尝试截断只读挂载点上的文件
+            int result = truncate(test_path, 5);
+            if (result == -1 && errno == EROFS) {
+                test_success("只读挂载点截断正确返回EROFS");
+            } else {
+                printf("WARN: 只读挂载点测试未按预期返回EROFS\n");
+            }
+            
+            unlink(test_path);
+        }
+        
+        umount(TEST_RO_MOUNT);
+    } else {
+        printf("INFO: 跳过只读挂载测试(可能不支持或权限不足)\n");
+    }
+    
+    // 清理
+    rmdir(TEST_RO_MOUNT);
+}
+
+// 测试边界条件
+static void test_boundary_conditions() {
+    printf("\n=== 测试边界条件 ===\n");
+    
+    // 创建测试文件
+    FILE *fp = fopen(TEST_FILE, "w");
+    test_assert(fp != NULL, "创建测试文件");
+    fprintf(fp, "Test content");
+    fclose(fp);
+    
+    // 测试负长度(应返回EINVAL)
+    int result = truncate(TEST_FILE, -1);
+    if (result == -1 && errno == EINVAL) {
+        test_success("负长度正确返回EINVAL");
+    } else {
+        printf("WARN: 负长度测试未按预期返回EINVAL\n");
+    }
+    
+    // 测试非常大的长度
+    test_assert(truncate(TEST_FILE, 0x7FFFFFFF) == 0, "大长度截断");
+    struct stat st;
+    test_assert(stat(TEST_FILE, &st) == 0, "获取大长度截断后状态");
+    printf("大长度截断后文件大小: %ld bytes\n", st.st_size);
+    test_success("大长度截断");
+    
+    // 清理
+    unlink(TEST_FILE);
+}
+
+// 测试与ftruncate的一致性
+static void test_ftruncate_consistency() {
+    printf("\n=== 测试与ftruncate的一致性 ===\n");
+    
+    // 创建测试文件
+    FILE *fp = fopen(TEST_FILE, "w");
+    test_assert(fp != NULL, "创建测试文件");
+    fprintf(fp, "Test content for consistency");
+    fclose(fp);
+    
+    // 使用truncate截断
+    printf("DEBUG: 调用 truncate(%s, 10)\n", TEST_FILE);
+    int result = truncate(TEST_FILE, 10);
+    printf("DEBUG: truncate 返回值: %d, errno: %d\n", result, errno);
+    test_assert(result == 0, "truncate截断");
+    
+    struct stat st1;
+    test_assert(stat(TEST_FILE, &st1) == 0, "获取truncate后状态");
+    printf("DEBUG: truncate后文件大小: %ld bytes\n", st1.st_size);
+    
+    // 使用ftruncate截断
+    int fd = open(TEST_FILE, O_RDWR);
+    test_assert(fd != -1, "打开文件");
+    printf("DEBUG: 调用 ftruncate(fd=%d, 5)\n", fd);
+    result = ftruncate(fd, 5);
+    printf("DEBUG: ftruncate 返回值: %d, errno: %d\n", result, errno);
+    test_assert(result == 0, "ftruncate截断");
+    close(fd);
+    
+    struct stat st2;
+    test_assert(stat(TEST_FILE, &st2) == 0, "获取ftruncate后状态");
+    printf("DEBUG: ftruncate后文件大小: %ld bytes (期望: 5)\n", st2.st_size);
+    
+    if (st2.st_size == 5) {
+        test_success("truncate和ftruncate行为一致");
+    } else {
+        printf("FAIL: ftruncate后文件大小应为5字节,实际为%ld字节\n", st2.st_size);
+    }
+    
+    // 清理
+    unlink(TEST_FILE);
+}
+
+int main() {
+    printf("开始 SYS_TRUNCATE 系统调用测试\n");
+    printf("================================\n");
+    
+    // 运行所有测试
+    test_normal_truncate();
+    test_directory_truncate();
+    test_symlink_truncate();
+    test_nonexistent_file();
+    test_readonly_mount();
+    test_boundary_conditions();
+    test_ftruncate_consistency();
+    
+    printf("\n================================\n");
+    printf("所有测试完成!\n");
+    
+    return 0;
+}