作者:韋東山
前言:
本文,4200字,研究代碼花了一天,寫出來花了一天;
錄視頻估計又得花半天;
真懷念以前簡單粗暴的生活啊:
拿起話筒就錄視頻,
先畫好圖?那是不需要的
文檔?那是不存在的
真是灑脫.....
現在,要寫文檔,又要畫流程圖,十幾、二十分鐘的視頻,
真是漚心瀝血做出來的,
各位,別浪費了,歡迎享受
韋東山老師正在錄本文配套的視頻,明天發佈。咱們先預習。
分爲7點:
Linux對中斷的擴展:硬件中斷,軟件中斷
中斷處理原則1:不能嵌套
中斷處理原則2:越快越好
要處理的事情實在太多:拆分爲:上半部,下半部
下半部的事情耗時不是太長:tasklet
下半部要做的事情太多並且很複雜:工作隊列
新技術:threaded irq
從2005年我接觸Linux到現在15年了,Linux中斷系統的變化並不大。比較重要的就是引入了threaded irq:使用內核線程來處理中斷。
Linux系統中有硬件中斷,也有軟件中斷。
對硬件中斷的處理有2個原則:不能嵌套,越快越好。
參考資料:
https://blog.csdn.net/myarrow/article/details/9287169
01 Linux對中斷的擴展:硬件中斷、軟件中斷
Linux系統把中斷的意義擴展了,對於按鍵中斷等硬件產生的中斷,稱之爲“硬件中斷”(hard irq)。每個硬件中斷都有對應的處理函數,比如按鍵中斷、網卡中斷的處理函數肯定不一樣。
爲方便理解,你可以先認爲對硬件中斷的處理是用數組來實現的,數組裏存放的是函數指針:
注意:上圖是簡化的,Linux中這個數組複雜多了。
當發生A中斷時,對應的irq_function_A函數被調用。硬件導致該函數被調用。
相對的,還可以人爲地製造中斷:軟件中斷(soft irq),如下圖所示:
注意:上圖是簡化的,Linux中這個數組複雜多了。
問題來了:
a. 軟件中斷何時生產?
由軟件決定,對於X號軟件中斷,只需要把它的flag設置爲1就表示發生了該中斷。
b. 軟件中斷何時處理?
軟件中斷嘛,並不是那麼十萬火急,有空再處理它好了。
什麼時候有空?不能讓它一直等吧?
Linux系統中,各種硬件中斷頻繁發生,至少定時器中斷每10ms發生一次,那取個巧?
在處理寫硬件中斷後,再去處理軟件中斷?就這麼辦!
有哪些軟件中斷?
查內核源碼include/linux/interrupt.h
怎麼觸發軟件中斷?最核心的函數是raise_softirq,簡單地理解就是設置softirq_veq[nr]的標記位:
怎麼設置軟件中斷的處理函數:
後面講到的中斷下半部tasklet就是使用軟件中斷實現的。
02 中斷處理原則1:不能嵌套
官方資料:中斷處理不能嵌套
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc
中斷處理函數需要調用C函數,這就需要用到棧。
中斷A正在處理的過程中,假設又發生了中斷B,那麼在棧裏要保存A的現場,然後處理B。
在處理B的過程中又發生了中斷C,那麼在棧裏要保存B的現場,然後處理C。
如果中斷嵌套突然暴發,那麼棧將越來越大,棧終將耗盡。
所以,爲了防止這種情況發生,也是爲了簡單化中斷的處理,在Linux系統上中斷無法嵌套:即當前中斷A沒處理完之前,不會響應另一箇中斷B(即使它的優先級更高)。
03 中斷處理原則2:越快越好
媽媽在家中照顧小孩時,門鈴響起,她開門取快遞:這就是中斷的處理。她取個快遞敢花上半天嗎?不怕小孩出意外嗎?
同理,在Linux系統中,中斷的處理也是越快越好。
在單芯片系統中,假設中斷處理很慢,那應用程序在這段時間內就無法執行:系統顯得很遲頓。
在SMP系統中,假設中斷處理很慢,那麼正在處理這個中斷的CPU上的其他線程也無法執行。
在中斷的處理過程中,該CPU是不能進行進程調度的,所以中斷的處理要越快越好,儘早讓其他中斷能被處理──進程調度靠定時器中斷來實現。
在Linux系統中使用中斷是挺簡單的,爲某個中斷irq註冊中斷處理函數handler,可以使用request_irq函數:
在handler函數中,代碼儘可能高效。
但是,處理某個中斷要做的事情就是很多,沒辦法加快。比如對於按鍵中斷,我們需要等待幾十毫秒消除機械抖動。難道要在handler中等待嗎?對於計算機來說,這可是一個段很長的時間。
怎麼辦?
04 要處理的事情實在太多,拆分爲:上半部、下半部
當一箇中斷要耗費很多時間來處理時,它的壞處是:在這段時間內,其他中斷無法被處理。換句話說,在這段時間內,系統是關中斷的。
如果某個中斷就是要做那麼多事,我們能不能把它拆分成兩部分:緊急的、不緊急的?
在handler函數裏只做緊急的事,然後就重新開中斷,讓系統得以正常運行;那些不緊急的事,以後再處理,處理時是開中斷的。
中斷下半部的實現有很多種方法,講2種主要的:tasklet(小任務)、work queue(工作隊列)。
05 下半部要做的事情耗時不是太長:tasklet
假設我們把中斷分爲上半部、下半部。發生中斷時,上半部下半部的代碼何時、如何被調用?
當下半部比較耗時但是能忍受,並且它的處理比較簡單時,可以用tasklet來處理下半部。tasklet是使用軟件中斷來實現。
寫字太多,不如貼代碼,代碼一目瞭然:
使用流程圖簡化一下:
假設硬件中斷A的上半部函數爲irq_top_half_A,下半部爲irq_bottom_half_A。
使用情景化的分析,才能理解上述代碼的精華。
a. 硬件中斷A處理過程中,沒有其他中斷髮生:
一開始,preempt_count = 0;
上述流程圖①~⑨依次執行,上半部、下半部的代碼各執行一次。
b. 硬件中斷A處理過程中,又再次發生了中斷A:
一開始,preempt_count = 0;
執行到第⑥時,一開中斷後,中斷A又再次使得CPU跳到中斷向量表。
注意:
這時preempt_count等於1,並且中斷下半部的代碼並未執行。
CPU又從①開始再次執行中斷A的上半部代碼:
在第①步preempt_count等於2;
在第③步preempt_count等於1;
在第④步發現preempt_count等於1,所以直接結束當前第2次中斷的處理;
注意:
重點來了,第2次中斷髮生後,打斷了第一次中斷的第⑦步處理。當第2次中斷處理完畢,CPU會繼續去執行第⑦步。
可以看到,發生2次硬件中斷A時,它的上半部代碼執行了2次,但是下半部代碼只執行了一次。
所以,同一個中斷的上半部、下半部,在執行時是多對一的關係。
c. 硬件中斷A處理過程中,又再次發生了中斷B:
一開始,preempt_count = 0;
執行到第⑥時,一開中斷後,中斷B又再次使得CPU跳到中斷向量表。
注意:
這時preempt_count等於1,並且中斷A下半部的代碼並未執行。
CPU又從①開始再次執行中斷B的上半部代碼:
在第①步preempt_count等於2;
在第③步preempt_count等於1;
在第④步發現preempt_count等於1,所以直接結束當前第2次中斷的處理;
注意:
重點來了,第2次中斷髮生後,打斷了第一次中斷A的第⑦步處理。當第2次中斷B處理完畢,CPU會繼續去執行第⑦步。
在第⑦步裏,它會去執行中斷A的下半部,也會去執行中斷B的下半部。
所以,多箇中斷的下半部,是彙集在一起處理的。
總結:
a. 中斷的處理可以分爲上半部,下半部
b. 中斷上半部,用來處理緊急的事,它是在關中斷的狀態下執行的
c. 中斷下半部,用來處理耗時的、不那麼緊急的事,它是在開中斷的狀態下執行的
d. 中斷下半部執行時,有可能會被多次打斷,有可能會再次發生同一個中斷
e. 中斷上半部執行完後,觸發中斷下半部的處理
f. 中斷上半部、下半部的執行過程中,不能休眠:中斷休眠的話,以後誰來調度進程啊?
06 下半部要做的事情太多並且很複雜:工作隊列
在中斷下半部的執行過程中,雖然是開中斷的,期間可以處理各類中斷。但是畢竟整個中斷的處理還沒走完,這期間APP是無法執行的。
假設下半部要執行1、2分鐘,在這1、2分鐘裏APP都是無法響應的。
這誰受得了?
所以,如果中斷要做的事情實在太耗時,那就不能用中斷下半部來做,而應該用內核線程來做:在中斷上半部喚醒內核線程。內核線程和APP都一樣競爭執行,APP有機會執行,系統不會卡頓。
這個內核線程是系統幫我們創建的,一般是kworker線程,內核中有很多這樣的線程:
kworker線程要去“工作隊列”(work queue)上取出一個一個“工作”(work),來執行它裏面的函數。
那我們怎麼使用work、work queue呢?
a. 創建work:
你得先寫出一個函數,然後用這個函數填充一個work結構體。比如:
b. 要執行這個函數時,把work提交給work queue就可以了:
上述函數會把work提供給系統默認的work queue:system_wq,它是一個隊列。
c. 誰來執行work中的函數?
不用我們管,schedule_work函數不僅僅是把work放入隊列,還會把kworker線程喚醒。此線程搶到時間運行時,它就會從隊列中取出work,執行裏面的函數。
d. 誰把work提交給work queue?
在中斷場景中,可以在中斷上半部調用schedule_work函數。
總結:
a. 很耗時的中斷處理,應該放到線程裏去
b. 可以使用work、work queue
c. 在中斷上半部調用schedule_work函數,觸發work的處理
d. 既然是在線程中運行,那對應的函數可以休眠。
07 新技術:threaded irq
使用線程來處理中斷,並不是什麼新鮮事。使用work就可以實現,但是需要定義work、調用schedule_work,好麻煩啊。
太懶了太懶了,就這2步你們都不願意做。
好,內核是爲懶人服務的,再殺出一個函數:
你可以只提供thread_fn,系統會爲這個函數創建一個內核線程。發生中斷時,內核線程就會執行這個函數。
說你懶是開玩笑,內核開發者也不會那麼在乎懶人。
以前用work來線程化的處理內核,一個worker線程只能由一個CPU執行,多箇中斷的work都由同一個worker線程來處理,在單CPU系統中也只能忍着了。但是在SMP系統中,明明有那麼多CPU空着,你偏偏讓多箇中斷擠在這個CPU上?
新技術threaded irq,爲每一箇中斷都創建一個內核線程;多箇中斷的內核線程可以分配到多個CPU上執行,這提高了效率。
☆ END ☆
我是韋東山,10多年一直在研究linux+ARM,希望我的分享對你有幫助,歡迎進店訂閱我的付費內容.