Kernel DMA

    为什么会有DMA(直接内存访问)?我们知道通常情况下,内存数据跟外设之间的通信是通过cpu来传递的。cpu运行io指令将数据从内存拷贝到外设的io端口,或者从外设的io端口拷贝到内存。由于外设的访问速度跟内存的访问速度相比非常慢,因此这种访问操作会严重浪费cpu的指令周期,DMA的出现就是为了解决这个问题。支持DMA机制的系统通常会包含一个DMA控制器,下挂支持DMA的外设,外设和内存之间的数据交换通过DMA控制器完成。

    那么,如何使用DMA?不同平台提供不同的DMA控制器,其控制器驱动也不尽相同,因此DMA控制器驱动不在夲篇分析。支持DMA的设备一般都会提供至少两个io端口(用于设置DMA地址和大小)和中断请求线,io端口用于指定缓存的物理地址(无iommu)或者总线地址(有iommu)和大小,中断请求线用于当设备完成dma操作后,通知cpu开始下一步操作。DMA控制器的通道请求,源地址和目的地址的设置等操作一般由外设完成,无需用户介入。一个DMA的读操作如下:

    (1)cpu分配一块连续的物理内存缓冲区。

    (2)cpu将分配好的内存缓冲区的物理地址或者总线地址和大小写入外设的DMA地址和大小io端口。

    (3)cpu设置外设的控制端口,启动外设进行DMA操作。然后,cpu清除读取完成标志,并将当前读取进程挂起,调度其他任务运行。

    (4)当外设完成了DMA操作之后,通过中断请求线,通知cpu操作完成。

    (5)cpu进入中断处理函数,设置读取完成标志,唤醒挂起在等待队列上的进程。

    (6)读取进程被唤醒后,检查读取完成标志。如果完成,对缓冲区数据进行下一步处理,否则,继续挂起等待。

    DMA的写操作类似这个过程。仔细分析可以发现,用户只需要提供缓存的物理地址或总线地址和大小,注册中断处理函数,并启动设备进行DMA操作即可。那么缓存的物理地址或总线地址是如何得到的呢?DMA缓存的分配是内核DMA编程的核心,下面我们主要分析两种DMA缓存的分配方法:DMA一致性缓存和DMA映射缓存

    (1)DMA一致性缓存分配接口 dma_alloc_coherent():

            我们知道,cpu访问内存使用的是虚拟地址,而外设访问内存使用的是io地址。当外设所在的总线不支持iommu时,io地址就是内存的物理地址,如果外设所在的总线支持iommu,那么io地址经过iommu映射之后才是内存的物理地址。因此,一个DMA缓存对应两种地址,cpu虚拟地址和外设io地址,而这些地址在不同平台配置也不同。以x86为例,x86没有iommu,其io地址其实就是内存的物理地址。而x86外设总线宽度为24位,这又限制了DMA内存的分配只能在低16M地址空间进行。dma_alloc_coherent() 一次性分配DMA缓存(DMA ZONE的连续物理内存),并返回缓存的cpu虚拟地址和外设的io地址。适用于在驱动模块内部分配内存,通常在驱动模块加载或驱动open操作时分配,并在整个驱动生命周期内生效。这种方式的缺点是,持有dma缓存时间太长,在某些系统上(如x86)dma资源紧缺,浪费资源。

static inline void *dma_alloc_coherent(struct device *dev, size_t size,
        dma_addr_t *dma_handle, gfp_t flag)
{
    return dma_alloc_attrs(dev, size, dma_handle, flag, 0);
}

static inline void *dma_alloc_attrs(struct device *dev, size_t size,
                       dma_addr_t *dma_handle, gfp_t flag,
                       unsigned long attrs)
{
    const struct dma_map_ops *ops = get_dma_ops(dev);    // 获取dma分配接口
    void *cpu_addr;

    BUG_ON(!ops);
    WARN_ON_ONCE(dev && !dev->coherent_dma_mask);

    if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
        return cpu_addr;

    /* let the implementation decide on the zone to allocate from: */
    flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);

    if (!arch_dma_alloc_attrs(&dev))
        return NULL;
    if (!ops->alloc)
        return NULL;

    cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);    // 调用分配接口
    debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
    return cpu_addr;
}

// 下面以通用的分配接口为例分析

const struct dma_map_ops dma_direct_ops = {
    .alloc            = dma_direct_alloc,
    .free            = dma_direct_free,
    .map_page        = dma_direct_map_page,
    .map_sg            = dma_direct_map_sg,
    .dma_supported        = dma_direct_supported,
    .mapping_error        = dma_direct_mapping_error,
};

void *dma_direct_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle,
        gfp_t gfp, unsigned long attrs)    // 接口返回值是cpu虚拟地址,dma_handle是io地址
{
    unsigned int count = PAGE_ALIGN(size) >> PAGE_SHIFT;    // 获取缓存占用的page数
    int page_order = get_order(size);
    struct page *page = NULL;
    void *ret;

    /* we always manually zero the memory once we are done: */
    gfp &= ~__GFP_ZERO;

    /* GFP_DMA32 and GFP_DMA are no ops without the corresponding zones: */
    if (dev->coherent_dma_mask <= DMA_BIT_MASK(ARCH_ZONE_DMA_BITS))
        gfp |= GFP_DMA;    // 可以在 DMA zone内分配

    if (dev->coherent_dma_mask <= DMA_BIT_MASK(32) && !(gfp & GFP_DMA))
        gfp |= GFP_DMA32;    // 可以在 DMA32 zone内分配

again:
    /* CMA can be used only in the context which permits sleeping */
    if (gfpflags_allow_blocking(gfp)) {
        page = dma_alloc_from_contiguous(dev, count, page_order,
                         gfp & __GFP_NOWARN);    // 优先从contiguous分配page

        if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
            dma_release_from_contiguous(dev, page, count);
            page = NULL;
        }
    }
    if (!page)
        page = alloc_pages_node(dev_to_node(dev), gfp, page_order);    // 其次从伙伴系统分配pages

    if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
        __free_pages(page, page_order);
        page = NULL;

        if (IS_ENABLED(CONFIG_ZONE_DMA32) &&
            dev->coherent_dma_mask < DMA_BIT_MASK(64) &&
            !(gfp & (GFP_DMA32 | GFP_DMA))) {
            gfp |= GFP_DMA32;
            goto again;
        }

        if (IS_ENABLED(CONFIG_ZONE_DMA) &&
            dev->coherent_dma_mask < DMA_BIT_MASK(32) &&
            !(gfp & GFP_DMA)) {
            gfp = (gfp & ~GFP_DMA32) | GFP_DMA;
            goto again;
        }
    }

    if (!page)
        return NULL;
    ret = page_address(page);    // page转cpu虚拟地址
    if (force_dma_unencrypted()) {
        set_memory_decrypted((unsigned long)ret, 1 << page_order);
        *dma_handle = __phys_to_dma(dev, page_to_phys(page));    // page转io地址
    } else {
        *dma_handle = phys_to_dma(dev, page_to_phys(page)); // page转io地址
    }
    memset(ret, 0, size);
    return ret;
}

    简而言之,上述接口本质上是从DMA/DMA32内存区分配连续物理内存,返回内存的cpu虚拟地址和映射的io地址。

    (2)DMA映射缓存 dma_map_single():

        DMA一致性缓存不仅有占用dma资源周期长的缺点,还有一种场景无法满足:如果缓存已经被其他内核模块或用户进程分配完成,使用DMA一致性缓存就必须进行一次cpu内存拷贝(因为一致性缓存是重新分配内存)。那么有没有一种接口可以避免DMA一致性缓存的这些缺点呢?DMA映射缓存可以满足。dma_map_single()不去重新分配内存,而是将已经存在的缓存的cpu虚拟地址直接转换成io地址,当完成DMA操作之后可以立即使用 dma_unmap_single()。不仅可以映射已经存在的内存,而且可以只在需要DMA传输的时候才占用DMA资源,节省DMA资源。

#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)

static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
                          size_t size,
                          enum dma_data_direction dir,
                          unsigned long attrs)
{
    const struct dma_map_ops *ops = get_dma_ops(dev);    // 获取dma分配接口
    dma_addr_t addr;

    BUG_ON(!valid_dma_direction(dir));
    addr = ops->map_page(dev, virt_to_page(ptr),
                 offset_in_page(ptr), size,
                 dir, attrs);    // 调用分配接口的map_page

    debug_dma_map_page(dev, virt_to_page(ptr),
               offset_in_page(ptr), size,
               dir, addr, true);
    return addr;
}

dma_addr_t dma_direct_map_page(struct device *dev, struct page *page,
        unsigned long offset, size_t size, enum dma_data_direction dir,
        unsigned long attrs)
{
    dma_addr_t dma_addr = phys_to_dma(dev, page_to_phys(page)) + offset;    // page转io地址

    if (!check_addr(dev, dma_addr, size, __func__))
        return DIRECT_MAPPING_ERROR;
    return dma_addr;    // 返回io地址
}

    (3)scatter/gather DMA映射传输dma_map_sg():

        无论是dma_alloc_coherent()还是dma_map_single(),都要求内存是物理连续的。当一次需要传输多个内存缓冲区的时候,上述两个接口都无法满足要求。dma_map_sg()可以满足一次传递多个内存缓冲区,但是要求每个缓冲区内部是物理连续的。dma_map_sg()以dma_direct_map_sg()为例:

int dma_direct_map_sg(struct device *dev, struct scatterlist *sgl, int nents,
        enum dma_data_direction dir, unsigned long attrs)
{
    int i;
    struct scatterlist *sg;

    for_each_sg(sgl, sg, nents, i) {    // 轮寻sgl的每个sg
        BUG_ON(!sg_page(sg));

        sg_dma_address(sg) = phys_to_dma(dev, sg_phys(sg));    // 映射dma地址
        if (!check_addr(dev, sg_dma_address(sg), sg->length, __func__))
            return 0;
        sg_dma_len(sg) = sg->length;    // 和长度
    }

    return nents;
}

    至此,我们已经基本介绍完了DMA的内核大体框架。当然DMA控制器驱动没有介绍,以及反弹缓冲区也没有介绍。这些细节和拓展后续再去分析和总结。

转载于:https://my.oschina.net/yepanl/blog/3053881

猜你喜欢

转载自blog.csdn.net/weixin_33827731/article/details/91871207
DMA