【摘要】本文详解了中断服务下半部机制的基础softirq。首先介绍了其数据结构,分析了softirq的执行时机及流程。接着介绍了软中断的API及如何添加自己的软中断程序,注册及其触发。最后了介绍了用于处理过多软中断的内核线程ksoftirqd,分析了触发ksoftirqd的原则及其执行流程。
【关键字】中断服务下半部,软中断softirq,softirq_action,open_softirq(),raise_softirq,ksoftirqd
1 软中断结构softirq_action. 1
2 执行软中断... 2
3 软中断的API 4
3.1 分配索引号... 4
3.2 软中断处理程序... 5
3.3 注册软中断处理程序... 6
3.4 触发软中断... 6
4 ksoftirqd. 7
4.1 Ksoftirqd的诞生... 7
4.2 启用Ksoftirqd的准则... 8
4.3 Ksoftirqd的实现... 8
1 软中断结构softirq_action
软中断使用得比较少,但其是tasklet实现的基础。而tasklet是下半部更常用的一种形式。软中断是在编译期间静态分配的。它不像tasklet那样能被动态地注册或去除。软中断由softirq_action结构表示,它定义在中:
246struct softirq_action
247{
248 void (*action)(struct softirq_action *);
249 void *data;
250};
Action: 待执行的函数;
Data: 传给函数的参数,任意类型的指针,在action内部转化
kernel/softirq.c中定义了一个包含有32个该结构体的数组。
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断,因为系统靠一个32位字的各个位来标识是否需要执行某个软中断。注意,这是注册的软中断数目的最大值没法动态改变。在当前版本的内核中,这个项中只用到6个。。
2 执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq )。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是,在合适的时刻,该软中断就会运行。在下列地方,待处理的软中断会被检查和执行:
² 从一个硬件中断代码处返回时。
² 在ksoftirqd内核线程中。
² 在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。
不管是用什么办法唤起,软中断都要在do_softirq()中执行。如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。
252#ifndef __ARCH_HAS_DO_SOFTIRQ
253
254asmlinkage void do_softirq(void)
255{
256 __u32 pending;
257 unsigned long flags;
258
259 if (in_interrupt()) //中断函数中不能执行软中断
260 return;
261
262 local_irq_save(flags);
263
264 pending = local_softirq_pending();
265
266 if (pending) //只有有软中断需要处理时才进入__do_softirq
267 __do_softirq();
/////////////////////////
195/*
196 * 最多循环执行MAX_SOFTIRQ_RESTART 次若中断,若仍然有未处理完的,则交由softirqd 在适当的时机处理。需要协调的是延迟和公平性。尽快处理完软中断,但不能过渡影响用户进程的运行。
203 */
204#define MAX_SOFTIRQ_RESTART 10
206asmlinkage void __do_softirq(void)
207{
208 struct softirq_action *h;
209 __u32 pending;
210 int max_restart = MAX_SOFTIRQ_RESTART;
211 int cpu;
212
213 pending = local_softirq_pending();
214 account_system_vtime(current);
215
216 __local_bh_disable((unsigned long)__builtin_return_address(0));
217 trace_softirq_enter();
218
219 cpu = smp_processor_id();
220restart:
221 /* Reset the pending bitmask before enabling irqs */
222 set_softirq_pending(0);
223
224 local_irq_enable();
225
226 h = softirq_vec;
227
228 do {
229 if (pending & 1) {
230 h->action(h);
231 rcu_bh_qsctr_inc(cpu);
232 }
233 h++;
234 pending >>= 1;
235 } while (pending);
236
237 local_irq_disable();
238
239 pending = local_softirq_pending();
240 if (pending && --max_restart)
241 goto restart;
242
243 if (pending)
244 wakeup_softirqd();
245
246 trace_softirq_exit();
247
248 account_system_vtime(current);
249 _local_bh_enable();
250}
//////////////////////////
269 local_irq_restore(flags);
270}
272EXPORT_SYMBOL(do_softirq);
do_softirq检查并执行所有待处理的软中断,具体要做的包括:
1) 用局部变量pending保存softirq_pending()宏的返回值。它是待处理的软中断的32位位图—如果第n位被设置为1,那么第n位对应类型的软中断等待处理。
2) 现在待处理的软中断位图已经被保存,可以将实际的软中断位图清零了
3) 将指针h指向softirq_vec的第一项。
4) 如果pending的第一位被置为1, h->action(h)被调用。
5) 指针加1,所以现在它指向softirq_vec数组的第二项。
6) 位掩码pending右移一位。这样会丢弃第一位,然后让其他各位依次向右移动一个位置。于是,原来的第二位现在就在第一位的位置上了(依次类推)。现在指针h指向数组的第二项,pending位掩码的第二位现在也到了第一位上。重复执行上面的步骤。
一直重复下去,直到pending变为0,这表明已经没有待处理的软中断了,我们的任务也就完成了。注意,这种检查足以保证h总指向softirq_vec的有效项,因为pending最多只可能设置32位,循环最多也只能执行32次。
一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断—甚至是相同类型的软中断—可以在其他处理器上同时执行。因此对于SMP,软中断处理函数需要考虑多处理器的竞争。
实际上在执行此步操作时需要禁止本地中断。如果中断不被屏蔽,在保存位图和清除它的间隙,可能会有一个新的软中断被唤醒,这可能会造成对此待处理的位进行不应该的清除。
3 软中断的API
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统—网络和SCSI—直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。如果你想加入一个新的软中断,首先应该问问自己为什么用tasklet实现不了。tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,而且它们的性能也非常不错。当然,对于时间要求严格并能自己高效地完成加锁工作的应用,软中断会是正确的选择。
3.1 分配索引号
在编译期间,可以通过中定义的一个枚举类型来静态地声明软中断。
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ
};
内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号大的软中断之前执行。
tasklet类型列表
建立一个新的软中断必须在此枚举类型中加入新的项。而加入时不能像在其他地方一样,简单地把新项加到列表的末尾。相反,必须根据希望赋予它的优先级来决定加入的位置。习惯上,HI_ SOFTIRQ通常作为第一项,而TASKLET_ SOFTIRQ作为最后一项。新项可能插在网络相关的那些项之后、TASKLET_SOFTIRQ之前。
3.2 软中断处理程序
软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action*)
当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其惟一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数:
My_softirq->action(my_softirq)
当你看到内核把整个结构体都传递给软中断处理程序而不仅仅是传递数据值的时候,你可能会很吃惊。这个小技巧可以保证将来在结构体中加入新的域时,无须对所有的软中断处理程序都进行变动。如果需要,软中断处理程序可以方便地解析它的参数,从数据成员中提取数值。
同一个软中断可以同时在多个CPU上运行,而同一CPU上可以同时运行多个软中断,因此软中断处理程序在访问共享资源时应该实现各种互斥机制。
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个软中断处理程序运行的时候,当前处理器上的软中断被禁止,但其他的处理器仍可以执行别的软中断。实际上,如果同一个软中断在它被执行的同时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这意味着任何共享数据—甚至是仅在软中断处理程序内部使用的全局变量—都需要严格的锁保护。这点很重要,它也是为什么tasklet更受青睐的原因。单纯地禁止你的软中断处理程序同时执行不是很理想。如果仅仅通过互斥的加锁方式来防止它自身的并发执行,那么使用软中断就没有任何意义。因此,大部分软中断处理程序都通过采取单处理器数据(仅属于某一个处理器的数据,因此根本不需要加锁)或其他一些技巧来避免显式地加锁,从而提供更出色的性能。
引入软中断的主要原因是其可扩展性。如果不需要扩展到多个处理器,那么,就使用tasklet吧。tasklet本质上也是软中断,只不过同一个处理程序的多个实例不能在多个处理器上同时运行。
3.3 注册软中断处理程序
接着,在运行时通过调用open_softirq()注册软中断处理程序。
326void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
327{
328 softirq_vec[nr].data = data;
329 softirq_vec[nr].action = action;
330}
该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:
open_softirq(NET_TX_ SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_ SOFTIRQ, net_rx_action, NULL);
3.4 触发软中断
通过在枚举类型的列表中添加新项以及调用open_softirq进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do softirq()函数时投入运行。
317void fastcall raise_softirq(unsigned int nr)
318{
319 unsigned long flags;
320
321 local_irq_save(flags);
322 raise_softirq_irqoff(nr);
//////////////////////////////////////////////raise_softirq_irqoff
298inline fastcall void raise_softirq_irqoff(unsigned int nr)
299{
300 __raise_softirq_irqoff(nr);
//////////////////////////////__raise_softirq_irqoff
255#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
187#define set_softirq_pending(x) (local_softirq_pending() = (x))
188#define or_softirq_pending(x) (local_softirq_pending() |= (x))
///////////////////////////__raise_softirq_irqoff
302 /*
303 * 如果当前在中断或者软中断中,直接返回,否则唤醒ksoftirqd
304 */
311 if (!in_interrupt())
312 wakeup_softirqd();
///////////////////////////wakeup_softirqd
55static inline void wakeup_softirqd(void)
56{
57 /* Interrupts are disabled: no need to stop preemption */
58 struct task_struct *tsk = __get_cpu_var(ksoftirqd);
59
60 if (tsk && tsk->state != TASK_RUNNING)
61 wake_up_process(tsk); //唤醒ksoftirqd内核线程
62}
//////////////////////////wakeup_softirqd
313}
314
315EXPORT_SYMBOL(raise_softirq_irqoff);
//////////////////////////////////////////////raise_softirq_irqoff
323 local_irq_restore(flags);
324}
该函数在触发一个软中断之前先要禁止中断,触发后再恢复回原来的状态。如果中断本来就已经被禁止了,那么可以调用另一函数raise_softirq_irqoff,这会带来一些优化效果。
在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq()函数。于是软中断开始执行中断处理程序留给它去完成的剩余任务。
4 ksoftirqd
4.1 Ksoftirqd的诞生
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。
对于软中断,内核会选择在几个特殊时机进行处理。而在中断处理程序返回时处理是最常见的。软中断被触发的频率有时可能很高(像在进行大流量的网络通信期间)。更不利的是,处理函数有时还会自行重复触发。也就是说,当一个软中断执行的时候,它可以重新触发自己以便再次得到执行(事实上,网络子系统就会这么做)。如果软中断本身出现的频率就高,再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器时间,因而处于饥饿状态。而且,单纯的对重新触发的软中断采取不立即处理的策略,也无法让人接受。当软中断最初提出时,就是一个让人进退维谷的问题,巫待解决,而直观的解决方案都不理想。首先,就让我们看看两种最容易想到的直观的方案。
第一种方案是只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这样做可以保证对内核的软中断采取即时处理的方式,关键在于,对重新触发的软中断也会立即处理。当负载很高的时候这样做就会出问题,此时会有大量被触发的软中断,而它们本身又会重复触发。系统可能会一直处理软中断,根本不能完成其他任务。用户空间的任务被忽略了—实际上,只有软中断和中断处理程序轮流执行,而系统的用户只能等待。只有在系统永远处于低负载的情况下,这种方案才会有理想的运行效果;只要系统有哪怕是中等程度的负载量,这种方案就无法让人满意。用户空间根本不能容忍有明显的停顿出现。
第二种方案选择不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理它们。但是,任何自行重新触发的软中断都不会马上处理,它们被放到下一个软中断执行时机去处理。而这个时机通常也就是下一次中断返回的时候,这等于是说,一定得等一段时间,新的(或者重新触发的)软中断才能被执行。可是,在比较空闲的系统中,立即处理软中断才是比较好的做法。很不幸,这个方案显然又是一个时好时坏的选择。尽管它能保证用户空间不处于饥饿状态,但它却让软中断忍受饥饿的痛苦,而根本没有好好利用闲置的系统资源。
在设计软中断时,开发者要意识到需要一些折中。最终在内核中实现的方案是不会立即处理重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证在软中断负担很重的时候用户程序不会因为得不到处理时间而处于饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断处理得非常迅速(因为仅存的内核线程肯定会马上调度)。
4.2 启用Ksoftirqd的准则
__do_softirq中若经过MAX_SOFTIRQ_RESTART次循环后,仍然有新的软中断需要执行,则激活ksoftirqd软中断内核线程,将其余的软中断交由ksoftirqd处理。这样可以防止用户进程常时间得不到执行。但是若不处理这些软中断的话,最长到下次tick中断才能执行这些软中断,即最大延时达到1/HZ。这样内核可以在无其他用户进程需要运行的时候调度ksoftirqd线程。
55static inline void wakeup_softirqd(void)
56{
57 /* Interrupts are disabled: no need to stop preemption */
58 struct task_struct *tsk = __get_cpu_var(ksoftirqd);
59 //若当前ksoftirqd已经是TASK_RUNNING,则返回,ksoftirqd适当时刻会被调度
60 if (tsk && tsk->state != TASK_RUNNING)
61 wake_up_process(tsk);
62}
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫ksoftirad/0和ksoftirad/l。
4.3 Ksoftirqd的实现
为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。这样可以充分利用SMP机器的性能。一旦该线程被初始化,它就会死循环等待处理过剩的软中断。
470static int ksoftirqd(void * __bind_cpu)
471{
472 set_user_nice(current, 19);
473 current->flags |= PF_NOFREEZE;
474
475 set_current_state(TASK_INTERRUPTIBLE);
476
477 while (!kthread_should_stop()) {
478 preempt_disable();
479 if (!local_softirq_pending()) {
480 preempt_enable_no_resched();
481 schedule(); //没有待处理软中断,让出CPU
482 preempt_disable();
483 }
484 //确保ksoftirqd为TASK_RUNNING状态
485 __set_current_state(TASK_RUNNING);
486
487 while (local_softirq_pending()) {
488 /* Preempt disable stops cpu going offline.
489 If already offline, we'll be on wrong CPU:
490 don't process */
491 if (cpu_is_offline((long)__bind_cpu))
492 goto wait_to_die;
493 do_softirq();
494 preempt_enable_no_resched();
495 cond_resched();
496 preempt_disable();
497 }
498 preempt_enable();
499 set_current_state(TASK_INTERRUPTIBLE);
500 }
501 __set_current_state(TASK_RUNNING);
502 return 0;
503
504wait_to_die:
505 preempt_enable();
506 /* Wait for kthread_stop */
507 set_current_state(TASK_INTERRUPTIBLE);
508 while (!kthread_should_stop()) {
509 schedule();
510 set_current_state(TASK_INTERRUPTIBLE);
511 }
512 __set_current_state(TASK_RUNNING);
513 return 0;
514}
只要有待处理的软中断(由soflirq_pending()函数负责发现),ksoftirq就会调用do_softirq去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。如果有必要的话,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行。
只要do_softirqQ函数发现已经执行过的内核线程重新触发了它自己,软中断内核线程就会被唤醒