中斷分類
在中斷的多種分類方法中,我們根據中斷的來源來分類:
內部中斷:例如軟件中斷指令, 溢出,除法錯誤等。操作系統從用戶態切換到內核態。都會在一條指令執行完后才會觸發,因為為同步。
外部中斷:由外設提供。由于不知道何時中斷會到來,因此為異步過程。
外部中斷的整體框架如下圖:
首先我們先了解幾個名詞:
軟件中斷irq:它是發生設備中斷時處理器從PIC中讀到的中斷號碼,在內核建立的中斷處理框架內,會使用這個irq號來表示一個外設的中斷,并調用對應的中斷處理例程。
中斷向量表(Vector table):除了外部中斷還有異常,陷阱等異常事件,中斷向量表里面的每一項都是一個中斷或異常處理函數的入口地址。外部設備的中斷常常對應向量表中的某一項,這是個通用的外部中斷處理函數入口,因此進入通用的中斷處理函數之后,系統必須要知道正在處理的中斷是哪一個設備產生的,而這正是由irq決定的。中斷向量表的內容由操作系統在初始化階段來填寫。對于外部中斷,操作系統負責實現一個通用的外部中斷處理函數,然后把這個函數的入口地址翻到中斷向量表中的對應位置。
可編程中斷控制器:PIC(Programmable Interrupt Controller) ,一般可通過處理器進行編程配置。
ARM GIC支持3種類型的中斷:
SGI(Software Generated Interrupt): 軟件產生的中斷,可以用于多核間通信。一個CPU可以通過寫GIC寄存器給另一個CPU產生中斷。多核間調度用的IPI_WAKEUP, IPI_TIMER,.....等都是由SGI產生
PPI(Private Peripheral Interrupt): 某個CPU私有外設的中斷,這類外設的中斷只能發送給綁定的那個CPU
SPI(Share Peripheral Interrupt):共享外設中斷,這類外設的中斷可以路由到任何一個CPU
Linux外部中斷處理程序框架
外部中斷處理程序架構如下:
內核中斷向量表初始化過程(下文在不做特別說明的狀況下均以Linux5.0內核為版本進行分析)
//arch/arm64/kernel/entry.S
/**Exception vectors*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endifEND(vectors)
對kernel_ventry宏做解讀:.align=7,說明該段代碼是以2^7=128字節對齊的,這和向量表中每一個offset的大小是一致的
代碼看似非常復雜,其實最終跳轉到了b el()\\el()_\\label, 翻譯一下,其實就是跳轉到了如下這樣的函數中
el1_sync_invalid
el1_irq_invalid
el1_fiq_invalid
el1_error_invalid
el1_sync:
...
el1_irq:
kernel_entry 1
enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
irq_handler
#ifdef CONFIG_PREEMPT
ldr x24, [tsk, #TSK_TI_PREEMPT] // get preempt count
cbnz x24, 1f // preempt count != 0
bl el1_preempt
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
...
el1_fiq:
...
el1_error:
...
el0_sync:
...
el0_irq:
...
el0_fiq:
...
el0_error:
...
ARM64中,當外部中斷發生時, 會調用el1_irq, 在上述20行中,會call handle_arch_irq. 此為一個函數指針, 在GIC中斷控制器在做初始化時設置的. 而中斷來臨時,handle_domain_irq會被call 到.
//kernel/irq/handle.c
int __init set_handle_irq(void(*handle_irq)(stuct pt_regs*)){
....
handle_arch_irq = handle_irq;
....
}
//drivers/irqchip/irq-gic.c
static void _exception_irq_entry gic_handle_irq(struct pt_regs* regs){
.....
//調用對應的通用中斷處理函數.
handle_domain_irq(gic- >domain,irqnr,regs);
.....
}
//drivers/irqchip/irq-gic.c
static int __init __gic_init_bases(struct gic_chip_data *gic,int irq_start, struct fwnode_handle *handle){
....
set_handle_irq(gic_handle_irq);//中斷向量指向的irq, 匯編語言會調用到此函數
....
}
//drivers/irqchip/irq-gic.c
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,struct fwnode_handle *handle){
....
//分配irq_desc, 里面會call gic_irq_domain_map函數, 設置一吃handle的電信號處理函數
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,numa_node_id());
....
}
//driver/irqchip/irq-gic.c
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,irq_hw_number_t hw){
struct gic_chip_data *gic = d- >host_data;
//根據中斷號的不同,處理不同的handle, driver 的實現依賴于硬件的GIC 硬件spec, 參考規范
if (hw < 32) {
irq_set_percpu_devid(irq);
//設置真正的PIC的一級handle,處理PIC的電信號
irq_domain_set_info(d, irq, hw, &gic- >chip, d- >host_data,handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
} else {
//設置真正的PIC的一級handle,處理PIC的電信號
irq_domain_set_info(d, irq, hw, &gic- >chip, d- >host_data,handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
irqd_set_single_target(irq_desc_get_irq_data(irq_to_desc(irq)));
}
return 0;
}
//kernel/irq/chip.c
//irq > 32的電信號處理函數, 下面幾個是函數的層級,一步一步call到最終bsp注冊處理的函數當中.
void handle_fasteio_iq(struct irq_desc *desc){
...
handle_irq_event(desc);
...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event(struct irq_desc *desc)){
...
handle_irq_event_percpu(desc);
...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc){
...
retval = __handle_irq_event_percpu(desc, &flags);
add_interrupt_randomness(desc- >irq_data.irq, flags);
...
}
//kernel/irq/handle.c
//電信號上半部最終處理的函數,這里面會去call bsp 工程師注冊的上半部的函數
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags){
...
for_each_action_of_desc(desc, action) {
...
res = action- >handler(irq, action- >dev_id);//設備driver的上半部callback函數
....
switch (res) {
//上半部的返回值決定了下半部的觸發方式,下節從BSP角度去看會用到.
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);//喚醒線程化傳輸的下半部thread_fn
/* Fall through to add to randomness */
case IRQ_HANDLED:
*flags |= action- >flags;
break;
....
}
}
...
}
//kernel/irq/handle.c
//喚醒線程化傳輸的下半部thread_fn
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action){
...
wake_up_process(action- >thread);
}