上半部与下半部

时间:2022-04-01 23:37:58

中断处理是分为两个部分:中断处理程序是上半部,它接收到一个中断,就立即执行,但只做有严格时限的工作;而另外被叫做下半部的另外一个部分主要做被允许能稍后完成的工作。这个下半部正是今天的重点。

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

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中:

1234 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会循环遍历每一个,调用他们的处理程序。核心部分如下:

1234567891011 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()注册中断处理程序,例如网络子系统,如下:
12 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中定义:

1234567 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中定义):

12 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,它对应的是处理器的编号。一旦该线程被初始化,它就会执行类似下面这样的死循环:

1234567891011 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()函数发现已经执行过的内核线程重新触发了自己,软中断内核线程就会被唤醒。

分类: Linux内核分析笔记帖