linux kernel软中断及其衍生品-定时器 tasklet的实现

软中断概念在嵌入式开发可以有两个不同的解释:
其一,软中断在处理器设计中是处理器异常之一,程序软件使用指定指令(如arm的SWI指令)引发该异常从而陷入内核态执行,最典型的软件应用就是系统调用。
其二,在kernel代码中实现了一套软中断机制,区别于硬件中断的硬件触发软件处理,而是软件触发软件处理。

今天来学习的是kernel下的软中断机制,

学习最常用的硬件中断,我们最关心的是中断触发(硬件)-中断分发-中断处理这个流程如何完成,对于软中断我们也需要搞明白这几点。

首先来看下kernel中跟软中断相关的结构体变量,在kernel/softirq.c中,如下:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

softirq_vec数组表示kernel下所有注册的软中断。

struct softirq_action
{
    void    (*action)(struct softirq_action *);
};
数组下标表示软中断号,softirq_action则代表软中断处理函数。

softirq_vec类似于kernel下表征硬件中断的irq_desc。

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

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
};
kernel下定义了10种软中断,其中包括应用于定时器 tasklet 网络收发(NAPI) 块设备读写等。
根据注释可以看出,kernel并不希望开发者来添加新的softirq,如果有需求,可以利用由softirq实现的tasklet实现需求。


下面来看softirq的注册 触发 和 分发处理

1 软中断注册

softirq注册使用函数open_softirq,类似于硬件中断的register_irq。定义在kernel/softirq.c中

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}
很简单,添加softirq_vec对应下标的处理函数即可。

在kernel启动start_kernel中就注册了定时器的softirq,如下:

void __init init_timers(void)
{
    int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
                (void *)(long)smp_processor_id());

    init_timer_stats();

    BUG_ON(err != NOTIFY_OK);
    register_cpu_notifier(&timers_nb);
    open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
定时器处理函数为run_timer_softirq,后面学习定时器实现还会分析。

2 软中断触发

区别于硬件中断由硬件触发,软中断由软件触发,由函数raise_softirq和raise_softirq_irqoff触发,实现如下:

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

    /*
     * If we're in an interrupt or softirq, we're done
     * (this also catches softirq-disabled code). We will
     * actually run the softirq once we return from
     * the irq or softirq.
     *
     * Otherwise we wake up ksoftirqd to make sure we
     * schedule the softirq soon.
     */
    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);
}

void __raise_softirq_irqoff(unsigned int nr)
{
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}
2个函数区别在于raise_softirq在触发softirq时会屏蔽irq。
触发softirq都是调用or_softirq_pending,将内核变量__softirq_pending的nr位置位。

所以__softirq_pending变量类似于硬件中断中intc的中断状态寄存器。我们可以称之为软中断状态变量!

3 软中断的分发处理


硬件中断是异步于处理器,异步于软件程序的。硬件中断触发-中断异常入口-中断分发处理,硬件中断分发处理时机取决于硬件中断的时机,软件不需要控制。
而软中断不能异步触发调用,软中断的分发处理函数是do_softirq,分发处理时机主要有3种:
    (1)在处理完一个硬件中断以后。
    (2)在ksoftirqd内核线程中。
    (3)在那些显式检查和执行待处理的软中断的代码中,如网络子系统中(显式调用do_softirq来收发数据包)。

我们主要来看前2种情况

(1) 硬件中断处理函数退出时
中断处理函数退出时会调用irq_exit,在kernel/softirq.c中如下:

static inline void invoke_softirq(void)
{
    if (!force_irqthreads) {
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
        __do_softirq();
#else
        do_softirq();
#endif
    } else {
        __local_bh_disable((unsigned long)__builtin_return_address(0),
                SOFTIRQ_OFFSET);
        wakeup_softirqd();
        __local_bh_enable(SOFTIRQ_OFFSET);
    }
}
/*
 * Exit an interrupt context. Process softirqs if needed and possible:
 */
void irq_exit(void)
{
    account_system_vtime(current);
    trace_hardirq_exit();
    sub_preempt_count(IRQ_EXIT_OFFSET);
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();

#ifdef CONFIG_NO_HZ
    /* Make sure that timer wheel updates are propagated */
    if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
        tick_nohz_irq_exit();
#endif
    rcu_irq_exit();
    sched_preempt_enable_no_resched();
}
检查__softirq_pending是否有置位,有则invoke_softirq调用do_softirq。

(2) ksoftirqd内核线程
该内核线程的实现也在kernel/softirq.c中,这里不贴代码了。
根据ksoftirqd线程实现可以看出,当内核中出现大量软中断的时候,内核进程ksoftirqd就会调用do_softirq处理它们。

软中断分发处理时机清楚了,下面来看do_softirq如何处理,在kernel/softirq.c中:

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())
        return;

    local_irq_save(flags);

    pending = local_softirq_pending();

    if (pending)
        __do_softirq();

    local_irq_restore(flags);
}
local_softirq_pending获取__softirq_pending软中断状态变量,如果有softirq的pending则调用__do_softirq,如下:
#define MAX_SOFTIRQ_RESTART 10
asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    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 */
    set_softirq_pending(0);

    local_irq_enable();

    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);
            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_bh_qs(cpu);
        }
        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();

    account_system_vtime(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
}
__do_softirq处理softirq之前获取并清空__softirq_pending,然后根据pending遍历softirq_vec,调用对应pending的action处理函数。
遍历完成后,会再次获取__softirq_pending,如果在遍历softirq_vec期间又产生了softirq,__do_softirq会跳转到restart再次遍历处理softirq。
最多反复处理MAX_SOFTIRQ_RESTART次。
如果还有softirq的pending,则唤醒ksoftirqd内核线程来处理剩余的softirq。


软中断的注册 触发 处理就是这样,软中断是kernel实现的纯软件机制,依托于中断退出和内核线程时机来做分发处理。
基于软中断,kernel实现了定时器 tasklet,下面来重点来看下内核定时器的实现原理。

一 内核定时器
内核定时器是通过内核时钟系统和软中断结合实现的。
在driver中对于定时器的使用,一般是这样:

    static struct timer_list test_timer;
    init_timer(&test_timer);
    test_timer.function = (void *)test_handle_function;
    test_timer.data = (u32) test;
    test_timer.expires = CHECK_TIME + jiffies;
    add_timer(&test_timer);
该段代码初始化timer_list各个成员,timer_list代表一个定时器对象,最后将该timer_list添加到内核定时器链表tvec_bases中。

下面来看下内核如何实现的定时器。
1 注册定时器软中断
前面软中断中讲过,start_kernel中init_timers注册了定时器软中断处理函数run_timer_softirq,软中断号是TIMER_SOFTIRQ。

2 触发定时器软中断
定时器软中断的触发是在kernel的时钟中断中,根据前一篇博文《linux kernel时钟系统的前世今生》链接地址:
对于tickless系统,中断处理函数是tick_nohz_handler,在kernel/time/tick-sched.c中,该函数除了更新xtime以及设置下次触发时间外,还会调用update_process_times。
该函数调用了run_local_timers,如下
void run_local_timers(void)
{
    hrtimer_run_queues();
    raise_softirq(TIMER_SOFTIRQ);
}
其中调用raise_softirq触发了定时器软中断。

3 处理定时器软中断
根据前面处理软中断的讲解,硬件中断退出或ksoftirqd内核线程中do_softirq遍历softirq_vec。
因触发了定时器软中断,所以遍历softirq_vec会调用softirq_action[TIMER_SOFTIRQ].action,就是run_timer_softirq。
run_timer_softirq遍历tvec_bases中所有定时器timer_list,对比timer_list.expires与当前的jiffies,如果一致,说明定时到期,调用timer_list.function。

内核定时器的实现类似于硬件中断的二级中断,根据intc的中断状态寄存器分发中断后,我们还会根据模块内的中断状态寄存器来确定模块内具体引起中断的子模块。
内核定时器是在时钟中断中触发定时器软中断,在所有硬件中断退出时分发处理定时器软中断,判断时间到期,则执行定时器处理函数,最后清除掉定时器软中断。



二 tasklet

1 注册tasklet软中断
在start_kernel中softirq_init中注册了tasklet软中断处理函数:
open_softirq(TASKLET_SOFTIRQ, tasklet_action);

2 注册tasklet
可以使用<linux/interrupt.h>中定义的两个宏中的一个DECLARE_TASKLET或 DECLARE_TASKLET_DISABLED来静态创建tasklet,前者把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。另一个把引入计数器设为1,所以该tasklet处于禁止状态。
还可以使用tasklet_init()动态创建一个tasklet。

3 触发和处理tasklet软中断
触发tasklet软中断的操作通过tasklet_schedule()函数间接调用raise_softirq()函数来实现。
此后则等待任何的硬件中断处理函数退出后调用do_softirq来执行TASKLET_SOFTIRQ软中断的处理函数tasklet_action。
tasket_action中遍历tasklet_vec链表,对于enable的tasklet执行其处理函数。



内核定时器 tasklet实现都类似于硬件中断的二级中断。
由于定时器 tasklet都会在硬件中断退出的时机去检查执行处理,所以会应用于中断底半部的处理。
中断处理要求尽可能的简洁迅速,因此对于时间要求不是很紧迫,允许稍后执行的部分操作可以放在tasklet和定时器处理函数中进行执行。




郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。