LV030-软中断
一、软中断请求(softirq)简介
1. 什么是软中断
Linux 的 softirq 机制是与 SMP 紧密不可分的。为此,整个 softirq 机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个 CPU 负责执行它所触发的软中断,而且每个 CPU 都有它自己的软中断触发与控制机制。这个设计思想也使得 softirq 机制充分利用了 SMP 系统的性能和特点。
2. 软中断请求描述符
Linux 在 interrupt.h - include/linux/interrupt.h 头文件中定义了数据结构 softirq_action ,来描述一个软中断请求,如下所示:
struct softirq_action
{
void (*action)(struct softirq_action *);
};Linux 在 softirq.c - kernel/softirq.c 文件中定义了一个全局的 softirq_vec 数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;NR_SOFTIRQS 是枚举类型:
enum
{
HI_SOFTIRQ=0, // 高优先级软中断
TIMER_SOFTIRQ, // 定时器软中断
NET_TX_SOFTIRQ,// 网络传输发送软中断
NET_RX_SOFTIRQ,// 网络传输接收软中断
BLOCK_SOFTIRQ, // 块设备软中断
IRQ_POLL_SOFTIRQ,// 中断轮询软中断
TASKLET_SOFTIRQ, // 任务软中断
SCHED_SOFTIRQ, // 调度软中断
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS // 表示软中断的总数, 用于指示软中断类型的数据
};在这里系统一共定义了 10 个软中断请求描述符,中断号的优先级越小, 代表优先级越高。 在驱动代码中, 我们可以使用 Linux 驱动代码中上述的软中断, 当然我们也可以自己添加软中断。软中断向量 i(0≤i≤9)所对应的软中断请求描述符就是 softirq_vec [i]。这个数组是个系统全局数组,即它被所有的 CPU(对于 SMP 系统而言)所共享。这里需要注意的一点是:每个 CPU 虽然都有它自己的触发和控制机制,并且只执行自己所触发的软中断请求,但是各个 CPU 所执行的软中断服务函数却是相同的,也即都是执行 softirq_vec [ ] 数组中定义的 action 软中断服务函数。
3. 在 linux 中的表现
我们可以用以下命令查看一下现在系统中支持哪些软中断:
cat /proc/softirqs
二、软中断相关操作
1. 中断处理函数?
1.1 注册一个中断处理函数
要使用软中断,必须先使用 open_softirq() 函数注册对应的软中断处理函数,open_softirq() 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}向内核注册一个软中断,其实质是设置软中断向量表相应槽位。
【参数说明】
nr:要开启的软中断。
action:软中断对应的处理函数。
【返回值】 无
1.2 注册的函数怎么执行?
软中断的核心处理函数是 do_softirq(),它处理当前 CPU 上的所有软中断。中间好复杂,暂时没有详细去研究了,在 arm 平台,最终好像调用到 __do_softirq()
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
//......
h = softirq_vec; // 取得软中断向量
while ((softirq_bit = ffs(pending))) {
//......
h->action(h); // 调用软中断
//......
}
//......
}2. 软中断触发机制
2.1 软中断的位图
要实现“谁触发,谁执行”的思想,就必须为每个 CPU 都定义它自己的触发和控制变量。为此,Linux 定义了一个 irq_cpustat_t 数据结构来描述一个 CPU 的中断信息,这个在多个头文件中都有定义:

这里以 hardirq.h - arch/arm/include/asm/hardirq.h 为例:
/* number of IPIS _not_ including IPI_CPU_BACKTRACE */
#define NR_IPI 7
typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;内核中使用这个数据结构定义了一个全局变量 irq_stat:
#ifndef __ARCH_IRQ_STAT
DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat); /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (per_cpu(irq_stat.member, cpu))
#endifirq_stat 的定义我们可以展开看一下:
#define DECLARE_PER_CPU_ALIGNED(type, name) \
DECLARE_PER_CPU_SECTION(type, name, PER_CPU_ALIGNED_SECTION) \
____cacheline_aligned这里牵扯的宏有点多,我就没深挖了,大概是这样的一个变量:
irq_cpustat_t irq_stat[NR_CPUS] __cacheline_aligned;NR_CPUS 为系统中 CPU 个数,这样,每个 CPU 都只操作它自己的中断统计信息结构。假设有一个编号为 id 的 CPU,那么它只能操作它自己的中断统计信息结构 irq_stat[id](0≤id≤NR_CPUS-1),从而使各 CPU 之间互不影响。
2.1.1 怎么确定是哪个 CPU 的?
irq_cpustat_t 中的 IPI 表示处理器间的中断(Inter-Processor Interrupts):
#define NR_IPI 7
typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;2.1.2 软中断位图
内核使用一个名为 irq_cpustat_t.__softirq_pending 的位图来描述软中断,每一个位对应一个软中断。内核提供了一些宏来对这个位图进行操作。
- or_softirq_pending() 用于设置相应的位(位或操作),local_softirq_pending() 用于取得整个位图
#ifndef local_softirq_pending_ref
#define local_softirq_pending_ref irq_stat.__softirq_pending
#endif
#define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref))
#define set_softirq_pending(x) (__this_cpu_write(local_softirq_pending_ref, (x)))
#define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x)))- __raise_softirq_irqoff()用于实现激活软中断。
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}2.2 软中断触发函数
触发软中断的函数为 raise_softirq()
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}raise_softirq()函数激活软中断,参数 nr 为要触发的软中断。这里使用术语“激活”而非“调用”, 是因为在很多情况下不能直接调用软中断。所以只能快速地将其标志为“可执行”,等待未来某一时刻调用。
Tips:为什么“在很多情况下不能直接调用软中断”?试想一下下半部引入的理念,就是为了让上半部更快地执行。 如果在中断程序代码中直接调用软中断函数,那么就失去了上半部与下半部的区别,也就是失去了其存在的意义。
我们来看一下这个 raise_softirq_irqoff() 函数:
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()) // in_interrupt 另一个作用是判断软中断是否被禁用。
wakeup_softirqd(); // wakeup_softirqd 唤醒软中断的守护进程 ksoftirq。
}(1)最重要的,就是置相应的位图,等待将来被处理;
(2)如果此时已经没有在中断上下文中,则立即调用(其实是内核线程的唤醒操作),现在就是将来;
3. 软中断的初始化
软中断必须在编译的时候静态注册! Linux 内核使用 softirq_init() 函数初始化软中断:
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}softirq_init() 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。这个部分在 linux 初始化的时候就做好了,我们不用再去初始化一遍了。
三、中断的下半部
前面我们知道,一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux 内核开始 BH 已经被抛弃了。其实 tasklet 也是一种软中断,下面简单了解下软中断完成下半部的流程吧。
1. 软中断怎么完成中断下半部?
中断的上下半部处理流程如下:

画成流程图就是:

假设硬件中断 A 的上半部函数为 irq_top_half_A ,下半部为 irq_bottom_half_A。
- 硬件中断 A 处理过程中,没有其他中断发生:
(1)一开始, preempt_count = 0;
(2)上述流程图 ①~⑨ 依次执行,上半部、下半部的代码各执行一次。
- 硬件中断 A 处理过程中,又再次发生了中断 A
(1)一开始, preempt_count = 0;
(2)执行到第 ⑥ 时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。注意:这时 preempt_count 等于 1,并且中断下半部的代码并未执行。
(3)CPU 又从 ① 开始再次执行中断 A 的上半部代码:
(4)在第 ① 步 preempt_count 等于 2;
(5)在第 ③ 步 preempt_count 等于 1;
(6)在第 ④ 步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理
注意:第 2 次中断发生后,打断了第一次中断的第 ⑦ 步处理。当第 2 次中断处理完毕, CPU 会继续去执行第 ⑦ 步。 可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是 下半部代码只执行了一次。
所以,同一个中断的上半部、下半部,在执行时是多对一的关系。
- 硬件中断 A 处理过程中,又再次发生了中断 B
(1)一开始, preempt_count = 0;
(2)执行到第 ⑥ 时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。注意:这时 preempt_count 等于 1,并且中断 A 下半部的代码并未执行。
(3)CPU 又从 ① 开始再次执行中断 B 的上半部代码:
(4)在第 ① 步 preempt_count 等于 2;
(5)在第 ③ 步 preempt_count 等于 1;
(6)在第 ④ 步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理
注意:第 2 次中断发生后,打断了第一次中断 A 的第 ⑦ 步处理。当第 2 次中断 B 处理完毕, CPU 会继续去执行第 ⑦ 步。在第 ⑦ 步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。
所以,多个中断的下半部,是汇集在一起处理的。
总结
(1)中断的处理可以分为上半部,下半部
(2)中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
(3)中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行
(4)中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
(5)中断上半部执行完后,触发中断下半部的处理
(6)中断上半部、下半部的执行过程中,不能休眠。(中断休眠的话,以后谁来调度进程?)
四、软中断 demo
1. demo 源码
1.1 linux 内核源码修改
这个我们要添加一个自定义软中断,我们打开 interrupt.h - include/linux/interrupt.h,找到软中断请求描述符的枚举,并添加我们自己的软中断号:

还有一个地方也要改一下,如下图编译驱动的时候会有警告,open_softirq 和 raise_softirq 没有被定义, 但是为什么还会提示这样的错误呢?这是因为 Linux 内核开发者不希望驱动工程师擅自在枚举类型中添加软中断。

我们需要将这两个函数导出到符号表:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
EXPORT_SYMBOL(open_softirq);void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
EXPORT_SYMBOL(raise_softirq);然后重新编译镜像:
# 编译自己移植的开发板镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alpha_emmc_defconfig
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- all -j16 # 全编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核镜像
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- dtbs -j16 # 只编译所有的设备树
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- imx6ull-alpha-emmc.dtb -j16 # 只编译指定的设备树
cp -avf arch/arm/boot/zImage ~/3tftp1.2 驱动源码
然后驱动的源码我们可以看这里:13_interrupt/05_nodts_soft_irq。我们在按键中断中触发一个软中断。
2. 开发板测试
我们更新内核,然后加载驱动:
insmod sdriver_demo.ko
然后我们按下按键,就会发现,按键的中断触发了,软中断也触发了:
