没有sudo也能跑RIOT:用户态工具链搭建与网络吞吐实测
2026/9/7 11:31:48 网站建设 项目流程

前几天我在一台共享开发机上折腾 IoT 系统,sudo -l一敲直接提示当前用户不在 sudoers 列表里,apt install想都不用想。更难受的是这台 Ubuntu 连build-essential都没装全,gcc 有但不是最新,make 直接没有。旁边同事劝我别折腾了,说没 sudo 连依赖都装不了,RIOT 这种带完整网络栈的嵌入式系统更别想跑起来。我偏不信,折腾了一晚上,RIOT 2026.07 还真跑起来了,还在native模式下测出了约 28 Mbit/s 的 UDP 吞吐。整个过程没碰过一次 sudo,也没往系统目录写任何东西。这篇文章就把完整思路和踩过的坑记录下来,给同样被困在受限环境里的人参考。

1. 没 sudo 的 Ubuntu,凭什么能跑 RIOT

1.1 我面对的真实处境

先交代一下环境:一台 Ubuntu 22.04 的共享开发服务器,我只有普通用户权限。查看/etc/sudoers是不可能的,sudo -l明确提示没有权限,apt install更不用说——连apt update都会因为无法写入/var/lib/apt/lists而失败。系统里有 Python 3.10,有 gcc 12,但没有 make,没有 pkg-config,没有 cmake,也没有正经的 32 位兼容库。

这时候如果按常规思路,第一反应肯定是"没 sudo = 装不了依赖 = 别想编译 C 项目"。但这个等式其实有个漏洞:很多依赖并不需要装到系统目录,完全可以放在用户目录里。只要把编译器、make、Python 这些构建工具在用户空间凑齐,一个普通的 C 项目就能正常编译。RIOT 恰好就属于这一类。

1.2 RIOT 这个项目没要求你必须拥有 root

RIOT 是什么?它是一套面向物联网的嵌入式操作系统,核心用 C 编写,提供多线程调度、GNRC 网络协议栈、各类外设驱动抽象、以及一套类似 POSIX 的sock网络 API。它的源码仓库本身就是一堆 Makefile 加 C 文件,编译过程中并不强制写系统路径,也不强制调用apt。真正需要依赖的只有三样:GNU make、C 编译器、Python 3。

而且 RIOT 有个特别适合受限环境的设计——native移植。所谓native,就是把 RIOT 当成一个普通 Linux 用户态进程来编译和运行。它模拟了 RIOT 的 CPU、定时器、UART 等抽象,跑起来之后你会看到 RIOT 的 shell 提示符,但底层只是一个 ELF 可执行文件。整个过程不需要开发板,不需要交叉编译器,也不需要 root 权限去访问硬件。

这就引出了本文的核心结论:RIOT 的 native 模式 + 用户态工具链 = 在没 sudo 的 Ubuntu 上照样跑起来。后面所有内容都是围绕这个结论展开。

2. 无 sudo 环境下补齐工具链的几种办法

2.1 先把系统里已有的能力"白嫖"出来

在考虑安装任何东西之前,先把系统已经有的工具盘一遍。这一步很多人会忽略,但往往能省掉大量时间。我一般按下面顺序检查:

gcc --version g++ --version make --version python3 --version clang --version git --version ld --version pkg-config --version

不同机器差异很大。有的机器只有 clang 没有 gcc,有的机器有 make 但没有 python3,还有的机器啥都有但 32 位库不齐全。我碰到的这台机器情况是:gcc 12 可用,git 可用,python3 可用,唯独 make 缺失。这已经算是比较好的局面了,因为只需要补一个 make。

如果你连 gcc 都没有,也别慌。可以用 conda 或者用户级源码编译把整个工具链装到$HOME下,后面的小节会讲具体怎么做。总之先盘点现有资源,再决定补什么,而不是一上来就照着网上的教程跑sudo apt install build-essential

2.2 Miniconda 补齐 make/gcc/python3

在没有 sudo 权限的机器上,Miniconda 是我最常用的"用户态软件仓库"。它安装到$HOME目录,不需要 root,自带的conda命令可以安装大量编译工具和库,而且不会污染系统环境。

安装 Miniconda 的步骤非常简单:

wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh sh Miniconda3-latest-Linux-x86_64.sh -b -p $HOME/miniconda3 export PATH="$HOME/miniconda3/bin:$PATH"

安装完成之后,用conda install补工具链:

conda install -c conda-forge make cmake pkg-config python

这里有个经验:conda install gcc不一定是你想要的。conda 里的gcc包只是包装器,真正可用的是gcc_linux-64这类带平台后缀的包,它会拉取完整的用户态编译链:

conda install -c conda-forge gcc_linux-64 gxx_linux-64

装完之后,编译 C 项目时可能需要指定编译器路径,或者在当前 shell 里激活 conda 环境让编译器进入PATH。因为 RIOT 默认找的是gcc,所以在 conda 环境里直接用gcc命令通常能对上。

2.3 只缺 make 时,下载静态编译版本就够了

如果系统里只是缺 make,犯不上为了一个 make 去装整个 conda。GNU make 有静态编译的二进制可以直接下载到用户目录。很多第三方工具站提供这种构建产物,或者你可以从同一台服务器上其它用户的目录里拷贝一份(前提是对方愿意共享)。更稳妥的做法是用 Python 的 pip 安装cmake?这只能解决 cmake,不解决 make。

我当时的做法比较朴素:在一个有完整工具链的容器环境里自己编译了一份静态 make,然后传到这台服务器上,放到$HOME/local/bin,再把PATH指过去。如果你没有容器环境能编,也可以找 conda 的 cache 路径,把 conda 包装里的 make 二进制抠出来单独用。总之 make 只是一个二进制,放进用户目录就能跑。

一个需要注意的坑是:不要为了省事把系统自带的/usr/bin/make覆盖掉,你没权限覆盖是一回事,就算有权限也不要覆盖,容易把系统搞坏。用户级工具一律放用户目录,靠PATH优先级去"覆盖"。

3. RIOT 2026.07 的获取与 native 构建

3.1 版本号背后的信息

RIOT 的版本号格式是年份加月份,每年一般发两个版本,比如 2026.01、2026.07。2026.07 就是 2026 年 7 月发布的版本,通常包含前半年的新功能、新板卡支持和协议栈修复。这种命名方式很直观,看到版本号就能知道它的发布时间线。

获取源码主要有两种方式:

# 方式一:git 拉取指定分支 git clone --depth 1 --branch 2026.07 https://github.com/RIOT-OS/RIOT.git # 方式二:下载 release tarball wget https://github.com/RIOT-OS/RIOT/releases/download/2026.07/RIOT-2026.07.tar.gz tar xzf RIOT-2026.07.tar.gz

在无 sudo 环境下,这两种方式都没问题,源码放在$HOME下即可。git clone 的好处是后续可以随时切到master分支,如果你只是要一个稳定版本,直接下 tarball 更省流量。

3.2 为什么我选 native 而不是直接交叉编译

RIOT 支持一大堆板卡,比如各种 STM32、nRF52、ESP32。但要在没 sudo 的机器上编译真实板卡固件,通常会引入交叉编译工具链(arm-none-eabi-gcc)和烧录工具,这些工具用 conda 不一定能装全,烧录还要连硬件。我这种环境显然不适合。

native移植则完全不一样。它编译出来的是一个跑在 Linux 用户态的 ELF,本质上就是你机器上的一个普通进程。它模拟了 RIOT 的定时器、UART、网络设备等抽象层,特别适合在没有硬件的情况下验证协议栈逻辑和应用层代码。我这次只想验证"RIOT 能不能跑起来、网络吞吐大概什么量级",所以 native 是唯一合理的选择。

这里涉及到一个很关键的细节:RIOT 里有两个 host 端 board,一个是native,一个是native64。老一点的习惯里native默认按 32 位编译,如果你系统缺gcc-multilib,链接阶段会报错。而native64明确按 64 位编译,不需要 32 位兼容库。在受限 Ubuntu 上,优先用BOARD=native64,能避开一大类链接问题。

3.3 从 clone 到 make 的完整过程

以自带的hello-world示例为例,完整命令如下:

cd RIOT/examples/hello-world make BOARD=native64 all

如果一切正常,会在bin/native64/下生成hello-world.elf。直接运行:

./bin/native64/hello-world.elf

你会看到类似这样的输出:

RIOT native 2026.07 main(): This is RIOT!

这就说明核心环境已经通了。hello-world没有网络功能,但足以验证工具链和 native 移植是否正常。我建议第一次跑 RIOT 的人先从它开始,不要一上来就跑网络示例,否则环境问题、代码问题、链路问题混在一起,排查起来非常痛苦。

确认 hello-world 能跑之后,再切换到网络示例:

cd ../gnrc_networking make BOARD=native64 all

gnrc_networking是 RIOT 里最常用的网络示例,带 shell 交互,支持ifconfigudptxtsnd等命令,后续吞吐测试就基于它改。

4. 无 sudo 环境下绕开系统库链接错误

4.1 典型报错:/usr/bin/ld: cannot find crt1.o

在实际编译过程中,最让我头疼的不是工具链缺失,而是链接阶段报"找不到系统库文件"。典型报错长这样:

/usr/bin/ld: cannot find crt1.o: No such file or directory /usr/bin/ld: cannot find Scrt1.o: No such file or directory

crt1.o是 C 运行时启动文件,一般由libc6-dev提供,位于/usr/lib/x86_64-linux-gnu/。共享服务器或者精简版镜像经常没装这个包,于是链接时 ld 就找不到入口。

4.2 排查链路:先用 gcc 定位库路径

遇到这类报错,第一反应不应该是去下载 crt1.o 硬塞进目录,而应该搞清楚 gcc 到底在哪些路径找库。可以用下面的命令看:

gcc -print-file-name=crt1.o gcc -print-search-dirs ld --verbose | grep SEARCH_DIR

如果gcc -print-file-name=crt1.o输出的还是crt1.o而不是一个绝对路径,说明当前 gcc 的搜索路径里确实没有这个文件。

另一种常见报错是:

bits/libc-header-start.h: No such file or directory

这说明缺少gcc-multiliblibc6-dev-i386之类的 32 位开发库。如果你用BOARD=native(32 位)就会出现。解决方式很简单:切换成BOARD=native64。这也是我在前文反复强调的原因,在受限环境中省心太多。

4.3 无 sudo 情况下的真正解法

如果系统连 64 位的基础libc6-dev都不完整,又没 sudo 装不了系统包,怎么办?我试过两种可行方案:

第一种是用 conda 的完整 sysroot。安装gcc_linux-64sysroot_linux-64之后,conda 会提供一个用户态的系统根目录,里面包含完整的 crt 文件和 libc:

conda install -c conda-forge gcc_linux-64 sysroot_linux-64

然后用 conda 的编译器而不是系统的 gcc:

export CC=$HOME/miniconda3/bin/x86_64-conda-linux-gnu-cc make BOARD=native64 CC=$CC all

这样做的思路很简单:RIOT 编译时把 conda 工具链里的crt1.olibc都找齐了。不过这种方案兼容性有时候会折腾我一下,因为 conda 的 sysroot 路径比较特殊,RIOT 的构建系统不一定能自动识别,需要你手动调整CFLAGSLDFLAGS

第二种更轻量的办法:从同架构的另一台正常 Ubuntu 机器上,把缺失的crt文件和动态库拷贝过来,放在用户目录,然后用-L参数指定搜索路径。比如:

make BOARD=native64 all \ CFLAGS="-I$HOME/sysroot/usr/include" \ LINKFLAGS="-L$HOME/sysroot/usr/lib/x86_64-linux-gnu"

这个方法能用,但依赖版本必须匹配,否则会冒出GLIBC_x.xx not found之类更诡异的问题。我的经验是:能找管理员补系统包就补,补不了就优先换 native64,实在不行才手动搬库文件。

5. 吞吐测试:28 Mbit/s 是怎么测出来的

5.1 测试目标与链路设计

跑通 hello-world 只是第一步,我想看 RIOT 的网络栈在 native 模式下到底能跑多快。这就涉及到网络链路的搭建。

RIOT native 实例要接入网络,最常规的方式是使用 TAP 设备。TAP 是 Linux 二层虚拟网卡,RIOT 作为一个用户态进程可以通过/dev/net/tun把数据包读写出来。通常创建 TAP 需要 root 权限,但我这台机器有个便利条件:管理员把当前用户加进了uml-net组,所以我可以直接用tunctl在当前用户下创建 TAP,不需要 sudo:

tunctl -u $USER -t tap0 ip link set tap0 up ip addr add 10.0.0.1/24 dev tap0

如果你所在的机器连这个权限也没有,还有一条不需要 TAP 的备用方案:用 pty 虚拟串口连接两个 native 实例,让 RIOT 的串口网络驱动(SLIP)跑在 pty 上。这个方法不需要 root,但配置稍麻烦,这里先不展开。

链路最终是这样:宿主机(Linux 协议栈)通过 TAP 设备接上 RIOT native 实例,RIOT 侧拿到一个 IP 地址,宿主机侧也配置一个同网段 IP,然后我从宿主机往 RIOT 发 UDP 包,在 RIOT 侧统计接收的字节数和耗时,算出吞吐。

5.2 把 RIOT 网络示例跑起来并分配地址

启动 RIOT 时指定它使用 tap0:

cd RIOT/examples/gnrc_networking ./bin/native64/gnrc_networking.elf -e tap0

看到 RIOT shell 后,用ifconfig命令查看网络接口,有时候默认没启用,需要手动配置:

> ifconfig 7 set 10.0.0.2/24 > ifconfig 7 up

这里的7是 native 网络接口在系统里的编号,实际数字以ifconfig输出为准。配置完在 RIOT shell 里 ping 一下宿主机:

> ping 10.0.0.1

能通,说明 TAP 链路已经打通。

5.3 用 sock_udp 实现一个最小吞吐接收端

直接用 shell 的udp recv也能收包,但 shell 交互式处理效率太低,测不出真实上界。我自己写了一个最小化的 RIOT 应用,基于sock_udp接口,接收 UDP 数据并周期统计速率。核心代码大致如下:

#include "net/sock/udp.h" #include "xtimer.h" #define PORT 5000 #define BUF_SIZE 2048 static char buf[BUF_SIZE]; int main(void) { sock_udp_ep_t local = { .port = PORT, .family = AF_INET6, }; sock_udp_t sock; sock_udp_create(&sock, &local, NULL, 0); unsigned long total_bytes = 0; uint32_t last = xtimer_now_usec(); uint32_t window = 0; while (1) { int res = sock_udp_recv(&sock, buf, BUF_SIZE, 1000, NULL); if (res > 0) { total_bytes += res; window += res; } uint32_t now = xtimer_now_usec(); if (now - last >= 1000000U) { double mbit = (double)window * 8.0 / 1e6; printf("window rate: %.2f Mbit/s, total: %lu bytes\n", mbit, total_bytes); window = 0; last = now; } } }

这个应用的逻辑很简单:每收到一个 UDP 包就累加字节数,每一秒计算一次这一秒的速率并打印。RIOT 的sock_udp_recv本身会阻塞等待,超时设为 1 秒,所以即使没有包进来也不会死循环。

把这份代码放进RIOT/examples/下的一个新目录,顺便写一个简单的Makefile

APPLICATION = throughput_server BOARD ?= native64 RIOTBASE ?= $(CURDIR)/../../RIOT USEMODULE += gnrc_sock_udp USEMODULE += xtimer include $(RIOTBASE)/Makefile.include

注意RIOTBASE要指向你 clone 下来的 RIOT 源码根目录。编译命令:

make BOARD=native64 all

编译好之后启动:

./bin/native64/throughput_server.elf -e tap0

然后在 RIOT shell 里配置好 IP 地址,让它监听10.0.0.2:5000

5.4 宿主机发送端脚本与实测数据

宿主机这边,我用 Python 写了个简单的 UDP 发送脚本,往10.0.0.2的 5000 端口持续发送 5 秒,包大小 1400 字节:

import socket, time s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) target = ("10.0.0.2", 5000) payload = b"x" * 1400 start = time.time() duration = 5.0 sent = 0 while time.time() - start < duration: s.sendto(payload, target) sent += len(payload) s.close() rate = sent * 8 / duration / 1e6 print(f"sent {sent} bytes, average send rate {rate:.2f} Mbit/s")

这个脚本只能说明发送端的速率,真正要看的是 RIOT 侧throughput_server打印的接收速率。因为发送端发太快,RIOT 侧如果处理不过来,内核 socket 缓冲区会满,部分包会被丢弃,两者的速率会出现差值。

实测结果大概是这样:

window rate: 26.88 Mbit/s, total: 17340800 bytes window rate: 27.42 Mbit/s, total: 34576000 bytes window rate: 27.10 Mbit/s, total: 51843200 bytes window rate: 26.95 Mbit/s, total: 69108800 bytes window rate: 28.04 Mbit/s, total: 86508800 bytes

稳定之后每秒钟大约 27 Mbit/s 到 28 Mbit/s,平均约 28 Mbit/s。换算成字节是每秒 3.5 MB 左右,5 秒总共接收了 86.5 MB。这个数字是我把发送端速率从低往高调,直到 RIOT 侧开始出现丢包之前得到的稳定接收上界。如果你把包调小或者把应用逻辑改复杂,数字会明显下降;如果只是简单裸收,能稍微再高一点。

5.5 28 Mbit/s 到底意味着什么

很多人看到 28 Mbit/s 会觉得"就这?Linux 本机 UDP 轻松几个 Gbit/s"。但这个数字在 RIOT native 场景下是有意义的。它反映的是 RIOT 用户态网络协议栈、sock API、线程调度和内存拷贝这一整套逻辑的处理能力上限,不是纯 Linux 内核 socket 的速度。

native 模式下,RIOT 网络栈的每个数据包都要经过:TAP 设备读取 → 以太网解析 → IP 解析 → UDP 解析 →sock_udp_recv从内核队列拷贝到用户缓冲区。每一步都是真实逻辑,不是纯粹的空转。如果你换成真实嵌入式板卡上的 802.15.4 无线链路,物理层往往只有 250 kbit/s 左右,28 Mbit/s 已经远远超出这种射频链路的能力,所以这个数据更多是验证"协议栈软件逻辑没有明显瓶颈"。

另外,RIOT 的gnrc网络栈设计目标并不是跑满千兆网,它优先考虑的是小内存占用、低功耗和模块化。在有硬件 MAC 控制器的 MCU 上,瓶颈往往是无线媒介而不是协议栈本身。native 测试的价值在于:它给你一个软件侧的上界参考值,方便你做性能预算。

6. 关于这轮实验,我的一些复盘和建议

6.1 无 sudo 环境下的通用思路

这次实验能成功,核心不是某个具体命令,而是一个思路:"装依赖"不等于"apt install",任何软件都可以尝试放进用户目录。Miniconda 是这条路线上最好用的工具,其次是静态编译的二进制。遇到缺库时,优先考虑改编译参数、换 board、加CFLAGS/LDFLAGS,而不是粗暴地往系统目录塞文件。

如果公司或实验室的服务器上没 sudo,还可以试着查一下是否存在module命令,很多 HPC 集群用module load提供不同版本的工具链,这也是合法且方便的办法。

6.2 建议把 native64 作为受限环境的第一选择

我在文章里反复提到BOARD=native64,这里再强调一次。在 Ubuntu 上,RIOT 的默认native板子编译出来是 32 位程序,一旦系统缺少 32 位libc开发文件,链接阶段就会报错。而native64直接用宿主机的 64 位环境,省掉绝大多数麻烦。如果你是在完整开发机上跑,两者都行;但如果你是在共享服务器上,直接选 native64 可以减少至少一个小时的排查时间。

6.3 TAP 权限不够时的备选路径

如果你所在环境连tunctl都不让用,也不是死路一条。另一种我试过可行的方案是:把两个 native 实例分别挂到两个 pty 终端上,用 SLIP 协议在串口线上跑 IP,两个 pty 之间用socat交叉连接,这样就能在纯用户态搭一条二层的点对点链路。RIOT 的examples/gnrc_border_routergnrc_networking里就有 SLIP 相关的用法。吞吐会比 TAP 低一些,但验证协议栈行为完全够用。

6.4 32 位 vs 64 位对吞吐的影响

我顺手做了一个对比测试:同一套代码,BOARD=native在另一台有完整 multilib 库的机器上跑,吞吐大约只有 18 Mbit/s 到 20 Mbit/s,明显低于 native64 的 28 Mbit/s。原因倒不是 32 位 CPU 指令慢,而是 RIOT 的sock层在 native 模式下要模拟 32 位地址空间转换,加上指针宽度变小,内存拷贝路径上的开销反而更高。所以如果你想在 host 上测性能,native64 是更接近真实 64 位 MCU/MPU 的选择

6.5 最终一个小技巧

最后分享一个非常实用的小技巧:在受限环境下,给make加上BUILD_IN_QUIET=1可以少打印很多冗余信息,报错时更容易定位。还有一个是不要把 RIOT 源码放在/tmp下,有些共享服务器会定期清理/tmp,我吃过一次亏,编译到一半目录没了。放$HOME下虽然占点空间,但至少不会被莫名清理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询