操作系统需要管理连接到计算机上的硬件设备,如果高效的管理这些硬件设备,好办法是提供一套机制:让硬件在需要的时候向内核发出信号,这就是中断机制。
中断使得硬件得以发出信号给处理器,中断本质上是一种特殊的电信号,由硬件设备发向处理器,处理器收到中断信号后,马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。硬件设备产生中断并不一定与处理器时钟同步,中断是随时产生的,所以内核可能被中断打断。
中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器相连的管线与处理器通讯,当收到中断后,中断控制器会给处理器发送一个电信号,处理器检测到次电信号后便中断自己的工作,转而处理中断,此后处理器会通知操作系统已经产生中断,这样操作系统就会处理这个中断了。不通的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这样操作系统才能给不同的中断提供其对应的中断处理程序。这些中断值被称为IRQ线,每个IRQ线都会被关联一个数量值,重点在于特定的中断总是与特定的设备相关联的,并且内核要知道这些信息。异常与中断不同,异常在产生时必须和处理器同步。
在相应一个中断的时候,内核会执行一个函数,这个函数就叫做中断处理程序,产生中断的设备都有一个相应的中断处理程序。一个设备的中断处理程序是它的驱动程序的一部分,设备驱动程序是对设备进行管理的内核代码。 中断处理程序与其他内核函数的区别在于:中断处理程序是被内核调用来响应中断的,而他们运行于我们称之为中断上下文的特殊上下文中。在中断上下文中执行的代码不可阻塞。中断是随时产生的,因此中断处理程序要随时可以执行,所以必须保证中断处理程序快速执行。
又想中断处理程序运行的快,又想中断处理程序完成的工作多,显然是有矛盾的。这两个目的存在此消彼长的关系,我们一般把中断处理分为两部分。中断处理程序是上半部分,收到一个中断后,就开始立即执行,但只做有严格时间限制的工作。能够被允许稍后完成的工作推迟到下半部分去完成,在合适的时间,下半部分会被执行。
中断处理程序是管理硬件的驱动程序的组成部分,每一个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序,驱动程序通过request_irq()函数注册一个中断处理程序。request_irq()函数可能会睡眠,所以不能在中断上下文或者其他不允许阻塞的代码中使用该函数。初始化硬件和注册中断处理程序的顺序很重要,以防止中断处理程序在设备初始化完成之前就开始执行。
当执行一个中断处理程序时,内核处于中断上下文中,中断上下文和进程上下文的关系?进程上下文是一直内核所处的操作模式,此时内核嗲表进程执行,进程是以进程上下文的形式连接到内核的,因此进程上下文可以睡眠,也可以调度程序。发现中断上下文与进程上下文没什么关系。中断上下文有较严格的时间限制,因为它打断了其他程序的执行,所以中断上下文的代码应该迅速执行。
中断处理系统在linux中的实现是是非常依赖体系结构的。linux内核提供了一组接口用于操作机器上的中断状态,这些接口为我们提供了能够禁止当前处理器的终端系统。一般来说,控制终端系统的原因是需要提供同步,通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码,禁止中断还可以禁止内核抢占,但是它们都没有提供任何保护机制来防止来自其他处理器的并发访问。linux支持多处理器,因此内核代码一般都需要某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时也伴随着禁止本地中断,锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。
/proc是一个虚拟文件系统,存在于内核内存中,/proc中的读写文件都需要调用内核函数,这些函数模拟真实的文件读写,/proc/interrupts文件则含有系统中与中断相关的统计信息。
内核为处理中断而提供了中断处理程序机制,但是由于本身的一些局限,它只能完成整个中断处理流程的上半部分,这些局限包括:
1 中断处理程序以异步方式执行,并且可能会打断其他代码,因此中断处理程序要快速执行完毕
2 如果一个中断处理程序正在执行,最好的情况是与该中断同级的其他中断被屏蔽,最坏的情况是,当前处理器上的所有中断都会被屏蔽。
3 由于中断处理程序需要对硬件进行操作,所以他们通常有严格的时限要求
4 中断处理程序不能被阻塞
而对于那些时间要求比较宽松的任务,则可以推迟到以后再执行,下半部的任务就是执行与中断处理密切相关,但中断处理程序本身不执行的工作。但是并不存在明确的说明哪些任务应该在上半部执行那些任务应该在下半部执行,这个决定完全取决于代码编写者。尽管不存在严格的规则,但是还是有提示借鉴的:
1 如果一个任务对时间非常敏感,感觉告诉我还是将其放在中断处理程序中执行是个好的选择。
2 如果一个任务和硬件相关,还是将其放在中断处理程序中执行吧。
3 如果一个任务要保证不被其他中断(特别是相同的中断)打断,那就将其放在中断处理程序中吧。
4 其他所有任务,除非你有更好的理由,否则全部丢到下半部执行。
我们希望减少中断处理程序的工作量,因为它在运行的时候,当前的中断线所在的处理器上都会被屏蔽,而缩短中断被屏蔽的时间对系统的相应能力和性能都至关重要。座椅很明显,我们必须缩短中断处理程序的执行时间,办法就是把一些任务放到以后去执行。下半部并不需要一个确切的时间执行任务,只要把这个任务推迟一点,让它们在系统不繁忙并且恢复中断的时候执行就可以了。下半部的执行关键在于允许响应其他所有的中断。上半部只能通过中断处理程序来实现,而下半部则可以通过多种机制来实现,在2.6内核中提供了三种不同的下半部实现机制:软中断、tasklet、工作队列。tasklet通过软中断实现,工作队列则与他们完全不同。
软中断
软中断用的不多,tasklet则更常用一些,软中断是在编译期间静态分配的,所以软中断不能被动态的注册或者取消。软中断由softirq_action结构体表示:
struct softirq_action{ void (*action)(struct softirq_action *); //待执行的函数 };最多只能有32个软中断,这是一个定制,无法改变。一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是中断处理程序,不过其他的软中断可以在其他处理器上运行。一个注册的软中断必须在被标记后才会执行,这个称作触发软中断,通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行,于是在合适的时间,就会执行该软中断。在一个硬件中断代码返回出或者ksoftirqd内核线程中或者显示的检查软中断的代码中会检查待处理的软中断。不管是什么办法唤醒软中断,软中断都要在do_softirq()中执行。如果有待处理的软中断,do_softirq会循环遍历每一个,调用他们的处理程序。
目前只有网络和scsi两个子系统直接使用软中断,内核定时器和tasklet都是建立在软中断上的。对于时间要求严格并能高效的完成加锁工作的应用,软中断效果不错。软中断处理程序执行的时候允许响应中断,但它自己不休眠,在一个处理程序运行的时候,当前的处理器的软中断被禁止了,但其他的处理器还是可以运行别的软中断的。如果一个软中断在它执行的时候被再次触发,那么其他处理器可以同时运行其处理程序,但是这会造成数据共享,需要严格的数据保护。这也是tasklet更受欢迎的原因。引入软中断主要是其可扩展性。tasklet本质上也是软中断,不过同一个处理程序的实例不能在多个处理器上同时运行,所以如果不需要扩展到多个处理器上,就用tasklet。
tasklet
tasklet和软中断本质上很相似,但是tasklet的接口更简单,锁保护要求较低。tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。它们的唯一区别在于HI_SOFTIRQ先于TASKLET_SOFTIRQ类型的软中断执行。tasklet由tasklet_struct结构体表示:
struct tasklet_struct{ struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; };结构体中的func是tasklet的处理程序。state则是tasklet的状态,state的成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE-RUN之间取值,SCHED表明tasklet已经被调度,正准备运行。RUN表示该tasklet正在运行。count是tasklet的引用计数器,如果不为0,则tasklet被禁止不许执行。只有为0时,tasklet才被激活。已经被调度的tasklet存放在两个数据结构中:tasklet_vec和tasklet_hi_vec中,这两个数据结构都是由tasklet_struct结构体构成的链表。其中的每一个结构体数据代表一个tasklet。Tasklets由tasklet_schedule()和tasklet_hi_schedule()进行调度,它们接收一个指向tasklet_struct结构的指针作为参数。
工作队列
工作队列和前面讨论的其他形式都不相同,它可以把工作推后,交由一个内核线程去执行----该工作总是会在进程上下文执行。这样,通过工作队列执行代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。相比较前边两个,这个选择起来就很容易了。我说过,前边两个是不允许休眠的,这个是允许休眠的,这就很明白了是不?这意味着在你需要获得大量内存的时候,在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用。
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程(worker threads).工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成一个把需要推后执行的任务交给特定的通用线程这样一种接口。缺省的工作线程叫做event/n.每个处理器对应一个线程,这里的n代表了处理器编号。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好还是使用缺省线程。
tasklet是基于软中断实现的,两者相近,工作队列机制与它们完全不同,靠内核线程来实现。软中断提供的序列化的保障最少,这就要求中断处理函数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。如果被考察的代码本身多线索化的工作做得非常好,它完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行效率很高的应用来说,它执行的也最快。否则选择tasklets意义更大。tasklet接口简单,而且两种同种类型的tasklet不能同时执行,所以实现起来也会简单一些。如果需要把任务推迟到进程上下文中完成,那你只能选择工作队列了。如果不需要休眠,那软中断和tasklet可能更合适。另外就是工作队列造成的开销最大,当然这是相对的,针对大部分情况,工作队列都能提供足够的支持。从方便度上考虑就是:工作队列,tasklets,最后才是软中断。我们在做驱动的时候,关于这三个下半部实现,需要考虑两点:首先,是不是需要一个可调度的实体来执行需要推后完成的工作(即休眠的需要),如果有,工作队列就是唯一的选择,否则最好用tasklet。性能如果是最重要的,那还是软中断吧。