Linux 中的软中断机制 gaunthan Posted on Mar 15 2017 ? Linux Kernel ? ## 概述 软中断,顾名思义,是由软件产生的中断。不同于硬件中断由硬件发出中断、打断处理器,软中断一般来说是由某个过程发起的,随后打断目的进程的执行,使其执行注册的软中断处理程序。 为什么要有软中断这种东西呢?这涉及到Linux对中断处理过程的实现。Linux将中断处理分成了两个部分:**上半部**(top-half)和**下半部**(bottom-half)。上半部完成与硬件紧密相关的工作,一般来说需要关闭中断执行(关闭所在中断线或本地中断);下半部完成可以在稍后执行的、不那么迫切的工作。由于下半部可以推后执行,因此可以让这部分代码委托给一个进程执行。为了能够告知该进程下半部的执行时机,就需要一种机制,能够中断这个进程,使它执行相应的处理函数。这就是软中断机制的目的。 实际上,下半部更常用的是tasklet,但是它也是由软中断实现的,因此研究软中断机制也是非常有用的。软中断的代码位于 kernel/softirq.c 文件中。 ## 软中断的实现 软中断是在编译期静态分配的,它不像tasklet那样可以被动态注册或注销。软中断由一个特有的结构`softirq_action`表示,它定义在`<linux/interrupt.h>`中: struct softirq_action { void (*action)(struct softirq_action *); }; 软中断的可用数量是在编译时期固定的,在 kernel/softiqr.c 中定义了一个与之相关的数组: static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; 其中,NR_SOFTIRQS 是已经使用的软中断的数量,也定义在该文件中: ```c /* PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequency threaded job scheduling. For almost all the purposes tasklets are more than enough. F.e. all serial device BHs et al. should be converted to tasklets, not to softirqs. */ enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS }; ``` 每个被注册的软中断都占据该数组的一项。rtasklet由其中的HI_SOFTIRQ和TASKLET_SOFTIRQ代表。 ### 软中断处理程序 软中断处理程序action的函数原型如下: void softirq_handler(struct softirq_action *); 当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向osftirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数: my_softirq->action(my_softirq); 这里传递my_softirq而不传递具体值,是因为使用一个聚类指针可以确保将来结构体中加入新的成员时,不用对所有的软中断处理程序进行接口的变更。如果需要,软中断处理程序只需从数据成员中提取数值。 **一个软中断不会抢占另一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断(甚至是相同类型的软中断)可以在其他处理器上同时执行。** ### 执行软中断 一个注册的软中断必须在被标记后才会执行。这被称为**触发软中断**(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行。于是,在合适的时刻,该软中断就会运行。该触发接口为: void raise_softirq(unsigned int nr); 其中`nr`是软中断处理程序在数组中的对应下标。 ## 软中断的处理时机 待处理的软中断在下面这些地方会被检查和执行: - 从一个硬件中断代码返回时; - 在`ksoftirq`内核线程中; - 在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。 不管用什么方法唤起,软中断都是在`do_softirq()`中得到执行的。该函数会遍历软中断请求位图,执行已请求的中断处理程序,同时还会清空该标志。下面是其简化后的核心代码: ```c pending = local_softirq_pending(); /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); do { if (pending & 1) { h->action(h); } h++; pending >>= 1; } while (pending); ``` ## 使用软中断 软中断保留给系统中对时间要求最严格以及最重要的下半部使用。如果你想加入一个新的软中断,首先应该问问自己为什么用tasklet实现不了。tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,同时它们的性能也很不错。当然,对于时间要求严格并且能自己高效地完成加锁工作的应用,软中断会是正确的选择。 ### 分配索引 在编译期间,通过在`<linux/interrupt.h>`中定义的一个枚举类型来静态地声明软中断。内核用从0开始的索引来表示一种相对优先级。索引号小的软中断先执行。 建立一个新的软中断,需要在此枚举类型中加入新的项。注意不能简单地在尾部添加,你必须根据希望赋予它的优先级来决定加入的位置。 ### 注册处理程序 接着,在运行时通过调用`open_softirq()`注册中断处理程序: ```c void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; } ``` 软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他的处理器仍可以执行别的软中断。实际上,如果同一个软中断在它被执行的同时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这意味着任何共享数据都需要严格的锁保护。这点很重要,它也是tasklet更受青睐的原因。单纯地禁止你的软中断处理程序同时执行不是很理想。如果仅仅通过互斥的加锁方式防止它自身的并发执行,那么使用软中断就没有任何意义了。因此,大部分软中断处理程序,都采用单处理器数据或其他一些技巧来避免显式加锁,从而提供更出色的性能。 引入软中断的主要原因是其扩展性。如果不需要扩展到多个处理器,那么就使用tasklet吧。tasklet本质也是软中断,只不过同一个处理程序的多个实例不能在多个处理器上同时运行。 ### 触发软中断 前面提到过,可以使用`raise_softirq()`函数将一个软中断设置为挂起状态,让它在下次调用`do_softirq()`函数时投入运行。这个函数的调用过程大致如下,为了方便,我把它调用的其他函数的定义在其下方给出: ```c 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); /* * 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(); } static inline void __raise_softirq_irqoff(unsigned int nr) { trace_softirq_raise(nr); or_softirq_pending(1UL << nr); } ``` 在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后推出。内核在执行完中断处理程序以后,马上就会调用`do_softirq()`函数。于是软中断开始执行中断处理程序留给它去完成的剩余任务。在这个例子中,“上半部”和“下半部”名字的含义一目了然。 ## ksoftirqd ### 引进原因 前面曾提到过,对于软中断,内核会选择在几个特殊时机进行处理。为什么要区分几个时机呢?要知道,软中断被触发的频率有时可能很高,而且它可能还会自行重复触发,这可能会导致用户空间饥饿。 对这种时机的调用方案最简单的一种是只要有软中断被触发并等待处理,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这样做可以保证内核的软中断采取即时处理的方式。但是,当负载很高时,就会出现上面提到的用户空间饥饿的问题。 第二种方案是不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理它们。但是,任何自行重新触发的软中断都不会马上处理,它们被放到下一个软中断执行时去处理。而这个时机通常也就是下一次中断返回的时候,这等于是说,一定得等一段时间,新的(或者重新触发的)软中断才能被执行。显然,在比较空闲的系统中,这不是一种好做法。 看来只能做些折中了。软中断的设计者最终实现的方案是不立即处理重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟一些其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证在软中断负担很重的时候,用户程序不会因为得不到处理时间而处于饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断处理非常迅速(因为仅存的内核线程肯定会马上调度)。 ### 执行过程 每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就会有两个这样的线程,分别叫ksoftirq/0和ksoftirq/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦该线程被初始化,它就会执行下面这个函数: ```c static int run_ksoftirqd(void * __bind_cpu) { set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { preempt_disable(); if (!local_softirq_pending()) { preempt_enable_no_resched(); schedule(); preempt_disable(); } __set_current_state(TASK_RUNNING); while (local_softirq_pending()) { /* Preempt disable stops cpu going offline. If already offline, we'll be on wrong CPU: don't process */ if (cpu_is_offline((long)__bind_cpu)) goto wait_to_die; local_irq_disable(); if (local_softirq_pending()) __do_softirq(); local_irq_enable(); preempt_enable_no_resched(); cond_resched(); preempt_disable(); rcu_note_context_switch((long)__bind_cpu); } preempt_enable(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0; wait_to_die: preempt_enable(); /* Wait for kthread_stop */ set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { schedule(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0; } ``` 每次迭代后都会进程调度,以便让更重要的进程得到处理机会。只有`__do_softirq()`函数发现已经执行过的内核线程重新触发了它自己,就会通过调用`wakeup_softirqd()`来唤醒软中断内核线程。 ### 查看已注册的ksoftirqd 可以使用`ps -A|grep ksoftirq`命令查看:  由于我的CPU是双核四线程的,相当于有四个逻辑核,因此有四个这样的内核线程。 ## References - RobertLove, 拉芙, 陈莉君,等. Linux内核设计与实现[M]. 机械工业出版社, 2011. - Linux 2.6.39 Kernel Source 赏 Wechat Pay Alipay 构造并发程序的方法 Linux 压缩/解压缩命令