早期计算机没有文件系统,数据直接按扇区读写,程序员必须记住数据在第几个扇区。文件系统的发明让人类可以用“文件”和“目录”的方式组织数据——这是计算机使用方式的根本变革。本章将实现一个虚拟文件系统(VFS)层来提供统一接口,并在其上构建一个基于内存的 SimpleFS 文件系统。
文件系统概述
文件系统解决了存储设备的抽象和管理问题:
- 存储抽象:将物理存储设备抽象为文件和目录的层次结构,简化用户访问
- 数据持久化:提供统一的数据存储和检索接口
- 空间管理:高效管理存储空间的分配和回收
- 访问控制:通过权限机制保护文件安全
在现代操作系统中,文件系统不仅支持磁盘,还可以是内存文件系统、网络文件系统等,这正是VFS发挥作用的地方。
文件系统的核心设计
虚拟文件系统(VFS)
不同文件系统(FAT32、ext2、NTFS等)的实现细节各不相同,但应用程序需要统一的接口。VFS提供了一层抽象,让内核可以用相同的方式操作不同类型的文件系统。
VFS通过函数指针机制(类似面向对象的接口)定义了文件系统的标准操作接口,具体的文件系统实现这些接口函数。
/* 文件系统操作接口 */typedef struct filesystem_ops { int (*mount)(superblock_t *sb, block_device_t *dev); inode_t *(*get_inode)(superblock_t *sb, uint32_t ino); void (*put_inode)(inode_t *inode); int (*read_inode)(inode_t *inode, void *buf, uint32_t offset, uint32_t size); int (*write_inode)(inode_t *inode, const void *buf, uint32_t offset, uint32_t size); dirent_t *(*readdir)(inode_t *dir, uint32_t index); inode_t *(*lookup)(inode_t *dir, const char *name); int (*create)(inode_t *dir, const char *name, file_type_t type, uint32_t mode); int (*unlink)(inode_t *dir, const char *name); int (*mkdir)(inode_t *dir, const char *name, uint32_t mode); int (*rmdir)(inode_t *dir, const char *name);} filesystem_ops_t;VFS的架构可以表示为:
SimpleFS - 内存文件系统
学习文件系统的复杂机制时,直接从磁盘文件系统入手会过于复杂。SimpleFS作为教学用的内存文件系统,展示了文件系统的核心概念:
- 文件的存储和访问
- 目录的层次结构
- 路径解析
- 文件操作接口
重要说明:本章实现的SimpleFS是内存文件系统,数据存储在kmalloc分配的内存中,不涉及磁盘I/O。系统重启后数据会丢失。
SimpleFS采用平面文件表设计,通过parent_ino字段实现目录层次结构。这种设计大大简化了实现,同时保留了文件系统的核心概念。
#define SFS_MAX_FILES 64#define SFS_MAX_NAME_LEN 32
typedef struct sfs_file { char name[SFS_MAX_NAME_LEN]; /* 文件名 */ uint32_t size; /* 文件大小 */ uint32_t capacity; /* 分配的内存容量 */ void *data; /* 文件数据(kmalloc分配) */ uint32_t blocks; /* 占用块数 */ file_type_t type; /* 文件类型 */ uint32_t mode; /* 权限模式 */ uint32_t parent_ino; /* 父目录inode号(关键!) */ uint32_t atime; /* 访问时间 */ uint32_t mtime; /* 修改时间 */ int in_use; /* 是否使用 */} sfs_file_t;平面文件表的结构示意:
通过parent_ino字段,可以轻松实现:
- 查找目录下的文件:筛选
parent_ino等于目录inode号的文件 - 构建层次结构:从根目录递归查找
路径解析
用户通过路径(如/home/user/file.txt)访问文件,但文件系统内部使用inode号。路径解析将路径转换为对应的inode。
路径解析采用逐级lookup的方式,从根目录或当前目录开始,依次查找路径中的每个组件。
static inode_t *resolve_path(const char *path){ if (!path || !path[0]) return NULL;
inode_t *current;
/* 绝对路径从根目录开始 */ if (path[0] == '/') { current = simple_fs_root_inode(); path++; } else { /* 相对路径从当前目录开始 */ current = get_inode_by_ino(cwd_ino); }
if (!current) return NULL; if (!path[0]) return current; /* 只有根路径 */
char component[SFS_MAX_NAME_LEN]; while (*path) { /* 跳过分隔符 */ while (*path == '/') path++; if (!*path) break;
/* 提取路径分量 */ int i = 0; while (*path && *path != '/' && i < SFS_MAX_NAME_LEN - 1) { component[i++] = *path++; } component[i] = '\0';
/* 在当前目录中查找下一个组件 */ filesystem_t *fs = registered_fs[0]; if (!fs || !fs->ops || !fs->ops->lookup) { kfree(current); return NULL; } inode_t *next = fs->ops->lookup(current, component); kfree(current); if (!next) return NULL; current = next; } return current;}路径解析流程图:
文件操作
fs_open - 打开文件
fs_open负责打开或创建文件,返回文件描述符。
fs_read - 读取文件
static int sfs_read(inode_t *inode, void *buf, uint32_t offset, uint32_t size){ if (!inode || !buf) return -1; sfs_file_t *f = &sfs.files[inode->ino];
/* 检查边界条件 */ if (!f->in_use || offset >= f->size) return 0;
/* 计算可读字节数 */ uint32_t avail = f->size - offset; uint32_t n = (size < avail) ? size : avail;
/* 从内存中复制数据 */ if (f->data) memcpy(buf, (uint8_t *)f->data + offset, n); f->atime = getTick(); /* 更新访问时间 */ return (int)n;}fs_write - 写入文件
写入时需要处理内存扩展的情况:
static int sfs_write(inode_t *inode, const void *buf, uint32_t offset, uint32_t size){ if (!inode || !buf) return -1; sfs_file_t *f = &sfs.files[inode->ino]; if (!f->in_use) return -1;
uint32_t new_end = offset + size;
/* 如果需要扩展内存容量 */ if (new_end > f->capacity) { /* 新容量至少512字节,或者直接分配到new_end */ uint32_t new_cap = new_end < 512 ? 512 : new_end; void *nd = kmalloc(new_cap); if (!nd) return -1;
memset(nd, 0, new_cap);
/* 复制旧数据到新内存 */ if (f->data && f->size > 0) memcpy(nd, f->data, f->size);
/* 释放旧内存 */ if (f->data) kfree(f->data);
f->data = nd; f->capacity = new_cap; }
/* 写入新数据 */ memcpy((uint8_t *)f->data + offset, buf, size);
/* 更新文件大小 */ if (new_end > f->size) f->size = new_end; f->blocks = (f->size + SFS_BLOCK_SIZE - 1) / SFS_BLOCK_SIZE; f->mtime = getTick(); /* 更新修改时间 */
return (int)size;}写入操作的内存扩展过程:
代码实现
文件结构
14.kernel-fs/├── boot/│ ├── mbr.S│ └── loader.S├── kernel/│ ├── include/│ │ ├── fs.h # VFS接口定义│ │ └── simple_fs.h # SimpleFS定义│ ├── fs/│ │ ├── fs.c # VFS层实现│ │ └── simple_fs.c # SimpleFS实现│ └── kernel.c # 测试代码└── MakefileVFS 数据结构
/* 超级块 - 描述文件系统整体信息 */typedef struct superblock { fs_type_t type; /* 文件系统类型 */ block_device_t *device; /* 块设备(SimpleFS不使用) */ uint32_t block_size; /* 块大小 */ uint32_t total_blocks; /* 总块数 */ uint32_t free_blocks; /* 空闲块数 */ uint32_t total_inodes; /* 总inode数 */ uint32_t free_inodes; /* 空闲inode数 */ void *private; /* 私有数据,指向simple_fs_t */} superblock_t;
/* Inode - 索引节点,描述文件的元数据 */typedef struct inode { uint32_t ino; /* Inode号(对应sfs.files数组索引) */ file_type_t type; /* 文件类型 */ uint32_t mode; /* 权限模式 */ uint32_t size; /* 文件大小 */ uint32_t blocks; /* 占用块数 */ uint32_t atime; /* 访问时间 */ uint32_t mtime; /* 修改时间 */ uint32_t ctime; /* 创建时间 */ uint32_t links; /* 链接数 */ uint32_t uid; /* 用户ID */ uint32_t gid; /* 组ID */ uint32_t direct_blocks[12]; /* 直接块指针(SimpleFS不使用) */ uint32_t indirect_block; /* 一级间接块(SimpleFS不使用) */ uint32_t double_indirect; /* 二级间接块(SimpleFS不使用) */ superblock_t *sb; /* 所属超级块 */ void *private; /* 私有数据 */} inode_t;
/* 文件描述符 */typedef struct file { inode_t *inode; /* 关联的inode */ uint32_t offset; /* 当前读写偏移 */ uint32_t flags; /* 打开标志 */ int refcount; /* 引用计数 */} file_t;
/* 目录项 */typedef struct dirent { uint32_t ino; /* Inode号 */ char name[256]; /* 文件名 */ file_type_t type; /* 文件类型 */} dirent_t;SimpleFS 数据结构
/* SimpleFS文件系统整体结构 */typedef struct simple_fs { superblock_t sb; /* 超级块 */ sfs_file_t files[SFS_MAX_FILES]; /* 平面文件表 */ int mounted; /* 是否已挂载 */} simple_fs_t;文件打开流程
目录遍历
目录查找
/* 在指定目录中查找文件名 */static int sfs_find_in_dir(uint32_t parent_ino, const char *name){ for (int i = 0; i < SFS_MAX_FILES; i++) { if (sfs.files[i].in_use && sfs.files[i].parent_ino == parent_ino && strcmp(sfs.files[i].name, name) == 0) { return i; /* 返回文件索引 */ } } return -1; /* 未找到 */}解析:这是SimpleFS的核心查找函数。它遍历所有文件,检查三个条件:
- 文件正在使用(
in_use == 1) - 文件的父目录inode号匹配(
parent_ino == parent_ino) - 文件名匹配(通过
strcmp比较)
这个函数实现了”在目录中查找文件”的功能,体现了平面文件表通过parent_ino实现层次结构的设计思想。
目录读取
static dirent_t *sfs_readdir(inode_t *dir, uint32_t index){ uint32_t count = 0; for (int i = 0; i < SFS_MAX_FILES; i++) { /* 查找属于该目录的所有文件 */ if (sfs.files[i].in_use && sfs.files[i].parent_ino == dir->ino) { if (count == index) { /* 找到第index个目录项 */ static dirent_t de; de.ino = i; strncpy(de.name, sfs.files[i].name, 255); de.name[255] = '\0'; de.type = sfs.files[i].type; return &de; } count++; } } return NULL; /* 索引超出范围 */}解析:readdir用于目录遍历,参数index表示要返回第几个目录项。函数统计属于该目录的文件数,当count == index时返回对应的目录项。注意这里使用了static dirent_t de,这是因为VFS层会立即使用返回值,不会并发调用。
VFS层文件操作分发
ssize_t fs_read(int fd, void *buf, size_t count){ /* 参数检查 */ if (fd < 0 || fd >= MAX_OPEN_FILES || !fd_table[fd].inode) return -1;
/* 获取文件系统操作函数 */ filesystem_t *fs = registered_fs[0]; if (!fs || !fs->ops->read_inode) return -1;
/* 调用具体文件系统的读取函数 */ int n = fs->ops->read_inode(fd_table[fd].inode, buf, fd_table[fd].offset, count); /* 更新文件偏移 */ if (n > 0) fd_table[fd].offset += n; return n;}解析:这段代码展示了VFS的典型工作模式:
- 检查文件描述符的有效性
- 获取注册的文件系统(这里是SimpleFS)
- 通过函数指针调用具体文件系统的
read_inode函数 - 更新文件描述符的偏移量
这种设计使得VFS可以透明地支持多个文件系统,只要它们实现了相同的接口。
运行与验证
编译运行
cd 14.kernel-fsmake clean && make all && make run预期输出
=== Chapter 14: File System ===
[VFS] Initialized[SimpleFS] Initialized (64 files max)Starting scheduler...
--- File System Test ---
Creating /home ... mkdir result: 0Creating /home/docs ... mkdir result: 0
Creating /hello.txt ... open result: fd=0 write result: 20 bytes
Reading /hello.txt ... open result: fd=1 read result: 20 bytes content: Hello from SimpleFS!
Creating /home/test.txt ... created and written
Listing / ... [DIR ] / (ino=0) [DIR ] home (ino=1) [FILE] hello.txt (ino=2)
Listing /home ... [DIR ] docs (ino=3) [FILE] test.txt (ino=4)
=== File System Test Complete ===输出分析
从输出可以看到:
- VFS初始化:注册了文件系统接口
- SimpleFS初始化:创建了64个文件槽位的平面文件表
- 目录创建:成功创建了
/home和/home/docs目录 - 文件读写:成功创建、写入、读取
/hello.txt - 目录遍历:正确列出了根目录和
/home目录的内容
每个文件都有一个唯一的inode号(文件表索引),通过parent_ino字段形成层次结构:
/的 ino=0home的 ino=1, parent_ino=0docs的 ino=3, parent_ino=1hello.txt的 ino=2, parent_ino=0test.txt的 ino=4, parent_ino=1
踩坑记录
-
问题:SimpleFS是磁盘文件系统吗?
解答:不是。SimpleFS是内存文件系统,所有文件数据都存储在通过
kmalloc分配的内存中,不涉及任何磁盘I/O操作。系统重启后所有数据都会丢失。 -
问题:为什么使用平面文件表而不是inode树?
解答:平面文件表大大简化了实现,同时通过
parent_ino字段仍然可以实现目录层次结构。这种设计适合教学演示,避免了复杂的磁盘块分配、inode管理等机制。 -
问题:文件描述符表的作用是什么?
解答:文件描述符表跟踪所有打开的文件,每个条目包含关联的inode、当前偏移量、打开标志等。这使得同一文件可以被多次打开,每次打开都有独立的偏移量。
-
问题:
parent_ino=0是什么意思?解答:根目录的inode号是0,
parent_ino=0表示该文件/目录直接位于根目录下。根目录自身也使用parent_ino=0作为自引用。 -
问题:为什么
readdir使用static变量?解答:
readdir返回的dirent_t结构使用static存储是为了避免动态内存分配。VFS层会立即使用返回值并复制需要的数据,这种简化在单线程环境下是安全的。
小结
VFS 提供了统一的文件操作接口,SimpleFS 在其上实现了基于内存的文件存储,路径解析将用户输入的字符串逐级转换为 inode,而 open/read/write/mkdir 等操作构成了文件系统的完整能力。这个文件系统虽然基于内存(重启后数据消失),但已经具备了真实文件系统的核心抽象。下一章将实现进程管理——fork 复制进程、exec 替换映像,这是 UNIX 进程模型的精髓。
SimpleFS作为教学用文件系统,虽然功能简化,但完整展示了文件系统的核心概念:inode、目录、路径解析、文件操作接口等。理解这些概念后,可以进一步学习ext2等真实磁盘文件系统的实现。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






