Linux中断子系统8(基于Linux6.6)---中断之softirq介绍
一、前情回顾
对于中断处理而言,linux将其分成了两个部分,一个叫做中断handler(top half),是全程关闭中断的,另外一部分是deferable task(bottom half),属于不那么紧急需要处理的事情。在执行bottom half的时候,是开中断的。有多种bottom half的机制,例如:softirq、tasklet、workqueue或是直接创建一个kernel thread来执行bottom half。本文主要讨论softirq机制。由于tasklet是基于softirq的,因此本文也会提及tasklet,但主要是从需求层面考虑,不会涉及其具体的代码实现。
在普通的驱动中一般是不会用到softirq,但是由于驱动经常使用的tasklet是基于softirq的,因此,了解softirq机制有助于撰写更优雅的driver。softirq不能动态分配,都是静态定义的。内核已经定义了若干种softirq number,例如网络数据的收发、block设备的数据访问(数据量大,通信带宽高),timer的deferable task(时间方面要求高)。
在 Linux 内核中,softirq
(软中断)是一个用于处理较为耗时、需要延迟的中断任务的机制。它是一种比硬中断(硬件中断)优先级较低,但比普通的进程调度高效的中断处理方式。
softirq
的作用与概念
softirq
是内核中处理软中断的机制,通常用于在硬中断处理过程中或者内核中断上下文中进行一些延迟操作。softirq
主要用于处理无法在硬中断上下文中直接处理的任务,比如网络包的接收处理、定时器任务的调度等。
中断处理模型的层次结构
Linux 内核的中断处理机制分为三个层次:
-
硬中断(硬件中断):
当外部硬件设备(如网络卡、硬盘等)发出中断信号时,CPU 会中断当前进程的执行,并立即进入中断处理程序。硬中断的处理通常很简单且迅速,主要是为了捕获硬件事件并进行必要的处理。 -
软中断(softirq):
当硬中断处理程序完成时,一些较为复杂或耗时的操作会被推迟到软中断阶段处理。软中断通常由内核定时器、网络协议栈等高效任务驱动。软中断机制允许这些任务在内核态高效地执行,而不会阻塞其他的硬中断或进程调度。 -
任务队列(tasklet):
任务队列(tasklets)是softirq
机制的另一层次,是软中断的一种延迟处理机制。tasklet 在软中断处理完成后执行,用来处理一些仍需要推迟处理的任务。任务队列的执行在特定的处理上下文中进行,因此它们不会与硬中断竞争。
softirq
的执行流程
在 Linux 中,软中断是通过 softirq
机制来执行的。以下是其执行的关键步骤:
-
硬中断处理:
- 当硬中断发生时,CPU 会首先进入硬中断处理程序。这部分的工作通常是非常简短且快速的,目的是进行中断的初步处理。
-
软中断触发:
- 在硬中断处理程序结束后,内核会通过调度软中断机制来延迟处理一些耗时操作。内核通过一个叫做
softirq
的内存结构来管理这些待处理的软中断。
- 在硬中断处理程序结束后,内核会通过调度软中断机制来延迟处理一些耗时操作。内核通过一个叫做
-
软中断上下文的处理:
- 软中断的处理由
do_softirq()
函数来执行。这个函数会遍历所有的软中断,并在软中断上下文中调用相应的处理函数。通常,软中断会在中断处理的一个空闲阶段由内核调用(即不阻塞的上下文)。
- 软中断的处理由
-
任务队列(tasklet)处理:
- 对于更加复杂和延迟的任务,软中断可以通过任务队列(tasklet)来进行处理。Tasklet 是一个特殊类型的软中断,通常用于处理一些网络数据包、文件系统操作等任务。
softirq
与其他中断机制的关系
-
硬中断(硬件中断):
硬中断通常是通过硬件设备触发的,它是最紧急的中断,需要优先处理。硬中断处理程序应尽量简短,以免阻塞系统其他关键操作。 -
软中断(softirq):
软中断用于处理相对不那么紧急但仍然需要及时处理的任务。比如,处理网络协议栈、定时器等操作。软中断通常不会阻塞系统其他操作,可以在硬中断之后继续执行。 -
任务队列(tasklet):
任务队列通常用于将一些非紧急且复杂的任务延迟到稍后的处理中。Tasklet 是软中断的一种形式,可以在软中断处理之后继续执行。
二、为何有softirq和tasklet
2.1、为何有top half和bottom half
在 Linux 内核中,softirq
和 tasklet
都是用于延迟处理、避免阻塞的机制,它们的主要目的是在处理硬中断(硬件中断)之后,能够在合适的时机处理一些不那么紧急但需要尽快处理的任务。虽然它们看似有些相似,但它们有不同的设计目标和使用场景。具体来说,softirq
和 tasklet
有以下几方面的区别和联系:
1. softirq
(软中断)的设计目标
softirq
是 Linux 内核为了解决中断上下文中需要处理复杂任务的需求而设计的机制。软中断用于处理一些需要尽早完成、但又不属于硬中断范围内的任务。软中断的执行在硬中断处理完成后、普通进程调度之前进行。
- 高效性:软中断是相对较轻量的,目的是不阻塞其他任务,同时能够尽量避免频繁的进程上下文切换。
- 并行性:一个软中断可以由多个处理器并行执行,不同的 CPU 可以独立处理同一个软中断的不同部分。
- 预定义类型:软中断的种类是固定的,内核提前定义了软中断的种类和它们的处理函数。每个软中断有一个特定的用途(比如网络处理、定时器等),并且每种软中断类型都被分配了一个 ID。
使用场景:
- 网络数据包的接收和处理。
- 定时器、调度器等周期性任务的处理。
- I/O 操作完成后的处理。
2. tasklet
的设计目标
tasklet
是基于 softirq
机制的进一步抽象,它的目的是通过更灵活的方式来延迟一些复杂的处理任务。虽然 tasklet
是一个软中断的一种实现,但它具有一些额外的特性,尤其是在延迟任务的管理上,提供了更细粒度的控制。
- 单线程执行:一个
tasklet
在每个时刻只能由一个 CPU 执行。这是tasklet
和softirq
最大的区别之一。每个tasklet
会在一个 CPU 上串行执行,确保同一个tasklet
不会并发执行。虽然这样可能限制了并行性,但也减少了复杂的同步问题。 - 动态创建:
tasklet
可以动态创建,并且可以与内核的其他机制(例如网络驱动、块设备等)更紧密地集成。开发者可以根据需要自定义tasklet
,以便延迟处理一些特定的任务。 - 优先级和执行顺序:
tasklet
比softirq
更灵活,它的执行顺序是由内核保证的,且可以在同一个上下文中按顺序执行。
使用场景:
- 处理网络数据包。
- 处理与硬件相关的任务,尤其是网络设备或存储设备相关的任务。
- 延迟处理某些非实时性任务,避免阻塞重要的中断处理。
3. softirq
和 tasklet
的关系
- 基于软中断的机制:
tasklet
实际上是softirq
的一个特殊形式,可以看作是软中断的进一步抽象。每个tasklet
是一个在软中断上下文中执行的延迟任务,但它有自己独立的控制流程。 - 任务延迟:虽然
softirq
和tasklet
都用于延迟处理任务,但softirq
主要侧重于内核预定义的任务(如网络、定时器等),而tasklet
则是为更细粒度的延迟任务提供了灵活性,允许开发者动态地创建和控制任务。 - 执行顺序:软中断的处理函数是按照固定的顺序执行的,而
tasklet
的执行顺序通常是由它们的优先级来控制的。
基于下面的系统进一步的进行讨论:
当网卡控制器的FIFO收到的来自以太网的数据的时候,可以将该事件通过irq signal送达Interrupt Controller。Interrupt Controller可以把中断分发给系统中的Processor A or B。
NIC的中断处理过程大概包括:
mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>handle Data in the ram----------->unmask interrupt controller
先假设Processor A处理了这个网卡中断事件,于是NIC的中断handler在Processor A上欢快的执行,这时候,Processor A的本地中断是disable的。NIC的中断handler在执行的过程中,网络数据仍然源源不断的到来,但是,如果NIC的中断handler不操作NIC的寄存器来ack这个中断的话,NIC是不会触发下一次中断的。还好,NIC interrupt handler总是在最开始就会ack,因此,这不会导致性能问题。ack之后,NIC已经具体再次trigger中断的能力。当Processor A上的handler 在处理接收来自网络的数据的时候,NIC的FIFO很可能又收到新的数据,并trigger了中断,这时候,Interrupt controller还没有umask,因此,即便还有Processor B(也就是说有处理器资源),中断控制器也无法把这个中断送达处理器系统。因此,只能眼睁睁的看着NIC FIFO填满数据,数据溢出,或者向对端发出拥塞信号,无论如何,整体的系统性能是受到严重的影响。
要解决上面的问题,最重要的是尽快的执行完中断handler,打开中断,unmask IRQ(或者发送EOI),方法就是把耗时的handle Data in the ram这个步骤踢出handler,让其在bottom half中执行。
2.2、为何有softirq和tasklet
在 Linux 内核中,top half 和 bottom half 是两种不同的中断处理机制,它们被用来优化中断的处理流程,以减少中断对系统性能的影响。
Top half 和 bottom half 主要用来将中断处理任务分为两个部分:第一部分是中断的紧急部分(top half),第二部分是可以稍微延迟处理的任务(bottom half)。这种分离的设计有助于在不影响系统响应的情况下,完成大量复杂的中断处理工作。
1. Top Half(中断的上半部分)
Top half 是指在硬中断上下文中执行的部分。这个部分的任务通常是非常紧急的,它直接与硬件设备的中断相关,需要尽可能快地处理。由于硬中断的执行上下文限制,它只能执行一些简单且直接的操作。
特点:
- 及时性:这部分必须尽快完成,以免影响系统的实时性和响应性。
- 简洁性:它的处理逻辑通常是简短的,避免了复杂的计算或耗时的操作。过于复杂的操作可能会导致其他中断无法及时响应。
- 硬中断上下文:它执行时,系统不能进行进程调度,因此不能进行会阻塞的操作,如内存分配、睡眠等。
示例:
硬中断通常会做一些必须立刻执行的任务,如从硬件设备中读取数据、清除硬件中断标志等。这些任务通常由硬中断处理程序(ISR,Interrupt Service Routine)来完成。
2. Bottom Half(中断的下半部分)
Bottom half 是指在硬中断处理完成后,由内核的软中断或任务队列机制(如 softirq
、tasklet
)处理的部分。这个部分通常不那么紧急,可以稍微延迟执行,但需要尽快执行以避免影响系统的吞吐量。
特点:
- 延迟执行:
bottom half
处理的是一些可以稍微延迟的任务,不会影响系统的即时响应能力。 - 进程调度:与
top half
不同,bottom half
允许进行进程调度、睡眠操作等,因此可以执行更复杂的操作。 - 分离的机制:
bottom half
机制通常是通过软中断(softirq
)或任务队列(tasklet
)来实现的。它们允许在中断服务程序(ISR)完成后异步处理任务。
示例:
- 网络数据包的处理:硬中断处理(top half)可能会接收一个网络数据包,而复杂的网络协议栈处理(如 IP 处理、TCP 重传等)可能会由软中断(
softirq
)或者tasklet
来完成。 - 定时任务的执行:如定时器超时后触发的任务,通常会在
bottom half
中执行。
3. 为什么需要 Top Half 和 Bottom Half 机制?
(1) 减少中断上下文中的开销
中断处理程序执行时,系统不能进行进程调度,因此,如果在硬中断上下文中进行复杂的处理,会导致系统响应变慢、丢失中断,甚至造成死锁或其他问题。因此,需要把复杂的任务推迟到后续执行,避免在中断处理期间阻塞系统。
(2) 提高系统的并发能力和吞吐量
通过将复杂的任务推迟到 bottom half
,可以在多个 CPU 核心上并行处理不同的任务,提高了系统的吞吐量。top half
只做最基本的中断处理,而 bottom half
则负责后续的、可以延迟的任务,避免了在硬中断期间进行过多的计算。
(3) 降低中断延迟和提高响应性
top half
主要执行紧急的任务,确保了中断处理的即时性,使得系统能够快速响应硬件事件。延迟的任务交由 bottom half
处理,避免了中断的过度延迟。
4. Top Half 和 Bottom Half 的实现
在 Linux 内核中,top half
和 bottom half
的实现通常依赖于以下几个机制:
-
硬中断处理程序(Top Half):这是由硬件设备驱动程序提供的,是在中断发生时立即调用的函数。它执行最基础的任务,如清除中断标志、读取设备数据等。此部分的代码执行时间要尽量短,避免影响中断的响应。
-
软中断(Softirq):是
bottom half
的一种实现。内核在硬中断处理之后,会在一个稍微延迟的上下文中执行软中断处理程序。软中断通常会处理一些周期性任务,如网络包的处理。 -
Tasklet:是基于软中断的一种任务延迟处理机制。它是一种轻量级的任务队列,可以确保任务在适当的时机异步执行。
-
工作队列(Workqueues):内核的工作队列机制允许将任务推迟到一个线程上下文中执行,适合那些需要执行更多复杂操作的任务。
进入实际的例子,在引入softirq之后,网络数据的处理如下:
关中断:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>raise softirq------>unmask interrupt controller
开中断:在softirq上下文中进行handle Data in the ram的动作
同样的,我先假设Processor A处理了这个网卡中断事件,很快的完成了基本的HW操作后,raise softirq。在返回中断现场前,会检查softirq的触发情况,因此,后续网络数据处理的softirq在processor A上执行。在执行过程中,NIC硬件再次触发中断,Interrupt controller将该中断分发给processor B,执行动作和Processor A是类似的,因此,最后,网络数据处理的softirq在processor B上执行。
为了性能,同一类型的softirq有可能在不同的CPU上并发执行,这给使用者带来了极大的痛苦,因为驱动工程师在撰写softirq的回调函数的时候要考虑重入,考虑并发,要引入同步机制。但是,为了性能,必须如此。
当网络数据处理的softirq同时在Processor A和B上运行的时候,网卡中断又来了(可能是10G的网卡吧)。这时候,中断分发给processor A,这时候,processor A上的handler仍然会raise softirq,但是并不会调度该softirq。也就是说,softirq在一个CPU上是串行执行的。这种情况下,系统性能瓶颈是CPU资源,需要增加更多的CPU来解决该问题。
如果是tasklet的情况会如何呢?为何tasklet性能不如softirq呢?如果一个tasklet在processor A上被调度执行,那么它永远也不会同时在processor B上执行,也就是说,tasklet是串行执行的(注:不同的tasklet还是会并发的),不需要考虑重入的问题。
进入实际的例子,假设使用tasklet,网络数据的处理如下:
关中断:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>schedule tasklet------>unmask interrupt controller
开中断:在softirq上下文中(一般使用TASKLET_SOFTIRQ这个softirq)进行handle Data in the ram的动作
同样的,先假设Processor A处理了这个网卡中断事件,很快的完成了基本的HW操作后,schedule tasklet(同时也就raise TASKLET_SOFTIRQ softirq)。在返回中断现场前,会检查softirq的触发情况,因此,在TASKLET_SOFTIRQ softirq的handler中,获取tasklet相关信息并在processor A上执行该tasklet的handler。在执行过程中,NIC硬件再次触发中断,Interrupt controller将该中断分发给processor B,执行动作和Processor A是类似的,虽然TASKLET_SOFTIRQ softirq在processor B上可以执行,但是,在检查tasklet的状态的时候,如果发现该tasklet在其他processor上已经正在运行,那么该tasklet不会被处理,一直等到在processor A上的tasklet处理完,在processor B上的这个tasklet才能被执行。
三、softirq基础
3.1、preempt_count
为了更好的理解下面的内容,我们需要先看看一些基础知识:一个task的thread info数据结构定义如下:arch/arm/include/asm/thread_info.h
/*
* low level task data that entry.S needs immediate access to.
* __switch_to() assumes cpu_context follows immediately after cpu_domain.
*/
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 abi_syscall; /* ABI type and syscall nr */
unsigned long tp_value[2]; /* TLS registers */
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
preempt_count这个成员被用来判断当前进程是否可以被抢占。如果preempt_count不等于0(可能是代码调用preempt_disable显式的禁止了抢占,也可能是处于中断上下文等),说明当前不能进行抢占,如果preempt_count等于0,说明已经具备了抢占的条件(当然具体是否要抢占当前进程还是要看看thread info中的flag成员是否设定了_TIF_NEED_RESCHED这个标记,可能是当前的进程的时间片用完了,也可能是由于中断唤醒了优先级更高的进程)。 具体preempt_count的数据格式可以参考下图:
preemption count用来记录当前被显式的禁止抢占的次数,也就是说,每调用一次preempt_disable,preemption count就会+1,调用preempt_enable,该区域的数值会-1。preempt_disable和preempt_enable必须成对出现,可以嵌套,最大嵌套的深度是255。
include/linux/preempt.h
#ifdef CONFIG_PREEMPT //定义抢占
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \ //减1后若是为0,执行抢占
} while (0)
#define preempt_disable() \
do { \
preempt_count_inc(); \ // + 1
barrier(); \
} while (0)
#else /* !CONFIG_PREEMPT */
#define preempt_enable() \
do { \
barrier(); \
preempt_count_dec(); \ //只是减1,没有抢占
} while (0)
#define preempt_disable() barrier()
#endif /* CONFIG_PREEMPT */
hardirq count描述当前中断handler嵌套的深度。对于ARM平台的Linux kernel,其中断部分的代码如下:arch/arm/kernel/irq.c
/*
* handle_IRQ handles all hardware IRQ's. Decoded IRQs should
* not come via this function. Instead, they should provide their
* own 'handler'. Used by platform code implementing C-based 1st
* level decoding.
*/
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irq_desc *desc;
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs))
desc = NULL;
else
desc = irq_to_desc(irq);
if (likely(desc))
handle_irq_desc(desc);
else
ack_bad_irq(irq);
}
说明:
- 处理无效中断:
if (unlikely(!irq || irq >= nr_irqs))
:这里检查中断号irq
是否有效。nr_irqs
是系统中可用的最大中断号。如果irq
为0(通常表示没有中断)或者大于或等于nr_irqs
(表示中断号超出了系统定义的范围),则认为这是一个无效的中断。desc = NULL;
:如果中断无效,将desc
指针设置为NULL
。desc
是指向irq_desc
结构的指针,这个结构包含了处理特定中断所需的信息。
- 获取中断描述符:
else desc = irq_to_desc(irq);
:如果中断有效,通过irq_to_desc
函数将中断号irq
转换为对应的irq_desc
结构指针。
- 处理中断:
if (likely(desc)) handle_irq_desc(desc);
:如果desc
不是NULL
(即成功获取了中断描述符),则调用handle_irq_desc
函数来处理这个中断。handle_irq_desc
函数会根据irq_desc
结构中的信息来执行相应的中断处理流程。else ack_bad_irq(irq);
:如果desc
是NULL
(即遇到了无效的中断),则调用ack_bad_irq
函数来处理这个错误的中断。这个函数通常会记录错误信息,并可能尝试清除错误中断的状态,以防止系统崩溃。
总结来说,handle_IRQ
函数的作用是接收一个中断请求,首先检查这个请求是否有效,然后根据请求的有效性来执行相应的处理流程。对于有效的中断,它会找到对应的中断描述符并处理;对于无效的中断,它会尝试以一种不会导致系统崩溃的方式来处理这个错误。
3.2、task的context
看完了preempt_count之后,介绍下各种context:include/linux/preempt.h
/*
* These macro definitions avoid redundant invocations of preempt_count()
* because such invocations would result in redundant loads given that
* preempt_count() is commonly implemented with READ_ONCE().
*/
#define nmi_count() (preempt_count() & NMI_MASK)
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#ifdef CONFIG_PREEMPT_RT
# define softirq_count() (current->softirq_disable_cnt & SOFTIRQ_MASK)
# define irq_count() ((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | softirq_count())
#else
# define softirq_count() (preempt_count() & SOFTIRQ_MASK)
# define irq_count() (preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK))
#endif
/*
* Macros to retrieve the current execution context:
*
* in_nmi() - We're in NMI context
* in_hardirq() - We're in hard IRQ context
* in_serving_softirq() - We're in softirq context
* in_task() - We're in task context
*/
#define in_nmi() (nmi_count())
#define in_hardirq() (hardirq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#ifdef CONFIG_PREEMPT_RT
# define in_task() (!((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | in_serving_softirq()))
#else
# define in_task() (!(preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
#endif
/*
* The following macros are deprecated and should not be used in new code:
* in_irq() - Obsolete version of in_hardirq()
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
这里首先要介绍的是一个叫做IRQ context的术语。这里的IRQ context其实就是hard irq context,也就是说明当前正在执行中断handler(top half),只要preempt_count中的hardirq count大于0(=1是没有中断嵌套,如果大于1,说明有中断嵌套),那么就是IRQ context。
softirq context并没有那么的直接,一般人会认为当sofirq handler正在执行的时候就是softirq context。这样说当然没有错,sofirq handler正在执行的时候,会增加softirq count,当然是softirq context。
不过,在其他context的情况下,例如进程上下文中,有可能因为同步的要求而调用local_bh_disable,这时候,通过local_bh_disable/enable保护起来的代码也是执行在softirq context中。当然,这时候其实并没有正在执行softirq handler。如果你确实想知道当前是否正在执行softirq handler,in_serving_softirq可以完成这个使命,这是通过操作preempt_count的bit 8来完成的。include/linux/preempt.h
#define in_interrupt() (irq_count())
所谓中断上下文,就是IRQ context + softirq context+NMI context。
四、softirq机制
softirq和hardirq是对应的,因此softirq的机制可以参考hardirq对应理解,当然softirq是纯软件的,不需要硬件参与。
4.1、softirq number
和IRQ number一样,对于软中断,linux kernel也是用一个softirq number唯一标识一个softirq,具体定义如下:include/linux/interrupt.h
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
HI_SOFTIRQ用于高优先级的tasklet,TASKLET_SOFTIRQ用于普通的tasklet。
TIMER_SOFTIRQ是for software timer的(所谓software timer就是说该timer是基于系统tick的)。
NET_TX_SOFTIRQ和NET_RX_SOFTIRQ是用于网卡数据收发的。
BLOCK_SOFTIRQ和BLOCK_POLL_SOFTIRQ是用于block device的。
SCHED_SOFTIRQ用于多CPU之间的负载均衡的。
HRTIMER_SOFTIRQ用于高精度timer的。
RCU_SOFTIRQ是处理RCU的。
4.2、softirq描述符
softirq是静态定义的,也就是说系统中有一个定义softirq描述符的数组,而softirq number就是这个数组的index。这个概念和早期的静态分配的中断描述符概念是类似的。具体定义如下:
include/linux/interrupt.h
/* softirq mask and active fields moved to irq_cpustat_t in
* asm/hardirq.h to get better cache usage. KAO
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
系统支持多少个软中断,静态定义的数组就会有多少个entry。____cacheline_aligned保证了在SMP的情况下,softirq_vec是对齐到cache line的。softirq描述符非常简单,只有一个action成员,表示如果触发了该softirq,那么应该调用action回调函数来处理这个soft irq。对于硬件中断而言,其mask、ack等都是和硬件寄存器相关并封装在irq chip函数中,对于softirq,没有硬件寄存器,只有“软件寄存器”,定义如下:include/asm-generic/hardirq.h
typedef struct {
unsigned int __softirq_pending;
#ifdef ARCH_WANTS_NMI_IRQSTAT
unsigned int __nmi_count;
#endif
} ____cacheline_aligned irq_cpustat_t;
DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
ipi_irqs这个成员用于处理器之间的中断。__softirq_pending就是这个“软件寄存器”。
softirq采用谁触发,谁负责处理的。
例如:当一个驱动的硬件中断被分发给了指定的CPU,并且在该中断handler中触发了一个softirq,那么该CPU负责调用该softirq number对应的action callback来处理该软中断。因此,这个“软件寄存器”应该是每个CPU拥有一个(专业术语叫做banked register)。为了性能,irq_stat中的每一个entry被定义对齐到cache line。
4.3、如何注册一个softirq
通过调用open_softirq接口函数可以注册softirq的action callback函数,具体如下:kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
softirq_vec是一个多CPU之间共享的数据,不过,由于所有的注册都是在系统初始化的时候完成的,那时候,系统是串行执行的。此外,softirq是静态定义的,每个entry(或者说每个softirq number)都是固定分配的,因此,不需要保护。
4.4、如何触发softirq?
在linux kernel中,可以调用raise_softirq这个接口函数来触发本地CPU上的softirq,具体如下:
kernel/softirq.c
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
虽然大部分的使用场景都是在中断handler中(也就是说关闭本地CPU中断)来执行softirq的触发动作,但是,这不是全部,在其他的上下文中也可以调用raise_softirq。因此,触发softirq的接口函数有两个版本,一个是raise_softirq,有关中断的保护,另外一个是raise_softirq_irqoff,调用者已经关闭了中断,不需要关中断来保护“soft irq status register”。
所谓trigger softirq,就是在__softirq_pending(也就是上面说的soft irq status register)的某个bit置一。从上面的定义可知,__softirq_pending是per cpu的,因此不需要考虑多个CPU的并发,只要disable本地中断,就可以确保对,__softirq_pending操作的原子性。
具体raise_softirq_irqoff的代码如下:kernel/softirq.c
/*
* This function must run with irqs disabled!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (!in_interrupt())
wakeup_softirqd();
}
(1)__raise_softirq_irqoff函数设定本CPU上的__softirq_pending的某个bit等于1,具体的bit是由soft irq number(nr参数)指定的。
(2)如果在中断上下文,只要set __softirq_pending的某个bit就OK了,在中断返回的时候自然会进行软中断的处理。但是,如果在context上下文调用这个函数的时候,必须要调用wakeup_softirqd函数用来唤醒本CPU上的softirqd这个内核线程。
4.5、disable/enable softirq
Linux kernel中,可以使用local_irq_disable和local_irq_enable来disable和enable本CPU中断。和硬件中断一样,软中断也可以disable,接口函数是local_bh_disable和local_bh_enable。虽然和想像的local_softirq_enable/disable有些出入,不过bh这个名字更准确反应了该接口函数的意涵,因为local_bh_disable/enable函数就是用来disable/enable bottom half的,这里就包括softirq和tasklet。
先看disable吧,毕竟禁止bottom half比较简单:include/linux/bottom_half.h
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);
barrier();
}
看起来disable bottom half比较简单,就是讲current thread info上的preempt_count成员中的softirq count的bit field9~15加上一就OK了。barrier是优化屏障(Optimization barrier),会在内核同步系列文章中描述。include/linux/bottom_half.h
static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
kernel/softirq.c
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
WARN_ON_ONCE(in_irq()); //-----1
lockdep_assert_irqs_enabled();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_disable();
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
trace_softirqs_on(ip);
/*
* Keep preemption disabled until we are done with
* softirq processing:
*/
preempt_count_sub(cnt - 1); // -----2
if (unlikely(!in_interrupt() && local_softirq_pending())) { // ----3
/*
* Run softirq if any pending. And do it in its own stack
* as we may be calling this deep in a task call stack already.
*/
do_softirq();
}
preempt_count_dec(); //-----4
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_enable();
#endif
preempt_check_resched(); //-----5
}
EXPORT_SYMBOL(__local_bh_enable_ip);
(1)disable/enable bottom half是一种内核同步机制。local_bh_enable/disable是给进程上下文使用的,用于防止softirq handler抢占local_bh_enable/disable之间的临界区的。
(2)在local_bh_disable中为preempt_count增加了SOFTIRQ_DISABLE_OFFSET,在local_bh_enable函数中应该减掉同样的数值。这一步,首先减去了(SOFTIRQ_DISABLE_OFFSET-1),为何不一次性的减去SOFTIRQ_DISABLE_OFFSET呢?考虑下面运行在进程上下文的代码场景:
……
local_bh_disable
……需要被保护的临界区……
local_bh_enable
……
在临界区内,有进程context 和softirq共享的数据,因此,在进程上下文中使用local_bh_enable/disable进行保护。假设在临界区代码执行的时候,发生了中断,由于代码并没有阻止top half的抢占,因此中断handler会抢占当前正在执行的thread。在中断handler中,raise了softirq,在返回中断现场的时候,由于disable了bottom half,因此虽然触发了softirq,但是不会调度执行。因此,代码返回临界区继续执行,直到local_bh_enable。一旦enable了bottom half,那么之前raise的softirq就需要调度执行了,因此,这也是为什么在local_bh_enable会调用do_softirq函数。因此,这里减去了(SOFTIRQ_DISABLE_OFFSET-1),既保证了softirq count的bit field9~15被减去了1,又保持了preempt disable的状态。
(3)如果当前不是interrupt context的话,并且有pending的softirq,那么调用do_softirq函数来处理软中断。
(4)在step 2中少减了1,这里补上,其实也就是preempt count-1。
(5)在softirq handler中很可能wakeup了高优先级的任务,这里最好要检查一下,看看是否需要进行调度,确保高优先级的任务得以调度执行。
4.6、如何处理soft irq
softirq是一种defering task的机制,也就是说top half没有做的事情,需要延迟到bottom half中来执行。那么具体延迟到什么时候呢?soft irq是如何调度执行的?
在上一节已经描述一个softirq被调度执行的场景,中断返回现场时候调度softirq的场景。中断退出的代码,具体如下:kernel/softirq.c
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
lockdep_assert_irqs_disabled();
#endif
account_hardirq_exit(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
if (IS_ENABLED(CONFIG_PREEMPT_RT) && local_pending_timers() &&
!(in_nmi() | in_hardirq()))
wake_timersd();
tick_irq_exit();
}
/**
* irq_exit_rcu() - Exit an interrupt context without updating RCU
*
* Also processes softirqs if needed and possible.
*/
void irq_exit_rcu(void)
{
__irq_exit_rcu();
/* must be last! */
lockdep_hardirq_exit();
}
/**
* irq_exit - Exit an interrupt context, update RCU and lockdep
*
* Also processes softirqs if needed and possible.
*/
void irq_exit(void)
{
__irq_exit_rcu();
ct_irq_exit();
/* must be last! */
lockdep_hardirq_exit();
}
代码中“!in_interrupt()”这个条件可以确保下面的场景不会触发sotfirq的调度:
(1)中断handler是嵌套的。也就是说本次irq_exit是退出到上一个中断handler。当然,在新的内核中,这种情况一般不会发生,因为中断handler都是关中断执行的。
(2)本次中断是中断了softirq handler的执行。也就是说本次irq_exit是不是退出到进程上下文,而是退出到上一个softirq context。这一点也保证了在一个CPU上的softirq是串行执行的(注意:多个CPU上还是有可能并发的)
继续看invoke_softirq的代码:kernel/softirq.c
static inline void invoke_softirq(void)
{
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/
__do_softirq();
#else
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/
do_softirq_own_stack();
#endif
} else {
wakeup_softirqd();
}
}
和上面一样,确定这个cpu中是不是有软中断被硬中断(top)所中断,如果有的话,检查是不是不着急的那种软中断(比如TASKLET_SOFTIRQ或HI_SOFTIRQ),其它如果是TIMER,NET之类的中软之前在运行期间被打断的话,要尽快回去执行,本次的软中断就延时处理了。当然之前的TIMER,NET之类如果是自己主动放弃(即此时状态不是RUNNING),那还是这次可以检查开启一个软中断的。
如果没有强制线程化,softirq的处理也分成两种情况,主要是和softirq执行的时候使用的stack相关。如果arch支持单独的IRQ STACK,这时候,由于要退出中断,因此irq stack已经接近全空,因此直接调用__do_softirq()处理软中断就OK了,否则就调用do_softirq_own_stack函数在softirq自己的stack上执行。当然对ARM而言,softirq的处理就是在当前的内核栈上执行的,因此do_softirq_own_stack的调用就是调用__do_softirq(),代码如下:
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;
/*
* Mask out PF_MEMALLOC s current task context is borrowed for the
* softirq. A softirq handled such as network RX might set PF_MEMALLOC
* again if the socket is related to swap
*/
current->flags &= ~PF_MEMALLOC;
pending = local_softirq_pending(); //-获取softirq pending的状态
account_irq_enter_time(current); //计算中断进入中断起始时间
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); //标识下面的代码是正在处理softirq
in_hardirq = lockdep_softirq_start();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0); //清除pending标志
local_irq_enable(); //执行软中断期间,是可以被硬中断给中断的
h = softirq_vec; //获取软中断描述符指针
while ((softirq_bit = ffs(pending))) { //寻找pending中第一个被设定为1的bit
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1; //指向pending的那个软中断描述符
vec_nr = h - softirq_vec; //获取soft irq number
prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr); //标记软中断被这个cpu已经在执行了
trace_softirq_entry(vec_nr);
h->action(h); //指行softirq handler
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}
rcu_bh_qs();
local_irq_disable(); //关闭本地中断
pending = local_softirq_pending(); //这里见下面分析----1 ---
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current); //记录中断结束时间(可以计算中断执行时间)
__local_bh_enable(SOFTIRQ_OFFSET); //标识softirq处理完毕
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}
分析1:
再次检查softirq pending,有可能上面的softirq handler在执行过程中,发生了中断,又raise了softirq。如果的确如此,那么我们需要跳转到restart那里重新处理soft irq。当然,也不能总是在这里不断的loop,因此linux kernel设定了下面的条件:
(1)softirq的处理时间没有超过2个ms。
(2)上次的softirq中没有设定TIF_NEED_RESCHED,也就是说没有有高优先级任务需要调度。
(3)loop的次数小于 10次。
因此,只有同时满足上面三个条件,程序才会跳转到restart那里重新处理soft irq。否则wakeup_softirqd就OK了。这样的设计也是一个平衡的方案。一方面照顾了调度延迟:本来,发生一个中断,系统期望在限定的时间内调度某个进程来处理这个中断,如果softirq handler不断触发,其实linux kernel是无法保证调度延迟时间的。另外一方面,也照顾了硬件的thoughput:已经预留了一定的时间来处理softirq。
内核中默认定义的软中断都是已经使用了的。因此不应该直接调用open_softirq和raise_softirq函数。