6.软中断
前面小节介绍了中断处理流程,确切地说是硬中断处理流程。内核出于性能等方面的考虑,将中断分为两部分:上半部处理中断中需要及时响应且处理时间较短的部分,下半部用于处理对响应时间要求不高且处理时间可能较长的部分。本节将主要介绍中断下半部的三种实现方式之一:软中断。
首先介绍一下软中断及其相关的数据结构。软中断是在编译期间静态分配的,由数据结构softirq_action表示:
-
<include/linux/interrupt.h>
-
struct softirq_action
-
{
-
void (*action)(struct softirq_action *);
-
};
softirq_action结构体非常简单,仅包含一个指向软中断处理函数的指针。为了管理软中断,内核维护着一个softirq_action结构体数组softirq_vec[NR_SOFTIRQS],称之为软中断向量表:
-
<kernel/softirq.c>
-
static struct softirq_action softirq_vec[NR_SOFTIRQS];
其中NR_SOFTIRQS是一个枚举类型内核支持的最大软中断数。
-
< include/linux/interrupt.h >
-
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
-
};
通过上面的枚举类型可知当前内核版本支持10种软中断,其中HI_SOFTIRQ和TASKLET_SOFTIRQ用于实现tasklet,TIMER_SOFTIRQ和HRTIMER_SOFTIRQ用于实现定时器,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于实现网络设备的发送和接受操作,BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ用于实现块设备操作,SCHED_SOFTIRQ用于调度器。
内核中通过调用open_softirq函数注册软中断处理程序,该函数接收两个参数:软中断索引号(枚举类型)和处理程序。根据索引号寻址到软中断向量表softirq_vec的指定元素,使其action成员指向处理程序。
-
<kernel/softirq.c>
-
void open_softirq(int nr, void (*action)(struct softirq_action *))
-
{
-
softirq_vec[nr].action = action;
-
}
例如,网络系统中注册接收和发送数据包的软中断:
-
<net/core/dev.c>
-
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
-
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
一个已经注册的软中断必须“触发”后才能在“适当的时机”被执行,raise_softirq函数和__raise_softirq_irqoff宏用于触发软中断,将一个软中断设置为挂起状态(pending)。raise_softirq最终也是通过调用__raise_softirq_irqoff来实现的:
-
<kernel/softirq.c>
-
inline void raise_softirq_irqoff(unsigned int nr)
-
{
-
__raise_softirq_irqoff(nr); //触发nr号软中断
-
-
/*
-
* 如果当前不处在中断上下文中,则唤醒内核线程ksoftirqd来处理软中断。
-
* 否则什么都不做,函数正常返回,等待“适当的时机”来处理软中断。
-
*/
-
if (!in_interrupt())
-
wakeup_softirqd();
-
}
-
void raise_softirq(unsigned int nr)
-
{
-
unsigned long flags;
-
-
local_irq_save(flags); //禁止中断
-
raise_softirq_irqoff(nr); //触发软中断之前先要禁止中断
-
local_irq_restore(flags); //开启中断
-
}
__raise_softirq_irqoff被定义为宏,以上述的枚举类型作为参数:
-
typedef struct {
-
unsigned int __softirq_pending; //软中断挂起位图,每bit代表一种软中断
-
unsigned int local_timer_irqs;
-
} ____cacheline_aligned irq_cpustat_t;
-
-
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned; //每个cpu拥有一个软中断状态结构体,所以即使是相同类型的软中断也可以在其他cpu上同时执行。
-
-
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
-
-
#define local_softirq_pending() \
-
__IRQ_STAT(smp_processor_id(), __softirq_pending)
-
-
#define or_softirq_pending(x) (local_softirq_pending() |= (x))//将本地cpu的软中断挂起位图与x做或运算
-
-
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)//将指定的nr号软中断设置为挂起状态
软中断并不是一旦触发就被执行,而是在之后的某个“适当的时机”执行,在Linux内核中共有三个“时机”:
1)从一个硬中断返回时,即在irq_exit中有机会执行软中断(详见前一节中的分析)。
2)在ksoftirqd内核线程中被执行,稍后分析。
3)在那些显式检查或执行待处理软中断的代码中。如网络系统中的netif_rx_ni函数,local_bh_enable以及local_bh_enable_ip等函数。
最终软中断都是通过do_softirq或__do_softirq函数被执行的,其中do_softirq也是通过调用__do_softirq来实现的。在内核需要执行软中断时,先通过local_softirq_pending函数检查是否有软中断挂起,如果有则调用do_softirq来执行软中断。
最后,我们再分析一下处理软中断的内核线程(ksoftirqd)。这实际上是内核为了解决大量软中断重复触发“霸占”CPU导致用户进程处于饥饿状态的一种折中方案。借助内核线程,可以保证在软中断负担很重的时候用户进程不会因为得不到处理时间而处于饥饿状态;同时也保证过量的软中断终究会得到处理。即使在空闲系统上,这种方案也表现良好,软中断处理得非常迅速,因为仅存的内核线程肯定会马上调度。
内核中有两个地方调用wakeup_softirqd唤醒ksoftirqd内核线程:
1)在__do_softirq函数中。
2)在raise_softirq_irqoff函数中。
-
<kernel/softirq.c>
-
static int ksoftirqd(void * __bind_cpu)
-
{
-
set_current_state(TASK_INTERRUPTIBLE);//将当前进程状态设置为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;
-
do_softirq(); //执行软中断
-
preempt_enable_no_resched();//开启内核抢占
-
cond_resched(); //如果当前进程设置了TIF_NEED_RESCHED标志则调用调度器
-
preempt_disable();//再次禁止内核抢占,进入下一次循环处理软中断。
-
rcu_sched_qs((long)__bind_cpu);
-
}//如果没有待处理的软中断,退出循环
-
preempt_enable(); //开启内核抢占
-
set_current_state(TASK_INTERRUPTIBLE);//将当前进程状态设置为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;
-
}
参考资料:
[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发——软件设计与优化. 沈建华, 译.
[2] Daniel P.Bovet, Marco Cesati. 深入理解Linux内核(第三版). 陈莉君, 张琼声, 张宏伟, 译.
[3] Pobert Love. Linux内核设计与实现. 陈莉君, 康华, 张波, 译.
[4] Wolfgang Mauerer. 深入Linux内核架构. 郭旭, 译.
[5] 陈学松. 深入Linux设备驱动程序内核机制.
[6] ARM Architecture Reference Manual
[7] The ARM-THUMB Procedure Call Standard
阅读(4251) | 评论(0) | 转发(5) |