关于spin_lock使用过程中的一次问题定位

1、        问题描述
软硬件约束条件:
软件平台:linux 3.4.35的kernel版本
硬件平台:海思3518ev200芯片(ARM926@440Mhz)
问题现象:
报警主机向slic芯片每100ms发送一个cid报文(DTMF双频音),slic芯片检测到双频音后触发中断,中断函数做相关的处理,主要是读走双频音数据。偶现的问题是cid报文会丢失,导致异常。

2、        问题定位
首先cid报文丢失,哪里丢失的?是应用丢失了,还是驱动丢失的?这个相对好确定。在应用取报文的接口加打印就可以确认了,应用并没有丢失数据,而是驱动丢失了数据。

第二,确定是驱动丢失了数据,那是驱动丢失中断,还是检测到中断,后续没有处理呢?这里我们先插入一些题外话,关于linux中断处理,随着不同需求的发展,中断处理逐渐分为上下两半部处理机制。上半部处理耗时较少的任务,下半部处理耗时较长的任务。上半部的限制比较多些,最主要的是不能调用休眠的函数,因为中断没有上下文,休眠后,永远不能再次调度。至于下半部实现方式,目前大致有四种:
Tasklet、工作队列、软中断和线程化irq,(宋宝华 linux设备驱动开发详解第10章 中断与时钟 p230)有详细的描述,本例只做简单的描述。Tasklet执行上下文是软中断,不能休眠;工作队列,执行上下文是内核线程,可以调度和休眠;软中断方式,tasklet就是基于软中断方式实现的,驱动编写者不会也不宜直接使用softirq;最后一种是线程化处理方式,内核会为相应的中断分配一个相应的内核线程,上半部执行返回IRQ_WAKE_THREAD后,内核会调度对应线程执行thread_fn对应的函数。本例中使用的就是线程化的处理方式。

第三,确定在什么时候丢失的cid报文
在驱动上半部中增加一个计数字段,每来一次都自增,在下半部掉进去后打出此值,同时下半部中取cid报文处也计数,这里发现,当cid丢失时,上半部是增加了,但是下半部没有做取cid的动作。于是可以确认:1、中断没有丢失;2、下半部处理可能出问题了,它没有取cid报文。

第四、为什么没有取cid报文
咨询si32178厂商后,得知没有取报文的原因是芯片下半部处理时间过长,导致描述该dtmf音有效的字段已经失效(该字段只有在dtmf音持续触发时间内,字段才有效)dtmf音已经停止触发了。DTMF音持续的时间是50ms,也就是说,中断下半部在50ms里面都没有来取cid报文。

第五,初步解决方式
既然需要检测dtmf有效位后再去取cid报文,能不能不检测dtmf的有效位,直接取cid数据呢?咨询si32178厂商后说,也可以,风险点在于不知道dtmf数据会不会被下一个cid覆盖,前面说了cid是100ms触发一次,而检测dtmf音需要持续触发13.3ms以上,那如果122ms(极限时间)没有取数据,数据也会丢失。于是初步尝试的版本有了,不去检测cid数据的有效性,中断来了后,直接取走cid数据。测了一段时间后,cid丢失的问题又出现了,于是在中断上部加入时间,在下半部取数据的点上也加入时间,打出时间差,发现时间差确实有100ms以上的。

第六,为什么会有这么多的延时
从中断上半部到下半部获取cid数据,为什么会有100ms以上的延时,这意味者什么?这里我没有仔细的分析,而是盲目的尝试了上述中断下半部处理方式的tasklet和work队列,以及在下半部中增加定时器来取数据,结果还遗憾都是会丢cid的。当时我高度怀疑linux的调度出了问题,是下半部没有得到及时调度引起的,因为linux是非实时的系统,无论是tasklet和work队列,系统都是在合适的时间去调度。于是另一个尝试的做法出现了,既然怀疑调度,那为什么不所有的工作都放到上半部处理呢?

第七、上半部的版本
在精简了代码,把能做的操作尽量减少,能不加的锁去掉后,上半部处理所有工作的版本出来了,心想这下总没有问题了吧!测试的时候,确实坚持了很久,但是(凡是都怕但是)还是丢cid了。当时我心想,没有方法了,该试的方式我都试了,还是解决不了。不知道还能做什么,不知道问题到底出在哪里。

第八、再次分析
从上半部到下半部的执行时间超过100ms,这个是调度的问题?
Cpu主频440Mhz, 这是什么概念?一秒中执行4.4亿次单指令周期的命令,100ms可以做4400万次基本操作。再来看优先级,中断下半部的优先级可以理解为fifo 999的优先级,可以说除了中断就是这个线程取操作,而现在耽搁这么长的时间没有执行,基本不会是调度的问题,而是其他操作出问题了。那我们在中断下半部到底做了什么操作呢?我们读slic的寄存器去清中断,读取cid数据。那问题是否出在读slic芯片的寄存器清中断呢?我们是如何读寄存器呢?

第九、深入分析
读寄存器使用的是模拟的spi接口,先发送一个ctrl字,在发送addr,最后发送数据,这整个过程已经spin_lock锁保证操作的原子性,这里有一个问题:
模拟Spi这个资源是有竞争的:1、普通的ioctrl会使用; 2、中断也会使用。
我们使用的spin_lock来保护互斥资源,考虑如下情况:当线程A调用ioctrl,它拿到了spi的spin_lock锁,正在操作的时候,这时中断来了,线程A被打断。转而执行中断,中断中也是用模拟spi,也要去拿锁,这个时候,拿不到锁,忙等,等待线程A释放锁,但是线程A没有机会得到调度,死锁。从逻辑上讲,spin_lock保护spi模拟资源会导致死锁的,因为它保护不住,但是为什么没有死锁呢?很奇怪。那什么锁能保护住,不让中断过来抢资源呢?spin_lockirqsave

第十,又是一个测试版本
模拟的spi换掉spin_lock使用spin_lockirqsave锁保护后,又出了一个版本给同事测试,这个锁会关中断,线程A操作的时候,不会来中断,所以它可以保护的住互斥资源。终于,测试到现在没有丢cid报文了。但是有个问题不解,之前用spin_lock这把锁,如果锁不住,设备会宕机,为什么没有宕机呢?

第十一,深挖spin_lock
在给出spin_lockirqsave的版本后,到此cid丢失的问题已经解决。但是还有一个问题是不和逻辑的,spin_lock这把锁是锁不住模拟spi资源的,为什么没有死锁?这个时候,我想最好的方法就是看内核源码了,你会发现spin_lock是一个条件编译,内核配置不同,spin_lock的实现是不同的,我们这个版本spin_lock是啥都没有做。自然保护不住模拟spi通信的原子性。于是乎,又有一个问题,为什么spin_lock实现需要条件编译去控制,spin_lock是自璇锁,为什么实现会是空,啥都不做呢?这个问题要追溯到spin_lock的由来了。Spin_lock本来是用在SMP系统上的,例如我们有两个核,A和B,当中断来的时候,A和B都要在中断里访问临界资源S,这个时候怎么保护S呢?使用spin_lock,A核触发中断,首先拿到锁,在临界区执行,此时B核中断也触发了,它也去拿锁,这个时候B拿不到锁,于是乎,它自璇在这里等待,独占B核的cpu资源。终于A核做完所有事情,A核释放锁资源。这个时候,B核拿到锁可以继续执行了。而我们的系统是UP系统,单核的,所以理论上不需要spin_lock这个东西。故而,实现为空。但是具体的要看内核代码,有些是关了抢占的。一切以代码为准。

那么,针对中断和线程竞争资源该使用什么锁,内核做了一些其他spin_lock的变种:
Spin_lock/spin_unlock
Spin_lock_bh/spin_unlock_bh
Spin_lock_irq/spin_unlock_irq
Spin_lock_irqsave/spin_lock_irqrestore
详细的用法介绍可以参见:
https://blog.csdn.net/wh_19910525/article/details/11536279

https://www.cnblogs.com/aaronLinux/p/5890924.html

http://blog.csdn.net/electrombile/article/details/51289813

https://www.cnblogs.com/sky-heaven/p/5730113.html

3问题总结
表象的背后是我们追求的真相,真相的背后是我们追求的真知。真知才能进一步指导我们的行为逻辑。
计算机中每秒钟运行的指令以亿为单位,任何逻辑上有风险的点,哪怕概率是亿万分之一,那么跑到的概率也是极大的。就像丢cid报文一样,中断和普通ioctrl就是撞到了一起,没有保护模拟spi通信的原子性。这个概率发生的也不高,但是它就是实实在在的发生了。此次Debug的时间非常久,多次讨论,多次尝试,终于找到问题所在。
1、        那么问题能否避免在coding阶段呢?还是有可能的,养成严谨的逻辑,良好的代码习惯很重要。
2、        Debug的时间能否缩短,也是有可能的。不要太多的盲目尝试,多一些理性的分析,了解所用接口的特性
3、未完 待续……

猜你喜欢

转载自blog.csdn.net/liukengpeng/article/details/80239560