diff --git a/os/StarryOS/kernel/src/pseudofs/proc.rs b/os/StarryOS/kernel/src/pseudofs/proc.rs index adb75343ec..0cbc19433b 100644 --- a/os/StarryOS/kernel/src/pseudofs/proc.rs +++ b/os/StarryOS/kernel/src/pseudofs/proc.rs @@ -461,7 +461,9 @@ fn render_task_status_fields(status: &TaskStatusFields<'_>) -> String { Cpus_allowed:\t{}\n\ Cpus_allowed_list:\t{}\n\ Mems_allowed:\t1\n\ - Mems_allowed_list:\t0", + Mems_allowed_list:\t0\n\ + voluntary_ctxt_switches:\t0\n\ + nonvoluntary_ctxt_switches:\t0", base.name, base.state, base.tgid, diff --git a/os/StarryOS/kernel/src/syscall/fs/ctl.rs b/os/StarryOS/kernel/src/syscall/fs/ctl.rs index a4977e0aaf..88720996b2 100644 --- a/os/StarryOS/kernel/src/syscall/fs/ctl.rs +++ b/os/StarryOS/kernel/src/syscall/fs/ctl.rs @@ -17,17 +17,23 @@ use ax_task::current; use axfs_ng_vfs::{DeviceId, MetadataUpdate, NodePermission, NodeType, path::Path}; use linux_raw_sys::{ general::*, - ioctl::{BLKGETSIZE64, BLKRAGET, BLKSSZGET, FIOASYNC, FIONBIO, TCGETS, TIOCGWINSZ}, + ioctl::{FIOASYNC, FIONBIO}, }; use starry_vm::{VmPtr, vm_write_slice}; use crate::{ - file::{Directory, FileLike, fd_is_path, get_file_like, resolve_at, with_fs}, + file::{Directory, FD_TABLE, FileLike, fd_is_path, get_file_like, resolve_at, with_fs}, mm::vm_load_string, task::AsThread, time::TimeValueLike, }; +/// `FIOCLEX` / `FIONCLEX`: set / clear the close-on-exec flag on a file descriptor +/// via `ioctl` (the ioctl spelling of `fcntl(fd, F_SETFD, ...)`). libc/musl and CPython +/// use these on freshly-opened fds; Linux implements them generically for any fd. +const FIOCLEX: u32 = 0x5451; +const FIONCLEX: u32 = 0x5450; + fn path_info_at(dirfd: i32, path: &str) -> AxResult<(String, bool)> { with_fs(dirfd, |fs| { let loc = fs.resolve_no_follow(path)?; @@ -51,19 +57,27 @@ pub fn sys_ioctl(fd: i32, cmd: u32, arg: usize) -> AxResult { f.set_async_mode(val != 0)?; return Ok(0); } + // FIOCLEX/FIONCLEX are fd-table operations (close-on-exec), not device commands — + // handle them here so any fd (not just ttys) accepts them, as Linux does. Without + // this, curses/CPython (glances) hit "Unsupported ioctl command". + if cmd == FIOCLEX || cmd == FIONCLEX { + FD_TABLE + .write() + .get_mut(fd as _) + .ok_or(AxError::BadFileDescriptor)? + .cloexec = cmd == FIOCLEX; + return Ok(0); + } f.ioctl(cmd, arg) .map(|result| result as isize) .inspect_err(|err| { if *err == AxError::NotATty { - // Applications commonly probe non-terminal/blobk fds with - // these ioctls; suppress noise. - if matches!( - cmd, - TIOCGWINSZ | TCGETS | BLKGETSIZE64 | BLKRAGET | BLKSSZGET - ) { - return; - } - warn!("Unsupported ioctl command: {cmd} for fd: {fd}"); + // `NotATty` is a legitimate negative answer to the isatty/termios/winsize/ + // console probes (TCGETS, KDGKBTYPE, TIOCGPGRP, ...) that libc, ncurses and + // CPython fire at every fd — not an unimplemented command. Log at debug + // only: a warn per probe spams the serial console and visibly corrupts + // full-screen TUIs (htop/glances) drawing on that same console. + debug!("ioctl {cmd} on non-tty fd {fd} -> ENOTTY (probe)"); } }) } diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/CMakeLists.txt new file mode 100644 index 0000000000..de1679b73c --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ioctl-cloexec C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-ioctl-cloexec src/main.c) +target_include_directories(test-ioctl-cloexec PRIVATE src) +target_compile_options(test-ioctl-cloexec PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-ioctl-cloexec RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/main.c new file mode 100644 index 0000000000..5655b82a04 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/main.c @@ -0,0 +1,76 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include + +/* + * ioctl(FIOCLEX) / ioctl(FIONCLEX) 回归测试 — 对应 PR #1168 (fix-tui-procfs-ioctl)。 + * + * 触发背景 (为什么写这个测例): + * ncurses / CPython (glances/htop) 在每个新开 fd 上用 ioctl(FIOCLEX) 设置 + * close-on-exec —— 这是 fcntl(fd, F_SETFD, FD_CLOEXEC) 的 ioctl 拼写。starry + * 此前只在 tty 设备路径里识别 ioctl, 普通(非 tty) fd 上 FIOCLEX 落到通用 + * 分支 -> "Unsupported ioctl command: 21585" 并被拒绝, FD_CLOEXEC 没被设置。 + * + * man 2 ioctl_tty / linux ioctl 通用语义: + * FIOCLEX (0x5451): set close-on-exec == fcntl(fd, F_SETFD, FD_CLOEXEC) + * FIONCLEX (0x5450): clear close-on-exec == fcntl(fd, F_SETFD, 0) + * Linux 对**任意** fd 都通用实现 (不限于 tty)。 + * + * starry 实现 (kernel/src/syscall/fs/ctl.rs sys_ioctl): + * FIOCLEX/FIONCLEX 在 ioctl 入口直接改 FD_TABLE 项的 cloexec 位 (镜像 fcntl + * F_SETFD), 任意 fd 接受; 无效 fd 由入口 get_file_like(fd) 先返回 EBADF。 + * + * 修复前: ioctl(FIOCLEX) 在普通 fd 上失败 / FD_CLOEXEC 不变 -> 本测例 FAIL。 + * 修复后: FD_CLOEXEC 经 fcntl(F_GETFD) 可见地被 set/clear -> PASS。 + * + * 用非 tty fd: pipe (与 test_ioctl_fionbio_int 同源), 不依赖 /dev/tty 是否存在。 + */ + +#ifndef FIOCLEX +#define FIOCLEX 0x5451 +#endif +#ifndef FIONCLEX +#define FIONCLEX 0x5450 +#endif + +int main(void) +{ + TEST_START("ioctl FIOCLEX/FIONCLEX -> FD_CLOEXEC"); + + int p[2]; + CHECK_RET(pipe(p), 0, "pipe() 创建非 tty fd 成功"); + + /* 新建 fd 默认 close-on-exec 关闭 */ + int fd0 = fcntl(p[0], F_GETFD); + CHECK(fd0 >= 0, "F_GETFD 初始读取成功"); + CHECK((fd0 & FD_CLOEXEC) == 0, "新 pipe fd 初始 FD_CLOEXEC 未设置"); + + /* ---- FIOCLEX 应 set FD_CLOEXEC (等价 fcntl F_SETFD FD_CLOEXEC) ---- */ + CHECK_RET(ioctl(p[0], FIOCLEX), 0, "ioctl(FIOCLEX) 在非 tty fd 上返回 0"); + int fd1 = fcntl(p[0], F_GETFD); + CHECK(fd1 >= 0, "F_GETFD (FIOCLEX 后) 成功"); + CHECK((fd1 & FD_CLOEXEC) != 0, "FIOCLEX 后 FD_CLOEXEC 被设置"); + + /* ---- FIONCLEX 应 clear FD_CLOEXEC ---- */ + CHECK_RET(ioctl(p[0], FIONCLEX), 0, "ioctl(FIONCLEX) 返回 0"); + int fd2 = fcntl(p[0], F_GETFD); + CHECK(fd2 >= 0, "F_GETFD (FIONCLEX 后) 成功"); + CHECK((fd2 & FD_CLOEXEC) == 0, "FIONCLEX 后 FD_CLOEXEC 被清除"); + + /* ---- 与 fcntl(F_SETFD) 互证: 两条路径应一致 ---- */ + CHECK_RET(fcntl(p[0], F_SETFD, FD_CLOEXEC), 0, "fcntl(F_SETFD, FD_CLOEXEC) 成功"); + CHECK((fcntl(p[0], F_GETFD) & FD_CLOEXEC) != 0, "fcntl 设置后 FD_CLOEXEC 可见"); + CHECK_RET(ioctl(p[0], FIONCLEX), 0, "ioctl(FIONCLEX) 再次清除 fcntl 设的位"); + CHECK((fcntl(p[0], F_GETFD) & FD_CLOEXEC) == 0, "FIONCLEX 清除 fcntl 设的位生效"); + + close(p[0]); + close(p[1]); + + /* ---- 无效 fd 应返回 EBADF (入口 get_file_like 先校验) ---- */ + CHECK_ERR(ioctl(-1, FIOCLEX), EBADF, "ioctl(FIOCLEX) 无效 fd=-1 返回 EBADF"); + CHECK_ERR(ioctl(p[0], FIOCLEX), EBADF, "ioctl(FIOCLEX) 已关闭 fd 返回 EBADF"); + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-ioctl-cloexec/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/CMakeLists.txt new file mode 100644 index 0000000000..444b0817f9 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-procfs-ctxt-switches C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-procfs-ctxt-switches src/main.c) +target_include_directories(test-procfs-ctxt-switches PRIVATE src) +target_compile_options(test-procfs-ctxt-switches PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-procfs-ctxt-switches RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/main.c new file mode 100644 index 0000000000..a638ccaed9 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/main.c @@ -0,0 +1,76 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include + +/* + * /proc//status 上下文切换计数回归测试 — 对应 PR #1168 (fix-tui-procfs-ioctl)。 + * + * 触发背景 (为什么写这个测例): + * htop / glances 解析 /proc//status 的 voluntary_ctxt_switches / + * nonvoluntary_ctxt_switches 来显示每进程上下文切换数。starry 此前 status + * 缺这两行 -> TUI 解析得不到字段。补 stub 行后可被解析。 + * + * man 5 proc §/proc/[pid]/status: + * "voluntary_ctxt_switches, nonvoluntary_ctxt_switches: Number of voluntary + * and involuntary context switches (since Linux 2.6.23)." + * 格式: ":\t"。 + * + * starry 实现 (kernel/src/pseudofs/proc.rs): status 末尾追加两行 + * voluntary_ctxt_switches:\t / nonvoluntary_ctxt_switches:\t。 + * + * 修复前: 字段不存在 -> 找不到 -> FAIL。 + * 修复后: 两字段都存在且能解析为非负整数 -> PASS。 + * + * 同时覆盖 /proc/self/status 与 /proc//status 两种路径。 + */ + +/* 在 status 文件里查找 ":" 行并把其后的整数解析到 *out。找到且可解析返回 0。 */ +static int find_status_field(const char *path, const char *name, unsigned long *out) +{ + FILE *f = fopen(path, "r"); + if (!f) + return -1; + char line[256]; + int rc = -1; + size_t nlen = strlen(name); + while (fgets(line, sizeof line, f)) { + if (strncmp(line, name, nlen) == 0 && line[nlen] == ':') { + if (sscanf(line + nlen + 1, "%lu", out) == 1) + rc = 0; + break; + } + } + fclose(f); + return rc; +} + +static void check_path(const char *path) +{ + unsigned long v = 0; + char msg[160]; + + int rc = find_status_field(path, "voluntary_ctxt_switches", &v); + snprintf(msg, sizeof msg, "%s: voluntary_ctxt_switches 存在且可解析为整数 (=%lu)", + path, v); + CHECK(rc == 0, msg); + + v = 0; + rc = find_status_field(path, "nonvoluntary_ctxt_switches", &v); + snprintf(msg, sizeof msg, "%s: nonvoluntary_ctxt_switches 存在且可解析为整数 (=%lu)", + path, v); + CHECK(rc == 0, msg); +} + +int main(void) +{ + TEST_START("/proc//status ctxt_switches"); + + check_path("/proc/self/status"); + + char pidpath[64]; + snprintf(pidpath, sizeof pidpath, "/proc/%d/status", (int)getpid()); + check_path(pidpath); + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-procfs-ctxt-switches/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0