中断下半部处理之软中断
1.内核为什么要把中断分为上半部和下半部
在Linux内核中,为了在中断执行时间尽可能短和中断处理需要完成大量工作之间找到一个平衡点,Linux将中断处理程序分为两个部分:上半部和下半部。中断处理程序的上半部接受到一个中断时就立即执行,但是只做比较紧急的工作,这些工作都是在所有中断被禁止的情况下完成的,所以要快,否则其他的中断就得不到及时的处理。哪些耗时又不紧急的工作被推迟到下半部去做。中断处理程序的下半部分几乎做了中断处理程序所有的事情。它们最大的不同是上半部分不可中断,而下半部分可以中断。在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。
1.1.上半部和下半部如何划分
对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,不过还是有一些习惯供参考:
- 如果该任务对时间比较敏感,将其放在上半部中执行。
- 如果该任务和硬件相关,一般放在上半部中执行。
- 如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。
- 其他不太紧急的任务, 一般考虑在下半部执行。
2.Linux内核实现下半部的机制
Linux实现下半部机制主要有软中断,tasklet(小任务),工作队列三种方式。
2.1软中断
软中断是随着SMP的出现而出现的,它也是tasklet实现的基础。在早期的BH(已经被内核移除)机制中有两个明显的缺陷:一是系统中一次只能有一个cpu可以执行BH代码,二是BH函数不允许嵌套。这在单处理器系统中或许没有关系,但是在SMP系统中是致命的缺陷。软中断机制和SMP是紧密相联的,在内核中,每个cpu都单独负责它自己所触发的软中断,即同一种类型的软中断可以在不同的处理器上运行,这极大的提高了SMP系统的性能。
在内核中使用结构体softirq_action结构体来描述一个软中断的请求:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
其中,唯一的成员函数指针action指向软中断请求的服务函数。系统定义了一个全局的软中断表softirq_vec[32],对应32个softirq_action结构体来表示软中断的描述符。到目前为止,Linux并没有使用到这32个软中断向量,在内核中只使用了10个这样的软中断向量,看下内核中的定义,通过枚举类型:
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
};
其中HI_SOFTIRQ表示最高优先级的软中断,而TASKLET_SOFTIRQ则用于实现诸如tasklet这样一般性的软中断。事实上,内核预定义的软中断向量已经可以满足大多数应用的需求,其他的向量保留给今后内核扩展的使用,我们不应该去使用它,我们也可以借助crash工具来看看内核中已经使用的中断向量。
除了上面的数据结构外,还有两个数据结构比较重要:
一个是thread_info结构体中的抢占计数器perrmpt_count字段
struct thread_info{
__u32 cpu;
int preempt_count;
.......
.......
}
- bits 0-7:这个计数器表示在内核代码中禁用本地内核抢占的次数,等于0表示允许内核抢占。
- bits 8-15:可延迟函数被禁用的程度,为0表示可延迟函数处于激活状态。
- bits 16-25:本地CPU上中断处理程序的嵌套数,嵌套(nested)数还受堆栈大小的限制,所以不一定能达到1024。
- bit 26:是否处于不可屏蔽中断(NMI)上下文中。
- bit 28:PREEMPT_ACTIVE标记,在进程调度的时候表示进程是通过抢占而被调度的
在中断处理上半部执行完毕后(irq_exit函数)中会使用in_intereupt检查current_thread_info()->preempt_count字段的硬中断计数器和软中断计数器,只要这两个计数器中的一个值为正数,该宏就I产生一个非零值,否则产生一个零值,产生非零值时,就会调用软中断。
另一个重要的数据结构是irq_cpustat_t
在每个cpu上都有一个32位掩码,用来描述软中断的挂起状态,它存放在irq_cpustat_t数据结构中的__softirq_pending字段中
typedef struct {
unsigned int __softirq_pending;
unsigned int local_timer_irqs;
} ____cacheline_aligned irq_cpustat_t;
通过查看__siftirq_pending成员的值就可以知道哪些软中断正等待被处理,内核为我们提供的获取这个成员的函数为local_softirq_pending().
2.2使用软中断
软中断时保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个字系统(网络和SISI)直接使用软中断,因此,在选用软中断时,应该考虑好为啥必须使用软中断而不使用其他的处理方式。下面就来看下软中断的使用流程;
1.分配索引
在编译期间,通过<linux/inerrupt.h>中定义的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示优先级。所以建立一个新的软中断必须在此枚举类型中加入新的项,当然你要想好自己的优先级。
2.注册你的处理程序
通过open_softirq函数注册一个软中断的处理程序,其实就是设置中断向量表相应的位置:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
3.触发你的软中断
通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下一次调用do_softirq()函数时投入运行。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
该函数在触发一个软中断前要禁止中断,触发后再恢复原来的状态。会调用raise_softirq_irqoff(nr);去挂起该软中断。
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();
}
宏__raise_softirq_irqoff是or_softirq_pending的包裹:
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
宏or_softirq_pending用于设置相应的位(位或操作):
#define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
local_softirq_pending用于取得整个位图(而非某一位):
#define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)
2.3软中断的执行
通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是,在合适的时刻,该软中断就会运行。在下列地方,待处理的软中断会被检查和执行:
- 1.处理完一个硬件中断以后
- 2.在ksoftirqd内核线程中,这个线程是当系统发送了很多的软中断时,那必然会占用很长的时间,这个时候内核会让这个线程去处理软中断。
- 3.在那些现实检查和执行带处理的软中断的代码中。
当检查到这样一个检查点(local_softirq_pending()不为0)的时候,表示有挂起的软中断需要被执行,内核就会调用一个do_softirq()函数来处理他们
#define do_softirq_onstack() __do_softirq()
#endif /* CONFIG_IRQSTACKS */
void do_softirq(void)
{
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
if (local_softirq_pending())
do_softirq_onstack();
local_irq_restore(flags);
}
in_interrupt():如果其值为1,则函数返回,这种情况说明要么在中断上下文中调用了do_softirq(),要么当前禁用软中断。
local_irq_save():保存IF标志的状态值,并禁用本地CPU上的中断。可以发现执行完毕软中断后会调用local_irq_restore()恢复状态值。
local_softirq_pending(),获取到当前被挂起的软中断,进入do_softirq_onstack(),可以发现这是个宏,最终调用的是__do_softirq()函数。
asmlinkage void __do_softirq(void)
{
................
................
pending = local_softirq_pending();
......................
........................
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);
trace_softirq_entry(h, softirq_vec);
h->action(h);
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
lockdep_softirq_exit();
account_system_vtime(current);
__local_bh_enable(SOFTIRQ_OFFSET);
}
#ifndef __ARCH_HAS_DO_SOFTIRQ
这里只列出了这个函数的最重要的部分:
(1)用局部变量pending保存local_softirq_pending()宏的返回值。它是待处理的软中断的32位位图,如果第n位被设置为1,那么第n位对应类型的软中断等待处理。
(2)现在待处理的软中断位图已经被保存,将指针h指向softirq_vec的第一项。
(3)如果pending的第一位被置为1,则h->action(h)被调用。
(4)指针加1,所以现在它指向softirq_vec数组的第二项。
(5)位掩码pending右移一位。
(6)现在h指针指向数组的第二项,pending位掩码的第二位现在也到了第一位上。重复执行上面的步骤。
(7)一直重复执行下去,知道pending变为0,这表明已经没有待处理的软中断了,我们的任务就完成了。注意,这种检查足以保证h总是指向softirq_vec的有效项,因为pending最多只可能是32位,所以循环最多执行32次。