参考:DMA-BUF 由浅入深
参考:dma-buf 由浅入深(三) —— map attachment
本文基于:arm64 linux5.4.226 分析。
ION to IOMMU概述
本文主要介绍ION,dma-buf, dma-mapping, iommu, smmu之间的联系以及对外提供的接口,框图如下:
1. dev/ion:负责ION Buffer的分配,分配方式有连续和非连续两种,对应DMA heap和system heap;当前系统支持的heap类型可在用户空间调IOCTL CMD:ION_IOC_HEAP_QUERY来获取,在分配memory时传入需要的heap id即可分配对应的memory。
2. anon:dmabuf:匿名文件,对应具体的ION Buffer,用于ION Buffer cache的同步。
ION
ION主要用于分配一块内存区域,代码路径:drivers/staging/android/ion
ION文件操作集:
static const struct file_operations ion_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = ion_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ion_ioctl,
#endif
};
ION的IOCTL CMD:ION_IOC_ALLOC用于用户空间分配ION内存;ION_IOC_ALLOC对应的驱动执行程序为:static int ion_alloc(size_t len, unsigned int heap_id_mask, unsigned int flags);
ion_alloc根据传入的heap_id_mask选择对应的heap分配ion buffer,并将该ion buffer和新创建的dam-buf建立联系,然后返回dma-buf的文件描述符fd。在同一进程中使用该fd,就可以获取到dma-buf相关的信息了。
static int ion_alloc(size_t len, unsigned int heap_id_mask, unsigned int flags)
{
struct ion_buffer *buffer = NULL;
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
int fd;
struct dma_buf *dmabuf;
//根据heap id mask找到对应的heap,并调用该heap分配内存
buffer = ion_buffer_create(heap, dev, len, flags);
exp_info.ops = &dma_buf_ops;
exp_info.priv = buffer;
//初始化dma_buf->ops为dma_buf_ops
dmabuf = dma_buf_export(&exp_info);
fd = dma_buf_fd(dmabuf, O_CLOEXEC);
return fd;
}
全局静态变量dma_buf_ops初始化如下:
static const struct dma_buf_ops dma_buf_ops = {
.map_dma_buf = ion_map_dma_buf,
.unmap_dma_buf = ion_unmap_dma_buf,
.mmap = ion_mmap,
.release = ion_dma_buf_release,
.attach = ion_dma_buf_attach,
.detach = ion_dma_buf_detatch,
.begin_cpu_access = ion_dma_buf_begin_cpu_access,
.end_cpu_access = ion_dma_buf_end_cpu_access,
.map = ion_dma_buf_kmap,
.unmap = ion_dma_buf_kunmap,
};
mmap:用户空间访问内存
map/unmap:内核空间访问内存
attach/detach/map_dma_buf/unmap_dma_buf:dma设备访问内存
begin_cpu_access/end_cpu_access:Cache操作,主要目的是保证数据的正确性,在CPU访问内存之前,调用begin_cpu_access来Invalidate Cache,这样CPU在后续访问时才能重新从DDR上加载最新的数据到Cache上;在CPU访问内存结束后,调用end_cpu_access来Fulsh Cache,将Cache中的数据全部会写到DDR上,这样后续DMA才能访问到正确的有效数据。
ION Heap
系统默认支持的ion heap type如下:
enum ion_heap_type {
ION_HEAP_TYPE_SYSTEM, //分配内存通过vmalloc
ION_HEAP_TYPE_SYSTEM_CONTIG, //分配内存通过kmalloc
ION_HEAP_TYPE_CARVEOUT,
ION_HEAP_TYPE_CHUNK,
ION_HEAP_TYPE_DMA, //分配内存通过DMA API
ION_HEAP_TYPE_CUSTOM, /*
* must be last so device specific heaps always
* are at the end of this enum
*/
};
以heap type为ION_HEAP_TYPE_SYSTEM为例:
ion的alloc函数最终调用ion_system_heap_allocate分配内存
ion内存映射到用户空间接口mmap最终调用ion_heap_map_user
ion内存映射到内核空间接口map最终调用ion_heap_map_kernel
备注:ION Buffer在close dmafd过程中释放
DMA-BUF
DMA-BUF主要提供了一些管理struct dma_buf的操作,习惯上将分配buffer的模块称为exporter,使用该buffer的模块称为importer。例如ION中调dma_buf_export的模块就是一个exporter;实际使用这块buffer的模块就是importer,要使用这块buffer必须先通过dma_buf_get(fd)函数获取到对应的dma_buf,然后在通过DMA-BUF提供的一些列接口操作dma-buf,比如内核空间&用户空间&dma设备访问内存的操作,以及cache的同步。
代码路径:drivers/dma-buf
dma-buf文件操作接口
static const struct file_operations dma_buf_fops = {
.release = dma_buf_file_release,
.mmap = dma_buf_mmap_internal,
.llseek = dma_buf_llseek,
.poll = dma_buf_poll,
.unlocked_ioctl = dma_buf_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = dma_buf_ioctl,
#endif
.show_fdinfo = dma_buf_show_fdinfo,
};
release,用户空间使用close(dmafd)时调用,该流程会释放ION buffer。
mmap, 用户空间访问ION buffer。
ioctl, 向用户空间提供CMD:DMA_BUF_IOCTL_SYNC用户cache同步。
dma-buf内核空间API
struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)
分配buffer时调用导出dma_buf
int dma_buf_fd(struct dma_buf *dmabuf, int flags)
根据输入参数dmabuf,获取对应的fd
struct dma_buf *dma_buf_get(int fd)
根据输入参数fd,获取对应的dma_buf
void dma_buf_put(struct dma_buf *dmabuf)
释放dma_buf
struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf, struct device *dev)
添加dev到dmabuf的attachments链表;attach的目的:同一个dmabuf可能会被多个DMA硬件访问,而每个DMA硬件可能因为自身能力的限制,对这块buffer有自己的特殊要求,比如硬件A的寻址能力只有0x0----0x10000000,硬件B的寻址能力0x0----0x80000000,那么在分配dma-buf的物理内存时,就必须以硬件A的能力为标准进行分配,这样硬件A和硬件B都可以访问这段内存。attach操作可以让exporter驱动根据不同的device硬件能力,来分配最合适的物理内存,通过device->dma_params参数,来告知exporter驱动该DMA硬件的能力限制。
void dma_buf_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *attach)
从dmabuf的attachments链表中移除attach对应的dev
struct sg_table *dma_buf_map_attachment(struct dma_buf_attachment *attach, enum dma_data_direction direction)
将attach对应的dma buffer映射到scatter list table中。返回sg_table而不是物理地址主要是为了兼容所有DMA硬件(带或者不带IOMMU),sg_table既可以是连续物理地址也可以是非连续物理地址。sg_table本质上是由一块单个物理连续的buffer所组成的链表,但是这个链表整体上看确实离散的,他的每一个链表项由scatterlist表示,一个scatterlist对应着一块物理连续的buffer,sg_dma_address(sgl)和sg_dma_len(sgl)可以用来获取连续物理buffer的其实物理地址和长度。通过将该物理地址和长度设置给DMA寄存器,就可以实现DMA硬件对该快物理buffer的访问。对于整个sg_table的访问就需要使用for循环设置每一个链表项的物理地址和长度到DMA寄存器,因此在for循环中每次配置完DMA寄存器后都需要等待本次DMA传输完成,然后才能进行下一次的循环,大降低了软件的执行效率。IOMMU的功能就是用来解析sg_table,他将sg_table内部的一个个离散的小的连续buffer映射到自己内部的设备地址空间,使得整块buffer在自己内部的设备地址空间(iova地址区间)是连续的。这样在访问离散buffer的时候,只需要将IOMMU映射后的设备地址(iova)和整块buffer的size配置到DMA硬件寄存器中即可,中途无需再多次配置,便完成将DMA硬件对整块离散buffer的访问,大大提高访问效率。
void dma_buf_unmap_attachment(struct dma_buf_attachment *attach, struct sg_table *sg_table, enum dma_data_direction direction)
dma_buf_map_attachmen的逆操作
int dma_buf_begin_cpu_access(struct dma_buf *dmabuf, enum dma_data_direction direction)
cpu访问dma buf之前invalid cache
int dma_buf_end_cpu_access(struct dma_buf *dmabuf, enum dma_data_direction direction)
cpu访问结束后,flush cache
void *dma_buf_kmap(struct dma_buf *dmabuf, unsigned long page_num)
映射一页大小的dambuf到内核地址空间
void dma_buf_kunmap(struct dma_buf *dmabuf, unsigned long page_num, void *vaddr)
dma_buf_kmap的逆操作
void *dma_buf_vmap(struct dma_buf *dmabuf)
映射多页非连续(或连续)物理页到内核地址空间
void dma_buf_vunmap(struct dma_buf *dmabuf, void *vaddr)
dma_buf_vmap的逆操作
int dma_buf_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma, unsigned long pgoff)
映射dmabuf到用户空间
插播:设备A和设备B共享dma-buf的步骤(同一进程)
1. 设备A创建dma-buf,在调用dma_buf_fd()函数获取对应的fd,并将该fd返回到用户空间;
2. 用户空间将该fd传递到设备B,设备B的驱动调用dma_buf_get()函数获取到dma-buf;
3. 设备B的驱动拿到dma-buf后调用dma_buf_attach(),建立device和dma-buf的联系;
4. 设备B的驱动(设备B运行独立的系统,且系统中含有iommu硬件)继续调用dma_buf_map_attachment(),建立iova到dma-buf的映射,返回结构体 struct *sg_table;
5. 设备B的驱动调用sg_dma_address(),返回dma可访问地址;设备B的驱动把该地址传递到设备B运行的系统中,设备B使用该地址就可访问dma buf
6. 设备B使用完后,设备B的驱动需调用dma_buf_detach()和dma_buf_put().
备注:不同进程不同设备之间的dmabuf共享,也是通过fd实现的,但是fd属于进程上下文(进程A的fd在进程B中不能使用),所以在进程间传递fd需要做一些处理,linux上常用socket,android上常用binder,也可以自己实现fd的透传,参考系统调用dup()的实现。
DMA MAPPING
Dma-mapping向外提供了一系列的接口,供ION或者第三方驱动调用,主要如下几类接口:map接口、cache同步接口、其它接口。
map接口:实现为宏函数
#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)
#define dma_unmap_single(d, a, s, r) dma_unmap_single_attrs(d, a, s, r, 0)
#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)
#define dma_unmap_sg(d, s, n, r) dma_unmap_sg_attrs(d, s, n, r, 0)
#define dma_map_page(d, p, o, s, r) dma_map_page_attrs(d, p, o, s, r, 0)
#define dma_unmap_page(d, a, s, r) dma_unmap_page_attrs(d, a, s, r, 0)
#define dma_get_sgtable(d, t, v, h, s) dma_get_sgtable_attrs(d, t, v, h, s, 0)
#define dma_mmap_coherent(d, v, c, h, s) dma_mmap_attrs(d, v, c, h, s, 0)
Cache同步接口
dma_sync_single_for_cpu() //Invalid Cache
dma_sync_single_for_device() //Flush Cache
dma_sync_single_range_for_cpu()
dma_sync_single_range_for_device()
dma_sync_sg_for_cpu()
dma_sync_sg_for_device()
其他接口
set_dma_ops()
get_dma_ops()
dma_set_mask()
dma_get_mask()
IOMMU
iommu框架实现结构体struct dma_map_ops中的方法,以及向第三方驱动提供iommu接口,并抽象出iommu_ops结构体,供更底层的smmu硬件实现。
代码路径:drivers/iommu
结构体struct dma_map_ops中的回调函数实现:
static const struct dma_map_ops iommu_dma_ops = {
.alloc = iommu_dma_alloc,
.free = iommu_dma_free,
.mmap = iommu_dma_mmap,
.get_sgtable = iommu_dma_get_sgtable,
.map_page = iommu_dma_map_page,
.unmap_page = iommu_dma_unmap_page,
.map_sg = iommu_dma_map_sg,
.unmap_sg = iommu_dma_unmap_sg,
.sync_single_for_cpu = iommu_dma_sync_single_for_cpu,
.sync_single_for_device = iommu_dma_sync_single_for_device,
.sync_sg_for_cpu = iommu_dma_sync_sg_for_cpu,
.sync_sg_for_device = iommu_dma_sync_sg_for_device,
.map_resource = iommu_dma_map_resource,
.unmap_resource = iommu_dma_unmap_resource,
.get_merge_boundary = iommu_dma_get_merge_boundary,
};
IOMMU框架定义了几个重要的结构体struct iommu_device, struct iommu_group,struct iommu_domain,他们之间的关系为:在struct iommu_device中包含struct device,struct device中包含struct iommu_group,在struct iommu_group中包含struct iommu_domain;因此每一个iommu device都可以索引到一个对应的iommu_domain
iommu_domain代表具体的设备使用iommu的情况
结构体iommu_domain和iommu_device中的ops初始化信息如下:
iommu_device->ops指向&arm_smmu_ops,在arm_smmu_device_probe()函数中调用iommu_device_set_ops(&smmu->iommu, &arm_smmu_ops)初始化。
iommu_domain->ops指向&arm_smmu_ops,初始化流程为: arm_smmu_add_device ----> iommu_group_get_for_dev ----> __iommu_domain_alloc ----> domain->ops = bus->iommu_ops。
bus->iommu_ops指向&arm_smmu_ops, 在arm_smmu_device_probe()函数中调用bus_set_iommu(&platform_bus_type, &arm_smmu_ops)初始化。
SMMU
smmu是arm平台上的一个实现iommu功能的硬件设备。主要实现结构体struct iommu_ops定义的回调函数。
static struct iommu_ops arm_smmu_ops = {
.capable = arm_smmu_capable,
.domain_alloc = arm_smmu_domain_alloc,
.domain_free = arm_smmu_domain_free,
.attach_dev = arm_smmu_attach_dev,
.map = arm_smmu_map,
.unmap = arm_smmu_unmap,
.flush_iotlb_all = arm_smmu_flush_iotlb_all,
.iotlb_sync = arm_smmu_iotlb_sync,
.iova_to_phys = arm_smmu_iova_to_phys,
.add_device = arm_smmu_add_device,
.remove_device = arm_smmu_remove_device,
.device_group = arm_smmu_device_group,
.domain_get_attr = arm_smmu_domain_get_attr,
.domain_set_attr = arm_smmu_domain_set_attr,
.of_xlate = arm_smmu_of_xlate,
.get_resv_regions = arm_smmu_get_resv_regions,
.put_resv_regions = arm_smmu_put_resv_regions,
.pgsize_bitmap = -1UL, /* Restricted during device attach */
};
以dma_buf_map_attachment()函数为例讲解dma-buf到smmu的调用流程
下面函数的实现代码只包含了调用的关键函数。
attach->dmabuf->ops->map_dma_buf()回调函数指向ion_map_dma_buf()函数。在ion.c中初始化。
struct sg_table *dma_buf_map_attachment(struct dma_buf_attachment *attach,
enum dma_data_direction direction)
{
struct sg_table *sg_table;
might_sleep();
sg_table = attach->dmabuf->ops->map_dma_buf(attach, direction);
return sg_table;
}
ion_map_dma_buf()函数中调用dma_map_sg()函数。
static struct sg_table *ion_map_dma_buf(struct dma_buf_attachment *attachment,
enum dma_data_direction direction)
{
struct ion_dma_buf_attachment *a = attachment->priv;
struct sg_table *table;
table = a->table;
if (!dma_map_sg(attachment->dev, table->sgl, table->nents,
direction))
return ERR_PTR(-ENOMEM);
return table;
}
dma_map_sg()函数在include/linux/dma-mapping.h中实现。
#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)
static inline int dma_map_sg_attrs(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir,
unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
int ents;
if (dma_is_direct(ops))
ents = dma_direct_map_sg(dev, sg, nents, dir, attrs);
else
ents = ops->map_sg(dev, sg, nents, dir, attrs);
return ents;
}
ops->map_sg()回调函数指向iommu_dma_map_sg()函数,ops在drivers/iommu/dma-iommu.c文件中初始化为iommu_dma_ops; iommu_dma_map_sg()主要作用是分配一个iommu空间的iova地址,然后将该地址和sg table建立映射,dma设备使用iova地址访问dma buffer。
static int iommu_dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir, unsigned long attrs)
{
dma_addr_t iova;
iova = iommu_dma_alloc_iova(domain, iova_len, dma_get_mask(dev), dev);
iommu_map_sg(domain, iova, sg, nents, prot);
return __finalise_sg(dev, sg, nents, iova);
}
iommu_map_sg()函数建立iova到sg的映射,nents表示sg链表的长度,即dma buffer被切割为多少个离散buffer。
size_t iommu_map_sg(struct iommu_domain *domain, unsigned long iova,
struct scatterlist *sg, unsigned int nents, int prot)
{
size_t len = 0, mapped = 0;
phys_addr_t start;
unsigned int i = 0;
while (i <= nents) {
iommu_map(domain, iova + mapped, start, len, prot);
mapped += len;
}
return mapped;
}
iommu_map()函数建立iova和物理地址的映射。
int iommu_map(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot)
{
const struct iommu_ops *ops = domain->ops;
while (size) {
size_t pgsize = iommu_pgsize(domain, iova | paddr, size);
ret = ops->map(domain, iova, paddr, pgsize, prot);
iova += pgsize;
paddr += pgsize;
size -= pgsize;
}
return ret;
}
ops->map()回调函数指向arm_smmu_map()函数。
arm smmu更多细节请参考:
2. ARM SMMU学习笔记