- 论坛徽章:
- 0
|
最近在看Linux中断处理的过程,写了以下几点体会,忘高手给予指点,以便我去补充,使得这个知识点对于自己更加完整。
首先看一下中断发生时硬件和软件的大致处理过程,然后再来对每一步做稍微详细的介绍。
1.当中断发生时,cpu control unit会截获这一事件,并做一些基本的保留,同时disable local interrupt,表示不再响应中断。这里涉及到control unit保存的内容和来读取相应idt、gdt的内容来验证中断源的合法性和interrupt handler的指令地址,接下来执行的第一条软件指令便是interrupt handler。
2.Interrupt handler的首要动作便是save all,保存一切有可能会被中断处理程序用到的寄存器值,然后执行函数do_irq,do_irq调用irq_enter增加preemt_count的Hardirq counter数值。做一些与8k内核堆栈相关的动作,然后调用__do_irq。
3.__do_irq的首要任务便是acknowledge apic并且mask掉产生中断的线,使得该线在中断处理完成之前不再接收再次中断。此时local interrupt也是disable的(由1中的control unit关闭),接下来调用isr。
4.在执行isr之前,首先根据该isr的要求,开启或关闭local interrupt,一般都是开启local interrupt,然后依次调用isr。除非注册isr(request_irq)的时候特别用flag指明必须local interrupt disable,否则一般的isr都是运行在产生中断的那根线disable掉和local interrupt enable的情况下,执行完成之后并关闭local interrupt。
5.退到上层的__do_irq,该函数会调用pic的end函数,使得对应那根线(irq line)的中断开启。
6.退到上层的do_irq,此时会调用irq_exit,在irq_exit中首先减少preemt_count中的Hardirq counter,然后调用了softirq.可知softirq是运行在全部的中断线和local interrupt enable的情况下,意味着此时内核已经可以再次响应同样的中断。
7.完成之后。。。。。。。。。。。。。调用iret指令。
8.iret指令使得control unit恢复先前保存的所有东西,enable local interrupt。
上述第一点中,中断源的合法性比较重要,那么硬件上有哪些机制保证了该中断源的合法性呢?要了解此我们先来看一下当中断发生时,control unit做了哪些动作。首先它会根据中断源和idtr一起来找到相应的idt中的某个entry,并从该entry当中取出segment seletor,由此seletor再加上gdtr寄存器,就可以找到该interrupt handler的segment descriptor,由于该segment descriptor当中有个DPL,它表示了中断处理程序应该在哪个级别下运行,一般都是在0的level即kernel态下运行interrupt handler。因此如果发现当前进程的cs中的低两位数值比中断处理程序的DPL还小(数值越小,level也高,kernel的数值为0),那么就直接出现异常,因为不可能会有某个进程它的运行级别会被interrupt handler还低。经过这部确认之后,cs和eip分别被赋值成IDT当中的segment selector和offset,做完这一步意味着下一个执行的指令便是interrupt handler中的指令了。
第二步当中的interrupt handler的整条命令如下:
pushl $n-256
jmp common_interrupt
common_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
其实存在于idt的地址指向的指令便是上述的前两条语句,接下来就是统一地保存所有cpu的寄存器,以免丢失。然后就调用函数do_IRQ来执行。 do_IRQ如上述所说,它基本就是增加硬中断的数值,然后直接调用__do_irq,__do_irq基本会通知中断控制器已经收到中断信号,告诉它将中断线的信号取消,一般此时会中断控制器会mask掉该跳线的中断。接下来__do_irq会调用ISR也即驱动程序利用函数 request_irq函数注册进去的中断处理历程。一般来说,在执行中断处理历程的时候,kernel会根据driver传进来的flag决定是否开启 local的中断,一般来说都是开启的,但是这里需要我们注意的是对于8259A这个中断控制芯片来说,此时尽管cpu local的中断是enabled,但是8259A上产生中断的哪条线还是disable的,意味着这条线上的中断不会被硬件觉察到。对于intel IO apic还没有研究过。由此可以看出,一般driver的中断处理历程都是运行local interrupt enable的情况下进行的,因此会产生中断嵌套之说。当__do_irq执行完ISR之后,并enable8259A上的哪条中断线,意味着将接收该线上产生的中断。到此,__do_irq算基本完成,退到上层函数do_irq,do_irq会执行irq_exit,在该函数中会减掉由 irq_enter增加的硬中断数值,接下来就判断是否是中断context,即如果函数in_interrupt返回false,且有pending 的软中断,那么就去执行软中断,即中断的底半步。关于linux中断的上半步和底半步就不详细说明了,这里只说明整个流程是怎么走的。
kernel在系统初始化的时候注册了6个不同的softirq,分别为HI_SOFTIRQ,TIMER_SOFTIRQ(内核定时器的实现就是依赖这个softirq),NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,TASKLET_SOFTIRQ(tasklet的实现就是依赖于这个softirq),SCHED_SOFTIRQ,RCU_SOFTIRQ。。。。。当中断处理程序执行到irq_exit函数,在该函数中调用invoke_softirq即__do_softirq,正是在这个函数里,kernel对每个有pending的softirq执行起其相应的函数。那么哪个softirq pending是谁来设置的呢?可以参考函数raise_softirq_irqoff,这个函数传进来的参数就是上述6个softirq的值。softirq会在每次中断返回时会被检查是否要调用,到真正执行softirq的各个函数时,in_interrupt()值一定不为真。(由于softirq不能嵌套执行,因此它在执行之前会local_bh_disable来提升preemt_count的softirq counter值,使得下次softirq在in_interrupt()的时候直接返回。那么在什么情况下会发生softirq的中断嵌套呢?因为softirq的时候所有中断都是打开的,因此有可能新的interrupt handler里面又注册了这个softirq,等到嵌套的中断退出时(此时preemt_count中的hardirq counter 为0,请参考do_irq分析),又会调到softirq,此时就实现了softirq的中断嵌套。
这里将tasklet(即软中断中的一种)做个简单的解析,使用过tasklet来帮助我们driver中的实现中断底半步的兄弟都知道,一般我们都是通过如下两个步骤来实现:
tasklet_init
tasklet_schedule。
我们来看看上述两个函数的代码实现:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
tasklet_init很简单,它只是将一个tasklet_struct赋值成想要的函数和参数,为该tasklet被运行做了一些初始化工作。
再来看
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int 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.
*/
if (!in_interrupt())
wakeup_softirqd();
}
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
由红色的代码我们可以看到函数tasklet_schedule只是将每个cpu对应的关于软中断的变量的那个关于tasklet的bit置成1,就结束了。
到此为止,我们再回想当中断处理程序完成之后,它会根据当前是否有pending的softirq,此时有,因此函数tasklet_action就会被调用,原因是内核初始化的时候有这么一条语句:open_softirq(TASKLET_SOFTIRQ, tasklet_action);。再看函数tasklet_action:
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __get_cpu_var(tasklet_vec).head;
__get_cpu_var(tasklet_vec).head = NULL;
__get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
红色的代码就是我们通过函数tasklet_init注册进去的函数。
以此类推,我们可以找到kernel timer的实现原理和其他一些软中断的执行过程。 |
|