上一篇中提到了cam_smmu_alloc_iova 通过从已经构建好的内存池中获取虚拟地址然后调用iommu_map_sg 去建立映射关系。 dma_buf_map_attachment 建立映射关系时也需要获取虚拟地址。
虚拟地址的其实地址,大小也是在设备树中设置好的,需要一个地方从设备树中读取然后保存起来,在使用的时候从其中去分配。
1.获取虚拟地址
初始化的流程: of_dma_configure->arch_setup_dma_ops→arm_iommu_setup_dma_ops→arm_iommu_get_dma_window .
arm_iommu_get_dma_window 函数 通过 "qcom,iommu-dma-addr-pool" 从设备树中获取起始地址和长度。 然后 将获得的dma_base, size 保存到mapping 中 传递给arm_iommu_get_dma_cookie。
static int arm_iommu_get_dma_cookie(struct device *dev,
struct dma_iommu_mapping *mapping)
{
.....
iommu_domain_get_attr(mapping->domain, DOMAIN_ATTR_S1_BYPASS,
&s1_bypass);
iommu_domain_get_attr(mapping->domain, DOMAIN_ATTR_FAST, &is_fast);
if (s1_bypass)
mapping->ops = &arm64_swiotlb_dma_ops;
else if (is_fast)
err = fast_smmu_init_mapping(dev, mapping);
else
err = iommu_init_mapping(dev, mapping);
......
return err;
}
arm_iommu_get_dma_cookie 判断了属性,根据不通的属性初始化也不相同。 arm64_swiotlb_dma_ops 这个根据网上资料是在没有硬件smmu 时候,软件替代。这里有种fast 属性但是需要在dts 中配置。arm_smmu_setup_default_domain 函数通过"qcom,iommu-dma" 读取设置
Additional properties for Iommu Clients:
- qcom,iommu-dma:
Optional, String.
Can be one of "bypass", "fastmap", "atomic", "disabled".
--- "default":
Standard iommu translation behavior.
The iommu framework will automatically create a domain for the client.
iommu and DMA apis may not be called in atomic context.
--- "bypass":
DMA APIs will use 1-to-1 translation between dma_addr and phys_addr.
Allows using iommu and DMA apis in atomic context.
--- "fastmap":
DMA APIs will run faster, but use several orders of magnitude more memory.
Also allows using iommu and DMA apis in atomic context.
--- "atomic":
Allows using iommu and DMA apis in atomic context.
--- "disabled":
The iommu client is responsible for allocating an iommu domain, as
well as calling iommu_map to create the desired mappings
.
不同设置的相关介绍如上。 camera 的dtsi 并没有设置qcom,iommu-dma。所以是不会有DOMAIN_ATTR_FAST 这个属性的设置,arm_iommu_get_dma_cookie最后会运行iommu_init_mapping。
iommu_init_mapping主要代码:
....
own_cookie = !domain->iova_cookie;
if (own_cookie) {
ret = iommu_get_dma_cookie(domain); // 分配一个iova_cookie结构体,iova_cookie 结构体中包含了iova_domain, iova_domain包含一个红黑树,后面分配的时候内存会挂到红黑树中
//所有和domain 相关的虚拟地址信息都保存在iova_domain中(前面从设备树中得到起始地址和size 都会保存到iova_domain)
if (ret) {
dev_err(dev, "iommu_get_dma_cookie failed: %d\n", ret);
return ret;
}
}
ret = iommu_dma_init_domain(domain, dma_base, size, dev); //初始化iova_cookie中的iova_domain
if (ret) {
dev_err(dev, "iommu_dma_init_domain failed: %d\n", ret);
if (own_cookie)
iommu_put_dma_cookie(domain); =
return ret;
}
mapping->ops = &iommu_dma_ops; //这里的ops 会赋值给dev 的ops.dma_buf_map_attachment 等函数其实就是访问iommu_dma_ops中对应的函数
iommu_dma_init_domain 代码如下有一些类型判断省略了
// 第二个参数base 从前面传递过来就是通过"qcom,iommu-dma-addr-pool" 的, 它就是在设备树中设置的开始地址
int iommu_dma_init_domain(struct iommu_domain *domain, dma_addr_t base,
u64 size, struct device *dev)
{
......
/* Use the smallest supported page size for IOVA granularity */
order = __ffs(domain->pgsize_bitmap);
base_pfn = max_t(unsigned long, 1, base >> order);
end_pfn = (base + size - 1) >> order; //end_pfn 是开始地址加上大小 就是结束地址。 >>order 只是为了对齐
.......
iovad->end_pfn = end_pfn; //iovad(struct iova_domain) 中的start_pfn end_pfn 这两个分别保存了其实地址和结束地址
init_iova_domain(iovad, 1UL << order, base_pfn);//base_pfn 传入后赋值给了 start_pfn
if (!dev)
return 0;
return iova_reserve_iommu_regions(dev, domain);
}
现在就获得了虚拟地址的起始地址和结束地址。在分配虚拟地址时就是从这区间找到一个合适的地址。 这个区间具体保存在domain→iova_cookie→iovad中。init_iova_domain函数是一个非常重要的函数,后面很多要使用的参数都是
在这里设置好的。
void
init_iova_domain(struct iova_domain *iovad, unsigned long granule,
unsigned long start_pfn)
{
/*
* IOVA granularity will normally be equal to the smallest
* supported IOMMU page size; both *must* be capable of
* representing individual CPU pages exactly.
*/
BUG_ON((granule > PAGE_SIZE) || !is_power_of_2(granule));
spin_lock_init(&iovad->iova_rbtree_lock);
iovad->rbroot = RB_ROOT;
iovad->cached_node = &iovad->anchor.node; //cached_node 指向上次访问的node。anchor 是一个iova 结构体 --struct iova anchor; /* rbtree lookup anchor */
iovad->cached32_node = &iovad->anchor.node;
iovad->granule = granule;
iovad->start_pfn = start_pfn;//base 地址,也是起始地址
iovad->dma_32bit_pfn = 1UL << (32 - iova_shift(iovad));
iovad->flush_cb = NULL;
iovad->fq = NULL;
iovad->anchor.pfn_lo = iovad->anchor.pfn_hi = IOVA_ANCHOR; // pfn_lo 实际保存着虚拟地址,默认都是0xFFFFFFFFF
rb_link_node(&iovad->anchor.node, NULL, &iovad->rbroot.rb_node);//插入新的node 节点
rb_insert_color(&iovad->anchor.node, &iovad->rbroot);//调整红黑树
iovad->best_fit = false; //是否使能最佳匹配,默认是不使能
init_iova_rcaches(iovad);
}
要理解iova 的结构后面使用时候才好理解
struct iova {
struct rb_node node; //红黑树的node
unsigned long pfn_hi; /* Highest allocated pfn *///申请的虚拟结束地址
unsigned long pfn_lo; /* Lowest allocated pfn */ //申请虚拟内存的起始地址
};
2. 分配虚拟地址
dma_buf_map_attachment->map_dma_buf→ion_map_dma_buf(ion.c 文件中,因为camera 使用的ion分配的buf)→dma_map_sg_attrs
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); //get_dma_ops 主要内容dev->dma_ops 这里获得dev 的ops ,dev ops 就是iommu_dma_ops
具体为什么devops 是iommu_dma_ops,参考文章 https://blog.csdn.net/qq_28637193/article/details/103551684
....
ents = ops->map_sg(dev, sg, nents, dir, attrs);
BUG_ON(ents < 0);
debug_dma_map_sg(dev, sg, nents, ents, dir);
return ents;
}
iommu_dma_ops 位于arch/arm64/mm/dma-mapping.c 文件中
.map_sg = __iommu_map_sg_attrs
dma_buf_map_attachment->map_dma_buf→ion_map_dma_buf(ion.c 文件中,因为camera 使用的ion分配的buf)→dma_map_sg_attrs->__iommu_map_sg_attrs(iommu_dma_ops)→iommu_dma_map_sg
int iommu_dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, int prot)
{
struct iommu_domain *domain;
struct iommu_dma_cookie *cookie;
struct iova_domain *iovad; //前面提到了这里面有红黑树有虚拟地址的起始地址和结束地址
dma_addr_t iova;
size_t iova_len;
domain = iommu_get_domain_for_dev(dev);
if (!domain)
return 0;
cookie = domain->iova_cookie;
iovad = &cookie->iovad;
iova_len = iommu_dma_prepare_map_sg(dev, iovad, sg, nents);
iova = iommu_dma_alloc_iova(domain, iova_len, dma_get_mask(dev), dev); //分配虚拟地址
....
if (iommu_map_sg(domain, iova, sg, nents, prot) < iova_len) //将虚拟地址iova 和sg 建立映射关系
goto out_free_iova;
.....
}
dma_buf_map_attachment->map_dma_buf→ion_map_dma_buf(ion.c 文件中,因为camera 使用的ion分配的buf)→dma_map_sg_attrs->__iommu_map_sg_attrs(iommu_dma_ops)→iommu_dma_map_sg->iommu_dma_alloc_iova→alloc_iova_fast->alloc_iova
alloc_iova(struct iova_domain *iovad, unsigned long size,
unsigned long limit_pfn,
bool size_aligned)
{
struct iova *new_iova;
int ret;
...
if (iovad->best_fit) { //best_fit 是一个使能最佳匹配的标志位
ret = __alloc_and_insert_iova_best_fit(iovad, size,
limit_pfn + 1, new_iova, size_aligned);
} else {
ret = __alloc_and_insert_iova_range(iovad, size, limit_pfn + 1,
new_iova, size_aligned);
}
....
}
best_fit 默认是false 也就是说默认都是最快匹配,性能好但是这样有个问题是会导致内存碎片化严重。所以要根据实际任务选择最佳算法。
best_fit 被赋值false在init_iova_domain 函数中,也是这个给start_pfn赋值地方。 但是有接口能够赋值位true,这样就使能了最佳分配算法iommu_dma_enable_best_fit_algo 函数就是完成这一任务的。
现在camera 使能了最佳算法来减少碎片化。 但是在自建的内存池从里面分配虚拟地址时高通却没有使用最佳算法。
2.1 最快匹配算法 __alloc_and_insert_iova_range
每次分配和释放都是更新cache_node 来保存最近操作的node ,分配时候直接从最近访问的node 开始查找,感觉这样查找虽然快但是碎片严重。
static int __alloc_and_insert_iova_range(struct iova_domain *iovad,
unsigned long size, unsigned long limit_pfn,
struct iova *new, bool size_aligned)
{
struct rb_node *curr, *prev;
struct iova *curr_iova;
unsigned long flags;
unsigned long new_pfn;
unsigned long align_mask = ~0UL;
if (size_aligned)
align_mask <<= limit_align(iovad, fls_long(size - 1));
/* Walk the tree backwards */
spin_lock_irqsave(&iovad->iova_rbtree_lock, flags); //自旋锁
curr = __get_cached_rbnode(iovad, limit_pfn); //__get_cached_rbnode: iovad->cached_node cache_node存放着最近被访问的node
curr_iova = rb_entry(curr, struct iova, node);//获得node 中的红黑树
do {
limit_pfn = min(limit_pfn, curr_iova->pfn_lo);//pfn_lo 最开始是0xFFFFFFFF 所以肯定是limit_pfn小,如果不是第一次分配,一般是pfn_lo小
new_pfn = (limit_pfn - size) & align_mask;//结束地址减去size 然后对齐就是起始地址
prev = curr;
curr = rb_prev(curr);//获取curr 前一个node ,也就是他的左孩子
curr_iova = rb_entry(curr, struct iova, node);
} while (curr && new_pfn <= curr_iova->pfn_hi); // 小于说明从当前node的起始地址,向前减去size 获得的地址在左孩子的区间,这样是不行的
if (limit_pfn < size || new_pfn < iovad->start_pfn) {
spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
return -ENOMEM;
}
/* pfn_lo will point to size aligned address if size_aligned is set */
new->pfn_lo = new_pfn;
new->pfn_hi = new->pfn_lo + size - 1;
/* If we have 'prev', it's a valid place to start the insertion. */
iova_insert_rbtree(&iovad->rbroot, new, prev);//插入node 到树中
__cached_rbnode_insert_update(iovad, new);//更新最近访问的node
spin_unlock_irqrestore(&iovad->iova_rbtree_lock, flags);
return 0;
}
因为从高地址往低地址去分配,加上红黑树原理,要维护右边的node 没有空闲的内存就要在free 时候加上判断,如果释放的node 在上次释放的node左边不更新cache_node
static void
__cached_rbnode_delete_update(struct iova_domain *iovad, struct iova *free)
{
struct iova *cached_iova;
cached_iova = rb_entry(iovad->cached32_node, struct iova, node);
if (free == cached_iova ||
(free->pfn_hi < iovad->dma_32bit_pfn &&
free->pfn_lo >= cached_iova->pfn_lo))
iovad->cached32_node = rb_next(&free->node);
cached_iova = rb_entry(iovad->cached_node, struct iova, node);
if (free->pfn_lo >= cached_iova->pfn_lo)
iovad->cached_node = rb_next(&free->node);
}