当执行到网卡通过硬件中断(IRQ)通知CPU有数据来了,CPU会根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数。驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。

于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数。

软中断

硬中断处理函数(handler)执行时,会屏蔽部分或全部(新的)硬中断。中断被屏蔽的时间越长,丢失事件的可能性也就越大。有耗时的操作都应该从硬中断处理逻辑中剥离出来,硬中断因此能尽可能快地执行,然后再重新打开硬中断。
对于网络栈,我们接下来只看软中断ksoftirqd
可以把软中断系统想象成一系列内核线程(每个 CPU 一个),这些线程执行针对不同事件注册的处理函数(handler),内核子系统(比如网络)能通过open_softirq() 注册软中断处理函数。通过 top 命令,会注意到 ksoftirqd/0 这个内核线程,其表示这个软中断线程跑在 CPU 0 上,如下图所示。
2020082417085412.png

ksoftirqd

软中断对分担硬中断的工作量至关重要,因此软中断线程在内核启动的很早阶段就 spawn 出来了
kernel/softirq.c 中对 ksoftirqd 系统进行了初始化:

  1. static struct smp_hotplug_thread softirq_threads = {
  2. .store = &ksoftirqd,
  3. .thread_should_run = ksoftirqd_should_run,
  4. .thread_fn = run_ksoftirqd,
  5. .thread_comm = "ksoftirqd/%u",
  6. };
  7. static __init int spawn_ksoftirqd(void)
  8. {
  9. register_cpu_notifier(&cpu_nfb);
  10. BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
  11. return 0;
  12. }
  13. early_initcall(spawn_ksoftirqd);

看到注册了两个回调函数: ksoftirqd_should_run 和run_ksoftirqd。这两个函数都会从 kernel/smpboot.c 里调用,作为事件处理循环的一部分。

kernel/smpboot.c 里面的代码首先调用 ksoftirqd_should_run 判断是否有 pending 的软中断,如果有,就执行 run_ksoftirqd,后者做一些 bookeeping 工作,然后调用 __do_softirq。

__do_softirq 做的几件事情:

判断哪个 softirq 被 pending
计算 softirq 时间,用于统计
更新 softirq 执行相关的统计数据
执行 pending softirq 的处理函数

  1. asmlinkage __visible void __do_softirq(void)
  2. {
  3. unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
  4. unsigned long old_flags = current->flags;
  5. int max_restart = MAX_SOFTIRQ_RESTART;
  6. struct softirq_action *h;
  7. bool in_hardirq;
  8. __u32 pending;
  9. int softirq_bit;
  10. /*
  11. * Mask out PF_MEMALLOC s current task context is borrowed for the
  12. * softirq. A softirq handled such as network RX might set PF_MEMALLOC
  13. * again if the socket is related to swap
  14. */
  15. current->flags &= ~PF_MEMALLOC;
  16. pending = local_softirq_pending(); //获取当前CPU的软中断寄存器__softirq_pending值到局部变量pending。
  17. account_irq_enter_time(current);
  18. __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); //增加preempt_count中的softirq域计数,表明当前在软中断上下文中。
  19. in_hardirq = lockdep_softirq_start();
  20. restart:
  21. /* Reset the pending bitmask before enabling irqs */
  22. set_softirq_pending(0); //清除软中断寄存器__softirq_pending。
  23. local_irq_enable(); //打开本地中断
  24. h = softirq_vec; //指向softirq_vec第一个元素,即软中断HI_SOFTIRQ对应的处理函数。
  25. while ((softirq_bit = ffs(pending))) { //ffs()找到pending中第一个置位的比特位,返回值是第一个为1的位序号。这里的位是从低位开始,这也和优先级相吻合,低位优先得到执行。如果没有则返回0,退出循环。
  26. unsigned int vec_nr;
  27. int prev_count;
  28. h += softirq_bit - 1; //根据sofrirq_bit找到对应的软中断描述符,即软中断处理函数。
  29. vec_nr = h - softirq_vec; //软中断序号
  30. prev_count = preempt_count();
  31. kstat_incr_softirqs_this_cpu(vec_nr);
  32. trace_softirq_entry(vec_nr);
  33. h->action(h); //执行对应软中断函数
  34. trace_softirq_exit(vec_nr);
  35. if (unlikely(prev_count != preempt_count())) {
  36. pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
  37. vec_nr, softirq_to_name[vec_nr], h->action,
  38. prev_count, preempt_count());
  39. preempt_count_set(prev_count);
  40. }
  41. h++; //h递增,指向下一个软中断
  42. pending >>= softirq_bit; //pending右移softirq_bit位
  43. }
  44. rcu_bh_qs();
  45. local_irq_disable(); //关闭本地中断
  46. pending = local_softirq_pending(); //再次检查是否有软中断产生,在上一次检查至此这段时间有新软中断产生。
  47. if (pending) {
  48. if (time_before(jiffies, end) && !need_resched() && max_restart) //再次触发软中断执行的三个条件:1.软中断处理时间不超过2jiffies,200Hz的系统对应10ms;2.当前没有有进程需要调度,即!need_resched();3.这种循环不超过10次。
  49. goto restart;
  50. wakeup_softirqd(); //如果上面的条件不满足,则唤醒ksoftirq内核线程来处理软中断。
  51. }
  52. lockdep_softirq_end(in_hardirq);
  53. account_irq_exit_time(current);
  54. __local_bh_enable(SOFTIRQ_OFFSET); //减少preempt_count的softirq域计数,和前面增加计数呼应。表示这段代码处于软中断上下文。
  55. WARN_ON_ONCE(in_interrupt());
  56. tsk_restore_flags(current, old_flags, PF_MEMALLOC);
  57. }

监测

软中断的信息可以从 /proc/softirqs 读取:

总结

中断是一种异步的事件处理机制,用来提高系统的并发处理能力。中断事件发生,会触发执行中断处理程序,而中断处理程序被分为上半部和下半部这两个部分。上半部对应硬中断,用来快速处理中断;下半部对应软中断,用来异步处理上半部未完成的工作。Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,我们可以查看 proc 文件系统中的 /proc/softirqs ,观察软中断的运行情况。在 Linux 中,每个 CPU 都对应一个软中断内核线程,名字是 ksoftirqd/CPU 编号。当软中断事件的频率过高时,内核线程也会因为 CPU 使用率过高而导致软中断处理不及时,进而引发网络收发延迟、调度缓慢等性能问题。

参考资料:
https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/
https://www.cnblogs.com/luoahong/p/10815283.html
https://blog.csdn.net/qq_34258344/article/details/108202096