中断分为上半部和下半部,先介绍上半部。
每个中断都有一个编号,CPU支持多个中断编号,但这是 远远不够的,那么就会有许多中断共享编号称为中断共享。
一、硬件中断
中断不能由处理器外部的外设直接产生,而必须借助于一个称为中断控制器(interrupt controller)的标准组件来请求,该组件存在于每个系统中。外部设备会向中断控制器发送中断请求,控制器在执行了各种电工任务之后,将中断请求转发到CPU的中断输入。因为外部设备不能直接发出中断,而必须通过上述组件请求中断,所以这种请求更正确的叫法是IRQ,或中断请求(interrupt request)。
二、处理中断
1. 进入和退出任务
如图14-2所示,中断处理划分为3部分。首先,必须建立一个适当的环境,使得处理程序函数能够在其中执行,接下来调用处理程序自身,最后将系统复原(在当前程序看来)到中断之前的状态。调用中断处理程序前后的两部分,分别称为进入路径(entry path)和退出路径(exit path)。
进入和退出任务还负责确保处理器从用户态切换到核心态。进入路径的一个关键任务是,从用户态栈切换到核心态栈。但是,只有这一点还不够。因为内核还要使用CPU资源执行其代码,进入路径必须保存用户应用程序当前的寄存器状态,以便在中断活动结束后恢复。这与调度期间用于上下文切换的机制是相同的。
在退出路径中,内核会检查下列事项。
- 调度器是否应该选择一个新进程代替旧的进程。
- 是否有信号必须投递到原进程。
从中断返回之后,只有确认了这两个问题,内核才能完成其常规任务,即还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态,或切换到一个不同的保护环。
在中断到达时,处理器可能处于用户态或核心态,这使得中断的进入和退出路径中的工作更为困难。这需要另外几个技术上的修改,为确保图示简明,没有在图14-2中给出。
2. 中断处理程序
中断处理程序可能会遇到困难,特别是,在处理程序执行期间,发生了其他中断。尽管可以通过在处理程序执行期间禁用中断来防止,但这会引起其他问题,如遗漏重要的中断。屏蔽(Masking,这个术语用于表示选择性地禁用一个或多个中断)因而只能短时间使用。
因为需要屏蔽一些中断,因此ISR必须满足如下两个要求。
- 实现(特别是在禁用其他中断时)必须包含尽可能少的代码,以支持快速处理。
- 可以在其他ISR执行期间调用的本中断处理程序例程,不能彼此干扰。
尽管后一个要求可以通过高超的编程和精巧的ISR设计来满足,然而前一个要求更难满足。根据具体的中断,必须运行某个程序,来满足中断处理的最低要求。因而代码长度无法任意缩减。内核如何解决这种两难问题呢?并非ISR的每个部分都同等重要。通常,每个处理程序例程都可以划分为3个部分,具有不同的意义。
(1) 关键操作必须在中断发生后立即执行。否则,无法维持系统的稳定性,或计算机的正确运作。
在执行此类操作期间,必须禁用其他中断。
(2) 非关键操作也应该尽快执行,但允许启用中断(因而可能被其他系统事件中断)。
(3) 可延期操作不是特别重要,不必在中断处理程序中实现。内核可以延迟这些操作,在时间充裕时进行。内核提供了tasklet,用于在稍后执行可延期操作。
3.数据结构
为响应外部设备的IRQ,内核必须为每个潜在的IRQ提供一个函数。该函数必须能够动态注册和注销。静态表组织方式是不够的,因为可能为设备编写模块,而且设备可能与系统的其他部分通过中断进行交互。IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ 0在位置0,IRQ 15在位置15,等等。IRQ最终映射到哪个处理器中断,在这里是不相关的。
IRQ的最大可能数目是通过一个平台相关的常数 NR_IRQS 指定的。大多数体系结构下,该常数定义在处理器相关的头文件 include/asm-14.1 中断 arch/irq.h 中。 不同处理器间及同一处理器家族内,该常数的值变化都很大,主要取决于辅助CPU管理IRQ的辅助芯片。Alpha计算机在小型系统上可支持32个中断,而在Wildfire主板上可支持2048个中断,真令人难以置信。IA-64处理器的中断数目总是256。
- 电流层ISR由 handle_irq 提供。 handler_data 可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用 handle_irq 。该函数负责使用 chip 中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。用于不同中断类型的默认函数由内核提供。
- action 提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作,一会讨论。
IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变: status 描述了IRQ的当前状态。 <irq.h> 文件定义了各种数,可用于描述IRQ电路当前的状态。每个常数表示位串中一个置位的标志位,只要不相互冲突,几个标志可以同时设置。
- IRQ_DISABLED 用于表示被设备驱动程序禁用的IRQ电路。该标志通知内核不要进入处理程序。
- 在IRQ处理程序执行期间,状态设置为 IRQ_INPROGRESS 。与 IRQ_DISABLED 类似,这会阻止其余的内核代码执行该处理程序。
- 在CPU注意到一个中断但尚未执行对应的处理程序时, IRQ_PENDING 标志位置位。
- 为正确处理发生在中断处理期间的中断,需要 IRQ_MASKED 标志。具体参见14.1.4节。
- 在某个IRQ只能发生在一个CPU上时,将设置 IRQ_PER_CPU 标志位。(在SMP系统中,该标志使几个用于防止并发访问的保护机制变得多余。)
- IRQ_LEVEL 用于Alpha和PowerPC系统,用于区分电平触发和边沿触发的IRQ。
- IRQ_REPLAY 意味着该IRQ已经禁用,但此前尚有一个未确认的中断。
- 如果当前IRQ可以由多个设备共享,不是专属于某一设备,则置位 IRQ_NOREQUEST 标志。
处理函数:
该结构中最重要的成员是处理程序函数本身,即 handler 成员,这是一个函数指针,位于结构的起始处。在设备请求一个系统中断,而中断控制器通过引发中断将该请求转发到处理器的时候,内核将调用该处理程序函数。在考虑如何注册处理程序函数时,我们再仔细考察其参数的语义。但请注意,处理程序的类型为 irq_handler_t ,与电流处理程序的类型 irq_flow_handler_t 显然是不同的。name 和 dev_id 唯一地标识一个中断处理程序。 name 是一个短字符串,用于标识设备(例如,“ e100 ”、“ ncr53c8xx ”,等等),而 dev_id 是一个指针,指向在所有内核数据结构中唯一标识了该设备的数据结构实例,例如网卡的 net_device 实例。如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息。
flags 是一个标志变量,通过位图描述了IRQ(和相关的中断)的一些特性,位图中的各个标志位照例可通过预定义的常数访问。 <interrupt.h> 中定义了下列常数。
对共享的IRQ设置 IRQF_SHARED ,表示有多于一个设备使用该IRQ电路。
如果IRQ对内核熵池(entropy pool)有贡献,将设置 IRQF_SAMPLE_RANDOM 。
IRQF_DISABLED 表示IRQ的处理程序必须在禁用中断的情况下执行。
IRQF_TIMER 表示时钟中断。
next 用于实现共享的IRQ处理程序。几个 irqaction 实例聚集到一个链表中。链表的所有元素都必须处理同一IRQ编号(处理不同编号的实例,位于 irq_desc 数组中不同的位置)。在14.1.7节讨论过,在发生一个共享中断时,内核扫描该链表找出中断实际上的来源设备。特别是在单芯片(只有一个中断)上集成了许多不同的设备(网络、USB、FireWire、声卡等)的笔记本电脑中,此类处理程序表可能包含大约5个元素。但我们预期的情况是,每个IRQ下都注册一个设备。
上面说道电流层ISR由 handle_irq 提供,和这里的handle有什么区别。我这这么理解的,中断的触发流程是 外部设备->中断控制器->CPU,外部设备触发电信号通知中断控制器,此时调用的函数就是电流层ISR,这时候做了一些非常底层的状态处理可能。然后中断控制器开始通知CPU,这时候才触发我们理解的中断处理程序,这里的这个handle估计也是一些预处理操作,真正的处理操作是在action链上的函数,并且从链上根据dev_id找到相应的处理程序。
1. 注册IRQ
由设备驱动程序动态注册ISR的工作,可以使所述的数据结构非常简单地进行。在内核版本2.6
重写中断子系统之前,该函数是由平台相关代码实现的。很自然,其原型在所有体系结构上都是相
同的,因为对编写平台无关的驱动程序来说,这是一个绝对的先决条件。现在,该函数由通用代码
实现:
内核首先生成一个新的 irqaction 实例,然后用函数参数填充其内容。当然,其中特别重要的是处理程序函数 handler 。所有进一步的工作都委托给 setup_irq 函数,它将执行下列步骤。
(1) 如果设置了 IRQF_SAMPLE_RANDOM ,则该中断将对内核熵池有所贡献,熵池用于随机数发生器/dev/random 。rand_initialize_irq 将该IRQ添加到对应的数据结构。
(2) 由 request_irq 生成的 irqaction 实例被添加到所属IRQ编号对应的例程链表尾部,该链表表头为 irq_desc[NUM]->action 。在处理共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同。
(3) 如果安装的处理程序是该IRQ编号对应链表中的第一个,则调用 handler->startup 初始化函14.1 中断 数。 1 如果该IRQ此前已经安装了处理程序,则没有必要再调用该函数。
(4) register_irq_proc 在 proc 文件系统中建立目录 /proc/irq/NUM 。而 register_handler_proc 生成 proc/irq/NUM/name 。接下来,系统中就可以看到对应的IRQ通道在使用中。
前面讲述的机制只适用于由系统外设的中断请求所引发的中断。但内核还必须考虑由处理器本身或者用户进程中的软件机制所引发的中断。与IRQ相比,内核无需提供接口,供此类中断动态注册处理程序。这是因为,所使用的编号在初始化时就是已知的,此后不会改变。中断和异常的注册在内核初始化时进行,其分配在运行时并不改变。
下面简单介绍处理硬件IRQ,没有详细看代码,原书将的很细致,这里简单记录。
以IA-32系统上的处理为例。
硬件中断发生后,中断处理程序要分几步来处理,首先记录下栈,这里分为用户态栈和内核态栈,如果此时程序流处在用户态,那么记录用户态的栈,处于核态记录核态的栈。栈帧记录完成开始执行do_IRQ函数,有do_IRQ来调用高层的IRQ。这里主要记录下do_IRQ都做了什么。
与AMD64的情况类似,同样会调用 set_irq_regs 和 irq_enter 函数,来达到同样的目的。内核必须切换到IRQ栈。当前栈可以通过调用辅助函数 current_thread_info 获得,该函数返回一个指向当前使用的 thread_info 实例的指针。回想上文可知,该实例与当前栈在同一个 union 中。而指向适当的IRQ栈的指针可以从上文讨论的 hardirq_ctx 获得。有如下两种可能的情况。
(1) 进程已经在使用IRQ栈,因为是在处理嵌套的IRQ。在这种情况下,内核不需要做什么,所有
的设置都已经完成。可以调用 irq_desc[irq]->handle_irq 来激活保存在IRQ数据库中的ISR。
(2) 当前栈不是IRQ栈( curctx != irqctx ),需要在二者之间切换。在这种情况下,内核执行所需的底层汇编语言操作来切换栈,然后调用 irq_desc[irq]->handle_irq ,最后再将栈切换回去。
请注意,在这两种情况下, ISR都是直接调用的,而不像AMD64系统上通过 generic_handle_irq迂回。
剩余工作的执行与AMD64系统相同。 irq_exit 处理一些记录并激活软中断,而 set_irq_regs将寄存器集合指针恢复到IRQ发生之前的状态。在栈长度为8 KiB时,即使用两个页帧,IRQ的处理会被简化,因为无须考虑栈切换,直接调用irq_desc[irq]->handle_irq 即可。
高层的IRQ做了一下事情:这里可以理解为处理高层的中断处理程序。
如果第一个处理程序函数中没有设置 IRQF_DISABLED ,则用 local_irq_enable_in_hardirq启用(当前CPU的)中断。换句话说,该处理程序可以被其他IRQ中断。但根据电流类型,也可能一直屏蔽刚处理的IRQ。
逐一调用所注册的IRQ处理程序的 action 函数。
如果对该IRQ设置了 IRQF_SAMPLE_RANDOM ,则调用 add_interrupt_randomness ,将事件的时间作为熵池的一个源(如果中断的发生是随机的,那么它们是理想的源)。
local_irq_disable 禁用中断。因为中断的启用和禁用是不嵌套的,与中断在处理开始时是否启用是不相关的。handle_IRQ_event 在调用时禁用中断,在退出时仍然预期禁用中断。
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs); //获取寄存器中信息
struct irq_desc * desc;
/* high bit used in ret_from_ code */
unsigned vector = ~regs->orig_ax;//获得返回地址
/*
* NB: Unlike exception entries, IRQ entries do not reliably
* handle context tracking in the low-level entry code. This is
* because syscall entries execute briefly with IRQs on before
* updating context tracking state, so we can take an IRQ from
* kernel mode with CONTEXT_USER. The low-level entry code only
* updates the context if we came from user mode, so we won't
* switch to CONTEXT_KERNEL. We'll fix that once the syscall
* code is cleaned up enough that we can cleanly defer enabling
* IRQs.
*/
entering_irq();//进入硬中断
/* entering_irq() tells RCU that we're not quiescent. Check it. */
RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");
desc = __this_cpu_read(vector_irq[vector]);
if (!handle_irq(desc, regs)) {//遍历注册的硬中断
ack_APIC_irq();
if (desc != VECTOR_RETRIGGERED) {
pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
__func__, smp_processor_id(),
vector);
} else {
__this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
}
}
exiting_irq();//退出时执行do_softirq
set_irq_regs(old_regs);
return 1;
}
desc->handle_irq()执行了对应的硬中断处理程序.
下面看看硬中断退出处理函数exiting_irq():
exiting_irq->irq_exit->invoke_softirq->__do_softirq().
5、实现处理程序
在实现ISR时,主要的问题是它们在所谓的中断上下文 (interrupt context) 中执行。内核代码有时在常规上下文运行,有时在中断上下文运行。为区分这两种不同情况并据此设计代码,内核提供了 in_interrupt 函数,用于指明当前是否在处理中断。
中断上下文与普通上下文的不同之处主要有如下3点。
- (1) 中断是异步执行的。换句话说,它们可以在任何时间发生。因而从用户空间来看,处理程序例程并不是在一个明确定义的环境中执行。这种环境下,禁止访问用户空间,特别是与用户空间地址之间来回复制内存数据的行为。例如,对网络驱动程序来说,不能将接收的数据直接转发到等待的应用程序。毕竟,内核无法确定等待数据的应用程序此时是否在运行(事实上,这种可能性很低)。
- (2) 中断上下文中不能调用调度器。因而不能自愿地放弃控制权。
- (3) 处理程序例程不能进入睡眠状态。只有在外部事件导致状态改变并唤醒进程时,才能解除睡眠状态。但中断上下文中不允许中断,进程睡眠后,内核只能永远等待下去 1 。因为也不能调用调度器,不能选择进程来执行。当然,只确保处理程序例程的直接代码不进入睡眠状态,这是不够的。其中调用的所有过程和函数(以及被这些函数/过程调用的函数/过程,依此类推)都不能进入睡眠状态。对此进行的检查并不简单,必须非常谨慎,特别是在控制路径存在大量分支时。
关键函数:local_irq_enable()和local_irq_disable()启用禁止硬件中断
local_bh_disable()和local_bh_enable()启用禁止软中断
以上简介的都是硬件中断流程。
2021.7.29
---------------------------------------------------------------------------------------------------------------------------------
ARM平台下的设备描述信息都是以DTS的模式以串口0为例:
interrupts域的值为5,表明在主板上为第5号中断,系统初始化时经过以下函数调用:
最终会调用alloc_descs函数申请中断描述符:
注册中断: