# Linux 硬中断处理过程
我们知道:中断机制是一个可以让 CPU 与 设备 异步执行的高性能通讯手段 ------ CPU 执行指令,设备执行设备操作,当设备完成后 通过中断线通知 CPU 工作完成,CPU 响应中断从而完成设备的数据处理。那么CPU 是如何检测中断的呢?我们在混沌学堂说过,每次CPU 执行完当前指令后,会存在一个中断处理周期,CPU可以在此周期内检测中断高低电平,从而响应中断。那么本文将详细描述 CPU处理 整个硬中断过程的原理。
# request_irq 函数
内核通常使用 request_irq 函数来注册中断处理(这里我们以 i386 平台来举例)。
struct irqaction {
irqreturn_t (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next; // 通过该属性形成上下文链表
}; // 中断上下文
// irq :设备响应的中断号(也叫中断线) handler:当CPU检测发生中断时回调的函数
// irqflags : 标识中断类型(SA_SHIRQ(共享中断线 --- 也即,多个驱动程序注册到该中断号上,驱动程序自己维护CPU硬中断的关闭与否)、SA_INTERRUPT(当中断处理时,是否屏蔽 CPU 中断)、SA_SAMPLE_RANDOM(当前中断是否影响到硬件随机数熵))
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
{
int retval;
struct irqaction * action; // 用于表示当前中断上下文
if (irqflags & SA_SHIRQ) {
if (!dev_id)
printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);
}
if (irq >= NR_IRQS)
return -EINVAL;
if (!handler)
return -EINVAL;
action = (struct irqaction *)
kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
if (!action)
return -ENOMEM;
// 初始化结构变量
action->handler = handler;
action->flags = irqflags;
action->mask = 0;
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
retval = setup_irq(irq, action); // 将上下文安装到内核中
if (retval)
kfree(action);
return retval;
}
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
# setup_irq 函数
该函数用于将中断上下文 struct irqaction * new 注册到内核结构中,当硬中断发生时,进行中断号匹配后,回调其中的handler 函数。
typedef struct irq_desc { // 中断描述符,其中保存了中断上下文
unsigned int status;
hw_irq_controller *handler;
struct irqaction *action; // 当前中断号所对应的中断上下文链表
unsigned int depth;
unsigned int irq_count;
unsigned int irqs_unhandled;
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
extern irq_desc_t irq_desc [NR_IRQS]; // 每个中断号对应该数组下标
// 宏定义挺有意思(i386):当开启 APIC 时能响应的中断号非常多,如果没有开启中断号,那么只能最多 16个中断号(使用 8259A 中断芯片 两片级联,详细请看 混沌学堂的描述)
#ifdef CONFIG_X86_IO_APIC
#define NR_IRQS 224
# if (224 >= 32 * NR_CPUS)
# define NR_IRQ_VECTORS NR_IRQS
# else
# define NR_IRQ_VECTORS (32 * NR_CPUS)
# endif
#else // 两片 8259A 级联
#define NR_IRQS 16
#define NR_IRQ_VECTORS NR_IRQS
#endif
int setup_irq(unsigned int irq, struct irqaction * new)
{
int shared = 0;
unsigned long flags;
struct irqaction *old, **p;
irq_desc_t *desc = irq_desc + irq; // 获取保存当前中断上下文的中断描述符
if (desc->handler == &no_irq_type) // 描述符已经设置该中断号不响应任何中断,那么直接返回
return -ENOSYS;
if (new->flags & SA_SAMPLE_RANDOM) { // 处理随机数熵(忽略)
rand_initialize_irq(irq);
}
spin_lock_irqsave(&desc->lock,flags); // 对当前中断描述符上自旋锁,保证操作安全
p = &desc->action;
if ((old = *p) != NULL) { // 存在旧的中断上下文
if (!(old->flags & new->flags & SA_SHIRQ)) { // 若没有设置共享中断标志位,那么表示当前中断号独占,那么直接解锁返回
spin_unlock_irqrestore(&desc->lock,flags);
return -EBUSY;
}
// 共享中断号,那么将当前上下文关联到当前中断号对应的中断上下文链表的末尾
do {
p = &old->next;
old = *p;
} while (old);
shared = 1; // 标识共享中断线
}
*p = new;
if (!shared) { // 独占中断线,那么设置状态位后回调硬中断控制器的回调函数(硬中断在初始化时将会设置用于响应该中断号的回调函数)
desc->depth = 0;
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING | IRQ_INPROGRESS);
desc->handler->startup(irq);
}
spin_unlock_irqrestore(&desc->lock,flags);
register_irq_proc(irq); // 将中断号信息注册到 /proc/irq 文件系统中,比如:create /proc/irq/1234
return 0;
}
// 这里我们看看 8259A 中断控制器的初始化
void make_8259A_irq(unsigned int irq)
{
disable_irq_nosync(irq);
io_apic_irqs &= ~(1<<irq);
irq_desc[irq].handler = &i8259A_irq_type; // 设置中断控制器的回调函数
enable_irq(irq);
}
// 了解下即可,具体函数就是利用 io 指令读写 8259A 的端口寄存器
static struct hw_interrupt_type i8259A_irq_type = {
"XT-PIC",
startup_8259A_irq,
shutdown_8259A_irq,
enable_8259A_irq,
disable_8259A_irq,
mask_and_ack_8259A,
end_8259A_irq,
NULL
};
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# init_IRQ 函数
该函数用于初始化 IRQ 处理,流程如下:
1、初始化 8259A 芯片与 irq_desc 数组
2、对所有中断号设置中断处理函数
3、执行控制芯片初始化(比如第二块级联的 8259A 芯片)
4、设置时钟中断
5、设置 FPU 处理器
asmlinkage void __init start_kernel(void){ // 内核启动代码
...
init_IRQ();
...
}
// i386 初始化 8259A 中断控制器,并注册中断相应函数
void __init init_IRQ(void)
{
int i;
pre_intr_init_hook(); // 初始化 8259A 芯片与 irq_desc 数组
for (i = 0; i < NR_IRQS; i++) { // 对所有中断号设置中断处理函数。对于 8259A 前面说过 最多 16个
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
intr_init_hook(); // 执行控制芯片初始化,在这里初始化第二块级联的 8259A 芯片的 IRQ 号和处理函数
setup_timer(); // 设置时钟中断
if (boot_cpu_data.hard_math && !cpu_has_fpu) // 设置 FPU 处理器
setup_irq(FPU_IRQ, &fpu_irq);
}
// 初始化 8259A 芯片和中断描述数组
void __init pre_intr_init_hook(void){
init_ISA_irqs();
}
void __init init_ISA_irqs (void)
{
int i;
init_8259A(0); // 初始化 8259A 的内部寄存器状态
for (i = 0; i < NR_IRQS; i++) { // 初始化 irq_desc 数组
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].action = 0;
irq_desc[i].depth = 1;
if (i < 16) { // 我们使用 16个中断号即可,注册上述的芯片控制器回调函数
irq_desc[i].handler = &i8259A_irq_type;
} else {
irq_desc[i].handler = &no_irq_type;
}
}
}
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
# interrupt[i] 数组生成
在上面我们看到会遍历该数组将数组中的地址信息放入 IDT 中,CPU 将在接受到中断后查找 IDT 表,找到入口函数调用。我们看到这里所有中断处理地址都是 do_IRQ 函数,这是为了统一内核的硬中断处理,稍后我们会看到该函数的执行过程。
// 宏定义,定义全局名字并对齐
#define ENTRY(name) \
.globl name; \
ALIGN; \
name:
.data
ENTRY(interrupt) // 定义 interrupt 数组
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS // 重复生成 16 个中断处理函数入口
ALIGN
1: pushl $vector-256 // 每次进入common_interrupt前,将当前中断向量压入栈中
jmp common_interrupt // 跳转到 common_interrupt 地址处继续处理中断
.data // 该数据表示指向 标号为 1的入口地址,将包含在 interrupt 数组 中
.long 1b
.text
vector=vector+1
.endr
ALIGN
common_interrupt:
SAVE_ALL // 保存所有寄存器
call do_IRQ // 调用 do_IRQ 地址处理当前中断
jmp ret_from_intr // 调用中断后返回进程进入中断前的状态
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
# do_IRQ 函数
该函数用于统一响应 Linux 硬中断,当 CPU 检测到 中断后 执行该方法前,将会压入保存的寄存器以及中断号放入栈中传递,这时我们可以通过 struct pt_regs 获取这些保存的寄存器的值。处理流程如下:
1、设置状态位,表示当前正在处理硬中断
2、调用中断控制器的 ack 函数,回复中断控制器,当前已经接收到该中断
3、若当前硬中断状态不为 IRQ_DISABLED(已关闭) 和 IRQ_INPROGRESS(正在处理),那么取出该硬中断的处理函数 action,并设置标志位 表示该中断将要被处理
4、回调该 IRQ 设置 action 回调函数
5、回调 end 函数 并退出中断处理
6、检测是否存在挂起的软中断,若存在,那么执行这些未处理的软中断
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
int irq = regs.orig_eax & 0xff; // 获取中断号
irq_desc_t *desc = irq_desc + irq; // 获取当前中断号的描述符
struct irqaction * action;
unsigned int status;
irq_enter(); // 设置状态位,表示当前正在处理硬中断
kstat_this_cpu.irqs[irq]++;
spin_lock(&desc->lock);
desc->handler->ack(irq); // 调用中断控制器的 ack 函数,回复中断控制器,当前已经接收到该中断
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING); // 去除中断状态位中的 IRQ_REPLAY 和 IRQ_WAITING 状态
status |= IRQ_PENDING; // 当前硬中断还没处理,所以现在设置状态为:待处理
action = NULL;
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) { // 若当前硬中断状态不为 IRQ_DISABLED(已关闭) 和 IRQ_INPROGRESS(正在处理),那么取出该硬中断的处理函数 action,并设置标志位 表示该中断将要被处理
action = desc->action;
status &= ~IRQ_PENDING; // 去除 IRQ_PENDING 标志位
status |= IRQ_INPROGRESS; // 设置 当前中断 即将被处理状态位
}
desc->status = status;
// 如果该硬中断正在被处理或者被禁用,那么直接退出
if (unlikely(!action))
goto out;
for (;;) {
irqreturn_t action_ret;
spin_unlock(&desc->lock); // 由于标志位已经被设置,所以,这里可以释放该中断号描述符的自旋锁
action_ret = handle_IRQ_event(irq, ®s, action); // 回调该中断号注册的中断处理函数
spin_lock(&desc->lock); // 处理完成后,由于需要重新设置状态位,所以这里重新获取自旋锁
if (!noirqdebug) // 如果开启了 irq 调试,那么对当前 irq 的处理状态进行检测:如果前10万个中断中的99,900个没有被处理,那么我们可以假定该IRQ以某种方式卡住了,那么将其删除并尝试关闭该 IRQ,这里我们了解即可
note_interrupt(irq, desc, action_ret);
if (likely(!(desc->status & IRQ_PENDING))) // 若当前中断在处理过程中没有再次被中断(可能由其他 CPU 的中断控制器设置),那么退出
break;
desc->status &= ~IRQ_PENDING; // 否则继续循环处理该硬中断
}
desc->status &= ~IRQ_INPROGRESS; // 标识当前硬中断已经处理完成
out: // 回调 end 函数,同时解锁
desc->handler->end(irq);
spin_unlock(&desc->lock);
irq_exit(); // 设置状态位表示 CPU 完成处理 硬中断,同时检测是否存在软中断,若存在,那么触发软中断执行
return 1;
}
// 检测是否发生软中断,若存在,那么执行
#define irq_exit()
do {
preempt_count() -= IRQ_EXIT_OFFSET;
if (!in_interrupt() && softirq_pending(smp_processor_id())) // in_interrupt() 表示当前软中断已经处理,但是可能在处理过程中,又发生了 硬中断,所以这里需要检测是否 该软中断 正在被处理,若在,那么直接退出,后续的执行函数将会继续处理软中断
do_softirq();
preempt_enable_no_resched();
} while (0)
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
57
# handle_IRQ_event 函数
该函数较为简单:遍历 响应该中断号 的处理函数链。
int handle_IRQ_event(unsigned int irq, struct pt_regs *regs, struct irqaction *action)
{
int status = 1;
int retval = 0;
if (!(action->flags & SA_INTERRUPT)) // 若当前中断处理函数的标志位 为 SA_INTERRUPT,表名在回调函数时,需要开启硬中断的响应,也即执行:sti 指令(我们在混沌学堂中看到,当 CPU 执行中断门时会自动关闭硬中断标志位:EFLAGS 中的 IF 标志位)
local_irq_enable();
do { // 循环调用该中断的处理函数链
status |= action->flags;
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM) // 当前硬中断的处理标识用于生成 随机数的熵,那么进行处理
add_interrupt_randomness(irq);
local_irq_disable(); // 关闭当前 CPU 的硬中断响应,此时 CPU 将不相应硬中断信号(NMI 不可屏蔽的硬中断处理除外)
return retval;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18