首页 > 系统相关 >Linux内核机制—softirq

Linux内核机制—softirq

时间:2023-01-01 11:45:55浏览次数:42  
标签:__ SOFTIRQ 中断 内核 Linux local pending softirq

 基于Linux-5.10.110

一、软中断简介

1. 软中断是一种中断底半部机制,允许在中断上下文中,因此软中断函数中不能休眠。

2. 软中断是函数是在开中硬断的环境下调用,但是调用前判断了是否在中断上下文()中,软中断自身能保证在同一个CPU上的互斥(但不同CPU上可以并发执行)。

2. 系统支持哪些软中断是预先设置好的,如下数组,越靠前的优先级越高。其中 HI_SOFTIRQ 用于高优先级的tasklet,TASKLET_SOFTIRQ 用于普通的tasklet。TIMER_SOFTIRQ 用于基于系统tick的timer。
NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ 是用于网卡数据收发的。BLOCK_SOFTIRQ 和 BLOCK_IOPOLL_SOFTIRQ 是用于block device的。SCHED_SOFTIRQ 用于多CPU之间的负载均衡的。HRTIMER_SOFTIRQ 用于高精
度timer的。RCU_SOFTIRQ是处理RCU的。

enum //include/linux/interrupt.h
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,

    NR_SOFTIRQS
};

 

二、相关数据结构

1. 软中断描述符 struct softirq_action

struct softirq_action //include/linux/interrupt.h
{
    void (*action)(struct softirq_action *);
};

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; //softirq.c

每个软中断号都对应一个 softirq_action 结构,softirq_vec 类似与软中断向量表,它是全局唯一的,所有CPU共享一个软中断向量表。

2. 软中断pending状态 irq_cpustat_t

typedef struct { //asm/hardirq.h
    unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat); //softirq.c

可以看到此结构中只有一个 __softirq_pending 成员,有点类似中断状态寄存器,它是一个位掩码,当对应的软中断pending等待执行时,此软中断对应的bit位将被置1。

系统中还定义了一个per-cpu的 irq_stat,用来描述每个CPU上软中断的pending状态,使用 local_softirq_pending() 来判断是否有pending状态的软中断。

3. __u32 per-cpu active_softirqs

DEFINE_PER_CPU(__u32, active_softirqs);

是个位掩码,表示目前 __do_softirq() 正在执行哪些软中断的回调函数。

4. kstat.softirqs[irq]

//sched/core.c
struct kernel_stat {
    unsigned long irqs_sum;
    unsigned int softirqs[NR_SOFTIRQS];
};

DEFINE_PER_CPU(struct kernel_stat, kstat);
EXPORT_PER_CPU_SYMBOL(kstat);

用于在 __do_softirq() 中统计各类型的软中断在各个CPU上分别执行的次数。


三、相关函数

1. 上下文判断

#define in_irq()        (preempt_count() & HARDIRQ_MASK) //bit16-bit19
#define in_softirq()    (preempt_count() & SOFTIRQ_MASK) //bit8-bit15
#define in_interrupt()    (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)) //bit20-bit23

#define in_serving_softirq()    ((preempt_count() & SOFTIRQ_MASK) & SOFTIRQ_OFFSET) //bit8

可以看到软中断也是属于中断上下文的。preempt_count() 是 current_thread_info()->preempt.count 的只,它是current线程的,因此也是一个per-cpu的变量,只能用于判断当前CPU所处于的上下文,因
此这里define的函数可以直接使用来判断当前CPU是出于什么上下文的。

in_serving_softirq()这个函数需要特殊介绍下,若进程和软中断实现中存在竞争,在进程上下文中调用 local_bh_disable()/local_bh_enable() 保护的临界区也是运行在软中断上下文的,那么如何判断是
哪种情况的软中断上下文呢,这个函数就起到了作用,其只判断了bit8。若为1表示当前CPU正在处理软中断函数,因为 __do_softirq() 中对软中断bit位加1(加的是bit8),而开关底半部操作的是软中断bit位
的bit2(bit9)。

2. 开关中断底半部

为什么开关软中断的函数取名上不是 local_softirq_{enable/disable}(),而是下面两个函数呢,因为中断底半部的另一个机制tasklet也是基于软中断实现的,若关了软中断等同于将tasklet也关闭了。

(1) local_bh_disable() 关软中断

static inline void local_bh_disable(void) //linux/bottom_half.h
{
    /* 软中断对应的是 bit8-bit15,注意这里加上的是bit9 */
    __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET); //2 * 1<<8
}

static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt) //linux/bottom_half.h
{
    preempt_count_add(cnt);
    barrier();
}

关软中只做了一件事,就是就是将软中断对应的bit位加2(而不是加1)。

(2) local_bh_enable() 开软中断

static inline void local_bh_enable(void) //linux/bottom_half.h
{
    __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);  //2 * 1<<8
}

/* 进程上下文执行 */
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
    /* 在硬中断上下文(硬中断handler执行或关硬中断临界区)中使能软中断会触发断言 */
    WARN_ON_ONCE(in_irq());
    /* 断言,需要是开着硬中断的 */
    lockdep_assert_irqs_enabled();

    /* Keep preemption disabled until we are done with softirq processing: */
    preempt_count_sub(cnt - 1); //---------注释(1)

    /* 
     * 若当前不在中断(不可屏蔽中断/硬中断/软中断)上下文,且有pending的软中断,则调用软中断 
     * in_interrupt()的判断保证了在同一个CPU上软中断函数的互斥。
     */
    if (unlikely(!in_interrupt() && local_softirq_pending())) { //---------注释(2)
        /* 执行软中断处理函数,在进程上下文调用 */
        do_softirq();
    }

    /* 此函数只是将抢占计数减1(没有触发抢占) */
    preempt_count_dec();

    /* 检查是否需要抢占,若抢占计数为0且使能抢占,则进行抢占调度 */
    preempt_check_resched();
}
EXPORT_SYMBOL(__local_bh_enable_ip);

在开软中断时就会判断是否需要调用软中断处理函数。开软中断也是一个抢占点,但是抢占点发生在软中断处理函数执行完毕之后。

注释(1): 在 local_bh_disable() 中为 preempt_count 增加了 SOFTIRQ_DISABLE_OFFSET,但在 local_bh_enable()首先减去的是 SOFTIRQ_DISABLE_OFFSET-1,为何不一次性的减去
SOFTIRQ_DISABLE_OFFSET 呢。假设这样一个场景,进程和软中断共享了数据,在进程临界区中使用 local_bh_disable()/local_bh_enable() 进行保护,由于临界区中是没有关中断的,
那么就有可能产生了硬中断,然后在硬中断中触发了软中断,由于当前CPU是关闭软中断的状态,因此不会执行要延迟到开软中断时才能执行。当进程执行完临界区,调用 local_bh_enable()
开软中断时,若是减去的是 SOFTIRQ_DISABLE_OFFSET,那么就可能发生抢占,因为抢占计数可能减为0了。但是此时是不能抢占的,因为抢占后当前任务就可能运行在其它CPU上了,而软
中断的处理依赖 per-cpu 的 irq_cpustat_t irq_stat 变量。比如抢占发生在上面函数的 local_softirq_pending() 和 do_softirq() 中关中断之间,抢占后当前任务从CPU-A跳到了CPU-B
上,这就导致了在CPU-A上判断有pending的软中断,却调用的是CPU-B上pending的软中断函数。

因此这里减去 SOFTIRQ_DISABLE_OFFSET-1 即保证了软中断对应的bit位减去了1(没有嵌套的话就变为0了),又保证的抢占bit位不为0不发生抢占,当软中断处理完后再在抢占bit位上减去1,
打开抢占。

注释(2): 这里的 in_interrupt() 的话就不在调用软中断,来保证在同一个CPU上软中断函数的互斥。

 

四、软中断实现

1. 注册软中断函数

/* 比如 open_softirq(RCU_SOFTIRQ, rcu_core_si) */
void open_softirq(int nr, void (*action)(struct softirq_action *)) //softirq.c
{
    softirq_vec[nr].action = action;
}

 

2. 触发软中断

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

    /* 注意关中断是不会设置 preempt_count 的硬中断标志位的 */
    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

/* 若调用时已经是关着中断的,可直接调用这个函数 */
inline void raise_softirq_irqoff(unsigned int nr)
{
    /* 在当前CPU对应的 irq_stat.__softirq_pending 或上 nr bit位 */
    __raise_softirq_irqoff(nr);

    /* 若是在中断上下文,中断退出后会执行软中断。否则唤醒软中断线程执行 */
    if (!in_interrupt())
        wakeup_softirqd();
}

这里关中断是为了保证per-cpu的 irq_stat.__softirq_pending 或上对应软中断bit位的原子性,关了中断也就关了抢占和软中断。

 

3. 软中断执行路径

(1) 中断退出执行

/* 调用路径:irq_exit --> __irq_exit_rcu */
static inline void __irq_exit_rcu(void) //softirq.c
{
    /* 此时正在退出中断,断言硬中断是关闭的 */
    lockdep_assert_irqs_disabled();
    /* 恢复硬中断对应的抢占计数 */
    preempt_count_sub(HARDIRQ_OFFSET);

    if (!in_interrupt() && local_softirq_pending()) //注释(3)
        invoke_softirq();
}

static inline void invoke_softirq(void)
{
    /* 若不抢占中断线程化,直接调用 */
    __do_softirq();
}

注释(3):这里 in_interrupt() 判断来保证在单个CPU上软中断执行函数的互斥,若 !in_interrupt() 成立,则说明是硬中断退出到软中断的,因此不调用软中断处理函数。

(2) 唤醒ksoftirqd内核线程执行

可以看到上面 raise_softirq() 函数,若判断当前不在中断上下文中就唤醒per-cpu的 ksoftirqd/X 内核线程。

static void wakeup_softirqd(void)
{
    /* Interrupts are disabled: no need to stop preemption */
    struct task_struct *tsk = __this_cpu_read(ksoftirqd);

    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
}

static struct smp_hotplug_thread softirq_threads = {
    .store            = &ksoftirqd, //per-cpu的二级指针
    .thread_should_run    = ksoftirqd_should_run,
    .thread_fn        = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};

static __init int spawn_ksoftirqd(void)
{
    smpboot_register_percpu_thread(&softirq_threads);
    return 0;
}
early_initcall(spawn_ksoftirqd);

创建的内核线程的函数体为 smpboot_thread_fn(), 位于 smpboot.c 中,进程名为"ksoftirqd/%u", 这个内核线程是prio=120的CFS线程。这个线程是个死循环,被唤醒后,当判断 thread_should_run 回
调返回为真的话就调用 thread_fn 回调进行处理。

static int ksoftirqd_should_run(unsigned int cpu)
{
    return local_softirq_pending();
}

/* 进程上下文调用 */
static void run_ksoftirqd(unsigned int cpu)
{
    /* 关中断的(也就关了抢占),__do_softirq()中又开了中断 */
    local_irq_disable();
    if (local_softirq_pending()) {
        /* 调用软中断函数 */
        __do_softirq();
        local_irq_enable();
        /* CONFIG_PREEMPTION 使能下它是空函数 */
        cond_resched();
        return;
    }
    local_irq_enable();
}

(3) local_bh_enable()

如上面"开关中断底半部"小节的分析,当开中断底半部时,会调用软中断处理函数。

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
    ...
    if (unlikely(!in_interrupt() && local_softirq_pending())) {
        /* 执行软中断处理函数,在进程上下文调用 */
        do_softirq();
    }
    ...
}

asmlinkage __visible 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);
}

 

4. 执行软中断

asmlinkage __visible void __softirq_entry __do_softirq(void) //softirq.c
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME; // jiffies + 1
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART; // 10
    struct softirq_action *h;
    bool in_hardirq;
    __u32 deferred;
    __u32 pending;
    int softirq_bit;

    /* 获取softirq pending的状态 */
    pending = local_softirq_pending();
    /*
     * 若阻塞了RT线程,且有可能处理耗时较长类型的软中断,从pending中清理这些
     * 可能耗时较长的软中断类型,并将被清理的类型赋值给deferred。
     */
    deferred = softirq_deferred_for_rt(pending);
    /* 软中断的处理时间也记录 */
    account_irq_enter_time(current);
    /*
     * 标识下面的代码是正在处理softirq, 就是在bit8-15上加1,因此判断这个bit8可
     * 以决定是否在处理软中断而不是关底半部
     */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

restart:
    /* Reset the pending bitmask before enabling irqs */
    /* 设置回去后,就相当于只取出了认为处理耗时不长的软中断类型到pending成员中 */
    set_softirq_pending(deferred);
    /* 这个per-cpu的变量表示目前正在执行哪些软中断的回调函数 */
    __this_cpu_write(active_softirqs, pending);

    /* softirq handler是开中断执行的 */
    local_irq_enable();

    /* 获取软中断描述符指针 */
    h = softirq_vec;

    /*
     * 寻找pending中第一个被设定为1的bit。最低位从1开始数,返回从最低位开始第几bit为1,
     * 如0b01返回1, 0b101返回3。因此id越小的软中断优先级越高。但是由上面过滤可能耗时长
     * 的软中断类型来看,高优先级的不一定先被调用。
     */
    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;

        /* 转换成从0开始数的数组下标的元素 */
        h += softirq_bit - 1;

        /* 获取soft irq number */
        vec_nr = h - softirq_vec;
        prev_count = preempt_count();

        /* 统计这个类型的软中断在这个CPU上又执行了一次 */
        kstat_incr_softirqs_this_cpu(vec_nr);

        /* 这个trace是每个软中断处理函数都会打印 */
        trace_softirq_entry(vec_nr);
        /* 调用 softirq handler,此时上下文:开着中断关着软中断 */
        h->action(h);
        trace_softirq_exit(vec_nr);

        /* 软中断处理函数中存在开关抢占不匹配的情况,而实际上这个函数不应该关注是否抢占(关bh调用的) */
        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;
    }

    /* 处理完后将正在处理的软中断掩码设置为0 */
    __this_cpu_write(active_softirqs, 0);

    /* 若是唤醒ksoftirqd线程执行的软中断处理函数,标记当前的宽限期不需要等待稍后在此 CPU 
      * 上启动的任何 RCU 读端临界区。TODO:  啥意思? */
    if (__this_cpu_read(ksoftirqd) == current)
        rcu_softirq_qs();

    /* 关闭本地中断 */
    local_irq_disable();

    pending = local_softirq_pending();
    /* 
     * 若当前CPU上阻塞了RT线程,且有可能处理耗时较长类型的软中断,从 pending 中清理这些可能耗
     * 时较长的软中断类型,被清理的放在 deferred 成员中。
     */
    deferred = softirq_deferred_for_rt(pending);

    /*
     * 再次检查softirq pending,有可能上面的softirq handler在执行过程中,发生了中断,又raise了
     * softirq。如果的确如此,判断是否需要继续处理,需要同时满足条件:
     *
     * (1) softirq的处理时间没有超过2个ms(实际是8ms)
     * (2) 没有设定TIF_NEED_RESCHED,也就是说没有有高优先级任务需要调度
     * (3) loop的次数小于10次
     */
    if (pending) {
        /*
         * jiffies 在 end 后,即 jiffies - end > 0, 而 jiffies - (jiffies' + 1) > 0
         * ==> jiffies - jiffies' > 1 ==> jiffies - jiffies' > 2 ==> 单看时间,最长关8ms的中断
         */
        if (time_before(jiffies, end) && !need_resched() && --max_restart)
            goto restart;
    }

#ifdef CONFIG_RT_SOFTINT_OPTIMIZATION
    /* 针对在中断退出和开底半部的情况,若还有pending的软中断,则唤醒softirqd内核线程处理 */
    if (pending | deferred)
        wakeup_softirqd();
#endif
    /* 开软中断 */
    __local_bh_enable(SOFTIRQ_OFFSET);
    /* 断言,不能处于中断上下文了(上面enable已经离开软中断上下文了) */
    WARN_ON_ONCE(in_interrupt());
}

认为处理耗时长的软中断类型:见 LONG_SOFTIRQ_MASK 宏的定义,包含 NET_TX_SOFTIRQ NET_RX_SOFTIRQ BLOCK_SOFTIRQ IRQ_POLL_SOFTIRQ TASKLET_SOFTIRQ。
认为处理耗时短的软中断类型:HI_SOFTIRQ TIMER_SOFTIRQ SCHED_SOFTIRQ HRTIMER_SOFTIRQ RCU_SOFTIRQ

 

五、相关调试接口

1. /proc/softirqs

# cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
          HI:      12790        359        267       5853         36         15         28          2
       TIMER:    2276660    2053707    2350707    2060809      70210      21416      24869      13223
      NET_TX:       4730        279      17481        290         76        102       1926          3
      NET_RX:         93        121          0        210          1          0          0          0
       BLOCK:     165235      92305        269      94644      48067      52765      81151      66118
    IRQ_POLL:          0          0          0          0          0          0          0          0
     TASKLET:      11556       4776        511        875         70         26         41         13
       SCHED:    2971396    2222283    2098633    1721765     163320      41069      61706       9154
     HRTIMER:          0          0          0          0          0          0          0          0
         RCU:    1430869    1339538    1366060    1308709     149514      70305      76098      42497

只读文件,对应 fs/proc/softirqs.c 中的 show_softirqs(),打印每个CPU上每种软中断处理函数的执行次数。


2. /proc/stat

# cat /proc/stat
...
softirq 24549722 19350 8849915 24825 425 600046 0 17860 9266328 0 5770973

只读文件,对应 fs/proc/stat.c 中的 show_stat(),第一个数字是所有CPU上所有软中断类型执行次数之和,也是后面这 NR_SOFTIRQS 个数字的和。后面的 R_SOFTIRQS 个数字依次是所有CPU上各类型的软中断执行次数
分别取和的值。


六、总结

1. 对比三个调用路径可以发现,虽然调用 __do_softirq() 都是关着本地中断后调用的,但是在 __do_softirq() 函数中,在处理软中断函数的时候又是开着中断的,因此所有软中断处理函数都是开着
中断调用的。

2. 软中断处理函数有时候运行在软中断上下文,因为整个调用过程 current::preempt_count 都是加 SOFTIRQ_OFFSET 的。软中断上下文等效是关抢占的,这也是为什么软中断处理函数即使在普通进程
中运行还体现出高优先级的原因。

3. local_irq_disable()/local_irq_enable() 并没有设置 current::preempt_count,只是在调用硬中断时的 __handle_domain_irq --> irq_enter()/irq_exit() 中进行设置 HARDIRQ_OFFSET,

4. 软中断触发是在哪个CPU,执行就是在哪个CPU(通过关抢占和关bh以及per-cpu的ksoftirqd内核线程实现)。

5. 高优先级的软中断并不一定比底优先级的软中断先处理,__do_softirq() 中有判断哪些软中断类型可能耗时较长,对于耗时较长的软中断类型延后处理。

 

标签:__,SOFTIRQ,中断,内核,Linux,local,pending,softirq
From: https://www.cnblogs.com/hellokitty2/p/17017887.html

相关文章

  • Linux上安装虚拟conda环境和神经网络学习框架pytorch
    学习目标:​​Linux系统安装Anaconda3​​;​​配置GPU,安装显卡toolkit​​;创建虚拟环境,安装深度学习框架;学习内容:创建虚拟环境,安装深度学习框架pytorch​​前面已经在anaco......
  • Linux面试必备的红黑树问题,这可能是zui全的一篇了!
    原文网址:https://zhuanlan.zhihu.com/p/471318214首先上一个红黑树知识点结构图1.stl中的set底层用的什么数据结构?2.红黑树的数据结构怎么定义的?3.红黑树有哪些性......
  • Linux 或 Windows 安装 Kafka,示例实现生产与消费消息(一)
    下载:wgethttps://downloads.apache.org/kafka/3.3.1/kafka_2.12-3.3.1.tgz  注意:kafka正常运行,必须配置zookeeper,kafka安装包已经包括zookeeper服务解压:tar-zxvf k......
  • linux 中wget命令的几个常用选项
     001、-O选项,指定输出的文件名[root@PC1test]#wget-Odownload.filehttps://sra-downloadb.be-md.ncbi.nlm.nih.gov/sos5/sra-pub-zq-11/SRR001/770/SRR1770413/S......
  • Linux 安装Nginx集群测试
    5.1停止Nginx服务的四种方法从容停止服务这种方法较stop相比就比较温和一些了,需要进程完成当前工作后再停止。nginx-squit立即停止服务这种方法比较强硬,无论进程......
  • linux 安装redis
    一,redis的官网:https://redis.io///下载wgethttps://download.redis.io/releases/redis-7.0.7.tar.gz二、安装准备//安装gccgcc--versionyuminstallgcc/......
  • linux 中对带有空格、括号的文件进行重命名
     001、[root@PC1test]#lsSraRunInfo(2).csv[root@PC1test]#mvSraRunInfo(2).csvnew_name##复制文件名重命名报错-bash:syntaxerrornearunexpe......
  • linux 中删除文件名中的括号
     001、[root@PC1test]#ls##测试数据SraRunInfo(1).csvSraRunInfo(2).csvSraRunInfo(3).csvSraRunInfo(4).csvSraRunInfo(5).......
  • Linux线程控制
    写在前面我们今天来看线程的知识点,这个博客的内容很多,主要就是为了我们后面的网络做铺垫,最关键的是相比较于进程而言,线程是更加优秀的,我们现在的计算机大多采用的就是线程.......
  • Linux线程互斥
    写在前面这个博客的内容很少,但是很关键,这是我们线程安全相关的内容,里面会涉及到线程互斥和加锁的相关观念,总体而言还是很难的.线程互斥先看一下下面的代码,这里是一切的......