虚拟化技术
七月 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 被转发、回包也到了 |
中间被证伪的两个错误理论(记录备查):
- "包到了宿主被本机配置丢弃"——wlan0 抓包证明 ARP 请求到达了宿主,VM 的应答在宿主侧也正常发出;丢的是返回方向。
- "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 │
└──────────────┴──────────────┴──────────────┴──────────────────────┘
- 802.11 头里的地址 2(TA)是"物理发射者"身份,硬件写死为站点自己的 MAC;VM 的 MAC 只存在于帧体内的以太网头
- AP 拆掉 802.11 头后按以太网源 MAC 做转发决策:VM 的 MAC 在"已关联客户端表"里查无此站 → 单播帧丢弃;广播帧不校验源 MAC,所以 VM 的广播 ARP 能出去
- 宿主自己回包正常,是因为它的帧"源 MAC = 自己关联的 MAC",身份一致
- 解决之道是 WDS/4-address 模式(802.11 头加地址 4 承载原始源 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 流程(核心,理解了就理解整个方案):
- 局域网 → VM:本机广播"谁是 .160" → AP 转发广播 → 宿主 wlan0 收到 → 宿主查路由(有
/32 via 192.168.122.160)→ proxy-ARP 替答"is-at <宿主 wlan0 MAC>" → 本机把包填宿主 MAC 发出 → 宿主路由到 virbr0 → ARP .122.160 → VM 应答 → 送达 - VM → 局域网:VM 只有 .160/32 是本机,其余走默认路由 via 192.168.122.1 → 宿主收到(源 IP .160)→ 以自己身份在 wlan0 上 ARP 目标 → 转发,L2 源 MAC 重写为宿主自己的 → 本机收到"来自 .160、MAC 是宿主"的包,回包按 .160→宿主 MAC 的 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 代理。
本次教训
- 假阳性陷阱:验证连通性必须核对 hostname/指纹,IP 冲突场景下"通了"可能是别人的机器
- 双端抓包方法论:丢包定位要在路径两端同时抓,配合"同时刻其他设备流量正常"的对照组,才能把丢包点钉死在某个区间(这次是宿主的 TX 抓到、本机的 RX 抓不到,锁死 AP)
- 让用户参与质疑有价值:两轮"AP 封锁/宿主丢包"的错误理论都是被用户的追问逼着做对照实验推翻的,避免了一条道走到黑
- Wi-Fi 桥接的限制:普通 AP 上别指望 macvtap/网桥能让 VM 以独立 MAC 出现在局域网,除非路由器支持 WDS/4-address;"共享宿主 MAC + L3 转发"是通用出路
QEMU 用户态 vs 系统级模拟
qemu-riscv64 | qemu-system-riscv64 | |
|---|---|---|
| 模式 | 用户态模拟(User mode) | 系统级模拟(System mode) |
| 功能 | 运行单个可执行 ELF | 模拟完整裸机系统 |
| 系统调用 | 转换为宿主系统调用 | 需要运行完整内核 |
用户态模拟通过将 RISC-V Linux 系统调用转换为 x86-64 系统调用来执行单个应用。
JOS 相关内容(IDT/GDT/LDT、PLT/GOT、内核调试、页表与 CR3、汇编器语法)已拆分为独立文章:JOS 学习笔记。