中断上下文和进程上下文
设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。但是这个良好的愿望往往与现实并不吻合。 在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。如下图描述了Linux内核的中断 处理机制。为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味 着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。现在,中断处理工作的重心就落在了底半部 的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部 往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。其实上面这一段大致说明一个问题,那就是:中断要尽可能耗时比较短,尽快恢复系统正常调试,所以把中断触发、中断执行分开,也就是所说的“上半部分(中断触发)、底半部(中断执行)”,其实就是我们后面说的中断上下文。下半部分一般有tasklet、工作队列实现,触摸屏中中断实现以工作队列形式实现的。
中断上下文
进程上下文
- 一般的进程运行在用户态,如果这个进程进行了系统调用,那么此时用户空间中的程序就进入了内核空间,并且称内核代表该进程运行于内核空间中。由于用户空间和内核空间具有不同的地址映射,并且用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。这样就产生了进程上下文。所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容。当内核需要切换到另一个进程时(上下文切换),它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态继续执行。上述所说的工作队列所要做的工作都交给工作者线程来处理,因此它可以表现出进程的一些特性,比如说可以睡眠等。
- 对于中断而言,是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。因此处于中断上下文的tasklet不会有睡眠这样的特性。
工作队列的作用
- 工作队列(work queue):是另外一种将中断的部分工作推后的一种方式,它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker
thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。因此,选择tasklet还是工作队列来完成下半部分应该不难选择。当
推后的那部分中断程序需要睡眠时,工作队列毫无疑问是你的最佳选择;否则,还是用tasklet吧。
下面分别介绍顶半部和底半部的实现机制
底半部机制有:tasklet、工作队列、软中断。
tasklet
- tasklet是中断处理下半部分最常用的一种方法,驱动程序一般先申请中断,在中断处理函数内完成中断上半部分的工作后调用tasklet。tasklet有如下特点:
1.tasklet只可以在一个CPU上同步地执行,不同的tasklet可以在不同地CPU上同步地执行。
2.tasklet的实现是建立在两个软件中断的基础之上的,即HI_SOFTIRQ和TASKLET_SOFTIRQ,本质上没有什么区别,只不过HI_SOFTIRQ的优先级更高一些
3.由于tasklet是在软中断上实现的,所以像软中断一样不能睡眠、不能阻塞,处理函数内不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。
4.一个 tasklet 能够被禁止并且之后被重新使能; 它不会执行直到它被使能的次数与被禁止的次数相同.
5.tasklet的串行化使tasklet函数不必是可重入的,因此简化了设备驱动程序开发者的工作。
6.每个cpu拥有一个tasklet_vec链表,具体是哪个cpu的tasklet_vec链表,是根据当前线程是运行在哪个cpu来决定的。
(1)我们需要定义tasklet机器处理器并将两者关联:例如
void my_tasklet_func(unsigned long);//定义一个处理函数
DELARE_TASKLET(my_tasklet,my_tasklet_func,data);
(2)调度:
/*在中断处理顶半部调用中断处理低半部函数*/
irqReturn_t xxx_irpInterrupt(int irp,void *dev_id)
{
//TODO
tasklet_sechedule(&my_tasklet) //使用此函数就能让CPU在适当的时候进行调度
//TODO
}
/*设备驱动模块的加载函数*/
int __init xxx_init(void)
{
//TODO
/*申请中断号*/
result = request_irq(xxx_irq, xxx_irpInterrupt, IRQF_DISABLED,"xxx",NULL);
//TODO
return IRQ_HANDLED;
}
/*设备驱动模块的卸载函数*/
void __exit xxx_exit(void)
{
//TODO
//释放中断
free_irq(xxx_irq,xxx_irqInterrupt);
//TODO
}
底半部实现方法之二---工作队列
使用方法和tasklet类似
相关操作:
struct work_struct my_wq; /定义一个工作队列/
void my_wq_func(unsigned long);/定义一个处理函数/
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定
INIT_WORK(&my_wq,(void ()(void ))my_wq_func,NULL);
/初始化工作队列并将其与处理函数绑定/
schedule_work(&my_wq);/调度工作队列执行/
使用模板:
/*定义工作队列和关联函数*/
struct work_struct xxx_wq(unsigned long);
/*中断处理底半部*/
void xxx_do_work(unsigned long){...}
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
//TODO
schedule_work(&my_wq);
}
/*设备驱动模块加载函数*/
int xxx_init(void)
{
//TODO
//申请中断
//原型:int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
request = request_irq(xxx_wq,xxx_interrupt,IRQF_DISABLED,"XXX",NULL);
...
// 初始化工作队列
INIT_WORK(&my_wq,(void(*)(void *))xxx_do_work,NULL )
}
/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
//TODO
//释放中断
free_irq(xxx_irq,xxx_interrupt);
//TODO
}
中断共享
中断共享是指多个设备共享一根中断线的情况
中断共享的使用方法:
- 在申请中断时,使用IRQF_SHARED标识
- 在中断到来时,会遍历共享此中断的所有中断处理程序,直到某一个函数返回
IRQ_HANDLED,在中断处理程序顶半部中,应迅速根据硬件寄存器中的信息参照dev_id参数判断是否为本设备的中断,若不是立即返回IR1_NONE
共享中断编程模板