裸启动Linux

Hacker News Top 工具

摘要

本文介绍如何创建一个运行单进程的最小化Linux启动,精简通常的initramfs,使启动时间小于一秒。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/15 17:59

# 裸机启动 Linux · ……还有一件事…… 来源:https://nick.zoic.org/art/boot-naked-linux/ 2026\-05\-19c (https://nick.zoic.org/tag/c)/linux (https://nick.zoic.org/tag/linux) *启动一个 Linux 内核来承载单个进程,而不是一个完整的操作系统……而且在一秒内完成!* 在我还是个孩子的时候 (https://nick.zoic.org/art/ultima-iv-reflections/),计算机并不会被娇惯着 24/7 运行,当你用完它们时,你就把它们关掉,当你想再次使用它们时,你只需打开电源,在一秒左右的时间里,它们就会加载磁盘驱动器里的任何内容。在 2000 年代初期,新引入的 SSD 曾让启动变得很快,但一如既往,科技行业不断加码,以至于即使是一个拥有快速 SSD 的 16 核怪兽,仍然需要一分钟才能站稳脚跟。所以我想尝试一种替代方案:保留 Linux 内核,但尽可能剥离其他一切。开始吧……嗯,不是“什么也不做”,而是“少做很多”。**更新**:按照悠久的传统,在尝试修复几个细节时,我发现了“从零开始构建微型 Linux (https://blinry.org/tiny-linux/)”,它用 Rust 实现了我这里的大部分功能,而且是一年前做的,所以也值得一看。 ## Hello, World! Linux 系统做的第一件事就是运行某种“init”程序,它会加载所有其他进程、配置等内容。这个程序并没有什么特别之处,它只是一个普通的可执行文件或脚本,而且多年来已经有几种不同的处理方法。所以我们可以用 C 语言写一个新的……这是 `init.c`: ```c #include #include #include int main(int argc, char **argv) { fprintf(stderr, "Hello from init.c!"); reboot(RB_POWER_OFF); } ``` 它所做的就是打印一条消息,然后重启计算机。如果我们的 init 进程*退出*,内核就会 panic,所以我们不使用忙等待1 (https://nick.zoic.org/art/boot-naked-linux/#fn:1) 或永久休眠等方法,而是使用 `reboot(RB_POWER_OFF)` 来有序地关闭虚拟机。 ## 制作 initrd 现代 Linux 支持相当复杂的多阶段过程 (https://wiki.debian.org/initramfs)。外面有大量资源 (https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html),很多都已经过时 20 年了,而且也有一些变化 (https://www.kernel.org/doc/html/v4.14/admin-guide/initrd.html)。以下是我认为它在 2026 年 Linux 6.8 左右的当前工作方式的总结: - 引导加载程序使用内核和一个名为“initrd”的虚拟文件系统运行。 - 内核尝试将“initrd”文件解包到“initramfs”中,这是一个位于 RAM 中的根文件系统,允许初始化工具运行。 - 它寻找一个名为 `/init` 的文件(或者 `rdinit=` 内核参数指定的文件)。 - 如果存在,则运行它,该进程接管初始化。 - 否则,它会回退到: - 挂载由 `root=` 内核参数指定的根分区。 - 在 `/dev` 挂载 `devtmpfs` 文件系统。 - 从那里运行 `/init`(或者 `init=` 内核参数指定的文件)。 - 否则,或者如果 init 进程退出,内核就会 panic。 大多数现代发行版使用第一个分支:提供一个相当大的 initrd 文件系统,以便在尝试启动真正的文件系统之前加载模块和固件。我 PC 启动所用的 `initrd` 文件大小为 73MB,根据 `lsinitramfs` 显示,它包含 2163 个文件!有一些关于如何构建带有替代 init 的文件系统的例子 (https://medium.com/@mustafaakin/writing-my-own-init-with-go-part-1-22e81495a246),但我想更简单一些,替换整个 `initrd`。 如果我们静态编译我们的示例代码,例如包含所有需要的库,我们就可以制作自己的只有一个文件的 `initrd`: ```bash gcc -static init.c -o init echo 'init' | cpio -o --format=newc | gzip -c > initrd ``` ### 关于 cpio `cpio` 是一个非常奇怪且古老的程序,它的命令行让 `tar` 看起来都很友好。但我们暂时不关心细节。稍后我会注意到,如果你收到内核消息: ``` Initramfs unpacking failed: no cpio magic ``` ……这意味着 cpio 格式或压缩方式或类似的东西与你的内核不兼容。内核会尝试继续,但稍后会出现类似这样的错误消息: ``` check access for rdinit=/init failed: -2, ignoring ``` ……这意味着要么 initramfs 没有发生*,要么你的二进制文件放在了错误的位置(`-2` 是 `-ENOENT`,表示文件未找到)。你也可能会收到关于不兼容架构的消息。这条消息出现在启动过程中相当早的阶段,内核会尝试继续,所以你必须仔细往回看。而如果你看到: ``` Trying to unpack rootfs image as initramfs... ``` ……然后什么也没有,那是个好迹象。它直到很晚才会记录成功,那时应该会显示: ``` Run /init as init process ``` 顺便提一下,如果你想把许多文件打包到一个 cpio 归档中,你需要类似这样的命令: ```bash (cd $SOURCE_DIR; find . | cpio -o -H newc) | gzip -c > $OUTPUT_FILE ``` 通过管道传输文件列表可能看起来很奇怪,但 `cpio` 比 `tar` 更老,比*shell 文件名通配*还老,所以也许我们可以原谅它 (https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html#why-cpio-rather-than-tar) ## 虚拟化 在真实硬件上实现这个会涉及令人厌烦的 USB 密钥交换,所以我使用 QEMU (https://qemu.org/) 来创建一个虚拟系统进行实验。QEMU 允许你直接从命令行使用内核和文件系统镜像启动 (https://qemu-project.gitlab.io/qemu/system/linuxboot.html)。目前,我没有使用完整的 QEMU,而是使用 KVM 来运行虚拟化系统。对于完整仿真,你也可以用 `qemu-system-x86_64` 或适合你系统的仿真器来运行这些示例。它稍微慢一些,但其他方面都一样。 首先我们需要一个内核,我只是使用我机器上的当前内核,但 `/boot/vmlinuz` 文件只有 root 可读,所以我们先在当前工作目录中复制一份并更改其所有权: ```bash sudo cp /boot/vmlinuz . sudo chown $USER:$GROUP vmlinuz ``` 如果你愿意自己构建一个精简的内核 (https://weeraman.com/building-a-tiny-linux-kernel/),那就更好了。 我们现在有了两个二进制文件:内核 `vmlinuz` 和仅包含我们程序 `init` 的 init 镜像 `initrd`。所以我们可以这样启动系统: ```bash kvm -m 1G -nographic -kernel vmlinuz \ -initrd initrd -append "console=ttyS0" ``` `-nographic` 和 `-append "console=ttyS0"` 选项为我们提供了一个终端控制台来监视 stderr,而不是弹出一个图形控制台。 当内核启动时,它将我们的 `initrd` 解包到 ram 磁盘中,并运行我们的 `init` 二进制文件: ``` [ 0.000000] Linux version 6.8.0-111-generic (buildd@lcy02-amd64-088) [ 0.000000] Command line: console=ttyS0 [ 0.489390] Trying to unpack rootfs image as initramfs... [ 0.805419] Run /init as init process Hello from init.c! [ 0.807535] reboot: Power down ``` *(简化版)* ## 设备 即使我们不需要任何文件系统,我们可能也需要一些永久存储。但在我们的 `/init` 运行时,我们还没有挂载根文件系统,所以没有可用的设备!设备通过内核机制“devtmpfs”变得可用,所以我们首先要做的就是激活它。我们可以从 C 程序中使用 `mount("devtmpfs", "/dev", "devtmpfs", 0, NULL)` 来挂载 devtmpfs。 QEMU 可以使用 `-hda` 选项将主机文件作为块设备呈现给客户机,该文件在客户机中会显示为 `/dev/sda`。所以让我们修改之前的代码,挂载 `devtmpfs`,打开 `/dev/sda` 并读取该文件的前几个字节: ```c #include #include #include #include #include #include int main(int argc, char **argv) { fprintf(stderr, "Hello from init.c!\n"); mount("devtmpfs", "/dev", "devtmpfs", 0, NULL); int fd = open("/dev/sda", O_RDWR); uint32_t buffer[2]; read(fd, buffer, sizeof(buffer)); close(fd); fprintf(stderr, "Read %08x %08x\n", ntohl(buffer[0]), ntohl(buffer[1])); reboot(RB_POWER_OFF); } ``` *(是的,我知道,规范的 C 代码需要散布返回值检查和对 errno 的合理报告。为了清晰起见,我已经省略了这些。)* 在运行之前,我们需要一个磁盘镜像。让我们在文件中生成一些随机字节: ```bash dd if=/dev/random of=diskimage bs=1K count=1K ``` 然后我们可以像之前一样编译并运行: ```bash gcc -static -o init init.c echo 'init' | cpio -o --format=newc | gzip -c > initrd kvm -m 1G -nographic -kernel vmlinuz \ -initrd initrd -append "console=ttyS0" \ -no-reboot -hda diskimage ``` ... 最终输出类似于(为简洁起见编辑过): ``` [ 0.000000] Linux version 6.8.0-111-generic (buildd@lcy02-amd64-088) [ 0.000000] Command line: console=ttyS0 [ 0.010975] Memory: 975024K/1048056K available (22528K kernel code, 4438K rwdata, 14412K rodata, 4924K init, 4788K bss, 72772K reserved, 0K cma-reserved) [ 0.493923] Trying to unpack rootfs image as initramfs... [ 0.712522] ata1.00: ATA-7: QEMU HARDDISK, 2.5+, max UDMA/100 [ 0.713091] ata1.00: 2048 sectors, multi 16: LBA48 [ 0.816634] Run /init as init process Hello from init.c! Read 4463823c a5c37e77 [ 0.819849] reboot: Power down ``` 就是这样,从开机到运行程序并读取磁盘,不到一秒钟。 ## 启动真实硬件 外面有很多相互矛盾的指令,而且大多数似乎都假设你正在尝试更新正在使用的系统的引导加载程序,而不是向 USB 密钥添加 EFI 启动,但以下是我成功的方法: - 插入 USB 密钥 - 使用 lsblk 绝对确定它是正确的设备(在我的情况下是 /dev/sda,但小心,这些命令会破坏你指向的任何驱动器。) - 使用 `dd if=/dev/zero of=/dev/sda bs=1M count=1` 完全清除引导扇区和分区表。 - 使用 `sudo cfdisk` 设置分区: - 将设备格式化为“dos” - 创建一个类型为“EFI”(十六进制 ID `ef`)的分区,大小为 512M,并标记为可引导。 - 创建另一个分区供以后使用,暂时保留类型为“Linux”(十六进制 ID `83`) 它最终应该看起来像这样: ``` Disk: /dev/sda Size: 3.76 GiB, 4037017600 bytes, 7884800 sectors Label: dos, identifier: 0xdeadd0d0 Device Boot Start End Sectors Size Id Type /dev/sda1 * 2048 1050623 1048576 512M ef EFI (FAT-12/16/32) /dev/sda2 1050624 7884799 6834176 3.3G 83 Linux ``` 很多地方似乎说分区表必须是 GPT 类型,并且 EFI 分区必须是其中的一种特殊类型,但这就是我的 Mint Linux 安装 USB 的样子,并且在这个破笔记本电脑上对我有效。 现在我们可以为分区创建文件系统: ```bash sudo mkfs.fat -F 32 /dev/sda1 sudo mkfs.ext3 /dev/sda2 ``` 理论上,UEFI 可以从多个位置加载多个文件,并运行一个名为 `startup.nsh` 的小脚本等等,但这台笔记本电脑完全忽略这些,唯一能让它愉快启动的方法是在 ` /EFI/BOOT/BOOTX64.EFI` 创建一个文件。所以我需要制作一个“统一内核”,其中包含加载存根、内核本身以及包含我们程序的 initrd 文件。我们可以使用 `ukify` 工具来实现: ```bash sudo apt install systemd-ukify systemd-boot-efi ukify build --linux=vmlinuz --initrd=initrd ``` 然后我们只需将新的统一内核文件复制到正确的位置: ```bash sudo mount /dev/sda1 /mnt sudo mkdir -p /mnt/EFI/BOOT sudo cp vmlinuz.unsigned.efi /mnt/EFI/BOOT/BOOTX64.EFI sudo umount /mnt ``` ... 然后启动 USB 密钥!然后发现它重启太快,没来得及拍照,于是添加一个 `sleep(5);` 进去。然后再启动一次,拍张照片: 真实笔记本电脑屏幕上的消息照片 ## 微型内核 一开始我只是从我的运行桌面复制了 Linux 内核:这是一个 Ubuntu 安装,作为内核来说相当庞大,支持许多与我们这个一次一程序项目无关的东西。不过构建一个新的 Linux 内核相当容易: ```bash git clone https://github.com/torvalds/linux.git cd linux make tinyconfig make menuconfig ``` 这将带你进入 Linux 内核配置菜单,一个精彩绝伦的文本菜单系统,有上千个选项,已经困扰新用户大约 30 年了。 Linux 内核配置菜单 `make tinyconfig` 几乎关闭了所有东西,实际上关闭得太多。根据“从零开始构建微型 Linux (https://blinry.org/tiny-linux/)”,有些东西需要重新打开才能使我们的内核工作。 ### 配置 我尝试按顺序排列这些,以便能够在菜单中逐个选择: - “General setup” - “Local version” 我将其设置为“tiny”,以便更容易区分哪个是哪个内核。 - “Initial RAM filesystem and RAM disk (initramfs/initrd) support” 更多选项会出现。我保留了“Support initial ramdisk/ramfs compressed using gzip”,并关闭了其他选项,我们最好在内核中只保留一个解压器。“Initramfs source file(s)” 很有趣,我稍后会回来看它。 - Compiler optimization level 我将其更改为“Optimize for size (-Os)”。 - “Configure standard kernel features (expert users)” 别被这个吓人的名字吓到。这既是一个开关(按空格),也是一个嵌套菜单(按回车),非常令人困惑,而且内容取决于它是打开还是关闭。 - “Multiple users, groups and capabilities support” 我暂时关闭了它,因为我们并不真正需要它。 - “Enable support for printk” 可以通过关闭它来节省一点空间,但没有它,你就不知道内核为什么没有启动。 - “64-bit kernel” 就连我手头最破的笔记本电脑也是 64 位的。如果你从垃圾堆里翻出更旧的东西,你可能更喜欢 32 位。 - “Processor type and features” 这也取决于你的目标平台,但我选择了打开“Symmetric multi-processing support”,并将“Maximum number of CPUs”设置为 4,但关闭了“Cluster scheduler support”和“Multi-core scheduler support”。 - “Enable the block layer” 我认为这是访问块设备(如“/dev/sda1”)所必需的,但我关闭了“Legacy autoloading support”和“Allow writing to mounted block devices”。 - “Executable file formats” - “Kernel support for ELF binaries” 我们的 `init` 程序被编译为 ELF 格式,我们或许可以将其编译为更旧的格式,但没有真正的优势。我关闭了“Kernel support for scripts starting with #!”,因为我们那里没有 shell…… - “Device Drivers” - “Generic Driver Options” - “Maintain a devtmpfs filesystem to mount at /dev” 这就是我们实际在系统上找到设备的方法。 - “Character devices” - “Enable TTY” 我们绝对需要它来与我们的程序通信…… - “Serial drivers” ……并且我们的‘qemu’仿真将文本控制台作为虚拟串行设备提供,所以我打开了“8250/16550 and compatible serial support”。 - “Graphics support” - “Frame buffer devices” 只是为了好玩,我打开了它,只选择了“Provide legacy /dev/fb* device”,看看我们的程序是否真的能*啊哈*做图形处理,会很有趣! - “File systems” 我们可能不会立即需要它们,但我打开了“The Extended 3 (ext3) filesystem”和“The Extended 4 (ext4) filesystem”。 - “DOS/FAT/EXFAT/NT Filesystems” 我也打开了“MSDOS fs support”、“VFAT (Windows-95) fs support”和“exFAT filesystem support”。能够从 EFI 分区读取更多文件可能会很方便。 - “Kernel hacking” 同样,不要被菜单名字吓到。我只是进入了“printk and dmesg options”,并设置了“Show timing information on printks”,这能给 printk 消息加上漂亮的时间戳。 如果你想制作带 UEFI 存根的内核,

相似文章

Frood:一个基于Alpine Initramfs的NAS

Hacker News Top

描述了一种完全从Alpine Linux initramfs运行NAS的方法,支持干净启动、A/B部署、声明式git跟踪配置,并相比Alpine的无盘模式降低了复杂性。

用 x86_64 汇编写成的 Linux 桌面

Lobsters Hottest

一位开发者借助 Claude Code,用纯 x86_64 汇编重建了完整的 Linux 桌面栈——从 shell、终端、窗口管理器到各种工具,实现微秒级启动,并延长数小时续航。

理解Linux内核:Linux内核启动

Hacker News Top

本文以太空殖民地隐喻描述初始化阶段,解释了x86_64架构下Linux内核的启动过程,涵盖从引导程序交接至用户空间初始化的完整流程。

人生苦短,别用慢终端

Lobsters Hottest

本文详细介绍了通过避免框架、缓存补全以及懒加载工具来加速终端启动的实用技巧,实现了30毫秒的shell启动。