在 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

讨论一下环境变量

这里所说的环境变量并不仅仅指 Shell 中的变量,而是每一个进程各自拥有的,能够通过系统 API 获取的变量。当然,Shell 通常会提供环境变量的操作方法,我们也经常通过在 Shell 中管理环境变量。但是 Shell 中的变量和环境变量实际上并不完全相同,在使用 Shell 时可能会混淆这两种概念,这里便稍微分辨一下。 一、环境变量 从程序的角度来看,环境变量很简单。环境变量是每个进程各自拥有的键值对集合。进程可以从其环境变量中读取已有变量,修改已有变量或创建新的变量。当进程创建其子进程时,子进程会继承父进程的环境变量,但子进程的环境变量的修改并不会影响父进程的环境变量。 为了方便,这里以 python 为例。 进程可以读取已有环境变量: # inherit.py import os print(os.environ["HOME"]) 也可以写入或创建环境变量: os.environ["MY_VAR"] = "my_value" 子进程会继承父进程的环境变量: # env.py import os import sys assert len(sys.argv) == 2 father_proc = bool(int(sys.argv[1])) print_my_var = lambda: print( f"process={'father' if father_proc else 'child'}, " f"MY_VAR={os.environ.get('MY_VAR', None)}" ) print_my_var() if father_proc: os.environ["MY_VAR"] = "1" print_my_var() os.system(f"{sys.executable} {sys.argv[0]} 0") print_my_var() else: os.environ["MY_VAR"] = "2" print_my_var() $ python inherit....

十一月 2, 2024 · 2 分钟 · 300 字 · Wokron