ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RISC-V 无 MMU Linux 实践:从内核编译到用户态

RISC-V 无 MMU Linux 实践:从内核编译到用户态

最近把 RISC-V 无 MMU 的 Linux 从内核一路跑到用户态,完整走了一遍,过程中踩了不少坑,也把整个路径理顺了。这个项目有意思的点在于:大部分人接触 RISC-V Linux 都是带 MMU 的标准形态,但无 MMU 场景在 MCU 级别的 RISC-V 芯片上非常常见,国内做 IoT、嵌入式控制和极简 Linux 网关的团队基本都会碰到。这篇文章把我的完整路径写下来,从内核编译到根文件系统,再到在无 MMU 环境里跑通一个普通 C 程序,学习路径完整、可复现,希望对正在入门 RISC-V 无 MMU Linux 的同行有帮助。

这个项目要解决的问题很明确:在去掉 MMU 和页表机制之后,Linux 内核能否正常完成启动、挂载根文件系统、拉起用户态进程,并且让用户态应用正常读写文件、访问外设。适合三类读者:一是嵌入式方向想搞懂 noMMU 内核差异的,二是准备在低成本 RISC-V 芯片上跑 Linux 的开发者,三是纯粹想加深对内核内存管理理解的爱好者。

1. 项目背景与整体思路设计

1.1 无 MMU Linux 是什么,为什么要跑这个项目

MMU 全称 Memory Management Unit,负责虚拟地址到物理地址的转换,同时提供页级权限保护。传统 Linux 依赖 MMU 实现进程隔离、按页分配内存、写时复制这些机制。但在很多低成本 RISC-V 芯片上,比如部分 MCU 级别的内核(E31 这类单核 RV32),根本没有 MMU 模块,SRAM 或者内嵌 Flash 直接连在总线上,所有代码和数据跑在物理地址空间里。

没有 MMU 时 Linux 依然能工作,只是内核换成 noMMU 配置,进程不再有独立的虚拟地址空间,所有进程共享同一物理地址空间。这意味着没有 page fault 缺页异常、没有按页的访问保护,malloc 的实现也要跟着改变。这类系统通常叫 uClinux(MicroController Linux),最早在 m68k 上出现,后来移植到 ARM、RISC-V 等架构。

我跑这个项目的直接动因是手头一块 RISC-V 开发板,RAM 只有 8MB,带 MMU 的 Linux kernel 虽然也能编出来,但跑起来光页表分配就要吃掉不少内存,而且很多经典启动流程在这种硬件上相当别扭。后来查到 RISC-V 内核本身就支持 nommu 配置,于是决定完整梳理一遍:从内核源码配置开始,到用户态编译,全部手动搞一遍,不依赖现成 BSP 镜像,彻底弄明白每个环节在干什么。

1.2 传统 MMU 与 noMMU 的技术差异

先把两种模式的核心差异摆在桌面上,后续所有配置和代码都围绕这些差异展开:

对比项标准 MMU LinuxnoMMU Linux
地址转换虚拟地址通过页表映射到物理地址无页表,CPU 直接访问物理地址
进程隔离每个进程独立地址空间,互不可见所有进程共享地址空间,无硬件隔离
内存保护页级读写权限、内核/用户隔离几乎没有页级保护,只能靠代码约束
缺页异常支持按需调页,虚拟内存可大于物理内存不支持,所有内存必须物理存在
可执行文件加载ELF 按节加载,可加载到任意虚拟地址要求链接地址与运行地址匹配,通常用平坦 ELF
fork 实现依靠写时复制fork 必须完整拷贝地址空间,代价很大,通常替代方案是 vfork
适用场景桌面、服务器、主流应用MCU、低 RAM、实时性要求高的嵌入式控制

对开发体验影响最大的就是最后几条。普通 Linux 应用在 MMU 环境下从编译到运行基本无感知,但 noMMU 下稍大一点的程序就可能因为地址空间重叠或者堆栈越界产生极其诡异的行为,而且没有段错误这种明确的报错机制,排查全靠经验。

1.3 本次项目选择的硬件与软件环境

我这次选择在 QEMU 虚拟环境里跑 rv32 架构的 noMMU Linux,之后再放到真实板卡上验证。原因很简单:QEMU 可以直接指定 CPU 特性,模拟无 MMU 的 RISC-V 核很方便,而且调试内核崩溃时可以用 gdb 连接 QEMU,定位比实体板卡高效太多。

软件栈选型如下:

  • 内核:Linux 6.1 LTS,riscv 架构,配置 nommu
  • 工具链:riscv32-unknown-linux-gnu,配合 uClibc-ng 库(noMMU 场景最常用)
  • 根文件系统:initramfs 方案,内置 busybox 和自定义测试程序
  • 模拟器:QEMU 7.2,qemu-system-riscv32
  • 构建辅助:Buildroot 可以整体拉起来,但为了展开细节,我先手动构建一遍,最后用 Buildroot 做对照验证

之所以选 uClibc-ng 而不是 glibc,是因为 glibc 在 noMMU 下的支持一直不完整,尤其动态链接、线程实现这些都对 MMU 有依赖。uClibc-ng 专门为嵌入式设计,noMMU 支持成熟,RISC-V 支持也很完整。musl 其实也部分支持 noMMU,但当时在 RISC-V 平台上还有不少坑,后面会提到。

2. 工具链准备与基础环境搭建

2.1 交叉工具链的两种获取方式

构建 noMMU Linux 第一步是准备交叉编译工具链。这里有两条路,我建议新手直接用 Buildroot 构建,经验足够的可以手动安装或使用发行版交叉编译器。

方式一:直接安装发行版工具链。Ubuntu 上可以安装gcc-riscv64-unknown-elf,但注意这个工具链通常是 bare-metal 用的,没有 Linux 用户态的 glibc/uClibc 支持,编译内核可以,编译用户态应用就做不到了。所以更推荐安装riscv64-linux-gnu-gcc这类带 Linux 库的工具链,然后在编译应用时做成静态链接,绕开动态加载器在 noMMU 下的兼容问题。

方式二:Buildroot 统一构建。这是我在项目后期采用的方案,Buildroot 会自动处理内核、工具链、uClibc-ng、busybox、应用的全部构建,并且针对 noMMU 有专门的BR2_RISCV_32、BR2_USE_MMU这类配置项。

具体到本次手动搭建,我用的是 Buildroot 生成的 riscv32 noMMU 工具链,好处是 uClibc-ng 的配置和内核匹配,不会出现库函数编译期依赖了 MMU 特性的情况。

2.2 内核源码获取与分支选择

内核我直接选了 6.1 LTS,这个版本对 RISC-V nommu 的支持已经比较稳定。Linux 的 RISC-V noMMU 支持从 5.x 时代就开始合并,到 6.1 已经相当成熟,Kconfig 里的依赖关系捋得比较清楚。

获取源码后,需要先确认三个关键 Kconfig 项的状态:

CONFIG_MMU=n CONFIG_CPU_HAS_NO_MMU=y CONFIG_SYSVIPC=y # 可选,如果应用需要 SysV IPC 则开启,noMMU 下部分 IPC 有限制

这里有个关键点:CONFIG_MMU是全局内存管理开关,RISC-V 的 Kconfig 会根据 CPU 能力自动限制。如果你选的 CPU 类型不支持 MMU,那么CONFIG_MMU会被强制设为 n,相关依赖项也会跟着联动。我一开始不理解为什么核内明明有 MMU 选项但配置不生效,后来打开arch/riscv/Kconfig仔细看才发现,它读的是CPU_SUPPORTS_MMU这个能力位,如果 SoC 型号声明里没有这个标志,后续一切都不会有。

2.3 内核配置文件生成

noMMU 内核不是直接make defconfig就能出来的,需要先选对基础配置,再做裁剪。RISC-V 架构默认的 defconfig 是针对带 MMU 的通用平台,直接编译会要求 MMU 相关功能,而且默认配置会打开大量用不到的设备驱动,拉长编译时间。

我的操作路径是:

make ARCH=riscv CROSS_COMPILE=riscv32-linux- nommu_virt_defconfig

Linux 6.1 里没有直接的nommu_virt_defconfig,这个目标名不存在,正确做法是先make ARCH=riscv virt_defconfig,再手动改配置。打开menuconfig后重点改以下几项:

General setup ---> [*] Initial RAM filesystem and RAM disk support [*] Support INITRAMFS with gzip Kernel Features ---> [ ] Use the MMU (取消勾选) Executable file formats ---> [*] Use ELF format for user binaries [ ] (如果有 FDPIC 选项,noMMU 下用户态可以用 FDPIC,后面选型会细说) Device Drivers ---> 关闭不需要的块设备、网络设备驱动,减少编译时间

取消 MMU 后,内核会自动弹出Memory Model的选择。noMMU 下通常选Flat Memory Model,这是最简单直接的内存模型,整个物理内存就当成一段连续空间,不搞 NUMA 和多段映射。选完保存退出。

执行make -j$(nproc)之前,建议先确认CROSS_COMPILE环境变量是否正确。我在第一次编译时因为CROSS_COMPILE设成了riscv64-linux-,编译过程报了一堆寄存器宽度不匹配的错误,这里必须严格用 riscv32 的交叉前缀。

2.4 内核链接地址与运行地址的关系

noMMU 内核与 MMU 内核有一个本质差别:内核镜像的运行地址必须等于链接地址。带 MMU 时,内核可以链接在虚拟地址0xffffffff80000000,运行时通过页表映射到底层物理地址;而没有 MMU 时,CPU 取指直接用物理地址,链接器写的地址就是 CPU 看到的地址,必须确保链接脚本里的地址段落在目标板 RAM 的物理地址范围内。

QEMU virt 平台的virt机器默认 RAM 起始地址是0x80000000,所以内核默认链接地址就是这里。如果换成真实芯片,一定要查数据手册确认内部 SRAM 的基地址,然后可能需要修改arch/riscv/kernel/vmlinux.lds.S里的内存布局段,或者通过设备树中/memory节点调整。

这个坑我在真实板卡上踩过一次,编译没问题,QEMU 上跑得好好的,烧到板子上就是起不来,最后加串口打印才发现第一条指令就取指失败了。根源就是链接地址 0x80000000 在芯片上根本没有内存映射。

3. 内核编译与启动验证

3.1 编译命令与产物说明

配置完成后,执行编译。完整命令如下:

make ARCH=riscv CROSS_COMPILE=riscv32-linux- -j8

编译产物中,最重要的是两个文件:

  • vmlinux:带有调试信息的完整 ELF,QEMU 可以直接加载
  • arch/riscv/boot/Image:去掉了调试信息的原始内核镜像

QEMU noMMU 启动推荐直接用vmlinux或者Image,不要用uImage或者vmlinuz,因为 noMMU 平台通常也没有 decompressor 支持,直接用 ELF 最省事。

编译期间如果遇到fatal error: linux/compiler-gcc.h: No such file or directory,通常是工具链和内核版本不匹配,检查一下交叉编译器版本。Linux 6.1 要求 GCC 版本在 5.1 以上,我用的是 GCC 12,没问题。

编译时间取决于机器,我实机 8 核编译大概三分钟完成,比较快,因为裁剪了大量驱动。

3.2 QEMU 启动参数与现象解读

内核编译完成后,先做一个最小启动测试,不挂任何根文件系统,看内核能不能跑到最后的 panic:

qemu-system-riscv32 -M virt -kernel vmlinux -nographic -m 64M

注意这里-m 64M给的是 64MB RAM,对 noMMU Linux 来说已经算非常宽裕。设置完内存后,观察串口输出,正常启动到最后会停在Kernel panic - not syncing: VFS: Unable to mount root fs,这说明内核已经完成了 CPU 初始化、内存初始化、中断控制器初始化,只是找不到根文件系统。到这里,内核侧已经通了。

QEMU 的-M virt平台对 noMMU 内核有一个需要注意的地方:virt 平台的设备树里默认描述了很多外设,包括 PCIe、virtio 等,但 noMMU 内核没有启用对应的驱动时,这些节点会被忽略,不会造成问题。

还有一个细节,启动时如果看到No working init found. Try passing init= option to kernel,说明内核已经成功挂载了根文件系统但找不到 init 程序,这是下一步要解决的。

3.3 initramfs 最小根文件系统

要让 Linux 完整启动到用户态,最简单的方式是制作 initramfs,把它编进内核镜像里,这样不需要额外的存储介质,尤其适合 noMMU 平台——很多低端芯片根本没有磁盘控制器,从内存直接加载 initramfs 是最可靠的路径。

我采用usr/gen_initramfs.sh脚本,把预先做好的根文件系统目录打包进去。步骤是:

mkdir -p rootfs/{bin,sbin,etc,proc,sys,dev,usr/bin,usr/sbin,lib} # 将 busybox 可执行文件和测试程序放进去 ./usr/gen_initramfs.sh -o usr/initramfs_data.cpio.gz rootfs/

再回到内核配置里确认CONFIG_INITRAMFS_SOURCE指向这个 cpio 文件,重新编译内核。也可以不重新编译,直接在 QEMU 命令行加-initrd rootfs.cpio.gz,并且在内核启动参数中指定rdinit=/init。我更推荐先用-initrd方式做验证,因为改根文件系统不用重编内核,迭代速度快。

下决心把 initramfs 内置也是后面才改的,因为真实板卡上没有 bootloader 可以方便地加载外部 initrd,直接烧进内核镜像最省事。

4. 根文件系统构建与 BusyBox 配置

4.1 BusyBox 的交叉编译细节

BusyBox 是 noMMU Linux 用户态的基本盘,提供了 shell、文件工具、网络工具等。编译时需要注意几个点,这些都是实际踩过之后才总结出来的。

首先,BusyBox 默认会尝试使用CONFIG_FORK_PTRACE这类依赖 MMU 的功能,编译时要在配置里关掉。其次,BusyBox 的编译器检测可能会尝试用-fstack-protector,noMMU 下 stack protector 依赖特定库支持,直接关掉最省心。

我编译 BusyBox 的配置流程:

make ARCH=riscv CROSS_COMPILE=riscv32-linux- defconfig make ARCH=riscv CROSS_COMPILE=riscv32-linux- menuconfig

在 menuconfig 里调整:

Settings ---> [ ] Build static binary (no shared libs) (推荐静态编译) [ ] 如果你必须用动态库,则确保 uClibc-ng 的共享库路径正确 Shells ---> [*] ash (首选,busybox 自带轻量 shell)

我这里为了简单,直接选择静态编译,这样可执行文件不依赖动态加载器,减少 noMMU 下动态链接的坑。静态编译的缺点是文件体积大,实测 busybox 静态编译后大约 800KB,在 noMMU 平台普遍 8MB-16MB 内存下还是可以接受的。

编译完成后,将busybox复制到根文件系统bin/busybox,然后在bin目录下创建符号链接:

cd rootfs/bin ln -s busybox sh ln -s busybox ls ln -s busybox mount ln -s busybox ps

实际上完整做法是执行busybox --install -s,它会自动创建所有集成命令的符号链接。如果目标根文件系统目录还没有/bin,先建目录再安装。

4.2 init 脚本与设备节点

内核启动到用户态后,会执行/init或者rdinit指定的程序。我写了一个最小化的 init 脚本,核心只有三件事:挂载关键文件系统、设置主机名、启动 shell。

#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev echo "Welcome to RISC-V NOMMU Linux" exec /bin/sh

注意这里 devtmpfs 的挂载。noMMU 下内核会配置CONFIG_DEVTMPFS,这是设备节点自动管理的机制,挂载后/dev下会自动出现 console、null、zero 等节点,否则串口输出可能都找不到设备文件。

init 脚本需要有执行权限,并且建议所有涉及路径的工具都用绝对路径,避免 PATH 还没初始化时找不到命令。

设备节点的另一种方式是手动创建:

mknod -m 666 dev/console c 5 1 mknod -m 666 dev/null c 1 3

如果CONFIG_DEVTMPFS_MOUNT没开或者挂载失败,手动 mknod 是兜底方案。我在部分极简内核配置下遇到过 devtmpfs 不可用的情况,手动创建节点后问题解决。

4.3 普通 C 程序在 noMMU 下的编译方式

跑通 shell 之后,最关键的一步是编译并运行一个自己的 C 程序。这个环节最能感受到 noMMU 和 MMU 的差异——链接地址、内存布局、堆栈都和有 MMU 时不一样。

以最经典的 Hello World 为例:

#include <stdio.h> #include <unistd.h> int main(void) { printf("Hello from RISC-V NOMMU!\n"); sleep(1); return 0; }

编译命令:

riscv32-linux-gcc -static -o hello hello.c

这里必须加-static。noMMU 下动态链接需要加载器把共享库映射到进程地址空间,而进程地址空间本身没有 MMU 保护,随意加载很容易和栈或者堆冲突。静态链接直接从根本回避了这个问题。

编译完成后,把 hello 放进 rootfs/bin,在 shell 里直接执行。如果你的程序正常打印出字符串,整个链路就通了。

但这个简单程序对 noMMU 来说其实没有真正挑战到难点。我建议实际测试一个稍大的程序,比如用 malloc 分配 1MB 内存再写入数据,或者写一个递归程序把栈耗尽,体验一下 noMMU 下资源受限的真实表现。

5. noMMU 应用开发的特殊注意事项

5.1 fork/vfork 的选择与进程创建

noMMU 上多进程编程是最容易出问题的地方。标准 Linux 的 fork 依赖写时复制,子进程只需要复制父进程的页表,虚拟内存可以共享物理页,创建过程非常快。但 noMMU 下 fork 必须把父进程的整个地址空间完整拷贝一遍,包括所有堆和栈内存,如果父进程占用了 8MB 内存,fork 一次就要拷贝 8MB,既慢又容易失败。

正确的做法是用 vfork。vfork 的特点是不拷贝地址空间,父进程挂起,子进程直接使用父进程的内存,子进程调用 exec 后地址空间被替换,再交还父进程。但 vfork 的使用有严格限制:子进程在 exec 之前不能修改任何全局变量,不能从当前函数返回,只能直接调用 exec 系列函数或者_exit。

实际上在 noMMU 平台上,包括 BusyBox 内部的大量程序,都会自动使用 vfork 来避免 fork 失败。你可以用strace观察,在普通 MMU 系统上ls | grep x会看到 fork,而在 noMMU 系统上同一命令会用 vfork。

共享内存的进程间通信也要注意。noMMU 下内核配置CONFIG_SYSVIPC后可以支持信号量和消息队列,但共享内存是有限制的。我在测试时发现shmget偶尔失败,原因是共享内存段需要固定在物理地址上,多个进程映射同一物理地址时如果地址冲突就会失败。实际项目中如果要在 noMMU 多进程间传数据,优先考虑管道、消息队列或者直接读写文件。

5.2 堆栈与内存布局对应用的影响

noMMU 内核创建用户态进程时,会为每个进程分配一块独立的区域作为栈和堆,而且地址是固定的。这与 MMU 系统每个进程都从相同虚拟地址(比如0x7fff...)开始完全不同。

我用一个简单的内存地址打印程序做了测试:

#include <stdio.h> #include <stdlib.h> int global_var; int main(void) { int stack_var; int *heap_var = malloc(128); printf("stack: %p\n", &stack_var); printf("heap: %p\n", heap_var); printf("global: %p\n", &global_var); return 0; }

在 MMU Linux 上,两个独立进程分别运行这个程序,打印出的地址往往一模一样。但在 noMMU Linux 上,地址很可能不同,因为内核为每个进程选择的加载地址和栈顶地址都取决于当时的内存分配情况。

这给调试带来一个很大的麻烦:noMMU 下程序崩溃时,没有页表翻译的层级关系可供回溯,也没有“非法访问”这类明确的错误码,往往是直接挂掉或者跳到一个奇怪地址。我调试时用的笨但有效的方法是:在程序里大量加入打印函数,二分定位到最后一个正常打印的位置,再缩小范围。

应用层的栈大小也需要显式控制。noMMU 下进程栈不是按需增长的,而是创建进程时一次性分配好。如果程序递归深度大或者申明了大的局部数组,要么用 ulimit 调整栈上限,要么在编译时通过链接脚本控制。我见过一个同事写的程序,局部变量声明了一个 2MB 的数组,在 MMU 系统上跑得好好的,到 noMMU 系统上直接启动失败——后来查明白,内核为这个进程分配的栈空间根本不够用,修改链接脚本给这个程序分配更大的栈后解决。

5.3 动态库与 FDPIC 的取舍

noMMU 平台并不是完全不能用动态库,只是需要特殊格式,比如 FDPIC。FDPIC 是一种允许共享库加载到不同地址的 ELF 变体,ARM 的 noMMU 平台用得比较多,RISC-V 的支持也在完善中。

我的建议是:除非你对动态链接机制非常熟悉,否则项目初期全部静态编译。静态编译唯一的缺点是镜像大,但在 noMMU 场景下,一个程序的体积增大换来的是部署和调试的复杂度大幅降低。等系统跑通、内存空间允许之后,再考虑把应用层换成动态编译,内核里开启 FDPIC 支持,可以省不少 Flash 空间。

实际对比数据供参考:一个简单的 bzip2 工具,静态编译约 220KB,动态编译加共享库整体约 80KB。如果根文件系统只有 1MB 空间,动态化的收益是比较明显的。

6. 完整启动验证与常见问题排查

6.1 完整启动流程图与验证清单

整个系统跑通后,启动日志里会有几个关键的“里程碑”节点,我把它整理成一个检查清单,方便对照排错:

序号日志特征说明
1Linux version 6.1.0内核入口代码执行成功
2CPU: rv32或相关架构信息CPU 探测完成,无 MMU
3Built 1 zonelists, mobility grouping on内存初始化完成
4Kernel command line:启动参数解析完成
5Freeing unused kernel image memory内核启动最后阶段
6Run /init as init process切换到用户态
7Welcome to RISC-V NOMMU Linuxinit 脚本执行成功
8Hello from RISC-V NOMMU!自定义应用运行成功

如果你启动到一个阶段就卡住,直接对照这个表格就能缩小范围:卡在 2 之前是内核入口问题,卡在 5-6 之间大概率是 rootfs 问题,卡在 6 之后是 init 脚本或者用户态程序问题。

6.2 QEMU 常见启动失败清单

我整个过程中遇到的最典型的启动失败有以下几类,每类都附上排查方法:

第一种,启动立刻报错,串口只有几个字符或者完全无输出。这种通常是 QEMU 参数不对,或者内核链接地址与 QEMU 内存不匹配。检查-m指定的内存地址范围是否覆盖了内核链接地址 0x80000000,如果不放心就用-d in_asm让 QEMU 打印执行的指令,看第一条指令地址。

第二种,内核启动到Kernel panic - not syncing: VFS: Unable to mount root fs。这说明整条内核初始化路径没问题,rootfs 没进去。确认 initramfs 是否编进去了,或者-initrd参数是否拼写正确。

第三种,Run /init as init process之后立刻出现Kernel panic - not syncing: Attempted to kill init!。这说明内核找到了 init,但 init 程序无法运行。大概率是 init 缺少执行权限、解释器找不到,或者 init 链接的库不存在。把 init 换成静态编译的 busybox 试试,最容易定位。

第四种,程序运行过程中突然无响应或者 QEMU 退出了。noMMU 下用户态程序直接访问了非法地址,触发 CPU 异常。这种情况在 MMU 系统上会以段错误形式体现,noMMU 下则经常表现为系统直接挂死。处理方式是打开内核的CONFIG_DEBUG_USER选项,让内核在用户态异常时打印更多信息,能帮助你定位是哪个程序、哪条指令出的问题。

6.3 真实板卡与 QEMU 的差异

最后特别提醒一下 QEMU 和真实芯片的差异。QEMU 的 virt 平台是一个通用模拟平台,CPU 特性、内存布局、外设都比较规整。真实 RISC-V noMMU 芯片往往有更多限制:

  • 内部 SRAM 可能只有 64KB 到 2MB,initramfs 都放不下,这时要考虑 XIP(execute in place),让内核直接从 Flash 执行
  • 真实芯片的串口寄存器地址和 virt 平台完全不同,必须配置正确的 earlycon 参数,否则从开机就看不到任何输出
  • 芯片厂商的 BSP 很可能基于老版本内核,noMMU 配置项的名称和位置都可能有差异,不要想着把 6.1 的 config 直接套到厂商 4.x 内核上

我在真实板卡上调试时,最有效的工具就是加一个 GPIO 翻转函数放在启动早期代码里,配合示波器观察程序执行到了哪里。这个办法虽然原始,但比盲调效率高十倍。

6.4 后续扩展方向

项目跑通到“内核起来、用户态应用跑一遍”只是第一步,后续还有一批值得深入的方向。我简单列几个,供想继续往下走的人参考:

  • 把根文件系统从 initramfs 换成外部 SPI Flash 挂载,更贴近真实产品形态
  • 在 noMMU 内核里跑一个简单的网络协议栈,验证 TCP/IP 在无隔离环境下的稳定性
  • 用 Buildroot 把整体构建脚本固化下来,实现一键产出内核和 rootfs
  • 对比 glibc、uClibc-ng、musl 三者在 noMMU RISC-V 上的运行差异,为具体项目选型提供数据支撑

我个人在实际操作中的体会是,noMMU Linux 并不神秘,它只是把“内存管理”这层外衣脱掉,让开发者直接面对物理地址空间。这在带来挑战的同时,也迫使你更深入地理解进程、内存、编译链接之间的关系。跑通了之后,很多以前看内核源码时模模糊糊的概念,现在都变得非常具体。比如“页表”“写时复制”“进程地址空间”这些词,在有 MMU 时是抽象的机制,在 noMMU 下你会直观地看到它们被一个个取消掉之后系统是怎么工作的。如果刚接触这方向,强烈建议找一块便宜的 RISC-V 开发板,从无 MMU 的配置入手,亲手编译、亲手烧录,完整跑一遍,收获会比只看文档大得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进