第1章:内核内存管理基础
各位同学,咱们今天聊聊内核内存管理。说实话,这是驱动开发里最容易踩坑的地方之一。我做了十几年车规级驱动,见过太多因为内存问题导致的死机、数据错乱,甚至整车上电后黑屏。嗯,咱们从最基础的开始讲。
1.1 页分配器(Buddy System)
页分配器,说白了就是内核管理物理内存的"大管家"。它把物理内存分成2的幂次方大小的块,比如4KB、8KB、16KB……一直到4MB。这种设计叫Buddy System,伙伴系统。
为什么用这种设计?我举个例子。你申请一块8KB内存,系统就从8KB的空闲链表里拿。如果没有8KB的,就拆一个16KB的,一半给你,另一半挂回8KB链表。释放时,如果相邻的伙伴也是空闲的,就合并回16KB。这样能最大限度减少碎片。
关键点:页分配器分配的最小单位是页(通常4KB)。你申请1字节,它也给你一页。所以驱动里别频繁申请小内存,否则浪费惊人。
我在项目中遇到过一个问题:某个传感器驱动每10ms申请一次4KB内存,用完后释放。看起来没问题对吧?但运行几小时后,系统内存碎片化严重,再也申请不到连续的大块内存。后来改成内存池预分配,问题解决。
1.2 Slab分配器
Slab分配器就是来解决上面那个问题的。它基于页分配器,但专门管理小对象。比如你经常要分配一个struct foo,大小是64字节。Slab会提前申请一页或多页,切成多个64字节的槽位,用的时候直接拿,用完放回。
我个人习惯在驱动初始化时,用kmem_cache_create()创建一个专属缓存。这样分配和释放都很快,而且不会产生碎片。
// 创建一个slab缓存
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_driver_cache",
sizeof(struct my_data),
0, 0, NULL);
// 分配对象
struct my_data *p = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放对象
kmem_cache_free(my_cache, p);
// 销毁缓存
kmem_cache_destroy(my_cache);
小技巧:用slabtop命令可以查看系统里所有slab缓存的使用情况。调试内存泄漏时特别有用。
1.3 kmalloc vs vmalloc
这两个函数是驱动开发里最常用的内存分配接口。但很多人搞混它们。
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 连续 | 不连续 |
| 虚拟连续性 | 连续 | 连续 |
| 分配速度 | 快 | 慢(需要建立页表) |
| 最大大小 | 受限于连续物理内存 | 仅受虚拟地址空间限制 |
| 适用场景 | DMA、硬件访问 | 大数据块、模块加载 |
你想想看,DMA需要物理连续的内存,所以必须用kmalloc。而vmalloc分配的内存物理上可能分散在各处,但虚拟地址是连续的。我建议驱动里优先用kmalloc,除非你要分配很大的缓冲区(比如几百KB以上)。
警告:在中断上下文或持有自旋锁时,绝对不能使用vmalloc。因为它可能会睡眠等待内存。kmalloc可以用GFP_ATOMIC标志来避免睡眠。
1.4 DMA内存分配与一致性
车规级驱动里,DMA是家常便饭。但DMA有个头疼的问题:缓存一致性。
CPU有缓存(Cache),DMA设备直接读写物理内存。如果CPU修改了某块内存,但数据还在Cache里没写回物理内存,DMA读到的就是旧数据。反过来,DMA写入了新数据,但CPU的Cache里还是旧数据,CPU读到的也是错的。
解决这个问题有两种方式:
- 一致性DMA缓冲区:用
dma_alloc_coherent()分配。它保证CPU和DMA看到的视图一致。代价是性能稍差,因为每次访问都要绕过Cache或刷Cache。 - 流式DMA映射:用
dma_map_single()。你需要手动管理同步,用dma_sync_single_for_cpu()和dma_sync_single_for_device()。性能好,但容易出错。
// 一致性分配
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// 流式映射
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
// 使用完后
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
我曾经在ADAS摄像头驱动里,因为忘记在DMA传输前调用dma_sync_single_for_device(),导致图像数据全是花的。查了两天才找到原因。嗯,从那以后我养成了习惯:每次DMA操作前后都检查同步。
1.5 内存屏障与缓存一致性
内存屏障,这是个更底层的话题。它用来保证CPU对内存访问的顺序。为什么需要这个?因为现代CPU会乱序执行,也会对内存访问进行重排。
举个例子:你写了一个标志位,然后启动DMA。如果CPU把写标志位的操作重排到了启动DMA之后,DMA可能看到的是旧标志位,导致逻辑错误。
Linux内核提供了几种内存屏障:
mb():全能屏障,保证读写都按顺序rmb():读屏障,保证读操作顺序wmb():写屏障,保证写操作顺序dma_wmb():DMA专用的写屏障,轻量级
核心原则:在驱动里,如果CPU和DMA设备共享内存,一定要在适当位置插入内存屏障。否则,你永远不知道CPU什么时候真正把数据写到了物理内存。
我建议你这样用:在启动DMA之前,加一个wmb()或dma_wmb()。在DMA完成后,CPU读取数据前,加一个rmb()。这样能保证顺序正确。
好了,这一章的内容就这些。内存管理是驱动开发的地基,地基不稳,上层再漂亮的代码也是白搭。下一章咱们聊聊中断处理和下半部机制,那也是车规级驱动里的重头戏。