《从零手写操作系统 (11):文件系统初探——initrd与VFS抽象层》

前言:从"内存数组"到"文件抽象"

在上一章中,我们实现了ELF加载器,但程序数据仍然是以C数组形式硬编码在内核镜像里的。如果要运行新程序,就得重新编译内核;如果要读取配置或保存日志,更是无从谈起。一个没有文件系统的操作系统,就像一个没有记忆的大脑。

本章我们将为内核引入初始内存盘(initrd) 和虚拟文件系统(VFS)抽象层 。initrd是一个打包了多个文件的简单归档格式,随内核一起加载到内存中;VFS则提供统一的open/read/close接口,屏蔽底层存储细节。这是让你的OS拥有持久化数据能力和标准POSIX语义的关键一步。

本章里程碑:

  • ✅ 设计并实现简易initrd归档格式与解析器
  • ✅ 构建VFS抽象层:inode、file描述符、操作表
  • ✅ 实现sys_open、sys_read、sys_close系统调用
  • ✅ 将ELF加载器重构为基于VFS的文件读取
  • ✅ 用户程序通过syscall读取initrd中的文件内容
  • ✅ 验证多文件并发读取与EOF正确处理

核心概念:initrd、VFS与文件描述符
为什么选择initrd作为起点?

真实文件系统(ext2/FAT)涉及磁盘驱动、块缓存、目录树遍历等巨大复杂度。initrd将所有文件预加载到内存中,以线性归档形式存在,零I/O开销、零驱动依赖,让我们能专注于VFS抽象和syscall语义的实现。Linux早期启动也采用相同策略。

VFS的核心抽象

VFS通过三层抽象隔离具体文件系统:

抽象层 职责 本章实现
vfs_file_t 打开的文件实例,含偏移量、引用计数 ✓
vfs_inode_t 文件元数据(大小、类型、数据指针) ✓
vfs_ops_t 具体文件系统的操作函数表 ✓ (initrd_ops)

⚠️ 关键设计 :文件描述符(fd)是进程级别的整数索引,指向该进程的打开文件表。同一个inode可以被多个fd、多个进程同时打开,每个fd维护独立的读写偏移。混淆fd与inode是最常见的VFS bug。

initrd归档格式设计

我们自定义一个极简的扁平归档格式(教学专用,非生产标准):

复制代码
[Header] magic(4B) + file_count(4B)
[Entry 0] name_len(4B) + data_size(4B) + name[name_len] + data[data_size]
[Entry 1] ...
[Entry N] ...

所有字段小端序,name不含\0终止符,name_len包含实际字符数。这种格式可以用几十行C代码解析,且支持任意文件名和二进制内容。


实战代码
initrd解析器
复制代码
// initrd.h & initrd.c
#include <stdint.h>
#include <string.h>
#include "serial.h"

#define INITRD_MAGIC 0x52494E49  // "INIR" LE

typedef struct {
    char     name[64];
    uint32_t size;
    uint8_t *data;       // 指向内核内存中的原始数据
} initrd_file_t;

static initrd_file_t initrd_files[64];
static uint32_t initrd_file_count = 0;

int initrd_parse(const uint8_t *archive, uint32_t archive_size) {
    if (archive_size < 8) return -1;
    
    uint32_t magic = *(uint32_t *)archive;
    if (magic != INITRD_MAGIC) {
        kprintf("[INITRD] Bad magic: 0x%x\n", magic);
        return -1;
    }
    
    initrd_file_count = *(uint32_t *)(archive + 4);
    if (initrd_file_count > 64) {
        kprintf("[INITRD] Too many files: %d\n", initrd_file_count);
        return -1;
    }
    
    uint32_t offset = 8;
    for (uint32_t i = 0; i < initrd_file_count; i++) {
        if (offset + 8 > archive_size) return -1;
        
        uint32_t name_len = *(uint32_t *)(archive + offset);
        uint32_t data_size = *(uint32_t *)(archive + offset + 4);
        offset += 8;
        
        if (name_len >= 64 || offset + name_len + data_size > archive_size) return -1;
        
        memcpy(initrd_files[i].name, archive + offset, name_len);
        initrd_files[i].name[name_len] = '\0';
        initrd_files[i].size = data_size;
        initrd_files[i].data = (uint8_t *)(archive + offset + name_len);
        
        kprintf("[INITRD] File '%s' size=%d @ 0x%x\n", 
                initrd_files[i].name, data_size, (uint32_t)initrd_files[i].data);
        
        offset += name_len + data_size;
    }
    
    kprintf("[INITRD] Parsed %d files.\n", initrd_file_count);
    return 0;
}

// 按名查找,返回索引或-1
int initrd_find(const char *name) {
    for (uint32_t i = 0; i < initrd_file_count; i++) {
        if (strcmp(initrd_files[i].name, name) == 0) return (int)i;
    }
    return -1;
}
VFS抽象层
复制代码
// vfs.h
#ifndef VFS_H
#define VFS_H

#include <stdint.h>

#define MAX_OPEN_FILES 64
#define MAX_FDS_PER_PROC 16

typedef struct vfs_inode {
    uint32_t size;
    uint8_t *data;        // initrd专用:直接内存指针
    uint32_t refcount;
} vfs_inode_t;

typedef ssize_t (*read_fn_t)(vfs_inode_t *inode, void *buf, size_t count, uint32_t offset);

typedef struct vfs_ops {
    read_fn_t read;
    // write_fn_t write;  // 本章只读
} vfs_ops_t;

typedef struct vfs_file {
    vfs_inode_t *inode;
    vfs_ops_t   *ops;
    uint32_t     offset;
    uint32_t     refcount;
} vfs_file_t;

// 全局打开文件表
extern vfs_file_t open_file_table[MAX_OPEN_FILES];

// 初始化VFS
void vfs_init(void);

// 分配/释放全局文件槽位
int vfs_alloc_file(vfs_inode_t *inode, vfs_ops_t *ops);
void vfs_release_file(int fd_global);

// initrd专用操作
ssize_t initrd_read(vfs_inode_t *inode, void *buf, size_t count, uint32_t offset);

#endif

// vfs.c
#include "vfs.h"
#include "string.h"

vfs_file_t open_file_table[MAX_OPEN_FILES];
static vfs_ops_t initrd_ops = { .read = initrd_read };

void vfs_init(void) {
    memset(open_file_table, 0, sizeof(open_file_table));
}

int vfs_alloc_file(vfs_inode_t *inode, vfs_ops_t *ops) {
    for (int i = 0; i < MAX_OPEN_FILES; i++) {
        if (open_file_table[i].refcount == 0) {
            open_file_table[i].inode = inode;
            open_file_table[i].ops = ops;
            open_file_table[i].offset = 0;
            open_file_table[i].refcount = 1;
            return i;
        }
    }
    return -1; // ENFILE
}

void vfs_release_file(int idx) {
    if (idx >= 0 && idx < MAX_OPEN_FILES && open_file_table[idx].refcount > 0) {
        open_file_table[idx].refcount--;
    }
}

ssize_t initrd_read(vfs_inode_t *inode, void *buf, size_t count, uint32_t offset) {
    if (!inode || !inode->data) return -1;
    if (offset >= inode->size) return 0; // EOF
    
    uint32_t available = inode->size - offset;
    uint32_t to_read = count < available ? count : available;
    
    memcpy(buf, inode->data + offset, to_read);
    return (ssize_t)to_read;
}
进程级FD表与系统调用
复制代码
// process.h 补充
typedef struct process {
    // ... 已有字段 ...
    int fd_table[MAX_FDS_PER_PROC]; // 值为全局open_file_table索引,-1=空闲
} process_t;

// syscall.c 补充
#include "vfs.h"
#include "initrd.h"

#define SYS_OPEN  3
#define SYS_READ  4
#define SYS_CLOSE 5

// 辅助:用户空间缓冲区安全检查
static int check_user_buf(const void *ptr, size_t len) {
    uint32_t addr = (uint32_t)ptr;
    return (addr < 0xC0000000 && addr + len <= 0xC0000000);
}

static void handle_syscall(interrupt_frame_t *frame) {
    switch (frame->eax) {
        case SYS_OPEN: {
            const char *path = (const char *)frame->ebx;
            if (!check_user_buf(path, 1)) { frame->eax = -1; break; }
            
            // 简化:不支持路径,仅文件名匹配
            int idx = initrd_find(path);
            if (idx < 0) { frame->eax = -1; break; } // ENOENT
            
            // 创建inode(initrd文件只读,共享数据指针)
            static vfs_inode_t inodes[64]; // 简化:静态池
            inodes[idx].size = initrd_files[idx].size;
            inodes[idx].data = initrd_files[idx].data;
            inodes[idx].refcount = 1;
            
            int gfd = vfs_alloc_file(&inodes[idx], &initrd_ops);
            if (gfd < 0) { frame->eax = -1; break; }
            
            // 在当前进程fd表中找空位
            int ufd = -1;
            for (int i = 0; i < MAX_FDS_PER_PROC; i++) {
                if (current_process->fd_table[i] == -1) {
                    current_process->fd_table[i] = gfd;
                    ufd = i;
                    break;
                }
            }
            if (ufd < 0) { vfs_release_file(gfd); frame->eax = -1; break; }
            
            frame->eax = ufd;
            break;
        }
        
        case SYS_READ: {
            int ufd = frame->ebx;
            void *buf = (void *)frame->ecx;
            size_t count = frame->edx;
            
            if (ufd < 0 || ufd >= MAX_FDS_PER_PROC || 
                current_process->fd_table[ufd] == -1) {
                frame->eax = -1; break;
            }
            if (!check_user_buf(buf, count)) { frame->eax = -1; break; }
            
            int gfd = current_process->fd_table[ufd];
            vfs_file_t *f = &open_file_table[gfd];
            
            ssize_t n = f->ops->read(f->inode, buf, count, f->offset);
            if (n > 0) f->offset += n;
            
            frame->eax = (uint32_t)n;
            break;
        }
        
        case SYS_CLOSE: {
            int ufd = frame->ebx;
            if (ufd < 0 || ufd >= MAX_FDS_PER_PROC || 
                current_process->fd_table[ufd] == -1) {
                frame->eax = -1; break;
            }
            
            int gfd = current_process->fd_table[ufd];
            vfs_release_file(gfd);
            current_process->fd_table[ufd] = -1;
            frame->eax = 0;
            break;
        }
        
        // ... 其他syscall保持不变 ...
    }
}
initrd打包工具(宿主机端)
复制代码
#!/usr/bin/env python3
# tools/mkinitrd.py
import struct, sys, os

MAGIC = b'INIR'

def pack(files):
    out = bytearray()
    out += MAGIC
    out += struct.pack('<I', len(files))
    for path in files:
        name = os.path.basename(path).encode()
        with open(path, 'rb') as f:
            data = f.read()
        out += struct.pack('<II', len(name), len(data))
        out += name
        out += data
    return bytes(out)

if __name__ == '__main__':
    files = sys.argv[1:]
    blob = pack(files)
    # 输出为C数组头文件
    print('const uint8_t initrd_blob[] = {')
    for i in range(0, len(blob), 16):
        chunk = blob[i:i+16]
        print('    ' + ', '.join(f'0x{b:02x}' for b in chunk) + ',')
    print('};')
    print(f'const uint32_t initrd_size = {len(blob)};')

Makefile集成:

复制代码
initrd.h: user/hello.elf user/config.txt
	python3 tools/mkinitrd.py $^ > $@

kernel.bin: ... initrd.h
	$(LD) -T linker.ld -o $@ $^

关键细节解析

1. 为什么fd要分进程级和全局级两层?

Unix语义要求:fork()后子进程继承父进程的fd,且共享同一文件偏移量。这意味着多个fd可能指向同一个打开文件实例。全局open_file_table存储真正的文件状态(偏移、inode),进程fd_table只是索引。如果只用一层,就无法实现fd共享和独立关闭语义。

2. 为什么initrd_read不需要加锁?

当前内核是单CPU、不可抢占式调度(中断处理中不调度)。initrd数据是只读的,不存在并发写竞争。当未来引入SMP或内核抢占时,需要为vfs_file_t.offset添加自旋锁或原子操作。现在不加锁是正确的,过早优化反而引入死锁风险。

3. 用户缓冲区检查为什么不够安全?

当前的check_user_buf仅验证地址范围<0xC0000000。但如果用户传入的地址跨越了未映射的页面,memcpy仍会触发#PF。生产级内核应使用copy_from_user包装器,在临时禁用页错误异常或使用特殊段覆盖的前提下进行拷贝,并在#PF处理中检测是否为合法的用户访问失败。本章为教学简化,假设用户程序行为良好。


调试Checklist:文件系统相关崩溃排查
症状 可能原因 排查方法
initrd解析报Bad magic 打包脚本字节序错误 / C数组嵌入方式不对 xxd initrd_blob.h对比前4字节是否为49 4e 49 52;确认objcopy/linker正确链接
open返回-1但文件确实存在 文件名不匹配 / initrd_find字符串比较bug kprintf打印initrd中所有文件名及长度;确认用户传入字符串无尾部\0问题
read返回0但文件非空 offset已到达末尾 / inode.size错误 在initrd_read入口kprintf打印offset和size;确认parse阶段size赋值正确
read返回正确字节数但内容乱码 数据指针偏移计算错误 / 用户buf未正确拷贝 dump inode->data验证源数据;检查memcpy参数顺序
close后再read仍成功 fd_table未清零 / refcount逻辑错误 确认close中fd_table[ufd]=-1;检查release后refcount是否归零

🔧 黄金法则 :文件系统bug往往表现为"偶尔正确、偶尔错误"的数据损坏。在测试VFS之前,先用一个独立的单元测试程序验证initrd_parse的正确性:手动构造已知内容的归档,解析后逐字节比对。不要等到syscall层才发现问题。


本章小结与下一步

今天我们让内核拥有了"记忆":

  • ✅ 设计并实现了可工作的initrd归档格式与解析器
  • ✅ 构建了VFS抽象层,分离了文件接口与存储实现
  • ✅ 实现了open/read/close三个核心文件syscall
  • ✅ 用户程序能够通过标准接口读取文件内容

从此,你的操作系统不再是一个只能运行硬编码程序的演示品,而是拥有了数据持久化能力和标准化的I/O语义。VFS抽象层更为未来接入真实磁盘文件系统奠定了架构基础。

下一章预告:《键盘驱动与Shell:让人类与OS对话》

有了文件系统和系统调用,我们终于可以构建交互式界面了。下一章将实现PS/2键盘驱动、行编辑缓冲区和一个简易命令行Shell,让你的OS第一次接受人类指令,从initrd加载并执行程序。


参考资料
  • Linux Kernel Documentation/filesystems/vfs.rst
  • OSDev Wiki - Initial Ramdisk / VFS
  • xv6 Source: fs.c, file.c, sysfile.c
  • The Design of the UNIX Operating System (Maurice Bach), Chapter 5
  • 本系列完整代码:你的GitHub仓库链接(Commit: v1f1s1r)

📝 作者注 :这是《从零手写操作系统》系列的第11篇。VFS是第一个让你体会"抽象之美"的章节------当你用同一套read()接口既能读initrd、未来又能读ext2时,你会真正理解Unix哲学的力量。如果你被fd/inode两层结构绕晕,请记住:这恰恰是Unix文件系统最精妙的设计之一。建议先画出进程fd_table→全局open_file_table→inode→data的四层指针关系图,再动手写代码。下一章,我们让OS学会"倾听"!

相关推荐
kyriewen1 小时前
我实测了前端日期的 5 个坑:差 8 小时只是开始
前端·javascript·程序员
天天向上2911 小时前
Spring AI 实战:14 个踩坑记录,其中一个让我的测试全绿但产品是坏的
java·开源
今年下半年1 小时前
【VUE3】移动端:使用腾讯地图显示区域统计与放大显示资产点位
javascript·前端框架·vue·地图
正在走向自律1 小时前
从只写接口到独立交付前后端,飞算JavaAI能给Java后端工程师多少溢价?
java·springboot·ai编程·全栈开发·java求职·飞算javaai·金九银十
Chen—LSN2 小时前
C语言——文件操作(2)
c语言·开发语言·c++·经验分享·笔记·算法·c#
a努力。2 小时前
LangChain Agent消息链路全解析
java·前端·javascript
今年下半年2 小时前
【前端】ant-design-vue 表格「行、列都动态」的处理方案
前端·javascript·vue.js
SL_staff2 小时前
物模型是设备的数字身份证:属性、事件、服务如何决定IoT系统扩展性
java·物联网·全栈
程序员yu2 小时前
会编网络:别被入门教程骗了,Python实用能力不在语法本身
开发语言·python·学习·算法