Linux中断内幕

近年来陆陆续续对Linux内核各模块做些研究,本文是对Linux内核中断的一点研究。主要参考ULK以及毛批,结合自己的一些深入的理解,以及从源代码中翻箱倒柜获取的心得。

Linux中断内幕

早在学习8086汇编的时候,就接触过中断的概念。而在现代操作系统如Linux, Windows上,对中断的理解,概多源于道听途说,也就一知半解,时常被各种细分的术语搞得云里雾里。在阅读了大量的文献与Linux相关源码后,我将对Linux的中断内幕进行一波小小的归纳。有道是纸上得来终觉浅。

外中断、内中断、硬中断、软中断、异常、陷阱、错误、终止、可编程中断/异常、IRQ、Tasklet、工作队列

如果你对这些了如指掌,辨识得清清楚楚,绕过本文即可。

中断的宏观意义

毛批和ULK在谈到中断时,标题都是将中断和异常并列而论,甚至毛批中将系统调用这么个子类也放在了标题,或许是因为系统调用太重要了。

在研究了大量的资料后,私以为使用宏观意义的中断概念来做标题或许更佳,这也是本文标题的由来。

追根溯源,中断只是个宏观概念,参见程序员素质二连,要么轮询、要么中断。这一思想在程序设计中随处可见,如自旋锁和睡眠锁,前者一直在BB着:“我现在可以起床了吗”,而后者则不声不响,等待其他人(进程)的唤醒(Hey guy,it's time to get up!)。

所以中断的意义,我引用鱼C甲鱼兄的诠释:中断就是你在房间里看苍老师的片儿,你妈妈突然进来告诉你让你下去打酱油,于是你按下暂停键,出去打酱油,回来之后再continue。

一个严格的归纳:从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器(如 8259A)。如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理。

中断分类

从信号源来分,中断有两种,由外部的硬件设备产生(这称为外中断)或者是CPU内部产生的(这称为内中断,内部产生可以有两种,一种是指令执行出错比如除0,另一种是使用软中断指令int x)。而从另一个角度来看,由硬件引起的叫硬中断,软件引起的叫软中断。Linux中软中断是个“二义”的概念,之所打上引号,是因为实际上殊途同归,后面会谈到。

Linux中很难看到内外中断的说法,但是软硬中断则随处可见。按照我的理解,外中断就是硬中断,而内中断不完全是软中断(之所以这样说,是因为硬中断是Linux处理IRQ的上半部分,下半部分交由软中断完成,这里的软中断入口方式和直接使用int x指令不同,但最终结果都是找服务例程),采用软硬是为了更好的区分中断信号是硬件给的还是由软件产生的。

而按照Intel手册的区分,中断应该分为同步中断和异步中断。显然,硬中断是异步的,因为你不知道它什么时候来搞事情(如键盘中断)。相反的,软中断则是同步的,因为只有在一条指令终止执行后CPU才会发出中断(如系统调用)。

Intel手册将异步中断称为中断(interrupt),同步中断称为异常(exception)。它们的不同已经清楚了,而共同点则是都有一个中断服务例程,也就是具体要做的事(例子中的打酱油)。

为了区分宏观意义的中断和异步中断,下文统一将前者称为中断,后者称为interrupt。

Interrupt

Interrupt分为可屏蔽中断(maskable interrupt)和非屏蔽中断(nonmaskable interrupt)。可屏蔽中断由两种状态:已屏蔽(masked)和未屏蔽(unmasked),非屏蔽中断顾名思义不可屏蔽(有限的几个危急事件如硬件故障,所以我们常说硬中断是可屏蔽的,这并不包括nonmaskable interrupt)。

Exception

Exception分为三类,错误(fault)、陷阱(trap)和终止(abort)。

三者的区别主要在于返回的位置不同。fault在返回后依然会重新执行引起故障的指令(缺页异常就是fault,要保证连贯性);trap返回后执行下一条指令(调试器断点);abort则不会返回,这往往意味着发生了一个严重错误,异常中止处理程序会终止引起abort的进程。

小结

所以中断分为四类:interrupt, fault, trap和abort。

类别 原因 异步/同步 返回行为
interrupt 来自I/O设备的信号 异步 总是返回到下一条指令
fault 潜在可恢复的错误 同步 返回到当前指令
trap 有意的异常 同步 总是返回到下一条指令
abort 不可恢复的错误 同步 不返回

IRQ

IRQ和中断的关联,我直接引用blcblc的归纳:

中断处理程序比一个进程要“轻”(中断的上下文很少,建立或终止中断处理需要的时间也很少)

中断处理是内核执行的最敏感的任务之一,因此它必须满足下列约束:

​ ◎ 当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分,内核随后执行。 ​◎ 因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个不同类型的中断又发生了。内核应该尽可能地允许这种情况发生,因为这能维持更多的I/O设备得到处理的机会。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核也应能切换到另外的进程。 ​◎ 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。

因为外部设备不能直接发出中断,而必须通过中断控制器的标准组件来请求中断,所以这种请求更正确的叫法是IRQ,或中断请求(Interrupt Request)。中断不能由处理器外部的外设直接产生,而必须借助于一个称为可编程中断控制器(programmable interrupt controller, PIC)的标准组件来请求,该组件存在于每个系统中。外部设备,会有电路连接到用于向中断控制器发送中断请求的组件。控制器在执行了各种电工任务之后,将中断请求转发到CPU的中断输入中。

每个能够发出中断请求的硬件设备控制器都有这么一条名为IRQ的输出线。所有现有的IRQ线都会与这个中断控制器(PIC)的硬件电路的输入引脚相连。下面来看看这种中断控制器执行下列动作:

​ 1) 监视IRQ线,检查产生的信号 。如果有一条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。

​ 2) 如果一个引发信号出现在IRQ线上:

​ a) 把接收到的引发信号转换成对应的向量(索引)。

​ b) 把这个向量存放在中断控器的一个I/O端口,从而允许CPU通过数据总线读取此向量

​ c) 把引发信号发送到处理器的INTR引脚,即产生一个中断

​ d) 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。

​ 3) 返回到第一步。

​ IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0。与IRQn关联的Intel缺省向量是n+32。如前所述,通过向中断控制器端口发布合适的指令,就可以修改IRQ和向量之间的映射。

​ 可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们,禁止的中断是丢失不了的,它们一旦激活,PIC就又把它们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ。

IDT

中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中存放的是相应的中断或异常处理程序的入口地址。内核在允许中断发生前,也就是在系统初始化时,必须把 IDT 表的初始化地址装载到 idtr 寄存器中,初始化表中的每一项。

当处于实模式下时,IDT 被初始化并由 BIOS 程序所使用。然而,一旦 Linux 开始接管,IDT 就被移到 RAM 的另一个区域,并进行第二次初始化,因为 Linux 不使用任何 BIOS 程序,而使用自己专门的中断服务程序(例程)(interrupt service routine,ISR)。中断和异常处理程序很像常规的 C 函数

IDT的格式和GDT和LDT相似,表中每一项对应一个中断(interrupt or exception)向量,每个向量由8个字节组成,共256项。idtr使IDT可以位于内存的任何地方,它指定IDT的线性基地址及其限制(最大长度)。在允许中断之前,必须用lidt汇编指令来初始化idtr

IDT包含三种类型描述符作为表项,任务门描述符、中断门描述符和陷阱门描述符。

  • 任务门:
    • 当中断信号发生时,必须取代当前进程的那个进程的TSS选择符放在任务门中。
    • TSS段选择符和CS、DS相似,通过GDT和LDT指向特殊的“系统段”中的一种,称为任务状态段。TSS用于保护现场,包括CPU中所有与具体进程有关的寄存器内容(包含页面目录指针CR3)以及3个堆栈指针。
    • P标志位为1表示在内存中,DPL描述项优先级别,类型码为00101。
    • 中断发生时,如果CPU找到的项是任务门,且DPL安检通过,CPU就将现场保存在TSS中,将任务门指向的任务变成当前任务,实现任务切换。(为了完成切换回路,CPU还有一个TR寄存器,用于指向当前的TSS)
    • Intel硬件设计任务门旨在让OS使用它来完成进程切换,但Linux并没有采用这一硬件特性,实际上Linux仅在处理Default fault Exception时使用了任务门,表示一种内核错误。
  • 中断门:
    • 包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。
    • 指向的是一个子程序,所以除了段选择码外,还需要结合段内偏移
  • 陷阱门:
    • 与中断们类似,只是控制权传递到一个适当的段时处理器不修改IF标志。
    • 通过类型码区分中断门和陷阱门。

实际上还有个调用门,它和中断门、陷阱门结构一致,类型码不同。IDT中不包含调用门,Linux不用它做中断处理。

Linux分别抽离了三个陷阱门和一个中断门,美其名曰系统中断门和系统门。前者用于用户态进程访问陷阱门(门DPL值为3),通过系统门激活3个异常处理程序,分别对应向量4,5,128,也就是intoboundint 80H三个指令;后者也是用户态访问中断门(DPL为3),提供向量3相关的处理程序,也就是int 3, 单步中断。除了这四个个DPL为3的中断/陷阱门外,其他的DPL都是0,且Linux也把它们称为中断/陷阱门。

IDT初始化

内核初始化阶段完成虚存管理初始化后会调用trap_init()和init_IRQ()初始化IDT:

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
/*
* 调用trap_init()来设置系统规定的异常与中断
*/
void __init trap_init(void)
{
#ifdef CONFIG_EISA
void __iomem *p = ioremap(0x0FFFD9, 4);
if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {
EISA_bus = 1;
}
iounmap(p);
#endif

#ifdef CONFIG_X86_LOCAL_APIC
init_apic_mappings();
#endif

/*
* 设置了0~19的中断/异常处理程序,这些都是intel所规定的,除些之后设置了系统调用入口(用户空间的 int SYSCALL_VECTOR )
*/
set_trap_gate(0,&divide_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
/* 通过GDT_ENTRY_DOUBLEFAULT_TSS 取得GDT 第32 表项的值填充eip和esp
* 并在私有栈上执行doublefault_fn() 异常处理函数
*/
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);

/*
* 系统调用
* 内核初始化期间,建立对应于向量128的中断描述符表表项
* 当用户态进程发出 int $0x80 指令时,CPU切换到内核态并开始从地址system_call处开始执行指令
*/
set_system_gate(SYSCALL_VECTOR,&system_call);

/*
* Should be a barrier for any external CPU state.
*/
cpu_init();

trap_init_hook();
}

源码中也可以看出前文描述符的Linux门的分类。这些set操作无非就是对DPL进行了封装:

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
/*设定中断门*/
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}

/*设定系统中断门*/
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n, 14, 3, addr, __KERNEL_CS);
}

/*设定陷阱门*/
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}

/*设定系统门*/
static void __init set_system_gate(unsigned int n, void *addr)
{
/*
* 设置如下字段
* Segment Selector: 内核代码段 __KERNEL_CS的段选择符
* Offset: 指向system_call()系统调用处理程序的指针
* Type: 设置为15,表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断
* DPL: 设置为3,这就允许用户态进程调用这个异常处理程序
*/
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}

/*设定任务门*/
static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
{
_set_gate(idt_table+n,5,0,0,(gdt_entry<<3));
}

无非就是DPL, 类型码不同罢了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* @gate_addr:相应IDT项的地址.
* @type:设置IDT项的TYPE字段, 15表示系统门,14表示中断门.
* @dpl:IDT项对应的DPL值,
* @addr:中断处理程序的地址,
* @seg:IDT中对应项的段选择符
*/
#define _set_gate(gate_addr,type,dpl,addr,seg) \
do { \
int __d0, __d1; \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %4,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"3" ((char *) (addr)),"2" ((seg) << 16)); \
} while (0)

牺牲可读性而提升效率的典型案例。AT&T秀的我脑壳疼。

读AT&T要有耐性,先把输入输出部按部就班的和%x以及寄存器对应上,然后再读汇编指令。不清楚为什么要设计这么反人类的语法。。。

再看对外部中断的IRQ初始化:

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
/*IRQ中断的设置*/
void __init init_IRQ(void)
{
int i;

/* all the set up before the call gates are initialised */
//8259初始化,初始化了irq_desc[]数组
pre_intr_init_hook();

/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
/*
* 通过替换setup_idt() 所建立的中断门来更新IDT
* interrupt 数组第n 项中存放IRQn 的中断处理程序的地址
* 这里不包括128(0x80) 号中断向量相关的中断门,因为它用于系统调用的编程异常
*/
//FIRST_EXTERNAL_VECTOR:第一个可用号,前面部份均为系统保留
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
//跳过系统调用号
if (vector != SYSCALL_VECTOR)
//调用了set_intr_gate(vector, interrupt[i])为第n条中断线设置的中断处理函数为interrupt[n- FIRST_EXTERNAL_VECTOR].
set_intr_gate(vector, interrupt[i]);
}

/* setup after call gates are initialised (usually add in
* the architecture specific gates)
*/
intr_init_hook();

/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
setup_pit_timer();

/*
* External FPU? Set up irq13 if so, for
* original braindamaged IBM FERR coupling.
*/
if (boot_cpu_data.hard_math && !cpu_has_fpu)
setup_irq(FPU_IRQ, &fpu_irq);

irq_ctx_init(smp_processor_id());
}

CPU处理流程

CPU执行下一条指令前,控制单元会检查是否已经发生了一个interrupt或exception。如果有,则进行这些操作:

  1. 确定关联的向量i
  2. 读由idtr指向的IDT表的第i项(忽略任务门,我们假设是个中断门或陷阱门)
  3. gdtr寄存器获得GDT基地址,在GDT中查找,读取IDT表项中选择符所标志的段描述符。这个描述符指定处理程序的基地址。
  4. 安检。将当前特权级CPL(cs寄存器低2位)与段描述符(GDT中)的DPL比较,如果CPL小于DPL则产生通用保护异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则进一步安检:比较CPL与IDT中门描述符的DPL,如果DPL小于CPL,也产生通用保护异常,这是为了防止用户程序访问特殊的陷阱门或中断门。
  5. 检查是否发生了特权级的变化(CPL是否不同于所选择的段描述符的DPL,用户栈->内核栈),如果是,控制单元必须开始使用与新的特权级相关的栈。操作如下:
    1. tr寄存器,访问运行进程的TSS段
    2. 用与新特权级相关的栈段和栈指针的正确值装载ssesp寄存器。这些值可以在TSS中找到。
    3. 在新的栈中保存ssesp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
  6. 如果故障已发生,用引起异常的指令地址装载cseip寄存器,使得指令可以重新执行。
  7. 栈中保存eflagscseip
  8. 如果异常产生了一个硬件错误码,则也保存在栈中。
  9. 装载cseip,值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值指定了处理程序的第一条指令的逻辑地址。

处理完成后,程序的最后一定有一个iret指令,回到被中断的进程,过程如下:

  1. 用保存在栈中的值装载cs,eipeflags。如果曾经压了错误码,还需要iret前弹出(这个事情显然由处理例程做了,处理例程知道自己是interrupt还是exception)。
  2. 检查处理程序的CPL是否等于cs中最低两位的值(这意味着中断的进程和处理程序运行在同一特权级),如果是,iret终止执行;否则继续下一步。
  3. 从栈中装载ssesp寄存器,因此返回到了旧特权级栈(内核栈->用户栈)。
  4. 检查ds,es,fs,gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户应用程序(CPL=3)利用内核以前所利用的段寄存器(DPL=0)。

一图胜千言:

IRQ数据结构

每个IDT表项都有一个中断请求队列,这个请求队列用一个irq_desc_t描述符描述,所有的irq_desc_t组成了irq_desc[]数组。

Linux内核的定义:

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
struct hw_interrupt_type {
const char * typename; /*中断控制器的名字*/
unsigned int (*startup)(unsigned int irq); /*允许从IRQ线产生中断*/
void (*shutdown)(unsigned int irq); /*禁止从IRQ线产生中断*/

/*enable与disable函数在8259A中与上述的startup shutdown函数相同*/
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);

void (*ack)(unsigned int irq); /*在IRQ线上产生一个应答*/
void (*end)(unsigned int irq); /*在IRQ处理程序终止时被调用*/
void (*set_affinity)(unsigned int irq, cpumask_t dest); /*在SMP系统中,设置IRQ处理的亲和力*/
};

typedef struct hw_interrupt_type hw_irq_controller;

typedef struct irq_desc {
hw_irq_controller *handler; /*指向一个中断控制器的指针(用来控制该中断线行为的函数指针)*/
void *handler_data;
struct irqaction *action; /* IRQ action list */ /* 挂在IRQ上的中断处理程序 */
unsigned int status; /* IRQ status */ /* IRQ的状态;IRQ 是否被禁止了,有关IRQ的设备当前是否正被自动检测*/
unsigned int depth; /* nested irq disables */ /* 为0:该IRQ被启用,如果为一个正数,表示被禁用 */
unsigned int irq_count; /* For detecting broken interrupts */ /* 该IRQ发生的中断的次数 */
unsigned int irqs_unhandled; /*该IRQ线上没有被处理的IRQ总数*/
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc [NR_IRQS];

在看irqaction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct irqaction {
irqreturn_t (*handler)(int, void *, struct pt_regs *); //中断处理例程

/*flags:
* SA_INTERRUPT:中断嵌套
* SA_SAMPLE_RANDOM:这个中断源于物理随机性
* SA_SHIRQ:中断线共享
*/
unsigned long flags;

cpumask_t mask; //在x86平台无用
const char *name; //产生中断的硬件名字
void *dev_id; //设备ID,一般由厂商指定
struct irqaction *next; //下一个irqaction.共享的时候,通常一根中断线对应很多硬件设备的中断处理例程
int irq;
struct proc_dir_entry *dir;
};

handler即指向了最为重要的中断服务程序。IDT表初始化完成之初,这些中断服务队列都是空的,所以当CPU通过门进入了总服务程序后,仅仅是做了对中断控制器的ack()end()表面应答,没什么实际内容。而一个真正的服务程序都是通过request_irq()登记的:

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
/**
* 设备驱动程序利用IRQ前,调用request_irq。
*/
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags, const char * devname, void *dev_id)
{
struct irqaction * action;
int retval;

/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
if ((irqflags & SA_SHIRQ) && !dev_id)
return -EINVAL;
if (irq >= NR_IRQS)
return -EINVAL;
if (!handler)
return -EINVAL;

/**
* 先建立一个新的irqaction描述符,并用参数值初始化它。
*/
action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
if (!action)
return -ENOMEM;

action->handler = handler;
action->flags = irqflags;
cpus_clear(action->mask);
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;

/**
* setup_irq函数把action描述符插入到合适的IRQ链表。
*/
retval = setup_irq(irq, action);
/**
* 如果setup_irq返回一个错误码,
* 说明IRQ线已经被另一个设备使用,并且设备不允许中断共享。
*/
if (retval)
kfree(action);

return retval;
}

展开setup:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* 将irqaction插入到链表中
* irq-IRQ号
* new-要插入的描述符
*/
int setup_irq(unsigned int irq, struct irqaction * new)
{
struct irq_desc *desc = irq_desc + irq;
struct irqaction *old, **p;
unsigned long flags;
int shared = 0;

if (desc->handler == &no_irq_type)
return -ENOSYS;
/*
* Some drivers like serial.c use request_irq() heavily,
* so we have to be careful not to interfere with a
* running system.
*/
/*
* 如果中断可生成随机熵,则初始化随机熵机制
*/
if (new->flags & SA_SAMPLE_RANDOM) {
/*
* This function might sleep, we want to call it first,
* outside of the atomic block.
* Yes, this might clear the entropy pool if the wrong
* driver is attempted to be loaded, without actually
* installing a new handler, but is this really a problem,
* only the sysadmin is able to do this.
*/
rand_initialize_irq(irq);
}

/*
* The following block of code has to be executed atomically
*/
spin_lock_irqsave(&desc->lock,flags);
/**
* 检查是否已经有设备在使用这个IRQ了。
*/
p = &desc->action;
/**
* 有设备在使用了。
*/
if ((old = *p) != NULL) {
/* Can't share interrupts unless both agree to */
/**
* 如果有设备在使用这个IRQ线,就再次检查它是否允许共享IRQ。
* 在这里,仅仅检查第一个挂接到IRQ上的设备是否允许共享就行了。
* 其实,第一个设备允许共享就代表这个IRQ上的所有设备允许共享。
*/
if (!(old->flags & new->flags & SA_SHIRQ)) {
/**
* IRQ线不允许共享,那就打开中断,并返回错误码。
*/
spin_unlock_irqrestore(&desc->lock,flags);
return -EBUSY;
}

/* add new interrupt at end of irq queue */
/**
* 在这里,我们已经知道设备上挂接了设备,那就循环,找到最后一个挂接的设备
* 我们要插入的设备应该挂接到这个设备的后面。
*/
do {
p = &old->next;
old = *p;
} while (old);
/**
* IRQ上有设备,并且运行到这里了,表示IRQ允许共享。
*/
shared = 1;
}

/**
* 把action加到链表的末尾。
*/
*p = new;

/**
* 判断是否是与其他设备共享IRQ
*/
if (!shared) {
/**
* 不是共享IRQ,就说明本设备是IRQ上的第一个设备
*/
/*
* 初始化相应IRQ描述符,
* 清除IRQ_DISABLED, IRQ_AUTODETECT, IRQ_WAITING ,IRQ_INPROGRESS标志
*/
desc->depth = 0;
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT |
IRQ_WAITING | IRQ_INPROGRESS);
/**
* startup 或enable是为了确保IRQ信号被激活。
*/
if (desc->handler->startup)
desc->handler->startup(irq);
else
desc->handler->enable(irq);
}
spin_unlock_irqrestore(&desc->lock,flags);

/**
* 建立proc文件
*/
new->irq = irq;
register_irq_proc(irq);
new->dir = NULL;
register_handler_proc(irq, new);

return 0;
}

在内核中,设备驱动程序一般都要通过request_irq()登记中断服务程序。

中断的嵌套

Linux中断处理程序是无需重入的。当一个给定的中断处理在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。

由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。这大大简化了中断处理程序的编写(不然同步处理得烦死,硬件的设计也很理性,否则还不是坑自己)。

Linux下硬中断可以嵌套,且没有优先级的概念,也就是说任何一个中断都可以打断正在执行的中断(当然,根据前面的描述,同种中断除外)。软中断则不能嵌套,但同类型软中断可以在不同CPU上并行执行。

关于Linux软中断的疑问:

​ Q: Linux的下半部BH机制、软中断机制和tasklet机制。我印象中只有BH机制使用了int指令和中断向量表。软中断机制的软中断向量表难道不是内核自己维护的数据结构吗,软中断(int)和软中断(下半部)不应该是两码事吗?

​ A: 软中断是软件引起的中断,区别于硬件引起的。可以由int指令引发然后执行中断向量表中的函数,也可以由内核设置标志位然后唤醒事先注册的处理函数。实现方式不同就是了。

​ r00tk1t: 实际上我是赞同这一理解方式的,但严格来说softirq和软中断信号是两回事。关于此,毛批中的说法可能是最恰当的:硬中断是外部设备对CPU的中断,softirq通常是硬中断服务程序对内核的中断,而软中断信号是内核对某个进程的中断。后两者都是由软件产生的,只是来源不同,但最终处理殊途同归。

中断服务程序

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
/*interrupt数组定义*/
ENTRY(interrupt)
.text


vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
/*
* 把中断号减去256的结果保存在栈中,内核用负数表示所有的中断,因为正数表示系统调用,
* 当引用这个数的时候,可以对所有的中断处理程序都执行相同的代码,这段通用代码开始于标签common_interrupt
*/
/*
* 相当于,interrupt[i]执行下列操作:
* Pushl $i-256 //中断号取负再压栈
* Jmp common_interrupt //跳转至一段公共的处理函数
*/
1: pushl $vector-256
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr

之所以设计成了$i-256而不是直接push序号,是因为正数是给系统调用用的。common_interrupt是一个公共的处理函数,展开看一下:

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
ALIGN

/*IRQ入口,中断入口*/
common_interrupt:
/*common_interrupt 首先调用 SAVE ALL保存寄存器*/
SAVE_ALL

/*寄存器保存后,栈顶的地址被存放到eax寄存器里面*/
movl %esp,%eax
/*
* 调用do_IRQ()函数,
* 调用相应的中断处理函数
*/
call do_IRQ
/*
* 执行do_IRQ的ret指令的时候,控制转移到ret_from_intr()函数
* 从中断返回
*/
jmp ret_from_intr

/*
* SAVE_ALL可以在栈中保存中断处理程序可能会使用的所有CPU寄存器
* 但eflags, cs, eip, ss 及esp除外,因为这些寄存器已经由控制单元自动保存了
* 然后这个宏把用户数据段的选择符装到ds和es寄存器
*/
/*
* 相当于把中断发生时,硬件没有保存的寄存器压栈保存下来.把DS.ES设为了__USER_DS是有一定原因的,参考上节所述.
*
* 设置为__USER_DS的原因:
* (检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相关的段寄存器。
* 控制单元这么做是为了禁止用户态的程序利用内核以前所用的段寄存器。如果不清除这些寄存器的话,恶意的用户程序就会利用他们来访问内核地址空间。
* 注意到4: 举例说明一下.如果通过系统调用进入内核态.然后将DS,ES的值赋为__KERNEL_DS(在2.4 的内核里),处理完后(调用iret后),
* 恢复CS,EIP的值,此时CS的CPL是3.因为DS,ES被设为了__KERNEL_DS,所以其DPL是 0,所以要将DS,ES中的值清除.在2.6内核中,发生中断或异常后,
* 将DS,ES的值设为了__USER_DS,避免了上述的清除过程,提高了效率.)
*/
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;

SAVE_ALL的功能一目了然。注意到这里没有eflags,cs,eip,ss,esp。因为这些已经由控制单元自动保存了。

前面所做的一切,实际上都是为了给do_IRQ()建立一个模拟的子程序调用环境。而do_IRQ()就是处理具体中断请求队列的地方。

在展开do_IRQ之前,先看看返回到的地方:

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
# userspace resumption stub bypassing syscall exit tracing
ALIGN

/*异常返回分析: 异常返回的后半部份与IRQ中断返回相比只是多了一个preempt_stop的处理.*/
ret_from_exception:
/**
* 在中断返回前,handle_IRQ_event调用local_irq_disable禁止了中断
* 所以在中断返回时,不用关中断,但是在异常返回时,需要将中断关闭。
*/
preempt_stop

/*IRQ中断返回分析*/
ret_from_intr:
/**
* 把当前thread_info半截到ebp中。
*/
GET_THREAD_INFO(%ebp) //取得当前过程的task描述符
/**
* 接下来判断EFLAGS和CS,确定是否运行在用户态,是否是VM模式。
*/
movl EFLAGS(%esp), %eax # mix EFLAGS and CS //中断前的EFLAGS中的值存进EAX
movb CS(%esp), %al //将中断前的CS低16移至AL
//#至此EFLAGS 的H16和CS的L16构成了EAX的内容

testl $(VM_MASK | 3), %eax //EFLAGS中有一位表示是否运行在vm86模式中,CS的最低二位表示当前进程的运行级别
/**
* 如果是运行在内核态,并且不是VM模式,就跳到resume_kernel,
* 否则跳转到resume_userspace
*/
jz resume_kernel # returning to kernel or vm86-space // 如果中断前不是在用户空间,且不是在VM86模式下,跳转到resume_kernel
/**
* 恢复用户态程序的流程入口。
*/
ENTRY(resume_userspace)
/**
* 不清楚为什么还要禁用中断
*/
//开中断,以防中断丢失
// TODO: cli不是关中断么?
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
/**
* 检查thread_info的flag
*/
movl TI_flags(%ebp), %ecx //将task->flags成员的值存进ecx
/**
* 如果设置了_TIF_WORK_MASK中任何一位,就表示有等待处理的事情
* 跳到work_pending处理这些挂起的事件。
* 否则调用restore_all回到用户态。
*/
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
# int/exception return? //还有事情没做完?
jne work_pending //还有事情没做完?
jmp restore_all //所有事情都处理完了

....


//restore_all被定义成RESTRORE_ALL
restore_all:
/*
* 恢复保存在内核栈中的寄存器的值,并执行iret汇编指令以重新开始用户态的执行
*/
RESTORE_ALL

......

#define RESTORE_ALL \
RESTORE_REGS \ //pop在SAVE_ALL中入栈的寄存器
addl $4, %esp; \ //记否?在SAVE_ALL之前压入了一个中断向量的负值或者是系统调用号
1: iret; \ //iret中断返回,交给硬件完成中断的返回工作
.section .fixup,"ax"; \
2: sti; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es; \
movl $11,%eax; \
call do_exit; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 1b,2b; \
.previous

再看do_IRQ():

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/** 
* do_IRQ执行与一个中断相关的所有中断服务例程.
*/
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
/* high bits used in ret_from_ code */
int irq = regs->orig_eax & 0xff;
#ifdef CONFIG_4KSTACKS
union irq_ctx *curctx, *irqctx;
u32 *isp;
#endif

/**
* irq_enter增加中断嵌套计数
*/
irq_enter();
#ifdef CONFIG_DEBUG_STACKOVERFLOW
/* Debugging check for stack overflow: is there less than 1KB free? */
{
long esp;

__asm__ __volatile__("andl %%esp,%0" :
"=r" (esp) : "0" (THREAD_SIZE - 1));
if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
printk("do_IRQ: stack overflow: %ld\n",
esp - sizeof(struct thread_info));
dump_stack();
}
}
#endif

/*CONFIG_4KSTACKS: 如果thread_union大小为4KB*/
#ifdef CONFIG_4KSTACKS

/**
* 如果中断栈使用不同的的栈,就需要切换栈.
*/

/*
* 执行current_thread_info 以获取与内核栈相连的thread_info描述符的地址保存到curctx
*/
curctx = (union irq_ctx *) current_thread_info();
/*
* 硬中断请求栈地址保存到irqctx
*/
irqctx = hardirq_ctx[smp_processor_id()];

/*
* this is where we switch to the IRQ stack. However, if we are
* already using the IRQ stack (because we interrupted a hardirq
* handler) we can't do that and just have to keep using the
* current stack (which is the irq stack already after all)
*/
/**
* 当前在使用内核栈,而不是硬中断请求栈.就需要切换栈
*
* curctx 和irqctx 相等,说明内核已经在使用硬件中断请求栈,
* 这种情况发生在内核处理另外一个中断时又产生了中断请求的时候
* 不相等就要切换内核栈
*/
if (curctx != irqctx) {
int arg1, arg2, ebx;

/* build the stack frame on the IRQ stack */
isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
/**
* 保存当前进程描述符指针
*/
irqctx->tinfo.task = curctx->tinfo.task;
/**
* 把esp栈指针寄存器的当前值存入irqctx的thread_info(内核oops时使用)
* current_stack_pointer 用来在C 中获得当前的栈指针
*/
irqctx->tinfo.previous_esp = current_stack_pointer;

/**
* 将中断请求栈的栈顶装入esp,isp即为中断栈顶
* 调用完__do_IRQ后,从ebx中恢复esp
*/
asm volatile(
" xchgl %%ebx,%%esp \n"
" call __do_IRQ \n"
" movl %%ebx,%%esp \n"
: "=a" (arg1), "=d" (arg2), "=b" (ebx)
: "0" (irq), "1" (regs), "2" (isp)
: "memory", "cc", "ecx"
);
} else/* 否则,发生了中断嵌套,不用切换 */
#endif
__do_IRQ(irq, regs);

/**
* 递减中断计数器并检查是否有可延迟函数
*/
irq_exit();

/**
* 结束后,会返回ret_from_intr函数.
*/
return 1;
}

只是些外围的操作,增加嵌套深度,检查栈溢出等,核心处理在:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*
* do_IRQ handles all normal device IRQ's (the special
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
irq_desc_t *desc = irq_desc + irq;
struct irqaction * action;
unsigned int status;

/**
* 中断发生次数计数.
*/
kstat_this_cpu.irqs[irq]++;
if (desc->status & IRQ_PER_CPU) {
irqreturn_t action_ret;

/*
* No locking required for CPU-local interrupts:
*/
desc->handler->ack(irq);
action_ret = handle_IRQ_event(irq, regs, desc->action);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
desc->handler->end(irq);
return 1;
}

/**
* 虽然中断是关闭的,但是还是需要使用自旋锁保护desc
*/
spin_lock(&desc->lock);
/**
* 如果是旧的8259A PIC,ack就是mask_and_ack_8259A,它应答PIC上的中断并禁用这条IRQ线.屏蔽IRQ线是为了确保在这个中断处理程序结束前,
* CPU不进一步接受这种中断的出现.
* do_IRQ是以禁止本地中断运行,事实上,CPU控制单元自动清eflags寄存器的IF标志.因为中断处理程序是通过IDT中断门调用的.
* 不过,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断.
* 在使用APIC时,应答中断信赖于中断类型,可能是ack,也可能延迟到中断处理程序结束(也就是应答由end方法去做).
* 无论如何,中断处理程序结束前,本地APIC不进一步接收这种中断,尽管这种中断可能会被其他CPU接受.
*/
desc->handler->ack(irq);
/*
* REPLAY is when Linux resends an IRQ that was dropped earlier
* WAITING is used by probe to mark irqs that are being tested
*/
/**
* 初始化主IRQ描述符的几个标志.设置IRQ_PENDING标志.也清除IRQ_WAITING和IRQ_REPLAY
* 这几个标志可以很好的解决中断重入的问题.
* IRQ_REPLAY标志是"挽救丢失的中断"所用.在此不详述.
*/
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */

/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
/**
* IRQ_DISABLED和IRQ_INPROGRESS被设置时,什么都不做(action==NULL)
* 即使IRQ线被禁止,CPU也可能执行do_IRQ函数.首先,可能是因为挽救丢失的中断,其次,也可能是有问题的主板产生伪中断.
* 所以,是否真的执行中断代码,需要根据IRQ_DISABLED标志来判断,而不仅仅是禁用IRQ线.
* IRQ_INPROGRESS标志的作用是:如果一个CPU正在处理一个中断,那么它会设置它的IRQ_INPROGRESS.这样,其他CPU上发生同样的中断
* 就可以检查是否在其他CPU上正在处理同种类型的中断,如果是,就什么都不做,这样做有以下好处:
* 一是使内核结构简单,驱动程序的中断服务例程式不必是可重入的.二是可以避免弄脏当前CPU的硬件高速缓存.
*/
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc->action;
/*
* 确定我们要处理了,就设置IRQ_INPROGRESS 标志,
* 去除IRQ_PENDING 标志表示确认我们要处理这个中断了
*/
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;

/*
* If there is no IRQ handler or it was disabled, exit early.
* Since we set PENDING, if another processor is handling
* a different instance of this same irq, the other processor
* will take care of it.
*/
/**
* 当前面两种情况出现时,不需要(或者是不需要马上)处理中断.就退出
* 或者没有相关的中断服务例程时,也退出.当内核正在检测硬件设备时就会发生这种情况.
*/
if (unlikely(!action))
goto out;

/*
* Edge triggered interrupts need to remember
* pending events.
* This applies to any hw interrupts that allow a second
* instance of the same irq to arrive while we are in do_IRQ
* or in the handler. But the code here only handles the _second_
* instance of the irq, not the third or fourth. So it is mostly
* useful for irq hardware that does not mask cleanly in an
* SMP environment.
*/
/**
* 这里是需要循环处理的,并不是说调用一次handle_IRQ_event就行了.
*/
for (;;) {
irqreturn_t action_ret;

/**
* 现在打开自旋锁了,那么,其他CPU可能也接收到同类中断,并设置IRQ_PENDING标志.
* xie.baoyou注:请注意开关锁的使用方法.有点巧妙,不可言传.
*/
spin_unlock(&desc->lock);

/**
* 调用中断服务例程.
*/
action_ret = handle_IRQ_event(irq, regs, action);

spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
/**
* 如果其他CPU没有接收到同类中断,就退出
* 否则,继续处理同类中断.
*/
if (likely(!(desc->status & IRQ_PENDING)))
break;
/**
* 清除了IRQ_PENDING,如果再出现IRQ_PENDING,就说明是其他CPU上接收到了同类中断.
* 注意,IRQ_PENDING仅仅是一个标志,如果在调用中断处理函数的过程中,来了多次的同类中断,则意味着只有一次被处理,其余的都丢失了.
*/
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;

out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
/**
* 现在准备退出了,end方法可能是应答中断(APIC),也可能是通过end_8259A_irq方法重新激活IRQ(只要不是伪中断).
*/
desc->handler->end(irq);
/**
* 好,工作已经全部完成了,释放自旋锁吧.注意两个锁的配对使用方法.
*/
spin_unlock(&desc->lock);

return 1;
}

感兴趣的是对服务例程的调用:

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
/**
* 执行中断服务例程
*/
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;

/**
* 如果没有设置SA_INTERRUPT,说明中断处理程序是可以在开中断情况下执行的
* 这也是程序中少见的,调用local_irq_enable的地方。
* 一般来说,调用local_irq_enable是危险的,不允许,绝不允许。这里只是例外。
*/
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();

/**
* 一开始,action是irqaction链表的头,irqaction表示一个ISR
*/
do {
/**
* handler是中断服务例程的处理函数。它接收三个参数:
* irq-IRQ号,它允许一个ISR处理几条IRQ。
* dev_id-设备号,注册中断服务例程时指定,此时回传给处理函数。它允许一个ISR处理几个同类型的设备。
* regs-指向内核栈的pt_regs。它允许ISR访问内核执行上下文。可是,哪个ISR会用它呢?
*/
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
/**
* 一般来说,handler处理了本次中断,就会返回1
* 返回0和1是有用的,这样可以让内核判断中断是否被处理了。
* 如果过多的中断没有被处理,就说明硬件有问题,产生了伪中断。
*/
retval |= ret;
action = action->next;
} while (action);

/**
* 如果中断是随机数的产生源,就添加一个随机因子。
*/
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);

/**
* 退出时,总是会关中断,这里不判断if (!(action->flags & SA_INTERRUPT))
* 是因为:判断的汇编指令比直接执行cli费时,既然无论如何都是需要保证处于关中断状态,为什么多作那些判断呢。
*/
local_irq_disable();

return retval;
}

最终在泛用接口中通过action->handler找到特化接口。

附:中断和异常服务例程返回的流程图

硬中断服务例程的下半身

softirq

前面已星星点点渗透了软中断的定义。这里谈到的softirq即是指“硬件中断服务例程对内核的中断”。

前文也已了解到,之所以要有这种softirq,将硬中断的处理分成上下两部分,是因为兼顾硬中断及时的处理(比如网卡如果处理的慢了,一旦缓冲区满了就开始丢包)以及softirq任务的繁重,上下两部分的设计提高了效率,但却容易使后来者一时云里雾里。

而事实上,在Linux 2.6内核中,早已不是最原始的仅通过softirq来实现的版本了。Linux对于下半部分的实现机制,除了softirq以外,还有tasklet和工作队列。tasklet实际上是通过软中断实现的,但和软中断有所不同。而工作队列则是另一回事了。

softirq实际上应用场合很少,因为tasklet往往是足够用的,且更易编写,而softirq是在编译期间静态分配的,也就相对僵硬。

softirq由一个数组表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 所有的软中断,目前使用了前六个。数组的下标就是软中断的优先级。
* 下标越低,优先级越高。
*/
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;

struct softirq_action
{
/**
* 软中断处理函数
*/
void (*action)(struct softirq_action *);
/**
* 回传给软中断处理函数的数据。
*/
void *data;
};

6个softirq分别是:

1
2
3
4
5
6
7
8
9
enum
{
HI_SOFTIRQ=0, //处理高优先级的tasklet
TIMER_SOFTIRQ, //和时钟中断相关的tasklet
NET_TX_SOFTIRQ, //把数据包传送到网卡
NET_RX_SOFTIRQ, //从网卡接收到数据包
SCSI_SOFTIRQ, //SCSI命令的后台中断处理
TASKLET_SOFTIRQ //处理常规tasklet
};

这里隐隐约约可以看出tasklet确实是依靠softirq实现的,且还有2种优先级,分别是最高和最低(数字越小优先级越高,softirq按优先级处理)。

softirq放到这个数组中,就完成了softirq的注册,想要执行softirq必须先标记注册好的softirq,这个过程被称为触发softirq

注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 初始化软中断
* nr-软中断下标
* action-软中断处理函数
* data-软中断处理函数的参数。执行处理函数时,将它回传给软中断。
*/

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}

触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 激活软中断
* nr-要激活的软中断下标
*/
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;

/**
* 禁用本地CPU中断。
*/
local_irq_save(flags);
/**
* raise_softirq_irqoff是本函数的执行体,不过它是在关中断下运行。
*/
raise_softirq_irqoff(nr);
/**
* 打开本地中断
*/
local_irq_restore(flags);
}

通常,中断处理程序(就是上半部分)会在返回之前标记它的softirq,所以不必担心,然后在合适的时刻就会执行该软中断。

合适的时刻:

  1. 从一个硬件中断代码处返回时(可以展开看do_IRQ()最后的irq_exit())
  2. ksoftirqd内核线程中
  3. 在那些显示检查和执行待处理的softirq的代码中

不管是上面哪个时刻,软中断最终都是会被执行的,调用do_softirq()该函数会循环遍历(循环检查pending的每一个位,所以循环最多只能执行32次)

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
/**
* 处理挂起的软中断
*/
asmlinkage void do_softirq(void)
{
unsigned long flags;
struct thread_info *curctx;
union irq_ctx *irqctx;
u32 *isp;

/**
* 如果in_interrupt返回真,说明系统要么是处于中断中,要么是禁用了软中断。
* 请注意in_interrupt()的实现代码:preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK)
* 它判断当前是否在硬件中断中,或者是否在软中断中。
*/
if (in_interrupt())
return;

/**
* 在此时需要关闭中断,因为接下来我们需要将判断是否有挂起的中断
* 如果不在关中断的情况下访问这个标志,那么,这个标志就可能被中断程序修改。
* 从另一个方面来说,我们还会在后面切换堆栈,这也需要在关中断中进行。
* 开中断的时机在__do_softirq中。当然,本函数结束时,也会恢复中断标志。
*/
local_irq_save(flags);

if (local_softirq_pending()) {/* 有挂起的软中断 */
/**
* 根据配置来确定是否切换堆栈(请注意,本段代码受宏CONFIG_4KSTACKS的控制)。
* 当然,这里用的是softirq_ctx而不是hardirq_ctx来保存当前进程。
*/
curctx = current_thread_info();
irqctx = softirq_ctx[smp_processor_id()];
irqctx->tinfo.task = curctx->task;
irqctx->tinfo.previous_esp = current_stack_pointer;

/* build the stack frame on the softirq stack */
/**
* isp保存的是软中断栈的栈顶。
* 可以放心的是,软中断在单个CPU上不会重入,而softirq_ctx是每CPU变量
* 所以,在这里我们可以放心的切换栈顶了。
*/
isp = (u32*) ((char*)irqctx + sizeof(*irqctx));

/**
* 切换栈到isp,并调用__do_softirq.
* 然后再恢复栈(可能是恢复到中断栈、进程内核栈、内核线程栈)。
*/
asm volatile(
" xchgl %%ebx,%%esp \n"
" call __do_softirq \n"
" movl %%ebx,%%esp \n"
: "=b"(isp)
: "0"(isp)
: "memory", "cc", "edx", "ecx", "eax"
);
}

/*
* 恢复保存的IF 标志的状态并返回
*/
local_irq_restore(flags);
}

ksoftirqd内核线程中:

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
static int ksoftirqd(void * __bind_cpu)
{
set_user_nice(current, 19);
current->flags |= PF_NOFREEZE;

set_current_state(TASK_INTERRUPTIBLE);

while (!kthread_should_stop()) {
/**
* 没有挂起的中断,调度出去。
*/
if (!local_softirq_pending())
schedule();

/**
* 在上次循环结尾处,可能设置状态为TASK_INTERRUPTIBLE,现在把它改过来。
*/
__set_current_state(TASK_RUNNING);

while (local_softirq_pending()) {
/* Preempt disable stops cpu going offline.
If already offline, we'll be on wrong CPU:
don't process */
/**
* 现在是增加抢占计数,而不是软中断计数。
* 增加软中断计数,防止软中断重入是在do_softirq中。
*/
preempt_disable();
if (cpu_is_offline((long)__bind_cpu))
goto wait_to_die;
/**
* 回想一下,do_softirq会设置软中断计数标志,而ininterrupt会根据这个标志返回是否处于中断上下文。
* 其实,现在我们是在线程上下文执行do_softirq。
* 所以说,ininterrupt有点名不符实。
* liufeng: 线程上线问就不会发现有软中断计数器的增加?
*/
do_softirq();
preempt_enable();
/**
* 增加一个调度点,仅此而已。
*/
cond_resched();
}

/**
* 没有挂起的软中断,就将状态设置为TASK_INTERRUPTIBLE
* 下次循环时,就会调度出去。
*/
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;

wait_to_die:
preempt_enable();
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}

tasklet

直接引用leno对tasklet的归纳。

因为tasklet是使用软中断实现的,所以tasklet本身就是个软中断,我们是通过tasklet来实现下半部的机制的,所以在处理方式上和软中断十分的相似,tasklettasklet结构体表示,每一个结构体单独代表一个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
/**
* taskletra描述符
*/
struct tasklet_struct
{
/**
* 指向下一个描述符
*/
struct tasklet_struct *next;
/**
* tasklet的状态。
*
*/
unsigned long state;
/**
* 锁计数器。
*/
atomic_t count;
/**
* tasklet功能函数
*/
void (*func)(unsigned long);
/**
* 传给功能函数的参数。
*/
unsigned long data;
};

其中tasklet的状态一共只有三种:0,TASKLET_STATE_SCHED,TASKLET_STATE_RUN,只能在这三种之间取值,0表示啥也没有等待调度,SCHED表示已经调度,RUN表示该tasklet正在运行。

已经被调度的tasklet结构体存放在两种单处理器数据结构当中,分别是tasklet_vec(普通优先级的tasklet)和tasklet_hi_vec(高优先级的tasklet),几乎没区别,只是优先级不一样,调度的步骤如下

  1. 检查tasklet的状态是否为TASKLET_STATE_SCHED,如果是,就证明不需要调度了,直接返回
  2. 调用_tasklet_schedule()函数进行调度
  3. 保存中断状态,禁止本地中断,防止数据被其他中断拿去更改
  4. 头插加入链表,就刚才说的那两个优先级不同的链表
  5. 唤起tasklet中断(封装好的软中断)
  6. 恢复中断并返

运行的步骤如下:

  1. 禁止中断,检测两个链表里面有没有东西
  2. 把当前处理器的该链表设置为NULL(意思就是我要把链表里的东西全弄完,先置成NULL)
  3. 允许响应中断
  4. 循环遍历tasklet链表上的每一个节点
  5. 如果是多处理器系统,查看节点状态如果是RUN就证明在其他处理器上运行中,直接跳到下一个节点(因为同一时间里,相同类型的tasklet只有一个能执行)
  6. 如果当前节点的状态不是RUN,就设置成RUN,以防其他处理器调用
  7. 检查count是不是0(看看别人是否正在占用)如果不是0则被禁止,跳到下一个挂起的tasklet
  8. 安全确保,开始执行
  9. 一直循环,直到没有tasklet了(因为我们把链表置为NULL了,必须把拿出来的东西处理完)

其实tasklet给人的感觉就是一个对软中断的封装的简单接口而已。。

每个处理器都有一组辅助处理软中断(当然也就包括tasklet)的内核线程,那么什么时候执行这些软中断呢,上面在软中断部分也阐述了,但是这样有个问题,那就是软中断如果继续调软中断,就会不停的执行软中断。。这样在处理器负载很严重的时候就不太好了,会导致用户空间进程饥饿,还有一种方案,那就是并不立即处理软中断,而是等待一段时间,但是在处理器比较闲的时候这么做很显然不太好,因为完全可以立即执行你却让处理器闲着。作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载,关键来了,这些带着软中断的线程的优先级会被设置到最低的优先级上(nice值取最高为19),这样的话在处理器比较忙的时候,这些软中断不会跟用户空间进程争夺处理器资源,而且最终一定会被执行,处理器空闲的时候也可以直接得到运行。

更为精细的叙述可以参考ULK。

tasklet_schedule为例,简单做展开,tasklet_hi_schedule就不展开了,毕竟理解了softirq,也就理解了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
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
/**
* 调度tasklet,将其挂到软中断中。
*/
static inline void tasklet_schedule(struct tasklet_struct *t)
{
/**
* 检查任务的TASKLET_STATE_SCHED标志,如果已经设置,就退出。
* 设置了TASKLET_STATE_SCHED,表示它已经被调度到软中断上了,现在处于挂起状态
* 不能(也不用)再调度了。
*
* 既然TASKLET_STATE_SCHED标志已经保证一个tasklet不会在多个CPU上同时被插入到链表中,为什么还需要TASKLET_STATE_RUN标志呢?
* 这是因为在调用回调函数前,TASKLET_STATE_SCHED可能被清除,如果只有TASKLET_STATE_SCHED这个标志的话,可能还是会造成过程重入。
*/
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);

void fastcall __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;

/**
* 首先禁止本地中断。
*/
local_irq_save(flags);
/**
* 将tasklet挂到tasklet_vec[n]链表的头。
*/
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
/**
* raise_softirq_irqoff激活TASKLET_SOFTIRQ软中断。
* 它与raise_soft相似,但是它假设已经关本地中断了。
*/
raise_softirq_irqoff(TASKLET_SOFTIRQ);

/**
* 恢复IF标志。
*/
local_irq_restore(flags);
}

inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
/**
* 标记nr对应的软中断为挂起状态。
*/
__raise_softirq_irqoff(nr);

/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
/**
* in_interrupt是判断是否在中断上下文中。
* 程序在中断上下文中,表示:要么当前禁用了软中断,要么处在硬中断嵌套中,此时都不用唤醒ksoftirqd内核线程。
*/
if (!in_interrupt())
wakeup_softirqd();
}

/*
* 唤醒本地CPU的ksoftirqd内核线程
*/
static inline void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __get_cpu_var(ksoftirqd);

if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}

工作队列

工作队列是另外一种比较新的将工作推后的形式,和之前的两种处理方式不同,它会把工作交给一个内核线程去执行,这就意味着是由进程上下文来处理了!就可以睡眠了!!(中断是不允许睡眠的,如果睡了将没有进程能唤醒他,形成死锁)所以很简单就可以在这两种方法之间做出选择。

每一个处理器都有一个对应的工作者线程:

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
/**
* 工作队列描述符,它包含有一个NR_CPUS个元素的数组。
* 分多个队列的主要目的是每个CPU都有自己的工作队列,避免了多个CPU访问全局数据时,造成TLB不停刷新
*/
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list; /* Empty if single thread */
};

/**
* 每个CPU的工作队列描述符
* 工作队列与可延迟函数的主要区别在于:工作队列运行在进程上下文,而可延迟函数运行在中断上下文。
*/
struct cpu_workqueue_struct {
/**
* 保护该工作队列的自旋锁
* 虽然每个CPU都有自己的工作队列,但是有时候也需要访问其他CPU的工作队列
* 所以也需要自旋锁
*/
spinlock_t lock;

/**
* flush_workqueue使用的计数
*/
long remove_sequence; /* Least-recently added (next to run) */
long insert_sequence; /* Next to add */

/**
* 挂起链表的头结点
*/
struct list_head worklist;
/**
* 等待队列,其中的工作者线程因为等待更多的工作而处于睡眠状态
*/
wait_queue_head_t more_work;
/**
* 等待队列,其中的进程由于等待工作队列被刷新而处于睡眠状态
*/
wait_queue_head_t work_done;

/**
* 指向workqueue_struct的指针,workqueue_struct中包含了本描述符
*/
struct workqueue_struct *wq;
/**
* 指向工作者线程的进程描述符指针
*/
task_t *thread;

/**
* 当前的执行深度(当工作队列链表中的函数阻塞时,这个字段的值会比1大)
*/
int run_depth; /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;

cpu_wq中的每一项对应系统中的一个处理器。

这些工作的结构体被连成链表,当链表上的所有工作都做完了之后,线程就会休眠

实现方式也很简单,

  1. 线程首先把自己设置为休眠状态(只是设置,并没有立即进入休眠)并把自己加入等待队列
  2. 如果工作链表是空的,就用schedule()调度函数进入睡眠状态
  3. 如果链表中有对象,线程就不会睡眠了,就把自己的状态改为TASK_RUNNING,然后从等待队列中出来
  4. 如果链表非空,执行那些被退后的下半部分应该干的工作(就是循环一直找。。。)

下半部机制的选择

  • 如果你对共享有很高的要求,虽然比较麻烦,但还是使用软中断吧,因为可以各种操作(虽然保障这些很麻烦)
  • 如果你不是对共享有那么高的要求,推荐使用tasklet,因为两种同类型的tasklet不能同时并行
  • 如果你想在进程上下文中解决下半部分的问题,使用工作队列吧,当然如果你想睡眠,你也没得选了

参考文献

《Understanding the Linux Kernel, 3rd Edition》

《Linux内核源代码情景分析(上)》

http://blog.csdn.net/zhangskd/article/details/21992933

https://www.cnblogs.com/lenomirei/p/5562086.html

http://www.cnblogs.com/lenomirei/p/5564131.html

https://www.ibm.com/developerworks/cn/linux/l-cn-linuxkernelint/


Linux中断内幕
https://r00tk1ts.github.io/2017/12/21/Linux中断内幕/
作者
r00tk1t
发布于
2017年12月21日
许可协议