mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1944 字
5 分钟
文件系统:VFS 与 SimpleFS
2021-09-18

早期计算机没有文件系统,数据直接按扇区读写,程序员必须记住数据在第几个扇区。文件系统的发明让人类可以用“文件”和“目录”的方式组织数据——这是计算机使用方式的根本变革。本章将实现一个虚拟文件系统(VFS)层来提供统一接口,并在其上构建一个基于内存的 SimpleFS 文件系统。

文件系统概述#

文件系统解决了存储设备的抽象和管理问题:

  1. 存储抽象:将物理存储设备抽象为文件和目录的层次结构,简化用户访问
  2. 数据持久化:提供统一的数据存储和检索接口
  3. 空间管理:高效管理存储空间的分配和回收
  4. 访问控制:通过权限机制保护文件安全

在现代操作系统中,文件系统不仅支持磁盘,还可以是内存文件系统、网络文件系统等,这正是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的架构可以表示为:

graph TD A[应用程序] -->|fs_open, fs_read, fs_write| B[VFS层] B -->|函数指针调用| C[SimpleFS实现] B -->|函数指针调用| D[ext2实现] B -->|函数指针调用| E[FAT32实现] C -->|kmalloc/kfree| F[内核堆]

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;

平面文件表的结构示意:

graph TD A[sfs.files数组] B[索引0: /<br/>type=DIRECTORY<br/>parent_ino=0] C[索引1: home<br/>type=DIRECTORY<br/>parent_ino=0] D[索引2: docs<br/>type=DIRECTORY<br/>parent_ino=1] E[索引3: hello.txt<br/>type=REGULAR<br/>parent_ino=0] F[索引4: test.txt<br/>type=REGULAR<br/>parent_ino=1] A --> B A --> C A --> D A --> E A --> F C -.->|parent_ino=0| B D -.->|parent_ino=1| C E -.->|parent_ino=0| B F -.->|parent_ino=1| C

通过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;
}

路径解析流程图:

flowchart TD A[开始: /home/user/file.txt] --> B{路径是否以/开头?} B -->|是| C[从根目录开始] B -->|否| D[从当前目录开始] C --> E[提取第一个组件: home] D --> E E --> F[在当前目录中查找home] F --> G{找到home?} G -->|是| H[提取下一个组件: user] G -->|否| I[返回NULL: 路径不存在] H --> J[在home目录中查找user] J --> K{找到user?} K -->|是| L[提取下一个组件: file.txt] K -->|否| I L --> M[在user目录中查找file.txt] M --> N{找到file.txt?} N -->|是| O[返回file.txt的inode] N -->|否| I

文件操作#

fs_open - 打开文件#

fs_open负责打开或创建文件,返回文件描述符。

flowchart TD A[fs_open path, flags] --> B[解析路径获取inode] B --> C{inode存在?} C -->|是| D[进入文件打开流程] C -->|否| E{flags包含CREATE?} E -->|否| F[返回-1: 文件不存在] E -->|是| G[解析父目录和文件名] G --> H[创建文件] H --> I[获取新inode] I --> D D --> J[分配文件描述符] J --> K[初始化fd_table] K --> L[返回fd]

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;
}

写入操作的内存扩展过程:

sequenceDiagram participant App as 应用程序 participant VFS as VFS层 participant SimpleFS as SimpleFS participant KHeap as 内核堆 App->>VFS: fs_write(fd, "Hello", 5) VFS->>SimpleFS: sfs_write(inode, "Hello", offset, 5) SimpleFS->>SimpleFS: 检查offset+size > capacity? SimpleFS->>KHeap: kmalloc(new_capacity) KHeap-->>SimpleFS: 返回新内存指针 SimpleFS->>SimpleFS: memcpy(旧数据到新内存) SimpleFS->>KHeap: kfree(旧内存) SimpleFS->>SimpleFS: memcpy(新数据到offset) SimpleFS-->>VFS: 返回写入字节数 VFS-->>App: 返回5

代码实现#

文件结构#

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 # 测试代码
└── Makefile

VFS 数据结构#

/* 超级块 - 描述文件系统整体信息 */
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;

文件打开流程#

flowchart TD A[fs_open /home/file.txt FS_OPEN_CREATE] --> B[resolve_path] B --> C{文件存在?} C -->|否| D[解析父目录路径 /home] D --> E[解析文件名 file.txt] E --> F[resolve_path获取父目录inode] F --> G[调用fs_ops->create] G --> H[在sfs.files中分配新条目] H --> I[设置parent_ino为父目录ino] I --> J[返回新文件的ino] J --> K[get_inode获取inode结构] K --> L[分配文件描述符fd] L --> M[初始化fd_table] M --> N[返回fd] C -->|是| N

目录遍历#

flowchart TD A[fs_opendir /home] --> B[resolve_path获取目录inode] B --> C[分配文件描述符dir_fd] C --> D[初始化fd_table[dir_fd].offset=0] D --> E[返回dir_fd] F[fs_readdir dir_fd] --> G[获取目录inode] G --> H[调用fs_ops->readdir目录, offset] H --> I[遍历sfs.files数组] I --> J{找到parent_ino匹配的文件?} J -->|否| K[返回NULL: 遍历完成] J -->|是| L[返回dirent_t结构] L --> M[fd_table[dir_fd].offset++] M --> N[返回dirent_t指针] O[fs_closedir dir_fd] --> P[释放inode] P --> Q[清除fd_table[dir_fd]]

目录查找#

/* 在指定目录中查找文件名 */
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的核心查找函数。它遍历所有文件,检查三个条件:

  1. 文件正在使用(in_use == 1
  2. 文件的父目录inode号匹配(parent_ino == parent_ino
  3. 文件名匹配(通过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的典型工作模式:

  1. 检查文件描述符的有效性
  2. 获取注册的文件系统(这里是SimpleFS)
  3. 通过函数指针调用具体文件系统的read_inode函数
  4. 更新文件描述符的偏移量

这种设计使得VFS可以透明地支持多个文件系统,只要它们实现了相同的接口。

运行与验证#

编译运行#

cd 14.kernel-fs
make 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: 0
Creating /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 ===

输出分析#

从输出可以看到:

  1. VFS初始化:注册了文件系统接口
  2. SimpleFS初始化:创建了64个文件槽位的平面文件表
  3. 目录创建:成功创建了/home/home/docs目录
  4. 文件读写:成功创建、写入、读取/hello.txt
  5. 目录遍历:正确列出了根目录和/home目录的内容

每个文件都有一个唯一的inode号(文件表索引),通过parent_ino字段形成层次结构:

  • / 的 ino=0
  • home 的 ino=1, parent_ino=0
  • docs 的 ino=3, parent_ino=1
  • hello.txt 的 ino=2, parent_ino=0
  • test.txt 的 ino=4, parent_ino=1

踩坑记录#

  1. 问题:SimpleFS是磁盘文件系统吗?

    解答:不是。SimpleFS是内存文件系统,所有文件数据都存储在通过kmalloc分配的内存中,不涉及任何磁盘I/O操作。系统重启后所有数据都会丢失。

  2. 问题:为什么使用平面文件表而不是inode树?

    解答:平面文件表大大简化了实现,同时通过parent_ino字段仍然可以实现目录层次结构。这种设计适合教学演示,避免了复杂的磁盘块分配、inode管理等机制。

  3. 问题:文件描述符表的作用是什么?

    解答:文件描述符表跟踪所有打开的文件,每个条目包含关联的inode、当前偏移量、打开标志等。这使得同一文件可以被多次打开,每次打开都有独立的偏移量。

  4. 问题parent_ino=0是什么意思?

    解答:根目录的inode号是0,parent_ino=0表示该文件/目录直接位于根目录下。根目录自身也使用parent_ino=0作为自引用。

  5. 问题:为什么readdir使用static变量?

    解答readdir返回的dirent_t结构使用static存储是为了避免动态内存分配。VFS层会立即使用返回值并复制需要的数据,这种简化在单线程环境下是安全的。

小结#

VFS 提供了统一的文件操作接口,SimpleFS 在其上实现了基于内存的文件存储,路径解析将用户输入的字符串逐级转换为 inode,而 open/read/write/mkdir 等操作构成了文件系统的完整能力。这个文件系统虽然基于内存(重启后数据消失),但已经具备了真实文件系统的核心抽象。下一章将实现进程管理——fork 复制进程、exec 替换映像,这是 UNIX 进程模型的精髓。

SimpleFS作为教学用文件系统,虽然功能简化,但完整展示了文件系统的核心概念:inode、目录、路径解析、文件操作接口等。理解这些概念后,可以进一步学习ext2等真实磁盘文件系统的实现。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

文件系统:VFS 与 SimpleFS
https://blog.souloss.com/posts/os/filesystem-vfs-simplefs/
作者
Souloss
发布于
2021-09-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时