Chinaunix首页 | 论坛 | 博客
  • 博客访问: 780049
  • 博文数量: 610
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 3752
  • 用 户 组: 普通用户
  • 注册时间: 2016-11-11 09:12
  • 认证徽章:
个人简介

To be a better coder

文章分类

全部博文(610)

文章存档

2019年(165)

2018年(217)

2017年(147)

2016年(82)

分类: LINUX

2016-12-23 09:11:11

摘要:本文主要讲述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.0ULK3的基础上增加了几个软中断。可搜索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_SOFTIRQTASKLET_SOFTIRQ。处理函数是tasklet_hi_actiontasklet_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);

}

 

 

阅读(1098) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册