Fork me on GitHub
0%

中断及异常原理

引言

在计算机体系结构体中,CPU与外设的工作速度不匹配一直是存在的性能瓶颈点,而如何使CPU与外设正常的协调工作成为了重要问题。当然存在像轮询这样的解决办法,每隔一段时间去询问一次,但是这样无疑会降低处理效率。中断显然是更有效的解决办法。

基础

中断(Interrupt)是一种机制,允许计算机在执行当前任务的同时响应和处理来自外部设备或其他源的事件。

分类

按中断来源

  • 硬件中断

    硬件中断是由计算机硬件生成的,通常是外部设备请求 CPU 处理的事件。例如,键盘、鼠标、定时器、网络适配器等设备可以触发硬件中断。通常通过中断控制器(如 PIC 或 APIC)进行管理和分发,然后由操作系统处理。

  • 软件中断

    也称为异常或陷阱,是由计算机程序生成的,通常表示程序执行期间的异常情况。例如,除零错误、越界访问等情况可以触发软件中断。通常由程序执行指令(如 int 指令或系统调用)来触发,并由操作系统或程序本身进行处理。

同步方式

  • 同步中断

    软件中断通常是同步中断。同步中断是与程序执行相关的、可预测的中断,通常由程序中的异常情况引发。

  • 异步中断

    硬件中断通常是异步中断。异步中断是与程序执行无关的、不可预测的中断,通常由外部事件或设备触发。

中断与异常区别

中断 异常
例子 敲击键盘、插入usb 除0、缺页中断
来源 通常是硬件 通常是软件由于编程失误导致
处理 do_IRQ 给进程发送信号
位置 根据架构有所不同,但每一种固定 可屏蔽中断的中断向量则是可配的

处理流程

  • 中断触发及检测。满足中断触发条件中断来到,并检测是哪种原因的中断
  • 现场保存。保存寄存器的值(下一条指令的地址)并屏蔽中断,确保中断正常执行和恢复
  • 跳入中断向量表。根据原因进入中断向量表查看对应的中断处理流程
  • 中断处理程序
  • 恢复现场。清楚中断标志位并恢复寄存器的值

proc/interrupts

1
cat /proc/interrupts

proc是一个虚拟文件系统存在于内核中。通过上述命令就可以查看系统中中断号、个数、名字等等内容。这个是和体系相关的,show_interrupts()函数实现的。

中断控制方法

image

中断上下半部

中断的执行会打断系统正常的运行,因此为了提高系统的响应度和效率就需要中断程序的执行尽可能要快速,中断恢复系统正常运行。但是实际中有些中断的工作就会导致阻塞,因此可以将这部分内容等到系统不忙的时候执行。因此将中断处理过程分为两个部分:上半部(Top Half)和下半部(Bottom Half)。

  • 上半部:时间敏感、硬件相关、原子执行不希望被其它中断打断
  • 下半部:执行耗时长

下半部实现方式

  • 软中断(中断上下文。静态创建,操作多处理器变量需要注意)
  • tasklet(中断上下文。不能并发执行,如果不能确定是否是多处理器,选择这个)
  • 工作队列(进程上下文。执行时间可以推迟到进程上下文完成,推荐!!)

工作队列的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*定义工作队列和关联函数*/
struct work_struct my_wq;
struct work_struct my_delay_wq;

/*中断处理底半部*/
void xxx_do_work(unsigned long)
{
}
void xxx_do_delay_work(unsigned long)
{
}

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
schedule_work(&my_wq);
schedule_delay_work(&my_delay_wq);
}

/*设备驱动模块加载函数*/
int xxx_init(void)
{
request = request_irq(xxx_wq,xxx_interrupt,IRQF_DISABLED,"XXX",NULL);
...
// 初始化工作队列(可以是一个延时队列,指定时间)
INIT_WORK(&my_wq,(void(*)(void *))xxx_do_work,NULL)
INIT_DELAYED_WORK(&my_delay_wq,(void(*)(void *))xxx_do_delay_work,NULL)
}

/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
//释放中断
free_irq(xxx_irq,xxx_interrupt);
}

中断描述符

Linux中断机制的核心数据结构 irq_desc, 它完整地描述了一条中断线 (或可简单理解为 “一个中断源” )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list *///该中断线的中断服务程序
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
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;
struct cpumask *percpu_enabled;
#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
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

中断初始化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// kernel/init/main.c (version-4.4.94)
asmlinkage __visible void __init start_kernel(void)
{
local_irq_disable();
early_boot_irqs_disabled = true;
...
trap_init();
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
...
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable(); // 开启本地中断
...
}

trap_init

中断初始化函数入口 trap_init(),不同体系结构下的实现可能有所不同,但是完成的功能基本一致,完成填写IDT描述符构成中断向量表的功能。

MIPS中断异常类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
///arch/mips/include/asm/mipsregs.h
/*
* Cause.ExcCode trap codes.
*/
#define EXCCODE_INT 0 /* Interrupt pending */
#define EXCCODE_MOD 1 /* TLB modified fault */
#define EXCCODE_TLBL 2 /* TLB miss on load or ifetch */
#define EXCCODE_TLBS 3 /* TLB miss on a store */
#define EXCCODE_ADEL 4 /* Address error on a load or ifetch */
#define EXCCODE_ADES 5 /* Address error on a store */
#define EXCCODE_IBE 6 /* Bus error on an ifetch */
#define EXCCODE_DBE 7 /* Bus error on a load or store */
#define EXCCODE_SYS 8 /* System call */
#define EXCCODE_BP 9 /* Breakpoint */
#define EXCCODE_RI 10 /* Reserved instruction exception */
#define EXCCODE_CPU 11 /* Coprocessor unusable */
#define EXCCODE_OV 12 /* Arithmetic overflow */
#define EXCCODE_TR 13 /* Trap instruction */
#define EXCCODE_MSAFPE 14 /* MSA floating point exception */
#define EXCCODE_FPE 15 /* Floating point exception */
#define EXCCODE_TLBRI 19 /* TLB Read-Inhibit exception */
#define EXCCODE_TLBXI 20 /* TLB Execution-Inhibit exception */
#define EXCCODE_MSADIS 21 /* MSA disabled exception */
#define EXCCODE_MDMX 22 /* MDMX unusable exception */
#define EXCCODE_WATCH 23 /* Watch address reference */
#define EXCCODE_MCHECK 24 /* Machine check */
#define EXCCODE_THREAD 25 /* Thread exceptions (MT) */
#define EXCCODE_DSPDIS 26 /* DSP disabled exception */
#define EXCCODE_GE 27 /* Virtualized guest exception (VZ) */
#define EXCCODE_CACHEERR 30 /* Parity/ECC occured on a core */

/* Implementation specific trap codes used by MIPS cores */
#define MIPS_EXCCODE_TLBPAR 16 /* TLB parity error exception */

/* Implementation specific trap codes used by Loongson cores */
#define LOONGSON_EXCCODE_GSEXC 16 /* Loongson-specific exception */

x86中断异常类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// /arch/x86/include/asm/trapnr.h
/* Interrupts/Exceptions */
#define X86_TRAP_DE 0 /* Divide-by-zero */
#define X86_TRAP_DB 1 /* Debug */
#define X86_TRAP_NMI 2 /* Non-maskable Interrupt */
#define X86_TRAP_BP 3 /* Breakpoint */
#define X86_TRAP_OF 4 /* Overflow */
#define X86_TRAP_BR 5 /* Bound Range Exceeded */
#define X86_TRAP_UD 6 /* Invalid Opcode */
#define X86_TRAP_NM 7 /* Device Not Available */
#define X86_TRAP_DF 8 /* Double Fault */
#define X86_TRAP_OLD_MF 9 /* Coprocessor Segment Overrun */
#define X86_TRAP_TS 10 /* Invalid TSS */
#define X86_TRAP_NP 11 /* Segment Not Present */
#define X86_TRAP_SS 12 /* Stack Segment Fault */
#define X86_TRAP_GP 13 /* General Protection Fault */
#define X86_TRAP_PF 14 /* Page Fault */
#define X86_TRAP_SPURIOUS 15 /* Spurious Interrupt */
#define X86_TRAP_MF 16 /* x87 Floating-Point Exception */
#define X86_TRAP_AC 17 /* Alignment Check */
#define X86_TRAP_MC 18 /* Machine Check */
#define X86_TRAP_XF 19 /* SIMD Floating-Point Exception */
#define X86_TRAP_VE 20 /* Virtualization Exception */
#define X86_TRAP_CP 21 /* Control Protection Exception */
#define X86_TRAP_VC 29 /* VMM Communication Exception */
#define X86_TRAP_IRET 32 /* IRET Exception */

ARM中断异常类型

1
2
3
4
5
6
7
8
9
10
// ./arch/arm/kernel/entry-armv.S
__vectors_start:
W(b) vector_rst // 复位异常
W(b) vector_und // 未定义指令异常
W(ldr) pc, __vectors_start + 0x1000 // 软件中断
W(b) vector_pabt // 中止(预取)
W(b) vector_dabt // 中止(数据)
W(b) vector_addrexcptn // 保留
W(b) vector_irq // 一般中断请求
W(b) vector_fiq // 快速中断请求

MIPS实现过程

  • 将异常处理函数放在中断异常的地址入口(except_vec3_generic放到BASE+0x180位置)
  • 填充中断向量表(填充exception_handlers,exception_handlers[0] = 中断入口)
  • 当发生中断的时候,进入中断异常地址入口
  • 做保护现场等动作
  • 跳转进入except_vec3_generic中断异常处理函数
  • 向量表第一项即会调入平台相关的中断中

如下代码所示即为mips对于trap_init函数的实现(只保留关键代码)。其主要作用是判断寄存器或者系统相关变量的值来设置中断向量表,而中断向量表是一个全局变量,其在genex.S汇编中进行了引用。而在汇编中,通过指令mfc0把控制寄存器CP0_CAUSE值拷贝到通用寄存器k1。CP0_CAUSE保存了这次中断发生的原因。用andi指令把k1的值加上0x7c,然后用指令ld加载具体一个中断处理程序的入口地址exception_handlers(k1)。exception_handlers就是我们记录的中断处理程序,根据k1的值找到当前系统需要执行哪个中断处理程序。然后使用指令jr跳转到这个中断处理程序。

k0和k1是为MIPS系统所保留,专门保留给系统发生中断时程序使用的寄存器。这里的”k”代表keep。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// kernel/arch/mips/kernel/traps.c
unsigned long exception_handlers[32];
void __init *set_except_vector(int n, void *addr)
{
...
old_handler = xchg(&exception_handlers[n], handler);
...
}
void __init trap_init(void)
{
...
// 把except_vec3_generic函数指针放在地址为 KSEG0 + 0x180 处,这个地址就是中断异常的入口地址。
set_handler(0x180, &except_vec3_generic, 0x80);
...
set_except_vector(0, using_rollback_handler() ? rollback_handle_int: handle_int);
set_except_vector(1, handle_tlbm);
set_except_vector(2, handle_tlbl);
set_except_vector(3, handle_tlbs);
set_except_vector(4, handle_adel);
set_except_vector(5, handle_ades);
set_except_vector(6, handle_ibe);
set_except_vector(7, handle_dbe);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// kernel/arch/mips/kernel/genex.S
NESTED(except_vec3_generic, 0, sp)
.set push
.set noat
#if R5432_CP0_INTERRUPT_WAR
mfc0 k0, CP0_INDEX
#endif
mfc0 k1, CP0_CAUSE
andi k1, k1, 0x7c
#ifdef CONFIG_64BIT
dsll k1, k1, 1
#endif
ld k0, exception_handlers(k1)
jr k0
.set pop
END(except_vec3_generic)


// handle_int
BUILD_ROLLBACK_PROLOGUE handle_int
NESTED(handle_int, PT_SIZE, sp)
#ifdef CONFIG_TRACE_IRQFLAGS
/*
┆* Check to see if the interrupted code has just disabled
┆* interrupts and ignore this interrupt for now if so.
┆*
┆* local_irq_disable() disables interrupts and then calls
┆* trace_hardirqs_off() to track the state. If an interrupt is taken
┆* after interrupts are disabled but before the state is updated
┆* it will appear to restore_all that it is incorrectly returning with
┆* interrupts disabled
┆*/
.set push
.set noat
...
# Check if already on IRQ stack
PTR_LI t1, ~(_THREAD_SIZE-1)
and t1, t1, sp
beq t0, t1, 2f

/* Switch to IRQ stack */
li t1, _IRQ_STACK_START
PTR_ADD sp, t0, t1

/* Save task's sp on IRQ stack so that unwinding can follow it */
LONG_S s1, 0(sp)
2:
jal plat_irq_dispatch
/* Restore sp */
move sp, s1

j ret_from_irq
#ifdef CONFIG_CPU_MICROMIPS
nop
#endif
END(handle_int)

如上代码所示,最终跳转到plat_irq_dispatch函数,很明显这个函数是与平台相关的,具体代码不做分析。主要完成的功能是:根据中断状态寄存器判断中断类型和中断号等等,随后通过一系列跳转最终调到 desc->handle_irq(desc) 这样一个语句,即注册的中断服务程序中。

early_irq_init

early_irq_init属于与硬件和平台无关的通用逻辑层,为irq_desc[]中个元素的某些成员填充默认值。

完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM / MIPS体系没有实现该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// kernel/irq/irqdesc.c
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};

int __init early_irq_init(void)
{
int count, i, node = first_online_node;
struct irq_desc *desc;
init_irq_default_affinity();

printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);

desc = irq_desc;
count = ARRAY_SIZE(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);
}
return arch_early_irq_init();
}

init_IRQ

初始化不同SOC的中断处理器等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __init init_IRQ(void)
{
int i;
for (i = 0; i < NR_IRQS; i++)
irq_set_noprobe(i);

arch_init_irq(); // 调用不同厂家基于MIPS架构实现的中断初始化函数

for_each_possible_cpu(i) {
int irq_pages = IRQ_STACK_SIZE / PAGE_SIZE;
void *s = (void *)__get_free_pages(GFP_KERNEL, irq_pages);

irq_stack[i] = s;
pr_debug("CPU%d IRQ stack at 0x%p - 0x%p\n", i,
irq_stack[i], irq_stack[i] + IRQ_STACK_SIZE);
}
}

参考资料