内核内存管理与DMA:从ZONE划分到跨设备共享的实战指南
写驱动绕不开两件事:内存怎么分,DMA怎么搬,内核把物理内存切成ZONE,每个ZONE有自己的分配策略和限制;DMA让设备直取内存,但cache一致性、地址映射、缓冲区生命周期全是坑。本文基于Linux 6.8内核,从ZONE划分出发,覆盖kmalloc/vmalloc/mempool等分配API,再深入DMA一致性映射和流式映射,最后看dma-buf跨设备共享和内存调试手段。
一、内核内存区。
物理内存不是一整块随便用的,内核按地址范围和访问限制把内存划分为不同的ZONE,每个ZONE有独立的分配策略。
各ZONE说明
| ZONE | 地址范围 | 典型场景 | 说明 |
|---|---|---|---|
| ZONE_DMA | 0 ~ 16MB | ISA设备DMA | 24位地址线的老硬件只能访问低16MB,现代x86基本不用 |
| ZONE_DMA32 | 0 ~ 4GB | 32位DMA设备 | 64位系统上,32位地址线的网卡、磁盘控制器只能访问4GB以下 |
| ZONE_NORMAL | 直接映射区 | 内核通用分配 | 64位系统上覆盖全部物理内存,32位系统只有896MB |
| ZONE_HIGHMEM | 896MB以上 | 32位大内存 | 64位系统不存在此ZONE,32位系统通过kmap临时映射访问 |
| ZONE_MOVABLE | 可迁移页 | 内存热插拔 | 页面可迁移,用于内存碎片整理和热插拔,不绑定固定物理地址 |
64位系统上ZONE_NORMAL覆盖全部物理内存,ZONE_HIGHMEM不存在,这是64位驱动开发比32位轻松的核心原因。
二、内存分配API
内核提供了多种内存分配接口,各有适用场景,选错API轻则浪费内存,重则触发oops。
2.1 kmalloc / kfree
最常用的内核内存分配函数,返回物理连续的内存块。
#include <linux/slab.h>
/* 分配物理连续内存 */
void *ptr = kmalloc(1024, GFP_KERNEL);
if (!ptr)
return -ENOMEM;
/* 使用完毕释放 */
kfree(ptr);
关键字。
- 分配的内存物理连续,适合DMA
- 最大分配大小取决于体系结构,通常几MB(order-5到order-11,即32页到2048页)
- GFP_KERNEL可能睡眠,不能在中断上下文使。- 中断上下文用GFP_ATOMIC,但分配可能失败
2.2 kzalloc
kmalloc + memset清零的组合:
/* 分配并清*/
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* dev所有字段已清零,无需手动memset */
驱动开发中kzalloc比kmalloc用得更多,结构体分配后清零是基本操作集
2.3 vmalloc / vfree
分配虚拟连续但物理不连续的内存:
#include <linux/vmalloc.h>
/* 分配1MB虚拟连续内存 */
void *buf = vmalloc(1024 * 1024);
if (!buf)
return -ENOMEM;
/* 释放 */
vfree(buf);
注意:vmalloc分配的内存不能用于DMA*,因为物理页面不连续,适合大块临时缓冲区。
2.4 __get_free_pages / free_pages
以页为单位分配连续物理内存:
#include <linux/gfp.h>
/* 分配4页(16KB)连续物理内存,order=2表示2^2=4*/
unsigned long addr = __get_free_pages(GFP_KERNEL, 2);
if (!addr)
return -ENOMEM;
/* 释放,order必须与分配时一*/
free_pages(addr, 2);
order表示分配2^order个页,order=0。。4KB),order=1。。8KB),以此类推。MAX_ORDER通常。1,即最。048。8MB)。
2.5 devm_kzalloc
设备资源管理的自动释放版本:
#include <linux/device.h>
/* probe中分配,remove时自动释*/
struct my_state *state = devm_kzalloc(dev, sizeof(*state), GFP_KERNEL);
if (!state)
return -ENOMEM;
/* 不需要手动kfree,驱动卸载时自动释放 */
devm_kzalloc绑定了device的生命周期,驱动remove时自动释放,省去手动管理释放的麻烦,也避免忘记释放导致泄漏。
API对比总表
| API | 物理连续 | 大小限制 | 可用上下文 | 速度 | 典型场景 |
|---|---|---|---|---|---|
| kmalloc | 连续 | 几MB | 进程上下文(GFP_KERNEL)/任意(GFP_ATOMIC) | 小块内存、DMA缓冲区 | |
| kzalloc | 连续 | 几MB | 同kmalloc | 结构体分配 | |
| vmalloc | 不连续 | 几GB | 进程上下文 | 大块临时缓冲 | |
| __get_free_pages | 连续 | 8MB(MAX_ORDER) | 进程上下文 | 页级对齐的DMA | |
| devm_kzalloc | 连续 | 几MB | 进程上下文 | 设备私有数据 |
三、kmalloc vs vmalloc
这两个是最容易混淆的分配接口,放在一起对比
详细对比
| 对比 | kmalloc | vmalloc |
|---|---|---|
| 物理连续 | 连续 | 不连续 |
| 虚拟连续 | 连续 | 连续 |
| 最大分配 | 几MB(受slab/buddy限制) | 几GB(受虚拟地址空间限制) |
| 分配速度 | 快(slab缓存命中极快) | 慢(需建立页表映射) |
| 访问速度 | 快(TLB命中率高) | 慢(物理不连续,TLB miss多) |
| 可否DMA | 可以 | 不可以 |
| 可否原子上下文 | 可以(GFP_ATOMIC) | 不可以(可能睡眠) |
| 底层实现 | slab分配器 | 修改页表映射 |
| 典型用途 | 结构体、小缓冲区、DMA | 大块临时缓冲 |
slab分配器简。
kmalloc的底层是slab分配器。slab在buddy分配器之上又加了一层缓存:预分配相同大小的对象缓存,分配和释放都是O(1)操作。Linux 6.8默认使用SLUB实现(CONFIG_SLUB=y),相比老版SLOB和SLAB,SLUB在多核场景下锁竞争更少。
slab的核心思路径
1. buddy分配置*按页分配置KB为单位),粒度太。2. slab**把页切成固定大小的对象(8B。6B。2B...直到几MB),按对象大小分cache
3. 分配时从对应大小的cache取一个空闲对象,释放时归还cache
4. cache空了就从buddy要新页,cache对象全空闲就归还buddy
这就是为什么kmalloc小对象特别快——cache里大概率有现成的。
四、DMA缓冲区管理
DMA(Direct Memory Access)让设备绕过CPU直接读写内存。网卡收包、磁盘读写、摄像头采集,全靠DMA搬运数据。但DMA引入了cache一致性、地址映射、缓冲区生命周期等新问题。
4.1 DMA原理
CPU和DMA设备看到的是同一块物理内存,但CPU有cache,DMA设备直接操作物理内存。如果CPU写数据后还留在cache里没写回内存,DMA设备读到的就是旧数据——这就是cache一致性问题。
4.2 一致性DMA映射
一致性DMA(Coherent DMA)在分配时就保证CPU和设备看到的内存内容一致,不需要手动同步。适合控制寄存器、描述符表等长期存在的缓冲区。
#include <linux/dma-mapping.h>
struct my_dev {
struct device *dev;
void *virt_addr; /* CPU访问的虚拟地址 */
dma_addr_t bus_addr; /* 设备访问的总线地址 */
size_t size;
};
static int alloc_coherent_buf(struct my_dev *md)
{
md->size = 4096;
/* 分配一致性DMA缓冲区,返回CPU虚拟地址,bus_addr输出总线地址 */
md->virt_addr = dma_alloc_coherent(md->dev, md->size,
&md->bus_addr, GFP_KERNEL);
if (!md->virt_addr)
return -ENOMEM;
/* CPU写数*/
memset(md->virt_addr, 0xAA, md->size);
/* 设备用bus_addr访问同一块内存,数据一定是最新的 */
dev_info(md->dev, "coherent buf: virt=%p bus=%pad size=%zu\n",
md->virt_addr, &md->bus_addr, md->size);
return 0;
}
static void free_coherent_buf(struct my_dev *md)
{
if (md->virt_addr) {
dma_free_coherent(md->dev, md->size,
md->virt_addr, md->bus_addr);
md->virt_addr = NULL;
}
}
*一致性DMA的特。
- 分配时硬件自动保证cache一致性,代价是可能禁用该区域的cache
- 适合频繁CPU和设备交替访问的缓冲。- 缺点:性能略低(cache被禁用),且不能在中断上下文分配
4.3 流式DMA映射
流式DMA(Streaming DMA)临时映射一块已有的内存给设备使用,需要手动管理cache同步。适合一次性的数据传输,性能比一致性DMA高。
struct stream_buf {
void *virt;
dma_addr_t bus;
size_t size;
enum dma_data_direction dir;
};
static int map_stream_buf(struct device *dev, struct stream_buf *sb)
{
/* 映射:返回总线地址,同时处理cache一致*/
sb->bus = dma_map_single(dev, sb->virt, sb->size, sb->dir);
if (dma_mapping_error(dev, sb->bus))
return -EIO;
return 0;
}
static void sync_for_device(struct device *dev, struct stream_buf *sb)
{
/* CPU写完数据后,刷cache让设备看到最新内存*/
dma_sync_single_for_device(dev, sb->bus, sb->size, sb->dir);
}
static void sync_for_cpu(struct device *dev, struct stream_buf *sb)
{
/* 设备写完数据后,使cache无效让CPU看到最新内存*/
dma_sync_single_for_cpu(dev, sb->bus, sb->size, sb->dir);
}
static void unmap_stream_buf(struct device *dev, struct stream_buf *sb)
{
dma_unmap_single(dev, sb->bus, sb->size, sb->dir);
}
4.4 DMA映射方向
| 方向 | 含义 | 典型场景 |
|---|---|---|
| DMA_TO_DEVICE | CPU→设备 | 发送网络包、写磁盘 |
| DMA_FROM_DEVICE | 设备→CPU | 接收网络包、读磁盘 |
| DMA_BIDIRECTIONAL | 双向 | 控制寄存器、共享缓冲区 |
方向必须正确声明。声明DMA_TO_DEVICE的缓冲区,设备写入的数据CPU可能看不到(cache没失效)。
4.5 dma_addr_t与总线地址
内核中涉及三种地址。
| 地址类型 | 说明 | 谁用 |
|:---------|:-----|:-----|
| 虚拟地址 | CPU通过MMU访问的地址 | CPU(软件) |
| 物理地址 | 内存条上的实际地址 | 内核页表管理 |
| 总线地址(dma_addr_t) | 设备通过总线看到的地址 | DMA设备 |
在x86上,总线地址通常等于物理地址。但在有IOMMU的系统上,总线地址是IOMMU映射后的地址,和物理地址完全不同。驱动必须用dma_map_single等API获取总线地址,不能假设总线地址等于物理地址。
4.6 完整DMA驱动示例
下面是一个完整的虚拟DMA驱动,演示一致性DMA和流式DMA的使用:
// dma_demo.c - DMA缓冲区管理示例(Linux 6.8验证:#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#define COHERENT_SIZE 4096
#define STREAM_SIZE 2048
struct dma_demo_dev {
struct device *dev;
/* 一致性DMA */
void *coh_virt;
dma_addr_t coh_bus;
/* 流式DMA */
void *str_virt;
dma_addr_t str_bus;
};
static int dma_demo_probe(struct platform_device *pdev)
{
struct dma_demo_dev *md;
int ret;
md = kzalloc(sizeof(*md), GFP_KERNEL);
if (!md)
return -ENOMEM;
md->dev = &pdev->dev;
platform_set_drvdata(pdev, md);
/* 检查DMA寻址能力 */
ret = dma_set_mask_and_coherent(md->dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(md->dev, "dma_set_mask failed: %d\n", ret);
goto err_free;
}
/* 1. 一致性DMA分配 */
md->coh_virt = dma_alloc_coherent(md->dev, COHERENT_SIZE,
&md->coh_bus, GFP_KERNEL);
if (!md->coh_virt) {
dev_err(md->dev, "dma_alloc_coherent failed\n");
ret = -ENOMEM;
goto err_free;
}
/* CPU写数据,设备立即可见 */
memset(md->coh_virt, 0x55, COHERENT_SIZE);
dev_info(md->dev, "coherent: virt=%p bus=%pad\n",
md->coh_virt, &md->coh_bus);
/* 2. 流式DMA分配 */
md->str_virt = kzalloc(STREAM_SIZE, GFP_KERNEL);
if (!md->str_virt) {
ret = -ENOMEM;
goto err_free_coh;
}
/* CPU准备发送数*/
memset(md->str_virt, 0xAA, STREAM_SIZE);
/* 映射给设*/
md->str_bus = dma_map_single(md->dev, md->str_virt,
STREAM_SIZE, DMA_TO_DEVICE);
if (dma_mapping_error(md->dev, md->str_bus)) {
dev_err(md->dev, "dma_map_single failed\n");
ret = -EIO;
goto err_free_str;
}
dev_info(md->dev, "stream: virt=%p bus=%pad\n",
md->str_virt, &md->str_bus);
/* 传输完成后解除映*/
dma_unmap_single(md->dev, md->str_bus, STREAM_SIZE, DMA_TO_DEVICE);
dev_info(md->dev, "DMA demo probe ok\n");
return 0;
err_free_str:
kfree(md->str_virt);
err_free_coh:
dma_free_coherent(md->dev, COHERENT_SIZE, md->coh_virt, md->coh_bus);
err_free:
kfree(md);
return ret;
}
static void dma_demo_remove(struct platform_device *pdev)
{
struct dma_demo_dev *md = platform_get_drvdata(pdev);
if (md->str_virt)
kfree(md->str_virt);
if (md->coh_virt)
dma_free_coherent(md->dev, COHERENT_SIZE,
md->coh_virt, md->coh_bus);
kfree(md);
dev_info(&pdev->dev, "DMA demo removed\n");
}
static const struct of_device_id dma_demo_of_match[] = {
{ .compatible = "linuxros,dma-demo" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dma_demo_of_match);
static struct platform_driver dma_demo_driver = {
.probe = dma_demo_probe,
.remove = dma_demo_remove,
.driver = {
.name = "dma-demo",
.of_match_table = dma_demo_of_match,
},
};
module_platform_driver(dma_demo_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("DMA缓冲区管理示例);
对应的Makefile。
obj-m := dma_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
五、DMA-BUF与跨设备共享
单个设备的DMA缓冲区管理用4.2。.3的方法就够了。但当多个设备需要共享同一块缓冲区时(比如摄像头采集后GPU做推理),问题来了:每个设备有自己的DMA映射,怎么保证它们访问同一块物理内存?
5.1 dma_buf机制
dma_buf是内核提供的跨设备缓冲区共享框架,核心思路径
1. 导出*(exporter):拥有缓冲区的设备,通过dma_buf_export导出为fd
2. 导入*(importer):需要访问缓冲区的设备,通过dma_buf_get获取fd,再dma_buf_attach映射到自己的地址空间
5.2 核心API
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
/* 导出方:将自有缓冲区导出为dma_buf */
struct dma_buf_export_info exp_info = {
.exp_name = "my-exporter",
.owner = THIS_MODULE,
.ops = &my_dma_buf_ops, /* 实现attach/detach/map/unmap等回调*/
.size = buf_size,
.flags = O_RDWR,
.priv = my_buf, /* 传给ops回调的私有数*/
};
struct dma_buf *dmabuf = dma_buf_export(&exp_info);
int fd = dma_buf_fd(dmabuf, O_RDWR | O_CLOEXEC);
/* 导入方:通过fd获取dma_buf */
struct dma_buf *dmabuf = dma_buf_get(fd);
struct dma_buf_attachment *attach = dma_buf_attach(dmabuf, dev);
struct sg_table *sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
/* sgt中包含scatter-gather列表,可获取dma_addr_t */
/* 使用完毕 */
dma_buf_unmap_attachment(attach, sgt, DMA_BIDIRECTIONAL);
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);
5.3 与V4L2/GPU/DRM的关。
dma_buf在多媒体和GPU领域是基础设施。
- V4L2:摄像头通过VIDIOC_EXPBUF导出dma_buf fd
- DRM/KMS:GPU通过DRM_IOCTL_PRIME_HANDLE_TO_FD导出dma_buf fd
- Wayland:合成器通过dma_buf在客户端和合成器间传递画。
摄像头采。。V4L2导出dma_buf fd 。传给GPU 。GPU零拷贝推。渲染,整条链路零拷贝,不经过CPU搬运。本系列。4篇会深入V4L2+dma_buf的完整链路径
六、内存池(mempool)
mempool在常规分配之上加了一层预分配保障:初始化时预留若干对象,分配时先走常规路径,常规路径失败就从预留池里取,保证在内存紧张时也能分配成功能
6.1 核心API
#include <linux/mempool.h>
/* 自定义分配和释放函数 */
static void *my_alloc(gfp_t gfp_mask, void *pool_data)
{
return kmalloc(1024, gfp_mask);
}
static void my_free(void *element, void *pool_data)
{
kfree(element);
}
/* 创建内存池,预分支个对*/
mempool_t *pool = mempool_create(5, my_alloc, my_free, NULL);
if (!pool)
return -ENOMEM;
/* 从池中分支*/
void *obj = mempool_alloc(pool, GFP_KERNEL);
/* 归还池中 */
mempool_free(obj, pool);
/* 销毁池 */
mempool_destroy(pool);
6.2 预分配保证不失败
mempool的核心价值:在内存紧张时仍能分配成功。工作流程:
mempool_alloc先调常规分配函数(如kmalloc。2. 常规分配成功,直接返。3. 常规分配失败,从预留池取一个对象返。4.mempool_free时,如果预留池未满,对象归还预留池;否则调常规释放函数
6.3 适用场景
中断上下文不允许分配失败。典型的场景:
- 块设备驱动*:I/O请求在内存紧张时仍需分配完成结构
- 网络驱动:紧急包的sk_buff分配
- 中断处理**:bottom half需要分配内存但不能失败
注意:mempool不是万能药。预留池大小有限,频繁分配仍可能耗尽。合理设置min_nr,通常4~16。
七、内存调试
内核提供了多种内存调试工具,不需要额外装软件,内核参数或proc文件就能用。
7.1 slabinfo
# 查看所有slab缓存信息
cat /proc/slabinfo
# 查看特定缓存
cat /proc/slabinfo | grep kmalloc
输出关键字段。
| 字段 | 含义 |
|:-----|:-----|
| active_objs | 当前使用中的对象 |
| num_objs | 总对象数(含空闲) |
| objsize | 单个对象大小 |
| objperslab | 每个slab的对象数 |
| pagesperslab | 每个slab占用的页 |
7.2 slub_debug
内核启动参数加slub_debug开启SLUB调试。
# 在GRUB中添加内核参数slub_debug=FZP
# 参数含义。# F - 对齐检查(Faults。# Z - 红区检查(Red Zoning。# P - 毒化(Poisoning),分配时填。x6b,释放时填充0x6b
# T - 追踪(Tracing),记录分配/释放调用。# U - 用户追踪(User Tracking```
```bash
# 只调试特定slab缓存
slub_debug=FZP,kmalloc-64
# 运行时启。echo 1 > /sys/kernel/slab/kmalloc-64/validate
7.3 kmemleak
kmemleak是内核的内存泄漏检测器,原理类似垃圾回收的标记-清除:扫描内存找不出任何指针指向的已分配块,判定为泄漏。
# 内核启动参数开销kmemleak=on
# 运行时操。echo scan > /sys/kernel/debug/kmemleak # 触发扫描
cat /sys/kernel/debug/kmemleak # 查看泄漏
echo clear > /sys/kernel/debug/kmemleak # 清除报告
典型输出。
unreferenced object 0xffff888100123400 (size 64):
comm "insmod", pid 1234, jiffies 4294901234
hex dump (first 32 bytes):
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
backtrace:
[<ffffffff81234567>] kmalloc_trace+0x67/0x120
[<ffffffffc0001000>] my_probe+0x3a/0x100 [mymod]
backtrace直接定位到泄漏的代码行。
7.4 vmallocinfo
# 查看vmalloc分配详情
cat /proc/vmallocinfo
# 典型输出
0xffffc90000000000-0xffffc90000001000 4096 dma_alloc_coherent+0x45/0x120 pages=1
0xffffc90000001000-0xffffc90000003000 8192 vmalloc+0x32/0x80 pages=2
| 字段 | 含义 |
|---|---|
| 地址范围 | 虚拟地址区间 |
| 大小 | 分配的字节数 |
| 调用者 | 哪个函数分配的 |
| pages | 占用的物理页 |
八、常见问题
Q1:kmalloc在原子上下文失败怎么办?
GFP_ATOMIC从紧急预留池分配,内存紧张时确实会失败。解决方案:
/* 方案1:用mempool预分支*/
mempool_t *pool = mempool_create(8, my_alloc, my_free, NULL);
void *buf = mempool_alloc(pool, GFP_ATOMIC); /* 不会失败 */
/* 方案2:减小分配大小,拆成多次小分支*/
/* 一次分支4KB可能失败,改。。6KB */
/* 方案3:在进程上下文预分配,中断里直接*/
struct my_dev *md = devm_kzalloc(dev, sizeof(*md), GFP_KERNEL);
/* 中断处理函数直接用md,不再分支*/
Q2:DMA数据不对?检查cache一致。
这是DMA开发最常见的问题。现象:CPU写的数据设备读不到,或者设备写的CPU看不到。
/* 发送方向:CPU写完必须同步给设*/
memcpy(buf, tx_data, len);
dma_sync_single_for_device(dev, bus_addr, len, DMA_TO_DEVICE);
/* 之后设备才能*/
/* 接收方向:设备写完必须同步给CPU */
dma_sync_single_for_cpu(dev, bus_addr, len, DMA_FROM_DEVICE);
/* 之后CPU才能*/
data = buf[0];
一致性DMA(dma_alloc_coherent)不需要手动同步,但性能略低。流式DMA必须手动同步,忘记同步就是玄学Bug。
Q3:vmalloc分配慢?
vmalloc慢在两个地方。
1. 分配置*:要修改页表,逐页映射,比kmalloc的slab缓存命中慢得。2. 访问题*:物理不连续导致TLB miss频繁,每次miss都要走页表查看
优化思路径
- 能用kmalloc就用kmalloc,vmalloc只在大块内存场景:- 如果必须vmalloc,考虑用vmap替代(自己管理物理页分配置- 64位系统上ZONE_NORMAL覆盖全部内存,大块kmalloc的成功率32位高很多
九、总结
速查看
| 场景 | API | 关键字 |
|---|---|---|
| 小块内存分配 | kmalloc/kzalloc | 物理连续,适合DMA,GFP_ATOMIC用于中断 |
| 大块临时缓冲 | vmalloc | 物理不连续,不能DMA,可能睡眠 |
| 页级对齐分配 | __get_free_pages | order最大11,连续物理页 |
| 设备自动释放 | devm_kzalloc | 绑定device生命周期 |
| DMA长期缓冲 | dma_alloc_coherent | cache自动一致,性能略低 |
| DMA临时传输 | dma_map_single | 需手动sync,性能最优 |
| 跨设备共享 | dma_buf | exporter/importer模型,fd传递 |
| 不允许失败 | mempool | 预分配保障,中断上下文必备 |
| 内存泄漏排查 | kmemleak | 扫描无指针引用的分配 |
| slab调试 | slub_debug | 红区+毒化+追踪,定位越界和UAF |
本文首发于linuxros.cn,转载请注明出处。