【深度】韋東山:一文看懂linux對(duì)中斷處理的前世今生


作者:韋東山
前言:
本文,4200字,研究代碼花了一天,寫出來花了一天;
錄視頻估計(jì)又得花半天;
真懷念以前簡(jiǎn)單粗暴的生活?。?/p>
拿起話筒就錄視頻,
先畫好圖?那是不需要的
文檔?那是不存在的
真是灑脫.....
現(xiàn)在,要寫文檔,又要畫流程圖,十幾、二十分鐘的視頻,
真是漚心瀝血做出來的,
各位,別浪費(fèi)了,歡迎享受
韋東山老師正在錄本文配套的視頻,明天發(fā)布。咱們先預(yù)習(xí)。
分為7點(diǎn):
Linux對(duì)中斷的擴(kuò)展:硬件中斷,軟件中斷
中斷處理原則1:不能嵌套
中斷處理原則2:越快越好
要處理的事情實(shí)在太多:拆分為:上半部,下半部
下半部的事情耗時(shí)不是太長(zhǎng):tasklet
下半部要做的事情太多并且很復(fù)雜:工作隊(duì)列
新技術(shù):threaded irq
從2005年我接觸Linux到現(xiàn)在15年了,Linux中斷系統(tǒng)的變化并不大。比較重要的就是引入了threaded irq:使用內(nèi)核線程來處理中斷。
Linux系統(tǒng)中有硬件中斷,也有軟件中斷。
對(duì)硬件中斷的處理有2個(gè)原則:不能嵌套,越快越好。
參考資料:
https://blog.csdn.net/myarrow/article/details/9287169
01?Linux對(duì)中斷的擴(kuò)展:硬件中斷、軟件中斷
Linux系統(tǒng)把中斷的意義擴(kuò)展了,對(duì)于按鍵中斷等硬件產(chǎn)生的中斷,稱之為“硬件中斷”(hard irq)。每個(gè)硬件中斷都有對(duì)應(yīng)的處理函數(shù),比如按鍵中斷、網(wǎng)卡中斷的處理函數(shù)肯定不一樣。
為方便理解,你可以先認(rèn)為對(duì)硬件中斷的處理是用數(shù)組來實(shí)現(xiàn)的,數(shù)組里存放的是函數(shù)指針:

注意:上圖是簡(jiǎn)化的,Linux中這個(gè)數(shù)組復(fù)雜多了。
當(dāng)發(fā)生A中斷時(shí),對(duì)應(yīng)的irq_function_A函數(shù)被調(diào)用。硬件導(dǎo)致該函數(shù)被調(diào)用。
相對(duì)的,還可以人為地制造中斷:軟件中斷(soft irq),如下圖所示:

注意:上圖是簡(jiǎn)化的,Linux中這個(gè)數(shù)組復(fù)雜多了。
問題來了:
a. 軟件中斷何時(shí)生產(chǎn)?
由軟件決定,對(duì)于X號(hào)軟件中斷,只需要把它的flag設(shè)置為1就表示發(fā)生了該中斷。
b. 軟件中斷何時(shí)處理?
軟件中斷嘛,并不是那么十萬火急,有空再處理它好了。
什么時(shí)候有空?不能讓它一直等吧?
Linux系統(tǒng)中,各種硬件中斷頻繁發(fā)生,至少定時(shí)器中斷每10ms發(fā)生一次,那取個(gè)巧?
在處理寫硬件中斷后,再去處理軟件中斷?就這么辦!
有哪些軟件中斷?
查內(nèi)核源碼include/linux/interrupt.h

怎么觸發(fā)軟件中斷?最核心的函數(shù)是raise_softirq,簡(jiǎn)單地理解就是設(shè)置softirq_veq[nr]的標(biāo)記位:

怎么設(shè)置軟件中斷的處理函數(shù):

后面講到的中斷下半部tasklet就是使用軟件中斷實(shí)現(xiàn)的。
02?中斷處理原則1:不能嵌套
官方資料:中斷處理不能嵌套
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc
中斷處理函數(shù)需要調(diào)用C函數(shù),這就需要用到棧。
中斷A正在處理的過程中,假設(shè)又發(fā)生了中斷B,那么在棧里要保存A的現(xiàn)場(chǎng),然后處理B。
在處理B的過程中又發(fā)生了中斷C,那么在棧里要保存B的現(xiàn)場(chǎng),然后處理C。
如果中斷嵌套突然暴發(fā),那么棧將越來越大,棧終將耗盡。
所以,為了防止這種情況發(fā)生,也是為了簡(jiǎn)單化中斷的處理,在Linux系統(tǒng)上中斷無法嵌套:即當(dāng)前中斷A沒處理完之前,不會(huì)響應(yīng)另一個(gè)中斷B(即使它的優(yōu)先級(jí)更高)。
03?中斷處理原則2:越快越好
媽媽在家中照顧小孩時(shí),門鈴響起,她開門取快遞:這就是中斷的處理。她取個(gè)快遞敢花上半天嗎?不怕小孩出意外嗎?
同理,在Linux系統(tǒng)中,中斷的處理也是越快越好。
在單芯片系統(tǒng)中,假設(shè)中斷處理很慢,那應(yīng)用程序在這段時(shí)間內(nèi)就無法執(zhí)行:系統(tǒng)顯得很遲頓。
在SMP系統(tǒng)中,假設(shè)中斷處理很慢,那么正在處理這個(gè)中斷的CPU上的其他線程也無法執(zhí)行。
在中斷的處理過程中,該CPU是不能進(jìn)行進(jìn)程調(diào)度的,所以中斷的處理要越快越好,盡早讓其他中斷能被處理──進(jìn)程調(diào)度靠定時(shí)器中斷來實(shí)現(xiàn)。
在Linux系統(tǒng)中使用中斷是挺簡(jiǎn)單的,為某個(gè)中斷irq注冊(cè)中斷處理函數(shù)handler,可以使用request_irq函數(shù):

在handler函數(shù)中,代碼盡可能高效。
但是,處理某個(gè)中斷要做的事情就是很多,沒辦法加快。比如對(duì)于按鍵中斷,我們需要等待幾十毫秒消除機(jī)械抖動(dòng)。難道要在handler中等待嗎?對(duì)于計(jì)算機(jī)來說,這可是一個(gè)段很長(zhǎng)的時(shí)間。
怎么辦?
04?要處理的事情實(shí)在太多,拆分為:上半部、下半部
當(dāng)一個(gè)中斷要耗費(fèi)很多時(shí)間來處理時(shí),它的壞處是:在這段時(shí)間內(nèi),其他中斷無法被處理。換句話說,在這段時(shí)間內(nèi),系統(tǒng)是關(guān)中斷的。
如果某個(gè)中斷就是要做那么多事,我們能不能把它拆分成兩部分:緊急的、不緊急的?
在handler函數(shù)里只做緊急的事,然后就重新開中斷,讓系統(tǒng)得以正常運(yùn)行;那些不緊急的事,以后再處理,處理時(shí)是開中斷的。

中斷下半部的實(shí)現(xiàn)有很多種方法,講2種主要的:tasklet(小任務(wù))、work queue(工作隊(duì)列)。
05?下半部要做的事情耗時(shí)不是太長(zhǎng):tasklet
假設(shè)我們把中斷分為上半部、下半部。發(fā)生中斷時(shí),上半部下半部的代碼何時(shí)、如何被調(diào)用?
當(dāng)下半部比較耗時(shí)但是能忍受,并且它的處理比較簡(jiǎn)單時(shí),可以用tasklet來處理下半部。tasklet是使用軟件中斷來實(shí)現(xiàn)。

寫字太多,不如貼代碼,代碼一目了然:

使用流程圖簡(jiǎn)化一下:

假設(shè)硬件中斷A的上半部函數(shù)為irq_top_half_A,下半部為irq_bottom_half_A。
使用情景化的分析,才能理解上述代碼的精華。
a. 硬件中斷A處理過程中,沒有其他中斷發(fā)生:
一開始,preempt_count = 0;
上述流程圖①~⑨依次執(zhí)行,上半部、下半部的代碼各執(zhí)行一次。
b. 硬件中斷A處理過程中,又再次發(fā)生了中斷A:
一開始,preempt_count = 0;
執(zhí)行到第⑥時(shí),一開中斷后,中斷A又再次使得CPU跳到中斷向量表。
注意:
這時(shí)preempt_count等于1,并且中斷下半部的代碼并未執(zhí)行。
CPU又從①開始再次執(zhí)行中斷A的上半部代碼:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步發(fā)現(xiàn)preempt_count等于1,所以直接結(jié)束當(dāng)前第2次中斷的處理;
注意:
重點(diǎn)來了,第2次中斷發(fā)生后,打斷了第一次中斷的第⑦步處理。當(dāng)?shù)?次中斷處理完畢,CPU會(huì)繼續(xù)去執(zhí)行第⑦步。
可以看到,發(fā)生2次硬件中斷A時(shí),它的上半部代碼執(zhí)行了2次,但是下半部代碼只執(zhí)行了一次。
所以,同一個(gè)中斷的上半部、下半部,在執(zhí)行時(shí)是多對(duì)一的關(guān)系。
c. 硬件中斷A處理過程中,又再次發(fā)生了中斷B:
一開始,preempt_count = 0;
執(zhí)行到第⑥時(shí),一開中斷后,中斷B又再次使得CPU跳到中斷向量表。
注意:
這時(shí)preempt_count等于1,并且中斷A下半部的代碼并未執(zhí)行。
CPU又從①開始再次執(zhí)行中斷B的上半部代碼:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步發(fā)現(xiàn)preempt_count等于1,所以直接結(jié)束當(dāng)前第2次中斷的處理;
注意:
重點(diǎn)來了,第2次中斷發(fā)生后,打斷了第一次中斷A的第⑦步處理。當(dāng)?shù)?次中斷B處理完畢,CPU會(huì)繼續(xù)去執(zhí)行第⑦步。
在第⑦步里,它會(huì)去執(zhí)行中斷A的下半部,也會(huì)去執(zhí)行中斷B的下半部。
所以,多個(gè)中斷的下半部,是匯集在一起處理的。
總結(jié):
a. 中斷的處理可以分為上半部,下半部
b. 中斷上半部,用來處理緊急的事,它是在關(guān)中斷的狀態(tài)下執(zhí)行的
c. 中斷下半部,用來處理耗時(shí)的、不那么緊急的事,它是在開中斷的狀態(tài)下執(zhí)行的
d. 中斷下半部執(zhí)行時(shí),有可能會(huì)被多次打斷,有可能會(huì)再次發(fā)生同一個(gè)中斷
e. 中斷上半部執(zhí)行完后,觸發(fā)中斷下半部的處理
f. 中斷上半部、下半部的執(zhí)行過程中,不能休眠:中斷休眠的話,以后誰來調(diào)度進(jìn)程???
06?下半部要做的事情太多并且很復(fù)雜:工作隊(duì)列
在中斷下半部的執(zhí)行過程中,雖然是開中斷的,期間可以處理各類中斷。但是畢竟整個(gè)中斷的處理還沒走完,這期間APP是無法執(zhí)行的。
假設(shè)下半部要執(zhí)行1、2分鐘,在這1、2分鐘里APP都是無法響應(yīng)的。
這誰受得了?
所以,如果中斷要做的事情實(shí)在太耗時(shí),那就不能用中斷下半部來做,而應(yīng)該用內(nèi)核線程來做:在中斷上半部喚醒內(nèi)核線程。內(nèi)核線程和APP都一樣競(jìng)爭(zhēng)執(zhí)行,APP有機(jī)會(huì)執(zhí)行,系統(tǒng)不會(huì)卡頓。
這個(gè)內(nèi)核線程是系統(tǒng)幫我們創(chuàng)建的,一般是kworker線程,內(nèi)核中有很多這樣的線程:

kworker線程要去“工作隊(duì)列”(work queue)上取出一個(gè)一個(gè)“工作”(work),來執(zhí)行它里面的函數(shù)。
那我們?cè)趺词褂脀ork、work queue呢?
a. 創(chuàng)建work:
你得先寫出一個(gè)函數(shù),然后用這個(gè)函數(shù)填充一個(gè)work結(jié)構(gòu)體。比如:

b. 要執(zhí)行這個(gè)函數(shù)時(shí),把work提交給work queue就可以了:

c. 誰來執(zhí)行work中的函數(shù)?
不用我們管,schedule_work函數(shù)不僅僅是把work放入隊(duì)列,還會(huì)把kworker線程喚醒。此線程搶到時(shí)間運(yùn)行時(shí),它就會(huì)從隊(duì)列中取出work,執(zhí)行里面的函數(shù)。
d. 誰把work提交給work queue?
在中斷場(chǎng)景中,可以在中斷上半部調(diào)用schedule_work函數(shù)。
總結(jié):
a. 很耗時(shí)的中斷處理,應(yīng)該放到線程里去
b. 可以使用work、work queue
c. 在中斷上半部調(diào)用schedule_work函數(shù),觸發(fā)work的處理
d. 既然是在線程中運(yùn)行,那對(duì)應(yīng)的函數(shù)可以休眠。
07?新技術(shù):threaded irq
使用線程來處理中斷,并不是什么新鮮事。使用work就可以實(shí)現(xiàn),但是需要定義work、調(diào)用schedule_work,好麻煩啊。
太懶了太懶了,就這2步你們都不愿意做。
好,內(nèi)核是為懶人服務(wù)的,再殺出一個(gè)函數(shù):

你可以只提供thread_fn,系統(tǒng)會(huì)為這個(gè)函數(shù)創(chuàng)建一個(gè)內(nèi)核線程。發(fā)生中斷時(shí),內(nèi)核線程就會(huì)執(zhí)行這個(gè)函數(shù)。
說你懶是開玩笑,內(nèi)核開發(fā)者也不會(huì)那么在乎懶人。
以前用work來線程化的處理內(nèi)核,一個(gè)worker線程只能由一個(gè)CPU執(zhí)行,多個(gè)中斷的work都由同一個(gè)worker線程來處理,在單CPU系統(tǒng)中也只能忍著了。但是在SMP系統(tǒng)中,明明有那么多CPU空著,你偏偏讓多個(gè)中斷擠在這個(gè)CPU上?
新技術(shù)threaded irq,為每一個(gè)中斷都創(chuàng)建一個(gè)內(nèi)核線程;多個(gè)中斷的內(nèi)核線程可以分配到多個(gè)CPU上執(zhí)行,這提高了效率。
☆ END ☆
我是韋東山,10多年一直在研究linux+ARM,希望我的分享對(duì)你有幫助,歡迎進(jìn)店訂閱我的付費(fèi)內(nèi)容:100ask.taobao.com
聯(lián)系我們:
如果您沒有沒懂沒關(guān)系,可以加韋東山官方微信群進(jìn)行討論。
先加管理員微信:13163769879,若購(gòu)買產(chǎn)品,拉您到對(duì)應(yīng)的售后群,否則拉入粉絲群。