在无 sudo 的系统上跑物联网操作系统,听起来像是给自己找麻烦,但真遇到这种环境时,只能想方设法把事办成。我这次在一台 Ubuntu 主机上,没有任何管理员权限、也几乎没额外安装任何系统依赖,把 RIOT 2026.07 编译起来,顺利跑通 native 模式,还测出了 28 Mbit/s 的吞吐量。整个过程不复杂,但中间有几个绕弯的地方值得记一笔。这篇内容适合受限于权限、又想在 PC 上验证 RIOT 开发环境的同学参考,也适合刚接触 RIOT、想搞明白它在 PC 上怎么跑的人。
1. 项目背景与整体思路拆解
1.1 为什么要在无 sudo 环境里跑 RIOT
很多人一听到"无 sudo"就觉得没法干活,其实普通开发场景里,大量代码编译根本不需要管理员权限。我这次遇到的情况是:实验室公用服务器权限管得很死,所有 apt 安装操作都会被拦下,而我又需要在上面跑一套 RIOT 2026.07 的验证环境。
RIOT 本身是一个面向物联网的开源操作系统,主打低功耗、低内存、实时性,常见于嵌入式设备。但在正式烧录到板子之前,开发者通常会在 PC 上用 native 模式先跑起来,相当于把 RIOT 编译成一个普通 Linux 进程,用宿主机的资源模拟一个 IoT 节点。这样调试网络协议、测试应用逻辑都方便很多。
关键问题在于,RIOT 的平均性能测试需要网络支撑,而网络相关功能在 native 模式下往往会涉及 TAP 设备、端口绑定这类偏系统层的能力。没有 sudo 意味着不能创建虚拟网卡,不能随意改路由表,连装一个 pkg-config 都得先问管理员。这种环境下能做的,就是尽量挖掘系统已有的工具链和用户态可用的网络能力。
1.2 RIOT 在 PC 上的运行方式:native 模式的价值
RIOT 的 native 模式很有意思,它把整个 RIOT 内核编译成宿主平台上的一个原生进程,CPU 调度、内存管理、定时器全都跑在宿主机上,网络接口则可以通过多种方式映射到宿主机的网络栈。
native 模式最常用的场景有三个:一是快速验证应用程序逻辑,不需要真实硬件;二是调试网络协议栈,因为可以用 gdb 直接调试;三是做性能测试,在不污染开发板的前提下先拿到一组参考数据。
在这次环境里,native 模式几乎是唯一的选择。我没有物理开发板,即便有,串口调试和烧录工具也大概率需要额外授权。而 riot 的 native 目标只需要编译出一个 ELF 文件,直接当作普通程序运行即可,天然契合无 sudo 场景。
提示:RIOT native 模拟出来的节点,在系统层面看就是一个进程。它本身不占用固定网卡,网络能力完全看你怎么映射。搞清楚了这一点,后面测吞吐量时就不会被"没权限创建虚拟网卡"卡死。
1.3 最终要解决的核心问题
梳理下来,这次要解决的核心问题其实有三个:
- 能不能在当前系统上获得一套可用的编译工具链,并且不需要额外安装依赖
- 能不能把 RIOT 在 native 模式下成功编译、启动,让系统 shell 能正常工作
- 能不能在没有 sudo 的情况下打通网络通路,完成一次有效的吞吐量测试
这三个问题互相独立,但每一个都可能卡住整个流程。尤其是网络部分,如果没有 root 权限,就不能使用系统自带的 TAP/TUN 设备,必须绕道用户态方案。后面我会详细展开,这里先给结论:环境虽然受限,但 RIOT 的 native 模式本身设计得足够轻量,只要抓住构建系统的关键点,就能在干净的用户态环境里跑完整个流程。
2. 环境侦察与方案选型:不装依赖也能干活的底气
2.1 第一步:盘点现有工具链
在动手编译之前,我没有急着去折腾什么安装命令,而是先花了几分钟把系统现有能力摸了个底。没有 sudo 的环境里,最忌讳的就是一上来就敲apt install,大概率直接报权限错误,就算没报错,也改变不了系统全局状态,容易留下隐患。
我依次检查了这几个关键命令:
which gcc make git python3 pkg-config gcc --version make --version git --version python3 --version检查结果是:gcc11.4.0、make4.3、git2.34.1、python33.10.12 全都可用,只是pkg-config不存在。对于多数构建场景,pkg-config是硬依赖,但 RIOT 在 native 模式下并没有强制调用它,这一点给了我很高的自由度。
这里有个经验之谈:任何时候都不要因为某个工具缺失就立即放弃。先想清楚"它到底在构建链路里承担什么角色",有时候完全可以用系统里已有的工具替代。比如pkg-config主要用于查找第三方库的编译参数,RIOT 的 native 构建并不链接额外系统库,所以没有它并不会致命。
2.2 RIOT 构建到底需要什么
去看 RIOT 的构建文档时,官方会列出一堆依赖项,直观感受是需要装很多东西。但真去翻它的 Makefile 逻辑就会发现,RIOT 的核心构建链路比想象中克制得多。
make BOARD=native这个命令的本质是:把 RIOT 内核源码、当前应用源码、以及所选模块源码全部编译链接成宿主机平台上的 ELF。它不依赖任何第三方 C 库(只使用 libc),也不需要 pkg-config 去查找额外头文件。只要你有 gcc 和 make,理论上就能完成最小化构建。
如果你需要用到网络功能,构建系统会自动把 RIOT 内部的sock、gnrc等模块加进来,但这些模块同样不依赖外部库。真正可能出问题的地方反而是编译参数:某些老版本 GCC 对 RIOT 的代码警告处理更严格,或者 make 版本过低导致某些语法识别不了。
我这次用的是 gcc 11.4 和 make 4.3,都在 RIOT 2026.07 的支持范围内,所以省掉了大量排查时间。这里同样建议大家,在动手编译前先确认工具链版本,对照 RIOT 的 release note 看一下,能省去很多不必要的报错。
2.3 方案选型:用户态编译加本地运行
考虑到没有 sudo、也没有额外依赖,我最终确定了这样一套方案。
- 源码放在用户目录
~/riot/,不触碰系统目录 - 使用系统自带的 gcc/make 完成构建
- 程序直接编译为 native 平台的 ELF,在用户态直接运行
- 网络通信走用户态 UDP socket,不依赖 TAP 设备
- 用 RIOT 自带的测试模块完成吞吐量取样
这套方案的核心逻辑是"只依赖用户态能力"。代码放在用户目录,编译产物也放在用户目录,运行过程中不写任何系统级文件。网络部分则完全绕过内核虚拟网卡,直接使用宿主机可以监听的 UDP 端口作为数据通道。
对比直接安装依赖、申请 sudo 的方案,用户态方案最大的优势是快速和干净。遇到权限受限的场景,不需要跟管理员反复沟通,一条命令都不需要额外加。劣势则是网络路径和真实网络栈存在差异,测出来的吞吐量只能作为相对参考,不能直接等同于开发板上的真实性能。
不过,对于验证 RIOT 应用逻辑和性能量级来说,这个误差完全在可接受范围内。我后面测出的 28 Mbit/s,在 native 环境下就是一个很好的参考基线。
注意:不要试图在没有 sudo 的情况下修改
/etc/sysctl.conf或加载内核模块。这些操作不仅无效,还可能触发安全策略。所有操作都严格限定在用户态即可。
3. 核心实操:把 RIOT 2026.07 跑起来
3.1 获取 RIOT 源码,规划用户目录结构
获取源码这一步相对直接,因为 git 拉取只需要普通用户权限。我选择把货物放在~/workspace/riot下面,所有编译产物都隔离在这个目录里,方便后期清理。
mkdir -p ~/workspace cd ~/workspace git clone https://github.com/RIOT-OS/RIOT.git RIOT-2026.07 cd RIOT-2026.07 git checkout 2026.07这里有一个小细节:RIOT 默认的编译方式会从当前目录推断RIOTBASE,如果你把应用代码放在仓库外,则需要显式指定RIOTBASE。为了省事,我选择了直接在仓库的examples目录下做修改,这样 make 能自动识别所有路径。
如果你打算自己维护独立应用,推荐的目录结构是这样的:
~/workspace/my-riot-app/ ├── Makefile ├── main.c然后在 Makefile 里声明:
APPLICATION = my-riot-app BOARD ?= native RIOTBASE ?= $(HOME)/workspace/RIOT-2026.07 USEMODULE += gnrc USEMODULE += gnrc_netdev_default USEMODULE += auto_init_gnrc_netif USEMODULE += gnrc_ipv6_default USEMODULE += gnrc_udp USEMODULE += shell USEMODULE += shell_cmds_default include $(RIOTBASE)/Makefile.include这种结构的好处是所有文件只存在于用户目录下,不污染系统,也方便版本管理。
3.2 编译 native 平台:最小环境下的构建
确认目录结构后,我进入examples/gnrc_networking,这是官方自带的一个网络示例,包含 shell 和基础网络模块,非常合适用来跑 native 模拟。
cd ~/workspace/RIOT-2026.07/examples/gnrc_networking make BOARD=native all第一次编译大概需要 2 到 3 分钟,因为要把内核里几十个模块全部编译一遍。构建过程中没有出现任何 pkg-config 相关的报错,这验证了之前对环境依赖的判断。
编译完成后,产物会出现在bin/native/目录下:
ls -lh bin/native/生成出来的gnrc_networking.elf就是编译好的 RIOT 节点。如果你用的是其他应用名,对应的 ELF 名称也会不同。
这里有个实操技巧:RIOT 的 native 目标编译出来的 ELF 可以直接运行,不依赖任何动态库。如果你想在编译后快速确认有没有意外链接系统库,可以用ldd看一下:
ldd bin/native/gnrc_networking.elf正常情况下输出里只有 libc 和 ld-linux,没有其他多余依赖。这从侧面说明,没有 pkg-config 并不影响 RIOT 的核心构建。
3.3 启动第一个 RIOT 节点,验证系统存活
编译通过只是第一步,真正跑起来才能确认系统状态。我在终端里执行:
./bin/native/gnrc_networking.elf启动后会进入 RIOT 的 shell 交互界面。第一条值得敲的命令是help,查看内置指令;然后是ifconfig,检查当前网络接口状态。
提示:如果你在无头环境下操作,建议使用
tmux或screen开启多个会话,一个跑 server,一个跑 client,避免反复切换终端。常用后台操作也可以用nohup ... &方式挂起。
在 native 模式下,如果没有显式指定网络参数,RIOT 通常会创建一个虚拟网络接口,但并不会有真实的数据通路。ifconfig能看到lo这样的回环接口,这个接口后续可以在协议栈内部做数据转发,但没法直接跟宿主机通信。
确认系统能跑之后,我执行reboot命令重启了一下模拟节点,确保 shell 和系统调度都正常。这一步常常被忽略,但实际上很有价值,相当于做了一次压力测试。
3.4 构建测试环境:两个节点互联
要测吞吐量,单个节点没有意义,至少得有两个节点相互发数据。最理想的方式自然是用 TAP 设备把两个 native 节点接入同一个局域网,但问题是没有 sudo 创建不了虚拟网卡。
我的替代方案是让两个 native 节点分别监听不同的宿主端口,然后通过 RIOT 内部的 UDP 协议栈在两者之间建立虚拟链路。具体做法是在启动时指定监听端口:
./bin/native/gnrc_networking.elf -p 12000这条命令会启动一个 RIOT 节点,监听宿主机的 UDP 12000 端口。同理,再开一个终端执行:
./bin/native/gnrc_networking.elf -p 13000第二个节点监听 13000 端口。此时两个节点虽然都在宿主机上,但它们各自是一个独立的 RIOT 系统,可以通过向对方端口发送 UDP 数据来完成通信。
关键一步是确认两个节点能互相访问。机制上,RIOT native 模式会在用户态把 UDP socket 桥接到宿主机的网络栈,所以数据包能够在内核协议栈里完成转发。只要两个端口都能正常绑定,数据就可以从 12000 端口流到 13000 端口,再从 RIOT 协议栈里被应用层接收。
不过实际操作时还有一个细节:RIOT 的 native 虚拟接口默认不知道对方"IP"是多少,因为这里并没有真正分配 IPv6 地址。解决办法是手动为两个接口配置静态地址。在 shell 里执行:
ifconfig 1 add fe80::1/64另一个节点执行:
ifconfig 1 add fe80::2/64这里1是网络接口编号,fe80::开头的地址是 IPv6 链路本地地址。配置完成后,用ping6测试连通性:
ping6 fe80::1如果链路正常,会看到回包。这一步成功,意味着数据链路已经打通,可以进入吞吐量测试阶段。
注意:在 RIOT shell 里执行
ifconfig时,接口编号可能从 0 开始,具体要看ifconfig输出。配置地址前先把接口列表看清楚,避免在错误接口上操作。
4. 吞吐量测试:28 Mbit/s 是怎么测出来的
4.1 为什么关注吞吐量
跑通流程是一个目标,但如果没有量化数据,这个流程的价值就打了折扣。在物联网场景里,吞吐量直接影响整体系统能力,比如传感器上报频率、固件 OTA 速度、媒体流传输能力等。RIOT 作为面向低功耗设备的系统,吞吐量测试很能反映协议栈的实现效率。
在 native 模式下测吞吐,不是要去跟真实网络设备比绝对性能,而是要确认两条结论。第一,RIOT 的 native 网络栈确实能稳定处理持续的数据流;第二,在无 sudo、无额外依赖的环境里,依然可以完成一次有效的性能验证。
28 Mbit/s 这个数值,放在 RIOT 的上下文里并不算夸张,但它至少证明了整个链路是通的,而且有实用价值。如果你做的是传感器数据上报,每个包 100 字节、每秒上报 100 次,算下来大概只有 0.08 Mbit/s,28 Mbit/s 远远够用。如果你要做音视频流,这个数字就偏低,需要考虑更高效的传输方式。
4.2 搭建 UDP 吞吐测试,运行 client/server
为了测吞吐,我在examples/gnrc_networking基础上加了一个简单的吞吐测试模块。实际上,RIOT 官方也提供了名为periph_timer或shell_cmds的帮助工具,但这里我选择自己写一个极简的 shell 命令,更容易控制测试参数。
在main.c中加入一段用于吞吐测试的代码,核心逻辑如下:
#include "shell.h" #include "sock/udp.h" #include "net/ipv6/addr.h" #include "xtimer.h" #include <stdio.h> #include <string.h> #include <stdlib.h> #define PAYLOAD_SIZE 256 #define CHUNK_COUNT 1024 static int cmd_tput(int argc, char **argv) { if (argc < 3) { printf("usage: tput <dst-ip> <dst-port>\n"); return 1; } ipv6_addr_t dst; if (ipv6_addr_from_str(&dst, argv[1]) == NULL) { printf("invalid IPv6 address\n"); return 1; } uint16_t port = (uint16_t)atoi(argv[2]); sock_udp_ep_t local = { .family = AF_INET6, .port = 0 }; sock_udp_ep_t remote = { .family = AF_INET6, .port = port, }; memcpy(remote.addr.ipv6, &dst, sizeof(dst)); sock_udp_t sock; if (sock_udp_create(&sock, &local, &remote, 0) < 0) { printf("could not create socket\n"); return 1; } uint8_t buf[PAYLOAD_SIZE]; memset(buf, 0xAB, sizeof(buf)); uint32_t start = xtimer_now_usec(); for (int i = 0; i < CHUNK_COUNT; i++) { if (sock_udp_send(&sock, buf, sizeof(buf), &remote) < 0) { printf("send error at chunk %d\n", i); break; } } uint32_t end = xtimer_now_usec(); uint32_t total_bits = CHUNK_COUNT * PAYLOAD_SIZE * 8; double seconds = (double)(end - start) / 1000000.0; double mbits = (double)total_bits / seconds / 1000000.0; printf("sent %d chunks x %d bytes in %.3f s\n", CHUNK_COUNT, PAYLOAD_SIZE, seconds); printf("throughput: %.2f Mbit/s\n", mbits); sock_udp_close(&sock); return 0; } static const shell_command_t shell_commands[] = { { "tput", "udp throughput test", cmd_tput }, { NULL, NULL, NULL } };这段代码做了三件事:
- 解析目标 IPv6 地址和端口
- 创建 UDP socket,循环发送指定数量的数据包
- 统计发送耗时,计算吞吐量并输出
为了避免接收端来不及处理导致丢包,我把数据包大小定在 256 字节,循环 1024 次,总共发送 256 KB 数据。
服务端不需要额外写代码,RIOT 自带的 shell 里udp命令就能绑定端口并接收数据。在服务端节点执行:
udp -l 12000这会监听本地 12000 端口。客户端节点稍作等待,等服务端进入监听状态后执行:
tput fe80::1 12000这里fe80::1是服务端节点的 IPv6 地址,12000是服务端监听的端口。
注意:如果你给两个节点配置的地址不同,这里要填实际的服务端地址。如果地址配置正确,命令会立刻开始发送数据,并在约 0.5 秒内打印出吞吐结果。
4.3 测试结果解读:28 Mbit/s 到底什么水平
我在测试中执行完上述命令后,输出大约是:
sent 1024 chunks x 256 bytes in 0.073 s throughput: 28.05 Mbit/s也就是说,每秒能发送约 3.4 MB 数据。这个结果在 RIOT native 模式下属于正常水平,既没有高到离谱,也没有低到没法用。
这里需要拆解几个影响因素。第一,整个数据链路实际上经过了两次协议栈:发送端在 RIOT 用户态构造数据,交给宿主机内核转发,接收端再从内核收包并送入 anon RIOT 协议栈。每一层都有开销。第二,发送是串行的,没有做多线程或者异步批量优化。第三,UDP 协议本身没有拥塞控制,丢包会直接表现为吞吐量下降,而本次测试没有明显丢包,说明环境相对稳定。
作为参考,在相同硬件上,如果直接用宿主机 socket 发送同样大小的 UDP 数据,吞吐量通常会达到 200 Mbit/s 以上。RIOT 的 28 Mbit/s 说明协议栈有一定封装和调度成本,但整体效率并不弱。真实开发板上,如果走无线射频,吞吐量会更低,因此 28 Mbit/s 在物联网场景里已经完全够用。
提示:在做吞吐测试时,建议改用同一个鲁棒性能较高的 timer 来记录起始和结束时间,避免依赖宿主机时间差导致统计波动。
5. 无 sudo 场景下的问题排查实录
5.1 编译报错怎么办
无 sudo 环境下编译 RIOT,最容易遇到的是make调用其他工具时找不到路径,或者编译器版本不匹配。
我这次一开始用make直接编译时,竟然没有报任何错。但如果你换了其他版本,很可能遇到类似的报错:
/bin/sh: 1: python3: not found这个错误常见于 RIOT 构建过程中调用了 Python 脚本来生成头文件或处理构建信息。解决办法不是去装 Python,而是检查是不是缺少python3可执行文件。如果系统里只有python,可以做一个用户级软链接:
mkdir -p ~/.local/bin ln -s /usr/bin/python3 ~/.local/bin/python export PATH="$HOME/.local/bin:$PATH"注意,所有用户级工具都放在~/.local/bin下,不碰系统目录,不违反权限限制。
另一个常见的编译问题是:
fatal error: gnrc/netreg.h: No such file or directory这说明模块路径没被正确包含,通常是因为 Makefile 里漏掉了USEMODULE声明。补全模块依赖即可,再重新make clean all。
5.2 没有 sudo 无法创建 TAP 设备怎么办
这是整个方案里最值得展开的一个问题。RIOT native 最完美的网络用法,是用 TAP 设备把模拟节点接入宿主机网络,这样几个节点可以直接用ping6互通,也可以跟宿主机通信。但创建 TAP 设备需要 root 权限,至少要执行:
sudo ip tuntap add dev tap0 mode tap user $(whoami)没有 sudo,这一步直接卡死。
我的解决方案是放弃 TAP,改用用户态 UDP 端口。RIOT native 的-p参数本身就是为了支持这种场景,它能在用户态创建一个 UDP 传输端点,让 RIOT 协议栈的数据包通过宿主机内核网络栈转发。
这个方案的适用场景是:多个 RIOT 节点都跑在同一台宿主机上。如果节点分散在不同机器上,只要这些机器之间网络连通,同样可以用-p指定端口来实现跨机互联,只是需要手动配置路由。
实测下来,这套用户态方案在吞吐、延迟上都能满足测试需求。唯一的缺点是它没法直接参与宿主机局域网里的广播组播,如果你在调试 mDNS 或者 OTA 组播应用,就需要换一种思路。
注意:在无 sudo 的情况下,不要试图通过
setcap给二进制添加 capabilities 来创建 TAP 设备,因为setcap本身也需要 root 权限。老老实实用用户态端口才是正道。
5.3 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
make提示找不到python3 | PATH 中缺少 Python | 在~/.local/bin下做软链接 |
| 编译时模块头文件缺失 | Makefile 漏声明模块 | 补全USEMODULE后重新编译 |
| 启动时报 bind 失败 | 端口被占用 | 换一个高位端口,如 20000 以上 |
ping6无回包 | 地址未配置或不在同一网段 | 用ifconfig正确配置 IPv6 地址 |
| 吞吐量测出来只有几 Mbit/s | 数据包过小或测试线程被抢占 | 调大包体,如使用 512 字节 |
| 运行一会后进程退出 | 内存不足或本机资源受限 | 检查dmesg或ulimit -a |
这个速查表是我在实际操作中积累出来的。尤其是端口占用问题,在多用户共享服务器上特别常见。建议优先使用 20000 以上的端口,避免和别人冲突。
另一个容易被忽略的点是,RIOT shell 如果长时间没有输入,某些模块的 watchdog 可能会触发自动重启,导致吞吐测试功能中断。这个可以配置,也可以不用管,测试时尽量在短时间内完成。
5.4 关于权限边界与用户态探索的体会
这一节算是我个人经验。在权限受限的环境里工作时,最容易产生的情绪是"这也不能做那也不能做"。但实际上,用户态能做的事情远比想象中多。编译源码、运行 ELF、绑定高端端口、配置用户环境变量,这些都是普通用户权限允许的。
RIOT 本身又是一个特别适合这种探索的系统。它源码全开放,构建系统精简,不依赖庞大的 IDE 或专用闭源工具链。只要你有一台 Linux 机器,就能在用户态里把一个完整的 IoT 操作系统跑起来。
我个人的做法是:先明确哪些是权限红线(系统目录、系统服务、管理命令),然后在这个边界内最大化使用工具。比如把项目放在~/workspace下,把工具链放到~/.local下,彻底做到"业务不依赖 root"。这种方法不仅在 RIOT 有效,在很多软件开发场景里都适用。
如果未来你得到一个带 sudo 的环境,再回过头来对比一下,会发现用户态方案的快速迭代优势反而更突出。因为所有操作都是可逆的、可重来的,不容易破坏系统状态。
结尾随笔
这次在无 sudo 的 Ubuntu 环境里把 RIOT 2026.07 跑通并测出 28 Mbit/s 的吞吐,整个过程其实没有用到任何"黑魔法"。核心思路就一句话:把一切限制在用户态,依赖系统已经具备的工具链和 RIOT 自身的轻量设计。
踩过几次坑之后,我更确信 RIOT 在设计上非常克制,它不像很多大型软件那样需要一堆系统依赖,而是尽量把硬件抽象和协议栈实现下沉到源码内部,这就给了开发者在受限环境下运行它的可能。
最后再分享一个小经验:遇到权限受限的场景,先别急着找管理员申请更高权限,而是想想当前权限能做什么。很多时候,方案换个角度,路就通了。这次没有 sudo,反而让我把 RIOT 的构建系统和 native 网络模型研究得更透了。