linux驱动开发(20)-DMA(四)

分散/聚集映射

分散/聚集映射通过将虚拟地址上分散的DMA缓冲区通过一个类型为struct scatterlist的数组或者链表组织起来,然后通过一次的DMA传输操作在主存RAM与设备之间传输数据,如图所示:

linux驱动开发(20)-DMA(四)_第1张图片

图中显示了主存中三个分散的物理页面与设备之间进行的一次DMA传输时分散/聚集映射示意,其中单个物理页面与设备之间可以看做是一个单一的流式映射,每个这样的单一映射在内核中有数据结构struct scatterlist来表示:

<include/asm-generic/scatterlist.h>
struct scatterlist {
    unsigned long   page_link;
    unsigned int offset;
    unsigned int length;
    dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
    unsigned int dma_length;
#endif
};

如果从CPU的角度看这种分散/聚集映射,它对应的需求是有三块数据(分别存放在三段分散的虚拟地址空间中)需要和设备进行交互(发送或者接收)​,通过建立struct scatterlist类型的数组/链表在一次DMA传输中完成所有的数据传递。在struct scatterlist结构体中,page_link指明了虚拟地址所对应的物理页面struct page对象的地址(因为地址的最低两位总是0,所以内核在page_link的后两位中安插了一些其他信息。例如:如果最低两位是01,表示当前对象的page_link将会是指向下一个scatterlist数组的首地址,此时形成scatterlist的链式结构;如果最低两位是10,表明当前对象是scatterlist数组中的最后一个)​,比如下面的代码:

//sg是struct scatterlist类型指针
struct page*spage=(struct page*)(sg->page_link&~0x03);

offset是数据在DMA缓冲区中的偏移地址,length是要传输的数据块的大小,dma_address则是设备DMA操作要使用的DMA地址(物理地址)​。内核中的DMA层为分散/聚集映射所提供的接口函数为dma_map_sg,其原型为:

int dma_map_sg(struct device *dev, struct scatterlist *sg,
                int nents, enum dma_data_direction dir);

参数dev是设备对象的指针,sg是struct scatterlist类型数组的首地址,nents表示当前的分散/聚集映射中单一流式映射的个数,也是struct scatterlist数组/链表中的元素个数,dir用于指明DMA传输中数据流的方向。接下来分别以x86和ARM平台来讨论dma_map_sg建立分散/聚集映射的内核机制。首先是x86平台,该平台的dma_map_sg通过struct dma_map_ops对象来调用其map_sg方法,此处依然选择以nommu_dma_ops对象为例,其map_sg方法的函数实现为:

<arch/x86/kernel/pci-nommu.c>
static int nommu_map_sg(struct device *hwdev, struct scatterlist *sg,
            int nents, enum dma_data_direction dir,
            struct dma_attrs *attrs)
{
    struct scatterlist *s;
    int i;
    WARN_ON(nents == 0 || sg[0].length == 0);
    for_each_sg(sg, s, nents, i) {
        BUG_ON(!sg_page(s));
        s->dma_address = sg_phys(s);
        if (!check_addr("map_sg", hwdev, s->dma_address, s->length))
            return 0;
        s->dma_length = s->length;
    }
    flush_write_buffers();
    return nents;
}

函数的主体通过for_each_sg遍历sg数组/链表的所有元素,对于每个元素,都执行一个流式映射,对于x86而言,如果没有IOMMU的介入,设备的DMA操作时使用的DMA地址就是物理地址,因此只需通过sg_phys获得当前元素所对应的物理地址即可,其代码如下:

<include/linux/scatterlist.h>
static inline dma_addr_t sg_phys(struct scatterlist *sg)
{
    return page_to_phys(sg_page(sg)) + sg->offset;
}

sg_page通过scatterlist对象sg的page_link成员取得所对应的物理页面的struct page对象地址:

static inline struct page *sg_page(struct scatterlist *sg)
{return (struct page *)((sg)->page_link & ~0x3);
}

sg_phys再将返回的struct page指针通过page_to_phys获得页面的起始物理地址,加上实际数据块在页面中的偏移值sg->offset,就获得了本次DMA操作的DMA地址。ARM平台的dma_map_sg的实现为:

<arch/arm/mm/dma-mapping.c>
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
        enum dma_data_direction dir)
{for_each_sg(sg, s, nents, i) {
        s->dma_address = dma_map_page(dev, sg_page(s), s->offset,
                        s->length, dir);
        if (dma_mapping_error(dev, s->dma_address))
            goto bad_mapping;
    }
    return nents;
bad_mapping:
    for_each_sg(sg, s, i, j)
        dma_unmap_page(dev, sg_dma_address(s), sg_dma_len(s), dir);
    return 0;
}

函数通过dma_map_page来映射scatterlist上的page_link,跟x86平台不一样的是,ARM架构需要通过软件来保证cache一致性问题,所以做完这种虚拟物理地址的转换之后,ARM需要做的是使映射区对应的cache无效,以保证设备通过DMA将数据放到主存之后,CPU读到的不是cache中的数据,或者是保证CPU写到RAM中的数据立刻反映到RAM中,而不是暂时缓存到cache中,这样后续DMA在把主存中的数据传到设备中时,才能确保数据的有效性。通过上面的讨论可以看到,分散/聚集DMA映射本质上是通过一次DMA操作把主存中分散的数据块在主存与设备之间进行传输,对于其中的每个数据块内核都会建立对应的一个流式DMA映射。另外,分散/聚集DMA映射需要设备的支持,而不完全由内核或者驱动程序决定。

回弹缓冲区(bounce buffer)

如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立所谓的回弹缓冲区,它相当于一个中转站的作用,在把数据往设备方向传输时,驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后再启动DMA操作,反之亦然。所以回弹缓冲区必然是可以直接与设备进行DMA传输的,当传输结束时,再通过CPU的介入把回弹缓冲区中的数据搬移到最终的目标,所以除非外部传入的地址不能进行DMA传输,否则不应当使用它。下图展示了一个回弹缓冲区的使用:

linux驱动开发(20)-DMA(四)_第2张图片

DMA池

前面在讨论一致性DMA映射时,知道这种DMA映射所建立的缓冲区大小是单个页面的整数倍,如果驱动程序需要更小的一致性映射的DMA缓冲区,可以使用内核提供的DMA池机制。DMA池机制非常类似于Linux内存管理中的slab机制,它的实现建立在一致性DMA映射所获得的连续物理页面的基础之上,通过DMA池的接口函数在物理页面之上分配所谓块大小的DMA缓冲区,为方便叙述,我们称这样的块为DMA缓冲块,以区别于一致性DMA映射中页面级大小的缓冲区。显然为了管理跟踪物理页面中DMA缓冲块的分配和余下空闲空间的多少,内核需要引入对应的管理数据结构,struct dma_pool就是内核用来完成该任务的数据结构,其定义如下:

<mm/dmapool.c>
struct dma_pool {
    struct list_head page_list;
    spinlock_t lock;
    size_t size;
    struct device *dev;
    size_t allocation;
    size_t boundary;
    char name[32];
    wait_queue_head_t waitq;
    struct list_head pools;
};

一些成员的作用如下:struct list_head page_list用来将一致性DMA映射建立的页面组织成链表。size_t size该DMA池用来分配一致性DMA映射的缓冲区的大小,也称块大小。struct device *dev进行DMA操作的设备对象指针。char name[32]DMA池的名称,主要在调试或者诊断时使用。struct list_head pools用来将当前DMA池对象加到dev->dma_pools链表中。

在利用DMA池进行缓冲区分配之前,首先需要创建一个DMA池,这是通过函数dma_pool_create来完成的,该函数的核心实现为:

<mm/dmapool.c>
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                  size_t size, size_t align, size_t boundary)
{
    struct dma_pool *retval;
    size_t allocation;
    …
    allocation = max_t(size_t, size, PAGE_SIZE);
    if (!boundary) {
        boundary = allocation;
    } else if ((boundary < size) || (boundary & (boundary - 1))) {
        return NULL;
    }
    retval = kmalloc_node(sizeof(*retval), GFP_KERNEL, dev_to_node(dev));
    if (!retval)
        return retval;
    strlcpy(retval->name, name, sizeof(retval->name));
    retval->dev = dev;
    INIT_LIST_HEAD(&retval->page_list);
    spin_lock_init(&retval->lock);
    retval->size = size;
    retval->boundary = boundary;
    retval->allocation = allocation;
    init_waitqueue_head(&retval->waitq);list_add(&retval->pools, &dev->dma_pools);}

函数的核心工作就是分配一个struct dma_pool对象并初始化。不过有些细节还是值得探讨一下,先看函数的参数,name用于指定即将创建的DMA池的名称,size用于指定在该DMA池中分配缓冲块的大小,align用于指定当前DMA池分配操作所遵守的对齐方式。函数首先确定当前DMA池分配的对齐指标。接下来allocation用来保存参数size与PAGE_SIZE之间的最大值,所以如果size小于一个页面大小的话,allocation将等于PAGE_SIZE,在后续的DMA池的页面分配部分,该值用来决定需要分配的连续物理页的数量。如果函数调用时没有指定boundary(boundary为空)​,那么boundary就是allocation的大小。kmalloc_node函数用来分配一个struct dma_pool对象,紧接着就是对该对象进行初始化。以上是dma_pool_create函数总体框架,现在我们有了一个已经被初始化过的DMA池对象,如果要在该对象中分配一个一致性映射的DMA缓冲区块,应该使用dma_pool_alloc函数:

<mm/dmapool.c>
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
            dma_addr_t *handle)
{
    unsigned long flags;
    struct dma_page *page;
    size_t offset;
    void *retval;
    spin_lock_irqsave(&pool->lock, flags);
restart:
    list_for_each_entry(page, &pool->page_list, page_list) {
        if (page->offset < pool->allocation)
            goto ready;
    }
    page = pool_alloc_page(pool, GFP_ATOMIC);
    if (!page) {
        if (mem_flags & __GFP_WAIT) {
            DECLARE_WAITQUEUE(wait, current);
            __set_current_state(TASK_INTERRUPTIBLE);
            __add_wait_queue(&pool->waitq, &wait);
            spin_unlock_irqrestore(&pool->lock, flags);
            schedule_timeout(POOL_TIMEOUT_JIFFIES);
            spin_lock_irqsave(&pool->lock, flags);
            __remove_wait_queue(&pool->waitq, &wait);
            goto restart;
        }
        retval = NULL;
        goto done;
    }
ready:
    page->in_use++;
    offset = page->offset;
    page->offset = *(int *)(page->vaddr + offset);
    retval = offset + page->vaddr;
    *handle = offset + page->dma;
done:
    spin_unlock_irqrestore(&pool->lock, flags);
    return retval;
}

这个函数的主线框架是,如果当前DMA池中有页面满足接下来的缓冲块分配需求,那么就在该页面上分配,否则通过调用pool_alloc_page来重新分配一段连续物理页。DMA池中每段这样的页面都用一个struct dma_page类型的对象来表示:

<mm/dmapool.c>
struct dma_page {
    struct list_head page_list;
    void *vaddr;
    dma_addr_t dma;
    unsigned int in_use;
    unsigned int offset;
};

dma_pool_alloc函数返回DMA池中某一段物理页面中空闲块的虚拟地址,其对应的DMA地址由参数handle返回。如果调用dma_pool_alloc函数时在mem_flags中指定了__GFP_WAIT标志,那么在系统中暂时没有一段连续的物理页面满足分配需求时,函数会进入睡眠等待状态,等POOL_TIMEOUT_JIFFIES指定的时间到期,或者前面的分配请求可以被满足(比如有模块调用了dma_pool_free来释放当前DMA池中的某一DMA缓冲块)​,该函数才会从睡眠等待状态中醒来。与dma_pool_alloc相对应,如果要从一个DMA池中释放某一DMA缓冲块,则应该调用dma_pool_free函数,该函数原型为:

<include/linux/dmapool.h>
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);

参数pool用于指明要释放的DMA缓冲块隶属的DMA池,vaddr是要释放的DMA缓冲块的虚拟地址,addr则是其DMA地址。如果一个DMA池不再使用,应该调用函数dma_pool_destroy销毁之,该函数的原型为:

<include/linux/dmapool.h>
void dma_pool_destroy(struct dma_pool *pool);

参数pool是指向要销毁的DMA池对象的指针。函数调用者需要保证调用该函数时DMA池中已经没有DMA缓冲块还在使用,而且一旦DMA池对象被销毁,后续将没有模块试图再去使用它。

你可能感兴趣的:(linux驱动开发,驱动开发,linux,服务器)