Linux内核学习之中断处理
0 前言
文本基于x86架构讲解Linux中对中断的处理,因此本文假定你已经阅读了x86架构学习之中断,了解了x86硬件中断的结构和行为。
1 中断处理程序的嵌套执行
- 每个中断处理程序都代表一个内核控制路径,中断的嵌套执行(抢占),同时也意味着内核控制路径的嵌套执行,一个操作系统如果要保证高效的响应和IO,就必须允许中断嵌套。这也就意味着,Linux在系统运行的大部分时刻,都是开中断的。
- 中断处理程序永远不会被用户态任务阻塞,这也意味着,在处理中断的时候,是不允许发生任务切换的。
- 一个设计好、并经过大量测试的Linux内核,除了个别特定用途的异常,是不会在内核态产生异常的。异常要么由编程错误(如除以0)产生,要么由编程异常(int n指令)或调试指令(debug、bound指令)产生。除了缺页异常 ,Linux内核没有编程错误(如果没有bug),同时也不会调用产生异常的显式指令。缺页异常 被内核用于内存交换,并且被设计成绝不会再嵌套产生异常,因此内核态中,最多有两个嵌套异常(系统调用+缺页)。
1 Linux对x86异常的处理
中断号 | 异常 | 异常处理程序 | 关联的Linux信号 |
---|---|---|---|
0 | diveide error | devide_error() | SIGFPE |
1 | debug | debug() | SIGTRAP |
2 | NMI | nmi() | NONE |
3 | break point | int3() | SIGTRAP |
4 | overflow | overflow() | SIGSEGV |
5 | bound check | bounds() | SIGSEGV |
6 | invalid opcode | invalid_op() | SIGILL |
7 | device not available | device_not_available() | NONE |
8 | double fault | doublefault_fn() | NONE |
9 | coprocessor segment overrun | coprocessor_segment_oversun() | SIGFPE |
10 | invalid TSS | invalid_tss() | SIGSEGV |
11 | segment not present | segment_not_present() | SIGBUS |
12 | stack exception | stack_segment() | SIGBUS |
13 | general exception | general_protection() | SIGSEGV |
14 | page fault | page_fault() | SIGSEGV |
15 | intel reserved | none() | NONE |
16 | float point error | coprocessor_error() | SIGFPE |
17 | alignment check | alignment_check() | SIGSEGV |
18 | machine check | machine_check() | NONE |
19 | SIMD float point | simd_coprocessor_error() | SIGFPE |
Linux中向量用途
向量范围 | 用途 |
---|---|
0~31 | intel预定义的异常或保留 |
32~127 | 外部中断 |
128 | 用于系统调用 |
129~238 | 外部中断 |
239 | 本地APIC中断 |
240 | 本地APIC高温中断 |
241~250 | Linux保留 |
251~253 | 处理器间中断 |
254 | 本地APIC错误中断 |
255 | 本地APIC伪中断 |
1 Linux中的中断门描述符
x86架构中,被中断用到的中断描述符被其称为中断门描述符,Linux则进一步对这个门描述符进行细分类:
- 中断门。用户态进程无法访问的x86中断门(通过设置DPL字段为0),所有的Linux中断处理程序都通过中断门激活,并且全部限制在内核态。
- 系统陷阱门。用户态可以访问的一个x86陷阱门(设置DPL为3),通过系统门来激活三个Linux异常处理程序,它们的向量(中断号)是4、5、128,即,用户态下,可以使用
into, bound, int 0x80
三条指令,int 0x80
是Linux中的系统调用。 - 系统中断门。能够被用户态进程访问的x86中断门(设置DPL为3),与向量3相关的异常处理程序是由系统中断门激活的,因此,用户态下可以使用
int3
指令。 - 陷阱门。用户态进程不能访问的一个x86陷阱门(设置DPL为0),大部分Linux异常处理程序都通过陷阱门来激活。
- 任务门。用户态无法访问的x86任务门(设置DPL为0),Linux对double fault异常处理程序通过任务门激活。
刚上电时,CPU运行在实模式,IDT由BIOS初始化使用,之后由Linux接管重新初始化。内核代码中涉及初始化中断的描述符的代码如下:
/* 设置门描述符,gete:中断号,type:门类型,addr:中断程序入口逻辑地址,
dpl:描述符特权级,ist:中断栈索引,具体参考前言中文章关于ist的描述 */
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
write_idt_entry(idt_table, gate, &s);
}
// 设置中断门:GATE_INTERRUPT + DPL=0
static inline void set_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
}
// 设置系统中断门:GATE_INTERRUPT + DPL=3
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS);
}
// 设置系统陷阱门:GATE_TRAP + DPL=3
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}
// 设置陷阱门:GATE_TRAP + DPL=0
static inline void set_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS);
}
// 设置中断门:GATE_TASK + DPL=0
static inline void set_task_gate(unsigned int n, unsigned int gdt_entry)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TASK, (void *)0, 0, 0, (gdt_entry<<3));
}
Linux中的中断描述符
Linux中用另一种描述符来描述中断状态和响应程序入口,在中断发生时,先通过intel的中断描述符表找到中断程序最初始的入口,在该程序中,Linux再访问自己配置号的中断描述符表,从表中提取指定中断号的描述符数据结构irq_desc,irq_desc中包含中断状态,以及中断处理程序,其原文字段含义如下。
field | description |
---|---|
irq_data | per irq and chip data passed down to chip functions |
kstat_irqs | irq stats per cpu |
handle_irq | highlevel irq-events handler |
preflow_handler | handler called before the flow handler (currently used by sparc) |
action | the irq action chain |
status | status information |
core_internal_state__do_not_mess_with_it | core internal status information |
depth | disable-depth, for nested irq_disable() calls |
wake_depth | enable depth, for multiple irq_set_irq_wake() callers |
irq_count | stats field to detect stalled irqs |
last_unhandled | aging timer for unhandled count |
irqs_unhandled | stats field for spurious unhandled interrupts |
threads_handled | stats field for deferred spurious detection of threaded handlers |
threads_handled_last | comparator field for deferred spurious detection of theraded handlers |
lock | locking for SMP |
affinity_hint | hint to user space for preferred irq affinity |
affinity_notify | context for notification of affinity changes |
pending_mask | pending rebalanced interrupts |
threads_oneshot | bitfield to handle shared oneshot threads |
threads_active | number of irqaction threads currently running |
wait_for_threads | wait queue for sync_irq to wait for threaded handlers |
dir | /proc/irq/ procfs entry |
name | flow handler name for /proc/interrupts output |
现在只需重点关注handle_irq、action、status三个字段:
- handle_irq指向服务于整个中断线的处理程序;
- action是一个链表,服务于共享该中断线的每个设备;
- status是中断处理状态,目前有三种状态,空闲、挂起、处理中、禁用。
当硬件发生一个中断信号时,从硬件到Linux软件的处理流程如下:
硬中断
这个概念是针对中断紧急程度提出的,也是相对后文软中断而言的,指的是中断的实际处理程序就放在中断处理函数当中。在Linux中这种中断是紧急的(critical)或非紧急的(noncritical)。
紧急硬中断:
- 中断实际处理程序在中断上下文,并在中断返回前执行完。
- 中断执行期间,由硬件屏蔽其他中断(针对当前CPU)。紧急硬中断使用的是intel的中断门, 中断通过中断门进入中断处理程序时,会清理eflags.if标志位,即屏蔽中断1。
非紧急硬中断:
- 中断实际处理程序在中断上下文,并在中断返回前执行完。
- 中断执行期间,中断是开启的(针对当前CPU),能够被高优先级中断打断(不能被同优先级打断)。非紧急硬中断使用的是intel的陷阱门,中断通过陷阱门进入中断处理程序时,不会清理eflags.if标志位,即保持中断开启。
软中断和tasklet
软中断即相对硬中断而言的,即非紧急可延迟中断(noncritical deferrable),指的是中断的实际处理程序没有放在中断处理函数当中,而是放在了中断返回之后。中断处理函数只负责简单的“应答”操作。推迟的中断处理被 Linux放在了专用的内核线程当中。
软中断
硬件中断信号发生时,触发触发中断处理函数的执行,硬中断会在中断上下文立即处理中断,而对于软中断,实际处理程序不在中断上下文,而被推迟到设计好的执行点执行。
- 软中断执行主体不在中断上下文当中,但仍在内核态下执行。
- 软中断使用的是intel的陷阱门,即开中断,不同于非紧急硬中断的是,软中断的实际处理程序由于放在中断上下文外,因此是可被低优先级中断打断的,但是,如果这个低优先级中断也是软中断,那么其实际处理程序的执行还是要经过排队的。
- 软中断是可重入的。
- 软中断是静态分配的(编译时)。
Linux为软中断分配了一个长度为32的数组(softirq_vec[32]),数组中每个元素(softirq_action)对应一个软中断,目前(3.10.89)仅使用9个。
Linux中使用的软中断(非最新内核):
软中断 | 下标(优先级) | 说明 |
---|---|---|
HI | 0 | 处理高优先级tasklet |
TIMER | 1 | 时钟中断相关tasklet |
NET_TX | 2 | 网卡发包 |
NET_RX | 3 | 网卡接包 |
BLOCK | 4 | |
BLOCK_IOPOLL | 5 | |
TASKLET | 6 | 处理常规tasklet |
SCHED | 7 | 内核调度中断 |
HRTIMER | 8 | |
RCU | 9 | RCU 锁中断 |
reserved | 10-31 | 保留 |
查看系统每CPU软中断运行次数:
[root@localhost ~]# cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 2 0 44 0
TIMER: 74161634 108560055 104975829 97259447
NET_TX: 0 0 0 1
NET_RX: 1611551 1540001 1480112 1868864
BLOCK: 692874 288353 242228 223828
BLOCK_IOPOLL: 0 0 0 0
TASKLET: 493 0 396 0
SCHED: 21956594 24238388 22492040 20494488
HRTIMER: 0 0 0 0
RCU: 49602186 71933466 69620972 64742279
tasklet2
tasklet是在软中断的基础上设计出来新子类,在上文中,HI 和 TASKLET 被用于tasklet,前者表高优先级的 tasklet,后者表低优先级的tasklet。
tasklet以拉链的形式挂载在 HI 和 TASKLET 上
每个节点是个tasklet任务,tasklet任务具有如下特点:
- 相同的类型tasklet被设计成串行执行的,因此不必是可重入的。
- 同类tasklet不可同时在不同CPU上执行(保证同类串行)
- 不同类tasklet可以同时在不同CPU上执行。
ksoftirqd内核线程
前面说过,软中断被推迟到设计好的执行点执行,这个执行点就是内核线程 ksoftirqd,Linux为每个CPU都创建的一个ksoftirqd线程3,命名为 ksoftirqd/n,n为cpu逻辑号,ksoftirqd 被定时器周期性唤醒,也会别任意一次中断处理函数的退出流程唤醒(irq_exit())。
从ps命令hanksoftirqd/n
kworker内核线程
这个线程不处理中断,但是,做的工作和 ksoftirqd 相似,都是做延时任务,不同的是,kworker负责的是内核延迟函数,而不是中断延迟函数,另一个不同点是,每个CPU不仅有一个kworker线程,还有一个各自的工作队列,这和tasklet是不同的,tasklet只有固定的两个高低优先级队列。各个工作队列之间如果出现严重不均衡时,kworker还负责调整队列。