1. 程式人生 > >x86 kernel 中斷分析三——中斷處理流程

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的佈局如下:
這裡寫圖片描述