linux arm32中断子系统学习总结(四)--- 软中断

四、linux软中断实现机制

  软中断是一种内核机制,又叫做中断的“底半部”;内核定义了10种软中断,从程序实现上看,软中断就是一组函数,一种软中断一个函数,只不过内核设计了一种机制来调用这些函数;我们在使用软中断的时候,只要把我们的函数挂在这些软中断的执行函数里面,那么,内核就会通过软中断运行机制调用我们的函数;软中断执行时,有可能处于硬件中断上下文,因此软中断执行函数中不能有引起任务切换的函数;

  本文通过以下几个方面来总结软中断的实现机制:

  1、内核通过哪些数据结构来组织软中断

  2、软中断是在什么时候被运行的

  3、以设备驱动常用的tasklet为例,来说明软中断的使用方法

4.1 内核通过哪些数据结构来组织软中断

  linux内核中定义了10种软中断,把各软中断的执行函数放在softirq_vec[NR_SOFTIRQS]数组里面;然后,定义了一个数组irq_stat[NR_CPUS],每个数组的元素由一个cpu使用,里面是软中断的pending标志位,表示该cpu有哪些软中断待处理,当需要调度某个软中断运行的时候,只需要把对应的pendging标志位置1,那么当内核处理软中断时,检测pendging标志位,就会执行该软中断的函数。

  linux4.14内核中定义了10种软中断,如下的枚举成员表示10种软中断的类型:

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

  内核定义了一个softirq_action结构体数组,每个数组元素表示一种软中断,里面挂接了各软中断的执行函数,数组下标就是各软中断的枚举值,由于内核在执行软中处理函数时是按数组下标遍历如下数组执行,因此,数组下标越小表示软中断的优先级越高,也就是上述软中断枚举中,枚举值越小,表示优先级越高,软中断中优先级最高的是HI_SOFTIRQ

struct softirq_action
{
    void    (*action)(struct softirq_action *);
};
/* 存放各软中断执行函数的数组 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

  内核定义了一个irq_stat[NR_CPUS]结构体数组,每个数组元素由一个cpu使用,里面有该cpu软中断的pengding标志位__softirq_pending,用于表示该cpu有哪些软中断待处理,当需要让某个cpu执行某个软中断时,只需要将该cpu的软中断pengding标志位置1就可以了。

typedef struct {
    unsigned int __softirq_pending;
    unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;
/* 表示各cpu软中断pengding标志位的数组 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

  从上面的数据结构可以看出,各软中断的处理函数是各cpu共享的,但是每个cpu有各自的软中断pending标志位,内核中软中断激活接口也是激活某个cpu的软中断,于是软中断的处理函数需要是可重入的,但是两个用于实现tasklet的软中断例外(HI_SOFTIRQ、TASKLET_SOFTIRQ),内核对这两个tasklet的软中断执行流程做了特殊处理,保证同一时刻只有一个cpu在执行同一个tasklet,当然,不同的tasklet可以并发在不同的cpu执行。

  与软中断相关的关键数据结构还有task_struct结构体中的thread_info.preempt_count,preempt_count字段用于记录当前任务所处的context状态;

PREEMPT_BITS[0:7]:该字段记录显示禁用本地cpu内核抢占的次数(怎么禁用本地cpu内核抢占);
                   用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1;
SOFTIRQ_BITS[8:15]:软中断计数器,表示可延迟函数被禁用的程度,local_bh_disable()会增加该字段的值
                ;用于同步处理,关掉下半部的时候加1,打开下半部的时候减1;
HARDIRQ_BITS[16:27]:硬中断计数,表示本地cpu上中断处理程序的嵌套数(irq_enter宏递增它的值,irq_exit()宏递减它的值)
                ;用于表示处于硬件中断上下文中;
PREEMPT_ACTIVE标志[28]:

4.2 软中断是在什么时候被运行的

  如下3个点会运行软中断,还有其他的运行点,这里没有一一列出:

  1、硬件中断退出函数irq_exit()里面会运行软中断处理函数

  2、软中断使能函数local_bh_enable()里面会运行软中断处理函数

  3、软中断的内核线程ksoftirqd会运行软中断的处理函数  ---  这个内核线程的优先级比较低,可以保证其他线程有机会运行

  这3个运行点都是调用__do_softirq函数来执行softirq_vec数组里面的软中断函数,具体内容如下:

/*
 * __do_softirq
 * 运行本cpu处于pending状态的软中断执行函数
 * 本函数设计了一个restart的大循环,每次大循环都会读取本cpu的pending标志位
 * restart开始运行时,使能cpu本地中断,因此在处理软中断过程中可能会有新的pending标志位被置上
 * 然后,在restart循环里面设计了一个while循环,while循环中会调用所有处于pending的软中断执行函数
 * while循环处理完所有pengding里面的软中断后,会检查如下条件,看是否再次进行restart大循环
 * 1. 再次读取pending标志位,如果没有待处理的软中断,则退出restart
 * 2. 本次__do_softirq函数运行的时间已经超过MAX_SOFTIRQ_TIME,则退出restart
 * 3. 当前线程需要被调度出去need_resched,则退出restart
 * 4. while循环已经进行了MAX_SOFTIRQ_RESTART次,则退出restart
 * 如果while循环结束后,再次读取的pending标志位里面有新的待处理软中断,但是满足restart退出条件,那么调度软中断线程,退出restart
*/
__do_softirq
    /* 取出软中断pending */
    pending = local_softirq_pending();
    /*
     * 禁止软中断 
     * 软中断必须以串行的方式在cpu运行,如果该cpu此时有硬件中断,那么硬中断退出时也会调用软中断处理函数
     * 禁止软中断后,in_interrupt()函数会返回1
    */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
        preempt_count_add(cnt);
    /*
     * 这里会循环执行,直到满足条件才退出循环restart
     * 退出条件1:本cpu的软中断pending标志位没有待处理的软中断
     * 退出条件2:本次软中断处理时间超时MAX_SOFTIRQ_TIME
     * 退出条件3:当前线程不需要被调度出去need_resched
     * 退出条件4:处理了MAX_SOFTIRQ_RESTART软中断,每次处理pengding里面所有的标志位
    */
restart:
    /* 处理软中断回调函数前,使能本地cpu硬件中断 */
    local_irq_enable();
    /* 各软中断回调函数的数组首地址 */
    h = softirq_vec;
    /* 
     * 取出待处理优先级最高的软中断 
     * 按优先级处理pengding的软中断
     * 
    */
    while ((softirq_bit = ffs(pending)))
        unsigned int vec_nr;
        int prev_count;
        /* 本次处理的软中断 */
        h += softirq_bit - 1;
        /*
         * 不懂,这是干嘛
        */
        vec_nr = h - softirq_vec;
        kstat_incr_softirqs_this_cpu(vec_nr);
        /*
         * 跟踪preempt_count标志位
         * 处理软中断回调函数的过程中,如果该标志位发生变化,会打印log提示
        */
        prev_count = preempt_count(); --- 抢占标志位在哪些情况下会发生变化
        /*
         * 调用软中断的回调函数
        */
        trace_softirq_entry(vec_nr);
        h->action(h);
        trace_softirq_exit(vec_nr);
        /*
         * 如果preempt_count标志位发生变化,打印提示
        */
        if (unlikely(prev_count != preempt_count())) {
            pr_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_set(prev_count);
        }
        /* 更新临时变量 */
        h++;
        pending >>= softirq_bit;
        /* 至此,完成一次pending标志位的循环处理 */
    /*
     * RCU软中断处理,不懂。。
    */
    rcu_bh_qs();
    /*
     * 禁止cpu本地硬件中断
    */
    local_irq_disable();
    /*
     * 读取pending标志位,并判断是否继续循环处理软中断
    */
    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        wakeup_softirqd();
    }
    /* 以上为软中断循环处理逻辑 */
    /*
     * 以下为软中断处理函数__do_softirq退出时的处理
    */
    /* 标志位处理 */
    lockdep_softirq_end(in_hardirq);
    /*
     * 使能软中断,与上面的__local_bh_disable_ip对应
    */
    __local_bh_enable(SOFTIRQ_OFFSET);
        preempt_count_sub(cnt)

4.2.1 硬件中断退出函数irq_exit()里面运行软中断处理函数

  在硬件中断处理函数退出的irq_exit函数里面会执行软中断,如下

irq_exit
    /* 禁用本地中断 */
    local_irq_disable();
    /* 递减preempt_count字段的硬中断计数器 */
    preempt_count_sub(HARDIRQ_OFFSET);
    /*
     * 不在硬件中断上下文、没有禁用软中断、并且有软中断待处理,那么开始处理软中断
    */
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq
            /*
             * 如果软中断内核线程ksoftirq正在执行,则直接返回
            */
            if (ksoftirqd_running())
                return;
            /*
             * 如果中断强制线程化的全局变量被置1,那么转去调度ksoftirq来执行软中断
             * 如果没有强制中断线程化,那么开始处理软中断
            */
            if (!force_irqthreads)
                /*
                 * 中断没有强制线程化
                 * __do_softirq这个函数里面最多直接处理10个软中断,剩余的软中断通过调度ksoftirq线程去处理
                */
                __do_softirq
            else
                /*
                 * 中断强制线程化,那么调度ksoftirq线程去处理软中断
                */
                wakeup_softirqd

4.2.2 软中断使能函数local_bh_enable()里面运行软中断处理函数

local_bh_enable
    __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
        /*
         * 禁止本地cpu中断
        */
        #ifdef CONFIG_TRACE_IRQFLAGS
            local_irq_disable();
        #endif
        /*
         * 如果preempt_count里面的软中断字段为SOFTIRQ_DISABLE_OFFSET
        */
        if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
            trace_softirqs_on(ip)  --- 会使能软中断,但不清楚具体干嘛
        /*
         * 调用软中断回调函数前,
         * 把preempt_count中软中断字段减去SOFTIRQ_DISABLE_OFFSET - 1
        */
        preempt_count_sub(cnt - 1);
        /* 
         * 执行软中断回调函数
        */
        if (unlikely(!in_interrupt() && local_softirq_pending()))
            do_softirq();
                /* 在硬件中断上下文,或者禁止软中断,直接返回 */
                if (in_interrupt())
                    return;
                /*
                 * 又一次禁止cpu本地中断
                */
                local_irq_save(flags);
                pending = local_softirq_pending();
                /*
                 * 如果有待处理的软中断,并且软中断线程没有在执行,那么调用软中断处理函数执行
                */
                if (pending && !ksoftirqd_running())
                    do_softirq_own_stack();
                        __do_softirq();
                /*
                 * 使能cpu本地硬件中断
                */
                local_irq_restore(flags);
        /*
         * 处理完软中断回调函数后,再把preempt_count中软中断字段减去1
        */
        preempt_count_dec();
        /*
         * 使能cpu本地硬件中断
        */
        #ifdef CONFIG_TRACE_IRQFLAGS
            local_irq_enable();
        #endif
        /*
         * 按条件是否执行调度
        */
        preempt_check_resched();
            if (should_resched(0))
                __preempt_schedule();

4.2.3 软中断的内核线程ksoftirqd中运行软中断的处理函数

  每个cpu都有一个软中断线程

/*
 * 内核线程ksoftirqd创建流程
*/
static struct smp_hotplug_thread softirq_threads = {
    .store            = &ksoftirqd,
    .thread_should_run    = ksoftirqd_should_run,
    .thread_fn        = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};

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));

    return 0;
}
early_initcall(spawn_ksoftirqd);
        
/*
 * 软中断的内核线程ksoftirqd中,软中断的执行流程
 * ksoftirqd回调函数run_ksoftirqd
 * 哪个cpu激活的软中断,那么这个软中断就由那个cpu执行
*/
run_ksoftirqd
    /* 禁止cpu本地中断 */
    local_irq_disable();
    /* 如果有pending的软中断,则进行处理 */
    if (local_softirq_pending())
        __do_softirq();  --- irq_exit、local_bh_enable函数中都调用这个函数处理软中断
    /* 使能cpu本地中断 */
    local_irq_enable();

4.3 tasklet使用方法

4.3.1 tasklet初始化

/*
 * tasklet初始化
 * 初始化只需要定义一个结构体变量struct tasklet_struct
 * 然后,使用tasklet_init函数,将tasklet的执行函数绑定到结构体变量struct tasklet_struct
*/
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->data = data;
} 

4.3.2 tasklet挂接

  tasklet可以挂接在两个软中断中运行:HI_SOFTIRQ:优先级最高的软中断、TASKLET_SOFTIRQ:优先级较低的软中断;

  tasklet_hi_schedule接口用来将tasklet绑定到软中断HI_SOFTIRQ;

/*
 * tasklet_hi_schedule
 * 函数执行内容如下
*/
tasklet_hi_schedule
    /* 如果这个tasklet正在被调度运行,那么,直接返回,否则,将tasklet挂接到优先级最高的软中断 */
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_hi_schedule(t);
            /* 禁止cpu本地硬件中断 */
            local_irq_save(flags);
            t->next = NULL;
            /* 将tasklet挂接到本cpu的HI_SOFTIRQ软中断对应的链表尾部 */
            *__this_cpu_read(tasklet_hi_vec.tail) = t;
            /* 将链表尾指针置空 */
            __this_cpu_write(tasklet_hi_vec.tail,  &(t->next));
            /* 
             * 将本cpu的软中断pending标志位的HI_SOFTIRQ位置1 
             * 那么在上面所述的3个软中断运行点中,就会执行HI_SOFTIRQ软中断的回调函数,从而调用到本tasklet的执行函数
            */
            raise_softirq_irqoff(HI_SOFTIRQ);
                __raise_softirq_irqoff(nr);
                    or_softirq_pending(1UL << nr);
            /* 如果不在硬件中断上下文,并且软中断没有被置位,那么唤醒软中断线程运行 */
            if (!in_interrupt())
                wakeup_softirqd();
                    struct task_struct *tsk = __this_cpu_read(ksoftirqd);
                    if (tsk && tsk->state != TASK_RUNNING)
                        wake_up_process(tsk);
            /* 使能cpu本地中断 */
            local_irq_restore(flags);

  tasklet_schedule接口用来将tasklet绑定到软中断TASKLET_SOFTIRQ;

/*
 * tasklet_schedule
 * 函数执行内容如下
 * 执行流程与tasklet_hi_schedule一致,只是将tasklet与TASKLET_SOFTIRQ软中断绑定
*/
static inline void tasklet_schedule(struct tasklet_struct *t)
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
            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);
            local_irq_restore(flags);

4.3.3 tasklet执行函数被调用的流程

  tasklet的执行函数由软中断TASKLET_SOFTIRQ或HI_SOFTIRQ的执行函数调用执行;TASKLET_SOFTIRQ或HI_SOFTIRQ的执行函数在软中断初始化时绑定,

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使用了TASKLET_STATE_SCHED做判断,使得同一个tasklet不能并发在不同cpu执行,因此,tasklet的执行函数不需要是可重入函数;

/*
 * tasklet执行函数被调用的流程
 * HI_SOFTIRQ
 * TASKLET_SOFTIRQ软中断的执行流程与HI_SOFTIRQ一样
*/
__do_softirq
    /*
     * 禁止软中断,使能本地cpu硬件中断
    */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    local_irq_enable();
    /*
     * 调用各软中断的action函数
     * HI_SOFTIRQ对应的action函数是:tasklet_hi_action
    */
    h->action(h);  --- 也就是:tasklet_hi_action
    /* tasklet_hi_action */
        /*
         * 把tasklet_struct链表从软中断的数组中拷贝出来,并且把软中断中的链表置空
         * 为什么这个操作需要关中断进行?因为只有硬件中断会打断软中断,然后,操作软中断链表
        */
        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();
        /*
         * 遍历运行链表中的所有tasklet
        */
        while (list)
            /*
             * 通过标志位TASKLET_STATE_SCHED判断本tasklet有没有在其他cpu执行
             * 如果在其他cpu执行,那么不执行tasklet的执行函数,将本tasklet再放回软中断的链表,等下次软中断被调度时再执行
            */
            if (tasklet_trylock(t))
            {
                t->func(t->data);  --- tasklet的执行函数
                tasklet_unlock(t);
                continue;
            }
            tasklet_unlock(t);
            /* 将tasklet重新放回软中断的链表 */
            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();
    /*
     * 使能软中断,禁止本地cpu硬件中断
    */
    local_irq_disable();
    __local_bh_enable(SOFTIRQ_OFFSET);

参考资料:

《深入理解linux内核》

https://www.cnblogs.com/LoyenWang/p/13124803.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值