Linux内核中的中断


中断处理程序是被内核调用来响应中断的,它运行在中断上下文,中断处理程序是上半部,当接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成。能够被允许稍后完成的工作会推迟到下半部去。

中断处理程序的注册是通过request_irq函数,由于该函数内部有分配内存的操作,所以它不能在中断上下文或其他不允许阻塞的代码中调用。Linux中的中断处理程序是无须重入的,因为当一个给定的中断处理程序正在执行时,所有其他的中断都是打开的,而当前中断线总是被禁止的,由此可见,同一个中断处理程序绝不会被同时调用以处理嵌套的中断。中断处理程序不用关心中断栈和内核栈的设置,尽量节约内核栈空间就是了。

锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供的保护机制则是防止来自其他中断处理程序的并发访问。禁止中断包括禁止当前处理器的所有中断和禁止一条中断线两种,禁止所有中断可以使用local_irq_save和local_irq_restore函数,禁止一个中断线可以使用disable_irq函数。

查看使用的中断号可以用cat /proc/interrupts得到。中断系统的状态可以通过几个函数获得,irqs_disabled函数查看本地中断传递是否被禁止;in_interrupt函数查看是否在中断上下文;in_irq查看是否是当前正在执行中断处理程序。

为什么要引入下半部呢?因为中断处理流程的上半部有一些局限:其一,中断以异步方式执行,它有可能打断其他重要代码,为了避免打断时间过长,中断处理程序应该执行的快些。其二,如果当前有一个中断处理程序正在执行,需要做一个禁止其他中断的操作,禁止中断后硬件和操作系统无法通信了,所以也有中断处理快点。其三,中断处理往往需要对硬件操作,所以也需要快点。其四,中断处理程序不在进程上下文,不能睡眠,这也限制了它们所做的事。

下半部的任务就是执行与中断处理密切相关中断处理程序本身不执行的工作。中断处理程序往往需要通过操作硬件对中断的到达进行确认,有时它还会从硬件拷贝数据。我们将对时间非常敏感,与硬件相关,要保证不被其他中断打断的事情放在中断处理程序中执行,其他任务考虑放到下半部执行。下半部执行的关键在于当它们运行的时候,运行相应所有的中断。

在2.6内核版本中,下半部实现的机制包括软中断、tasklet和工作队列。

软中断是一组静态定义的下半部接口,有32个,可以在所以处理器上同时执行,软中断必须在编译期间就进行静态注册,这里的软中断不是系统调用中提到的软中断。软中断在下面地方会被执行:其一,一个硬件中断代码处返回时。其二,在ksoftirqd内核线程中(每个处理器都有一组辅助处理软中断和tasklet的内核线程,当大量软中断出现时,内核会唤醒一组内核线程来处理这些负载,这个内核线程就是ksoftirqd线程)。其三,在那些显式检查和执行处待理的软件中断的代码中,如网络子系统。只有网络和SCSI子系统直接使用软中断,对于世界要求严格并能自己高效地完成加锁工作的应用,软中断是正确的选择。

tasklet有两种类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,前者优先级更高。所有的tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。当一个tasklet被调用时,内核就会唤醒这两个软中断中的一个,随后该软中断会被特定的函数处理,执行所有已调度的tasklet,这个函数保证同一时间里只有一个给定类别的tasklet会被执行。Tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或其他什么阻塞式的函数,同时由于tasklet运行时允许响应中断,所以你必须做好预防(如屏蔽中断然后获得一个锁),另外,你可以调用tasklet_disable函数禁止某个指定的tasklet,你也可以使用tasklet_kill从挂起队列中去掉一个tasklet。

DECLARE_TASKLET(test_tasklet,test_tasklet_func,0); //定义

void test_tasklet_func(void)          //处理函数

{

 printk("tasklet is executing!\n");

}

tasklet_schedule(&test_tasklet);      //调度

 

工作队列可以把工作推后,交给一个内核线程去执行,这个下半部分总是会在进程上下文中执行,工作队列运行重新调度甚至是睡眠,它是唯一能在进程上下文中运行的下半部实现机制,也只有它可以睡眠。尽管操作处理函数运行在进程上下文,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射,通常只有发生系统调用时,内核才会代表用户空间的进行运行。

工作队列子系统提供了一个缺省的工作者线程,我们只要把需要推后执行的任务交给特定的通用线程就好了,缺省的工作者线程叫events/n,我们一般使用这个缺省的工作者线程,但是如果你需要在工作者线程中执行大量的处理操作,创建自己的工作者线程就更好了。系统的每个CPU都会有一个工作者线程,每个工作者线程都是由struct cpu_workqueue_struct结构体表示,而struct workqueue_struct则表示给定类型(即同类型)的所有工作者线程。

INIT_WORK(&button_dev->work, gpio_keys_report_event);  //定义

static void gpio_keys_report_event(struct work_struct *work)  //处理函数

{   

      key_values[0] = '0' ;    //清除按键标识

      input_report_key(channel, BTN_0, !!ev_press);  //向input子系统报告按键事件

      input_sync(channel);                                //同步操作

      ev_press = 0;                          //清除按键值

}

schedule_work(&button_dev->work);  //调度工作队列处理函数

cancel_work_sync(&button_dev->work);      //删除工作队列

tasklet基于软件中断,而工作队列是靠内核线程实现的,如果你有休眠的需要,那么你使用工作队列,否则最好使用tasklet机制。为了保证共享数据,一般先得到一个锁,然后使用local_bh_disable函数禁止下半部,但local_bh_disable函数并不能禁止工作队列的执行,因为工作队列不涉及异步执行,但是由于软中断和tasklet是异步发送的,所以内核必须禁止它们。

相关内容