中断处理程序运行在中断上下文中,所以不允许被抢占,所以执行时间应该越短越好,但有时中断处理程序还需要做很多工作,为了两点都满足,所以一般我们把中断处理切为两个部分:上半部(top half)和下半部(bottom half).

上半部:接收到一个中断,它就立即开始执行,但只做有严格时限的工作.例如对接收的中断进行应答或者复位硬件,这些工作都是在所有中断被禁止的情况下完成的.

下半部:能够被允许稍后完成的工作会推迟到下半部.这里的"稍后"在时间上强调只要不是现在必须做的就行。下半部的任务就是执行与中断处理程序密切相关但中断处理程序本身不执行,推后执行的工作。

举个top half和bottom half的例子,比如说网卡驱动:

top half:应答硬件,把skb铐到内存,读取网卡更多的数据包.

bottom hafl:处理和操作数据包.

 

中断上下文和进程上下文的区别.

进程上下文是一种内核所处的操作模式,此时内核代表进程执行.在此上下文中,可以使用current宏关联当前进程.可以睡眠,也可以调用调度程序.

中断上下文和进程并没有什么关系,所以与current无关,不可以睡眠,不可以调用调度函数.因此,如果一个函数可能睡眠,则就不能在中断处理程序中使用.

注意:中断处理程序打断了其他的代码.正是因为这种异步的特性,所以所有的中断处理程序必须尽可能的迅速,简洁.尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行

 

中断处理程序(上半部)的局限:

* 中断处理程序以异步方式执行并且它有可能会打断其他重要代码的执行。因此,它们应该执行得越快越好。
    * 如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其他中断会被屏蔽,在最坏的情况下,所有其他中断都会被屏蔽。因此,仍应该让它们执行得越快越好。
    * 由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
    * 中断处理程序不在进程上下文中运行,所以它们不能阻塞。

一个工作是放在上半部还是放在下半部去执行,可以参考下面四条:

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

 

linux2.6内核提供了三种不同形式的下半部实现机制:软中断,tasklets和工作对列

软中断

软中断的实现:软中断的代码位于kernel/softirq.c文件中。其由softirq_action结构表示,定义在< linux/interrupt.h>中。在kernel/softirq.c中定义了一个包含有多个该结构体的数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

2.6.35内核中使用了如下软中断:

enum

{

     HI_SOFTIRQ=0,

     TIMER_SOFTIRQ,

     NET_TX_SOFTIRQ,

     NET_RX_SOFTIRQ,

     BLOCK_SOFTIRQ,

     BLOCK_IOPOLL_SOFTIRQ,

     TASKLET_SOFTIRQ,

     SCHED_SOFTIRQ,

     HRTIMER_SOFTIRQ,

     RCU_SOFTIRQ,  /* Preferable RCU should always be the last softirq */

 

     NR_SOFTIRQS

};

 

在下列地方,待处理的软中断会被检查和执行:
    * 在处理完一个硬件中断以后。
    * 在ksoftirqd内核线程中。
    * 在那些显式检查和执行待处理的软中断的代码中,如网络子系统中(显式调用do_softirq)。

不管用什么办法唤起,软中断都要在do_softirq()中执行。该函数很简单,如果有待处理的软中断,do_softirq()就会循环遍历每一个,调用它们的处理程序。

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。网络和SCSI直接使用软中断,内核定时器和tasklets都是建立在软中断上的。对于时间要求严格并能自己高效地完成加锁工作的应用,软中断会是正确的选择。

使用软中断

1)分配索引
   在编译期间,通过<linux/interrupt.h>中定义的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号大的软中断之前执行。建立一个新的软中断必须在此枚举类型中加入新的项。而加入时,不能像在其他地方一样,简单地把新项加到列表的末尾。相反,你必须根据你希望赋予它的优先级来决定加入的位置。
   2)注册你的处理程序
   接着,在运行时通过调用open_softirq()注册软中断处理程序。软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他处理器仍可以执行别的软中断。实际上,如果一个软中断在它被执行的同时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这意味着任何共享数据-甚至是仅在软中断处理程序内部使用的全局变量都需要严格的锁保护。如果仅仅通过互斥的加锁方式来防止它自身的并发执行,那么使用软中断就没有任何意义。因此大部分软中断处理程序都通过采取单处理器数据或其他一些技巧来避免显式地加锁,从而提供更出色的性能。
   3)触发你的软中断
   raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。该函数在触发一个软中断之前先要禁止中断,触发后再恢复回原来的状态。如果中断已经被禁止了,那可以调用另一函数raise_softirq_irqoff(),这会带来一些优化效果。在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq()函数。于是软中断开始执行中断处理程序留给它去完成的剩余任务。

每个处理器都有一组辅助处理软中断的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。这些内核线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源,但它们最终肯定会被执行。

Tasklets

Tasklets是利用软中断实现的一种下半部机制。它和进程没有任何关系。Tasklets和软中断在本质上很相似,行为表现也相近,但是它的接口更简单,锁保护也要求较低。选择到底是用软中断还是tasklets其实很简单:通常你应该用tasklets,软中断的使用者屈指可数。它只在那些执行频率很高和连续性要求很高的情况下才需要。

使用Tasklets
   1)声明自己的Tasklet
   既可以使用<linux/interrupt.h>中定义的两个宏中的一个DECLARE_TASKLET或 DECLARE_TASKLET_DISABLED来静态创建tasklet,前者把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。另一个把引入计数器设为1,所以该tasklet处于禁止状态。还可以使用tasklet_init()动态创建一个tasklet。
   2)编写自己的tasklet处理程序
   tasklet处理程序必须符合规定的函数类型:void tasklet_handler(unsigned long data)。因为是靠软中断实现,所以tasklet不能睡眠。这意味着你不能在tasklet中使用信号量或其他什么阻塞式函数。如果你的 tasklet和其他的tasklet或软中断共享了数据,你必须进行适当的锁保护。
   3)调度自己的tasklet
   通过调用tasklet_schedule()函数来调度。在tasklet被调度以后在其还没有得到运行机会之前,如果一个相同的tasklet又被调度了,那么它仍只会运行一次。而如果这时它已经开始运行了,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,一个tasklet总在调度它的处理器上执行-这是希望更好地利用处理器的高速缓存。可以调用tasklet_disable()函数来禁止某个指定的tasklet,也可以调用tasklet_enable()函数激活一个tasklet。还可以调用tasklet_kill()函数从挂起的队列中去掉一个tasklet。

tasklet和软中断的关系

首先系统在初始化的时候在start_kernel函数中通过调用softirq_init()函数,在softirq_init()函数中通过调用open_softirq(TASKLET_SOFTIRQ,tasklet_action);来注册一个软中断,其处理函数为tasklet_action。注册之后还需调用raise_softirq函数来告诉内核TASKLET_SOFTIRQ软中断已挂起,以使处理器可以尽早处理该型软中断。挂起的操作通过tasklet_schedule()函数间接调用raise_softirq()函数来实现。此后则等待中断处理函数退出后调用do_softirq来执行TASKLET_SOFTIRQ软中断的处理函数tasklet_action。

内核定时器

内核的定时器是通过定时器软中断实现的。

内核部分:

1.注册软中断:在内核启动时,在start_kernel函数中通过调用init_timers->open_softirq(TIMER_SOFTIRQ, run_timer_softirq)

实现注册TIMER_SOFTIRQ软中断,其中run_timer_softirq函数为软中断处理函数。

2.中断挂起:timer_interrupt->update_process_times->run_local_timers->raise_softirq(TIMER_SOFTIRQ);

其中timer_interrupt函数为定时器中断处理函数

驱动部分:

1.初始化一个timer_list,特别是初始化其中的func函数指针

2.func函数的实现,此为内核定时器中断处理函数。即定时时间到后,会调用此函数。

具体执行

在内核时间片到,定时器硬件中断退出时,会调用do_softirq函数。在此函数中先调用软中断处理函数run_timer_softirq,在此处理

函数中查看和修改内核定时器链表上的定时器。若定时时间到,则通过timer->func调用定时器处理函数。

 

工作队列

工作队列是另外一种将工作推后执行的形式,它和我们之前讨论过的所有其他形式都不相同。工作队列可以把工作推后,交由一个内核线程去执行-该工作总是会在进程上下文执行。如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在你需要获得大量的内存时、在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用。
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程。工作队列子系统提供了一个缺省的工作者线程来处理需要推后的工作。不过如果需要在工作者线程中执行大量的处理操作,也可以创建属于自己的工作者线程。这么做有助于减轻缺省线程的负担,避免工作队列中其他需要完成的工作处于饥饿状态。
如何使用自己的工作队列

1)创建需要推后执行的工作。

静态:DECLARE_WORK(name, void(*func), void *data);

动态:INIT_WORK(struct work_struct *work, void(*func)(void *), void *data);

2)编写工作的处理函数。

函数原型为:

void work_handler(void *data)

注意:由于这个函数会由一个工作者线程调度,所以会运行在进程上下文中。但它不能够访问用户空间。只有在系统调用发生时,才有可能访问用户空间。

3)对工作进行调度。

也就是把工作加入到工作者线程的工作队列中:如果把工作交给默认的events,则只需调用:

schedule_work(&work);

这样work就会被加入到events的任务队列中,当所在处理器上的相应工作者线程被唤醒时,它就会被执行。或者:

schedule_delayed_work(&work, delay);

delay个时钟节拍以后将work加入到工作者的任务队列中。

4)刷新操作

有时,在进行下一步之前必须确保一些操作已经执行完毕,此时可以用刷新操作。(也就是等待操作???)

void flush_schedule_work(void);

此函数会一直等待,直到队列中所有对象都被执行以后才返回。

注意,该函数不会取消任何延迟执行的工作。取消延迟执行的工作应该调用:

int cancel_delayed_work(struct work struct *work);

这个函数可以取消任何与work_struct相关的挂起操作。

ok,到这里如何创建我们的‘工作’和如何把它加入到工作队列中我们已经知道了。下面来看看当默认工作者不能满足要求时,此时应该创建一类新的工作者线程。但是要注意,前面提到,随便创建一个内核线程不是一个好的办法,所以当必须要这么做时,可以考虑创建自己的工作队列。方法如下:

可以调用以下函数:

struct workqueue_struct *create_workqueue(const char *name);

比如我们的events队列的创建调用的是:

struct workqueue_struct *keventd_wq;

keventd_wq = create_workqueue("events");

把工作加入到自己定义的工作者线程的工作队列的函数是:

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);

跟默认的相比,只是多了要给出对应的工作者线程。

刷新指定的工作队列:

flush_workqueue(struct workqueue_struct *wq);


下半部机制的选择

简单地说,一般的驱动程序编写者需要做两个选择。首先,你是不是需要一个可调度的实体来执行需要推后完成的工作-你有休眠的需要吗?要是有,工作队列就是你的唯一选择。否则最好用tasklet。要是必须专注于性能的提高,那么就考虑软中断吧。

如果推后执行的工作需要睡眠,就选择工作队列。如果推后执行的工作不需要睡眠,最好用tasklet,要是必须专注于性能的提高,那么就考虑软中断吧。。也可以这么记忆,如果你的工作需要获得大量的内存时,需要获取信号量时,需要执行阻塞式的I/O操作时,工作队列会很有用。

 

另外一个可以用于将工作推后执行的机制是内核定时器。也就是说,如果想把任务推后执行,有两种方法可供选择:

1)放到下半部:软中断,tasklet和工作队列。这种方法对时间的要求不确定,只要不是现在就行。

2)内核定时器:推后到确定的时间后执行。

Logo

更多推荐