1、基本输入输出方式
现代体系结构的基本输入输出方式有三种:
(1)程序查询:
CPU周期性询问外部设备是否准备就绪。该方式的明显的缺点就是浪费CPU资源,效率低下。
但是,不要轻易的就认为该方式是一种不好的方式(漂亮的女人不一定好,不漂亮的女人通常很可爱),通常效率低下是由于CPU在大部分时间没事可做造成的,这种轮询方式自有应用它的地方。例如,在网络驱动中,通常接口(Interface)每接收一个报文,就发出一个中断。而对于高速网络,每秒就能接收几千个报文,在这样的负载下,系统性能会受到极大的损害。
为了提高系统性能,内核开发者已经为网络子系统开发了一种可选的基于查询的接口NAPI(代表new API)。当系统拥有一个高流量的高速接口时,系统通常会收集足够多的报文,而不是马上中断CPU。
(2)中断方式
这是现代CPU最常用的与外围设备通信方式。相对于轮询,该方式不用浪费稀缺的CPU资源,所以高效而灵活。中断处理方式的缺点是每传送一个字符都要进行中断,启动中断控制器,还要保留和恢复现场以便能继续原程序的执行,花费的工作量很大,这样如果需要大量数据交换,系统的性能会很低。
(3)DMA方式
通常用于高速设备,设备请求直接访问内存,不用CPU干涉。但是这种方式需要DMA控制器,增加了硬件成本。在进行DMA数据传送之前,DMA控制器会向CPU申请总线控制 权,CPU如果允许,则将控制权交出,因此,在数据交换时,总线控制权由DMA控制器掌握,在传输结束后,DMA控制器将总线控制权交还给CPU。
2、中断概述
2.1、中断向量
X86支持256个中断向量,依次编号为0~255。它们分为两类:
(1)异常,由CPU内部引起的,所以也叫同步中断,不能被CPU屏蔽;它又分为Faults(可更正异常,恢复后重新执行),Traps(返回后执行发生trap指令的后一条指令)和Aborts(无法恢复,系统只能停机);
(2)中断,由外部设备引起的。它又分为可屏蔽中断(INTR)和非可屏蔽中断(NMI)。
Linux对256个中断向量分配如下:
(1)0~31为异常和非屏蔽中断,它实际上被Intel保留。
(2)32~47为可屏蔽中断。
(3)余下的48~255用来标识软中断;Linux只用了其中一个,即128(0x80),用来实现系统调用。当用户程序执行一条int 0x80时,就会陷入内核态,并执行内核函数system_call(),该函数与具体的架构相关。
2.2、可屏蔽中断
X86通过两个级连的8259A中断控制器芯片来管理15个外部中断源,如图所示:
外部设备要使用中断线,首先要申请中断号(IRQ),每条中断线的中断号IRQn对应的中断向量为n+32,IRQ和向量之间的映射可以通过中断控制器商端口来修改。X86下8259A的初始化工作及IRQ与向量的映射是在函数init_8259A()(位于arch/i386/kernel/i8259.c)完成的。
CPU通过INTR引脚来接收8259A发出的中断请求,而且CPU可以通过清除EFLAG的中断标志位(IF)来屏蔽外部中断。 当IF=0时,禁止任何外部I/O请求,即关中断 (对应指令cli)。另外,中断控制器有一个8位的中断屏蔽寄存器(IMR),每位对应8259A中的一条中断线,如果要禁用某条中断线,相应的位置1即可,要启用,则置0。
IF标志位可以使用指令STI和CLI来设置或清除。并且只有当程序的CPL<=IOPL时才可执行这两条指令,否则将引起一般保护性异常(通常来说,in,ins,out,outs,cli,sti只有在CPL<=IOPL时才能执行,这些指令称为 I/O敏感指令 )。
以下一些操作也会影响IF标志位:
(1)PUSHF指令将EFLAGS内容存入堆栈,且可以在那里修改。POPF可将已经修改过的内容写入EFLAGS寄存器。
(2)任务切换和IRET指令会加载EFLAGS寄存器。因此,可修改IF标志。
(3)通过中断门处理一个中断时,IF标志位被自动清除,从而禁止可尽屏蔽中断。但是, 陷阱门不会复位IF。
2.3、异常及非屏蔽中断
异常就是CPU内部出现的中断,也就是说,在CPU执行特定指令时出现的非法情况。非屏蔽中断就是计算机内部硬件出错时引起的异常情况。从上图可以看出,二者与外部I/O接口没有任何关系。Intel把非屏蔽中断作为异常的一种来处理,因此,后面所提到的异常也包括了非屏蔽中断。在CPU执行一个异常处理程序时,就不再为其他异常或可屏蔽中断请求服务,也就是说,当某个异常被响应后,CPU清除EFLAG的中IF位,禁止任何可屏蔽中断(IF不能禁止异常和非可屏蔽中断)。但如果又有异常产生,则由CPU锁存(CPU具有缓冲异常的能力),待这个异常处理完后,才响应被锁存的异常。我们这里讨论的异常中断向量在0~31之间,不包括系统调用(中断向量为0x80)。
2.4、中断描述符表
2.4.1、中断描述符
在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中断处理程序的入口地址。但是,在保护模式下,由四字节的表项构成的中断向量表显然满足不了要求。这是因为,除了两个字节的段描述符,偏移量必用四字节来表示;要有反映模式切换的信息。因此,在保护模式下,中断向量表中的表项由8个字节组成,中断向量表也改叫做中断描述符表IDT(Interrupt Descriptor Table)。其中的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。门描述符的一般格式如下:
中断描述符表中可放三类门描述符:
(1)中断门(Interrupt gate)
其类型码为110,它包含一个中断或异常处理程序所在的段选择符和段内偏移。控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。 中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门 。所有的中断处理程序都由中断门激活,并全部限制在内核态。设置中断门的代码如下:
void set_intr_gate(unsigned int n, void * addr)
{ // type=14,dpl=0,selector=__KERNEL_CS
_set_gate(idt_table + n, 14 , 0 ,addr,__KERNEL_CS);
}
struct desc_struct idt_table[ 256 ] __attribute__((__section__( " .data.idt " ))) = { { 0 , 0 }, };
// 描述符结构
struct desc_struct {
unsigned long a,b;
};
其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。其设置代码如下:
{
_set_gate(idt_table + n, 15 , 0 ,addr,__KERNEL_CS);
}
IDT中的任务门描述符格式与GDT和LDT中的任务门格式相同,含有一个任务TSS段的选择符,该任务用于处理异常或中断,Linux用于处理Double fault。其设置代码如下:
{
_set_gate(idt_table + n, 5 , 0 , 0 ,(gdt_entry << 3 ));
}
此外,在Linux中还有系统门(System gate),用于处理用户态下的异常overflow,bound以及系统调用int 0x80;以及系统中断门(system interrupt gate),用来处理int3,这样汇编指令int3就能在用户态下调用。
{
_set_gate(idt_table + n, 15 , 3 ,addr,__KERNEL_CS);
}
// 设置系统调用门描述符,在trap.c中被trap_init()调用
set_system_gate(SYSCALL_VECTOR, & system_call);
// 设置系统中断门
static inline void set_system_intr_gate(unsigned int n, void * addr)
{
_set_gate(idt_table + n, 14 , 3 , addr, __KERNEL_CS);
}
// 位于arch/i386/kernel/traps.c
void __init trap_init( void )
{
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);
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);
set_system_gate(SYSCALL_VECTOR, & system_call);
}
中断描述表的最终初始化是init/main.c中的start_kernel()中完成的
{
// 陷阱门初始化
trap_init();
// 中断门初始化
init_IRQ();
// 软中断初始化
softirq_init();
}
void __init init_IRQ( void )
{
// 调用init_ISA_irqs
pre_intr_init_hook();
// 设置中断门
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]);
}
}
3、内核的中断处理
3.1、中断处理入口
由上节可知,中断向量的对应的处理程序位于interrupt数组中,下面来看看interrupt:
342 ENTRY(interrupt)
343 .text
344
345 vector= 0
346 ENTRY(irq_entries_start)
347 .rept NR_IRQS # 348 -354行重复NR_IRQS次
348 ALIGN
349 1 : pushl $vector- 256 #vector在354行递增
350 jmp common_interrupt #所有的外部中断处理函数的统一部分,以后再讲述
351 .data
352 .long 1b #存储着指向349行的地址,但是随着348行-354被gcc展开,每次的值都不同
353 .text
354 vector=vector+ 1
355 .endr #与347行呼应
356
357 ALIGN
#公共处理函数
common_interrupt:
SAVE_ALL /*寄存器值入栈*/
movl %esp,%eax /*栈顶指针保存到eax*/
call do_IRQ /*处理中断*/
jmp ret_from_intr /*从中断返回*/
首先342行和352行都处于.data段,虽然看起来它们是隔开的,但实际上被gcc安排在了连续的数据段内存 中,同理在代码段内存中,354行与350行的指令序列也是连续存储的。另外,348-354行会被gcc展开NR_IRQS次,因此每次352行都会存 储一个新的指针,该指针指向每个349行展开的新对象。最后在代码段内存中连续存储了NR_IRQS个代码片断,首地址由 irq_entries_start指向。而在数据段内存中连续存储了NR_IRQS个指针,首址存储在interrupt这个全局变量中。这样,例如 IRQ号是0 (从init_IRQ()调用,它对应的中断向量是FIRST_EXTERNAL_VECTOR)的中断通过中断门后会触发 interrput[0],从而执行:
pushl 0-256
jmp common_interrupt
的代码片断,进入到Linux内核安排好的中断入口路径。
3.2、数据结构
3.2.1、IRQ描述符
Linux支持多个外设共享一个IRQ,同时,为了维护中断向量和中断服务例程(ISR)之间的映射关系,Linux用一个irq_desc_t数据结构来描述,叫做IRQ描述符。除了分配给异常的
32个向量外,其余224(NR_IRQS)个中断向量对应的IRQ构成一个数组irq_desc[],定义如下:
typedef struct irq_desc {
unsigned int status; /* IRQ status */
hw_irq_controller * handler;
struct irqaction * action; /* IRQ action list */
unsigned int depth; /* nested irq disables */
unsigned int irq_count; /* For detecting broken interrupts */
unsigned int irqs_unhandled;
spinlock_t lock ;
} ____cacheline_aligned irq_desc_t;
// IRQ描述符表
extern irq_desc_t irq_desc [NR_IRQS];
status:描述IRQ中断线状态,在irq.h中定义。如下:
#define IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define IRQ_PENDING 4 /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
#define IRQ_REPLAY 8 /* 当Linux重新发送一个已被删除的IRQ时 */
#define IRQ_AUTODETECT 16 /* 当进行硬件设备探测时,内核使用这条IRQ中断线 */
#define IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
action:指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的irqaction结构。下面有描述。
depth:如果启用这条IRQ中断线,depth则为0,如果禁用这条IRQ中断线不止一次,则为一个正数。每当调用一次disable_irq(),该函数就对这个域的值加1;如果depth等于0,该函数就禁用这条IRQ中断线。相反,每当调用enable_irq()函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。
lock:保护该数据结构的自旋锁。
IRQ描述符的初始化:
void __init init_ISA_irqs ( void )
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
// 初始化8259A
init_8259A( 0 );
// IRQ描述符的初始化
for (i = 0 ; i < NR_IRQS; i ++ ) {
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].action = NULL;
irq_desc[i].depth = 1 ;
if (i < 16 ) {
/*
* 16 old-style INTA-cycle interrupts:
*/
irq_desc[i].handler = & i8259A_irq_type;
} else {
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc[i].handler = & no_irq_type;
}
}
}
3.2.2、中断控制器描述符hw_interrupt_type
这个描述符包含一组指针,指向与特定的可编程中断控制器电路(PIC)打交道的低级I/O例程,定义如下:
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, cpumask_t dest);
};
typedef struct hw_interrupt_type hw_irq_controller;
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
};
3.2.3、中断服务例程描述符irqaction
为了处理多个设备共享一个IRQ,Linux中引入了irqaction数据结构。定义如下:
struct irqaction {
irqreturn_t ( * handler)( int , void * , struct pt_regs * );
unsigned long flags;
cpumask_t mask;
const char * name;
void * dev_id;
struct irqaction * next;
int irq;
struct proc_dir_entry * dir;
};
flags:用一组标志描述中断线与I/O设备之间的关系。
SA_INTERRUPT
中断处理程序必须以禁用中断来执行
SA_SHIRQ
该设备允许其中断线与其他设备共享。
SA_SAMPLE_RANDOM
可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器。(用户可以从/dev/random 和/dev/urandom设备文件中取得随机数而访问这种特征)
SA_PROBE
内核在执行硬件设备探测时正在使用这条中断线。
name:I/O设备名(通过读取/proc/interrupts文件,可以看到,在列出中断号时也显示设备名。)
dev_id:指定I/O设备的主设备号和次设备号。
next:指向irqaction描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。
irq:IRQ线。
3.2.4、中断服务例程(Interrupt Service Routine)
在Linux中,中断服务例程和中断处理程序(Interrupt Handler)是两个不同的概念。可以这样认为,中断处理程序相当于某个中断向量的总处理程序,它与中断描述表(IDT)相关;中断服务例程(ISR)在中断处理过程被调用,它与IRQ描述符相关,一般来说,它是设备驱动的一部分。
(1) 注册中断服务例程
中断服务例程是硬件驱动的组成部分,如果设备要使用中断,相应的驱动程序在初始化的过程中可以通过调用request_irq函数注册中断服务例程。
/* irq:IRQ号
**handler:中断服务例程
**irqflags:SA_SHIRQ,SA_INTERRUPT或SA_SAMPLE_RANDOM
**devname:设备名称,这些名称会被/proc/irq和/proc/interrupt使用
**dev_id:主要用于设备共享
*/
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;
// 分配数据结构空间
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完成真正的注册,驱动程序也可以调用它来完成注册
retval = setup_irq(irq, action);
if (retval)
kfree(action);
return retval;
}
static int __init rtc_init( void )
{
request_irq(RTC_IRQ, rtc_int_handler_ptr, SA_INTERRUPT, " rtc " , NULL);
}
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, " timer " , NULL, NULL};
// 由time_init()调用
void __init time_init_hook( void )
{
setup_irq( 0 , & irq0);
}
整个流程如下:
所有I/O中断处理函数的过程如下:
(1)把IRQ值和所有寄存器值压入内核栈;
(2) 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求;
(3)执行共享这个IRQ的所有设备的中断服务例程(ISR);
(4)跳到ret_from_intr()处结束。
3.3.1、保存现场与恢复现场
中断处理程序做的第一件事就是保存现场,由宏SAVE_ALL(位于entry.S中)完成:
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 ;
恢复现场由宏RESTORE_ALL完成
RESTORE_REGS \
addl $ 4 , %esp ; \
1 : 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
该函数的大致内容如下:
fastcall unsigned int do_IRQ( struct pt_regs * regs)
{
/* high bits used in ret_from_ code */
// 取得中断号
int irq = regs -> orig_eax & 0xff ;
// 增加代表嵌套中断数量的计数器的值,该值保存在current->thread_info->preempt_count
irq_enter();
__do_IRQ(irq, regs);
// 减中断计数器preempt_count的值,检查是否有软中断要处理
irq_exit();
}
long ebx ;
long ecx ;
long edx ;
long esi ;
long edi ;
long ebp ;
long eax ;
int xds ;
int xes ;
long orig_eax ;
long eip ;
int xcs ;
long eflags ;
long esp ;
int xss ;
} ;
3.3.3、__do_IRQ()函数
该函数的内容如下:
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 ;
}
/* 加自旋锁.对于多CPU系统,这是必须的,因为同类型的其它中断可能产生,并被其它的CPU处理,
**没有自旋锁,IRQ描述符将被多个CPU同时访问.
*/
spin_lock( & desc -> lock );
/* 确认中断.对于8259A PIC,由mask_and_ack_8259A()完成确认,并禁用当前IRQ线.
**屏蔽中断是为了确保该中断处理程序结束前,CPU不会又接受这种中断.虽然,CPU在处理中断会自动
**清除eflags中的IF标志,但是在执行中断服务例程前,可能重新激活本地中断.见handle_IRQ_event.
*/
/* 在多处理器上,应答中断依赖于具体的中断类型.可能由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_REPLAY和IRQ_WAITING标志
status = desc -> status & ~ (IRQ_REPLAY | IRQ_WAITING);
/* IRQ_PENDING表示一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
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;
/* 现在开始检查是否真的需要处理中断.在三种情况下什么也不做.
*(1)IRQ_DISABLED被设置.即使在相应的IRQ线被禁止的情况下,do_IRQ()也可能执行.
*(2)IRQ_INPROGRESS被设置时,在多CPU系统中,表示其它CPU正在处理同样中断的前一次发生.Linux中,同类型
*中断的中断服务例程由同一个CPU处理.这样使得中断服务例程不必是可重入的(在同一CPU上串行执行).
*
*(3)action==NULL.此时,直接跳到out处执行.
*/
if (likely( ! (status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc -> action;
// 清除IRQ_PENDING标志
status &= ~ IRQ_PENDING; /* we commit to handling */
/* 表示当前CPU正在处理该中断,其它CPU不应该处理同样的中断,而应该让给本CPU处理.一旦设置
*IRQ_INPROGRESS,其它CPU即使进行do_IRQ,也不会执行该程序段,则action==NULL,则其它CPU什么也不做.
*当调用handle_IRQ_event执行中断服务例程时,由于释放了自旋锁,其它CPU可能接受到同类型的中断(本CPU
*不会接受同类型中断),而进入do_IRQ(),并设置IRQ_PENDING.
*/
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.
*/
for (;;) {
irqreturn_t action_ret;
// 释放自旋锁
spin_unlock( & desc -> lock );
action_ret = handle_IRQ_event(irq, regs, action);
// 加自旋锁
spin_lock( & desc -> lock );
if ( ! noirqdebug)
note_interrupt(irq, desc, action_ret);
/* 如果此时IRQ_PENDING处于清除状态,说明中断服务例程已经执行完毕,退出循环.反之,说明在执行中断服务例程时,
*其它CPU进入过do_IRQ,并设置了IRQ_PENDING.也就是说其它CPU收到了同类型的中断.此时,应该清除
*IRQ_INPROGRESS,并重新循环,执行中断服务例程,处理其它CPU收到的中断.
*/
if (likely( ! (desc -> status & IRQ_PENDING)))
break ;
desc -> status &= ~ IRQ_PENDING;
}
/* 所有中断处理完毕,则清除IRQ_INPROGRESS */
desc -> status &= ~ IRQ_INPROGRESS;
out :
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
// 结束中断处理.对end_8259A_irq()仅仅是重新激活中断线.
/* 对于多处器,end应答中断(如果ack方法还没有做的话)
*/
desc -> handler -> end(irq);
// 最后,释放自旋锁,
spin_unlock( & desc -> lock );
return 1 ;
}
//kernel/irq/handle.c
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;
//开启本地中断,对于单CPU,仅仅是sti指令
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
//依次调用共享该中断向量的服务例程
do {
//调用中断服务例程
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
//关本地中断,对于单CPU,为cli指令
local_irq_disable();
return retval;
}
4、下半部
在中断处理过程中,不能睡眠。另外,它运行的时候,会把当前中断线在所有处理器上都屏蔽(在ack中完成屏蔽);更糟糕的情况是,如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁上所有本地中断(通过cli指令完成),所以,中断处理应该尽可能快的完成。所以Linux把中断处理分为上半部和下半部。
上半部由中断处理程序完成,它通常完成一些和硬件相关的操作,比如对中断的到达的确认。有时它还会从硬件拷贝数据,这些工作对时间非常敏感,只能靠中断处理程序自己完成。而把其它工作放到下半部实现。
下半部的执行不需要一个确切的时间,它会在稍后系统不太繁忙时执行。下半部执行的关键在于运行的时候允许响应所有的中断。最早,Linux用”bottom half”实现下半部,这种机制简称BH,但是即使属于不同的处理器,也不允许任何两个bottom half同时执行,这种机制简单,但是却有性能瓶颈。不久,又引入任务队列(task queue)机制来实现下半部,但该机制仍不够灵活,没法代替整个BH接口。
从2.3开始,内核引入软中断(softirqs)和tasklet,并完全取代了BH。2.5中,BH最终舍去,在2.6中,内核用有三种机制实现下半部:软中断,tasklet和工作队列。Tasklet是基于软中断实现的。软中断可以在多个CPU上同时执行,即使它们是同一类型的,所以,软中断处理程序必须是可重入的,或者显示的用自旋锁保护相应的数据结构。而相同的tasklet不能同时在多个CPU上执行,所以tasklet不必是可重入的;但是,不同类型的tasklet可以在多个CPU上同时执行。一般来说,tasklet比较常用,它可以处理绝大部分的问题;而软中断用得比较少,但是对于时间要求较高的地方,比如网络子系统,常用软中断处理下半部工作。
4.1、软中断
内核2.6中定义了6种软中断:
下标越低,优先级越高。
4.1.1、数据结构
(1)软中断向量
struct softirq_action
{
void ( * action)( struct softirq_action * ); // 待执行的函数
void * data; // 传给函数的参数
};
// kernel/softirq.c
// 软中断向量数组
static struct softirq_action softirq_vec[ 32 ] __cacheline_aligned_in_smp;
(2) preempt_count字段
位于任务描述符的preempt_count是用来跟踪内核抢占和内核控制路径嵌套关键数据。其各个位的含义如下:
位 描述
0——7 preemption counter,内核抢占计数器(最大值255)
8——15 softirq counter,软中断计数器(最大值255)
16——27 hardirq counter,硬件中断计数器(最大值4096)
28 PREEMPT_ACTIVE标志
第一个计数用来表示内核抢占被关闭的次数,0表示可以抢占。第二个计数器表示推迟函数(下半部)被关闭的次数,0表示推迟函数打开。第三个计数器表示本地CPU中断嵌套的层数,irq_enter()增加该值,irq_exit减该值。
宏in_interrupt()检查current_thread_info->preempt_count的hardirq和softirq来断定是否处于中断上下文。如果这两个计数器之一为正,则返回非零。
(3) 软中断控制/状态结构
softirq_vec是个全局量,系统中每个CPU所看到的是同一个数组。但是,每个CPU各有其自己的“软中断控制/状态”结构,这些数据结构形成一个以CPU编号为下标的数组irq_stat[](定义在include/asm-i386/hardirq.h中)
unsigned int __softirq_pending;
unsigned long idle_timestamp;
unsigned int __nmi_count; /* arch dependent */
unsigned int apic_timer_irqs; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
// 位于kernel/softirq.c
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
可以通过open_softirq注册软中断处理程序:
// nr:软中断的索引号
// softirq_action:处理函数
// data:传递给处理函数的参数值
void open_softirq( int nr, void ( * action)( struct softirq_action * ), void * data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
// 软中断初始化
void __init softirq_init( void )
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
4.1.3、触发软中断
raise_softirq会将软中断设置为挂起状态,并在下一次运行do_softirq中投入运行。
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;
// 保存IF值,并关中断
local_irq_save(flags);
// 调用wakeup_softirqd()
raise_softirq_irqoff(nr);
// 恢复IF值
local_irq_restore(flags);
}
inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
// 把软中断设置为挂起状态
__raise_softirq_irqoff(nr);
// 唤醒内核线程
if ( ! in_interrupt())
wakeup_softirqd();
}
在中断服务例程中触发软中断是最常见的形式。而中断服务例程通常作为设备驱动的一部分。例如,对于网络设备,当接口收到数据时,会产生一个中断,在中断服务例程中,最终会调用netif_rx函数处理接到的数据,而netif_rx作相应处理,最终以触发一个软中断结束处理。之后,内核在执行中断处理任务后,会调用do_softirq()。于是软中断就通过软中断处理函数去处理留给它的任务。
4.1.4、软中断执行
(1) do_softirq()函数
asmlinkage void do_softirq( void )
{
// 处于中断上下文,表明软中断是在中断上下文中触发的,或者软中断被关闭
/* 这个宏限制了软中断服务例程既不能在一个硬中断服务例程内部执行,
*也不能在一个软中断服务例程内部执行(即嵌套)。但这个函数并没有对中断服务例程的执行
*进行“串行化”限制。这也就是说,不同的CPU可以同时进入对软中断服务例程的执行,每个CPU
*分别执行各自所请求的软中断服务。从这个意义上说,软中断服务例程的执行是“并发的”、多序的。
*但是,这些软中断服务例程的设计和实现必须十分小心,不能让它们相互干扰(例如通过共享的全局变量)。
*/
if (in_interrupt())
return ;
// 保存IF值,并关中断
local_irq_save(flags);
// 调用__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);
asmlinkage void __do_softirq( void )
{
struct softirq_action * h;
__u32 pending;
/* 最多迭代执行10次.在执行软中断的过程中,由于允许中断,所以新的软中断可能产生.为了使推迟函数能够在
*较短的时间延迟内执行,__do_softirq会执行所有挂起的软中断,这可能会执行太长的时间而大大延迟返回用户
*空间的时间.所以,__do_softirq最多允许10次迭代.剩下的软中断在软中断内核线程ksoftirqd中处理.
*/
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
// 用局部变量保存软件中断位图
pending = local_softirq_pending();
/* 增加softirq计数器的值.由于执行软中断时允许中断,当do_IRQ调用irq_exit时,另一个__do_softirq实例可能
*开始执行.这是不允许的,推迟函数必须在CPU上串行执行.
*/
local_bh_disable();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
// 重置软中断位图,使得新的软中断可以发生
local_softirq_pending() = 0 ;
// 开启本地中断,执行软中断时,允许中断的发生
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1 ) {
// 执行软中断处理函数
h -> action(h);
rcu_bh_qsctr_inc(cpu);
}
h ++ ;
pending >>= 1 ;
} while (pending);
// 关闭中断
local_irq_disable();
// 再一次检查软中断位图,因为在执行软中断处理函数时,新的软中断可能产生.
pending = local_softirq_pending();
if (pending && -- max_restart)
goto restart;
/* 如果还有多的软中断没有处理,通过wakeup_softirqd唤醒内核线程处理本地CPU余下的软中断.
*/
if (pending)
wakeup_softirqd();
// 减softirq counter的值
__local_bh_enable();
}
内核会周期性的检查是否有挂起的软中断,它们位于内核代码的以下几个点:
(1)内核调用local_bh_enable()函数打开本地CPU的软中断:
void local_bh_enable( void )
{
preempt_count() -= SOFTIRQ_OFFSET - 1 ;
if (unlikely( ! in_interrupt() && local_softirq_pending()))
do_softirq(); // 软中断处理
// ……
}
(3)内核线程ksoftirqd被唤醒。
(4) smp_apic_timer_interrupt()完成处理本地时钟中断。
http://www.cnblogs.com/hustcat/archive/2009/08/11/1543889.html
http://www.cnblogs.com/hustcat/archive/2009/08/14/1546011.html
http://www.cnblogs.com/hustcat/archive/2009/08/15/1546601.html