提示
本篇章将详细介绍软中断。
一、软中断的定义与背景
在操作系统中,中断机制是硬件和系统内核之间沟通的重要方式。中断主要分为两种:
- 硬中断:由硬件设备直接触发,比如网卡收到数据或键盘按下时,会立刻通知 CPU 有紧急任务需要处理。这类中断必须快速响应,但处理时会暂时关闭其他中断,避免任务被打断。
- 软中断:是操作系统自己设计的机制。它的作用是把硬中断里复杂耗时的工作"先记下来",等合适的时机再慢慢处理。
软中断解决的问题
- 硬中断处理期间系统无法响应其他中断,容易造成"堵车"
- 复杂任务卡在硬中断里会拖慢系统整体反应速度
举个例子:当你下载文件时,网卡通过硬中断告诉 CPU "有新数据来了",然后软中断负责把数据整理到存储空间。
1、软中断的实现原理
软中断的核心思想是通过一种轻量级的任务调度机制,在硬中断上下文之外完成那些不需要立即执行但又不能完全延后的任务。Linux 内核定义了一张软中断向量表,其中每个软中断类型对应一个唯一的编号以及一个具体的处理函数:
struct softirq_action {
void (*action)(struct softirq_action *);
};2
3
软中断向量表的定义如下:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;2、软中断的种类及其应用场景
目前 Linux 内核定义了以下 10 种软中断类型:
| 软中断类型 | 描述 |
|---|---|
| HI_SOFTIRQ | 高优先级的小任务软中断 |
| TIMER_SOFTIRQ | 定时器软中断 |
| NET_TX_SOFTIRQ | 网络栈发送报文的软中断 |
| NET_RX_SOFTIRQ | 网络栈接收报文的软中断 |
| BLOCK_SOFTIRQ | 块设备软中断 |
| IRQ_POLL_SOFTIRQ | 支持 I/O 轮询的块设备软中断 |
| TASKLET_SOFTIRQ | 低优先级的小任务软中断 |
| SCHED_SOFTIRQ | 调度软中断,用于多处理器负载均衡 |
| HRTIMER_SOFTIRQ | 高精度定时器软中断 |
| RCU_SOFTIRQ | RCU 软中断 |
3、软中断的执行流程
三个阶段
- 触发阶段:软中断通常由硬中断处理程序触发。
- 调度阶段:软中断的实际执行依赖于内核的软中断调度机制。
- 执行阶段:
do_softirq()是软中断的核心执行入口。
二、注册软中断的处理函数
open_softirq() 函数的核心功能是为指定的软中断编号设置对应的处理函数:
void open_softirq(int nr, void (*action)(struct softirq_action *));参数解析:
nr:表示软中断的编号action:指向软中断处理函数的指针
设计要求
- 可重入性:软中断处理函数可能会被多个 CPU 并发调用,因此必须是可重入的。
- 临界区保护:必须使用锁机制(如自旋锁或互斥锁)来保护临界区。
- 高效性:处理函数应尽可能简洁高效。
三、触发软中断
函数 raise_softirq 用来触发软中断,参数是软中断编号:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}2
3
4
5
6
7
raise_softirq_irqoff() 是在已经禁止中断的情况下调用函数来触发软中断:
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt() && should_wake_ksoftirqd())
wakeup_softirqd();
}2
3
4
5
6
四、执行软中断
内核执行软中断的地方如下:
- 在中断处理程序的后半部分执行软中断,对执行时间有限制:不能超过 2 毫秒,并且最多执行 10 次。
- 每个处理器有一个软中断线程,调度策略是
SCHED_NORMAL,优先级是 120。 - 开启软中断的函数
local_bh_enable()。
1、中断处理程序执行软中断
在中断处理程序的后半部分,调用函数 irq_exit() 以退出中断上下文,处理软中断:
// kernel/softirq.c
void irq_exit(void)
{
__irq_exit_rcu();
rcu_irq_exit();
lockdep_hardirq_exit();
}
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();
tick_irq_exit();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__do_softirq 是执行软中断的核心函数,主要逻辑:
- 把局部变量
pending设置为当前处理器的待处理软中断位图 - 把当前处理器的待处理软中断位图重新设置为 0
- 开启硬中断
- 从低位向高位扫描待处理软中断位图,依次执行每个软中断处理函数
- 禁止硬中断
- 如果软中断处理函数又触发软中断,根据时间限制处理或唤醒软中断线程
2、软中断线程
每个处理器有一个软中断线程,名称是 ksoftirqd/ 后面跟着处理器编号。软中断线程的核心函数是 run_ksoftirqd():
static void run_ksoftirqd(unsigned int cpu)
{
ksoftirqd_run_begin();
if (local_softirq_pending()) {
__do_softirq();
ksoftirqd_run_end();
cond_resched();
return;
}
ksoftirqd_run_end();
}2
3
4
5
6
7
8
9
10
11
五、抢占计数器
每个进程都有一个"抢占计数器"(preempt_count):
- 当计数器是 0 时,进程可以被其他更紧急的任务打断
- 当计数器大于 0 时,进程不会被中断
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 42
3
4
各种场景分别利用各自的位禁止或开启抢占:
| 场景 | 函数 |
|---|---|
| 普通场景(PREEMPT_MASK) | preempt_disable() / preempt_enable() |
| 软中断场景(SOFTIRQ_MASK) | local_bh_disable() / local_bh_enable() |
| 硬中断场景(HARDIRQ_MASK) | _irq_enter() / _irq_exit() |
| 不可屏蔽中断场景(NMI_MASK) | nmi_enter() / nmi_exit() |
// include/linux/preempt.h
#define in_nmi() (nmi_count())
#define in_hardirq() (hardirq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_task() (!(in_nmi() | in_hardirq() | in_serving_softirq()))2
3
4
5
六、禁止/开启软中断
如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中断。禁止软中断的函数是 local_bh_disable():
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
#define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET)2
3
4
5
6
设计原因
为什么禁止软中断的函数 local_bh_disable() 把抢占计数器的软中断计数加 2,而不是加 1?目的是区分禁止软中断和正在执行软中断这两种情况。如果软中断计数是奇数,可以确定正在执行软中断。