软中断和tasklet

Linux的中断分为上下部机制,上半部在中断上下文中关闭了本地CPU中断响应,下半部是在中断线程中处理。在Linux系统没有引入中断线程化机制之前,就已经出现了一些下半部的机制,如软中断SoftIRQ,Tasklet和workqueue。
SoftIRQ是预留给系统对时间要求比较严格进行使用的,Linux系统已经定义了软中断的类型,通常情况下用户部需要修改软中断的类型,对于用户来说可以使用tasklet机制。

enum
{
    HI_SOFTIRQ=0,    最高优先级的软中断
    TIMER_SOFTIRQ,   Timer定时器软中断
    NET_TX_SOFTIRQ,  网络发包软中断
    NET_RX_SOFTIRQ,  网络收包软中断
    BLOCK_SOFTIRQ,   块设备软中断
    IRQ_POLL_SOFTIRQ, IO轮询的块设备软中断 
    TASKLET_SOFTIRQ,  tasklet软中断
    SCHED_SOFTIRQ,   进程调度以及负载均衡软中断
    HRTIMER_SOFTIRQ, 高精度定时器软中断
    RCU_SOFTIRQ,     RCU服务软中断

    NR_SOFTIRQS
};

抢占计数

在讨论softirq前,我们先来讨论下抢占计数,这与softirq有密切的关系。Linux配置打开了CONFIG_PREEMPT表示允许高优先级的任务抢占低优先级任务,但是在spin lock,中断/软中断上下文中依旧不允许抢占的。在linux系统中使用了一个Per-CPU的32位变量来标识一些特殊场景,如下。

  • PREEMPT_BITS:用于记录禁止抢占计数,当调用preempt_enable/preempt_disable会进行减加操作,直到count位0才能允许开启抢占。上述还包括spin lock,read lock等都会禁止抢占。
  • SOFTIRQ_BITS:软中断计数区域,进入软中断会调用local_bh_disble计数+1,退出软中断调用local_bh_enable计数-1,当SOFTIRQ_BITS为正数就表明处于softirq上下文中,in_softirq为真。其中第8位表示正在处理软中断。
  • HARDIRQ_BITS:硬件中断计数区,进入+1,退出时-1,当HARDIRQ_BITS为正数时表明处于hardirq上下文中,in_hardirq为真。
  • MNI_BITS:置位表示进入MNI中断上下文。
#define nmi_count() (preempt_count() & NMI_MASK)
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#ifdef CONFIG_PREEMPT_RT
# define softirq_count()    (current->softirq_disable_cnt & SOFTIRQ_MASK)
#else
# define softirq_count()    (preempt_count() & SOFTIRQ_MASK)
#endif
#define irq_count() (nmi_count() | hardirq_count() | softirq_count())

/*
 * Macros to retrieve the current execution context:
 *
 * in_nmi()     - We're in NMI context
 * in_hardirq()     - We're in hard IRQ context
 * in_serving_softirq() - We're in softirq context
 * in_task()        - We're in task context
 */
#define in_nmi()        (nmi_count())
#define in_hardirq()        (hardirq_count())
#define in_serving_softirq()    (softirq_count() & SOFTIRQ_OFFSET)
#define in_task()       (!(in_nmi() | in_hardirq() | in_serving_softirq()))

/*
 * The following macros are deprecated and should not be used in new code:
 * in_irq()       - Obsolete version of in_hardirq()
 * in_softirq()   - We have BH disabled, or are processing softirqs
 * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
 */
#define in_irq()        (hardirq_count())
#define in_softirq()        (softirq_count())
#define in_interrupt()      (irq_count())
  • in_irq: 说明HARDIRQ_BITS位有置起,说明当前有正在处理的中断handler(top half),只要hardirq_count大于0,就说明是IRQ Context。
  • in_softirq:说明SOFTIRQ_BITS位有置起,但是也不能完全说是当前正在执行softirq,其稍微有点特殊。当softirq正在执行的时候,softirq count会增加,那是处于softirq context没有错,但是再其他场景下也会将softirq count增加,比如在进程上下文中出于对临界区的包含,会调用local_bh_disable/enable保护临界区,那么这也会置起SOFTIRQ_BITS,但是并没有在执行softirq,因此对于softirq还是使用in_serving_softirq来判断是否softirq正在处理。

当MNI_BITS|HARDIRQ_BITS|SOFTIRQ_BITS置位表示处于中断上下文中,in_interrupt为真,当使能CONFIG_PREEMPT后,中断返回会检测这个preempt_count变量,当该值如果为0时才能允许被抢占。

注册软中断

系统中为每个CPU都创建了一个中断线程用于处理软中断。

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
EXPORT_PER_CPU_SYMBOL_GPL(ksoftirqd);

static struct smp_hotplug_thread softirq_threads = {
    .store          = &ksoftirqd,
    .thread_should_run  = ksoftirqd_should_run,
    .thread_fn      = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/
};

static __init int spawn_ksoftirqd(void)
{
    cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
                  takeover_tasklets);

    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
    //为每个CPU创建一个softirq处理线程
    return 0;
}
early_initcall(spawn_ksoftirqd);
struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

static struct softirq_action softirq_vec[NR_SOFTIRQS]

系统中定义了一个全局struct softirq_action变量用于存储各类型中断的回调函数,系统通过open_softirq函数来注册回调函数。

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

在linux系统中为每个CPU都维护了一个softirq的状态信息。

typedef struct {
    unsigned int __softirq_pending;
#ifdef ARCH_WANTS_NMI_IRQSTAT
    unsigned int __nmi_count;
#endif
} ____cacheline_aligned irq_cpustat_t;

DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);

触发softirq

从上图可以看出触发softirq主要有两个函数raise_softirq和raise_softirq_irqoff。raise_softirq会关闭本地中断响应,而raise_softirq_irqoff不会。
raise_softirq是用于进程上下文触发软中断的,而在进程上下文关闭本地cpu的中断响应是为了保护临界访问,软中断的触发实际是设置软中断的pending位,而只需要关闭本地中断响应是因为每个cpu都维护了一个软中断的pending位。
raise_softirq_irqoff是用于中断上下文触发中断的,实际上大部分的软中断都是在中断上下文触发的,因为本身就处于中断上下文了,本地中断响应已经关闭了,因此不需要再支持local_irq_save。
软中断处理过程中,调用__raise_softirq_irqoff设置软中断的pengding位,这样再执行软中断是会进行检测该位,如果使能才会执行软中断。
在raise_softirq_irqoff中还会进行判断是否处于中断上下文中且是否有软中断置位,对于处于进程上下文触发的软中断,那么可以直接激活软中断线程进程处理软中断。而处于中断上下文中,则什么都不用做,因为在中断退出时才会检测软中断进行执行软中断,因此对于处于中断上下文触发的软中断,仅仅是设置软中断的pending位。

执行softirq

软中断执行时机可以分为3种情况,分别是在中断退出时检测是否有软中断执行、进程上下文中主动执行、在spin_unlock_bh(实际调用__local_bh_enable_ip)。

先来看一下执行中断退出执行软中断的场景,如上图进程被中断打断后进入到中断上下文中,进入中断后cpu硬件会自动关闭cpu本地中断响应,处理中断完成后在执行irq_exit中断退出时,当检测到有软中断pending时执行软中断;如果软中断是在中断上下文执行时,在软中断处理中会调用local_irq_enable打开CPU本地中断响应再处理软中断程序,如果是触发的软中断线程,硬中断已经完成退出也会使能本地中断。因此在软中断执行过程中打开了中断响应,所以可能会再次进入硬中断上下文。

再来看上面这张图,在一个task中处理一个变量此时被硬件中断打断进行中断处理函数,在中断处理快结束时如果有软中断pending将会先处理软中断,如果软中断中也访问了该变量,那么就出现竞态异常,因此为了处理进程和软中断的竞态,调用spin_lock_bh和spin_unlock_bh进行保护,在硬件中断处理完要进入软中断将会被禁止,硬件中断会被直接退出,继而task可以继续运行,当task再执行spin_unlock_bh时会触发执行软中断。另外如果软中断处理函数中的竞态可能在多核直接发生,为了保护多核的临界处理在软中断中只需要调用spin_lock和spin_unlock即可,不需要调用spin_lock_bh和spin_unlock_bh,因为每个cpu上只有一个软中断可以运行不需要做软中断之间的临界保护。总结就是在进程上下文中要避免软中断和多核的竞态保护就调用spin_lock_bh和spin_unlock_bh,软中断中避免多核的竞态保护就调用spin_lock和spin_unlock即可。

Tasklet

Tasklet是软中断类型的一种,便于用户实现软中断。

struct tasklet_struct
{
    struct tasklet_struct *next; 多个tasklet串成一个链表
    unsigned long state;       标记tasklet的状态
    atomic_t count;           正数表示本地已经有运行的tasklet,只有为0才能运行。
    bool use_callback;         
    union {
        void (*func)(unsigned long data);
        void (*callback)(struct tasklet_struct *t);
    };                      tasklet的处理回调函数
    unsigned long data;       传参
};

Tasklet是softirq的一种,系统中分配了两个标志可用于tasklet软中断,分别是高优先级的HI_SOFTIRQ和普通优先级的TASKLET_SOFTIRQ,系统中每个CPU对应维护两个tasklet链表,一个HI_SOFTIRQ类型的链表和一个TASKLET_SOFTIRQ类型的链表,用于管理维护用户注册的tasklet。在进行处理软中断是,会优先级处理HI_SOFTIRQ类型的tasklet。

/*
 * Tasklets
 */
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);

Softirq在初始化时会为tasklet注册全局的软中断回调函数。

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

tasklet的创建

用户可以使用下面两个宏来定义一个tasklet,区别一个已经处于enable另外一个处于disable。

#define DECLARE_TASKLET(name, _callback)        \\
struct tasklet_struct name = {              \\
    .count = ATOMIC_INIT(0),            \\
    .callback = _callback,              \\
    .use_callback = true,               \\
}
#define DECLARE_TASKLET_DISABLED(name, _callback)   \\
struct tasklet_struct name = {              \\
    .count = ATOMIC_INIT(1),            \\
    .callback = _callback,              \\
    .use_callback = true,               \\
}


如:static DECLARE_TASKLET(fst_tx_task, fst_process_tx_work_q);

除了使用宏来定义外,还可以使用tasklet_init来动态初始化分配。

void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->use_callback = false;
    t->data = data;
}
EXPORT_SYMBOL(tasklet_init);

触发执行tasklet

触发tasklet得到执行,也比较简单,上面以普通的tasklet为例。用户可调用tasklet_schedule,该函数将tasklet插入到链表中然后调用raise_softirq_irqoff触发软中断,最终触发了softirq,关于softirq的运行在上一章节中已经描述,最后softirq运行tasklet_action,在该函数中进行while遍历链表执行对应的tasklet回调函数。