UML 使用指南

简介

欢迎使用用户模式 Linux

用户模式 Linux 是第一个开源虚拟化平台(首次发布日期为 1991 年)和第二个用于 x86 PC 的虚拟化平台。

UML 与使用虚拟化软件包 X 的虚拟机有何不同?

我们已经习惯认为虚拟化也意味着某种程度的硬件仿真。事实上,并非如此。只要虚拟化软件包为操作系统提供操作系统可以识别并具有驱动程序的设备,这些设备就不需要仿真真实的硬件。如今,大多数操作系统都内置了对许多仅在虚拟化下使用的“虚假”设备的支持。用户模式 Linux 将这一概念发挥到了极致 - 没有一个真实的设备可见。它是 100% 人工的,或者如果我们使用正确的术语,它是 100% 半虚拟化的。所有 UML 设备都是抽象概念,它们映射到主机提供的某些东西 - 文件、套接字、管道等。

UML 与各种虚拟化软件包之间的另一个主要区别在于,UML 内核和 UML 程序的操作方式之间存在明显的差异。UML 内核只是一个在 Linux 上运行的进程 - 与任何其他程序相同。它可以由非特权用户运行,并且不需要任何特殊的 CPU 功能。然而,UML 用户空间略有不同。主机上的 Linux 内核会协助 UML 拦截 UML 实例上运行的程序尝试执行的所有操作,并使 UML 内核处理其所有请求。这与其他不区分来宾内核和来宾程序的虚拟化软件包不同。这种差异导致 UML 与 QEMU 相比存在一些优点和缺点,我们将在本文档后面介绍。

为什么我想要用户模式 Linux?

  • 如果用户模式 Linux 内核崩溃,您的主机内核仍然正常。它没有以任何方式加速(vhost、kvm 等),并且它没有尝试直接访问任何设备。事实上,它就像任何其他进程一样。

  • 您可以以非 root 用户身份运行用户模式内核(您可能需要为某些设备安排适当的权限)。

  • 您可以运行一个非常小的虚拟机,其占用空间很小,用于特定任务(例如 32M 或更少)。

  • 对于任何“内核特定任务”(例如转发、防火墙等),您都可以获得极高的性能,同时仍与主机内核隔离。

  • 您可以在不破坏任何东西的情况下尝试内核概念。

  • 您不受“仿真”硬件的约束,因此您可以尝试一些奇怪而奇妙的概念,这些概念在仿真真实硬件时很难支持,例如时间旅行和使您的系统时钟依赖于 UML 的行为(对于测试之类的东西非常有用)。

  • 这很有趣。

为什么不运行 UML

  • UML 使用的系统调用拦截技术使其在任何用户空间应用程序中都固有地较慢。虽然它可以执行与大多数其他虚拟化软件包相当的内核任务,但其用户空间非常**慢**。根本原因是 UML 创建新进程和线程的成本非常高(大多数 Unix/Linux 应用程序认为这是理所当然的)。

  • 目前,UML 严格来说是单处理器。如果您想运行一个需要多个 CPU 才能正常运行的应用程序,那么这显然是错误的选择。

构建 UML 实例

任何发行版中都没有 UML 安装程序。虽然您可以使用现成的安装介质使用虚拟化软件包安装到空白虚拟机中,但没有 UML 等效项。您必须在主机上使用适当的工具来构建可行的文件系统镜像。

这在 Debian 上非常容易 - 您可以使用 debootstrap 来完成。在 OpenWRT 上也很容易 - 构建过程可以构建 UML 镜像。所有其他发行版 - YMMV。

创建镜像

创建一个稀疏的原始磁盘镜像

# dd if=/dev/zero of=disk_image_name bs=1 count=1 seek=16G

这将创建一个 16G 的磁盘镜像。操作系统最初只会分配一个块,并在 UML 写入时分配更多块。从内核版本 4.19 开始,UML 完全支持 TRIM(通常由闪存驱动器使用)。通过指定 discard 作为挂载选项或运行 tune2fs -o discard /dev/ubdXX 在 UML 镜像内部使用 TRIM 将请求 UML 将任何未使用的块返回给操作系统。

在磁盘镜像上创建文件系统并挂载它

# mkfs.ext4 ./disk_image_name && mount ./disk_image_name /mnt

此示例使用 ext4,任何其他文件系统(如 ext3、btrfs、xfs、jfs 等)也可以正常工作。

在挂载的文件系统上创建最小的操作系统安装

# debootstrap buster /mnt http://deb.debian.org/debian

debootstrap 不会设置 root 密码、fstab、主机名或任何与网络相关的内容。这取决于用户来完成。

设置 root 密码 - 最简单的方法是 chroot 到挂载的镜像中

# chroot /mnt
# passwd
# exit

编辑关键系统文件

UML 块设备称为 ubds。由 debootstrap 创建的 fstab 将为空,它需要一个根文件系统的条目

/dev/ubd0   ext4    discard,errors=remount-ro  0       1

镜像主机名将设置为与您正在创建其镜像的主机相同。最好更改它,以避免“哎呀,我重启了错误的机器”。

UML 支持两种类型的网络设备 - 较旧的 uml_net 设备,这些设备计划废弃。这些称为 ethX。它还支持较新的矢量 IO 设备,这些设备速度明显更快,并且支持某些标准的虚拟网络封装,如 Ethernet over GRE 和 Ethernet over L2TPv3。这些称为 vec0。

根据使用的是哪一种,/etc/network/interfaces 将需要如下条目

# legacy UML network devices
auto eth0
iface eth0 inet dhcp

# vector UML network devices
auto vec0
iface vec0 inet dhcp

现在我们有一个几乎可以运行的 UML 镜像,我们所需要的只是一个 UML 内核及其模块。

大多数发行版都有 UML 包。即使您打算使用自己的内核,使用 stock 内核测试镜像始终是一个好的开始。这些包附带一组模块,应将其复制到目标文件系统。位置取决于发行版。对于 Debian,它们位于 /usr/lib/uml/modules 下。将此目录的内容递归复制到挂载的 UML 文件系统

# cp -rax /usr/lib/uml/modules /mnt/lib/modules

如果您已经编译了自己的内核,则需要通过运行以下命令来使用通常的“将模块安装到某个位置”过程

# make INSTALL_MOD_PATH=/mnt/lib/modules modules_install

这将将模块安装到 /mnt/lib/modules/$(KERNELRELEASE) 中。要指定完整的模块安装路径,请使用

# make MODLIB=/mnt/lib/modules modules_install

此时,镜像已准备好启动。

设置 UML 网络

UML 网络旨在模拟以太网连接。此连接可以是点对点连接(类似于使用背靠背电缆的机器之间的连接)或与交换机的连接。UML 支持多种方法来构建这些连接:本地机器、远程机器、本地和远程 UML 以及其他 VM 实例。

传输

类型

功能

吞吐量

tap

矢量

校验和、tso

> 8Gbit

混合

矢量

校验和、tso、多数据包接收

> 6GBit

原始

矢量

校验和、tso、多数据包接收、发送”

> 6GBit

EoGRE

矢量

多数据包接收、发送

> 3Gbit

Eol2tpv3

矢量

多数据包接收、发送

> 3Gbit

bess

矢量

多数据包接收、发送

> 3Gbit

fd

矢量

取决于 fd 类型

各不相同

vde

矢量

取决于 VDE VPN:虚拟网络定位器

各不相同

tuntap

旧式

~ 500Mbit

守护程序

旧式

~ 450Mbit

套接字

旧式

~ 450Mbit

ethertap

旧式

已过时

~ 500Mbit

vde

旧式

已过时

~ 500Mbit

  • 所有具有 tso 和校验和卸载的传输都可以在 TCP 流上提供接近 10G 的速度。

  • 所有具有多数据包接收和/或发送功能的传输方式都可以提供高达 1Mps 或更高的 pps 速率。

  • 所有传统的传输方式通常限制在约 600-700MBit 和 0.05Mps。

  • GRE 和 L2TPv3 允许连接到以下所有对象:本地机器、远程机器、远程网络设备和远程 UML 实例。

  • Socket 仅允许 UML 实例之间的连接。

  • Daemon 和 bess 需要运行本地交换机。此交换机也可以连接到主机。

网络配置权限

大多数受支持的网络模式都需要 root 权限。例如,在传统的 tuntap 网络模式中,用户需要成为与隧道设备关联的组的成员。

对于较新的网络驱动程序(如向量传输),需要 root 权限来触发 ioctl 以设置 tun 接口和/或在需要时使用原始套接字。

这可以通过授予用户特定功能而不是以 root 身份运行 UML 来实现。在向量传输的情况下,用户可以将 CAP_NET_ADMINCAP_NET_RAW 功能添加到 uml 二进制文件中。此后,UML 可以使用普通用户权限运行,并具有完整的网络功能。

例如

# sudo setcap cap_net_raw,cap_net_admin+ep linux

配置向量传输

所有向量传输都支持类似的语法

如果 X 是接口编号,如 vec0、vec1、vec2 等,则选项的通用语法是

vecX:transport="Transport Name",option=value,option=value,...,option=value

常用选项

这些选项对于所有传输都是通用的

  • depth=int - 设置向量 IO 的队列深度。这是 UML 将尝试在单个系统调用中读取或写入的数据包数量。默认数字为 64,对于大多数需要 2-4 Gbit 吞吐量的应用程序来说通常足够。更高的速度可能需要更大的值。

  • mac=XX:XX:XX:XX:XX - 设置接口 MAC 地址值。

  • gro=[0,1] - 启用或关闭 GRO。启用接收/发送卸载。此选项的效果取决于所配置的传输中主机端的支持。在大多数情况下,它将启用 TCP 分段和 RX/TX 校验和卸载。主机端和 UML 端的设置必须相同。如果不是,UML 内核将产生警告。例如,GRO 默认在本地机器接口(例如 veth 对、网桥等)上启用,因此应在相应的 UML 传输(raw、tap、混合)中的 UML 中启用,以便网络正常运行。

  • mtu=int - 设置接口 MTU

  • headroom=int - 调整默认的预留空间(32 字节),如果数据包需要重新封装,例如封装到 VXLAN 中。

  • vec=0 - 禁用多数据包 IO 并回退到一次处理一个数据包的模式

共享选项

  • ifname=str 绑定到本地网络接口的传输具有一个共享选项 - 要绑定的接口的名称。

  • src, dst, src_port, dst_port - 所有使用具有源和目标和/或源端口和目标端口概念的套接字的传输都使用这些来指定它们。

  • v6=[0,1] 指定是否希望所有通过 IP 运行的传输使用 v6 连接。此外,对于在 v4 和 v6 上操作方式存在一些差异的传输(例如 EoL2TPv3),设置正确的操作模式。在没有此选项的情况下,套接字类型根据 src 和 dst 参数解析/解析的结果确定。

tap 传输

示例

vecX:transport=tap,ifname=tap0,depth=128,gro=1

这将将 vec0 连接到主机上的 tap0。tap0 必须已存在(例如使用 tunctl 创建)并处于 UP 状态。

tap0 可以配置为点对点接口并分配 IP 地址,以便 UML 可以与主机通信。或者,可以将 UML 连接到连接到网桥的 tap 接口。

虽然 tap 依赖于向量基础设施,但它目前不是真正的向量传输,因为 Linux 不支持普通用户空间应用程序(如 UML)在 tap 文件描述符上的多数据包 IO。这是一种特权,仅提供给可以通过专用接口(如 vhost-net)在内核级别连接到它的东西。计划在未来某个时候为 UML 提供一个类似于 vhost-net 的助手。

所需权限:tap 传输需要以下任一权限

  • tap 接口存在,并且使用 tunctl 创建为持久性且由 UML 用户拥有。示例 tunctl -u uml-user -t tap0

  • 二进制文件具有 CAP_NET_ADMIN 权限

混合传输

示例

vecX:transport=hybrid,ifname=tap0,depth=128,gro=1

这是一种实验性/演示传输,它将 tap 用于发送,并将原始套接字用于接收。原始套接字允许多数据包接收,从而产生比普通 tap 显著更高的数据包速率。

所需权限:混合传输需要 UML 用户具有 CAP_NET_RAW 功能以及 tap 传输的要求。

原始套接字传输

示例

vecX:transport=raw,ifname=p-veth0,depth=128,gro=1

此传输在原始套接字上使用向量 IO。虽然您可以绑定到任何接口(包括物理接口),但最常见的用法是绑定到 veth 对的“对等”端,另一端配置在主机上。

Debian 的示例主机配置

/etc/network/interfaces:

auto veth0
iface veth0 inet static
     address 192.168.4.1
     netmask 255.255.255.252
     broadcast 192.168.4.3
     pre-up ip link add veth0 type veth peer name p-veth0 && \
       ifconfig p-veth0 up

UML 现在可以像这样绑定到 p-veth0

vec0:transport=raw,ifname=p-veth0,depth=128,gro=1

如果 UML 客户机配置为 192.168.4.2 和子网掩码 255.255.255.0,则它可以与 192.168.4.1 上的主机通信

原始传输还提供对将某些过滤卸载到主机的支持。用于控制它的两个选项是

  • bpffile=str 要加载为套接字过滤器的原始 bpf 代码的文件名

  • bpfflash=int 0/1 允许从用户模式 Linux 内部加载 bpf。此选项允许使用 ethtool 加载固件命令来加载 bpf 代码。

在任何一种情况下,bpf 代码都会加载到主机内核中。虽然目前仅限于传统的 bpf 语法(不是 ebpf),但它仍然存在安全风险。除非用户模式 Linux 实例被认为是受信任的,否则不建议允许此操作。

所需权限:原始套接字传输需要 CAP_NET_RAW 功能。

GRE 套接字传输

示例

vecX:transport=gre,src=$src_host,dst=$dst_host

这将配置一个以太网 over GRE(又名 GRETAPGREIRB)隧道,该隧道将 UML 实例连接到主机 dst_host 上的 GRE 端点。GRE 支持以下其他选项

  • rx_key=int - 如果设置,则为 rx 数据包的 GRE 32 位整数密钥,txkey 也必须设置

  • tx_key=int - 如果设置,则为 tx 数据包的 GRE 32 位整数密钥,rx_key 也必须设置

  • sequence=[0,1] - 启用 GRE 序列

  • pin_sequence=[0,1] - 假装每次数据包上的序列都重置(需要与某些真正损坏的实现互操作)

  • v6=[0,1] - 分别强制 IPv4 或 IPv6 套接字

  • 目前不支持 GRE 校验和

GRE 有一些注意事项

  • 每个 IP 地址只能使用一个 GRE 连接。由于每个 GRE 隧道都直接在 UML 实例上终止,因此无法多路复用连接。

  • 密钥实际上不是安全功能。虽然它最初是这样设计的,但它的“安全性”是可笑的。但是,它是确保隧道未配置错误的有用功能。

一个 Linux 主机(本地地址为 192.168.128.1)连接到 192.168.129.1 上的 UML 实例的示例配置

/etc/network/interfaces:

auto gt0
iface gt0 inet static
 address 10.0.0.1
 netmask 255.255.255.0
 broadcast 10.0.0.255
 mtu 1500
 pre-up ip link add gt0 type gretap local 192.168.128.1 \
        remote 192.168.129.1 || true
 down ip link del gt0 || true

此外,GRE 已针对各种网络设备进行了测试。

所需权限:GRE 需要 CAP_NET_RAW

l2tpv3 套接字传输

_警告_。L2TPv3 有一个“错误”。它是被称为“比 GNU ls 具有更多选项”的“错误”。虽然它有一些优点,但通常有更简单(且不那么冗长)的方法将 UML 实例连接到某些东西。例如,大多数支持 L2TPv3 的设备也支持 GRE。

示例

vec0:transport=l2tpv3,udp=1,src=$src_host,dst=$dst_host,srcport=$src_port,dstport=$dst_port,depth=128,rx_session=0xffffffff,tx_session=0xffff

这将配置一个以太网 over L2TPv3 固定隧道,该隧道将 UML 实例连接到主机 $dst_host 上使用 L2TPv3 UDP 风味和 UDP 目标端口 $dst_port 的 L2TPv3 端点。

L2TPv3 始终需要以下其他选项

  • rx_session=int - 用于接收数据包的 l2tpv3 32 位整数会话

  • tx_session=int - 用于发送数据包的 l2tpv3 32 位整数会话

由于隧道是固定的,因此它们不会协商,并且会在两端预先配置。

此外,L2TPv3 支持以下可选参数。

  • rx_cookie=int - 用于接收数据包的 l2tpv3 32 位整数 cookie - 与 GRE 密钥相同的功能,更多地用于防止配置错误,而不是提供实际的安全性

  • tx_cookie=int - 用于发送数据包的 l2tpv3 32 位整数 cookie

  • cookie64=[0,1] - 使用 64 位 cookie 而不是 32 位。

  • counter=[0,1] - 启用 l2tpv3 计数器

  • pin_counter=[0,1] - 假装每次数据包上的计数器都重置(需要与某些真正损坏的实现互操作)

  • v6=[0,1] - 强制 v6 套接字

  • udp=[0,1] - 使用该协议的原始套接字 (0) 或 UDP (1) 版本

L2TPv3 有一些注意事项

  • 在原始模式下,每个 IP 地址只能使用一个连接。由于每个 L2TPv3 隧道都直接在 UML 实例上终止,因此无法多路复用连接。UDP 模式可以使用不同的端口来实现此目的。

以下是如何配置 Linux 主机以通过 L2TPv3 连接到 UML 的示例

/etc/network/interfaces:

auto l2tp1
iface l2tp1 inet static
 address 192.168.126.1
 netmask 255.255.255.0
 broadcast 192.168.126.255
 mtu 1500
 pre-up ip l2tp add tunnel remote 127.0.0.1 \
        local 127.0.0.1 encap udp tunnel_id 2 \
        peer_tunnel_id 2 udp_sport 1706 udp_dport 1707 && \
        ip l2tp add session name l2tp1 tunnel_id 2 \
        session_id 0xffffffff peer_session_id 0xffffffff
 down ip l2tp del session tunnel_id 2 session_id 0xffffffff && \
        ip l2tp del tunnel tunnel_id 2

所需权限:L2TPv3 在原始 IP 模式下需要 CAP_NET_RAW 权限,而在 UDP 模式下则不需要特殊权限。

BESS 套接字传输

BESS 是一个高性能的模块化网络交换机。

https://github.com/NetSys/bess

它支持简单的顺序数据包套接字模式,在较新的版本中,该模式使用矢量 IO 以获得高性能。

示例

vecX:transport=bess,src=$unix_src,dst=$unix_dst

这将配置一个 BESS 传输,使用 unix_src Unix 域套接字地址作为源,unix_dst 套接字地址作为目标。

有关 BESS 配置以及如何分配 BESS Unix 域套接字端口的信息,请参阅 BESS 文档。

https://github.com/NetSys/bess/wiki/Built-In-Modules-and-Ports

BESS 传输不需要任何特殊权限。

VDE 向量传输

虚拟分布式以太网 (VDE) 是一个项目,其主要目标是为虚拟网络提供高度灵活的支持。

http://wiki.virtualsquare.org/#/tutorials/vdebasics

VDE 的常见用途包括快速原型设计和教学。

示例

vecX:transport=vde,vnl=tap://tap0

使用 tap0

vecX:transport=vde,vnl=slirp://

使用 slirp

vec0:transport=vde,vnl=vde:///tmp/switch

连接到 VDE 交换机

vecX:transport=\"vde,vnl=cmd://ssh remote.host //tmp/sshlirp\"

连接到远程 slirp(即时 VPN:将 SSH 转换为 VPN,它使用 sshlirp) https://github.com/virtualsquare/sshlirp

vec0:transport=vde,vnl=vxvde://234.0.0.1

连接到局域云(所有使用相同多播地址的 UML 节点在同一多播域(LAN)中的主机上运行,将会自动连接到一个虚拟 LAN)。

配置旧式传输

旧式传输现在被认为是过时的。请使用向量版本。

运行 UML

本节假设已在主机上安装了发行版中的用户模式 Linux 软件包或自定义构建的内核。

这些会向系统添加一个名为 linux 的可执行文件。这是 UML 内核。它可以像任何其他可执行文件一样运行。它会将大多数普通的 Linux 内核参数作为命令行参数。此外,它还需要一些 UML 特有的参数才能执行一些有用的操作。

参数

强制参数:

  • mem=int[K,M,G] - 内存量。默认单位为字节。它也接受 K、M 或 G 限定符。

  • ubdX[s,d,c,t]= 虚拟磁盘规范。这不是真正的强制性的,但在几乎所有情况下都可能需要它,以便我们可以指定根文件系统。最简单的镜像规范是文件系统的镜像文件名(使用创建镜像中描述的方法之一创建)。

    • UBD 设备支持写入时复制 (COW)。更改保存在一个单独的文件中,该文件可以被丢弃,从而允许回滚到原始的原始镜像。如果需要 COW,则 UBD 镜像指定为:cow_file,master_image。示例:ubd0=Filesystem.cow,Filesystem.img

    • 可以将 UBD 设备设置为使用同步 IO。任何写入都会立即刷新到磁盘。通过在 ubdX 规范后添加 s 来完成此操作。

    • UBD 对指定为单个文件名的设备执行一些启发式方法,以确保没有将 COW 文件指定为镜像。要关闭它们,请在 ubdX 之后使用 d 标志。

    • UBD 支持 TRIM - 要求主机操作系统回收镜像中任何未使用的块。要关闭它,请在 ubdX 之后指定 t 标志。

  • root= 根设备 - 很可能是 /dev/ubd0(这是一个 Linux 文件系统镜像)

重要的可选参数

如果 UML 以“linux”运行且没有额外的参数,它将尝试为镜像中配置的每个控制台(在大多数 Linux 发行版中最多 6 个)启动一个 xterm。每个控制台都在一个 xterm 中启动。这使得在带有 GUI 的主机上轻松使用 UML 变得简单。但是,如果将 UML 用作测试工具或在纯文本环境中运行,则这是错误的方法。

为了更改此行为,我们需要指定一个替代控制台并将其连接到受支持的“线路”通道之一。为此,我们需要映射一个控制台以使用与默认 xterm 不同的东西。

将控制台编号 1 转移到标准输入/输出的示例

con1=fd:0,fd:1

UML 支持各种串行线路通道,这些通道使用以下语法指定

conX=channel_type:options[,channel_type:options]

如果通道规范包含用逗号分隔的两个部分,则第一部分是输入,第二部分是输出。

  • 空通道 - 丢弃所有输入或输出。示例 con=null 将默认将所有控制台设置为空。

  • fd 通道 - 使用文件描述符编号进行输入/输出。示例:con1=fd:0,fd:1.

  • 端口通道 - 在 TCP 端口号上启动 telnet 服务器。示例:con1=port:4321。主机必须具有 /usr/sbin/in.telnetd(通常是 telnetd 软件包的一部分)以及 UML 实用程序的 port-helper(请参阅下面的 xterm 通道信息)。在客户端连接之前,UML 不会启动。

  • pty 和 pts 通道 - 使用系统 pty/pts。

  • tty 通道 - 绑定到现有的系统 tty。示例:con1=/dev/tty8 将使 UML 使用主机的第 8 个控制台(通常未使用)。

  • xterm 通道 - 这是默认值 - 在此通道上启动一个 xterm 并将其 IO 指向它。请注意,为了使 xterm 工作,主机必须安装 UML 发行版软件包。这通常包含 port-helper 和 UML 与 xterm 通信所需的其他实用程序。或者,需要从源代码编译和安装这些实用程序。适用于控制台的所有选项也适用于在 UML 内部表示为 ttyS 的 UML 串行线路。

启动 UML

我们现在可以运行 UML。

# linux mem=2048M umid=TEST \
 ubd0=Filesystem.img \
 vec0:transport=tap,ifname=tap0,depth=128,gro=1 \
 root=/dev/ubda con=null con0=null,fd:2 con1=fd:0,fd:1

这将运行一个具有 2048M RAM 的实例,并尝试使用名为 Filesystem.img 的镜像文件作为根。它将使用 tap0 连接到主机。除了 con1 之外的所有控制台都将被禁用,控制台 1 将使用标准输入/输出,使其显示在启动它的同一终端中。

登录

如果在生成镜像时没有设置密码,则必须关闭 UML 实例,挂载镜像,chroot 到其中并进行设置 - 如生成镜像部分所述。如果已经设置了密码,则可以直接登录。

UML 管理控制台

除了使用普通的系统管理工具从“内部”管理镜像外,还可以使用 UML 管理控制台执行一些低级操作。UML 管理控制台是运行中的 UML 实例的内核的低级接口,有点像 i386 SysRq 接口。由于 UML 下有一个成熟的操作系统,因此比 SysRq 机制具有更大的灵活性。

您可以使用 mconsole 接口执行许多操作

  • 获取内核版本

  • 添加和删除设备

  • 停止或重启计算机

  • 发送 SysRq 命令

  • 暂停和恢复 UML

  • 检查 UML 内部运行的进程

  • 检查 UML 内部 /proc 状态

您需要 mconsole 客户端 (uml_mconsole),它是大多数 Linux 发行版中可用的 UML 工具包的一部分。

您还需要在 UML 内核中启用 CONFIG_MCONSOLE(在“常规设置”下)。当您启动 UML 时,您会看到类似这样的行

mconsole initialized on /home/jdike/.uml/umlNJ32yL/mconsole

如果在 UML 命令行上指定唯一的计算机 ID,例如 umid=debian,您将看到此内容

mconsole initialized on /home/jdike/.uml/debian/mconsole

该文件是 uml_mconsole 将用于与 UML 通信的套接字。使用 umid 或完整路径作为参数运行它

# uml_mconsole debian

或者

# uml_mconsole /home/jdike/.uml/debian/mconsole

您将获得一个提示符,您可以在其中运行以下命令之一

  • 版本

  • 帮助

  • halt

  • reboot

  • 配置

  • 移除

  • sysrq

  • 帮助

  • cad

  • 停止

  • 运行

  • 进程

  • 堆栈

version

此命令不带任何参数。它会打印 UML 版本

(mconsole)  version
OK Linux OpenWrt 4.14.106 #0 Tue Mar 19 08:19:41 2019 x86_64

这有一些实际用途。它是一个简单的空操作,可用于检查 UML 是否正在运行。它也是向 UML 发送设备中断的一种方式。UML mconsole 在内部被视为 UML 设备。

help

此命令不带任何参数。它会打印一个简短的帮助屏幕,其中包含支持的 mconsole 命令。

halt 和 reboot

这些命令不带任何参数。它们会立即关闭计算机,不会同步磁盘,也不会干净地关闭用户空间。因此,它们非常接近于崩溃计算机

(mconsole)  halt
OK

config

“config”向虚拟机添加新设备。大多数 UML 设备驱动程序都支持此功能。它带有一个参数,即要添加的设备,其语法与内核命令行相同

(mconsole) config ubd3=/home/jdike/incoming/roots/root_fs_debian22

remove

“remove”从系统中删除一个设备。它的参数只是要删除的设备的名称。该设备必须在驱动程序认为必要的任何意义上处于空闲状态。对于 ubd 驱动程序,删除的块设备不能被挂载、交换或以其他方式打开,而对于网络驱动程序,设备必须处于关闭状态

(mconsole)  remove ubd3

sysrq

此命令带有一个参数,它是一个字母。它调用通用内核的 SysRq 驱动程序,该驱动程序执行该参数所需的操作。请参阅您喜欢的内核树中Linux Magic 系统请求密钥黑客技术中的 SysRq 文档,以了解哪些字母有效以及它们的作用。

cad

这会在运行的镜像中调用 Ctl-Alt-Del 操作。最终执行什么操作取决于 init、systemd 等。通常情况下,它会重启机器。

停止

这会使 UML 进入循环读取 mconsole 请求的状态,直到收到 'go' mconsole 命令。这作为调试/快照工具非常有用。

开始

在被 'stop' 命令暂停后,这将恢复 UML。请注意,当 UML 恢复后,TCP 连接可能已超时,如果 UML 暂停很长时间,crond 可能会有点混乱,运行它之前没有执行的所有作业。

进程

这需要一个参数 - /proc 中要打印到 mconsole 标准输出的文件名

堆栈

这需要一个参数 - 进程的 pid 号。其堆栈将打印到标准输出。

高级 UML 主题

虚拟机之间共享文件系统

不要尝试简单地通过从同一文件启动两个 UML 来共享文件系统。这和从共享磁盘启动两台物理机是一样的。会导致文件系统损坏。

使用分层块设备

在两个虚拟机之间共享文件系统的方法是使用 ubd 块驱动程序的写入时复制 (COW) 分层功能。任何更改的块都存储在私有 COW 文件中,而读取则来自任一设备 - 如果请求的块在其内部有效,则来自私有设备,否则来自共享设备。使用此方案,大部分未更改的数据在任意数量的虚拟机之间共享,每个虚拟机都有一个包含其所做更改的小得多的文件。对于从大型根文件系统启动的大量 UML,这可以节省大量的磁盘空间。

共享文件系统数据也有助于提高性能,因为主机将能够使用更少的内存来缓存共享数据,因此 UML 磁盘请求将从主机的内存而不是其磁盘中提供。在多插槽 NUMA 机器上执行此操作时有一个主要的警告。在这种硬件上,使用共享主映像和 COW 更改运行多个 UML 实例可能会导致诸如来自过多的插槽间流量的 NMI 等问题。

如果你在这种高端硬件上运行 UML,请确保使用 taskset 命令将 UML 绑定到位于同一插槽上的一组逻辑 CPU,或者查看“调整”部分。

要向现有块设备文件添加写入时复制层,只需将 COW 文件的名称添加到相应的 ubd 开关

ubd0=root_fs_cow,root_fs_debian_22

其中 root_fs_cow 是私有 COW 文件,root_fs_debian_22 是现有的共享文件系统。COW 文件不需要存在。如果不存在,驱动程序将创建并初始化它。

磁盘使用情况

UML 支持 TRIM,它会将其磁盘映像文件中任何未使用的空间释放到底层操作系统。使用 ls -ls 或 du 验证实际文件大小非常重要。

COW 有效性。

对主映像的任何更改都将使所有 COW 文件失效。如果发生这种情况,UML 将不会自动删除任何 COW 文件,并且将拒绝启动。在这种情况下,唯一的解决方案是恢复旧映像(包括其上次修改时间戳),或者删除所有 COW 文件,这将导致重新创建它们。COW 文件中的任何更改都将丢失。

奶牛会哞叫 - uml_moo:将 COW 文件与其后备文件合并

根据你如何使用 UML 和 COW 设备,可能建议时不时地将 COW 文件中的更改合并到后备文件中。

执行此操作的实用程序是 uml_moo。其用法是

uml_moo COW_file new_backing_file

无需指定后备文件,因为该信息已在 COW 文件头中。如果你很偏执,请启动新的合并文件,如果你对它感到满意,请将其移动到旧的后备文件上。

uml_moo 默认创建一个新的后备文件作为安全措施。它还具有破坏性合并选项,该选项会将 COW 文件直接合并到其当前的后备文件中。这实际上仅在后备文件只有一个与之关联的 COW 文件时才可用。如果一个后备文件关联了多个 COW,则其中一个的 -d 合并将使所有其他 COW 失效。但是,如果你的磁盘空间不足,它很方便,并且它也应该比非破坏性合并明显更快。

uml_moo 与 UML 发行包一起安装,并且作为 UML 实用程序的一部分提供。

主机文件访问

如果你想从 UML 内部访问主机上的文件,你可以将其视为单独的机器,并从主机 nfs 挂载目录,或者使用 scp 将文件复制到虚拟机中。但是,由于 UML 在主机上运行,它可以像任何其他进程一样访问这些文件,并在虚拟机内部提供它们,而无需使用网络。这可以通过 hostfs 虚拟文件系统实现。使用它可以将主机目录挂载到 UML 文件系统中,并像在主机上一样访问其中包含的文件。

安全警告

如果 UML 镜像不带任何参数,Hostfs 将允许该镜像挂载主机文件系统的任何部分并写入。如果运行 UML,请始终将 hostfs 限制在特定的“无害”目录中(例如 /var/tmp)。如果 UML 作为 root 用户运行,这一点尤其重要。

使用 hostfs

首先,请确保 hostfs 在虚拟机内部可用,方法是

# cat /proc/filesystems

应该列出 hostfs。如果不是,则使用配置为其中的 hostfs 重新构建内核,或者确保 hostfs 作为模块构建并在虚拟机内部可用,并使用 insmod 加载它。

现在你需要做的就是运行 mount

# mount none /mnt/host -t hostfs

会将主机的 / 挂载到虚拟机的 /mnt/host 上。如果你不想挂载主机根目录,则可以使用 mount 的 -o 开关指定要挂载的子目录

# mount none /mnt/home -t hostfs -o /home

会将主机的 /home 挂载到虚拟机的 /mnt/home 上。

将 hostfs 用作根文件系统

可以使用 hostfs 从主机上的目录层次结构启动,而不是使用文件中的标准文件系统。首先,你需要该层次结构。最简单的方法是循环挂载现有的 root_fs 文件

#  mount root_fs uml_root_dir -o loop

你需要将 etc/fstab/ 的文件系统类型更改为“hostfs”,以便该行看起来像这样

/dev/ubd/0       /        hostfs      defaults          1   1

然后,你需要将该目录中 root 拥有的所有文件 chown 为你自己的用户。这对我有用

#  find . -uid 0 -exec chown jdike {} \;

接下来,请确保你的 UML 内核已编译 hostfs,而不是作为模块。然后使用指向该目录的启动设备运行 UML

ubd0=/path/to/uml/root/directory

然后,UML 应该像往常一样启动。

Hostfs 警告

Hostfs 不支持跟踪主机上主机文件系统的更改(UML 外部)。因此,如果某个文件在 UML 不知情的情况下被更改,UML 将不会知道它,并且其自己的文件内存中缓存可能已损坏。尽管可以修复此问题,但目前尚未进行相关工作。

调整 UML

目前的 UML 严格来说是单处理器。但是,它将启动多个线程来处理各种功能。

UBD 驱动程序、SIGIO 和 MMU 模拟就是这样做的。如果系统空闲,这些线程将被迁移到 SMP 主机上的其他处理器。不幸的是,由于内核之间的所有缓存/内存同步流量,这通常会导致性能降低。因此,UML 通常会受益于固定在单个 CPU 上,尤其是在大型系统上。这可能会在某些基准测试中导致 5 倍或更高的性能差异。

同样,在大型多节点 NUMA 系统上,如果 UML 的所有内存都从它将运行的同一 NUMA 节点分配,则 UML 将受益。操作系统默认不会这样做。为了做到这一点,系统管理员需要创建一个适合绑定到特定节点的 tmpfs ramdisk,并通过在 TMP 或 TEMP 环境变量中指定它,将其用作 UML RAM 分配的源。UML 将查看 TMPDIRTMPTEMP 的值。如果失败,它将查找挂载在 /dev/shm 下的 shmfs。如果其他一切都失败,则无论其使用的文件系统类型如何,都使用 /tmp/

mount -t tmpfs -ompol=bind:X none /mnt/tmpfs-nodeX
TEMP=/mnt/tmpfs-nodeX taskset -cX linux options options options..

为 UML 做出贡献并使用 UML 进行开发

UML 是开发新的 Linux 内核概念(文件系统、设备、虚拟化等)的绝佳平台。它提供了无与伦比的机会来创建和测试它们,而无需局限于模拟特定的硬件。

例如 - 想尝试一下 Linux 如何使用 4096 个“适当的”网络设备工作?

UML 没有问题。与此同时,这对于其他虚拟化软件包来说是困难的 - 它们受到它们试图模拟的硬件总线上允许的设备数量的限制(例如 qemu 中的 PCI 总线上的 16 个)。

如果您有任何贡献,例如补丁、bug修复、新功能,请发送至 [email protected]

请遵循所有标准的 Linux 补丁指南,例如抄送相关的维护人员,并在您的补丁上运行 ./scripts/checkpatch.pl。有关更多详细信息,请参阅 Documentation/process/submitting-patches.rst

请注意 - 邮件列表不接受 HTML 或附件,所有电子邮件必须格式化为纯文本。

开发总是与调试密切相关。首先,您始终可以在 gdb 下运行 UML,稍后会有一个完整的章节介绍如何执行此操作。然而,这并不是调试 Linux 内核的唯一方法。通常,添加跟踪语句和/或使用 UML 特有的方法(例如 ptracing UML 内核进程)会提供更多信息。

跟踪 UML

运行时,UML 由一个主内核线程和多个辅助线程组成。对于跟踪而言,感兴趣的不是那些已经被 UML 作为其 MMU 模拟一部分进行 ptraced 的线程。

这些通常是在 ps 显示中可见的前三个线程。PID 号最低且 CPU 使用率最高的线程通常是内核线程。其他线程是磁盘 (ubd) 设备辅助线程和 SIGIO 辅助线程。在此线程上运行 ptrace 通常会产生以下画面

host$ strace -p 16566
--- SIGIO {si_signo=SIGIO, si_code=POLL_IN, si_band=65} ---
epoll_wait(4, [{EPOLLIN, {u32=3721159424, u64=3721159424}}], 64, 0) = 1
epoll_wait(4, [], 64, 0)                = 0
rt_sigreturn({mask=[PIPE]})             = 16967
ptrace(PTRACE_GETREGS, 16967, NULL, 0xd5f34f38) = 0
ptrace(PTRACE_GETREGSET, 16967, NT_X86_XSTATE, [{iov_base=0xd5f35010, iov_len=832}]) = 0
ptrace(PTRACE_GETSIGINFO, 16967, NULL, {si_signo=SIGTRAP, si_code=0x85, si_pid=16967, si_uid=0}) = 0
ptrace(PTRACE_SETREGS, 16967, NULL, 0xd5f34f38) = 0
ptrace(PTRACE_SETREGSET, 16967, NT_X86_XSTATE, [{iov_base=0xd5f35010, iov_len=2696}]) = 0
ptrace(PTRACE_SYSEMU, 16967, NULL, 0)   = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=16967, si_uid=0, si_status=SIGTRAP, si_utime=65, si_stime=89} ---
wait4(16967, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP | 0x80}], WSTOPPED|__WALL, NULL) = 16967
ptrace(PTRACE_GETREGS, 16967, NULL, 0xd5f34f38) = 0
ptrace(PTRACE_GETREGSET, 16967, NT_X86_XSTATE, [{iov_base=0xd5f35010, iov_len=832}]) = 0
ptrace(PTRACE_GETSIGINFO, 16967, NULL, {si_signo=SIGTRAP, si_code=0x85, si_pid=16967, si_uid=0}) = 0
timer_settime(0, 0, {it_interval={tv_sec=0, tv_nsec=0}, it_value={tv_sec=0, tv_nsec=2830912}}, NULL) = 0
getpid()                                = 16566
clock_nanosleep(CLOCK_MONOTONIC, 0, {tv_sec=1, tv_nsec=0}, NULL) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_value={int=1631716592, ptr=0x614204f0}} ---
rt_sigreturn({mask=[PIPE]})             = -1 EINTR (Interrupted system call)

这是来自一个大多空闲的 UML 实例的典型画面。

  • UML 中断控制器使用 epoll - 这是 UML 等待 IO 中断的情况

    epoll_wait(4, [{EPOLLIN, {u32=3721159424, u64=3721159424}}], 64, 0) = 1

  • ptrace 调用的序列是 MMU 模拟和运行 UML 用户空间的一部分。

  • timer_settime 是 UML 高分辨率定时器子系统的一部分,它将 UML 内部的定时器请求映射到主机高分辨率定时器。

  • clock_nanosleep 是 UML 进入空闲状态(类似于 PC 执行 ACPI 空闲的方式)。

正如您所看到的,即使在空闲状态下,UML 也会生成相当多的输出。当观察 IO 时,输出会非常有用。它显示了实际的 IO 调用、它们的参数和返回值。

内核调试

您现在可以在 gdb 下运行 UML,尽管它不一定会同意在 gdb 下启动。如果您试图跟踪运行时 bug,最好将 gdb 附加到正在运行的 UML 实例并让 UML 运行。

假设与前一个示例中的 PID 号相同,这将是

# gdb -p 16566

这将停止 UML 实例,因此您必须在 GDB 命令行中输入 cont 来请求它继续运行。最好将其制作成 gdb 脚本并作为参数传递给 gdb。

开发设备驱动程序

几乎所有的 UML 驱动程序都是单片的。虽然可以将 UML 驱动程序构建为内核模块,但这会将可能的功能限制为仅限内核且非 UML 特有的功能。原因是,为了真正利用 UML,需要编写一段用户空间代码,将驱动程序概念映射到实际的用户空间主机调用。

这构成了驱动程序的所谓“用户”部分。虽然它可以重用许多内核概念,但它通常只是另一段用户空间代码。此部分需要一些匹配的“内核”代码,这些代码驻留在 UML 映像内部,并实现 Linux 内核部分。

注意: “内核”和“用户”之间的交互方式几乎没有限制.

UML 没有严格定义的内核到主机 API。它不尝试模拟特定的架构或总线。UML 的“内核”和“用户”可以共享内存、代码并根据需要进行交互,以实现软件开发人员所想的任何设计。唯一的限制纯粹是技术上的。由于许多函数和变量具有相同的名称,因此开发人员应注意他们尝试引用的 include 和库。

因此,许多用户空间代码由简单的包装器组成。例如,os_close_file() 只是对 close() 的包装,它确保用户空间函数 close 不会与内核部分中类似命名的函数发生冲突。

将 UML 用作测试平台

UML 是设备驱动程序开发的绝佳测试平台。与大多数 UML 事物一样,“可能需要进行一些用户组装”。构建仿真环境取决于用户。目前,UML 仅提供内核基础结构。

此基础结构的一部分是能够加载和解析 Arm 或 Open Firmware 平台中使用的 fdt 设备树 blob。这些作为内核命令行的可选附加参数提供

dtb=filename

设备树在启动时加载和解析,驱动程序可以通过查询它来访问。目前,此设施仅用于开发目的。UML 自己的设备不会查询设备树。

安全注意事项

驱动程序或任何新功能应默认为不接受任意文件名、bpf 代码或其他可能从 UML 实例内部影响主机的参数。例如,在 UML 命令行中指定用于驱动程序和主机之间 IPC 通信的套接字在安全方面是没问题的。允许将其作为可加载的模块参数则不然。

如果特定应用程序需要此类功能(例如,为原始套接字网络传输加载 BPF“固件”),则应默认关闭,并且应在启动时作为命令行参数显式打开。

即使考虑到这一点,UML 和主机之间的隔离级别也相对较弱。如果允许 UML 用户空间加载任意内核驱动程序,攻击者可以使用它来突破 UML 的限制。因此,如果在生产应用程序中使用 UML,建议在启动时加载所有模块,并在之后禁用内核模块加载。