参考链接:
https://docs.hust.openatom.club/linux-insides-zh/interrupts
什么是中断
中断就是当软件或者硬件需要使用 CPU 时引发的 事件(event),通过APIC来处理中断请求。当一个中断发生时,操作系统必须确保下面的步骤顺序:
-
内核必须暂停执行当前进程(取代当前的任务);
-
内核必须搜索中断处理程序并且转交控制权(执行中断处理程序);
-
中断处理程序结束之后,被中断的进程能够恢复执行。
每个中断处理程序的地址都保存在中断描述符表(Interrupt Descriptor Table, IDT)中。处理器使用一个唯一的数字来识别中断和异常的类型,这个数字被称为 中断标识码(vector number)。一个中断标识码就是一个 IDT 的标识。中断标识码范围是有限的,从 0 到 255。从 0 到 31 的 32 个中断标识码被处理器保留,用作处理架构定义的异常和中断。从 32 到 255 的中断标识码设计为用户定义中断并且不被系统保留。这些中断通常分配给外部 I/O 设备,使这些设备可以发送中断给处理器。
中断分为两个主要类型:硬中断和软中断。硬中断是外部或者硬件引起的中断,软中断是软件引起的中断。
中断可以分为 可屏蔽的(maskable) 和 不可屏蔽的(non-maskable)。可屏蔽的中断可以被阻塞,使用 x86_64 的指令 - sti 和 cli。这两个指令修改了在中断寄存器中的 IF 标识位, sti 指令设置 IF 标识,cli 指令清除这个标识。
如果多个异常或者中断同时发生,处理器以事先设定好的中断优先级处理他们。
中断描述符表(IDT)
IDT 保存了中断和异常处理程序的入口指针,IDT 的表项被称为 门(gates)。中断描述符表 可以在线性地址空间和基址的任何地方被加载,只要在 x86 上以 8 字节对齐,在 x86_64 上以 16 字节对齐。IDT 的基址存储在一个特殊的寄存器 - IDTR。在 x86 上 IDTR 寄存器是 48 位,包含了下面的信息:
+-----------------------------------+----------------------+
| | |
| Base address of the IDT | Limit of the IDT |
| | |
+-----------------------------------+----------------------+
47 16 15 0
以下是设置一个空IDT的代码:
/*
* Set up the IDT
*/
// 此处的 gdt_prt不是代表 GDTR寄存器而是代表 IDTR寄存器,
// 之所以在Linux内核代码中没有idt_ptr结构体,是因为其与
// gdt_prt具有相同的结构而仅仅是名字不同,因此没必要定义两个重复的数据结构。
static void setup_idt(void)
{
static const struct gdt_ptr null_idt = {0, 0};
asm volatile("lidtl %0" : : "m" (null_idt));
}
struct gdt_ptr {
u16 len;
u32 ptr;
} __attribute__((packed));
IDT 入口结构体,它是一个在 x86 中被称为门的 16 字节数组。它拥有下面的结构:
127 96
+-------------------------------------------------------------------------------+
| |
| Reserved |
| |
+--------------------------------------------------------------------------------
95 64
+-------------------------------------------------------------------------------+
| |
| Offset 63..32 |
| |
+-------------------------------------------------------------------------------+
63 48 47 46 44 42 39 34 32
+-------------------------------------------------------------------------------+
| | | D | | | | | | |
| Offset 31..16 | P | P | 0 |Type |0 0 0 | 0 | 0 | IST |
| | | L | | | | | | |
-------------------------------------------------------------------------------+
31 16 15 0
+-------------------------------------------------------------------------------+
| | |
| Segment Selector | Offset 15..0 |
| | |
+-------------------------------------------------------------------------------+
处理器使用中断或异常的唯一的数字或 中断标识码 作为索引来寻找对应的 中断描述符表 的条目。在表中的 IDT 条目由下面的域组成:
-
0-15 bits - 段选择器偏移,处理器用它作为中断处理程序的入口指针基址;
-
16-31 bits - 段选择器基址,包含中断处理程序入口指针;
-
IST - 在 x86_64 上的一个新的机制,下面我们会介绍它;
-
DPL - 描述符特权级;
-
P - 段存在标志;
-
48-63 bits - 中断处理程序基址的第二部分;
-
64-95 bits - 中断处理程序基址的第三部分;
-
96-127 bits - CPU 保留位。
Type 域描述了 IDT 条目的类型。有三种不同的中断处理程序:
-
中断门(Interrupt gate)
-
陷入门(Trap gate)
-
任务门(Task gate)
中断描述符表 使用 gate_desc 的数组描述:
extern gate_desc idt_table[];
#ifdef CONFIG_X86_64
...
...
...
typedef struct gate_struct64 gate_desc;
...
...
...
#endif
struct gate_struct64 {
u16 offset_low;
u16 segment;
unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
u16 offset_middle;
u32 offset_high;
u32 zero1;
} __attribute__((packed));
per-cpu 中断栈
在 x86_64 架构中,每一个活动的线程在 Linux 内核中都有一个很大的栈,这个栈的大小由 THREAD_SIZE 定义。这块栈空间保存着有用的数据,只要线程是活动状态或者僵尸状态。但是当线程在用户空间的时候,这个内核栈是空的,除了 thread_info 结构在这个栈空间的底部。
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int saved_preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1;
};
thread_info结构包含了特定体系架构相关的线程信息,在X86_64架构上内核栈是逆生成而thread_union.thread_info结构则是正生长。用一张图来描述栈内存空间,如下图所示:
+-----------------------+
| |
| |
| stack |
| |
|_______________________|
| | |
| | |
| | |
|__________↓____________| +--------------------+
| | | |
| thread_info |<----------->| task_struct |
| | | |
+-----------------------+ +--------------------+
活动的或者僵尸线程并不是在他们栈中的唯一的线程,与每一个 CPU 关联的特殊栈也存在于这个空间。Per-cpu 变量是一项内核特性,每个 CPU 上都会有一个此变量的拷贝,与CPU 关联的特殊栈就是percpu其中的一个变量。每一个 CPU 也有一个特殊的 per-cpu 栈。首先是给外部中断使用的 中断栈(interrupt stack)。Per-cpu 的中断栈在 x86_64 架构中使用 irq_stack_union 联合描述:
union irq_stack_union {
char irq_stack[IRQ_STACK_SIZE];
struct {
char gs_base[40];
unsigned long stack_canary;
};
};
gs_base 总是指向 irqstack 联合底部的 gs 寄存器。所有的 per-cpu 标志初始值为零,并且 gs 指向 per-cpu 区域的开始。而irq_stack_union 是 percpu 的第一个数据,所以gs指向中断栈地址,即栈底。
Model-Specific Registers(MSR)是x86架构CPU中的一组寄存器,它们提供了对CPU特定功能的访问和控制,其中MSR_GS_BASE保存了被 gs 寄存器指向的内存段的基址,为以后内核态和用户态切换做准备。
中断栈表 Interrupt Stack Table,IST
中断栈表中的栈 与 上章中percpu中断栈 不同,独立于percpu中断栈。当发生不可屏蔽中断、双重错误等等的时候,这个组件提供了切换到新栈的功能。IST 并不是所有的中断必须的,一些中断可以继续使用传统的栈切换模式。IST 机制在任务状态段(Task State Segment)或者 TSS 中提供了 7 个 IST 指针。TSS 是一个包含进程信息的特殊结构,用来在执行中断或者处理 Linux 内核异常的时候做栈切换。每一个指针都被 IDT 中的中断门引用。
内核中断初始化
对IDT的配置在go_to_protected_mode函数中完成,该函数首先调用了 setup_idt函数配置了IDT空表,然后将处理器的工作模式切换为 保护模式:
void go_to_protected_mode(void)
{
...
setup_idt();
...
}
static void setup_idt(void) // 见中断描述符表(IDT)代码分析
{
static const struct gdt_ptr null_idt = {0, 0};
asm volatile("lidtl %0" : : "m" (null_idt));
}
在设置完与架构相关的工作后,就进入到与平台无关的通用内核代码。首先为中断堆栈设置Stack Canary值,再禁用/使能本地中断(sti 和 cli)。
接下来是在setup_arch 函数中的early_trap_init 函数,setup_arch 函数是特定与结构的初始化。
void __init early_trap_init(void)
{
set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
#ifdef CONFIG_X86_32
set_intr_gate(X86_TRAP_PF, page_fault);
#endif
load_idt(&idt_descr);
}
// 与set_intr_gate_ist仅有一个地方不同,_set_gate 函数的第四个参数是 0x3,
// 而在 set_intr_gate_ist函数中这个值是 0x0, 这个参数代表的是 DPL或称为特权等级。
// 其中,0代表最高特权等级而 3代表最低等级。
static inline void set_system_intr_gate_ist(int n, void *addr, unsigned ist)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0x3, ist, __KERNEL_CS);
}
static inline void set_intr_gate_ist(int n, void *addr, unsigned ist)
{
BUG_ON((unsigned)n > 0xFF); // 检查了参数 n 即中断向量编号 是否不大于 0xff 或 255
_set_gate(n, GATE_INTERRUPT, addr, 0, ist, __KERNEL_CS); // _set_gate 函数将中断门设置到了 IDT 表中
}
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
// 通过 pack_gate 函数填充了一个表示 IDT 入口项的 gate_desc 类型的结构体,
// 参数包括基地址,限制范围,中断栈表, 特权等级 和中断类型。
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
// 通过 write_idt_entry 宏填入了 IDT 中,这个宏展开后是
write_idt_entry(idt_table, gate, &s); native_write_idt_entry
write_trace_idt_entry(gate, &s);
}
#define write_idt_entry(dt, entry, g) native_write_idt_entry(dt, entry, g)
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
memcpy(&idt[entry], gate, sizeof(*gate));
}
static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func,
unsigned dpl, unsigned ist, unsigned seg)
{
gate->offset_low = PTR_LOW(func);
gate->segment = __KERNEL_CS;
gate->ist = ist;
gate->p = 1;
gate->dpl = dpl;
gate->zero0 = 0;
gate->zero1 = 0;
gate->type = type;
gate->offset_middle = PTR_MIDDLE(func);
gate->offset_high = PTR_HIGH(func);
}
#define set_intr_gate(n, addr) \
do { \
BUG_ON((unsigned)n > 0xFF); \
_set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0, \
__KERNEL_CS); \
_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\
0, 0, __KERNEL_CS); \
} while (0)
这些异常允许x86_64体系结构进行早期异常处理,以便通过kgdb进行调试。
- #DB - 调试异常,将控制从被中断的进程转移到调试处理程序;
- #BP - 断点异常,由int 3指令引起。
处理上述两个异常的处理程序不在*.c文件中,而是以汇编代码存在,asmlinkage表示从汇编调用该函数。
asmlinkage void debug(void);
asmlinkage void int3(void);
在early_trap_init函数之后,调用early_trap_pf_init函数设置缺页异常。设置完后返回到start_kernel函数中,执行完setup_arch后执行trap_init函数,这个函数初始化剩余的异常处理程序。
DE(除0)、NMI(不可屏蔽中断)、OF(溢出)、BR(越界)、UD(非法操作码)、NM(设备不可用)、DF(异常中出现异常但不能处理)、CSO(协处理器段超出)、TS(无效TSS)、NP(段不存在)、SS(栈错误)、GP(General Protection)、Spurious Interrupt(伪中断)、MF(x87 FPU浮点错误)、AC(未对齐内存操作数)、MC(机器检查)、XF(SIMD浮点异常)。
set_intr_gate(X86_TRAP_DE, divide_error);
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);
set_intr_gate(X86_TRAP_OLD_MF, &coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, &invalid_TSS);
set_intr_gate(X86_TRAP_NP, &segment_not_present);
set_intr_gate_ist(X86_TRAP_SS, &stack_segment, STACKFAULT_STACK);
set_intr_gate(X86_TRAP_GP, &general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, &spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, &coprocessor_error);
set_intr_gate(X86_TRAP_AC, &alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
#endif
set_intr_gate(X86_TRAP_XF, &simd_coprocessor_error);
在early_trap_pf_init函数(缺页中断设置)之后,
下一步,填充used_vectors数组(中断的bitmap),填充前32个固定中断:
DECLARE_BITMAP(used_vectors, NR_VECTORS);
#define FIRST_EXTERNAL_VECTOR 0x20
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors)
下一步,设置ia32_syscall中断,提供在兼容模式下执行32位进程的能力,used_vectors为0x80。
#ifdef CONFIG_IA32_EMULATION
set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
下一步,将IDT映射到固定的物理地址,并写入idt_descr.address:
__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
idt_descr.address = fix_to_virt(FIX_RO_IDT);
下一步,调用cpi_init函数初始化所有的cpu状态,包括设置TSS和IST。
在trap_init函数的末尾,将idt_table复制到nmi_dit_table中,并为Debug异常和Breakpoint异常设置异常处理程序。虽然已经在前面的部分设置了这些中断门,但因为之前在early_trap_init函数中初始化,任务状态段还没有准备好,但是现在在调用cpu_init函数之后它已经准备好了:
set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
...
...
...
#ifdef CONFIG_X86_64
memcpy(&nmi_idt_table, &idt_table, IDT_ENTRIES * 16);
set_nmi_gate(X86_TRAP_DB, &debug);
set_nmi_gate(X86_TRAP_BP, &int3);
#endif
异常处理程序
每个异常处理程序可以由两部分组成。第一部分是通用部分,对于所有异常处理程序都是一样的。异常处理程序应该在堆栈上保存通用寄存器,如果来自用户空间的异常则要切换到内核堆栈,并将控制转移到异常处理程序的第二部分;第二部分,根据特定的异常完成特定的工作。例如,页面错误异常处理程序应该找到给定地址的虚拟页面,无效操作码异常处理程序应该发送SIGILL信号等等。(不可屏蔽中断的处理程序不是用sym宏定义的,而是用nmi)
两个异常处理代码在 arch/x86/entry/entry_64.S中,以idtentry宏形式定义:
idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
...
...
...
END(\sym)
.endm
- sym:定义具有.global名称的全局符号,是异常处理程序的一个条目;
- do_sym:符号名,表示异常处理程序的辅助项;
- has_error_code:是否存在异常错误码的信息;
- paranoid:如何检查当前模式,是否来自用户空间,或者不是一个异常处理程序。分为快慢两种方式;
- shift_ist:是否需要为异常处理程序打开IST堆栈。
如果异常提供了错误代码,处理器将错误代码推送到堆栈上。但debug和int3异常没有错误代码,会将假错误代码-1放入堆栈开始。-1还表示无效的系统调用号,因此不会触发系统调用重启逻辑。
.ifeq \has_error_code
pushq $-1
.endif
在堆栈上推送虚假错误码之后,为通用寄存器分配空间:
ALLOC_PT_GPREGS_ON_STACK
.macro ALLOC_PT_GPREGS_ON_STACK addskip=0
addq $-(15*8+\addskip), %rsp
.endm
+------------+
+160 | %SS |
+152 | %RSP |
+144 | %RFLAGS |
+136 | %CS |
+128 | %RIP |
+120 | ERROR CODE |
|------------|
+112 | |
+104 | |
+96 | |
+88 | |
+80 | |
+72 | |
+64 | |
+56 | |
+48 | |
+40 | |
+32 | |
+24 | |
+16 | |
+8 | |
+0 | | <- %RSP
+------------+
在为通用寄存器分配空间之后,根据paranoid做一些检查来了解异常是否来自用户空间,如果是,需要移回中断的进程堆栈或留在异常堆栈上:
.if \paranoid
.if \paranoid == 1
testb $3, CS(%rsp)
jnz 1f
.endif
call paranoid_entry
.else
call error_entry
.endif
在error_entry中,先将所有通用寄存器保存在堆栈上先前分配的区域中:
SAVE_C_REGS 8
SAVE_EXTRA_REGS 8
+------------+
+160 | %SS |
+152 | %RSP |
+144 | %RFLAGS |
+136 | %CS |
+128 | %RIP |
+120 | ERROR CODE |
|------------|
+112 | %RDI |
+104 | %RSI |
+96 | %RDX |
+88 | %RCX |
+80 | %RAX |
+72 | %R8 |
+64 | %R9 |
+56 | %R10 |
+48 | %R11 |
+40 | %RBX |
+32 | %RBP |
+24 | %R12 |
+16 | %R13 |
+8 | %R14 |
+0 | %R15 | <- %RSP
+------------+
在内核将通用寄存器保存到堆栈之后,再次检查我们来自用户空间。在调用error_entry之后,将用户态下的栈同步到内核态,并切换到内核栈:
movq %rsp, %rdi
call sync_regs
movq %rax, %rsp
asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs)
{
// task_ptr_regs宏展开为thread.sp0的地址,指向正常内核堆栈的指针
struct pt_regs *regs = task_pt_regs(current);
*regs = *eregs;
return regs;
}
#define task_pt_regs(tsk) ((struct pt_regs *)(tsk)->thread.sp0 - 1)
将指向pt_regs结构的指针传递给%rdi寄存器,该结构包含保留的通用寄存器,作为次要异常处理程序的第一个参数传递。将错误代码传递给%rsi寄存器,作为异常处理程序的第二个参数,并在堆栈上将其设置为-1,以防止重新启动系统调用:
movq %rsp, %rdi
.if \has_error_code
movq ORIG_RAX(%rsp), %rsi
movq $-1, ORIG_RAX(%rsp)
.else
xorl %esi, %esi
.endif
最后,调用二级异常处理程序:
call \do_sym
dotraplinkage void do_debug(struct pt_regs *regs, long error_code);
当内核中发生异常时,且paranoid=1。paranoid=1意味着我们应该使用我们在这部分开始看到的更慢的方式来检查我们是否真的来自内核空间。与用户态发生异常类似,首先需要检查异常是否来自内核空间,然后也是切换堆栈,调用异常处理方式也相同。除此之外,在调用异常处理程序之前的最后一步是清理新的IST堆栈帧:
.if \shift_ist != -1
subq $EXCEPTION_STKSZ, CPU_TSS_IST(\shift_ist)
.endif
如果shift_ist 不等于-1,通过shift_ist索引从中断堆栈表中获得指向堆栈的指针并设置它。最后调用异常处理程序,然后退出:
call \do_sym
jmp error_exit
所有的异常处理程序用DO_ERROR宏定义:
DO_ERROR(X86_TRAP_DE, SIGFPE, "divide error", divide_error)
DO_ERROR(X86_TRAP_OF, SIGSEGV, "overflow", overflow)
DO_ERROR(X86_TRAP_UD, SIGILL, "invalid opcode", invalid_op)
DO_ERROR(X86_TRAP_OLD_MF, SIGFPE, "coprocessor segment overrun", coprocessor_segment_overrun)
DO_ERROR(X86_TRAP_TS, SIGSEGV, "invalid TSS", invalid_TSS)
DO_ERROR(X86_TRAP_NP, SIGBUS, "segment not present", segment_not_present)
DO_ERROR(X86_TRAP_SS, SIGBUS, "stack segment", stack_segment)
DO_ERROR(X86_TRAP_AC, SIGBUS, "alignment check", alignment_check)
DO_ERROR宏有4个参数:
- 中断的矢量数;
- 将被发送到中断进程的信号号;
- 描述异常的字符串;
- 异常处理程序入口点。
这个宏定义在同一个源代码文件中,并扩展为do_handler名称的函数:
#define DO_ERROR(trapnr, signr, str, name) \
dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \
{ \
do_error_trap(regs, error_code, str, trapnr, signr); \
}
do_error_trap函数从以下两个函数开始和结束:
enum ctx_state prev_state = exception_enter();
...
...
...
exception_exit(prev_state);
Linux内核子系统中的上下文跟踪提供了内核边界探测,以跟踪具有两个层次上下文之间的转换:用户和内核上下文。exception_enter函数检查是否启用了上下文跟踪。如果启用,则exception_enter读取前一个上下文,并将其与CONTEXT_KERNEL进行比较。如果前一个上下文是用户,调用context_tracking_exit函数,通知上下文跟踪子系统处理器正在退出用户模式并进入内核模式:
if (!context_tracking_is_enabled())
return 0;
prev_ctx = this_cpu_read(context_tracking.state);
if (prev_ctx != CONTEXT_KERNEL)
context_tracking_exit(prev_ctx);
return prev_ctx;
exception_exit函数检查上下文跟踪是否启用,如果前一个上下文是user,则调用context_tracking_enter函数。context_tracking_enter函数通知上下文跟踪子系统,处理器将从内核模式进入用户模式:
static inline void exception_exit(enum ctx_state prev_ctx)
{
if (context_tracking_is_enabled()) {
if (prev_ctx != CONTEXT_KERNEL)
context_tracking_enter(prev_ctx);
}
}
在exception_enter和exception_exit之间看到以下代码,NOTIFY_STOP是一个宏,用于表示通知机制应该停止进一步的处理:
if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, signr) !=
NOTIFY_STOP) {
conditional_sti(regs);
do_trap(trapnr, signr, str, regs, error_code,
fill_trap_info(regs, signr, trapnr, &info));
}
要获得内核panic、内核oops、不可屏蔽中断或其他事件的通知,调用者需要将自己插入到notify_die链中,由notify_die函数执行。Linux内核有特殊的机制,允许内核询问什么时候发生了什么,这种机制称为通知器或通知链。通知链是一个简单的单链表。当Linux内核子系统希望收到特定事件的通知时,它会填充一个特殊的notifier_block结构,并将其传递给notifier_chain_register函数。事件可以通过调用notifier_call_chain函数发送。
如果do_error_trap中的notify_die没有返回NOTIFY_STOP,执行conditional_sti函数,检查中断标志的值并根据它启用中断:
static inline void conditional_sti(struct pt_regs *regs)
{
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}
do_trap函数定义了tsk变量,该变量具有task_struct类型,代表当前中断的进程。在定义了任务之后,我们可以看到do_trap_no_signal函数的调用:
struct task_struct *tsk = current;
if (!do_trap_no_signal(tsk, trapnr, str, regs, error_code))
return;
do_trap_no_signal函数做两项检查:
- 从虚拟8086模式来的吗?
- 从内核空间来吗?
我们将不考虑第一种情况,因为长模式不支持虚拟8086模式。在第二种情况下,我们调用fixup_exception函数,它将尝试恢复错误,如果我们不能,它将死亡。die函数打印有关堆栈、寄存器、内核模块和引起的内核错误的有用信息:
if (!fixup_exception(regs)) {
tsk->thread.error_code = error_code;
tsk->thread.trap_nr = trapnr;
die(str, regs, error_code);
}
如果我们来自用户空间,do_trap_no_signal函数将返回-1,do_trap函数将继续执行。由处理器引起的大多数异常被Linux解释为错误条件,例如被零除、无效的操作码等。当异常发生时,Linux内核向被中断的进程发送一个信号,该信号将导致异常,通知它有不正确的条件。因此,在do_trap函数中,我们需要发送带有给定数字的信号(SIGFPE表示除法错误,SIGILL表示非法指令,等等)。在此之后,我们检查是否需要打印有关中断进程的未处理信号的信息,并向被中断进程发送给定信号。
外部硬件中断
在完成trap_init函数后,下一步是调用early_irq_init函数。外部中断处理取决于中断的类型:
- I / O中断;
- 定时器中断;
- 处理机间中断。
一个I/O中断的处理程序必须足够灵活,能够同时服务多个设备。例如,在PCI总线体系结构中,几个设备可能共享相同的IRQ线。当发生I/O中断时,Linux内核必须以最简单的方式做以下事情:
- 将IRQ的值和寄存器的内容保存在内核堆栈上;
- 向正在服务IRQ线路的硬件控制器发送一个确认;
- 执行与设备相关联的中断服务程序ISR;
- 恢复寄存器并从中断中返回;
early_irq_init函数对irq_desc结构进行早期初始化,irq_desc是Linux内核中中断管理代码的基础。early_irq_init函数的实现依赖于CONFIG_SPARSE_IRQ内核配置选项,现在我们考虑在没有设置CONFIG_SPARSE_IRQ内核配置选项的情况下实现early_irq_init函数:
int __init early_irq_init(void)
{
int count, i, node = first_online_node;
struct irq_desc *desc;
...
...
...
}
#if MAX_NUMNODES > 1
#define first_online_node first_node(node_states[N_ONLINE])
#else
#define first_online_node 0
#define first_node(src) __first_node(&(src))
static inline int __first_node(const nodemask_t *srcp)
{
// 返回最小节点或第一个在线节点
return min_t(int, MAX_NUMNODES, find_first_bit(srcp->bits, MAX_NUMNODES));
}
#define MAX_NUMNODES (1 << NODES_SHIFT)
...
...
...
#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT CONFIG_NODES_SHIFT
#else
#define NODES_SHIFT 0
#endif
下一步是调用init_irq_default_affinity()函数,依赖于CONFIG_SMP内核配置选项,分配一个给定的cpumask结构:
init_irq_default_affinity();
#if defined(CONFIG_SMP)
cpumask_var_t irq_default_affinity;
static void __init init_irq_default_affinity(void)
{
alloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
cpumask_setall(irq_default_affinity);
}
#else
static void __init init_irq_default_affinity(void)
{
}
#endif
为防止多个设备发送相同的中断,建立了IRQ系统,每个设备都分配了自己的特殊IRQ,使其中断是唯一的。Linux内核可以将特定的irq分配给特定的处理器。这称为SMP IRQ affinity,它允许用户控制系统如何响应各种硬件事件(这就是为什么只有设置了CONFIG_SMP内核配置选项,系统才会有特定的实现)。在分配irq_default_affinity cpumask之后,我们可以看到printk的输出,NR_IRQS是irq描述符的最大数目,NR_IRQS取决于处理器数量和中断向量数量,在Linux内核配置过程中使用CONFIG_NR_CPUS配置选项设置处理器的数量:
printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);
下一步,将IRQ描述符数组赋值给在early_irq_init函数开头定义的irq_desc变量,并使用ARRAY_SIZE宏计算irq_desc数组的计数:
desc = irq_desc;
count = ARRAY_SIZE(irq_desc);
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq, // the highlevel irq-event handler
.depth = 1, // 如果IRQ线启用,则为0; 如果至少禁用一次,则为正值
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock), // 用于序列化对IRQ描述符的访问的自旋锁
}
};
在计算中断计数并初始化irq_desc数组之后,开始在循环中填充描述符:
for (i = 0; i < count; i++) {
desc[i].kstat_irqs = alloc_percpu(unsigned int);
alloc_masks(&desc[i], GFP_KERNEL, node);
raw_spin_lock_init(&desc[i].lock);
lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
desc_set_defaults(i, &desc[i], node, NULL);
}
使用alloc_percpu宏为irq内核统计分配percpu变量。这个宏为系统上的每个处理器分配一个给定类型对象的实例。 raw_spin_lock_init初始化自旋锁。lockdep_set_class为给定的中断描述符的锁设置锁验证器irq_desc_lock_class。desc_set_defaults设置desc中的属性,包括中断号,irq芯片等。
下一步,为给定描述符设置访问器的状态,并设置中断的禁用状态:
...
...
...
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
...
...
...
下一步,将高级中断处理程序设置为handle_bad_irq,它处理虚假的和未处理的IRQ(由于硬件还没有初始化,我们设置了这个处理程序),将irq_desc.depth 设置为1,这意味着IRQ被禁用,重置未处理中断和中断的计数:
...
...
...
desc->handle_irq = handle_bad_irq;
desc->depth = 1;
desc->irq_count = 0;
desc->irqs_unhandled = 0;
desc->name = NULL;
desc->owner = owner;
...
...
...
下一步,使用for_each_possible_cpu助手遍历所有可能的处理器,并将给定中断描述符的kstat_irqs设置为零:
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = 0;
并调用desc_smp_init函数,该函数初始化给定中断描述符的NUMA节点,设置默认SMP亲和并清除给定中断描述符的pending_mask取决于CONFIG_GENERIC_PENDING_IRQ内核配置选项的值:
static void desc_smp_init(struct irq_desc *desc, int node)
{
desc->irq_data.node = node;
cpumask_copy(desc->irq_data.affinity, irq_default_affinity);
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_clear(desc->pending_mask);
#endif
}
最后,返回arch_early_irq_init函数的返回值:
return arch_early_irq_init();
当配置CONFIG_SPARSE_IRQ后,与没配置时稍有不同,在early_irq_init函数的开头看到相同的变量定义和init_irq_default_affinity的调用:
#ifdef CONFIG_SPARSE_IRQ
int __init early_irq_init(void)
{
int i, initcnt, node = first_online_node;
struct irq_desc *desc;
init_irq_default_affinity();
...
...
...
}
#else
...
...
...
但之后调用的是arch_probe_nr_irqs:
initcnt = arch_probe_nr_irqs();
arch_probe_nr_irqs函数计算预分配IRQs的计数,并用这个数字更新nr_irqs。为什么会有预分配的irq ?还有另一种形式的中断,称为PCI中可用的消息信号中断(Message Signaled Interrupts)。设备不需要为中断请求分配一个固定的编号,而是允许在特定的物理内存地址(实际上就是在本地APIC上显示的)记录一条消息。MSI允许设备分配1、2、4、8、16或32个中断,而MSI- x允许设备分配多达2048个中断。
arch_probe_nr_irqs之后的下一个是打印IRQs数量的信息:
printk(KERN_INFO "NR_IRQS:%d nr_irqs:%d %d\n", NR_IRQS, nr_irqs, initcnt);
下一步,遍历所有需要在循环中分配的中断描述符,并为描述符分配空间并插入到irq_desc_tree基数树中:
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, NULL);
set_bit(i, allocated_irqs);
irq_insert_desc(i, desc);
}
最后,返回arch_early_irq_init函数调用的值,就像没有设置CONFIG_SPARSE_IRQ选项时所做的那样:
return arch_early_irq_init();
IRQs的非早期初始化
在early_irq_init后,调用与架构相关的init_IRQ函数。init_IRQ函数初始化名为vector_irq的percpu变量,vector_irq存储了中断的向量数,并将在中断处理和与外部硬件中断相关的其他东西的初始化期间使用:
void __init init_IRQ(void)
{
int i;
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
...
...
...
}
DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
[0 ... NR_VECTORS - 1] = -1,
};
typedef int vector_irq_t[NR_VECTORS];
#define NR_VECTORS 256
在init_IRQ函数的末尾:
x86_init.irqs.intr_init();
现在,我们对native_init_IRQ感兴趣。native_init_IRQ函数的名称包含native前缀,这意味着该函数是特定于体系结构。执行本地APIC的一般初始化和ISA irqs的初始化。native_init_IRQ函数从以下函数的执行开始:
x86_init.irqs.pre_vector_init();
pre_vector_init指向init_ISA_irqs函数,对ISA相关的中断进行初始化。init_ISA_irqs函数从定义irq_chip类型的chip变量开始:
void __init init_ISA_irqs(void)
{
struct irq_chip *chip = legacy_pic->chip; // 表示硬件中断芯片描述符
...
...
之后,取决于CONFIG_X86_64和CONFIG_X86_LOCAL_APIC内核配置选项,调用init_bsp_APIC函数,初始化bootstrap processor的APIC:
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
init_bsp_APIC();
#endif
下一步,调用clear_local_APIC函数,并通过将value设置为APIC_SPIV_APIC_ENABLED来启用第一个处理器的APIC:
value = apic_read(APIC_SPIV);
value &= ~APIC_VECTOR_MASK;
value |= APIC_SPIV_APIC_ENABLED;
apic_write(APIC_SPIV, value);
在为bootstrap processor启用APIC之后,返回到init_ISA_irqs函数,初始化旧的可编程中断控制器,并为每个旧的irq设置旧的芯片和处理程序:
legacy_pic->init(0);
for (i = 0; i < nr_legacy_irqs(); i++)
irq_set_chip_and_handler(i, chip, handle_level_irq);
在init_ISA_irqs函数之后,可以返回到native_init_IRQ函数。下一步是调用apic_intr_init函数,该函数分配特殊的中断门,这些中断门由SMP体系结构用于处理器间中断。alloc_intr_gate宏用于分配中断描述符:
#define alloc_intr_gate(n, addr) \
do { \
alloc_system_vector(n); \
set_intr_gate(n, addr); \
} while (0)
apic_intr_init函数完成其工作后的下一步是设置从FIRST_EXTERNAL_VECTOR或0x20到0x100的中断门:
i = FIRST_EXTERNAL_VECTOR;
#ifndef CONFIG_X86_LOCAL_APIC
#define first_system_vector NR_VECTORS
#endif
for_each_clear_bit_from(i, used_vectors, first_system_vector) {
set_intr_gate(i, irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR));
}
在native_init_IRQ函数的末尾,可以看到以下检查。acpi_ioapic变量表示存在I/O APIC,!of_ioapic && nr_legacy_irqs()检查 不使用开放固件I/O APIC和遗留中断控制器:
if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
// setup_irq从给定的向量号开始初始化中断描述符
// 2是中断号;irq2表示IRQ 2 line,用于查询级联设备
setup_irq(2, &irq2);
static struct irqaction irq2 = {
.handler = no_action,
.name = "cascade",
.flags = IRQF_NO_THREAD,
};
延后中断
软中断是一种延后中断,硬件中断有时需要快速响应,但中断需要处理的任务比较多,所以需要将一部分任务延后执行,即作为软中断执行。当系统比较空闲并且处理器上下文允许处理中断时,被延后的剩余任务就会开始执行。
软中断在 Linux 内核编译时就静态地确定了,软中断数量是固定的,软中断的处理函数通常在系统启动时或模块加载时注册。open_softirq 函数负责 softirq 初始化:
// 参数是 软中断的编号 和 指向处理函数的指针
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
raise_softirq标记一个软中断需要被处理,但不会立即执行软中断处理函数。内核会在适当的时候(例如,在硬件中断处理程序执行完毕后)检查是否有待处理的软中断,并调用相应的处理函数来执行软中断。:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}