基于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