小技巧:让内核“自带文件系统”:把 init 程序直接塞进内核二进制!
在内核还不支持读取磁盘、不支持文件系统时,如果使用一个临时的方式支持一个简单的文件系统? 今天教你一招——把文件系统直接‘编译进内核’! 无需磁盘、无需驱动,内核启动即执行 init,极致简洁!”
💡 问题:自制 OS 的第一道坎 —— “内核如何找到 init?”
当你写完第一个自制操作系统内核时,一定会遇到这个经典难题:
“我的内核已经运行了,但怎么加载并执行 init 进程呢?”
传统方案:
✅
从磁盘读取
:需要实现文件系统、块设备驱动、分区表解析…
❌
太复杂
:对于初学者来说,这简直是“地狱级”任务。
有没有更简单的办法?
当然有!
🧠 核心思路:把“文件系统”直接塞进内核二进制!
什么是 initramfs?
在 Linux 中, initramfs (Initial RAM Filesystem)是一个 内存中的根文件系统 ,它被压缩后嵌入到内核镜像中。内核启动时将其解压到内存,然后挂载为根文件系统,再执行 /sbin/init 。
我们不搞复杂的 initramfs,而是用一个 极简版的“内联文件系统” :
把 init 程序、配置文件等,直接以二进制数据的形式,作为“静态数据”编译进内核!
🛠️ 实现方法:用汇编或 C 语言将文件内容“硬编码”进内核
把文件系统塞内核二进制
创建文件系统文件结构
将内容打包为cpio格式
脚本文件: cpio_fs.sh
用法: bash cpio_fs.sh build
将cpio文件编译进内核elf文件
🔍 内核如何“读取”这些数据?
步骤 1:获取数据地址和大小
步骤 2:解析cpio文件格式
cpio (copy in / copy out)是一种用于归档和传输文件的 Unix 工具和文件格式。它最早出现在 Unix System III(1981 年)中,比 tar 更早被标准化(POSIX.1-1988 就包含了 cpio ),尽管在日常使用中不如 tar 流行,但它在 Linux 内核 initramfs、RPM 包管理器等关键系统组件中被广泛使用。
一、cpio 的工作原理
cpio 本身不直接遍历目录,而是 从标准输入读取文件路径列表 (通常由 find 或 echo 生成),然后:
-o
(copy-out)模式
:将这些文件打包成一个归档流,输出到 stdout。
-i
(copy-in)模式
:从 stdin 读取归档流,解包到文件系统。
-p
(pass)模式
:直接复制文件到指定目录(不生成归档)。
因此, cpio 的核心思想是: 归档 = 文件列表 + 文件内容的线性拼接 。
二、cpio 的归档格式
cpio 支持多种格式,主要分为三类:
- binary(旧格式,已废弃)
非可移植,依赖字节序
已淘汰,不推荐使用 -
ASCII / “newc” 格式(最常用)
可读的文本头 + 二进制数据
被 Linux initramfs、RPM 等广泛采用
文件名和元数据以
ASCII 十六进制字符串
存储
支持长文件名(最多 256 字节,默认)
所有字段固定长度(便于解析)
newc 格式的头部结构(每字段 8 字节,共 112 字节):
| 字段 | 长度 | 说明 |
|---|---|---|
| c_magic | 6 字节 | 魔数 “070701” (newc)或 “070702” (crc) |
| c_ino | 8 字节 | inode 号(十六进制 ASCII) |
| c_mode | 8 字节 | 文件权限(如 0100644 ) |
| c_uid | 8 字节 | 用户 ID |
| c_gid | 8 字节 | 组 ID |
| c_nlink | 8 字节 | 硬链接数 |
| c_mtime | 8 字节 | 修改时间(Unix 时间戳) |
| c_filesize | 8 字节 | 文件内容长度(字节) |
| c_devmajor | 8 字节 | 设备主设备号(仅设备文件) |
| c_devminor | 8 字节 | 设备次设备号 |
| c_rdevmajor | 8 字节 | 特殊文件的主设备号 |
| c_rdevminor | 8 字节 | 特殊文件的次设备号 |
| c_namesize | 8 字节 | 文件名长度(包含结尾 ) |
| c_checksum | 8 字节 | 校验和(newc 中通常为 0) |
所有数值字段均为 8 字节 ASCII 十六进制字符串 (小端?不,是字符串!比如 1024 存为 “00000400” )
文件记录布局:
文件名后
必须
有 null 字符
文件名和内容之间
自动对齐到 4 字节边界
(用
填充)
文件内容后也
对齐到 4 字节边界
归档结束标志:
一个特殊文件名为
“TRAILER!!!”
的条目
其
c_filesize = 0
表示归档结束
- crc 格式(newc + 校验和)
与 newc 类似,但
c_checksum
字段包含文件内容的 CRC32 校验和
魔数为
“070702”
创建和解包 cpio 归档
创建归档(newc 格式):
解包归档:
列出内容:
步骤 3:解析并执行 init
完整代码:
🎯 优势与适用场景
✅ 优势
极度简化
:无需磁盘驱动、文件系统、分区表
快速启动
:内核一启动就能执行 init,无需等待磁盘 I/O
调试友好
:所有代码都在一个二进制中,调试方便
可移植性强
:适用于任何能运行内核的环境(QEMU、物理机、嵌入式)
🎯 适用场景
自制操作系统学习
:初学者的最佳起点
嵌入式系统
:资源受限,需要最小化启动时间
引导加载程序
:在进入完整操作系统前提供简单功能
安全/恢复环境
:无需依赖外部存储
📦 实际应用:让内核“自带” init 进程
想象一下,你的内核二进制文件 kernel.bin 不仅包含内核代码,还包含了:
/sbin/init
(你的初始化程序)
/etc/fstab
(文件系统挂载表)
/bin/sh
(Shell)
/usr/bin/ls
(常用命令)
这一切都 编译进同一个文件 !
启动流程变成:
没有磁盘、没有驱动、没有文件系统——只有纯粹的内核和它的“内置文件”!
💡 高阶技巧:动态生成文件系统
如果你的 init 程序是用 C/C++ 编写的,可以写一个 构建脚本 ,自动将源文件编译成二进制,再嵌入到内核中:
🚫 注意事项
内存占用
:所有文件都加载到内存,不适合大文件
更新困难
:修改文件需重新编译内核
安全性
:所有代码都在内核中,一旦出错可能导致系统崩溃
✅ 总结:一个简单却强大的技巧
这个小技巧的核心思想是:
“不要去读取磁盘上的文件,而是把文件直接‘变成’内核的一部分。”
它完美解决了自制 OS 初期的“鸡生蛋、蛋生鸡”问题:
没有文件系统 → 无法读取 init
没有 init → 无法挂载文件系统
通过这个技巧,我们绕过了这个死循环,让内核 自给自足 。
关注我,带你从零开始,玩转操作系统开发!
评论