前言
最近项目上出了个比较重大的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 | int i, count = dma_map_sg(dev, sglist, nents, direction); |
参数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 | dma-api/all_errors 只要不为0,就会打印错误调用栈 |
遇到的问题回顾
IOMMU报物理地址错误,但是那个地址其实已经释放掉了,理论上不会再用到的。这个主要是同步内存给设备的时候,代码都是按64字节大小依次同步的,每个队列有一片缓冲区用于下发命令给设备,每次都最后一段时,错误就发生了。需要扩充缓冲区的大小,最后一片64字节其实也是按dma_get_cache_alignment即128字节同步的,最后64字节长度不足128字节,同步就异常了,再往后扩个64字节问题就解决了。
结果报wrong result,这个主要就是在内存mapping之后,没有调用xxx_for_cpu就去更新内存,导致最终内容没有同步到设备,设备使用的数据是错误的。还有一种情况,就是dma_map_single的内存不是cacheline对齐的,这个通过在相应的结构体中使用____cacheline_aligned定义,使该成员的首地址是cacheline对齐的。
umap的时候报错,这个主要是映射后地址没有调用dma_mapping_error接口检查导致的。
挂死问题,这个是由于使用的是sg->length而不是sg_dma_len函数获取映射后的长度,最终导致地址访问越界。
其他就是特殊的异常分支处理的问题了。
更多
其实问题也就几类,但是架不住文件多啊,而且先前是两三个人协作写的代码,大家便于调试,很多本可以合并的代码都是重复存在了好几处,改起来也是累,最后花时间重构了一部分代码,代码行数都少了3000多行。剩余的代码交给另外一个同事改的,能解决问题就ok了,等有空再去重构吧。
中间遇到一部分新员工解bug的代码,各种if else判断,遇到问题解决问题,补丁一大堆,剪不断理还乱的,各种条件写的莫名其妙,调试起来还是一堆错。无奈,又花了三四天琢磨算法标准,解决异常处理问题。
这个救火任务前后也花了我一个月左右,终于要告一段落。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。
博客地址: https://fishmwei.github.io
掘金主页: https://juejin.cn/user/2084329776486919