linux驱动之中断.docx
- 文档编号:23148000
- 上传时间:2023-05-08
- 格式:DOCX
- 页数:14
- 大小:26.52KB
linux驱动之中断.docx
《linux驱动之中断.docx》由会员分享,可在线阅读,更多相关《linux驱动之中断.docx(14页珍藏版)》请在冰豆网上搜索。
linux驱动之中断
Linux设备驱动程序学-中断处理
可以让设备在产生某个事件时通知处理器的方法就是中断。
一个“中断”仅是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。
Linux处理中断的方式非常类似在用户空间处理信号的方式。
大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。
本质上来讲,中断处理例程和其他的代码并行运行。
因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。
透彻地理解并发控制技术对中断来讲非常重要。
安装中断处理例程
内核维护了一个中断信号线的注册表,类似于I/O端口的注册表。
模块在使用中断前要先请求一个中断通道(或者IRQ中断请求),并在使用后释放它。
所用的函数声明在
intrequest_irq(unsignedintirq,
irqreturn_t(*handler)(int,void*,structpt_regs*),
unsignedlongflags,
constchar*dev_name,
void*dev_id);
voidfree_irq(unsignedintirq,void*dev_id);
request_irq的返回值:
0指示成功,或返回一个负的错误码,如-EBUSY表示另一个驱动已经占用了你所请求的中断线。
函数的参数如下:
unsignedintirq:
请求的中断号
irqreturn_t(*handler):
安装的处理函数指针。
unsignedlongflags:
一个与中断管理相关的位掩码选项。
constchar*dev_name:
传递给request_irq的字符串,用来在/proc/interrupts来显示中断的拥有者。
void*dev_id:
用于共享中断信号线的指针。
它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。
若中断没有被共享,dev_id可以设置为NULL,但推荐用它指向设备的数据结构。
flags中可以设置的位如下:
SA_INTERRUPT:
快速中断标志。
快速中断处理例程运行在当前处理器禁止中断的状态下。
SA_SHIRQ:
在设备间共享中断标志。
SA_SAMPLE_RANDOM:
该位表示产生的中断能对/dev/random和/dev/urandom使用的熵池(entropypool)有贡献。
读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。
若设备以真正随机的周期产生中断,就应当设置这个标志。
若设备中断是可预测的,这个标志不值得设置。
可能被攻击者影响的设备不应当设置这个标志。
更多信息看drivers/char/random.c的注释。
中断处理例程可在驱动初始化时或在设备第一次打开时安装。
推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。
这样调用free_irq的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。
但这种方式的缺点是必须为每个设备维护一个打开计数。
以下是中断申请的示例(并口):
if(short_irq>=0)
{
result=request_irq(short_irq,short_interrupt,
SA_INTERRUPT,"short",NULL);
if(result){
printk(KERN_INFO"short:
can'tgetassignedirq%i\n",
short_irq);
short_irq=-1;
}else{/*打开中断硬件的中断能力*/
outb(0x10,short_base+2);
}
}
i386和x86_64体系定义了一个函数来查询一个中断线是否可用:
intcan_request_irq(unsignedintirq,unsignedlongflags);/*当能够成功分配给定中断,则返回非零值。
但注意,在can_request_irq和request_irq的调用之间给定中断可能被占用*/
快速和慢速处理例程
快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。
在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。
在现代内核中,快速和慢速中断的区别已经消失,剩下的只有一个:
快速中断(使用SA_INTERRUPT)执行时禁止所有在当前处理器上的其他中断。
注意:
其他的处理器仍然能够处理中断。
除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.
x86中断处理内幕
这个描述是从2.6内核arch/i386/kernel/irq.c,arch/i386/kernel/apic.c,arch/i386/kernel/entry.S,arch/i386/kernel/i8259.c,和include/asm-i386/hw_irq.h中得出,尽管基本概念相同,硬件细节与其他平台上不同。
底层中断处理代码在汇编语言文件entry.S。
在所有情况下,这个代码将中断号压栈并且跳转到一个公共段,公共段会调用do_IRQ(在irq.c中定义)。
do_IRQ做的第一件事是应答中断以便中断控制器能够继续其他事情。
它接着获取给定IRQ号的一个自旋锁,阻止其他CPU处理这个IRQ,然后清除几个状态位(包括IRQ_WAITING)然后查找这个IRQ的处理例程。
若没有找到,什么也不做;释放自旋锁,处理任何待处理的软件中断,最后do_IRQ返回。
从中断中返回的最后一件事可能是一次处理器的重新调度。
IRQ的探测是通过为每个缺乏处理例程的IRQ设置IRQ_WAITING状态位来完成。
当中断发生,因为没有注册处理例程,do_IRQ清除这个位并且接着返回。
当probe_irq_off被一个函数调用,只需搜索没有设置IRQ_WAITING的IRQ。
/proc接口
当硬件中断到达处理器时,内核提供的一个内部计数器会递增,产生的中断报告显示在文件/proc/interrupts中。
这一方法可以用来检查设备是否按预期地工作。
此文件只显示当前已安装处理例程的中断的计数。
若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。
/proc/stat记录了几个关于系统活动的底层统计信息,包括(但不仅限于)自系统启动以来收到的中断数。
stat的每一行以一个字符串开始,是该行的关键词:
intr标志是中断计数。
第一个数是所有中断的总数,而其他每一个代表一个单独的中断线的计数,从中断0开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。
以上两个文件的一个不同是:
/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在
ARM的定义为:
#defineNR_IRQS 128
自动检测IRQ号
驱动初始化时最迫切的问题之一是决定设备要使用的IRQ线,驱动需要信息来正确安装处理例程。
自动检测中断号对驱动的可用性来说是一个基本需求。
有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:
if(short_irq<0)/*依靠使并口的端口号,确定中断*/
switch(short_base){
case0x378:
short_irq=7;break;
case0x278:
short_irq=2;break;
case0x3bc:
short_irq=5;break;
}
有的驱动允许用户在加载时覆盖默认值:
insmodxxxxx.koirq=x
当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。
但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。
这个工作技术上非常简单:
驱动告知设备产生中断并且观察发生了什么。
如果一切顺利,则只有一个中断信号线被激活。
尽管探测在理论上简单,但实现可能不简单。
有2种方法来进行探测中断:
调用内核定义的辅助函数和DIY探测。
(1)调用内核定义的辅助函数
Linux内核提供了一个底层设施来探测中断号,且只能在非共享中断模式下工作,它包括2个函数,在
unsignedlongprobe_irq_on(void);
/*这个函数返回一个未分配中断的位掩码。
驱动必须保留返回的位掩码,并在后面传递给probe_irq_off。
在调用probe_irq_on之后,驱动应当安排它的设备产生至少一次中断*/
intprobe_irq_off(unsignedlong);
/*在请求设备产生一个中断后,驱动调用这个函数,并将probe_irq_on返回的位掩码作为参数传递给probe_irq_off。
probe_irq_off返回在"probe_on"之后发生的中断号。
如果没有中断发生,返回0;如果产生了多次中断,probe_irq_off返回一个负值*/
程序员应当注意在调用probe_irq_on之后启用设备上的中断,并在调用probe_irq_off前禁用。
此外还必须记住在probe_irq_off之后服务设备中待处理的中断。
以下是LDD3中的并口示例代码,(并口的管脚9和10连接在一起,探测五次失败后放弃):
intcount=0;
do
{
unsignedlongmask;
mask=probe_irq_on();
outb_p(0x10,short_base+2);/*enablereporting*/
outb_p(0x00,short_base);/*clearthebit*/
outb_p(0xFF,short_base);/*setthebit:
interrupt!
*/
outb_p(0x00,short_base+2);/*disablereporting*/
udelay(5);/*giveitsometime*/
short_irq=probe_irq_off(mask);
if(short_irq==0){/*noneofthem?
*/
printk(KERN_INFO"short:
noirqreportedbyprobe\n");
short_irq=-1;
}
}while(short_irq<0&&count++<5);
if(short_irq<0)
printk("short:
probefailed%itimes,givingup\n",count);
最好只在模块初始化时探测中断线一次。
大部分体系定义了这两个函数(即便是空的)来简化设备驱动的移植。
(2)DIY探测
DIY探测与前面原理相同:
使能所有未使用的中断,接着等待并观察发生什么。
我们对设备的了解:
通常一个设备能够使用3或4个IRQ号中的一个来进行配置,只探测这些IRQ号使我们能不必测试所有可能的中断就探测到正确的IRQ号。
下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。
trials数组列出要尝试的中断,以0作为结尾标志;tried数组用来跟踪哪个中断号已经被这个驱动注册。
inttrials[]= {3,5,7,9,0};
inttried[]={0,0,0,0,0};
inti,count=0;
for(i=0;trials[i];i++)
tried[i]=request_irq(trials[i],short_probing,
SA_INTERRUPT,"shortprobe",NULL);
do
{
short_irq=0;/*nonegot,yet*/
outb_p(0x10,short_base+2);/*enable*/
outb_p(0x00,short_base);
outb_p(0xFF,short_base);/*togglethebit*/
outb_p(0x00,short_base+2);/*disable*/
udelay(5);/*giveitsometime*/
/*等待中断,若在这段时间有中断产生,handler会改变short_irq*/
/*thevaluehasbeensetbythehandler*/
if(short_irq==0){/*noneofthem?
*/
printk(KERN_INFO"short:
noirqreportedbyprobe\n");
}
}while(short_irq<=0&&count++<5);
/*endofloop,uninstallthehandler*/
for(i=0;trials[i];i++)
if(tried[i]==0)
free_irq(trials[i],NULL);
if(short_irq<0)
printk("short:
probefailed%itimes,givingup\n",count);
以下是handler的源码:
irqreturn_tshort_probing(intirq,void*dev_id,structpt_regs*regs)
{
if(short_irq==0)short_irq=irq;/*found*/
if(short_irq!
=irq)short_irq=-irq;/*ambiguous*/
returnIRQ_HANDLED;
}
若事先不知道"可能的"IRQ ,就需要探测所有空闲的中断,所以不得不从IRQ0探测到IRQNR_IRQS-1。
处理例程的参数及返回值
传递给一个中断处理例程的参数有:
intirq、void*dev_id和structpt_regs*regs。
intirq(中断号):
若要打印log消息时,是很有用。
void*dev_id:
一种用户数据类型(驱动程序可用的私有数据),传递给request_irq的void*参数,会在中断发生时作为参数传给处理例程。
我们通常传递一个指向设备数据结构的指针到dev_id中,这样一个管理若干相同设备的驱动在中断处理例程中不需要任何额外的代码,就可以找出哪个设备产生了当前的中断事件。
structpt_regs*regs很少用到。
中断处理例程的典型使用如下:
staticirqreturn_tsample_interrupt(intirq,void*dev_id,structpt_regs*regs)
{
structsample_dev*dev=dev_id;
/*now`dev'pointstotherighthardwareitem*/
/*....*/
}
和这个处理例程关联的打开代码如下:
staticvoidsample_open(structinode*inode,structfile*filp)
{
structsample_dev*dev=hwinfo+MINOR(inode->i_rdev);
request_irq(dev->irq,sample_interrupt,0/*flags*/,"sample",dev/*dev_id*/);
/*....*/
return0;
}
中断处理例程应当返回一个值指示是否真正处理了一个中断。
如果处理例程发现设备确实需要处理,应当返回IRQ_HANDLED;否则返回值IRQ_NONE。
以下宏可产生返回值:
IRQ_RETVAL(handled)/*若要处理中断,handled应是非零*/
有位网友在处理返回值是按惯例return0;,导致了oops。
吸取经验教训,我们应特别注意这种返回值,以下是有关中断处理例程的返回值的内核定义(#include
typedefintirqreturn_t;
#defineIRQ_NONE (0)
#defineIRQ_HANDLED
(1)
#defineIRQ_RETVAL(x) ((x)!
=0)
实现中断处理例程
中断处理例程唯一的特别之处在中断时运行,它能做的事情受到了一些限制.这些限制与我们在内核定时器上看到的相同:
(1)中断处理例程不能与用户空间传递数据,因为它不在进程上下文执行;
(2)中断处理例程也不能做任何可能休眠的事情,例如调用wait_event,使用除GFP_ATOMIC之外任何东西来分配内存,或者锁住一个信号量;
(3)处理者不能调用schedule()。
中断处理例程的作用是将关于中断接收的信息反馈给设备并根据被服务的中断的含义读、写数据。
中断处理例程第一步常常包括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。
这也要根据硬件的工作原理决定,这一步也可能需要在最后做而不是开始;这里没有通用的规则。
一些设备不需要这步,因为它们没有一个"中断挂起"位;这样的设备是少数。
一个中断处理的典型任务是:
如果中断通知它所等待的事件已经发生(例如新数据到达),就会唤醒休眠在设备上的进程。
不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。
如果需要进行长时间计算,最好的方法是使用tasklet或者workqueue在一个更安全的时间来调度计算任务。
启用和禁止中断
有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。
并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。
注意:
应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。
禁止单个中断
有时(但是很少!
)一个驱动需要禁止一个特定中断。
但不推荐这样做,特别是不能禁止共享中断(在现代系统中,共享的中断是很常见的)。
内核提供了3个函数,是内核API的一部分,声明在
voiddisable_irq(intirq);/*禁止给定的中断,并等待当前的中断处理例程结束。
如果调用disable_irq的线程持有任何中断处理例程需要的资源(例如自旋锁),系统可能死锁*/
voiddisable_irq_nosync(intirq);/*禁止给定的中断后立刻返回(可能引入竞态)*/
voidenable_irq(intirq);
调用任一函数可能更新在可编程控制器(PIC)中的特定irq的掩码,从而禁止或使能所有处理器特定的IRQ。
这些函数的调用能够嵌套,即如果disable_irq被连续调用2次,则需要2个enable_irq重新使能IRQ。
可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。
禁止所有中断
在2.6内核,可使用下面2个函数中的任一个(定义在
voidlocal_irq_save(unsignedlongflags);/*在保存当前中断状态到flags之后禁止中断*/
voidlocal_irq_disable(void);/*关闭中断而不保存状态*/
/*如果调用链中有多个函数可能需要禁止中断,应使用local_irq_save*/
/*打开中断使用:
*/
voidlocal_irq_restore(unsignedlongflags);
voidlocal_irq_enable(void);
/*在2.6内核,没有方法全局禁用整个系统上的所有中断*/
顶半部和底半部
中断处理需要很快完成并且不使中断阻塞太长,所以中断处理的一个主要问题是如何在处理例程中完成耗时的任务。
Linux(连同许多其他系统)通过将中断处理分为两部分来解决这个问题:
“顶半部”是实际响应中断的例程(request_irq注册的那个例程)。
“底半部”是被顶半部调度,并在稍后更安全的时间内执行的函数。
他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。
典型的情况是:
顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出:
这个操作非常快。
底半部接着进行任何其他需要的工作。
这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。
Linux内核有2个不同的机制可用来实现底半部处理:
(1)tasklet(首选机制),它非常快,但是所有的tasklet代码必须是原子的;
(2)工作队列,它可能有更高的延时,但允许休眠。
tasklet和工作队列在《时间、延迟及延缓操作》已经介绍过,具体的
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- linux 驱动 中断