内核内存管理
内核地址空间布局
在 64 位 ARM(arm64)系统上,典型的虚拟地址空间划分:
0xFFFFFFFFFFFFFFFF ┐
│ 内核空间(高地址)
0xFFFF000000000000 │ vmalloc 区域
│ 直接映射区(线性映射物理内存)
0xFFFF800000000000 ┘
0x0000FFFFFFFFFFFF ┐
│ 用户空间(低地址)
0x0000000000000000 ┘驱动开发主要关注内核空间的内存分配。
物理内存分配:页分配器
最底层的分配器,以页(4KB)为单位:
c
#include <linux/gfp.h>
/* 分配 2^order 个连续物理页 */
struct page *page = alloc_pages(GFP_KERNEL, order);
void *addr = page_address(page); /* 获取虚拟地址 */
/* 释放 */
__free_pages(page, order);
/* 快捷接口:分配单页并返回虚拟地址 */
unsigned long addr = __get_free_page(GFP_KERNEL);
free_page(addr);GFP 标志
| 标志 | 说明 | 使用场景 |
|---|---|---|
GFP_KERNEL | 可睡眠,允许内存回收 | 进程上下文(最常用) |
GFP_ATOMIC | 不可睡眠,不允许回收 | 中断上下文、持有自旋锁时 |
GFP_DMA | 分配 DMA 可访问内存(低 16MB) | ISA DMA |
GFP_DMA32 | 32 位 DMA 地址范围 | 老旧 PCI 设备 |
GFP_NOWAIT | 不等待,失败立即返回 | 实时路径 |
__GFP_ZERO | 分配后清零 | 安全敏感场景 |
规则:中断处理函数、tasklet、持有自旋锁的代码段,必须使用
GFP_ATOMIC,否则可能死锁。
slab 分配器:kmalloc / kfree
对于小于一页的内存,使用 slab 分配器(现代内核默认为 SLUB):
c
#include <linux/slab.h>
/* 分配 size 字节,物理连续 */
void *ptr = kmalloc(size, GFP_KERNEL);
if (!ptr)
return -ENOMEM;
/* 分配并清零 */
void *ptr = kzalloc(size, GFP_KERNEL);
/* 重新分配 */
ptr = krealloc(ptr, new_size, GFP_KERNEL);
/* 释放 */
kfree(ptr);自定义 slab 缓存
频繁分配/释放同一类型对象时,创建专用缓存可显著提升性能:
c
/* 驱动初始化时创建缓存 */
static struct kmem_cache *my_cache;
my_cache = kmem_cache_create(
"my_object", /* 名称(出现在 /proc/slabinfo) */
sizeof(struct my_obj),/* 对象大小 */
0, /* 对齐(0 = 默认) */
SLAB_HWCACHE_ALIGN, /* 标志 */
NULL /* 构造函数 */
);
/* 分配对象 */
struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 释放对象 */
kmem_cache_free(my_cache, obj);
/* 驱动卸载时销毁缓存 */
kmem_cache_destroy(my_cache);vmalloc:虚拟连续内存
当需要大块内存但不要求物理连续时使用:
c
#include <linux/vmalloc.h>
void *ptr = vmalloc(size); /* 虚拟连续,物理可不连续 */
vfree(ptr);
void *ptr = vzalloc(size); /* 分配并清零 */kmalloc vs vmalloc 对比:
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续 | ✅ 是 | ❌ 否 |
| 可用于 DMA | ✅ 是 | ❌ 否 |
| 最大分配 | ~4MB(受 order 限制) | 受 vmalloc 区域限制(GB 级) |
| 分配速度 | 快 | 慢(需建立页表) |
| 适用场景 | 小对象、DMA 缓冲区 | 大型固件缓冲区、模块代码 |
DMA 内存分配
DMA 传输要求物理连续且设备可访问的内存,使用 DMA API:
c
#include <linux/dma-mapping.h>
/* 一致性 DMA 内存(CPU 和设备都能访问,无需手动同步) */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* cpu_addr:CPU 访问的虚拟地址 */
/* dma_handle:设备使用的总线地址,写入设备寄存器 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
/* 流式 DMA(性能更高,需手动同步) */
dma_addr_t dma_handle = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle))
return -ENOMEM;
/* DMA 传输完成后同步 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);DMA 方向
| 宏 | 说明 |
|---|---|
DMA_TO_DEVICE | CPU → 设备(写操作) |
DMA_FROM_DEVICE | 设备 → CPU(读操作) |
DMA_BIDIRECTIONAL | 双向 |
内存屏障
在多核系统和 DMA 场景中,编译器和 CPU 可能重排内存访问顺序,需要显式屏障:
c
#include <linux/compiler.h>
#include <asm/barrier.h>
/* 编译器屏障(防止编译器重排) */
barrier();
/* 内存屏障(防止 CPU 重排) */
mb(); /* 读写屏障 */
rmb(); /* 读屏障 */
wmb(); /* 写屏障 */
/* DMA 场景专用 */
dma_wmb(); /* 写入描述符前确保数据已写入内存 */
dma_rmb(); /* 读取描述符后确保数据可见 */内存泄漏检测
bash
# 查看 slab 使用情况
cat /proc/slabinfo
# 查看内存使用概况
cat /proc/meminfo
# kmemleak:内核内存泄漏检测器(需 CONFIG_DEBUG_KMEMLEAK=y)
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak常见错误与规避
| 错误 | 后果 | 规避方法 |
|---|---|---|
中断上下文使用 GFP_KERNEL | 死锁/睡眠崩溃 | 改用 GFP_ATOMIC |
kfree 后继续访问 | use-after-free | 释放后立即置 NULL |
重复 kfree | 内存损坏 | 使用 kfree_and_null() 或检查 NULL |
忘记 dma_unmap | DMA 地址泄漏 | 在 remove() 中配对释放 |
| vmalloc 内存用于 DMA | 设备访问错误地址 | DMA 必须用 dma_alloc_coherent |