中斷——中斷處理程序的進入與退出 (基于3.16-rc4)上一篇博文我們分析了中斷描述符表的中斷門初始化過程,并且在interrupt數組中初始化過程中,可以看到每個中斷處理程序都會跳入common_interrupt中。下面我們分析下common_interrupt匯編片段(arch/x86/kernel/entrt_32.S)。
1 .p2align CONFIG_X86_L1_CACHE_SHIFT 2 common_interrupt: 3 ASM_CLAC 4 addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */ 5 SAVE_ALL 6 TRACE_IRQS_OFF 7 movl %esp,%eax 8 call do_IRQ 9 jmp ret_from_intr10 ENDPROC(common_interrupt)11 CFI_ENDPROC
第5行SAVE_ALL也是一個匯編片段(宏),用來將當前多個寄存器壓棧,因為在do_IRQ中可能會用到這些寄存器。第8行調用了do_IRQ函數,接下來我們分析do_IRQ函數(arch/x86/kernel/irq.c)。
1 __visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) 2 { 3 struct pt_regs *old_regs = set_irq_regs(regs); 4 5 /* high bit used in ret_from_ code */ 6 unsigned vector = ~regs->orig_ax; 7 unsigned irq; 8 9 irq_enter();10 exit_idle();11 12 irq = __this_cpu_read(vector_irq[vector]);13 14 if (!handle_irq(irq, regs)) {15 ack_APIC_irq();16 17 if (irq != VECTOR_RETRIGGERED) {18 pr_emerg_ratelimited("%s: %d.%d No irq handler for vector (irq %d)/n",19 __func__, smp_processor_id(),20 vector, irq);21 } else {22 __this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED);23 }24 }25 26 irq_exit();27 28 set_irq_regs(old_regs);29 return 1;30 }
第12行的vector_irq數組保存了中斷向量號和中斷線號(中斷號)的對應關系,利用__this_cpu_read函數獲得當前中斷向量號所對應的中斷號。第14行中handle_irq函數,使用中斷號irq作為參數,進入該中斷號所對應的中斷服務例程中,下面分析下handle_irq函數(arch/x86/kernel/irq_32.c)。
1 bool handle_irq(unsigned irq, struct pt_regs *regs) 2 { 3 struct irq_desc *desc; 4 int overflow; 5 6 overflow = check_stack_overflow(); 7 8 desc = irq_to_desc(irq); 9 if (unlikely(!desc))10 return false;11 12 if (user_mode_vm(regs) || !execute_on_irq_stack(overflow, desc, irq)) {13 if (unlikely(overflow))14 print_stack_overflow();15 desc->handle_irq(irq, desc);16 }17 18 return true;19 }
第8行獲取到中斷號irq所對應的struct irq_desc結構體指針,內核使用struct irq_desc類型結構體數組來存放所有的中斷服務例程,中斷號irq作為數組元素下標,如下所示(kernel/irq/irqdesc.c)。
1 struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {2 [0 ... NR_IRQS-1] = {3 .handle_irq = handle_bad_irq,4 .depth = 1,5 .lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),6 }7 };
再來看下struct irq_desc結構體(include/linux/irqdesc.h)。
1 struct irq_desc { 2 struct irq_data irq_data; 3 unsigned int __percpu *kstat_irqs; 4 irq_flow_handler_t handle_irq; 5 #ifdef CONFIG_IRQ_PREFLOW_FASTEOI 6 irq_preflow_handler_t preflow_handler; 7 #endif 8 struct irqaction *action; /* IRQ action list */ 9 unsigned int status_use_accessors;10 unsigned int core_internal_state__do_not_mess_with_it;11 unsigned int depth; /* nested irq disables */12 unsigned int wake_depth; /* nested wake enables */13 unsigned int irq_count; /* For detecting broken IRQs */14 unsigned long last_unhandled; /* Aging timer for unhandled count */15 unsigned int irqs_unhandled;16 atomic_t threads_handled;17 int threads_handled_last;18 raw_spinlock_t lock;19 struct cpumask *percpu_enabled;20 #ifdef CONFIG_SMP21 const struct cpumask *affinity_hint;22 struct irq_affinity_notify *affinity_notify;23 #ifdef CONFIG_GENERIC_PENDING_IRQ24 cpumask_var_t pending_mask;25 #endif26 #endif27 unsigned long threads_oneshot;28 atomic_t threads_active;29 wait_queue_head_t wait_for_threads;30 #ifdef CONFIG_PROC_FS31 struct proc_dir_entry *dir;32 #endif33 int parent_irq;34 struct module *owner;35 const char *name;36 } ____cacheline_internodealigned_in_smp;
真正的中斷處理程序并不直接放在struct irq_desc結構體中,而是存放在struct irq_desc結構體的action成員所指向的struct irqaction結構體中,第8行。下面看下struct irqaction結構體類型(include/linux/interrupt.h)。
1 struct irqaction { 2 irq_handler_t handler; 3 void *dev_id; 4 void __percpu *percpu_dev_id; 5 struct irqaction *next; 6 irq_handler_t thread_fn; 7 struct task_struct *thread; 8 unsigned int irq; 9 unsigned int flags;10 unsigned long thread_flags;11 unsigned long thread_mask;12 const char *name;13 struct proc_dir_entry *dir;14 } ____cacheline_internodealigned_in_smp;
第1行handler中存放著中斷服務例程。第5行next中存放下一個該類型結構體指針。因為一個中斷號可以對應多個中斷服務例程(中斷線共享),內核將中斷號相同的多個中斷服務例程組織成一個鏈表,掛到以irq號作為下標的irq_desc數組元素中。
回到handle_irq函數中,第8行獲取到irq號所對應的struct irq_desc結構體指針desc,接著第15行執行了desc->handle_irq函數,在該函數中執行irq號的所有中斷服務例程。
在這里,一定要區別清楚IDT表idt_table和irq_desc數組的區別,idt_table中存放的是中斷處理程序,而且這些中斷處理程序的開頭部分代碼都是相同的,都要跳到common_interrupt函數中,進而去尋找中斷服務例程。而irq_desc數組中存放的是中斷服務例程,中斷處理程序最終要通過該數組找到對應的中斷服務例程并執行它。我們在編寫驅動程序時,很多時候需要編寫設備的中斷服務例程,我們將中斷服務例程存放在申請的struct irqaction結構體當中,并將該結構體掛到irq_desc數組的對應鏈表中,當中斷發生后,系統會自動通過IDT--->GDT--->中斷處理程序--->common_interrupt(屬于中斷處理程序)--->do_IRQ--->handle_irq,然后將irq_desc[NR_IRQS]數組中irq號對應的所有中斷服務例程全部執行一遍。
當中斷服務例程執行結束后,就會返回文章最開始的common_interrupt函數中,開始執行第9行,跳入到ret_from_intr函數中(arch/x86/kernel/entry_32.S)。
1 ALIGN 2 RING0_PTREGS_FRAME 3 ret_from_exception: 4 preempt_stop(CLBR_ANY) 5 ret_from_intr: 6 GET_THREAD_INFO(%ebp) 7 #ifdef CONFIG_VM86 8 movl PT_EFLAGS(%esp), %eax # mix EFLAGS and CS 9 movb PT_CS(%esp), %al10 andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax11 #else12 /*13 * We can be coming here from child spawned by kernel_thread().14 */15 movl PT_CS(%esp), %eax16 andl $SEGMENT_RPL_MASK, %eax17 #endif18 cmpl $USER_RPL, %eax19 jb resume_kernel # not returning to v8086 or userspace20 21 ENTRY(resume_userspace)22 LOCKDEP_SYS_EXIT23 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt24 # setting need_resched or sigpending25 # between sampling and the iret26 TRACE_IRQS_OFF27 movl TI_flags(%ebp), %ecx28 andl $_TIF_WORK_MASK, %ecx # is there any work to be done on29 # int/exception return?30 jne work_pending31 jmp restore_all32 END(ret_from_exception)
第15行從當前棧中將cs寄存器的值存入eax中,第16行通過掩碼計算,將eax中的DPL字段提取出來再存入eax,第18行比較eax(DPL)和用戶空間權限的大小,如果DPL權限大的話,執行19行,恢復到內核態中,否則恢復到用戶態,21行。