1. 程式人生 > >關於spin_lock使用過程中的一次問題定位

關於spin_lock使用過程中的一次問題定位

1、        問題描述
軟硬體約束條件:
軟體平臺:linux 3.4.35的kernel版本
硬體平臺:海思3518ev200晶片([email protected]
問題現象:
報警主機向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、未完 待續……