
如果你第一次接触“RISC-V Non-MMU Linux”这个组合大概率和我当初的反应一样这能跑没有 MMU意味着没有虚拟内存、没有缺页异常、没有进程间内存保护Linux 这种出了名依赖虚拟内存的“大块头”系统居然能在这种条件下运行我花了两个晚上从内核源码一路折腾到用户态应用在 QEMU 里看到完整的 shell 提示符再把自己写的 C 程序跑起来的那一刻确实有点兴奋。这篇文章就记录我“从内核到应用跑通一遍”的完整过程重点讲清楚 NoMMU 环境的选型思路、内核配置路径、根文件系统的组织方式以及应用层必须遵守的编译铁律。适合想搞懂 Non-MMU Linux 底层机制的嵌入式工程师、芯片验证同学也适合那些想在低资源 RISC-V MCU 上体验 Linux 的折腾派。1. 为什么要在没有MMU的RISC-V上跑Linux1.1 没有MMU到底少了什么先把概念拉齐。MMU 做的事情是虚拟地址到物理地址的转换附带权限控制和按页换入换出。有 MMU 时每个进程都以为自己在独占整个地址空间程序访问的地址是“虚拟”的底层映射到哪块物理内存由内核决定。没有 MMU 时程序拿到的地址就是物理地址所有进程直接暴露在同一片内存里。我习惯用一个生活化类比有 MMU 的系统好比一栋单元楼每个进程一个独立单间门牌号是物业统一编的你住在 302 还是 502 根本不重要反正邮件送到门牌号就行。NoMMU 系统则是一间大开间所有人住一起谁都能看到别人的行李也没有墙和门锁。这带来的连锁反应是进程 A 可以改掉进程 B 的数据一个空指针就可能把整个系统打崩无法按页换入换出意味着物理内存里所有内容常驻内存根本没有“写回磁盘再换入”这回事。Linux 内核之所以能运行在这种环境是因为从 uClinux 时代开始就保留了 nommu 分支后来这条路被合并进主线。现在只要在配置里关掉 CONFIG_MMU内核就会走另一套内存管理路径没有 vm_area_struct 按页管理、没有缺页异常所有驱动、文件系统、调度器都得面对“无虚拟内存”的现实。但内核本身完全可以跑而且跑得稳。1.2 谁需要这种组合RISC-V 生态里大量 MCU 级芯片是没有 MMU 的典型代表有 GD32VF103、CH32V307、ESP32-C3、K210 这类内部 RAM 从几百 KB 到几 MB 不等。在这个档次上跑 FreeRTOS 等 RTOS 是主流选择但如果你想要文件系统、网络协议栈、用户态隔离、标准 Linux 工具链裸机 RTOS 就有点力不从心了。NoMMU Linux 恰好卡在这个生态位上比 RTOS 功能强得多又不要求芯片必须带 MMU。工业控制、物联网网关、低功耗数据采集设备都是典型场景。另外一些 FPGA 验证平台和定制 SoC 前期并不带 MMU用 Linux 跑通一遍可以快速验证基础外设、中断和启动链路。对于学生和爱好者在 QEMU 上免费跑通整个流程也是理解操作系统底层机制极好的切入点。2. 动手之前的选型与认知准备2.1 真机与QEMU第一遍该选谁我的建议非常明确第一遍不要碰真机直接在 QEMU 上跑。原因很简单真机的问题会淹没主线问题。你拿一块 K210 或者 CH32V307 出来首先得搞定调试串口、程序烧录、时钟树配置、Flash 启动方式这些环节任何一步出错都会让你误以为“NoMMU 内核有问题”。实际上内核在这类平台上的适配是单独一个工程涉及链接地址、启动汇编、设备树匹配这些都不应该在第一篇里掺和。QEMU 的 virt 机器是标准平台串口、中断控制器、时钟都是现成的配合内核源码自带的 nommu_virt_defconfig基本是开箱即用。命令非常简单qemu-system-riscv64 -M virt -m 128M -nographic -kernel arch/riscv/boot/Image我下面以 RV64 为例因为 K210 这类常见 NoMMU 芯片也是 RV64 架构。如果你的目标是 RV32 MCU思路完全一致只是工具链前缀和 ARCH 相关选项换成 rv32 版本。第一遍用 RV64 能把整条链路走顺RV32 的具体适配留到后面再说。2.2 工具链的三条铁律工具链选型是 NoMMU Linux 最容易踩雷的地方我总结了三条经验基本是铁律。第一内核工具链和应用工具链要分开。编译内核建议用 riscv64-unknown-elf-gcc这是裸机工具链不依赖 glibc链接时自带 libgcc正好匹配内核这种没有标准 C 库依赖的裸环境。用 riscv64-linux-gnu-gcc 也能编但会引入更多变量第一遍最好减少未知数。第二应用层不要用 glibc 交叉编译器。我在 NoMMU 系统上折腾过最久的问题就出在这里用 Ubuntu 自带的 riscv64-linux-gnu-gcc 编译 hello 程序塞进 rootfs运行直接报 “error while loading shared libraries”因为默认是动态链接而 NoMMU 环境里根本没有 ld.so 动态链接器这回事。换成 musl 交叉工具链riscv64-linux-musl-gcc之后一次性成功。musl 对静态链接和 PIE 的支持都非常干净特别适合小内存环境。第三静态编译和 PIE 必须绑在一起。NoMMU 下没有虚拟地址可以随便映射可执行文件必须编译成地址无关代码加载时由内核在物理内存里找一块空间放进去再跳转执行。如果你不加 -fPIE -pie链接出来的程序入口地址是固定的装载地址一不一致第一条指令就跑飞了。这一点很多人忽略后面我会专门演示。3. 内核编译Nommu配置没那么神秘3.1 获取源码与确定defconfig内核版本我推荐 6.1 LTS。为什么选它NoMMU 相关特性在 6.1 已经非常稳定资料也多而且之后如果要针对某款芯片加补丁LTS 的维护周期长不容易孤立无援。你直接去 kernel.org 下载源码包解压或者 git clone 官方仓库都行。进入源码目录后先设置环境变量export ARCHriscv export CROSS_COMPILEriscv64-unknown-elf-然后生成配置make nommu_virt_defconfig这个 defconfig 是整个流程的起点它就放在 arch/riscv/configs/ 目录下。名称里已经写明白了面向 virt machine 的 NoMMU 配置。如果你以后要移植到真机就不存在现成的 nommu defconfig 了得自己从已有的 sifive 系列或定制平台配置开始改但这属于第二篇的内容。3.2 关键内核配置项的解读编译前我习惯用 make menuconfig 过一遍虽然 nommu_virt_defconfig 已经很干净但几个核心开关还是要心里有数。CONFIG_MMU 必须不选。这是整条链路的根开关关掉之后很多虚拟内存相关的符号会自动消失驱动里调用 remap 类接口时会退化成更简单的 ioremap 实现。CONFIG_BINFMT_ELF_FDPIC 必须打开。RISC-V 的 NoMMU 可执行文件格式默认走 ELF FDPIC而不是传统 MMU 系统的标准 ELF。FDPIC 的好处是让可执行文件以与地址无关的方式加载允许代码段在某些场景下被多个进程共享。第一遍你只管把它开上后面编译应用时才不会在 exec 阶段卡住。CONFIG_INITRAMFS_SOURCE 暂时留空。这个配置用来把根文件系统打进内核镜像第四章节会回过头来填它。CONFIG_PROC_FS 建议开启。跑通之后挂载 procfs 看内存、看进程都比较方便对排查问题节省时间。如果你不是用 nommu_virt_defconfig 而是从全功能 defconfig 改很容易遗漏一个隐藏问题默认 defconfig 里 CONFIG_MMUy编译阶段一切正常跑起来也和普通 Linux 一样你会以为自己成功了但其实根本没进入 NoMMU 世界。所以第一步一定要确认 .config 里写着 CONFIG_MMU is not set。3.3 编译命令和产物配置好之后直接编译make -j$(nproc) Image工具链齐全的话几分钟就能完成。产物在 arch/riscv/boot/Image注意这是未压缩的 ELF 镜像NoMMU 内核一般直接加载这份镜像不像传统系统那样还要 uImage、Image.gz 之类的压缩包装。原因是压缩镜像需要自解压而自解压要分配一大块内存再拷贝到最终位置无 MMU 环境下这等于多了一层不确定性和一次大内存消耗所以默认不做。如果你确实指望省空间内核也支持 CONFIG_ZBOOT 配置来启用压缩但第一遍完全没必要。这里提醒一个我在 RV32 上遇到过的编译错误undefined reference to __muldi3。这是 32 位工具链的典型问题因为 32 位 CPU 做 64 位乘除法时要调用 libgcc 里的辅助函数而内核链接阶段有时没有自动带上 libgcc。RV64 很少遇到如果你的目标平台是 RV32记得检查工具链版本或在链接时补上对应库文件。4. 根文件系统与初始化4.1 为何优先选择initramfsNoMMU 系统没有虚拟内存页表文件系统的页缓存能力天然受限。虽然你也可以在 SD 卡或 Flash 上挂 ext4、romfs但对于“跑通一遍”这个目标来说最省事的方式是把整个用户态环境打进内核镜像里的 initramfs。这样做的收益非常直接不需要写块设备驱动不需要处理分区不需要在引导阶段传 root 参数。内核初始化完成后直接把打包好的 cpio 归档解压到内存中的 ramfs然后执行 /init。整个过程和 MMU 系统上的 initramfs 动作类似但因为底层没有页缓存机制实现细节会有一点差异。我推荐的节奏是第一遍全内嵌把“内核能起来、应用能跑”这个核心链路确认了第二遍再研究 romfs、NFS 这种外置存储方案。先跑通再添花别一步到位。4.2 基于BusyBox的最小文件系统BusyBox 是标配。用 musl 工具链交叉编译make ARCHriscv CROSS_COMPILEriscv64-linux-musl- defconfig make ARCHriscv CROSS_COMPILEriscv64-linux-musl- menuconfig菜单里需要确认两个关键项。一是 Settings - Build static binary (no shared libs) 必须选 YNoMMU 系统没有动态链接器busybox 如果是动态链接直接起不来。二是检查有没有打开疑似依赖动态库的模块尽量减少外部依赖。然后编译安装make ARCHriscv CROSS_COMPILEriscv64-linux-musl- -j$(nproc) make ARCHriscv CROSS_COMPILEriscv64-linux-musl- install CONFIG_PREFIX$PWD/_install完成后你会得到一个 _install 目录里面有 bin、sbin、usr 结构。这一步相当于集齐了常用命令的批发包。接下来把我们的测试程序塞进去再写一个 init 脚本。init 脚本我建议第一版写得尽量朴素#!/bin/sh echo RISC-V Non-MMU Linux up /bin/hello exec /bin/sh先不要挂载 proc 和 sysfs。虽然 procfs 在 NoMMU 下基本可用但 sysfs 可能因为配置差异产生告警纯内存环境里先把基础功能跑通确认没问题再逐步加挂载点。脚本写完记得chmod x _install/init4.3 让initramfs跟随内核在 rootfs 目录里执行打包find . -print | cpio -o -H newc ../rootfs.cpio不压缩就够了initramfs 天然支持再嵌套压缩但我们这个规模完全不需要。打包时有个细节必须用 find . -print 而不是 find . -print0 之类的高阶参数虽然都能生成 cpio但权限和目录项的表现有时会出幺蛾子简单指令最稳。回到内核源码目录把 cpio 的位置告诉内核scripts/config --enable INITRAMFS_SOURCE scripts/config --set-str INITRAMFS_SOURCE /home/user/rootfs.cpio重新编译内核。启动后你会发现不需要单独传 initrdinit 和 busybox 已经住进内核镜像里了。这步如果没生效多半是 CONFIG_INITRAMFS_SOURCE 写的相对路径改成绝对路径再编一次。5. 应用层适配会写代码不等于能跑5.1 从fork到vforkNoMMU的进程心智模型应用层是 NoMMU Linux 和普通 Linux 差异最大的地方因为很多人在 MMU 系统上养成的编程习惯完全带不过来。第一个大坑就是 fork()。在 MMU 系统里fork 依赖 copy-on-write子进程和父进程先共享物理页谁写谁复制效率很高。但在 NoMMU 系统里没有页表这层间接层也没有写时复制机制fork 要么被禁用要么代价高到不可接受。RISC-V NoMMU 内核里sys_fork 直接返回 ENOSYS 是常事你写一段带 fork 的程序跑起来就报 Function not implemented。替代方案是 vfork 和 posix_spawn。vfork 的语义是子进程共享父进程地址空间同时父进程被挂起直到子进程调用 exec 或者 exit。这样整个过程不复制内存代价是子进程里修改任何变量都会直接影响父进程所以在 vfork 出来的子进程里最安全的操作就是直接 _exit 或者 exec不要做花哨的输出。如果你第一遍只跑一个 hello完全可以单进程。但为了演示多进程的思路我写了一个用 vfork 的简单例子代码里子进程只做退出所有打印放到父进程。5.2 编译静态PIE应用并部署写一个 hello.c#include stdio.h #include unistd.h #include sys/wait.h int main(void) { printf(Hello from RISC-V Non-MMU Linux, pid%d\n, getpid()); pid_t pid vfork(); if (pid 0) { _exit(0); } waitpid(pid, NULL, 0); printf(child exited\n); return 0; }然后编译riscv64-linux-musl-gcc -static -fPIE -pie hello.c -o hello三个参数缺一不可。 -static 告诉链接器不要生成动态依赖-fPIE 生成地址无关代码-pie 把可执行文件标记成 PIE 类型。内核加载器看到这个标记才会把程序放到任意物理内存位置执行。我见过很多人只加 -static 不加 PIE结果一运行就段错误。也有拿 -fPIC 来编译可执行文件的链接器通常直接报 “cannot use -fPIC for executable”其实你把 -fPIC 换成 -fPIE 就好。记住 NoMMU 场景下静态和 PIE 是铁绑定的分开谈都是坑。把 hello 放进 rootfs重新做 cpio重新编内核启动后 init 脚本会打印类似这样的信息 RISC-V Non-MMU Linux up Hello from RISC-V Non-MMU Linux, pid1 / #看到这个 shell 提示符说明整条链路已经通了。5.3 运行验证与细节观察进入 shell 以后第一件事就是 ls、cat /proc/meminfo前提是你开了 proc然后再跑一次 hello。你会注意到几个和普通 Linux 截然不同的细节ps 里进程数量少得可怜一个进程的地址空间并不是和另一个进程隔离的有些依赖 fork 的 shell 特性表现会受限。这种“受限感”是 NoMMU Linux 的常态。它不是简单阉割而是在一套不同约束下保持完整形态的操作系统。应用层只要绕过 fork 假设、动态链接假设和固定地址假设平时该用的文件 I/O、socket、线程如果芯片够强都可以正常工作。6. 常见问题与排查技巧实录6.1 问题速查表这一路下来踩过的坑不少我整理成表方便你照着排查现象原因解决启动时 Kernel panic: VFS: Unable to mount root fsinitramfs 没有成功嵌入内核或 cpio 归档损坏检查 CONFIG_INITRAMFS_SOURCE 是否为绝对路径重新打包并重编内核Failed to execute /initinit 脚本不可执行或格式不受 BINFMT_FDPIC 支持init 必须 chmod x且第一行是 #!/bin/sh二进制程序必须编成静态 PIE运行应用报 error while loading shared libraries使用了 glibc 默认动态链接工具链产生了 .so 依赖换成 musl 工具链编译时加 -static -fPIE -pie应用启动秒 Segfault没加 -fPIE -pie或可执行文件不是真正的 PIE 类型加参数重编用 readelf -l 确认没有动态段依赖fork 报 Function not implementedNoMMU 内核禁用了 fork因为没有 COW用 vfork 或 posix_spawn 替代QEMU 启动无任何输出串口驱动没编入或 dtb 不匹配用 nommu_virt_defconfig 起步必要时从 QEMU dumpdtb 提取设备树6.2 我的三个独家避坑技巧第一内核第一次启动尽量用 -nographic 直连串口。NoMMU 内核调试手段比 MMU 系统少很多没有成熟的 crash 工具链串口就是命脉。如果早期初始化阶段就没有输出大概率是设备树或者串口地址的问题这时候不要瞎猜先检查内核是不是真的选对了 defconfig再用 -dtb 参数显式传设备树。第二善用 scripts/extract-ikconfig Image。这个命令可以从编好的内核镜像里抽出 .config直接验证 CONFIG_MMU、CONFIG_INITRAMFS_SOURCE 是否真的生效。比翻源码目录里的 .config 可靠得多因为有时候你编的是旧镜像或者 source 目录已经被修改过。第三真机上最容易踩的坑是内存太小。NoMMU 内核把所有物理内存统一交给管理和分配实际可用空闲内存往往会吓你一跳。QEMU 里用 -m 128M 非常宽裕但真机如果只有几 MB SRAM各种 OOM、驱动申请失败会接踵而至。所以先 QEMU 验证功能再针对真机做裁剪、考虑 XIP这个顺序千万别反。7. 从QEMU到真机移植的一点延伸QEMU 跑通只是万里长征第一步真正困难的是把同一套内核搬到真实 NoMMU 芯片上。这三个差异你迟早要面对。第一是设备树必须完全匹配真实硬件。串口地址、中断控制器、GPIO、时钟树每一个都得对得上。QEMU 的 virt 机器现成设备树就在那里真机则要自己从启动汇编一路看到外设驱动初始化工作量不小。第二是内存布局与 XIP。芯片自带 Flash 时常见做法是开 CONFIG_XIP_KERNEL让内核代码直接从 Flash 执行把宝贵的 RAM 留给数据段和堆栈。这需要修改链接脚本把代码段定位到 Flash 的映射地址数据段定位到 RAM 地址。这个环节对嵌入式功底要求比较高建议先在 QEMU 上学会读 System.map 和 vmlinux 符号地址再来折腾真机。第三是驱动选择。NoMMU 内核里不少驱动依赖 MMU 接口比如 DMA 映射、remap 操作驱动选型时优先选 simple 或者 nommu 兼容实现否则 SD 卡、以太网的 DMA 缓冲区分配很容易失败。这些内容我准备放到系列后面的文章里展开。现阶段只要你在 QEMU 上把 Image 做出来、rootfs 跑起来、静态 PIE 应用点亮RISC-V Non-MMU Linux 的骨架就已经牢牢抓在手里了。最后说一点个人体会刚开始我以为“无 MMU 的 Linux”就是个残废版本越往里钻越发现它是另一套完整的设计哲学牺牲虚拟内存带来的便利换来更小的运行时体积、更确定的中断响应和更贴近 MCU 环境的资源模型。如果你真想搞懂这套东西建议先动手再谈理论PIE、initramfs、vfork 这些概念亲手跑一遍比读十遍文档都有用。