我最初折腾QEMU,不是被启动参数难倒的,是被"传文件"这件事卡了整整一个下午。镜像装好了、系统能开机了,结果发现代码在宿主机上,虚拟机里却拿不到。想拖拽文件进去,没反应;开共享文件夹,不知道参数;问了一圈才知道,QEMU传文件根本不像VMware那样开箱即用。
这篇文章就把我在QEMU和宿主机之间传文件的完整实践经验总结出来,覆盖临时小文件、开发调试、大块数据迁移这几类真实需求。每个方案我都会讲清楚原理、适用场景、完整操作步骤和踩过的坑,照着做基本能一次跑通。
1. 先搞清楚你要传的是什么,再决定用哪种方案
很多人一上来就打开搜索引擎找"QEMU传文件命令",然后被一堆术语劝退。其实问题不在命令有多难,而是你压根没想清楚自己要传的东西属于哪一类。
1.1 三种典型需求:临时文件、开发调试、大块数据
我把日常需求粗暴分成三类。
第一类是临时小文件。比如从宿主机给虚拟机丢一个安装包、一份PDF、一个几十MB的日志。这类需求追求的是"省事",最好一条命令解决,不想为了传个文件去改虚拟机的启动配置。
第二类是开发调试场景。你正在写代码,或者跑编译任务,需要频繁在宿主机和虚拟机之间同步文件。这类需求追求的是"效率和自动同步",每次手动拷贝会疯掉,最好建一条稳定的双向通道。
第三类是大块数据。比如你要把几十GB的磁盘镜像、数据库备份、数据集搬进虚拟机,或者反过来从虚拟机里导出来。这类需求如果还走网络或者共享目录,速度会让人崩溃,而且动不动就中断,必须用更底层的办法。
1.2 方法全景图:先知道有哪些路可以走
QEMU传文件,本质上只有四条路:
- 共享目录:让虚拟机和宿主机共享同一个文件夹,类似VMware的共享文件夹功能,对应9p和virtiofs协议。
- 网络传输:把虚拟机当成一台普通机器,通过SSH、scp、rsync、HTTP等方式传文件,需要先配置好网络。
- 磁盘镜像操作:直接在宿主机层面操作虚拟机的磁盘镜像文件,适合大块数据和整个磁盘级别的迁移。
- 外设通道:模拟USB存储设备、剪贴板共享等,适用于少量特定场景。
每个方案都有它的甜点区,也有它的大坑。比如9p兼容性好但性能一般,virtiofs性能好但对内核有要求,网络传输方案灵活但需要先解决网络配置问题。下面我把每个方案都拆开了讲。
2. 共享目录实战:9p和virtiofs的选型、配置与排查
共享目录是大多数人最先想到的方案,因为物理机上我们早就习惯了"某个文件夹两边都能访问"这种模式。QEMU里这套东西做是能做,但很多人的第一反应是:"怎么挂不上去?"
2.1 两者的底层差异与性能差异
9p是Plan 9遗产,几十年前的设计,QEMU一直内置支持。它的好处是通用,Linux内核默认编译了9p的驱动模块,基本不用额外装东西。但9p协议设计得太早,没有针对现代硬件做过优化,大量小文件操作时延迟明显,遇到目录层级深一点的文件树,性能会断崖式下跌。
virtiofs是后起之秀,专门为虚拟机共享文件设计的半虚拟化方案。它把共享目录的一部分页缓存直接映射给虚拟机,所以大文件顺序读写的性能非常接近本地磁盘。我实测在同一个机器上跑内核编译,virtiofs比9p快了一倍不止,缓存命中后的随机读延迟也低很多。
但是,virtiofs有两个硬性前提:宿主机内核要支持virtiofsd(一般Linux发行版内核都自带,但需要额外安装virtiofsd这个守护进程),虚拟机内核也要支持virtiofs协议,而且启动参数要指定-fsdev和-mount tag。如果你是老内核,比如CentOS 7默认的3.10内核,virtiofs基本是没指望的,老老实实用9p。
2.2 命令行启动参数与挂载示例
先讲我实际用过的9p启动方式。假设宿主机上要共享的目录是/data/share,虚拟机挂载到/mnt/share,QEMU命令大致长这样:
qemu-system-x86_64 \ -m 4096 \ -smp 4 \ -drive file=/path/to/disk.qcow2,if=virtio \ -fsdev local,id=shared,path=/data/share,security_model=none,multidevs=remap \ -device virtio-9p-pci,fsdev=shared,mount_tag=hostshare \ -netdev user,id=net0 \ -device virtio-net-pci,netdev=net0解释几个关键参数:
security_model=none:宿主机不检查文件权限差异,所有文件以当前用户的权限呈现给虚拟机。这是最省事也最常用的方式,但要注意,如果你以root用户启动了QEMU,虚拟机里的用户就能拿到宿主机上root能读写的文件,有安全风险。multidevs=remap:这是个容易忽略的参数。如果共享目录里包含多个设备挂载点(比如你在/data/shares下面又挂了一个NFS),不设置multidevs=remap的话,9p可能因为inode冲突报"duplicate ownership"错误。加上这个参数后,QEMU会重新映射多个设备的inode,避免冲突。
虚拟机内部挂载命令:
sudo mkdir -p /mnt/share sudo mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/share注意挂载源是hostshare,对应的就是QEMU启动参数里的mount_tag,两边错了任何一个都挂不上。
如果要用virtiofs,命令稍有不同。首先确保宿主机装了virtiofsd:
# Debian/Ubuntu sudo apt install virtiofsd # RHEL/CentOS/Fedora sudo dnf install virtiofsd然后启动QEMU时用:
qemu-system-x86_64 \ -m 4096 -smp 4 \ -drive file=/path/to/disk.qcow2,if=virtio \ -chardev socket,id=char0,path=/tmp/virtiofs.sock \ -device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=hostshare \ -object memory-backend-file,id=mem,size=4096M,mem-path=/dev/shm,share=on \ -numa node,memdev=mem \ -netdev user,id=net0 \ -device virtio-net-pci,netdev=net0看到没,virtiofs除了要指定chardev和vhost-user-fs-pci之外,还要求虚拟机内存改成共享内存后端(memory-backend-file),否则vhost-user设备起不来。这个细节我当初折腾了很久,后来才在virtiofs官方文档里看到。虚拟机内部挂载:
sudo mkdir -p /mnt/share sudo mount -t virtiofs hostshare /mnt/share这里不需要指定trans=virtio,因为virtiofs本身就是标准virtio设备,挂载源同样是mount_tag。
2.3 virt-manager图形界面配置
如果不想记命令行参数,用virt-manager图形界面也能配共享目录。选中虚拟机,打开"显示细节"里的"IDE/SATA/USB控制器"或者"内存和CPU"旁边的"添加硬件"选项,找到"Filesystem"设备。
在device type里选virtiofs或9p,填写source path(宿主机目录)和target path(挂载点,即mount_tag)。virt-manager会自动把命令行参数拼出来,不用你手工处理。
但注意,你如果在virt-manager里选了virtiofs,它也强制要求虚拟机内存是共享内存。virt-manager一般会自动改,但如果之后你手动调整过内存设置,要把"共享内存"勾选上,不然启动虚拟机时会报"the memory backend must be shared"。
2.4 挂载失败排查:权限、缓存模式、SELinux
共享目录挂载失败,90%的情况是下面几个原因:
第一个是security_model选错导致虚拟机内读写权限异常。如果你用security_model=mapped-xattr,宿主机上的文件会被打上扩展属性来模拟权限,但很多文件系统(比如FAT、某些网络盘)不支持这些xattr,挂载后创建文件会报"Operation not supported"。实在搞不定权限,直接用security_model=none最省心。
第二个是SELinux捣乱。宿主机开了SELinux enforce模式,虚拟机通过9p/virtiofs访问的文件可能会被SELinux策略拦截,表现是"Permission denied"但权限明明是够的。临时验证就执行sudo setenforce 0,想长期解决就配置SELinux布尔值,或者干脆给共享目录加z标签:
sudo chcon -Rt svirt_sandbox_file_t /data/share第三种是缓存模式。9p默认的缓存策略对某些应用不友好,比如数据库类应用需要看到文件内容实时变化,不加缓存又会慢。9p支持cache=loose、cache=mmap等模式。我一般命令行直接加:
sudo mount -t 9p -o trans=virtio,version=9p2000.L,cache=loose hostshare /mnt/sharecache=loose不做强一致保证,但换来了吞吐量。如果你在虚拟机和宿主机两边同时频繁改同一个文件,建议别开loose,宁可慢一点,否则可能读到过期数据。
2.5 实际性能测试:别被"虚拟文件系统"骗了
我专门拿一个约8GB的小型代码仓库做过对比。9p模式下,git status在虚拟机里跑居然要40多秒,virtiofs模式下十几秒能出来,虽然还是比本地磁盘慢不少,但体感差异非常明显。如果是编译内核这种极端场景,9p会卡到让人怀疑机器是不是死机了。
所以我的建议是:能用virtiofs就用virtiofs,内核太老才退回到9p。如果你只是传几个配置文件,那这两个方案都足够,没必要纠结性能。
3. 走网络这条路:QEMU内的SSH、scp与rsync
共享目录虽然方便,但在某些场景下你会发觉它不够用。比如你想测试的是真实的网络环境,或者你用的是Windows虚拟机、BSD系统,9p和virtiofs不一定都能跑。这时候,把虚拟机当成一台普通机器,走网络传文件才是通用的做法。
3.1 用户网络与端口转发配置
QEMU默认的网络模式是用户模式(user mode),虚拟机的网卡通过QEMU内置的NAT访问外部网络。这种模式下,宿主机访问虚拟机,需要做端口转发。
启动虚拟机时,一般这样配置:
qemu-system-x86_64 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0hostfwd=tcp::2222-:22的意思是:宿主机上监听2222端口的所有TCP连接,全部转发到虚拟机网卡的22端口。这样你在宿主机上执行ssh -p 2222 user@localhost,就能连进虚拟机。
关键点:QEMU的用户模式网络自己的网段默认是10.0.2.0/24,虚拟机一般拿到的IP是10.0.2.15,网关是10.0.2.2,DNS是10.0.2.3。这些都可以修改,但默认值非常固定。如果虚拟机里ip addr看到的地址不是这个网段,说明你用了其他网络模式。
3.2 桥接网络与直接访问
如果你想让虚拟机和宿主机处于同一个局域网,虚拟机从路由器获取IP,宿主机之间可以直接用内网IP互访,那就用桥接模式。
创建桥接接口(Linux下通常用brctl或ip命令,管理麻烦一点),然后QEMU参数改为:
qemu-system-x86_64 \ -netdev bridge,id=net0,br=br0 \ -device virtio-net-pci,netdev=net0前提是宿主机的物理网卡已经加到了br0这个网桥上。桥接模式下,虚拟机就像是局域网里另一台真机,SSH、scp、rsync、HTTP全部走真实网络路径,传文件时的吞吐量一般比用户模式高,延迟也小。但就是配置麻烦,天生依赖宿主机网络环境,笔记本换Wi-Fi后桥接经常不通。我自己的习惯是:日常用用户模式+端口转发,只有需要高带宽传输时才切桥接。
3.3 scp/rsync常用命令与调优
网络通了之后,传文件其实就到了Linux基础应用层面。
单次拷贝用scp就够了:
scp -P 2222 /data/bigfile.tar.gz user@localhost:/home/user/注意:scp用-P指定端口,和ssh一样,-p是保留时间戳的,别记混了。端口映射那里hostfwd=tcp::2222-:22已经绑定了宿主机的2222端口,所以这里也要带上-P 2222。
如果文件很大,scp可能会断,我的建议是用rsync。rsync可以断点续传,而且只传变化的部分。典型用法:
rsync -avzP -e "ssh -p 2222" /data/bigfile.tar.gz user@localhost:/home/user/-a保留属性,-v显示详细输出,-z在传输时压缩,-P表示显示进度且支持断点续传。对于大目录同步,rsync基本是标准答案。
网络传文件最常见的坑是防火墙。虚拟机里的sshd没装或者没启动,宿主机上ssh -p 2222 user@localhost会显示"Connection refused"。检查顺序是:虚拟机里systemctl status sshd,再ss -tlnp | grep 22看sshd在不在监听,然后看宿主机防火墙是否放行了2222端口。
3.4 单向复制的坑与自定义端口映射
端口转发有一个隐藏坑:如果你在宿主机上同时运行多个QEMU虚拟机,都映射了2222端口,第二个虚拟机必然失败。解决办法是每个虚拟机映射不同端口,比如第一台映射2222,第二台映射2223。
另外,hostfwd实际上还支持从虚拟机访问宿主机端口的反向映射。QEMU默认虚拟机可以访问宿主机的10.0.2.2,也就是宿主机在用户网络里的"网关"地址。但如果你想通过宿主机端口访问宿主机上的服务(比如宿主机起了个HTTP服务在8080),虚拟机里直接访问10.0.2.2:8080是通的,不需要额外配置。这个机制在开发联调时很常用,我经常把宿主机上的mock服务供虚拟机调用。
4. 大块头怎么办:镜像转换、磁盘扩容与宿主机直接读写
共享目录和网络传输解决的是"文件"层面,但有些需求是"磁盘"层面。比如你要把整个虚拟机的数据复制出来,或者要给虚拟机磁盘扩容,或者你的虚拟机里没有网络、没法用共享目录,只能从宿主机往raw格式的磁盘镜像里直接塞文件。这时候就得操作虚拟机磁盘镜像文件本身。
4.1 qcow2转raw与raw转qcow2
QEMU的磁盘镜像默认是qcow2格式,支持快照、压缩、按需分配,不用的空间不会占满宿主机的磁盘。但有的时候,比如你要把虚拟机迁移到另一个不支持qcow2的环境,就得转成raw。
转换命令相当简单:
qemu-img convert -f qcow2 -O raw disk.qcow2 disk.raw反过来:
qemu-img convert -f raw -O qcow2 disk.raw disk.qcow2qemu-img convert的智能之处在于它会按内容复制数据,不是逐扇区复制。所以如果你在qcow2里删除了大量文件,转换成raw后再转回qcow2,镜像会明显变小。我遇到过几次这种情况,转换一次能"减肥"20%到30%。
4.2 宿主机上挂载qemu镜像,直接读写内部文件
有些场景你不想启动虚拟机,但想从宿主机直接读虚拟机磁盘里的文件。比如虚拟机崩溃了,你想抢救数据;或者不想走网络,就想一次性把一堆文件塞进虚拟机的根文件系统。
对于raw格式镜像,可以用Linux的loop设备直接挂载:
sudo losetup /dev/loop0 disk.raw sudo partprobe /dev/loop0 sudo mount /dev/loop0p1 /mnt/vmdiskN.B.:如果磁盘里只有一个分区,挂载点是/dev/loop0p1;如果有多个分区,/dev/loop0p2、/dev/loop0p3以此类推。分区表是GPT还是MBR会影响partprobe的结果,但基本都通用。
对于qcow2格式,不能直接losetup,需要先转换成raw,或者用qemu-nbd挂载:
sudo modprobe nbd max_part=8 sudo qemu-nbd --connect=/dev/nbd0 disk.qcow2 sudo mount /dev/nbd0p1 /mnt/vmdisk挂载完成后,你就可以直接用cp、rsync等命令往虚拟机磁盘里塞文件。操作完成后要记得先卸载再断开nbd连接:
sudo umount /mnt/vmdisk sudo qemu-nbd --disconnect /dev/nbd0这套操作我用于备份、恢复和离线修改虚拟机系统配置。唯一要注意的是,如果虚拟机不是关机状态,此时磁盘里可能有未落盘的数据,直接操作会导致文件系统损坏,所以怀孕前一定要确保虚拟机处于关机状态。
4.3 磁盘扩容:给虚拟机"换个大硬盘"
另一个经常被问到的需求是扩容。虚拟机磁盘用着用着空间不够了,不想重建镜像,那就扩容。
qcow2可以用qemu-img resize:
qemu-img resize disk.qcow2 +50G这条命令只是把磁盘镜像容量变成原来的+50G,但分区表不会自动变。你还需要进入虚拟机,对新空间进行分区和文件系统扩容。如果你用LVM,过程相对简单:使用pvresize扩展PV,再lvextend扩展LV,最后resize2fs或xfs_growfs扩展文件系统。
经验之谈:如果你要扩容的是根文件系统,且里面跑着重要服务,我强烈建议先做快照。qcow2支持内置快照:
qemu-img snapshot -c before-resize disk.qcow2扩容出问题随时可以回滚:
qemu-img snapshot -a before-resize disk.qcow2我就在一次扩容中把分区的起始偏移搞错了,导致虚拟机起不来。还好有快照,几分钟就恢复了,不然重建系统会让人崩溃。
5. 那些零散的传输方式:剪贴板、USB和spice通道
共享目录和网络已经能覆盖绝大多数情况,但总有些场景不按常理出牌。比如Windows虚拟机想和宿主机共享剪贴板,或者你想在宿主机和虚拟机之间拖拽文件。这些"零碎"需求,其实也有对应方案。
5.1 SPICE协议与剪贴板共享
如果你用的是协议栈SPICE(默认的QXL显示协议在性能上不如SPICE好用),剪贴板共享是可以在图形界面里直接配的。
启动QEMU时,加上:
qemu-system-x86_64 \ -spice port=5930,disable-ticketing \ -vga qxl然后用virt-viewer或者remote-viewer连接:
remote-viewer spice://localhost:5930这时,宿主机和虚拟机的剪贴板共享通常是直接生效的。比如在虚拟机里复制一串命令,切回宿主机就能Ctrl+V粘贴到终端。如果没生效,检查一下虚拟机里有没有装spice-vdagent:
sudo apt install spice-vdagentspice-vdagent这个服务,还负责实现拖拽文件功能(部分发行版支持),也负责自动调整分辨率。所以遇到"QEMU虚拟机分辨率怎么改大"的问题,先怀疑是不是没装spice-vdagent。
5.2 USB设备直通
有些场景,你想把宿主机上插的一个U盘、一块移动硬盘直接"插"进虚拟机。这属于外设直通,不是严格意义上的传文件,但确实是很多人拿来传文件的办法。
QEMU支持USB直通,最简单的是:
qemu-system-x86_64 \ -usb -device usb-host,vendorid=0x1234,productid=0x5678vendorid和productid你可以先用lsusb查到,然后把对应id填进去。启动虚拟机后,U盘就直接出现在虚拟机里了,像插在本机一样。
这个方案对"宿主机上有没有驱动"完全无感,因为USB设备是被虚拟机看到的。缺点是同一时间只能有一端能访问它,还有一个更麻烦的坑是:如果宿主机上某个进程正在占着这个设备(比如文件管理器在扫描),虚拟机里会显示设备被占用。
5.3 virtio-win驱动:Windows虚拟机必备
聊到Windows虚拟机传文件,有个东西绕不开:virtio-win驱动包。Windows不自带virtio驱动,如果你用virtio磁盘、virtio网卡或者virtiofs,Windows默认系统里是没有对应驱动的,设备管理器里全是黄色感叹号,网络和磁盘根本起不来。
解决方法是:在装Windows时,用-cdrom virtio-win.iso挂载virtio-win驱动ISO,安装时选择加载驱动,或者在装完系统后用这个ISO补装。virtio-win里也包含spice-guest-tools,装了之后剪贴板共享和鼠标指针偏移问题基本会消失。
如果你已经建好了一个Windows虚拟机,可以在QEMU命令行里挂载virtio-win的ISO:
qemu-system-x86_64 \ -drive file=/path/to/virtio-win-0.1.229.iso,media=cdrom进入Windows后,打开光驱,直接运行virtio-win-guest-tools,把所有组件安装一遍。
6. 一套可复用的排错顺序与个人心得
说完了各种方案,最后聊一聊我在实践中总结的一套排错顺序。遇到传文件不成功,我现在的检查路径非常固定。
6.1 从下往上查:链路、权限、协议
我的排错逻辑是从物理层到协议层逐步排查。
第一步,先确认基本连通性。如果是网络传输,先在虚拟机里ping一下网关、在外边ping一下虚拟机的映射IP;如果是共享目录,先确认挂载点存在、mount命令没报错。连通性都不通,直接定位到网络配置或共享参数上。
第二步,看权限。虚拟机和宿主机用户权限不一致,经常导致能挂载但不能写入。用security_model=none能消除大部分坑,但要注意安全边界。
第三步,看协议细节。比如9p的version=9p2000.L、virtiofs的mount_tag是否匹配、ssh端口是否写对。细节问题要逐项核对。
我专门整理过一个简表,可以帮你快速定位:
| 症状 | 可能原因 | 解决方向 |
|---|---|---|
| 虚拟机里mount 9p报"Host requires a more recent Windows 9P server" | 虚拟机内核旧 | 换用version=9p2000.L;升级内核 |
| 9p挂载成功但写入卡死 | 缓存模式/cache=loose与文件使用场景冲突 | 换cache=none观察;检查多设备冲突 |
| virtiofs启动报shared memory相关错误 | QEMU内存没配成memory-backend-file | 检查启动参数中的-object memory-backend-file |
| SSH连接被拒绝 | 虚拟机里sshd未启动 | 检查虚拟机22端口;检查hostfwd映射目标端口 |
| 虚拟机能ping通外网但SSH连回来超时 | 端口映射或防火墙问题 | 检查hostfwd和宿主机防火墙 |
| qemu-nbd挂载时报错 | nbd模块未加载或镜像损坏 | modprobe nbd max_part=8;用qemu-img检查 |
| Windows虚拟机设备管理器一堆感叹号 | 缺virtio驱动 | 挂上virtio-win ISO安装 |
6.2 我踩过的几个典型坑
第一个坑是9p挂载加了multidevs=remap还是报inode冲突。后来发现,问题出在共享目录本身挂载了另一个设备,而那个设备的文件系统和宿主机根分区不是同一个,9p在遍历目录时看到不同设备的inode会混乱。解决的办法其实很简单:不要在共享目录里跨设备塞东西,要么就把共享目录拆成多个独立的9p挂载,而不是用一层目录强行包住所有设备。
第二个坑是virtiofs的共享内存配置。网上很多教程都省略了-object memory-backend-file,我照抄之后虚拟机一直起不来,日志提示"vhost-user requires shared memory"。后来仔细看文档才发现,virtiofs依赖共享内存后端才能实现page cache共享。这个参数必须写,不能省。
第三个坑是rsync传大目录时,因为虚拟机里磁盘性能一般,跑到一半报"rsync error: some files/attrs were not transferred"。原因是在传输过程中,虚拟机的文件被其他进程改了,导致rsync的校验失败。解决方式是加上--ignore-errors或者先停掉写频繁的服务,再重新跑。
6.3 我的通用建议
我现在的实践中,日常开发用的最多的是virtiofs,因为它性能好,而且可以直接把宿主机上的代码目录挂进虚拟机,改完代码虚拟机里马上能看到,跑测试、编译都方便。
临时传小文件,我反而最常用的是用户模式网络加scp。在QEMU命令行里给端口转发配好,然后一条scp就搞定,不依赖9p/virtiofs的内核支持。
大块数据或者要备份整个虚拟机,我会优先考虑qemu-img convert加rsync配合,先在宿主机上把qcow2转成raw(或者打快照),然后归档。这样不管是备份还是迁移,都绕开了虚拟机的网络瓶颈。
另外,个人习惯上,我会尽量让QEMU的共享目录是只读的。给开发同学用的共享目录,写权限往往很容易造成两边文件错乱。如果是生产环境、数据敏感的虚拟机,尽量别用security_model=none,否则虚拟机的root一旦被入侵,宿主机等于门洞大开。这个安全边界一定要想清楚。
最后,一个很容易被忽略的小技巧:如果是纯文本或配置类文件,直接用串口传也是可行的。QEMU里给虚拟机加一个-serial stdio,然后在虚拟机里把文件内容打到串口,宿主机这边直接读终端输出。这个方法看起来原始,但在虚拟机网络和共享目录全都不可用的时候,往往是最后一根救命稻草。