『7x24小时有问必答』
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/O

0x20020000 └────────────┘

每个地址都是真实的物理地址

// 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需求。
// 一致性DMA

dma_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用工具看内存——内存管理的本质没变,变的是谁来扛碎片、谁来挡越界。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码