Linux中斷(interrupt)子系統之二:arch相關的硬體封裝層
Linux的通用中斷子系統的一個設計原則就是把底層的硬體實現儘可能地隱藏起來,使得驅動程式的開發人員不用關注底層的實現,要實現這個目標,核心的開發者們必須把硬體相關的內容剝離出來,然後定義一些列標準的介面供上層訪問,上層的開發人員只要知道這些介面即可完成對中斷的進一步處理和控制。對底層的封裝主要包括兩部分:
- 實現不同體系結構中斷入口,這部分程式碼通常用asm實現;
- 中斷控制器進行封裝和實現;
本文的內容正是要討論硬體封裝層的實現細節。我將以ARM體系進行介紹,大部分的程式碼位於核心程式碼樹的arch/arm/目錄內。
/*****************************************************************************************************/
宣告:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
1. CPU的中斷入口
我們知道,arm的異常和復位向量表有兩種選擇,一種是低端向量,向量地址位於0x00000000,另一種是高階向量,向量地址位於0xffff0000,Linux選擇使用高階向量模式,也就是說,當異常發生時,CPU會把PC指標自動跳轉到始於0xffff0000開始的某一個地址上:
地址 | 異常種類 |
---|---|
FFFF0000 | 復位 |
FFFF0004 | 未定義指令 |
FFFF0008 | 軟中斷(swi) |
FFFF000C | Prefetch abort |
FFFF0010 | Data abort |
FFFF0014 | 保留 |
FFFF0018 | IRQ |
FFFF001C | FIQ |
中斷向量表在arch/arm/kernel/entry_armv.S中定義,為了方便討論,下面只列出部分關鍵的程式碼:
-
.globl __stubs_start
-
__stubs_start:
-
vector_stub irq, IRQ_MODE, 4
-
.long __irq_usr @ 0 (USR_26 / USR_32)
-
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
-
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
-
.long __irq_svc @ 3 (SVC_26 / SVC_32)
-
vector_stub dabt, ABT_MODE, 8
-
.long __dabt_usr @ 0 (USR_26 / USR_32)
-
.long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
-
.long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
-
.long __dabt_svc @ 3 (SVC_26 / SVC_32)
-
vector_fiq:
-
disable_fiq
-
subs pc, lr, #4
-
......
-
.globl __stubs_end
-
__stubs_end:
-
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
-
.globl __vectors_start
-
__vectors_start:
-
ARM( swi SYS_ERROR0 )
-
THUMB( svc #0 )
-
THUMB( nop )
-
W(b) vector_und + stubs_offset
-
W(ldr) pc, .LCvswi + stubs_offset
-
W(b) vector_pabt + stubs_offset
-
W(b) vector_dabt + stubs_offset
-
W(b) vector_addrexcptn + stubs_offset
-
W(b) vector_irq + stubs_offset
-
W(b) vector_fiq + stubs_offset
-
.globl __vectors_end
-
__vectors_end:
程式碼被分為兩部分:
- 第一部分是真正的向量跳轉表,位於__vectors_start和__vectors_end之間;
- 第二部分是處理跳轉的部分,位於__stubs_start和__stubs_end之間;
vector_stub irq, IRQ_MODE, 4
以上這一句把巨集展開後實際上就是定義了vector_irq,根據進入中斷前的cpu模式,分別跳轉到__irq_usr或__irq_svc。
vector_stub dabt, ABT_MODE, 8
以上這一句把巨集展開後實際上就是定義了vector_dabt,根據進入中斷前的cpu模式,分別跳轉到__dabt_usr或__dabt_svc。
系統啟動階段,位於arch/arm/kernel/traps.c中的early_trap_init()被呼叫:
-
void __init early_trap_init(void)
-
{
-
......
-
/*
-
* Copy the vectors, stubs and kuser helpers (in entry-armv.S)
-
* into the vector page, mapped at 0xffff0000, and ensure these
-
* are visible to the instruction stream.
-
*/
-
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
-
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
-
......
-
}
以上兩個memcpy會把__vectors_start開始的程式碼拷貝到0xffff0000處,把__stubs_start開始的程式碼拷貝到0xFFFF0000+0x200處,這樣,異常中斷到來時,CPU就可以正確地跳轉到相應中斷向量入口並執行他們。
圖1.1 Linux中ARM體系的中斷向量拷貝過程
對於系統的外部裝置來說,通常都是使用IRQ中斷,所以我們只關注__irq_usr和__irq_svc,兩者的區別是進入和退出中斷時是否進行使用者棧和核心棧之間的切換,還有程序排程和搶佔的處理等,這些細節不在這裡討論。兩個函式最終都會進入irq_handler這個巨集:
-
.macro irq_handler
-
#ifdef CONFIG_MULTI_IRQ_HANDLER
-
ldr r1, =handle_arch_irq
-
mov r0, sp
-
adr lr, BSYM(9997f)
-
ldr pc, [r1]
-
#else
-
arch_irq_handler_default
-
#endif
-
9997:
-
.endm
如果選擇了MULTI_IRQ_HANDLER配置項,則意味著允許平臺的程式碼可以動態設定irq處理程式,平臺程式碼可以修改全域性變數:handle_arch_irq,從而可以修改irq的處理程式。這裡我們討論預設的實現:arch_irq_handler_default,它位於arch/arm/include/asm/entry_macro_multi.S中:
-
.macro arch_irq_handler_default
-
get_irqnr_preamble r6, lr
-
1: get_irqnr_and_base r0, r2, r6, lr
-
movne r1, sp
-
@
-
@ routine called with r0 = irq number, r1 = struct pt_regs *
-
@
-
adrne lr, BSYM(1b)
-
bne asm_do_IRQ
-
......
get_irqnr_preamble和get_irqnr_and_base兩個巨集由machine級的程式碼定義,目的就是從中斷控制器中獲得IRQ編號,緊接著就呼叫asm_do_IRQ,從這個函式開始,中斷程式進入C程式碼中,傳入的引數是IRQ編號和暫存器結構指標,這個函式在arch/arm/kernel/irq.c中實現:
-
/*
-
* asm_do_IRQ is the interface to be used from assembly code.
-
*/
-
asmlinkage void __exception_irq_entry
-
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
-
{
-
handle_IRQ(irq, regs);
-
}
到這裡,中斷程式完成了從asm程式碼到C程式碼的傳遞,並且獲得了引起中斷的IRQ編號。
2. 初始化
與通用中斷子系統相關的初始化由start_kernel()函式發起,呼叫流程如下圖所視:
圖2.1 通用中斷子系統的初始化
- 首先,在setup_arch函式中,early_trap_init被呼叫,其中完成了第1節所說的中斷向量的拷貝和重定位工作。
- 然後,start_kernel發出early_irq_init呼叫,early_irq_init屬於與硬體和平臺無關的通用邏輯層,它完成irq_desc結構的記憶體申請,為它們其中某些欄位填充預設值,完成後呼叫體系相關的arch_early_irq_init函式完成進一步的初始化工作,不過ARM體系沒有實現arch_early_irq_init。
- 接著,start_kernel發出init_IRQ呼叫,它會直接呼叫所屬板子machine_desc結構體中的init_irq回撥。machine_desc通常在板子的特定程式碼中,使用MACHINE_START和MACHINE_END巨集進行定義。
- machine_desc->init_irq()完成對中斷控制器的初始化,為每個irq_desc結構安裝合適的流控handler,為每個irq_desc結構安裝irq_chip指標,使他指向正確的中斷控制器所對應的irq_chip結構的例項,同時,如果該平臺中的中斷線有多路複用(多箇中斷公用一個irq中斷線)的情況,還應該初始化irq_desc中相應的欄位和標誌,以便實現中斷控制器的級聯。
3. 中斷控制器的軟體抽象:struct irq_chip
正如上一篇文章Linux中斷(interrupt)子系統之一:中斷系統基本原理所述,所有的硬體中斷在到達CPU之前,都要先經過中斷控制器進行彙集,合乎要求的中斷請求才會通知cpu進行處理,中斷控制器主要完成以下這些功能:
- 對各個irq的優先順序進行控制;
- 向CPU發出中斷請求後,提供某種機制讓CPU獲得實際的中斷源(irq編號);
- 控制各個irq的電氣觸發條件,例如邊緣觸發或者是電平觸發;
- 使能(enable)或者遮蔽(mask)某一個irq;
- 提供巢狀中斷請求的能力;
- 提供清除中斷請求的機制(ack);
- 有些控制器還需要CPU在處理完irq後對控制器發出eoi指令(end of interrupt);
- 在smp系統中,控制各個irq與cpu之間的親緣關係(affinity);
通用中斷子系統把中斷控制器抽象為一個數據結構:struct irq_chip,其中定義了一系列的操作函式,大部分多對應於上面所列的某個功能:
-
struct irq_chip {
-
const char *name;
-
unsigned int (*irq_startup)(struct irq_data *data);
-
void (*irq_shutdown)(struct irq_data *data);
-
void (*irq_enable)(struct irq_data *data);
-
void (*irq_disable)(struct irq_data *data);
-
void (*irq_ack)(struct irq_data *data);
-
void (*irq_mask)(struct irq_data *data);
-
void (*irq_mask_ack)(struct irq_data *data);
-
void (*irq_unmask)(struct irq_data *data);
-
void (*irq_eoi)(struct irq_data *data);
-
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
-
int (*irq_retrigger)(struct irq_data *data);
-
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
-
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
-
void (*irq_bus_lock)(struct irq_data *data);
-
void (*irq_bus_sync_unlock)(struct irq_data *data);
-
void (*irq_cpu_online)(struct irq_data *data);
-
void (*irq_cpu_offline)(struct irq_data *data);
-
void (*irq_suspend)(struct irq_data *data);
-
void (*irq_resume)(struct irq_data *data);
-
void (*irq_pm_shutdown)(struct irq_data *data);
-
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
-
unsigned long flags;
-
/* Currently used only by UML, might disappear one day.*/
-
#ifdef CONFIG_IRQ_RELEASE_METHOD
-
void (*release)(unsigned int irq, void *dev_id);
-
#endif
-
};
各個欄位解釋如下:
name 中斷控制器的名字,會出現在 /proc/interrupts中。irq_startup 第一次開啟一個irq時使用。
irq_shutdown 與irq_starup相對應。
irq_enable 使能該irq,通常是直接呼叫irq_unmask()。
irq_disable 禁止該irq,通常是直接呼叫irq_mask,嚴格意義上,他倆其實代表不同的意義,disable表示中斷控制器根本就不響應該irq,而mask時,中斷控制器可能響應該irq,只是不通知CPU,這時,該irq處於pending狀態。類似的區別也適用於enable和unmask。
irq_ack 用於CPU對該irq的迴應,通常表示cpu希望要清除該irq的pending狀態,準備接受下一個irq請求。
irq_mask 遮蔽該irq。
irq_unmask 取消遮蔽該irq。
irq_mask_ack 相當於irq_mask + irq_ack。
irq_eoi 有些中斷控制器需要在cpu處理完該irq後發出eoi訊號,該回調就是用於這個目的。
irq_set_affinity 用於設定該irq和cpu之間的親緣關係,就是通知中斷控制器,該irq發生時,那些cpu有權響應該irq。當然,中斷控制器會在軟體的配合下,最終只會讓一個cpu處理本次請求。
irq_set_type 設定irq的電氣觸發條件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。
irq_set_wake 通知電源管理子系統,該irq是否可以用作系統的喚醒源。
以上大部分的函式介面的引數都是irq_data結構指標,irq_data結構的由來在上一篇文章已經說過,這裡僅貼出它的定義,各欄位的意義請參考註釋:
-
/**
-
* struct irq_data - per irq and irq chip data passed down to chip functions
-
* @irq: interrupt number
-
* @hwirq: hardware interrupt number, local to the interrupt domain
-
* @node: node index useful for balancing
-
* @state_use_accessors: status information for irq chip functions.
-
* Use accessor functions to deal with it
-
* @chip: low level interrupt hardware access
-
* @domain: Interrupt translation domain; responsible for mapping
-
* between hwirq number and linux irq number.
-
* @handler_data: per-IRQ data for the irq_chip methods
-
* @chip_data: platform-specific per-chip private data for the chip
-
* methods, to allow shared chip implementations
-
* @msi_desc: MSI descriptor
-
* @affinity: IRQ affinity on SMP
-
*
-
* The fields here need to overlay the ones in irq_desc until we
-
* cleaned up the direct references and switched everything over to
-
* irq_data.
-
*/
-
struct irq_data {
-
unsigned int irq;
-
unsigned long hwirq;
-
unsigned int node;
-
unsigned int state_use_accessors;
-
struct irq_chip *chip;
-
struct irq_domain *domain;
-
void *handler_data;
-
void *chip_data;
-
struct msi_desc *msi_desc;
-
#ifdef CONFIG_SMP
-
cpumask_var_t affinity;
-
#endif
-
};
根據裝置使用的中斷控制器的型別,體系架構的底層的開發只要實現上述介面中的各個回撥函式,然後把它們填充到irq_chip結構的例項中,最終把該irq_chip例項註冊到irq_desc.irq_data.chip欄位中,這樣各個irq和中斷控制器就進行了關聯,只要知道irq編號,即可得到對應到irq_desc結構,進而可以通過chip指標訪問中斷控制器。
4. 進入流控處理層
進入C程式碼的第一個函式是asm_do_IRQ,在ARM體系中,這個函式只是簡單地呼叫handle_IRQ:
-
/*
-
* asm_do_IRQ is the interface to be used from assembly code.
-
*/
-
asmlinkage void __exception_irq_entry
-
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
-
{
-
handle_IRQ(irq, regs);
-
}
handle_IRQ本身也不是很複雜:
-
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
-
{
-
struct pt_regs *old_regs = set_irq_regs(regs);
-
irq_enter();
-
/*
-
* Some hardware gives randomly wrong interrupts. Rather
-
* than crashing, do something sensible.
-
*/
-
if (unlikely(irq >= nr_irqs)) {
-
if (printk_ratelimit())
-
printk(KERN_WARNING "Bad IRQ%u\n", irq);
-
ack_bad_irq(irq);
-
} else {
-
generic_handle_irq(irq);
-
}
-
/* AT91 specific workaround */
-
irq_finish(irq);
-
irq_exit();
-
set_irq_regs(old_regs);
-
}
irq_enter主要是更新一些系統的統計資訊,同時在__irq_enter巨集中禁止了程序的搶佔:
-
#define __irq_enter() \
-
do { \
-
account_system_vtime(current); \
-
add_preempt_count(HARDIRQ_OFFSET); \
-
trace_hardirq_enter(); \
-
} while (0)
CPU一旦響應IRQ中斷後,ARM會自動把CPSR中的I位置位,表明禁止新的IRQ請求,直到中斷控制轉到相應的流控層後才通過local_irq_enable()開啟。你可能會奇怪,既然此時的irq中斷都是都是被禁止的,為何還要禁止搶佔?這是因為要考慮中斷巢狀的問題,一旦流控層或驅動程式主動通過local_irq_enable打開了IRQ,而此時該中斷還沒處理完成,新的irq請求到達,這時程式碼會再次進入irq_enter,在本次巢狀中斷返回時,核心不希望進行搶佔排程,而是要等到最外層的中斷處理完成後才做出排程動作,所以才有了禁止搶佔這一處理。
下一步,generic_handle_irq被呼叫,generic_handle_irq是通用邏輯層提供的API,通過該API,中斷的控制被傳遞到了與體系結構無關的中斷流控層:
-
int generic_handle_irq(unsigned int irq)
-
{
-
struct irq_desc *desc = irq_to_desc(irq);
-
if (!desc)
-
return -EINVAL;
-
generic_handle_irq_desc(irq, desc);
-
return 0;
-
}
最終會進入該irq註冊的流控處理回撥中:
-
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
-
{
-
desc->handle_irq(irq, desc);
-
}
5. 中斷控制器的級聯
在實際的裝置中,經常存在多箇中斷控制器,有時多箇中斷控制器還會進行所謂的級聯。為了方便討論,我們把直接和CPU相連的中斷控制器叫做根控制器,另外一些和跟控制器相連的叫子控制器。根據子控制器的位置,我們把它們分為兩種型別:
- 機器級別的級聯 子控制器位於SOC內部,或者子控制器在SOC的外部,但是是某個板子系列的標準配置,如圖5.1的左邊所示;
- 裝置級別的級聯 子控制器位於某個外部裝置中,用於彙集該裝置發出的多箇中斷,如圖5.1的右邊所示;
圖5.1 中斷控制器的級聯型別
對於機器級別的級聯,級聯的初始化程式碼理所當然地位於板子的初始化程式碼中(arch/xxx/mach-xxx),因為只要是使用這個板子或SOC的裝置,必然要使用這個子控制器。而對於裝置級別的級聯,因為該裝置並不一定是系統的標配裝置,所以中斷控制器的級聯操作應該在該裝置的驅動程式中實現。機器裝置的級聯,因為得益於事先已經知道子控制器的硬體連線資訊,核心可以方便地為子控制器保留相應的irq_desc結構和irq編號,處理起來相對簡單。裝置級別的級聯則不一樣,驅動程式必須動態地決定組合裝置中各個子裝置的irq編號和irq_desc結構。本章我只討論機器級別的級聯,裝置級別的關聯可以使用同樣的原理,也可以實現為共享中斷,我會在本系列接下來的文章中討論。
要實現中斷控制器的級聯,要使用以下幾個的關鍵資料結構欄位和通用中斷邏輯層的API:
irq_desc.handle_irq irq的流控處理回撥函式,子控制器在把多個irq彙集起來後,輸出端連線到根控制器的其中一個irq中斷線輸入腳,這意味著,每個子控制器的中斷髮生時,CPU一開始只會得到根控制器的irq編號,然後進入該irq編號對應的irq_desc.handle_irq回撥,該回調我們不能使用流控層定義好的幾個流控函式,而是要自己實現一個函式,該函式負責從子控制器中獲得irq的中斷源,並計算出對應新的irq編號,然後呼叫新irq所對應的irq_desc.handle_irq回撥,這個回撥使用流控層的標準實現。
irq_set_chained_handler() 該API用於設定根控制器與子控制器相連的irq所對應的irq_desc.handle_irq回撥函式,並且設定IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST標誌,這幾個標誌保證驅動程式不會錯誤地申請該irq,因為該irq已經被作為級聯irq使用。
irq_set_chip_and_handler() 該API同時設定irq_desc中的handle_irq回撥和irq_chip指標。
以下例子程式碼位於:/arch/arm/plat-s5p/irq-eint.c:
-
int __init s5p_init_irq_eint(void)
-
{
-
int irq;
-
for (irq = IRQ_EINT(0); irq <= IRQ_EINT(15); irq++)
-
irq_set_chip(irq, &s5p_irq_vic_eint);
-
for (irq = IRQ_EINT(16); irq <= IRQ_EINT(31); irq++) {
-
irq_set_chip_and_handler(irq, &s5p_irq_eint, handle_level_irq);
-
set_irq_flags(irq, IRQF_VALID);
-
}
-
irq_set_chained_handler(IRQ_EINT16_31, s5p_irq_demux_eint16_31);
-
return 0;
-
}
該SOC晶片的外部中斷:IRQ_EINT(0)到IRQ_EINT(15),每個引腳對應一個根控制器的irq中斷線,它們是正常的irq,無需級聯。IRQ_EINT(16)到IRQ_EINT(31)經過子控制器彙集後,統一連線到根控制器編號為IRQ_EINT16_31這個中斷線上。可以看到,子控制器對應的irq_chip是s5p_irq_eint,子控制器的irq預設設定為電平中斷的流控處理函式handle_level_irq,它們通過API:irq_set_chained_handler進行設定。如果根控制器有128箇中斷線,IRQ_EINT0--IRQ_EINT15通常佔據128內的某段連續範圍,這取決於實際的物理連線。IRQ_EINT16_31因為也屬於跟控制器,所以它的值也會位於128以內,但是IRQ_EINT16--IRQ_EINT31通常會在128以外的某段範圍,這時,代表irq數量的常量NR_IRQS,必須考慮這種情況,定義出超過128的某個足夠的數值。級聯的實現主要依靠編號為IRQ_EINT16_31的流控處理程式:s5p_irq_demux_eint16_31,它的最終實現類似於以下程式碼:
-
static inline void s5p_irq_demux_eint(unsigned int start)
-
{
-
u32 status = __raw_readl(S5P_EINT_PEND(EINT_REG_NR(start)));
-
u32 mask = __raw_readl(S5P_EINT_MASK(EINT_REG_NR(start)));
-
unsigned int irq;
-
status &= ~mask;
-
status &= 0xff;
-
while (status) {
-
irq = fls(status) - 1;
-
generic_handle_irq(irq + start);
-
status &= ~(1 << irq);
-
}
-
}
在獲得新的irq編號後,它的最關鍵的一句是呼叫了通用中斷邏輯層的API:generic_handle_irq,這時它才真正地把中斷控制權傳遞到中斷流控層中來。