分类: LINUX
2014-08-09 23:22:40
摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的中断处理过程的软中断部分。主要包括软中断和tasklet。
法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友()发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。
本连载文章并不是为了形成一本适合出版的书籍,而是为了向有一定内核基本的读者提供一些linux3.0源码分析。因此,请读者结合《深入理解LINUX内核》第三版阅读本连载。
1.1 软中断处理过程
1.1.1 在中断上下文处理软中断
中断处理完ISR后,会调用irq_exit,在irq_exit函数中会判断当前是否有挂起的软中断,则会调用invoke_softirq处理软中断:
/**
* 在中断上下文中处理挂起的软中断。
*/
static inline void invoke_softirq(void)
{
if (!force_irqthreads)/* 没有强制进行中断线程化 */
/**
* 我们分析的A9单板目前运行在关中断状态,因此可以直接调用__do_softirq而不必调用do_softirq
*/
__do_softirq();
else {/* 中断线程化了,软中断统一在线程上下文处理 */
/**
* 这里增加软中断计数,还不太清楚增加计数的目的。因为此时处于中断上下文,似乎没有必要。
*/
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
/**
* 唤醒本CPU上的softirqd守护线程。由该线程处理挂起的软中断。
*/
wakeup_softirqd();
/**
* 递减软中断计数。
*/
__local_bh_enable(SOFTIRQ_OFFSET);
}
}
1.1.1.1 软中断处理函数
/**
* 处理软中断。注意这个函数可能在中断上下文中调用,也可能在线程上下文中调用。
*/
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
/**
* 在一次调用__do_softirq的过程中,最多循环处理10次软中断。
* 这样做的目的是为了避免软中断大量占用CPU,导致应用程序被饿死。
* 实际上,这样做达不到目的。要完全避免饿死应用程序,还是需要软中断线程化。
* 当然,这里的判断应当是历史原因。
*/
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
/**
* 得到当前挂起软中断掩码。注意当前仍然是关中断状态,可以安全的获得掩码。
*/
pending = local_softirq_pending();
account_system_vtime(current);
/**
* 这里加上软中断计数,这样在本函数中开中断后,发生中断后不会再重入本函数。
*/
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
lockdep_softirq_enter();/* 这个东东仅仅是调试用 */
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
/**
* 我们已经将软中断掩码放到局部变量中了,将掩码清0.
*/
set_softirq_pending(0);
/**
* 下面将开始处理软中断了,由于软中断执行时间一般较长(如协议栈代码),因此需要在开中断下运行。
* 这里将中断打开,避免长时间关中断。
*/
local_irq_enable();
/**
* 从第一个软中断开始遍历,softirq_vec中保存的是所有软中断描述符
* 这个数组是不会动态变化,因此不需要进行特殊的保护。
*/
h = softirq_vec;
do {
if (pending & 1) {/* 该软中断挂起否 */
/**
* 将当前指针与数组起始地址相差,即得到数组偏移,即软中断号。
*/
unsigned int vec_nr = h - softirq_vec;
/**
* 在调用软中断处理函数前,保存抢占计数。
* 避免软中断函数破坏计数。
*/
int prev_count = preempt_count();
/**
* 跟踪软中断在每个核上的执行计数。
*/
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
/**
* 调用该软中断的回调函数。我们经常用到的协议栈、定时器都是这里被调用的。
* 内核实现的软中断请参见ULK3.不过linux3.0在ULK3的基础上增加了几个软中断。可搜索NR_SOFTIRQS。
*/
h->action(h);
trace_softirq_exit(vec_nr);
/**
* 软中断回调函数破坏了抢占计数。这里打印最高级别的警告。
* 并恢复正确的抢占计数。
* 不过这里比较奇怪,没有对打印进行限制。也许今后的版本会修改。
*/
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", vec_nr,
softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count() = prev_count;
}
/**
* 这里是处理下半部分RCU静止状态。太复杂,今后分析RCU时再详述。
*/
rcu_bh_qs(cpu);
}
/**
* 后移软中断,并将挂起软中断右移,实质上已经处理下一个软中断。
*/
h++;
pending >>= 1;
} while (pending);/* 没有挂起的软中断了就结束循环 */
/**
* 处理完一轮软中断,在开中断期间,可能发生了中断并重新触发了软中断。
* 在进行第二轮处理以前,必须关中断,防止软中断挂起标志被修改。
*/
local_irq_disable();
/**
* 重新获取挂起软中断标志。
*/
pending = local_softirq_pending();
/**
* 如果有新的软中断被触发,并且还没有处理完10轮软中断,则继续处理。
*/
if (pending && --max_restart)
goto restart;
if (pending)/* 处理完10轮软中断,仍然有挂起软中断,则唤醒ksoftirqd守护线程处理软中断 */
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
/**
* 递减软中断计数。注意这里是关中断状态,否则,不停到来的中断可能会将进程堆栈击穿。
*/
__local_bh_enable(SOFTIRQ_OFFSET);
}
1.1.1.2 在线程上下文处理软中断
有几种情况可能导致软中断在线程上下文中执行:
ü 在中断上下文中处理了10轮软中断后,还有未处理的软中断。为了避免软中断长时间占用CPU,将其放到softirqd守护线程中执行。
ü 软中断被线程化后,统一将软中断放到softirqd守护线程中执行。
ü 在线程上下文屏蔽软中断后,线程被中断打断,中断处理过程唤醒了软中断。被屏蔽的软中断延后执行。当线程调用local_bh_enable打开软中断后,将延后的软中断放到当前线程上下文执行。
1.1.1.1 Ksoftirqd守护线程Ksoftirqd守护线程的主体执行代码是run_ksoftirqd
/**
* ksoftirq守护线程的执行代码
*/
static int run_ksoftirqd(void * __bind_cpu)
{
set_current_state(TASK_INTERRUPTIBLE);
/**
* 循环处理本CPU上的所有软中断。
*/
while (!kthread_should_stop()) {/* 当CPU被移除或者其他原因导致softirqd被停止时,退出本循环 */
preempt_disable();/* 这里关闭抢占,应当是避免在调用do_softirq的过程中被其他任务打断。造成内核数据结构的不一致。可能是历史原因需要关闭抢占吧 */
if (!local_softirq_pending()) {/* 没有挂起的软中断需要处理,将自己调度出去,等待中断将本线程唤醒。 */
preempt_enable_no_resched();/* 打开抢占,但是并不判断是否需要抢占调度,因为接下来马上就要主动调用了,没有必要多调用一次schedule */
schedule();
preempt_disable();/* 被唤醒后,说明有待处理的软中断,在继续运行调用do_softirq前,需要再次关闭抢占 */
}
__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))/* CPU已经离线,不需要再处理CPU上的软中断,退出 */
goto wait_to_die;
local_irq_disable();/* 这里需要关中断后再次判断挂起标志。因为中断可能打断本线程并在中断上下文处理了软中断,也就是说挂起标志在关中断前已经发生变化 */
if (local_softirq_pending())/* 在关中断的情况下再次判断挂起标志,此时的标志才是有效的,如果确实有挂起中断,调用do_softirq处理 */
__do_softirq();
local_irq_enable();/* 注意不管是否进入了do_softirq,运行到这里都是处于关中断状态,需要将其打开。 */
preempt_enable_no_resched();/* 对do_softirq的调用已经完毕,可以开抢占了。同理,cond_resched会处理调度,这里只开抢占,而不调用schedule */
cond_resched();/* 如果有高优先级任务需要处理,则切换到高优先级任务。 */
preempt_disable();/* 再次循环前,也需要关闭抢占。 */
rcu_note_context_switch((long)__bind_cpu);/* 向rcu子系统标示系统进入一次静止状态。 */
}
/**
* 运行到这里,说明已经没有挂起软中断,任务需要睡眠并等待被唤醒。
* 当然,在下一次循环中,需要将抢占再次打开,因为循环开始处会关抢占并再次判断是否真的没有挂起软中断。
*/
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;
}
1.1.1.2 local_bh_enable执行被延后的软中断
local_bh_enable是对_local_bh_enable_ip的简单封装:
/**
* 恢复被屏蔽的软中断
*/
static inline void _local_bh_enable_ip(unsigned long ip)
{
/**
* 在中断上下文,是不需要禁止和打开软中断的。
* 在中断被禁止时,也不需要禁止和打开软中断。
* 这两种情况下,都给出警告,可能是用法错误。
*/
WARN_ON_ONCE(in_irq() || irqs_disabled());
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_disable();
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
trace_softirqs_on(ip);
/*
* Keep preemption disabled until we are done with
* softirq processing:
*/
/**
* 在减少软中断计数的同时,禁止抢占。
*/
sub_preempt_count(SOFTIRQ_DISABLE_OFFSET - 1);
/**
* 不在中断,也不在软中断上下文,并且有挂起的中断,才处理挂起的软中断。
*/
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq();/* 注意,此时的执行上下文是在线程上下文,中断是打开的,因此调用do_softirq而不是__do_softirq */
/**
* 由于打开了CONFIG_TRACE_IRQFLAGS时,此时处于关中断状态。打开抢占不能进行抢占调度。
* 因此,此时先减少抢占计数,待中断打开后再判断是否需要处理抢占。
*/
dec_preempt_count();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_enable();
#endif
preempt_check_resched();
}
1.1 tasklettasklet是一种特殊的软中断。它和其他软中断的最大区别是:在同一时刻,同一个tasklet只可能在某一个CPU上执行,而不会在多个CPU上同时执行。但是,不同的tasklet可能同时在不同的CPU上执行。
Tasklet分高优先级tasklet和低优先级tasklet两类。对应的软中断分别是HI_SOFTIRQ和TASKLET_SOFTIRQ。处理函数是tasklet_hi_action和tasklet_action。
下面以tasklet_action为例解释低优先级tasklet的执行过程:
/**
* 低优先级tasklet的执行函数。
*/
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
/**
* 由于中断可能注册tasklet,因此,在获取待处理的tasklet链表时,需要关闭中断。
*/
local_irq_disable();
/**
* 将本CPU上的任务链表头加载到局部变量中,并将任务链表头置空,这样可以快速获取整个链表。
* 在后续的处理中,不必长时间的关闭中断。
*/
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;
/**
* 其他核上的中断可能会调度一个tasklet开始运行。因此这里试图获得它的锁再执行其他回调。
*/
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {/*没有禁止该tasklet */
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) /* 任务没有被禁止,又没有被调度,但是又在链表中,是不正常的 */
BUG();
t->func(t->data);/* 可以安全的调用tasklet的回调函数了 */
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));
/**
* 触发TASKLET_SOFTIRQ,这样,在do_softirq的下一轮将会处理该任务。
*/
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
/**
* 链表操作完毕,可以安全的打开中断了。
*/
local_irq_enable();
}
}
任务调度函数:
/**
* 调度任务,允许它被软中断执行
*/
static inline void tasklet_schedule(struct tasklet_struct *t)
{
/**
* 原子设置TASKLET_STATE_SCHED,表示任务需要被调度执行。如果还没有被调度过,则调用__tasklet_schedule将它加到本CPU的链表中
*/
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
/**
* 将任务加到链表中,并触发tasklet软中断。
*/
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);/* 禁止中断,这样可以避免与软中断冲突。 */
/**
* 将任务添加到本CPU链表中。
*/
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
/**
* 触发TASKLET_SOFTIRQ软中断.
*/
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}