第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读到的也是错的。

解决这个问题有两种方式:

  1. 一致性DMA缓冲区:dma_alloc_coherent()分配。它保证CPU和DMA看到的视图一致。代价是性能稍差,因为每次访问都要绕过Cache或刷Cache。
  2. 流式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()。这样能保证顺序正确。

好了,这一章的内容就这些。内存管理是驱动开发的地基,地基不稳,上层再漂亮的代码也是白搭。下一章咱们聊聊中断处理和下半部机制,那也是车规级驱动里的重头戏。