文件系统与系统文件用途
一个约 8 MB 的裸机系统,为什么还要自己写文件系统?答案在分工:romfs 只读、随镜像走;TinyFS 可写、持久化到磁盘。下面按模块拆解两套 FS、分区表、多磁盘抽象,以及 romfs 里 20 个真实文件的用途。
引言:两套文件系统,各管一摊
8 MB 的体量装不下一个通用文件系统(ext2/fat 都太重、还要驱动层)。Genesis 把需求劈成两半:稳定运行所需的、永远不变的代码与配置放进 romfs;用户会改、要活过重启的数据放进 TinyFS。两者都挂进同一个 VFS,对用户态看起来是同一棵树。
- romfs 只读 · 压缩 · 编译进内核镜像
- VFS 统一挂载点 /
- TinyFS 可写 · 持久化 · 落在磁盘分区
-
romfs
内核编译期由 tools/build_users.py 打包、压缩,启动时由内核解压挂载。不可写 —— 所有写请求走到这里会被 VFS 拒绝。
-
TinyFS
自研、落在磁盘分区(基址由 MBR 决定)。首次启动自动格式化并写入内置文件,之后的用户改动都活在这里。
romfs:只读、压缩、编译进内核
romfs 是一条极简的串行记录流。内核启动时由 vfs_load_romfs 调 kernel/inflate.c 的 inflate_raw 解压,再挂上 VFS。它没有目录树结构 —— 目录只是名字里带斜杠的“伪文件”,由 VFS 在挂载时展开。
记录格式(每条文件一条)
[u32 namelen] name 的字节长度
[name] 路径字符串,如 "/bin/tinysh.TNCR"
[u8 flag] 0 = 原样存储;1 = zlib raw-deflate 压缩
[u32 orig_len] 解压后长度(字节)
[u32 stored_len] 实际存储长度(字节)
[data] 文件内容(flag=1 时为已压缩数据)
约定:namelen == 0 表示记录流结束(EOF 哨兵)。
压缩方案
flag=1 时,内容用 zlib 的 raw-deflate 编码(Python 侧 compressobj(wbits=-15) 生成)。内核侧不依赖完整 zlib,而是自己实现 kernel/inflate.c 里的 inflate_raw 做流式解压,从而把解压器压进极小的内核镜像。
两边必须同步改格式
打包在 tools/build_users.py,解包在内核 kernel/vfs.c。只要改了记录布局(例如多一个字段),这两个文件必须一起同步:打包端写出的字节顺序、长度字段语义,必须和内核解包端逐字节对应,否则启动期解压挂载会错位、文件系统“看不见”。
实测体积
压缩后 romfs 实测 56 KB(上一代同类系统 336 KB,约 6× 更小)。这 56 KB 里塞着 20 个文件:从 4.6 KB 的 tinysh 到 210 KB 的 Lua 解释器都在内。
TinyFS:可写、持久化、在磁盘上
TinyFS 是 Genesis 自研的文件系统,实现于 kernel/fs/。它不写死任何起始地址 —— 基址 LBA 2048 只是一个“分区表决定后才生效”的默认值。布局从基址起:bitmap 32 扇区 + table 32 扇区,数据从第 65 扇区开始。
磁盘布局(从基址 LBA 2048 起)
所有偏移都相对分区基址,不是绝对扇区号;基址来自 MBR,见下节。
首次启动:自动格式化
TinyFS 卷不存在时,内核会在首次启动自动格式化并写入内置文件(/etc 下的配置、安装标记等)。这意味着用户拿到的是“已经能用的盘”,无需手动 mkfs。
用 MBR 定位分区基址
挂载前必须调 mbr_lookup() 从分区表读出 TinyFS 分区的起始 LBA。漏掉这一步会回退到默认 LBA 2048:对于自定义分区的盘,重启后内核找错位置,表现为“认不出文件系统”并重新格式化 —— 用户数据凭空消失。这是 TinyFS 最容易踩的坑。
tfs_set_limit():防止格式化越界
tfs_set_limit() 显式限定 TinyFS 分区能占用的扇区数。设置上限后,格式化只会在划定范围内写 bitmap/table/data,不会越界吃掉同盘上别的分区(例如用户空间或别的系统)。
分区表:扇区 0 既是引导扇区,又是 MBR
Genesis 让引导扇区和标准 MBR 共用同一个扇区 0。引导代码与数据只有 446 字节预算(0x000..0x1BD),剩下的 0x1BE..0x1FD 是 4 条标准分区表项;末尾 0x1FE..0x1FF 是 0x55AA 引导签名。
sector 0 (512 bytes)
+----------------------------+ 0x000
| boot code + data (≤ 446 B) | ← 超出会令 nasm 因 TIMES 为负报错
+----------------------------+ 0x1BE
| partition entry 0 (16 B) |
| partition entry 1 (16 B) |
| partition entry 2 (16 B) |
| partition entry 3 (16 B) |
+----------------------------+ 0x1FE
| 0x55 0xAA (boot signature) |
+----------------------------+ 0x200
分区类型号
-
0x7E
内核分区,标记为活动(active)。引导器从这里加载内核。
-
0x7F
TinyFS 分区。mbr_lookup() 按这个类型号找到它,从而拿到正确的起始 LBA。
为什么不能写死 LBA
基址来自分区表,不是常数。把 LBA 2048 写死进代码,遇到非默认分区布局的盘就会读错位置(见 TinyFS 一节)。正确做法是 mbr_lookup() 动态取出,再用 tfs_set_limit() 收住边界。
多磁盘:ATA 抽象层
ATA 多磁盘抽象层(kernel/disk/)用一个注册表支持多块盘。每块盘是一个 disk_t;控制器驱动自己管寄存器与队列,因为读写回调收的是 void *drv 私有状态。
typedef struct disk_t {
const char *name; // 盘名,如 "ide0"
const char *model; // 型号字符串
int loc; // 总线位置(控制器编号等)
u32 sectors; // 总扇区数
int is_boot; // 是否启动盘
disk_ops_t ops; // { read, write } 函数指针
} disk_t;
typedef struct disk_ops_t {
// drv 是驱动私有状态,各控制器自己解释
int (*read) (void *drv, u32 lba, u32 count, void *buf);
int (*write)(void *drv, u32 lba, u32 count, const void *buf);
} disk_ops_t;
默认盘转发:存量调用者零改动
抽象层保留一个“默认盘”指针。旧的、无参的 ata_read / ata_write 等 API 全部转发到默认盘 —— 这样早期直接调这些无参函数的代码不必改成“先查盘、再带 drv 调 ops”,存量调用者几乎不用动。
系统文件用途总览(romfs 里 20 个文件)
下面是 romfs 里真实存在的 20 个条目,逐个说明用途与大小(字节)。目录 / /bin /home /etc /sys /dev 由 VFS 从带斜杠的名字展开,不占 romfs 记录。
| 路径 | 大小 | 用途 |
|---|---|---|
| /bin/tinysh.TNCR | 104144 | 默认用户态 shell(TNCR 程序),登录后自动进入。 |
| /bin/LUA.TNCR | 210584 | Lua 5.4.7 解释器(TNCR 程序),可 REPL 或执行 .lua 脚本。 |
| /bin/SSHD.TNCR | 114500 | SSH 服务端(TNCR 程序),独立的用户态进程。 |
| /bin/CC.TNCR | 162680 | C 编译器(TNCR 程序),独立的用户态进程。 |
| /bin/EDIT.TNCR | 45980 | 内置编辑器(TNCR 程序),独立的用户态进程。 |
| /bin/JVM.TNCR | 5851 | JVM 运行时(TNCR 程序),独立的用户态进程。 |
| /bin/hello.TNCR | 214 | 示例程序:最小可运行用例。 |
| /bin/count.TNCR | 877 | 示例程序:计数演示。 |
| /bin/basic_demo.TNCR | 500 | 示例:基础语法演示。 |
| /bin/cpp_demo.TNCR | 339 | 示例:C++ 风格语法演示。 |
| /bin/minic_demo.TNCR | 500 | 示例:最小程序演示。 |
| /bin/net_demo.TNCR | 467 | 示例:网络用法演示。 |
| /bin/gui_demo.TNCR | 470 | 示例:图形界面用法演示。 |
| /bin/tiny_demo.TNCR | 238 | 示例:迷你程序演示。 |
| /DESKTOP.TNCR | 34 | 桌面程序,VGA 文本桌面的入口。 |
| /home/readme.txt | 804 | 系统说明文档(只读,随镜像走)。 |
| /home/notes.txt | 660 | 笔记文件(只读,随镜像走)。 |
| /home/hello.mc | 1215 | 方块脚本示例(Minecraft 风格 / 方块世界脚本,非 C)。 |
| /etc/passwd | 85 | 账号数据库:用户名与 UID 等(不含口令)。 |
| /etc/shadow | 224 | 口令哈希库:存放各账号的口令哈希(受保护)。 |
目录(由 VFS 展开,不占 romfs 记录)
根下的目录:/ /bin /home /etc /sys /dev。/sys 与 /dev 是内核在挂载时虚拟出来的(设备与系统信息),不是 romfs 里的真实条目。
首次启动后写到磁盘的文件(TinyFS,可写)
-
/etc/sshd.conf
SSH 服务配置,由安装向导/SSHD 写入。
-
/etc/net.conf
网络配置,记录 IP 等安装时设定的网络参数。
-
/etc/installed
TinyOS Genesis v0.1 installed by the setup wizard安装标记;内容即上方字符串,表明已跑过安装向导。
用户程序如何访问文件系统
TNCR 用户程序不能直接调内核的 vfs_* 函数。它们只能通过 tinyos_api_t 函数指针表访问内核能力 —— 这是一道架构边界:用户态拿到的是受控接口,内核维持对 VFS、内存、进程的唯一控制权。
Genesis v0.1 在表末尾追加的扩展字段
下表字段都追加在 tinyos_api_t 的末尾。因为只是“往后加”,磁盘上已有的旧 TNCR 程序在调用时仍然按老布局对得上前面的字段,新字段对它不可见 —— 所以旧程序不会被破坏(向后兼容)。
// appended at the END of tinyos_api_t (Genesis v0.1)
mkdir // 建目录
rm_file // 删文件
fs_list // 列目录
stat // 取文件信息
set_cwd // 设当前工作目录
proc_list_all // 列出所有进程
proc_kill // 终止进程
dev_list_all // 列出所有设备
dev_read // 读设备寄存器
dev_write // 写设备寄存器
sysinfo // 系统信息
date // 日期
version // 版本
绑定方式:静态初始化器
这些字段由 kernel/genesis_api.c 实现,并在 kernel/api.c 的 g_api 静态初始化器里完成绑定。绑定发生在编译期,不依赖运行期初始化顺序 —— 即便别的模块还没跑起来,函数指针表已经就绪。
架构含义:为什么不能直接调 vfs_*
vfs_* 是内核私有符号,用户态既看不到也没有调用特权。所有访问都经 tinyos_api_t 这一道“安检门”,内核可以校验路径、权限与配额,也方便将来替换底层实现而不影响用户程序。
磁盘占用实测(来自引导日志)
下面是真实引导日志里 TinyFS 挂载时打印的一行,可直接核对上面对布局的描述。
[tfs] layout: base LBA 2048, total 129024 bitmap 32 table 32 data start 65
-
total 129024
TinyFS 分区总扇区数(约 63 MB @ 512B/扇区)。
-
bitmap 32 / table 32
位图与索引表各占 32 扇区,排在分区基址之后。
-
data start 65
数据区起点扇区 = 基址 + 32 + 32 + 1(扇区从 0 计),与上节布局图一致。
注意:日志里的 2048 来自 MBR 分区表,不是写死常数;若分区布局不同,这个数字会变。