QEMU 串口控制台接线:-nographic、-serial mon:stdio 与 -serial null 的区别
在 认识 QEMU:从零看懂虚拟机、镜像与快照 里建立了虚拟机的基本直觉,在 QEMU 安装与第一个 RISC-V 虚拟平台 里第一次看到了 OpenSBI 的启动横幅。横幅能出现在你的终端里,靠的不是显卡和显示器,而是一块叫 UART 的虚拟串口。同一条启动命令,换成 -serial null 之后终端就一片寂静——串口接到哪里,决定了你能不能看到客户机说了什么。本篇把 -nographic、-serial mon:stdio、-serial null 三种接线放在同一台 virt 机器上做对照实验,用 QMP 的 query-chardev 直接查看 QEMU 内部的实际接线,而不是停留在参数手册的字面描述。
本文要回答的问题
- 嵌入式板子没有显示器,启动日志到底是从哪个硬件出来的?
- QEMU 里的虚拟 UART 和
-serial参数指向的 chardev,是同一层东西吗? -nographic看起来是关图形窗口,它对串口还隐含了什么动作?mon:stdio里的mux是什么,为什么Ctrl+A再按C能在串口和监视器之间切换?-serial null之后客户机的打印去了哪里,什么场景才需要这样接?
一、串口是什么:嵌入式世界的「显示器和键盘」
桌面计算机靠显卡接显示器、靠 USB 接键盘交互,而绝大多数嵌入式 SoC 压根没有显卡。从路由器、交换机到手机基带,开发者与系统对话的传统通道是串口:SoC 内部集成一个通用异步收发器(Universal Asynchronous Receiver/Transmitter,UART),芯片引脚上以高低电平逐位收发字符,板卡上再配一颗转换芯片把 TTL 电平转成 USB 或 RS-232,接到电脑的串口终端。固件、引导程序、内核早期启动代码在网卡和显卡驱动都还没就绪的时候,只能往 UART 的寄存器里写字符——这就是为什么几乎所有嵌入式教程第一步都让你「接串口看控制台」。
QEMU 的 virt 机器模型里同样有一颗 16550 兼容 UART。OpenSBI 固件启动时把横幅逐字节写进这颗虚拟 UART 的发送寄存器,后面的内核驱动、裸机程序也通过同一套寄存器收发字符。理解本篇的关键,是把这件事拆成两层:UART 是客户机侧的虚拟硬件,Guest 软件只认这颗芯片的寄存器,根本不关心字符最终流向宿主机的哪里;chardev(字符设备后端)是 QEMU 进程侧的落点,决定 UART 收到的字节被转发到终端、文件、套接字还是直接丢弃。-serial 参数操作的是后半层——它不动虚拟硬件,只改接线。
下面这张流程卡给出本篇的阅读路径:先建立两层模型,再看三种落点的实测表现,最后回到选型。
二、一张接线图:UART 与 chardev 的三个落点
-serial 每出现一次,就为虚拟串口指定一个 chardev 后端。QEMU 支持的后端类型很多(文件、TCP/UDP 套接字、Unix domain socket、pty 等),入门阶段只需要吃透三个:stdio 把字节直连到 QEMU 进程的标准输入输出,也就是你启动它的那个终端;mon:stdio 在 stdio 前面加一层多路复用器(mux),让串口和 QEMU 监视器共用同一个终端;null 是一个只写黑洞,写进去的数据全部丢弃,读操作永远返回空。
读这张图抓住「一个 UART、三个落点」这条主线。左侧蓝色节点是客户机侧唯一不变的东西:无论参数怎么写,OpenSBI 面对的都是同一颗 16550 UART。变化全部发生在右侧的 chardev 层——绿色的 stdio 是双向直连,字节原样进出终端;紫色的 mux 在一条终端链路上分时承载串口与监视器两个通道;红色的 null 没有出线,因为数据到这里就被扔掉了。图中没有画箭头从 null 回到终端,这正是下一节实测里横幅计数为 0 的原因。
三、三轮对照实测:query-chardev 里的真实接线
口说无凭,QEMU 提供了 QMP(QEMU Machine Protocol)接口,可以在虚拟机运行时查询内部对象状态。三轮实验用完全相同的机器模型与固件,只改串口相关参数,并通过 Unix domain socket 下发 query-chardev 命令查看 chardev 列表;同时把 QEMU 进程的 stdio 重定向到捕获文件,统计 OpenSBI 横幅在文件中出现的次数。先看 A、B 两轮。
A 轮的 -nographic 是一个复合开关,它至少做了两件事:关闭图形显示窗口,并把第一路串口接到 stdio,同时让监视器复用这条终端链路。query-chardev 的返回印证了这一点:serial0 的 filename 直接是 stdio,frontend-open: true 表示虚拟 UART 与后端处于接通状态;同一轮里并口 parallel0 落在 null,因为没有图形窗口可以承载并口输出。stdio 捕获文件里 OpenSBI 横幅计数为 1,说明固件的每一行打印都抵达了终端。
B 轮显式拆开写:-display none 只负责关窗口,-serial mon:stdio 负责接线。返回里出现了两个相关对象——serial0 的 filename 是 mux,底下还有一个 serial0-base 指向 stdio。这正是复用器的结构:UART 先接到 mux,mux 再挂在 stdio 上,监视器作为另一个前端共享同一个 mux。横幅计数同样是 1,串口输出没有因为复用而丢失。注意 B 轮的 parallel0 落点是 vc(虚拟控制台)而非 null,因为 -display none 与 -nographic 对并口的隐含处理并不相同,这也是两个参数不能简单画等号的证据之一。
再看 C 轮,把串口明确接向黑洞。
C 轮的 query-chardev 里 serial0 的 filename 变成了 null,虚拟 UART 依然 frontend-open: true——从 Guest 视角看,串口芯片完好、寄存器照常工作,没有任何报错;但所有写出发送寄存器的字节在 chardev 层被静默丢弃,stdio 捕获文件中的横幅计数为 0。这组对照把两层模型钉死了:接线改变的是主机侧落点,客户机硬件状态完全无感。三轮结果汇总如下。
| 维度 | A:-nographic -no-reboot | B:-display none -serial mon:stdio | C:-display none -serial null |
|---|---|---|---|
| serial0 落点 | stdio 直连 | mux,底层 serial0-base 为 stdio | null 黑洞 |
| parallel0 落点 | null | vc 虚拟控制台 | vc 虚拟控制台 |
| OpenSBI 横幅可见性 | 计数 1,终端可见 | 计数 1,终端可见 | 计数 0,输出被丢弃 |
| 监视器入口 | 复用同一终端,C-a c 切换 | 复用同一终端,C-a c 切换 | 不在串口终端上,走 QMP/其他通道 |
| 适用场景 | 入门实验、一条命令搞定交互 | 想显式控制显示与接线的调试场景 | 自动化后台运行、串口噪音无关紧要 |

这张记录把三轮实验按编号排开,适合对着上面的表格逐行核对。A、B 两轮都能在终端里看到 OpenSBI 启动横幅,区别只在 query-chardev 返回的落点——一个是 stdio 直连,一个是 mux 下挂 serial0-base;到 C 轮,虚拟机照常启动、QMP 查询照常应答,终端却再也等不到横幅,横幅计数从 1 变成 0。同一个 Guest、同一套固件,只改主机侧接线就出现可见与不可见的差别,这正是 chardev 后端模型最直观的证据。
四、mux 复用:一条终端如何容纳两个通道
B 轮返回里的 mux 值得单独展开。串口控制台和 QEMU 监视器是两个性质不同的前端:前者属于 Guest,敲进去的字符进 Guest 的 UART;后者属于 QEMU 进程本身,用来查询虚拟机状态、挂载设备、做快照,命令体系详见 QEMU 监视器入门:运行时掌控虚拟机的控制通道。当两者都想要 stdio 这唯一一条终端链路时,mux 用分时复用解决冲突:任一时刻终端只绑定一个通道,靠转义序列切换。
默认状态下终端属于串口,输入直接送给 Guest。按住 Ctrl+A 再按 C 切换到监视器,出现 (qemu) 提示符后就可以执行 info status、info chardev 等监视器命令;再按一次 Ctrl+A C 切回串口。另外两个常用转义是 Ctrl+A X 直接退出 QEMU,Ctrl+A H 打印全部转义帮助。需要记住转义前缀只在 mux 模式下存在——C 轮那种 null 接线下串口没有终端承载,这些按键自然也就无人接收。
那 -nographic 和手写 -display none -serial mon:stdio 到底选哪个?功能上 A、B 两轮的串口行为等价,区别在隐含动作的覆盖面:-nographic 一口气关掉图形窗口、接好串口并复用监视器,还会把并口等其他图形相关设备一并重定向,适合「我就想要一个纯终端虚拟机」的大多数入门场景;拆开写则把每个决策显式化,脚本可读性更好,也方便只替换其中一环(比如串口改接文件而保留无窗口设定)。逐参数精读完整启动命令可以配合 QEMU 命令行参数精读:一条启动命令的逐段注解,其中对 -nographic -serial 这组参数的隐含行为有专门注解。
五、三种接线怎么选
选型其实只需要回答两个问题:需不需要看到串口输出,需不需要人在终端交互。
纯手动调试、学习固件启动链路的场景,直接用 -nographic。一条参数拿到无窗口纯终端,横幅、内核日志、登录提示符全部直达眼前,Ctrl+A X 随时退出,这也是本系列入门实验的统一写法。写自动化脚本或复现实验包时,更推荐显式的 -display none -serial mon:stdio:每个行为都由独立参数表达,别人读脚本时不需要记住 -nographic 的全部隐含动作,需要临时介入时 Ctrl+A C 还能进监视器。
-serial null 并非没用,只是用途相反。当虚拟机作为后台服务运行——比如持续跑回归测试、由 CI 系统通过 QMP 或 gdbstub 远程管控——串口打印既没人看又会污染进程日志,接 null 让它静默消失是干净的做法。另一个常见用途是排错对照:怀疑某个卡死现象与串口打印的 IO 开销有关时,把串口接 null 跑一轮,用 C 轮这样的横幅计数差异验证输出链路本身是否正常。但只要你还指望从终端读到任何 Guest 信息,就不要选它——计数 0 的捕获文件就是最直白的提醒。
代码获取:本篇三轮对照实验的可复现脚本见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-serial-console-wiring/。克隆后在该目录执行./run.sh即可依次跑完三种接线,自动通过 QMP 采集query-chardev结果并统计 OpenSBI 横幅计数,输出与本篇终端实录逐行对应。
实验环境
- 机器/系统:MacBook Pro(Intel x86_64)/ macOS 26
- 工具版本:QEMU 11.1.2(全系统模拟
qemu-system-riscv64)、nc(与 QMP Unix domain socket 交互下发query-chardev) - 日期:2026-11-07
- 完整命令:三轮启动命令与 QMP 查询均封装在公开仓库
experiments/qemu-serial-console-wiring/run.sh,执行方式./run.sh,原始输出即本篇两处终端实录引用的日志
这一篇在知识体系里的位置
本篇解决的是「Guest 的字符如何抵达宿主终端」这条数据通路,是后续一切交互式实验的底座。往硬件方向,UART 是 SoC 外设建模的第一个完整样本,理解了「虚拟硬件寄存器 + 主机侧 chardev 落点」的两层结构,再看中断控制器、定时器、virtio 设备会发现都是同一套前端/后端思路;往调试方向,mux 切出的监视器通道在 QEMU 监视器入门 里展开,而串口本身也是 gdbstub 之外最朴素的裸机调试输出口;往工程方向,启动命令里每个参数的来历与隐含行为可以逐段对照 QEMU 命令行参数精读。入门路线的下一批实验会在这条可靠的串口通道上加载裸机程序,让你自己写的字符第一次出现在终端里。
总结
QEMU 串口控制台的全部困惑,都可以用「UART 是客户机硬件、chardev 是主机落点」这两层模型化解:-nographic 是关窗口加串口接 stdio 并复用监视器的复合开关,适合纯终端交互;-display none -serial mon:stdio 把同样的接线显式拆开,mux 让串口与监视器共用一条终端,靠 Ctrl+A C 切换;-serial null 保持虚拟 UART 完好却丢弃全部输出,适合无人值守的后台运行。三轮 query-chardev 实测里,stdio 直连与 mux 复用的 OpenSBI 横幅计数均为 1,null 接线为 0——参数会骗人,查询到的内部接线不会。