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。(也即只有真正产生中断的设备处理例程会被执行)
共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。
释放处理例程是通过执行free_irq来实现的。这里dev_id参数被用来从该中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须唯一的原因。
void free_irq(unsigned long flags,void *dev_id);
使用共享处理例程的驱动程序需要注意:不能使用enable_irq和disable_irq。如果使用了,共享中断信号线的其他设备就无法工作了;即使在很短的时间内禁用中断,也会因为这种延迟而为设备和其他用户带来问题。驱动程序并不独占IRQ,所以它的行为必须比独占IRQ更具"社会化"。
运行处理例程:当内核受到中断时,所有已注册的处理例程都会被调用。一个共享中断处理例程必须能够将要处理的中断和其他设备产生的中断区分开来。
当装载short模块时,需要指定shared = 1选项。
irqreturn_t short_sh_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
int value,written;
struct timeval tv;
value = inb(short_base); /*因为并口没有"interrupt-pending"位可以检查,所以用ACK位。如果该位为高,则执行中断例程 。如果该位为低,则表示该设备没有中断,中断例程返回IRQ_NONE,不进行后续操作。
if(!(value & 0x80))
return IRQ_NONE;
outb(0x70,short_base);//这步清中断,使得例程可以继续相应中断。
/ *其余如前边*/
....
/proc接口和共享的中断
在系统上安装共享的中断处理例程不会对/proc/stat造成影响,但是从/proc/interrupt中会表示出来
中断驱动的I/O
如果与驱动程序管理的硬件之间的数据传输因为某种原因被延迟的话,驱动程序作者就应该实现缓冲。数据缓冲区有助于将数据的传送和接受与系统调用write和read分离开来,从而提高系统性能。
一个好的缓存机制需采用中断驱动的 I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
要正确进行中断驱动的数据传输,则要求硬件应该能够按照下满的语义来产生中断:
1.对于输入来说,当新的数据已经到达并且处理器准备好就收它时,设备就中断处理器,实际执行的动作取决于设备使用的是I/O端口、内存映射、还是DMA。
2.对于输出来说(针对CPU来说),当设备准备好接受新数据或则对成功的数据传输进行应答时,就要发出中断。内存映射和具有DMA能力的设备,通常通过产生中断来通知系统他们对缓冲区的处理已结束。
在这章的例子中融合以前学过的大部分知识,自己也都分析了下,弄清楚以前一些不是很明白的地方,这里列出代码,备忘:
struct IO_irq_dev *IO_irq_devices;/* allocated in scull_init_module */
static atomic_t IO_irq_available = ATOMIC_INIT(1);
static spinlock_t IO_irq_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_WAIT_QUEUE_HEAD(IO_irq_wait);
static unsigned long prevjiffies = 0;
static unsigned int prevkey = 0;
static struct IO_irq_key key1;
static struct IO_irq_key key2;
static struct IO_irq_key key3;
static struct IO_irq_key key4;
struct workqueue_struct *tekkamanwork;
static struct delayed_work irq_work_delay;
static struct work_struct irq_work;
static struct tasklet_struct keytask;
struct kfifo *tekkamanfifo;
static spinlock_t tekkamanlock= SPIN_LOCK_UNLOCKED;
static unsigned char *tekkamantmp;
static unsigned char *tekkamanbuf ; 这是开头定义的全局变量;
1、static DECLARE_WAIT_QUEUE_HEAD(IO_irq_wait);定义等待队列
wait_event_interruptible (IO_irq_wait, atomic_read (&IO_irq_available);
2.struct workqueue_struct *tekkamanwork; 定义工作队列
tekkamanwork = create_workqueue("tekkamanwork"); 初始化
result =queue_delayed_work(tekkamanwork , &irq_work_delay, DELAY))!=1 这里说明下这是实现后半部的工作队列方式,安排工作队列的方法有以下几种:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);
schedule_work(struct work_struct *work);
3.static struct delayed_work irq_work_delay;
static struct work_struct irq_work;
定义了两种工作队列,第一个为延时形式。
result = queue_delayed_work(...,&irq_work_delay,DELAY);
result = schedule_work(&irq_work)
两种调度方式
4.注意这几个结构的定义方式:
工作队列: struct workqueue_struct
struct work_struct
struct delay_work
这三种形式的不同和运用方法。
具体可以参考代码
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。(也即只有真正产生中断的设备处理例程会被执行)
共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。
释放处理例程是通过执行free_irq来实现的。这里dev_id参数被用来从该中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须唯一的原因。
void free_irq(unsigned long flags,void *dev_id);
使用共享处理例程的驱动程序需要注意:不能使用enable_irq和disable_irq。如果使用了,共享中断信号线的其他设备就无法工作了;即使在很短的时间内禁用中断,也会因为这种延迟而为设备和其他用户带来问题。驱动程序并不独占IRQ,所以它的行为必须比独占IRQ更具"社会化"。
运行处理例程:当内核受到中断时,所有已注册的处理例程都会被调用。一个共享中断处理例程必须能够将要处理的中断和其他设备产生的中断区分开来。
当装载short模块时,需要指定shared = 1选项。
irqreturn_t short_sh_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
int value,written;
struct timeval tv;
value = inb(short_base); /*因为并口没有"interrupt-pending"位可以检查,所以用ACK位。如果该位为高,则执行中断例程 。如果该位为低,则表示该设备没有中断,中断例程返回IRQ_NONE,不进行后续操作。
if(!(value & 0x80))
return IRQ_NONE;
outb(0x70,short_base);//这步清中断,使得例程可以继续相应中断。
/ *其余如前边*/
....
/proc接口和共享的中断
在系统上安装共享的中断处理例程不会对/proc/stat造成影响,但是从/proc/interrupt中会表示出来
中断驱动的I/O
如果与驱动程序管理的硬件之间的数据传输因为某种原因被延迟的话,驱动程序作者就应该实现缓冲。数据缓冲区有助于将数据的传送和接受与系统调用write和read分离开来,从而提高系统性能。
一个好的缓存机制需采用中断驱动的 I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
要正确进行中断驱动的数据传输,则要求硬件应该能够按照下满的语义来产生中断:
1.对于输入来说,当新的数据已经到达并且处理器准备好就收它时,设备就中断处理器,实际执行的动作取决于设备使用的是I/O端口、内存映射、还是DMA。
2.对于输出来说(针对CPU来说),当设备准备好接受新数据或则对成功的数据传输进行应答时,就要发出中断。内存映射和具有DMA能力的设备,通常通过产生中断来通知系统他们对缓冲区的处理已结束。
在这章的例子中融合以前学过的大部分知识,自己也都分析了下,弄清楚以前一些不是很明白的地方,这里列出代码,备忘:
struct IO_irq_dev *IO_irq_devices;/* allocated in scull_init_module */
static atomic_t IO_irq_available = ATOMIC_INIT(1);
static spinlock_t IO_irq_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_WAIT_QUEUE_HEAD(IO_irq_wait);
static unsigned long prevjiffies = 0;
static unsigned int prevkey = 0;
static struct IO_irq_key key1;
static struct IO_irq_key key2;
static struct IO_irq_key key3;
static struct IO_irq_key key4;
struct workqueue_struct *tekkamanwork;
static struct delayed_work irq_work_delay;
static struct work_struct irq_work;
static struct tasklet_struct keytask;
struct kfifo *tekkamanfifo;
static spinlock_t tekkamanlock= SPIN_LOCK_UNLOCKED;
static unsigned char *tekkamantmp;
static unsigned char *tekkamanbuf ; 这是开头定义的全局变量;
1、static DECLARE_WAIT_QUEUE_HEAD(IO_irq_wait);定义等待队列
wait_event_interruptible (IO_irq_wait, atomic_read (&IO_irq_available);
2.struct workqueue_struct *tekkamanwork; 定义工作队列
tekkamanwork = create_workqueue("tekkamanwork"); 初始化
result =queue_delayed_work(tekkamanwork , &irq_work_delay, DELAY))!=1 这里说明下这是实现后半部的工作队列方式,安排工作队列的方法有以下几种:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);
schedule_work(struct work_struct *work);
3.static struct delayed_work irq_work_delay;
static struct work_struct irq_work;
定义了两种工作队列,第一个为延时形式。
result = queue_delayed_work(...,&irq_work_delay,DELAY);
result = schedule_work(&irq_work)
两种调度方式
4.注意这几个结构的定义方式:
工作队列: struct workqueue_struct
struct work_struct
struct delay_work
这三种形式的不同和运用方法。
具体可以参考代码