关闭

linux内核分析笔记---中断实现

标签: linux内核中断
531人阅读 评论(0) 收藏 举报
分类:

转载地址:

http://www.cnblogs.com/hanyan225/archive/2011/07/17/2108609.html

http://www.cnblogs.com/hanyan225/archive/2011/07/19/2110434.html

http://www.cnblogs.com/hanyan225/archive/2011/07/20/2111522.html

那么什么叫中断呢, 中断还是打断,这样一说你就不明白了。唉,中断还真是有点像打断。我们知道linux管理所有的硬件设备,要做的第一件事先是通信。然后,我们天天在说一句话:处理器的速度跟外围硬件设备的速度往往不在一个数量级上,甚至几个数量级的差别,这时咋办,你总不能让处理器在那里傻等着你硬件做好了告诉我一声吧。这很容易就和日常生活联系起来了,这样效率太低,不如我处理器做别的事情,你硬件设备准备好了,告诉我一声就得了。这个告诉,咱们说的轻松,做起来还是挺费劲啊!怎么着,简单一点,轮训(polling)可能就是一种解决方法,缺点是操作系统要做太多的无用功,在那里傻傻的做着不重要而要重复的工作,这里有更好的办法---中断,这个中断不要紧,关键在于从硬件设备的角度上看,已经实现了从被动为主动的历史性突破。

       中断的例子我就不说了,这个很显然啊。分析中断,本质上是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反应此信号的带来,然后就由OS负责处理这些新到来的数据,中断可以随时发生,才不用操心与处理器的时间同步问题。不同的设备对应的中断不同,他们之间的不同从操作系统级来看,差别就在于一个数字标识-----中断号。专业一点就叫中断请求(IRQ)线,通常IRQ都是一些数值量。有些体系结构上,中断好是固定的,有的是动态分配的,这不是问题所在,问题在于特定的中断总是与特定的设备相关联,并且内核要知道这些信息,这才是最关键的,不是么?哈哈.

       用书上一句话说:讨论中断就不得不提及异常,异常和中断不一样,它在产生时必须要考虑与处理器的时钟同步,实际上,异常也常常称为同步中断,在处理器执行到由于编程失误而导致的错误指令的时候,或者是在执行期间出现特殊情况,必须要靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常以及处理中断的方式类似,因此,内核对它们的处理也很类似。这里的讨论,大部分都是适合异常,这时可以看成是处理器本身产生的中断。

       中断产生告诉中断控制器,继续告诉操作系统内核,内核总是要处理的,是不?这里内核会执行一个叫做中断处理程序或中断处理例程的函数。这里特别要说明,中断处理程序是和特定中断相关联的,而不是和设备相关联,如果一个设备可以产生很多中断,这时该设备的驱动程序也就需要准备多个这样的函数。一个中断处理程序是设备驱动程序的一部分,这个我们在linux设备驱动中已经说过,就不说了,后面我也会提到一些。前边说过一个问题:中断是可能随时发生的,因此必须要保证中断处理程序也能随时执行,中断处理程序也要尽可能的快速执行,只有这样才能保证尽可能快地恢复中断代码的执行。

       但是,不想说但是,大学第一节逃课的情形现在仍记忆犹新:又想马儿跑,又想马儿不吃草,怎么可能!但现实问题或者不像想象那样悲观,我们的中断说不定还真有奇迹发生。这个奇迹就是将中断处理切为两个部分或两半。中断处理程序上半部(top half)---接收到一个中断,它就立即开始开始执行,但只做严格时限的工作,这些工作都是在所有中断被禁止的情况下完成的。同时,能够被允许稍后完成的工作推迟到下半部(bottom half)去,此后,下半部会被执行,通常情况下,下半部都会在中断处理程序返回时立即执行。我会在后面谈论linux所提供的是实现下半部的各种机制。

       说了那么多,现在开始第一个问题:如何注册一个中断处理程序。我们在linux驱动程序理论里讲过,通过一下函数可注册一个中断处理程序:

1
int request_irq(unsignedint irq,irqreturn_t (*handler)(int,void *,struct pt_regs *),unsigned long irqflags,const char * devname,void *dev_id)

       有关这个中断的一些参数说明,我就不说了,一旦注册了一个中断处理程序,就肯定会有释放中断处理,这是调用下列函数:

1
void free_irq(unsignedint irq,void *dev_id)

       这里需要说明的就是要必须要从进程上下文调用free_irq().好了,现在给出一个例子来说明这个过程,首先声明一个中断处理程序:

1
static irqreturn_t intr_handler(int irq,void *dev_id,struct pt_regs *regs)

       注意:这里的类型和前边说到的request_irq()所要求的参数类型是匹配的,参数不说了。对于返回值,中断处理程序的返回值是一个特殊类型,irqrequest_t,可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED.当中断处理程序检测到一个中断时,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断时,返回IRQ_HANDLED.C此外,也可以使用宏IRQ_RETVAL(x),如果x非0值,那么该宏返回IRQ_HANDLED,否则,返回IRQ_NONE.利用这个特殊的值,内核可以知道设备发出的是否是一种虚假的(未请求)中断。如果给定中断线上所有中断处理程序返回的都是IRQ_NONE,那么,内核就可以检测到出了问题。最后,需要说明的就是那个static了,中断处理程序通常会标记为static,因为它从来不会被别的文件中的代码直接调用。另外,中断处理程序是无需重入的,当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一个中断上接收另外一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断总是被禁止的。由此可见,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。      

       下面要说到的一个问题是和共享的中断处理程序相关的。共享和非共享在注册和运行方式上比较相似的。差异主要有以下几点:

1.request_irq()的参数flags必须设置为SA_SHIRQ标志。
2.对每个注册的中断处理来说,dev_id参数必须唯一。指向任一设备结构的指针就可以满足这一要求。通常会选择设备结构,因为它是唯一的,而且中
   断处理程序可能会用到它,不能给共享的处理程序传递NULL值。
3.中断处理程序必须能够区分它的设备是否真的产生了中断。这既需要硬件的支持,也需要处理程序有相关的处理逻辑。如果硬件不支持这一功能,那中
   断处理程序肯定会束手无策,它根本没法知道到底是否与它对应的设备发生了中断,还是共享这条中断线的其他设备发出了中断。

       在指定SA_SHIRQ标志以调用request_irq()时,只有在以下两种情况下才能成功:中断当前未被注册或者在该线上的所有已注册处理程序都指定了SA_SHIRQ.A。注意,在这一点上2.6与以前的内核是不同的,共享的处理程序可以混用SA_INTERRUPT.  一旦内核接收到一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此一个处理程序必须知道它是否应该为这个中断负责。如果与它相关的设备并没有产生中断,那么中断处理程序应该立即退出,这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检查。毫无疑问,大多数设备都提这种功能。

       当执行一个中断处理程序或下半部时,内核处于中断上下文(interrupt context)中。对比进程上下文,进程上下文是一种内核所处的操作模式,此时内核代表进程执行,可以通过current宏关联当前进程。此外,因为进程是进程上下文的形式连接到内核中,因此,在进程上下文可以随时休眠,也可以调度程序。但中断上下文却完全不是这样,它可以休眠,因为我们不能从中断上下文中调用函数。如果一个函数睡眠,就不能在中断处理程序中使用它,这也是对什么样的函数能在中断处理程序中使用的限制。还需要说明一点的是,中断处理程序没有自己的栈,相反,它共享被中断进程的内核栈,如果没有正在运行的进程,它就使用idle进程的栈。因为中断程序共享别人的堆栈,所以它们在栈中获取空间时必须非常节省。内核栈在32位体系结构上是8KB,在64位体系结构上是16KB.执行的进程上下文和产生的所有中断都共享内核栈。
       下面给出中断从硬件到内核的路由过程(截图选自liuux内核分析与设计p61),然后做出总结:

        中断处理路由

                                          图一       中断从硬件到内核的路由

       上面的图内部说明已经很明确了,我这里就不在详谈。在内核中,中断的旅程开始于预定义入口点,这类似于系统调用。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可以知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核调用函数do_IRQ().从这里开始,大多数中断处理代码是用C写的。do_IRQ()的声明如下:

1
unsignedint do_IRQ(struct pt_regs regs)

       因为C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈上的。中断的值也会得以保存,所以,do_IRQ()可以将它提取出来,X86的代码为:

1
int irq = regs.orig_eax & 0xff

       计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机器上,这些操作是由mask_and_ack_8259A()来完成的,该函数由do_IRQ()调用。接下来,do_IRQ()需要确保在这条中断线上有一个有效的处理程序,而且这个程序已经启动但是当前没有执行。如果这样的话, do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序,有关处理例子,可以参考linux内核设计分析一书,我这里就不细讲了。在handle_IRQ_event()中,首先是打开处理器中断,因为前面已经说过处理器上所有中断这时是禁止中断(因为我们说过指定SA_INTERRUPT)。接下来,每个潜在的处理程序在循环中依次执行。如果这条线不是共享的,第一次执行后就退出循环,否则,所有的处理程序都要被执行。之后,如果在注册期间指定了SA_SAMPLE_RANDOM标志,则还要调用函数add_interrupt_randomness(),这个函数使用中断间隔时间为随机数产生熵。最后,再将中断禁止(do_IRQ()期望中断一直是禁止的),函数返回。该函数做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr().该函数类似初始入口代码,以汇编编写,它会检查重新调度是否正在挂起,如果重新调度正在挂起,而且内核正在返回用户空间(也就是说,中断了用户进程),那么schedule()被调用。如果内核正在返回内核空间(也就是中断了内核本身),只有在preempt_count为0时,schedule()才会被调用(否则,抢占内核是不安全的)。在schedule()返回之前,或者如果没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。在x86上,初始化的汇编例程位于arch/i386/kernel/entry.S,C方法位于arch/i386/kernel/irq.c其它支持的结构类似。

       下边给出PC机上位于/proc/interrupts文件的输出结果,这个文件存放的是系统中与中断相关的统计信息,这里就解释一下这个表:

        interruptinfo

       上面是这个文件的输入,第一列是中断线(中断号),第二列是一个接收中断数目的计数器,第三列是处理这个中断的中断控制器,最后一列是与这个中断有关的设备名字,这个名字是通过参数devname提供给函数request_irq()的。最后,如果中断是共享的,则这条中断线上注册的所有设备都会列出来,如4号中断。

        Linux内核给我们提供了一组接口能够让我们控制机器上的中断状态,这些接口可以在<asm/system.h>和<asm/irq.h>中找到。一般来说,控制中断系统的原因在于需要提供同步,通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问,获取这些锁的同时也伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。

       在linux设备驱动理论帖里详细介绍过linux的中断操作接口,这里就大致过一下,禁止/使能本地中断(仅仅是当前处理器)用:

1
2
local_irq_disable();
local_irq_enable();

       如果在调用local_irq_disable()之前已经禁止了中断,那么该函数往往会带来潜在的危险,同样的local_irq_enable()也存在潜在的危险,因为它将无条件的激活中断,尽管中断可能在开始时就是关闭的。所以我们需要一种机制把中断恢复到以前的状态而不是简单地禁止或激活,内核普遍关心这点,是因为内核中一个给定的代码路径可以在中断激活饿情况下达到,也可以在中断禁止的情况下达到,这取决于具体的调用链。面对这种情况,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需把中断恢复到它们原来的状态:

1
2
3
unsignedlong flags;
local_irq_save(flags);
local_irq_restore(flags);

       参数包含具体体系结构的数据,也就是包含中断系统的状态。至少有一种体系结构把栈信息与值相结合(SPARC),因此flags不能传递给另一个函数(换句话说,它必须驻留在同一个栈帧中),基于这个原因,对local_irq_save()的调用和local_irq_restore()的调用必须在同一个函数中进行。前面的所有的函数既可以在中断中调用,也可以在进程上下文使用。

       前面我提到过禁止整个CPU上所有中断的函数。但有时候,好奇的我就想,我干么没要禁止掉所有的中断,有时,我只需要禁止系统中一条特定的中断就可以了(屏蔽掉一条中断线),这就有了我下面给出的接口:

1
2
3
4
void disable_irq(unsignedint irq);
void disable_irq_nosync(unsignedint irq);
void enable_irq(unsignedint irq);
void synchronise_irq(unsignedint irq);

       对有关函数的说明和注意,我前边已经说的很清楚了,这里飘过。另外,禁止多个中断处理程序共享的中断线是不合适的。禁止中断线也就禁止了这条线上所有设备的中断传递,因此,用于新设备的驱动程序应该倾向于不使用这些接口。另外,我们也可以通过宏定义在<asm/system.h>中的宏irqs_disable()来获取中断的状态,如果中断系统被禁止,则它返回非0,否则,返回0;用定义在<asm/hardirq.h>中的两个宏in_interrupt()和in_irq()来检查内核的当前上下文的接口。由于代码有时要做一些像睡眠这样只能从进程上下文做的事,这时这两个函数的价值就体现出来了。

       最后,作为对这篇博客的总结,这里给出我前边提到的用于控制中断的方法列表:

      interruptslist


 下半部的任务就是执行与中断处理密切相关但中断处理程序本生身不执行的任务。最好情况当然是中断处理程序把所有的工作都交給下半部执行,而自己啥都不做。因为我们总是希望中断处理程序尽可能快的返回。但是,中断处理程序注定要完成一部分工作。遗憾的是,并没有谁严格规定说什么任务应该在哪个部分中完成,换句话说,这个决定完全由想我们这样的驱动工程师来做。记住,中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线,因此将中断处理程序缩短的越小就越好。当然啦,没有规则并不是没有经验和教训:

1.如果一个任务对时间非常敏感,感觉告诉我还是将其放在中断处理程序中执行是个好的选择。
2.如果一个任务和硬件相关,还是将其放在中断处理程序中执行吧。
3.如果一个任务要保证不被其他中断(特别是相同的中断)打断,那就将其放在中断处理程序中吧。
4.其他所有任务,除非你有更好的理由,否则全部丢到下半部执行。

       总之,一句话:中断处理程序要执行的越快越好。

       我们前边老是说下半部会在以后执行,那么这个以后是个什么概念呢?遗憾的说,这个只是相对于马上而言的。下半部并需要指定一个明确的时间,只要把这个任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序已返回就会马上执行,关键在于当它们运行时,允许相应所有的中断。

       上半部只能通过中断处理程序来完成,下半部的实现却是有很多种方式。这些用来实现下半部的机制分别由不同的接口和子系统组成。最早的是“bottom half”,这种机制也被称为“BH”,它提供的接口很简单,提供了一个静态创建,由32个bottom half组成的链表,上半部通过一个32位整数中的一位来标识出哪个bottom half可执行。每个BH都在全局范围内进行同步,即使分属于不同的处理器,也不允许任何两个bottom half同时执行。这种方式使用方便但不够灵活,简单却有性能瓶颈。以需要更好的方法了。第二种方法,任务队列(task queues).内核定义了一组队列。其中每个队列都包含一个由等待调用的函数组成链表。根据其所处队列的位置,这些函数会在某个时刻被执行,驱动程序可根据需要把它们自己的下半部注册到合适的队列上去。这种方法已经不错,但仍然不够灵活,它没办法代替整个BH接口。对于一些性能要求较高的子系统,像网络部分,它也不能胜任。在2.3开发版中,又引入了软中断(softirqs)和tasklet,这里的软中断和实现系统调用所提到的软中断(软件中断)不是同一个概念。如果无须考虑和过去开发的驱动程序相兼容的话, 软中断和tasklet可以完全代替BH接口。软中断是一组静态定义的下半部接口,有32个,可以在所有的处理器上同时执行----即使两个类型完全相同。task是一种基于软中断实现的灵活性强,动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。tasklet其实是一种在性能和易用性之间寻求平衡的产物。软中断必须在编译期间就进行静态注册,而tasklet可以通过代码进行动态注册。现在都是2.6内核了,我们说点新鲜的吧,linux2.6内核提供了三种不同形式的下半部实现机制:软中断,tasklets和工作对列,这些会依次介绍到。这时,可能有人会想到定时器的概念,定时器也确实是这样,但定时器提供了精确的推迟时间,我们这里还不至于这样,所以先放下,我们后面再说定时器。好,下面我开始说说详细的各种机制:

       1.软中断   实际上软中断使用的并不多,反而是后面的tasklet比较多,但tasklet是通过软中断实现的,软中断的代码位于/kernel/softirq.c中。软中断是在编译期间静态分配的,由softirq_action结构表示,它定义在linux/interrupt.h中:

1
2
3
4
struct softirq_action{
    void (*action)(struct softirq_action *);  //待执行的函数
    void *data;//传给函数的参数
};

kernel/softirq.c中定义了一个包含有32个结构体的数组:

1
static struct softirq_action softirq_vec[32]

       每个被注册的软中断都占据该数组的一项,因此最多可能有32个软中断,这是没法动态改变的。由于大部分驱动程序都使用tasklet来实现它们的下半部,所以现在的内核中,只用到了6个。上面的软中断结构中,第一项是软中断处理程序,原型如下:

1
void softirq_handler(struct softirq_action *)

       当内核运行一个软中断处理程序时,它会执行这个action函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的实现,那么内核会用如下的方式调用软中断处理程序中的函数:

1
my_softirq->action(my_softirq)

       一个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断----甚至是相同类型的软中断-----可以在其他类型的机器上同时执行。一个注册的软中断必须在被标记后才能执行----触发软中断(rasing the softirq).通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行。在下列地方,待处理的软中断会被检查和执行:

1.处理完一个硬件中断以后                   2.在ksoftirqd内核线程中                 3.在那些显示检查和执行待处理的软中断的代码中,如网络子系统中。

       软中断会在do_softirq()中执行,如果有待处理的软中断,do_softirq会循环遍历每一个,调用他们的处理程序。核心部分如下:

1
2
3
4
5
6
7
8
9
10
11
u32 pending = softirq_pending(cpu);
if(pending){
    struct softirq_action *h = softirq_vec;
    softirq_pending(cpu) = 0;
    do{
      if(pending &1)
        h->action(h);
      h++;
      pending >>=1;
    }while(pending);
}

       上述代码会检查并执行所有待处理的软中断,softirq_pending(),用它来获得待处理的软中断的32位位图-----如果第n位被设置为1,那么第n位对应类型的软中断等待处理,一旦待处理的软中断位图被保存,就可以将实际的软中断位图清零了。pending &1是判断pending的第一位是否被置为1.一旦pending为0,就表示没有待处理的中断了,因为pending最多可能设置32位,循环最多也只能执行32位。软中断保留给系统中对时间要求最严格以及最重要的下半部使用。所以使用之前还是要想清楚的。下面简要的说明如何使用软中断:

1.分配索引:在编译期间,要通过<linux/interrupt.h>中建立的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示一种相对优先级,
  索引号越小就越先执行。所以,可以根据自己的需要将自己的索引号放在合适的位置。
2.注册处理程序:接着,在运行时通过调用open_softirq()注册中断处理程序,例如网络子系统,如下:
1
2
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);
  函数有三个参数,软中断索引号,处理函数和data域存放的数组。软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运
  行的时候,当前处理器的软中断被禁止,但其他的处理器仍可以执行别的软中断。实际上,如果一个软中断在它被执行的时候同时再次被触发了,那么
  另外一个处理器可以同时运行其处理程序。这意味着对共享数据都需要严格的锁保护。大部分软中断处理程序都通过采取单处理数据(仅属于某一个处
  理器的数据)或者其他一些技巧来避免显示的加锁,从而提供更出色的性能。 
3.触发软中断:经过上面两项,新的软中断处理程序就能够运行,raist_softirq(中断索引号)函数可以将一个软中断设置为挂起状态,从而让它在下次调
  用do_softirq()函数时投入运行。该函数在触发一个软中断之前先要禁止中断,触发后再恢复回原来的状态,如果中断本来就已经被禁止了,那么可以调用另一函数raise_softirq_irqoff(),这会带来一些优化效果。

       在中断处理程序中触发软中断是最常见的形式,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序后,马上就会调用do_softirq()函数。于是,软中断就开始执行中断处理程序留给它去完成的剩下任务。

       2.Tasklets:tasklet是通过软中断实现的,所以它们本身也是软中断。它由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ.区别在于前者会先于后者执行。Tasklets由tasklet_struct结构表示,每个结构体单独代表一个tasklet,在linux/interrupt.h中定义:

1
2
3
4
5
6
7
struct tasklet_struct{
    struct tasklet_struct *next;
    unsignedlong state;
    atomic_t count;
    void (*func)(unsignedlong);
    unsignedlong data;
};

       结构体中func成员是tasklet的处理程序,data是它唯一的参数。state的取值只能是0,TASKLET_STATE_SCHED(表明tasklet已经被调度,正准备投入运行)和TASKLET_STATE_RUN(表明该tasklet正在运行,它只有在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行)之间取值。count是tasklet的引用计数器,如果不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且被设置为挂起状态时,该tasklet才能够执行。已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和task_hi_vec高优先级的tasklet)中,这两个数据结构都是由tasklet_struct结构体构成的链表,链表中的每个tasklet_struct代表一个不同的tasklet。Tasklets由tasklet_schedule()和tasklet_hi_schedule()进行调度,它们接收一个指向tasklet_struct结构的指针作为参数。两个函数非常相似(区别在于一个使用TASKLET_SOFTIRQ而另外一个使用HI_SOFTIRQ).有关tasklet_schedule()的操作细节:

1.检查tasklet的状态是否为TASKLET_STATE_SCHED.如果是,说明tasklet已经被调度过了,函数返回。
2.保存中断状态,然后禁止本地中断。在执行tasklet代码时,这么做能够保证处理器上的数据不会弄乱。
3.把需要调度的tasklet加到每个处理器一个的tasklet_vec链表或task_hi_vec链表的表头上去。
4.唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。
5.恢复中断到原状态并返回。

       那么作为tasklet处理的核心tasklet_action()和tasklet_hi_action(),具体做了些什么呢:

1.禁止中断,并为当前处理器检索tasklet_vec或tasklet_hi_vec链表。
2.将当前处理器上的该链表设置为NULL,达到清空的效果。
3.运行相应中断。
4.循环遍历获得链表上的每一个待处理的tasklet。
5.如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳
   到下一个待处理的tasklet去。
6.如果当前这个tasklet没有执行,将其状态设置为TASKLETLET_STATE_RUN,这样别的处理器就不会再去执行它了。
7.检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止,则跳到下一个挂起的tasklet去。
8.现在可以确定这个tasklet没有在其他地方执行,并且被我们设置为执行状态,这样它在其他部分就不会被执行,并且引用计数器为0,现在可以执行
   tasklet的处理程序了。
9.重复执行下一个tasklet,直至没有剩余的等待处理的tasklets。

       说了这么多,我们该怎样使用这个tasklet呢,这个我在linux设备驱动理论帖讲的太多了。但别急,下边为了博客完整,我仍然会大致讲讲:

       1.声明自己的tasklet:投其所好,既可以静态也可以动态创建,这取决于选择是想有一个对tasklet的直接引用还是间接引用。静态创建方法(直接引用),可以使用下列两个宏的一个(在linux/interrupt.h中定义):

1
2
DECLARE_TASKLET(name,func,data)
DECLARE_TASKLET_DISABLED(name,func,data)

       这两个宏之间的区别在于引用计数器的初始值不同,前面一个把创建的tasklet的引用计数器设置为0,使其处于激活状态,另外一个将其设置为1,处于禁止状态。而动态创建(间接引用)的方式如下:

1
tasklet_init(t,tasklet_handler,dev);

       2.编写tasklet处理程序:函数类型是void tasklet_handler(unsigned long data).因为是靠软中断实现,所以tasklet不能休眠,也就是说不能在tasklet中使用信号量或者其他什么阻塞式的函数。由于tasklet运行时允许响应中断,所以必须做好预防工作,如果新加入的tasklet和中断处理程序之间共享了某些数据额的话。两个相同的tasklet绝不能同时执行,如果新加入的tasklet和其他的tasklet或者软中断共享了数据,就必须要进行适当地锁保护。

       3.调度自己的tasklet:前边的工作一做完,接下来就剩下调度了。通过一下方法实现:tasklet_schedule(&my_tasklet).在tasklet被调度以后,只要有合适的机会就会得到运行。如果在还没有得到运行机会之前,如果有一个相同的tasklet又被调度了,那么它仍然只会运行一次。如果这时已经开始运行,那么这个新的tasklet会被重新调度并再次运行。一种优化策略是一个tasklet总在调度它的处理器上执行。调用tasklet_disable()来禁止某个指定的tasklet,如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_disable_nosync()也是来禁止的,只是不用在返回前等待tasklet执行完毕,这么做安全性就不咋嘀了(因为没法估计该tasklet是否仍在执行)。tasklet_enable()激活一个tasklet。可以使用tasklet_kill()函数从挂起的对列中去掉一个tasklet。这个函数会首先等待该tasklet执行完毕,然后再将其移去。当然,没有什么可以阻止其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止在中断上下文中使用它。

       接下来的问题,我在前边说过,对于软中断,内核会选择几个特殊的实际进行处理(常见的是中断处理程序返回时)。软中断被触发的频率有时会很好,而且还可能会自行重复触发,这带来的结果就是用户空间的进程无法获得足够的处理器时间,因为处于饥饿状态。同时,如果单纯的对重复触发的软中断采取不立即处理的策略也是无法接受的。两种极端但完美的情况是什么样的呢:

1.只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。问题在于,用户进程可能被忽略而使其
   处于饥饿状态。
2.选择不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理它们,但是,任何自行重新触发的软中断都
   不会马上处理,它们被放到下一个软中断执行时机去处理。问题在于新的或重新触发的软中断必要要等一定的时间才能被执行。

       我现在想的是来个折衷方案吧,那多好,内核开发者门还真是想到了。内核选中的方案是不会立即处理重新触发的软中断,作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低优先级上运行(nice值为19)。这种这种方案能够保证在软中断负担很重的时候用户程序不会因为得不到处理时间而处理饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断处理得非常迅速(因为仅存的内存线程肯定会马上调度)。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。所有线程的名字都叫做ksoftirad/n,区别在于n,它对应的是处理器的编号。一旦该线程被初始化,它就会执行类似下面这样的死循环:

1
2
3
4
5
6
7
8
9
10
11
for(;;){
    if(!softirq_pending(cpu))
        schedule();
    set_current_state(TASK_RUNNING);
    while(softirq_pending(cpu)){
        do_softirq();
        if(need_resched())
            schedule();
    }
    set_current_state(TASK_INTERRUPTIBLE);
}

       softirq_pending()负责发现是否有待处理的软中断。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行。最后,只要do_softirq()函数发现已经执行过的内核线程重新触发了自己,软中断内核线程就会被唤醒。

 接着上节的来,我们在上节说了软中断和tasklet,那这最后就是工作队列了哦..

       工作队列和前面讨论的其他形式都不相同,它可以把工作推后,交由一个内核线程去执行----该工作总是会在进程上下文执行。这样,通过工作队列执行代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。相比较前边两个,这个选择起来就很容易了。我说过,前边两个是不允许休眠的,这个是允许休眠的,这就很明白了是不?这意味着在你需要获得大量内存的时候,在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用(先说话, 这个不是我说的,是书上这么说的哦)。

       工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程(worker threads).工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成一个把需要推后执行的任务交给特定的通用线程这样一种接口。缺省的工作线程叫做event/n.每个处理器对应一个线程,这里的n代表了处理器编号。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好还是使用缺省线程。

       1.工作这线程结构用下面的结构表示:

1
2
3
struct workqueue_struct{
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
}

       结构中数组的每一项对应系统的一个CPU.接下来,在看看在kernel/workqueue.c中的核心数据结构cpu_workqueue_struct:

1
2
3
4
5
6
7
8
9
10
struct cpu_workqueue_struct{
    spinlock_t lock;
    atomic_t nr_queued;
    struct list_head worklist;
    wait_queue_head_t more_work;
    wait_queue_head_t work_done;
    struct workqueue_struct *wq;
    task_t *thread;
    struct completion exti;
}

       2.表示工作的数据结构:所有的工作者线程都是用普通的内核线程来实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行一个死循环执行一个循环并开始休眠,当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的时候,它又会继续休眠。工作有work_struct(linux/workqueue)结构表示:

1
2
3
4
5
6
7
8
struct work_struct{
    unsignedlong pending;
    struct list_head entry;       //连接所有工作的链表
    void (*func)(void *);        //处理函数
    void *data;          //传递给处理函数的参数
    void *wq_data;
    struct timer_list timer;      //y延迟工作队列所用到的定时器
}

       当一个工作线程被唤醒时,它会执行它的链表上的所有工作。工作一旦执行完毕,它就将相应的work_struct对象从链表上移去,当链表不再有对象时,它就继续休眠。woker_thread函数的核心流程如下:

1
2
3
4
5
6
7
8
9
10
11
for(;;){
    set_task_state(current,TASK_INTERRUPTIBLE);
    add_wait_queue(&cwq->more_work,&wait);
    if(list_empty(&cwq->worklist))
        schedule();
    else
        set_task_state(current,TASK_RUNNING);
    remove_wait_queue(&cwq->more_work,&wait);
    if(!list_empty(&cwq->worklist))
        run_workqueue(cwq);
}

       分析一下上面的代码。首先线程将自己设置为休眠状态并把自己加入等待队列。如果工作对列是空的,线程调用schedule()函数进入睡眠状态。如果链表有对象,线程就将自己设为运行态,脱离等待队列。然后,再次调用run_workqueue()执行推后的工作。好了,接下来,问题就纠结在run_workqueue(),它完成实际推后到此的工作:

1
2
3
4
5
6
7
8
while(!list_empty(&cwq->worklist)){
    struct work_struct *work = list_entry(cwq->worklist.next,struct work_struct,entry);
    void (*f)(void *) = work->func;
    void *data = work->data;
    list_del_init(cwq->worklist.next);
    clear_bit(0,&work->pending);
    f(data);
}

       该函数循环遍历链表上每个待处理的工作,执行链表上每个结点上的work_struct的func成员函数:

1.当链表不为空时,选取下一个节点对象。
2.获取我们希望执行的函数func及其参数data。
3.把该结点从链表上接下来,将待处理标志位pending清0。
4.调用函数。
5.重复执行。

       老师说的好:光说不练,不是好汉。现在我们继续来看看怎么用吧:

       1.首先,实际创建一些需要推后完成的工作,可以在编译时静态地创建该数据结构:

1
DECLARE_WORK(name,void (*func)(void *),void *data);

当然了,如果愿意,我们当然可以在运行时通过指针动态创建一个工作:

1
INIT_WORK(struct work_struct *work, void (*func)(void *),void *data);

       2.工作队列处理函数,会由一个工作者线程执行,因此,函数会运行在进程上下文中,默认情况下,允许相应中断,并且不持有锁。如果需要,函数可以睡眠。需要注意的是,尽管处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相应的内存映射。函数原型如下:

1
void work_hander(void *data);

       3.对工作进行调度。前面的准备工作做完以后,下面就可以开始调度了,只需调用schedule_work(&work).这样work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。当然如果不想快速执行,而是想延迟一段时间执行,按就用schedule_delay_work(&work,delay);delay是要延迟的时间节拍,后面讲.

       4.刷新操作。插入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在继续下一步工作之前,你必须保证一些操作已经执行完毕等等。由于这些原因,内核提供了一个用于刷新指定工作队列的函数:void flush_scheduled_work(void); 这个函数会一直等待,直到队列中所有的对象都被执行后才返回。在等待所有待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。需要说明的是,该函数并不取消任何延迟执行的工作。取消延迟执行的工作应该调用:int cancel_delayed_work(struct work_struct *work);这个函数可以取消任何与work_struct 相关挂起的工作。

       5.创建新的工作队列。前边说过最好使用缺省线程,可如果你坚持要使用自己创建的线程,咋办?这时你就应该创建一个新的工作队列和与之相应的工作者线程,方法很简单,用下面的函数:struct workqueue_struct *create_workqueue(const char *name);name是新内核线程的名字。这样就会创建所有的工作者线程(系统中的每个处理器都有一个)并且做好所有开始处理工作之前的准备工作。在创建之后,就调用下面的函数吧:

1
2
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq,struct work_struct *work,unsigned long delay);

       这两个函数和schedule_work()和schedule_delayed_work()相近,唯一的区别在于它们可以针对特定的工作队列而不是缺省的event队列进行操作。

       好了,工作队列也说完了,我还是结合前边一篇,把这三个地板不实现的策略比较一下,方便以后选择.

       首先,tasklet是基于软中断实现的,两者相近,工作队列机制与它们完全不同,靠内核线程来实现。软中断提供的序列化的保障最少,这就要求中断处理函数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。如果被考察的代码本身多线索化的工作做得非常好,它完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行效率很高的应用来说,它执行的也最快。否则选择tasklets意义更大。tasklet接口简单,而且两种同种类型的tasklet不能同时执行,所以实现起来也会简单一些。如果需要把任务推迟到进程上下文中完成,那你只能选择工作队列了。如果不需要休眠,那软中断和tasklet可能更合适。另外就是工作队列造成的开销最大,当然这是相对的,针对大部分情况,工作队列都能提供足够的支持。从方便度上考虑就是:工作队列,tasklets,最后才是软中断。我们在做驱动的时候,关于这三个下半部实现,需要考虑两点:首先,是不是需要一个可调度的实体来执行需要推后完成的工作(即休眠的需要),如果有,工作队列就是唯一的选择,否则最好用tasklet。性能如果是最重要的,那还是软中断吧。

       最后,就是一些禁止下半部的相关部分了,给一个表:

函数

描述

void local_bh_disable() 禁止本地处理器的软中断和tasklet的处理
void local_bh_enable() 激活本地处理器的软中断和tasklet的处理

       这些函数有可能被嵌套使用----最后被调用的local_bh_enable()最终激活下半部。函数通过preempt_count为每个进程维护一个计数器。当计数器变为0时,下半部才能够被处理。因为下半部的处理已经被禁止了,所以local_bh_enable()还需要检查所有现存的待处理的下半部并执行它们。

       好了,这一次讲完了,画了两次,我们在这两次中提到了一些同时发生的问题,这时可能存在数据共享互斥访问的问题,这个就是内核同步方面的事情了,我们后面再慢慢说这个事。


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:47835次
    • 积分:896
    • 等级:
    • 排名:千里之外
    • 原创:34篇
    • 转载:23篇
    • 译文:0篇
    • 评论:4条
    最新评论