ESC
输入关键词搜索文章标题和内容

内核内存管理与DMA:从ZONE划分到跨设备共享的实战指南

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

内核内存管理与DMA:从ZONE划分到跨设备共享的实战指南

写驱动绕不开两件事:内存怎么分,DMA怎么搬,内核把物理内存切成ZONE,每个ZONE有自己的分配策略和限制;DMA让设备直取内存,但cache一致性、地址映射、缓冲区生命周期全是坑。本文基于Linux 6.8内核,从ZONE划分出发,覆盖kmalloc/vmalloc/mempool等分配API,再深入DMA一致性映射和流式映射,最后看dma-buf跨设备共享和内存调试手段。

一、内核内存区。

物理内存不是一整块随便用的,内核按地址范围和访问限制把内存划分为不同的ZONE,每个ZONE有独立的分配策略。

flowchart TB A(["物理内存"]) --> B["ZONE_DMA<br/>0 ~ 16MB"] A --> C["ZONE_DMA32<br/>0 ~ 4GB"] A --> D["ZONE_NORMAL<br/>直接映射"] A --> E["ZONE_HIGHMEM<br/>32位高端内存"] A --> F["ZONE_MOVABLE<br/>可迁移页"] B -->|"ISA设备DMA"| G["老旧硬件<br/>24位地址"] C -->|"32位DMA设备"| H["网卡/磁盘控制器<br/>32位地址"] D -->|"内核通用分配"| I["kmalloc/slab<br/>绝大多数场景"] E -->|"32位系统只有896MB"| J["临时映射<br/>kmap/kunmap"] F -->|"内存热插拔碎片整理"| K["大页迁移<br/>CMA分配"] style B fill:#FFEBEE style C fill:#FFF8E1 style D fill:#E3F2FD style E fill:#F3E5F5 style F fill:#E8F5E9 style G fill:#FFEBEE style H fill:#FFF8E1 style I fill:#E3F2FD style J fill:#F3E5F5 style K fill:#E8F5E9

各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

这两个是最容易混淆的分配接口,放在一起对比

flowchart TB A{"需要DMA?"} -->|"| B["kmalloc<br/>物理连续"] A -->|"| C{"大小 > 几MB?"} C -->|"| D["vmalloc<br/>虚拟连续"] C -->|"| E{"中断上下。"} E -->|"| F["kmalloc(GFP_ATOMIC)"] E -->|"| G["kmalloc(GFP_KERNEL)"] style A fill:#FFF8E1 style C fill:#FFF8E1 style E fill:#FFF8E1 style B fill:#E8F5E9 style D fill:#E3F2FD style F fill:#FFEBEE style G fill:#E8F5E9

详细对比

对比 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原理

flowchart TB A["CPU写入数据<br/>到内存缓冲区"] --> B{"DMA方向"} B -->|"DMA_TO_DEVICE"| C["dma_sync_single_for_device<br/>刷CPU cache到内存"] C --> D["设备从内存读取数"] B -->|"DMA_FROM_DEVICE"| E["设备写入数据<br/>到内存"] E --> F["dma_sync_single_for_cpu<br/>使CPU cache无效"] F --> G["CPU从内存读取数"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#E8F5E9 style E fill:#E3F2FD style F fill:#E8F5E9 style G fill:#E3F2FD

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高。

来自 linuxros.cn · linuxROS
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映射到自己的地址空间

flowchart TB A["导出方设br/>(如V4L2摄像头)"] -->|"dma_buf_export"| B["dma_buf对象<br/>+ fd"] B -->|"fd传递<br/>(Unix socket/scm| C["导入方设<br/>(如GPU"] C -->|"dma_buf_get(fd)"| D["dma_buf_attach<br/>+ dma_buf_map_attachment"] D --> E["获得dma_addr_t<br/>设备可访问的地址"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#F3E5F5 style E fill:#E8F5E9

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的核心价值:在内存紧张时仍能分配成功。工作流程:

  1. 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,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页