前几章文件系统都是只读的。这一章实现 ext2 的写路径,让 echo hello > /tmp/a.txt && cat /tmp/a.txt 能正常工作,且重启后文件仍然存在。过程中还修了 fd 引用计数和 tty 字符设备两个问题。
ext2 磁盘结构回顾
块组 0:
[超级块][组描述符][block bitmap][inode bitmap][inode table][数据块...]
关键字段:
sb->s_free_inodes_count/gd->bg_free_inodes_count:空闲 inode 计数sb->s_free_blocks_count/gd->bg_free_blocks_count:空闲 block 计数gd->bg_inode_bitmap:inode bitmap 所在块号gd->bg_block_bitmap:block bitmap 所在块号gd->bg_inode_table:inode table 起始块号
inode 号从 1 开始;前 10 个是系统保留的(root=2,lost+found=11)。
alloc_inode / alloc_block
alloc_inode
uint32_t alloc_inode(void) {
// 1. 读 inode bitmap 块
// 2. 找第一个为 0 的位(bit i → inode i+1)
// 3. 置位,写回 bitmap
// 4. 更新超级块和组描述符的 free_inodes_count
// 5. 返回 inode 号(从 1 开始)
}
alloc_block
uint32_t alloc_block(void) {
// 1. 读 block bitmap 块
// 2. 找第一个为 0 的位,跳过 block 0(保留)
// 3. 置位,写回 bitmap
// 4. 更新超级块和组描述符的 free_blocks_count
// 5. 清零新分配的块(避免垃圾数据)
// 6. 返回块号
}
注意:block bitmap 的 bit 0 对应 block 0,必须跳过(block 0 不存在)。实际第一个可分配的块由文件系统布局决定(本项目中是 610)。
sync_meta
写入 inode、bitmap、超级块后必须调用 bcache_sync() 将脏页刷到磁盘,否则重启后数据丢失。
ext2_dir_add:向目录写入新条目
ext2 目录条目格式:
[inode:4][rec_len:2][name_len:1][file_type:1][name:name_len]
写入流程:
- 读目录的数据块
- 遍历所有条目,找到最后一条(
inode=0或pos + rec_len >= block_size) - 将最后一条的
rec_len缩短到其实际占用(8 + name_len对齐到 4 字节) - 在空出的空间写新条目,新条目的
rec_len填满剩余空间 - 若目录块不够,分配新块
ext2_truncate:清空文件内容
void ext2_truncate(uint32_t ino) {
// 1. 读 inode
// 2. 释放所有直接块(i_block[0..11]):清 block bitmap,将 i_block[i] 置 0
// 3. 间接块暂不处理(简化实现)
// 4. 将 inode 的 i_size = 0,i_blocks = 0
// 5. 写回 inode,sync_meta
}
VFS fd 引用计数
问题
Shell 执行 echo hello > /tmp/a.txt 时,busybox ash 的重定向流程:
open("/tmp/a.txt", O_WRONLY|O_CREAT|O_TRUNC) → fd=3
dup2(3, 1) ← 将 stdout 重定向到文件
close(3) ← 关闭原始 fd
echo 写 fd=1
dup2(0, 1) ← 用 stdin 恢复 stdout
dup2(3, 1) 之后:fd_table[1] 和 fd_table[3] 指向同一 VFS fd。close(3) 之后:若直接释放 VFS fd,则 fd_table[1] 成了悬空引用,write(1, ...) 触发 use-after-free。
修复:refcnt
在 VFile 结构中加 refcnt 字段:
typedef struct {
VNode *vnode;
uint32_t offset;
int used;
int type;
int refcnt; // ← 新增
} VFile;
规则:
alloc_fd分配时refcnt = 1vfs_ref(fd):refcnt++,在fd_dup2和fd_copy_table(fork 继承)时调用vfs_close(fd):refcnt--,只有降到 0 时才真正释放
tty chardev:让 stdin/stdout/stderr 进入 VFS
问题
之前 fd_table[0/1/2] 都是 -1(内核直接调用 tty_read/tty_write),但 dup2(3, 1) 要能把 VFS fd 写到 fd_table[1],且之后 dup2(0, 1) 要能从 fd_table[0] 复制。
修复
- 注册
"tty"字符设备,backed bytty_read/tty_write proc_exec启动 shell 时,为 fd 0/1/2 各vfs_open_cdev("tty")
for (int tfd = 0; tfd < 3; tfd++) {
int vfd = vfs_open_cdev("tty");
if (vfd >= 0) p->fd_table[tfd] = vfd;
}
惰性 tty 重建
busybox ash 在启动时(job control 失败路径)会 close(0),导致 fd_table[0]=-1。之后 dup2(0, 1) 恢复 stdout 时,fd_table[0]=-1 → fd_dup2 失败 → EBADF 死循环。
修复:SYS_DUP2 中惰性重建
case SYS_DUP2: {
int32_t oldfd = (int32_t)a1;
// 若 oldfd 是 0/1/2 但已被关闭,惰性重新打开 tty
if (oldfd >= 0 && oldfd < PROC_MAX_FD
&& current->fd_table[oldfd] < 0
&& (oldfd == 0 || oldfd == 1 || oldfd == 2)) {
int tvfd = vfs_open_cdev("tty");
if (tvfd >= 0) current->fd_table[oldfd] = tvfd;
}
int32_t r = fd_dup2(current->fd_table, oldfd, (int32_t)a2);
return (r < 0) ? (uint64_t)(-EBADF) : (uint64_t)(int64_t)r;
}
错误返回值:-1 vs -EBADF
fd_dup2 返回 int32_t -1 表示失败。若直接 return (uint64_t)(int64_t)(-1),用户态看到的是 0xFFFFFFFFFFFFFFFF,errno = -(int64_t)ret = 1 = EPERM。
必须显式映射:
return (r < 0) ? (uint64_t)(-EBADF) : (uint64_t)(int64_t)r;
最终结果
/ # echo hello > /tmp/a.txt
/ # cat /tmp/a.txt
hello
重启后:
/ # cat /tmp/a.txt
hello
文件写入正常,重启后持久化。
小结
ext2 写操作的核心是 bitmap 管理:alloc_inode 和 alloc_block 找空闲位、置位、更新计数;ext2_dir_add 在最后一条目后插入新条目;sync_meta 确保元数据落盘。
fd 引用计数是实现 dup2 的基础,少了它就会 use-after-free。tty 进入 VFS 则是让标准输入输出能参与 dup2 重定向的前提。这些组合在一起,才能让 shell 的 I/O 重定向完整工作。