上一章 ls 能运行了,但打印出来的目录内容是空的——因为还没有实现 getdents64。这一章把目录列表、cdpwd 全部补齐,同时碰到了一个 -O2 下内存乱序导致的调度器死循环 Bug。

SYS_GETDENTS64

Linux 接口

int getdents64(int fd, struct linux_dirent64 *dirp, unsigned int count);

每个条目的结构:

struct linux_dirent64 {
    uint64_t d_ino;       // inode 号
    int64_t  d_off;       // 到下一条目的偏移(可用递增序号代替)
    uint16_t d_reclen;    // 本条目总字节数(含填充,8 字节对齐)
    uint8_t  d_type;      // 文件类型(4=目录,8=普通文件)
    char     d_name[];    // 文件名(null 结尾)
};

d_reclen 必须 8 字节对齐,计算方式:

uint16_t reclen = (uint16_t)((19 + namelen + 1 + 7) & ~7);
//                             ↑固定头部  ↑名字  ↑null  ↑对齐

内核实现

在 VFS 层实现 vfs_getdents,遍历 ext2 目录的所有条目,逐个填写 linux_dirent64 并写入用户缓冲区:

int vfs_getdents(int fd, uint64_t uva, uint32_t count) {
    // 1. 检查 fd 是否为目录
    // 2. 从 offset 开始遍历 ext2 目录
    // 3. 跳过 "." 和 ".."(busybox ls 不显示它们)
    // 4. 每个条目:填 d_ino、d_type、d_name、d_reclen
    // 5. 通过 copy_to_user 写入用户空间
    // 6. 更新 file->offset,返回写入的总字节数
}

关键:返回 0 表示目录已读完(EOF),busybox ls 据此停止调用。

ext2 目录遍历

ext2 目录条目格式(ext2_dirent):

[inode:4][rec_len:2][name_len:1][file_type:1][name:name_len]

遍历时 pos += de->rec_len 跳到下一条目,直到 pos >= block_size

SYS_CHDIR

进程 cwd 字段

process_t 中增加 cwd[256] 字段存储当前目录路径,初始化为 "/"

typedef struct process {
    // ...
    char cwd[256];
} process_t;

fork 时子进程继承父进程的 cwd(*child = *parent 已自动复制)。exec 时保留 cwd,不重置(POSIX 规定 execve 不改变工作目录)。

实现

case SYS_CHDIR: {
    char path[256];
    copy_str_from_user(path, a1, sizeof(path));
    // 验证路径存在且是目录
    int ino = vfs_path_lookup(path, current->cwd);
    if (ino < 0) return (uint64_t)(-ENOENT);
    // 规范化并更新 cwd
    if (path[0] == '/') {
        strncpy(current->cwd, path, 255);
    } else {
        // 拼接:cwd + "/" + path,再做 ".." 处理
        path_join(current->cwd, path);
    }
    return 0;
}

SYS_GETCWD

case SYS_GETCWD: {
    uint32_t len = (uint32_t)a2;
    uint32_t cwdlen = strlen(current->cwd) + 1;
    if (cwdlen > len) return (uint64_t)(-ERANGE);
    copy_to_user(a1, (uint8_t *)current->cwd, cwdlen);
    return a1;   // 返回用户缓冲区地址(Linux 约定)
}

VFS 相对路径支持

之前 vfs_path_lookup 只支持绝对路径(从 / 出发)。为了让 cd bin(相对路径)和 openat(AT_FDCWD, "bin/ls") 正常工作,需要传入起始 inode:

int vfs_path_lookup_from(const char *path, int start_ino);
  • 路径以 / 开头:从根 inode 出发
  • 路径不以 / 开头:从 start_ino 出发

syscall 层的 openatnewfstatatgetdents 等收到 AT_FDCWD(-100)时,传入 vfs_lookup_inode(current->cwd) 作为起始 inode。

Bug:proc_exit 后 kresume 死循环

现象

ls 打印完目录内容后调用 exit_group(0),但串口日志显示调度器一直在保存和恢复 pid=2(ls):

[sc2] nr=0xe7 (exit_group) a1=0
[sched] save ksp pid=2 ksp=0x100f48
[sched] ->kresume pid=2
[sched] save ksp pid=2 ksp=0x100f48
[sched] ->kresume pid=2
...(无限循环)

原因:-O2 内存乱序

proc_exit 的 C 代码逻辑是:

current->state = PROC_ZOMBIE;   // 1. 标记为 ZOMBIE
current        = NULL;           // 2. 清空 current 指针
current_proc   = -1;             // 3. 清空 current_proc
__asm__ volatile(
    "mov %0, %%rsp\n\t"
    "sti\n\t"                    // 4. 开中断
    "1: hlt\n\t"
    "jmp 1b"
    :: "r"(kstack), "r"(cr3val)
);

-O2 编译时,GCC 可能将步骤 1/2/3 的内存写延迟到 inline asm 之后(编译器认为 asm 不依赖这些变量)。

结果:PIT 在 sti 之后触发,此时:

  • current_proc 仍为 2(没有被提前写)
  • procs[2].state 仍为 PROC_RUNNING(ZOMBIE 写被推迟)
  • 调度器条件满足,把 ksp 保存到 pid=2,然后 kresume pid=2
  • 进入死循环

修复:内存屏障

在 inline asm 前加 __asm__ volatile("" ::: "memory"),强制编译器在屏障前提交所有内存写:

current->state = PROC_ZOMBIE;
current        = NULL;
current_proc   = -1;
__asm__ volatile("" ::: "memory");   // ← 内存屏障,确保以上写操作已提交
__asm__ volatile(
    "mov %0, %%rsp\n\t"
    "sti\n\t"
    "1: hlt\n\t"
    "jmp 1b"
    :: "r"(kstack), "r"(cr3val)
);

经验:内核中的 inline asm 前后,凡是有语义依赖的内存写,必须加 "memory" clobber 或显式屏障。-O2 对非 volatile 变量的乱序是合法的优化,不加屏障是 UB。

关键:QEMU 启动参数

if=floppy / if=ide 方式在新版 QEMU(10.x)下 ATA 会卡死(DRQ 永远不置位)。必须用原始参数:

qemu-system-x86_64 \
    -m 256M \
    -drive format=raw,file=myos.img \
    -drive format=raw,file=ext2.img \
    -nographic -serial mon:stdio

两个 -drive 不指定 if=,QEMU 自动把第二个挂到 IDE bus 0,ATA PIO 能正常工作。

最终结果

/ # ls
bin         etc         lost+found
/ # cd bin
/bin # pwd
/bin
/bin # ls
busybox  cat      echo     ls       sh
/bin #

ls 正确列出目录内容,cd / pwd 正常工作,shell 提示符动态更新。

小结

getdents64 的实现主要是格式转换:把 ext2 的 dirent 格式翻译成 Linux 的 linux_dirent64 格式,注意 d_reclen 的 8 字节对齐。相对路径支持需要把 AT_FDCWD 语义下沉到 VFS 层。

内存屏障 Bug 是这一章最有价值的教训:内核代码里的 inline asm 和周围 C 代码的内存操作顺序,在 -O2 下不能想当然地认为按源码顺序执行,凡是有跨越 asm 边界的语义依赖,必须显式加屏障。