【嵌入式Linux學習七步曲之第五篇 Linux內核及驅動編程】深入剖析Linux中斷機制之三--Linux對異常和中斷的處理

轉載: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

203

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

222

223        .ltorg

當前處於用戶空間時,對於ARM系統來說是處於USR模式,此時產生中斷,中斷處理完畢後,無論是否是可剝奪內核,都調轉到統一的用戶模式出口ret_to_user,其檢查是否需要進行進程調度,若需要進行進程調度,則進程切換,否則直接返回到被中斷的用戶空間。

404        .align  5

405__irq_usr:

406        usr_entry

407

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

417

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

428

429        mov     why, #0

430        b       ret_to_user

432        .ltorg

1.3.2      保存現場

105/*

106 * SVC mode handlers

107 */

108

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}

120

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

128

129        mov     r1, lr

130

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)。下圖給出它們的調用關係:

 

asm_do_IRQ

 

    do_IRQ()

 

handle_IRQ_event()

 

中斷服務

例程1

例程

 

中斷服務

例程2

例程


                               中斷處理函數的調用關係

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;

116

121        if (irq >= NR_IRQS)

122desc = &bad_irq_desc;

123

124irq_enter(); //記錄硬件中斷狀態,便於跟蹤中斷情況確定是否是中斷上下文

125

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

130

131irq_exit(); //中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套

132        set_irq_regs(old_regs);

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;

170        struct irqaction *action;

171        unsigned int status;

172

173kstat_this_cpu.irqs[irq]++;

186

187spin_lock(&desc->lock);

188        if (desc->chip->ack) //首先響應中斷,通常實現爲關閉本中斷線

189desc->chip->ack(irq);

190

194status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);

195status |= IRQ_PENDING; /* we _want_ to handle it */

196

201action = NULL;

202        if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {

203action = desc->action;

204                status &= ~IRQ_PENDING; /* we commit to handling */

205status |= IRQ_INPROGRESS; /* we are handling it */

206        }

207desc->status = status;

208

215        if (unlikely(!action))

216                goto out;

217

218        /*

219         * Edge triggered interrupts need to remember

220         * pending events.

227         */

228        for (;;) {

229irqreturn_t action_ret;

230

231                spin_unlock(&desc->lock);//解鎖,中斷處理期間可以響應其他中斷,否則再次進入__do_IRQ時會死鎖

233                action_ret = handle_IRQ_event(irq, action);

237                spin_lock(&desc->lock);

238                if (likely(!(desc->status & IRQ_PENDING)))

239                        break;

240                desc->status &= ~IRQ_PENDING;

241        }

242desc->status &= ~IRQ_INPROGRESS;

243

244out:

249        desc->chip->end(irq);

250spin_unlock(&desc->lock);

251

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;

133        unsigned int status = 0;

134

135handle_dynamic_tick(action);

136       // 如果沒有設置IRQF_DISABLED,則中斷處理過程中,打開中斷

137        if (!(action->flags & IRQF_DISABLED))

138local_irq_enable_in_hardirq();

139

140        do {

141ret = action->handler(irq, action->dev_id);

142                if (ret == IRQ_HANDLED)

143status |= action->flags;

144retval |= ret;

145                action = action->next;

146        } while (action);

147

150local_irq_disable();

151

152        return retval;

153}

這個循環依次調用請求隊列中的每個中斷服務例程。這裏要說明的是,如果設置了IRQF_DISABLED,則中斷服務例程在關中斷的條件下進行(不包括非屏蔽中斷),但通常CPU在穿過中斷門時自動關閉中斷。但是,關中斷時間絕不能太長,否則就可能丟失其它重要的中斷。也就是說,中斷服務例程應該處理最緊急的事情,而把剩下的事情交給另外一部分來處理。即後半部分(bottom half)來處理,這一部分內容將在下一節進行討論。

不同的CPU不允許併發地進入同一中斷服務例程,否則,那就要求所有的中斷服務例程必須是“可重入”的純代碼。可重入代碼的設計和實現就複雜多了,因此,Linux在設計內核時巧妙地“避難就易”,以解決問題爲主要目標。

1.3.3.4         irq_exit()

中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套

////////////////////////////////////////////////////////////

linux+v2.6.19/kernel/softirq.c

285void irq_exit(void)

286{

287account_system_vtime(current);

288trace_hardirq_exit();

289sub_preempt_count(IRQ_EXIT_OFFSET);

290        if (!in_interrupt() && local_softirq_pending())

291invoke_softirq();

////////////

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

  27

  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

  36

  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

  51

  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()函數進行進程調度。

 

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