无需虚拟化软件的 Linux 虚拟机 – 用户模式 Linux
若仔细研读 Linux 内核文档,你会发现这样一个有趣的声明:
Linux 甚至已被移植到自身之上。现在你可以将内核作为用户空间应用程序运行——这被称为用户模式 Linux(UML)。
今天我们将探索如何通过在Linux内核内部运行内核进程来创建非传统虚拟机。这种方法既无需安装QEMU等虚拟化软件,也不需要root权限,从而开启了诸多令人着迷的可能性。
内核的硬件抽象机制
内核的核心职责在于抽象硬件并为用户空间提供统一接口,包括为多任务管理CPU、内存等共享资源。它通过底层硬件识别机制(如某些平台的设备树,该树列出系统组件)确定硬件架构,并连接相应的驱动程序。
这种硬件也可能完全虚拟化。例如在QEMU虚拟机中,内存和附加磁盘等资源由QEMU用户空间应用程序虚拟化,这会带来一定的性能开销。CPU呈现出有趣的案例——它同样可在用户空间实现虚拟化,尤其在模拟不同架构时。
虚拟化硬件驱动程序的精妙之处在于它们可以实现 启明(或更正式地称为 半虚拟化)。这意味着驱动程序能感知自身运行于虚拟化硬件环境,并通过特殊方式与硬件交互来发挥优势。具体实现虽复杂,但可想象驱动程序与虚拟硬件的交互方式远超物理硬件的局限。在线资料表明,半虚拟化技术能实现接近传统驱动物理设备性能的水准。
UML——用户空间进程中的内核
个人认为UML属于半虚拟化内核配置。它并非直接运行在裸机上,而是基于现有内核实例运行,并利用其部分用户空间功能。例如,控制台驱动程序无需连接物理UART,可直接使用标准用户空间输入/输出;块设备驱动程序也可指向主机文件系统中的文件而非物理磁盘。
在此架构中,UML本质上是巧妙运用文件和套接字等概念的用户空间进程,能够启动可运行自身进程的新Linux内核实例。这些进程与主机的具体映射关系——特别是CPU虚拟化机制——尚不完全明确,欢迎在评论区分享见解。可以设想一种实现方案:宾客线程和进程映射到主机对应对象,但系统可见性受限(类似容器),同时仍在嵌套的Linux内核中运行。
内核文档中的此页面对此机制有相当清晰的图解:
+----------------+
| Process 2 | ...|
+-----------+----------------+
| Process 1 | User-Mode Linux|
+----------------------------+
| Linux Kernel |
+----------------------------+
| Hardware |
+----------------------------+
强烈建议查阅该页获取更详尽的文档,特别是其中列举的实用性理由极具说服力。最后一点尤其吸引人:
- 它极具趣味性。
这正是我们今日深入探讨的缘由!
构建UML内核
首要前提:必须明确UML内核仅能在x86平台运行。你可以将x86 UML内核叠加在现有x86内核之上;据我所知,目前不支持其他配置方案。
接下来我们将构建UML二进制文件。配置流程从以下步骤开始:
ARCH=um make menuconfig
配置过程与常规内核配置类似。初始配置页面上你会立即注意到若干UML专属选项。我倾向于将这些视为“开明”驱动程序,其设计理念是利用主机的用户空间设施作为虚拟硬件。
本次演示中特别启用了BLK_DEV_UBD选项。文档说明如下:
用户模式Linux端口包含名为UBD的驱动程序,可让您将主机计算机上的任意文件作为块设备访问。除非确定无需此类虚拟块设备,否则请在此处选择 Y。
该选项默认未启用(这让我有些意外),因此建议设置为 Y。完成配置后,编译过程非常简单:
ARCH=um make -j16
这会直接生成 linux 二进制文件!
$ file linux
linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=742d088d46f7c762b29257e4c44042f321dc4ad5, with debug_info, not stripped
值得注意的是,它动态链接了C标准库:
$ ldd linux
linux-vdso.so.1 (0x00007ffc0a3ce000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3490409000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3490601000)
构建用户空间
要在嵌套内核中实现有效功能,我们需要用户空间。为简化流程,我选择下载最新版Buildroot并为x86/64架构进行构建。
如果你渴望冒险,想尝试从零构建一个精简的用户空间,却不知从何入手,那么结合微型Linux发行版实践会带来不少乐趣。
运行嵌套内核
为增加趣味性,我决定向嵌套内核提供块设备,向其中写入数据,再从主机系统验证该数据。
首先创建磁盘映像:
$ dd if=/dev/urandom of=./disk.ext4 bs=1M count=100
接着使用ext4格式化:
$ sudo mkfs.ext4 ./disk.ext4
现在在用户空间启动内核。我将使用Buildroot镜像(Buildroot提供的ext2文件)作为根文件系统:
./linux ubd0=/tmp/uml/rootfs.ext2 ubd1=/tmp/uml/disk.ext4 root=/dev/ubda
转眼间,熟悉的内核启动序列便映入眼帘!
Core dump limits :
soft - 0
hard - NONE
Checking that ptrace can change system call numbers...OK
Checking syscall emulation for ptrace...OK
Checking environment variables for a tempdir...none found
Checking if /dev/shm is on tmpfs...OK
Checking PROT_EXEC mmap in /dev/shm...OK
Linux version 6.14.7 (uros@debian-home) (gcc (Debian 12.2.0-14) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #6 Mon May 19 16:27:13 PDT 2025
Zone ranges:
Normal [mem 0x0000000000000000-0x0000000063ffffff]
Movable zone start for each node
Early memory node ranges
node 0: [mem 0x0000000000000000-0x0000000003ffffff]
Initmem setup node 0 [mem 0x0000000000000000-0x0000000003ffffff]
random: crng init done
Kernel command line: ubd0=/tmp/uml/rootfs.ext2 ubd1=/tmp/uml/disk.ext4 root=/dev/ubda console=tty0
printk: log buffer data + meta data: 16384 + 57344 = 73728 bytes
Dentry cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
Inode-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
Sorting __ex_table...
Built 1 zonelists, mobility grouping on. Total pages: 16384
mem auto-init: stack:all(zero), heap alloc:off, heap free:off
SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
NR_IRQS: 64
clocksource: timer: mask: 0xffffffffffffffff max_cycles: 0x1cd42e205, max_idle_ns: 881590404426 ns
Calibrating delay loop... 8931.73 BogoMIPS (lpj=44658688)
Checking that host ptys support output SIGIO...Yes
pid_max: default: 32768 minimum: 301
Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Memory: 57488K/65536K available (3562K kernel code, 944K rwdata, 1244K rodata, 165K init, 246K bss, 7348K reserved, 0K cma-reserved)
...
最终进入Buildroot登录界面:
Run /sbin/init as init process
EXT4-fs (ubda): warning: mounting unchecked fs, running e2fsck is recommended
EXT4-fs (ubda): re-mounted 23cafb4d-e18f-4af4-829d-f0dc7303e6c4 r/w. Quota mode: none.
EXT4-fs error (device ubda): ext4_mb_generate_buddy:1217: group 1, block bitmap and bg descriptor inconsistent: 7466 vs 7467 free clusters
Seeding 256 bits and crediting
Saving 256 bits of creditable seed for next boot
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Starting network: OK
Starting crond: OK
Welcome to Buildroot
buildroot login:
启动过程出乎意料地迅速。
现在在UML实例中创建磁盘挂载点:
# mkdir /mnt/disk
接着将第二个UBD设备(ubdb)挂载至此挂载点:
# mount /dev/ubdb /mnt/disk/
挂载完成后,可写入测试文件:
# echo "This is a UML test!" > /mnt/disk/foo.txt
# cat /mnt/disk/foo.txt
This is a UML test!
现在可关闭UML虚拟机:
# poweroff
执行后显示:
# Stopping crond: stopped /usr/sbin/crond (pid 64)
OK
Stopping network: OK
Stopping klogd: OK
Stopping syslogd: stopped /sbin/syslogd (pid 40)
OK
Seeding 256 bits and crediting
Saving 256 bits of creditable seed for next boot
EXT4-fs (ubdb): unmounting filesystem e950822b-09f7-49c2-bb25-9755a249cfa1.
umount: devtmpfs busy - remounted read-only
EXT4-fs (ubda): re-mounted 23cafb4d-e18f-4af4-829d-f0dc7303e6c4 ro. Quota mode: none.
The system is going down NOW!
Sent SIGTERM to all processes
Sent SIGKILL to all processes
Requesting system poweroff
reboot: Power down
在主机系统上:
$ sudo mount ./disk.ext4 ./img
$ cat ./img/foo.txt
This is a UML test!
这个小实验证实:我们成功使用UML运行了虚拟机,向其块设备写入了数据,且这些变更得以持久化,可在主机系统访问。
结论
本文始终将UML称为虚拟机,您对此产生疑问实属合理。一方面,它通过主机用户空间设施实现了硬件虚拟化理念,且环境拥有独立内核;另一方面,这个客户机内核与主机内核存在内在关联。虽然它追求隔离性,但无法达到基于KVM的QEMU虚拟机所能实现的隔离程度。
那么实际应用价值何在?UML是否适合运行隔离工作负载?我的专业判断是:多数生产场景可能并不适用。我认为UML的核心价值在于内核调试,而非作为完整的生产级虚拟化方案。对于可靠的虚拟机需求,运行于不同架构层的KVM虚拟化技术经过了更充分的实战检验。当然,若工作负载可接受共享主机内核,容器技术也提供了另一种选择。UML在这两者之间开辟了独特的利基:既提供独立内核实例,又保持与宿主内核的特殊连接。这确实是个引人入胜的概念。
或许未来这项有趣的技术会获得更多关注并得到更广泛应用。但就目前而言,它仍是绝佳的实验工具,至少能带来许多探索乐趣!
祝编程愉快!
本文文字及图片出自 Linux VM without VM software - User Mode Linux

> 在这种架构中,UML本质上是一个用户空间进程,它巧妙地运用文件和套接字等概念来启动一个新的Linux内核实例,该实例能够运行自己的进程。这些进程与主机的精确映射关系——特别是CPU如何实现虚拟化——是我尚未完全理解的部分,欢迎在评论区分享见解。可以设想一种实现方案:宾客线程和进程映射到主机对应进程,但系统可见性受限(类似容器),同时仍在嵌套的Linux内核中运行。
至少在第一代UML中,宾客进程实际上就是主机进程。宾客内核(作为用户空间进程)本质上通过ptrace()机制运行宾客进程,捕获所有系统调用并重定向至宾客内核内部执行。但这些进程在主机CPU上的运行方式与主机进程无异。
更妙的是,宾客内核巧妙地重定向了宾客端的ptrace()调用,使得您仍可在宾客环境中使用strace或gdb!
其精妙程度甚至允许深入运行UML嵌套UML。
> 实际应用价值何在?UML是否适合运行隔离工作负载?我的专业判断是:多数生产场景可能并不适用。
早年曾有主机商提供UML虚拟机租赁服务。这正是Linode公司的创业起点!
第二代是“skas”(独立内核地址空间),更多背景参见:https://user-mode-linux.sourceforge.net/old/skas.html
虽然SKAS内核补丁最终未被合并(可能有充分理由),但该技术与Xen/VM硬件支持的结合,使UML失去了存在的意义。
它对大规模托管场景确实失去意义,但在开发过程中搭建和拆除环境仍极具价值——尤其当你需要直接调试内核时,通过GDB进行调试变得轻而易举
知道为何人们放弃它吗?它本可成为Docker容器与KVM虚拟机之间极具潜力的折中方案
> 你知道人们为何放弃它吗?
并非完全放弃。它仍在维护中,甚至持续开发。
> 它似乎是Docker容器与KVM虚拟机之间有潜力的中间方案
当年我确实用它运行“虚拟机”,甚至有公司基于UML销售VPS账户。当时其他虚拟化方案要么远未成熟,要么价格高昂(据我记忆VMWare当时已相当完善,但尚无免费可靠的开源选项),而UML提供的隔离性(包含独立根目录的完整环境)远胜于简单chroot进程树(更完整的容器技术当时也不存在,所有用户都完全存在于主机环境中,无法授予网络根权限)。如今KVM等技术及更先进的容器化方案,已高效解决了多数人使用UML的目标场景(尤其在涉及大量内核交互——包括文件系统或网络访问时,UML相较其他方案性能明显劣势)。
不过UML仍对其原始用途极具价值:测试和调试某些内核级组件,如文件系统(FUSE在此领域存在竞争,但并非所有场景都适用)、网络驱动程序和过滤器等。当系统出错时,它能提供VMS和容器难以企及的深度追踪能力。
主要是性能问题。
我曾供职于一家销售基于UML虚拟机的托管公司,期间我们曾试用Xen作为替代方案,最终转而采用KVM。
此外KVM支持实时迁移和virtio驱动等特性,使自定义接口和可移植性更易处理。
对于多数应用场景而言,其运行速度过慢。
若想了解当前UML的应用实例:https://netdevconf.info/0x14/pub/slides/8/UML%20Time%20Trave…
这是测试场景。启用时间旅行模式可跳过睡眠操作,大幅加速单元测试。
……除非测试涉及大量系统调用,否则速度可能下降10-100倍 :(。该模式也不支持对称多处理。我真心期待更优版本的出现,这功能对我们至关重要——尤其考虑到“CPU占用期间时间暂停”的特性,能避免主机高负载时测试因耗时延长而随机失败。遗憾的是,解决此问题超出了我的专业领域。
初次了解[FreeBSD Jails]时我深受震撼,不禁好奇:若在容器化兴起前,该概念能为其需求进一步完善(是否可能?),是否会形成更高效的容器化平台?
FreeBSD 监狱环境:https://docs.freebsd.org/en/books/handbook/jails/
监狱环境在概念上与UML完全不同;它们共享主机内核,大致类似于容器/命名空间。UML则是完全独立的内核,以用户模式进程运行。
为何要用/dev/urandom而非/dev/zero初始化磁盘映像?既然不是加密磁盘容器,我看不出这样做的合理性,或许是我遗漏了什么?
可能是为了规避零写入优化。强制实际分配磁盘空间存储数据,而非模拟分配。
所以是为了让未来性能更可预测?
体验很棒。记得二十年前试用过。初次启动时,我在提示符下直接输入“linux”,内核就在终端里启动了。
结果系统因缺少根文件系统而崩溃。不过没关系,根文件系统就在这儿啊!
第二次我输入“linux root=/dev/hda1”(当时我们用的是并行ATA硬盘)。
系统成功启动并挂载了根文件系统——而这恰恰是主机启动时使用的根文件系统。
总之重启后系统恢复正常,无需重新安装。最重要的是我学到了宝贵教训:永远别再这么干。这往往才是最关键的经验。
我曾用同样方法修复过/boot损坏的机器。通过Live CD启动并构建UML内核(在另一块硬盘上操作——内存不足且为防万一未挂载主分区),成功从主根分区启动并轻松重建了initrd等组件。当时觉得自己聪明极了!
在supernested脚本中(该脚本用于测试KVM的嵌套极限),我们确实会在虚拟机中挂载根盘,但通过快照实现,因此相对安全。http://git.annexia.org/?p=supernested.git;a=tree
我常想,若UML能在Darwin上构建,我们就能获得无需虚拟化的MacOS容器方案。但这涉及两大未解难题:在非Linux系统上构建UML,以及在非x86架构上构建UML。
很久以前用过这个方案,可惜它仅支持单CPU,导致某些SMP漏洞无法显现。
不知实现SMP是否困难?若大量代码使用类似#ifdef CONFIG_ARCH_IS_UM的条件判断来区分单核/多核环境,可能会有难度。
SMP功能近期已实现,排入下个版本发布计划。
有意思
这感觉简直像放鞭炮
等你发现QEmu(和dosbox)也能做到时就知道了——在运行Windows或《沙丘2》时同样可行,旧版VirtualBox也支持(新版不确定)
> 今日我们将探讨如何通过在Linux内核内部运行Linux内核进程来启动非传统虚拟机。此方法既无需安装QEMU等虚拟化软件,也不需要root权限,由此开启了诸多有趣的可能性。
这已在开头几句说明过了。
我的观点是:虚拟机软件(尤其是老旧软件和仿真软件)本就不依赖虚拟化技术或root权限。确实容易混淆,因为QEMU后来演变为虚拟化软件(VirtualBox亦然)。但它们最初根本不使用硬件虚拟化技术,Dosbox至今仍是如此。
真希望有人能开发出这样的工具:编译Dockerfile后,直接通过普通套接字API模拟网络,立即在仿真环境中启动虚拟机。
没错,但若专门针对Linux内核设计为普通用户进程运行,就无需绕行CPU仿真代码。理论上调用主机的mmap()函数本应比折腾仿真MMU更高效。