linux中断处理的上半部和下半部

设备中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务尽可能的短小精悍。但是,在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。

Linux内核中,为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。中断处理程序的上半部在接收到一个中断时就立即执行,但只做比较紧急的工作,这些工作都是在所有中断被禁止的情况下完成的,所以要快,否则其它的中断就得不到及时的处理。那些耗时又不紧急的工作被推迟到下半部去。中断处理程序的下半部分(如果有的话)几乎做了中断处理程序所有的事情。它们最大的不同是上半部分不可中断,而下半部分可中断。在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。

对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,不过还是有一些习惯供参考:

如果该任务对时间比较敏感,将其放在上半部中执行。

如果该任务和硬件相关,一般放在上半部中执行。

如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。

其他不太紧急的任务, 一般考虑在下半部执行。

下半部分并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太忙并且中断恢复后执行就可以了。通常下半部分在中断处理程序一返回就会马上运行。内核中实现下半部的手段不断演化,目前已经从最原始的BHbottom half)衍生出BH(在2.5中去除)、软中断(softirq2.3引入)、tasklet(在2.3引入)、工作队列(work queue2.5引入)。稍后笔者将介绍后两种方式。

尽管上半部和下半部的结合能够改善系统的响应能力,但是,Linux设备驱动中的中断处理并不一定要分成两个半部。如果中断要处理的工作本身就很少,则完全可以直接在上半部全部完成。

前面说过,Linux实现下半部的机制主要有tasklet、工作队列和软中断等。下面对tasklet机制和工作队列机制在Linux系统中的实现作简单介绍。

 tasklet机制

tasklet可以理解为软件中断的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次。

软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。

tasklet的使用相当简单,我们只需要定义tasklet及其处理函数并将二者关联:

void my_tasklet_func(unsigned long);

DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);

其中my_tasklet_func(unsigned long)定义了tasklet的处理函数;DECLARE_TASKLET (my_tasklet,my_tasklet_func,data)实现了将名称为my_tasklettaskletmy_tasklet_func()函数相关联。然后,在需要调度tasklet的时候引用下面的的API就能使系统在适当的时候进行调度运行:

tasklet_schedule(&my_tasklet);

此外,Linux还提供了另外一些其它的控制tasklet调度与运行的API

DECLARE_TASKLET_DISABLED(name,function,data); //DECLARE_TASKLET类似,但等待tasklet被使能

tasklet_enable(struct tasklet_struct *); //使能

tasklet tasklet_disble(struct tasklet_struct *); //禁用

tasklet tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); //类似DECLARE_TASKLET()

tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可调度位,即不允许调度该tasklet

tasklet的具体实现代码如下:

#include

//定义与绑定tasklet函数
void test_tasklet_action(unsigned long t);
DECLARE_TASKLET(test_tasklet, test_tasklet_action, 0);

void test_tasklet_action(unsigned long t)
{
printk("tasklet is executing\n");
}



ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{

if (copy_from_user(&global_var, buf, sizeof(int)))
{
return - EFAULT;
}

//调度tasklet执行
tasklet_schedule(&test_tasklet);
return sizeof(int);
}

它的功能是:在globalvar被写入一次后,就调度一个tasklet,函数中输出"tasklet is executing"。

工作队列机制

工作队列是Linux 2.6 内核中新增加的一种下半部机制。它与其它几种下半部分机制最大的区别就是它可以把工作推后,交由一个内核线程去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。因此,如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要获得大量的内存、需要获取信号量或者需要执行阻塞式的I/O操作时,使用工作队列的方式将非常有用。

工作队列的使用方法和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:

struct work_struct my_wq; //定义一个工作队列

void my_wq_func(unsigned long); //定义一个处理函数

通过INIT_WORK()可以初始化该工作队列并将工作队列与处理函数绑定,如下所示:

INIT_WORK(&my_wq, (void(*) (void *))my_wq_func, NULL);

tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如下:

schedule_work(&my_wq); //调度工作队列执行

 

你可能感兴趣的:(内核类相关)