📄 (ldd) ch09-中断处理(下)(转载).htm
字号:
<TD bgColor=#000000>
<P align=center><A href="http://joyfire.net/lsdp/index.htm"><FONT
color=#ffffff size=2>目录页</FONT></A> | <A
href="http://joyfire.net/lsdp/11.htm"><FONT color=#ffffff
size=2>上一页</FONT></A> | <A href="http://joyfire.net/lsdp/13.htm"><FONT
color=#ffffff size=2>下一页</FONT></A></P>
<P align=center><FONT face=黑体 color=#ffffff size=6>(LDD) Ch09-中断处理(下)(转载)
</FONT></P><SPAN style="LINE-HEIGHT: 1; LETTER-SPACING: 0pt"><FONT
color=#ffffff size=3>
<P>发信人: Altmayer (alt), 信区: GNULinux<BR>标 题: (LDD) Ch09-中断处理(下)(转载)<BR>发信站: 饮水思源 (2001年12月13日08:57:27 星期四), 站内信件<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>UNIXpost </FONT>讨论区 】<BR>【 原文由<FONT
color=#00ff00> altmayer.bbs@bbs.nju.edu.cn,</FONT> 所发表 】<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>altmayer </FONT>的信箱 】<BR> <BR> <BR>共享中断<BR> PC机一个众所周知的“特性”就是不能将不同的设备挂到同一个中断信号线上。<BR>但是,Linux打破了这一点。甚至我的ISA硬件手册―一本没提到Linux的书―也说“最多<BR>只有一个设备”可以挂到中断信号线上,除非硬件设备设计的不好,电信号上并无这样<BR>的限制。问题在于软件。<BR> <BR> <BR> <BR> Linux软件对共享的支持是为PCI设备做的,但也可用于ISA卡。不必说,非PC平<BR>台和总线也支持共享。<BR> <BR> <BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> 为了开发能处理共享中断信号的驱动程序,必须考虑一些细节。下面会讨论到,<BR>使用共享中断的驱动程序不能使用本章描述的一些特性。但最好尽可能对共享中断提供<BR>支持,因为这样对最终用户来说比较方便。<BR> <BR>安装共享的处理程序<BR> 和已经拥有的中断一样,要与它共享的中断也是通过request_irq函数来安装的<BR>,但它们有两处不同:<BR> <BR> <BR> <BR>l 申请共享中断时,必须在flags参数中指定SA_SHIRQ位<BR> <BR> <BR> <BR>l dev_id参数必须是唯一的。任何指向模块的地址空间的指针都可以,当然dev_<BR>id一定不能设为NULL。<BR> <BR> <BR> <BR>内核为每个中断维护了一张共享处理函数的列表,并且这些处理函数的dev_id各不相同<BR>,就象是驱动程序的签名。如果两个驱动程序都将NULL注册为它们对同一个中断的签名<BR>,那么在卸载时会混淆起来,当中断到达时内核就会出现oops消息。我第一次测试共享<BR></P></FONT><FONT
color=#ffffff size=3>
<P>,那么在卸载时会混淆起来,当中断到达时内核就会出现oops消息。我第一次测试共享<BR>中断时就发生过这种事情(当时我只是想着“一定要将SA_SHIRQ位加到这两个驱动程序上<BR>”)。<BR> <BR> <BR> <BR>满足这些条件之后,如果中断信号线空闲或者下面两个条件同时得到满足,那么request<BR>_irq就会成功:<BR> <BR> <BR> <BR>l 前面注册的处理函数的flags参数指定了SA_SHIRQ位。<BR> <BR> <BR> <BR>l 新的和老的处理函数同为快速处理函数,或者同为慢速处理函数。<BR> <BR> <BR> <BR>需要满足这些要求的原因很明显:快速和慢速处理函数处于不同的环境,不能互相混淆<BR>。类似的,你也不能与已经安装为不共享的中断处理函数共享相同的中断。但关于快速<BR>和慢速处理函数的限制对最近的2.1版的内核来说是不必要的,因为两种处理函数已经合<BR>并了。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>并了。<BR> <BR> <BR> <BR>当两个或两个以上的驱动程序共享同一根中断信号线,而硬件又通过这根信号线中断了<BR>处理器时,内核激活这个中断注册的所有处理函数,并将自己的dev_id传递给它们。因<BR>此,共享处理函数必须能够识别出它对应于哪个中断。<BR> <BR> <BR> <BR>如果你在申请中断信号之前需要探测你的设备的话,内核无法提供帮助。没有共享中断<BR>的探测函数。仅当使用的中断信号线空闲时,标准的探测机制才能奏效;但如果被其它<BR>的具有共享特性的驱动程序占用的话,那么即使你的程序已经可以正常工作了,探测也<BR>会失败。<BR> <BR> <BR> <BR>那么,唯一的可以用来探测共享中断信号的技术就是DIY探测。驱动程序必须为所有可能<BR>的中断信号线申请共享处理函数,然后观察中断在何处报告。这里和前面介绍的DIY的探<BR>测之间的差别在于,此时探测处理函数必须检查是否真的发生了中断,因为为响应共享<BR>中断信号线上的其它设备的中断它可能已经被调用过了。<BR> <BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> <BR>释放处理函数同样是通过执行release_irq来实现的。这里dev_id参数用于从该中断的共<BR>享处理函数列表中正确地选出要释放的那个处理函数。这就是dev_id指针必须唯一的原<BR>因。<BR> <BR> <BR> <BR>使用共享处理程序的驱动程序时还要小心:不能使用enable_irq和disable_irq。如果它<BR>使用了这两个函数,共享中断信号线的其它设备就无法正常工作了。一般地,程序员必<BR>须牢记他的驱动程序并不独占这个中断,因此它的行为必须比独占中断信号线时更“社<BR>会化”些。<BR> <BR>运行处理函数<BR> 如上所述,当内核接收到中断时,所有注册过的处理函数都会被激活。共享中断<BR>处理程序必须能将需要处理的中断和其它设备产生的中断区分开来。<BR> <BR> <BR> <BR> 装载short时指定shared=1将安装下面的处理程序而不是缺省的处理程序:<BR> <BR> <BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>void short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)<BR> <BR>{<BR> <BR> int value;<BR> <BR> struct timeval tv;<BR> <BR> <BR> <BR> /* 如果不是short,立即返回 */<BR> <BR> value = inb(short_base);<BR> <BR> if (!(value & 0x80)) return;<BR> <BR> <BR> <BR> /* 清除中断位 */<BR> <BR> outb(value & 0x7F, short_base);<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> <BR> <BR> /* 其余不变 */<BR> <BR> <BR> <BR> do_gettimeofday(&tv);<BR> <BR> short_head += sprintf((char *)short_head,"%08u.%06u\n",<BR> <BR> (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));<BR> <BR> if (short_head == short_buffer + PAGE_SIZE)<BR> <BR> short_head = short_buffer; /* 绕回来 */<BR> <BR> <BR> <BR> wake_up_interruptible(&short_queue); /* 唤醒所有读进程 */<BR> <BR>}<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> <BR> <BR> 解释如下。因为并口没有 “待处理的中断”位可供检查,为此处理函数使用了A<BR>CK位。如果该位为高,报告的中断就是送给short的,并且处理函数将清除该位。<BR> <BR> <BR> <BR> 处理函数是通过将并口的数据端口的高位清零来清除中断位的-short假定并口<BR>的9和10引脚是连在一起的。如果与short共享同一中断的设备产生了一个中断,short会<BR>知道它的信号线并未激活,因此什么也不会做。<BR> <BR> <BR> <BR> 显然,真正的驱动程序做的工作会更多些;特别的,它要使用dev_id参数来得到<BR>自己的硬件结构。<BR> <BR> <BR> <BR> 特性完全的驱动程序可能会将工作划分为上半部和下半部,但这很容易添加,对<BR>实现共享的代码并无太大影响。<BR> <BR>/proc接口<BR></P></FONT><FONT
color=#ffffff size=3>
<P>/proc接口<BR> 系统中安装的共享中断处理程序不会影响/proc/stat文件(该文件甚至并不知道<BR>处理程序的存在)。但是,/proc/interrupts文件会有些变化。<BR> <BR> <BR> <BR> 为同一个中断号安装的处理程序会出现在/proc/interrupts文件的同一行上。下<BR>面的快照取自我的计算机,是在我将short和我的帧捕捉卡装载为共享中断处理程序之后<BR>:<BR> <BR> <BR> <BR> 0: 1153617 timer<BR> <BR> 1: 13637 keyboard<BR> <BR> 2: 0 cascade<BR> <BR> 3: 14697 + serial<BR> <BR> 5: 190762 NE2000<BR> <BR> 7: 2094 + short, + cx100<BR></P></FONT><FONT
color=#ffffff size=3>
<P> 7: 2094 + short, + cx100<BR> <BR> 13: 0 math error<BR> <BR> 14: 47995 + ide0<BR> <BR> 15: 12207 + ide1<BR> <BR> <BR> <BR> 这里共享中断信号是IRQ7号中断;激活的处理程序列在同一行,用逗号隔开。显<BR>然内核是无法区分short中断和捕捉卡(cx100)中断的。<BR> <BR>中断驱动的I/O<BR> 如果和处理的硬件间的数据传输因为某些原因会被延迟的话,那么驱动程序的写<BR>函数必须实现缓冲。数据缓冲可以将数据的发送和接收与write及read系统调用分离开来<BR>,提高系统的整体性能。<BR> <BR> <BR> <BR> 一个好的缓冲机制是“中断驱动的I/O”,它在中断时间内填充一个输入缓冲区<BR>并由读设备的进程将其取空;或由写设备的进程来填充一个输入缓冲区并在中断时间内<BR>将其取空。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>将其取空。<BR> <BR> <BR> <BR> 中断驱动的数据传输要正确进行,要求硬件必须安下面的语义产生中断:<BR> <BR> <BR> <BR>l 对输入而言,当新数据到达,系统处理器准备读取它时,设备就中断处理器。<BR>实际执行的动作取决于设备是否使用了I/O端口,内存映射或者DMA。<BR> <BR> <BR> <BR>l 对输出而言,当设备准备好接收新数据或对成功的数据传输进行确认时都会发<BR>出中断。内存映射和能进行DMA的设备通常是通过产生中断来通知系统它们的对缓冲区的<BR>处理已经结束。<BR> <BR> <BR> <BR>read或write调用时间和实际的数据到达时间之间的关系是在第5章“字符设备驱动程序<BR>的扩展操作”的“阻塞型和非阻塞型操作”一节中介绍的。中断驱动的I/O引入了共享数<BR>据项的并发进程间的同步问题,因此所有这些问题都与竞争条件有关。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>竞争条件<BR> 当变量或其它数据项在中断时间内被修改时,由于竞争条件的存在,驱动程序的<BR>操作就有可能造成它们的不一致。当操作不是原子地执行时,竞争条件就会发生,但在<BR>执行时仍假定数据会保持一致性。因此“竞争”是在非原子性的操作和其它可能被同时<BR>执行的代码之间发生的。典型的,竞争条件会在三种情况下发生:在函数内隐式地调用s<BR>chedule,阻塞操作和由中断代码或系统调用访问共享数据。最后一种情况发生得最频繁<BR>,因此我们在这一章处理竞争条件。<BR> <BR> <BR> <BR> 处理竞争条件是编程时最麻烦的一部分,因为相关的臭虫满足的条件很苛刻,不<BR>容易再现,很难分辨出中断代码和驱动程序的方法间是否存在竞争条件。程序员必须极<BR>为小心地避免数据或元数据的冲突。<BR> <BR> <BR> <BR> 一般用于避免竞争条件的技术是在驱动程序的方法中实现的,这些方法必须保证<BR>当数据项受到没有预料到的修改时得到正确的处理。但另一方面,中断处理函数并不需<BR>要特别的处理,因为相对设备的方法,它的操作是原子性的。<BR> <BR> <BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> 可以使用不同的技术来防止数据冲突,我下面将介绍最常用的一些技术。我不给<BR>出完整的代码,因为各种情况下最好的实现代码取决于被驱动的设备的操作模式以及程<BR>序员的不同爱好。<BR> <BR> <BR> <BR> 最常用的防止数据被并发地访问的方法有:<BR> <BR> <BR> <BR>l 使用循环缓冲区和避免使用共享变量。<BR> <BR> <BR> <BR>l 在访问共享变量的方法里暂时禁止中断。<BR> <BR> <BR> <BR>l 使用锁变量,它是原子地增加和减少的。<BR> <BR> <BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>当访问可能在中断时间内被修改了的变量时,不论你选用的是哪种方法,都必须决定如<BR>何进行处理。这样的变量可以声明为volatile的,来阻止编译器对该值的访问进行优化(<BR>例如,它阻止编译器在整个函数的运行期内将这个值放进一个寄存器中)。但是,使用vo<BR>latile变量后,编译器产生的代码会很糟糕,因此你可能会转向使用cli和sti。Linux实<BR>现这些函数时使用了gcc的制导来保证在中断标志位被修改之前处理器处于安全状态。<BR> <BR>使用循环缓冲区<BR> 使用循环缓冲区是处理并发访问问题的一种有效方法:当然最好的处理方法还是<BR>不允许并发访问。<BR> <BR> <BR> <BR> 循环缓冲区使用了一种被称为“生产者和消费者”的算法-一个进程将数据放进<BR>缓冲区中,另一个则将它取出来。如果只有一个生产者和一个消费者,那就避免了并发<BR>访问。在short模块中有两个生产者和消费者的例子。其中一个情形是,读进程等待消费<BR>在中断时间里生产的数据;而另一个情形是,下半部消费上半部生产的数据。<BR> <BR> <BR> <BR> 共有两个指针用于对循环缓冲区进行寻址:head和tail。head是数据的写入位置<BR>,由数据的生产者更新。数据从tail处读出,它是由消费者更新的。正如我上面提到的<BR>,如果数据是在中断时间内写的,那么多次访问head多次时就必须小心。你必须将head<BR></P></FONT><FONT
color=#ffffff size=3>
<P>,如果数据是在中断时间内写的,那么多次访问head多次时就必须小心。你必须将head<BR>定义成volatile的或者在进入竞争条件前将中断禁止。<BR> <BR> <BR> <BR> 循环缓冲区在填满前工作的很好。如果缓冲区满了,就可能出问题,但你可以有<BR>多种不同的解决方法可供选择。short中的实现就是简单地丢弃数据;并不检查溢出,如<BR>果head超过了tail,那么整个缓冲区中的数据都丢失了。其它的实现还有丢弃最后那个<BR>数据项;覆盖缓冲区的tail,printk是这么实现的(参见第4章的“消息是如何记录的”<BR>一节);或者阻塞生产者,scullpipe是这么实现的;或者分配一个临时的附加的缓冲区<BR>作为主力缓冲区的候补。最好的解决方案取决于数据的重要性和其它一些具体情况下的<BR>问题,所以我就不在这讨论了。<BR> <BR> <BR> <BR> 虽然循环缓冲区看来解决了并发访问的问题,但当read函数进入睡眠时仍有出现<BR>竞争条件的可能。下面的代码给出short中这个问题出现的位置:<BR> <BR> <BR> <BR>while (short_head==short_tail) {<BR> <BR> interruptible_sleep_on(&short_queue);<BR></P></FONT><FONT
color=#ffffff size=3>
<P> interruptible_sleep_on(&short_queue);<BR> <BR> /* ... */<BR> <BR> }<BR> <BR> <BR> <BR> 执行这个语句时,新数据有可能在while条件被测试是否为真后和进程进入睡眠<BR>前到达。中断中携带的信息就无法被进程及时读取;因此即使此时head != tail进程也<BR>将进入睡眠,直到下一项数据到达时它才会被唤醒。<BR> <BR> <BR> <BR> 我并没有为short实现正确的锁,因为short_read的源码在第8章的“驱动程序样<BR>例”一节中就包括了,当时还没有讨论到这一点。而且,short处理的数据也不值得我们<BR>为它这么做。<BR> <BR> <BR> <BR> 尽管short收集的数据并不重要,而且在连续的两条指令时间间隔内发生中断的<BR>可能性小到可以忽略,但是有些时候你还是不能在还有待处理的数据时冒险地进入睡眠<BR>。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>。<BR> <BR> <BR> <BR> 但这个问题一般来说还是值得对它进行特别的处理的,我们将它留到本章后面的<BR>“无竞争地进入睡眠”一节,那里我将会更详细地进行讨论。<BR> <BR> <BR> <BR> 值得注意的是,循环缓冲区只能处理生产者和消费者的情形。程序员必须经常地<BR>通过更复杂的数据结构来解决并发访问的问题。生产者/消费者的情形实际上是这些问题<BR>中最简单的一种;其它的数据结构,比如象链接表,就不能简单地使用循环缓冲区的实<BR>现方案。<BR> <BR>禁止中断<BR> 获得对共享数据独占访问的通用方法是调用cli来禁止处理器的中断报告。当数<BR>据项(例如链接表)在中断时间内要被修改并且是被生存于正常的计算流中的函数修改时<BR>,那么随后的函数在访问这些数据前就必须先禁止中断。<BR> <BR> <BR> <BR> 这种情况下,竞争条件会发生在读共享数据项的指令和使用刚获得与数据有关的<BR>信息的指令之间。例如,如果链接表在中断时间内被修改过了,那么下面的循环在读这<BR></P></FONT><FONT
color=#ffffff size=3>
<P>信息的指令之间。例如,如果链接表在中断时间内被修改过了,那么下面的循环在读这<BR>个表时就可能会失败。<BR> <BR> <BR> <BR>for (ptr=listHead; ptr; ptr=ptr->next)<BR> <BR> /* do somthing */;<BR> <BR> <BR> <BR> 在ptr已经被读取后但在使用它之前,一个中断可能会改变了ptr的值。如果发生<BR>了这种情况,你一使用ptr就会有问题,因为这个指针当前的值与链接表已经没有关系了<BR>。<BR> <BR> <BR> <BR>一个可能的解决的方法就是在整个关键循环期间都将中断禁止。虽然禁止中断的代码早<BR>在第2章的“ISA内存”一节中就已经引入了,但仍值得在这里再重复一遍:<BR> <BR> <BR> <BR>unsigned long flags;<BR></P></FONT><FONT
color=#ffffff size=3>
<P>unsigned long flags;<BR> <BR>save_flags(flags);<BR> <BR>cli();<BR> <BR>/* 临界区代码 */<BR> <BR>restore_flags(flags);<BR> <BR> <BR> <BR> 实际上,在驱动程序的方法中,可以就用简单的cli/sti对来替代,因为你可以<BR>认为当进程进入系统调用时中断会被打开。但是,在要被其它代码所调用的代码中,你<BR>不得不使用更安全的save_flags/restore_flags解决方法,因为此时无法确定中断标志<BR>位(IF) 当前的值。<BR> <BR>使用锁变量<BR> 共享数据变量的第三种方法是使用使用原子指令进行访问的锁。当两个无关的实<BR>体(比如象中断处理程序和read系统调用,或者是SMP对称多处理器计算机中的两个处理<BR>器)需要并发地对共享的数据项进行访问时,它们必须先申请锁。如果得不到锁,它就必<BR>须等待。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> <BR> <BR> Linux内核开放了两套函数来对锁进行处理:位操作和对“原子性”数据类型的<BR>访问。<BR> <BR>位操作<BR> 经常的,我们要使有单个位的锁变量或者要在中断时间内更新设备状态位-而进<BR>程可能正在访问它们。内核为此提供了一套原子地修改和测试位的函数。因为整个操作<BR>是单步完成的,因此不会介入任何中断。<BR> <BR> <BR> <BR> 原子性的位操作运行的很快,因为它们通常不禁止中断,使用单条机器指令来完<BR>成相应操作。这些函数与体系结构相关,在头文件<asm/bitops.h>中声明。即使在SMP机<BR>器上它们也能保证是原子的,因此是推荐的保持处理器间一致性的方式。<BR> <BR> <BR> <BR> 不幸的是,这些函数的数据类型也是体系结构相关的。nr参数和返回值在Alpha<BR>上是unsigned long类型,而在其它体系结构上是int类型。下面的列表描述了1.2到2.1.<BR>37各版的位操作形式。但该列表在2.1.38版中有了改变,详情可参见第17章“近期发展<BR>”的“位操作”一节。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>”的“位操作”一节。<BR> <BR> <BR> <BR>set_bit(nr, void *addr);<BR> <BR>这个函数用于设置addr指向的数据项的第nr个位。该函数作用在一个unsigned long上,<BR>即使addr指向void。返回的是该位原先的取值-0或非零。<BR> <BR> <BR> <BR>clear_bit(nr, void *addr);<BR> <BR>这个函数用于清除addr指向的unsigned long数据中的指定位。它的语义和set_bit类似<BR>。<BR> <BR> <BR> <BR>change_bit(nr, void *addr);<BR> <BR> 这个函数用于切换指定位,其它方面和前面的set_bit和clear_bit函数类似。<BR> <BR> <BR></P></FONT><FONT
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -