3. Linux内核内存管理机制

DMA,说白了就是让硬件自己搬数据,CPU 别插手。我做了这么多年驱动,见过太多人一上来就写 DMA 代码,结果不是数据错乱就是系统卡死。嗯,这一章咱们就把 DMA 驱动开发的三个核心环节讲透:控制器怎么初始化、描述符链怎么管、软硬件怎么同步。

3.1 DMA控制器初始化——先把地基打牢

初始化 DMA 控制器,不是简单配几个寄存器就完事。你得想清楚:系统里有多少个 DMA 通道?每个通道给谁用?中断怎么路由?

我个人习惯,先看芯片手册里的 DMA 控制器框图。搞清楚下面几件事:

  • 通道数量:是 8 通道还是 16 通道?有些 SoC 把 DMA 通道分成了两组,每组有自己的仲裁器。
  • 传输粒度:支持 8/16/32/64 位?burst 长度最大多少?
  • 地址对齐要求:源地址和目标地址有没有对齐限制?
  • 中断映射:每个通道完成中断挂到哪个 GIC 中断号上?

核心要点:初始化 DMA 控制器时,必须先把时钟和复位搞定。我遇到过一块芯片,DMA 寄存器死活写不进去,查了两天才发现是时钟门控没开。

下面是一个典型的 DMA 控制器初始化流程:

/* DMA控制器初始化示例 */
static int dma_controller_init(struct platform_device *pdev)
{
    struct dma_device *dma_dev = platform_get_drvdata(pdev);
    void __iomem *base;
    int i, ret;

    /* 1. 获取寄存器基地址 */
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 2. 使能时钟和复位 */
    ret = clk_prepare_enable(dma_dev->clk);
    if (ret)
        return ret;

    reset_control_deassert(dma_dev->rstc);

    /* 3. 配置全局控制寄存器 */
    writel(DMA_GLOBAL_CFG_ENABLE | DMA_GLOBAL_CFG_ARB_ROUND_ROBIN,
           base + DMA_GLOBAL_CFG);

    /* 4. 初始化每个通道 */
    for (i = 0; i < dma_dev->num_channels; i++) {
        struct dma_chan *chan = &dma_dev->channels[i];
        chan->idx = i;
        chan->base = base + DMA_CHAN_OFFSET(i);
        chan->state = DMA_CHAN_IDLE;

        /* 配置通道中断 */
        writel(DMA_CHAN_CFG_IE | DMA_CHAN_CFG_TC_IE,
               chan->base + DMA_CHAN_CFG);
    }

    /* 5. 注册DMA引擎 */
    ret = dma_async_device_register(dma_dev);
    if (ret)
        dev_err(&pdev->dev, "DMA device register failed\n");

    return ret;
}

3.2 描述符链管理——DMA的“任务清单”

描述符链,就是 DMA 要干活的清单。每个描述符告诉 DMA:从哪搬数据、搬到哪、搬多少、搬完下一个任务是什么。

你想想看,如果每次只发一个描述符,CPU 就得频繁参与。描述符链的好处是:一次提交一串任务,DMA 自己按顺序执行,CPU 可以干别的事。

经验之谈:描述符链最好用环形缓冲区管理。我曾经在一个项目中用了链表,结果描述符释放时出现了内存碎片,导致 DMA 卡死。换成环形缓冲区后,问题就解决了。

描述符的结构通常长这样:

/* DMA描述符结构 */
struct dma_descriptor {
    dma_addr_t src_addr;      /* 源地址 */
    dma_addr_t dst_addr;      /* 目标地址 */
    uint32_t   length;        /* 传输长度 */
    uint32_t   ctrl;          /* 控制位:中断使能、链使能等 */
    dma_addr_t next_desc;     /* 下一个描述符地址(物理地址) */
    uint32_t   status;        /* 状态:由DMA硬件写入 */
} __attribute__((packed));

描述符链的管理,核心是三个操作:

  1. 分配描述符:从描述符池中取一个空闲的描述符,填充传输参数。
  2. 链接描述符:把新描述符的 next_desc 指向上一个描述符,形成链。
  3. 提交描述符链:把链首地址写入 DMA 控制器的描述符地址寄存器,触发传输。

注意:描述符必须放在非 Cacheable 的内存区域,或者确保 Cache 一致性。否则 DMA 读到的描述符可能是过时的数据。我见过有人把描述符放在栈上,结果 DMA 读到的是垃圾数据。

描述符链的典型管理流程:

/* 提交描述符链 */
int dma_submit_chain(struct dma_chan *chan, struct dma_descriptor *chain_head)
{
    unsigned long flags;
    int ret = 0;

    spin_lock_irqsave(&chan->lock, flags);

    /* 检查通道状态 */
    if (chan->state != DMA_CHAN_IDLE) {
        ret = -EBUSY;
        goto out;
    }

    /* 确保描述符在DMA可见的内存区域 */
    dma_sync_single_for_device(chan->dev,
                               virt_to_phys(chain_head),
                               sizeof(*chain_head) * chain_head->count,
                               DMA_TO_DEVICE);

    /* 把链首地址写入DMA控制器 */
    writel(virt_to_phys(chain_head),
           chan->base + DMA_CHAN_DESC_ADDR);

    /* 启动DMA传输 */
    writel(DMA_CHAN_CTRL_START | DMA_CHAN_CTRL_CHAIN_EN,
           chan->base + DMA_CHAN_CTRL);

    chan->state = DMA_CHAN_BUSY;

out:
    spin_unlock_irqrestore(&chan->lock, flags);
    return ret;
}

3.3 硬件与软件同步机制——别让数据“飞了”

DMA 是异步的。你提交了任务,DMA 在后台搬数据,CPU 继续跑。那问题来了:你怎么知道 DMA 搬完了?搬完的数据 CPU 能看到吗?

同步机制,说白了就是解决两个问题:

  • 传输完成通知:DMA 搬完了怎么告诉 CPU?
  • 数据一致性:DMA 写的数据,CPU 的 Cache 里有没有?

3.3.1 中断同步——最常用的方式

DMA 传输完成时触发中断,CPU 在中断处理函数里收尾。这是最直接的方式。

/* DMA完成中断处理 */
irqreturn_t dma_done_irq(int irq, void *dev_id)
{
    struct dma_chan *chan = dev_id;
    struct dma_descriptor *desc;
    unsigned long flags;

    spin_lock_irqsave(&chan->lock, flags);

    /* 读取状态寄存器,确认是哪个通道完成 */
    uint32_t status = readl(chan->base + DMA_CHAN_STATUS);
    if (!(status & DMA_STATUS_TC)) {
        spin_unlock_irqrestore(&chan->lock, flags);
        return IRQ_NONE;
    }

    /* 清除中断标志 */
    writel(DMA_STATUS_TC, chan->base + DMA_CHAN_STATUS);

    /* 获取当前完成的描述符 */
    desc = chan->active_desc;

    /* 同步Cache:让CPU看到DMA写的数据 */
    dma_sync_single_for_cpu(chan->dev,
                            desc->dst_addr,
                            desc->length,
                            DMA_FROM_DEVICE);

    /* 回调通知上层 */
    if (chan->callback)
        chan->callback(chan->callback_param);

    chan->state = DMA_CHAN_IDLE;
    chan->active_desc = NULL;

    spin_unlock_irqrestore(&chan->lock, flags);

    return IRQ_HANDLED;
}

3.3.2 轮询同步——低延迟场景的选择

有些场景中断延迟太大,比如音频 DMA。这时候可以用轮询:CPU 不断检查 DMA 的状态寄存器。

我的建议:轮询适合短传输、高频率的场景。但要注意,轮询会占 CPU,别在主线程里死等。我曾经在一个项目中用轮询等 DMA 完成,结果把整个系统的实时性拖垮了。

/* 轮询等待DMA完成 */
int dma_poll_completion(struct dma_chan *chan, int timeout_us)
{
    uint32_t status;
    int timeout = timeout_us;

    while (timeout-- > 0) {
        status = readl(chan->base + DMA_CHAN_STATUS);
        if (status & DMA_STATUS_TC) {
            writel(DMA_STATUS_TC, chan->base + DMA_CHAN_STATUS);
            return 0;
        }
        udelay(1);
    }

    return -ETIMEDOUT;
}

3.3.3 数据一致性——Cache 是个“捣蛋鬼”

DMA 直接读写内存,但 CPU 有 Cache。如果 DMA 写了一块内存,而 CPU 的 Cache 里还存着旧数据,CPU 读到的就是错的。反过来也一样:CPU 写了数据到 Cache,还没刷回内存,DMA 就去搬,搬的是旧数据。

解决办法就两个:

  • 使用一致性内存:dma_alloc_coherent() 分配的内存,硬件保证 Cache 和内存一致。
  • 手动同步:用 dma_sync_single_for_cpu() 和 dma_sync_single_for_device() 在合适的时机刷 Cache。

避坑指南:我曾经在一个项目中,DMA 从网卡收数据,CPU 处理时总丢包。查了三天,发现是 Cache 没刷。DMA 写完了数据,CPU 的 Cache 里还是旧数据,一读就错。加上 dma_sync_single_for_cpu() 后,问题立刻消失。

3.4 知识体系总览

下面这张图,把 DMA 驱动开发的三个核心环节串起来了:

DMA驱动开发核心知识体系 DMA驱动开发核心知识体系 控制器初始化 描述符链管理 软硬件同步 时钟/复位 通道配置 中断路由 描述符分配 链式链接 环形缓冲 中断同步 轮询同步 Cache一致性 三个环节环环相扣:初始化是基础,描述符链是核心,同步机制是保障 任何一个环节出问题,DMA都可能产生数据错乱或系统崩溃

3.5 常见问题与调试技巧

问题现象 可能原因 排查方法
DMA传输未启动 时钟未使能、描述符地址错误 检查时钟寄存器、打印描述符物理地址
数据错位 地址对齐不满足要求 检查源/目标地址对齐方式
中断丢失 中断标志未清除、中断优先级问题 在中断处理函数开头打印状态寄存器
数据不一致 Cache未同步 检查 dma_sync_single_for_cpu/device 调用
描述符链断裂 next_desc 指向了无效地址 遍历描述符链,打印每个节点的 next_desc

最后提醒一句:调试 DMA 驱动时,别只盯着代码。用逻辑分析仪抓一下 DMA 控制器的信号,往往能发现寄存器配置的问题。我遇到过好几次,代码看着没问题,但示波器一抓,发现时序完全不对。