中断概述
硬件中断
硬件中断概述
中断可以用下面的流程来表示 :
中断产生源
--> 中断向量表 (idt)
--> 中断入口 ( 一般简单处理后调用相应的函数 )
--> do_IRQ
--> 后续处理 ( 软中断等工作 )
如图 :
具体地说,处理过程如下 :
中断信号由外部设备发送到中断芯片 ( 模块 ) 的引脚 中断芯片将引脚的信号转换成数字信号传给 CPU ,例如 8259 主芯片引脚 0 发送的是 0x20 CPU 接收中断后,到中断向量表 IDT 中找中断向量 根据存在中断向量中的数值找到向量入口 由向量入口跳转到一个统一的处理函数 do_IRQ 在 do_IRQ 中可能会标注一些软中断,在执行完 do_IRQ 后执行这些软中断。
下面一一介绍。
8259 芯片
本文主要参考周明德《微型计算机系统原理及应用》和 billpan 的相关帖子
1. 中断产生过程
(1) 如果 IR 引脚上有信号,会使中断请求寄存器 (Interrupt Request Register,IRR) 相应的位置位,比如图中 , IR3, IR4, IR5 上有信号,那么 IRR 的 3 , 4 , 5 为 1
(2) 如果这些 IRR 中有一个是允许的,也就是没有被屏蔽,那么就会通过 INT 向 CPU 发出中断请求信号。屏蔽是由中断屏蔽寄存器 (Interrupt Mask Register,IMR) 来控制的,比如图中位 3 被置 1 ,也就是 IRR 位 3 的信号被屏蔽了。在图中,还有 4 , 5 的信号没有被屏蔽,所以,会向 CPU 发出请求信号。
(3) 如果 CPU 处于开中断状态,那么在执行指令的最后一个周期,在 INTA 上做出回应 , 并且关中断 .
(4)8259A 收到回应后,将中断服务寄存器 (In-Service Register) 置位 , 而将相应的 IRR 复位:
8259 芯片会比较 IRR 中的中断的优先级,如上图中,由于 IMR 中位 3 处于屏蔽状态,所以实际上只是比较 IR4,I5, 缺省情况下, IR0 最高,依次往下, IR7 最低 ( 这种优先级可以被设置 ) ,所以上图中, ISR 被设置为 4.
(5) 在 CPU 发出下一个 INTA 信号时, 8259 将中断号送到数据线上,从而能被 CPU 接收到,这里有个问题:比如在上图中, 8259 获得的是数 4, 但是 CPU 需要的是中断号 ( 并不为 4) ,从而可以到 idt 找相应的向量。所以有一个从 ISR 的信号到中断号的转换。在 Linux 的设置中, 4 对应的中断号是 0x24.
(6) 如果 8259 处于自动结束中断 (Automatic End of Interrupt AEOI) 状态,那么在刚才那个 INTA 信号结束前, 8259 的 ISR 复位 ( 也就是清 0), 如果不处于这个状态,那么直到 CPU 发出 EOI 指令,它才会使得 ISR 复位。
2. 一些相关专题
(1) 从 8259
在 x86 单 CPU 的机器上采用两个 8259 芯片,主芯片如上图所示, x86 模式规定 , 从 8259 将它的 INT 脚与主 8259 的 IR2 相连,这样,如果从 8259 芯片的引脚 IR8-IR15 上有中断,那么会在 INT 上产生信号,主 8259 在 IR2 上产生了一个硬件信号,当它如上面的步骤处理后将 IR2 的中断传送给 CPU, 收到应答后,会通过 CAS 通知从 8259 芯片,从 8259 芯片将 IRQ 中断号送到数据线上,从而被 CPU 接收。
由此,我猜测它产生的所有中断在主 8259 上优先级为 2 ,不知道对不对。
(2) 关于屏蔽
从上面可以看出,屏蔽有两种方法,一种作用于 CPU, 通过清除 IF 标记,使得 CPU 不去响应 8259 在 INT 上的请求。也就是所谓关中断。
另一种方法是,作用于 8259, 通过给它指令设置 IMR, 使得相应的 IRR 不参与 ISR( 见上面的 (4)), 被称为禁止 (disable), 反之,被称为允许 (enable).
每次设置 IMR 只需要对端口 0x21( 主 ) 或 0xA1( 从 ) 输出一个字节即可,字节每位对应于 IMR 每位 , 例如 :
outb(cached_21,0x21);
为了统一处理 16 个中断, Linux 用一个 16 位 cached_irq_mask 变量来记录这 16 个中断的屏蔽情况 :
static unsigned int cached_irq_mask = 0xffff;
为了分别对应于主从芯片的 8 位 IMR, 将这 16 位 cached_irq_mask 分成两个 8 位的变量 :
#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))
在禁用某个 irq 的时候 , 调用下面的函数 :
void disable_8259A_irq(unsigned int irq){
unsigned int mask = 1 << irq;
unsigned long flags;
spin_lock_irqsave(&i8259A_lock, flags);
cached_irq_mask |= mask;/*-- 对这16位变量设置 */
if (irq & 8)/*-- 看是对主8259设置还是对从芯片设置 */
outb(cached_A1,0xA1);/*-- 对从8259芯片设置 */
else
outb(cached_21,0x21);/*-- 对主8259芯片设置 */
spin_unlock_irqrestore(&i8259A_lock, flags);
}
(3) 关于中断号的输出
8259 在 ISR 里保存的只是 irq 的 ID, 但是它告诉 CPU 的是中断向量 ID, 比如 ISR 保存时钟中断的 ID 0, 但是在通知 CPU 却是中断号 0x20. 因此需要建立一个映射。在 8259 芯片产生的 IRQ 号必须是连续的,也就是如果 irq0 对应的是中断向量 0x20, 那么 irq1 对应的就是 0x21,...
在 i8259.c/init_8259A() 中,进行设置 :
outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */
outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */
if (auto_eoi)
outb_p(0x03, 0x21); /* master does Auto EOI */
else
outb_p(0x01, 0x21); /* master expects normal EOI */
outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */
outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */
outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be investigated) */
这样,在 IDT 的向量 0x20-0x2f 可以分别填入相应的中断处理函数的地址了。
向量表设置
注 : 图片均取自 i386 开发者手册 .
i386 中断门描述符
描述符如图 :
段选择符和偏移量决定了中断处理函数的入口地址,如下图。
在这里段选择符指向内核中唯一的一个代码段描述符的地址 __KERNEL_CS(=0x10) ,而这个描述符定义的段为 0 到 4G:
---------------------------------------------------------------------------------
ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
... ...
---------------------------------------------------------------------------------
而偏移量就成了绝对的偏移量了,在 IDT 的描述符中被拆成了两部分,分别放在头和尾。
P 标志着这个代码段是否在内存中,本来是 i386 提供的类似缺页的机制,在 Linux 中这个已经不用了,都设成 1( 当然内核代码是永驻内存的,但即使不在内存,推测 linux 也只会用缺页的标志 ) 。
DPL 在这里是 0 级 ( 特权级 )
0D110 中, D 为 1 ,表明是 32 位程序 ( 这个细节见 i386 开发手册 ).110 是中断门的标识,其它 101 是任务门的标识 , 111 是陷阱 (trap) 门标识。
Linux 对中断门的设置
于是在 Linux 中对硬件中断的中断门的设置为 :
init_IRQ(void)
---------------------------------------------------------
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[ i]);
}
----------------------------------------------------------
其中, FIRST_EXTERNAL_VECTOR=0x20, 恰好为 8259 芯片的 IR0 的中断门 ( 见 8259 部分 ), 也就是时钟中断的中断门 ),interrupt[ i] 为相应处理函数的入口地址
NR_IRQS=224, =256(IDT 的向量总数 )-32(CPU 保留的中断的个数 ), 在这里设置了所有可设置的向量。
SYSCALL_VECTOR=0x80, 在这里意思是避开系统调用这个向量。
而 set_intr_gate 的定义是这样的 :
----------------------------------------------------
void set_intr_gate(unsigned int n, void *addr){
_set_gate(idt_table+n,14,0,addr);
}
----------------------------------------------------
其中,需要解释的是 :14 是标识指明这个是中断门 , 注意上面的 0D110=01110=14; 另外, 0 指明的是 DPL.
中断入口
以 8259 的 16 个中断为例 :
通过宏 BUILD_16_IRQS(0x0), BI(x,y), 以及
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
" pushl $"#nr"-256\n\t" \
"jmp common_interrupt");
得到的 16 个中断处理函数为 :
IRQ0x00_interrupt:
push $0x00 - 256
jump common_interrupt
IRQ0x00_interrupt:
push $0x01 - 256
jump common_interrupt
... ...
IRQ0x0f_interrupt:
push $0x0f - 256
jump common_interrupt
这些处理函数简单的把中断号 -256( 为什么 -256 ,也许是避免和内部中断的中断号有冲突 ) 压到栈中,然后跳到 common_interrupt
其中 common_interrupt 是由宏 BUILD_COMMON_IRQ() 展开 :
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));
.align 4,0x90common_interrupt:
SAVE_ALL 展开的保护现场部分
push $ret_from_intrcall
do_IRQ:
jump do_IRQ;
从上面可以看出,这 16 个的中断处理函数不过是把中断号 -256 压入栈中,然后保护现场,最后调用 do_IRQ . 在 common_interrupt 中,为了使 do_IRQ 返回到 entry.S 的 ret_from_intr 标号,所以采用的是压入返回点 ret_from_intr, 用 jump 来模拟一个从 ret_from_intr 上面对 do_IRQ 的一个调用。
和 IDT 的衔接
为了便于 IDT 的设置,在数组 interrupt 中填入所有中断处理函数的地址 :
void (*interrupt[NR_IRQS])(void) = {
IRQ0x00_interrupt,
IRQ0x01_interrupt,
... ...
}
在中断门的设置中,可以看到是如何利用这个数组的。
硬件中断处理函数 do_IRQ
do_IRQ 的相关对象
在 do_IRQ 中,一个中断主要由三个对象来完成,如图 :
其中 , irq_desc_t 对象构成的 irq_desc[] 数组元素分别对应了 224 个硬件中断 (idt 一共 256 项, cpu 自己前保留了 32 项, 256-32=224 ,当然这里面有些项是不用的,比如 x80 是系统调用 ).
当发生中断时,函数 do_IRQ 就会在 irq_desc[] 相应的项中提取各种信息来完成对中断的处理。
irq_desc 有一个字段 handler 指向发出这个中断的设备的处理对象 hw_irq_controller, 比如在单 CPU ,这个对象一般就是处理芯片 8259 的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备 ( 比如 8259 芯片 ) 进行操作,于是可以通过这个指针指向的对象进行操作。
irq_desc 还有一个字段 action 指向对象 irqaction ,后者是产生中断的设备的处理对象,其中的 handler 就是处理函数。由于一个中断可以由多个设备发出, Linux 内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。
例如,硬盘 1 ,硬盘 2 都产生中断 IRQx, 在 do_IRQ 中首先找到 irq_desc[x], 通过字段 handler 对产生中断 IRQx 的设备进行处理 ( 对 8259 而言,就是屏蔽以后的中断 IRQx), 然后通过 action 先后运行硬盘 1 和硬盘 2 的处理函数。
hw_irq_controller
hw_irq_controller 有多种 :
1. 在一般单 cpu 的机器上,通常采用两个 8259 芯片,因此 hw_irq_controller 指的就是 i8259A_irq_type
2. 在多 CPU 的机器上,采用 APIC 子系统来处理芯片, APIC 有 3 个部分组成,一个是 I/O APIC 模块,其作用可比做 8259 芯片,但是它发出的中断信号会通过 APIC 总线送到其中一个 ( 或几个 )CPU 中的 Local APIC 模块,因此,它还起一个路由的作用;它可以接收 16 个中断。
中断可以采取两种方式,电平触发和边沿触发,相应的, I/O APIC 模块的 hw_irq_controller 就有两种 :
ioapic_level_irq_type
ioapic_edge_irq_type
( 这里指的是 intel 的 APIC, 还有其它公司研制的 APIC, 我没有研究过 )
3. Local APIC 自己也能单独处理一些直接对 CPU 产生的中断,例如时钟中断 ( 这和没有使用 Local APIC 模块的 CPU 不同,它们接收的时钟中断来自外围的时钟芯片 ), 因此,它也有自己的 hw_irq_controller:
lapic_irq_type
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
} ;
typedef struct hw_interrupt_type hw_irq_controller;
startup 是启动中断芯片 ( 模块 ) ,使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽
shutdown 反之,使得芯片不再接收中断
enable 设某个引脚可以接收中断,也就是取消屏蔽
disable 屏蔽某个引脚,例如,如果屏蔽 0 那么时钟中断就不再发生
ack 当 CPU 收到来自中断芯片的中断信号,给相应的引脚的处理 , 这个各种情况下 (8259, APIC 电平,边沿 ) 的处理都不相同
end 在 CPU 处理完某个引脚产生的中断后,对中断芯片 ( 模块 ) 的操作。
irqaction
将一个硬件处理函数挂到相应的处理队列上去 ( 当然首先要生成一个 irqaction 结构 ):
-----------------------------------------------------
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
-----------------------------------------------------
参数说明在源文件里说得非常清楚。
handler 是硬件处理函数,在下面的代码中可以看得很清楚 :
---------------------------------------------
do {
status |= action->flags;
action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
---------------------------------------------
第二个参数就是 action 的 dev_id, 这个参数非常灵活,可以派各种用处。而且要保证的是,这个 dev_id 在这个处理链中是唯一的,否则删除会遇到麻烦。
第三个参数是在 entry.S 中压入的各个积存器的值。
它的大致流程是 :
1. 在 slab 中分配一个 irqaction ,填上必需的数据 以下在函数 setup_irq 中。
2. 找到它的 irq 对应的结构 irq_desc
3. 看它是否想对随机数做贡献
4. 看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的 ( 由于传递性,只需判断一个就可以了 )
5. 挂到队列最后
6. 如果这个 irq_desc 只有它一个 irqaction, 那么还要进行一些初始化工作
7.在 proc/ 下面登记 register_irq_proc(irq)( 这个我不太明白 )
将一个处理函数取下 :
void free_irq(unsigned int irq, void *dev_id)
首先在队列里找到这个处理函数 ( 严格的说是 irqaction), 主要靠 dev_id 来匹配,这时 dev_id 的唯一性就比较重要了。
将它从队列里剔除。
如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断 :
if (!desc->action) {
desc->status |= IRQ_DISABLED;
desc->handler->shutdown(irq);
}
如果其它 CPU 在运行这个处理函数,要等到它运行完了,才释放它 :
#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */
while (desc->status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);
do_IRQ
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
1. 首先取中断号 , 并且获取对应的 irq_desc:
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
2. 对中断芯片 ( 模块 ) 应答 :
desc->handler->ack(irq);
3 .修改它的状态(注:这些状态我觉得只有在 SMP 下才有意义):
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
IRQ_REPLAY 是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY 。
IRQ_WAITING 探测用,探测时,会将所有没有挂处理函数的中断号上设置 IRQ_WAITING, 如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS 是为了确保 :
同一个中断号的处理程序不能重入 不能丢失这个中断号的下一个处理程序
具体的说 , 当内核在运行某个中断号对应的处理程序 ( 链 ) 时,状态会设置成 IRQ_INPROGRESS 。如果在这期间,同一个中断号上又产生了中断,并且传给 CPU, 那么当内核打算再次运行这个中断号对应的处理程序 ( 链 ) 时,发现已经有一个实例在运行了,就将这下一个中断标注为 IRQ_PENDING, 然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
这些状态的操作不是在什么情况下都必须的,事实上,一个 CPU ,用 8259 芯片,无论即使是开中断,也不会发生中断重入的情况,因为在这期间,内核把同一中断屏蔽掉了。
多个 CPU 比较复杂,因为 CPU 由 Local APIC, 每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个 CPU 都可能产生,它们都会调用时钟中断处理函数。
从 I/O APIC 传过来的中断 , 如果是电平触发,也不会,因为在结束发出 EOI 前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么 I/O APIC 选择不同的 CPU, 在这两种情况下,会有重入的可能。
/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->action;
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 (!action)
goto out;/* 要么该中断没有处理函数;要么被禁止运行 (IRQ_DISABLE); 要么有一个实例已经在运行了 */
/*
* 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.
*/
for (;;) {
spin_unlock(&desc->lock);
handle_IRQ_event(irq, ®s, action);/* 执行函数链 */
spin_lock(&desc->lock);
if (!(desc->status & IRQ_PENDING))/* 发现期间有中断,就再次执行 */
break;
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.
*/
desc->handler->end(irq);/* 给中断芯片一个结束的操作 , 一般是允许再次接收中断 */
spin_unlock(&desc->lock);
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();/* 执行软中断 */
return 1;
}
软中断 softirq
softirq 简介
提出 softirq 的机制的目的和老版本的底半部分的目的是一致的,都是将某个中断处理的一部分任务延迟到后面去执行。
Linux 内核中一共可以有 32 个 softirq, 每个 softirq 实际上就是指向一个函数。当内核执行 softirq(do_softirq), 就对这 32 个 softirq 进行轮询:
(1) 是否该 softirq 被定义了,并且允许被执行 ?
(2) 是否激活了 ( 也就是以前有中断要求它执行 )?
如果得到肯定的答复,那么就执行这个 softirq 指向的函数。
值得一提的是,无论有多少个 CPU, 内核一共只有 32 个公共的 softirq, 但是每个 CPU 可以执行不同的 softirq, 可以禁止 / 起用不同的 softirq, 可以激活不同的 softirq, 因此,可以说,所有 CPU 有相同的例程,但是
每个 CPU 却有自己完全独立的实例。
对 (1) 的判断是通过考察 irq_stat[ cpu ].mask 相应的位得到的。这里面的 cpu 指的是当前指令所在的 cpu. 在一开始, softirq 被定义时,所有的 cpu 的掩码 mask 都是一样的。但是在实际运行中,每个 cpu 上运行的程序可以根据自己的需要调整。
对 (2) 的判断是通过考察 irq_stat[ cpu ].active 相应的位得到的 .
虽然原则上可以任意定义每个 softirq 的函数, Linux 内核为了进一步加强延迟中断功能,提出了 tasklet 的机制。 tasklet 实际上也就是一个函数。在第 0 个 softirq 的处理函数 tasklet_hi_action 中,我们可以看到,当执行这个函数的时候,会依次执行一个链表上所有的 tasklet.
我们大致上可以把 softirq 的机制概括成 :
内核依次对 32 个 softirq 轮询,如果遇到一个可以执行并且需要的 softirq ,就执行对应的函数,这些函数有可能又会执行一个函数队列。当执行完这个函数队列后,才会继续询问下一个 softirq 对应的函数。
挂上一个软中断
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;
spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
for (i=0; i<NR_CPUS; i++)
softirq_mask(i) |= (1<<nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}
其中对每个 CPU 的 softirq_mask 都标注一下,表明这个 softirq 被定义了。
tasklet
在这个 32 个 softirq 中,有的 softirq 的函数会依次执行一个队列中的 tasklet ,如第一帖中图所示。
tasklet 其实就是一个函数。它的结构如下 :
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
next 用于将 tasklet 串成一个队列
state 表示一些状态,后面详细讨论
count 用来禁用 (count = 1 ) 或者启用 ( count = 0 ) 这个 tasklet. 因为一旦一个 tasklet 被挂到队列里,如果没有这个机制,它就一定会被执行。 这个 count 算是一个事后补救措施,万一挂上了不想执行,就可以把它置 1 。
func 即为所要执行的函数。
data 由于可能多个 tasklet 调用公用函数,因此用 data 可以区分不同 tasklet.
如何将一个 tasklet 挂上
首先要初始化一个 tasklet, 填上相应的参数
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->func = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}
然后调用 schedule 函数,注意,下面的函数仅仅是将这个 tasklet 挂到 TASKLET_SOFTIRQ 对应的软中断所执行的 tasklet 队列上去, 事实上,还有其它的软中断,比如 HI_SOFTIRQ, 会执行其它的 tasklet 队列,如果要挂上,那么就要调用 tasklet_hi_schedule(). 如果你自己写的 softirq 执行一个 tasklet 队列,那么你需要自己写类似下面的函数。
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
{
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
/**/ t->next = tasklet_vec[cpu].list;
/**/ tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
这个函数中 /**/ 标注的句子用来挂接上 tasklet,
__cpu_raise_softirq 用来激活 TASKLET_SOFTIRQ, 这样,下次执行 do_softirq 就会执行这个 TASKLET_SOFTIRQ 软中断了
__cpu_raise_softirq 定义如下 :
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
tasklet 的运行方式
我们以 tasklet_action 为例,来说明 tasklet 运行机制。事实上,还有一个函数 tasklet_hi_action 同样也运行 tasklet 队列。
首先值得注意的是,我们前面提到过,所有的 cpu 共用 32 个 softirq ,但是同一个 softirq 在不同的 cpu 上执行的数据是独立的,基于这个原则, tasklet_vec 对每个 cpu 都有一个,每个 cpu 都运行自己的 tasklet 队列。
当执行一个 tasklet 队列时,内核将这个队列摘下来,以 list 为队列头,然后从 list 的下一个开始依次执行。这样做达到什么效果呢?在执行这个队列时,这个队列的结构是静止的,如果在运行期间,有中断产生,并且往这个队列里添加 tasklet 的话,将填加到 tasklet_vec[cpu].list 中, 注意这个时候,这个队列里的任何 tasklet 都不会被执行,被执行的是 list 接管的队列。
见 /*1*//*2/ 之间的代码。事实上,在一个队列上同时添加和运行也是可行的,没这个简洁。
-----------------------------------------------------------------
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
/*1*/ local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
/*2*/ local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->next;
/*3*/ if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
/*?*/ smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
/*4*/ local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
/*5*/ local_irq_enable();
}
}
-------------------------------------------------------------
/*3*/ 看其它 cpu 是否还有同一个 tasklet 在执行,如果有的话,就首先将这个 tasklet 重新放到 tasklet_vec[cpu].list 指向的预备队列 ( 见 /*4*/~/*5*/) ,而后跳过这个 tasklet.
这也就说明了 tasklet 是不可重入的,以防止两个相同的 tasket 访问同样的变量而产生竞争条件 (race condition)
tasklet 的状态
在 tasklet_struct 中有一个属性 state ,用来表示 tasklet 的状态:
tasklet 的状态有 3 个:
1. 当 tasklet 被挂到队列上,还没有执行的时候,是 TASKLET_STATE_SCHED
2. 当 tasklet 开始要被执行的时候,是 TASKLET_STATE_RUN
其它时候,则没有这两个位的设置
其实还有另一对状态,禁止或允许, tasklet_struct 中用 count 表示 , 用下面的函数操作
-----------------------------------------------------
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
atomic_dec(&t->count);
}
-------------------------------------------------------
下面来验证 1,2 这两个状态 :
当被挂上队列时 :
首先要测试它是否已经被别的 cpu 挂上了,如果已经在别的 cpu 挂上了,则不再将它挂上,否则设置状态为 TASKLET_STATE_SCHED
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
... ...
}
为什么要这样做 ? 试想,如果一个 tasklet 已经挂在一队列上,内核将沿着这个队列一个个执行,现在如果又被挂到另一个队列上,那么这个 tasklet 的指针
指向另一个队列,内核就会沿着它走到错误的队列中去了。
tasklet 开始执行时 :
在 tasklet_action 中 :
------------------------------------------------------------
while (list != NULL) {
struct tasklet_struct *t = list;
/*0*/ list = list->next;
/*1*/ if (tasklet_trylock(t)) {
/*2*/ if (atomic_read(&t->count) == 0) {
/*3*/ clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
/*4*/ tasklet_unlock(t);
continue;
}
---------------------------------------------------------------
1 看是否是别的 cpu 上这个 tasklet 已经是 TASKLET_STATE_RUN 了,如果是就跳过这个 tasklet
2 看这个 tasklet 是否被允许运行 ?
3 清除 TASKLET_STATE_SCHED ,为什么现在清除,它不是还没有从队列上摘下来吗?事实上,它的指针已经不再需要的,它的下一个 tasklet 已经被 list 记录了 (/*0*/) 。这样,如果其它 cpu 把它挂到其它的队列上去一点影响都没有。
4 清除 TASKLET_STATE_RUN 标志
1 和 4 确保了在所有 cpu 上,不可能运行同一个 tasklet, 这样在一定程度上确保了 tasklet 对数据操作是安全的,但是不要忘了,多个 tasklet 可能指向同一个函数,所以仍然会发生竞争条件。
可能会有疑问 : 假设 cpu 1 上已经有 tasklet 1 挂在队列上了, cpu2 应该根本挂不上同一个 tasklet 1, 怎么会有 tasklet 1 和它发生重入的情况呢 ?
答案就在 /*3*/ 上,当 cpu 1 的 tasklet 1 已经不是 TASKLET_STATE_SCHED ,而它还在运行,这时 cpu2 完全有可能挂上同一个 tasklet 1, 而且使得它试图运行,这时 /*1*/ 的判断就起作用了。
软中断的重入
如图,一般情况下,在硬件中断处理程序后都会试图调用 do_softirq 执行软中断,但是如果发现现在已经有中断在运行,或者已经有软中断在运行,则
不再运行自己调用的中断。也就是说,软中断是不能进入硬件中断部分的 , 并且软中断在一个 cpu 上是不可重入的,或者说是串行化的 (serialize)
其目的是避免访问同样的变量导致竞争条件的出现。在开中断的中断处理程序中不允许调用软中断可能是希望这个中断处理程序尽快结束。
这是由 do_softirq 中的
if (in_interrupt())
return;
保证的 .
其中,
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
前者 local_irq_count(_cpu):
当进入硬件中断处理程序时, handle_IRQ_event 中的 irq_enter(cpu, irq) 会将它加 1, 表明又进入一个硬件中断
退出则调用 irq_exit(cpu, irq)
后者 local_bh_count(__cpu) :
当进入软中断处理程序时, do_softirq 中的 local_bh_disable() 会将它加 1 ,表明处于软中断中
local_bh_disable();
一个例子:
当内核正在执行处理定时器的软中断时,这期间可能会发生多个时钟中断,这些时钟中断的处理程序都试图再次运行处理定时器的软中断,但是由于 已经有个软中断在运行了,于是就放弃返回。
软中断调用时机
最直接的调用:
当硬中断执行完后,迅速调用 do_softirq 来执行软中断 ( 见下面的代码 ) ,这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都成功的,见前面关于重入的帖子。
-----------------------------------------------------
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
... ...
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
}
-----------------------------------------------------
还有,不是每个被标注的软中断都能在这次陷入内核的部分中完成,可能会延迟到下次中断。
其它地方的调用 :
在 entry.S 中有一个调用点 :
handle_softirq:
call SYMBOL_NAME(do_softirq)
jmp ret_from_intr
有两处调用它,一处是当系统调用处理完后 :
ENTRY(ret_from_sys_call)
#ifdef CONFIG_SMP
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
一处是当异常处理完后:
注意其中的 irq_stat, irq_stat +4 对应的就是字段 active 和 mask
既然我们每次调用完硬中断后都马上调用软中断,为什么还要在这里调用呢 ?
原因可能都多方面的 :
(1) 在系统调用或者异常处理中同样可以标注软中断,这样它们在返回前就能得以迅速执行
(2) 前面提到,有些软中断要延迟到下次陷入内核才能执行,系统调用和异常都陷入内核,所以可以尽早的把软中断处理掉
(3) 如果在异常或者系统调用中发生中断,那么前面提到,可能还会有一些软中断没有处理,在这两个地方做一个补救工作,尽量避免到下次陷入内核才处理这些软中断。
另外,在切换前也调用。
bottom half
2.2.x 中的 bottom half :
2.2.x 版本中的 bottom half 就相当于 2.4.1 中的 softirq. 它的问题在于只有 32 个 , 如果要扩充的话,需要 task 队列 ( 这里 task 不是进程,而是函数 ) ,还有一个比较大的问题,就是虽然 bottom half 在一个 CPU 上是串行的 ( 由 local_bh_count[cpu] 记数保证 ), 但是在多 CPU 上是不安全的,例如,一个 CPU 上在运行关于定时器的 bottom half, 另一个 CPU 也可以运行同一个 bottom half, 出现了重入。
2.4.1 中的 bottom half
2.4.1 中,用 tasklet 表示 bottom half, mark_bh 就是将相应的 tasklet 挂到运行队列里 tasklet_hi_vec[cpu].list, 这个队列由 HI_SOFTIRQ 对应的 softirq 来执行。
另外,用一个全局锁来保证,当一个 CPU 上运行一个 bottom half 时,其它 CPU 上不能运行任何一个 bottom half 。这和以前的 bottom half 有所不同,不知道是否我看错了。
用 32 个 tasklet 来表示 bottom half:
struct tasklet_struct bh_task_vec[32];
首先,初始化所有的 bottom half:
void __init softirq_init()
{
... ...
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
... ...
}
这里 bh_action 是下面的函数,它使得 bottom half 运行对应的 bh_base 。
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
/*1*/ if (!spin_trylock(&global_bh_lock))
goto resched;
if (!hardirq_trylock(cpu))
goto resched_unlock;
if (bh_base[nr])
bh_base[nr]();
hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;
resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
ret_from_exception:
#ifdef CONFIG_SMP
GET_CURRENT(%ebx)
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
/*1*/ 试图上锁 , 如果得不到锁,则重新将 bottom half 挂上,下次在运行。
当要定义一个 bottom half 时用下面的函数 :
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}
取消定义时,用 :
void remove_bh(int nr)
{
tasklet_kill(bh_task_vec+nr);
bh_base[nr] = NULL;
}
tasklet_kill 确保这个 tasklet 被运行了,因而它的指针也没有用了。
激活一个 bottom half, 就是将它挂到队列中 :
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}