anew___头像
关注

《从零手写操作系统 (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.c

        VFS是中间那层“翻译官”。它让内核代码只关心“打开一个文件”,而不关心这个文件在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_parent=1,则保证父目录存在
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:移除dentry,inode引用减一
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->size
unlink后文件仍可访问refcount未正确递减/fd仍持有引用dump目标inode.refcount;确认close调用了inode_unref;检查是否有泄漏的fd
mkdir报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-6
  • Ext2 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。文件系统的正确性不是功能问题,是安全性问题——每一个未检查的边界条件都可能成为未来的提权漏洞。下一章,我们让数据真正“活过重启”!

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/anew___/article/details/166938446

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--