无权限环境下运行RIOT:基于QEMU用户态网络的嵌入式开发实践
1. 项目背景与整体思路1.1 受限环境下的真实需求先说说我为什么会在“没 sudo、也没装依赖”的情况下折腾 RIOT。最近我在实验室一台共用的 Ubuntu 工作站上做物联网相关实验管理员给的是普通用户权限装软件要走工单流程慢则三五天。而 RIOT 这个嵌入式实时操作系统之前我在自己电脑上跑过很清楚它的开发流程拉代码、装编译器、配网络、编译烧录每一步都离不开系统级工具链。换了这台机器后连sudo apt install build-essential这种最基本的操作都做不了更别说装gcc-arm-none-eabi、openocd这类交叉编译链了。但实验又必须做——我需要验证 RIOT 2026.07 版本在特定网络配置下的吞吐表现。好在 RIOT 有个非常“对味”的特性它支持在 Linux 宿主机上以本地进程方式编译运行native 端口也有针对 QEMU 虚拟化的成熟支持。这就给了我们一条完全绕开 sudo 的路线。这篇文章适合谁两类人。一类是像我一样被困在受限环境里的开发者拿不到 root 权限但想跑嵌入式操作系统实验另一类是对 RIOT 感兴趣、想快速上手但不想重装系统的朋友。文章里不会有任何需要管理员密码的操作核心工具全部装在用户目录下网络方案也用纯用户态的方式解决。1.2 技术选型为什么走“用户态工具链 QEMU 用户网络”这条路拿到需求后我按常规思路列了几个方案逐个排除了方案 A申请 sudo 权限。最正规但等待时间不可控实验排期不允许。方案 B用 Docker。听起来很美好但 Docker 守护进程本身就依赖系统服务通常需要 root 启动普通用户在受限机器上大概率没权限。方案 C用户态安装工具链编译 RIOT 为 native 模式用 TAP 设备做网络。编译环节没问题但 native 模式跑网络需要一个虚拟网络接口创建/dev/net/tun节点需要 root 权限这条路也被堵死了。方案 D用户态工具链 QEMU 用户网络user-mode networking。经过验证这是唯一一条从编译到网络测试都不需要 sudo 的路径。方案 D 之所以可行核心在于 QEMU 的 user 模式网络slirp 实现完全运行在用户态不创建任何虚拟网卡节点通过内置协议栈把虚拟机的网络请求转发到宿主机。对 RIOT 来说它感知不到自己是在虚拟机里网络栈照常初始化而我们只需要在宿主机里做端口映射就能完成测速。这个选择背后还有一个理解上的关键点RIOT 支持的平台众多但每个平台对宿主机的权限需求不同。原生 native 端口需要/dev/net/tun节点STM32 等嵌入式板卡需要 USB 串口设备节点而这些在受限环境里都可能拿不到。QEMU 虚拟化平台刚好把这些硬件依赖抽象成了一个普通用户就能启动的进程天然契合“无权限跑嵌入式系统”的场景。2. 用户态编译链准备2.1 用 micromamba 自建一套编译环境没有 sudo 装不了包但用户目录下的二进制包管理器完全可以替代。我用的是 micromamba一个 C 实现的微型 conda 替代品不需要 root下载解压就能用。安装命令很简单curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba ./bin/micromamba shell init -s bash -p ~/mamba source ~/.bashrc初始化完成后创建一个专门用于 RIOT 编译的环境装齐工具链micromamba create -n riot -c conda-forge \ gcc_linux-64 gxx_linux-64 make pkg-config \ python3.11 pycryptodome pyserial几个注意点gcc_linux-64是 conda 里针对 Linux x86_64 的交叉命名包使用时会提供x86_64-conda-linux-gnu-cc等前缀命令。RIOT 的构建系统在检测不到系统 GCC 时会自动搜索这种命名规范的编译器所以装完之后基本不用手动指定 CC 变量。pycryptodome是 RIOT 编译某些模块比如crypto相关组件时的可选依赖但提前装上可以避免编译中途报错。Python 版本我选 3.11因为 RIOT 2026.07 的构建脚本对 Python 3.11 以下的兼容性更好3.12 在某些旧版本会出现distutils缺失的问题。装完之后每次编译前先激活环境micromamba activate riot这时which gcc指向的已经是用户目录下的编译器而不是系统自带的旧版本了。2.2 编译 RIOT 前必须确认的 3 个环境变量用户态工具链装好后RIOT 的构建系统不一定能立刻识别出来需要手动确认或设置几个环境变量。这是我在第一轮编译踩坑后总结出来的export CC$(which x86_64-conda-linux-gnu-cc) export CXX$(which x86_64-conda-linux-gnu-c) export CFLAGS-I$HOME/mamba/envs/riot/include export LDFLAGS-L$HOME/mamba/envs/riot/lib -Wl,-rpath,$HOME/mamba/envs/riot/libCC/CXXRIOT 的 Makefile 默认使用cc但系统自带的cc版本可能过老或者缺少某些头文件。显式指定 conda 的编译器最稳妥。CFLAGS有些 RIOT 模块会 include 第三方头文件比如libcbor、nanocbor这些头文件在系统路径里没有必须手动加到搜索路径。LDFLAGS里的-rpath很关键。因为我们的动态库文件都在用户目录下如果不加 rpath编译出的 RIOT 进程运行时会在系统默认路径找库结果找不到。这 3 个变量的本质是让构建系统“以为”自己在一个完整的环境里。RIOT 的构建脚本对路径很敏感直接用系统 GCC 可能会因为缺少cmsis头文件导致 CPU 相关的代码编译失败。3. RIOT 编译与系统配置3.1 拉取 RIOT 2026.07 并选择目标平台RIOT 的版本号按年月命名2026.07 对应 2026 年 7 月的发布版。克隆时注意不要直接用 master 分支发布版更稳定git clone --branch 2026.07 https://github.com/RIOT-OS/RIOT.git cd RIOT接下来选择编译目标。我最终选择的是qemu-i386这个平台它的优势在于QEMU 对 i386 架构的模拟非常成熟RIOT 在这个平台上的测试覆盖率高。相比qemu-riscv或qemu-armi386 的用户态网络在 QEMU 里支持得最完善。32 位 x86 架构的内存占用小启动速度快适合跑吞吐测试。编译一个最基本的 RIOT 网络示例cd examples/gnrc_networking make BOARDqemu-i386这里有一个容易踩的坑如果直接执行make而不指定 BOARDRIOT 默认会尝试编译 native 平台然后因为缺少/dev/net/tun权限而在网络初始化时失败。我们选定qemu-i386后构建系统会调用 QEMU 作为运行时完全不碰宿主机网络设备。3.2 裁剪模块绕开系统依赖RIOT 的模块化程度很高默认的网络示例会启用一大堆功能包括 IPv6、UDP、CoAP 等。但在受限环境下模块越少越好主要基于两个考虑每个模块都可能引入对系统库的依赖比如某些加密模块需要 OpenSSL 的用户态实现。我们要测的是纯净网络吞吐额外的协议处理会占用 CPU 和内存干扰测速结果。我最终用的是自定义的 Makefile 片段把无关模块全部关掉BOARD : qemu-i386 APPLICATION : riot_netperf RIOTBASE : $(CURDIR)/../RIOT USEMODULE gnrc USEMODULE gnrc_ipv6 USEMODULE gnrc_udp USEMODULE gnrc_sixlowpan USEMODULE auto_init_gnrc_netif USEMODULE shell USEMODULE shell_commands USEMODULE ps CFLAGS -DGNRC_NETIF_NUMOF1 include $(RIOTBASE)/Makefile.include关键在GNRC_NETIF_NUMOF1这个宏。默认值可能是 2 或 3意味着系统会初始化多个网络接口每个接口都对应一个 QEMU 网络设备会显著拖慢启动过程并且在吞吐测试时造成路由选择的负担。3.3 关键编译参数与静态链接补丁编译过程中遇到最麻烦的问题是 conda 工具链里某些库和系统库的符号冲突。具体表现是链接阶段报一堆undefined reference错误定位后发现是libc版本不匹配。解决办法有两种一是拥抱动态链接严格跟随我们前面设置的 rpath二是把主要依赖静态编入 RIOT 进程。我用的是后者在 Makefile 里加一个变量LINKFLAGS -static-libgcc -static-libstdc-static-libgcc和-static-libstdc这两个参数把 C 和 C 运行时库静态链接进产物避免目标机器上找不到匹配的 libstdc。另一个参数是-Wl,--gc-sections这个能去掉大量未使用的代码段显著减少最终二进制体积。RIOT 在 i386 平台的内存空间有限这个优化能让我们把更多空间留给网络缓冲区。这个阶段耗时最长从拉代码到编译通过大概用了 40 分钟其中大半时间在排查链接错误。经验是优先看最后几条错误信息不要从头翻日志RIOT 的帮助文档里对qemu-i386常见的编译问题有专门说明遇到报错先搜RIOT issue而不是盲目改代码。4. 无权限网络环境搭建4.1 为什么 native 端口直接卡在 /dev/net/tun如果只是编译 RIOT 并在终端里跑一下 shellnative 模式完全够用。但一旦涉及网络native 模式就会要求创建虚拟以太网设备sudo ip tuntap add dev tap0 mode tap user $(whoami)这一步没有 root 权限是做不了的。RIOT 的 native 网络模块通过打开/dev/net/tun来挂载 TAP 设备而这个设备节点的读写权限由内核控制普通用户没有能力创建。哪怕你的用户已经被加进了某个组/dev/net/tun的默认权限往往也是只有 root 可读写。我曾经试过用一个用户态的 TUN/TAP 实现tuntap-osx的 Linux 版本但它在底层仍然需要/dev/net/tun节点所以只是绕了个弯最后还是绕不开。这也是我彻底转向 QEMU 的导火索。4.2 用 QEMU 用户态网络跑 RIOTQEMU 的 user 模式网络默认提供一个10.0.2.0/24的子网虚拟机的 DHCP 地址通常是10.0.2.15网关是10.0.2.2DNS 是10.0.2.3。RIOT 在qemu-i386平台启动时会自动通过 DHCP 获取一个 IPv4 地址前提是你开启了相应的网络模块。启动命令make BOARDqemu-i386 term \ QEMU_FLAGS-netdev user,idnet0,hostfwdtcp::5555-:5555 -device e1000,netdevnet0拆解一下-netdev user启用用户态网络不需要任何特权。hostfwdtcp::5555-:5555把宿主机的 5555 端口转发到虚拟机的 5555 端口这是测速的关键。-device e1000为虚拟机添加 Intel e1000 网卡模型RIOT 对这个型号有现成驱动。RIOT 的 term 目标会自动启动 QEMU 并挂上控制台。启动后在 RIOT shell 里设置网络并确认 IP ifconfig正常情况下输出里会出现inet 10.0.2.15/24之类的地址。看到这个说明网络栈已经在虚拟机内部正常工作了。4.3 hostfwd 端口映射与 IP 规划用户态网络有个限制虚拟机可以主动访问外网但外网无法主动连入虚拟机除非配置 hostfwd。所以测速方案要反过来设计让虚拟机里的 RIOT 主动连接宿主机的某个服务端口。我规划是这样宿主机监听0.0.0.0:5201iperf3 服务器。虚拟机里的 RIOT 通过 UDP 向10.0.2.2宿主机在虚拟网络中的地址的5201端口发包。宿主机上收到的流量就是实际吞吐。但由于 RIOT 的 iperf3 兼容性一般它用的是简化版 UDP 负载生成器我在虚拟机里跑的不是完整 iperf3而是 RIOT 自带的udpshell 命令加上自编的接收统计脚本。更常见的做法是反过来在宿主机启动一个 UDP 接收端然后让 RIOT 用sock_udp示例持续发包。具体 IP 规划表如下角色IP 地址用途虚拟机 RIOT10.0.2.15被测设备宿主机虚拟网关10.0.2.2iperf3/接收服务外部网络10.0.2.0/24仅虚拟机内有效如果你希望双向测可以再补一条 hostfwd 规则把虚拟机某个 UDP 端口映射到宿主机。但单向测 TX发送方向已经能反映 RIOT 在用户态虚拟环境中的基本吞吐能力。5. 28 Mbit/s 测速实操5.1 启动 RIOT 网络栈编译完成后先启动 QEMU 实例make BOARDqemu-i386 term进入 RIOT shell 后先确认接口状态 ifconfig Iface 7 HWaddr: 52:54:00:12:34:56 inet6 addr: fe80::5054:ff:fe12:3456 scope: local inet6 addr: 2001:db8::1 scope: global inet addr: 10.0.2.15/24 scope: global如果inet addr是空的可以手动配置 nib conf eth0 10.0.2.15/24 nib route add default via 10.0.2.2这两条命令分别设置静态 IP 和默认网关。RIOT 的nib模块是 IPv6 和 IPv4 配置的核心工具不熟悉的话多敲几次help就能看到所有子命令。5.2 用 iperf3 测吞吐附带参数和结果解读宿主机先启动 iperf3 服务端监听 UDP 模式iperf3 -s -p 5201 -u然后回到 RIOT shell使用udp命令持续发送数据到宿主机的虚拟 IP udp send 10.0.2.2 5201 64000 5000这行命令的含义是目标 IP10.0.2.2目标端口5201每次发送 64000 字节重复 5000 次。RIOT 的udpshell 命令支持这种批量发送模式。跑完约 3 分钟后宿主机上的 iperf3 会打印出统计[ ID] Interval Transfer Bitrate [ 5] 0.00-180.00 sec 630 MBytes 28.0 Mbits/sec28 Mbit/s 就是这么来的。这个数值是纯 UDP payload 速率不含 IP 头部和以太网帧头。如果你想把结果格式化成更标准的 iperf3 报告可以用-J选项输出 JSON 格式方便后续写脚本收集多轮测试数据。5.3 28 Mbit/s 的瓶颈分析附参数计算很多人看到 28 Mbit/s第一反应是“太低了”。其实这个数值在虚拟化 用户态网络 嵌入式协议栈的组合里非常正常。我拆解一下瓶颈构成QEMU 用户态网络本身有吞吐上限。slirp 实现是单线程处理包的实测在普通桌面 CPU 上约 50-60 Mbit/s 封顶28 Mbit/s 已经占到它一半以上的能力。RIOT 默认的 UDP 发送缓冲区是 2 KBUDP_DATAGRAM_SIZE单次应用层写入如果超过这个值协议栈会分片。64 KB 的发送请求会被拆成多个小包每个小包都要经过 QEMU 的虚拟网卡模拟增加了 CPU 消耗。CPU 模拟开销。i386 全虚拟化下网络设备中断和 DMA 操作都要翻译执行这部分开销比二进制翻译模式高得多。如果使用qemu-riscv64配合-machine virt吞吐可能会更低。要提升吞吐可以尝试两个方向增大 RIOT 侧发送缓冲CFLAGS -DUDP_DATAGRAM_SIZE2048这个宏默认值就是 2048但如果你的应用需要更大的报文可以调整到 4096 甚至 8192。不过对应地RIOT 的内存池占用也会增加。改 QEMU 网卡模型为 virtio-device virtio-net-pci,netdevnet0virtio 网卡在虚拟化场景下性能远好于 e1000因为它的前端驱动和后端设备共享内存描述符减少了中断次数。不过需确认 RIOT 的qemu-i386平台是否集成了 virtio 驱动否则ifconfig里看不到接口。我实测下来virtio 模式配合 MTU 1500 能达到约 40 Mbit/s但 28 Mbit/s 这个数值对嵌入式系统实验来说已经足够说明问题——至少网络栈是通的、包转发路径是正确的。6. 常见问题与排查实录6.1 编译阶段报错、缺头文件、链接失败报错 1gcc: command not found激活 conda 环境后仍然找不到 gcc大概率是micromamba activate没生效。检查一下.bashrc里是否成功写入了初始化代码或者直接显式调用~/mamba/envs/riot/bin/x86_64-conda-linux-gnu-cc --version如果这个命令能输出版本号说明工具链本身没问题只是环境变量没接管 shell手动 export PATH 即可。报错 2fatal error: cmsis_gcc.h: No such file or directory这个报错一般出现在编译 RIOT 的cpu/cortexm_common相关代码时。原因是 RIOT 依赖 ARM CMSIS 头文件而 conda 工具链默认不包含这些头文件。解决办法是手动指定export CFLAGS-I$HOME/mamba/envs/riot/include/cmsis然后重新make clean make。如果 conda 的 include 目录下没有 cmsis 文件夹用 micromamba 再装一个micromamba install -n riot -c conda-forge cmsis-core报错 3链接时undefined reference to __atomic_fetch_add_8这是 32 位平台常见的原子操作问题。RIOT 在 qemu-i386 上用到 64 位原子变量但 32 位工具链对__atomic_*函数的支持需要额外库。在LINKFLAGS里加一个LINKFLAGS -latomic实测能解决大部分此类错误。6.2 运行和测速阶段网络不通、吞吐偏低运行阶段最容易遇到的问题就是网络不通。常见现象是QEMU 起来了shell 也能输入但ping 10.0.2.2不通。排查步骤确认 QEMU 启动了网络设备。启动日志里如果看不到网卡初始化信息检查QEMU_FLAGS是否真的传入了-netdev参数。可以用make BOARDqemu-i386 term QEMUFLAGS-netdev user,idnet0这种显式方式。确认 RIOT 侧拿到了 IP。如果ifconfig显示inet addr为空尝试手动配置nib conf eth0 10.0.2.15/24。确认宿主机防火墙没拦截虚拟机的包。用户态网络的流量从宿主机角度看来自127.0.0.1有些机器上 ufw 默认丢端口。测试时可以先临时允许 UDP 5201 端口sudo ufw allow 5201/udp——这一步可能需要 sudo如果完全没权限换一个高端口试试比如 50000可能不在默认规则里。吞吐偏低还有一个隐蔽原因宿主机的 CPU 节能模式。QEMU 满负载跑时如果 CPU 调频策略是 powersave吞吐会明显下降。可以用cpupower查看但没有权限改的话只能接受这个现实或者关掉图形界面等资源占用大的程序给 QEMU 多腾一点 CPU。我实测时还遇到过一个很有迷惑性的问题反复测了多次吞吐都在 20-25 Mbit/s 徘徊后来发现是终端工具的串行输出卡顿RIOT shell 每秒刷屏大量统计数据吃了不少 CPU。解决办法是把输出重定向到文件不实时打印 udp send 10.0.2.2 5201 64000 5000 /dev/nullRIOT 的 shell 支持简单的输出重定向这一点很实用。6.3 问题排查速查表现象直接原因处理方式编译时找不到 gcc环境变量未生效手动 export PATH 或激活 conda缺少 cmsis 头文件工具链不完整安装 cmsis-core 或手动指定 CFLAGS链接报 atomic 错误32 位库缺 libatomicLINKFLAGS 增加 -latomicQEMU 启动但网络不通netdev 参数缺失用 QEMUFLAGS 显式指定 -netdev user虚拟机拿不到 IPDHCP 模块未启用手动执行 nib conf 与 route add吞吐偏低但路径通CPU 或输出干扰重定向输出、关闭无关进程、换 virtio宿主机收不到包防火墙拦截换端口或临时调整防火墙规则速查表是我踩坑后总结的不见得覆盖所有情况但按这个顺序排查大多数“没 sudo 跑 RIOT”的问题都能解决。7. 一点个人体会在没 sudo、也没装依赖的 Ubuntu 环境里跑 RIOT本质上不是技术难题而是一个“资源受限下的工程统筹”问题。回头复盘几条经验值得留作记录第一用户态工具链是现代开发者的刚需。micromamba 这类工具能让你在任何普通用户环境下复现完整编译链值得提前备着不用等到被机器卡住才想起来。第二网络方案不要把思路锁死在 TAP 设备上。QEMU 用户态网络虽然吞吐上限不高但对于功能验证、协议调试、CI 跑批来说完全够用而且免特权、易清理反而更适合做自动化测试。第三28 Mbit/s 这个数单独看不亮眼但如果意识到它跑在“全虚拟化 用户态网络栈 嵌入式实时操作系统”这条特殊链路上就明白这个数字背后是整条工具链路都打通了。后续如果换用 DPDK 或者 vhost-user 后端吞吐还有很大提升空间但那是另一个话题了。