Skip to content

内核内存管理

内核地址空间布局

在 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_DMA3232 位 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 对比:

特性kmallocvmalloc
物理连续✅ 是❌ 否
可用于 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_DEVICECPU → 设备(写操作)
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_unmapDMA 地址泄漏remove() 中配对释放
vmalloc 内存用于 DMA设备访问错误地址DMA 必须用 dma_alloc_coherent

褚成志的笔记