原创X86下ipipe接管中断/异常

Posted 木多

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原创X86下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 初始中断门描述符

上面内核已经完成异常和陷阱门初始化,下面进行进行中断门的初始化,中断门的初始化必须提到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_infohtirq_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处理过程如下;

ipipe初始化

用与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
......
#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,然后根据条目信息跳转到处理入口执行中断和异常处理。

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)进行进一步处理,ipipeline是怎样在两个内核之间管理中断的,在后面文章中介绍。

以上是关于原创X86下ipipe接管中断/异常的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Linux内核---中断和异常(x86平台)

17_页面异常接管

x86 - 操作系统:中断陷阱异常故障终止

异常和TCP通讯

中断异常和系统调用

masm x86 程序集崩溃中的 DOS 中断