Linux 中断子系统(一)综述
硬件链接相关描述
在上图中主要分为三部分,产生中断的外部设备(中断源)、用于管理中断的中断控制器以及处理中断信号的 cpu。
中断处理流程
在 ARM 处理器上会有一个中断向量的连续地址,一般是零地址开始,之后地址每增加 1 的地址上存放一条跳转指令,用于处理不同的中断信号,当有不同的中断发生时就到对应的地址执行跳转指令,而具体不同的中断信号就在不同中断标号位置进行处理。
保存现场(当前线程环境)— 跳转执行中断服务程序 — 返回恢复现场。
Linux 系统对中断处理的方式
首先需要先明确几个问题:
- 中断处理过程中是关全局中断的,也就是中断程序处理期间不能响应新的中断。
- 基于第一点,Linux 内核不允许中断嵌套,其实第一点就已经决定了中断不能嵌套。
基于以上两点,当 Linux 系统的中断任务比较多的时候系统响应就会有很大延迟,为此 Linux 系统将中断设计为上半部和下半部,上半部处理紧急的必须要处理的任务而耗时的不紧急的任务放到中断下半部来处理,中断下半部就是普通的软件程序(但是有一点区别就是 softirq 和 tasklet 运行在中断上下文,而 workqueue 和 threaded irq 运行在进程上下文),中断仍然是开启的,在中断的下半部仍然可以响应中断。
中断上半部应该主要处理如下任务:
- 对实时性要求强的任务。
- 和硬件相关的操作,像清中断标志位,如果是共享中断则获取硬件中断号。
- 不能休眠,休眠会引发系统任务调度,而在中断状态下任务调度是关闭的,这个一定不能休眠。
中断下半部内核提供了多种处理机制,下面挨个介绍中断下半部处理方式
softirq(性能好)
在普通的驱动中一般是不会用到 softirq,softirq 不能动态分配,都是静态定义的。内核已经定义了若干种 softirq number,例如网络数据的收发、block 设备的数据访问(数据量大,通信带宽高),timer 的 deferable task(时间方面要求高)。softirq 的一些特性:
- softirq 是在编译期间静态分配的。
- 产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序(硬中断处理完成会打开全局中断)。
- 可以并发运行在多个 CPU 上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个 CPU 同时操作), 因此也需要使用自旋锁来保护其数据结构。
- 软中断可能运行在中断上下文,软中断处理函数中不能睡眠,从硬中断返回的时候调用 do_softirq()。
- 在 Linux 内核中,用 softirq_action 结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的指针的参数。使用 open_softirq() 函数可以注册软中断对应的处理函数,而 raise_softirq() 函数可以触发一个软中断。
Tasklet(易用)
内核把普通优先级和高优先级的 tasklet 维护在两个不同的链表中。tasklet_schedule 将 tasklet 添加到普通优先级链表中,用 TASKLET_SOFTIRQ 来标志调度相关的 softirq。tasklet_hi_schedule 将 tasklet 添加到高优先级链表中,用 HI_SOFTIRQ 来标志调度相关的 softirq。下面是 tasklet 的一些特点:
- 一个 Tasklet 同一时刻只能在一个处理器上执行,不要求处理函数是可重入的(易用性的体现),不同的 Tasklet 可以同时运行在不同的 cpu 上。
- Tasklet 可以在运行时添加或删除(易用)。
- 在已经被调度但还未开始执行的 tasklet 上调用 tasklet_schedule 将不会执行任何操作,该 tasklet 最终也只执行一次。
- 可以在 tasklet 中调用 tasklet_schedule,意味着 tasklet 可以调度自己。
- 高优先级的 tasklet 总是在普通优先级的 tasklet 之前执行,滥用高优先级的 tasklet 会导致系统延时增加。
调用 tasklet_kill 可以停止 tasklet(等待当前执行完毕后再杀掉)。
x 与软中断和 tasklet 不同,他们运行在中断上下文,不可抢占,而 workqueue 运行在进程上下文中,可以抢占,可以调度当然最直接的就是可以睡眠。这里的睡眠是指可以执行导致线程睡眠的操作,比如持有互斥锁,调用 usleep 等操作。 工作队列是构建在内核线程之上的,内核有两种方法处理工作队列。
- 一种是默认的共享工作队列,由一组内核线程处理,每个内核线程运行在一个 cpu 上。一旦有工作任务需要调度,就让该工作到全局工作队列中排队,他将在合适的时候执行。
- 另一种是专用内核线程内运行工作队列。这意味着无论何时需要执行工作队列处理程序,都会唤醒专用内核线程来处理它,而不是默认的预定义内核线程。
threaded irq
线程化中断的主要目标是将中断禁用时间减少到最低限度。使用线程化中断,注册中断处理程序的方式将得到简化。甚至不必自己调度下半部,线程化核心会完成。下半部在专用内核线程中执行。这里使用 request_threaded_irq() 来替代 request_irq();
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |
request_threaded_irq() 函数在其参数中接收两个函数。
@handler 函数:这与使用 request_irq() 注册时使用的函数一样。它表示上半部函数,在中断上下文(或原子上下文)中运行。如果它能更快地处理中断,就可以根本不用下半部,它应该返回 IRQ_HANDLED。但是,如果中断处理需要 100us 以上,如前所述,则应该使用下半部。在这种情况下,它应该返回 IRQ_WAKE_THREAD,从而导致调度 thread_fn 函数(必须提供)。
@thread_fn 函数:这代表下半部,由上半部调度。当硬中断处理程序(handler 函数)返回 IRQ_WAKE_THREAD 时,将调度与该下半部相关联的内核线程,在内核线程运行时调用 thread_fn 函数。thread_fn 函数完成时必须返回 IRQ_HANDLED。执行后,再重新触发该中断,并且在硬中断返回 IRQWAKE_THREAD 之前,内核线程不会被再次调度。
在任何能够使用工作队列调度下半部的地方,都可以使用线程化中断。真正的线程化中断必须定义 handler 和 thread_fn。如果 handler 为 NULL,而 thread_fn 不为 NULL,则内核将安装默认的硬中断处理程序,它将简单地返回 IRQ_WAKE_THREAD 来调度下半部。handler 总是在中断上下文中调用,无论是开发人员定义还是由内核默认提供。
QA
SoftIRQ 为什么不能休眠
SoftIRQ 可能运行在中断上下文中,中断上下文中是不能 sleep 的,因为 sleep 会触发调度。这里软中断是通过 do_softirq() 函数执行的,而执行该函数的时机是
- 从硬中断处理函数返回时(硬中断返回时调用 irq_exit()->invoke_softirq()->do_doftirq(), 这个时候还在中断上下文)。
- 在 ksoftirq 内核线程中(进程上下文)。
- 在显示调用软中断中,比如网络子系统中的 NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ(进程上下文)。
ISR 里为什么不能 sleep
sleep 会导致 call scheduler 以选择另一个进程来运行,内核代码里有大量的 critical section (临界区),critical section 本质上是一段会访问或操作共享资源的代码,在 critical section 里,是不能 call scheduler 的。因为已经有一个进程持有锁了,如果这时切换到另一个进程,最好的情况下是等待一段无法预测的时间后前一个进程会将锁释放出来,最坏的情况是死锁。硬件中断是随时可能发生的,即便内核执行的路径正处于 critical section 中。如果想在 ISR 里支持 sleep,也就是支持 call scheduler 的话,那么所有的 critical section 都必须得禁用中断,否则硬件中断一旦来临系统就会出现 race condition,接下来大概率是死锁。
硬件中断是超级宝贵的资源,想在中断里睡眠的话就得在大量的 critical section 中关闭中断才能避免 race condition,而关闭硬件中断将会大大地增加中断响应的延迟,降低系统的反应速度,这是操作系统的用户所无法接受的,因此内核开发者采用的设计是在中断里不允许睡眠,并且 ISR 应尽快执行并返回以便系统里的进程继续运行。
参考文献
http://www.wowotech.net/irq_subsystem/interrupt_subsystem_architecture.html
https://zhuanlan.zhihu.com/p/403276552
https://blog.csdn.net/ludongguoa/article/details/121226484
《linux 设备驱动开发》