轉載:http://blog.csdn.net/sailor_8318/archive/2008/07/09/2627136.aspx
深入剖析Linux中斷機制之三
--Linux對異常和中斷的處理
【摘要】本文詳解了Linux內核的中斷實現機制。首先介紹了中斷的一些基本概念,然後分析了面向對象的Linux中斷的組織形式、三種主要數據結構及其之間的關係。隨後介紹了Linux處理異常和中斷的基本流程,在此基礎上分析了中斷處理的詳細流程,包括保存現場、中斷處理、中斷退出時的軟中斷執行及中斷返回時的進程切換等問題。最後介紹了中斷相關的API,包括中斷註冊和釋放、中斷關閉和使能、如何編寫中斷ISR、共享中斷、中斷上下文中斷狀態等。
【關鍵字】中斷,異常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,軟中斷,進程切換,中斷註冊釋放request_irq,free_irq,共享中斷,可重入,中斷上下文
1 Linux對異常和中斷的處理
1.1 異常處理
Linux利用異常來達到兩個截然不同的目的:
² 給進程發送一個信號以通報一個反常情況
² 管理硬件資源
對於第一種情況,例如,如果進程執行了一個被0除的操作,CPU則會產生一個“除法錯誤”異常,並由相應的異常處理程序向當前進程發送一個SIGFPE信號。當前進程接收到這個信號後,就要採取若干必要的步驟,或者從錯誤中恢復,或者終止執行(如果這個信號沒有相應的信號處理程序)。
內核對異常處理程序的調用有一個標準的結構,它由以下三部分組成:
² 在內核棧中保存大多數寄存器的內容(由彙編語言實現)
² 調用C編寫的異常處理函數
² 通過ret_from_exception()函數從異常退出。
1.2 中斷處理
當一箇中斷髮生時,並不是所有的操作都具有相同的急迫性。事實上,把所有的操作都放進中斷處理程序本身並不合適。需要時間長的、非重要的操作應該推後,因爲當一箇中斷處理程序正在運行時,相應的IRQ中斷線上再發出的信號就會被忽略。另外中斷處理程序不能執行任何阻塞過程,如I/O設備操作。因此,Linux把一箇中斷要執行的操作分爲下面的三類:
² 緊急的(Critical)
這樣的操作諸如:中斷到來時中斷控制器做出應答,對中斷控制器或設備控制器重新編程,或者對設備和處理器同時訪問的數據結構進行修改。這些操作都是緊急的,應該被很快地執行,也就是說,緊急操作應該在一箇中斷處理程序內立即執行,而且是在禁用中斷的狀態下。
² 非緊急的(Noncritical)
這樣的操作如修改那些只有處理器纔會訪問的數據結構(例如,按下一個鍵後,讀掃描碼)。這些操作也要很快地完成,因此,它們由中斷處理程序立即執行,但在啓用中斷的狀態下。
² 非緊急可延遲的(Noncritical deferrable)
這樣的操作如,把一個緩衝區的內容拷貝到一些進程的地址空間(例如,把鍵盤行緩衝區的內容發送到終端處理程序的進程)。這些操作可能被延遲較長的時間間隔而不影響內核操作,有興趣的進程會等待需要的數據。
所有的中斷處理程序都執行四個基本的操作:
² 在內核棧中保存IRQ的值和寄存器的內容。
² 給與IRQ中斷線相連的中斷控制器發送一個應答,這將允許在這條中斷線上進一步發出中斷請求。
² 執行共享這個IRQ的所有設備的中斷服務例程(ISR)。
² 跳到ret_to_usr( )的地址後終止。
1.3 中斷處理程序的執行流程
1.3.1 流程概述
現在,我們可以從中斷請求的發生到CPU的響應,再到中斷處理程序的調用和返回,沿着這一思路走一遍,以體會Linux內核對中斷的響應及處理。
假定外設的驅動程序都已完成了初始化工作,並且已把相應的中斷服務例程掛入到特定的中斷請求隊列。又假定當前進程正在用戶空間運行(隨時可以接受中斷),且外設已產生了一次中斷請求,CPU就在執行完當前指令後來響應該中斷。
中斷處理系統在Linux中的實現是非常依賴於體系結構的,實現依賴於處理器、所使用的中斷控制器的類型、體系結構的設計及機器本身。
設備產生中斷,通過總線把電信號發送給中斷控制器。如果中斷線是激活的,那麼中斷控制器就會把中斷髮往處理器。在大多數體系結構中,這個工作就是通過電信號給處理器的特定管腳發送一個信號。除非在處理器上禁止該中斷,否則,處理器會立即停止它正在做的事,關閉中斷系統,然後跳到內存中預定義的位置開始執行那裏的代碼。這個預定義的位置是由內核設置的,是中斷處理程序的入口點。
對於ARM系統來說,有個專用的IRQ運行模式,有一個統一的入口地址。假定中斷髮生時CPU運行在用戶空間,而中斷處理程序屬於內核空間,因此,要進行堆棧的切換。也就是說,CPU從TSS中取出內核棧指針,並切換到內核棧(此時棧還爲空)。
若當前處於內核空間時,對於ARM系統來說是處於SVC模式,此時產生中斷,中斷處理完畢後,若是可剝奪內核,則檢查是否需要進行進程調度,否則直接返回到被中斷的內核空間;若需要進行進程調度,則svc_preempt,進程切換。
190 .align 5
191__irq_svc:
192 svc_entry
197#ifdef CONFIG_PREEMPT
198 get_thread_info tsk
199 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
200 add r7, r8, #1 @ increment it
201 str r7, [tsk, #TI_PREEMPT]
202#endif
204 irq_handler
205#ifdef CONFIG_PREEMPT
206 ldr r0, [tsk, #TI_FLAGS] @ get flags
207 tst r0, #_TIF_NEED_RESCHED
208 blne svc_preempt
209preempt_return:
210 ldr r0, [tsk, #TI_PREEMPT] @ read preempt value
211 str r8, [tsk, #TI_PREEMPT] @ restore preempt count
212 teq r0, r7
213 strne r0, [r0, -r0] @ bug()
214#endif
215 ldr r0, [sp, #S_PSR] @ irqs are already disabled
216 msr spsr_cxsf, r0
221 ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
223 .ltorg
當前處於用戶空間時,對於ARM系統來說是處於USR模式,此時產生中斷,中斷處理完畢後,無論是否是可剝奪內核,都調轉到統一的用戶模式出口ret_to_user,其檢查是否需要進行進程調度,若需要進行進程調度,則進程切換,否則直接返回到被中斷的用戶空間。
404 .align 5
405__irq_usr:
406 usr_entry
411 get_thread_info tsk
412#ifdef CONFIG_PREEMPT
413 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
414 add r7, r8, #1 @ increment it
415 str r7, [tsk, #TI_PREEMPT]
416#endif
418 irq_handler
419#ifdef CONFIG_PREEMPT
420 ldr r0, [tsk, #TI_PREEMPT]
421 str r8, [tsk, #TI_PREEMPT]
422 teq r0, r7
423 strne r0, [r0, -r0] @ bug()
424#endif
429 mov why, #0
430 b ret_to_user
432 .ltorg
1.3.2 保存現場
105/*
106 * SVC mode handlers
107 */
115 .macro svc_entry
116 sub sp, sp, #S_FRAME_SIZE
117 SPFIX( tst sp, #4 )
118 SPFIX( bicne sp, sp, #4 )
119 stmib sp, {r1 - r12}
121 ldmia r0, {r1 - r3}
122 add r5, sp, #S_SP @ here for interlock avoidance
123 mov r4, #-1 @ "" "" "" ""
124 add r0, sp, #S_FRAME_SIZE @ "" "" "" ""
125 SPFIX( addne r0, r0, #4 )
126 str r1, [sp] @ save the "real" r0 copied
127 @ from the exception stack
129 mov r1, lr
131 @
132 @ We are now ready to fill in the remaining blanks on the stack:
133 @
134 @ r0 - sp_svc
135 @ r1 - lr_svc
136 @ r2 - lr_<exception>, already fixed up for correct return/restart
137 @ r3 - spsr_<exception>
138 @ r4 - orig_r0 (see pt_regs definition in ptrace.h)
139 @
140 stmia r5, {r0 - r4}
141 .endm
1.3.3 中斷處理
因爲C的調用慣例是要把函數參數放在棧的頂部,因此pt- regs結構包含原始寄存器的值,這些值是以前在彙編入口例程svc_entry中保存在棧中的。
linux+v2.6.19/include/asm-arm/arch-at91rm9200/entry-macro.S
18 .macro get_irqnr_and_base, irqnr, irqstat, base, tmp
19 ldr /base, =(AT91_VA_BASE_SYS) @ base virtual address of SYS peripherals
20 ldr /irqnr, [/base, #AT91_AIC_IVR] @ read IRQ vector register: de-asserts nIRQ to processor (and clears interrupt)
21 ldr /irqstat, [/base, #AT91_AIC_ISR] @ read interrupt source number
22 teq /irqstat, #0 @ ISR is 0 when no current interrupt, or spurious interrupt
23 streq /tmp, [/base, #AT91_AIC_EOICR] @ not going to be handled further, then ACK it now.
24 .endm
26/*
27 * Interrupt handling. Preserves r7, r8, r9
28 */
29 .macro irq_handler
301: get_irqnr_and_base r0, r6, r5, lr
31 movne r1, sp
32 @
33 @ routine called with r0 = irq number, r1 = struct pt_regs *
34 @
35 adrne lr, 1b
36 bne asm_do_IRQ
58 .endm
中斷號的值也在irq_handler初期得以保存,所以,asm_do_IRQ可以將它提取出來。這個中斷處理程序實際上要調用do_IRQ(),而do_IRQ()要調用handle_IRQ_event()函數,最後這個函數才真正地執行中斷服務例程(ISR)。下圖給出它們的調用關係:
|
|
|
|
|
中斷處理函數的調用關係
1.3.3.1 asm_do_IRQ
112asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
113{
114 struct pt_regs *old_regs = set_irq_regs(regs);
115 struct irqdesc *desc = irq_desc + irq;
122desc = &bad_irq_desc;
124irq_enter(); //記錄硬件中斷狀態,便於跟蹤中斷情況確定是否是中斷上下文
126 desc_handle_irq(irq, desc);
///////////////////desc_handle_irq
33static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
34{
35desc->handle_irq(irq, desc); //通常handle_irq指向__do_IRQ
36}
///////////////////desc_handle_irq
131irq_exit(); //中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套
133}
1.3.3.2 __do_IRQ
157 * __do_IRQ - original all in one highlevel IRQ handler
167fastcall unsigned int __do_IRQ(unsigned int irq)
168{
169 struct irq_desc *desc = irq_desc + irq;
173kstat_this_cpu.irqs[irq]++;
188 if (desc->chip->ack) //首先響應中斷,通常實現爲關閉本中斷線
194status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
195status |= IRQ_PENDING; /* we _want_ to handle it */
202 if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
204 status &= ~IRQ_PENDING; /* we commit to handling */
205status |= IRQ_INPROGRESS; /* we are handling it */
206 }
218 /*
219 * Edge triggered interrupts need to remember
220 * pending events.
227 */
228 for (;;) {
231 spin_unlock(&desc->lock);//解鎖,中斷處理期間可以響應其他中斷,否則再次進入__do_IRQ時會死鎖
233 action_ret = handle_IRQ_event(irq, action);
238 if (likely(!(desc->status & IRQ_PENDING)))
239 break;
240 desc->status &= ~IRQ_PENDING;
241 }
242desc->status &= ~IRQ_INPROGRESS;
250spin_unlock(&desc->lock);
252 return 1;
253}
該函數的實現用到中斷線的狀態,下面給予具體說明:
#define IRQ_INPROGRESS 1 /* 正在執行這個IRQ的一個處理程序*/
#define IRQ_DISABLED 2 /* 由設備驅動程序已經禁用了這條IRQ中斷線 */
#define IRQ_PENDING 4 /* 一個IRQ已經出現在中斷線上,且被應答,但還沒有爲它提供服務 */
#define IRQ_REPLAY 8 /* 當Linux重新發送一個已被刪除的IRQ時 */
#define IRQ_WAITING 32 /*當對硬件設備進行探測時,設置這個狀態以標記正在被測試的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
這8個狀態的前5個狀態比較常用,因此我們給出了具體解釋。
經驗表明,應該避免在同一條中斷線上的中斷嵌套,內核通過IRQ_PENDING標誌位的應用保證了這一點。當do_IRQ()執行到for (;;)循環時,desc->status 中的IRQ_PENDING的標誌位肯定爲0。當CPU執行完handle_IRQ_event()函數返回時,如果這個標誌位仍然爲0,那麼循環就此結束。如果這個標誌位變爲1,那就說明這條中斷線上又有中斷產生(對單CPU而言),所以循環又執行一次。通過這種循環方式,就把可能發生在同一中斷線上的嵌套循環化解爲“串行”。
在循環結束後調用desc->handler->end()函數,具體來說,如果沒有設置IRQ_DISABLED標誌位,就啓用這條中斷線。
1.3.3.3 handle_IRQ_event
當執行到for (;;)這個無限循環時,就準備對中斷請求隊列進行處理,這是由handle_IRQ_event()函數完成的。因爲中斷請求隊列爲一臨界資源,因此在進入這個函數前要加鎖。
handle_IRQ_event執行所有的irqaction鏈表:
130irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
131{
132irqreturn_t ret, retval = IRQ_NONE;
135handle_dynamic_tick(action);
136 // 如果沒有設置IRQF_DISABLED,則中斷處理過程中,打開中斷
137 if (!(action->flags & IRQF_DISABLED))
138local_irq_enable_in_hardirq();
140 do {
141ret = action->handler(irq, action->dev_id);
142 if (ret == IRQ_HANDLED)
153}
這個循環依次調用請求隊列中的每個中斷服務例程。這裏要說明的是,如果設置了IRQF_DISABLED,則中斷服務例程在關中斷的條件下進行(不包括非屏蔽中斷),但通常CPU在穿過中斷門時自動關閉中斷。但是,關中斷時間絕不能太長,否則就可能丟失其它重要的中斷。也就是說,中斷服務例程應該處理最緊急的事情,而把剩下的事情交給另外一部分來處理。即後半部分(bottom half)來處理,這一部分內容將在下一節進行討論。
不同的CPU不允許併發地進入同一中斷服務例程,否則,那就要求所有的中斷服務例程必須是“可重入”的純代碼。可重入代碼的設計和實現就複雜多了,因此,Linux在設計內核時巧妙地“避難就易”,以解決問題爲主要目標。
1.3.3.4 irq_exit()
中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套
////////////////////////////////////////////////////////////
linux+v2.6.19/kernel/softirq.c
286{
287account_system_vtime(current);
289sub_preempt_count(IRQ_EXIT_OFFSET);
290 if (!in_interrupt() && local_softirq_pending())
////////////
276#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
277# defineinvoke_softirq() __do_softirq()
278#else
279# defineinvoke_softirq() do_softirq()
280#endif
////////////
292preempt_enable_no_resched();
293}
////////////////////////////////////////////////////////////
1.3.4 從中斷返回
asm_do_IRQ()這個函數處理所有外設的中斷請求後就要返回。返回情況取決於中斷前程序是內核態還是用戶態以及是否是可剝奪內核。
² 內核態可剝奪內核,只有在preempt_count爲0時,schedule()纔會被調用,其檢查是否需要進行進程切換,需要的話就切換。在schedule()返回之後,或者如果沒有掛起的工作,那麼原來的寄存器被恢復,內核恢復到被中斷的內核代碼。
² 內核態不可剝奪內核,則直接返回至被中斷的內核代碼。
² 中斷前處於用戶態時,無論是否是可剝奪內核,統一跳轉到ret_to_user。
雖然我們這裏討論的是中斷的返回,但實際上中斷、異常及系統調用的返回是放在一起實現的,因此,我們常常以函數的形式提到下面這三個入口點:
ret_to_user()
終止中斷處理程序
ret_slow_syscall ( ) 或者ret_fast_syscall
終止系統調用,即由0x80引起的異常
ret_from_exception( )
終止除了0x80的所有異常
565/*
566 * This is the return code to user mode for abort handlers
567 */
568ENTRY(ret_from_exception)
569 get_thread_info tsk
570 mov why, #0
571 b ret_to_user
57ENTRY(ret_to_user)
58ret_slow_syscall:
由上可知,中斷和異常需要返回用戶空間時以及系統調用完畢後都需要經過統一的出口ret_slow_syscall,以此決定是否進行進程調度切換等。
linux+v2.6.19/arch/arm/kernel/entry-common.S
16 .align 5
17/*
18 * This is the fast syscall return path. We do as little as
19 * possible here, and this includes saving r0 back into the SVC
20 * stack.
21 */
22ret_fast_syscall:
23 disable_irq @ disable interrupts
24 ldr r1, [tsk, #TI_FLAGS]
25 tst r1, #_TIF_WORK_MASK
26 bne fast_work_pending
28 @ fast_restore_user_regs
29 ldr r1, [sp, #S_OFF + S_PSR] @ get calling cpsr
30 ldr lr, [sp, #S_OFF + S_PC]! @ get pc
31 msr spsr_cxsf, r1 @ save in spsr_svc
32 ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
33 mov r0, r0
34 add sp, sp, #S_FRAME_SIZE - S_PC
35 movs pc, lr @ return & move spsr_svc into cpsr
37/*
38 * Ok, we need to do extra processing, enter the slow path.
39 */
40fast_work_pending:
41 str r0, [sp, #S_R0+S_OFF]! @ returned r0
42work_pending:
43 tst r1, #_TIF_NEED_RESCHED
44 bne work_resched
45 tst r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING
46 beq no_work_pending
47 mov r0, sp @ 'regs'
48 mov r2, why @ 'syscall'
49 bl do_notify_resume
50 b ret_slow_syscall @ Check work again
52work_resched:
53 bl schedule
54/*
55 * "slow" syscall return path. "why" tells us if this was a real syscall.
56 */
57ENTRY(ret_to_user)
58ret_slow_syscall:
59 disable_irq @ disable interrupts
60 ldr r1, [tsk, #TI_FLAGS]
61 tst r1, #_TIF_WORK_MASK
62 bne work_pending
63no_work_pending:
64 @ slow_restore_user_regs
65 ldr r1, [sp, #S_PSR] @ get calling cpsr
66 ldr lr, [sp, #S_PC]! @ get pc
67 msr spsr_cxsf, r1 @ save in spsr_svc
68 ldmdb sp, {r0 - lr}^ @ get calling r1 - lr
69 mov r0, r0
70 add sp, sp, #S_FRAME_SIZE - S_PC
71 movs pc, lr @ return & move spsr_svc into cpsr
進入ret_slow_syscall後,首先關中斷,也就是說,執行這段代碼時CPU不接受任何中斷請求。然後,看調度標誌是否爲非0(tst r1, #_TIF_NEED_RESCHED),如果調度標誌爲非0,說明需要進行調度,則去調用schedule()函數進行進程調度。