ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《从零手写操作系统 (18):文件系统进阶——inode、目录树与VFS抽象》

《从零手写操作系统 (18):文件系统进阶——inode、目录树与VFS抽象》 前言从“扁平归档”到“层次化存储”在前面的章节中我们通过initrd让用户程序能够读取文件但那只是一个只读的、扁平的文件列表。没有子目录无法创建新文件不能重命名或删除重启后所有修改灰飞烟灭。这不是一个文件系统只是一个打包器。真正的文件系统是操作系统的骨架——它定义了数据的组织方式、访问语义和持久化契约。本章我们将实现一个完整的内存文件系统RamFS作为未来磁盘文件系统的原型。它将引入Unix文件系统的三大核心抽象inode元数据、dentry目录项和VFS虚拟文件系统接口。你的OS将首次拥有可读写、分层级、支持增删改查的真正存储栈。本章里程碑✅ 设计并实现inode结构体与全局inode表✅ 实现目录项dentry与树形目录遍历✅ 构建VFS层统一open/read/write/close/unlink/mkdir接口✅ 实现基于PMEM的RamFS后端支持动态分配与释放✅ 路径解析算法处理.、..、绝对/相对路径✅ 验证完整文件操作创建目录、写入文件、删除、ls递归核心概念三层解耦与inode的本质VFS ≠ 具体文件系统Unix文件系统的经典分层模型层级职责本章实现系统调用层open/read/write/close等用户APIsys_open / sys_read 等VFS层路径解析、权限检查、fd管理、路由到具体FS★ vfs.c / path.c具体FS层inode/dentry/block的物理布局与读写★ ramfs.cVFS是中间那层“翻译官”。它让内核代码只关心“打开一个文件”而不关心这个文件在RamFS、Ext2还是FAT上。今天写的VFS代码明天换磁盘FS时一行不用改。inode是“身份”dentry是“名字”这是Unix文件系统最精妙的设计分离inode存储文件的元数据大小、权限、时间戳、数据块指针。不含文件名。每个文件有且仅有一个inode。dentry存储(name, inode_ptr)映射。多个dentry可以指向同一个inode硬链接。目录本身也是一个特殊文件其内容是dentry列表。⚠️关键洞察rm file不是删除文件而是删除一个dentry。只有当inode的引用计数归零时文件数据才真正释放。这解释了为什么Linux允许删除正在被打开的文件——unlink移除dentry但open持有的inode引用阻止了数据回收。RamFS作为教学载体的优势为什么不直接写Ext2因为Ext2的on-disk格式复杂超级块、组描述符、位图、间接块调试时需要频繁hex dump磁盘镜像。RamFS将所有结构放在内存中可以用GDB直接检视、用kprintf实时dump整棵树。当你彻底理解RamFS的inode/dentry/VFS交互后迁移到Ext2只需替换具体FS层的读写函数上层逻辑完全复用。实战代码inode与dentry核心结构// fs/vfs.h #define MAX_INODES 256 #define MAX_DENTRY_CHILDREN 32 #define NAME_MAX 31 typedef enum { FT_REG 1, FT_DIR 2 } filetype_t; // ★ inode纯元数据无名字 typedef struct inode { uint32_t ino; // inode编号数组下标 filetype_t type; uint32_t size; uint32_t refcount; // 引用计数dentry open fd uint32_t mode; // 权限位简化0755/0644 // RamFS专用数据直接存内存指针 uint8_t *data; // REG: 文件内容; DIR: dentry数组 uint32_t data_capacity; // 已分配容量 // DIR专用 struct dentry *children[MAX_DENTRY_CHILDREN]; int child_count; } inode_t; // ★ dentry名字 → inode 映射 typedef struct dentry { char name[NAME_MAX 1]; inode_t *inode; struct dentry *parent; // 用于 .. 解析 } dentry_t; // 全局inode表 extern inode_t inode_table[MAX_INODES]; extern dentry_t *root_dentry; // VFS API int vfs_open(const char *path, int flags); ssize_t vfs_read(int fd, void *buf, size_t count); ssize_t vfs_write(int fd, const void *buf, size_t count); int vfs_close(int fd); int vfs_unlink(const char *path); int vfs_mkdir(const char *path);inode分配与引用计数// fs/inode.c #include vfs.h #include memory.h inode_t inode_table[MAX_INODES]; static uint32_t next_ino 1; // 0保留为无效 inode_t *inode_alloc(filetype_t type) { for (int i 1; i MAX_INODES; i) { if (inode_table[i].refcount 0 inode_table[i].ino 0) { inode_t *node inode_table[i]; node-ino i; node-type type; node-size 0; node-refcount 1; node-mode (type FT_DIR) ? 0755 : 0644; node-data NULL; node-data_capacity 0; node-child_count 0; return node; } } return NULL; // ENOSPC } void inode_ref(inode_t *node) { if (node) node-refcount; } void inode_unref(inode_t *node) { if (!node || node-refcount 0) return; node-refcount--; if (node-refcount 0) { // ★ 真正释放回收数据内存清除inode槽位 if (node-data) { kfree(node-data); node-data NULL; } node-ino 0; node-type 0; } }路径解析与目录查找// fs/path.c #include vfs.h #include string.h // ★ 核心将路径字符串解析为dentry // 返回NULL表示不存在若create_parent1则保证父目录存在 dentry_t *path_resolve(const char *path, int create_parent) { if (!path || path[0] ! /) return NULL; // 仅支持绝对路径 dentry_t *cur root_dentry; const char *p path 1; // 跳过根/ while (*p) { // 提取下一个路径分量 const char *slash strchr(p, /); int len slash ? (slash - p) : strlen(p); if (len 0) { p; continue; } // 处理 // if (len NAME_MAX) return NULL; char component[NAME_MAX 1]; memcpy(component, p, len); component[len] \0; // 处理 . 和 .. if (strcmp(component, .) 0) { p len; if (*p /) p; continue; } if (strcmp(component, ..) 0) { cur cur-parent ? cur-parent : cur; // 根目录的..仍是根 p len; if (*p /) p; continue; } // 在当前目录中查找子项 inode_t *dir_inode cur-inode; dentry_t *found NULL; for (int i 0; i dir_inode-child_count; i) { if (strcmp(dir_inode-children[i]-name, component) 0) { found dir_inode-children[i]; break; } } if (!found) { // 未找到 if (create_parent !slash) { // 最后一个分量 create模式返回父目录供调用者创建 return cur; } return NULL; // 路径不存在 } // 中间分量必须是目录 if (slash found-inode-type ! FT_DIR) return NULL; cur found; p len; if (*p /) p; } return cur; }RamFS文件读写与目录操作// fs/ramfs.c #include vfs.h #include memory.h // ★ 文件写入动态扩容 ssize_t ramfs_write(inode_t *node, uint32_t offset, const void *buf, size_t count) { if (node-type ! FT_REG) return -1; uint32_t end offset count; // 按需扩容倍增策略 if (end node-data_capacity) { uint32_t new_cap node-data_capacity ? node-data_capacity : 64; while (new_cap end) new_cap * 2; uint8_t *new_data kmalloc(new_cap); if (!new_data) return -1; // ENOMEM if (node-data) { memcpy(new_data, node-data, node-size); kfree(node-data); } // 零填充空洞区域 memset(new_data node-size, 0, new_cap - node-size); node-data new_data; node-data_capacity new_cap; } memcpy(node-data offset, buf, count); if (end node-size) node-size end; return count; } // ★ mkdir创建目录dentry并关联新inode int ramfs_mkdir(dentry_t *parent_de, const char *name) { inode_t *parent parent_de-inode; if (parent-type ! FT_DIR) return -1; if (parent-child_count MAX_DENTRY_CHILDREN) return -1; // 检查重名 for (int i 0; i parent-child_count; i) { if (strcmp(parent-children[i]-name, name) 0) return -1; // EEXIST } inode_t *new_inode inode_alloc(FT_DIR); if (!new_inode) return -1; dentry_t *new_de kmalloc(sizeof(dentry_t)); if (!new_de) { inode_unref(new_inode); return -1; } strncpy(new_de-name, name, NAME_MAX); new_de-inode new_inode; new_de-parent parent_de; parent-children[parent-child_count] new_de; return 0; } // ★ unlink移除dentryinode引用减一 int ramfs_unlink(dentry_t *parent_de, const char *name) { inode_t *parent parent_de-inode; for (int i 0; i parent-child_count; i) { if (strcmp(parent-children[i]-name, name) 0) { dentry_t *target parent-children[i]; // 目录必须为空才能删除 if (target-inode-type FT_DIR target-inode-child_count 0) { return -1; // ENOTEMPTY } inode_unref(target-inode); kfree(target); // 从父目录数组中移除移动最后一个填补空缺 parent-children[i] parent-children[--parent-child_count]; return 0; } } return -1; // ENOENT }初始化与根目录创建// fs/init.c void fs_init(void) { // 清零inode表 memset(inode_table, 0, sizeof(inode_table)); // ★ 创建根目录 / inode_t *root_inode inode_alloc(FT_DIR); root_dentry kmalloc(sizeof(dentry_t)); strcpy(root_dentry-name, /); root_dentry-inode root_inode; root_dentry-parent root_dentry; // 根的parent是自己 // 创建初始目录结构 dentry_t *dev NULL, *tmp NULL; ramfs_mkdir(root_dentry, dev); ramfs_mkdir(root_dentry, tmp); ramfs_mkdir(root_dentry, home); kprintf([FS] RamFS initialized. Root inode%d\n, root_inode-ino); }关键细节解析1. 为什么inode不含文件名如果文件名存在inode中硬链接就无法实现两个名字对应同一份元数据。更深层的原因是文件名是目录的属性不是文件的属性。目录是一个特殊的文件其内容是(name, ino)对的列表。这种分离使得文件系统操作rename、link、unlink只需修改目录内容无需触碰inode本身极大简化了并发控制和崩溃恢复。2. 为什么path_resolve要区分create_parent模式open(/a/b/c, O_CREAT)需要确保/a/b存在但c可以不存在。如果path_resolve总是要求完整路径存在O_CREAT就无法工作。如果总是允许缺失那么open(/a/b/c, O_RDONLY)在b不存在时会错误地返回父目录而非ENOENT。create_parent标志让同一个解析函数服务于两种截然不同的语义避免了代码重复。3. RamFS的数据扩容为什么用倍增而非固定页小文件占多数。如果每次write都分配4KB页一个10字节的配置文件就浪费4086字节。倍增策略64→128→256...保证了摊销O(1)的写入复杂度同时对小文件友好。注意生产级FS使用extent或间接块映射避免大文件时的memcpy开销。RamFS的memcpy是可接受的教学简化。调试Checklist文件系统排查症状可能原因排查方法open返回-1但文件确实存在path_resolve路径分量提取错误/大小写敏感问题kprintf在resolve每层dump当前component和匹配结果确认strncpy正确截断write成功但read读到旧数据offset计算错误/data指针未更新/缓存一致性问题dump inode.data地址和size前后变化确认write后更新了node-sizeunlink后文件仍可访问refcount未正确递减/fd仍持有引用dump目标inode.refcount确认close调用了inode_unref检查是否有泄漏的fdmkdir报EEXIST但目录不存在重名检查遍历范围错误/name比较含尾部\0问题dump parent.child_count和所有children.name确认strcmp参数正确路径/a/../b解析失败..处理未更新cur指针/根目录parent未自指单步调试path_resolve的..分支确认root_dentry-parent root_dentry内存泄漏inode_unref未释放data/kmalloc的dentry未free实现fs_dump_stats()定期统计已分配inode数和总data字节数对比操作前后黄金法则文件系统调试的终极武器是树状dump函数。实现fs_dump_tree(dentry_t *root, int depth)递归打印整棵目录树缩进显示层级、inode号、类型、大小、refcount。在每次create/unlink/write前后调用它视觉化验证状态变迁。不要试图脑内模拟树的结构变化——人脑不擅长追踪引用计数。本章小结与下一步今天我们赋予了操作系统真正的“记忆”能力✅ 实现了inode/dentry分离的经典Unix文件系统模型✅ 构建了VFS抽象层解耦系统调用与具体存储后端✅ RamFS支持完整的文件生命周期创建、读写、删除、目录嵌套✅ 路径解析处理了.、..、多级目录与边界情况✅ 引用计数确保了资源安全释放与硬链接语义基础从此你的操作系统拥有了可持久化的层次化存储。当你在自制Shell中执行mkdir /tmp/test echo hello /tmp/test/msg cat /tmp/test/msg rm /tmp/test/msg并看到预期输出时你见证的是一个完整存储栈的诞生。下一章预告《Ext2文件系统实战从内存到磁盘的跨越》RamFS重启即失忆。下一章将实现真正的磁盘文件系统Ext2读取超级块、解析块组描述符、遍历inode表、读取间接块让你的OS能够从QEMU虚拟磁盘中启动并持久保存数据。参考资料Linux Kernel:fs/inode.c,fs/namei.c,fs/ramfs/The Design of the UNIX Operating System (Bach), Chapter 5-6Ext2 Filesystem Specification: https://www.nongnu.org/ext2-doc/xv6 Source:kernel/fs.c,kernel/file.c本系列完整代码[你的GitHub仓库链接]Commit:v1f2s3r作者注这是《从零手写操作系统》系列的第18篇。文件系统是整个教程中数据结构最密集、不变量最多的章节。如果你的unlink导致后续open随机崩溃几乎一定是refcount或dentry数组管理的bug。建议先实现只读的路径解析inode查找确认树结构正确后再加入write/unlink/mkdir。文件系统的正确性不是功能问题是安全性问题——每一个未检查的边界条件都可能成为未来的提权漏洞。下一章我们让数据真正“活过重启”
返回列表