【原創】X86 linux異常處理與Ipipe接管中斷/異常


版權聲明:本文為本文為博主原創文章,轉載請注明出處。如有問題,歡迎指正。博客地址:https://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用一個門描述符來表示,也稱為中斷門,其結構入下。

idt-64

描述符大小為128位,其主要保存了段選擇符、權限和中斷處理程序入口地址。在計算機的內存里,會保存一個中斷描述符表(IDT),共256項。為了直接定位中斷描述符表,每個CPU都有個特殊的寄存器IDTR來保存IDT的在內存中的位置。

當CPU收到一個中斷/異常后,CPU 執行以下流程:

  1. 讀取由IDTR寄存器保存的IDT(中斷向量表)中對應的門描述符。CPU將vector乘以16作為偏移地址來找到該vector的中斷描述符條目(32位系統是乘以8)。
  2. 從中斷門描述符中得到保存的段選擇符。
  3. 根據段選擇符獲取對於的段描述符。
  4. 進行DPL特權級檢查。
  5. 切換堆棧。
  6. 壓棧保存原來上下文。
  7. 執行IDT中的中斷服務程序。
  8. 返回原來上下文。

2021020710205874

(保護模式下的中斷處理,圖來源:https://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表示:

IDT

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_arrayarch/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)

中斷處理函數debugint3page_faultarch\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,它用於記錄中斷向量表中哪些中斷已經被系統注冊和使用,哪些未被注冊使用。

ipipe初始化

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 初始中斷門描述符

上面內核已經完成異常和陷阱門初始化,下面進行進行中斷門的初始化,中斷門的初始化也是處於start_kernel()函數中,分為兩個部分,分別是early_irq_init()init_IRQ()early_irq_init()是第一步的初始化,其工作主要是跟硬件無關的一些初始化,比如一些變量的初始化,分配必要的內存等。init_IRQ()是第二步,其主要就是關於硬件部分的初始化了,其中就會填充剩余的中斷門。

2.5.1 IRQ number與HW interrupt ID

IRQ:在PIC和單核時代,irq、vector、pin這個概念的確是合三為一的,irq就是PIC控制器的pin引腳,irq也暗示着中斷優先級,例如IRQ0比IRQ3有着更高的優先級。當進入MP多核時代,多核CPU下中斷處理帶來很多問題(如如何決定哪個中斷在哪個核上處理,如何保證各核上中斷負載均衡等),為了解決這些問題,vector、pin等概念都從irq中剝離出來,irq不再含有特定體系架構下中斷控制器的硬件屬性,只是linux內核中對中斷的一個通用的軟件抽象,與特定硬件解耦,增強其通用性。

在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,上面內核已經完成異常和陷阱門初始化,剩下主要有APIC和SMP中斷,硬件連接決定了他們就是固定的vector。

所以僅看下圖中填充IDT部分即可,native_init_IRQ處理過程如下;

ipipe初始化

2.5.2 APIC與SMP IDT填充

用與APIC與SMP的vector在arch\x86\kernel\idt.c義如下,中斷入口均在rch\x86\entry\entry_64.S使用宏picinterruptapicinterrupt2apicinterrupt3定義:

#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
......
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

APIC 和 SMP idt 描述符數組,用INTG宏初始化,只需要把這些項拷貝到IDT表中就行了。

/*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
};
2.5.3 剩余IDT填充

除了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,然后根據條目信息跳轉到處理入口執行中斷和異常處理。

idt_index-1-m

四、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-inter-g-g

接下來分析__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_dataipipe domian管理),然后取出產生中斷的vector,x86架構中,產生中斷的vector是存放在寄存器orig_ax中的,然后將vector轉換為中斷號irq,最后調用__ipipe_dispatch_irq(irq, flags)進行進一步處理,ipipe如何管理和處理中斷?ipipeline是怎樣在兩個內核之間管理中斷的?下篇文章會介紹。

參考鏈接

1. linux inside


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM