软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。软中断也是其他一些内核机制的基础,比如tasklet,高分辨率timer等。
/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/
1. 软件中断的数据结构
1.1 struct softirq_action
内核用softirq_action结构管理软件中断的注册和激活等操作,它的定义如下:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
非常简单,只有一个用于回调的函数指针。软件中断的资源是有限的,内核目前只实现了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
};
内核的开发者们不建议我们擅自增加软件中断的数量,如果需要新的软件中断,尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应数组中的一项:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
1.2 irq_cpustat_t
typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
__softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。
1.3 软中断的守护进程ksoftirqd
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。
2. 触发软中断
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
再看看raise_softirq_irqoff:
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
......
if (!in_interrupt())
wakeup_softirqd();
}
先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。
3. 软中断的执行
3.1 在irq_exit中执行
void irq_exit(void)
{
......
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
......
}
如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:
asmlinkage void __do_softirq(void)
{
......
pending = local_softirq_pending();
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
......
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
......
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
lockdep_softirq_exit();
__local_bh_enable(SOFTIRQ_OFFSET);
}
- 首先取出pending的状态;
- 禁止软中断,主要是为了防止和软中断守护进程发生竞争;
- 清除所有的软中断待决标志;
- 打开本地cpu中断;
- 循环执行待决软中断的回调函数;
- 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:
- 没有新的软中断等待执行;
- 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;
- 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;
- 推出前恢复软中断;
3.2 在ksoftirqd进程中执行
- 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能;
- 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,守护进程将被唤醒;
4. tasklet
4.1 tasklet_struct
void __init softirq_init(void)
{
......
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
内核用一个tasklet_struct来表示一个tasklet,它的定义如下:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
next用于把同一个cpu的tasklet链接成一个链表,state用于表示该tasklet的当前状态,目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行:
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
原子变量count用于tasklet对tasklet_disable和tasklet_enable的计数,count为0时表示允许tasklet执行,否则不允许执行,每次tasklet_disable时,该值加1,tasklet_enable时该值减1。func是tasklet被执行时的回调函数指针,data则用作回调函数func的参数。
4.2 初始化一个tasklet
- DECLARE_TASKLET(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于0。
- DECLARE_TASKLET_DISABLED(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于1。
struct tasklet_struct tasklet_xxx;
......
tasklet_init(&tasklet_xxx, func, data);
4.3 tasklet的使用方法
- tasklet_disable() 通过给count字段加1来禁止一个tasklet,如果tasklet正在运行中,则等待运行完毕才返回(通过TASKLET_STATE_RUN标志)。
- tasklet_disable_nosync() tasklet_disable的异步版本,它不会等待tasklet运行完毕。
- tasklet_enable() 使能tasklet,只是简单地给count字段减1。
- tasklet_schedule(struct tasklet_struct *t) 如果TASKLET_STATE_SCHED标志为0,则置位TASKLET_STATE_SCHED,然后把tasklet挂到该cpu等待执行的tasklet链表上,接着发出TASKLET_SOFTIRQ软件中断请求。
- tasklet_hi_schedule(struct tasklet_struct *t) 效果同上,区别是它发出的是HI_SOFTIRQ软件中断请求。
- tasklet_kill(struct tasklet_struct *t) 如果tasklet处于TASKLET_STATE_SCHED状态,或者tasklet正在执行,则会等待tasklet执行完毕,然后清除TASKLET_STATE_SCHED状态。
4.4 tasklet的内部执行机制
struct tasklet_head
{
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
回到4.1节,我们知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的中断回调函数是tasklet_action,我们看看它的代码:
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
解析如下:
- 关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。
- 遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:
- 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。
- 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。
- 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;
- 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;
- 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;
- 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;
- 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;