Lab 028c · 路径规范化与 stat:把相对路径和文件信息跑通
028c 给文件系统加了工作目录和 stat。这个 lab 分两半:前半是「纸上练兵」——
path_canonicalize是个纯字符串函数,特别适合在 host 上拿一批刁钻输入去喂它、断言结果,把./../根目录保护这些边界彻底搞懂;后半是「真刀真枪」——在跑起来的 Cinux 里cd/pwd/stat,再用debugfs对照 stat 读出来的数字和磁盘上的是不是一致。最后留一个整洁度改进任务:把 ext2 里两份一模一样的stat收成一个。
实验目标
- 用 host 单测驱动
path_canonicalize/path_resolve,覆盖.、..、重复斜杠、根目录越过等边界,亲手验证「cd /..还是/」。 - 在 Cinux shell 里端到端走
cd/pwd/stat,确认相对路径解析、工作目录记忆、文件信息查询都正确。 - 用
debugfs对照sys_stat返回的st_size/st_ino/st_mode与磁盘 inode 的真实值,理解「stat 字段从哪来、哪些是空的」。 - (改进)消除
Ext2FileOps::stat与Ext2DirOps::stat的重复。
前置条件
- 028b 的写能力可用(
touch/echo >/mkdir能跑)。 - 028c 的代码已构建:
cmake --build build,且ctest --test-dir build -R cwd_stat能跑。 - host 有
debugfs(e2fsprogs)。准备一块干净镜像:./scripts/create_ext2_disk.sh /tmp/lab.ext2。 - 读懂主书第 028c 章的「路径规范化」和「struct stat 与 InodeOps::stat」两节。
任务分解
任务 1:用 host 单测压测 path_canonicalize
path_canonicalize(char* buf) 是纯函数:输入一个(可能乱七八糟的)路径字符串,就地改成规范绝对路径。它不碰磁盘、不查存在性,纯字符串处理——正是单测的好材料。
参考 test/unit/test_cwd_stat.cpp 的纯函数测试段,自己构造一组输入,对每个断言规范化结果。重点覆盖这几类(括号里是预期输出):
/a/b/../c → /a/c (.. 弹一层)
/a/./b//c → /a/b/c (. 跳过,// 折叠)
/.. → / (根的 .. 仍是根 —— 重点!)
/a/b/../../.. → / (连续 .. 不能越过根)
/ → / (单独的根)
/a/ → /a (尾斜杠去掉)
/a/../.. → / (弹到根再弹,仍根)
//a//b// → /a/b (全是重复斜杠)关键是后三条:任何情况下结果都不该小于 /,也不该出现 out_pos 为负或负数下标。如果你把 path_canonicalize 里的根目录保护(if (out_pos > 1))故意去掉,重新跑这组用例,你应该能看到 /.. 或 /a/../.. 产生错误结果(比如空串、或越界)——这就反证了那行保护的价值。
接口约束:
path_canonicalize入参buf必须可写、长度 <PATH_MAX(4096);返回值无(就地修改);对空串或 nullptr 直接返回不改。
任务 2:path_resolve 的相对/绝对分支
path_resolve(cwd, path, out) 有两条分支:path 以 / 开头走绝对(直接拷贝+规范化),否则走相对(拼 cwd + "/" + path 再规范化)。构造用例覆盖:
cwd="/etc", path="motd" → /etc/motd
cwd="/etc", path="/hello.txt" → /hello.txt (绝对优先)
cwd="/", path="etc/motd" → /etc/motd
cwd="/a/b", path="../c" → /a/c (相对路径里的 ..)
cwd="/a/b/", path="c" → /a/b/c (cwd 带尾斜杠,别拼出 //)注意最后一条:cwd 以 / 结尾时,拼接逻辑要避免出现 //(主书讲过 path_resolve 会判断 cwd 是否已有尾斜杠)。验你的实现不会拼出双斜杠。
任务 3:端到端 cd / pwd
在跑起来的 Cinux shell 里(带交互 shell 的运行方式),验证工作目录真的被记住:
$ pwd → /
$ cd etc
$ pwd → /etc
$ cd ..
$ pwd → / (.. 在 shell 里要能正确解析;注意 shell 怎么把 ".." 传给 sys_chdir)
$ cd /etc/nonexistent → 应被拒(sys_chdir 的目录类型/存在性检查)
$ pwd → 仍是上一个有效 cwd(失败的 cd 不该改 cwd)要核对的点:失败的 cd(目标不存在或不是目录)不能改变当前 cwd。看 sys_chdir 的实现——它在 lookup 失败和类型检查失败时都是 return -1,没动 current->cwd。你的 shell 行为应符合这一点。
任务 4:stat 与 debugfs 对照
这是把「内核 stat」和「磁盘真相」对齐的环节。先用 debugfs 在干净镜像上记下几个文件的 inode 信息:
debugfs -R "stat <hello.txt 的 inode>" /tmp/lab.ext2 # 记下 size、mode、links
debugfs -R "ls -l /" /tmp/lab.ext2 # 记下各文件的 inode 号然后在 Cinux 里对同一个文件 stat(具体 shell 的 stat 命令格式以 cmd_stat.cpp 为准),对比:
st_size应等于debugfs stat显示的 size。st_ino应等于debugfs ls -l显示的 inode 号。st_mode的类型位(目录0x4000/普通文件0x8000)应与文件实际类型一致。st_nlink应等于 inode 的 links count(普通文件通常 1,目录至少 2)。st_blksize应是 ext2 的 block size(1024)。st_dev、st_rdev应是 0(Cinux 无设备号)。- 三个时间戳应是 0(无 RTC)——别被「全是 1970」吓到,这是预期。
如果内核 stat 打出的 size 和 debugfs 不一致,要么 lookup 拿错了 inode,要么 stat 读错了 disk_inode——回去查。
任务 5(改进):给 stat 去重
主书指出,Ext2FileOps::stat 和 Ext2DirOps::stat 逐字相同。这是个明显的重复。你的任务:把这段公共逻辑提到一个地方,让两个 Ops 类都复用,且不改变行为。
可选做法(任选其一,想清楚取舍):
- 在
Ext2类里加一个私有 helper(比如fill_stat_from_inode(const Inode*, struct stat*)),两个stat都调它。 - 或者在基类
InodeOps里给stat一个默认实现(因为 ext2 的文件和目录 stat 完全一样),让两个派生类不再 override——但要想清楚:别的文件系统后端(ramdisk)会不会需要不同的 stat?默认实现会不会反而限制了扩展?
改完重跑 ctest -R cwd_stat,确认行为不变。这是个纯重构,测试不该有任何变化。
接口约束
这一章涉及的关键接口,lab 里你测的每个点都对应其中之一:
cinux::fs::path_canonicalize(char* buf):就地规范化,结果恒为绝对路径;不查存在性。cinux::fs::path_resolve(cwd, path, out):绝对优先,相对拼 cwd;输出缓冲至少PATH_MAX。cinux::syscall::validate_user_ptr(uint64_t):canonical 地址校验(inline)。cinux::syscall::resolve_user_path(path_virt, out):校验 + 取Scheduler::current()->cwd(nullptr 退回 "/")+ path_resolve。cinux::proc::Task::cwd[256]:per-process 工作目录,绝对路径,定长 256。cinux::proc::Scheduler::current()/set_current(Task*):取/设当前进程。cinux::fs::InodeOps::stat(const Inode*, struct stat*):第 7 个虚方法,后端把磁盘 inode 翻译成 stat。sys_stat/sys_fstat/sys_chdir/sys_getcwd:号 4/5/12/79。
验证步骤
- 任务 1–2:在
test/unit/test_cwd_stat.cpp里(或新建一个测试文件)加你的用例,ctest --test-dir build -R cwd_stat --output-on-failure全绿。故意去掉根目录保护、重跑,确认/..类用例变红——反证保护有效,然后改回来。 - 任务 3–4:用带交互 shell 的 Cinux 运行(或读
kernel/test/test_cwd_stat.cpp看它怎么驱动 shell 命令),手敲命令;stat结果与debugfs逐一对照。 - 任务 5:
cmake --build build && ctest --test-dir build -R cwd_stat,全绿且无行为变化。 - 全程用你自己的
/tmp/lab.ext2拷贝,别动 CI 那份(会被regenerate-ext2-image冲掉)。
常见故障
cd /..或cd a/../../..后路径乱了 / 内核崩了:path_canonicalize漏了根目录保护(if (out_pos > 1))。..在根时应是 no-op,漏了会越界写out[]。- 相对路径永远被当绝对路径 / 永远相对 "/":
resolve_user_path没取到 current task 的 cwd(要么Scheduler::current()返回 nullptr,要么launch_first_user没set_current)。症状是cd etc后pwd还是/或解析错位。 cd失败后 cwd 变了:sys_chdir在 lookup/类型检查失败前就改了current->cwd。正确顺序是「全检查通过才写 cwd」。- stat 的 size 对不上:多半是 lookup 拿到了错的 inode(路径解析错),或者 stat 读的
disk_inode不是这个 inode 的。先pwd/debugfs双向核对路径。 - stat 时间是 0,以为坏了:不是 bug。Cinux 无 RTC,
st_atime/mtime/ctime取自磁盘 inode(全 0)。st_dev/st_rdev同理是 0。 getcwd返回 -1 但缓冲明明够:看sys_getcwd的长度判断——它算的是「含 NUL 的长度」,你的缓冲 size 至少要strlen(cwd)+1。- 改了 stat 去重后测试挂了:重构动了行为。纯重构不该改变任何输出,逐字段核对你的 helper 和原实现是否一字不差。
通过标准
- 任务 1–2 的所有规范化/解析用例通过,且能解释「为什么
/..必须是/」。 - 任务 3 里
cd/pwd行为正确,失败的 cd 不改变 cwd。 - 任务 4 里
stat的st_size/st_ino/st_mode/st_nlink/st_blksize与debugfs完全一致;st_dev/st_rdev/时间戳为 0 且你能解释原因。 - 任务 5 完成去重,
ctest -R cwd_stat全绿、行为不变。 - 能口头回答:相对路径是怎么变成绝对路径的?cwd 挂在哪里?为什么第一个用户进程要
set_current?