本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
回顧
上篇文章linux中斷源碼分析 - 初始化(二)已經描述了中斷描述符表和中斷描述符數組的初始化,由於在初始化期間系統關閉了中斷(通過設置CPU的EFLAGS寄存器的IF標志位為0),當整個中斷和異常的初始化完成后,系統會開啟中斷(設置CPU的EFLAGS寄存器的IF標志位為1),此時整個系統的中斷已經開始可以使用了。本篇文章我們具體研究一次典型中斷發生時的運行流程。
禁止調度和搶占
首先我們需要了解,當系統處於中斷上下文時,是禁止發生調度和搶占的。進程的thread_info中有個preempt_count成員變量,其作為一個變量,包含了3個計數器和一個標志位,如下:
位 |
描述 |
解釋 |
0~7 |
搶占計數器 |
也可以說是鎖占有數 |
8~15 |
軟中斷計數器 |
記錄軟中斷被禁用次數,0表示可以進行軟中斷 |
16~27 |
硬中斷計數器 |
表示中斷處理嵌套次數,irq_enter()增加它,irq_exit()減少它 |
28 |
PREEMPT_ACTIVE標志 |
表明正在進行內核搶占,設置此標志也禁止了搶占 |
當進入到中斷時,中斷處理程序會調用irq_enter()函數禁止搶占和調度。當中斷退出時,會通過irq_exit()減少其硬件計數器。我們需要清楚的就是,無論系統處於硬中斷還是軟中斷,調度和搶占都是被禁止的。
中斷產生
我們需要先明確一下,中斷控制器與CPU相連的三種線:INTR、數據線、INTA。
在硬件電路中,中斷的產生發生一般只有兩種,分別是:電平觸發方式和邊沿觸發方式。當一個外部設備產生中斷,中斷信號會沿着中斷線到達中斷控制器。中斷控制器接收到該外部設備的中斷信號后首先會檢測自己的中斷屏蔽寄存器是否屏蔽該中斷。如果沒有,則設置中斷請求寄存器中中斷向量號對應的位,並將INTR拉高用於通知CPU,CPU每當執行完一條指令時都會去檢查INTR引腳是否有信號(這是CPU自動進行的),如果有信號,CPU還會檢查EFLAGS寄存器的IF標志位是否禁止了中斷(IF = 0),如果CPU未禁止中斷,CPU會自動通過INTA信號線應答中斷控制器。CPU再次通過INTA信號線通知中斷控制器,此時中斷控制器會把中斷向量號送到數據線上,CPU讀取數據線獲取中斷向量號。到這里實際上中斷向量號已經發送給CPU了,如果中斷控制器是AEIO模式,則會自動清除中斷向量號對應的中斷請求寄存器的位,如果是EIO模式,則等待CPU發送的EIO信號后在清除中斷向量號對應的中斷請求寄存器的位。
用步驟描述就是:
- 中斷控制器收到中斷信號
- 中斷控制器檢查中斷屏蔽寄存器是否屏蔽該中斷,若屏蔽直接丟棄
- 中斷控制器設置該中斷所在的中斷請求寄存器位
- 通過INTR通知CPU
- CPU收到INTR信號,檢查是否屏蔽中斷,若屏蔽直接無視
- CPU通過INTA應答中斷控制器
- CPU再次通過INTA應答中斷控制器,中斷控制器將中斷向量號放入數據線
- CPU讀取數據線上的中斷向量號
- 若中斷控制器為EIO模式,CPU發送EIO信號給中斷控制器,中斷控制器清除中斷向量號對應的中斷請求寄存器位
SMP系統
在SMP系統,也就是多核情況下,外部的中斷控制器有可能會於多個CPU相連,這時候當一個中斷產生時,中斷控制器有兩種方式將此中斷送到CPU上,分別是靜態分發和動態分發。區別就是靜態分發設置了指定中斷送往指定的一個或多個CPU上。動態分發則是由中斷控制器控制中斷應該發往哪個CPU或CPU組。
CPU已經接收到了中斷信號以及中斷向量號。此時CPU會自動跳轉到中斷描述符表地址,以中斷向量號作為一個偏移量,直接訪問中斷向量號對應的門描述符。在門描述符中,有個特權級(DPL),系統會先檢查這個位,然后清除EFLAGS的IF標志位(這也說明了發發生中斷時實際上CPU是禁止其他可屏蔽中斷的),之后轉到描述符中的中斷處理程序中。在上一篇文章我們知道,所有的中斷門描述符的中斷處理程序都被初始化成了interrupt[i],它是一段匯編代碼。
中斷和異常發生時CPU自動完成的工作
我們先注意看一下中斷描述符表,里面的每個中斷描述符都有一個段選擇符和一個偏移量以及一個DPL(特權級),而偏移量其實就是中斷處理程序的入口地址,當中斷或異常發生時:
- CPU首先會確定是中斷或異常的號,然后根據這個號作為偏移量,通過讀取idtr中保存的中斷描述符表(IDT)的基地址獲取相應的中斷描述符。並從中斷描述符中拿出其中的段選擇符
- 根據段選擇符從GDT中獲取這個段的段描述符(為什么只從GDT中獲取?因為初始化所有中段描述符時使用的段選擇符幾乎都是__USER_CS,__KERNEL_CS,TSS,這幾個段選擇符對應的段描述符都保存在GDT中)。而這幾個段描述符中的基地址都是0x00000000,所以偏移量就是中斷處理程序入口地址。
- 這時還沒有進入到中斷處理程序,CPU會先使用CS寄存器的當前特權級(CPL)與中斷描述符中對應的段描述符的DPL進行比較,如果DPL的值 <= CPL的值,則通過檢查,而DPL的值 > CPL的值時,會產生一個"通用保護"異常。這種情況發生的可能性很小,因為在上一篇初始化的文章中也可以看出來,中斷初始化所用的段選擇符都是__KERNEL_CS,而異常的段選擇符幾乎也都是__KERNEL_CS,只除了極特殊的幾個除外。也就是大多數中斷和異常的段選擇符DPL都是0,CPL無論是在內核態(CPL = 0)或者是用戶態(CPL = 3),都可以執行這些中斷和異常。
- 如果是用戶程序的異常(非CPU內部產生的異常),這里還需要再進行多一步的檢查(中斷和CPU內部異常則不用進行這步檢查), 我們回憶一下中斷初始化的文章,里面介紹了門描述符,在門描述符中有一個DPL位,用戶程序的異常還要對這個位進行檢查,當前特權級CPL的值 > DPL的值時,則通過檢查,否則不能通過檢查,而只有系統門和系統中斷門的DPL是3,其他的異常門的DPL都為0。這樣做的好處是避免了用戶程序訪問陷阱門、中斷門和任務門。
- 上面的檢查結束后就可以轉去執行中斷或異常處理程序了,會將剛才獲取到的段選擇符加載到CS寄存器,段描述符加載到CS對應的非編程寄存器中,也就是CS寄存器保存的是__KERNEL_CS,CS寄存器的非編程寄存器中保存的是對應的段描述符。而根據段描述符中的段基址+段內偏移量(保存在門描述符中),則得到了處理程序入口地址,實際上我們知道段基址是0x00000000,段內偏移量實際上就是處理程序入口地址,這個門描述符的段內偏移量會被放到IP寄存器中。
- 如果CS寄存器的特權級發生變化(陷入內核),則CPU會訪問此CPU的TSS段(通過tr寄存器),在TSS段中讀取當前進程的內核棧堆棧棧頂地址到ESP寄存器中,並且將__KERNEL_DS裝載到SS寄存器中。之后把之前的SS寄存器和ESP寄存器的值保存到當前內核棧中。
interrupt[i]
interrupt[i]的每個元素都相同,執行相同的匯編代碼,這段匯編代碼實際上很簡單,它主要工作就是將中斷向量號和被中斷上下文(進程上下文或者中斷上下文)保存到棧中,最后調用do_IRQ函數。
# 代碼地址:arch/x86/kernel/entry_32.S # 開始 1: pushl_cfi $(~vector+0x80) /* Note: always in signed byte range */ # 先會執行這一句,將中斷向量號取反然后加上0x80壓入棧中 .if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6 jmp 2f # 數字定義的標號為臨時標號,可以任意重復定義,例如:"2f"代表正向第一次出現的標號"2:",3b代表反向第一次出現的標號"3:" .endif .previous # .previous使匯編器返回到該自定義段之前的段進行匯編,則回到上面的數據段 .long 1b # 在數據段中執行標號1的操作 .section .entry.text, "ax" # 回到代碼段 vector=vector+1 .endif .endr 2: jmp common_interrupt common_interrupt: ASM_CLAC addl $-0x80,(%esp) # 此時棧頂是(~vector + 0x80),這里再加上-0x80,實際就是中斷向量號取反,用於區別系統調用,系統調用是正數,中斷向量是負數 SAVE_ALL # 保存現場,將寄存器值壓入棧中 TRACE_IRQS_OFF # 關閉中斷跟蹤 movl %esp,%eax # 將棧指針保存到eax寄存器,供do_IRQ使用 call do_IRQ # 調用do_IRQ jmp ret_from_intr # 跳轉到ret_from_intr,進行中斷返回的一些處理 ENDPROC(common_interrupt) CFI_ENDPROC
do_IRQ
這是中斷處理的核心函數,來到這里時,系統已經做了兩件事
- 系統屏蔽了所有可屏蔽中斷(清除了CPU的IF標志位,由CPU自動完成)
- 將中斷向量號和所有寄存器值保存到內核棧中
在do_IRQ中,首先會添加硬中斷計數器,此行為導致了中斷期間禁止調度發送,此后會根據中斷向量號從vector_irq[]數組中獲取對應的中斷號,並調用handle_irq()函數出來該中斷號對應的中斷出來例程。
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) { /* 將棧頂地址保存到全局變量__irq_regs中,old_regs用於保存現在的__irq_regs值,這一行代碼很重要,實現了嵌套中斷情況下的現場保存與還原 */ struct pt_regs *old_regs = set_irq_regs(regs); /* 獲取中斷向量號,因為中斷向量號是以取反方式保存的,這里再次取反 */ unsigned vector = ~regs->orig_ax; /* 中斷向量號 */ unsigned irq; /* 硬中斷計數器增加,硬中斷計數器保存在preempt_count */ irq_enter(); /* 這里開始禁止調度,因為preempt_count不為0 */ /* 退出idle進程(如果當前進程是idle進程的情況下) */ exit_idle(); /* 根據中斷向量號獲取中斷號 */ irq = __this_cpu_read(vector_irq[vector]); /* 主要函數是handle_irq,進行中斷服務例程的處理 */ if (!handle_irq(irq, regs)) { /* EIO模式的應答 */ ack_APIC_irq(); /* 該中斷號並沒有發生過多次觸發 */ if (irq != VECTOR_RETRIGGERED) { pr_emerg_ratelimited("%s: %d.%d No irq handler for vector (irq %d)\n", __func__, smp_processor_id(), vector, irq); } else { /* 將此中斷向量號對應的vector_irq設置為未定義 */ __this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED); } } /* 硬中斷計數器減少 */ irq_exit(); /* 這里開始允許調度 */ /* 恢復原來的__irq_regs值 */ set_irq_regs(old_regs); return 1; }
do_IRQ()函數中最重要的就是handle_irq()處理了,我們看看
bool handle_irq(unsigned irq, struct pt_regs *regs) { struct irq_desc *desc; int overflow; /* 檢查棧是否溢出 */ overflow = check_stack_overflow(); /* 獲取中斷描述符 */ desc = irq_to_desc(irq); /* 檢查是否獲取到中斷描述符 */ if (unlikely(!desc)) return false; /* 檢查使用的棧,有兩種情況,如果進程的內核棧配置為8K,則使用進程的內核棧,如果為4K,系統會專門為所有中斷分配一個4K的棧專門用於硬中斷處理棧,一個4K專門用於軟中斷處理棧,還有一個4K專門用於異常處理棧 */ if (user_mode_vm(regs) || !execute_on_irq_stack(overflow, desc, irq)) { if (unlikely(overflow)) print_stack_overflow(); /* 執行handle_irq */ desc->handle_irq(irq, desc); } return true; }
好的,最后執行中斷描述符中的handle_irq指針所指函數,我們回憶一下,在初始化階段,所有的中斷描述符的handle_irq指針指向了handle_level_irq()函數,文章開頭我們也說過,中斷產生方式有兩種:一種電平觸發、一種是邊沿觸發。handle_level_irq()函數就是用於處理電平觸發的情況,系統內建了一些handle_irq函數,具體定義在include/linux/irq.h文件中,我們羅列幾種常用的:
- handle_simple_irq() 簡單處理情況處理函數
- handle_level_irq() 電平觸發方式情況處理函數
- handle_edge_irq() 邊沿觸發方式情況處理函數
- handle_fasteoi_irq() 用於需要EOI回應的中斷控制器
- handle_percpu_irq() 此中斷只需要單一CPU響應的處理函數
- handle_nested_irq() 用於處理使用線程的嵌套中斷
我們主要看看handle_level_irq()函數函數,有興趣的朋友也可以看看其他的,因為觸發方式不同,通知中斷控制器、CPU屏蔽、中斷狀態設置的時機都不同,它們的代碼都在kernel/irq/chip.c中。
/* 用於電平中斷,電平中斷特點: * 只要設備的中斷請求引腳(中斷線)保持在預設的觸發電平,中斷就會一直被請求,所以,為了避免同一中斷被重復響應,必須在處理中斷前先把mask irq,然后ack irq,以便復位設備的中斷請求引腳,響應完成后再unmask irq */ void handle_level_irq(unsigned int irq, struct irq_desc *desc) { raw_spin_lock(&desc->lock); /* 通知中斷控制器屏蔽該中斷線,並設置中斷描述符屏蔽該中斷 */ mask_ack_irq(desc); /* 檢查此irq是否處於運行狀態,也就是檢查IRQD_IRQ_INPROGRESS標志和IRQD_WAKEUP_ARMED標志。大家可以看看,還會檢查poll */ if (!irq_may_run(desc)) goto out_unlock; desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING); /* 增加此中斷號所在proc中的中斷次數 */ kstat_incr_irqs_this_cpu(irq, desc); /* * If its disabled or no action available * keep it masked and get out of here */ /* 判斷IRQ是否有中斷服務例程(irqaction)和是否被系統禁用 */ if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) { desc->istate |= IRQS_PENDING; goto out_unlock; } /* 在里面執行中斷服務例程 */ handle_irq_event(desc); /* 通知中斷控制器恢復此中斷線 */ cond_unmask_irq(desc); out_unlock: raw_spin_unlock(&desc->lock); }
這個函數還是比較簡單,看handle_irq_event()函數:
irqreturn_t handle_irq_event(struct irq_desc *desc) { struct irqaction *action = desc->action; irqreturn_t ret; desc->istate &= ~IRQS_PENDING; /* 設置該中斷處理正在執行,設置此中斷號的狀態為IRQD_IRQ_INPROGRESS */ irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); raw_spin_unlock(&desc->lock); /* 主要,具體看 */ ret = handle_irq_event_percpu(desc, action); raw_spin_lock(&desc->lock); /* 取消此中斷號的IRQD_IRQ_INPROGRESS狀態 */ irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); return ret; }
再看handle_irq_event_percpu()函數:
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action) { irqreturn_t retval = IRQ_NONE; unsigned int flags = 0, irq = desc->irq_data.irq; /* desc中的action是一個鏈表,每個節點包含一個處理函數,這個循環是遍歷一次action鏈表,分別執行一次它們的處理函數 */ do { irqreturn_t res; /* 用於中斷跟蹤 */ trace_irq_handler_entry(irq, action); /* 執行處理,在驅動中定義的中斷處理最后就是被賦值到中斷服務例程action的handler指針上,這里就執行了驅動中定義的中斷處理 */ res = action->handler(irq, action->dev_id); trace_irq_handler_exit(irq, action, res); if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n", irq, action->handler)) local_irq_disable(); /* 中斷返回值處理 */ switch (res) { /* 需要喚醒該中斷處理例程的中斷線程 */ case IRQ_WAKE_THREAD: /* * Catch drivers which return WAKE_THREAD but * did not set up a thread function */ /* 該中斷服務例程沒有中斷線程 */ if (unlikely(!action->thread_fn)) { warn_no_thread(irq, action); break; } /* 喚醒線程 */ __irq_wake_thread(desc, action); /* Fall through to add to randomness */ case IRQ_HANDLED: flags |= action->flags; break; default: break; } retval |= res; /* 下一個中斷服務例程 */ action = action->next; } while (action); add_interrupt_randomness(irq, flags); /* 中斷調試會使用 */ if (!noirqdebug) note_interrupt(irq, desc, retval); return retval; }
其實代碼上很簡單,我們需要注意幾個屏蔽中斷的方式:清除EFLAGS的IF標志、通知中斷控制器屏蔽指定中斷、設置中斷描述符的狀態為IRQD_IRQ_INPROGRESS。在上述代碼中這三種狀態都使用到了,我們具體解釋一下:
- 清除EFLAGS的IF標志:CPU禁止中斷,當CPU進入到中斷處理時自動會清除EFLAGS的IF標志,也就是進入中斷處理時會自動禁止中斷。在SMP系統中,就是單個CPU禁止中斷。
- 通知中斷控制器屏蔽指定中斷:在中斷控制器處就屏蔽中斷,這樣該中斷產生后並不會發到CPU上。在SMP系統中,效果相當於所有CPU屏蔽了此中斷。系統在執行此中斷的中斷處理函數才會要求中斷控制器屏蔽該中斷,所以沒必要在此中斷的處理過程中中斷控制器再發一次中斷信號給CPU。
- 設置中斷描述符的狀態為IRQD_IRQ_INPROGRESS:在SMP系統中,同一個中斷信號有可能發往多個CPU,但是中斷處理只應該處理一次,所以設置狀態為IRQD_IRQ_INPROGRESS,其他CPU執行此中斷時都會先檢查此狀態(可看handle_level_irq()函數)。
所以在SMP系統下,對於handle_level_irq而言,一次典型的情況是:中斷控制器接收到中斷信號,發送給一個或多個CPU,收到的CPU會自動禁止中斷,並執行中斷處理函數,在中斷處理函數中CPU會通知中斷控制器屏蔽該中斷,之后當執行中斷服務例程時會設置該中斷描述符的狀態為IRQD_IRQ_INPROGRESS,表明其他CPU如果執行該中斷就直接退出,因為本CPU已經在處理了。