分类: LINUX
Linux中的中断机制
X86里面中断发生时CPU控制单元工作流程(也就是硬件需要做的事情),在CPU执行下一条instruction之前,首先判断有没有发生异常和中断。如果发生了,那么进行一下步骤:
1. 查看所发生的中断(异常)向量i。(0-255)
2. 根据idtr寄存器读取IDT中第i个入口地址。
3. 根据gdtr寄存器读取GDT中关于IDT的段描述符,这个段描述符指定了中断(异常)程序的基地址。
(IDT表项是64位的,这64位里面包含了一些属性信息和一个48位的指针,48位的指针中16位是一个段选择子,另外32位是段内偏移,系统会用这16位的段选择子在GDT中找到对应的段,然后把段首地址跟IDT中的32位偏移相加就得到了中断处理函数的入口地址。ULK3中所说的GDT的DPL就是用那16位段选择子在GDT中找到的段描述符的DPL
也就是说IDT表项里面的这个段选择子标识着这个中断服务程序的运行级别,用这个级别来对比当前cs中的运行级别。)
4. 证明中断是可信源发出的,检查CPL与GDT里面对应的DPL,如果CPL<DPL,就发出一个通用保护异常,这可以阻止用户进程访问指定的中断。
5. 如果优先级发生改变了,也即是说CPL和DPL不一致的情况下,那么控制单元就会使用新的优先级的堆栈:(1)通过tr寄存器,读取TSS段描述符(2)通过TSS,读取对应的ss和esp。(3)在新的堆栈里面存储老优先级对应的堆栈ss和esp。
6. 如果fault发生,那么会去除cs和eip,再执行一次。
7. 将eflags,cs,eip入栈。
8. 如果异常产生了一个硬件错误码,那么将其入栈。
9. 在IDT对应的门中,取出cs和eip,并且执行。
当中断处理完成,硬件控制器的工作如下:
1. 将cs,eip,eflags出栈。(如果有硬件错误码,则必须在iret指令之前pop出去,软件实行)。
2. 检查中断程序的CPL和当前cs(注意这里已经是以前老得代码段cs)是否一致,如果一致,直接执行iret。否则跳到下一步。
3. 将ss和esp出栈,从而将堆栈置为以前的优先级。
4. 检查ds,es,fs和gs,如果优先级高,则清零这些寄存器,为了防止用户进程进入内核空间。
如何设置IDT里面的中断门:
1. 函数set_intr_gate:
/*将中断处理函数地址加入到IDT的第n个入口处*/
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}
2. 宏_set_gate:
/*第一个参数是IDT里面第n个入口的地址
第二个参数是中断门里面的type字段的设置
第三个参数是dpl值
第四个参数是函数地址
第五个参数是指定段,都为__KERNEL_CS */
#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)
当进入中断处理程序以后了,linux会怎么做呢,请看下面的代码:
/*
* Build the entry stubs and pointer table with
* some assembler magic.
*/
/*中断入口处,interrupt[n]*/
.data
ENTRY(interrupt)
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1: pushl $vector-256 /*将中断号进行压栈到内核栈中*/
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr
ALIGN
common_interrupt:
SAVE_ALL /*压栈寄存器的值,注意eflags,cs,eip在前面已经压栈了*/
movl %esp,%eax /*将堆栈栈顶地址作为参数传给do_IRQ函数*/
call do_IRQ
jmp ret_from_intr
每个中断向量程序都对应着一个数据结构,里面包含着处理中断程序的各个流程和状态的转换。这个数据结构如下:
/*
* This is the "IRQ descriptor", which contains various information
* about the irq, including what kind of hardware handling it has,
* whether it is disabled etc etc.
*
* Pad this out to 32 bytes for cache and indexing reasons.
*/
typedef struct irq_desc {
hw_irq_controller *handler; /*对应PIC对象,用于处理IRQ line上的各种交互*/
void *handler_data;
struct irqaction *action; /* IRQ action list */ /*ISR链表的第一个地址*/
unsigned int status; /* IRQ status */ /*对应IRQ line的状态*/
unsigned int depth; /* nested irq disables */ /*如果没有嵌套,则为0*/
unsigned int irq_count; /* For detecting broken interrupts */
unsigned int irqs_unhandled;
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
extern irq_desc_t irq_desc [NR_IRQS];
下面我们来看看字段status的各个状态的意义,理解这些状态对理解状态转换的过程和整个中断处理流程有很大的关系。
/*
* IRQ line status.
*/
#define IRQ_INPROGRESS 1 /* IRQ handler active - do not enter! */
/*说明IRQ handler正在运行*/
#define IRQ_DISABLED 2 /* IRQ disabled - do not enter! */
/*说明IRQ是关闭的*/
#define IRQ_PENDING 4 /* IRQ pending - replay on enable */
/*说明IRQ出现在line上,并且已经发了ACK,但是内核并没有对此IRQ进行服务*/
#define IRQ_REPLAY 8 /* IRQ has been replayed but not acked yet */
/*IRQ line被禁止了,但是上一个IRQ还没有给PIC发送ACK*/
#define IRQ_AUTODETECT 16 /* IRQ is being autodetected */
/*内核正使用IRQ line来检查硬件设备*/
#define IRQ_WAITING 32 /* IRQ not yet seen - for autodetection */
#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 */
IRQ_DISABLED与字段里面的depth用来觉得IRQ是开启还是关闭的。
参看disable_irq()和enable_irq()函数。
可以通过depth的值来通知状态是否为IRQ_DISABLED,并且告知PIC是否开启或者关闭IRQ。
下面又看看action这个字段,这个是一个irqaction类型。
struct irqaction {
irqreturn_t (*handler)(int, void *, struct pt_regs *); /*指向特定I/O设备ISR的指针*/
unsigned long flags; /*描述IRQ line和I/O设备的关系,参加下面介绍*/
cpumask_t mask; /*not used*/
const char *name; /*IO设备的名字*/
void *dev_id;
struct irqaction *next; /**/
int irq; /*IRQ line*/
struct proc_dir_entry *dir;
};
下面来看看__do_IRQ函数的处理方式
/*
* 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;
spin_lock(&desc->lock); /*锁住这个IRQ*/
desc->handler->ack(irq); /*给PIC发送这个IRQ line的ACK*/
/*
* 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);
/*发送了ACK但没有处理,这个情况下,应置status为pending*/
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;
if (likely(!(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 (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;
/*在处理IRQ_evnet的时候,把锁打开,这样其他的处理器也可以处理同样类型的中断,不过这些中断最后还是要被当前处理器处理完成*/
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, regs, action);
spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
if (likely(!(desc->status & IRQ_PENDING))) /*如果中断状态不为pending的话就退出,其他处理器试图去处理中断的话就会置这个中断状态为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函数,这个函数的目的是处理IRQ上面的各个ISR
/*
* Have got an event to handle:
*/
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))
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;
}
现在我们来看看设备怎么动态的获取到IRQ lines:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags, const char * devname, void *dev_id)
这个函数,irq是中断号,handler是中断服务程序,用来将handler注册到这个IRQ line上。