HardBirch

中断处理与【irqsoft】、【tasklet】和【work queue】使用实例

时间:10-04-15 栏目:系统技术篇 作者:鲁智森也有文化 评论:0 点击: 2,256 次

中断处理
由于I/O操作的不确定因素,以及处理器和I/O设备之间速度的不匹配,设备往往通过某种硬件信号异步地唤起处理器的注意。这些硬件信号就是所谓的中断。每个中断设备都被分配给一个相关的标识符,被称为中断请求(IRQ)号。当处理器检测到某一IRQ号对应的中断产生时,它将停止它现在的工作,并引用该IRQ所对应的中断服务例程(ISR)。中断处理函数ISR在中断上下文执行。
中断上下文
ISR是与硬件交互的非常重要的代码片段。它们被给予了立即执行的特权,以便最大化系统的性能。不过,如果ISR执行过慢、负载太重的化,就违背了自身的设计哲学。贵宾都被给予了优惠待遇,但是,尽量减少由此造成的对公众的不便也是他们的义务。为了对粗暴打断当前执行线程的行为进行补偿,ISR不得不礼貌地执行于受限制的环境下,即所谓的中断上下文(或原子上下文)。
下面给出了中断上下文可为和不可为事项的列表:
1.如果你的中断上下文进入睡眠,它是一项应该被处以监禁的罪行。中断处理函数不能通过调用schedule_timeout()等睡眠函数放弃处理器,在中断处理函数中调用一个内核API之前,应该仔细分析它以确保其内部不会触发阻塞等待。例如,input_register_device()表面上看起来没有问题,但是它内部以GFP_KERNEL为参数调用了kmalloc()。从第2章《内核一瞥》可以看出,用这种方式调用kmalloc()的话,如果系统的空闲内存低于某门限,kmalloc()将睡眠等待swapper释放内存。
2.为了在中断处理函数中保护临界区,你不能使用互斥体,因为它们也许导致睡眠。应该使用自旋锁代替互斥体,但是一定要记住的是只有真正需要的时候才采用它。
3.中断处理函数不能与用户空间直接交互数据,因为它们经由进程上下文与用户空间建立连接。这也是为什么中断处理函数不能睡眠的第2个理由:调度器工作于进程之间,如果中断处理函数睡眠并被调度出去,它们怎么返回到运行队列呢?
4.中断处理函数一方面需要快速地出来,另一方面又需要完成它的工作。为了规避这种冲突,中断处理函数通常被分成2个部分。瘦小的顶半部标志一个响应以宣称它已经服务了该中断,而重大的工作负载都被丢给了肥胖的底半部。底半部的执行被延后,在其执行环境中,所有的中断都是使能的。在讨论softirqtasklet的时候,你将学习到真也难怪开发底半部。
5.中断处理函数不必是可重用的。当某中断被执行的时候,在它返回之前,相应的IRQ都被禁止了。因此,与进程上下文代码不同的是,同一中断处理函数的不同实例不可能同时运行在多个处理器上。
6.中断处理函数可以被更高优先级IRQ的中断处理函数打断。如果你请求内核将你的中断处理函数作为快中断处理的话,此类中断嵌套将被禁止。快中断服务函数运行的时候,本处理器上的所有中断都会被禁止。在禁止中断或将你的中断标识为快中断之前,请意识到中断屏蔽对系统性能的坏处。中断屏蔽的时间越长,中断延迟就会更长,或者说已经被产生的中断得到服务的延迟就会越久。中断延迟与系统真实的响应时间成反比。
函数中可以检查in_interrupt()的返回值以查看自身是否位于中断上下文。
与外部硬件产生的异步中断不一样,也存在同步到达的中断。同步中断意味着它们不会不期而遇,它们由处理器本身执行某指令而产生。外部中断和同步中断在内核中使用相同的机制处理。
同步中断的例子包括:
1)异常,被用于报告严重的运行时错误;
2)软中断,如int 0x80指令,被用户实现x86体系结构上的系统调用。
分配IRQ
设备驱动必须将它们的IRQ号与一个中断处理函数连接。因此,它们需要知道它们正在驱动的设备的IRQ号。IRQ的分配可以很直接,也可能需要复杂的探测过程。在PC体系结构中,例如,定时器中断被分配了IRQ 0RTC中断也是IRQ 8。现代的中断技术(如PCI)足够强大,它能够响应对IRQ的查询(系统启动过程中由BIOS分配),PCI驱动能够访问设备配置空间的相应区域并获得IRQ。对于较老的设备,如基于工业标准体系结构(ISA)的卡而言,驱动也许不得不利用特定硬件的知识以探测和解析IRQ
通过/proc/interrupts可以查看系统中活动的IRQ的列表。
设备实例:辊轮
现在你已经学习了中断处理的基本知识,现在我们来实现一个辊轮设备实例的中断处理。在一些手机和PDA上能找到辊轮,它支持3种动作(顺时针旋转,逆时针旋转和按键),可便利菜单导航。本例辊轮中的任何运行都会向处理器产生IRQ 7。通用目的I/OGPIO)端口D的低3位与辊轮设备连接。这些引脚上产生的波形与图4.3中不同的辊轮运动一致。中断处理函数的工作是通过查看端口DGPIO数据寄存器解析出辊轮的运动。
 
 

驱动必须首先请求IRQ并将一个中断处理函数与其绑定:
#define ROLLER_IRQ  7
static irqreturn_t roller_interrupt(int irq, void *dev_id);
 
 
if (request_irq(ROLLER_IRQ, roller_interrupt, IRQF_DISABLED |
                IRQF_TRIGGER_RISING, "roll", NULL)) {
  printk(KERN_ERR "Roll: Can't register IRQ %d/n", ROLLER_IRQ);
  return -EIO;
}
我们看一下传递给request_irq()的参数,本例中没有查询或探测IRQ号,而是直接硬编码为ROLLER_IRQ。第2个参数roller_interrupt()是中断处理函数。中断处理函数的原型的返回值类型为irqreturn_t,如果中断处理成功,则返回IRQ_HANDLED,否则,返回IRQ_NONE。对于PCII/O而言,该返回值的意义更重要,因为多个设备可能共享同一IRQ
IRQF_DISABLED标志意味着这个中断处理为快中断,因此,在调用该处理函数的时候,内核将禁止所有的中断。IRQF_TRIGGER_RISING暗示辊轮将在中断线上产生一个上升沿以发出中断。换句话说,辊轮是一个边沿触发的设备。有一些设备是电平触发的,在CPU服务其中断之前,它一直将中断线保持在一个电平上。使用IRQF_TRIGGER_HIGHIRQF_TRIGGER_LOW可以标识一个中断为高/低电平触发。该参数其他的可能值包括IRQF_SAMPLE_RANDOM(第5章《字符设备驱动》的《伪字符设备驱动》一节会用到)、IRQF_SHARED(定义这个IRQ被多个设备共享)。
下一个参数"roll",用于标识这个设备,在/proc/interrupts等文件中也会利用它产生数据。最后一个参数(本例中为NULL),仅在共享中断的时候有用,用于区分共享同一IRQ线的每个设备。
2.6.19内核开始,中断处理接口发生了一些变化。以前的中断处理函数的第3个参数为struct pt_regs *,它指向存放CPU寄存器的地址,在2.6.19中已经移除。另外,IRQF_xxx型中断标志取代了SA_xxx型中断标志。例如,在较早的内核中,你应该使用SA_INTERRUPT而不是IRQF_DISABLED来将中断处理标识为快中断处理。
驱动初始化的时候申请IRQ并不是太好,因为这样会导致甚至设备未被使用的时候,有价值的资源也被占用。因此,设备驱动通常在设备被应用打开的时候申请IRQ。类似地,IRQ也在应用关闭设备的时候释放IRQ,而不是在退出驱动模块的时候进行。使用下面的方法可以释放一个IRQ
free_irq(int irq, void *dev_id);
清单4.1给出了辊轮中断处理的实现。
roller_interrupt()2个参数,IRQ和设备标识符(传递给request_irq()的最后一个参数)。请对照图4.3查看清单4.1
    清单4.1 辊轮中断处理
spinlock_t roller_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_WAIT_QUEUE_HEAD(roller_poll);
 
static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  int i, PA_t, PA_delta_t, movement = 0;
 
  /* Get the waveforms from bits 0, 1 and 2
     of Port D as shown in Figure 4.3 */
  PA_t = PORTD & 0x07;
 
  /* Wait until the state of the pins change.
     (Add some timeout to the loop) */
  for (i=0; (PA_t==PA_delta_t); i++){
    PA_delta_t = PORTD & 0x07;
  }
 
  movement = determine_movement(PA_t, PA_delta_t); /* See below */
 
  spin_lock(&roller_lock);
 
  /* Store the wheel movement in a buffer for
     later access by the read()/poll() entry points */
  store_movements(movement);
 
  spin_unlock(&roller_lock);
 
  /* Wake up the poll entry point that might have
     gone to sleep, waiting for a wheel movement */
  wake_up_interruptible(&roller_poll);
 
  return IRQ_HANDLED;
}
int
determine_movement(int PA_t, int PA_delta_t)
{
  switch (PA_t){
    case 0:
      switch (PA_delta_t){
      case 1:
        movement = ANTICLOCKWISE;
        break;
      case 2:
        movement = CLOCKWISE;
        break;
      case 4:
        movement = KEYPRESSED;
        break;
      }
      break;
    case 1:
      switch (PA_delta_t){
      case 3:
        movement = ANTICLOCKWISE;
        break;
      case 0:
        movement = CLOCKWISE;
        break;
      }
      break;
    case 2:
      switch (PA_delta_t){
      case 0:
        movement = ANTICLOCKWISE;
        break;
      case 3:
        movement = CLOCKWISE;
        break;
      }
      break;
    case 3:
      switch (PA_delta_t){
      case 2:
        movement = ANTICLOCKWISE;
        break;
      case 1:
        movement = CLOCKWISE;
        break;
      }
    case 4:
      movement = KEYPRESSED;
      break;
  }
}
驱动入口点(read()poll())尾随roller_interrupt()进行操作。例如,当中断处理函数解析完一个辊轮运动后,它唤醒正在等待的poll()线程(可能已经因为X Windows等应用发起的select()系统调用而睡眠)。请在学习完第5章字符设备驱动的知识后,重新查看清单4.1并实现辊轮设备的完整驱动。
7章《输入设备驱动》的清单7.3利用了内核的输入接口,将辊轮转化为辊鼠标。
在本节结束前,我们介绍一下使能和禁止特定IRQ的函数。enable_irq(ROLLER_IRQ)用于使能辊轮运动的中断发生,disable_irq(ROLLER_IRQ)则进行相反的工作。disable_irq_nosync(ROLLER_IRQ)禁止辊轮中断,并且不等待任何正在执行的roller_interrupt()实例的返回。disable_irq()的非同步变体执行地更快,但是可能导致潜在的竞态。只有在你确认没有竞争的尽快下,才可以这样使用。drivers/ide/ide-io.c由一个使用disable_irq_nosync()的例子,在初始化过程中,它阻止了一些中断,因为一些系统中可能在此方面存在问题。
软中断(SoftirqTasklet
正如以前讨论的那样,中断处理有2个矛盾的要求:它们需要完成大量的设备数据处理,但是又不得不尽可能快地退出。为了摆脱这一困境,中断处理过程被分成2部分:一个急切的且抢占的与硬件交互的顶半部,和一个在所有中断都使能情况下并非十分急切的处理大量工作的底半部。如顶半部不一样,底半部是同步的,因为内核决定了它什么时候会执行它们。如下机制都可用于内核中延后一个工作到底半部执行:softirqtasklet和工作队列(work queue)。
Softirq是一种基本的底半部机制,有较强的加锁需求。仅仅在一些对性能敏感的子系统(如网络层、SCSI层和内核定时器)中才会使用softirqTasklet建立在softirq之上,使用起来更简单。除非有严格的可扩展性和速度要求,都建议使用TaskletSoftirqTasklet的主要不同是前者是可重用的而后者则不需要。Softirq的不同实例可运行在不同的处理器上,而tasklet则不允许。
为了论证SoftirqTasklet的用法,假定前例中的辊轮由存在由于运动部件导致的潜在问题(如旋轮偶尔被卡住)从而导致不同于spec的波形。一个被卡住的旋轮会不停地产生假的中断,并可能使系统冻结。为了解决这个问题,可以捕获波形,进行一些分析,并在发现卡住的情况下动态地从中断模式切换到轮询模式,如果旋轮恢复正常,软件也恢复到正常模式。我们在中断处理函数中捕获波形,并在底半部分析它。清单4.24.3分别用SoftirqTasklet对此进行了实现。
它们都是清单4.1的简化的变体,它们将中断处理简化为2个函数:从GPIO端口D捕获波形的roller_capture()和对波形进行算术分析并按需切换到轮询模式的roller_analyze()
清单4.2 使用Softirq 分担中断处理的负载
void __init
roller_init()
{
  /* ... */
 
  /* Open the softirq. Add an entry for ROLLER_SOFT_IRQ in
     the enum list in include/linux/interrupt.h */
  open_softirq(ROLLER_SOFT_IRQ, roller_analyze, NULL);
}
 
 
/* The bottom half */
void
roller_analyze()
{
  /* Analyze the waveforms and switch to polled mode if required */
}
/* The interrupt handler */
static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  /* Capture the wave stream */
  roller_capture();
 
  /* Mark softirq as pending */
  raise_softirq(ROLLER_SOFT_IRQ);
 
  return IRQ_HANDLED;
}
为了定义一个softirq,你必须在include/linux/interrupt.h中静态地添加一个入口。你不能动态地定义softirqraise_softirq()用于宣布相应的softirq需要被执行。内核会在下一个可获得的机会里执行它。可能发生在退出硬中断处理函数的时候,也可能在ksoftirqd内核线程中。
清单4.3使用tasklet分担中断处理的负载
struct roller_device_struct { /* Device-specific structure */
  /* ... */
  struct tasklet_struct tsklt;
  /* ... */
}
 
void __init roller_init()
{
  struct roller_device_struct *dev_struct;
  /* ... */
 
  /* Initialize tasklet */
  tasklet_init(&dev_struct->tsklt, roller_analyze, dev);
}
 
 
/* The bottom half */
void
roller_analyze()
{
/* Analyze the waveforms and switch to
   polled mode if required */
}
/* The interrupt handler */
static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  struct roller_device_struct *dev_struct;
 
  /* Capture the wave stream */
  roller_capture();
 
  /* Mark tasklet as pending */
  tasklet_schedule(&dev_struct->tsklt);
 
  return IRQ_HANDLED;
}
tasklet_init()用于动态地初始化一个tasklet,该函数不会为tasklet_struct分配内存,相反地,你必须将已经分配好的地址传递给它。tasklet_schedule()用于宣布相应的tasklet需要被执行。和中断类似,内核提供了一系列用于控制在多处理器系统中tasklet执行状态的函数:
1tasklet_enable()使能tasklet
2tasklet_disable()禁止tasklet,并等待正在执行的tasklet退出;
你已经看到了中断处理函数和底半部的不同,但是,也有几个相似点。中断处理函数和tasklet都不需要可重用。而且,二者都不能睡眠。另外,中断处理函数、taskletsoftirq都不能被抢占。
工作队列是中断处理延后执行的第3种方式。它们在进程上下文执行,允许睡眠,因此可以使用mutex这类可能导致睡眠的函数。在前一章分析内核辅助接口的时候,我们已经讨论了工作队列。表4.1softirqtasklet和工作队列进行了对比分析。


清单4.4 使用workqueue分担中断处理的负载

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/interrupt.h>

#include <linux/workqueue.h>

 

static void intr_print(void* data)

{

    printk("/n%s/n", (char*)data);

}

 

static struct work_struct works;

static char list[20];

static int intr_init(void)

{

    strlcpy(list, "Hello world", 20);

    INIT_WORK(&works, (void*)intr_print, (void*)list);

    schedule_work(&works);

    return 0;

}

 

static void intr_exit(void)

{

    printk("intr_exit");

}

 

module_init(intr_init);

module_exit(intr_exit);

 

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Shakespeare");

 

 

 work queue例子如上所示

4.1 softirqtasklet和工作队列对比
 
Softirq
Tasklet
Work Queue
执行上下文
延后的工作,运行于中断上下文
延后的工作,运行于中断上下文
延后的工作,运行于进程上下文
可重用
可以在不同的CPU上同时运行
不能在不同的CPU上同时运行,但是不同的CPU可以运行不同的tasklet
可以在不同的CPU上同时运行
睡眠
不能睡眠
不能睡眠
可以睡眠
抢占
不能抢占/调度
不能抢占/调度
可以抢占/调度
易用性
不容易使用
容易使用
容易使用
何时使用
如果延后的工作不会睡眠,而且有严格的可扩展性或速度要求
如果延后的工作不会睡眠
如果延后的工作会睡眠

LKML正在进行一项去除tasklet的可行性的讨论。Tasklet比进程上下文的代码优先级更高,因此它们可能存在延迟问题。另外,你已经学习到,它们不允许睡眠,且只能在同一CPU上执行。因此,有人提议将现存的tasklet基于其场景随机应变地转换为softirq或工作队列。


 

中断处理与【irqsoft】、【tasklet】和【work queue】使用实例:等您坐沙发呢!

发表评论


QQ群互动

Linux系统与内核学习群:194051772

WP建站技术学习交流群:194062106

魔豆之路QR

魔豆的Linux内核之路

魔豆的Linux内核之路

优秀工程师当看优秀书籍

优秀程序员,要看优秀书!

赞助商广告

友荐云推荐