0%

周谈(51)- DMA问题定位小结

前言

最近项目上出了个比较重大的bug,驱动注册的时候会偶发挂死、计算结果不正确,而且在不同内核版本,不同PAGE_SIZE下还表现不一样。
鉴于有合作方已经在使用了我们的驱动了,问题等级就上升了。项目组的小妹搞了快1个半月了,依然没有任何头绪,期间组内也组织过一次代码评审,都没有看出啥问题。
最后我就临时被拉去救火了,其他的工作全部暂停。
目前大部分问题已经定位解决,除了异常分支错误处理外,其他就是DMA-API的滥用了。

DMA-API

关于DMA映射API的使用在去年八月份有写过一篇文章介绍周谈(34)-DMA地址映射api使用,文章是刚接触DMA学习后的小结,只是对API函数及概念有了基础的理解,这次是遇到事儿,再看一遍内核的Document,就有不同的看法了。

带着问题去学习目的是比较明确的,在看API接口的同时会有更多的理解,下面简要地讲下遇到的问题及以后需要注意的点。

一致性和流式DMA

DMA分为两种,一致性DMA和流式DMA。

前者使用的是带coherent的API,不需要开发者关心内存的一致性,系统会自动进行同步(但是你需要保证buffer在被设备读取时已经flush了?没太看懂这个说法?),申请时一般至少以页为单位,即使你申请小于1页,内部也会占用一个页的空间,一般在驱动注册时申请,卸载后注销,使用的生命周期比较长一些。

流式DMA,则是通过已有的内存进行映射,使用完后再注销,最后才释放内存。流式DMA需要开发者关注内存的DMA方向,一旦进行映射后,理论上这块内存就属于设备的了,如果在注销映射之前CPU要访问这块内存,需要调用XXX_for_cpu的接口获取该内存的所有权,xxx_for_cpu接口根据传入的direction参数,会进行数据的同步,如果是DMA_FROM_DEVICE,那么会把数据从设备端同步到内存,并无效掉对应的CACHE,这样CPU访问的内容就是设备返回的内容了。如果是DMA_TO_DEVICE,那么CPU就可以往该内存写入数据。CPU访问结束后,再通过xxx_for_device接口把内存的所有权还给设备,设备就可以使用修改后的内存了。如果xxx_for_cpu方向为DMA_FROM_DEVICE,那么写入内存的地址是无法同步到设备端的,这个要特别注意。

工具接口

dma_max_mapping_size会返回映射API接口支持的最大内存长度,一般为0xffffffff;
dma_need_sync返回一个dma地址是否需要调用xxx_for_cpu/device修改内存权限,奇怪的是即使你调用的是xxx_coherent接口申请的地址,返回的也是true;
dma_get_merge_boundary返回DMA合并的边界,这个也没太搞懂,默认为PAGE_SIZE-1,当dma_map_sg的sglist大小超过boundary时,开启dma api debug时会报错误;
dma_get_max_seg_size获取dma_map_sg支持的segment的最大长度,默认返回64k;
dma_mapping_error用于判断返回的地址是否合法,当我们开启了DMA-API DEBUG功能时,如果某个地址没有调用该API接口判断,那么在注销映射的时候将会有警告调用栈输出;
dma_get_cache_alignment返回处理器的缓存对齐,处理mapping后的内存需要以该大小对齐进行处理,可能返回的值大于实际的缓存行,基于此后续进行mapping的地址最好都是CACHELIEN对齐的,长度至少是CACHELINE大小。

dma_map_sg

这个接口用来映射scatterlist的,使用方式如下:

1
2
3
4
5
6
7
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;

for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}

参数nents为sglist的段数,返回count可能小于nents,因为接口内部会合并相邻物理地址连续的段,需要注意的是,经过dma_map_sg后,我们需要使用sg_dma_address, sg_dma_len接口获取地址的值及长度。失败的话,count返回值为0。相应的注销时调用dma_unmap_sg, 参数必须和映射时一致。

dma api debug

dma-api有许多限制,随着硬件IOMMU的出现,驱动程序变得越来越重要不要违反这些限制,任意一个地方违法了就会导致系统挂掉。开启DMA API debug功能,可以帮助开发者查找隐藏的bug,在编译内核的时候,在kernel configuration中勾选 “Enable debugging of DMA-API usage”。

通过开启了DMA API debug后,我也是找到了许多问题了的。dma api debug还通过debugfs暴露了一些接口用于调试定位。

默认情况只会输出一个错误/警告信息,其他错误只会计数不输出, 这样可以防止内核信息泛滥,可以通过debugfs过滤指定的驱动,并配置相应的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dma-api/all_errors 只要不为0,就会打印错误调用栈

dma-api/disabled 指示当前debug功能是否禁用了,一般只有内存耗尽或者启动时禁用时为1

dma-api/dump 显示当前所有的内存映射

dma-api/error_count 显示总的错误数

dma-api/num_errors 设置最多打印的错误数目默认是1

dma-api/min_free_entrie 为0时会继续申请entry

dma-api/num_free_entries free的entry数

dma-api/nr_total_entries 已申请的entry数,包含free、used

dma-api/driver_filter 可以用于指定驱动, 比如要调试test.ko, 则echo test>/sys/kernel/debug/dma-api/driver_filter,那么前面的计数也都只针对这个驱动了。

遇到的问题回顾

  1. IOMMU报物理地址错误,但是那个地址其实已经释放掉了,理论上不会再用到的。这个主要是同步内存给设备的时候,代码都是按64字节大小依次同步的,每个队列有一片缓冲区用于下发命令给设备,每次都最后一段时,错误就发生了。需要扩充缓冲区的大小,最后一片64字节其实也是按dma_get_cache_alignment即128字节同步的,最后64字节长度不足128字节,同步就异常了,再往后扩个64字节问题就解决了。

  2. 结果报wrong result,这个主要就是在内存mapping之后,没有调用xxx_for_cpu就去更新内存,导致最终内容没有同步到设备,设备使用的数据是错误的。还有一种情况,就是dma_map_single的内存不是cacheline对齐的,这个通过在相应的结构体中使用____cacheline_aligned定义,使该成员的首地址是cacheline对齐的。

  3. umap的时候报错,这个主要是映射后地址没有调用dma_mapping_error接口检查导致的。

  4. 挂死问题,这个是由于使用的是sg->length而不是sg_dma_len函数获取映射后的长度,最终导致地址访问越界。

  5. 其他就是特殊的异常分支处理的问题了。

更多

其实问题也就几类,但是架不住文件多啊,而且先前是两三个人协作写的代码,大家便于调试,很多本可以合并的代码都是重复存在了好几处,改起来也是累,最后花时间重构了一部分代码,代码行数都少了3000多行。剩余的代码交给另外一个同事改的,能解决问题就ok了,等有空再去重构吧。

中间遇到一部分新员工解bug的代码,各种if else判断,遇到问题解决问题,补丁一大堆,剪不断理还乱的,各种条件写的莫名其妙,调试起来还是一堆错。无奈,又花了三四天琢磨算法标准,解决异常处理问题。

这个救火任务前后也花了我一个月左右,终于要告一段落。


行动,才不会被动!

欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。

欢迎关注

博客地址: https://fishmwei.github.io

掘金主页: https://juejin.cn/user/2084329776486919