您的位置:首页 > 运维架构 > Linux

《LINUX3.0内核源代码分析》第二章:中断和异常(3)

2013-07-11 09:25 288 查看
原文地址:《LINUX3.0内核源代码分析》第二章:中断和异常(3) 作者:xiebaoyou

摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的中断处理过程的软中断部分。主要包括软中断和tasklet。

法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友(scxby@163.com)发表于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 tasklet
tasklet是一种特殊的软中断。它和其他软中断的最大区别是:在同一时刻,同一个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);

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: