分类:
2014-02-22 09:24:01
原文地址:Linux中断基础构架 作者:heijita
CPU硬件平台IA32
一、概念
我们知道,在计算机系统中,cpu要与外界设备进行交互,是靠两种基本方式来进行:中断和轮询。中断是指外界设备在有事件发生的情况下,发送硬件信号给CPU,cpu检测到这个硬件信号以后,停止当前手头上的工作(比如正在运行某个进程),转去处理这个硬件信号。比如网卡,在接受到packet从网络上传来的时候,会发送一个硬件信号给CPU,然后cpu会立即把网卡上接受到的packet copy到系统内存中,以便进行下一步的处理。这个下一步的处理含义很广,对于网卡驱动来说,就是协议栈的处理,即进行TCP/IP的代码处理。轮询是另外一种cpu与外界设备通信的方式,即cpu主动去获取外界设备的状态,但这时CPU相当于处于繁忙状态,不能进行动作;但中断机制可以把cpu从这个繁忙的轮询中剥离出来,有时间处理其他事情,只有外设有事情的时候才会占用到cpu,这样一来就大大提高了CPU的功效。
在LINUX中,中断系统是由硬件和软件共同组成的,缺一不可。
在中断系统中,硬件主要完成中断权限检查,系统堆栈自动切换,CPU状态保存等功能和中断处理代码自动跳转。
软件分为上半部和下半部。上半部用于完成一些需要快速处理的功能,具体到网卡驱动,就是把packet从网卡的buffer中拷贝到系统内存中,并且回给网卡一些硬件信号,表示CPU已经收到了这个中断,网卡可以再次向CPU发送中断请求。下半部完成的功能一般是需要处理的时间比较长,并且不是非常紧急的任务,比如TCP/IP协议栈的处理。之所以要把中断系统分为上半部和下半部,是为了提高中断的效率。因为在中断的处理中,默认情况下是关中断的,也就是说在处理一个中断的时候,CPU不会响应另外一个中断(通过屏蔽CPU的状态寄存器EFLAGS的IF位实现),所以如果中断处理程序执行的时间太长,会导致中断延迟过大甚至丢中断,造成系统级问题,所以Linux采用了一个很巧妙的办法,把中断处理分为上半部和下半部。上半部用于处理一些与硬件状态相关的任务,让外设能快速得到响应,并且上半部的处理中CPU是关中断的(CPU通过中断门时会自动关中断,即把CPU的状态寄存器EFLAG的IF位置0);下半部用于处理复杂的软件功能,在下半部的处理中,CPU是开中断的(通过sti汇编指令把CPU状态寄存器ELFAGS的IF位置1),这时允许另外的中断打断下半部的执行。
二、硬件动作
CPU在执行完一条指令后,处理器的控制单元会检测在执行指令的过程中是否发生了中断或异常,如果有则进入中断、异常处理周期。此时处理器的控制单元会根据产生中断、异常时系统的情况不同会做相应的处理,然后才由软件负责进行进一步的处理。
(1)在发生中断、异常后,处理器中的中断控制逻辑首先判断当前中断、异常的类型似乎否为软中断(用户使用INTn, INT3 , INTO指令故意引发的异常)。如果不是软中断,跳到第3步;如果是软中断,则顺序到第2步做进一步的处理。
(2)此时处理器的控制单元需要判断当前的运行级别是否有权限访问相应的门描述符,在CPL>门描述符的DPL的情况下,产生一个通用保护异常"General protection"。CPL, DPL的取值为0、1、2、3,且数值越大权限越低。通过该权限检查,可以防止用户通过编程手段访问未经授权的处理程序,保证系统的安全。
(3)根据该中断、异常对应的向量号(这个是由硬件得出),在中断描述表中获得对应的门描述符,获取门描述符中所指示的处理程序所在的段选择子,根据该段选择子读取相应的段描述符。
(4)确保发生中断、异常时运行级别的权限不比中断、异常处理程序所要求的运行级别的权限高(这是一种通用的环保护机制),在CPL<处理程序所在段的段描述符的DPL的情况下,产生一个通用保护异常"General protection"。
(5)此时开始判断这次中断、异常是否导致了系统运行级别的切换。如果没有发生运行级别切换跳到第7步,否则顺序做下一步的处理。其中判断是否有运行级别切换的依据是当前运行级别CPL与对应的处理程序所在段的段描述符的DPL。把这两个值进行比较,如果CPL=DPL,即当前的运行级别对应的权限和处理程序所要求的运行级别对应的权限相同,此时无运行级别的切换;如果CPL>DPL, 即当前的运行级别对应的权限比处理程序所要求的运行级别对应的权限低,此时有运行级别的切换。
(6)针对有运行级别切换的情况做特殊处理,通过读取任务寄存器TR获得被中断的当前进程的任务状态段TSS,根据任务状态中的栈基址、栈指针的值保存在系统栈中,待系统中断、异常处理完毕恢复被中断现场时使用。
(7)如果发生的是异常且异常时FAULT类型时,控制重新设置指令寄存器%eip的值为上一条指令的地址,即导致产生该异常的指令地址。
(8)将控制单元将系统状态寄存器EFLAGS、代码段选择子寄存器%cs,指令指针寄存器%eip压入系统当前的栈,如果发生的异常有标识错误类型的错误号,则将错误号也压入当前的栈中。
(9)读取向量号对应的门描述符的类型。如果是中断门描述符,则控制单元设置系统标志状态寄存器EFLAGS中的中断标志位IF为0来禁用中断。
(10)设置代码段选择子寄存器cs和指令指针寄存器eip的值为对应中断、异常门描述符中的段选择子和段内偏移值字段。这两个字段给出了中断、异常处理程序的第一条指令的逻辑地址。此时系统开始运行中断、异常处理程序。
这时,内核栈的布局如下:
这是没有发生运行级别切换时的情况(在这种情况下,肯定是系统在中断前就处于内核态,所以是占用的内核态的堆栈,中断处理也在同一个堆栈中进行,所以不发生堆栈的切换,所有动作都在同一个堆栈中发生)
下面是发生运行级别时的堆栈情况(在这种情况下,只可能是在发生中断以后,系统处于用户态,用的是用户态的堆栈,发生了中断以后,系统进行了运行级别的切换,同时堆栈也切换到了内核态的堆栈中,下图就是切换到内核态的堆栈以后的情况)
由此硬件工作就完成了,剩下来的就是我们自己编写的软件了,即中断处理例程。
一、软件动作
1. 驱动程序注册中断处理函数的接口:
举个网卡驱动的例子,看8139too.c中的rtl8139_open()函数,有这么一句话:
request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
dev->irq:需要注册到的irq中断线;
rtl8139_interrupt:中断处理函数上半部,用于从网卡硬件中读取packet到系统本地内存,并且响应网卡寄存器,保证网卡应将工作正常;
SA_SHIRQ:表示这条irq可以被其他驱动程序共享;
dev->name:给设备命名;
dev: 设备的结构体,用于存放设备的一些私有信息.
OK,我们开始分析request_irq() ,请看代码中的注释:
/**
* request_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*
* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* SA_SHIRQ Interrupt is shared
* SA_INTERRUPT Disable local interrupts while processing
* SA_SAMPLE_RANDOM The interrupt can be used for entropy
*
*/
/*这个接口用于驱动程序注册中断*/
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).
*/
/*如果dev_id为NULL,那么该irq就不能被多个driver共享。原因很简单,多个driver共享一条irq的话,
在中断处理函数中,只有通过dev_id来区分是这条irq上的哪个驱动*/
if ((irqflags & SA_SHIRQ) && !dev_id)
return -EINVAL;
if (irq >= NR_IRQS) /*irq不能超过系统处理的最大值*/
return -EINVAL;
if (!handler) /*中断处理函数不能为空,否则还需要中断干嘛 呢?*/
return -EINVAL;
action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC); /*从通用slab中分配一个struct irqaction,这个结构体是用来表示一个具体的中断处理的*/
if (!action)
return -ENOMEM;
/*对action赋值*/
action->handler = handler;
action->flags = irqflags;
cpus_clear(action->mask);
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
retval = setup_irq(irq, action); /*把action注册到具体的irq中,其实就是irq_desc[]数组中,irq是该数组的下标,见下面分析*/
if (retval)
kfree(action);
return retval;
}
/*
* Internal function to register an irqaction - typically used to
* allocate special interrupts that are part of the architecture.
*/
int setup_irq(unsigned int irq, struct irqaction * new)
{
struct irq_desc *desc = irq_desc + irq; /*取得具体的irq_desc, 即描述irq的结构体指针*/
struct irqaction *old, **p;
unsigned long flags;
int shared = 0;
/*有效性判断*/
if (irq >= NR_IRQS)
return -EINVAL;
if (desc->handler == &no_irq_type) /*irq必须具有对中断控制器的响应处理*/
return -ENOSYS;
/*
* Some drivers like serial.c use request_irq() heavily,
* so we have to be careful not to interfere with a
* running system.
*/
/*如果需要注册的action的flags字段中包含了SA_SAMPLE_RANDOM,那么表示可以为系统伪随机数做贡献*/
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
*/ /*关闭本地中断,禁止内核抢占,防止SMP中其他cpu对同一个irq结构进行操作*/
spin_lock_irqsave(&desc->lock,flags);
p = &desc->action;
if ((old = *p) != NULL) {
/* Can't share interrupts unless both agree to */
/*该irq上的其他action的flags的SA_SHIRQ状态必须与要注册的action的flags的SA_SHIRQ状态一致,
即要么都支持SA_SHIRQ,要么都不支持SA_SHIRQ. */
if (!(old->flags & new->flags & SA_SHIRQ)) {
spin_unlock_irqrestore(&desc->lock,flags);
return -EBUSY;
}
/* add new interrupt at end of irq queue *//*把需要注册的action加入到该irq的action链表尾部*/
do {
p = &old->next;
old = *p;
} while (old);
shared = 1;
}
*p = new;
if (!shared) { /*如果该irq是第一次被注册上驱动程序*/
desc->depth = 0;
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT |
IRQ_WAITING | IRQ_INPROGRESS);
if (desc->handler->startup) /*对中断控制器进行初始化*/
desc->handler->startup(irq);
else
desc->handler->enable(irq);
}
spin_unlock_irqrestore(&desc->lock,flags); /*打开本地中断,enable内核抢占,解开自旋锁*/
/*在proc中显示出该irq的信息*/
new->irq = irq;
register_irq_proc(irq);
new->dir = NULL;
register_handler_proc(irq, new);
return 0;
}
这样一来,设备驱动程序就把自己的中断处理函数注册到了对应的irq中。一旦设备在该irq上有中断产生,cpu就会通过中断门以及中断号运行到设备注册的中断处理函数,在这个例子中,中断处理函数是rtl8139_interrupt( ).
2. Linux中断系统初始化:
Start_kernel() à init_IRQ( )
void __init init_IRQ(void)
{
int i;
/* all the set up before the call gates are initialised */
pre_intr_init_hook(); /*初始化了irq_desc[NR_IRQS]这个数组*/
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
/*之所以要减去32,是以为IA32中,中断系统的前32个表项是用于NMI和异常,后面的才是用于可编程可屏蔽的IRQ*/
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
/*设置中断描述符表中的项为对应的中断处理例程,除了0X80外(0x80用于编程异常)
这个interrupt[]数组在entry.S汇编文件中生成*/
if (vector != SYSCALL_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);
/*当系统配置成4K内核堆栈时才会用到,这里不分析*/
irq_ctx_init(smp_processor_id());
}
Init_ IRQ() 调用到了pre_intr_init_hook () ,这个函数初始化了irq_desc[NR_IRQS]这个数组,分析如下:
Pre_intr_init_hook() àinit_ISA_irqs( )
void __init init_ISA_irqs (void)
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC /*APIC配置,不分析*/
init_bsp_APIC();
#endif
init_
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 = &i
} else {
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc[i].handler = &no_irq_type;
}
}
}
我们看到在init_IRQ()中,有一个for循环在设置中断描述符对应的代码段的内容为interrupt[i],
set_intr_gate(vector, interrupt[i]);
那我们看下这个interrupt[]数组是在哪里定义的吧:
文件arch/i386/kernel/entry.S
.data
ENTRY(interrupt)
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS ##在.rept和.endr这段中的代码会重复运行NR_IRQS次。在
ALIGN
1: pushl $vector-256 ##中断向量号压入堆栈
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr
在这里初始化了interrupt数组的内容,每个中断处理函数会运行:
pushl $vector-256 ##中断向量号压入堆栈
jmp common_interrupt
即先把产生的中断号压入堆栈,然后运行commoni_interrupt
3. Linux系统中断上半部处理
前面我们讲到,每个中断处理的上半部都会运行
pushl $vector-256 ##中断向量号压入堆栈
jmp common_interrupt
那我们现在看common_interrupt是怎么实现的:
common_interrupt:
SAVE_ALL #保存现场(SS, ESP,EFLAGS, CS,EIP以及异常时的error code是由硬件压入的,这里的SAVE_ALL压入其他的寄存器)
movl %esp,%eax /*把当前堆栈指针传递给eax,作为函数do_IRQ()的参数。*/
call do_IRQ
jmp ret_from_intr #注意,ret_from_intr里面处理了内核抢占。。。
正如注释中所说,首先保存现场,即中断产生前cpu中各个寄存器的值,把这些寄存器的值压入内核态栈中,然后把内核态栈的指针作为参数,传递给函数do_IRQ().注意do_IRQ()的原型是
fastcall unsigned int do_IRQ(struct pt_regs *regs)
前面的fastcall表示这个函数不从堆栈中提取参数struct pt_regs *regs, 而从寄存器eax中提取这个指针参数,而这个指针参数正好是中断产生前的cpu各个寄存器现场值。
那我们现在来分析do_IRQ(),下面的代码中省去了CONFIG_DEBUG_STACKOVERFLOW和CONFIG_4KSTACKS。
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
/* high bits used in ret_from_ code */
int irq = regs->orig_eax & 0xff; /*获取中断向量号*/
irq_enter();
__do_IRQ(irq, regs);
irq_exit(); /*进行下半部的处理*/
return 1;
}
重点分析__do_IRQ(irq, regs);
/*
* 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]++; /*用于统计,kstat_this_cpu是一个per-cpu变量*/
if (CHECK_IRQ_PER_CPU(desc->status)) {
irqreturn_t action_ret;
/*
* No locking required for CPU-local interrupts:
*/
if (desc->handler->ack)
desc->handler->ack(irq);
action_ret = handle_IRQ_event(irq, regs, desc->action);
desc->handler->end(irq);
return 1;
}
spin_lock(&desc->lock);
if (desc->handler->ack) /*给中断控制器回响应,具体的函数见i
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
*/
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING); /*清除该中断线irq的状态*/
status |= IRQ_PENDING; /* we _want_ to handle it */ /*设置IRQ_PENDING,表示有中断需要处理(上半部)*/
/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc->action; /*action是中断处理的具体函数,在driver中编写出来的*/
status &= ~IRQ_PENDING; /* we commit to handling *//*清除IRQ_PENGDING,表示现在已经在处理了*/
/*表示现在正在处理中断,这个标记有效的阻止了同一个中断处理函数的重入问题,
即如果cpu正在处理中断A,在中断A的上半部处理完成之前,是不可能被同样的中断A
打断的,这样就保证了中断处理函数不用写成可重入的函数,为程序员减轻了负担*/
status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status; /*重置该irq的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.
*/
/*action为NULL有两种可能:
1.该irq上根本就没有注册中断,即表示没有设备连接到该irq上,所以不予处理
2. 如果cpu正在处理该irq上的函数 ,同时打开了中断,这时如果该irq上再来一个同样的中断,cpu不会重入它
相反,会在前面通过status != IRQ_PENGDING来表示该irq又有中断产生了(因为可能多个设备共享同一个irq),所以在
执行了action = NULL以后,紧接着的这条if判断没有满足,就直接从if(!action)中goto out了。那这个后来产生的中断在
啥时候运行呢?可以看到handle_IRQ_event()里把这种情况考虑进去了*/
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); /*加锁,防止SMP中其他处理器同时运行这个中断上半部*/
/*运行该irq上注册的中断处理程序,因为可能一个irq上注册了多个驱动,所以handle_IRQ_event()里会遍历action链表*/
action_ret = handle_IRQ_event(irq, regs, action); /*在handle_IRQ_event()里面,中断有可能是打开的,所以这个时候CPU有可能会再在这个IRQ中接受到同样的中断*/
spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret, regs);
if (likely(!(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);
return 1;
}
再来看下handle_IRQ_event()
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;
if (!(action->flags & SA_INTERRUPT)) /*如果该irq允许共享中断,则要通过sti指令设置EFLAGS的IF位,使能本地中断*/
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); /*为系统随机数做贡献*/
local_irq_disable();/*再次关闭本地中断*/
return retval;
}
注意,action->handler(irq, action->dev_id, regs)这里就在运行我们在驱动中通过request_irq()注册上的中断处理函数上半部。
最后返回到do_IRQ()中,在return之前运行irq_exit()的__softirq_pending字段,则表示需要激活下半部,那么在irq_exit()中就要运行下半部的处理(当然,这个下半部的处理函数也是需要驱动程序来注册的,我会在后面的文章中详细分析)
/*
* Exit an interrupt context. Process softirqs if needed and possible:
*/
void irq_exit(void)
{
account_system_vtime(current);
sub_preempt_count(IRQ_EXIT_OFFSET);
/*如果在上半部中设置了per-cpu变量irq_stat的__softirq_pending字段,则运行下半部的处理函数*/
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
preempt_enable_no_resched();
}
在下半部的处理完成之后(我们不考虑下半部线程化的问题),会回到common_interrupt的处理中,
common_interrupt:
SAVE_ALL #保存现场(SS, ESP,EFLAGS, CS,EIP以及异常时的error code是由硬件压入的,这里的SAVE_ALL压入其他的寄存器)
movl %esp,%eax /*把当前堆栈指针传递给eax,作为函数do_IRQ()的参数。*/
call do_IRQ
jmp ret_from_intr #注意,ret_from_intr里面处理了内核抢占。。。
最后运行ret_from_intr
在这里就不详细分析ret_from_intr的代码了,它的功能是判断是否需要运行内核抢占(内核抢占会在中断返回到kernel态时运行),并且判断是否运行信号处理函数等。
下面的图中给一个框架性的总结:
四、总结
处理中断上半部,需要了解中断门的一些特性(比如cpu通过中断门的时候会自动关中断),并且要了解linux中是怎样来运行中断上半部的。在运行中断上半部的时候,会有很多边界情况要考虑,比如有的中断上半部运行时需要打开中断,以便产生中断嵌套,而又的中断处理函数时就需要关闭中断,防止嵌套。这完全是由驱动程序编写人员自己决定。另外,对于同一条irq而言,linux处理机制保证了不会嵌套执行同一个irq的处理函数,这样就大大简化了编程人员的负担,让他们不需要编程可重入函数了。