0%

作者: 王浩旸 (silicalet)
仓库: rcore-os/tgoskits | 个人 fork: silicalet/tgoskits
跟踪分支: 001-starryos-signal-syscalls002-starryos-nix-smoke003-starryos-nixpkgs

训练营这三条分支走下来,我最大的感受是:内核兼容性工作的难点不在写代码,而在搞清楚”问题到底是什么”。三次都经历了同一个循环——拿到一个模糊的现象,花大量时间排除自己以为是根因的方向,最后发现真正的问题在完全没料到的地方。

001 改测试不改内核,002 顺着 Nix 挂起追到 ext4 位图缓存,003 花了一周才搞明白”QEMU 退出码 0 不代表成功”。三条分支共用一套记录方式:Spec Kit 的 spec/plan/tasks/findings 管结构,本地 ./silicalet/ 目录存实验日志,不进 upstream。本文记的是从头到尾怎么走过来的,以及过程中那些走错的弯路。


001:只改测试,不改内核

001 的任务边界一开始让我有点不适应。spec 写得很死:本轮只改测试和文档,不动内核实现。对六个信号扩展系统调用做 readiness 评审,补齐源码级回归测试。

刚开始觉得这个约束像在限制产出。但做到一半就理解了。信号子系统的 bug 不会在功能测试里暴露,它藏在特定的时序、掩码组合、siginfo 元数据场景下。StarryOS 这些 syscall 在 dispatch 层都有实现,但用户态覆盖参差不齐——有几个甚至没有独立测试。如果不知道”当前行为是什么”就直接改内核,等于在黑暗里装修。先把”正确行为”用测试固化下来,后续谁动信号子系统,这些测试就是回归防线。

实际写测试时,最难的不是写断言,是确定”参考行为”。比如 rt_sigtimedwait 的 info 参数允许传 NULL,NULL 时 siginfo 不回写但信号仍被消费——这种细节 man page 一句话带过,Linux 内核为什么这么实现、StarryOS 的实现是否符合,得自己想清楚。每个 syscall 写一篇 beginner-oriented 笔记的过程逼着我把语义想透,比单写测试难得多。

六个测试在 Linux 参考环境下断言全过后,我想去 StarryOS 上验证,结果卡在一个完全无关的地方。grouped syscall case 按字母序执行,test-raw-msg-peek 排在我的 signal 测试之前,它的 prebuild 脚本在 staging rootfs 里跑 apk wrapper 时触发了 ELOOP,整个 grouped case 在预构建阶段就断了,根本到不了我的测试。

这个阻塞让我意识到一件事:源码级测试的隔离性在 grouped runner 里并不保证。测试本身写对了、Linux 参考过了,但 grouped case 的 prebuild 依赖链可能让它在 StarryOS 上根本跑不到。001 的交付因此是完整的——六个测试、分析文档、六篇笔记——但 StarryOS 侧的 expected-vs-observed 对照只能推迟到 prebuild 阻塞解除后。这是这一轮最大的遗憾。


002:从”Nix 挂起”到 ext4 位图缓存

002 的目标很小:让 Nix 在 StarryOS 上完成一个最小 derivation 构建。但从第一天起,现象就模糊得让人抓狂。

第一个阻塞是 Nix 启动直接 abort,报”saving parent mount namespace”。这个好查——Linux 基线显示 Nix 要访问 /proc/self/ns/mnt,Starry 的 procfs 没这个节点。加了之后 Nix 能启动了。

但 nix-build 随后挂在 build 阶段,日志只有一句”waiting for lock on /nix/store/…”。这个锁是 builder 自己创建的,不存在竞争,Linux 上一秒就完成。为什么 Nix 拿不到自己的锁?

面对这种模糊现象,最容易犯的错是直接猜根因改代码。我选了相反的策略:从离现象最近的层开始,每一层先证明它不是问题,再往下走。锁语义、进程创建、PTY、clone、execve——一层层查下来全 PASS。到第六层 flock 操作,终于出现红灯:两个不同的锁文件共享同一个 inode 号,flock 按 (device, inode) 键控,自冲突。再往下查一层,是 ext4 的 inode 分配器把同一个 inode 号分配了两次——第一次分配的位图更新丢了。

这个根因和我最初想象的方向差了十万八千里。如果一开始就猜”ext4 有问题”,我会在 ext4 几百个代码路径里盲目搜索。但七层排除下来,搜索空间被一步步缩小,最后落到 inode 分配器的位图缓存路径上。每一层 PASS 不是浪费时间,它们构成了”已排除清单”,让我在进入下一层时确信”锁语义、进程、PTY、clone、execve 都没问题,问题一定在更底层”。

修完 ext4 位图缓存之后,Nix smoke 还是没通。一个 bug 修完暴露出下一个,我前后修了六七个配套问题:PID namespace 孤儿进程回收、pipe EOF 事件上报、rsext4 删除仍被 fd 引用的 inode 时过早释放、unshare(CLONE_FS) 未支持、stale flock 进程退出后不释放。每一个单独看都不大,但叠在一起才让 Nix smoke 真正跑通。

这个过程中最让我印象深刻的是 rsext4 open-unlink 的设计。Nix 创建 .lock 文件后立刻 unlink,但仍然持有 fd 并通过 fd 写入。Linux 的语义是:unlink 只删除目录项,inode 在所有 fd 关闭后才释放。Starry 原来是删了就释放,导致 Nix 通过 fd 写入到一个已被回收的 inode。修这个需要对齐 Linux 的 i_links_count 和 i_count 双计数语义。这个设计不是我凭空想出来的——是对齐 Linux 已有语义的结果。

最后 nix-nosandbox 终于 PASSED 的那一刻,我盯着日志看了很久。从”waiting for lock”到完整的构建输出,中间隔了将近一周、七层插桩、六七个配套修复。一个最小 derivation 构建能跑通,背后是一整条兼容性链路的打通。


003:QEMU 退出码 0 是个谎言

003 在 002 基础上激活 nixpkgs 源码构建。这一轮我花时间最多的不是修 bug,是搞清楚”测试到底有没有失败”。

nixpkgs 测试的现象是:no-sandbox 阶段通过,Nix 开始下载解包 nixpkgs,没有 panic,没有失败标志,QEMU 退出码是 0。如果只看 runner,这像一次正常关机。但测试脚本的 EXIT trap 没执行,shell 不是通过正常退出离开的。这个细节一开始没引起我足够重视——我以为就是测试在某个点正常退出了,只是没打 pass 标志。

最先怀疑的是 OOM。但采样显示 nix 进程 RSS 才几十 MB,系统内存还有大半空闲。把 QEMU 内存从 2GB 加到 8GB,失败只是从 80 秒延后到 400 秒,仍然静默退出,退出时还有 3.8GB 空闲。加内存只延后了失败,没消除它。这不是简单的内存耗尽。

接下来一周我陷入了排除法的泥潭。在 StarryOS 已知的三个关机路径加诊断——PID 1 退出、primary panic、recursive panic——都没触发。我当时用排除法猜”是 page cache 回收期间的递归 panic”,在 panic 路径本身加诊断后直接被推翻:panic_shutdown 和 page_cache_reclaim 都没触发。排除法能缩小范围,但确认根因必须找到触发证据,不能靠”其他都不对所以这个对”。这是我这一轮最大的教训。

转折点是追踪 system_off 的调用链。QEMU 退出码 0 来自 someboot 的 x86_64 shutdown 函数往 0x604 端口写 0x2000,触发 QEMU Q35 的 ACPI S5 关机。顺着 system_off 的五个调用点查,前四个都排除了,第五个是 libc_compat 里的 abort()。在 abort 和 shutdown 加直接 COM1 端口写诊断,重新跑,两个标记都触发了。

原来 nix(C++ 二进制)遇到致命错误时,musl libc 调用 abort()。StarryOS 的 libc_compat 把 abort() 映射成 ax_terminate → system_off → QEMU 关机。QEMU 退出码 0 不是测试成功,是整机关机。

这个结论也推翻了”几 GiB free 就排除 OOM”的判断。进程 RSS 只描述用户进程映射,不代表内核 page cache 占用。真正增长的是脏页缓存——Nix 解包源码、构建 Git cache 时创建大量文件,没有周期写回时 dirty page cache 持续增长,挤压 Rust 全局分配器的堆空间,触发 rust_oom → abort。所以”free 还很多”是假象,因为我看的是错误的 free 指标。

修了两处:后台 worker 每五秒同步一次文件缓存,避免脏页无界增长;ax-alloc 的字节分配失败路径改为调用已注册的 page reclaim callback 后重试。修完之后静默关机不再出现。

但完整 nixpkgs 仍然没通过。89,704 个条目的 archive 在 StarryOS 上解压需要 6-7 分钟,远超 180 秒预算。试了加 vCPU、换压缩格式、用 tmpfs 绕过 ext4,都没改变三分钟结果——tar-to-Git-cache 这一步本质上是单线程的。

这里我差点犯一个错。Spec Kit 任务希望有一个 grouped system regression。我测了一堆合成 VFS workload,全通过,无法复现真实 nixpkgs 超时。如果用一个本来就通过的 benchmark 冒充红色回归测试,CI 会变绿但什么也防不住。最后我选择诚实记录”当前没有有效的小型红色复现”,而不是造假。未来合适的回归层级是 axfs-ng 的 bounded page-cache pressure 测试,不是把完整 nixpkgs archive 塞进普通 syscall suite。


回头看

三条分支走完,几个感受比较深。

模糊现象是最难的起点。 Nix 挂起、QEMU 退出码 0、测试跑不到——这些现象本身不告诉你任何信息。002 的七层插桩和 003 的 abort 路径追踪都证明了同一件事:现象越模糊,越不能猜根因改代码,必须从离现象最近的层开始逐层排除。每一层 PASS 都是在缩小搜索空间。

排除法会误导自己。 003 我曾用排除法猜”recursive panic”,排除了三个已知路径就下结论,结果被下一轮诊断直接推翻。排除法只能排除你想到的路径,确认根因必须找到触发证据。

观测指标要问覆盖范围。 003 早期看到”几 GiB free”就排除 OOM,是错的——free 指标不覆盖 page cache。002 一开始想从 Nix 用户态日志找原因,但那里只有”waiting for lock”,看不到 inode 号。观测点选在哪决定了能看到什么。

不为必须有回归测试造假。 合成 workload 全通过时,诚实记录”当前没有有效复现”比造一个与根因无关的测试强。CI 变绿但防不住真正的 bug,是更糟糕的状态。

本地笔记是调查记忆。 三条分支的 silicalet/ 目录存了几十份实验日志,通过 .git/info/exclude 排除在 upstream 之外。002 从 NIX-SMOKE-001 到 NIX-SMOKE-011 每一步诊断都有记录,003 从 rev9 到 rev13 每一轮排除都有笔记。没有这些笔记,rev12 推翻 rev11 结论时,我可能都不记得 rev11 具体测了什么。切换上下文是常态,结构化笔记让调查可恢复。


交付状态

分支 交付 状态
001 六个 C 回归测试 + 分析文档 + 六篇 per-syscall 笔记 Linux 参考全过;StarryOS 验证被前置 case prebuild 阻塞
002 ext4 位图缓存修复 + PID ns 孤儿回收 + pipe EOF + rsext4 open-unlink + unshare(CLONE_FS) + stale flock nix-nosandbox PASSED
003 周期 writeback + reclaim retry + 本地 store 闭包校准 no-sandbox 通过;nixpkgs 受 180s 预算约束

致谢

感谢陈渝老师、周睿老师在训练营期间的指导。感谢交流群里的同学们,很多问题的思路是在讨论里逐渐清楚的。感谢开源社区,StarryOS、ArceOS、Linux 这些项目背后无数开发者的工作让后来者能站在巨人肩膀上学习。


报告日期:2026-06-28

一、项目背景

本次训练营中,我参与的工作主要围绕 StarryOS 的系统调用完善、系统回归测试和应用兼容性验证展开。StarryOS 是一个面向 Linux 应用兼容的操作系统项目,想要让更多真实 Linux 程序在 StarryOS 上运行,就需要不断补充系统调用语义、完善文件系统和管道等基础能力,同时也需要通过真实应用和测试框架验证系统行为是否符合预期。

我的工作可以分为两个阶段。

第一阶段主要围绕高级零拷贝 IO 相关系统调用展开,完成了 sendfilecopy_file_rangesplice 三个系统调用相关的实现与测试。这部分工作主要涉及文件描述符、VFS、pipe、文件偏移以及不同 fd 类型之间的数据传输。

第二阶段主要围绕 StarryOS 的应用兼容性和系统测试展开。一方面完成了 apk-cmake system regression 测试,用于验证 StarryOS guest 环境中 CMake 和 C/C++ 构建链是否能够真实工作;另一方面调研并推进了 memcached 服务端应用 stress 测试,希望通过真实服务端程序验证 StarryOS 在 socket、epoll、多线程、内存分配和基础读写等方面的兼容性。

这次训练营对我来说,不只是完成几个具体任务,更重要的是比较完整地经历了真实开源操作系统项目中的开发流程:阅读已有代码、理解接口语义、实现功能、补充测试、提交 PR、根据 review 修改、排查 CI 问题,并在反馈中不断收敛修改范围。

二、第一阶段:高级零拷贝 IO 系统调用

第一阶段的主要工作是实现和测试 sendfilecopy_file_rangesplice 三个高级零拷贝 IO 相关系统调用。

在刚开始接触这些系统调用时,我对“零拷贝”的理解还比较抽象,只知道它的目标是减少用户态和内核态之间的数据拷贝,提高 IO 效率。但实际实现后我发现,零拷贝 IO 并不是一个孤立功能,而是和文件描述符表、VFS 接口、pipe buffer、文件偏移以及不同文件对象之间的数据流动紧密相关。

这三个系统调用表面上都和数据传输有关,但它们面向的场景并不完全相同。sendfile 更偏向文件到另一个输出 fd 的传输,copy_file_range 更偏向文件到文件之间的复制,而 splice 则强调 pipe 参与的数据搬运。通过对比实现它们,我对 StarryOS 内部文件系统和系统调用层的理解比之前具体了很多。

2.1 sendfile 系统调用

sendfile 的主要作用是在两个文件描述符之间传输数据,典型场景是将普通文件中的内容直接发送到 socket 或其他输出文件描述符中。

普通的数据传输方式通常是用户程序先调用 read,把数据从输入文件读到用户态 buffer 中,再调用 write,把用户态 buffer 中的数据写到输出文件描述符。这样会经过用户态中转。sendfile 的目标则是让内核在输入文件和输出文件之间完成数据搬运,减少用户态参与。

在实现过程中,我主要处理了以下几个问题。

首先,需要根据传入的 in_fdout_fd 从当前进程的文件描述符表中获取对应文件对象,并检查文件描述符是否合法。如果输入 fd 或输出 fd 不存在,需要返回对应错误。

其次,需要处理 offset 参数。sendfile 中的 offset 语义比较重要:当用户传入 offset 指针时,读取位置应当从指定 offset 开始,并在传输完成后更新该 offset;如果 offset 为空,则使用并更新输入文件自身的当前偏移。这部分让我进一步理解了“显式 offset”和“文件对象内部 offset”之间的区别。

然后,需要根据 count 参数控制最大传输长度。实现时不能简单假设一次就能传完所有数据,而是需要按照指定长度循环读取和写入,同时累计实际传输的字节数。如果中途读到 EOF,或者写入失败,就要根据已经传输的字节数和错误情况决定最终返回值。

通过实现 sendfile,我对 StarryOS 中系统调用参数处理、fd 表查找、文件对象读写接口和文件偏移维护有了更具体的认识。

2.2 copy_file_range 系统调用

copy_file_range 主要用于在两个文件之间复制数据,可以看作是更偏向文件系统内部的数据复制接口。

sendfile 相比,copy_file_range 的一个特点是输入文件和输出文件都可能带有显式 offset,因此实现时需要同时关注输入端和输出端的偏移管理。

在实现中,我主要关注了四个方面。

第一,根据 fd_infd_out 获取输入、输出文件对象,并检查文件描述符是否合法。如果任意一端文件描述符无效,需要及时返回错误。

第二,处理 off_inoff_out。如果用户传入了 offset 指针,就需要从用户指定的位置开始读写,并在复制完成后更新对应 offset;如果没有传入 offset,则使用文件对象自身的当前偏移。这里需要分别处理输入文件和输出文件两个方向的偏移,不能混淆。

第三,按照 len 指定的长度进行复制。实际可读数据可能少于用户请求的长度,因此最终返回值应当是实际复制的字节数,而不是直接返回用户传入的 len

第四,处理边界情况。例如输入文件到达 EOF 时应当停止复制;如果复制过程中已经成功复制了一部分数据,再遇到错误,也需要根据系统调用语义返回已经完成的字节数或错误。

通过实现 copy_file_range,我对 VFS 层读写接口、文件偏移维护和系统调用参数检查有了更清楚的认识。

2.3 splice 系统调用

splice 相比 sendfilecopy_file_range 更强调 pipe 在数据传输中的作用。它可以在文件描述符和管道之间移动数据,是 Linux 零拷贝 IO 机制中比较重要的一部分。

在实现 splice 时,首先需要根据传入的输入 fd 和输出 fd 判断两端文件对象的类型。splice 的典型使用方式要求至少一端是 pipe,因此实现时需要区分普通文件、pipe 以及其他可能的文件对象,并对不支持的组合返回错误。

之后,需要处理输入端和输出端的数据传输逻辑。如果是普通文件到 pipe,需要从文件中读取数据并写入 pipe buffer;如果是 pipe 到普通文件或其他输出对象,则需要从 pipe 中取出数据并写入目标 fd。这个过程让我进一步理解了 StarryOS 中 pipe 的读写逻辑,以及 pipe buffer 在进程间通信中的作用。

此外,splice 同样需要处理 len、offset、EOF、部分传输和错误返回等问题。由于它涉及的文件对象类型更多,实现时比前两个系统调用更容易出现边界情况,因此测试也需要覆盖不同 fd 组合下的行为。

通过这一部分工作,我对零拷贝 IO 的理解不再停留在“减少内存复制”这个概念上,而是更具体地理解了系统调用如何通过文件描述符、VFS 接口和 pipe 机制把不同数据通路连接起来。

2.4 第一阶段小结

第一阶段的工作让我对 StarryOS 的文件系统和系统调用层有了更具体的理解。之前我对文件描述符、VFS、pipe、offset 这些概念的理解比较分散,但在实现 sendfilecopy_file_rangesplice 的过程中,这些概念被串联到了一起。

这三个系统调用的侧重点可以简单概括为:

系统调用 主要场景 实现关注点
sendfile 文件到输出 fd 的数据传输 输入 offset、输出写入、返回实际传输长度
copy_file_range 文件到文件复制 输入/输出双 offset、EOF、部分复制
splice pipe 参与的数据移动 fd 类型判断、pipe buffer、不同方向的数据搬运

对我来说,这部分工作最大的收获是开始真正理解系统调用不是单独存在的接口,而是和内核中的多个抽象层联系在一起。一个看似简单的数据复制系统调用,背后往往需要考虑 fd 管理、文件对象、偏移语义、pipe 行为和错误返回等细节。

三、第二阶段:apk-cmake 系统回归测试

第二阶段中,我首先完成的是 StarryOS apk-cmake 系统回归测试,对应 PR #1017。这个测试的目标是验证 StarryOS guest 环境中 CMake 以及 C/C++ 构建链是否能够真实工作。

最初我认为只要在 guest 中能够安装 cmakebuild-base,就可以说明 CMake 环境可用。但实际推进后发现,系统兼容性测试不能只停留在“工具存在”或“安装成功”这一层,而应该验证完整使用链路。

因此最终测试设计为:在 StarryOS guest 中创建一个最小 C/C++ CMake 工程,执行 cmake --versionctest --versioncmake -Ecmake -S -Bcmake --buildctest --test-dir,并最终运行生成的 C 和 C++ 程序。只有完整流程全部成功,才输出 APK_CMAKE_STABLE_TEST_PASSED

3.1 主要修改内容

本次 PR 最终主要涉及以下文件:

1
2
3
4
5
6
7
test-suit/starryos/qemu-smp1/system/apk-cmake/CMakeLists.txt
test-suit/starryos/qemu-smp1/system/apk-cmake/src/apk-cmake.sh
test-suit/starryos/qemu-smp1/system/prebuild.sh
其中,`prebuild.sh` 负责在 staging rootfs 阶段安装依赖:

```sh
apk add curl build-base cmake

这里保留 curl 是因为已有的 apk-curl-equivalence 用例仍然需要使用它,而 build-basecmake 则用于新增的 apk-cmake 测试。

apk-cmake.sh 是最终在 StarryOS guest 中执行的测试脚本。它不再执行 runtime apk add,而是直接检查 guest 中是否已经具备所需工具,并创建最小 C/C++ 工程进行 configure、build、ctest 和运行测试。

CMakeLists.txt 则负责把 staging rootfs 中的 CMake、CTest、GCC/G++、make、binutils、头文件、库文件和 CMake runtime files 显式安装到 guest overlay 中,保证 QEMU guest 运行测试时能够找到完整构建链。

3.2 测试流程设计

最终的 apk-cmake 测试不是只检查 cmake --version,而是完整覆盖了 C/C++ 工程的构建流程。

测试脚本会先检查基本工具是否存在,包括:

1
2
3
4
5
cmake
ctest
make
cc
c++

随后脚本会在 guest 的临时目录中创建一个最小 C/C++ CMake 工程,其中包含一个 C 程序和一个 C++ 程序。接着执行 CMake configure,生成构建目录,然后执行 build,最后通过 CTest 运行测试,并直接运行生成的二进制程序。

整个流程大致包括:

1
2
3
4
5
6
7
8
cmake --version
ctest --version
cmake -E
cmake -S ... -B ...
cmake --build ...
ctest --test-dir ...
运行生成的 C 程序
运行生成的 C++ 程序

只有这些步骤全部成功后,才输出:

1
APK_CMAKE_STABLE_TEST_PASSED

如果缺少工具、configure 失败、build 失败、ctest 失败或运行生成程序失败,都会输出失败 marker 并返回非零状态。这样可以避免测试“假通过”。

3.3 遇到的问题与解决过程

在实现 apk-cmake 测试时,我遇到了几个比较典型的问题。

3.3.1 测试路径错误

最初我把测试文件放到了:

1
test-suit/starryos/normal/qemu-smp1/system/...

但 reviewer 指出,StarryOS system grouped test 的发现路径是:

1
test-suit/starryos/qemu-smp1/system

并不包含中间的 normal 目录。因此后续我将测试文件移动到正确路径,并删除了错误路径下的内容。

3.3.2 PR 范围过大

早期 PR 中包含了 xattr、stress、curl、memcached、ptrace、io_uring、COW 等无关改动,导致 review 难以进行。根据 reviewer 意见,我后续重新整理分支,只保留 apk-cmake system regression 相关内容,避免无关修改影响合入。

这个问题让我认识到,真实开源项目中的 PR 不只是“代码能跑就行”,还需要保证修改范围清晰、主题单一、便于 reviewer 审查。

3.3.3 runtime apk add 导致 OOM

最开始我尝试在 apk-cmake.sh 中直接运行:

1
apk add --no-cache build-base cmake

但在 CI 中,x86_64 和 loongarch64 job 都出现了安装阶段内存分配失败的问题。

更重要的是,早期脚本曾经把安装失败当作 skip,导致测试可能显示为 green,但实际上并没有真正执行 CMake 构建流程。reviewer 指出这种做法不能证明测试真实通过。于是我移除了 runtime apk add,改为在 prebuild.sh 中提前安装依赖。

3.3.4 staging rootfs 依赖不会自动进入 guest

只在 prebuild.sh 中安装 build-basecmake 后,guest 中仍然可能找不到:

1
2
3
4
cmake
ctest
c++
make

原因是 prebuild.sh 只影响 staging rootfs,不会自动把工具复制到最终 QEMU guest overlay 中。后来我参考已有的 apk-curl-equivalence 用例,在 apk-cmake/CMakeLists.txt 中显式把所需工具和目录从 STARRY_STAGING_ROOT 安装到 guest overlay。

3.3.5 GCC 内部程序 cc1 缺失

最开始我只复制了 /usr/bin/cc/usr/bin/c++cmakectestmake 等前端命令,但 CMake configure 阶段仍然失败,错误信息类似:

1
cc: fatal error: cannot execute 'cc1': posix_spawnp: No such file or directory

这说明只复制编译器前端是不够的,还需要复制 GCC/G++ 内部目录、binutils、头文件、库文件等完整工具链依赖。因此我继续补充了 usr/libusr/lib/gccusr/libexec/gccusr/includelibusr/share/cmake 等目录,最终使 C/C++ configure、build、ctest 和运行流程完整通过。

3.4 验证结果

最终 CI 日志中,apk-cmakeqemu-smp1/system 中真实执行并通过。关键日志包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
APK_CMAKE_CTEST_BEGIN_prebuilt
Test project /tmp/cmake-build
Start 1: hello_c_runs
1/2 Test #1: hello_c_runs ................. Passed
Start 2: hello_cpp_runs
2/2 Test #2: hello_cpp_runs ............... Passed

100% tests passed, 0 tests failed out of 2

APK_CMAKE_RUN_BEGIN_prebuilt
hello from CMake C build
hello from CMake C++ build
APK_CMAKE_REPO_TEST_DONE_prebuilt
APK_CMAKE_STABLE_TEST_PASSED
STARRY_SYSTEM_TEST_PASSED: /usr/bin/starry-test-suit/apk-cmake

这说明新增用例已经完成以下完整路径:

1
2
3
4
5
6
CMake configure
C build
C++ build
CTest
运行 C 二进制
运行 C++ 二进制

这部分工作让我认识到,系统测试不应只检查命令是否存在,而应尽可能验证真实使用场景。只有完整应用链路跑通,才能说明系统兼容性有了实际提升。

四、第二阶段:memcached 服务端应用测试

第二阶段的另一部分工作是服务端应用兼容性测试调研与 memcached stress 测试推进。

4.1 从 PostgreSQL 到 memcached

最开始我调研的是 PostgreSQL。PostgreSQL 是一个比较复杂的数据库服务端应用,它依赖 mmap、thread、socket、pipe、semaphore、shared memory 等系统能力,能够较全面地验证 StarryOS 的系统调用、进程通信、网络栈和文件系统稳定性。

但在提交 PostgreSQL 测试用例后,经过社区 review 发现,当前 dev 分支中已经存在较完整的 PostgreSQL 测试用例,并且我最初提交的目录结构与现有测试框架不完全匹配。因此我没有继续重复实现 PostgreSQL,而是重新选择新的服务端应用测试目标。

之后我选择了 memcached。memcached 是一个典型的轻量级 key-value cache server,主要依赖 socket、epoll、多线程、内存分配以及基础 read/write 等系统能力。相比 PostgreSQL、MySQL 等数据库类应用,memcached 的启动链路更简单,测试目标更清晰,但仍然能有效验证 StarryOS 对网络服务、后台进程、客户端连接和数据读写的支持情况。

4.2 memcached 测试设计

我设计的 memcached 测试流程主要包括:

  1. 通过 apk 安装 memcached 和 busybox-extras;
  2. 启动 memcached 服务并确认进程存在;
  3. 使用 nc 连接本地 11211 端口;
  4. 执行 set 命令写入键值对;
  5. 执行 get 命令读取键值对;
  6. 检查返回结果中是否包含 STOREDVALUE starry_keyhello_starry
  7. 执行 stats 命令,检查输出中是否包含 STAT pid
  8. 关闭 memcached 进程;
  9. 成功时输出 MEMCACHED_TEST_DONE=<timestamp>,失败时输出 MEMCACHED_TEST_FAILED

通过这样的测试,可以验证 memcached 在 StarryOS guest 中不仅能够启动,而且能够接受客户端连接、处理基本 key-value 读写请求,并返回服务状态信息。

计划中的测试目录为:

1
test-suit/starryos/stress/memcached/

并参考已有的:

1
test-suit/starryos/stress/postgresql/

为 aarch64、riscv64、x86_64、loongarch64 四个架构分别添加 QEMU TOML 测试配置,使其能够作为 StarryOS 服务端应用压力测试的一部分。

4.3 PR review 中发现的问题

在 memcached PR review 阶段,也发现了当前提交中仍然需要继续修复的问题。

第一个问题是 PR 中误包含了与 memcached 无关的 xattr 修改。这部分修改导致 dev 分支中已有的完整 xattr 实现被回退为 stub,属于合并冲突处理错误。后续需要恢复 origin/dev 中的 xattr 相关实现,保证本 PR 只保留 memcached 测试相关改动。

第二个问题是 syscall/mod.rs 中重复添加了 xattr syscall 调度,导致出现重复 match arm。这部分也需要删除,只保留 dev 分支中原有的正确实现。

第三个问题是四个架构的 memcached TOML 配置文件中出现了重复字段,例如 argsuefito_binshell_prefix 等,并且部分文件中混入了 riscv64 的 QEMU 参数。后续需要分别清理 aarch64、riscv64、x86_64、loongarch64 四个架构的配置文件,保证每个文件只保留对应架构的 QEMU 参数和统一的 memcached 测试逻辑。

因此,memcached 这部分目前更准确地说是完成了测试目标调研、测试流程设计和 PR 推进,但仍需要继续根据 review 意见修复无关改动和配置问题。后续修复完成后,需要重新运行格式检查、差异检查,并优先在 riscv64 架构上验证通过,再扩展到其他架构。

4.4 memcached 工作的意义

虽然 memcached 测试还需要继续根据 review 意见修复,但这个方向对 StarryOS 的应用兼容性验证是有价值的。

和简单命令行工具相比,服务端应用通常会涉及更多系统能力,例如进程启动和后台运行、socket 监听和客户端连接、epoll 或类似事件机制、多线程、内存分配、文件和网络 IO、服务状态查询与退出清理等。

因此,memcached 可以作为一个相对轻量但有代表性的服务端测试目标,为后续 Redis、nginx、数据库类应用等更复杂测试场景提供参考。

五、遇到的问题和收获

这次训练营中,除了具体代码实现,我也遇到了很多工程上的问题。这些问题一开始看起来比较琐碎,但实际上对我理解真实项目开发帮助很大。

5.1 PR 范围需要足够清晰

apk-cmake 和 memcached 两个方向上,我都遇到了 PR 范围过大的问题。早期 PR 中混入了与当前主题无关的修改,导致 review 变得困难,也增加了出错概率。

这让我认识到,开源协作中一个 PR 最好只解决一个明确问题。即使本地调试过程中临时修改了很多文件,最终提交前也需要重新整理 commit 和 diff,确保 review 范围清晰。

5.2 测试不能只追求 green

apk-cmake 早期版本中,runtime apk add 失败曾被当作 skip 处理,这会让 CI 看起来通过,但实际上没有证明 CMake 构建链真实可用。

这个问题让我意识到,测试的目标不是让 CI 显示 green,而是提供可信的验证结果。对于系统回归测试来说,必须有明确的 pass marker,也必须在失败时返回非零状态。

5.3 staging rootfs 和 guest overlay 需要区分

apk-cmake 中一个重要问题是:在 prebuild.sh 中安装依赖,并不意味着这些依赖会自动出现在最终 guest overlay 中。只有显式将相关工具和依赖目录安装到 overlay,guest 运行测试时才能真正使用它们。

这个问题让我对 StarryOS 测试框架中 staging rootfs、guest overlay 和 system regression 之间的关系有了更清楚的理解。

5.4 真实应用测试比单点功能测试更复杂

memcached 测试让我认识到,真实应用往往不是只依赖一个系统调用,而是同时依赖进程、网络、内存、文件、事件机制等多个模块。一个系统调用能跑通,不代表应用一定能跑通;一个命令能启动,也不代表服务能够正常处理请求。

因此,应用兼容性测试的价值在于覆盖更完整的真实使用链路。

六、后续计划

后续我希望继续沿着 StarryOS 应用兼容性和系统回归测试方向推进。

短期内,我希望继续完善 memcached 测试,修复 review 中指出的无关改动和多架构配置问题,尽量推动其合入。

之后,我也希望在已有工作的基础上,继续尝试 Redis、nginx、数据库类应用等更复杂的服务端测试场景,逐步构建更完整的 StarryOS 应用兼容性测试体系。

同时,我还需要继续补充操作系统、Rust、文件系统、网络栈和 CI 调试方面的基础。通过这次训练营,我已经对开源操作系统项目的开发流程有了更具体的认识,之后希望能够在更复杂的任务中继续提升自己的能力。

七、总结

作为一名大一的学生,这次训练营对我来说,更像是第一次真正参与一个开源操作系统项目的完整开发过程。以前我对操作系统的理解更多来自课程、书本和一些零散实验,而这次是在 StarryOS 这样的真实项目里,围绕具体问题去读代码、改代码、补测试、提交 PR,再根据 review 和 CI 结果不断调整。

我的工作主要分成两部分。第一部分是高级零拷贝 IO 相关系统调用,实现和测试了 sendfilecopy_file_rangesplice。这部分让我对文件描述符、VFS、pipe、文件偏移和系统调用语义有了更实际的理解。第二部分是应用兼容性和系统测试,主要包括 apk-cmake 系统回归测试,以及 memcached 服务端应用测试的调研和推进。apk-cmake 让我认识到,系统测试不能只看命令是否存在,而是要验证 configure、build、test、run 这样的完整链路;memcached 则让我开始理解真实服务端应用会同时牵涉网络、进程、线程、内存和基础 IO 等多个系统能力。

我也能明显感觉到自己现在还处在打基础阶段。很多问题刚开始并不能马上看懂,需要一边查资料,一边读已有代码,再一点点把任务拆开处理。所以这次完成的工作可能还不是特别复杂,但对我来说已经很有意义:我第一次比较完整地经历了从理解需求、实现功能、补充测试,到处理 review、排查 CI、收敛 PR 范围的过程。

这次训练营让我收获最大的地方,不只是学会了某几个系统调用或某个测试怎么写,而是开始理解真实项目里的工程开发方式。代码能跑只是第一步,后面还要考虑测试是否可信、PR 是否足够聚焦、修改是否影响已有功能、CI 失败该如何判断原因。这些经验在平时课程作业里很难完整接触到。

作者: 胡鑫鸿 (aptacc2421)
仓库: rcore-os/tgoskits | 个人 fork: aptacc2421/tgoskits
跟踪 Issue: #580
训练营时间: 2026-05-11 — 2026-06-26


摘要

七周内,在 StarryOS(基于 ArceOS 的 Linux 兼容内核)上完成 8 个 upstream merged PR,覆盖 syscall 语义对齐、/proc 进程内存统计、Redis 应用支持、RK3588 板级 SMP、ACT 模型推理五个方向。

本次训练营最核心的技术故事是 /proc RSS 计账模块的开发与调试:初版设计在社区 review 中被推翻重构(从全局物理页框计数改为 per-VA BTreeMap),测试过程中又遭遇了”测量工具自身污染测量值”的问题——musl libc 的 fopen 在读取 /proc/self/status 时分配的匿名 mmap 被计入了进程自身的内存统计,导致断言恰好偏差 1 页(4KB)。定位这个根因的过程历时 12 小时、遍历了 6 条被逐一证伪的假说,最终通过内核 tracepoint 和 strace 对照实验找到了真相。

核心发现:在 OS 内核调试中,先验证观测手段、再怀疑内核;人懂底层约束(如 smp=1、fork 地址空间隔离、±1 页优先查 libc)是 AI 不可替代的环节。

方案 内容 状态
方案一 syscall 语义(memfd/pidfd/mmap/waitpid)与 /proc 内存统计 8 个 upstream merged PR
方案二 Redis 应用与 VFS/ext4 #807 merged;#808 closed(被 Kevin #802 覆盖)
方案三 RK3588 SMP / ACT 推理 #1196 merged;ACT QEMU 推理管线打通

一、背景:StarryOS 训练营选题

StarryOS 是构建在 ArceOS 模块化内核之上的 Linux 兼容内核。它与传统宏内核不同——内核模块以 Rust crate 组织,通过 #![no_std] 在裸机上运行,目标是让未经修改的 Linux 用户程序(如 Redis、psutil、Python 推理引擎)在嵌入式设备上直接运行。

训练营选题在 #580 中拆为三条线:

  1. Syscall 测例与语义完善 — 补齐 StarryOS 缺少的 Linux 系统调用(memfd、pidfd、mmap、waitpid),使其通过现有的 Linux 测试套件
  2. 拟支持 Redis — 让 redis-server 在 StarryOS rootfs 上运行,排查文件系统层的阻塞点
  3. RK3588 板级 SMP 与 ACT 推理 — 板级 SMP bring-up(GIC v3 驱动、IPI readiness、TLB shootdown);ACT(Action Chunking with Transformers)模型在 StarryOS 上的推理部署探索

以下按技术主题组织,而非时间线复述。每个主题包含”问题 → 过程 → 结果 → 收获”的完整叙事。


二、中心故事:/proc RSS 计账与 12 小时调试马拉松

这是训练营中技术深度最高、社区协作最密集、个人收获最大的模块。它包含了两个独立但接连发生的故事——一个是架构被 review 推倒重来的设计故事,另一个是测量工具自毁测量的调试故事。

2.1 问题:StarryOS 需要 per-process RSS

Linux 通过 /proc/[pid]/status/proc/[pid]/statm 向用户态暴露每个进程的 RSS(Resident Set Size,常驻内存页数),按页类型分为 Anon(匿名页,如堆/栈/COW 产物)、File(文件映射页)和 Shmem(共享内存页)。toppsutilhtop 等工具依赖这些字段。

StarryOS 原有代码仅有几个 coarse-grained 原子计数器,无法区分页类型、无法处理 fork/COW 场景中的计账转移。目标是在两个 PR 中补齐 /proc 字段(Plan1)和 per-process RSS 计账(Plan2)。

2.2 初版的架构缺陷与 review 驱动的重构

Plan1(PR #1171, 6 月 8 日 merge):暴露 /proc/[pid]/status 的 VmRSS、VmSize 等字段,搭建 C 语言测试框架。

Plan2(PR #1173, 6 月 8 日开 PR,6 月 25 日 merge,持续 17 天)的核心挑战是 COW(Copy-on-Write)场景的计账:

  • fork() 后父子进程共享已映射页面。两个进程都可以读取,但一旦有一方写入,内核触发 COW——复制物理页并修改页表。
  • COW break 将页面从 File 重分类为 Anon。这个变化需要同时反映在父进程和子进程各自的 RSS 中,且不能互相污染。

我的初版设计在物理页框层面维护了一个全局的 FrameRefCnt.kind 字段,追踪每个物理页是 File 还是 Anon。提交 PR 后,社区 Maintainer ZR233 在第一轮 review 中就指出了根本性的架构问题:

物理页框层面的全局类型标记无法区分不同进程对同一物理页的映射语义。一个进程以只读方式映射(记为 File),另一个进程写入后触发 COW(变为 Anon),但 FrameRefCnt 只存了一个 kind 值——必然有一个进程的统计是错误的。追踪状态的数据结构必须对齐隔离边界:如果隔离单位是进程,计账结构就应该是 per-process 的。

这句话成为了整个 redesign 的指导原则。最终架构改为每个 AddrSpace 持有一份独立的计账结构:

1
2
3
4
5
6
7
struct MemoryAccounting {
rss_anon: AtomicU64, // Anon 页原子计数
rss_file: AtomicU64, // File 页原子计数
rss_shmem: AtomicU64, // Shmem 页原子计数
generation: AtomicU64, // 每次 BTreeMap 修改后单调递增
charges: BTreeMap<VirtAddr, RssKind>, // 虚拟地址 → 页类型
}

每个进程拥有独立的 BTreeMap,VA → RssKind 映射。populate(首次映射)、unmap(解除映射)、fork(clone_map 复制 charge map)、COW break(remove File + record Anon)四条路径各自通过 record_charge/remove_charge 调用更新计账。fork 时 try_clone() 创建全新的 AddrSpace,逐 VMA 调用 clone_map 将父进程的 charge 复制到子进程——父子进程的 MemoryAccounting 从 fork 完成那一刻起就是两块独立的物理内存。

这个架构不是 AI 设计的,也不是我独立设计的。它是在 ZR233 指出”从 global 到 per-process”的方向后,我在 review 讨论中逐步调整出来的。这是训练营最有价值的经历——不是”写对了代码”,而是”被指出了为什么原来的设计是错的”。

2.3 现象:恰好差 1 页

测试由 Cursor autocomplete 参照 Linux kselftest 风格生成骨架,后经人工迭代重构。覆盖了以下场景:

  • fork 后父子进程的 RSS 值是否一致(继承正确性)
  • 子进程对 COW 共享页写入后,Anon 是否 +1(COW break 计账)
  • 父进程的 RSS 在子进程写入后是否保持不变(计账隔离)

就在验证 test_fork_reclassify_writer_sibling_unmap 这个用例时,遇到了训练营期间最诡异的问题:

测试断言 fork 后子进程 RssAnon 应为 24 页(父进程 23 页 + COW 新增 1 页),但实际读到 23 页。进一步定位发现——父进程在读取 /proc/self/status(时刻 T1)时得到 anon=23,在调用 fork() 内部做计账快照(时刻 T2)时变成 anon=22。而 T1 和 T2 之间的用户代码只有两行 pipe() 系统调用。

内核在没有 munmap/mprotect/mmap 的情况下,anon 自动少了 1 页。我开始追查”内核 charge 泄漏”。

2.4 6 条被逐一证伪的假说

在接下来的 12 小时内(跨越 6 月 13 日下午至 6 月 14 日凌晨,共 59 轮 CLI 对话),我通过 ChatGPT Web(对话策略规划)和 Claude CLI(内核代码搜索与日志分析)协同,在内核中添加了一系列 tracepoint(内核日志插桩),逐一提出并排除了以下假说:

# 假说 证伪过程和关键证据 如果早知道的话…
1 clone_map 路径发生 AB-BA 死锁,导致测试超时后数值错乱 在 fork 路径插入 CLONE-PROGRESS 日志,发现只是串口 115200 波特率下的日志输出瓶颈——串口打日志太慢导致 QEMU 超时
2 BTreeMap 中的 charge 条目消失(”138→95 泄漏”) 早期分析混淆了全局 RECORD_OK 计数器(所有进程共享)和 per-process RSS。加了 pid 过滤的 CHARGE-MUTATION 日志后证明每个进程的 BTreeMap 条目数稳定
3 sync_rss_atomics_from_charges() 的 load-compare-store 被另一 CPU 并发修改,导致原子计数被错误覆写 插入 SYNC-OVERWRITE 计数器,值永远为 0——sync 在任何调用中都是 no-op(atomics 在 sync 前后始终相等) 系统配置是 smp=1,只有一个 CPU 核心。不存在”另一 CPU 并发”。 这个信息写在配置文件里,不需要任何日志就能确认
4 fork 继承(clone_map)遗漏了某个 VMA,导致子进程少了一个 charge 在 fork 路径插入 FORK-INHERIT 日志——每个 fork 边界上 parent_anon == child_anon 完全一致,继承完全正确
5 子进程退出时的地址空间清理(aspace_clear)意外修改了父进程的 charge map 给所有 unmap 路径添加 caller= tracepoint,发现 23→22 的 remove_charge 来源是 sys_munmap,不是 aspace_clear。不同 acct_ptr,不同地址空间 fork 创建独立的地址空间,子进程的页表和 BTreeMap 与父进程完全隔离。子进程退出时 unmap 的是自己的 VA,不可能碰到父进程的数据结构。 这是虚拟内存的基本语义
6 MemoryAccounting 对象被释放后,同一块内存被 allocator 复用给了新对象(”指针地址复用”),导致看起来像同一个对象在变异 给 MemoryAccounting 加入单调递增的 generation_id:T1 时 gen=1168,T2 时 gen=1169,同一个 acct_ptr 上 generation 只增了 1——同一对象的连续 mutation,不是 allocator 复用

假说 #3 和 #5 特别值得反思:smp=1 在 QEMU 配置文件里写着,fork 地址空间隔离在任何 OS 教材的第一章就讲了。这些不需要猜——是已知事实。但在密集的日志插桩和假说枚举中,这些”明显的事实”被绕过了。

2.5 突破:从 tracepoint 到 fopen

转折点来自于新增的 SYSCALL-MMAP/SYSCALL-MUNMAP tracepoint(记录每个 mmap/munmap 系统调用的 VA、长度、是否为文件映射)。在 qemu9.log 中捕获到:

1
2
[SYSCALL-MMAP]  ts=2697269616 pid=23 va=0x16000 len=0x1000 file=false
[SYSCALL-MUNMAP] ts=2699002320 pid=23 va=0x16000 len=0x1000

三个关键线索:

  1. file=false — 这是一个匿名映射,不是测试代码中创建的 MAP_PRIVATE 文件映射(后者的 VA 是 0x15000,file=true
  2. 映射长度恰好 4096 字节(1 页) — 不像数据区,像缓冲区
  3. mmap 时间在 T1(/proc 读取)之前,munmap 在 T2(fork)之前 — 它在测量窗口内存在,在测量窗口后消失

回溯测试源码找到调用者:测试在读取 RSS 基线值时调用了 fopen("/proc/self/status", "r")。问题的最后一块拼图来自 libc 实现差异的知识:

musl libc(StarryOS 使用的 C 标准库)的 fopenFILE 结构体的缓冲区分配一个 4096 字节的匿名 mmap(MAP_PRIVATE | MAP_ANONYMOUS)。而 glibc 的 fopenbrk 分配堆内存——因此同样的测试代码在 glibc 环境下不会触发此问题。

完整的污染链:

1
2
3
4
5
6
测试调用 fopen("/proc/self/status")
→ musl: mmap(4096, MAP_ANONYMOUS) → 内核 record_charge → anon: 22→23
测试调用 fgets 读取 "RssAnon: 92 kB" → 读到 23 页(多了一页 fopen 的缓冲区)
测试调用 fclose → musl: munmap(4096) → 内核 remove_charge → anon: 23→22
测试调用 fork() → 子进程继承 anon=22(此时 fopen 缓冲区已不存在)
断言期望: anon=23 + 1(COW) = 24 → 实际 anon=22 + 1 = 23 → FAIL

根因不是内核 bug,不是测试逻辑 bug,是测试工具本身改变了被测量的状态——这是”观察者效应”在内核调试中的实际案例。

2.6 修复与验证

修复本身仅 62 行:将五个 /proc 读取函数从 fopen/fgets/fclose 改为 POSIX 原语 open/read/close——栈缓冲区不产生额外的 mmap,RSS 测量不再受测量工具污染。

独立验证步骤:

  1. 在 WSL(宿主机)上用 strace 对比 musl 和 glibc 的 fopen 行为——musl 确实多了一条 mmap(NULL, 4096, ...),glibc 走 brk
  2. StarryOS QEMU 上跑修改后的测试——所有断言通过
  3. 在 StarryOS QEMU 上跑修改前的测试 + 手动 strace——确认 mmap 存在且 RSS 多一页

三、syscall 语义对齐:memfd、pidfd、mmap、waitpid

memfd 和 pidfd 是 Linux 较新的文件描述符类型。它们有一个共同的动机:传统的文件描述符依赖于文件路径或 PID 整数,前者需要管理命名空间,后者存在 PID 复用风险(进程退出后 PID 可能被分配给新进程)。memfd 创建匿名的内存文件(无磁盘路径),pidfd 以稳定的”进程引用”替代 PID 整数。

StarryOS 需要支持这些 syscall 以通过现有的 Linux 测试套件。以下是对齐工作的四个 PR。

3.1 memfd_create 与 seals

memfd_create("name", flags) 创建一个只存在于内存中的匿名文件,返回 fd。访问方式与普通文件完全相同(read/write/mmap),但无磁盘后盾。

在 StarryOS 中的实现:利用 tmpfs 创建匿名 inode,挂载到内部 tmpfs 实例。支持四种 seal(F_SEAL_SHRINK/F_SEAL_GROW/F_SEAL_WRITE/F_SEAL_SEAL)——一旦设置了某种 seal,对应操作被内核拒绝。测试使用 kselftest 风格,每个 seals 组合独立验证。

已知差距F_SEAL_FUTURE_WRITE(阻止后续写映射)在当前实现中不支持;不支持 MEMFD_NOEXEC_SEAL 或大页(MFD_HUGETLB)。这些都是向后兼容的——不对现有功能造成回退。

3.2 pidfd:三个 syscall

syscall 行为 实现要点
pidfd_open(pid, flags) 打开目标进程的 fd(不受 PID 复用影响) StarryOS 对 flags 严格检查,仅支持 PIDFD_NONBLOCK
pidfd_getfd(pidfd, targetfd, flags) 从目标进程复制一个 fd 到当前进程 flags 必须为 0;在内核中持有目标进程的 fd_table 锁进行复制
pidfd_send_signal(pidfd, sig, info, flags) 通过 pidfd 向目标进程发送信号 siginfo 为 NULL 时使用默认值

测试覆盖了跨进程 fd 传递、进程回收后的 pidfd 行为、无效参数的 errno 检查。StarryOS 与 Linux 在语义上的主要差异是:StarryOS 对 flags 参数的检查比 Linux 更严格(未知 flag 返回 EINVAL 而非忽略)。

3.3 MAP_FIXED 与 waitpid:两个小语义修正

MAP_FIXED(PR #691,7 行核心改动):Linux 的 mmap(MAP_FIXED) 语义要求先做权限检查,再 unmap 旧映射——如果新映射的权限不足,必须保留旧映射不变(”原子”语义)。StarryOS 原实现先 unmap 再检查,导致权限不足时旧映射已丢失。修复为:先对重叠区域做权限预检,仅在通过后才执行 unmap+mmap。

waitpid(PR #686,4 行核心改动):退出码写入(vm_write)必须在子进程从进程表中被移除(reap)之前完成——否则父进程可能读到未初始化的 siginfo。这是一个典型的多线程同步问题——“写完整才能读”的约束需要用正确的调用顺序保障,而非锁。

3.4 测试方法

syscall 测试以 C 语言编写,组织在 StarryOS 的 grouped qemu-smp1 system test 框架下。测试模式参照 Linux kselftest:每个 syscall 先用正常参数验证功能,再用非法参数验证 errno(如给 memfd_create 传非法 flags、给 pidfd_open 传不存在的 PID)。

测试骨架初期由 Cursor autocomplete 快速生成,后期人工补全——AI 擅长生成”一个正常参数的测试用例”骨架,但边角 case(seal 组合爆炸、标志位互斥)和 errno 对照表需要人逐条校对 Linux man page 确认。


四、应用与板级

4.1 Redis:ext4 文件系统阻塞点

Redis 在 StarryOS 上启动后,AOF(Append-Only File)持久化流程的 rewrite 操作阻塞在 rename("temp-rewriteaof-xxx", "appendonlydir/"),返回 EINVAL。AOF rewrite 是 Redis 保证数据持久性的关键流程——阻塞在此意味着 Redis 能启动但不能持久化。

排查从症状收敛到两层缺陷:

  1. VFS 层祖先检查过严axfs-ng-vfs 的 rename 实现中,跨目录 rename 的合法性检查逻辑错误地将 temp-rewriteaofappendonlydir/ 判为非法——它误认为目标父目录和源文件不能在同一文件系统层级
  2. ext4 层 hash-tree dentry delete:删除目标位置的旧目录项时,ext4 hash-tree 索引未正确更新,导致后续在目标路径上的查找操作失败

PR #807 修复了这两个问题。整个过程是从症状逐步缩小到具体代码行的——初始假设(ext4 有通用 rename 问题)过于宽泛,最后发现是两个独立但需要同时修复的缺陷。

教训:开应用层 Redis test-suit PR(#808)前,未先检查社区是否有同类 PR——等我提交后发现 Kevin #802 已经覆盖了相同的范围。#808 关闭。开源协作中,开 PR 前先搜索 upstream open PR 是基本流程

4.2 真机 SMP:Orange Pi 5 Plus IPI readiness

在 Orange Pi 5 Plus(RK3588, 8 核 Cortex-A76 + Cortex-A55)真机上测试 StarryOS SMP 时,开启 max_cpu_num>1 后立即触发 panic。

根因:TLB shootdown(多核间同步页表变更的操作)需要向所有 CPU 核心发送核间中断(IPI),但 IPI 子系统(GIC v3 中断控制器驱动 + 核心间通信队列)在启动早期尚未完成初始化。第一个 TLB shootdown 请求到达时,IPI 发送函数访问了未初始化的 GIC 寄存器,触发 panic。

修复(PR #1196):在 axruntime 启动流程中插入 IPI 就绪同步屏障,确保 IPI 子系统在应用代码执行前完成初始化。关键改动在 GIC v3 驱动和 axtask 的 SMP online 流程中,保证 secondary CPU bring-up 与 IPI 初始化的顺序依赖。

修复前曾使用 max_cpu_num=1 workaround 临时绕过(仅启动单核),但最终交付的是根因 fix。workaround 是临时工具,upstream patch 需要解决根因——区分临时绕过与最终修复是参与开源项目的基本素养

4.3 ACT 模型推理:认识 Transformer

Pro57 比赛要求将 ACT(Action Chunking with Transformers)模型部署到嵌入式平台。ACT 是一个 Encoder-only 的 Transformer 变体,用于机器人动作预测。我的工作打通了从 PyTorch 模型到 StarryOS QEMU 的推理管线:从 PyTorch checkpoint 导出 ONNX 格式(194MB),用 Rust tract 推理引擎交叉编译为 riscv64 musl 静态二进制,在 StarryOS QEMU RISC-V 虚拟机上加载并成功推理。还做了 structured pruning 实验(按比例保留权重后逐层测量内存占用)和 RISC-V Vector 扩展 microbenchmark。

在这个过程中,我对 Transformer 有了从”纸上公式”到实际计算图的认识:Multi-Head Self-Attention 的 Q/K/V 投影、Feed-Forward Network、Layer Normalization 这些组件在 ONNX 计算图中是什么形态,推理时内存主要消耗在哪(中间激活张量通常比权重矩阵更大),以及 tract 这类推理引擎在编译期做的图优化(常量折叠、算子融合)。这些认识来源于逐算子梳理计算图、测量每层内存占用的过程——不是读论文能得到的。

StarryOS 侧的改动是 app 层 QEMU smoke,推理栈工作在独立工作区——将”内核能跑”和”推理栈优化”分开,互不阻塞。

五、量化成果

5.1 Upstream merged PR

PR 合并日 标题 +/-
#565 05-19 feat: anonymous memfd, seals, and pidfd tests +2064/-144
#686 05-19 fix: waitpid reap order (exit code before reaping) +7/-3
#691 05-19 fix: MAP_FIXED failure preserves prior mapping +106/-22
#707 05-24 fix: pidfd open/getfd/send_signal conformance +1406/-30
#807 05-21 fix: ext4 rename & VFS ancestor check +477/-225
#1171 06-08 feat: /proc memory stats exposure +869/-34
#1173 06-25 fix: Cow RSS per-VA charge tracking +2270/-365
#1196 06-12 fix: aarch64 SMP IPI readiness +159/-22

5.2 Issue #580 完成度

子项 状态 关键 PR
memfd + seals + pidfd ✅ merged #565, #707
waitpid / MAP_FIXED ✅ merged #686, #691
Redis VFS blocker ✅ merged #807
/proc RSS ✅ merged #1171, #1173
Orange Pi SMP ✅ merged #1196
Redis app test-suit ⏸ 被覆盖 #808 closed(Kevin #802)

5.3 其他统计

  • Closed unmerged PR:5 个早期 fix 分支(内容并入 #565/#707)+ #808(被 Kevin 覆盖)
  • 对已有功能无 regression(所有 merged PR 通过了 QEMU CI 和板级测试)

六、经验与教训

6.1 约束前置:设计函数时把所有不合法的路径判死

做 OS 不能假设调用者(用户程序)按预期行为,也不能假设内核自己的状态总是合法的。以 RSS 为例——如果 snapshot_resident_charges() 返回时断言 BTreeMap 条目统计值 == 原子计数器值,fopen 贡献的额外 1 页 Anon 会在第一次运行时就触发断言失败,直接定位到”有人多计了一页”,不需要追 12 小时。

测例设计同理——AI 生成的测例倾向于假设”前面的步骤都正确”,断言只检查最终输出。但在后期遇到的一个动态链接缺页 bug 中,第一步就错了(共享库映射缺少一个页),后续所有断言都在错误的基础上推演。每个测试步骤都应独立自证前置条件。

6.2 人依然需要懂底层:AI 不会主动排除不可能

RSS 调查最值得反思的不是”AI 花了 12 小时才找到根因”,而是其中 4 条假说不应该被提出——它们违反了已知的约束条件:

  • smp=1(单核)→ 不存在跨 CPU 并发修改 → sync 覆写假说自动作废
  • fork 不共享地址空间 → 子进程退出清理不可能碰到父进程的 BTreeMap → aspace_clear 假说自动作废
  • 偏差恰好 1 页(4KB)→ 最可能的解释是 libc/动态链接产生的临时映射 → 应该先查 libc,再查内核
  • 已知 musl 用 mmap 做 FILE 缓冲区而 glibc 用 brk → 用 strace 在两种 libc 上对照测试,可以快速排除内核嫌疑

这些不是需要”猜”的知识,是操作系统教材的第一章、man page、libc 源码。AI 在长上下文推理时会丢失这些”明显的事实”——smp=1 在 300 轮对话之前提过一次,之后不再是 active context。人会记住”我们只有一个核”因为这是物理直觉,AI 不会——除非被反复提醒。

人作为审计员的核心价值:当 AI 提出假说时,先问”这个假说违反了哪些已知约束”,而不是”这个假说有没有证据”。AI 擅长枚举可能性——从 100 种可能的原因中找出最像的 10 个。人擅长排除不可能性——从 10 个最像的原因中去掉违反物理/语义约束的 8 个。

6.3 从 review 中学架构

#1173 初版 global FrameRefCnt 被完全推翻,然后重新设计为 per-VA BTreeMap。ZR233 的那句”追踪状态的数据结构必须对齐隔离边界”不仅是代码修改建议——它是一条 OS 设计原理的具体应用:状态追踪的粒度不应小于隔离的粒度。 如果隔离单位是进程(每个进程有独立的地址空间),那么计账数据的归属就应该是 per-process 的。

这种架构直觉不是 AI 能教的——它不仅需要读懂代码,还需要理解设计者的意图、识别意图与实现之间的偏差、并提出”向哪个方向改”的指导。这是 review 文化最宝贵的价值。

6.4 每个阶段写 blog,不要拖

训练营第一周我完成了 memfd/pidfd 的 syscall 实现,当时的细节还很清晰——seal 的组合语义、pidfd 的 flags 检查与 Linux 的差异点、MAP_FIXED 的原子性保证。如果能在那时趁热写一篇 blog,不仅是给社区留下参考资料,更是对自己理解的一次强制检验——写清楚一件事情比做清楚一件事情更难。

但我没有。我把 blog 拖到了训练营最后。结果是:6 月中下旬期末考试和各种实验同时压上来,memfd 的设计细节已经模糊了,只能靠着 git log 和 PR 描述来反推当时的决策。最终只来得及写 RSS 调查这一篇 blog,而 syscall、Redis ext4、Orange Pi SMP 的故事都没有被记录下来。

每个阶段结束时写 blog,把 blog 当成阶段交付物的一部分,而不是事后补的作业。 即时写 blog 的成本是半天,事后靠回忆重建的成本是同样的半天——但事后写的质量差得多,而且大概率根本没时间写。

6.5 先查规范,再读代码;先验证测量工具,再怀疑被测对象

三条具体教训:

  1. 先查社区 open PR:避免重复工作(#808)
  2. 先查 man page / 标准文档:确认”应该是怎样”——AI 擅长 grep 代码查”实际是怎样”,但规范回答得更快
  3. 先验证测量工具/proc 的读取行为(fopen mmap)本身就是 RSS 测量的一部分——在 OS 调试中,观测手段往往是 bug 的一部分,而非独立于 bug 之外

七、AI 协作:放大器,不是替代品

本次训练营的主要生产工具是 AI(Cursor + Claude CLI + ChatGPT Web)。真实情况是三者在不同阶段犯错、也在不同阶段起到了不可替代的作用:

  • Cursor autocomplete:在 C 测试用例编写时提供了大量骨架代码,但也随手写了 fopen——它不知道 musl 的 fopen 会 mmap
  • Claude CLI(通过 DeepSeek API):在内核代码搜索、tracepoint 插桩、日志 pattern 识别上远超人工速度——但它会在第 59 轮对话中忘记第 1 轮就确认过的 smp=1
  • ChatGPT Web:擅长规划结构化调查框架(如 “Audit Mode:Q1-Q5,只允许已证实事实,禁止猜测”),但不擅长从具体日志中推断因果关系

人的不可替代部分:

  • 物理世界验证:在 QEMU/真机上跑测试、读串口输出、用 strace 做对照实验——AI 只能生成命令,不能确认结果
  • 读懂 review 并调整架构方向:从 “global FrameRefCnt 有缺陷” 到 “per-VA BTreeMap” 的跳跃不是 AI 独立完成的
  • 判断什么是不可能的:知道 smp=1、fork 语义、libc 差异——这些是领域知识,不是 prompt 技巧

AI 在此次训练营中的本质角色是认知放大器——放大生产力(快速生成代码骨架、批量搜索日志 pattern),也放大认知偏差(在错误前提下快速递推几十轮)。与 AI 协作最核心的技能不是”写好 prompt”,而是能在 AI 的输出中识别哪些结论违反了已知约束——然后删掉,不追

训练营让我意识到:AI 的好处和危险是同一个——它能让你以更快的速度到达任何地方,无论那个方向是对的还是错的。所以驾驭它需要驾驶者自己知道方向。


致谢

感谢周睿老师(ZR233)在 RSS 模块开发过程中给予的耐心指导。感谢陈渝老师在训练营期间的组织和指导,感谢 Mr Graveyard、Ajax 老师以及所有参与训练营的老师们提供的帮助。

感谢一起参与训练营的同学们。许多问题的思路并不是在敲代码时产生的,而是在讨论、争论和交流中逐渐清晰的。感谢刘柯、王夯以及交流群中的伙伴们,大家分享的经验、踩过的坑和提出的问题,都让我受益匪浅。

更要感谢开源社区。无论是 StarryOS、ArceOS,还是 Linux、RTEMS,这些项目背后无数开发者长期积累的工作,让后来者能够站在巨人的肩膀上学习。对于一个学生而言,能够参与真实项目、阅读真实代码、接受真实 review,本身就是一种幸运。

从三月到六月,训练营持续了四个月。回头看,收获最大的并不是完成了多少 PR,也不是解决了多少 Bug,而是第一次真正接触到了系统软件开发的过程:阅读代码、理解设计、提出假设、验证实验、接受 review、推翻错误的方案,再重新构建更合理的实现。

训练营期间最深刻的体会来自 RSS 调试。一个看似简单的”一页误差”,最终追查到了 libc 的实现细节;一个最初看起来合理的设计,在 review 中被指出了根本性的架构缺陷。相比于”找到答案”,这些经历让我学会了如何接近答案。

参加训练营之前,我对操作系统的理解更多来自课本和课程实验。训练营结束后,我依然只是一个刚刚入门的学习者,但已经能够透过现象去观察系统背后的机制。那些曾经看起来理所当然的事情——进程、文件、内存、fork、COW、系统调用——不再是抽象概念,而是具体的数据结构、代码路径和硬件约束。

计算机的世界里没有魔法。

所谓操作系统,不过是把这些”魔法”拆开来看;而所谓成长,大概就是逐渐学会理解它们为何如此运作。

感谢这段经历,也感谢一路上帮助过我的每一个人。


附录 A:技术地图

下图展示了本次训练营涉及的各模块及它们之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
Application Layer
├── Redis ──→ VFS rename ──→ ext4 dentry hash-tree
├── psutil/top ──→ /proc pseudo-fs ──→ MemoryAccounting (BTreeMap<VA,Kind>)
└── ACT infer ──→ StarryOS QEMU RISC-V

Kernel Layer
├── Syscall: memfd_create / pidfd_open / pidfd_getfd / pidfd_send_signal
├── MM: COW break → remove_charge(File) + record_charge(Anon)
├── fork: try_clone() → clone_map → reconcile_fork_charges_from_parent()
└── aspace_clear: AddrSpace::clear() on process exit

Hardware Layer
└── RK3588 (Orange Pi 5 Plus) ──→ GIC v3 IPI ──→ SMP secondary CPU bring-up

附录 B:工作时间线

时间段 模块 关键事件
05-11 → 05-19 Syscall memfd/pidfd 内核实现 + 测试;#565/#686/#691 merge
05-16 → 05-22 Redis 诊断 rename EINVAL;#807 merge;#808 发现与 Kevin #802 重复后关闭
05-20 → 05-24 Syscall pidfd 完善 + #707 merge;MAP_FIXED 测试补全
05-23 → 06-06 ACT ONNX 计算图分析、tract 推理引擎集成、QEMU 推理验证、内存 profile
06-07 → 06-08 /proc RSS #1171(Plan1)merge:暴露 /proc 字段
06-08 → 06-25 /proc RSS #1173(Plan2)开发:global FrameRefCnt → per-VA BTreeMap;6 轮 review 迭代
06-09 → 06-12 Orange Pi #1179 真机 panic 诊断 → #1196 IPI readiness merge
06-13 → 06-14 /proc RSS RSS 12 小时调试马拉松:6 条假说证伪 → fopen 观察者效应根因 → 修复验证
06-15 → 06-26 /proc RSS COW PTE 修正、fork-rw 测试重构、musl 缺页文档、CI 重跑 → #1173 merge

报告日期:2026-06-27

StarryOS 宏内核 GDB支持项目汇报

零、文档结构

背景 讲述本次项目做了什么内容,以及为什么做

第一二阶段工作 讲述 syscall 测例、语义修复、以及支持具体 linux 应用时做出的修复工作

第三阶段工作报告 细致讲述了 GDB 是如何通过 sys_ptrace 来支持的,讲解了 ptrace 里状态机迁移的细节过程,信号处理流程,以及多线程 ptrace是如何处理的,最后解读了我认为值得品味的代码

测试与成果 介绍了测试代码大致结构,展示了真实可用的 gdb demo 截图

PR 记录与工作仓库 记录了我的 issue 链接以及所有的 PR 链接

总结与感想 记录了我参加训练营的感悟

文档中相当多内容都是我手敲的,虽然内容较冗长。

一、背景

本阶段我的工作先完成文件权限相关 syscall 的语义对齐和测试补齐,再补充 BusyBox、Lua、Redis 等应用场景。随着应用支持逐渐深入,单纯依靠日志、panic 信息和测试输出已经不足以定位复杂用户态问题,因此后续目标转向支持 GDB。GDB 支持的意义不只是“能运行一个调试器”,更重要的是让 StarryOS 具备真实 Linux 应用调试链路:可以观察进程状态、设置断点、读取寄存器、查看调用栈、单步执行,并通过 gdbserver 与宿主机调试器交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
                用户态
+---------------------------------------+
| GDB |
| 或者 gdbserver |
+-------------------▲-------------------+
│ ptrace()
│ waitpid()

==================== 系统调用 ====================

内核态
+---------------------------------------+
| ptrace subsystem |
| |
| TASK_TRACED |
| signal delivery |
| wait queue |
| register access |
| memory access |
| scheduler |
+-------------------▲-------------------+


被调试程序(tracee)

我的设计目标并不是一次性实现完整 Linux GDB/ptrace 兼容,而是先形成一个可用的调试子集。这个范围后来被收敛为“单进程 GDB 可用子集”:优先保证 riscv64 上 guest 内 native GDB、guest 内 gdbserver、host-to-guest remote GDB,以及简单 pthread 程序的可用调试语义。

二、宏内核项目第一、二阶段工作

第一阶段:Linux 基础 syscall 语义补齐

第一阶段主要围绕文件权限、所有权和基于 dirfd 的路径解析语义展开。目标让 StarryOS 对常见 POSIX 文件权限操作的行为更接近 Linux,并通过系统调用测例固化这些语义。

这一阶段覆盖的 syscall 包括:

  • chmod / fchmod / fchmodat
  • chown / fchown / fchownat
  • faccessat
  • umask

主要修复和设计点如下:

  1. 统一绝对路径与 dirfd 的处理规则

    Linux 的 *at 系列 syscall 有一个关键语义:当传入路径是绝对路径时,dirfd 应该被忽略;只有相对路径才需要以 dirfd 指向的目录作为起点。早期实现中,不同 syscall 分散处理 dirfd,容易出现某个 syscall 行为和 Linux 不一致的问题。

    因此我将绝对路径处理下沉到统一路径解析逻辑中,避免每个 syscall 单独写一份 lookup_dirfd 分支。这样 fchmodatfchownatfaccessat 等接口可以共享同一套路径解析语义,减少重复逻辑和边界条件不一致。

  2. 修复 fchownat flags 与错误码语义

    fchownat 不只是 chown 的 dirfd 版本,它还涉及 flags 校验、符号链接处理、绝对路径忽略 dirfd、非法 fd 返回错误等语义。PR588 为 fchownat 增加了独立测例,覆盖 Linux 兼容行为,包括:

    • 绝对路径下忽略 dirfd
    • 相对路径下使用 dirfd
    • 非法 flags 返回错误
    • 符号链接相关行为
    • bad dirfd 与普通路径的组合情况
  3. 修复 umask 只保留低 9 位

    Linux 中 umask 只使用权限掩码的低 9 位,也就是 0o777 范围。PR605 中将 sys_umask 的输入进行低 9 位截断,避免高位影响后续文件权限计算。

  4. 补齐跨架构 syscall 回归测试

    第一阶段不仅增加单个 riscv64 测试,还将新增 syscall 测例注册到多架构 QEMU 配置中,包括 riscv64、aarch64、loongarch64、x86_64。这样后续修改 VFS 或 syscall 语义时,可以避免只在一个架构上通过、其他架构退化的问题。

通过这一阶段,StarryOS 的文件权限和 *at 系列接口从“能调用”推进到“具备更接近 Linux 的路径解析、flags 校验和错误码语义”,为后续真实应用测试打基础。

第二阶段:应用场景驱动的兼容性修复

第二阶段从单个 syscall 的语义补齐,推进到 BusyBox、Lua、Redis 等实际应用场景。相比第一阶段,这一阶段的特点是:测试不再只验证某个 syscall 的返回值,而是通过真实应用暴露内核缺失的 procfs、socket、文件系统和网络协议细节。

1. BusyBox applet 测试与网络/procfs 兼容修复

BusyBox 是一组高度依赖 Linux 基础接口的命令行工具集合。为了让 StarryOS 能稳定运行 BusyBox applet,我补充了多个 applet 场景,并根据测试暴露的问题修复内核语义。

主要修复点包括:

  1. 补充网络设备 ioctl 支持

    BusyBox 的 ifconfigifenslave 等工具会查询网络设备信息,依赖 SIOCGIFCONFSIOCGIFFLAGSSIOCGIFADDRSIOCGIFHWADDRSIOCGIFMTU 等 socket ioctl。早期 StarryOS 对这些 ioctl 支持不足,导致工具无法正常识别 eth0lo

    我在网络文件路径中补充了这些 ioctl 的兼容返回,使 BusyBox 能获得接口列表、接口 flags、IP 地址、MAC 地址和 MTU 等基础信息。

  2. 补充 /proc/net/dev

    BusyBox 和一些网络工具会读取 /proc/net/dev 来获取网络设备统计信息。StarryOS 原来缺少该 procfs 节点,因此我在 pseudofs/proc 中补充了 /proc/net/dev,为 eth0lo 提供基本设备条目。即使统计值暂时是简化的,也能满足用户态工具对接口存在性的探测(后续可能已经更改)

  3. 修复 raw socket 下 ICMP loopback echo reply

    BusyBox ping -c 1 127.0.0.1 需要 raw socket 能收到 loopback ICMP echo reply。早期实现中 loopback reply 的生成位置不合适,可能绕过正常网络栈路径,影响其他 raw socket 测例。

    后续修复将 ICMP loopback reply 的生成移动到 smoltcp 发送路径的合适位置:既能让 BusyBox ping 收到回包,又保持 raw socket 数据仍通过正常网络路径投递,避免破坏 MSG_PEEK 等测试。

  4. 清理非语义 BusyBox 测例

    PR752 对 BusyBox 测试做了整理,删除不稳定或不具备明确内核语义价值的检查,使 applet 测试更聚焦于 StarryOS 应该保证的 Linux 兼容行为,而不是测试环境偶然输出。

这一阶段 BusyBox 测试从“能启动部分命令”推进到覆盖更多网络、procfs 和 shell 工具场景,也暴露了仅靠 syscall 单测难以发现的接口组合问题。

2. Lua / LuaRocks 运行时测试与文件系统语义修复

Lua 场景的目标是验证解释器、脚本加载、模块 require、文件访问和 LuaRocks 安装使用链路

主要工作包括:

  1. 新增 Lua 应用运行测试

    我为 Lua 添加了 QEMU app 测试,覆盖 Lua 主脚本、辅助模块、require、文件加载等运行时行为。相比 syscall 单测,Lua 能更真实地覆盖动态解释器对文件系统、路径解析和运行时库的使用。

  2. 新增 LuaRocks 测试

    LuaRocks 会涉及包安装、模块路径、文件创建和 require 验证。我补充了 LuaRocks 相关测试,验证在 StarryOS 中安装并加载 Lua 包的基本流程。

  3. 修复非空目录 rmdir 语义

    Lua/LuaRocks 场景暴露出文件系统删除目录语义不完整的问题:对非空目录执行 rmdir 应该返回 ENOTEMPTY,而不是错误地允许删除或返回不符合 Linux 的错误。PR777 在 axfs-ng 的 high-level fs 路径中加入非空目录检查,并新增 test-rmdir-nonempty 回归测试,覆盖四个架构。

这一阶段的意义在于:Lua 作为解释型语言,能覆盖大量普通 C syscall 测例不容易组合出的路径,例如脚本查找、模块加载、目录删除、包管理器临时目录等。

3. Redis 应用测试与网络/文件系统稳定性修复

Redis 是更复杂的真实服务端应用,涉及 TCP listen/accept、并发连接、非阻塞 connect、半关闭、recv、文件 rename 和压力场景

主要工作包括:

  1. 设计 Redis normal 与 stress 测试

    我将 Redis 场景整理为 Starry app,新增 normal 和 stress 两类测试:

    • normal 测试验证 Redis server 基础启动、客户端连接和基础命令交互。
    • stress 测试覆盖更高频连接和读写场景,用于暴露网络栈边界问题。

    同时为 riscv64、aarch64、loongarch64、x86_64 增加对应 build 和 QEMU 配置,使 Redis 测试可以跨架构运行。

  2. 修复 TCP listen/accept 路径

    Redis server 依赖稳定的监听和连接接受语义。PR802 修复了 ax-net-ng listen table 中和连接管理相关的问题,使 TCP server 能更可靠地处理客户端连接,避免并发连接或非阻塞 connect 场景下状态错误。

  3. 修复 TCP half-close 与 recv 语义

    Redis 和相关 socket 测例暴露出 shutdown(SHUT_WR)recv() 行为不正确的问题。Linux TCP 半关闭语义要求:关闭写方向后,读方向仍然可以继续接收对端数据。原先实现把某些状态过早视为 closed,导致 recv 无法继续 drain 队列。

    修复后,recv() 不再简单以“不是 Connected”作为拒绝条件,而是区分真正关闭和半关闭状态;同时调整 recv 内部顺序,优先读取已排队数据,保证 RST/Closed 但仍有队列数据时也能按预期读出。

  4. 补充并发 connect 和非阻塞 connect 回归测试

    为防止网络状态机后续回退,我增加了:

    • bug-tcp-concurrent-connect
    • bug-tcp-nonblocking-connect-so-error

    并将它们加入多架构 bugfix 配置。这样不仅验证 Redis 场景,也沉淀成更小的网络栈回归测例。

  5. 修复 rename 祖先目录检查

    Redis 使用过程中还暴露出文件系统 rename 语义问题。PR802 增加 bug-rename-file-into-child-dir,修复 axfs-ng-vfs rename 祖先关系检查,使非法 rename 能按预期拒绝,避免破坏目录树结构。

通过第二阶段,StarryOS 的验证对象从“单个接口”扩展到“真实用户程序”。BusyBox 暴露了 procfs、socket ioctl 和 raw socket 问题;Lua 暴露了文件系统目录语义问题;Redis 暴露了 TCP 状态机、半关闭、并发连接和 rename 检查问题。这些工作也说明:越接近真实应用,调试复杂问题越依赖可观察性工具,第三阶段转向 GDB/ptrace 支持是自然延伸。

三、第三阶段工作、技术报告

1. 技术路线总览

本阶段在 StarryOS 内核中实现一个可用的 Linux ptrace 子集,使 GDB 能够通过标准接口控制用户态程序。GDB 的命令转化为 ptrace()waitpid()/waitid()/proc 文件读取等 Linux 兼容接口。

2. ptrace状态组织

这条链路的核心是 PtraceState。它保存了 GDB 需要观察和修改的执行现场,早期 PR #931 实现了ptrace的大致框架,使得starryOS能够跑通单线程 RISC-V GDB,但它的 stop 状态为进程级,无法表达多个 LWP 同时存在时“哪个线程停住、读写哪个线程寄存器、恢复哪个线程”。后续提交的PR重构将 stop、resume、syscall trace、FP 状态改为按 TID 保存,使得多线程 GDB可用。

3. GDB 命令到内核路径状态机

以断点为例,完整状态迁移如下:

下面是更具体的几个命令对应执行流的讲解

3.1 启动目标程序:run

GDB 启动目标程序时,典型流程是 fork 出子进程,子进程执行 PTRACE_TRACEME,随后 execve 加载目标程序。StarryOS 内核处理流程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tracee: ptrace(PTRACE_TRACEME)
-> sys_ptrace()
-> ptrace_traceme()
-> ProcessData::set_ptrace_traceme()
-> 记录 tracer_pid 为父进程

tracee: execve(target)
-> sys_execve()
-> 装载 ELF,重建用户地址空间、栈和 auxv
-> ProcessData::set_ptrace_exec_stop_pending()

tracee: 返回用户态前
-> task/user.rs 用户态返回循环
-> take_ptrace_exec_stop_pending()
-> ptrace_notify_exec()
-> ptrace_stop_current(SIGTRAP)

tracer: waitpid()
-> sys_waitpid()
-> 发现 tracee 的 ptrace stop
-> 返回 SIGTRAP / PTRACE_EVENT_EXEC stop

这一步的意义是让 GDB 在新程序镜像刚装载完成、还没有真正运行用户代码时获得同步点。GDB 可以在这个时机读取 /proc/<pid>/maps/proc/<pid>/auxv,加载符号并设置断点。

3.2 设置断点:break

GDB 的软件断点不是一个单独的内核 syscall,而是通过 ptrace 写 tracee 内存实现:

1
2
3
4
5
6
7
8
9
10
11
GDB: break function
-> 读取目标地址原始指令
ptrace(PTRACE_PEEKDATA)
-> ptrace_peekdata()
-> 从 tracee AddrSpace 读取用户内存

-> 写入断点指令
ptrace(PTRACE_POKEDATA)
-> ptrace_pokedata()
-> 写 tracee AddrSpace
-> 必要时 flush icache

当 tracee 执行到断点指令后,会从用户态陷入内核:

1
2
3
4
5
6
7
8
9
10
11
tracee executes breakpoint instruction
-> UserContext::run() 返回 Exception(Breakpoint)
-> task/user.rs 判断 tracee 正在被 ptrace
-> ptrace_stop_current(SIGTRAP)
-> 保存 UserContext / siginfo / stop record
-> notify_ptrace_waiter()
-> tracee 阻塞等待 tracer resume

GDB: waitpid()
-> 收到 SIGTRAP stop
-> 读取寄存器,显示当前源码行
3.3 继续执行:continue

GDB 继续执行时调用 PTRACE_CONT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GDB: continue
-> ptrace(PTRACE_CONT, tid, 0, signo)
-> sys_ptrace()
-> ptrace_cont()
-> ptrace_stopped_tracee_with_tid()
-> 清除 single-step / syscall trace 状态
-> resume_ptrace_stop_with_signal_for(tid, signo)
-> 清除 stop record 中的 signo
-> wake ptrace_stop_event

tracee:
-> wait_ptrace_resume() 被唤醒
-> take_ptrace_stop_user_context_for(tid)
-> 应用 GDB 可能修改过的寄存器
-> restore_current_fp_for_ptrace(tid)
-> 返回用户态继续执行

这里的关键点是:tracee 在 ptrace_stop_current() 里睡眠,直到 GDB 通过 PTRACE_CONT 或其他 resume 请求清除对应 TID 的 stop 状态并唤醒它。

3.4 单步执行:stepi / nexti

GDB 单步执行依赖 PTRACE_SINGLESTEP。StarryOS 在不同架构上采用不同实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GDB: stepi
-> ptrace(PTRACE_SINGLESTEP, tid, 0, signo)
-> ptrace_singlestep()
-> set_ptrace_singlestep_for(tid, true)
-> resume_ptrace_stop_with_signal_for(tid, signo)

tracee 返回用户态前
-> task/user.rs 检查 is_ptrace_singlestep_for(tid)
-> ptrace_setup_singlestep(...)
-> 设置架构相关 single-step 机制

tracee 执行一条指令
-> 陷入 breakpoint/debug exception
-> ptrace_stop_current(SIGTRAP)
-> GDB waitpid 收到单步完成 stop

在 RISC-V、AArch64、LoongArch64 上,single-step 主要通过计算下一条 PC 并在 next-PC 临时插入断点指令实现;命中后恢复原始指令。在 x86_64 上,后续实现可以使用 RFLAGS 的 TF 位触发 #DB debug exception。这个差异被封装在架构相关的 ptrace_setup_singlestep() 中,对上层 GDB 仍表现为统一的 PTRACE_SINGLESTEP

3.5 查看寄存器:info registers

GDB 查看寄存器时会调用 legacy regs 或 regset 接口:

1
2
3
4
5
6
7
GDB: info registers
-> ptrace(PTRACE_GETREGS)
或 ptrace(PTRACE_GETREGSET, NT_PRSTATUS)
-> ptrace_getregs() / ptrace_getregset()
-> ptrace_stop_user_context_for(tid)
-> 将 UserContext 转换为目标架构 ABI 的用户寄存器结构
-> vm_write 到 GDB 提供的用户缓冲区

GDB 修改寄存器时则反向执行:

1
2
3
4
5
6
GDB: set $pc = ...
-> ptrace(PTRACE_SETREGS)
或 ptrace(PTRACE_SETREGSET, NT_PRSTATUS)
-> 从 GDB 用户缓冲区读取寄存器结构
-> 转换回 UserContext
-> set_ptrace_stop_user_context_for(tid, new_uctx)

真正写回 tracee 的时机不是 PTRACE_SETREGS 当场返回用户态,而是 tracee 被 resume 后在 wait_ptrace_resume() 中执行:

1
2
take_ptrace_stop_user_context_for(tid)
*uctx = resume_uctx

这样保证 GDB 对寄存器的修改会成为 tracee 下一次回到用户态时的真实执行现场。

3.6 查看内存和调用栈:x / bt

GDB 的内存查看命令,例如:

1
x/4gx $sp

会通过 PTRACE_PEEKDATA 读取 tracee 地址空间。内核路径为:

1
2
3
4
5
ptrace(PTRACE_PEEKDATA, tid, addr, data)
-> ptrace_peekdata()
-> 找到 tracee ProcessData
-> tracee.aspace().read(addr)
-> 将读取结果写回 tracer data 指针

bt 本身主要由 GDB 在用户态完成。内核需要提供以下基础信息:

  • 当前 PC、SP、FP、RA 等寄存器。
  • tracee 栈内存读取能力。
  • /proc/<pid>/maps 用于判断地址属于哪个映射。
  • /proc/<pid>/auxv 用于获取入口、页大小、硬件能力等辅助信息。
  • ELF 符号和 DWARF 调试信息由 GDB 从目标文件中读取。

因此 StarryOS 内核并不直接实现完整 GDB backtrace 算法,而是提供 GDB unwind 所需的寄存器、内存和 procfs 信息。

4. 信号处理流程

GDB 支持里最重要的状态机之一是 signal-delivery-stop。tracee 收到普通信号时,内核不能马上执行默认动作或用户 handler,而是要先停给 tracer,让 GDB 决定这个信号是否真正交给程序。

这里的 resume_signal_bypass 用于避免死循环。(后续代码解读里也有)例如 tracee 收到 SIGTERM 后停给 GDB,GDB 决定继续传递 SIGTERM。如果内核只是把 SIGTERM 重新放回信号队列,下一轮又会再次 ptrace stop。因此内核为 tracer 明确注入的信号设置一次性 bypass,使下一轮信号处理直接递送,不再重复停给 GDB。

5. 多线程与 LWP 设计

StarryOS 的进程/线程关系可以简化为:

GDB 中的 LWP 基本对应 StarryOS 的 TID。因此 ptrace 不能只保存一份进程级 stop,而需要按 TID 记录:

ptrace 的设计采用“进程级容器 + TID-keyed 状态”:

  • tracer_pidtracemeattached 表示进程级调试关系。
  • stop[tid] 保存每个 LWP 的停止现场。
  • resume_signo[tid] 保存每个 LWP resume 时的信号选择。
  • syscall_trace[tid] 保存每个 LWP 的 syscall entry/exit 状态。
  • stop_fp_data[tid] 保存每个 LWP 的 FP/SIMD/FPU 快照。

这个设计支撑了以下命令:

info threads:通过 /proc/<pid>/task 发现线程。

thread <n>:后续 ptrace 操作绑定到具体 LWP/TID。

info registers:读取对应 TID 的 UserContext

continue / stepi:只恢复或单步指定 TID 的 stop 状态。

!注:在第三或第四周会议时,陈老师表示当前 starryos 多线程还不完善,先保持单线程可用即可,之前 stop 状态为进程级虽然能达到要求,但难免有写了个 bug但能跑就不管了的意味,考虑到时间有限,做的多线程支持虽然仍是 all-stop 的,不追求完整 non-stop mode 和完整 Linux group-stop,但已经解决了早期“一个进程只有一份 stop 状态”无法表达 LWP 的问题。

6. 三架构支持方式

ptrace 的公共流程是统一的,但寄存器布局、FP/SIMD/FPU 状态和 single-step 机制都依赖架构。因此实现拆成“公共 ptrace 状态机 + 架构适配层”。(x86 架构适配层由杨铮同学 GitHub 54dK3n 完成)

架构 通用寄存器 FP/SIMD/FPU single-step
riscv64 pc/ra/sp/gp/tp/a0-a7/s0-s11/t0-t6UserContext 互转,支持 NT_PRSTATUS 读写通用寄存器。 支持 NT_FPREGSET 和 legacy FP 接口读写浮点寄存器与 fcsr 使用临时 ebreak 实现 PTRACE_SINGLESTEP:计算下一条 PC,保存原始指令,写入断点指令,命中后恢复。
aarch64 UserContext 中的 x0-x30sppcpstate 映射为 AArch64 GDB 期望的通用寄存器结构。支持 NT_PRSTATUS 读写通用寄存器。 支持 NT_FPREGSET 读写 v0-v31fpcrfpsr。进入 ptrace stop 时保存当前 FP/SIMD 状态,resume 时恢复,保证 GDB 修改 FP/SIMD 寄存器后能写回 tracee 执行态。 single-step 同样通过架构相关 next-PC / 临时断点路径接入公共 PTRACE_SINGLESTEP 流程。
loongarch64 UserContext 中的 r0-r31erabadv 等状态映射为 LoongArch GDB 期望的寄存器布局。支持 NT_PRSTATUS 读写通用寄存器。支持 NT_FPREGSET,并补充 legacy PTRACE_GETFPREGS / PTRACE_SETFPREGS 与 regset 路径复用。 保存和恢复 FPU 扩展状态,包括普通 FP、fccfcsr 以及后续扩展状态。 补充 LoongArch 分支/跳转等指令的 next-PC 处理接入PTRACE_SINGLESTEP

在写入用户 text 指令后刷新 icache,保证 tracee 执行到新写入的断点指令。

!注:本科学过riscv 和一部分x86 指令集,但由于软件工程专业背景所限,硬件指令集这块了解不深,靠codex和 asm test 测试驱动开发完成的任务。(埋个伏笔,看我一年后能不能完成一生一芯…)

7. procfs 辅助信息

除了 ptrace syscall,GDB 还依赖 procfs 获取进程信息。为此本阶段补充和修复了以下路径:

  • /proc/<pid>/maps:提供用户地址空间映射,支持 GDB 判断 text/data/heap/stack 等区域。
  • /proc/<pid>/auxv:提供 auxiliary vector,例如 AT_ENTRYAT_PHDRAT_PAGESZAT_HWCAP
  • /proc/<pid>/status:提供 TracerPid、线程数量、UID/GID 等状态信息。
  • /proc/<pid>/task:提供线程列表,使 GDB 能发现 LWP。

这些接口不直接执行调试控制,但决定了 GDB 是否能正确加载符号、识别动态链接器、展示线程和解释目标进程内存布局。

8. 当前能力边界

当前实现已经能够支撑基础 GDB 使用:

  • native GDB 调试用户程序。
  • guest 内 gdbserver 调试。
  • host-to-guest remote GDB。
  • 断点、继续、单步、寄存器查看/修改、内存查看、基础 backtrace。
  • 简单多线程程序的 LWP 发现和线程级寄存器/stop 操作。
  • riscv64、aarch64、loongarch64 (x86)的基础寄存器和 FP/SIMD/FPU regset。

但它仍然是可用子集,不是完整 Linux ptrace/GDB 语义。当前没有完成的有完整 non-stop mode、完整 group-stop / job-control 与 ptrace-stop 的 Linux 级细节、硬件断点和 watchpoint、core dump、所有复杂线程事件的完全 Linux 兼容、所有架构指令的完整 single-step next-PC 语义

9. 代码解读

本着学习的目的以及尽量不往 tgoskits 里塞屎山的态度,提交 PR 前我都会弄懂 codex 写了什么,也学习到一些我认为比较优质的代码设计。

解读的代码版本为 dev 分支的Commit afad791

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> TaskInner {
...
match reason {
ReturnReason::Syscall => {
let tid = thr.tid();
let trace_state = thr.proc_data.take_ptrace_syscall_trace_for(tid);
if matches!(trace_state, SyscallTraceState::Entry)
&& ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx).is_some()
{
match thr.proc_data.take_ptrace_syscall_trace_for(tid) {
SyscallTraceState::Entry | SyscallTraceState::Exit => {
thr.proc_data.set_ptrace_syscall_trace_state_for(
tid,
SyscallTraceState::Exit,
)
}
SyscallTraceState::None => {}
}
}

if let Some(exit_code) = ptrace_exit_event_code(saved_sysno, saved_a0)
&& crate::syscall::ptrace_notify_exit(
thr.proc_data.proc.pid(),
exit_code,
)
{
let _ = ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx);
}

handle_syscall(&mut uctx);
if thr.proc_data.has_ptrace_pending_event_for(tid)
&& let Some(_resume_sig) =
ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx)
{
continue;
}
if matches!(
thr.proc_data.take_ptrace_syscall_trace_for(tid),
SyscallTraceState::Exit
) {
let _ = ptrace_syscall_stop_current(thr, Signo::SIGTRAP, &mut uctx);
}
if thr.proc_data.take_ptrace_exec_stop_pending() {
let _is_event =
crate::syscall::ptrace_notify_exec(thr.proc_data.proc.pid());
if let Some(_resume_sig) =
ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx)
{
continue;
}
}
}
...
}

解读:User task 从用户态返回内核态时,内核会分析返回原因。当原因是 syscall时,会分析当前线程的 trace_state,如果是 SyscallTraceState::Entry,说明在实际 syscall 代码执行前,我们需要挂起当前tracee 线程,去唤醒 tracer 线程。

然后 tracer 即 GDB 会做什么?

可能 continue(PTRACE_CONT),把 None 写入 tracee 状态,match thr.proc_data.take_ptrace_syscall_trace_for(tid)会返回 None,后续不再追踪 syscall。 可能 stepi(PTRACE_SYSCALL),把Entry 写入状态,set_ptrace_syscall_trace_state_for 将 tracee 设为 Exit,syscall出口会再挂起 去唤醒 tracer。可能detach(PTRACE_DETA),后续脱离追踪。

然后到PTRACE_EVENT_EXIT 通知,这段代码在 handle_syscall 之前执行,用于在进程调用 exit/exit_group 即将退出时,给调试器最后一次查看的机会。如果先 handle_syscall 再通知,do_exit 已经执行完了,进程没了,调试器看不到退出码。

接着是handle_syscall 之后的三重 ptrace 事件处理

第一重:pending event stop,handle_syscall 内部可能发生了:fork、vfork、clone、 seccomp 规则触发,这些事件往往伴随进程状态变化(fork 产生子进程、seccomp 修改了寄存器),调试器恢复后应该重新进入主循环检查新的状态,而不是继续跑完当前系统调用的后续处理。

第二重:系统调用出口 stop,然后 wake tracer。

第三重:take_ptrace_exec_stop_pending() 是 execve 在 handle_syscall 时设置的布尔标记,此时ptrace_notify_exec 将标记转为正式事件,如果 tracer 开了 PTRACE_O_TRACEEXEC 则 tracee.set_ptrace_pending_event(tid, PTRACE_EVENT_EXEC, 0); execve 会完全替换进程地址空间——旧的可执行文件、栈、堆全没了,新程序加载完毕。调试器恢复后应该重新从主循环开始 所以 continue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
fn ptrace_stop_current_impl(
thr: &Thread,
signo: Signo,
uctx: &mut UserContext,
is_syscall_stop: bool,
) -> Option<Option<Signo>> {
...
while !thr.proc_data.claim_ptrace_stop(tid) {
block_on(poll_fn(|cx| {
if !thr.proc_data.has_ptrace_stop(tid) {
Poll::Ready(())
} else {
thr.proc_data.register_ptrace_stop_waker(cx.waker());
if !thr.proc_data.has_ptrace_stop(tid) {
Poll::Ready(())
} else {
Poll::Pending
}
}
}));
}
···
{
thr.proc_data.save_current_fp_for_ptrace(tid);
}
if is_syscall_stop {
thr.proc_data.set_ptrace_syscall_stop(tid, signo, uctx);
} else {
thr.proc_data.set_ptrace_stop(tid, signo, uctx);
}
notify_ptrace_waiter(thr, signo);

wait_ptrace_resume(thr, tid, uctx);
Some(thr.proc_data.take_ptrace_resume_signo_for(tid))
}

解读:claim_ptrace_stop 检查该线程是否有未消费的 stop 记录

为什么注册 waker 后又检查一次?这是防止竞态情况发生:has_ptrace_stop == true,tracee 准备进pending 分支,tracer 恰好清掉 stop 并唤醒
当前线程还没注册 waker,可能错过唤醒

thr.proc_data.save_current_fp_for_ptrace(tid);保存当前线程的 FP/SIMD 寄存器状态,原因是普通整数寄存器在 uctx 里,但浮点/SIMD 寄存器不完整保存在 UserContext 里,可能在 CPU lazy state 或架构专门的上下文里

set_ptrace_stop 代码就很简单,略过。然后去通知tracer,给 tracer 发送 SIGCHLD / CLD_TRAPPED,唤醒 tracer 的 wait/waitpid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn wait_ptrace_resume(thr: &Thread, tid: u32, uctx: &mut UserContext) {
current().clear_interrupt();
let wait_result = block_on(interruptible(poll_fn(|cx| {
if thr.proc_data.ptrace_stop_signo_for(tid).is_none() {
Poll::Ready(())
} else {
thr.proc_data.register_ptrace_stop_waker(cx.waker());
if thr.proc_data.ptrace_stop_signo_for(tid).is_none() {
Poll::Ready(())
} else {
Poll::Pending
}
}
})));

if wait_result.is_err() {
thr.proc_data.clear_ptrace_stop();
} else if let Some(resume_uctx) = thr.proc_data.take_ptrace_stop_user_context_for(tid) {
*uctx = resume_uctx;
thr.proc_data.restore_current_fp_for_ptrace(tid, uctx);
}
}

解读:这个函数是tracer 已被通知、tracee 在 stop 状态中挂起,等待tracer 说”继续跑”

第一行代码是因为当前线程上可能挂着未处理的中断标记(比如定时器中断正好在进入 stop 前触发)。不清掉的话 interruptible 会立即返回 Err,导致虚假唤醒。

接下来异步轮询等待,如果 stop signo 被清理了,说明tracer执行了 resume_ptrace_stop → 清除 signo。 tracee 已经可以恢复执行了。否则注册唤醒器, 注册期间被清除了,立即恢复,否则继续 pending。 这里也有经典的双重检查模式和上面一样防止 TOCTOU 竞态

wait result 为Err 表示 interruptible 被信号打断(比如 SIGKILL 来了)。直接清除 stop 状态,后续 check_signals 循环会出队 SIGKILL 并执行 do_exit。

take_ptrace_stop_user_context_for 取出tracer可能修改过的 uctx,然后覆盖当前uctx。tracer 也可能通过 PTRACE_SETFPREGS 修改了浮点寄存器,恢复执行时必须把这些修改写回硬件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
check_signals 中有如下代码片段

if signo != Signo::SIGKILL
&& !thr
.proc_data
.take_ptrace_resume_signal_bypass_for(thr.tid(), signo)
&& let Some(resume_signo) = ptrace_stop_current(thr, signo, uctx)
{
match resume_signo {
None => return true,
Some(new_signo) if new_signo != signo => {
thr.proc_data
.set_ptrace_resume_signal_bypass_for(thr.tid(), new_signo);
let _ = thr.signal.send_signal(SignalInfo::new_kernel(new_signo));
return true;
}
Some(_) => {}
}
}

解读 SIGKILL 不可拦截易理解,下面解释一下 bypass。

假设调试器通过 PTRACE_CONT 注入信号 SIGSTOP:

没有 bypass:

信号 SIGSTOP 入队 → check_signals 出队 → ptrace_stop_current→ 调试器说”传 SIGSTOP” → 信号重新入队 → check_signals 再出队→ 又 ptrace_stop_current → 调试器又说”传 SIGSTOP” → 入队…→ 永远循环

有 bypass:

调试器注入 SIGSTOP 时 set_bypass(SIGSTOP) → 信号入队 → check_signals 出队 → take_bypass 返回 true → 跳过 ptrace stop → 直接往下处理

后续 resume_signo 对应三种回复

我在 ProcessData 里添加了很多 ptrace 实现所需的字段,下面会简单解释一下

感觉这里应该把这些字段抽离出来成为一个结构体比如 ptraceInfo {···},这样会让 ProcessData 更清晰,不过直接添加到ProcessData 里能跑,先不管了,后续可能有同学看不惯简单重构一下就行

身份与追踪关系

字段 类型 作用
ptrace_tracer_pid AtomicU32 调试器的 PID。知道该给谁发 SIGCHLD 通知
ptrace_traceme AtomicBool 进程自己调用 PTRACE_TRACEME,声明”让父进程来追踪我”
ptrace_attached AtomicBool 调试器通过 PTRACE_ATTACH 强行附加到本进程

stop 状态

字段 类型 作用
ptrace_stop BTreeMap<tid, PtraceStopRecord> per-thread 的 stop 快照:signo(停因)、uctx(寄存器)、is_syscallsiginfo、是否已报告给调试器
ptrace_stop_tid AtomicU32 当前正 stop 的线程 TID,供 waitpid 快速定位
ptrace_stop_event Arc<PollSet> 异步事件,stop 状态变化时唤醒 waitpid 的挂起线程

恢复控制

字段 类型 作用
ptrace_resume_signo BTreeMap<tid, signo> 调试器通过 PTRACE_CONT 注入的信号号
ptrace_resume_signal_bypass BTreeMap<tid, signo> 已获调试器批准的信号,下次出队时跳过 ptrace stop,防止死循环

功能模式

字段 类型 作用
ptrace_singlestep_tid AtomicU32 处于单步模式的线程 TID
ptrace_syscall_trace BTreeMap<tid, Entry/Exit/None> 系统调用追踪状态:Entry(入口停)、Exit(出口停)、None(不追踪)
ptrace_options AtomicUsize PTRACE_O_TRACEFORK/EXEC/EXIT 等选项位掩码

事件系统

字段 类型 作用
ptrace_pending_event BTreeMap<tid, PtracePendingEvent> handle_syscall 触发的延时事件(PTRACE_EVENT_FORK/CLONE/VFORK/SECCOMP),在 syscall 返回后补一次 stop
ptrace_exec_stop_pending AtomicBool execve 执行后置位,提示 syscall 出口处需要 PTRACE_EVENT_EXEC 通知

硬件状态保存

字段 类型 作用
ptrace_ss_saved_insn BTreeMap<tid, (地址, 原始指令)> 非 x86 架构单步时,用断点替代下一条指令,保存被替换的原始指令以便恢复
ptrace_stop_fp_data BTreeMap<tid, PtraceStopFpData> stop 时的 FP/SSE 寄存器快照,调试器通过 PTRACE_GETFPREGS/SETFPREGS 读写此数据

ptrace.rs 是 sys ptrace 的具体实现,限于篇幅这里不详细讲了

四、测试与成果

1. 测试代码结构

GDB 支持相关测试分为三层:内核 ptrace 语义回归测试、guest 内 GDB/gdbserver smoke 测试、压力测试。

1.1 ptrace 语义回归测试

核心测试位于:

1
test-suit/starryos/qemu-smp1/system/test-ptrace-gdb/src/main.c

该测试不依赖真实 GDB,而是直接调用 Linux ptrace ABI,验证 GDB/gdbserver 所依赖的底层语义是否正确,覆盖内容包括:

该测试的作用是把 GDB 的复杂行为拆成更稳定的 syscall 级语义检查。例如:

  • PTRACE_GETREGSET / SETREGSET:验证 GDB 能读取和修改 tracee 的 PC、SP、通用寄存器。
  • PTRACE_PEEKDATA / POKEDATA:验证 GDB 能读写 tracee 用户地址空间,用于软件断点。
  • PTRACE_SINGLESTEP:验证 stepi 能单步执行一条用户指令。
  • PTRACE_SYSCALL:验证 syscall entry/exit stop,可支撑 strace 类功能。
  • PTRACE_O_TRACECLONE:验证线程创建事件能够被 tracer 观察。
  • PTRACE_GETSIGINFO / SETSIGINFO:验证信号 stop 的 siginfo 能被 tracer 查询和修改。
  • NT_FPREGSET:验证浮点/SIMD regset 的读写路径。

辅助测试包括:

1
2
3
4
5
test-suit/starryos/qemu-smp1/system/test-ptrace-traceme-stop/
test-suit/starryos/qemu-smp1/system/test-ptrace-wait-wall/
test-suit/starryos/qemu-smp1/system/test-ptrace-thread-traceclone-wall/
test-suit/starryos/qemu-smp1/system/test-proc-status-tracerpid/
test-suit/starryos/qemu-smp1/system/test-gdb-native-batch/

这些测试分别覆盖:

  • PTRACE_TRACEME + execve 后的 exec-stop。
  • waitpid/__WALL 对 traced LWP 的 stop 消费。
  • TRACECLONE 下新线程事件与新 TID 获取。
  • /proc/<pid>/statusTracerPid 展示。
  • 模拟 GDB 的动态链接程序调试流程。

其中 test-gdb-native-batch 是 raw-ptrace 版本的 GDB 流程模拟,主要步骤为:

1
2
3
4
5
6
7
fork + PTRACE_TRACEME + execve
读取 /proc/<pid>/auxv 中的 AT_ENTRY
在 AT_ENTRY 写入断点指令
PTRACE_CONT 运行到断点
恢复原指令并调整 PC
PTRACE_SINGLESTEP 执行原指令
PTRACE_CONT 继续运行到正常退出

它验证的是“不启动真实 GDB,也能通过 ptrace 完成一次简化调试会话”。

1.2 guest 内 GDB smoke 测试

GDB 应用测试位于:

1
apps/starry/gdb-smoke/

主要结构如下:

1
2
3
4
5
6
7
8
9
apps/starry/gdb-smoke/native/src/main.c
apps/starry/gdb-smoke/native/gdb-native-smoke.gdb
apps/starry/gdb-smoke/native-thread/src/main.c
apps/starry/gdb-smoke/native-thread/gdb-native-threads.gdb
apps/starry/gdb-smoke/gdbserver/src/main.c
apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.sh
apps/starry/gdb-smoke/gdbserver/gdbserver-smoke.gdb
apps/starry/gdb-smoke/gdbserver/host-manual.gdb
apps/starry/gdb-smoke/gdbserver/host-remote.gdb

其中 native/src/main.c 构造了一个清晰的调用链:

1
main -> demo_entry -> demo_worker -> native_marker

用于验证 GDB 的断点、运行、回溯、寄存器读取、内存查看和单步执行。

native-thread/src/main.c 创建两个 pthread 子线程,用于验证:

1
2
3
4
info threads
thread <id>
bt
/proc/<pid>/task

gdbserver/src/main.c 是远程调试目标程序,不是 gdbserver 本体。真实的 gdbserver 来自 rootfs 中的 /usr/bin/gdbserver。StarryOS 的工作是补齐其依赖的 ptrace/procfs 语义,使用户态 gdbserver 可以运行。

2. Demo 截图

四架构均可正常使用,我选取 aarch64 架构下进行截图演示

注:在 6.28 日我尝试在最新 dev 分支下验证 riscv 架构 gdb,出现了bfd requires flen 8, but target has flen 4 的问题。目标 ELF 为 double-float ABI,需要 flen=8,但 gdbserver 上报给 host GDB 的 target description 被识别为 flen=4。目标程序直接运行正常,说明 ELF 加载和执行路径可用,问题集中在 GDB/gdbserver 的 RISC-V target feature 描述或 FP regset 探测路径。

但 aarch 没有问题,两周前四架构均能正常演示

Demo1 gdb server

在终端 1 启动 aarch64 guest:

1
2
docker exec -it tgoskits-dev cargo xtask starry app qemu -t gdb-smoke --arch aarch64 \
--qemu-config qemu-aarch64-gdbserver-manual.toml

进入 StarryOS shell 后运行:

1
gdbserver 0.0.0.0:1234 /usr/bin/gdbserver-smoke-target

终端 2:host 侧连接

1
2
3
docker exec -it tgoskits-dev gdb-multiarch -q \
-x /workspace/apps/starry/gdb-smoke/gdbserver/host-manual-aarch64.gdb \
/workspace/target/gdb-smoke-host/aarch64/gdbserver-smoke-target

break compute_value
continue
bt
info registers
detach

验证了StarryOS guest 内 gdbserver 可以启动并监听端口。

Docker host 侧 gdb-multiarch 可以通过 QEMU hostfwd 连接 guest。

GDB 可以对 guest 用户程序设置断点、继续执行、命中断点、读取 backtrace 和寄存器。

这条链路覆盖:gdbserver -> ptrace -> wait/stop -> regset -> host GDB remote protocol

Demo2 native gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker exec -it tgoskits-dev cargo xtask starry app qemu -t gdb-smoke --arch aarch64 \
--qemu-config qemu-aarch64-gdbserver-manual.toml

gdb /usr/bin/gdb-native-smoke-target

gdb内输入
set pagination off
break native_marker
run
bt
info registers
x/4gx $sp
info proc mappings
info files
info auxv
shell pid="$(pidof gdb-native-smoke-target)" && cat "/proc/$pid/status"
continue
验证了aarch64 guest 内 native GDB 手工调试能力:断点、运行、回溯、寄存器、栈内存、procfs 信息读取。

Demo3 TUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker exec -it tgoskits-dev cargo xtask starry app qemu -t gdb-smoke --arch aarch64 \
--qemu-config qemu-aarch64-gdbserver-manual.toml

gdb -q -tui /usr/bin/gdb-native-smoke-target

break native_marker
run
layout asm
layout regs
refresh
bt
info registers
tui disable
continue

Demo4 LWP/pthread task

1
2
3
4
5
6
7
8
9
10
11
12
gdb /usr/bin/gdb-native-thread-target

set pagination off
set print thread-events on
set schedule-multiple on
break thread_marker
run
info threads
shell pid="$(pidof gdb-native-thread-target)" && ls "/proc/$pid/task"
bt
delete breakpoints
continue

五、工作 PR 记录、工作仓库

issue 记录:https://github.com/rcore-os/tgoskits/issues/586

第一阶段

Linux syscall 添加测例与修复 PR 588 PR605

包含:chmod、fchmod、chown、 fchown、faccessat、umask、fchmodat、fchownat

第二阶段

  1. busybox 十余个 applet 测例,修复内核语义 PR668 PR752

  2. 补齐 Lua 应用运行时的测试,修复阻碍 Lua 应用稳定运行的文件系统缺陷 PR777

  3. 新增 Redis normal、stress测试,修复 ax-net-ng TCP listen/accept 路径、修复 TCP recv 路径、修复 axfs-ng-vfs rename 检查PR802

第三阶段

  1. ptrace 初步框架完成,初期设计问题在后续 PR 解决了,这是一个接近 5000行代码的提交,包括2000 余行测试相关代码与 2000 余行内核 feature 添加与语义修复。PR931
  2. 修复上一 PR 设计问题,重构 ptrace stop 状态,调整 wait / waitid 语义,调整 clone/fork/vfork ptrace event,补充 procfs / GDB 可用性支持,增加测试,并在开会时准备了 demo 并演示 PR1167
  3. 前两个 PR 完成了 riscv64 的 gdb 可用,该 PR 完成了 aarch64、loongarch 的支持 PR1247
  4. 修复和补充了上一 PR 的缺口 PR1292
  5. review 了杨铮同学的 PR,发现了一些设计上的分歧,把x86 的实现与测例对齐到我前面的设计 PR1314

上述 11 个 PR 即是我本次训练营的所有工作

六、总结与感想

2024 秋冬第一次听说 os 训练营,参加第一阶段 rust 语言学习就十分折磨,深夜在武大宿舍里调 rustling 程序,最终完成了 100 多道题的练习,踩了很多坑 走了很多弯路(比如死磕 rust 语言圣经、rust 程序设计教材,然而里面的很多语法比如“无畏并发”等高级内容根本就没用上,看了一遍又一遍都无法理解,最后发现只有在项目里用上才能真正理解…)

2025 春夏花了一整个月学 rCore,那时候本着学习的态度,古法编程,每行代码得去看去理解,犹记得文件系统章节看完了 easy fs 的所有代码,结果发现 lab 只需要写几行代码就能解决的无奈,不过学到知识就是胜利✌️后续有事没继续做最终阶段

2026 春夏花了两个月,从啃 arceos 代码发现很多地方看不懂,到使用 codex 辅助编程完成了 starryos 里计划好的工作,我不仅学到了 os 的知识,也更加熟练地使用ai 辅助编程。感谢 oscamp 和 infiniTensor 训练营,让我学到了这么多!终于是都通关了,明年我要继续 compiler camp!

关于第三阶段的项目,GDB 支持不是单个 syscall 的实现,而是一条完整链路。一个断点能否命中,背后涉及 ELF 加载、用户地址空间、信号投递、ptrace stop 状态、wait 语义、寄存器 ABI、跨进程内存访问、procfs、rootfs、gdbserver、QEMU 网络转发和测试组织。任何一环不稳定,最终都可能导致 GDB 卡住或超时,在实现过程中我对 os 的进程、信号 IPC、ELF、内存访问等等诸多方面都有了更深的了解,不过内核代码十分复杂,参加完训练营也仅对 os 了有了一些粗浅的理解,后续会继续读陈海波老师的《操作系统 原理与实现》,加深理解,继续努力提高工程能力

最后感谢陈渝老师、向勇老师、朱懿学长以及训练营的各位助教老师们。受益于开源,也要回馈开源,未来有空了希望我也能参与贡献各种开源训练营

方案一:以 syscall 为引导的改进

这一阶段主要做的是进程控制与资源相关的 syscall 的测试。

这个方向的整体思路来自 Issue #650:挑几个 StarryOS 里尚未覆盖或实现有问题的系统调用,补测试、修 bug,一个个啃过去。最终合了 5 个 PR,还有 1 个在 review 中。有两个 syscall(prctlriscv_flush_icache)因为工作量和优先级原因搁置了。

PR #801 — feat(starry): 为 prlimit64 添加用户态测试

  • 修正了 sys_prlimit64 的错误优先级:无效 pid 应该比非法 resource 更早返回 ESRCH
  • 对提高 rlim_max 的路径补上了 CAP_SYS_RESOURCE 权限检查

PR #863 — test(starryos): capget 系统调用测试与 NULL datap 修复

  • 发现并修复了一个 bug:datap=NULL 时应视为版本探测,返回 EINVAL 并写回支持的版本号(对齐 Linux 行为)。StarryOS 原来的实现直接漏掉了这个分支

PR #1049 — fix(starry): arch_prctl 地址校验 + 异常信号映射修正

  • 看了 Linux 源码才发现,ARCH_SET_FS/ARCH_SET_GS 是有地址合法性校验的——超过 47-bit 规范边界的非 canonical 地址应该返回 EPERM,StarryOS 之前没做这个检查
  • 把用户态 #GP#SS#NP#TS 等保护异常从 SIGTRAP 改成了 SIGSEGV(对齐 Linux/POSIX)
  • 附带了一份 CPUID faulting 的后续支持方案(GetCpuid/SetCpuid 的占位实现)
  • 这个 PR 改动涉及面比较广,还在完善中。

方案二:以 Linux App 为引导的改进

从“跑通真实 Linux 应用”出发来改善 StarryOS 的兼容性。这个方向的工作记录在 Issue #1153(procps 支持分析)和 Issue #1324(strace 支持)中。

对 procps(ps/top/kill) 的支持分析:目前的 pskill 命令均可以正常运行,但是在测试过程中发现运行 top 命令会直接退出,没有结果输出。我主要针对这一问题进行了修复。

PR #1194 — fix(tui): 在 init.sh 中设置 TERM 环境变量

  • init.sh 里加了 export TERM=xterm,修复了 TUI 应用因为缺少 TERM 环境变量而无法启动的问题
  • 改动虽然小,但是发现这个错误费了比较长的时间。我在让 AI 排查原因的时候 AI 产生了幻觉,提供了一些无用的建议,我的想法是让 AI 阅读 top 的源码,让它判断在哪一步出现问题了。
  • 这件事给我的体会是,使用 AI 的关键是给它提供全面的 context. 而如何给出合适的 context 取决于使用者对问题的理解。

在排查 top 的问题时,我尝试使用 strace 来查看 top 的系统调用,但是发现 StarryOS 上目前还不支持 strace, 因此我又做了 strace 的支持。

PR #1323 — feat(starry-kernel): 实现 PTRACE_LISTEN(strace 支持)

这个 PR 的目标是让 strace 能在 StarryOS 上跑起来。一开始以为“实现 PTRACE_LISTEN 就行了”,结果发现远不止如此——strace 使用的是 ptrace 的 SEIZE 流程而非传统的 TRACEME,对内核的要求比想象中多得多。这一系列改动涉及 7 个文件,新增约 380 行代码(含 179 行的 C 测试用例),按依赖关系可以分为三个层次:

第一层:信号与时序(修不好这个,后面都无从谈起)

mustSIGCONT 不产生虚假 ptrace stopsignal.rs)。这个问题非常隐蔽:shell 启动前台作业时发送的 SIGCONT 可能在 strace child 自己发出的 SIGSTOP 之前被出队。此时内核会错误地产生一个 SIGCONT stop,把 strace 的初始化状态机彻底打乱。不修这个,无论是 TRACEME 还是 SEIZE 路径都起不来。修复方式是在信号处理中对 SIGCONT 类信号(以及 SIGKILLSIGSTOP 等不可屏蔽信号)做特殊判断,跳过 ptrace stop 的生成。

第二层:SEIZE 流程核心机制

这一层是让 strace 能跑起来的“骨架”,缺一个都会导致 strace 直接退出或行为异常:

mustPTRACE_LISTEN 实现ptrace.rs,opcode 0x4208)。strace 在 SEIZE 流程中会将 TE_GROUP_STOP 转为 job-control stop,而这一步依赖 PTRACE_LISTEN 让 tracee 进入 listening 状态。没有它,strace 直接报 Unsupported 退出——这也是整个 PR 命名的由来。

mustexec 时跳过 syscall exit stopuser.rs)。这是一个非常微妙的细节:在 execve 的处理中,如果先触发了 syscall exit stop,TCB_INSYSCALL 标志会被清除。之后 exec event 到达时,因为标志已清,strace 会报 Stray PTRACE_EVENT_EXEC,并进入错误恢复路径——更致命的是,这会导致后续所有 syscall 的 entry/exit 判定永久反转,整个 trace 输出完全错乱。修复方式是在 exec 路径上跳过 exit stop,确保 exec event 到达时 TCB_INSYSCALL 仍然置位。

highPTRACE_EVENT_STOP 编码ptrace.rs)。PTRACE_INTERRUPT 触发的 stop 需要在 wait status 的高 16 位标记 PTRACE_EVENT_STOP(128)。strace 正是根据这个标记来区分 group-stop 和普通信号 stop,从而走 TE_RESTART 而非 TE_GROUP_STOP 路径。没有这个标记,strace 的 ptrace 状态机会做出错误的决策。

highSEIZE 接收 optionsptrace.rs)。strace 调用 ptrace(PTRACE_SEIZE, pid, NULL, ptrace_setoptions) 时,会把 options(如 0x51)作为第 4 参数传入。StarryOS 原来的 SEIZE 实现忽略了 data 参数,导致 options(如 PTRACE_O_TRACESYSGOODPTRACE_O_TRACEEXEC)全部丢失。后果是 exec stop 没有 event tag,strace 会把它误判为普通的信号投递。

highATTACHED 也触发 exec stopexecve.rs)。这是 SEIZE 和 TRACEME 的一个重要差异:TRACEME 的 tracee 会主动调用 ptrace(TRACEME) 标记自己,而 SEIZE 的 tracee 是被动的,从未调用过 TRACEME。原来的代码只用 is_ptrace_traceme() 判断是否触发 exec stop,对于 SEIZE 的 child 永远为 false。需要额外加上 is_ptrace_attached() 的判断,否则 SEIZE 路径的 exec stop 永远不会触发。

第三层:兼容性打磨

mediumTRACESECCOMP/EXITKILL 加入 valid_maskptrace.rs)。strace 在启用 seccomp 过滤或 --kill-on-exit 选项时,会在 PTRACE_SETOPTIONS 中传入 PTRACE_O_TRACESECCOMPPTRACE_O_EXITKILL。这些 bit 不在 valid_mask 中的话,SETOPTIONS 返回 EINVAL,导致所有 options(包括前面那些关键的 exec/trace 相关选项)全部丢失。虽然不是 SEIZE 流程必需的,但缺了它,strace 的 seccomp 相关功能和 --kill-on-exit 就无法正常工作。

lowSEIZE self 恢复 EPERMptrace.rs)。之前的 workaround 在 SEIZE 自身时返回 EIO,现在 SEIZE 流程完整实现后改回 EPERM,对齐 Linux ABI 行为。

调试过程与测试

为验证这些修改,我写了一个 179 行的 C 用户态测试用例(test-ptrace-listen),覆盖 PTRACE_SEIZE → PTRACE_LISTEN 的基本流程。

在 riscv64 QEMU 上验证了 strace 的基本功能可以正常工作,能输出完整的系统调用追踪。不过,运行 strace -d 时仍然会看到:

1
2
strace: do_test_ptrace_get_syscall_info: PTRACE_GET_SYSCALL_INFO: Function not implemented
strace: PTRACE_SET_SYSCALL_INFO does not work

这是因为 PTRACE_GET_SYSCALL_INFO 还没实现——strace 在调试模式下会探测这个接口的可用性,但这不影响基本的 syscall trace 功能。这是一个后续可以继续完善的点。

另外,其他架构(x86_64、aarch64、loongarch64)的 ptrace 支持已有其他同学贡献(PR #1062, #1247, #1292),但 strace 还未在这些架构上测试过,也是后续可以做的事情。

这次 strace 适配让我对 ptrace 子系统有了比较深入的理解。最大的感受是:像 strace 这种看起来“只差一个接口”的工具,实际上对内核的 ptrace 实现要求非常严格——从信号处理到 exec 流程再到 wait status 编码,环环相扣,任何一个环节的遗漏都会导致整个链路断裂。做这类兼容性工作,不能只看单个接口的实现,需要把工具的实际使用流程(特别是像 SEIZE 这种复杂流程)完整走一遍,才能发现那些隐藏在调用序列中的隐含依赖。

后续的计划

短期来看,先把手里两个未合并的 PR 收尾(#1049 #1323),争取尽快合入。strace 这边还有一些可以完善的地方,比如 PTRACE_GET_SYSCALL_INFO 的实现、以及在其他架构上的测试验证,都是值得跟进的后续工作。

长远一点来说,这次训练营让我对操作系统内核开发有了比较切实的感受,而不只是停留在看书的层面。如果有机会的话,希望可以参加相关实习,继续往这个方向深入下去。

总结

回过头来看这几个月,从 Rust 基础到 rCore 手写内核,再到给 StarryOS 提 PR、修 bug、补系统调用——每一步都在加深我对操作系统的理解。

项目阶段是最有收获的部分。给一个真实的开源内核项目贡献代码,跟在教程框架里写实验是完全不同的体验。最大的区别在于:教程里的问题是有答案的,而真实项目中的问题没有现成的标准答案,需要自己去分析、去摸索。比如 strace 那条链路,一开始只想着”实现 PTRACE_LISTEN 就行了”,结果发现上游的 SIGCONT 时序、exec 的 stop 语义、甚至 wait status 的编码格式,一环扣一环,缺一个整个链路就断。这种 debug 不是”某个地方写错了”,而是”缺少了一整块逻辑”——能定位到问题本身,就已经是能力提升的体现。

比较遗憾的是,由于我几乎比别人晚开始一周,方案二这边其实有不少想法还没来得及展开。像 strace 的 PTRACE_GET_SYSCALL_INFO 支持、其他架构上的测试验证,这些都是后续可以继续深入的方向。

关于 AI 的使用,也是这次项目阶段体会比较深的一点。AI 确实很强,合理使用的话效率提升肉眼可见。但用下来的一个感受是:AI 特别容易在一个地方钻牛角尖,死磕某个错误的思路不肯回头。这时候就得靠人去给它指方向、补充相关的 context,帮它跳出那个坑。说到底,复杂的问题永远都是复杂的——AI 可以帮忙搬砖,但想清楚要搬什么、往哪搬,还是得靠自己。

最后感谢训练营中各位老师的指导,感谢 OS 训练营提供这样一个平台供我学习,感谢与我热心交流的各位同学们,祝 OS 训练营越办越好。

一、项目背景

StarryOS是基于ArceOS模块基础的Linux兼容OS。实现了绝大多数的linux syscall,并可以运行很多linux用户软件。本次训练营项目阶段,我通过添加测试,下载使用真实linux应用,与linux kernel对比等等方式,丰富和完善了starry的功能。


二、主要任务

本次训练营,我的工作主要分为了三个阶段来进行。

2.1 阶段一:socket数据面(Data Plane)syscall测例与语义完善

2.1.1 syscall簇功能介绍

此syscall簇共有四个子syscall。

  1. sendto - 向指定地址发送数据报。主要用于udp协议
  2. recvfrom - 接收数据报并获取源地址
  3. sendmsg - 发送高级消息(Scatter/Gather + 控制信息)
  4. recvmsg - 接收高级消息(状态回填 + 截断检测)

2.1.2 测试的添加过程

我与大模型合作,阅读了linux接口文档,根据所有的输出语义,生成了syscall的测试。结果76个测试中,错误了34个。大部分的socket处理不符合语义。详细阅读了源码,我发现了几个重大的问题,并且逐个进行修复。

2.1.3 测试过程中,进行的源码修复

(1) axerrno模块增强
  • 文件: components/axerrno/src/lib.rs
  • 新增错误类型:
    • DestAddrRequired (EDESTADDRREQ) - 目标地址必需
    • MessageTooLong (EMSGSIZE) - 消息太长
  • 用途: 支持更精确的socket错误返回
(2) sendto系统调用修复
  • 文件: os/StarryOS/kernel/src/syscall/net/io.rs
  • 修复内容:
    • 修复地址参数处理逻辑:addr=NULL时返回None(不管addrlen),addrlen=0时返回EINVAL
    • 简化send flags处理:使用SendFlags::from_bits_retain(flags)直接解析所有标志位
    • 支持更多MSG_*标志(MSG_MORE, MSG_OOB, MSG_DONTROUTE等)
(3) recvmsg截断状态跟踪
  • 文件: os/StarryOS/kernel/src/syscall/net/io.rs
  • 新增功能:
    • 添加truncated_out参数跟踪消息截断状态
    • 在recvmsg成功后正确设置msg.msg_flags(包含MSG_TRUNC标志)
(4) UDP模块 MSG_MORE Corking
  • 文件: os/arceos/modules/axnet-ng/src/udp.rs
  • 实现功能:
    • 支持MSG_MORE标志:缓冲多个sendto调用的数据
    • 在不带MSG_MORE的调用时一次性发送合并的数据
    • Cork状态捕获首次MSG_MORE调用的端点地址(Linux语义)
    • 限制cork缓冲区大小(UDP_TX_BUF_LEN)防止内存溢出
(5) UDP recv改进
  • 修复: 移除未绑定socket的NotConnected检查(允许bind后立即recv)
  • 功能: 支持truncated状态跟踪
(6) Socket标志位扩展
  • 文件: os/arceos/modules/axnet-ng/src/socket.rs
  • 新增SendFlags:
    • OOB (0x01) - 带外数据
    • DONTROUTE (0x04) - 不经过网关
    • EOR (0x80) - 记录终止
    • CONFIRM (0x800) - 链路层邻居确认
    • NOSIGNAL (0x4000) - 抑制SIGPIPE
    • MORE (0x8000) - cork/合并发送
  • 新增RecvOptions字段:
    • truncated - 跟踪消息截断状态
(7) Raw Socket修复
  • 文件: os/arceos/modules/axnet-ng/src/raw.rs
  • 修复内容:
    • 添加对MSG_OOB标志的检查(返回OperationNotSupported)
    • 添加对MSG_DONTROUTE的TODO注释
    • 添加对truncated状态的TODO注释

修复了这些问题后,绝大多数的返回值全部符合linux语义。可以说大部分跟tcp和udp协议相关的传输问题都根本上解决了。

2.2 阶段二:一些小中型应用的测例添加和内核修复

这里我也学习了很久,好在有其他同学的帮助,并且可以参考别人的测试架构。我也构建了smoke normal stress三层次的测试架构。逐步测试和完善应该有的功能。

2.2.1 pip(python包管理器)

测试了pip的各项功能,创建venv环境的能力,以及下载更新等管理py包的能力。

2.2.2 mosquitto(MQTT协议的物联网设备通信软件)

MQTT协议用于低带宽物联网设备通信。这里测试了日常使用的各项功能。

可能存在问题:当前容器内测试没法走网络模拟两个机子的通信。自己创建了一个server和client测试,识别到在一个ip就会直接loopback。需要补充真实板测来模拟结果。

2.2.3 ffmpeg(音视频处理软件)

用于测试starryos的大规模mmap,流媒体处理,音视频解码能力。使用python自建server来抓取流媒体信息来测试一些操作

2.2.4 app的适配过程中的内核修复

我通过apk包管理器从仓库下载并且运行,然后捕捉错误。同时借助本机linux环境验证和寻找错误。最后锁定了几个内核bug。
通过修复了以下几个重要内核错误,最终实现了三个app的支持。

(1) rsext4 delete_dir 递归删除

文件: components/rsext4/src/file/delete.rs

问题: rmdir会直接递归删除所有的文件。根源是系统会调用delete_dir函数,这个函数是递归删除文件树。(等价rm -rf)这是linux绝不允许的。(linux的标准应该是内核不能有递归删除功能,所有递归删除应该在用户态。因为直接删除所有文件还没提醒非常不安全)

修复内容:

  • 新增is_dir_empty()函数,检查目录是否为空
  • delete_dir函数中添加空目录检查,非空目录返回ENOTEMPTY
  • 添加Ext4Error::not_empty()构造函数

相关文件:

  • components/rsext4/src/error/mod.rs - 添加错误类型
  • components/rsext4/src/file/mod.rs - 导出is_dir_empty
  • components/rsext4/src/lib.rs - 导出is_dir_empty
  • os/arceos/modules/axfs-ng/.../inode.rs - VFS层添加检查
  • os/arceos/modules/axfs/src/fs/ext4fs.rs - 旧VFS层添加检查

测试: 新增bug-ext4-dir-ops测试套件,包含87个断言,覆盖4种架构

(2) xattr 系统调用缺失

文件: os/StarryOS/kernel/src/syscall/fs/ctl.rs

问题: pip会将要删除的文件从用户文件夹移动到/tmp文件夹,作为”临时回收站”。而他使用了rename来实现移动。但是,/root文件夹是rsext4系统,而/tmptmpfs系统,存在跨文件系统移动文件夹,于是返回EXDEV。从而使用复制黏贴到目标文件夹,然后再删除源文件夹这种手段。但是复制的时候,会复制文件的拓展属性。这里就调用了xattr族(这里是listxattr,列出附加属性)。而且必须且无法避免。

修复内容:

  • 新增12个xattr系统调用存根函数
  • listxattr系列:返回0(空列表)
  • getxattr系列:返回ENODATA
  • setxattr/removexattr系列:返回EOPNOTSUPP

相关文件:

  • os/StarryOS/kernel/src/syscall/mod.rs - 添加12个系统调用分发表项
  • 覆盖4种架构:x86_64, aarch64, riscv64, loongarch64

测试: 新增test-xattr测试套件,包含10个测试检查点

(3) socket option 缺失

文件: os/arceos/modules/axnet-ng/src/general.rs

问题: Python 3.12的ssl.SSLSocket._create()调用getsockopt(SO_TYPE)来验证socket类型。StarryOS没有实现这个选项,导致TLS握手失败,阻塞了pip install

修复内容:

  • GeneralOptions结构体中添加domainprotocol字段
  • 修改构造函数为GeneralOptions::new(socket_type: i32, domain: i32, protocol: i32)
  • getsockopt返回存储的值;setsockopt返回ENOPROTOOPT(只读,符合Linux语义)

相关文件:

  • os/arceos/modules/axnet-ng/src/options.rs - 添加SocketTypeSocketProtocolSocketDomain枚举变体
  • os/arceos/modules/axnet-ng/src/opt.rs - 添加SO_TYPE/SO_PROTOCOL/SO_DOMAIN映射
  • os/arceos/modules/axnet-ng/src/tcp.rsudp.rsraw.rs - 更新构造调用
  • os/arceos/modules/axnet-ng/src/unix/dgram.rsunix/stream.rsvsock/stream.rs - 传递正确的三元组

测试: 新增test-sockopt测试套件,包含9个测试用例,覆盖TCP和UDP socket

(4) rename缓存失效导致uninstall失败

文件: components/axfs-ng-vfs/src/node/dir.rs

问题: 当DirNode::rename通过mem::take转移子目录缓存时,每个子DirEntry的内部Reference.parent强引用仍然指向旧的父目录的DirEntry对象。后续通过新路径进行的unlink等操作会解析到错误的父目录,导致ENOENT错误。

修复内容:

  • 移除rename函数中的子目录缓存转移逻辑
  • 保留挂载点转移逻辑
  • 子目录条目现在通过延迟查找从磁盘加载,确保Reference.parent指向正确的父目录

相关文件:

  • components/axfs-ng-vfs/src/node/dir.rs - 修改DirNode::rename函数

测试: 新增7个测试用例,包括test_rename_then_unlinktest_rename_file_then_delete_old_path等,模拟pip卸载模式

(5) 删除文件时索引偏移导致删除出错

文件: components/rsext4/src/file/delete.rscomponents/rsext4/src/file/inode.rs

问题: read_dir使用逻辑索引(只计算未删除的条目),当rm -rf在连续的getdents64调用之间删除条目时,逻辑索引发生偏移,导致后续条目被跳过。

修复内容:

  • delete.rs: 修改remove_dentry_in_dir_block函数,只清零被删除条目的inode字段,保留rec_len不变
  • inode.rs: 修改read_dir函数,手动遍历原始目录块数据,包括已删除的条目(inode==0),累积物理字节偏移

相关文件:

  • components/rsext4/src/file/delete.rs - 修改删除逻辑
  • components/rsext4/src/file/inode.rs - 修改目录读取逻辑

测试: 新增2个测试用例:test_readdir_offset_after_deletetest_rm_rf_pattern,使用原始SYS_getdents64系统调用

2.3 阶段三:基于Wayland协议的图形界面的完善和应用测例的添加

在我进入这个阶段时,wayland已经初步支持好了。可以显示样例程序。我随即想要适配一下实际应用。这里遇到很大困难,全网关于此内容的学习资料实在太少。只有官方文档,少数中文博客。视频仅有b站plct实验室有相关内容。在适配的过程中,一半时间大概都是理论学习还有与ai对接内容。这段时间我的学习能力真的遇到极大挑战

2.3.1 ffplay(ffmgeg的播放器,基于sdl2)

视频播放器的适配。主要用于验证sdl2。同时sdl2必须适配opengl组件(它需要gl一个返回值才能正常工作),所以我一并适配了mesa GL库,用llvmpipe模式来进行cpu模拟渲染。

效果:可以以每秒5到10帧播放视频

2.3.2 doomgeneric(doom1993的开源实现,基于sdl2)

doom是非常著名的游戏,开发者非常为之着迷。有句话:有cpu有屏幕就能跑doom。

我基于sdl2进行了一个移植。一并修复和验证了鼠标键盘输入的问题。

效果:可以利用键盘鼠标进行游戏。

2.3.3 qt-calc(计算器应用,基于qt6)

以上两个都是基于sdl2库,我选择qt作为下一个测试的库。

alpain仓库里qt应用不多,我选择了qt-calc来适配。验证qt应用能否正常运行,同时也验证了desktop-shell的运行(堆叠桌面)

效果:可以点击计算结果,并且应用是窗口化的,可以随意移动窗口

2.3.4 app的适配过程中的内核修复

(1) 修复构建系统axbuild的软连接复制问题

文件: scripts/axbuild/src/rootfs/inject.rs

问题: 原先的build阶段,没法把软连接文件(xxxx.so.1等等)复制进starry。我修复了更改逻辑。现在可以成功复制,避免出现缺少文件的情况

修复内容:

  • 新增is_symlink()分支,检测符号链接并生成debugfs symlink命令
  • 采用两遍遍历策略:第一遍处理目录和常规文件,第二遍创建符号链接
  • 修复debugfs symlink参数顺序(v1.47.0实际语法与man page不一致)
  • 相对符号链接目标转换为绝对客户机路径

相关文件:

  • scripts/axbuild/src/rootfs/inject.rs - 修改collect_overlay_debugfs_commands函数

测试: 新增symlinks_are_emitted_after_regular_files测试,覆盖ldconfig风格的符号链接链

(2) 实现真正的PRIME dma-buf

文件: os/StarryOS/kernel/src/pseudofs/dev/card0.rs

问题: 原先的实现,PRIME_HANDLE_TO_FD是身份映射,在真实场景会导致为定义行为,这也是gl库段错误的根本原因。现在实现注册真正的fd

修复内容:

  • 新增DmaBufGem结构体,包含物理地址范围、引用计数页面和大小
  • 修改handle_prime_handle_to_fd函数,查找dumb表中的句柄,构造DmaBufGem,注册真正的内核fd对象
  • 修改handle_prime_fd_to_handle函数,使用downcast_ref::<DmaBufGem>()进行类型安全导入
  • DmaBufGem上实现device_mmap方法,添加边界检查

相关文件:

  • os/StarryOS/kernel/src/pseudofs/dev/drm.rs - 添加DRM_CAP_PRIME常量和处理函数

安全改进:

  • 类型安全:downcast_ref拒绝非dma-buf类型的fd
  • 命名空间隔离:fd号不再直接转换为GEM句柄
  • 引用计数:Arc::clone防止并发DESTROY_DUMBclose(fd)导致的use-after-free
(3) 修复输入事件传递

文件: os/StarryOS/kernel/src/pseudofs/dev/event.rs

问题: 解决鼠标/键盘事件无法从内核 evdev 到达 Weston/Qt 的问题。原先内核能接受输入事件,但是传到weston就丢失了。原来是IRQ不触发,libinput 用的是 epoll_wait,需要被唤醒才会去读。

修复内容:

  • 新增has_waiters: AtomicBool字段,跟踪是否有进程在等待
  • 新增start_polling()方法,作为后台轮询任务
  • 修改register_irq()方法,无条件调用start_polling()作为后备方案
  • 修改Pollable::register()方法,设置has_waiters = true

自适应频率:

  • 有等待者时(libinput在epoll_wait中):100Hz轮询(10ms间隔)
  • 无等待者时(无人读取设备):5Hz轮询(200ms间隔)

安全分析:

  • 锁顺序:释放内部锁后再调用wake(),无死锁风险
  • 内存顺序:正确使用Ordering::Acquire/Release
  • 竞态条件安全:与IRQ处理程序共享Mutex<Inner>
(4) DRM stride 修复

文件: os/StarryOS/kernel/src/pseudofs/dev/card0.rs

问题: present_fb() 原先使用 flat memcpy,假设源 buffer 和 axdisplay scanout 的 stride 完全一致。当 GL 渲染器使用的 buffer stride 与 scanout 不同时,每一行偏移若干像素,产生十几段稳定的斜撕裂线(pixman 模式无此问题)。

修复内容:

  • Framebuffer结构体中添加stride: u32字段
  • 修改handle_addfb2函数,从dumb buffer的pitch值存储stride
  • 重写present_fb函数,支持stride感知的帧缓冲区呈现:
    • stride匹配(快速路径):零开销的平面memcpy
    • stride不匹配(安全路径):逐行复制,带防御性边界检查

边界检查:

  • dst_limit = info.fb_size / info.stride.max(1) - 防止除零和边界溢出
  • bytes_per_row = (src_stride as usize).min(info.stride) - 确保源端不溢出
  • rows.min(dst_limit) - 确保复制不超过目标容量

三、达到的收获和效果

3.1 socket调用相关的语义基本对齐linux

修复前,大部分socket相关的syscall语义错误。经过修复后完善了starry的网络栈,网络能力得到了很大的加强。

3.2 多个有价值应用适配starry,并且修复了多个内核bug

修复多个bug,且适配了多个linux app。这些app都是实际生产常用的,证明了starryos实用能力。

3.3 Wayland对opengl cpu模拟模式的支持,对鼠标键盘输入的支持,对desktop-shell的支持

通过内核修复,wayland协议已经可以通过opengl库的llvmpipe(cpu模拟)来实现渲染。相比以前的pixman渲染,虽然都还是cpu渲染,但增强了拓展能力(sdl2和qt6都需要经过gl库进行检查,否则会直接崩溃)。而且理论上只要增加gpu支持,就可以使用硬件加速。因为starry可能在rk3588等有gpu的板子运行,还是很有必要的,后续也可以加入virgl的支持。键盘鼠标的事件之前无法传递到weston。修复后,点击就可以响应了。同时也支持了desptop-shell桌面环境,可以使用堆叠窗口。

总结:现在的StarryOS的桌面环境,已经很接近Linux桌面。可以使用堆叠桌面,使用键鼠交互,使用基于主流图形库的应用。

3.4 Wayland路径目前可运行大部分主流图形库

gtk4,sdl2,qt6三个重要图形库,全部经过实际应用测试。这三个库是大部分gui应用的底层库。这说明理论上很多的gui应用都可以在starry上以wayland协议启动了。


四、学习到的知识和经验

4.1 我学会了多人合作开发代码的基本流程。

在此之前,我只个人开发过一些小项目,没有多人的开发经验。现在我熟悉了基于git的多人工作的方式。这对我未来工作还是参加其他开源项目无疑是极大的帮助。同时也给了我参与开源项目的勇气。

4.2 我学会了与ai合作进行代码开发的基本工作流程。

我学会了如何使用ai加速我的开发速度。也学会了怎么借助大模型,来学习新知识和快速了解问题。在ai时代,学习使用大模型辅助自己,同时又不丢失自己学习的能力。这也是非常重要的技能。

4.3 我对操作系统理论的理解更进一步。

原先我对操作系统的了解仅限rcore这种教学项目,tgoskit更加贴近真实的操作系统内核,让我了解了更加复杂的操作系统的组成和运作方式。相信未来我着手其他更复杂的os项目也能更加得心应手。

4.4 我了解了为内核适配软件的基本方法。

开始我完全不了解怎么进行这个工作。在老师同学帮助下我渐渐熟悉了构建方法。知道了怎么通过导入包,根据各种测试等检测starry的功能,并最后修改kernel来完善功能。通过这些方式,我成功修复了很多问题,支持了数个linux软件,丰富了starry生态。

4.5 我了解了操作系统图形化界面产生的基本原理。

wayland,weston,EGL,DRM,KMS……等等一堆专业的术语,在此之前我完全看着就晕。但是经过几个月的学习,现在的我,已经了解了一个像素怎么产生,到最后怎么显示在一个屏幕上。这对我也是巨大的收获。


五、总结

本次是我第一次参加opencamp训练营。同时也是第一次进入项目阶段。本身我就对rust语言还有操作系统持有浓厚的兴趣。寒假时同学的推荐,给我打开新的大门。几个月的时间,我对os的理解从完全不懂到参与项目,并有一定自己的理解,能找到下一步学习的方向。这些都要感谢所有开源训练营的贡献者。以后我也会积极投入到开源项目的贡献中!

我也知道了ai的力量非常有限。遇到困难的问题,依然需要我去解决。或者写及其具体的提示词才可以解决。我也明白提升自己个人实力才是解决问题的关键还有ai时代要掌握的能力。

感谢opencamp让我有机会接触到这些知识,让我有这些经验。感谢各位老师和同学在这段时间给予的帮助和教导。这段时间收获镇的很多,感觉自己处理问题,学习新知识的能力提升了非常多。我很荣幸能学习这门优质的课程。


六、pr列表

  • GitHub ID: zyc107109102
  • 学习进度issue: issue #577

总计13个pr提交

序号 PR链接 说明
1 PR #598
2 PR #854
3 PR #882
4 PR #884
5 PR #938
6 PR #1001
7 PR #1002
8 PR #1072
9 PR #1086
10 PR #1191
11 PR #1268
12 PR #1396
13 PR #1415

摘要

本次训练营我的工作主线是:围绕 StarryOS 的 Linux 网络应用兼容性,持续做「应用驱动测试 -> 问题定位修复 -> 测试框架固化」。

阶段性成果可概括为三点:

  1. 完成 Nginx 和 Apache 测试框架从零到一的构建,建立 smoke/phase/debug 三层测试体系。
  2. 构建网络性能测试基础设施(net-bench),集成 eBPF 观测工具,为网络栈持续优化提供工具支撑。
  3. 完成 OS 功能赛 proj57(ACT 推理)QEMU 和 RK3588 两阶段交付,NPU 执行性能已优于 Linux。

一、训练营阶段工作

1.1 第一阶段:系统调用测试基础

在第一阶段以单一系统调用为目标,完成了事件复用和文件同步两个方向的测试与修复。

具体工作

  1. 事件复用系统调用

    • 实现 epoll_create1epoll_ctlepoll_pwaitepoll_pwait2 测试
    • 修复 sigsetsize 检查与 Linux ABI 不一致问题
  2. 文件同步系统调用

    • 对齐 fsyncfdatasyncsync_file_rangesyncsyncfs 的 Linux 语义
    • 修复返回值和错误码偏差

对应 PR


1.2 第二阶段:Nginx 测试框架与多架构适配

这一阶段从龙同学(LetsWalkInLine)手中接过初步的 Nginx 支持,将其发展为完整的测试框架。

1.2.1 统一测试入口与分层架构

问题背景

  • 初始版本测试脚本分散,QEMU 配置冗余
  • 缺少统一的测试模式切换机制
  • 难以在 smoke/phase/debug 间快速切换

解决方案

  • 实现 nginx-runner.sh 统一 guest 入口,支持:
    • smoke:CI 快速验证(APK 安装 + 基础启动)
    • phase:阶段性功能测试(phase20-70)
    • debug:问题定位(单步调试、日志增强)
    • stress:性能压力测试
  • 建立三层 QEMU 配置结构:
    • qemu/all/:完整功能测试配置
    • qemu/phase/:单个 phase 快速验证
    • qemu/debug/:调试专用配置(更多内存、日志输出)

代码组织

1
2
3
4
5
6
7
8
9
10
11
12
test-suit/starryos/normal/nginx/
├── scripts/
│ ├── nginx-runner.sh # 统一入口
│ ├── smoke/ # CI 验证脚本
│ ├── phase/ # 功能测试脚本
│ └── debug/ # 调试脚本
├── qemu/
│ ├── {arch}-nginx-all.toml # CI 默认入口
│ ├── all/*.toml # 完整测试
│ ├── phase/*.toml # 单阶段测试
│ └── debug/*.toml # 调试配置
└── build-*.toml # 构建配置

1.2.2 关键问题定位与修复

问题一:多 worker 卡死

  • 现象:worker_processes 4 时概率性卡死
  • 根因:信号中断处理和 EPOLLEXCLUSIVE 语义不完整
  • 修复:
    • 补齐 EPOLLEXCLUSIVEepoll_ctl 中的正确处理
    • 修正 accept() 被信号中断时的返回值和重试逻辑
  • PR:https://github.com/rcore-os/tgoskits/pull/1018 (Merged)

问题二:x86_64 短连接超时

  • 现象:phase31(短连接压测)在 x86_64 上偶发 HTTP 超时
  • 初步怀疑:网络栈性能问题
  • 深入定位:
    • 通过分段计时发现瓶颈在 guest 侧 curl 进程启动
    • x86 QEMU 下外部命令启动开销高达 1-3 秒
    • Nginx 响应时间实际在毫秒级,非瓶颈
  • 解决:将 curl 超时从 5 秒放宽到 15 秒,匹配 x86 实际开销
  • 教训:性能问题需要分段计时,避免误判根因

问题三:loongarch64 内存不足

  • 现象:loongarch64 启动后 OOM
  • 根因:默认内存配置 1GB 不足以运行完整测试
  • 修复:提升到 2GB,并规范化各架构内存配置

1.2.3 稳定性改进

  1. 固定 APK 镜像版本

    • 问题:使用 edge 仓库导致 glibc ABI 漂移,测试不稳定
    • 修复:固定到与 rootfs 一致的 Alpine 版本(3.19)
  2. 动态启动路径修复

对应 PR


1.3 第二阶段:Apache 测试实现

在 Nginx 测试框架稳定后,我参照同样的架构从零实现了 Apache MPM prefork 测试。

1.3.1 测试覆盖设计

Apache 测试分为 7 个 phase,覆盖从基础到高级的完整生命周期:

Phase 覆盖内容 关键验证点
phase20 多进程管理 prefork MPM、graceful 重启、worker 数量控制
phase30 HTTP 静态文件 GET 请求、Content-Type、Last-Modified
phase40 目录访问 DirectoryIndex、FancyIndexing、.htaccess
phase50 日志生命周期 access_log/error_log、graceful 后日志追加
phase55 sendfile 与 range 大文件传输、HTTP Range、mmap/sendfile 路径
phase70 CGI 脚本执行、环境变量、管道通信
phase80 模块特性 mod_rewrite、mod_alias、mod_headers

1.3.2 关键问题定位:TCP_DEFER_ACCEPT

问题背景

  • reviewer 环境下 Apache 启动失败,日志显示 AH00076 警告
  • 警告内容:setsockopt(TCP_DEFER_ACCEPT) failed: errno 92

深度调查

  1. 编写 C 语言探针 tcp-defer-accept-probe.c(172 行)
  2. 验证三个关键问题:
    • setsockopt() 返回值:-1
    • errno:92(协议不支持)
    • 监听 socket 是否可用:可用,能正常 accept()

结论

  • TCP_DEFER_ACCEPT 失败是警告而非错误
  • Apache 能正常回退到标准 accept() 路径
  • 监听 socket 完全可用,不影响功能

smoke 范围调整

  • 基于此结论,将 smoke 收敛到前 4 步:
    • package(APK 安装)
    • files(配置文件就位)
    • environment(环境变量)
    • config test(apachectl configtest
  • 移除 HTTP 测试到 debug/apache-smoke-full.sh

1.3.3 调试工具集

为问题定位实现了 9 个 debug 脚本:

  1. mpm-prefork-wait.sh:观测 prefork worker 启动和等待
  2. accept-mutex.sh:验证 accept mutex 行为
  3. graceful-signal.sh:测试 graceful 重启的信号传递
  4. htaccess-path-walk.sh:追踪 .htaccess 路径遍历
  5. sendfile-mmap-range.sh:对比 sendfile/mmap 性能
  6. cgi-pipe-exec.sh:验证 CGI 管道和脚本执行
  7. log-append-reopen.sh:测试日志追加和重开
  8. phase20-restart.sh:单步调试 phase20 重启逻辑
  9. tcp-defer-accept-probe.c:setsockopt 系统调用探针

当前状态


1.4 网络性能测试基础设施(net-bench)

在方案二与方案三之间,我构建了网络性能测试框架,为网络栈持续优化提供工具支撑。

1.4.1 设计目标与理念

  • “入口 + 参数明确”的严肃测试架构run.sh 为唯一严肃入口,显式指定场景(--scenario)、架构(--arch)、加速器(--accel)、重复次数(--repeat),保证可复现、可对照
  • 智能入口实验化:自动检测环境后委托 run.sh 执行,属于开发期便捷能力,不参与严肃测试
  • 多拓扑支持:vhost(主力性能)、tap(降级方案)、slirp(功能冒烟),新增 tap-smp4 和 vhost-smp4 多核扩展场景
  • 全流程自动化:一键宿主机配置、测试执行、结果汇总、清理回退

1.4.2 实现架构

统一入口与公共流程

围绕 run.sh 构建两级入口体系:

  • 严肃入口:run.sh 显式接收参数,获取公共流程封装后逐轮启动 iperf3 服务端、执行 guest 测试、收集结果、调用汇总脚本
  • 实验性入口:自动检测平台(WSL2/裸 Linux)、架构、KVM 和 vhost-net 可用性,推断参数后委托 run.sh(确保行为一致),测试结束自动清理;另有 WSL2 快捷封装

公共流程封装集中在一处,消除散落硬编码:

  • 常量:网段、端口、拓扑定义
  • 配置矩阵解析:根据 scenario/arch/accel 自动推导 QEMU 配置文件路径
  • iperf3 生命周期:启动、停止、进程级信号兜底(EXIT/INT/TERM)
  • 前置检查:KVM 可用性、vhost-net 可用性、TAP 设备就绪、DHCP 服务端监听校验(:67 端口)
  • 环境指纹:记录硬件/OS/内核/编译参数
  • 结果汇总:mean/stddev,NOISY 标记 >10% stddev

补齐配置矩阵

QEMU 配置文件按 <scenario>-<arch>-<accel>.toml 命名规范覆盖 20 个组合:

  • 场景:slirp、tap、vhost、tap-smp4、vhost-smp4
  • 架构:x86_64、aarch64
  • 加速模式:kvm(同架构硬件加速)、tcg(跨架构或 KVM 不可用,仅功能验证)

一键宿主机配置

bin/setup 实现全自动配置:

  • 自动安装缺失依赖(iperf3、brctl、jq、dnsmasq)
  • 自动加载 vhost_net 模块并放开 /dev/vhost-net/dev/kvm 权限
  • 创建桥接 br0、tap0 设备,启动 dnsmasq DHCP(iperf3 服务端由 run.sh 自管生命周期)
  • 资源与进程状态写入状态文件

bin/teardown 读取状态文件,逆序清理进程和设备,实现状态化回滚。

测试覆盖
| Test ID | 说明 | iperf3 参数 |
|———|——|————-|
| tcp1 | TCP 单流上行 | -c $HOST |
| tcp4 | TCP 4 并发流 | -P 4 |
| tcp1r | TCP 单流下行 | -R (reverse) |
| udp1g | UDP 大包 1Gbps | -u -b 1G |
| udp64 | UDP 64B 小包 | -u -l 64 |

每个测试:1 次 warmup + 5 次正式测量。延迟与短连接由 netperf(TCP_RR/UDP_RR/TCP_CRR)补充。

基于 WSL2 端到端测试发现的 4 个真实缺陷

  1. AX_IP/AX_GW 环境变量注入无效:guest 内核在 xtask 路径下只支持 DHCP 获取地址(axruntime 未解析静态 IP 环境变量),移除误导性环境变量注入并文档化 DHCP 依赖
  2. DHCP 服务端未校验:缺少 DHCP 时 guest 静默 DHCP timed out,现已对 :67 端口监听校验,缺失时直接报错
  3. iperf3 服务端残留:失败运行后 trap RETURNset -e 下未触发,改为进程级 EXIT/INT/TERM 兜底
  4. iperf3 服务端冲突:setup 自启 iperf3 导致与 run.sh 争用端口,改为仅由 run.sh 自管 iperf3 生命周期

1.4.3 eBPF 网络观测工具

依托 CN-TangLin 同学的 eBPF 内核运行时(#848、#850、#886),实现了 net_stats 观测工具。

实现原理

  • kprobe 入口:统计调用次数(*_pkts
  • kretprobe 出口:统计字节数(*_bytes
  • 探测符号:
    • ax_net::tcp::TcpSocket::send
    • ax_net::tcp::TcpSocket::recv
    • ax_net::udp::UdpSocket::send
    • ax_net::udp::UdpSocket::recv

技术特点

  • Rust v0 mangled 符号解析
  • Multi-symbol attach(同一探针 attach 到多个 monomorphized 符号)
  • 内置 loopback 自测(--test 模式)

已知限制

  • *_pkts 实际是调用次数,非真实包数
  • 统计层级是 socket 层,不含 TCP/IP header
  • SMP 并发计数非原子,高并发可能丢失增量

当前状态

  • x86_64 self-test 通过
  • x86_64 WSL2 端到端(slirp/tap/vhost/vhost-smp4/tap-smp4 全场景)全部 PASS
  • eBPF 与 net-bench 的自动集成尚在完善

对应分支


1.5 OS 功能赛 proj57:ACT 推理

1.5.1 赛题背景

在 rk3588/sg2002 开发板上实现 ACT(Action Chunking Transformer)模型推理,为移动机器人提供感知-决策能力。

推理规格

  • 输入:RGB 图像 224x224 + 机器人状态 [left_vel, right_vel]
  • 输出:8 步动作序列 [left_vel, right_vel, gripper_target]
  • 预处理:ImageNet 归一化 + QUANTILES 归一化
  • 后处理:动作反归一化 + 方向判断

1.5.2 QEMU 阶段(5月28日)

快速原型

  • 第一天即在 QEMU StarryOS 中跑通 ACT 推理
  • 推理引擎:tract(纯 Rust ONNX runtime)
  • 平台:QEMU riscv64 + musl

关键指标

  • 模型:193.25 MiB ONNX
  • 二进制:~17.26 MiB (riscv64)
  • 推理耗时:~25,233 ms
  • 验证:golden 对照通过

1.5.3 RK3588 阶段(6月2日-6月13日)

6月2日:rk3588 到货

6月3日-6月7日:镜像构建探索

  • 研究香橙派官方镜像结构
  • 分析 FIT image、U-Boot、DTB 布局
  • 手动构建 StarryOS 启动链

6月8日-6月9日:RKNN 推理实现

  • 集成 RKNPU2 runtime SDK
  • 实现 ONNX → RKNN 转换
  • 完成用户态推理程序

6月10日:多核与局部更新

  • 更新 StarryOS 镜像支持多核
  • 实现镜像局部更新脚本(避免完整重刷)

6月11日:突破与验证

  • 关键决策:更换为官方香橙派 DTB 以支持 NPU 驱动
  • 在板上跑通 ACT 推理,输出正确
  • 验证:golden 对照通过,左转/右转样例通过

6月13日-6月15日:性能对比与文档

  • 实现分阶段计时(模型加载/预处理/NPU 执行/后处理)
  • StarryOS vs Linux 对比测试
  • 固化镜像构建工作流(rootfs overlay + FIT repack)
  • 撰写交付报告和复现文档

1.5.4 关键技术突破

镜像构建固化

1
2
3
4
5
# 工作流
1. build-overlay-rootfs.sh # 构建 ACT 程序 overlay
2. make-dtb.sh # 提取官方 DTB
3. repack-fit.sh # 重新打包 FIT image
4. build-image.sh # 生成最终镜像

性能优化
| 阶段 | Linux | StarryOS | 倍数 |
|——|——-|———-|——|
| NPU 执行 | 32.4ms | 28.6ms | 0.88x |
| 端到端 | 22.3ms | 29.9ms | 1.34x |
| 模型加载 | 37ms | 228ms | 6.17x |

结论

  • NPU 执行阶段 StarryOS 已优于 Linux
  • 瓶颈在模型加载和预处理,非推理核心

1.5.5 SG2002 计划

  • 当前状态:尚未开始
  • 后续:申请 sg2002 开发板
  • 技术路径:ONNX → CVITEK TPU,参考 rk3588 流程

对应分支

  • proj57 (未合并)
  • 位置:wt-proj57/proj57/{qemu,rk3588}/

二、方法与经验

2.1 测试框架设计模式

在 Nginx 和 Apache 测试中形成的可复用模式:

  1. 三层测试体系

    • smoke:CI 快速验证,覆盖基础功能
    • phase:阶段性递进测试,覆盖完整生命周期
    • debug:自由度高的问题定位脚本
  2. 统一 runner 架构

    • 单一入口脚本
    • 模式切换通过参数控制
    • 超时和清理统一处理
  3. 分层 QEMU 配置

    • qemu/all/:完整测试(CI 默认)
    • qemu/phase/:单阶段快速验证
    • qemu/debug/:调试配置(更多资源)

2.2 典型教训

  1. 性能问题需分段计时

    • x86 短连接超时案例:误判为网络栈问题,实际是进程启动开销
    • 教训:先做分段计时,再下结论
  2. smoke 测试范围要保守

    • Apache TCP_DEFER_ACCEPT 案例:在 reviewer 环境失败
    • 策略:smoke 只覆盖确定可用的功能,可选特性放到 debug
  3. 小步快跑的重要性

    • 反面案例:方案三很多想法(网络命名空间、流水线推理)未落地
    • 原因:步子太大,未及时拆分和提交

2.3 工具建设的价值

net-bench 和 eBPF 工具虽然尚未完全成熟,但建立了方法论:

  • 自动化环境配置和清理
  • 状态跟踪和回退保护
  • 内核态观测能力(eBPF)

这些为后续网络栈持续优化提供了基础。


三、产出索引

3.1 已合并 PR(共 9 个)

  • #658: epoll 测试
  • #900: epoll/sigmask sigsetsize 修复
  • #903: 文件同步系统调用对齐
  • #1014: Nginx CI 初始实现
  • #1018: 多 worker 信号中断修复
  • #1038: Nginx 完整测试框架
  • #1267: 动态启动路径修复
  • #1297: x86 IPI 修复
  • #1316: VmPeak & VmHWM 增强

3.2 开放 PR(共 2 个)

  • #1311: Apache 测试框架 (Open)
  • #1417: net-bench 网络性能测试框架 (Open)

3.3 独立分支(未合并)

  • test/alpine-nginx:Nginx 测试最新版本
  • test/alpine-apache:Apache 测试
  • feat/net-enhance:网络性能测试框架
  • proj57:ACT 推理 QEMU + RK3588

3.4 工作仓库


四、致谢与总结

感谢助教 Mr Graveyard(方案一)、Ajax(rk3588)、周睿老师(rk3588 NPU 优化和 PR review)的帮助。感谢同组学员 aptacc2421、WellDown64 的讨论交流,感谢龙同学(LetsWalkInLine)交接的初步 Nginx 支持。特别感谢 CN-TangLin 同学的 eBPF 内核运行时工作,为网络观测工具提供了基础。

这次训练营让我从”写测试”进阶到”建框架”,从”跑通功能”深入到”定位根因”。最大的收获不是代码量,而是工程方法上的变化:更重视测试覆盖、问题跟踪和工具建设,也更能把复杂问题拆成可验证的小问题逐步推进。

后续计划:完善 net-bench 集成、推动 Apache 测试合并、完成 sg2002 ACT 推理、参与网络栈持续优化。

Read more »

摘要

本文总结我在2026春季操作系统训练营(Stage 7 专业阶段)中的工作。我在”方案二:以Linux App为引导的StarryOS改进”框架下的子课题3(基于eBPF的内核可观测性能力增强) 中,为StarryOS内核完成了eBPF子系统全栈构建、kprobe/kretprobe内核探针、LKM内核模块加载、三架构手写JIT编译器、多项系统调用增强与工程质量改进。累计贡献 11个已合并PR3个当前开放PR(均在CI验证中,已获bot APPROVED),历史关闭PR 25个,新增15000+行代码,378个commits。代码覆盖x86_64、RISC-V 64、AArch64、LoongArch四大架构。


PPT链接


一、项目背景

StarryOS是构建在ArceOS组件化框架之上的Linux兼容操作系统,目标是运行未经修改的Linux用户态程序。训练营要求在StarryOS中实现eBPF(extended Berkeley Packet Filter)子系统——这是Linux内核的核心可观测性基础设施,允许用户安全地在内核中运行沙箱化程序,广泛应用于性能分析、安全监控、网络追踪等场景。

训练营按三个方案阶段推进。我在方案一阶段完成了select/poll/ppoll/pselect6多路复用系统调用体系完善(43个测试模块)和sys_msync+SQLite支撑;在方案二阶段将工作重心转移到子课题3(eBPF内核可观测性),与助教linfeng(Godones)老师和LorenzLorentz同学三人协作,严格对齐《StarryOS迁移计划》交付eBPF全栈。

eBPF子课题架构全景图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌──────────────────────────────────────────────────────┐
│ 用户态工具层 │
│ aya eBPF 三件套 (PR #886) │ test-ebpf-advanced │
│ kret/rawtp/mytrace/... │ test-ebpf-attach │
│ qperf analyzer │ test-ebpf-basics │
├──────────────────────────────────────────────────────┤
│ 系统调用接口层 │
│ sys_bpf() │ sys_perf_event_open() │
│ BPF_PROG_LOAD │ PERF_EVENT_OPEN │
│ MAP_CREATE/LOOKUP │ RAW_TRACEPOINT_OPEN │
├──────────────────────────────────────────────────────┤
│ eBPF 运行时层 (kbpf-basic) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ eBPF VM │ │ Map Manager │ │ Verifier │ │
│ │ 解释器 │ │ Array/Hash/ │ │ 程序验证 │ │
│ │ ALU/JMP/ST │ │ PerfEventArr │ │ │ │
│ │ eBPF helpers │ │ fd table │ │ │ │
│ └─────────────┘ └──────────────┘ └─────────────┘ │
├──────────────────────────────────────────────────────┤
│ 数据通道 & 事件源层 │
│ perf_event 环形缓冲区 │ Tracepoint (静态追踪点) │
│ KCOV 覆盖率收集 │ debugfs tracing/ │
├──────────────────────────────────────────────────────┤
│ 动态探针基础层 (kprobe / kallsyms) │
│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ kprobe │ │stop_machine│ │ LKM loader │ │
│ │ kretprobe│ │ 安全代码段 │ │ kmod_loader │ │
│ │ 4架构 │ │ 修改 │ │ xtask build │ │
│ └──────────┘ └───────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────┘

二、训练营其他阶段工作

在进入Stage 7专业阶段之前,经过训练营早期任务的基础训练(Rust异步编程、协程并发模型等),我直接投入到tgOSKits仓库的StarryOS内核开发中。

2.1 传统I/O多路复用系统调用体系(方案一)

在方案一阶段,我主要负责了select/poll/pselect6/ppoll四个核心syscall的深度测试与边界验证:

改进方向 状态 PR
select/pselect6 用户态测试用例 #563
select/poll/pselect6/ppoll 深度测试套件(43个独立测试模块,5大阶段) #679

测试套件覆盖超时、空fd_set、pipe读写、信号中断(EINTR)、POLLHUP/POLLERR边界等场景。

2.2 sys_msync接口完善与SQLite支撑(方案一)

改进方向 状态 PR
sys_msync接口完善,处理页对齐约束,打通脏页回写 #602
SQLite集成压测在StarryOS上跑通 #602

2.3 多核并行与基础设施(方案二)

改进方向 状态 PR
SMP4多核压力测试扩展到x86_64/aarch64/riscv64/loongarch64四架构 #652
axbacktrace无锁化并发重构(static mut→UnsafeCell) #655
DRM逐缓冲内存分配,Weston合成器双缓冲铺垫 #667

三、主要工作(六条主线)

整体工作按五条主线展开,另有第六阶段(工程质量改进)正在进行中。以下各章节均标注了具体的文件路径与行号范围,格式为文件路径:起始行-结束行

主线一:kprobe/kretprobe内核探针(#847, MERGED)

kprobe是Linux内核最核心的动态追踪机制,允许在任意内核函数入口/出口处插入探针。我实现了完整的kprobe/kretprobe支持,跨越四大架构:

核心实现os/StarryOS/kernel/src/kprobe.rs (535行)

  • KernelKprobeOps实现KprobeAuxiliaryOps:copy_memory、set_writeable、alloc/free_kernel/exec_memory、kretprobe instance管理
  • 4架构trapframe_to_ptregs() / ptregs_write_back() 寄存器转换:x86_64、RISC-V 64、AArch64、LoongArch
  • 基于IPI的stop_machine机制,确保探针注册/注销时的代码修改安全
  • 增加kretprobe_stack字段,支持嵌套的kretprobe
  • KernelRawMutex = RawSpinNoIrq作为锁类型

AArch64架构修复(#887, MERGED):

  • 修复aarch64异常返回时SP指针未正确保存的严重Bug
  • 代码位置:components/axcpu/src/aarch64/context.rs + trap.S

主线二:eBPF子系统的全栈构建(#848, MERGED)

eBPF子系统的核心实现覆盖了BPF Map管理、程序加载与验证、Helper函数注册、perf event集成。整个子系统实现了bpf(2)系统调用的9条主要命令:

BPF命令 功能
BPF_MAP_CREATE 创建Map(Array/Hash/PerCPU/RingBuf等)
BPF_PROG_LOAD 加载BPF程序,经verifier验证
BPF_RAW_TRACEPOINT_OPEN 打开raw tracepoint
BPF_MAP_UPDATE_ELEM 更新Map元素
BPF_MAP_LOOKUP_ELEM 查找Map元素
BPF_MAP_DELETE_ELEM 删除Map元素
BPF_MAP_GET_NEXT_KEY 遍历Map
BPF_MAP_FREEZE 冻结Map(只读保护)
BPF_MAP_LOOKUP_AND_DELETE_ELEM 原子查找删除

核心代码文件(均在os/StarryOS/kernel/src/ebpf/下):

文件 行数 功能
mod.rs 174行 bpf(2)系统调用入口,9个命令调度
prog.rs 108行 BpfProg封装EbpfPreProcessor
map.rs 158行 BpfMap封装UnifiedMap,支持poll+mmap
transform.rs 302行 EbpfKernelAuxiliary桥接kbpf-basic到StarryOS内核
error.rs 19行 BpfError→AxError转换

perf_event子系统集成(均在os/StarryOS/kernel/src/perf/下),支持4种事件类型:

类型 状态 代码位置 备注
PERF_TYPE_KPROBE kprobe.rs (208行) 含kretprobe(config=1)
PERF_TYPE_SOFTWARE bpf.rs (361行) 软件事件,支持ringbuf mmap
PERF_TYPE_TRACEPOINT tracepoint.rs (160行) 通过id查找
PERF_TYPE_UPROBE uprobe.rs (92行) 解析ELF文件映射地址

perf事件调度器perf/mod.rs (312行) — perf_event_open(2)调度器,PERF_FILE表(fd→event弱引用)

eBPF运行时核心perf/bpf.rs (361行)

  • BpfPerfEventWrapper:ringbuf + poll支持
  • OwnedEbpfVm:核心执行引擎,先尝试JIT编译再fallback到解释器
  • 利用user_copy汇编接口打通probe_read_user
  • 手写实现核心Helper函数

eBPF用户态测试程序:位于apps/starry/ebpf/目录下(#874, MERGED),包含2个测试程序(test-ebpf-advanced 含26个测试模块 + test-ebpf-attach),覆盖kprobe、tracepoint、raw tracepoint、uprobe、kretprobe等全场景。

主线三:LKM内核模块支持(#849, MERGED)

实现了Linux兼容的内核模块(LKM)加载机制,使StarryOS能够加载和运行.ko格式的内核模块。

核心实现os/StarryOS/kernel/src/kmod/mod.rs (332行)

  • KmodHelper实现KernelModuleHelper:vmalloc / resolve_symbol / flush_cache
  • init_module():加载.ko ELF → 重定位 → 调用init函数,防止重复注册
  • delete_module():调用exit函数,释放内存
  • lwprintf-rs用于printk支持

主线四:eBPF JIT编译器(三架构手写)

这是我训练营期间最具挑战性的技术工作——从零实现了三个架构的eBPF字节码JIT编译器。该工作的最终产出物是提交到上游 qmonnet/rbpf 仓库的独立 PR(#152),而 tgoskits 仓库中的 feat/ebpf-jit-*-* 分支为早期探索和集成验证阶段代码。

4.1 JIT框架设计(rbpf上游PR #152)

JIT编译器采用统一的两遍编译(Two-Pass)架构,代码位于feat/ebpf-jit-*-*分支(未合入upstream/dev,§4.3 说明当前状态):

文件 行数 功能
ebpf_jit/mod.rs 352行 框架:JitBuffer、JitBackend trait、JitCompiler
ebpf_jit/jit_riscv64.rs ~1311行 RISC-V 64后端
ebpf_jit/jit_x86_64.rs 904行 x86_64后端
ebpf_jit/jit_aarch64.rs 1102行 AArch64后端
  • Pass 1(sizing):遍历BPF指令序列,仅计算每条指令生成的目标机器码长度
  • Pass 2(compile):再次遍历指令序列,生成实际机器码并写入JitBuffer
  • try_jit_compile():公共入口函数,成功返回生成的函数指针,失败自动fallback到解释器

4.2 跨架构寄存器映射

三个架构的BPF寄存器映射遵循统一原则——R0-R5映射到参数/返回值寄存器,R6-R9映射到callee-saved寄存器,R10映射到帧指针:

BPF寄存器 RISC-V 64 x86_64 AArch64
R0 A0 RAX X0
R1 A1 RDI X1
R2 A2 RSI X2
R3 A3 RDX X3
R4 A4 RCX X4
R5 A5 R8 X5
R6 S0(callee-saved) X19(callee-saved)
R7 S1 X20
R8 S2 X21
R9 S4 X22
R10(帧指针) S5 RBP X25

4.3 JIT当前状态

tgoskits 仓库中:三个JIT分支PR(#891/#892/#893 等)均处于CLOSED状态,代码完整但未合入upstream/dev。AArch64分支(feat/ebpf-jit-3-aarch64)基过旧(692 commits behind upstream/dev),当前upstream/dev使用rbpf::EbpfVmRaw::jit_compile()作为JIT实现路径。

rbpf 上游仓库:RISC-V 64 和 AArch64 JIT 后端已作为独立 PR 提交到上游:qmonnet/rbpf#152(OPEN,DCO ✅,AppVeyor ✅ 619 tests pass,等待 maintainer review)。这是 JIT 工作的最终交付物。

主线五:工程质量持续改进(Training Camp后期,进行中)

这一阶段的工作按照”小步快跑、聚焦单一改动”的原则,从初始14个小粒度PR合并为3个关联度更高的当前开放PR(另有#1414被#1412完整取代后关闭)。

5.1 消除魔术数字 + perf event fd read/poll/O_NONBLOCK [#1412]

状态:APPROVED,CI中

合并了7个原始PR的改动,涉及perf/ebpf/tracepoint三个子系统:

改动 关键代码位置
kprobe.rs添加PROBE_CONFIG_ENTRY=0/PROBE_CONFIG_RETURN=1常量 perf/kprobe.rs
消除BPF_JIT_MEM_PAGES=4KRETPROBE_MAX_ACTIVE=10硬编码 perf/bpf.rs, kprobe.rs
tracepoint模块消除4096硬编码、ebpf模块消除helper ID硬编码 ebpf/mod.rs
移除vm.register_allowed_memory(0..u64::MAX),启用rbpf地址空间边界检查 perf/bpf.rs
try_read_record:从perf_event_mmap_page读取环形缓冲区 perf/bpf.rs
PerfEvent::read从轮询式非阻塞读重构为block_on(poll_io(...))阻塞模式 perf/bpf.rs, perf/mod.rs
新增AtomicBool字段存储O_NONBLOCK状态,ringbuf为空立即返回EAGAIN perf/mod.rs

try_read_record签名修正(响应bot审查建议):

1
2
3
4
5
6
7
8
// 签名从 &self 改为 &mut self,由类型系统保证独占访问
// 调用方 PerfEvent::read 已通过 SpinNoPreempt 持有 event 锁
pub(crate) fn try_read_record(&mut self, dst: &mut [u8]) -> AxResult<usize> {
// SAFETY: &mut self 保证独占写入;mmap 页由 VMA 固定且内核可写
let mmap_mut = unsafe { &mut *(kvirt as *mut perf_event_mmap_page) };
mmap_mut.data_tail += record_size as u64;
// ...
}

代码位置:perf/bpf.rstry_read_record方法、data_head/data_tail环形缓冲区wrapping处理

关键安全修复:移除register_allowed_memory(0..u64::MAX)perf/bpf.rs),启用rbpf的check_mem地址边界检查。现代eBPF可观测性程序通过helper函数访问内存,无需直接加载任意地址——直接内存访问是安全漏洞而非功能特性。

5.2 替换panic!/todo!与消除伪文件系统魔术数字 [#1413]

状态:APPROVED,CI中(stacked on #1412)

合并了5个原始PR的改动:

改动 关键代码位置
card1.rs未知DRM ioctl触发panic!()→返回VfsError::OperationNotSupported drivers/card1.rs
ldisc.rs非规范模式VTIME>0触发todo!()→文档注释说明VTIME定时器未实现 tty/ldisc.rs
pseudofs中loop/memtrack/card0消除魔术数字 pseudofs/dev/loop.rs, memtrack.rs, card0.rs
tmpfs statfs和tracepoint路径缓冲区消除魔术数字 fs/tmpfs.rs
ELF loader vec![0;4096]vec![0;PAGE_SIZE_4K] mm/loader.rs

这两项panic修复的重要性:card1.rs中任何用户空间程序发送不支持的DRM ioctl即可导致内核崩溃;ldisc.rs中任何设置了VTIME的终端读取都导致内核panic——这些都是真实可触发的内核稳定性问题。

5.3 注册bpf_get_current_pid_tgid和bpf_get_current_comm helper [#1411]

状态:APPROVED,CI中(stacked on #1412)

kbpf-basic crate未实现helper #14 (bpf_get_current_pid_tgid)和#16 (bpf_get_current_comm)。在StarryOS侧init_ebpf()中直接往mutable BTreeMap插入两个自定义helper函数:

  • bpf_get_current_pid_tgid:使用ax_task::current().as_thread()获取pid/tid,返回(tgid << 32) | tid
  • bpf_get_current_comm:拷贝当前任务的name()到eBPF verifier验证过的内核buffer,null填充不足部分

5.4 #1414 — 已关闭(被#1412完整取代)

#1414 feat(starry-perf): implement read, blocking poll, and O_NONBLOCK support for perf event fd — 该PR的三个串行feature改动已完整合并入#1412,功能完全重叠,故关闭。


四、团队协作:eBPF迁移计划全景交付清单

子课题3由三位成员协作完成:linfeng(Godones)助教老师、LorenzLorentz同学和我(CN-TangLin)。

4.1 工作量较小的部分 —— 基础设施与动态探针(Godones & CN-TangLin)

# 工作内容 负责人 PR 状态
1 扩展axhal对break/debug异常的处理(x86_64 INT3、AArch64 BRK、RISC-V EBREAK) Godones #244 MERGED
2 proc/pid/maps支持 Godones #306 MERGED
3 dynamic debug支持(static-keys机制,/proc/dynamic_debug/control接口) Godones #446 MERGED
4 tracepoint静态内核追踪点宏系统(/sys/kernel/debug/tracing/events/ Godones #673 MERGED
5 内核符号表kallsyms支持(ksym crate) Godones #837 MERGED
6 kprobe支持(4架构TrapFrame↔PtRegs、stop_machine、kretprobe_stack) CN-TangLin #847 MERGED

4.2 工作量较大的部分 —— eBPF核心与LKM模块(CN-TangLin & LorenzLorentz)

# 工作内容 负责人 PR 状态
7 eBPF子系统(Map/Prog/VM/Helper/perf_event,ebpf/ 约750行) CN-TangLin #848 MERGED
7b eBPF测试套件(2个测试程序接入CI) CN-TangLin #874 MERGED
8 eBPF runtime porting(kbpf-basic+rbpf外部crate集成) LorenzLorentz #850 MERGED
8b aya eBPF生态移植 + 6个探针应用接入test-suit LorenzLorentz #886 MERGED
8c eBPF用户态demos(uprobe/kprobe/kretprobe/tracepoint) LorenzLorentz #1132 MERGED
9 LKM支持(init_module/finit_module、KmodSectionMem、R/W/X隔离) CN-TangLin #849 MERGED
9b LKM loader端口 LorenzLorentz #851 MERGED

4.3 技术路线融合

两条技术路线对比:

  • 我的路线(#847/#848/#888):手写eBPF子系统,自实现map/prog/VM/helper/perf_event,仅依赖标准库
  • LorenzLorentz路线(#850):基于外部crate kbpf-basic + rbpf,复用社区成熟组件

最终通过#848的合并,两条路线融合为统一的eBPF执行框架,用户态程序可透明切换使用两种实现路径。

4.4 Godones助教后续支持

PR 内容 状态
#1192 eBPF apps现代化,新增rawtp/upb2 demo MERGED
#1208 eBPF ringbuf mmap在LoongArch DMW上的支持 MERGED
#1256 BPF JIT内存对齐修复 MERGED
#1279 LoongArch DMW-backed kmods支持 MERGED

五、量化成果

5.1 PR统计

类别 数量 说明
已合并PR 11 #563, #602, #652, #655, #667, #679, #847, #848, #849, #874, #887
当前开放PR(APPROVED,CI中) 3 #1411, #1412, #1413
历史关闭PR 25 含JIT系列(#891/#892/#893等)、整合系列(#805/#888/#1035等)、小粒度PR(#1399-#1405等)、#1414(被#1412取代)

5.2 代码量统计

维度 数据
已合入dev的commits 378
新增代码行数 15000+
架构覆盖 x86_64 / RISC-V 64 / AArch64 / LoongArch
JIT代码量 RISC-V 64后端1311行 + x86_64后端904行 + AArch64后端1102行 = 3317行(含框架352行)
测试程序 2个eBPF用户态测试程序(advanced 26模块 + attach) + select/poll家族43个测试模块 + SMP并发测试

5.3 功能覆盖

已实现的eBPF功能在子课题中覆盖了最全面的技术栈:

  • BPF Map:Array、Hash、PerCPU、RingBuf、PerfEventArray等核心类型
  • BPF Prog:加载、verifier验证、解释器执行、JIT编译
  • Helper函数:含bpf_get_current_pid_tgid、bpf_get_current_comm等
  • perf_event:4种类型(KPROBE/SOFTWARE/TRACEPOINT/UPROBE)
  • kprobe/kretprobe/uprobe:完整支持,四个架构
  • LKM:.ko加载/卸载,kmod-helper集成
  • JIT:三架构手写JIT编译器

六、经验与教训

6.1 工程管理经验

PR粒度控制:训练营后期实践了”小PR→合并为大PR”的迭代模式。初期14个小粒度PR各有独立分支和CI,但过于碎片化。通过与reviewer沟通后,将关联度高的PR合并为3个(#1411/#1412/#1413),每个合并PR都有清晰的单一主题,便于review。另有#1414被#1412完整取代后关闭。

Conventional Commits + bot辅助审查:严格遵循type(scope): description格式,标题用英文、正文用中文。自动化CI审查大幅减少人工review负担,Bot的CHANGES_REQUESTED → APPROVED循环形成高效的迭代节奏。

小步快跑推PR:从代码完成到合入平均经历2-3轮review+CI,每轮24小时周期内响应反馈。

6.2 技术经验

JIT编译器的两遍Pass设计ebpf_jit/mod.rs):Pass 1仅计算代码长度(sizing),Pass 2生成实际代码。这种设计在不需额外内存的前提下解决了前向跳转指令中偏移量计算的问题——第一遍确定每条指令和每个label的位置,第二遍才能正确填充跳转偏移。

跨架构寄存器映射原则:R0-R5→参数/返回值寄存器(充分利用调用约定),R6-R9→callee-saved寄存器(跨helper调用不丢失),R10→帧指针寄存器。这种一致性设计使JIT框架代码保持架构无关。

eBPF安全边界perf/bpf.rs):移除register_allowed_memory(0..u64::MAX),启用rbpf的check_mem地址边界检查。现代eBPF可观测性程序通过helper函数访问内存,直接内存访问是安全漏洞。

try_read_record&self&mut self重构perf/bpf.rs):由类型系统保证ringbuf data_tail的独占写入,而非依赖注释+调用方纪律。调用方PerfEvent::read已通过SpinNoPreempt持有event锁。

稳定性修复优先级card1.rs未知ioctl→panic!()ldisc.rsVTIME→todo!()——任何用户程序都能触发内核崩溃。在添加新功能之前,消除已有代码中的崩溃路径是更高优先级。

6.3 教训

AArch64 JIT分支管理失误feat/ebpf-jit-3-aarch64分支的初始基选择不当,导致692个commits落后于upstream/dev。长期开发分支应及时rebase到最新的上游主干。

JIT代码未及时合入主线:三个JIT PR尽管代码完整、已通过CI验证,但未能推进到最终合并。技术方案的成功不仅取决于代码质量,也取决于与社区技术路线的兼容性和推进节奏。


七、后续方向

  • AArch64 JIT重新适配:从upstream/dev新开分支,提取JIT文件,适配当前kbpf-basic+rbpf版本
  • JIT回归测试集成到CI
  • BTF支持(CO-RE兼容)
  • LoongArch64 JIT(仅有的缺失架构)
  • BPF_PROG_ATTACH/DETACH/LINK_CREATE通用attach基础设施

八、工作仓库与链接

资源 链接
主仓库(fork) https://github.com/CN-TangLin/tgoskits
上游仓库 https://github.com/rcore-os/tgoskits
汇报Slide仓库 https://github.com/CN-TangLin/tgoskits/tree/dev/slides
Blog提交PR https://github.com/rcore-os/blog/pull/891
项目进展记录(Issue) https://github.com/rcore-os/tgoskits/issues/642
rbpf 上游 JIT PR https://github.com/qmonnet/rbpf/pull/152

当前开放PR(3个,均已APPROVED)

PR 标题 状态
#1411 feat(starry-ebpf): register bpf_get_current_pid_tgid and bpf_get_current_comm helpers CI中(stacked on #1412)
#1412 feat(starry-perf): implement perf event fd read, poll, O_NONBLOCK; replace magic numbers CI中
#1413 fix(starry): replace panics/todos and magic numbers in pseudofs, tmpfs, and mm CI中(stacked on #1412)

关闭说明:#1414(feat(starry-perf): read, poll, O_NONBLOCK for perf event fd)已关闭——该PR的三个串行feature改动已完整合并入#1412,功能完全重叠。

已合并PR(11个)

PR 标题
#563 test(starryos): add select, poll, pselect6 and ppoll syscall userspace tests
#602 feat(starry-kernel): implement sys_msync and add SQLite/msync test suites
#652 test(smp4): extend SMP tests to all 4 architectures and add concurrency testing
#655 refactor(axbacktrace): replace static mut with UnsafeCell in dwarf.rs
#667 feat(drm): per-buffer memory allocation for Weston bringup
#679 test(syscall): add comprehensive select/poll/pselect6/ppoll deep test suite
#847 feat(starry-kernel): add kprobe support
#848 feat(starry-kernel): add eBPF subsystem (maps, VM, helpers, perf events)
#849 feat(starry-kernel): add LKM support via kmod-loader integration
#874 test(starry-kernel): add eBPF advanced and attach/perf_event user-space test suites
#887 fix(axcpu): save SP in aarch64 TrapFrame for kprobe correctness

协作同学与助教PR

负责人 PR 标题
LorenzLorentz #850 feat(starry-kernel): port eBPF runtime (ebpf/, perf/, kprobe wiring)
LorenzLorentz #851 feat(starry-kernel): port LKM loader + cargo xtask starry kmod build
LorenzLorentz #886 feat(starry-kernel): eBPF kernel runtime (tracepoint / kprobe / perf)
LorenzLorentz #1132 feat(starry-apps): runnable eBPF demos
Godones #673 Starry: Add kernel tracepoint infrastructure and debugfs integration
Godones #837 Adds support for kernel symbol dumping via kallsyms
Godones #1192 Modernize eBPF apps and add rawtp/upb2 demos
Godones #1208 fix(starry): support eBPF ringbuf mmap on LoongArch DMW

总结

感谢操作系统训练营在整个项目阶段的指导。从最初学习异步编程,到深入参与StarryOS内核开发,再到独立实现eBPF JIT编译器,这段经历让我从一个只会读书的学生转变成了能解决实际问题的开发者。训练营不仅教会了我操作系统的理论知识,更重要的是建立了”刨根问底排bug、小步快跑推PR、严谨量化写报告”的工程方法论。

特别感谢陈渝老师、周睿老师、向勇老师的悉心指导,感谢王铮学长、朱懿学长、邵志航学长在技术方向和工程规范上的引领,感谢王鹏杰同学在eBPF子课题中的紧密合作,感谢戴骏翔助教、陈林峰助教在基础设施和代码审查上的鼎力支持。没有你们的努力,eBPF子系统不可能达到今天的完成度。同时也感谢训练营所有台前幕后的助教老师和同学们的辛勤付出。

感觉参与训练营之后,学校课程作业都变得异常简单。这段高强度、高密度的内核开发经历,真正锻炼了从定位问题到提交PR的完整工程能力。

Read more »