日期:2014-05-16  浏览次数:20740 次

LINUX设备驱动(十七)---中断(二)

LINUX设备驱动(十七)---中断(二)
2010年10月09日
  顶半部和底半部
  Linux系统通过将中断处理例程分成两部分来解决这个问题。称为"顶半部"的部分,是实际响应中断的例程,也就是用request_irq注册的中断例程;而所谓的"底半部"是一个被顶半部调度,并在稍后更安全的时间内执行的例程。顶半部处理例程和底半部处理例程之间最大的不同,就是当底半部处理例程执行时,所有的中断都是打开的---这就是所谓的在更安全的时间内运行。典型的情况是顶半部保存设备的数据到一个设备特定的换从去并调度它的底半部,然后退出。顶半部所做的操作是非常快的,然后,底半部执行其他必要的工作,例如:唤醒进程、启动另外的I/O操作等等。这种方式允许在底半部工作时间内,顶半部还可以继续为新的中断服务。
  linux内核有两种不同的机制可以用来实现底半部处理。
  1.tasklet通常是首选的机制,因为这种机制快,但是所有的tasklet代 码必须是原子的。
  2.工作队列,它可以具有较高的延时,但允许休眠。
  顶半部执行得都很快,因为它仅保存当前时间并调度底半部。然后底半部负责这些时间的编码,并唤醒可能等待数据的任何用户进程。
  这里有个帖子可以参考:http://judicious.bokee.com/5864176.html
  tasklet
  tasklet是一个可以在由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。他们可以被多次调度运行,但tasklet的调度不会累积;也就是说,实际只会运行一次,即使在激活tasklet的运行之前重复请求该tasklet的运行 也是一样,不会有同一tasklet的多个实例并行的运行,因为他们只运行一次,但是tasklet可以与其他的taklet并行地运行在对称多处理器系统上。这样驱动程序有多个tasklet,他们必须使用某种锁机制来避免彼此间的冲突。
  tasklet可以确保和第一次调度他们的函数运行在同样的CPU上,这样,因为tasklet在中断处理例程结束前并不会开始运行,所以此时的中断处理例程是安全的。但是在tasklet运行时,当然可以有其他的中断发生,因此tasklet和中断处理例程之间的锁还是必须的。
  实例:
  必须使用宏DECLEAR_TASKLET声明tasklet:
  DECLEAR_TASKLET(name,function,data);
  name是给tasklet起的名字,function是执行tasklet时调用的函数(它带有一个unsigned long型的参数并且返回void),data是一个用来传递给tasklet函数的unsigned long类型参数。
  驱动程序中的例子:
  void short_do_tasklet(unsigned long) //声明tasklet的处理函数
  DECLEAR_TASKLET(short_tasklet,short_do_tasklet,0);
  函数tasklet_schedule用来调度一个tasklet的运行。如果指定tasklet=1选项装载short,它就会安装一个不同的中断处理例程,这个例程保存数据并如下调度tasklet:
  irqreturn_t  short_tl_interrupt(int irq,void *dev_id,struct pt_regs *regs)
  {
  do_gettimeofday((struct timeval *)tv_head);  //强制转换一下以免出现"易失"性错误
  short_incr_tv(&tv_head);
  tasklet_schedule(&short_tasklet);
  short_wq_count++;  //记录中断产生次数
  return IRQ_HANDLED;
  }
  实际的tasklet例程,即short_do_tasklet会在系统方便时得到执行,就像先前提到,这个例程执行中断处理的大多数任务。
  void short_do_tasklet(unsigned long unused)
  {
  int savecount = short_wq_count,written;
  short_wq_count = 0;  //已经从队列中移除
  /*首先将调用此bh之前发生的中断数量写入
  written = sprintf((char *)short_head,"bh after %6i\n" ,savecount);
  short_incr_bp(&short_head,written);
  /*底半部读取有顶半部填充的tv数组,并向循环文本缓冲区中打印信息,而缓冲区的数据则由读进程获得
  /*然后写入时间值,每次写入16字节,所以它与PAGE_SIZE对齐
  do{
  written = sprintf((char *)short_head,"%08u.%06u\n",(int)(tv_tail->tv_sec % 10000000),(int)(tv_tail->tv_usec));
  short_incr_bp(&short_head,written);
  short_incr_tv(&tv_tail);
  }while(tv_tail != tv_head);
  wake_up_interrupt(&short_queue);
  }
  工作队列
  工作队列会在将来的某个时间,在某个特殊的工作者进程上下文中调用一个函数。因为工作队列函数运行在进程上下文中,因此可以进行休眠。但是我们不能从工作队列向用户空间复制数据(需要借助一些高级的技巧),要知道,工作者进程无法访问其他任何进程的地址空间。
  如果我们的驱动程序具有特殊的延迟需求(或者可能在工作队列函数中长时间休眠),则应创建我们自己的工作队列。我们需要一个work_struct结构。
  static struct work_struct short_wq;
  下面这行出现在short_init中
  INIT_WORK(&short_wq,(void (*)(void *))short_do_tasklet,NULL);
  在使用工作队列时, short构造了另一个中断处理例程
  irqreturn_t short_wq_interrupt(int irq,void *dev_id,struct pt_regs *regs)
  {
  do_gettimeofday((struct timeval *)tv_head);
  short_incr_tv(&tv_head);
  /*排序bh。不必关心多次调度的情况
  schedule_work(&short_wq);
  short_wq_count++;
  return IRQ_HANDLED;
  }
  注意:工作队列和等待队列是两码事。
  中断共享:linux内核支持所有总线的中断共享。 安装共享的处理例程
  就像普通非共享的中断一样,共享的中断都是通过request_irq安装的,不同在于:
  1。请求中断时,必须制定flags参数中的SA_SHIRQ位。
  2.dev_id参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但dev_id不能设置成NULL。
  内核为为每个中断维护一个共享处理例程的列表,这些处理例程的dev_id各不相同,就像是设备的签名。如果两个设备都注册NULL作为他们的签名,那么卸载的时候引起混淆,当中断到达时造成内核出现OOPS消息。由于这个原因,在注册共享中断时如果传递了NULL的dev_id,现代的内核就会给出警告。当满足下面条件之一时,request_irq就会成功:
  1.中断信号线空闲。
  2.任何已经注册了该中断信号线的处理例程也标示了IRQ是共享的。
  无论何时,当两个或则更多的驱动程序共享一根信号线,而硬件又通过这根信号线中断处理器时,内核会调用每一个为这个中断注册的处理例程,并将它们自己的dev_id传回去。因此,一个共享的处理例程必须能够识别属于自己的中断,并且在自己的设备没有被中断的时候迅速推出,返回IRQ_NONE。(也即只有真正产生中断的设备处理例程会被执行)