# Linux 软中断原理

前面我们描述了 Linux 对于硬中断的处理原理:

1、注册中断号对应的 irq_desc_t 描述符

2、在描述符中添加 irqaction 回调结构

3、硬中断发生时根据 CPU 提供的中断号,找到 irq_desc_t 数组中对应下标的 irq_desc_t 描述符

4、循环调用该描述符上注册的中断处理函数(包含在 irqaction 结构中)

本节我们将详细描述CPU 的软中断处理过程。我们知道硬中断的处理是需要关闭 CPU 的中断响应的,此时如果中断处理函数长时间占用CPU 处理时间,那么由于 CPU 不在响应任何中断,包括时钟中断、键盘、鼠标等等,那么将会让用户产生非常不好的体验:死机了。所以我们需要将一些处理下放到允许发生中断的上下文中执行,所以 Linux 将整个中断响应过程分为上下两部分:中断上半部(关闭硬中断处理)、中断下版本(也称之为软中断 softirq,此时可以响应硬中断)。

# raise_softirq 函数

该函数用于在硬中断处理过程中,设置软中断标志位,表示后续需要在中断下半部中继续完成未完成的中断处理,其中nr为软中断的标志位,前面我们看到网卡的中断下半部 nr 为 NET_RX_SOFTIRQ(处理接收数据)、NET_TX_SOFTIRQ(处理发送数据)。

void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags); // 关闭硬中断,保证原子性
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr); // 设置软中断标志位 nr 为 1,表示需要处理该位上的软中断
    if (!in_interrupt()) // 若当前已经存在 软中断和硬中断 正在处理,那么直接退出(为何?因为我们根本无需启动 softirqd 内核线程(后面会说)或者自己处理软中断,因为它们会负责处理,中断上半部处理完成后,会回调软中断处理,软中断如果已经在处理 那么更不需要当前进程参与了)
        wakeup_softirqd(); // 否则处理软中断【作者:黄俊 微信:bx_java】
}// 将对应位设置为 1
#define __raise_softirq_irqoff(nr) do { local_softirq_pending() |= 1UL << (nr); } while (0)
#define local_softirq_pending() softirq_pending(smp_processor_id())
#define softirq_pending(cpu)    __IRQ_STAT((cpu), __softirq_pending)unsigned int __softirq_pending; // 整形共 32 位,支持 最多 32 个  软中断标志位// 判断当前是否已经在处理硬中断或者软中断
#define in_interrupt()      (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK))
#define preempt_count() (current_thread_info()->preempt_count)
__s32           preempt_count; // 带符号的整型变量,用于标识当前是否允许抢占、是否正在执行软中断和硬中断等,值必须大于0,值为 0 标识可以在内核态抢占执行(什么是内核抢占?后面文章描述),小于0 那么是 bug 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# wakeup_softirqd 函数

该函数很简单,在 Linux 中 每个 CPU 都存在一个 内核线程 ksoftirqd,该内核线程将负责处理该CPU 上的软中断。为何需要存在该内核线程?考虑下,CPU 处理软中断 是不是 需要占用 被中断的进程的 内核态时间,如果长期占用,那么进程将不再执行任何用户代码,这个后果就不用我说了吧,所以 需要将该处理过程 剥离开,运行 软中断内核线程 与 应用进程 并发执行。

static inline void wakeup_softirqd(void)
{
    // 获取当前 CPU 的 ksoftirqd 内核线程,若它不处于运行状态,那么唤醒【作者:黄俊 微信:bx_java】
    struct task_struct *tsk = __get_cpu_var(ksoftirqd);
    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
}
1
2
3
4
5
6
7

# ksoftirqd 内核线程的创建

创建过程非常明显:

1、MASTER CPU 注册回调函数

2、SLAVE CPU 启动时发布 CPU_ONLINE 事件,监听事件后创建 ksoftirqd 内核线程

asmlinkage void __init start_kernel(void){
    ...
    rest_init();    
}static void rest_init(void)
{
    kernel_thread(init, NULL, CLONE_KERNEL); // 创建 init 1 号进程【作者:黄俊 微信:bx_java】
    unlock_kernel();
    cpu_idle();
}static int init(void * unused){
    ...
    smp_prepare_cpus(max_cpus);
    ...
}static void do_pre_smp_initcalls(void)
{
    ...
    spawn_ksoftirqd(); // 创建当前 CPU 的 ksoftirqd 内核线程
}// CPU 启动时将会回调该通知块的 cpu_callback 函数
static struct notifier_block __devinitdata cpu_nfb = {
    .notifier_call = cpu_callback
};
​
​
__init int spawn_ksoftirqd(void)
{
    cpu_callback(&cpu_nfb, CPU_ONLINE, (void *)(long)smp_processor_id()); // 注册当前 CPU 启动时的动作 CPU_ONLINE(后续会描述 SMP 对称多处理器的启动原理:MASTER 与 SLAVE)
    register_cpu_notifier(&cpu_nfb);
    return 0;
}// 当前CPU启动时回调
static int __devinit cpu_callback(struct notifier_block *nfb,
                  unsigned long action,
                  void *hcpu)
{
    int hotcpu = (unsigned long)hcpu;if (action == CPU_ONLINE) { // 事件类型为 CPU_ONLINE ,那么创建 ksoftirqd 内核线程
        if (kernel_thread(ksoftirqd, hcpu, CLONE_KERNEL) < 0) {
            printk("ksoftirqd for %i failed\n", hotcpu);
            return NOTIFY_BAD;
        }while (!per_cpu(ksoftirqd, hotcpu)) // 等待 ksoftirqd 线程完成启动
            yield();
    }
    return NOTIFY_OK;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# ksoftirqd 函数

该函数为 ksoftirqd 内核线程的执行函数。我们看到这里很简单:当存在软中断时,一直循环处理所有软中断,若不存在软中断可执行或者被设置抢占,那么释放 CPU 切换其他进程执行。

static int ksoftirqd(void * __bind_cpu)
{
    ...
    __set_current_state(TASK_INTERRUPTIBLE); // 设置 ksoftirqd  内核线程状态为 TASK_INTERRUPTIBLE(可中断唤醒注册),刚创建时,肯定为 该状态
    mb(); // 保证状态对其他CPU可见,同时能够读取到后续变量的最新值,也避免了 编译器对当前代码的优化
    __get_cpu_var(ksoftirqd) = current; // 设置当前结构为 ksoftirqd 处理函数
    for (;;) { // 循环处理所有软中断,若不存在任何软中断,那么将自己设置为 TASK_INTERRUPTIBLE状态后 切换到其他进程执行,释放当前CPU的占用
        if (!local_softirq_pending()) // 不存在软中断【作者:黄俊 微信:bx_java】
            schedule();__set_current_state(TASK_RUNNING); // 被唤醒后,设置状态为 运行状态
        while (local_softirq_pending()) { // 循环处理所有挂起的软中断
            do_softirq(); // 处理软中断
            cond_resched(); // 每次执行完成后,看看是否被抢占执行了,也即设置 TIF_NEED_RESCHED 标志位,如果发生抢占,那么切换CPU使用权
        }
        __set_current_state(TASK_INTERRUPTIBLE); // 处理完成后将自己设置为 可中断阻塞状态,当没有软中断执行时,切换 进程
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# do_softirq 函数

该函数我们之前在硬中断的处理末尾看到,同时也在 ksoftirqd 内核线程处理过程中看到。

#define irq_exit() // 硬中断退出时,检测是否发生了软中断,若存在,那么借用当前进程的内核上下文去执行 软中断
do {                                    
        preempt_count() -= IRQ_EXIT_OFFSET;         
        if (!in_interrupt() && softirq_pending(smp_processor_id())) // 硬中断与软中断没有在执行,同时当前 CPU的 软中断存在挂起的软中断,那么执行
            do_softirq();                   
        preempt_enable_no_resched();     // 开启内核抢占机制,但不检测抢占标志位 TIF_NEED_RESCHED
} while (0)
1
2
3
4
5
6
7

该函数的处理过程,很明显,读者自行查看如下描述。

#define MAX_SOFTIRQ_RESTART 10 // 借用当前进程内核上下文处理软中断的最大次数,超过该次数将唤醒 ksoftirqd 
​
asmlinkage void do_softirq(void)
{
    int max_restart = MAX_SOFTIRQ_RESTART; 
    __u32 pending;
    unsigned long flags;if (in_interrupt()) // 存在正在执行的软中断或者硬中断,那么退出(因为它们会负责处理软中断)
        return;local_irq_save(flags); // 关闭硬中断保证操作的原子性
    pending = local_softirq_pending(); // 获取当前挂起的软中断标志位 组合 整形变量【作者:黄俊 微信:bx_java】if (pending) { // 存在挂起的软中断,那么执行它们
        struct softirq_action *h;
        local_bh_disable(); // 关闭中断下版本,也即在 preempt_count 中设置 SOFTIRQ_OFFSET 位,此时后续进入软中断的进程调用 in_interrupt() 函数时,直接退出,保证软中断只有一个正在执行
restart:
        local_softirq_pending() = 0; // 回复软中断处理变量,因为我们当前已经将原来的软中断处理位保存在当前内核栈上
        local_irq_enable(); // local_softirq_pending已经保存了,那么我们只需要遍历执行即可,无需管其他进程是否在后续处理软中断过程中设置标志位,因为那是下一个软中断处理轮次了,所以此时放心的 开启硬中断(再一次表名了关闭硬中断保证原子性)
        h = softirq_vec; // 获取设置的软中断处理函数数组
        do { // 遍历所有待处理的软中断,回调它们的处理函数
            if (pending & 1)
                h->action(h);
            h++; 
            pending >>= 1;
        } while (pending);
        local_irq_disable(); // 处理完本轮次的软中断,那么再次关闭硬中断保证原子性
        pending = local_softirq_pending(); // 获取下一轮次待处理的软中断
        if (pending && --max_restart) // 若未到达最大次数且存在软中断,那么继续执行软中断
            goto restart;
        if (pending) // 否则唤醒 softirqd
            wakeup_softirqd();
        __local_bh_enable(); // 处理完成后 还原软中断的处理位,让其他进程得以处理后续发生的软中断
    }
    local_irq_restore(flags); // 开启硬中断
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37