【深度】韋東山:一文看看盡linux對中斷處理的前世今生

作者:韋東山

前言:

本文,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,希望我的分享對你有幫助,歡迎進店訂閱我的付費內容.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章