# 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;
}



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

# 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
#endifint 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
};
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
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;
        }
    }
}
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

# 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 // 调用中断后返回进程进入中断前的状态
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

# 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, &regs, 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)
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
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18