虚拟化技术

七月 18, 2025 更新: 八月 16, 2026 [虚拟化与容器] #qemu #kvm #虚拟化

虚拟化技术

KVM 环境检查

# Intel CPU
grep "vmx" /proc/cpuinfo

# AMD CPU
grep "svm" /proc/cpuinfo

# 检查 kvm 模块
lsmod | grep kvm

KVM 环境目录:/etc/libvirt/、/var/lib/libvirt/

虚拟机磁盘管理

创建虚拟磁盘

# 文件方式
qemu-img create -f qcow2 /var/lib/libvirt/images/vm.qcow2 10G

# libvirt 存储池方式
virsh vol-create-as vmfspool vm.img 10G --format qcow2

扩缩容

qemu-img resize /path/vm.qcow2 +10G
qemu-img check /path/vm.qcow2

热添加/移除磁盘

virsh attach-disk VM1 /data/vmfs/disk.qcow2 vdb --subdriver=qcow2
virsh detach-disk VM1 /data/vmfs/disk.qcow2

若报 No more available PCI slots:pc-i440fx 机器 PCI 槽位有限(31 个),每个 virtio-blk 设备占 1 个,需通过 PCI bridge 扩展。

镜像格式对比

格式特点
raw裸格式,无元数据,不支持快照
qcow2主流格式,支持 COW、多快照、zlib 压缩、AES 加密

增量镜像

qemu-img create -b base.qcow2 -f qcow2 child.qcow2  # 基于 base 创建 overlay

格式转换

qemu-img convert -f raw -O qcow2 vm.raw vm.qcow2

存储池管理

# 创建本地目录存储池
virsh pool-define-as vmfspool --type dir --target /data/vmfs
virsh pool-build vmfspool
virsh pool-start vmfspool
virsh pool-autostart vmfspool

# 查看
virsh pool-list --all
virsh pool-info vmfspool

# 删除(先 destroy 再 undefine)
virsh pool-destroy vmfspool
virsh pool-undefine vmfspool

虚拟机管理

virsh list --all                # 列出所有虚拟机
virsh start VM1                 # 启动
virsh suspend VM1               # 挂起
virsh resume VM1                # 恢复
virsh destroy VM1               # 强制关机
virsh dumpxml VM1               # 导出配置
virsh edit VM1                  # 编辑配置

# 克隆
virt-clone -o VM1 -n vm-clone --auto-clone

# 快照
virsh snapshot-create-as VM1 snap1
virsh snapshot-revert VM1 snap1

退出 virsh console:Ctrl+](英文键盘 Ctrl+Shift+])。

headless 虚拟机(无 GUI 安装与配置)

安装前提:用户权限

sudo usermod -aG kvm peter      # virt-install 以用户身份探测 /dev/kvm,需在 kvm 组
# polkit 授权(SSH 会话无图形代理,virsh 连 qemu:///system 会报
# "身份验证不可用:没有可以验证操作 'org.libvirt.unix.manage' 的 polkit 代理")
# /etc/polkit-1/rules.d/49-libvirt.rules:
polkit.addRule(function(action, subject) {
  if (action.id == "org.libvirt.unix.manage" && subject.user == "peter") {
    return polkit.Result.YES;
  }
});
sudo systemctl restart polkit.service

# 非 root 用户 virt-install 默认走 qemu:///session(找不到 default 网络),务必指定:
export LIBVIRT_DEFAULT_URI=qemu:///system

preseed 自动安装(串口无交互)

# 提取 d-i 内核(注意 Debian netinst ISO 布局是 install.amd,不是 install.amd64)
sudo mount -o loop,ro debian-13.6.0-amd64-netinst.iso /mnt/iso-loop
mkdir -p /mnt/nvme0n1p10/vm-iso/install.amd64
cp /mnt/iso-loop/install.amd/{vmlinuz,initrd.gz} /mnt/nvme0n1p10/vm-iso/install.amd64/

# HTTP 分发 preseed(比 --initrd-inject 可靠:HTTP 日志可确认 d-i 确实取到了文件)
python3 -m http.server 8000 --directory /mnt/nvme0n1p10/vm-iso --bind 0.0.0.0 &

virt-install \
  --name debian13-vm --memory 4096 --vcpus 4 \
  --disk path=/dev/nvme0n1p8,bus=virtio,format=raw \
  --network network=default,model=virtio \
  --graphics none \
  --console pty,target_type=serial \
  --channel unix,target_type=virtio,name=org.qemu.guest_agent \
  --location /mnt/nvme0n1p10/vm-iso/debian-13.6.0-amd64-netinst.iso \
  --os-variant debiantrixie \
  --extra-args "auto console=ttyS0,115200n8 preseed/url=http://192.168.122.1:8000/preseed.cfg" \
  --force --noautoconsole

preseed 踩坑:键盘问题没答全会卡在交互主菜单(console-setup/ask_detect false + console-setup/layoutcode us + keyboard-configuration/xkb-keymap us 三件套都要);--extra-args 只支持 --location(--cdrom 会报 "Kernel arguments are only supported with location")。

串口 console 登录

# /etc/default/grub:
GRUB_CMDLINE_LINUX="console=ttyS0,115200n8"
GRUB_TERMINAL=serial
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
sudo update-grub
sudo systemctl enable serial-getty@ttyS0   # 有 console=ttyS0 时 getty-generator 也会自动生成

验证:virsh console <vm> 出现 debian login:。注意 pty 属 root,非交互抓取要 sudo;virsh console 只显示连接后的输出,登录提示是静态的。

离线修改镜像(virt-customize,无需客户机密码)

忘记/没有客户机密码时,关机后在宿主机直接改 qcow2:

sudo apt install libguestfs-tools
sudo virt-customize -a /var/lib/libvirt/images/debian.qcow2 \
  --run-command "usermod -aG sudo peter" \
  --run-command "apt-get install -y qemu-guest-agent" \
  --run-command "systemctl enable qemu-guest-agent" \
  --upload /tmp/vm-grub:/etc/default/grub \
  --run-command "update-grub"

踩坑:--run "bash file" 报文件不存在(上传与执行环境问题),用 --run-command 逐个执行;命令含引号会被命令行层吃掉(GRUB_SERIAL_COMMAND="serial --speed..." 需整体 --upload 覆盖 /etc/default/grub);DVD 安装的镜像 sources.list 只有 cdrom 源,先换成网络源才能装包。

qemu-guest-agent

virsh qemu-agent-command debian '{"execute":"guest-ping"}'
virsh qemu-agent-command debian '{"execute":"guest-network-get-interfaces"}'
virsh qemu-agent-command debian '{"execute":"guest-get-osinfo"}'

Wi-Fi 宿主的 VM 网络:macvtap 失败 → proxy-ARP 路由方案(2026-08-16 完整排查)

场景:宿主 Debian 笔记本(Wi-Fi 接入家用路由器),VM 需要成为局域网成员,让有线侧设备能 ssh 直达。

迭代一:macvtap bridge 直连 —— 假阳性陷阱

sudo modprobe macvtap
# <interface type='direct'><source dev='wlan0' mode='bridge'/><model type='virtio'/></interface>
virsh attach-device debian /tmp/macvtap-iface.xml --live --config
# 踩坑:virsh attach-interface ... --source-mode bridge 报"无效的源模式"(所有 mode 值都被拒),
#       必须走 attach-device + XML;Wi-Fi 上 DHCP 无响应(路由器只给关联客户端发 offer),需静态 IP

给 VM 配静态 IP 192.168.2.200 后测试"成功"——ping 通、SSH 通。但仔细一看:SSH 返回的 hostname 是 xiao、还带着 tailscale IP(100.64.0.2)——那是占着 .200 的另一台设备!

教训一(假阳性陷阱):配置静态 IP 前必须扫描占用;验证连通性时核对 hostname/SSH 指纹,不能只看"通了"——IP 冲突时你连的可能是别人。本案例里 VM 从一开始就没被局域网访问到过,后续所有"冲突导致封锁"的猜测都建立在这个假阳性上。

迭代二:换 IP / 换 MAC —— 仍然不通

把 VM 改成 192.168.2.160,再换 VM 的 MAC(52:54:00:aa:bb:01 → .02,需 destroy + start 才会重载域定义),均不通。由此排除"冲突触发的 AP 临时封锁"(与 VM 的 MAC/IP 无关)。

双端抓包定位(关键方法论)

同时在两端抓包,一次实验看清全貌:

观测点实测结果结论
宿主 tcpdump -i wlan0本机(.100)的 ARP 广播到达,VM 应答(request+reply 完整交互)出方向正常,广播能进来
本机 tcpdump -i vmbr0只看到自己发出的 ARP 请求,应答从未到达;同时刻其他设备的 ARP 全部正常丢包发生在两者之间
对照:.150(另一无线设备)的 ARP 广播宿主 wlan0 能收到广播转发本身没问题,排除"AP 封锁宿主站点"
VM 内 ip neigh(guest-exec 执行)能解析出本机和网关的 MAC(STALE)VM 出方向的广播 ARP 被转发、回包也到了

中间被证伪的两个错误理论(记录备查):

  1. "包到了宿主被本机配置丢弃"——wlan0 抓包证明 ARP 请求到达了宿主,VM 的应答在宿主侧也正常发出;丢的是返回方向。
  2. "AP 封锁了宿主的无线站点"——对照实验发现其他无线设备的广播能到达宿主,广播转发是通的;且换 VM MAC 后依旧,封锁与 VM MAC 无关。

根因:802.11 无 4-address 模式,AP 丢弃站点代发的"外来源 MAC"单播

双端证据闭环:VM 的单播应答在宿主的 wlan0 上发出(TX 抓到),但从未出现在本机的网卡上(RX 抓到);同时刻其他所有设备的单播交互都正常。

原理(802.11 帧结构):

空中 802.11 数据帧:
┌──────────────┬──────────────┬──────────────┬──────────────────────┐
│ 地址1 RA      │ 地址2 TA      │ 地址3 SA/DA   │ 帧体:封装的以太网帧    │
│ (AP 的 MAC)   │ (宿主站点 MAC)│              │ 以太网头:源=VM 的 MAC  │
└──────────────┴──────────────┴──────────────┴──────────────────────┘

迭代三:ipvlan 思路 → proxy-ARP + 路由(最终方案)

思路:VM 不能用 ipvlan 设备(QEMU 需要独立 virtio MAC,ipvlan 是主机侧 L3 设备)。但 ipvlan 的核心——"共享宿主机 MAC"——可以等价实现:让 VM 的所有流量经宿主的 L3 协议栈转发,出站帧的源 MAC 一律是宿主的,AP 校验必然通过。

VM (enp1s0: 192.168.122.160/24 + 192.168.2.160/32)
   │ 默认路由 via 192.168.122.1
   ▼
宿主 ──virbr0── (L3 转发) ──wlan0── AP ── 局域网
   ▲                                    │
   └─ proxy_arp 应答"谁是 .160"(用 wlan0 的 MAC)

VM 侧配置(NM 配置直接写 .nmconnection 文件,绕开 nmcli 参数转义;需 chmod 600):

[ipv4]
method=manual
address1=192.168.122.160/24,192.168.122.1
address2=192.168.2.160/32,
dns=192.168.122.1;

宿主侧配置(持久化:/etc/sysctl.d/99-vm-proxy-arp.conf + NM dispatcher 脚本):

# 路由:把 .160 引到 VM(经 virbr0)
ip route add 192.168.2.160/32 via 192.168.122.160 dev virbr0
# proxy_arp:局域网问"谁是 .160"时宿主用自己 wlan0 的 MAC 应答
echo 1 | tee /proc/sys/net/ipv4/conf/wlan0/proxy_arp
# iptables:libvirt 的 FORWARD 链只放行 192.168.122.0/24,.160 会被 REJECT(icmp-port-unreachable)
iptables -I LIBVIRT_FWI 1 -d 192.168.2.160 -o virbr0 -j ACCEPT
iptables -I LIBVIRT_FWO 1 -s 192.168.2.160 -i virbr0 -j ACCEPT

ARP 流程(核心,理解了就理解整个方案):

验证:ping 192.168.2.160 通(ttl 63,经宿主路由);ssh peter@192.168.2.160 直达 VM。

边界情况:VM 关机时宿主仍会替 .160 应答 ARP(路由还在),virbr0 上 ARP .122.160 失败导致包在宿主丢弃(表现"ARP 能通但无响应");VM 改 IP 需同步更新宿主路由和 iptables;IPv6 需另行配置 NDP 代理。

本次教训

  1. 假阳性陷阱:验证连通性必须核对 hostname/指纹,IP 冲突场景下"通了"可能是别人的机器
  2. 双端抓包方法论:丢包定位要在路径两端同时抓,配合"同时刻其他设备流量正常"的对照组,才能把丢包点钉死在某个区间(这次是宿主的 TX 抓到、本机的 RX 抓不到,锁死 AP)
  3. 让用户参与质疑有价值:两轮"AP 封锁/宿主丢包"的错误理论都是被用户的追问逼着做对照实验推翻的,避免了一条道走到黑
  4. Wi-Fi 桥接的限制:普通 AP 上别指望 macvtap/网桥能让 VM 以独立 MAC 出现在局域网,除非路由器支持 WDS/4-address;"共享宿主 MAC + L3 转发"是通用出路

QEMU 用户态 vs 系统级模拟

qemu-riscv64qemu-system-riscv64
模式用户态模拟(User mode)系统级模拟(System mode)
功能运行单个可执行 ELF模拟完整裸机系统
系统调用转换为宿主系统调用需要运行完整内核

用户态模拟通过将 RISC-V Linux 系统调用转换为 x86-64 系统调用来执行单个应用。

JOS 相关内容(IDT/GDT/LDT、PLT/GOT、内核调试、页表与 CR3、汇编器语法)已拆分为独立文章:JOS 学习笔记。