文章目录
https://github.com/wdfk-prog/linux-study
mm/nommu.c NO-MMU内存管理(NO-MMU Memory Management) 适用于无内存管理单元的系统
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了让Linux操作系统能够运行在没有**内存管理单元(MMU)**的简单微控制器(MCU)和嵌入式处理器上。标准的Linux内核设计严重依赖MMU来实现以下核心功能:
- 虚拟内存:为每个进程提供独立的、巨大的、线性的虚拟地址空间。
- 内存保护:利用硬件机制防止一个进程访问另一个进程或内核的内存空间,保障系统稳定性和安全性。
- 内存映射与分页:实现写时复制(Copy-on-Write)、按需分页(Demand Paging)和交换(Swapping)等高级内存管理技术。
许多低成本、低功耗的嵌入式处理器(如ARM Cortex-M系列)为了节省芯片面积和功耗,并不包含MMU。mm/nommu.c
及其相关代码提供了一套替代的、简化的内存管理模型,使得功能强大的Linux内核能够在这种受限的硬件上运行,这通常被称为uClinux(Micro-Controller Linux)。
它的发展经历了哪些重要的里程碑或版本迭代?
NO-MMU支持最初源于一个独立的项目——uClinux。
- uClinux项目:该项目在20世纪90年代末启动,旨在将Linux裁剪并移植到无MMU的处理器上。它对内核的内存管理、进程管理等多个方面进行了重大修改。
- 主线合并:最重要的里程碑是uClinux项目的主要成果被逐步合并到Linux内核主线中。这使得NO-MMU不再是一个外部补丁集,而成为内核官方支持的一种配置(
CONFIG_MMU=n
)。mm/nommu.c
就是这一努力的核心成果。 - 持续完善:在并入主线后,社区持续对其进行改进,包括支持更多的处理器架构(ARM Cortex-M, MIPS, ColdFire, Blackfin等),优化其性能,并设法在NO-MMU环境下模拟或实现更多的标准Linux功能,例如通过特定机制支持共享库。
目前该技术的社区活跃度和主流应用情况如何?
NO-MMU是Linux内核中一个相对小众但至关重要的特性。其社区主要集中在嵌入式系统领域。它是让Linux运行在大量MCU上的基石,这些MCU被广泛应用于:
- 物联网(IoT)设备
- 工业自动化控制器
- 网络设备(如路由器、交换机)
- 消费电子产品
在这些场景下,使用带有MMU的全功能CPU在成本、功耗或复杂性上可能是过度的。项目如OpenWRT在某些特定的硬件目标上也依赖于NO-MMU支持。
核心原理与设计
它的核心工作原理是什么?
mm/nommu.c
的核心是完全抛弃了虚拟内存的概念,直接在物理内存上进行操作。
- 无虚拟地址:最根本的区别。程序中使用的所有地址都是物理地址,内核和硬件之间不存在地址转换层。
- 单一共享地址空间:所有进程(包括内核自身)都运行在同一个共享的物理地址空间中。这意味着没有硬件级别的内存隔离。一个进程中的指针理论上可以读写系统中任何其他进程乃至内核的内存。
mmap()
的重新实现:mmap()
系统调用在NO-MMU环境下被完全重写。它不再是将文件或设备映射到虚拟地址空间,而更像是一个增强版的malloc()
。它直接从全局的物理内存池中分配一块连续的内存区域。MAP_SHARED
和MAP_PRIVATE
的语义也因此变得不同且实现复杂。- 昂贵的
fork()
和重要的vfork()
:标准的fork()
系统调用变得极其低效,因为它需要完整地复制父进程的全部内存数据。因此,在NO-MMU程序设计中,vfork()
成为首选。vfork()
创建的子进程会“借用”父进程的内存空间,直到子进程调用execve()
加载新程序或退出为止,这期间父进程被挂起。 - 内存分配:系统使用一个简化的物理内存分配器(类似于slab或buddy system的变体)来管理整个RAM。由于没有分页机制,内存分配和释放可能导致物理内存碎片化。
- 栈管理:进程的栈是在创建时分配的一块固定大小的内存,通常不能动态增长。栈溢出将直接破坏相邻的内存区域,可能属于其他进程或内核。
它的主要优势体现在哪些方面?
- 广泛的硬件支持:使得Linux能够运行在海量的、无MMU的低成本、低功耗微控制器上。
- 实现简单:相对于复杂的MMU虚拟内存系统,NO-MMU的内存管理代码逻辑更简单。
- 低开销与确定性:没有地址转换带来的TLB(Translation Lookaside Buffer)未命中和页表遍历开销,内存访问延迟更具可预测性,这对某些软实时应用有益。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 无内存保护:这是其最致命的弱点。任何用户程序的错误(如野指针)都可能轻易地破坏其他进程或内核,导致整个系统崩溃。这使得系统非常脆弱和不安全。
- 无虚拟内存:程序可用的总内存不能超过物理RAM的大小,无法使用交换空间(swap)。
- 内存碎片:长时间运行的系统容易产生物理内存碎片,导致后续难以分配较大的连续内存块。
- 低效的进程创建:严重依赖
vfork()
,限制了传统多进程程序模型的使用。 - 固定的栈大小:程序需要仔细设计以避免栈溢出。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
- 微控制器(MCU)项目:这是其核心应用领域。例如,在一个基于ARM Cortex-M7的智能家居控制器中,开发者希望利用Linux强大的网络协议栈和文件系统,此时NO-MMU Linux是理想选择。
- 简单的网络设备:例如,一个SOHO路由器,其主要功能是数据包转发,不需要运行复杂的、多用户的应用程序,使用NO-MMU Linux可以降低硬件成本。
- 从RTOS迁移:当一个嵌入式项目的功能变得越来越复杂,传统的实时操作系统(RTOS)在网络、文件系统、USB支持等方面显得力不从心时,迁移到NO-MMU Linux可以重用大量现有的Linux软件生态,同时保持硬件成本不变。
是否有不推荐使用该技术的场景?为什么?
- 任何需要安全性的场景:由于没有内存保护,绝对不能用于运行不受信任代码或需要保护敏感数据的系统。
- 通用计算:桌面、服务器、智能手机等需要运行复杂、多样的应用程序,并要求稳定性和安全性的场景。
- 内存密集型应用:需要大量内存或依赖
fork()
创建大量进程的应用程序,在NO-MMU环境下会表现极差或根本无法运行。
对比分析
请将其 与 其他相似技术 进行详细对比。
与MMU-based Linux的对比
特性 | NO-MMU Linux (mm/nommu.c ) | MMU-based Linux (标准) |
---|---|---|
地址空间 | 单一、共享的物理地址空间 | 每个进程拥有独立的私有虚拟地址空间 |
内存保护 | 无。进程间和进程与内核间无硬件隔离。 | 完全。MMU硬件强制隔离,防止非法访问。 |
虚拟内存 | 无。无分页,无交换。可用内存等于物理RAM。 | 有。支持按需分页、交换到磁盘等。 |
fork() 实现 | 低效(完整内存拷贝),严重依赖vfork() 。 | 高效(写时复制, Copy-on-Write)。 |
性能特征 | 无地址转换开销,访问延迟更可预测。 | 存在页表遍历、TLB未命中等开销。 |
硬件要求 | 无需MMU的简单MCU/CPU。 | 必须有MMU的CPU。 |
与实时操作系统(RTOS)如FreeRTOS, Zephyr的对比
NO-MMU Linux的市场定位介于传统RTOS和全功能MMU Linux之间。
- 功能性:NO-MMU Linux提供了比绝大多数RTOS更丰富的功能,包括一个完整的POSIX兼容API、强大的网络协议栈(TCP/IP)、标准文件系统(Ext4, FAT32等)以及海量的用户空间程序和库。
- 实时性:虽然
PREEMPT_RT
补丁也可以应用于NO-MMU内核,但传统RTOS通常被设计为具有更低的、更可预测的中断和调度延迟,能提供更强的硬实时保证。 - 资源占用:RTOS的内核通常比Linux内核小得多,需要更少的RAM和ROM,启动速度也更快。
- 内存保护:许多现代RTOS(如Zephyr)支持利用**内存保护单元(MPU)**来实现任务间的内存隔离。MPU虽然不如MMU灵活,但提供了比NO-MMU Linux更好的健壮性。
结论:如果项目需要硬实时、内存占用极小,RTOS是更好的选择。如果项目需要丰富的网络功能、文件系统和Linux的软件生态,而硬件又没有MMU,那么NO-MMU Linux是最佳选择。
vmalloc 分配虚拟内存
void vfree(const void *addr)
{
kfree(addr);
}
EXPORT_SYMBOL(vfree);
void *__vmalloc_noprof(unsigned long size, gfp_t gfp_mask)
{
/*
* 您不能使用 kmalloc() 指定 __GFP_HIGHMEM,因为 kmalloc() 仅返回一个逻辑地址。
*/
return kmalloc_noprof(size, (gfp_mask | __GFP_COMP) & ~__GFP_HIGHMEM);
}
EXPORT_SYMBOL(__vmalloc_noprof);
mmap_init Mmap 初始化
static int sysctl_nr_trim_pages = CONFIG_NOMMU_INITIAL_TRIM_EXCESS;
static const struct ctl_table nommu_table[] = {
{
.procname = "nr_trim_pages",
.data = &sysctl_nr_trim_pages,
.maxlen = sizeof(sysctl_nr_trim_pages),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO,
},
};
/*
* 初始化 VM 和区域记录板的 perCPU 计数器
*/
void __init mmap_init(void)
{
int ret;
ret = percpu_counter_init(&vm_committed_as, 0, GFP_KERNEL);
VM_BUG_ON(ret);
vm_region_jar = KMEM_CACHE(vm_region, SLAB_PANIC|SLAB_ACCOUNT);
register_sysctl_init("vm", nommu_table);
}
dup_mmap 复制 mmap
int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
mmap_write_lock(oldmm);
dup_mm_exe_file(mm, oldmm);
mmap_write_unlock(oldmm);
return 0;
}
内存预留初始化 (init_user_reserve
& init_admin_reserve
)
这两个函数是Linux内核内存管理子系统在启动过程中的初始化例程。它们的核心作用是计算并设定两个关键的内存预留值: sysctl_user_reserve_kbytes
和 sysctl_admin_reserve_kbytes
。这两个预留值的根本目的是在系统可用内存即将耗尽的极端情况下, 依然为普通用户(特别是root用户)保留一小块"应急"内存, 以确保管理员至少有机会登录系统并终止失控的、大量消耗内存的"内存黑洞"进程, 从而避免系统完全锁死、只能硬重启的局面。
基本原理 (内存过量使用/Overcommit):
Linux内核在处理内存分配请求时, 采用了一种被称为"内存过量使用"的策略。这意味着内核允许进程申请的虚拟内存总量超过实际可用的物理内存与交换空间之和。这种策略基于一个普遍的观察: 大多数程序申请了内存后并不会立即全部使用。
然而, 当vm.overcommit_memory
sysctl参数被设置为2 (OVERCOMMIT_NEVER
)时, 内核会执行严格的记账。在允许一个新的内存分配之前, 它会检查系统总的"已承诺"内存加上新的请求量, 是否会超过一个安全的上限。这两个预留值就是从这个"安全上限"中扣除的部分。
-
sysctl_admin_reserve_kbytes
: 这是为root用户预留的内存。当一个非root进程请求内存时, 内核会确保即使分配成功后, 系统剩余的可承诺内存也不会低于这个管理员预留值。但是, 当一个root进程(如sshd、login、bash)请求内存时, 它被允许使用这部分预留内存。这就保证了即使系统被普通用户的进程塞满, root用户依然有足够的内存空间来启动一个shell和kill
等基本工具来恢复系统。 -
sysctl_user_reserve_kbytes
: 这是一个为所有用户(包括非root)预留的更小的内存池, 旨在防止单个用户进程耗尽所有内存, 导致该用户自己都无法启动新进程来处理问题。
init_user_reserve
函数解析
/*
* __meminit 宏告诉编译器, 这个函数和它引用的数据都位于".init"内存节.
* 在内核启动过程完成后, 这个内存节可以被安全地释放, 以回收内存.
*/
static int __meminit init_user_reserve(void)
{
unsigned long free_kbytes;
/*
* 调用 global_zone_page_state(NR_FREE_PAGES) 获取系统当前所有内存区域(zone)的空闲页面总数.
* K(x) 是一个宏, 定义为 ((x) << (PAGE_SHIFT-10)), 其作用是将页面数 x 转换为千字节(KB)数.
* - PAGE_SHIFT 通常为12 (对应4KB页面), 所以 PAGE_SHIFT-10 = 2.
* - x << 2 等同于 x * 4, 即 (页面数 * 4KB/页) = KB数.
*/
free_kbytes = K(global_zone_page_state(NR_FREE_PAGES));
/*
* 设置用户预留值. 它是以下两个值的较小者(min):
* 1. free_kbytes / 32: 当前空闲内存的 1/32, 约等于 3%.
* 2. 1UL << 17: 1左移17位, 等于131072. 这个值是128 * 1024, 即 128MB.
*
* 这个逻辑意味着: 用户预留值默认为空闲内存的3%, 但上限为128MB.
* 在内存小于约4GB的系统上, 它通常是3%; 在更大的服务器上, 它被封顶在128MB.
*/
sysctl_user_reserve_kbytes = min(free_kbytes / 32, 1UL << 17);
return 0;
}
/*
* subsys_initcall() 将此函数注册为在内核启动的"子系统初始化"阶段被调用.
* 这确保了它在内存管理核心已经就绪之后, 但在大多数驱动加载之前执行.
*/
subsys_initcall(init_user_reserve);
init_admin_reserve
函数解析
/*
* __meminit 宏, 同上, 用于启动后内存回收.
*/
static int __meminit init_admin_reserve(void)
{
unsigned long free_kbytes;
/*
* 获取当前空闲内存的总KB数, 逻辑同上.
*/
free_kbytes = K(global_zone_page_state(NR_FREE_PAGES));
/*
* 设置管理员预留值. 它是以下两个值的较小者(min):
* 1. free_kbytes / 32: 当前空闲内存的 1/32, 约等于 3%.
* 2. 1UL << 13: 1左移13位, 等于8192. 这个值是8 * 1024, 即 8MB.
*
* 逻辑与用户预留类似, 但上限更低, 为8MB.
* 注释中提到, 8MB被认为足够在内存紧张时启动sshd, bash和top等恢复工具.
* 在像STM32H750这样内存极小的系统上, 几乎总是会取 3% 这个值.
* 例如, 如果有1MB (1024KB) 空闲内存, 管理员预留将是 1024 / 32 = 32KB.
*/
sysctl_admin_reserve_kbytes = min(free_kbytes / 32, 1UL << 13);
return 0;
}
/* 将此函数注册为在"子系统初始化"阶段被调用. */
subsys_initcall(init_admin_reserve);