Linux中斷機制之三:中斷的執行

NO IMAGE

在核心程式碼中,對X86平臺中斷執行的基本過程是:
1、 通過IDT中的中斷描述符,呼叫common_interrupt;
2、 通過common_interrupt,呼叫do_IRQ,完成vector到irq_desc的轉換,進入Generic interrupt layer(呼叫處理函式generic_handle_irq_desc);
3、 呼叫在中斷初始化的時候,按照中斷特性(level觸發,edge觸發等、simple等)初始化的irq_desc:: handle_irq,執行不同的通用處理介面,比如handle_simple_irq;
4、 這些通用處理介面會呼叫中斷初始化的時候註冊的外部中斷處理函式;完成EOI等硬體相關操作;並完成中斷處理的相關控制。

common_interrupt

按照之前CPU執行中斷過程的描述,X86 CPU在準備好了中斷執行環境後,會呼叫中斷描述符定義的中斷處理入口;根據中斷相關初始化過程我們知道,對於使用者自定義中斷,中斷處理入口都是(對系統預留的,就直接執行定義的介面了):

pushq_cfi $(~vector 0x80)
jmp 2f (執行jmp common_interrupt)

就是在把vector入棧後,執行common_interrupt,common_interrupt在entry_64.S中定義,其中關鍵步驟為:呼叫do_IRQ,完成後會根據環境判斷是否需要執行排程,最後執行iretq指令完成中斷處理,iret指令的重要功能就是回覆中斷函式前的EFLAGS(執行中斷入口前被入棧儲存,並清零IF位關中斷),並恢復執行被中斷的程式(這裡不一定會恢復到之前的執行環境,可能執行軟中斷處理,或者執行排程)。

do_IRQ

do_IRQ的基本處理過程如下,其負責中斷執行環境建立、vector到irq的轉換等

do_IRQ
irq_enter
tick_check_idle 如果需要,呼叫該函式更新時鐘
__irq_enter
關鍵為add_preempt_count(HARDIRQ_OFFSET),表明當前系統在處理硬中斷
exit_idle主要是對外通告退出idle狀態    
根據vector從vector_irq中獲取irq號
handle_irq
desc = irq_to_desc(irq);根據irq號獲取irq_desc結構
generic_handle_irq_desc呼叫Generic interrupt layer完成中斷處理
irq_exit
sub_preempt_count(IRQ_EXIT_OFFSET)表明系統已經沒有處理硬中斷
invoke_softirq 如果發起軟中斷的處理

Generic interrupt layer

該層負責的是平臺無關/裝置無關的中斷通用邏輯,對這部分,在《Linux generic IRQ handling》中有詳細描述。其負責完成中斷處理的介面是generic_handle_irq_desc,該介面會執行irq_desc::handle_irq; Generic interrupt layer根據中斷特性的不同,把中斷分成幾類,包括:level type(handle_level_irq)、edge type(handle_edge_irq)、simple type(handle_simple_irq)等,這些中斷型別對應的處理函式是都在kernel/irq/chip.c中定義,併入前面的描述,在相關中斷初始化的時候,被賦值給irq_desc::handle_irq;對於PCI裝置,只用了兩種,level type(INT#模式)、edge type(MSI/MSI-X模式)。

edge 觸發中斷的基本處理過程:

電壓跳變觸發中斷===>中斷控制器接收中斷,記IRR暫存器===>中斷控制器置ISR暫存器===>CPU遮蔽本CPU中斷===>CPU處理中斷,發出EOI===>中斷控制器確認可以處理下一次中斷===>ISR清中斷源,電壓歸位===>中斷源可以發起下一次中斷===>CPU中斷處理完成,執行完現場處理後執行IRET,不再遮蔽本CPU中斷。
edge觸發的特點:
a) 中斷不會丟
如果中斷觸發時中斷被遮蔽,那麼中斷控制器會記錄下該中斷,在遮蔽取消的時候會再執行。
b) edge觸發的缺點是完成共享不方便:
比如A和B兩個中斷源共享一箇中斷,每次ISR先檢查A再檢查B,如果B先發生中斷,在ISR檢查完A,檢查B的過程中,A發生中斷。那麼在ISR處理開始的時候,A會告訴ISR,不是它乾的,然後ISR處理B的中斷,完成後通過清理中斷源把B的電壓歸位,但是由於A的中斷沒有得到處理,電壓沒有歸位,這個共享的中斷就不能得到再次觸發了。
edge觸發對應的通用邏輯介面

handle_edge_irq
if(如果已經有CPU在處理該中斷)
置irq_desc狀態為PENDING,讓當前處理該中斷的CPU處理完成後幫忙處理
mask_ack_irq關閉(mask)並ACK(對MSI就是傳送EOI)該中斷,為什麼必須這樣做?是因為後續中斷的工作本身會在pending的中斷處理時被執行?
END
desc->irq_data.chip->irq_ack ACK該中斷,chip為msi_chip
ack_apic_edge
irq_complete_move 處理irq在CPU之間遷移的情況
irq_move_irq 處理irq在CPU之間遷移的情況
ack_APIC_irq ACK該中斷,通知APIC可以觸發下一次中斷了
do{
如果是幫其它CPU處理PENGDING的中斷,需要unmask該中斷,因為該中斷已經開始處理,系統能夠接受下一個了
handle_irq_event
}while(PENGDING標誌位被設定)如果本CPU上次處理的時候,其它CPU上有中斷,其它CPU上的只是設定了PENGDING,本CPU負責處理。

level 觸發:

這種模式下,外設通過把電壓保持到某個門限值來完成觸發中斷,在處理完成(EOI)後,如果電壓還在門限值,就會再次觸發中斷的執行。
level觸發的特點:
a) 方便中斷共享
b) 對中斷觸發時中斷被遮蔽的情況,如果中斷遮蔽解除後仍然引腳電壓仍然在門限值,就執行該中斷的ISR,否則不執行。
需要說明的是:對於使用local APIC的系統,level觸發和edge觸發需要配置local APIC的Local Vector Table。
4、 level觸發對應的通用邏輯介面

handle_level_irq
mask_ack_irq 關閉(mask)並ACK(對MSI就是傳送EOI)該中斷
如果已經有CPU在處理該中斷,則退出執行
handle_irq_event處理該中斷
cond_unmask_irq使能該中斷

level觸發和edge觸發在通用邏輯層最大的不同就是當其他CPU正在處理該中斷的時候,系統的行為,對edge觸發,會把該中斷記錄下來,當前處理結束後再次執行,而level直接退出。產生這種差異的原因是:level觸發不怕丟?

無論是那種觸發方式,都會呼叫handle_irq_event處理中斷,該函式中會遍歷irq_desc::action連結串列,執行action->handler,也就是驅動在中斷初始化的時候,通過request_irq註冊的中斷處理介面。

總結

中斷的使能狀態

1、 在local APIC層次(當前CPU),一箇中斷正在處理的時候,不會有相同的中斷或者優先順序低於該中斷的其它中斷來打斷當前中斷的執行;但是高優先順序中斷可以打斷低優先順序中斷。
2、 在X86 CPU層次(當前CPU),從中斷執行開始到IRET,IF位都被清零,也就是隻有不可遮蔽中斷能夠打斷當前中斷的執行。
3、 在Generic interrupt layer層次,如果一箇中斷已經在系統中執行,會阻止該中斷在其它CPU上的執行。
4、 在外設/驅動中斷處理函式層次往往也有中斷使能的功能,比如啟用了NAPI的網絡卡,在中斷處理函式開始執行的時候,往往會通過硬體功能關閉該中斷,要在對應的軟中斷完成處理後才通過硬體功能使能該中斷。
注:NMI中斷雖然稱為不可遮蔽中斷,也有一個例外:NMI中斷執行過程中,該CPU遮蔽了後來的NMI中斷。

中斷的執行CPU

通過中斷初始化過程我們知道:中斷在那個CPU上執行,取決於在那個CPU上申請了vector並配置了對應的中斷控制器(比如local APIC)。如果想要改變一箇中斷的執行CPU,必須重新申請vector並配置中斷控制器。一般通過echo xxx > /proc/irq/xxx/affinity來完成調整,同時irq_balance一類軟體可以用於完成中斷的均衡。