构建最简 Linux 文件系统

上回说到了我们如何用 QEMU 搭建一个包含最新内核的开发环境。但试过一段时间后,我却感觉这样并不方便。 更多时候,我只需要运行单个程序(比如,单元测试)即可,而上文中所构建的完整环境却让这个过程便复杂了。本文介绍了一个更简单的方法。只需要一个简单的脚本即可构建完整运行环境。 initrd 上一篇文章中已经提到了使用 initrd 启动操作系统的方法。initrd 是一个只读文件系统镜像,内核启动时,会将镜像内容加载到内存中。程序可以像普通文件系统一样访问。 加载文件系统之后,内核会从其中寻找可用的 init 程序。因此我们只要编写合理的 init 程序,将其打包到一个 initrd 中即可。 busybox 在这种从零开始构建文件系统的场景下,使用动态库太过繁琐。因此我们直接用宿主机上的 busybox 来提供基本的程序。 准备阶段 首先创建一个 WORK_DIR 目录。此路径下的文件会被打包成文件系统镜像。 WORK_DIR=$(mktemp -d) trap "rm -rf $WORK_DIR" EXIT 之后寻找宿主机上的 busybox,将其复制到 $WORK_DIR/bin/busybox。 mkdir -p "$WORK_DIR/bin" # Copy busybox into the work directory BUSYBOX_PATH=$(which busybox) if [ -z "$BUSYBOX_PATH" ]; then echo "Error: busybox not found in PATH." exit 1 fi cp "$BUSYBOX_PATH" "$WORK_DIR/bin/busybox" 创建 $WORK_DIR/init 脚本,赋予其可执行权限。内核在启动中会识别并执行该脚本。 # Create init script cat << 'EOF' > "$WORK_DIR/init" #!...

一月 22, 2026 · 2 分钟 · 287 字 · Wokron

在 QEMU 上运行最新内核

最近,我的业余时间都花在了一个和系统编程有关的项目上。不久之后我遇到了一个问题:想要使用最新版本内核的特性并不是一件十分容易的事。发行版大多滞后于最新的内核版本,而我又不是那样激进的人,想要冒险在自己唯一的这台 Linux 电脑上升级内核(使用 installkernel 命令)。所以我需要一个办法,在保证我的系统安全的情况下,搭建一个具有新版本内核的开发环境。 这种情况容器帮不了我,虚拟机则是一个好选择。 我曾经写过一个简单的 QEMU 介绍。但实际上我对 QEMU 的了解也仅限于那篇文章的内容了。探索本文的内容花了我一些时间。在这个过程中,我也发现网上的相关内容要么包含了未充分说明的脚本,要么存在许多冗余选项。我觉得在此处以一个简单、充分解释的方式记录这一过程似乎是有意义的。 当然,从编译内核开始 如果你只需要一个能在虚拟机里运行的内核,不需要深度定制的话,那么编译内核其实很简单。这里有一个简单的教程,不过我们接下来要做的还要更简单。 首先,你要下载内核源码。如果不需要修改源码的话,从 kernel.org 下载是最方便的。这里下载了编写本文时的 stable 版本(6.17.9)。 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.9.tar.xz tar -xf linux-6.17.9.tar.xz cd ./linux-6.17.9 之后,配置编译选项。这将创建编译配置文件 .config make defconfig make kvm_guest.config 前一条命令选择使用默认编译配置。第二条命令在默认配置基础上设置编译选项,使编译后的镜像可以作为 kvm 上的虚拟机运行。如果有其他需求,可以在此基础上继续配置。 之后直接编译即可。由于只包含了作为虚拟机运行的最小配置,不需要编译驱动程序,这一过程应该很快。在我的笔记本上只用了一分钟。 make -j $(nproc) 编译后的内核镜像位于 arch/x86/boot/bzImage。当然,是 x86 架构。 file arch/x86/boot/bzImage # arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 6.17.9 (wokron@wokron-navi) #2 SMP PREEMPT_DYNAMIC Sun Nov 30 12:34:56 CST 2025, RO-rootFS, swap_dev 0XD, Normal VGA 准备根文件系统 简易根文件系统 这一点和我之前有关容器的文章中的内容类似。如果不考虑实际可用性,我们现在就可以运行一个内核了。...

十一月 29, 2025 · 4 分钟 · 655 字 · Wokron

用 Namespace 手搓一个容器

容器技术由 Linux 下三项技术构成。这三项技术分别是 Namespace、Cgroups 和 Unionfs。他们分别实现了系统逻辑资源的隔离、物理资源的限制以及容器的文件系统。 在这之中最为关键的是 Namespace。因为 Namespace 实现了虚拟化中最重要的隔离的功能。在 Namespace 之外即使不使用 Cgroups,用其他文件系统替代 Unionfs,依然能够实现一个容器的许多功能。 所以本文我们尝试用 Namespace 构建一个简单的容器。让我们首先想想,一个容器中的环境究竟需要与 host 隔离哪些资源。(请把容器想象成 host 之外的另一台机器。) 文件系统:容器中的进程不能访问 host 的文件系统。这意味着挂载点的隔离 – Mount Namespace 进程空间:容器中的进程无法查看容器外的进程信息。这意味着进程号的隔离 – PID Namespace 网络接口:容器中的进程拥有自己的网络接口,不使用 host 上的网络接口。这意味着网络的隔离 – Network Namespace 用户:容器中的用户和容器外的用户无关,例如容器内的 root 和容器外的 root 并不相同。这意味着用户的隔离 – User Namespace 物理资源:容器能够看到和管理的物理资源和容器外的资源不同。这意味着 Cgroups 视图的隔离 – Cgroups Namespace 时间:容器中的时间系统和容器外的时间不一定相同 – Time Namespace 主机名:容器中的主机名和容器外的主机名不一定相同 – UTS Namespace IPC:IPC,例如 Posix 消息队列,使用类似文件名的标识符,但是又并不真正存在于文件系统中。容器中的这些标识符和容器外的相同标识无关 – IPC Namespace 虽然种类很多,但是想要形成虚拟化的错觉只需要用到其中的一部分即可。为了构建我们的容器,我们选择只使用 Mount、PID 和 User。 隔离文件系统 容器化最重要的是隔离文件系统。所谓的程序运行环境,本质上就是文件系统中的各类库和应用程序。同一主机上的不同发行版的容器都运行在相同的内核上,他们只是在库和应用程序上存在不同。...

十月 13, 2025 · 6 分钟 · 1074 字 · Wokron

Linux 下的用户和容器中的用户

多用户操作系统的时代早就结束了。现在的人们一般不会通过多个用户登陆到同一个操作系统的方式共享计算资源,我们有更好的虚拟化技术。 不过用户依然在发挥作用,其中最主要的作用就是权限隔离。这一点似乎有许多内容可以讲,不过这里我们只会列出最关键的内容。容器中的用户会出现更多特殊情况,我们也会进行讨论。 用户与权限 用户和用户组 操作系统通过用户控制权限,特定的用户才能执行特定的操作。 操作系统中有一组配置好的用户。其信息被保存在 /etc/passwd 文件中。每个用户都属于一个或多个用户组。用户和用户组分别有 uid 和 gid。使用 id 命令可以查看当前用户的对应 ID。 $ id uid=1000(wokron) gid=1000(wokron) groups=1000(wokron) 1001(xxxxx) 1002(yyyyy) 其中 gid 指向的表示该用户的主组。可以通过 newgrp 命令切换主组。这会启动一个新的 shell。 $ newgrp xxxxx $ id uid=1000(wokron) gid=1001(xxxxx) groups=1000(wokron) 1001(xxxxx) 1002(yyyyy) 下面的内容忽略了许多细节。不过忽略的内容相对很少遇到。 当选择一个用户登陆系统时,这次登陆所创建的 shell 会将用户和其主组的 ID 附加到进程中。这两个 ID 称为该进程的进程凭证。 子进程创建时会继承父进程的的进程凭证。进程凭证表明了该进程代表哪一用户进行操作。 超级用户 进程的用户和用户组决定了其是否有权限执行某一操作。经典的 Unix 权限系统只区分了两种用户:超级用户和普通用户。超级用户能够执行操作系统所提供的所有操作,而普通用户则受到了限制。普通用户的操作不能够影响操作系统的状态。 超级用户的 uid 为 0,通常名为 root。其用户组通常只有 root(gid=0)。除此以外的用户都是普通用户,他们之间没有权限上的差异。 利用 sudo 命令可以临时提升当前用户的权限,使其以 root 用户的身份运行进程。(即进程和其子进程的进程凭证为 root)。 $ sleep 1 & ps -o user,group,uid,gid -p $!...

十月 12, 2025 · 3 分钟 · 485 字 · Wokron

试试触发(几乎)所有信号

输入 kill -L,你可以看到 Linux 下所有可用的标准信号,总共有 31 个。 $ kill -L 1 HUP 2 INT 3 QUIT 4 ILL 5 TRAP 6 ABRT 7 BUS 8 FPE 9 KILL 10 USR1 11 SEGV 12 USR2 13 PIPE 14 ALRM 15 TERM 16 STKFLT 17 CHLD 18 CONT 19 STOP 20 TSTP 21 TTIN 22 TTOU 23 URG 24 XCPU 25 XFSZ 26 VTALRM 27 PROF 28 WINCH 29 POLL 30 PWR 31 SYS 这回试试在这些信号原本的应用场景下触发它们。...

五月 23, 2025 · 10 分钟 · 2058 字 · Wokron

Linux 进程的内存管理

虽然我们都学过一个进程的内存由堆和栈组成。但是这样的模型还是太抽象了,其中掩盖了许多操作系统的细节。所以这里简单梳理一下进程的内存管理有关知识。 堆的增长 libc 中提供 malloc 函数申请堆上内存。底层由 brk 系统调用负责申请堆上内存。 在内核的视角下,堆空间是一个简单的结构。它由一个固定的堆底(符号 end)和可变的堆顶(称为 program break)组成。内核所要做的就是根据用户设定的 program break 将堆底和堆顶之间的内存标为有效。而 brk 系统调用的作用便是将某一地址设置为堆顶。 在 brk 系统调用之上,glibc 提供了两个不同的函数 int brk(void *addr) 和 void *sbrk(intptr_t increment)。前者直接设置 program break 地址,后者则根据 increment 取值调整 program break 位置。 下面使用 sbrk() 函数进行内存分配。sbrk() 会返回调用之前原本的 program break 地址,因此还需额外进行加减以获取当前地址。这一语义比较合理,因为这样 brk(N) 和 malloc(N) 的返回值都代表新分配的内存的起始地址。 #include <stdio.h> #include <unistd.h> extern char end; int main() { printf("Address of end symbol: %p\n", (void *)&end); printf("Current program break: %p\n", sbrk(0)); printf("New program break: %p\n", sbrk(1024) + 1024); printf("After deallocation: %p\n", sbrk(-512) - 512); } 运行结果如下...

五月 17, 2025 · 7 分钟 · 1484 字 · Wokron