linux 中断机制浅析
广义上的中断可以分为外部中断和内部中断(异常)
-
中断是由外部事件引起的,一般分为可屏蔽的中断与非可屏蔽的中断,所谓可屏蔽就是可以通过设置CPU的IF标志位进行屏蔽,而非可屏蔽的是一些非常紧急的事件,往往IF对其不起作用。
-
异常是由于内部事件造成的,比如说缺页异常,系统调用等
异常的产生
1,监视IRQ线,对引发信号检查(编号小者优先)
2,如果一个引发信号出现在IRQ线上
a,把此信号转换成对应的中断向量
b,把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读这个向量
c,把引发信号发送到处理器的INTR引脚,即产生一个中断
d,等待,直到CPU应答这个信号;收到应答后,清INTR引脚
3,返回到第1步
核心
其实中断最核心的东西在于中断描述符表(中断向量表)IDT,他里面记录了中断号与中断处理程序之间的对应关系(确切的说并不是与真正的中断处理程序对应,只是把中断向量号取反压入堆栈,然后跳转到common_interrupt,交给这个函数继续执行)。
)。表中的每一项称之为中断描述符为64位,linux中IDT表项共计256项,0~31内部中断,128系统调用中断,其他的可以自由使用。CPU的idtr寄存器指向IDT表的物理基地址,lidt指令
IDT初始化
在系统进入保护模式之前,中断向量表idt_table中的中断处理函数都是ignore_int,在start_kernel函数中需要重新设置itd_table,这个工作由tarp_init和init_IRQ完成。其中trap_init主要用来完成固定的映射(异常),而Init_IRQ除了填充中断向量表(外部中断)之外还要进行中断控制器的初始化。
Init_IRQ
LINUX经常采用面向对象的思想,抽象出来,对于硬件存在很多种中断控制器,Linux将其抽象为irq_chip。同样由于同一个外部中断号可能被多个外部设备共享,而每个不同的外部设备都可以动态地注册和撤销自己的中断处理程序,所以定义了Irq_desc结构,每一个外部中断(因为在256个中断中,32被保留内部使用,所以共有224个需要irq_desc)需要这样一个结构。
当发生n号中断时,中断处理函数会利用n索引到irq_desc数组的第n个irq_desc成员,然后调用irq_desc结构中的handle_irq函数,而handle_irq函数又会调用action中的每一个handler函数。
Init_IRQ(本质上是native_init_IRQ),初始化中断控制芯片,接着循环填充中断向量表IDT,讲interrupt[i](中断处理函数)填入到中断描述符中,其中interrupt[i]的含义是把中断向量号取反压入堆栈,然后跳转到common_interrupt处继续执行。
驱动程序编写
对于外部中断来说,设备驱动程序可以调用request_irq把一个中断处理程序handler挂起到中断请求队列中,其作用就是构建一个irqaction,并设置其handler指向驱动程序提供的handler,挂接到对应的irq_desc上去,在挂接的过程中,需要检查以前的action是否支持中断共享,如果不支持就不能挂载,否则可以。
-
普通进程可以被中断或异常处理程序打断
-
异常处理程序可以被中断程序打断
-
中断程序只可能被其他的中断程序打断
中断与异常处理
-
无特权转换。如果CPU的当前执行环境是在内核态,而被中断,我们称之为无特权转换;
-
特权转换。 如果CPU的当前执行环境是在用户态,而被中断,我们称之为特权转换;这个需要先从用户态的堆栈转换到内核态堆栈中,然后再处理。
中断处理
当执行到common_interrupt的时候肯定已经是在内核态了,
common_interrupt:
addl $-0×80,(%esp) /* Adjust vector into the [-256,-1] range */
SAVE_ALL //保存现场到数据结构pt_regs
movl %esp,%eax //通过eax传递参数pt_regs给函数do_IRQ用
call do_IRQ //关键的中断处理函数,这个并不是我们指明的ISR
jmp ret_from_intr
do_IRQ
进行中断处理,现在我们需要区分两个概念,禁止中断local_irq_disable,禁止抢占内核preempt_disable。
-
禁止中断,是指设置当前CPU的IF标志位,从而禁止接受中断请求,达到屏蔽中断的目的,在中断处理过程中考虑到多重中断,所以不会禁止中断的。
-
禁止抢占内核,是指当前进程的运行,不允许被其他进程抢占,因为在中断处理完毕后可能会调用其他进程执行,为了防止这种情况出现,强制在中断处理完毕后必须回到初始中断附属的进程继续执行,也就是说在禁止抢占内核的情况下是允许发生中断的。
中断处理的思想:根据传递过来的中断号,找到irq_desc,调用其中的handle_irq函数,继而调用action中的handler函数(这个才是真正的ISR),但是同一个中断处理程序之能在某时刻在一个CPU上运行,同时需要注意Linux对于同一个Irq号的中断处理。
do_IRQ ( )
{
…..
Irq_enter( );//上面的都是关闭中断的,由硬件自动关闭中断
Handle_irq()//执行我们定义的中断处理程序ISR,开中断执行,为了中断嵌套
Irq_exit ( );
…..
}
异常处理
因为异常不与外部中断设备打交道,所以其过程比较简单,就是在init_trap中填入到IDT中的中断处理程序,最后调用do_trap进行异常处理,关键也是注意是在用户态还是内核态的问题。异常处理程序在最后向当前进程发送一个信号,因为异常肯定是由当前进程引起的,所以同步于当前进程,由当前进程试图恢复故障(缺页)或者终止。这与中断不一样,中断不于当前进程有什么直接的关系。
驱动下半部分
我们知道驱动程序分为上半部分与下半部分,通常上半部分是紧急的部分,通常放在ISR中直接处理,对于非紧急的部分有两种方法:1. 软中断与tasklet 2. 工作队列
软中断
每一个softirq都用一个softirq_action来表示(里面包含软中断处理函数action),内核使用open_softirq()来注册一个软中断,共计32个软中断,用sotfirq_vec[32]来表示。
请求软中断
在我们驱动程序的ISR中,可以通过函数raise_softirq()来请求软件中断(因为其主要工作就是处理驱动程序的下半部分),其原理是将__softirq_pending中第i标志位置1
软中断处理
在do_IRQ()àirq_exit()àdo_softirq()来进行软中断的处理,其处理过程如下:
(1) 根据__softirq_pending,来确定有什么类型的软中断需要处理
(2) 当有软中断I时,调用softirq_vec[i]->action进行软中断处理,期间如果又有新的软中断到达,继续重复处理直到max_restart次,在进行软中断处理时是关中断的
(3) 剩下的软中断交给ksoftirqd来处理,这个进程会和正常进程一样去抢占CPU执行
注意:为了保证内核的强壮性,软中断只是由系统使用,是在启动时静态建立的,不会交给用户来处理,如果说我们的驱动程序要使用这种机制,就需要使用建立在软中断基础之上的tasklet机制。
Tasklet
在软中断存在两个系统的软中断HI_SOFTIRQ和TASKLET_SOFTIRQ,这就是用来专门为tasklet机制提供的,tasklet就是建立在HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上的。每一个CPU都有独立的tasklet队列,tasklet_hi_vec和tasklet_vec分别是高优先级和普通优先级tasklet的队列头。
内核通过tasklet向驱动程序模块提供处理下半部分的接口,在驱动程序中根据自己的tasklet的优先级,选择调用tasklet_schedule或者tasklet_hi_schedule,这两个函数的作用基本上一样。
Tasklet_schedule( )
{
Raise_soft_irqoff(TASKLET_SOFTIRQ);//高优先级的就请求HI_SOFTIRQ
}
这样在处理软中断的时候,就会最终调用到tasklet_action(自己定义的ISR下半部分)来处理,注意同一种类型的tasklet只能串行执行。
软中断与tasklet的区别和联系
联系:Tasklet是在软中断基础之上实现的
区别:软中断的分配是静态的(定义了6个中断号对应软中断,比如网卡数据包、时钟等),而tasklet的分配和初始化可以在运行时进行,软中断(即便是同一种类型的)可以同时运行在多个CPU中,但是tasklet同一类型的总是被串行执行,不同的tasklet类型可以在不同CPU上同时执行,所以对于软中断来说可以是可重入的,而tasklet不必是可重入的。
那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错
-
对于软中断来说,同一种类型的(处理函数是同一个函数)可以同时被多个CPU执行,所以该函数必须是可重入的不能包含任何全局数据等,
-
对于tasklet来说,同一种类型的不可以被同时执行,只能串行执行,所以对于函数P来说没有什么要求
工作队列
工作队列(work queue)是linux2.6后另外一种将工作推后执行的形式 ,它和我们前面讨论的所有其他形式都有不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
(1) 工作、工作队列和工作者线程
如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。
(2) 表示工作的数据结构
工作用中定义的work_struct结构表示:
struct work_struct{
unsigned long pending; /* 这个工作正在等待处理吗?*/
struct list_head entry; /* 连接所有工作的链表 */
void (*func) (void *); /* 要执行的函数 */
void *data; /* 传递给函数的参数 */
void *wq_data; /* 内部使用 */
struct timer_list timer; /* 延迟的工作队列所用到的定时器 */
};
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
总结:
从实现方式上大体上分为两大类:
-
在IDT中找到中断向量号,进而执行相应的中断处理程序
(1) 中断:需要在特定时间内完成的,在do_IRQ中调用ISR来处理
(2) 异常:类似于中断,但是通过do_trap来处理,系统自定义的
(3) 软中断:系统留用的,用来处理非紧急的程序,通常处理驱动程序的下半部分
-
不占用IDT中irq
(1) Tasklet:这是在软中断基础之上的,不会再IDT中存在
(2) 工作队列,用内核线程的方式进行处理