Linux内核设计与实现 中断I/O: 顶部
中断I/O
CPU与外设之间的一种通信方式。 与CPU内部的异常类似。但区别就在于异常的发生是与处理器的时钟信号的同步的,所以异常有时也被称为同步中断。而中断是异步的,可能发生在任何时候(如果中断被允许的话)。
中断处理程序
中断处理程序是内核用来响应相应中断的程序,通常来说每个设备都拥有对应的中断处理程序,来进行相应的处理。举例来说,如果用户按下键盘,则键盘会发送信号给中断控制器,接着中断控制器又通过CPU的中断引脚通知CPU,在执行完当前指令后,CPU立即检测中断引脚(似乎在每条指令执行后,都会进行一次检测),发现有中断发生。CPU经过一系列的检查后,如果当前允许中断,则确定中断是由键盘产生的,然后启动键盘的中断处理程序,读取用户的输入。
中断程序必须执行得尽可能的快,这对于系统性能来说十分重要。但是通常来说,中断程序的工作量十分浩大,比如说网络设备的中断程序:首先需要向设备确认已了解到中断的发生(设备不再发出中断信号),然后将设备中的数据复制到内存中,进行甄别,把数据发送到其对应的协议的栈或者是应用程序中。显然,在今天巨大的网络吞吐量下,这是个耗时的工作。
于是就产生了矛盾,所以我们把中断服务程序分为上下两部分。上半部分执行耗时极少的工作,比如确认已检测到中断或者将相应设备重置。然后将那些繁重的,却又不是很迫切而可以延迟的工作,交付给中断程序的下半部分。在顶部执行时,系统通常会禁止中断的投递,也就是说顶部的工作不会被其他中断打断(注意:此时在中断上下文),因为这部分工作相当重要。而底部的工作,会在未来某个恰当的时机执行,并且允许其他中断的发生。
登记中断处理程序
我们可以调用request_irq()函数(Kernel/irq.c)来向系统注册一个中断服务程序。
request_irq()接下来解析每个参数的含义。irq指明要申请的的中断号,对某些设备来说,其对应的中断号是硬编码的——也就是说不可更改,比如系统定时器和键盘。至于其他的设备所使用的的中断号,则是可以动态分配的。handler顾名思义就是对应的中断处理程序。irqflags可以为0,或者从以下的值中选择(可以使一个或者多个):
SA_INTERRUPT:这种取值的中断处理程序是快中断处理程序。Linux系统通常将中断程序相对地由快慢来划分。发展到了今天,个中的区 别就是,当快中断程序执行时,将当前处理器的中断禁用。这样做会使处理程序更快地完成执行,不会被其他中断打扰。默认情况下——也就是不启用该标志的情况下,除了与当前处理程序共享中断线的中断外,所有中断都是可用的。回忆《软硬件接口》中的中断屏蔽字位,可以建立起对应关系——对每个中断来说,确实存在私有的中断屏蔽字,当起处理程序执行时,禁用这些中断。
SA_SAMPLE_RANDOM:暂时不知道这是干啥的。
SA_SHIRQ:设置了此为的处理程序表示其irq参数所指定的中断线可以被其他中断处理程序分享。如果不设置此位,那么中断线是被单一设备独占的。早期的计算机系统中设备并不多,因此设备独占中断线号的方法是可行的,当往计算机里增加的设备越来越多时,独占的方式明显是不切实际的。因此产生了共享中断线号的思想。那么当一条共享中断线中发来中断信号,我们该如何辨别到底是哪个设备发生了中断?其实就是将该共享中断线上的所以处理程序调用,不过在每个中断处理程序的内部首先检查(参数信息以及设备硬件的支持)是不是这个中断处理程序对应的设备产生的中断,通常是通过读取该硬件设备提供的中断标志位进行判断。如果不是,立即返回,如果是,则处理完成,如果链表中没有一个是,则说明出现错误。
下面回到对参数的介绍。顾名思义devname就是设备的名字,ASCII的文本。这个参数会被/proc/irq和/proc/interrupts所使用。第五个参数dev_id是用于共享中断线的。如果不提供这样一个唯一的设备ID,那么我们就没有办法从共享中断线中移除指定的中断处理程序。想象一下,每个人都长得差不多,那么辨别我们的方法就是身份证了。通常来讲,这个参数会设置为硬件驱动的设备结构体——独一无二的,并且可能在处理程序中会被用到。
注意:dev_id不是用于识别究竟是哪个设备产生中断的!!!
释放中断处理程序
通过调用free_irq()来实现。
free_irq()可以看到dev_id发挥了重要作用,它被用于判定处理程序是否为我们想要移除和释放的处理程序。注意到第8行,用了一个没有释放内存但却很有趣的删除手段。
编写中断处理程序
中断处理程序定义是这样的:
static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs*regs)
在这里,传递dev_id的原因是用于区分共享中断处理程序的不同设备。在早期每个设备独占中断线号,用Irq区分即可。但今非昔比。还有比较重要的是返回值类型irqreturn_t——实际上就是int。其值只有两种。我们用宏IRQ_RETVAL(val)来检测,val非0就返回IRQ_HANDLED,否则就返回IRQ_NONE。IRQ_HANDLED表示设备确实有中断请求,我们调用了正确的处理程序。如果是IRQ_NONE则表示未检测到设备的中断请求。每个中断处理程序的实现取决于设备,但通常来说都要向设备确认:我已了解到中断了。
再次强调:当某中断服务程序执行时,其共享中断线号上的所有中断被屏蔽。并且相同的处理程序不可能在相同处理器并行地执行。
中断上下文
当处理程序运行时,无论是上下部分,它都是处于中断上下文的。
在Linux2.6里,每个进程的内核栈大小都初始地被分配为一个页面大小,减轻内存压力。中断处理进程也被单独地分配了一个页大小的空间用作栈——此前,中断进程将与被中断的进程共享其内核栈。如果自己新建中断服务程序也无须担心可用内存的大小——因为内核总是分配绝对的,最小的栈空间。
个人猜想:中断处理程序是预先驻留在内存给定位置的,比如8086机器,是存放在0x0000H开始的一段内存空间。当中断发生时,便根据中断号启动相应的服务程序。在此过程中,内核并没有给中断程序创建进程描述符,所以其是不可调度的,这也呼应了中断程序上半部分必须执行地尽可能快的原则。并且在整个中断执行过程中,current的值依旧指向原进程的进程描述符。
关于中断上下文的详细知识,我阅读更多源码后再补充。
中断控制
以下是几个关于中断控制的接口函数。
启用中断: local_irq_enable()
禁用中断: local_irq_disable()
保存中断状态:local_irq_save((unsigned long) flags)
恢复中断状态:local_irq_restore((unsigned long) flags)
禁止指定中断线:void disable_irq(unsigned int irq)
void disable_irq_nosync(unsigned int irq)
void sychronize_irq(unsigned int irq)
以上函数禁止指定中断线向所有处理器的投递,但个中关系还不明朗。
启用指定中断线:void enable_irq(unsigned int irq)
检查中断状态:#define in_interrupt() (irq_count()) 如果内核处于中断上下文返回非0值,包括正在执行的中断处理程序或是下半部分程序。
#define in_irq() (hardirq_count()) 当内核正在执行中断处理程序(特指顶部),则返回非0值。