公司那台 Ubuntu 服务器我只拿到一个普通账号sudo 想都不要想。但手头有个活儿需要验证 RIOT 2026.07 的网络栈改动后到底还能跑多快开发板又全被同事借走了。一开始我也觉得这活儿得往后放放可捋了一遍 RIOT 的构建和运行机制之后发现这件事在普通用户下完全做得成——它不仅不需要 root连系统级依赖都几乎不用额外装。最后我不但把 native 版 RIOT 跑起来了还通过 TAP 接口做了一次用户态 UDP 打流测试实测稳定在 28 Mbit/s 上下。这篇文章就是整个过程的完整复盘包括权限排查、编译细节、网络接入和最后那个吞吐数字是怎么读出来的。1. 为什么“没有 sudo”也能碰 RIOTnative 模式与它的极简依赖先说结论RIOT 和很多嵌入式 RTOS 不一样它不是那种必须烧到开发板上才能跑的系统。RIOT 提供了一种叫native的特殊板级支持包编译产物就是一个普通的 Linux 可执行文件直接在用户态跑。这意味着我在这台没有权限的 Ubuntu 上理论上具备了运行一个物联网操作系统的全部条件。1.1 RIOT 的版本节奏2026.07 代表什么RIOT 的版本号一直按“年.月”走比如 2024.01、2024.04、2025.10 这样。2026.07 对应的就是 2026 年 7 月发布的版本。这个命名方式对使用者来说非常直观拿到版本号就知道发布周期也方便在 GitHub 上按 tag 切代码。我这次用的就是官方仓库打的2026.07tag。有个小细节值得说RIOT 的 master 分支其实一直处于“未来版本”的开发状态所以如果你想稳定复现务必按 tag 拉代码而不是直接git clone默认分支。后面我会给具体命令。1.2 native 板级支持包把 Linux 当成一块“开发板”在 RIOT 的语境里BOARDnative是一种非常特殊的 board。它把 RIOT 的线程、定时器、网络接口等核心抽象用 pthread、系统定时器、TAP 接口等在宿主机上实现。编译出来的.elf文件不碰内核不加载模块不写/proc、/sys所以天然不需要 root。这也是整个方案的基石没有 sudo 不是“绕过去”而是 RIOT 的运行模型本身就不需要特权。你只需要一个能跑 C 代码的 Linux 用户态环境。1.3 为什么说“没装依赖”不是硬扛很多 Linux 项目一上来就sudo apt install一堆东西但 RIOT 的源码树非常自洽。它的模块化构建系统会把大多数驱动、协议栈实现都编进你的应用里而不是依赖系统的.so库。这意味着什么我只需要一个 C 编译器gcc 或 clangmakegit或者直接下载 release tar 包python3部分构建辅助脚本会用到这些在绝大多数 Ubuntu 服务器上都是自带的。我没有用 apt 装过任何东西这也是标题里“没装依赖”的真正含义——不是完全不依赖任何库而是不需要从系统层面新增任何依赖。2. 盘点 Ubuntu 上的无 root 可用条件工具链、TAP 权限与可绕开的依赖确认思路可行之后我没有急着拉代码而是先把这台机器的“家底”摸了一遍。没有 sudo 的环境最忌讳想当然。每一项能力都要亲眼验证。2.1 先确认四件套我依次执行了这些命令确保基本工具链在位gcc --version make --version git --version python3 --version输出都正常。这里多说一句检查gcc时要注意版本RIOT 对老版本编译器可能会有警告但一般不影响编译。如果连 gcc 都没有那就比较麻烦可能需要考虑conda这类用户态工具链或者在别的机器上交叉编译好再拷过来。好在这台机器没让我走到那一步。2.2 /dev/net/tun 权限整个网络方案的胜负手接下来是网络部分的关键。RIOT 的 native 模式一般通过 TAP 设备接入宿主机网络而 TAP 设备依赖/dev/net/tun。我检查了设备节点和当前用户的组ls -l /dev/net/tun id在普通 Ubuntu 上/dev/net/tun通常由root:tun管理。如果当前用户不在tun组打开设备的时候会直接报Permission denied。我这台机器的管理员已经把开发组成员加进了tun组所以我不用 sudo 也能用 TAP但还需要注意另一个权限给 TAP 接口配置 IP 地址。普通用户在宿主机上执行ip addr add通常需要CAP_NET_ADMIN能力。我这次实际是在一个 user namespace 里操作的这个命名空间内拥有完整的网络管理权限所以ip命令可以正常执行。如果你是在传统的多用户宿主机上没有这个权限那就需要管理员在环境层面给你 TAP 操作权限而不是给你 sudo。这和我们说的“没 sudo”不矛盾。2.3 依赖缺口的处理策略不碰系统只动用户目录如果编译过程中真的提示缺少某个工具比如pkg-config我的原则是不碰系统目录在用户目录里就地解决。RIOT 的构建系统对 pkg-config 的使用是可选的很多模块找不到对应库时会自动禁用该特性不影响主体编译。如果必须提供一个工具可以在~/.local/bin下放一个用户态版本然后把它加进PATH。这样既没有污染系统环境也不涉及任何提权操作。后面我在编译章节会讲到一次实际遇到的 python3 定位问题就是通过环境变量解决的。3. 拉取 RIOT 2026.07 并完成首次用户态编译环境盘点完心里有底了。接下来的流程就是标准的 RIOT 开发流程只不过所有操作都限定在当前用户目录以内。3.1 用 git 按 tag 拉取源码我先建立了一个工作目录然后克隆 RIOT 仓库并切到 2026.07 这个 tagmkdir -p ~/riot-work cd ~/riot-work git clone --depth 1 --branch 2026.07 https://github.com/RIOT-OS/RIOT.git cd RIOT git describe --tags正常会输出2026.07。这里用--depth 1是为了只拉这个 tag 的代码省时省流量也避免仓库太大。如果你的网络环境访问 GitHub 有问题可以换成 release tar 包下载后解压效果是一样的。3.2 编译官方示例验证工具链为了确认编译链路没问题我先跑了 RIOT 官方自带的gnrc_networking示例。这个示例包含 shell、IPv6、UDP 等网络功能最适合做冒烟测试cd examples/gnrc_networking make BOARDnative -j$(nproc)这里的BOARDnative就是前面说的关键。编译过程中RIOT 会打印大量模块和特性信息看到类似Building application gnrc_networking for native with MCU native这样的输出就说明构建系统已经正确识别了目标板。编译完成后当前目录下会生成bin/native/gnrc_networking.elf。我直接运行它./bin/native/gnrc_networking.elf终端进入 RIOT shell出现提示符。至此最小闭环已经跑通了。3.3 编译期小插曲python3 路径问题第一次编译时我遇到过一个报错大概是说找不到 python3。原因是这台机器上的python3位于一个比较特殊的路径而 RIOT 的构建脚本默认从PATH中查找。解决方式很简单不需要 sudo只需要在编译时显式指定make BOARDnative -j$(nproc) PYTHONpython3如果你在当前用户环境中使用 pyenv 或 conda也可以用对应的绝对路径。这种问题在用户态编译时很常见思路就是“构建系统找不到某个解释器时用一个环境变量指过去”。4. 把 RIOT 串进 Linux 网络TAP 接口与地址配置编译跑通只是第一步真正的目标是那个 28 Mbit/s 的吞吐数据。要测网络性能先得让 RIOT 的 native 网络栈和 Linux 网络栈“见上面”。4.1 RIOT 启动时发生了什么TAP 接口的生命周期当 RIOT native 进程启动时它会打开/dev/net/tun在 Linux 侧创建一个tap0接口。这个接口的生命周期和 RIOT 进程绑定进程起来接口在进程退出接口消失。在 RIOT shell 里输入ifconfig会看到类似这样的输出Iface 5 HWaddr: 3e:... L2-PDU:1500 MTU:1500 HL:64 Source address length: 6 Link type: wired inet6 addr: fe80::... scope: link VAL注意接口编号是 5这是 RIOT native 以太网接口的常见编号。后面配置地址时都要对着这个编号操作。4.2 给 Linux 侧 TAP 接口配地址RIOT 侧是“设备内部”视角Linux 侧同样需要给tap0配置一个 IPv6 地址这样两边才能互通。ip link set tap0 up ip addr add 2001:db8::1/64 dev tap0这两条命令在普通宿主机上需要 root 能力但我在前面提到的 user namespace 环境里可以正常执行。我知道很多读者会卡在这一步所以多说一句如果你没有这个权限请优先找管理员授权而不是试图绕过系统权限机制。授权 TAP 设备管理权和直接给 sudo是完全不同的两件事。4.3 RIOT 侧配置 IPv6 地址并验证连通性回到 RIOT shell给接口 5 添加一个固定的 IPv6 地址 ifconfig 5 add 2001:db8::2/64不同版本的 RIOT 在 ifconfig 子命令上可能有点差异如果你敲下去提示语法错误可以先输入ifconfig 5 help看看当前支持的子命令一般都有add或者类似的地址添加方式。地址配置完成后再从 Linux 侧 ping 一下 RIOT 的地址ping6 2001:db8::2能通说明链路已经建立。这里有个经验如果 ping 不通先看 RIOT 侧ifconfig输出里INET6地址是否显示为VALvalid再看 Linux 侧ip -6 addr show tap0是否正常最后检查防火墙。我在测试环境里发现 IPv6 邻居发现没问题但某些发行版默认防火墙会挡住转发。5. 28 Mbit/s 的来源一次 UDP 吞吐实测与复现方法网络通了接下来就是重头戏怎么测出 28 Mbit/s 这个数。5.1 为什么不用 iperf3没有 sudo 装不了最顺手的吞吐测试工具当然是 iperf3但这台机器上没有而且我没有 sudo装不了。很多人到这里可能就放弃了但其实系统自带的 python3 已经足够完成一个可靠的 UDP 打流测试。我不打算额外安装任何 Python 包只用标准库socket和time这样完全符合“没装依赖”的设定。5.2 一个极简 UDP 接收计数固件RIOT 官方的gnrc_networking示例虽然自带 shell但它没有专门的吞吐统计功能。为了不因为逐包打印干扰收包速度我专门写了一个极简固件创建一个 UDP socket持续接收数据每秒打印一次收包数量和总字节数。这是main.c#include stdio.h #include stdint.h #include net/sock/udp.h #include ztimer.h #define BUFFER_SIZE 2048 #define PORT 4242 int main(void) { uint8_t buf[BUFFER_SIZE]; sock_udp_t sock; sock_udp_ep_t local { .family AF_INET6, .port PORT, }; if (sock_udp_create(sock, local, NULL, 0) 0) { puts(failed to create UDP socket); return 1; } uint32_t count 0; uint32_t bytes 0; uint32_t start ztimer_now(ZTIMER_MSEC); while (1) { int res sock_udp_recv(sock, buf, sizeof(buf), 100000, NULL); if (res 0) { continue; } count; bytes res; uint32_t now ztimer_now(ZTIMER_MSEC); if (now - start 1000) { double mbps (bytes * 8.0) / (now - start) / 1000.0; printf(rx: %lu pkt, %lu bytes, %.2f Mbit/s\n, (unsigned long)count, (unsigned long)bytes, mbps); count 0; bytes 0; start now; } } return 0; }对应的MakefileAPPLICATION udpbench BOARD ? native RIOTBASE ? $(CURDIR)/../RIOT USEMODULE gnrc_netdev_default USEMODULE auto_init_gnrc_netif USEMODULE gnrc_ipv6_default USEMODULE sock_udp USEMODULE ztimer USEMODULE ztimer_msec include $(RIOTBASE)/Makefile.include这里我假设应用目录是~/riot-work/udpbenchRIOT 源码在~/riot-work/RIOT。如果你把代码放在别的位置记得调整RIOTBASE的路径。编译启动make BOARDnative -j$(nproc) ./bin/native/udpbench.elf然后按照第 4 章的方式给 TAP 接口配好地址不过这次 RIOT 侧只需要一个 IPv6 地址2001:db8::2/64。5.3 Linux 侧 UDP 打流脚本在 Linux 终端里运行一个简单的 Python 脚本向 RIOT 的 UDP 端口 4242 持续发送固定大小的 UDP 数据报import socket import time DEST 2001:db8::2 PORT 4242 PACKET_SIZE 1400 PACKET_COUNT 2500 sock socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) start time.monotonic() sent 0 for _ in range(PACKET_COUNT): sock.sendto(bx * PACKET_SIZE, (DEST, PORT)) sent PACKET_SIZE elapsed time.monotonic() - start mbps sent * 8 / elapsed / 1_000_000 print(fsent {PACKET_COUNT} packets, {sent} bytes, {elapsed:.3f}s, {mbps:.2f} Mbit/s)包大小选 1400 字节是有讲究的。IPv6 基本头 40 字节UDP 头 8 字节以太网 MTU 1500所以 1400 字节的 UDP payload 加上头后不会触发分片又能尽量减少包数量带来的协议开销。5.4 实测数据28 Mbit/s 是怎么读出来的我在同样的条件下重复跑了三次结果如下轮次发包数包大小 (B)耗时 (s)吞吐 (Mbit/s)1500014002.0227.72250014000.9828.63250014001.0127.7三次都稳定在 28 Mbit/s 上下RIOT 侧每秒统计窗口里也打印了相近的值说明测试结果不是偶发抖动而是这个配置下的稳态吞吐。5.5 为什么是 28 Mbit/s瓶颈到底在哪这个数字看起来不大但放在 RIOT 的 native 网络栈里其实是正常的。主要瓶颈有几个单线程事件循环RIOT 的网络栈虽然是模块化多线程设计但每个包的栈间传递、线程切换都有成本。sock_udp 的逐包拷贝每收到一个 UDP 数据报都要从网络栈内部缓冲区拷到应用缓冲区这个拷贝次数多了吞吐自然上不去。TAP 设备读写开销Linux 用户态和内核态之间经过 TAP 设备的路径比纯内核内转发要慢。Python 打流端能力限制Python 逐包sendto本身也不是高性能发包器它只是胜在不需要额外依赖。所以 28 Mbit/s 是“当前测试条件”下的结果不是 RIOT 网络栈的理论上限。如果你调大缓冲区、用批量收包接口或者写一个更高效的 C 语言发包工具数字还能往上走。但作为一次用户态吞吐验证这个数据已经足够说明问题了。6. 普通用户下玩 RIOT这几次踩坑值得记住最后把这些天踩过的坑和总结出来的经验集中说一下。这些东西文档里通常不会写但对后来人很有用。6.1 权限问题的三条退路如果你所在的环境连/dev/net/tun都访问不了网络测试方案会直接卡死。这时候我有三条路可以选找管理员开通 TAP 设备访问权限而不是申请 sudo。把需求说清楚通常管理员是愿意配合的。使用 rootless 容器或 user namespace在命名空间内部获得网络管理能力。这也是我在这次测试里实际采用的方式。如果只是验证 RIOT 编译和基础运行不测网络也完全可行。单纯跑一个hello-world或者 shell 交互是完全没有权限门槛的。6.2 接口编号别想当然RIOT 的 ifconfig 里不同板级支持包的接口编号差别很大。有的板子从 1 开始native 的以太网接口在 2026.07 上默认是 5但如果你同时启用了其它网络设备编号可能会变。配置地址之前一定先运行ifconfig看清楚不要直接照着别人的命令抄。我在测试时也差点把地址加到别的接口上还好先看了一眼输出。6.3 用户目录编译的隐藏坑用户态编译听起来简单但有几个坑容易在不经意间踩到磁盘配额RIOT 编译过程中会生成大量中间文件如果用户目录有配额限制中途可能报磁盘满。PATH不一致通过 cron 或远程 shell 执行编译时PATH可能不包含用户目录下的工具路径。并行编译内存占用-j$(nproc)在内存小的虚拟机上可能直接把编译进程杀掉必要时降为-j2。我的建议是工作目录单独放在一块空间足够的磁盘上编译前先df -h .看一眼余量。RIOT 这种项目不算大但多次清理重编之后也会积累不少中间产物。6.4 一个能节省半小时的小技巧RIOT 的构建系统支持增量编译改完main.c之后重新执行make会自动只重新编译改动部分。但如果你修改了 Makefile 里的USEMODULE强烈建议先做一次全量清理再编译make BOARDnative clean all -j$(nproc)不清洗的话模块变化可能导致一些奇怪的链接错误而且这类错误往往不会直接告诉你“请 clean 后再试”排查起来非常费时间。经验之谈先 clean 再编能省很多事。最后再分享一个我当时的小习惯每次跑完吞吐测试后我都会把 RIOT shell 里的ifconfig输出和 Linux 侧的ip -6 addr show tap0一起截图保存。因为这类用户态环境下的网络测试环境差异对结果影响极大留好现场信息后面排查问题会轻松很多。
