Linux内核软中断softirq和小任务tasklet分析(六)

1.概述

硬件的中断处理函数处于中断上半部分,在CPU关中断的状态下执行,中断线程、软中断(softirq)及小任务(tasklet)属于中断的下半部分(bottom half),在CPU开中断的状态下执行。小任务基于软中断实现,实质是对软中断的进一步封装, 在实际使用中应尽量使用小任务 。软中断及小任务的执行时机通常是中断上半部分返回(中断服务函数还未完全退出)的时候,其执行的上下文环境也处于(软)中断当中,因此其调用的处理函数不允许睡眠。这里说的软中断是中断下半部分的一种处理机制,和执行指令(ARM架构的SWI、SVC指令)触发中断的软中断不是一个概念。
软中断和小任务如果在某段时间内大量出现的话,内核会把多余的软中断和小任务放入ksoftirqd内核线程中执行。中断优先级高于软中断,软中断优先级高于线程。软中断适度线程化,可以缓解高负载情况下系统的响应。

2.软中断数据结构

Linux内核的软中断类型有10种。按优先级划分,优先级从0-9,数字越小优先级越高,优先级越高的软中断能得到优先执行。tasklet使用优先级为6的软中断。

    [include/linux/interrupt.h]
    enum
    {
        HI_SOFTIRQ=0,      // 优先级为0,最高优先级的软中断
        TIMER_SOFTIRQ,     // 优先级为1,Timer定时器软中断
        NET_TX_SOFTIRQ,    // 优先级为2,发送网络数据包的软中断
        NET_RX_SOFTIRQ,    // 优先级为3,接收网络数据包的软中断
        BLOCK_SOFTIRQ,     // 优先级为4,用于块设备的软中断
        IRQ_POLL_SOFTIRQ,  // 优先级为5,用于轮训中断的软中断
        TASKLET_SOFTIRQ,   // 优先级为6,tasklet类型的软中断
        SCHED_SOFTIRQ,     // 优先级为7,用于进程调度以及负载均衡的软中断
        HRTIMER_SOFTIRQ,   // 优先级为8,用于高精度定时器的软中断
        RCU_SOFTIRQ,       // 优先级为9,用于RCU的软中断
        NR_SOFTIRQS        // 软中断数量为10
    };

软中断执行的回调函数被封装在softirq_action的结构体中,其内部保存了一个函数指针,注册软中断时需要设置此函数指针。内核为所有软中断定义了一个类型为softirq_actionsoftirq_vec数组,数组长度为NR_SOFTIRQS,正好每个软中断对应一个softirq_action,数组的索引为软中断的优先级。softirq_vec数组为所有CPU共享。内核还定义了一个类型为irq_cpustat_t的数组irq_stat,数组长度为系统CPU的个数,索引为CPU的编号,每个CPU对应一个数组元素,此数组用来标记某个CPU上的某个软中断的状态,软中断pending,则对应的bit置1,否则为0。

    [include/linux/interrupt.h]
    struct softirq_action
    {
        void	(*action)(struct softirq_action *);
    };
    [kernel/softirq.c]
    static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
    irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;  // 软中断状态标记

    [include/asm-generic/hardirq.h]
    typedef struct {
        unsigned int __softirq_pending;
    } ____cacheline_aligned irq_cpustat_t;

3.注册软中断

使用open_softirq注册软中断,nr为软中断的优先级,action为软中断执行的回调函数。

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

4.触发及执行软中断

4.1.触发软中断

只有处于pending状态的软中断才会得到执行,可使用raise_softirqraise_softirq_irqoff函数使某个软中断处于pending状态(即触发软中断),raise_softirq会关闭中断,raise_softirq_irqoff不会关闭中断。由于__softirq_pending变量为Per-CPU类型,因此在修改此变量时只需要关闭本地中断即可,不需要额外的同步措施。raise_softirq首先关闭本地中断,然后调用raise_softirq_irqoff设置__softirq_pending变量,如果不处于硬中断、软中断及不可屏蔽中断上下文,则会唤醒内核线程ksoftirqd执行软中断,最后打开中断。可以看出一个CPU上的软中断执行是串行的,不会发生软中断嵌套的现象

    [kernel/softirq.c]
    void raise_softirq(unsigned int nr)
    {
        unsigned long flags;

        local_irq_save(flags);  // 保存cpsr,然后关闭中断
        raise_softirq_irqoff(nr);
        local_irq_restore(flags);  // 恢复cpsr
    }
    // 设置软中断pending标记的宏
    #define or_softirq_pending(x)  (local_softirq_pending() |= (x))       
    #define local_softirq_pending() __IRQ_STAT(smp_processor_id(), __softirq_pending)
    #define __IRQ_STAT(cpu, member)	(irq_stat[cpu].member)

    raise_softirq_irqoff
      ->__raise_softirq_irqoff
        ->or_softirq_pending(1UL << nr)  // 将本地cpu的__softirq_pending变量的nr bit设置为1
        // 如果不处于硬中断、软中断及不可屏蔽中断上下文中,则唤醒内核线程ksoftirqd执行软中断,
        if (!in_interrupt())  
            wakeup_softirqd
              ->__this_cpu_read(ksoftirqd)  // 获取ksoftirqd内核线程的task_struct指针
                // 如果线程没有运行,则唤醒线程
                if (tsk && tsk->state != TASK_RUNNING)
                    wake_up_process(tsk);  // 唤醒内核线程,由内核线程执行软中断    

4.2.执行软中断

软中断有两个执行时机,一个是在硬件中断处理结束的时候,二是触发软中断时唤醒内核线程之后。首先来看第一个,硬件中断处理结束的时候会调用irq_exitin_interrupt判断当前执行环境是否处于硬中断、软中断及不可屏蔽中断上下文中,local_softirq_pending判断本地CPU是否有软中断pending,如果当前执行环境不处于硬中断、软中断及不可屏蔽中断上下文且本地CPU有软中断pending,则调用invoke_softirq执行软中断。__local_bh_disable_ip(进入软中断使用)和__local_bh_enable(退出软中断使用)需要配对使用,前者增加preempt_count变量SOFTIRQ域的值,后者减小preempt_count变量SOFTIRQ域的值,若SOFTIRQ域大于0,说明软中断正在被执行,反之则说明软中断没有被执行,SOFTIRQ域保证了软中断在本地CPU上的串行执行。执行软中断之前需要调用local_irq_enable开启本地中断,执行完毕调用local_irq_disable关闭本地中断,说明软中断是在开中断的环境下执行。根据本地CPU软中断的pending状态,调用softirq_vec数组中对应的回调函数action执行软中断,直到处理完所有pending状态的软中断才跳出while循环。如果在开中断期间又有软中断触发,则会跳转到restart处继续处理软中断,但此时有条件限制,软中断执行时间小于2ms、不需要调度且restart执行次数不超过10次才会跳转,否则唤醒ksoftirqd内核线程处理软中断。

    [kernel/softirq.c]
    void irq_exit(void)
    {
        ......
        preempt_count_sub(HARDIRQ_OFFSET);  // 减少硬件中断计数
        if (!in_interrupt() && local_softirq_pending())  // 检查是否有挂起的软中断,如有则执行软中断
            invoke_softirq();
        ......
    }

    #define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)  // 最多执行2毫秒,超过唤醒内核线程执行
    #define MAX_SOFTIRQ_RESTART 10  // restart最多执行10次
    invoke_softirq
      ->__do_softirq
        end = jiffies + MAX_SOFTIRQ_TIME  // 软中断在这里最多执行2毫秒
        max_restart = MAX_SOFTIRQ_RESTART  // restart最对执行次数为10次
        pending = local_softirq_pending()  // 记录本地CPU软中断pending状态
        // 开始执行软中断之前增加软中断计数,表明要进入软中断上下文
        ->__local_bh_disable_ip  // 增加preempt_count变量SOFTIRQ域的值
    restart:  // restart标记
        // 在使能本地中断之前清空本地cpu软中断pending状态
        ->set_softirq_pending(0)
        ->local_irq_enable()  // 开启本地cpu中断,软中断是在开中断的状态下执行
        h = softirq_vec  // 获取softirq_vec数组的首地址
        // 循环执行所有pending状态的软中断
        while ((softirq_bit = ffs(pending))) { // ff获取pending第一个设置为1的bit序号
            unsigned int vec_nr;
            int prev_count;
            h += softirq_bit - 1;  // bit序号从0开始,h指向softirq_vec数组中需要执行的软中断
            vec_nr = h - softirq_vec;  // 获取软中断优先级,即软中断类型
            prev_count = preempt_count();  // 保存当前进程preempt_count字段值
            h->action(h);  // 调用回调函数执行软中断
            h++;
            pending >>= softirq_bit;  // 更新pending标志,丢弃已经执行的软中断pending位
        }
        ->local_irq_disable  // 关闭中断
        // 获取本地CPU软中断pending状态,cpu开中断期间可能再次触发软中断
        ->pending = local_softirq_pending()  
        if (pending) {
            // 如果时间小于2ms,不需要调度,restart执行次数不超过10次 
            // 则跳转到restart处继续执行软中断,否则唤醒内核线程处理软中断
            if (time_before(jiffies, end) && !need_resched() &&
                --max_restart)
                goto restart;
            wakeup_softirqd();  // 唤醒内核线程ksoftirqd执行软中断
        }
        // 退出软中断时减小软中断计数,表明要退出软中断上下文
        __local_bh_enable(SOFTIRQ_OFFSET)

接着分析一下执行软中断的内核线程ksoftirqdDECLARE_PER_CPU宏用于定义一个Per-CPU类型的变量,这里定义了内核线程ksoftirqdtask_struct结构体指针。可以看出每个CPU都会对应一个ksoftirqd线程,用于执行本地CPU的软中断。ksoftirqd线程调用run_ksoftirqd函数执行软中断,首先关闭中断,使用local_softirq_pending宏判断本地CPU是否有软中断pending,有则调用__do_softirq执行软中断,执行完毕开启中断,反之开启中断后退出。

    // Per-CPU类型,每个CPU都有一个ksoftirqd内核线程
    [kernel/softirq.c]
    DECLARE_PER_CPU(struct task_struct *, ksoftirqd);  
    // ksoftirqd线程执行的函数
    static void run_ksoftirqd(unsigned int cpu)
    {
        local_irq_disable();  // 关闭中断
        if (local_softirq_pending()) {  // 本地CPU是否有软中断pending
            __do_softirq();  // 执行软中断
            local_irq_enable();  // 开启中断
            cond_resched_rcu_qs();
            return;
        }
        local_irq_enable();  // 开启中断
    }

5.tasklet

tasklet是利用软中断实现的中断下半部分机制,运行在软中断上下文中。tasklet使用tasklet_struct描述。

    [include/linux/interrupt.h]
    struct tasklet_struct
    {
        struct tasklet_struct *next;  // tasklet_struct链表指针
        unsigned long state;  // state为一个位图,表示tasklet_struct运行状态
        atomic_t count;  // 0:tasklet可以执行,非0:tasklet不允许执行
        void (*func)(unsigned long);  // 执行tasklet的回调函数
        unsigned long data;  // 传递给执行tasklet的回调函数的参数
    };
    enum  // 表示tasklet的状态,即state成员的值
    {
        TASKLET_STATE_SCHED,	/* bit0为1表示tasklet被调度,正准备运行 */
        TASKLET_STATE_RUN	/* bit1位1表示tasklet正在运行 (SMP only) */
    };

每个CPU定义了两种tasklet,分别为tasklet_vectasklet_hi_vec,各自组成一个单项循环链表。在start_kernel中调用softirq_init初始化tasklet,tasklet_vec的软中断优先级为6,回调函数为tasklet_action;tasklet_hi_vec的软中断优先级为0,回调函数为tasklet_hi_actiontasklet_hi_vec的优先级高于tasklet_vec

    [kernel/softirq.c]
    // 每个CPU静态定义了两种tasklet
    static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
    static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
    struct tasklet_head {
        struct tasklet_struct *head;
        struct tasklet_struct **tail;
    };
    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);  // 注册tasklet_vec的软中断
        open_softirq(HI_SOFTIRQ, tasklet_hi_action);  // 注册tasklet_hi_vec的软中断
    }

tasklet_action是优先级为6的tasklet tasklet_vec的执行函数;首先保存链表的头节点指针,然后将头结点设置为NULL,链表的操作在关中断的情况进行,保证原子操作;接着遍历tasklet_vec的链表,检测每一个节点是否设置了TASKLET_STATE_RUN标志,若没有设置,则设置,反之则此节点的tasklet正在被执行,则跳过此节点,同时还要根据count变量的值判断节点是否可执行,为0时执行,反之则不执行;每遍历完一个节点,则从链表中删除此节点。tasklet_hi_actiontasklet_action的处理逻辑一致,这里不再赘述。

    [kernel/softirq.c]
    static void tasklet_action(struct softirq_action *a)
    {
        struct tasklet_struct *list;
        local_irq_disable();  // 关闭中断
        list = __this_cpu_read(tasklet_vec.head);  // 保存tasklet_vec链表的头指针
        __this_cpu_write(tasklet_vec.head, NULL); // tasklet_vec链表的头指针指向NULL
        __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
        local_irq_enable();  // 开启中断
        while (list) {  // 遍历tasklet_vec链表,调用所有回调函数
            struct tasklet_struct *t = list;
            list = list->next;
            // 单核tasklet_trylock返回1,SMP系统tasklet_trylock原子的设置TASKLET_STATE_RUN标志,
            // 并返回原来bit位的值,若原来设置了TASKLET_STATE_RUN标志,说明此节点的tasklet正在执行,
            // 则跳过此节点,否则执行此节点的回调函数
            if (tasklet_trylock(t)) {
                if (!atomic_read(&t->count)) {  // 原子的读取count字段,为0则执行tasklet,否则不执行
                    // 清除TASKLET_STATE_SCHED(bit0),并返回原来的值,如果已被清除,说明存在bug
                    if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
                        BUG();  // 报告bug信息
                    t->func(t->data);  // 调用回调函数
                    tasklet_unlock(t);  // 原子的清除TASKLET_STATE_RUN标志
                    continue;
                }
                tasklet_unlock(t);  // 原子的清除TASKLET_STATE_RUN标志
            }
            local_irq_disable();  // 关闭中断
            // 执行完tasklet_vec链表中的一个节点,则将这个节点从链表中删除
            t->next = NULL;
            *__this_cpu_read(tasklet_vec.tail) = t;
            __this_cpu_write(tasklet_vec.tail, &(t->next));
            __raise_softirq_irqoff(TASKLET_SOFTIRQ);
            local_irq_enable();  // 开启中断
        }
    }
    static void tasklet_hi_action(struct softirq_action *a)
    {
        struct tasklet_struct *list;
        local_irq_disable();
        list = __this_cpu_read(tasklet_hi_vec.head);
        __this_cpu_write(tasklet_hi_vec.head, NULL);
        __this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head));
        local_irq_enable();
        while (list) {
            struct tasklet_struct *t = list;
            list = list->next;
            if (tasklet_trylock(t)) {
                if (!atomic_read(&t->count)) {
                    if (!test_and_clear_bit(TASKLET_STATE_SCHED,
                                &t->state))
                        BUG();
                    t->func(t->data);
                    tasklet_unlock(t);
                    continue;
                }
                tasklet_unlock(t);
            }
            local_irq_disable();
            t->next = NULL;
            *__this_cpu_read(tasklet_hi_vec.tail) = t;
            __this_cpu_write(tasklet_hi_vec.tail, &(t->next));
            __raise_softirq_irqoff(HI_SOFTIRQ);
            local_irq_enable();
        }
    }

DECLARE_TASKLETDECLARE_TASKLET_DISABLED可以静态的定义一个tasklet_struct,前者定义的tasklet可以被执行,后者定义的tasklet不能被执行。tasklet_init可以动态的初始化tasklet_struct

    [include/linux/interrupt.h]
    #define DECLARE_TASKLET(name, func, data) \
        struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
    #define DECLARE_TASKLET_DISABLED(name, func, data) \
        struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

    [kernel/softirq.c]
    // 初始化tasklet链表节点
    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);  // count被初始化为0,说明处于可执行状态
        t->func = func;
        t->data = data;
    }

tasklet_enabletasklet_disable可开启和禁止tasklet,前置减少count字段的值,为0表示可执行,后者增大count字段的值,为1表示不执行。tasklet_schedule设置TASKLET_STATE_SCHED标志并将节点插入到tasklet链表中,最后触发TASKLET_SOFTIRQ类型的软中断,调用tasklet_schedule后,该节点的tasklet处于就绪状态,执行软中断时会得到处理,只有没有设置TASKLET_STATE_SCHED的节点才会加入链表,若设置了,则不做任何操作。tasklet_schedule将要执行的节点挂到tasklet_vec链表中,tasklet_hi_schedule将要执行的节点挂到tasklet_hi_vec链表中。

    [include/linux/interrupt.h]  // 使能tasklet
    static inline void tasklet_enable(struct tasklet_struct *t)
    {
        smp_mb__before_atomic();
        atomic_dec(&t->count);
    }
    static inline void tasklet_disable(struct tasklet_struct *t)  // 禁止tasklet
    {
        tasklet_disable_nosync(t);
        tasklet_unlock_wait(t);
        smp_mb();
    }
    // 设置tasklet状态为TASKLET_STATE_SCHED,同时将t节点插入到tasklet_vec链表中
    static inline void tasklet_schedule(struct tasklet_struct *t)
    {
        if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_schedule(t);
    }
    void __tasklet_schedule(struct tasklet_struct *t)
    {
        unsigned long flags;
        local_irq_save(flags);
        t->next = NULL;
        *__this_cpu_read(tasklet_vec.tail) = t;
        __this_cpu_write(tasklet_vec.tail, &(t->next));
        raise_softirq_irqoff(TASKLET_SOFTIRQ);  // 触发TASKLET_SOFTIRQ类型的软中断
        local_irq_restore(flags);
    }
    static inline void tasklet_hi_schedule(struct tasklet_struct *t)
    {
        if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_hi_schedule(t);
    }
    void __tasklet_hi_schedule(struct tasklet_struct *t)
    {
        unsigned long flags;
        local_irq_save(flags);
        t->next = NULL;
        *__this_cpu_read(tasklet_hi_vec.tail) = t;
        __this_cpu_write(tasklet_hi_vec.tail,  &(t->next));
        raise_softirq_irqoff(HI_SOFTIRQ); // 触发HI_SOFTIRQ类型的软中断
        local_irq_restore(flags);
    }

tasklet不是在某个固定的CPU上执行。tasklet挂接到那个CPU的链表上,则由那个CPU执行。一旦挂入某个CPU的链表,就必须等待该CPU清除TASKLET_STATE_SCHED标志后,才有机会到其他CPU上运行。执行tasklet_scheduletasklet_hi_schedule的CPU就是执行tasklet的CPU

参考资料

  1. Linux kernel V4.6版本源码
  2. 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
  3. 《Linux内核深度解析》

你可能感兴趣的:(#,Linux中断子系统,内核,linux,softirq,tasklet,中断下半部)