当前位置:首页>Linux>从MCU到Linux MMU嵌入式内存完整解析

从MCU到Linux MMU嵌入式内存完整解析

  • 2026-09-28 18:27:39
从MCU到Linux MMU嵌入式内存完整解析
MCU上不敢malloc怕碎片,Linux上随便new不操心——MMU加三层分配器让物理碎片不再致命,本文从MCU到Linux理清四层内存架构。

一、MCU内存模型 物理地址的囚笼

MCU内存是编译时定死的硬平面图,每个地址都是真实物理地址:

0x08000000 ┌────────────┐           │  .text     │ ← Flash:代码段(只读)0x08002000 ├────────────┤           │  .rodata   │ ← Flash:只读数据(字符串常量)0x20000000 ├────────────┤           │  .data     │ ← RAM:已初始化全局变量(启动代码从Flash搬过来)0x20001000 ├────────────┤           │  .bss      │ ← RAM:未初始化全局变量(启动代码清零)0x20002000 ├────────────┤           │  heap      │ → 往高地址长(malloc/free在这片区域)0x20008000 ├────────────┤           │  ...空闲...│0x20010000 ├────────────┤           │  stack     │ ← 往低地址长(局部变量、函数调用)0x20018000 ├────────────┤           │  外设寄存器 │ ← 内存映射I/O0x20020000 └────────────┘每个地址都是真实的物理地址
// MCU上malloc三宗罪// 宗罪一:外部碎片——分配和释放交替进行char *a = malloc(100);   // 分配100字节在 heap 底部char *b = malloc(50);    // 分配50字节紧挨着free(a);                 // 释放100字节 → heap中间有个100字节的洞char *c = malloc(80);    // 要找 ≥80 的洞→100那个洞刚好→分裂成80+20空洞// 跑久了:heap里全是小空洞,找不到连续大块→malloc(1024)失败// 宗罪二:堆栈碰撞——heap往高长、stack往低长,撞上→互相踩数据→HardFault// 宗罪三:无保护——野指针踩别人内存,没有任何硬件机制阻止

物理地址空间平坦无保护——碎片不可逆转、溢出不可检测、越界不可阻止,这就是MCU上不敢用malloc的原因。

本节要点:MCU没有MMU,所有地址都是物理地址,malloc的碎片和越界没有任何硬件兜底。

二、RTOS过渡 heap_1到heap_5选型

FreeRTOS的heap_x.c决定堆行为,选对实现比写业务代码还重要:

实现
是否支持free
是否支持动态大小
关键特征
适用场景
heap_1
❌ 不支持
❌ 固定大小
只分配不释放,无碎片
启动时一次性分配任务栈/TCB
heap_2
✅ 支持
❌ 固定大小
最佳适应,不合并相邻空闲块
已逐渐被heap_4取代
heap_3
✅ 支持
✅ 支持
封装标准库malloc/free
快速移植,实时性不可控
heap_4
✅ 支持
❌ 固定大小
首次适应+合并相邻空闲块
最常用,频繁分配释放
heap_5
✅ 支持
❌ 固定大小
heap_4基础上支持多段不连续内存
有外部SDRAM、片内RAM分段

heap_4维护按地址升序的空闲块链表,首次适应+合并相邻块。heap_5通过vPortDefineHeapRegions()注册多段物理内存:

HeapRegion_t xHeapRegions[] = {    { (uint8_t *)0x20000000, 0x00010000 },  // 片内RAM 64KB    { (uint8_t *)0xC0000000, 0x00800000 },  // 外部SDRAM 8MB    { NULL, 0 }                             // 数组结束标记};vPortDefineHeapRegions(xHeapRegions);// xHeapRegions 必须按地址从低到高排序

即使有heap_4/heap_5,老司机仍优先静态分配——任务生命周期贯穿系统,动态分配风险高、收益低,map文件直接给出地址和大小,HardFault时一眼定位:

// 推荐:静态创建任务static StaticTask_t s_xTaskTcb;static StackType_t  s_xTaskStack[256];TaskHandle_t xHandle = xTaskCreateStatic(    vTaskCode, "task", 256, NULL, 1,    s_xTaskStack, &s_xTaskTcb);
本节要点:FreeRTOS选heap_4最通用,但优先静态分配才是嵌入式正道。

三、MMU与Linux内核分配器

MMU在CPU和物理内存之间加了页表翻译——同一个虚拟地址在不同进程映射不同物理页,互不干扰:

x86_64用4级页表(PGD→PUD→PMD→PTE),每查一次4次内存访问,TLB缓存最近映射。缺页时CPU触发page fault——Major Fault从磁盘读,Minor Fault只改页表,非法访问发SIGSEGV杀进程:

层级
名称
作用
PGD
Page Global Directory
进程页表顶层,mm_struct.pgd指向它
P4D
Page 4th Directory
5级页表时存在,4级时折叠到PGD
PUD
Page Upper Directory
中间层
PMD
Page Middle Directory
中间层
PTE
Page Table Entry
指向物理页框,记录读写/执行/存在位
改变
MCU没有MMU
Linux有MMU
碎片
物理碎片无法消弭
虚拟地址拼连续大块,物理碎片不再要命
保护
野指针直接踩别人内存
非法访问→SIGSEGV,杀进程不杀系统
共享
全局变量就是共享内存
两进程页表指向同一物理页→mmap共享

Buddy系统:内核物理页分配器,只分配2^n × 页大小的块,相邻空闲块自动合并。优点防外部碎片+O(log n),缺点内部碎片(申请8字节也分4KB页)。物理内存按Zone划分:ZONE_DMA(低16MB)、ZONE_DMA32(低4GB)、ZONE_NORMAL(内核直接映射)。嵌入式通常只有DMA32和NORMAL。

初始:256个连续空闲页(1MB) 分配100KB(需要32页:128KB的块):   256页块 → 拆:128页 + 128页 → 分配出去 释放这128页:相邻128页块也空闲?→ 合并为256页块cat /proc/buddyinfo# Node 0, zone Normal  1000  500  200  50  10  5  2  1  0  0  0#                       ↑1页  ↑2页 ↑4页 ↑8页 ↑16页...

Slab/Slub:从Buddy拿一页切成同大小槽位,按size class分配。kmalloc(10)走kmalloc-32。三种状态:FULL(从Buddy要新页)、PARTIAL(优先分配)、EMPTY(可还给Buddy):

分配器
特征
适用场景
SLAB
原始实现,元数据开销大
大型SMP服务器
SLUB
现代Linux默认,调试支持好
通用服务器/桌面
SLOB
极简,内存占用最小
内存极小嵌入式设备
cat /proc/slabinfo | head# slab_name         active_objs  num_objs  objsize  objperslab# task_struct           234       240       3264        10# kmalloc-32           1200       1280       32        128

vmalloc:物理碎片化时,在虚拟地址空间拼连续区域,零散4KB页通过页表映射成连续虚拟地址。代价:TLB条目多、访问慢、不能DMA。kmalloc给小内存(物理连续),vmalloc给大块(虚拟连续):

void *p = kmalloc(256, GFP_KERNEL);    // 小内存,物理连续void *p = vmalloc(1024 * 1024);        // 大内存,虚拟连续// insmod加载.ko模块就是vmalloc——不需要物理连续

DMA与CMA:DMA设备看物理地址,要求缓冲区物理连续。dma_alloc_coherent()分配uncached缓冲区长期持有(环形缓冲区、描述符环);dma_map_single()临时映射已有内存(网络skb、文件页缓存)。CMA启动时预留可迁移区域,平时借给普通页,驱动需要大连续物理内存时迁移走,兼顾日常利用率和DMA需求。

// 一致性DMAdma_addr_t dma_handle;void *cpu_addr = dma_alloc_coherent(dev, 4096, &dma_handle, GFP_KERNEL);// cpu_addr:CPU虚拟地址;dma_handle:DMA总线地址// 流式DMA映射dma_addr_t dma_handle = dma_map_single(dev, vaddr, size, DMA_TO_DEVICE);dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
特性
dma_alloc_coherentdma_map_single
是否新建缓冲区
是
否,映射已有内存
cache一致性
硬件保证或uncached
驱动在map/unmap点显式同步
使用次数
长期持有
每次DMA传输前后map/unmap
典型场景
环形缓冲区、描述符环
网络包、磁盘IO
本节要点:MMU让虚拟地址掩盖物理碎片,Buddy管页、Slab管对象、vmalloc管大块、CMA管DMA,四层分工各管一摊。

四、用户态malloc glibc内存池

malloc(100)不走内核Buddy——先查glibc的freelist,有合适块直接返回不进内核。没有时:小内存(<128KB)走brk()扩展堆顶,大内存(≥128KB)走mmap()映射新区域,释放时munmap立刻还物理页:

// 小内存(< 128KB):用 brk()p = malloc(1024);// → brk 往上挪 → glibc切一块给p// 大内存(≥ 128KB):用 mmap()p = malloc(1024 * 1024);// → mmap 在虚拟地址空间找1MB空闲区域 → 映射物理页// 释放后 munmap 立刻还物理页给内核

glibc的freelist = 你手写内存池的next指针,只不过你的池从static uint8_t pool[SIZE]拿,glibc不够了找内核要。ASLR默认开启,每次启动地址随机偏移,阻止攻击者预测地址,调试需确定性时加nokaslr关闭。

本节要点:malloc先走用户态freelist,不够才进内核,glibc内存池本质就是你手写内存池的内核版。

五、全景对比与注意事项

维度
MCU裸机
FreeRTOS
Linux
地址类型
物理地址
物理地址
虚拟地址(MMU翻译)
堆实现
无/自己写内存池
heap_1~heap_5可选
Buddy+Slab+vmalloc+glibc
推荐策略
静态分配为主
静态分配为主,堆为辅
动态分配为主
碎片
真实碎片,不可逆转
heap_4可合并,仍有碎片
虚拟地址掩盖物理碎片
保护
野指针直接踩坏任何内存
同MCU,依赖MPU(可选)
MMU拦截非法访问→SIGSEGV
栈管理
固定大小,溢出不报错
任务栈固定,溢出触发HardFault
按需增长(缺页异常自动扩展)
共享内存
天然支持(全局变量=共享)
通过全局变量/消息缓冲区
需要mmap/MAP_SHARED建立映射
DMA
直接物理地址
直接物理地址,注意cache一致性
dma_alloc_coherent
 / CMA
调试工具
map文件分析
FreeRTOS heap钩子、栈溢出检测
kmemleak, slub_debug, valgrind, ASan
工具
检测范围
典型开销
适用阶段
kmemleak
内核未释放内存
低
运行时长期测试
slub_debug
内核slab越界/释放后使用
高
模块调试
valgrind
用户态内存错误
极高(10~50x)
单元测试
ASan
用户态缓冲区溢出、释放后使用
中(2~3x)
开发/CI
坑
现象
解法
kmalloc大块内存总失败
物理内存碎片导致没有连续大块
>128KB用vmalloc,或启动时预先kmalloc
用户态free后还读
use-after-free,偶发性崩溃
valgrind检测,或用智能指针(C++)
内核内存泄漏
free_proc()忘匹配kfree,内存越用越少
kmemleak检测
OOM Killer乱杀进程
内存用满,内核选了个"坏蛋"杀掉
限制进程内存:cgroup memory
vmalloc区域用完了
32位系统vmalloc区域只有约128MB
用64位内核
GFP_ATOMIC失败率高
原子上下文可用的紧急内存池太小
尽量在进程上下文分配GFP_KERNEL
本节要点:三层系统各有软肋——MCU怕越界无感知,RTOS怕碎片化,Linux怕OOM和泄漏。

六、总结 四层塔的分工

  • MCU内存管理是设计阶段的事,画张静态分配表就完
  • Linux内存管理是运行时基础设施,工具链完善但需理解层次
  • kmalloc跟malloc不比速度——一个走Slab/物理连续,一个走glibc/虚拟连续
  • 理解四层分工,在嵌入式和Linux之间切换

MCU用脑子管内存,Linux用工具看内存——内存管理的本质没变,变的是谁来扛碎片、谁来挡越界。

最新文章

随机文章