硬中断与软中断

Linux 的中断

中断其实就是由硬件或软件所发送的一种称为IRQ(中断请求)的信号。

中断允许让设备,如键盘,串口卡,并口等设备表明它们需要CPU。

一旦CPU接收了中断请求,CPU就会暂时停止执行正在运行的程序,并且调用一个称为中断处理器或中断服务程序(interrupt service routine)的特定程序。

中断服务程序或中断处理器可以在中断向量表中找到,而这个中断向量表位于内存中的固定地址中。

中断被CPU处理后,就会恢复执行之前被中断的程序。

在机器启动的时候,系统就已经识别了所有设备,并且也把相应的中断处理器加载到中断表中。

中断是系统用来响应硬件设备请求的一种机制,它会打断进程的正常调度和执行,然后调用内核中的中断处理程序来响应设备的请求。

中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。

例如: 当我们在键盘上按下一个按键时,键盘就会对CPU说,一个键已经被按下。

在这种情况下,键盘的IRQ线路中的电压就会发生一次变化,而这种电压的变化就是来自设备的请求,就相当于说这个设备有一个请求需要处理。

中断是系统用来响应硬件设备请求的一种机制,它会打断进程的正常调度和执行,然后调用内核中的中断处理程序来响应设备的请求。

中断分为上半部和下半部

中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。
由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。

特别是,中断处理程序在响应中断时,还会临时关闭中断。这就会导致上一次中断处理完成之前,其他中断都不能响应,也就是说 中断有可能会丢失。

为了解决中断处理程序执行过长和中断丢失的问题,Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:

  • 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。(硬中断)
  • 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。(软中断)

例如: 网卡接收到数据包后,会通过硬件中断的方式,通知内核有新的数据到了。这时,内核就应该调用中断处理程序来响应它。

  • 对上半部来说,既然是快速处理,其实就是要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个软中断信号,通知下半部做进一步的处理。
  • 而下半部被软中断信号唤醒后,需要从内存中找到网络数据,再按照网络协议栈,对数据进行逐层解析和处理,直到把它送给应用程序。

所以,这两个阶段也可以这样理解:

  • 上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行;
  • 而下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。

实际上,上半部会打断 CPU 正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为 “ksoftirqd/CPU 编号”,比如说, 0 号 CPU 对应的软中断内核线程的名字就是 ksoftirqd/0。

不过要注意的是,软中断不只包括了刚刚所讲的硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和 RCU 锁(Read-Copy Update 的缩写,RCU 是 Linux 内核中最常用的锁之一)等。

硬中断

硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。

基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。

处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。

在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,可以同时处理多个中断)。

硬中断可以直接中断CPU。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。

对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存在是为了让调度代码(或称为调度器)可以调度多任务。

/proc/interrupts

/proc/interrupts 是中断报告文件,这个文件包含有关于哪些中断正在使用和每个处理器各被中断了多少次的信息。
Linux内核通常会在第一个CPU上处理中断,以便最大化缓存本地性。

[root@centos]# cat /proc/interrupts 
           CPU0       CPU1       
  0:        137          0   IO-APIC   2-edge      timer
  1:          9          0   IO-APIC   1-edge      i8042
  4:          0        991   IO-APIC   4-edge      ttyS0
  8:          0          0   IO-APIC   8-edge      rtc0
  9:          0          0   IO-APIC   9-fasteoi   acpi
 11:          9          0   IO-APIC  11-fasteoi   virtio3, uhci_hcd:usb1, virtio2
 12:          0         15   IO-APIC  12-edge      i8042
 14:          0     474494   IO-APIC  14-edge      ata_piix
 15:          0          0   IO-APIC  15-edge      ata_piix
 24:          0          0   PCI-MSI 98304-edge      virtio1-config
 25:          0    6997318   PCI-MSI 98305-edge      virtio1-req.0
 26:          0          0   PCI-MSI 81920-edge      virtio0-config
 27:    1319230          1   PCI-MSI 81921-edge      virtio0-input.0
 28:          1          0   PCI-MSI 81922-edge      virtio0-output.0
 29:          0    1658708   PCI-MSI 81923-edge      virtio0-input.1
 30:          1          0   PCI-MSI 81924-edge      virtio0-output.1
NMI:          0          0   Non-maskable interrupts
LOC:  201894981  202754290   Local timer interrupts
SPU:          0          0   Spurious interrupts
PMI:          0          0   Performance monitoring interrupts
IWI:          0          0   IRQ work interrupts
RTR:          0          0   APIC ICR read retries
RES:   56870790   54704720   Rescheduling interrupts
CAL:    4770257    4398566   Function call interrupts
TLB:    4607381    4288388   TLB shootdowns
TRM:          0          0   Thermal event interrupts
THR:          0          0   Threshold APIC interrupts
DFR:          0          0   Deferred Error APIC interrupts
MCE:          0          0   Machine check exceptions
MCP:       1482       1482   Machine check polls
HYP:          0          0   Hypervisor callback interrupts
HRE:          0          0   Hyper-V reenlightenment interrupts
HVS:          0          0   Hyper-V stimer0 interrupts
ERR:          0
MIS:          0
PIN:          0          0   Posted-interrupt notification event
NPI:          0          0   Nested posted-interrupt event
PIW:          0          0   Posted-interrupt wakeup event
  • 第一列为IRQ号
  • 第二列表示在相应的CPU核心上被中断的次数。
  • NMI和LOC是系统所使用的驱动
  • IRQ号决定了需要被CPU处理的优先级。IRQ号越小意味着优先级越高。

硬中断主要分为两类

  • 非屏蔽中断(Non-maskable interrupts,即NMI): 不可以被忽略或取消
  • 可屏蔽中断(Maskable interrupts): 可以被忽略或延迟。

软中断

软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。

通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。

对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。

根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。

软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。

软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。

有一个特殊的软中断是Yield调用,它的作用是请求内核调度器去查看是否有一些其他的进程可以运行。

/proc/softirqs

[root@centos ~]# cat /proc/softirqs 
                    CPU0       CPU1       
          HI:          1          0
       TIMER:   19303943   22118877
      NET_TX:         26         38
      NET_RX:   32728272   31477349
       BLOCK:     260957    6240879
    IRQ_POLL:          0          0
     TASKLET:      10546      11074
       SCHED:   48797723   49599712
     HRTIMER:       1493        934
         RCU:   68247793   68676640
  • 第一列是类型,软中断包括了 10 个类别,分别对应不同的工作类型。比如 NET_RX 表示网络接收中断,而 NET_TX 表示网络发送中断。
  • 要注意同一种软中断在不同 CPU 上的分布情况,也就是同一行的内容。要注意同一种软中断在不同 CPU 上的分布情况,也就是同一行的内容。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多。
  • 例如,有可能发现,TASKLET 在不同 CPU 上的分布并不均匀。TASKLET 是最常用的软中断实现机制,每个 TASKLET 只运行一次就会结束 ,并且只在调用它的函数所在的 CPU 上运行。由于使用 TASKLET 特别简便,当然也会存在一些问题,比如说由于只在一个 CPU 上运行导致的调度不均衡,再比如因为不能在多个 CPU 上并行运行带来了性能限制。
[root@centos ~]# ps aux | grep soft
root          11  0.0  0.0      0     0 ?        S    Jun30   0:45 [ksoftirqd/0]
root          18  0.0  0.0      0     0 ?        S    Jun30   0:47 [ksoftirqd/1]

在 Linux 中,每个 CPU 都对应一个软中断内核线程,名字是 ksoftirqd/CPU 编号。当软中断事件的频率过高时,内核线程也会因为 CPU 使用率过高而导致软中断处理不及时,进而引发网络收发延迟、调度缓慢等性能问题。

软中断 CPU 使用率过高也是一种最常见的性能问题。

附录:/proc文件系统

/proc/cpuinfo 		- CPU 的信息 (型号, 构架, 缓存大小等)
/proc/mounts 		- 已加载的文件系统的列表
/proc/devices 		- 可用设备列表
/proc/filesystems	- 被支持的文件系统
/proc/modules 		- 已加载的模块列表
/proc/version 		- 内核版本字符串
/proc/cmdline 		- 系统启动时输入的内核命令行参数
/proc/config.gz 	- 内核配置压缩文件
/proc/zoneinfo 		- 内存域信息
/proc/buddyinfo 	- 伙伴系统信息
/proc/slabinfo 		- slab分配器信息
/proc/meminfo 		- 物理内存、交换空间等的信息
/proc/vmstat 		- 虚拟内存的统计信息
/proc/vmallocinfo 	- 虚拟地址分配映射信息
/proc/iomem 		- IO内存映射信息
/proc/ioports 		- IO端口映射信息
/proc/softirqs 		- 内核软中断信息
/proc/interrupts 	- 硬件中断信息
/proc/kallsyms 		- 内核符号信息,主要用于调试
/proc/kmsg 		- 内核日志接口文件
/proc/sysrq-trigger 	- SysRq触发文件
/proc/uptime		- 系统上电启动时间和空闲时间统计
/proc/partitions 	- 系统硬盘或flash分区信息
/proc/stat 		- 内核和系统的统计信息
/proc/mtd 		- MTD系统分区信息,主要在嵌入式系统中
/proc/loadavg 		- 系统负载统计数据
/proc/locks 		- 被内核锁定的文件
/proc/timer_list	- 内核各种计时器信息
/proc/misc 		- 被注册文misc杂项设备的信息

实例

记录一个软中断问题

前些天发现XEN虚拟机上的Nginx服务器存在一个问题:软中断过高,而且大部分都集中在同一个CPU,一旦系统繁忙,此CPU就会成为木桶的短板。

在问题服务器上运行「top」命令可以很明显看到「si」存在异样,大部分软中断都集中在 1 号CPU上,其它的CPU完全使不上劲儿:

# top
Cpu0: 11.3%us,  4.7%sy,  0.0%ni, 82.5%id,  ...  0.8%si,  0.8%st
Cpu1: 21.3%us,  7.4%sy,  0.0%ni, 51.5%id,  ... 17.8%si,  2.0%st
Cpu2: 16.6%us,  4.5%sy,  0.0%ni, 77.7%id,  ...  0.8%si,  0.4%st
Cpu3: 15.9%us,  3.6%sy,  0.0%ni, 79.3%id,  ...  0.8%si,  0.4%st
Cpu4: 17.7%us,  4.9%sy,  0.0%ni, 75.3%id,  ...  1.2%si,  0.8%st
Cpu5: 23.6%us,  6.6%sy,  0.0%ni, 68.1%id,  ...  0.9%si,  0.9%st
Cpu6: 18.1%us,  4.9%sy,  0.0%ni, 75.7%id,  ...  0.4%si,  0.8%st
Cpu7: 21.1%us,  5.8%sy,  0.0%ni, 71.4%id,  ...  1.2%si,  0.4%st

查询一下软中断相关数据,发现主要集中在 NET_RX 上,猜测是网卡问题:

watch -d -n 1 'cat /proc/softirqs'
                CPU0       CPU1       CPU2 ...       CPU7
      HI:          0          0          0 ...          0
   TIMER: 3692566284 3692960089 3692546970 ... 3693032995
  NET_TX:  130800410  652649368  154773818 ...  308945843
  NET_RX:  443627492 3802219918  792341500 ... 2546517156
   BLOCK:          0          0          0 ...          0
BLOCK_IOPOLL:      0          0          0 ...          0
 TASKLET:          0          0          0 ...          0
   SCHED: 1518716295  335629521 1520873304 ... 1444792018
 HRTIMER:        160       1351        131 ...        196
     RCU: 4201292019 3982761151 4184401659 ... 4039269755

查询宿主机,发现网卡其运行在单队列模式下:

grep -A 10 -i network /var/log/dmesg
Initalizing network drop monitor service
Intel(R) Gigabit Ethernet Network Driver - version 3.0.19
igb 0000:05:00.0: Intel(R) Gigabit Ethernet Network Connection
igb 0000:05:00.0: eth0: (PCIe:2.5GT/s:Width x4) 00:1b:21:bf:b3:2c
igb 0000:05:00.0: eth0: PBA No: G18758-002
igb 0000:05:00.0: Using MSI-X ... 1 rx queue(s), 1 tx queue(s)
igb 0000:05:00.1: Intel(R) Gigabit Ethernet Network Connection
igb 0000:05:00.1: eth1: (PCIe:2.5GT/s:Width x4) 00:1b:21:bf:b3:2d
igb 0000:05:00.1: eth1: PBA No: G18758-002
igb 0000:05:00.1: Using MSI-X ... 1 rx queue(s), 1 tx queue(s)

接着确认一下网卡的中断号,因为是单队列,所以只有一个中断号 45:

grep eth /proc/interrupts | awk '{print $1, $NF}'
45: eth0

知道了网卡的中断号,就可以查询其中断亲缘性配置「smp_affinity」:

cat /proc/irq/45/smp_affinity
02

这里的 02 实际上是十六进制,表示 1 号CPU,计算方法如下:

          Binary       Hex 
  CPU 0    0001         1 
  CPU 1    0010         2
  CPU 2    0100         4
+ CPU 3    1000         8
  -----------------------
  both     1111         f

说明:如果 4 个CPU都参与中断处理,那么设为 f;同理 8 个CPU的就设置成 ff:

echo ff > /proc/irq/45/smp_affinity

此外还有一个类似的配置「smp_affinity_list」:

cat /proc/irq/45/smp_affinity_list
1

两个配置是相通的,修改了一个,另一个会跟着变。不过「smp_affinity_list」使用的是十进制,相比较「smp_affinity」的十六进制,可读性更好些。

了解了这些基本知识,我们可以尝试换一个CPU试试看会发生什么:

echo 7 > /proc/irq/45/smp_affinity_list

再通过「top」命令观察,会发现处理软中断的CPU变成了 7 号CPU。
说明:如果希望多个CPU参与中断处理的话,可以使用类似下面的语法:

echo 3,5 > /proc/irq/45/smp_affinity_list
echo 0-7 > /proc/irq/45/smp_affinity_list

坏消息是对单队列网卡而言,「smp_affinity」和「smp_affinity_list」配置多CPU无效。

好消息是Linux支持RPS,通俗点来说就是在软件层面模拟实现硬件的多队列网卡功能。

首先看看如何配置RPS,如果CPU个数是 8 个的话,可以设置成 ff:

echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

接着配置内核参数rps_sock_flow_entries(官方文档推荐设置: 32768):

sysctl net.core.rps_sock_flow_entries=32768

最后配置rps_flow_cnt,单队列网卡的话设置成rps_sock_flow_entries即可:

echo 32768 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

说明:如果是多队列网卡,那么就按照队列数量设置成 rps_sock_flow_entries / N 。

做了如上的优化后,我们再运行「top」命令可以看到软中断已经分散到了两个CPU:

top
Cpu0: 24.8%us,  9.7%sy,  0.0%ni, 52.2%id,  ... 11.5%si,  1.8%st
Cpu1:  8.8%us,  5.1%sy,  0.0%ni, 76.5%id,  ...  7.4%si,  2.2%st
Cpu2: 17.6%us,  5.1%sy,  0.0%ni, 75.7%id,  ...  0.7%si,  0.7%st
Cpu3: 11.9%us,  7.0%sy,  0.0%ni, 80.4%id,  ...  0.7%si,  0.0%st
Cpu4: 15.4%us,  6.6%sy,  0.0%ni, 75.7%id,  ...  1.5%si,  0.7%st
Cpu5: 20.6%us,  6.9%sy,  0.0%ni, 70.2%id,  ...  1.5%si,  0.8%st
Cpu6: 12.9%us,  5.7%sy,  0.0%ni, 80.0%id,  ...  0.7%si,  0.7%st
Cpu7: 15.9%us,  5.1%sy,  0.0%ni, 77.5%id,  ...  0.7%si,  0.7%st

疑问:理论上讲,我已经设置了RPS为ff,应该所有 8 个CPU一起分担软中断才对,可实际结果只有两个