在QEMU虚拟机和宿主机之间传输文件,这件事说难不难,说简单也确实有点绕。很多人是从VMware或VirtualBox转过来的,习惯了“共享文件夹”“拖拽复制”这种开箱即用的功能,一到QEMU这儿就发现,命令行启动的虚拟机居然连个图形化的文件共享入口都没有,顿时就懵了。
我第一次用QEMU的时候也踩过这个坑。当时用-net user起了个arm64的虚拟机,系统能跑起来,但想把一个编译好的二进制塞进去,竟然找不到一个合理的方法。试过在宿主机上起HTTP服务,用wget去拉,能行,但总觉得太“野路子”;后来试了9p,结果权限、挂载参数折腾了半天。直到我把virtiofs、9p、网络传输、磁盘镜像这几条路全部走了一遍,才算彻底搞明白QEMU的文件传输到底该怎么做。
这篇文章我就把QEMU宿主机和虚拟机之间传文件的几种主流方案、底层原理、实操命令、还有我踩过的那些坑,一次性讲清楚。不管你是跑x86还是模拟arm64,是Linux虚拟机还是Windows虚拟机,都能在里面找到适合你的方案。
1. 方案选型:在开始传文件之前,先想清楚这三件事
很多人一上来就问“QEMU怎么共享文件夹”,但实际操作下来,选什么传输方案,取决于另外三个问题:你的虚拟机和宿主机之间是什么网络模式?虚拟机的操作系统是什么?你要传的是大文件还是小文件、是偶尔传一次还是频繁双向同步?
这三个问题的答案,直接决定你用下面哪一种方案。为了让你在后续阅读时不迷路,先把我用过的几条路线做个整体对比:
| 传输方案 | 适用场景 | 传输方向 | 性能表现 | 配置复杂度 |
|---|---|---|---|---|
| virtiofs 共享目录 | Linux虚拟机,需要高性能双向共享 | 双向实时共享 | 接近原生磁盘性能 | 中 |
| 9p (virtio-9p) 共享目录 | Linux虚拟机,轻量级共享 | 双向实时共享 | 一般,小文件尚可 | 低 |
| 网络传输(SSH/SFTP/HTTP) | 所有系统,尤其是Windows虚拟机 | 双向,但依赖网络 | 取决于网络和虚拟网卡配置 | 低 |
| 磁盘镜像挂载(qemu-nbd/libguestfs) | 虚拟机已关机时批量读写 | 单向/双向(离线) | 取决于挂载方式 | 中 |
| ISO/虚拟光驱传文件 | 一次性、只读传输 | 仅宿主机到虚拟机 | 安装介质级别速度 | 低 |
如果你只是想在宿主机和Linux虚拟机之间快速交换几个配置文件,那我建议直接用网络方案,省事;如果你需要在虚拟机和宿主机之间做开发编译,代码目录共享那种,那virtiofs是正解;如果是Windows虚拟机,那就老老实实走网络共享或者Samba。
我自己最常用的组合是:日常小文件走SSH/SFTP,开发环境走virtiofs,关机后的镜像批量操作走qemu-nbd挂载。这三个方案覆盖了我90%以上的使用场景。下面我逐个详细拆解,每个方案都给出实测可用的命令和配置。
2. virtiofs共享目录:性能最强的实时共享方案
2.1 virtiofs的原理和选型理由
virtiofs是QEMU/KVM虚拟化环境下专门为文件共享设计的一套virtio设备,它和老的9p方案最大的区别在于:9p是网络文件系统协议的虚拟化适配,而virtiofs是专门为虚拟化场景从头设计的高性能共享文件系统,它利用了内核的FUSE(Filesystem in Userspace)机制,加上DAX(直接访问)支持,理论上能达到接近原生磁盘的读写性能。
我实测的感觉是,在编译大型项目时,virtiofs比9p明显快一个档次,特别是大量小文件读写的场景,virtiofs几乎感觉不到“这是一块网络盘”,而9p在同样的场景下会出现明显的延迟。
2.2 宿主机侧:启动QEMU时添加virtiofs配置
用virtiofs,首先在宿主机上启动QEMU的命令里要加上相应的参数。下面是我跑arm64 Ubuntu虚拟机时实际用过的启动参数片段:
qemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -m 4096 \ -smp 4 \ -drive file=ubuntu.img,format=raw,if=virtio \ -device virtio-net-pci,netdev=net0 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -chardev socket,id=char0,path=/tmp/virtiofs.sock \ -device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=myfs \ -object memory-backend-file,id=mem,size=4096M,mem-path=/dev/shm,share=on \ -numa node,memdev=mem这里有几个关键参数要特别注意:
-chardev socket,id=char0,path=/tmp/virtiofs.sock:创建一个socket chardev,用于宿主机和虚拟机之间的virtiofs通信。-device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=myfs:这才是virtiofs设备本体。tag是挂载标签,后面虚拟机里挂载时会用到,可以自己取名字。-object memory-backend-file,id=mem,size=4096M,mem-path=/dev/shm,share=on:这是virtiofs能跑起来的关键中的关键,必须配共享内存后端,而且size要和-m指定的内存大小一致,share=on表示这块内存是共享的,允许传给vhost-user进程。
我当初第一次配置virtiofs时,漏了-object memory-backend-file和-numa node,memdev=mem这两行,结果虚拟机启动后根本看不到virtiofs设备,日志里报vhost_user_fs: Failed to start vhost-user backend。这个坑我必须先给你标出来。
2.3 虚拟机侧:挂载virtiofs目录
虚拟机的内核需要支持virtiofs,现代发行版(内核版本4.20以上基本都涵盖了)默认开启。启动虚拟机后,在虚拟机内部执行:
sudo mkdir -p /mnt/shared sudo mount -t virtiofs myfs /mnt/shared这里的myfs就是宿主机侧启动参数里tag=myfs设置的标签,两边必须一致。
挂载成功后,/mnt/shared就是宿主机和虚拟机之间的共享目录了。注意,virtiofs默认的映射方式是把宿主机根目录或指定目录“暴露”给虚拟机,具体暴露哪个目录由宿主机端的vhost-user-fs后端程序决定——通常你需要在宿主机上先运行一个virtiofsd守护进程,指定要共享的目录。比如宿主机上执行:
sudo virtiofsd --socket-path=/tmp/virtiofs.sock --shared-dir=/home/user/shared -o cache=auto--shared-dir就是你要共享给虚拟机的真实目录。如果你没启动virtiofsd,虚拟机里挂载virtiofs肯定会失败,这是很多人弄了半天没反应的原因。我建议把virtiofsd的启动命令和QEMU的启动命令一起写进脚本里,避免忘记。
2.4 virtiofs的权限和缓存问题
- 权限:virtiofs默认把宿主机上的文件权限直接映射到虚拟机里,如果你在宿主机上用普通用户创建的文件,在虚拟机里看到的是同一个UID/GID。如果两边用户ID不一致,可能会遇到权限不够的问题。最简单的办法:在宿主机和虚拟机上使用相同的用户名和UID。
- 缓存:
-o cache=auto模式下,virtiofs会积极缓存文件数据,写入操作会先落内存,后面再刷盘。如果你在宿主机侧修改了共享文件,虚拟机里可能不会立即看到变化。遇到这种“文件明明改了,虚拟机里却看不到”的情况,要么在宿主机侧执行sync,要么在挂载时直接-o cache=none禁用缓存,代价是性能会有一定下降。
提示:如果你是在QEMU里跑arm64虚拟机,而宿主机是x86_64,virtiofs的CPU架构差异不影响挂载,文件内容本身没有架构限制,放心用。
3. 9p共享目录:轻量灵活,小文件传输够用
3.1 9p方案的设计思路
9p是Plan 9操作系统遗留下来的网络文件系统协议,QEMU通过virtio-9p设备把它带到了虚拟化场景。和virtiofs相比,9p配置更简单,不需要额外的vhost-user后端进程,QEMU自己就把文件系统共享这件事干了。
9p适合的场景是:轻度使用、偶尔传几个配置文件、不想维护额外守护进程。如果你上了生产环境或者频繁做大量小文件读写,9p性能确实有点捉急,建议直接上virtiofs。
3.2 宿主机侧:一行参数搞定共享
9p的启动参数比virtiofs简单得多,不需要额外的socket、不需要共享内存配置、不需要单独启动守护进程。下面是实测可用的最小启动参数:
qemu-system-x86_64 \ -m 2048 \ -drive file=ubuntu.qcow2,format=qcow2,if=virtio \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 \ -virtfs local,path=/home/user/shared,mount_tag=hostshare,security_model=passthrough,id=share0参数解释:
local:表示使用本地目录来提供共享,这是最常见的模式。path=/home/user/shared:宿主机上要共享的目录,建议用绝对路径。mount_tag=hostshare:这个tag就是虚拟机里挂载时使用的标签。security_model=passthrough:这个参数决定了权限处理方式,后面细说。id=share0:设备ID,可以随便取。
3.3 虚拟机侧:挂载与权限细节
虚拟机启动后,在虚拟机内执行:
sudo mkdir -p /mnt/host sudo mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/host这里和virtiofs有很大的区别:9p的mount_tag是hostshare,而不是宿主机目录名。很多新手直接把path=/home/user/shared里的路径名当tag用,结果挂载时报No such file or directory。
security_model这个参数是9p最容易踩坑的地方,三种常见取值的行为区别很大:
passthrough:文件在虚拟机里表现为宿主机上的实际UID/GID,权限以宿主机为准。这种方式最透明,但会出现虚拟机的root在共享目录里“权限不够”的现象,因为虚拟机的root看到的文件owner可能是宿主机上的普通用户。mapped-xattr:将虚拟机UID/GID映射存储为文件扩展属性,让每个虚拟机用户都有独立权限,但需要文件系统支持user_xattr挂载选项。none:不做权限映射,虚拟机所有用户对共享目录都有完全控制权限,安全性差一点,但最省事。
我在实际使用中,如果是单用户虚拟机,直接用security_model=passthrough就够了;如果需要多用户隔离,建议mapped-xattr,不过要注意共享目录所在文件系统必须支持扩展属性(ext4、xfs都支持)。
注意:用9p挂载共享目录后,在虚拟机里写文件时,如果宿主机的目录权限不足,会报
Permission denied,这时候不是虚拟机里加sudo就能解决的,要回到宿主机上调整目录权限。
3.4 9p方案的一个小坑:QEMU用户态网络下的限制
如果你是用-netdev user的QEMU用户态网络栈,同时挂载了9p,有时候会碰到虚拟机网络和挂载同时异常的情况。别怀疑是9p的问题,多数时候是QEMU内部对user网络和virtio-9p同时使用的兼容性bug,解决方法是把网络换成-netdev tap或-netdev bridge,或者顺序调整一下设备的加载顺序。我遇到过一次,把-virtfs参数调整到-netdev参数之前,问题就消失了,原因说不清,但实测有效。
4. 网络传输:最通用的文件交换方式
4.1 为什么网络传输是最通用方案
如果虚拟机和宿主机之间的网络已经通了,服务器领域,直接传文件才是最省心的方式,毕竟它不依赖任何共享文件系统,不受9p/virtiofs那些权限模型的限制,Windows虚拟机、macOS虚拟机、BSD虚拟机都能用。
QEMU默认的-netdev user模式已经内置了一个TFTP服务器和SMB服务器,但说实话那两个都难用。我推荐的是:宿主机上有SSH服务,虚拟机里用scp/sftp拉取;或者反过来,虚拟机里开SSH服务,宿主机直接连进去传文件。
4.2 方案一:虚拟机访问宿主机文件
这种场景更常见。宿主机上有一些文件要传到虚拟机里,你可以在宿主机上起一个临时的HTTP服务,虚拟机里用wget或者curl去下载。
宿主机上,在要分享的目录执行:
cd /home/user/share_files python3 -m http.server 8000虚拟机里面执行:
wget http://10.0.2.2:8000/yourfile.tar.gz注意10.0.2.2这个地址,这是QEMU用户态网络栈里给宿主机保留的网关地址。很多人第一次用QEMU用户网络时,都好奇虚拟机里怎么访问宿主机,答案就是10.0.2.2。
这个方法本质上不需要启动SSH,也不需要配置Samba,起一个HTTP服务就完事了,特别适合一次性传几个文件,传完就关掉服务。
4.3 方案二:宿主机访问虚拟机文件
反过来,你想把虚拟机里的东西拷回宿主机,最优雅的方式是在虚拟机里安装并启动SSH服务,然后用hostfwd把22端口映射到宿主机的一个本地端口。
虚拟机里执行:
sudo apt install openssh-server sudo systemctl enable --now ssh启动QEMU时已经加过端口转发参数的话:
-netdev user,id=net0,hostfwd=tcp::2222-:22然后宿主机就可以用scp/sftp连接了:
scp -P 2222 user@localhost:/home/user/vm_file.txt . sftp -P 2222 user@localhost用-P 2222而不是默认端口,是因为我们把虚拟机的22端口映射到了宿主机的2222端口。注意scp的-P是大写,小写-p是保留文件属性的意思,别弄混了。
提示:如果你用的是Windows宿主机传文件到Linux虚拟机,可以用WinSCP或FileZilla连接
localhost:2222,本质上走的是同一个SSH协议,图形化界面比命令行更方便。
4.4 网络方案进阶:端口转发的局限与替代
hostfwd这个方案有一个局限性:它只能在宿主机上监听固定端口,再转发到虚拟机内部的服务。如果你想从另一台机器直接访问虚拟机里的服务,就没那么直接了,需要再加一层转发。
更强大的网络方案是用tap设备加网桥,把虚拟机直接放到宿主机所在的局域网里,虚拟机获得一个局域网内的真实IP,这样传文件就直接走局域网了,带宽和延迟都比hostfwd好。不过tap网络配置稍微复杂一点,需要root权限,还要配置网桥,我在这里不过多展开,如果你只是偶尔传文件,hostfwd完全够用。
5. 离线传输方案:用磁盘镜像挂载和ISO交换文件
5.1 qemu-nbd挂载虚拟机磁盘镜像
有时候虚拟机已经关机了,你想直接从它的磁盘镜像里读取或修改文件,比如虚拟机里的系统崩溃了需要紧急备份数据,这时候网络和共享目录都用不了,最好的办法就是用qemu-nbd把磁盘镜像挂载到宿主机上。
qemu-nbd是QEMU自带的网络块设备工具,可以把QEMU支持的各种磁盘镜像(qcow2、raw)暴露为Linux内核的nbd设备,然后直接像普通硬盘一样mount。
宿主机上需要先加载nbd内核模块:
sudo modprobe nbd max_part=8然后挂载镜像:
sudo qemu-nbd -c /dev/nbd0 /path/to/ubuntu.qcow2 sudo partprobe /dev/nbd0partprobe会重新读取分区表,让内核识别nbd0设备上的分区。如果你不确定虚拟机里分区结构,可以先看一下:
lsblk /dev/nbd0 sudo fdisk -l /dev/nbd0找到根分区(通常是/dev/nbd0p1或/dev/nbd0p2),然后挂载:
sudo mount /dev/nbd0p1 /mnt/vm_disk挂载完成后就能直接读写虚拟机里的文件了。操作完成后,一定要记得按顺序卸载:
sudo umount /mnt/vm_disk sudo qemu-nbd -d /dev/nbd05.2 用ISO镜像传文件
这个方法比较复古,但胜在通用性极强。把文件打包成ISO镜像,挂载为虚拟机的光驱,虚拟机里就能看到这些文件。对Windows虚拟机尤其好用,因为Windows虚拟机没有现成的virtiofs支持,网络配置又比较麻烦。
宿主机上把文件打包成ISO:
sudo apt install genisoimage genisoimage -o transfer.iso /path/to/files/启动QEMU时挂载ISO:
-drive file=transfer.iso,media=cdrom,if=virtio如果虚拟机已经在运行了,可以在QEMU monitor里动态挂载:
(qemu) change device eject cd0 (qemu) change device add transfer.iso虚拟机里就能看到光驱里的文件了。这个方法适合传比较大的文件包,而且因为是只读介质,虚拟机里的操作不会影响原文件,不用担心误改。
注意:用genisoimage打包ISO时,如果文件名包含中文或特殊字符,建议加上
-J -R参数生成Joliet和Rock Ridge扩展记录,否则Windows或Linux虚拟机里可能显示乱码。
5.3 libguestfs工具集:更安全的离线镜像操作
qemu-nbd直接挂载镜像有一个风险:如果操作不当,容易破坏虚拟机文件系统元数据。如果你对文件系统操作不够熟练,更推荐用libguestfs工具集,它是在用户态完成镜像操作的,不需要加载内核模块,也不会直接暴露块设备,安全性高很多。
安装libguestfs-tools后,你可以用guestfish或virt-copy-in/virt-copy-out来传文件:
# 把宿主机文件复制进虚拟机镜像 virt-copy-in -a ubuntu.qcow2 /home/user/hostfile.txt / # 把虚拟机镜像内文件复制出来 virt-copy-out -a ubuntu.qcow2 /etc/passwd /home/user/libguestfs底层用的是SELinux策略里放行的qemu进程完成的,属于官方推荐的镜像离线修改方案,比手动挂载nbd更不容易把镜像搞坏。我第一次用qemu-nbd直接挂载qcow2做实验时,因为虚拟机那个分区是LVM的,差点把数据弄丢,后来换成libguestfs才安心。
6. 常见问题与排查技巧实录
6.1 9p挂载报错:No such file or directory
这个错特别误导人,它不一定是你路径写错了,绝大多数情况是mount_tag对不上。注意QEMU启动参数里mount_tag=hostshare,虚拟机挂载时:
mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/hosthostshare这里必须是启动参数里mount_tag定义的值,而不是宿主机物理路径。如果你已经检查过tag没问题,再确认一下虚拟机内核是否支持9p:
grep 9p /proc/filesystems没有输出的话,就说明内核没编译9p支持,需要换内核或者使用其他方案。
6.2 virtiofs挂载后虚拟机里没反应
virtiofs的排查顺序很重要:
- 先确认宿主机上virtiofsd进程是否在运行,
ps aux | grep virtiofsd,没运行就启动。 - 确认QEMU启动参数里
-object memory-backend-file的size是否和-m大小一致,不一致会导致共享内存分配失败。 - 确认
/dev/shm是否有足够空间,内存大小别超过/dev/shm的容量。 - 确认虚拟机内核支持virtiofs:
grep virtiofs /proc/filesystems。 - 最后才是检查挂载参数里的tag是否一致。
顺序不能反,我遇到过几次都是virtiofsd忘了启动,导致虚拟机侧挂载时报transport endpoint not connected,这个报错很容易让人误以为网络问题,其实是后端进程没起来。
6.3 QEMU用户态网络下虚拟机ping不通宿主机
这个问题不算文件传输专属,但会连带影响网络传输方案。QEMU的user网络模式下,虚拟机里访问宿主机要用10.0.2.2,不是192.168.x.x。如果你在虚拟机里ping 10.0.2.2都ping不通,检查一下宿主机的防火墙是否拦截了来自TAP/TUN设备的流量,有些发行版默认防火墙会拦。
另外提醒一下,QEMU的user模式网络默认不支持ICMP(也就是ping不通宿主机网关),但TCP/UDP是通的,所以如果ping失败但wget/scp正常,别慌,这是正常的。
6.4 Windows虚拟机的文件传输方案推荐
Windows虚拟机的情况比较特殊,它不像Linux那样自带9p/virtiofs的内核支持。我用过的几种方法按推荐程度排序:
| 优先级 | 方案 | 说明 |
|---|---|---|
| 首选 | Samba共享网络磁盘 | 在Windows里通过\\10.0.2.2\share访问Linux宿主机的Samba共享,需要宿主机装Samba |
| 次选 | SSH/SFTP | Windows里用WinSCP连宿主机SSH端口,从宿主机拉取文件 |
| 备选 | ISO光驱 | 冷门但稳定,适合一次性传大量文件 |
| 不推荐 | virtiofs | 需要额外安装Windows的virtiofs驱动,折腾成本高 |
如果你在宿主机上配置了Samba,Windows虚拟机里访问路径是\\10.0.2.2\sharename,这个地址同样走QEMU的user网络网关。我第一次用的时候搞了半天,一直用宿主机的局域网IP去访问,结果访不到,后来想起user模式下就应该用10.0.2.2,一下就通了。
6.5 文件传输后的权限问题
经常有人遇到这种情况:用网盘方案或者共享目录往虚拟机里传了文件,但虚拟机的应用程序打不开,提示没有权限。根本原因是UID/GID映射不一致。
9p的passthrough模式下,宿主机文件owner的UID直接映射到虚拟机。比如宿主机文件是UID=1000的用户创建的,而虚拟机里登录用户UID是1000,但两者其实是不同用户,账号名可能一样也可能不一样。这种情况最简单的解决办法是:在宿主机上用chown把文件owner改成999或者其他数字,然后在虚拟机里也用chown改成对应UID。或者更省事一点,用security_model=none挂载,虚拟机里所有用户都能访问,不过安全性会降低。
6.6 传输大文件时性能很慢
如果你的方案是网络传输,传大文件很慢,多数原因是QEMUuser网络模式的虚拟网卡性能有限,还有可能宿主机和虚拟机之间走的是半虚拟化网卡(virtio-net)但CPU支持有问题。
几个提速方向:
- 启动参数加
-device virtio-net-pci而不是-net nic,半虚拟化网卡比模拟e1000快很多。 - 宿主机和虚拟机都确认网卡多队列(multi-queue)已开启,QEMU参数加
-smp 4和网卡的mq=on。 - 网络传输方案里,优先用scp而不是FTP,scp能吃到CPU的加密加速指令。
- 如果传的是超大文件,如几十GB的数据库备份,建议直接用qemu-nbd挂载镜像的离线方案,速度比网络快得多。
7. 综合推荐:不同场景下我用什么方案
到这里,几条路都讲完了。最后分享一套我自己在实际工作中的决策逻辑,你可以直接照着选。
场景:开发调试,代码在宿主机,虚拟机里编译运行用virtiofs。共享目录挂载到虚拟机的开发目录,宿主机改代码虚拟机立刻能看到。性能足够,不需要来回拷贝。
场景:临时传几个安装包或配置文件用HTTP服务加wget。宿主机起一个python3 -m http.server,虚拟机里直接下载,传完即关,零配置。或者反过来用scp,一条命令搞定。
场景:虚拟机里的数据要备份到宿主机如果虚拟机在运行,用scp/sftp从宿主机连接虚拟机的SSH服务,把需要备份的文件拉出来。如果虚拟机已经关机或者系统坏了,用libguestfs直接操作镜像。
场景:Windows虚拟机传文件宿主机配Samba共享,或者直接用WinSCP连宿主机SSH。别折腾virtiofs和9p,Windows驱动问题会让你怀疑人生。
场景:大批量文件,虚拟机处于关机状态libguestfs工具集,virt-copy-in和virt-copy-out,安全可靠,不用关心文件系统格式。
我个人在实际使用中最深刻的体会是:QEMU传文件没有“万能药”,不同的场景选择不同的方案,才能真正享受QEMU的灵活性。不要试图找一个像VMware共享文件夹那样一键搞定的东西,QEMU是模块化的,每一项能力都是独立组件,但也正因为如此,它在生产环境和嵌入式开发中的可定制性远超那些图形化虚拟机。
最后再分享一个小技巧:QEMU的-virtfs local参数可以多次添加,一次启动挂载多个共享目录。如果你有多个目录要共享,不用折腾什么新的工具,把所有-virtfs参数都写到QEMU启动脚本里,每个目录对应一个不同的mount_tag,虚拟机里分别挂载到不同位置就好了。这样一个QEMU实例就能同时支撑开发目录共享、数据目录共享、配置目录共享,互不干扰。