【原創】X86下ipipe接管中斷/異常
版權聲明:本文為本文為部落客原創文章,轉載請註明出處。如有問題,歡迎指正。部落格地址://www.cnblogs.com/wsg1100/
X86 ipipe接管中斷/異常
本文主要講述X86 下xenomai ipipe是如何接管中斷的,關於異常將會放到雙核異常處理介紹。
一、回顧
上篇文章(X86中斷/異常與APIC)我們詳細介紹了X86平台中斷處理機制:
X86平台有256個中斷向量,表示256個異常或中斷,前32個vector為處理器保留用作異常處理,從32到255的vector編號被指定為用戶定義的中斷,不被處理器保留。 這些中斷通常分配給外部I / O設備(部分固定為APIC中斷,如LAPIC Timer、溫度中斷等),以使這些設備能夠將中斷髮送到處理器,每個vector用一個門描述符來表示,也稱為中斷門,其結構入下。
描述符大小為128位,其主要保存了段選擇符、許可權和中斷處理程式入口地址。在電腦的記憶體里,會保存一個中斷描述符表(IDT),共256項。為了直接定位中斷描述符表,每個CPU都有個特殊的暫存器IDTR
來保存IDT的在記憶體中的位置。
當CPU收到一個中斷/異常後,CPU 執行以下流程:
- 讀取由IDTR暫存器保存的IDT(中斷向量表)中對應的門描述符。CPU將vector乘以16作為偏移地址來找到該vector的中斷描述符條目(32位系統是乘以8)。
- 從中斷門描述符中得到保存的段選擇符。
- 根據段選擇符獲取對於的段描述符。
- 進行DPL特權級檢查。
- 切換堆棧。
- 壓棧保存原來上下文。
- 執行IDT中的中斷服務程式。
- 返回原來上下文。
(保護模式下的中斷處理,圖來源://blog.csdn.net/qq_39376747/article/details/113736525?spm=1001.2014.3001.5501)
本文從軟體的角度,來看Linux中這個流程是怎樣的,著重於硬體相關部分,只有這部分涉及ipipe,linux通用的中斷子系統不涉及,所以linux通用的中斷子系統本文不做描述。
二、X86 linux異常中斷處理
1. 中斷門及IDT
CPU主要將門分為三種:任務門,中斷門,陷阱門。雖然CPU把門描述符分為了三種,但是linux為了處理更多種情況,把門描述符分為了五種,分別為中斷門,系統門,系統中斷門,陷阱門,任務門;但其存儲結構與CPU定義的門不變。門結構如下:
linux中中斷門由結構體struct gate_struct
描述,如下:
struct idt_bits {
u16 ist : 3, /*提供切換到新堆棧以進行中斷處理的功能*/
zero : 5,
type : 5,/*IDT條目類型:中斷,陷阱,任務門*/
dpl : 2,/*描述符許可權級別*/
p : 1;/*段是否處於記憶體中*/
} __attribute__((packed));
struct gate_struct {
u16 offset_low; /*中斷處理程式入口點的偏移低15bit*/
u16 segment; /*GDT或LDT中的程式碼段選擇子*/
struct idt_bits bits;
u16 offset_middle;/*中斷處理程式入口點的偏移中15bit*/
#ifdef CONFIG_X86_64
u32 offset_high;/*中斷處理程式入口點的偏移高32bit*/
u32 reserved;
#endif
} __attribute__((packed));
五種門結構可通過宏INTG(_vector, _addr)
、SYSG(_vector, _addr)
、ISTG(_vector, _addr)
、SISTG(_vector, _addr)
、TSKG(_vector, _addr)
來初始化。
/*arch\x86\kernel\idt.c*/
#define DPL0 0x0
#define DPL3 0x3
#define DEFAULT_STACK 0
#define G(_vector, _addr, _ist, _type, _dpl, _segment) \
{ \
.vector = _vector, \
.bits.ist = _ist, \
.bits.type = _type, \
.bits.dpl = _dpl, \
.bits.p = 1, \
.addr = _addr, \
.segment = _segment, \
}
/* Interrupt gate */
#define INTG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)
/* System interrupt gate */
#define SYSG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)
/* Interrupt gate with interrupt stack */
#define ISTG(_vector, _addr, _ist) \
G(_vector, _addr, _ist, GATE_INTERRUPT, DPL0, __KERNEL_CS)
/* System interrupt gate with interrupt stack */
#define SISTG(_vector, _addr, _ist) \
G(_vector, _addr, _ist, GATE_INTERRUPT, DPL3, __KERNEL_CS)
/* Task gate */
#define TSKG(_vector, _gdt) \
G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)
linux中vector 0-31、APIC和SMP相關門描述使用這幾個宏進行初始化,其餘中斷門描述符會通過函數set_intr_gate()
進行初始化。
static void set_intr_gate(unsigned int n, const void *addr)
{
struct idt_data data;
BUG_ON(n > 0xFF);/*大於255,出錯*/
memset(&data, 0, sizeof(data));
data.vector = n; /*vector*/
data.addr = addr; /*中斷程式入口地址*/
data.segment = __KERNEL_CS;/*內核程式碼段*/
data.bits.type = GATE_INTERRUPT; //門類型
data.bits.p = 1;
idt_setup_from_table(idt_table, &data, 1, false);/*寫入idt_table,不記錄到bitmap*/
}
中斷描述符表IDT 由數組idt_table[256]
描述,用來保存每個CPU的256個Vector的中斷門描述符:
/* Must be page-aligned because the real IDT is used in a fixmap. */
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss; /*IDT_ENTRIES = 256*/
保存中斷描述符表地址的特殊暫存器IDTR在Linux程式碼中使用struct desc_ptr
表示:
struct desc_ptr {
unsigned short size; /*16bit*/
unsigned long address; /*32bit*/
} __attribute__((packed)) ;
內核需要將itd_table
存儲到IDTR寄存中,中斷時CPU才能正確處理,Linux中用定義了一個idt_desc
變數來存放全局IDT資訊:
struct desc_ptr idt_descr __ro_after_init = {
.size = (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
.address = (unsigned long) idt_table,
};
通過指令lidt
將 idt_desc保存到IDTR暫存器:
static inline void native_load_idt(const struct desc_ptr *dtr)
{
asm volatile("lidt %0"::"m" (*dtr));
}
2. 初始化門描述符
中斷向量表中保存的是中斷和異常描述符。我們知道,內核需要經過多個階段才完成啟動。在啟動過程中,也會產生一些異常,這些異常輔助完成內核啟動工作,所以各個階段的中斷異常函數是不同的,這主要分為4個部分,1-3部門為各個啟動階段異常和陷阱的描述符(vector 0-31),第4部分為中斷描述符初始化(vector 32-255):
第一部分:引導程式結束後,進入head_64.s
後,start_kernel()
執行之前的early(早期)階段產生的異常處理,主要是處理page_fault
。
第二部分:start_kernel()
執行過程中,cpu_init()
準備TSS段前,此時異常處理堆棧還為準備好,填充DEFAULT_STACK 上運行的早期陷阱門,有debug、page_fault、int3。
第三部分:以上關於異常和陷阱的描述符只是臨時填充使用,最終的異常描述符將在trap_init()
中完整初始化,填充每個CPU完整的異常處理gate,cpu_init()
會設置每個CPU的idtr暫存器。
第四部分: 中斷描述符初始化,包含SMP、APIC中斷。
2.1 早期異常處理
在x86_64_start_kernel()
函數中,進入通用和獨立於體系結構的內核程式碼之前,做的最後一個工作就是填充early_idt_handle
,填充函數為 idt_setup_early_handler()
。
void __init idt_setup_early_handler(void)
{
int i;
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i])
#ifdef CONFIG_X86_32
for ( ; i < NR_VECTORS; i++)
set_intr_gate(i, early_ignore_irq);
#endif
load_idt(&idt_descr);
}
/*arch\x86\include\asm\segment.h*/
#define NUM_EXCEPTION_VECTORS 32
#define EARLY_IDT_HANDLER_SIZE 9
extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];
中斷向量 0-31的處理程式的入口設置為early_idt_handler_array[vector]
,set_intr_gate()
函數將early_idt_handler_array
按IDT條目格式填充到idt_table
,中斷向量32-255中斷處理入口設置為early_ignore_irq
。
early_idt_handler_array
裡面是什麼?在哪兒定義?early_idt_handler_array
在arch/x86/kernel/entry_64.S
中定義,彙編程式碼循環填充32個中斷入口,可以看到這個階段產生的中斷和異常統一由early_idt_handler_common
函數處理:
ENTRY(early_idt_handler_array)
i = 0 /*循環初始量*/
.rept NUM_EXCEPTION_VECTORS /*循環32*/
.if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0
UNWIND_HINT_IRET_REGS
pushq $0 # Dummy error code, to make stack frame uniform
.else
UNWIND_HINT_IRET_REGS offset=8
.endif
pushq $i # 72(%rsp) Vector number
jmp early_idt_handler_common /*執行中斷處理*/
UNWIND_HINT_IRET_REGS
i = i + 1
.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
.endr
UNWIND_HINT_IRET_REGS offset=16
END(early_idt_handler_array)
可以看到使用彙編宏生成32個一樣的異常的中斷處理程式。
處理流程為 ,如果異常具有錯誤程式碼,那麼我們什麼也不做;如果異常沒有錯誤程式碼,則將零壓入堆棧。 這樣做是因為堆棧是統一的。 之後,將vector編號壓入堆棧,然後跳轉到Early_idt_handler_common
,這是目前的階段所有異常中斷的處理程式。
early_idt_handler_array
數組每項有九個位元組,代表可選的錯誤程式碼壓棧、vcetor壓棧和跳轉到Early_idt_handler_common
三條指令。 可以在使用objdump util
查看:
$ objdump -D vmlinux
...
...
...
ffffffff81fe5000 <early_idt_handler_array>:
ffffffff81fe5000: 6a 00 pushq $0x0
ffffffff81fe5002: 6a 00 pushq $0x0
ffffffff81fe5004: e9 17 01 00 00 jmpq ffffffff81fe5120 <early_idt_han
dler_common>
ffffffff81fe5009: 6a 00 pushq $0x0
ffffffff81fe500b: 6a 01 pushq $0x1
ffffffff81fe500d: e9 0e 01 00 00 jmpq ffffffff81fe5120 <early_idt_han
dler_common>
ffffffff81fe5012: 6a 00 pushq $0x0
ffffffff81fe5014: 6a 02 pushq $x2
...
...
...
我們知道,CPU在調用中斷處理程式之前將暫存器flags、CS和RIP壓入堆棧。 因此,在執 early_idt_handler_common
之前,堆棧將包含以下數據:
|--------------------|
| %rflags |
| %cs |
| %rip |
| error code |
| vector number |<-- %rsp
|--------------------|
現在,讓我們看一下early_idt_handler_common
具體實現。 它位於相同的arch/x86/kernel/head_64.S
彙編文件中。 這裡有一個標誌位early_recursion_flag
,來防止在early_idt_handler_common
遞歸,進入前:
early_idt_handler_common:
cld
incl early_recursion_flag(%rip)
/*通用暫存器保存堆棧上:*/
pushq %rsi /* pt_regs->si */
movq 8(%rsp), %rsi /* RSI = vector number */
movq %rdi, 8(%rsp) /* pt_regs->di = RDI */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq %rax /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
pushq %rbx /* pt_regs->bx */
pushq %rbp /* pt_regs->bp */
pushq %r12 /* pt_regs->r12 */
pushq %r13 /* pt_regs->r13 */
pushq %r14 /* pt_regs->r14 */
pushq %r15 /* pt_regs->r15 */
UNWIND_HINT_REGS
cmpq $14,%rsi /* Page fault? */
jnz 10f /*非 page fault*/
GET_CR2_INTO(%rdi) /* Can clobber any volatile register if pv */
call early_make_pgtable /*早期創建頁表*/
andl %eax,%eax
jz 20f /* All good */
10:
movq %rsp,%rdi /* RDI = pt_regs; RSI is already trapnr */
call early_fixup_exception /*處理其他異常*/
20:
decl early_recursion_flag(%rip)
jmp restore_regs_and_return_to_kernel
END(early_idt_handler_common)
從中斷處理程式返回前,我們需要這樣做以防止暫存器的錯誤值。 此後,我們檢查向量編號,如果它是Page Fault
,則將值從cr2
放入rdi
暫存器(Page Fault異常會將訪問產生異常的地址放到cr2暫存器中),並調用early_make_pgtable
處理Page Fault異常。我們只了解異常發生及處理的過程,具體是怎樣處理的不關心,所以不再描述。
2.2 start_kernel中的異常向量初始化一
start_kernel()
執行過程中,cpu_init()
準備TSS段前,setup_arch()
中首先將debug(vector 1)、breakpoint(vector 3)、Page Fault(vector 14)異常處理條目添加到idt_table
。
void __init idt_setup_early_traps(void)
{
idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts),
true);
load_idt(&idt_descr);
}
static const __initconst struct idt_data early_idts[] = {
INTG(X86_TRAP_DB, debug),
SYSG(X86_TRAP_BP, int3),
#ifdef CONFIG_X86_32
INTG(X86_TRAP_PF, page_fault),
#endif
};
根據異常使用的中斷堆棧、特權級別、中斷類型不一樣使用不同的宏進行定義異常處理條目,當前堆棧還沒準備好,使用DEFAULT_STACK:
#define DEFAULT_STACK 0
/* Interrupt gate */
#define INTG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)
/* System interrupt gate *//*SYSG 代表DPL或特權級別,DPL3*/
#define SYSG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)
中斷處理函數debug
、int3
、page_fault
在arch\x86\entry\entry_64.S
中定義:
/*\arch\x86\entry\entry_64.S*/
idtentry debug do_debug has_error_code=0 paranoid=1 trapnr=1
idtentry int3 do_int3 has_error_code=0 trapnr=3
idtentry page_fault do_page_fault has_error_code=1 trapnr=14
idtentry stack_segment do_stack_segment has_error_code=1 trapnr=12
每個異常處理程式可以由兩部分組成。 第一部分是通用部分,所有異常處理程式都相同。 異常處理程式應將通用暫存器保存在堆棧上,如果異常來自用戶空間(處於不同特權等級),則應切換到內核堆棧,並將控制權轉移到異常處理程式的第二部分。 異常處理程式的第二部分完成某些工作取決於什麼異常。 例如,page fault異常處理程式應找到給定地址的虛擬頁面,invalid opcode異常處理程式應發送SIGILL訊號等。
異常處理程式處理入口使用idtentry
宏定義:
.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
......
END(\sym)
.endm
idtentry
是一個宏,有五個參數:
sym
—使用.globl name
定義全局符號,該符號將是異常處理程式入口點的名稱。do_sym
—表示異常處理程式的具體處理函數。has_error_code
—是否具有中斷錯誤程式碼,對於如debug和int3等沒有提供錯誤碼的異常,idtentry內部偽造一個錯誤碼-1。
最後兩個是可選參數:
paranoid
— 此參數= 1,則切換到特殊堆棧,定義是來自用戶空間還是來自異常處理程式,確定的最簡單方法是通過判斷CS段暫存器中的CPL或當前特權級別。如果等於3,則來自用戶空間,如果等於零,則來自內核空間:;shift_ist
— 中斷期間切換的堆棧
2.3 idtentry宏(DB異常為例)
以早期debug為例,看一下idtentry宏的實現:
idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
在早期發生中斷之後,當前堆棧將具有以下格式:
如果需要切換到特殊堆棧,檢查給定的參數是否正確。
/* Sanity check */
.if \shift_ist != -1 && \paranoid == 0
.error "using shift_ist requires paranoid=1"
.endif
如果中斷向量號具有與之相關的錯誤程式碼,則將錯誤程式碼壓入堆棧。對於未提供錯誤碼的異常,偽造一個錯誤碼放入堆棧,不僅是偽造的錯誤程式碼。此外,-1還代表無效的系統調用號碼,因此不會觸發系統調用重新啟動邏輯.
.if \has_error_code == 0
pushq $-1 /* ORIG_RAX: no syscall to restart */
.endif
檢查來自用戶空間的中斷.ORIRG_RAX宏為120位元組。 通用暫存器將佔用這120個位元組,因為在中斷處理期間將所有暫存器存儲在堆棧中。
.if \paranoid < 2
testb $3, CS-ORIG_RAX(%rsp) /* If coming from userspace, switch stacks */
jnz .Lfrom_usermode_switch_stack_\@
.endif
.if \paranoid
call paranoid_entry /**/
.else
call error_entry
.endif
在這裡,我們檢查CS中的第一位和第二位。 CS暫存器包含段選擇子,其中前兩位是RPL。 所有特權級別都是0到3範圍內的整數,其中最小的數字對應於最高的特權。 所以如果中斷來自內核模式,我們稱為paranoid_entry,否則跳轉到標籤.Lfrom_usermode_switch_stack_\@
上。 在paranoid_entry
中,我們將所有通用暫存器存儲在堆棧中,並在需要時將用戶gs切換到內核gs上:
ENTRY(paranoid_entry)
UNWIND_HINT_FUNC
cld
PUSH_AND_CLEAR_REGS save_ret=1
ENCODE_FRAME_POINTER 8
movl $1, %ebx
movl $MSR_GS_BASE, %ecx
rdmsr
testl %edx, %edx
js 1f /* negative -> in kernel */
SWAPGS
xorl %ebx, %ebx
1:
SAVE_AND_SWITCH_TO_KERNEL_CR3 scratch_reg=%rax save_reg=%r14
ret
END(paranoid_entry)
在接下來的步驟中,我們將pt_regs指針指向rdi,如果有錯誤程式碼,則將其保存在rsi中,然後從arch / x86 / kernel / traps.c調用中斷處理程式-do_debug。
movq %rsp, %rdi /* pt_regs pointer */
.if \has_error_code
movq ORIG_RAX(%rsp), %rsi /* get error code */
movq $-1, ORIG_RAX(%rsp) /* no syscall to restart */
.else
xorl %esi, %esi /* no error code */
.endif
.if \shift_ist != -1
subq $EXCEPTION_STKSZ, CPU_TSS_IST(\shift_ist)
.endif
call \do_sym /*二級異常處理程式*/
與其他處理程式一樣,do_debug也有兩個參數:
- pt_regs-是顯示一組CPU暫存器的結構,這些暫存器保存在進程的記憶體區域中;
- 錯誤程式碼-中斷的錯誤程式碼。
中斷處理程式完成工作後,調用paranoid_exit以恢復堆棧,如果中斷來自那裡,則打開用戶空間並調用iret。 就這樣。 當然,這還不是全部:),但是我們將在有關中斷的單獨章節中更深入地了解。
/* these procedures expect "no swapgs" flag in ebx */
.if \paranoid
jmp paranoid_exit
.else
jmp error_exit
.endif
這是早期#DB中斷的idtentry宏的一般視圖。 所有中斷都與此實現類似,並且也使用idtentry進行了定義。
2.4 start_kernel中的異常初始化二-trap_init()
系統中有個used_vectors
變數,是一個bitmap,它用於記錄中斷向量表中哪些中斷已經被系統註冊和使用,哪些未被註冊使用。
void __init idt_setup_traps(void)
{
idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}
static const __initconst struct idt_data def_idts[] = {
INTG(X86_TRAP_DE, divide_error),
INTG(X86_TRAP_NMI, nmi),
INTG(X86_TRAP_BR, bounds),
INTG(X86_TRAP_UD, invalid_op),
INTG(X86_TRAP_NM, device_not_available),
INTG(X86_TRAP_OLD_MF, coprocessor_segment_overrun),
INTG(X86_TRAP_TS, invalid_TSS),
INTG(X86_TRAP_NP, segment_not_present),
INTG(X86_TRAP_SS, stack_segment),
INTG(X86_TRAP_GP, general_protection),
INTG(X86_TRAP_SPURIOUS, spurious_interrupt_bug),
INTG(X86_TRAP_MF, coprocessor_error),
INTG(X86_TRAP_AC, alignment_check),
INTG(X86_TRAP_XF, simd_coprocessor_error),
#ifdef CONFIG_X86_32
TSKG(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS),
#else
INTG(X86_TRAP_DF, double_fault),
#endif
INTG(X86_TRAP_DB, debug),
#ifdef CONFIG_X86_MCE
INTG(X86_TRAP_MC, &machine_check),
#endif
SYSG(X86_TRAP_OF, overflow),
#if defined(CONFIG_IA32_EMULATION)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_compat),
#elif defined(CONFIG_X86_32)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),
#endif
};
入口函數還是由宏idtentry
定義:
idtentry divide_error do_divide_error has_error_code=0 trapnr=0
idtentry overflow do_overflow has_error_code=0 trapnr=4
idtentry bounds do_bounds has_error_code=0 trapnr=5
idtentry invalid_op do_invalid_op has_error_code=0 trapnr=6
idtentry device_not_available do_device_not_available has_error_code=0 trapnr=7
idtentry double_fault do_double_fault has_error_code=1 paranoid=2 trapnr=8
idtentry coprocessor_segment_overrun do_coprocessor_segment_overrun has_error_code=0 trapnr=9
idtentry invalid_TSS do_invalid_TSS has_error_code=1 trapnr=10
idtentry segment_not_present do_segment_not_present has_error_code=1 trapnr=11
idtentry spurious_interrupt_bug do_spurious_interrupt_bug has_error_code=0 trapnr=15
idtentry coprocessor_error do_coprocessor_error has_error_code=0 trapnr=16
idtentry alignment_check do_alignment_check has_error_code=1 trapnr=17
idtentry simd_coprocessor_error do_simd_coprocessor_error has_error_code=0 trapnr=19
到這,異常和陷阱已經初始化完畢,內核也已經開始使用新的中斷向量表了,BIOS的中斷向量表就已經遺棄,不再使用了。至於各種異常具體處理函數過程分析忽略。
2.5 初始中斷門描述符
上面內核已經完成異常和陷阱門初始化,下面進行進行中斷門的初始化,中斷門的初始化必須提到IRQ,所以會簡單帶過架構無關的Linux中斷子系統的知識。中斷門的初始化也是處於start_kernel()
函數中,分為兩個部分,分別是early_irq_init()
和init_IRQ()
。early_irq_init()
是第一步的初始化,其工作主要是跟硬體無關的一些初始化,比如一些變數的初始化,分配必要的記憶體等。init_IRQ()
是第二步,其主要就是關於硬體部分的初始化了。
2.5.1 IRQ
IRQ:在PIC和單核時代,irq、vector、pin這個概念的確是合三為一的,irq就是PIC控制器的pin引腳,irq也暗示著中斷優先順序,例如IRQ0比IRQ3有著更高的優先順序。當進入MP多核時代,多核CPU下中斷處理帶來很多問題(如如何決定哪個中斷在哪個核上處理,如何保證各核上中斷負載均衡等),為了解決這些問題,vector、pin等概念都從irq中剝離出來,irq不再含有特定體系架構下中斷控制器的硬體屬性,只是內核中對中斷的一個通用的軟體抽象,與特定硬體解耦,增強其通用性。
在linux kernel中,我們使用下面兩個ID來標識一個來自外設的中斷:
1、IRQ number。CPU需要為每一個外設中斷編號,我們稱之IRQ Number。這個IRQ number是一個虛擬的interrupt ID,和硬體無關,僅僅是被CPU用來標識一個外設中斷。
2、HW interrupt ID。對於interrupt controller而言,它收集了多個外設的interrupt request line並向上傳遞,因此,interrupt controller需要對外設中斷進行編碼。Interrupt controller用HW interrupt ID來標識外設的中斷。在interrupt controller級聯的情況下,僅僅用HW interrupt ID已經不能唯一標識一個外設中斷,還需要知道該HW interrupt ID所屬的interrupt controller(HW interrupt ID在不同的Interrupt controller上是會重複編碼的)。
這樣,CPU和interrupt controller在標識中斷上就有了一些不同的概念,但是,對於驅動工程師而言,我們和CPU視角是一樣的,我們只希望得到一個IRQ number,而不關係具體是那個interrupt controller上的那個HW interrupt ID。這樣一個好處是在中斷相關的硬體發生變化的時候,驅動軟體不需要修改。因此,linux kernel中的中斷子系統需要提供一個將HW interrupt ID映射到IRQ number上來的機制。(來自蝸窩科技)
上面說到的HW interrupt ID即我們說到中斷向量vector 32-255。
2.5.2 early_irq_init
int __init early_irq_init(void)
{
/*irq描述符計數器,循環計數器,記憶體節點和irq_desc描述符*/
int i, initcnt, node = first_online_node;
struct irq_desc *desc;
init_irq_default_affinity();
initcnt = arch_probe_nr_irqs();/*體系結構相關的程式碼來決定預先分配的中斷描述符的個數 */
printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
NR_IRQS, nr_irqs, initcnt);//33024 1448 16
/*NR_IRQS是irq描述符的最大數量,或者換句話說是最大中斷數,其值取決於CONFIG_X86_IO_APIC內核配置選項的狀態*/
if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
nr_irqs = IRQ_BITMAP_BITS;
if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
initcnt = IRQ_BITMAP_BITS;
if (initcnt > nr_irqs)
nr_irqs = initcnt;
/*遍歷所有需要在循環中分配的中斷描述符,並為描述符分配空間並插入到irq_desc_tree*/
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL);/*分配中斷描述符*/
set_bit(i, allocated_irqs);/*設定已經alloc的flag*/
irq_insert_desc(i, desc);/*irq與desc映射,使用radix tree*/
}
return arch_early_irq_init();/*對IO_APIC做早期初始化*/
}
1.init_irq_default_affinity()
我們知道,當硬體(如磁碟控制器或鍵盤)需要處理器注意時,它會拋出一個中斷。中斷告訴處理器發生了某些事情,處理器應該中斷當前進程並處理傳入事件。為了防止多個設備發送相同的中斷,建立了IRQ系統,linux為電腦系統中的每個設備分配了自己特定的IRQ,使其中斷是唯一的。Linux內核可以指派特定的IRQ到特定的處理器處理,這就是SMP IRQ affinity,它允許我們控制系統如何響應各種硬體事件。
2.首先調用arch_probe_nr_irqs()
獲取預先分配的irq數量initcnt
.
3.確定nr_irqs
數量
4.為預先分配的irq分配irq_desc
,並在Bitmap allocated_irqs
中標記已分配,將分配的irq與irq_desc
插入基數樹irq_desc_tree
,irq作為索引對應irq_desc
地址作為葉子節點。irq_desc_tree
是全局變數,定義如下:
/*include\linux\radix-tree.h*/
struct radix_tree_root {
gfp_t gfp_mask; /*標示記憶體從哪分配*/
struct radix_tree_node __rcu *rnode;
};
#define RADIX_TREE_INIT(mask) { \
.gfp_mask = (mask), \
.rnode = NULL, \
}
/*kernel\irq\irqdesc.c*/
static RADIX_TREE(irq_desc_tree, GFP_KERNEL);
static void irq_insert_desc(unsigned int irq, struct irq_desc *desc)
{
radix_tree_insert(&irq_desc_tree, irq, desc);
}
5.調用arch_early_irq_init()
對IO_APIC做早期初始化,為預先分配的irq 0 to 15分配apic_chip_data
空間,並設置到每個irq的irq_desc
。
創建Irq_domain x86_vector_domain
,並將其設置為irq_default_domain
,創建子irq_domain pci_msi_domain_info
、 htirq_domain
;
irq_desc
結構體是Linux中斷管理的基礎,代表一個中斷描述符在include/linux/irqdesc.h
中定義。
struct irq_desc {
struct irq_common_data irq_common_data;/* */
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;/*每個CPU的中斷狀態*/
#ifdef CONFIG_IPIPE
void (*ipipe_ack)(struct irq_desc *desc);
void (*ipipe_end)(struct irq_desc *desc);
#endif /* CONFIG_IPIPE */
irq_flow_handler_t handle_irq;/*高級irq事件處理程式*/
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list 標識IRQ發生時要調用的中斷服務程式; */
unsigned int status_use_accessors;/*包含中斷源的狀態,它是enum來自include/linux/irq.h的值和在同一源程式碼文件中定義的不同宏的組合;*/
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* 如果IRQ已啟用,則為正值,如果0已至少禁用一次*/
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* IRQ線路上發生中斷的計數器*/
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;/*未處理中斷的計數*/
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;/*用於序列化對IRQ描述符的訪問的自旋鎖;*/
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;/*等待重新平衡的中斷;*/
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
/*中斷描述符的所有者。中斷描述符可以從模組中分配。該欄位需要在提供中斷的模組上證明refcount;*/
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
關於linux中斷子系統後續文章介紹。
2.5.3 init_IRQ
init_IRQ
函數是特定於體系結構的,在arch/X86/kenel/irqinit.c
中定義,函數init_IRQ
首先每個CPU初始化一個irq_desc
指針數組vector_irq[]
;
/*\arch\ia64\include\asm\native\irq.h*/
#define NR_VECTORS 256
/*arch\x86\include\asm\hw_irq.h*/
typedef struct irq_desc* vector_irq_t[NR_VECTORS];
/*arch\x86\kernel\irqinit.c*/
DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
[0 ... NR_VECTORS - 1] = VECTOR_UNUSED, /*[256]*/
};
void __init init_IRQ(void)
{
int i;
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);/*使用中斷描述符填充vector_irq*/
x86_init.irqs.intr_init();/*native_init_IRQ ;在\arch\x86\kernel\x86_init.c中設置 */
}
在init_IRQ
開頭,填充cpu0上的vector_irq[]
0x30-0x3f項,用於ISA中斷,其實就是0-15,經過ISA_IRQ_VECTOR
宏轉換變為0x30-0x3f,如果這些IRQ由PIC等傳統中斷控制器處理,則此配置在引導後可能是靜態的,這裡只是預先填充,如果系統使用的是APIC,這些向量空間將會被動態分配使用。
/*arch\x86\kernel\i8259.c*/
struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY,/*16*/
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.probe = probe_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
};
struct legacy_pic *legacy_pic = &default_legacy_pic;
/*arch\x86\include\asm\i8259.h*/
static inline int nr_legacy_irqs(void)
{
return legacy_pic->nr_legacy_irqs;
}
只之後調用x86_init.irqs.intr_init()
,x86_init
一個平台相關結構,指向平台設置相關的功能,還有與記憶體、處理器、定時器等相關的函數,這裡中斷只用到irqs段:
struct x86_init_ops x86_init __initdata = {
/*與記憶體資源有關*/
.resources = {
.probe_roms = probe_roms,
.reserve_resources = reserve_standard_io_resources,
.memory_setup = e820__memory_setup_default,
},
/*與解析多處理器配置表有關*/
.mpparse = {
.mpc_record = x86_init_uint_noop,
.setup_ioapic_ids = x86_init_noop,
.mpc_apic_id = default_mpc_apic_id,
.smp_read_mpc_oem = default_smp_read_mpc_oem,
.mpc_oem_bus_info = default_mpc_oem_bus_info,
.find_smp_config = default_find_smp_config,
.get_smp_config = default_get_smp_config,
},
/*IRQ相關*/
.irqs = {
.pre_vector_init = init_ISA_irqs,
.intr_init = native_init_IRQ,
.trap_init = x86_init_noop,
},
.oem = {
.arch_setup = x86_init_noop,
.banner = default_banner,
},
.paging = {
.pagetable_init = native_pagetable_init,
},
.timers = {
.setup_percpu_clockev = setup_boot_APIC_clock,
.timer_init = hpet_time_init,
.wallclock_init = x86_init_noop,
},
.iommu = {
.iommu_init = iommu_init_noop,
},
.pci = {
.init = x86_default_pci_init,
.init_irq = x86_default_pci_init_irq,
.fixup_irqs = x86_default_pci_fixup_irqs,
},
.hyper = {
.init_platform = x86_init_noop,
.x2apic_available = bool_x86_init_noop,
.init_mem_mapping = x86_init_noop,
},
};
……..
發現展開有很多東西o(╥﹏╥)o,我們關注Vector 32-255的中斷門是怎樣填充的。所以僅看下圖即可,native_init_IRQ
處理過程如下;
用與APIC與SMP的vector在arch\x86\kernel\idt.c
義如下,中斷入口均在rch\x86\entry\entry_64.S
使用宏picinterrupt
、apicinterrupt2
、apicinterrupt3
定義:
#ifdef CONFIG_SMP
apicinterrupt3 IRQ_MOVE_CLEANUP_VECTOR irq_move_cleanup_interrupt smp_irq_move_cleanup_interrupt
apicinterrupt3 REBOOT_VECTOR reboot_interrupt smp_reboot_interrupt
#endif
apicinterrupt LOCAL_TIMER_VECTOR apic_timer_interrupt smp_apic_timer_interrupt
apicinterrupt X86_PLATFORM_IPI_VECTOR x86_platform_ipi smp_x86_platform_ipi
......
#ifdef CONFIG_IPIPE
apicinterrupt2 IPIPE_HRTIMER_VECTOR ipipe_hrtimer_interrupt
#endif
apicinterrupt ERROR_APIC_VECTOR error_interrupt smp_error_interrupt
apicinterrupt SPURIOUS_APIC_VECTOR spurious_interrupt smp_spurious_interrupt
#ifdef CONFIG_IRQ_WORK
apicinterrupt IRQ_WORK_VECTOR irq_work_interrupt smp_irq_work_interrupt
#endif
/*arch\x86\kernel\idt.c*/
/*
* The APIC and SMP idt entries
*/
static const __initconst struct idt_data apic_idts[] = {
#ifdef CONFIG_SMP
INTG(RESCHEDULE_VECTOR, reschedule_interrupt), /*重新調度*/
INTG(CALL_FUNCTION_VECTOR, call_function_interrupt),/**/
INTG(CALL_FUNCTION_SINGLE_VECTOR, call_function_single_interrupt),
INTG(IRQ_MOVE_CLEANUP_VECTOR, irq_move_cleanup_interrupt),
INTG(REBOOT_VECTOR, reboot_interrupt),
#ifdef CONFIG_IPIPE
INTG(IPIPE_RESCHEDULE_VECTOR, ipipe_reschedule_interrupt),
INTG(IPIPE_CRITICAL_VECTOR, ipipe_critical_interrupt),
#endif
#endif
#ifdef CONFIG_X86_THERMAL_VECTOR
INTG(THERMAL_APIC_VECTOR, thermal_interrupt),
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
INTG(THRESHOLD_APIC_VECTOR, threshold_interrupt),
#endif
#ifdef CONFIG_X86_MCE_AMD
INTG(DEFERRED_ERROR_VECTOR, deferred_error_interrupt),
#endif
#ifdef CONFIG_X86_LOCAL_APIC
INTG(LOCAL_TIMER_VECTOR, apic_timer_interrupt),
INTG(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi),
# ifdef CONFIG_HAVE_KVM
INTG(POSTED_INTR_VECTOR, kvm_posted_intr_ipi),
INTG(POSTED_INTR_WAKEUP_VECTOR, kvm_posted_intr_wakeup_ipi),
INTG(POSTED_INTR_NESTED_VECTOR, kvm_posted_intr_nested_ipi),
# endif
# ifdef CONFIG_IRQ_WORK
INTG(IRQ_WORK_VECTOR, irq_work_interrupt),
# endif
#ifdef CONFIG_X86_UV
INTG(UV_BAU_MESSAGE, uv_bau_message_intr1),
#endif
INTG(SPURIOUS_APIC_VECTOR, spurious_interrupt),
INTG(ERROR_APIC_VECTOR, error_interrupt),
#ifdef CONFIG_IPIPE
INTG(IPIPE_HRTIMER_VECTOR, ipipe_hrtimer_interrupt),
#endif
#endif
};
vector 32-236除APIC和SMP固定的vector外,其餘中斷的中斷入口地址在rq_entries_start
內定義,均將vector壓入棧後調用do_IRQ
處理。
該宏定義在arch\x86\entry\entry_64.S
中定義,32位系統相應的在entry_32.S
中。
.align 8
ENTRY(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR/*定義0x20-0xec個中斷*/
/*NR_VECTORS-FIRST_EXTERNAL_VECTOR個函數入口
.rept表示循環 236-32 */
.rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
UNWIND_HINT_IRET_REGS
pushq $(~vector+0x80) /* 壓入中斷向量號 然後跳轉到common_interrupt */
jmp common_interrupt
.align 8 /*8位元組對齊*/
vector=vector+1
.endr
END(irq_entries_start)
該宏使用rept
宏循環創建FIRST_EXTERNAL_VECTOR
個中斷入口,入口處的指令均為jmp common_interrupt
,這些中斷全都跳轉到common_interrupt
處理。common_interrupt
處程式碼如下。
common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
interrupt do_IRQ
/* 0(%rsp): old RSP */
ret_from_intr:
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel /*返回內核態*/
/* Interrupt came from user space */
GLOBAL(retint_user)/*返回用戶態*/
mov %rsp,%rdi
call prepare_exit_to_usermode
retint_user_early:
TRACE_IRQS_IRETQ
common_interrupt
首先判斷中斷向量號範圍,然後由do_IRQ
函數去處理中斷,接下來就是熟悉的linux中斷處理子系統了。
三、linux x86_64中斷/異常處理總結
總結X86中斷的基本框架,X86 系統中有256個vector,用來識別中斷或異常的類型,vector 0-31處理器保留,有固定的用途, 從32到255的vector編號被指定為用戶定義的中斷,不被處理器保留。 這些中斷通常分配給外部I / O設備(部分固定為APIC中斷),以使這些設備能夠將中斷髮送到處理器,每個vector的處理程式都保存在一個特殊的位置–IDT(中斷描述符表),IDT的基地址保存在暫存器IDTR,在64位x86下IDT是一個16位元組描述的數組(32位系統為8位元組),當中斷髮生時CPU將vector乘以16(32位系統是乘以8)來找到IDT中的對應條目idt_data,然後根據條目資訊跳轉到處理入口執行中斷和異常處理。
四、ipipe接管中斷處理
上面知道了打修補程式前Linux的異常處理流程,可以想到,ipipe要優先處理中斷那就不能給linux中斷子系統去處理,只能從中斷入口去攔截,ipipe也的確是這樣做的,打修補程式後的入口程式碼如下:
common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
#ifdef CONFIG_IPIPE
interrupt __ipipe_handle_irq /*IPIPE中斷攔截*/
testl %eax, %eax
jnz ret_from_intr
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel_early
jmp retint_user_early
#else
interrupt do_IRQ
#endif
/* 0(%rsp): old RSP */
ret_from_intr:
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel /*返回內核態*/
/* Interrupt came from user space */
GLOBAL(retint_user)/*返回用戶態*/
mov %rsp,%rdi
call prepare_exit_to_usermode
retint_user_early:
TRACE_IRQS_IRETQ
可以看到,啟用了CONFIG_IPIPE
後中斷就不是給do_IRQ()
處理了,而是由__ipipe_handle_irq()
處理,同樣對於APIC中斷:
/*
* APIC interrupts.
*/
#ifdef CONFIG_IPIPE
.macro apicinterrupt2 num sym
ENTRY(\sym)
UNWIND_HINT_IRET_REGS
ASM_CLAC
pushq $~(\num)
.Lcommon_\sym:
interrupt __ipipe_handle_irq /*IPIPE中斷攔截*/
testl %eax, %eax
jnz ret_from_intr
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel_early
jmp retint_user_early
END(\sym)
.endm
.macro apicinterrupt3 num sym do_sym
apicinterrupt2 \num \sym
.endm
#else /* !CONFIG_IPIPE */
.macro apicinterrupt3 num sym do_sym
ENTRY(\sym)
UNWIND_HINT_IRET_REGS
ASM_CLAC
pushq $~(\num)
.Lcommon_\sym:
interrupt \do_sym
jmp ret_from_intr
END(\sym)
.endm
#endif /* !CONFIG_IPIPE */
除CPU保留的vector 0-31外,均被ipipe插入函數__ipipe_handle_irq()
攔截,這是保證xenomai實時性的基礎,對於處理器保留的trap vector 0-31,不是由__ipipe_handle_irq()
處理,涉及xenomai核與linux核異常處理後面會單獨詳細說。
接下來分析__ipipe_handle_irq()
是怎麼實現中斷處理的。
int __ipipe_handle_irq(struct pt_regs *regs)
{
struct ipipe_percpu_data *p = __ipipe_raw_cpu_ptr(&ipipe_percpu);
int irq, vector = regs->orig_ax, flags = 0;
struct pt_regs *tick_regs;
struct irq_desc *desc;
if (likely(vector < 0)) {
vector = ~vector;
if (vector >= FIRST_SYSTEM_VECTOR) /*>0xec*/
irq = ipipe_apic_vector_irq(vector);
else {
desc = __this_cpu_read(vector_irq[vector]);/*獲取irq_desc*/
if (IS_ERR_OR_NULL(desc)) {
#ifdef CONFIG_X86_LOCAL_APIC
__ack_APIC_irq();
#endif
.....
}
irq = irq_desc_get_irq(desc);/*獲取irq*/
}
} else { /* 軟中斷*/
irq = vector;
flags = IPIPE_IRQF_NOACK;
}
ipipe_trace_irqbegin(irq, regs);
……
__ipipe_dispatch_irq(irq, flags); /*中斷分發*/
……
return 1;
}
中斷到達哪個CPU就由哪個CPU 調用__ipipe_handle_irq()
處理,首先先獲取到記錄管理該cpu上運行的情況的ipipe_percpu_data(ipipe domian管理),然後取出產生中斷的vector,x86架構中,產生中斷的vector是存放在暫存器orig_ax
中的,然後將vector轉換為中斷號irq
,最後調用__ipipe_dispatch_irq(irq, flags)
進行進一步處理,ipipeline是怎樣在兩個內核之間管理中斷的,在後面文章中介紹。