x86 kernel 中斷分析三——中斷處理流程
CPU檢測中斷
CPU在執行每條程式之前會檢測是否有中斷到達,即中斷控制器是否有傳送中斷訊號過來
查詢IDT
CPU根據中斷向量到IDT中讀取對應的中斷描述符表項,根據段選擇符合偏移確定中斷服務程式的地址見附錄2
interrupt陣列
在分析一中,我們看到,填充IDT中斷服務程式的是interrupt陣列的內容,所以第2步跳轉到interrupt陣列對應的表項,表項的內容之前也已分析過
push vector num and jmp to common_interrupt
778 /*
779 * the CPU automatically disables interrupts when executing an IRQ vector,
780 * so IRQ-flags tracing has to follow that:
781 */
782 .p2align CONFIG_X86_L1_CACHE_SHIFT
783 common_interrupt:
784 ASM_CLAC
785 addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */
786 SAVE_ALL
787 TRACE_IRQS_OFF
788 movl %esp,%eax
789 call do_IRQ
790 jmp ret_from_intr
791 ENDPROC(common_interrupt)
792 CFI_ENDPROC
addl $-0x80,(%esp)
根據第一篇分析,此時棧頂是(~vector + 0x80),這裡減去0x80,所以值為vector num取反,範圍在[-256, -1]。這麼做是為了和系統呼叫區分,正值為系統呼叫號,負值為中斷向量。
SAVE_ALL
儲存現場,將所有暫存器的值壓棧(cs eip ss esp由系統自動儲存)
186 .macro SAVE_ALL
187 cld
188 PUSH_GS
189 pushl_cfi %fs
190 /*CFI_REL_OFFSET fs, 0;*/
191 pushl_cfi %es
192 /*CFI_REL_OFFSET es, 0;*/
193 pushl_cfi %ds
194 /*CFI_REL_OFFSET ds, 0;*/
195 pushl_cfi %eax
196 CFI_REL_OFFSET eax, 0
197 pushl_cfi %ebp
198 CFI_REL_OFFSET ebp, 0
199 pushl_cfi %edi
200 CFI_REL_OFFSET edi, 0
201 pushl_cfi %esi
202 CFI_REL_OFFSET esi, 0
203 pushl_cfi %edx
204 CFI_REL_OFFSET edx, 0
205 pushl_cfi %ecx
206 CFI_REL_OFFSET ecx, 0
207 pushl_cfi %ebx
208 CFI_REL_OFFSET ebx, 0
209 movl $(__USER_DS), %edx
210 movl %edx, %ds
211 movl %edx, %es
212 movl $(__KERNEL_PERCPU), %edx
213 movl %edx, %fs
214 SET_KERNEL_GS %edx
215 .endm
movl %esp,%eax
將esp的值賦值給eax,eax作為do_IRQ的第一個引數,esp的值是以上壓棧的暫存器的內容,以pt_reg形式傳過去。
call do_IRQ
175 /*
176 * do_IRQ handles all normal device IRQ's (the special
177 * SMP cross-CPU interrupts have their own specific
178 * handlers).
179 */
180 __visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
181 {
182 struct pt_regs *old_regs = set_irq_regs(regs);
183
184 /* high bit used in ret_from_ code */
185 unsigned vector = ~regs->orig_ax; //獲取向量號,這裡有一個取反的操作,與之前的取反相對應得到正的向量號
186 unsigned irq;
187
188 irq_enter();
189 exit_idle();
190
191 irq = __this_cpu_read(vector_irq[vector]); //通過向量號得到中斷號
192
193 if (!handle_irq(irq, regs)) {
194 ack_APIC_irq();
195
196 if (irq != VECTOR_RETRIGGERED) {
197 pr_emerg_ratelimited("%s: %d.%d No irq handler for vector (irq %d)\n",
198 __func__, smp_processor_id(),
199 vector, irq);
200 } else {
201 __this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED);
202 }
203 }
204
205 irq_exit();
206
207 set_irq_regs(old_regs);
208 return 1;
209 }
irq_enter
319 /*
320 * Enter an interrupt context. //進入中斷上下文,因為首先處理的是硬中斷,所以我們可以把irq_enter認為是硬中斷的開始
321 */
322 void irq_enter(void)
323 {
324 rcu_irq_enter(); //inform RCU that current CPU is entering irq away from idle
325 if (is_idle_task(current) && !in_interrupt()) { //如果當前是pid==0的idle task並且不處於中斷上下文中
326 /*
327 * Prevent raise_softirq from needlessly waking up ksoftirqd
328 * here, as softirq will be serviced on return from interrupt.
329 */
330 local_bh_disable();
331 tick_irq_enter(); //idle程序會被中斷或者其他程序搶佔,在系統中斷過程中用irq_enter->tick_irq_enter()恢復週期性tick以得到正確的jiffies值(這段註釋摘錄自http://blog.chinaunix.net/uid-29675110-id-4365095.html)
332 _local_bh_enable();
333 }
334
335 __irq_enter();
336 }
__irq_enter
28 /*
29 * It is safe to do non-atomic ops on ->hardirq_context,
30 * because NMI handlers may not preempt and the ops are
31 * always balanced, so the interrupted value of ->hardirq_context
32 * will always be restored.
33 */
34 #define __irq_enter() \
35 do { \
36 account_irq_enter_time(current); \
37 preempt_count_add(HARDIRQ_OFFSET); \ //HARDIRQ_OFFSET等於1左移16位,即將preempt_count第16 bit加1,preempt_count的格式見附錄
38 trace_hardirq_enter(); \
39 } while (0)
exit_idle
如果系統正處在idle狀態,那麼退出IDLE
258 /* Called from interrupts to signify idle end */
259 void exit_idle(void)
260 {
261 /* idle loop has pid 0 */ //如果當前程序不為0,直接退出,不需要退出 idle
262 if (current->pid)
263 return;
264 __exit_idle(); //如果是idle程序,那麼通過__exit_idle呼叫一系列notification
265 }
handle_irq
165 bool handle_irq(unsigned irq, struct pt_regs *regs)
166 {
167 struct irq_desc *desc;
168 int overflow;
169
170 overflow = check_stack_overflow(); //x86架構下如果sp指標距離棧底的位置小於1KB,則認為有stack overflow的風險
171
172 desc = irq_to_desc(irq); //獲取desc,從剛開始的vector num-->irq num--> desc
173 if (unlikely(!desc))
174 return false;
175 //如果發生中斷時,CPU正在執行使用者空間的程式碼,處理中斷需切換到核心棧,但此時核心棧是空的,所以無需再切換到中斷棧
176 if (user_mode_vm(regs) || !execute_on_irq_stack(overflow, desc, irq)) { // 在CPU的irq stack執行,否則在當前程序的棧執行,呼叫下面的desc->handle_irq
177 if (unlikely(overflow))
178 print_stack_overflow();
179 desc->handle_irq(irq, desc);
180 }
181
182 return true;
183 }
中斷棧的定義及初始化
按照目前的核心設計,中斷有自己的棧,用來執行中斷服務程式,這樣是為了防止中斷巢狀破壞與之共享的
中斷棧的定義,可以看到與程序上下文的佈局相同,thread info + stack
58 /*
59 * per-CPU IRQ handling contexts (thread information and stack)
60 */
61 union irq_ctx {
62 struct thread_info tinfo;
63 u32 stack[THREAD_SIZE/sizeof(u32)];
64 } __attribute__((aligned(THREAD_SIZE)));
中斷棧的初始化:
建立percpu變數hardirq_ctx和softirq_ctx,型別為irq_ctx,所以每個cpu的軟硬中斷有各自的stack
66 static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
67 static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
native_init_IRQ->irq_ctx_init
hardirq_ctx和softirq_ctx的初始化方式相同,如下
116 /*
117 * allocate per-cpu stacks for hardirq and for softirq processing
118 */
119 void irq_ctx_init(int cpu)
120 {
121 union irq_ctx *irqctx;
122
123 if (per_cpu(hardirq_ctx, cpu))
124 return;
125
126 irqctx = page_address(alloc_pages_node(cpu_to_node(cpu), //分配2個page
127 THREADINFO_GFP,
128 THREAD_SIZE_ORDER));
129 memset(&irqctx->tinfo, 0, sizeof(struct thread_info)); //初始化其中的部分成員
130 irqctx->tinfo.cpu = cpu;
131 irqctx->tinfo.addr_limit = MAKE_MM_SEG(0);
132
133 per_cpu(hardirq_ctx, cpu) = irqctx; //賦值給hardirq_ctx
134
135 irqctx = page_address(alloc_pages_node(cpu_to_node(cpu),
136 THREADINFO_GFP,
137 THREAD_SIZE_ORDER));
138 memset(&irqctx->tinfo, 0, sizeof(struct thread_info));
139 irqctx->tinfo.cpu = cpu;
140 irqctx->tinfo.addr_limit = MAKE_MM_SEG(0);
141
142 per_cpu(softirq_ctx, cpu) = irqctx;
143
144 printk(KERN_DEBUG "CPU %u irqstacks, hard=%p soft=%p\n",
145 cpu, per_cpu(hardirq_ctx, cpu), per_cpu(softirq_ctx, cpu));
146 }
網上找的一張圖,如下
中斷棧的切換
發生中斷時需要從當前程序棧切換到中斷棧
80 static inline int
81 execute_on_irq_stack(int overflow, struct irq_desc *desc, int irq)
82 {
83 union irq_ctx *curctx, *irqctx;
84 u32 *isp, arg1, arg2;
85
86 curctx = (union irq_ctx *) current_thread_info(); //獲取當前程序的process context,即棧的起始地址
87 irqctx = __this_cpu_read(hardirq_ctx); //獲取硬中斷的hardirq context,即棧的起始地址
88
89 /*
90 * this is where we switch to the IRQ stack. However, if we are
91 * already using the IRQ stack (because we interrupted a hardirq
92 * handler) we can't do that and just have to keep using the
93 * current stack (which is the irq stack already after all)
94 */
95 if (unlikely(curctx == irqctx)) //如果當前程序的棧和中斷棧相同,說明發生了中斷巢狀,此時當前程序就是一箇中斷的服務例程
96 return 0; //這種情況下不能進行棧的切換,還是在當前棧中執行,只要返回0即可
97
98 /* build the stack frame on the IRQ stack */
99 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx)); //獲取中斷棧的isp
100 irqctx->tinfo.task = curctx->tinfo.task; //獲取當前程序的task和stack point
101 irqctx->tinfo.previous_esp = current_stack_pointer;
102
103 if (unlikely(overflow))
104 call_on_stack(print_stack_overflow, isp);
105
106 asm volatile("xchgl %%ebx,%%esp \n" //具體的棧切換髮生在以下彙編中,基本上就是儲存現場,進行切換,不深入研究彙編了...
107 "call *%%edi \n"
108 "movl %%ebx,%%esp \n"
109 : "=a" (arg1), "=d" (arg2), "=b" (isp)
110 : "0" (irq), "1" (desc), "2" (isp),
111 "D" (desc->handle_irq) //不管是共享棧還是獨立棧,最後都會呼叫到irq desc對應的handle_irq
112 : "memory", "cc", "ecx");
113 return 1;
114 }
handle_level_irq
kernel中對於中斷有一系列的中斷流處理函式
handle_simple_irq 用於簡易流控處理;
handle_level_irq 用於電平觸發中斷的流控處理;
handle_edge_irq 用於邊沿觸發中斷的流控處理;
handle_fasteoi_irq 用於需要響應eoi的中斷控制器;
handle_percpu_irq 用於只在單一cpu響應的中斷;
handle_nested_irq 用於處理使用執行緒的巢狀中斷;
我們在第二篇分析中,init_ISA_irqs把legacy irq的中斷流處理函式都設定為handle_level_irq,以此為例做分析:
//level type中斷,當硬體中斷line的電平處於active level時就一直保持有中斷請求,這就要求處理中斷過程中遮蔽中斷,響應硬體後開啟中斷
387 /**
388 * handle_level_irq - Level type irq handler //電平觸發的中斷處理函式
389 * @irq: the interrupt number
390 * @desc: the interrupt description structure for this irq
391 *
392 * Level type interrupts are active as long as the hardware line has
393 * the active level. This may require to mask the interrupt and unmask
394 * it after the associated handler has acknowledged the device, so the
395 * interrupt line is back to inactive.
396 */
397 void
398 handle_level_irq(unsigned int irq, struct irq_desc *desc)
399 {
400 raw_spin_lock(&desc->lock); //上鎖
401 mask_ack_irq(desc); //mask對應的中斷,否則一直接收來自interrupt line的中斷訊號
402
403 if (unlikely(irqd_irq_inprogress(&desc->irq_data))) //如果該中斷正在其他cpu上被處理
404 if (!irq_check_poll(desc)) //這邊不是很理解,irq的IRQS_POLL_INPROGRESS(polling in a progress)是什麼意思?只能等後續程式碼遇到這個巨集的時候再說。如果是在該狀態,cpu relax,等待完成
405 goto out_unlock; //直接解鎖退出
406 //清除IRQS_REPLAY和IRQS_WAITING標誌位
407 desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
408 kstat_incr_irqs_this_cpu(irq, desc); //該CPU上該irq觸發次數加1,總的中斷觸發次數加1
409
410 /*
411 * If its disabled or no action available
412 * keep it masked and get out of here
413 */
414 if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
415 desc->istate |= IRQS_PENDING; //設定為pending
416 goto out_unlock;
417 }
418
419 handle_irq_event(desc); //核心函式
420
421 cond_unmask_irq(desc); //使能中斷線
422
423 out_unlock:
424 raw_spin_unlock(&desc->lock);
425 }
426 EXPORT_SYMBOL_GPL(handle_level_irq);
handle irq event
182 irqreturn_t handle_irq_event(struct irq_desc *desc)
183 {
184 struct irqaction *action = desc->action; //獲取irqaction連結串列
185 irqreturn_t ret;
186
187 desc->istate &= ~IRQS_PENDING; //正式進入處理流程,清除irq desc的pending標誌位
188 irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); //處理中斷前設定IRQD_IRQ_INPROGRESS標誌
189 raw_spin_unlock(&desc->lock);
190
191 ret = handle_irq_event_percpu(desc, action);
192
193 raw_spin_lock(&desc->lock);
194 irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); //處理中斷後清除IRQD_IRQ_INPROGRESS標誌
195 return ret;
196 }
handle_irq_event_percpu
132 irqreturn_t
133 handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
134 {
135 irqreturn_t retval = IRQ_NONE;
136 unsigned int flags = 0, irq = desc->irq_data.irq;
137
138 do {
139 irqreturn_t res;
140
141 trace_irq_handler_entry(irq, action);
142 res = action->handler(irq, action->dev_id); //呼叫硬中斷處理函式
143 trace_irq_handler_exit(irq, action, res);
144
145 if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
146 irq, action->handler))
147 local_irq_disable();
148
149 switch (res) {
150 case IRQ_WAKE_THREAD: //執行緒化中斷的硬中斷,通常只是響應一下硬體ack,就返會IRQ_WAKE_THREAD,喚醒軟中斷執行緒
151 /*
152 * Catch drivers which return WAKE_THREAD but
153 * did not set up a thread function
154 */
155 if (unlikely(!action->thread_fn)) {
156 warn_no_thread(irq, action);
157 break;
158 }
159
160 irq_wake_thread(desc, action); //喚醒軟中斷執行緒
161
162 /* Fall through to add to randomness */
163 case IRQ_HANDLED: //表示已經在硬中斷中處理完畢
164 flags |= action->flags;
165 break;
166
167 default:
168 break;
169 }
170
171 retval |= res;
172 action = action->next; //對於共享中斷,所有irqaction掛在同一desc下
173 } while (action);
174
175 add_interrupt_randomness(irq, flags); //這塊程式碼其實和中斷流程的關係不大,利用使用者和外設作為噪聲源,為核心隨機熵池做貢獻....(http://jingpin.jikexueyuan.com/article/23923.html)
176
177 if (!noirqdebug)
178 note_interrupt(irq, desc, retval);
179 return retval;
180 }
以上就是中斷處理流程的簡要分析,有個問題,中action的handler及執行緒化的軟中斷從何而來?下篇分析見。
附錄1:
CPU使用IDT查到的中斷服務程式的段選擇符從GDT中取得相應的段描述符,段描述符裡儲存了中斷服務程式的段基址和屬性資訊,此時CPU就得到了中斷服務程式的起始地址。這裡,CPU會根據當前cs暫存器裡的CPL和GDT的段描述符的DPL,以確保中斷服務程式是高於當前程式的,如果這次中斷是程式設計異常(如:int 80h系統呼叫),那麼還要檢查CPL和IDT表中中斷描述符的DPL,以保證當前程式有許可權使用中斷服務程式,這可以避免使用者應用程式訪問特殊的陷阱門和中斷門[3]。
如下圖顯示了從中斷向量到GDT中相應中斷服務程式起始位置的定位方式:
附錄2. preempt_count:
44 #define HARDIRQ_OFFSET (1UL << HARDIRQ_SHIFT) // 1左移16位
32 #define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS) // 8 + 8 = 16
31 #define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS) // 0 + 8 = 8
30 #define PREEMPT_SHIFT 0
25 #define PREEMPT_BITS 8
26 #define SOFTIRQ_BITS 8
2500 void __kprobes preempt_count_add(int val)
2501 {
2502 #ifdef CONFIG_DEBUG_PREEMPT
2503 /*
2504 * Underflow?
2505 */
2506 if (DEBUG_LOCKS_WARN_ON((preempt_count() < 0)))
2507 return;
2508 #endif
2509 __preempt_count_add(val); //除去debug相關的內容,只有這一行關鍵程式碼,將preempt_count中第16 bit加1
2510 #ifdef CONFIG_DEBUG_PREEMPT
2511 /*
2512 * Spinlock count overflowing soon?
2513 */
2514 DEBUG_LOCKS_WARN_ON((preempt_count() & PREEMPT_MASK) >=
2515 PREEMPT_MASK - 10);
2516 #endif
2517 if (preempt_count() == val)
2518 trace_preempt_off(CALLER_ADDR0, get_parent_ip(CALLER_ADDR1));
2519 }
2520 EXPORT_SYMBOL(preempt_count_add);
preempt_count的佈局如下: