【原创】X86 linux异常处理与Ipipe接管中断/异常
- X86 ipipe接管中断/异常
- 一、回顾
- 二、X86 linux异常中断处理
- 1. 中断门及IDT
- 2. 初始化门描述符
- 2.1 早期异常处理
- 2.2 start_kernel中的异常向量初始化一
- 2.3 idtentry宏(DB异常为例)
- 2.4 start_kernel中的异常初始化二-trap_init()
- 2.5 初始中断门描述符
- 2.5.1 IRQ number与HW interrupt ID
- 2.5.2 APIC与SMP IDT填充
- 2.5.3 剩余IDT填充
- 三、linux x86_64中断/异常处理总结
- 四、ipipe接管中断处理
- 参考链接
版权声明:本文为本文为博主原创文章,转载请注明出处。如有问题,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/
X86 ipipe接管中断/异常
本文主要讲述X86 下xenomai ipipe是如何接管中断的,关于异常将会放到双核异常处理介绍。
一、回顾
上篇文章()我们详细介绍了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中的中断服务程序。
- 返回原来上下文。
(保护模式下的中断处理,图来源: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
表示:
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 :
ffffffff81fe5000: 6a 00 pushq $0x0
ffffffff81fe5002: 6a 00 pushq $0x0
ffffffff81fe5004: e9 17 01 00 00 jmpq ffffffff81fe5120
ffffffff81fe5009: 6a 00 pushq $0x0
ffffffff81fe500b: 6a 01 pushq $0x1
ffffffff81fe500d: e9 0e 01 00 00 jmpq ffffffff81fe5120
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 初始中断门描述符
上面内核已经完成异常和陷阱门初始化,下面进行进行中断门的初始化,中断门的初始化也是处于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
处理过程如下;
2.5.2 APIC与SMP IDT填充
用与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
......
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,然后根据条目信息跳转到处理入口执行中断和异常处理。
四、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)
进行进一步处理,ipipe如何管理和处理中断?ipipeline是怎样在两个内核之间管理中断的?下篇文章会介绍。
参考链接
1. linux inside