一、中断上下文
- 当执行一个中断处理程序时,内核处于中断上下文(interrput context) 中
- 让我们先回忆一下进程上下文。进程上下文是一种内核所处的操作模式,此时内核代表进程执行——例如, 执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,为进程是以进程上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序
- 不可调用睡眠函数:与上面相反,中断上下文和进程并没有什么瓜葛。与current宏也是不相干的(尽管它会指向被中断的进程)。因为没有后备进程,所以中断上下文不可以睡眠,否则又怎能再对它重新调度呢?因此,不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在你的中断处理程序中使用它——这是对什么样的函数可以在中断处理程序中使用的限制
- 时间限制:中断上下文具有较为严格的时间限制,因为它打断了其他代码。中断上下文中的代码应当迅 速 、简洁,尽量不要使用循环去处理繁重的工作。有一点非常重要,请永远牢记:中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序 中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行
中断处理程序栈
- 中断处理程序栈的设置是一个配置选项。曾经,中断处理程序并不具有自己的栈。相反,它们共享所中断进程的内核栈。内核栈的大小是两页,具体地说,在32位体系结构上是8KB,在64位体系结构上是16KB。因为在这种设置中,中断处理程序共享别人的堆栈,所以它们在栈中获取空间时必须非常节约。当然,内核栈本来就很有限,因此,所有的内核代码都应该谨慎利用它
- 在2.6版早期的内核中,增加了一个选项,把栈的大小从两页减到一页,也就是在32位的系统上只提供4KB的栈。这就减轻了内存的压力,因为系统中每个进程原先都需要两页连续,且不可换出的内核内存。为了应对栈大小的减少,中断处理程序拥有了自己的栈,每个处理器一 个,大小为一页。这个栈就称为中断栈,尽管中断栈的大小是原先共享栈的一半,但平均可用栈空间大得多,因为 断处理程序把这一整页占为己有。
- 你的中断处理程序不必关心栈如何设置,或者内核栈的大小是多少。总而言之,尽量节约内核栈空间
二、中断处理机制的实现
- 中断处理系统在Linux中的实现是非常依赖于体系结构的,想必你对此不会感到特別惊讶。 实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身
- 下图是中断从硬件到内核的路由
- 设备产生中断,通过总线把电信号发送给中断控制器
- 如果中断线是激活的(它们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上进制该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点
- 在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对干每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务):然后,内核调用函数do_IRQ()从这里开始,大多数中断处理代码是用C编写的——但它们依然与体系结构相关
- do_IRQ()的声明如下:
- 因为C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈中的。中断的值也会得以保存,所以,do_IRQ()可以将它提取出来
- 计算出中断号后,do_IRQ对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机上,这些操作是由mask_and_ack_8259A()来完成的
- 接下来,do_IRQ()需要确保在这条中断线上有一个有效的处理程序,而且这个程序已经启 动,但是当前并没有执行。如果是这样的话,do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序。 handle_IRQ_event()方法被定义在文件kernel/irq/handler.c中
- 首先,因为处理器禁止中断,这里要把它们打开,就必须在处理程序注册期间指定IRQF_DISABLED标志。回想一下,IRQF_DISABLED表示处理程序必须在中断禁止的情况下运行。接下来,每个潜在的处理程序在循环中依次执行。如果这条线不是共享的,第一次执行后就退出循环。否则,所有的处理程序都要被执行。之后,如果在注册期间指定了IRQF_SAMPLE_RANDOM标志,则还要调用函数add_interrupt_randomness()。这个函数使用中断间隔时间为随机数产生器产生熵。最后,再将中断禁止(do_IRQ()期望中断一直是禁止的),函数返回。回到do_IRQ(),该加湿机做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_ intr()
- ret_from_intr()例程类似于初始入口代码,以汇编语言编写。这个例程检査重新调度是否正在挂起(回想一下进程调度的文章,这意味着设置了need_resched)。如果重新调度正在挂起,而且内核正在返回用户空间(也就是说,中断了用户进程),那么,schedule()被调用。如果内核正在返回内核空间(也就是说,中断了内核本身),只有在preempt_count为0时,schedule()才会被调用,否则,抢占内核便是不安全的。在schedule()返回之后,或者如果没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。
- 在x86上,初始的汇编例程位于arch/x86/kemel/entiy_64.S(文件entry_32.S对应32位的x86的体系架构),C方法位于arch/x86/kerad/irq.c。其他所支持的结构与此类似
三、/proc/interrupts
- procfs是一个虚拟文件系统,它只存在于内核内存,一般安装于/proc目录。在procfs中读写文件都要调用内核函数,这些函数模拟从真实文件中读或写。与此相关的例子是/proc/interrupts文件,该文件存放的是系统中与中断相关的统计信息
- 下面是从单处理器PC上输出的:
- 第1列是中断线。在这个系统中,现有的中断号为0~2、4、5、12及15。这里没有显示没有安装处理程序的中断线
- 第2列是一个接收中断数目的计数器。事实上,系统中的每个处理器都存在这样的列,但是,这个机器只有一个处理器。我们看到,时钟中断已接收3602371次中断,这里,声卡(EMU10K 1)没有接收一次中断(这表示机器启动以来还没有使用它)
- 第3列是处理这个中断的中断控制器。XT-PIC对应于标准的PC可编程中断控制器。在具有I/O APIC的系统上,大多数中断会列出IO-APIC-level或IO-APIC-edge,为自己的中断控制器
- 最后一列是与这个中断相关的设备名字。这个名字是通过参数devname提供给函数request_irq()的,前面已讨论过了。如果中断是共享的(例子中的4号中断就是这种情况),则这条中断线上注册的所有设备都会列出来
- 对于想深入探究procfs内部的人来说,procfs代码位于fs/proc中。不必惊讶,提供/proc/interrupts的函数是与体系结构相关的,叫做show_interrupts()
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/119860.html