Interrupts(中断)

概述

参考:

Interrupte Request(终端请求,简称 IRQ),是一种信号,该信号来源于外围硬件(相对于 CPU 和内存)的异步信号或者来自软件的同步信号,收到该信号后进行相应的硬件、软件处理。中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。

Linux 内核对计算机上所有的设备进行管理,进行管理的方式是内核和设备之间的通信。解决通信的方式有两种:

  1. 轮询。轮询是指内核对设备状态进行周期性的查询
  2. 中断。中断是指在设备需要 CPU 的时候主动发起通信

从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如 8259A)的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断线。

中断可以分为 NMI(不可屏蔽中断) 和 INTR(可屏蔽中断)。其中 NMI 通常用于电源掉电和物理存储器奇偶校验;INTR 是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。

常见的两种中断控制器:

  1. Programmable Interrupt Controller(可编程中断控制器,简称 PIC) 8259A
  2. Advanced Programmable Interrupt Controller(高级可编程中断控制器,简称 APIC)

传统的 PIC 是由两片 8259A 风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 IRQ。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个

硬中断与软中断

中断处理分为两部分,上半部与下半部。

  1. 硬中断,也就是中断处理的上半部
    1. 外围硬件发给 CPU 或者内存的异步信号就称之为硬中断
    2. 由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。
  2. 软中断:也就是中断处理的下半部
    1. 由软件系统本身发给操作系统内核的中断信号,称之为软中断。通常是由硬中断处理程序或进程调度程序对操作系统内核的中断,也就是我们常说的系统调用(System Call)
    2. 为了满足实时系统的要求,中断处理应该是越快越好。linux 为了实现这个特点,当中断发生的时候,硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。

也就是说,如果在一个完整的中断流程中,硬中断首先产生,然后硬中断的处理程序将会发出中断信号后,再有软中断进行处理。

硬中断(中断的上半部)

网络设备的中断

在内核中,网络设备驱动是通过中断的方式来接受和处理数据包。当网卡设备上有数据到达的时候,会触发一个硬件中断来通知 CPU 来处理数据,此类处理中断的程序一般称作 Interrupt Service Routines(中断服务程序,简称 ISR)。ISR 程序不宜处理过多逻辑,否则会让设备的中断处理无法及时响应。因此 Linux 中将中断处理函数分为上半部和下半部。上半部是只进行最简单的工作,快速处理然后释放 CPU。剩下将绝大部分的工作都放到下半部中,下半部中逻辑由内核线程选择合适时机进行处理。

软中断(中断的下半部)

Linux 2.4 以后内核版本采用的下半部实现方式是软中断,由 ksoftirqd 内核线程全权处理, 正常情况下每个 CPU 核上都有自己的软中断处理数队列和 ksoftirqd 内核线程。软中断实现只是通过给内存中设置一个对应的二进制值来标识,软中断处理的时机主要为以下 2 种:

  • 硬件中断 irq_exit退出时;
  • 被唤醒 ksoftirqd 内核线程进行处理软中断;

常见的软中断类型如下,代码:include/linux/interrupt.h

enum {
    HI_SOFTIRQ=0,          // tasklet
    TIMER_SOFTIRQ,         // timer
    NET_TX_SOFTIRQ,        // 网络数据包发送软中断
    NET_RX_SOFTIRQ,        // 网络数据包接受软中断
    BLOCK_SOFTIRQ,         // IO
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,       // tasklet
    SCHED_SOFTIRQ,         // schedule
    HRTIMER_SOFTIRQ,       // timer
    RCU_SOFTIRQ,           // lock
    NR_SOFTIRQS
};

/proc/softirqs 文件中显示的内容,即是这一段代码的实例化。

优先级自上而下,HI_SOFTIRQ 的优先级最高。其中 NET_TX_SOFTIRQ 对应于网络数据包的发送, NET_RX_SOFTIRQ 对应于网络数据包接受,两者共同完成网络数据包的发送和接收。网络相关的中断程序在网络子系统初始化的时候进行注册, NET_RX_SOFTIRQ 的对应函数为 net_rx_action() ,在 net_rx_action() 函数中会调用网卡设备设置的 poll 函数,批量收取网络数据包并调用上层注册的协议函数进行处理,如果是为 ip 协议,则会调用 ip_rcv,上层协议为 icmp 的话,继续调用 icmp_rcv 函数进行后续的处理。

硬中断与软中断之区别与联系?

  • 硬中断是由外设硬件发出的,需要有中断控制器之参与。其过程是外设侦测到变化,告知中断控制器,中断控制器通过 CPU 或内存的中断脚通知 CPU,然后硬件进行程序计数器及堆栈寄存器之现场保存工作(引发上下文切换),并根据中断向量调用硬中断处理程序进行中断处理
  • 软中断则通常是由硬中断处理程序或者进程调度程序等软件程序发出的中断信号,无需中断控制器之参与,直接以一个 CPU 指令之形式指示 CPU 进行程序计数器及堆栈寄存器之现场保存工作(亦会引发上下文切换),并调用相应的软中断处理程序进行中断处理(即我们通常所言之系统调用)
  • 硬中断直接以硬件的方式引发,处理速度快。软中断以软件指令之方式适合于对响应速度要求不是特别严格的场景
  • 硬中断通过设置 CPU 的屏蔽位可进行屏蔽,软中断则由于是指令之方式给出,不能屏蔽
  • 硬中断发生后,通常会在硬中断处理程序中调用一个软中断来进行后续工作的处理
  • 硬中断和软中断均会引起上下文切换(进程/线程之切换),进程切换的过程是差不多的

查看中断情况

查看中断分布情况即 CPU 都在哪些设备上干活,干了多少(也可以使用 itop 工具实时查看)?

注意:下面查看的信息只列出了前 4 个 CPU,实际该设备有 128 核

cat /proc/interrupts 命令查看硬中断信息

# 从左至右依次显示IRQ编号,每个cpu对该IRQ的处理次数(每个CPU占一列),中断控制器的名字,IRQ的名字以及驱动程序注册该IRQ时使用的名字
~]# cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  0:        620          0          0          0  IR-IO-APIC-edge      timer
  8:          1          0          0          0  IR-IO-APIC-edge      rtc0
  9:      20774          0          0          0  IR-IO-APIC-fasteoi   acpi
 16:         28          0          0          0  IR-IO-APIC-fasteoi   ehci_hcd:usb1
 23:        243          0          0          0  IR-IO-APIC-fasteoi   ehci_hcd:usb2
 88:          0          0          0          0  DMAR_MSI-edge      dmar0
 89:          0          0          0          0  DMAR_MSI-edge      dmar1
 90:          0          0          0          0  IR-PCI-MSI-edge      PCIe PME
......
101:     169988          0          0          0  IR-PCI-MSI-edge      i40e-enp25s0f0-TxRx-0
134:    1900138          0          0          0  IR-PCI-MSI-edge      eth2-q0
150:    4262209          0          0          0  IR-PCI-MSI-edge      eth3-q0
166:          4          0          0          0  IR-PCI-MSI-edge      ioat-msix
......
NMI:        710        280        658        235   Non-maskable interrupts
LOC:    4230314    2640664    2427443    1338890   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:        710        280        658        235   Performance monitoring interrupts
IWI:          0          0          0          0   IRQ work interrupts
RES:     679921    1369165    1013002     573776   Rescheduling interrupts
CAL:      46507      67439      67569      67567   Function call interrupts
TLB:       6547       3416       1798       1015   TLB shootdowns
TRM:          0          0          0          0   Thermal event interrupts
THR:          0          0          0          0   Threshold APIC interrupts
MCE:          0          0          0          0   Machine check exceptions
MCP:        569        569        569        569   Machine check polls
ERR:          0
MIS:          0

这些信息在不同环境下,内容不同。比如对于网卡来说,物理机上一般是以 网卡名表示,比如上面的 eth2-q0 等等。而对于 kvm 虚拟机,一般是 virtio0-input.0 、 virtio0-output.0、virtio0-input.1、virtio0-output.1 等等,virtio0 这网卡有几个队列,就有几个对应的 input 和 output,input 和 output 分别表示该网卡队列的输出和输出的中断情况。

在 CPU 数量过多时,输出的信息非常杂乱,通常有这么几种方式可以简化输出

cat /proc/interrupts | tr -s " " # 让数据更紧凑,逐行查看

for i in $(egrep "-input." /proc/interrupts |awk -F ":" '{print $1}');do cat /proc/irq/$i/smp_affinity_list;done # 从 KVM 虚拟机中找到处理 IRQ 的 CPU。

如果是物理机的话,把 grep 筛选的 -input. 这段内容改为物理机里网络设备名称即可

cat /proc/softirqs 命令查看软中断请求信息

~]# cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3
          HI:          0          0          0          1
       TIMER:   64617617   68513491   69044942   72115635
      NET_TX:      69847        732        763        789
      NET_RX:   27520994   45465624   45524602   56388657
       BLOCK:    1107314    1290437    1396335    1263193
BLOCK_IOPOLL:          0          0          0          0
     TASKLET:     444161     163638     518738     264470
       SCHED:   34176300   27939686   24531928   24977357
     HRTIMER:          0          0          0          0
         RCU:  152761131  170249352  156225062  158460201

在 CPU 数量过多时,输出的信息非常杂乱,通常有这么几种方式可以简化输出

awk 脚本:

BEGIN{
  cpucount = 6
}
NR == 1{
  num = 5;
  printf("%30s",$1);
  for (i=2;i<=cpucount;i++) {
    printf("%15s\t",$i);
  }
  printf(RS);
}
NR > 1{
  for (i=1;i<=cpucount+1;i++) {
    printf("%15s\t",$i);
  }
  printf(RS);
}

awk 命令持续观察

#!/bin/bash
#
run() {
  # 这个是输出指定列。即看个别 CPU 的软中断
 # awk 'NR>1{printf("%15s\t%10s\t%10s\t%10s\t%10s\n",$1,$3,$4,$5,$6)}' </proc/softirqs | grep NET_RX
  # 通过循环输出每行所有列。即看所有 CPU 的软中断
  awk 'NR>1{for(i=1;i<10;i++){printf("%10s",$i)}printf("\n")}' </proc/softirqs
}

printf -v run_str '%q' "$(declare -f run); run"

watch -c -d "bash -c $run_str"

其他

总的中断次数可以通过 vmstat 或者 dstat 查看,其中 vmstat 中的 in 表示每秒的中断次数;

通过 mpstat -P ALL 2,每隔两秒查看下所有核状态信息,其中%irq 为硬中断,%soft 为软中断

root@geekwolf:~# mpstat -P ALL 2
08:42:04 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
08:42:05 AM  all    4.31    0.00    0.70    0.00    0.00    0.06    0.00    0.00   94.93
08:42:05 AM    0    5.26    0.00    1.05    0.00    0.00    60.05    0.00    0.00   92.63
08:42:05 AM    1    7.07    0.00    1.01    0.00    0.00    0.00    0.00    0.00   91.92
08:42:05 AM    2    8.91    0.00    0.99    0.00    0.00    0.00    0.00    0.00   90.10
08:42:05 AM    3    8.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00   91.00
......

通过 “取外卖” 事件来类比中断

说到中断,我在前面关于“上下文切换”的文章,简单说过中断的含义,先来回顾一下。

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

你可能要问了,为什么要有中断呢?我可以举个生活中的例子,让你感受一下中断的魅力。

比如说你订了一份外卖,但是不确定外卖什么时候送到,也没有别的方法了解外卖的进度,但是,配送员送外卖是不等人的,到了你这儿没人取的话,就直接走人了。所以你只能苦苦等着,时不时去门口看看外卖送到没,而不能干其他事情。

不过呢,如果在订外卖的时候,你就跟配送员约定好,让他送到后给你打个电话,那你就不用苦苦等待了,就可以去忙别的事情,直到电话一响,接电话、取外卖就可以了。

这里的“打电话”,其实就是一个中断。没接到电话的时候,你可以做其他的事情;只有接到了电话(也就是发生中断),你才要进行另一个动作:取外卖。

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

由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。特别是,中断处理程序在响应中断时,还会临时关闭中断。这就会导致上一次中断处理完成之前,其他中断都不能响应,也就是说中断有可能会丢失。那么还是以取外卖为例。假如你订了 2 份外卖,一份主食和一份饮料,并且是由 2 个不同的配送员来配送。这次你不用时时等待着,两份外卖都约定了电话取外卖的方式。但是,问题又来了。

当第一份外卖送到时,配送员给你打了个长长的电话,商量发票的处理方式。与此同时,第二个配送员也到了,也想给你打电话。

但是很明显,因为电话占线(也就是关闭了中断响应),第二个配送员的电话是打不通的。所以,第二个配送员很可能试几次后就走掉了(也就是丢失了一次中断)。

软中断

如果你弄清楚了“取外卖”的模式,那对系统的中断机制就很容易理解了。事实上,为了解决中断处理程序执行过长和中断丢失的问题,Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:

  1. 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。
  2. 上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行
  3. 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。
  4. 下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。

比如说前面取外卖的例子:

  • 上半部就是你接听电话,告诉配送员你已经知道了,其他事儿见面再说,然后电话就可以挂断了
  • 下半部才是取外卖的动作,以及见面后商量发票处理的动作。

这样,第一个配送员不会占用你太多时间,当第二个配送员过来时,照样能正常打通你的电话。

除了取外卖,我再举个最常见的网卡接收数据包的例子,让你更好地理解。

网卡接收到数据包后,会通过硬件中断的方式,通知内核有新的数据到了。这时,内核就应该调用中断处理程序来响应它。你可以自己先想一下,这种情况下的上半部和下半部分别负责什么工作呢?

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

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

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

那要怎么知道你的系统里有哪些软中断呢?

查看软中断和内核线程

运行下面的命令,查看 /proc/softirqs 文件的内容,你就可以看到各种类型软中断在不同 CPU 上的累积运行次数:

/proc/softirqs 提供了软中断的运行情况;

/proc/interrupts 提供了硬中断的运行情况

[root@master-1 ~]# cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3
          HI:          0          0          0          1
       TIMER:   64617617   68513491   69044942   72115635
      NET_TX:      69847        732        763        789
      NET_RX:   27520994   45465624   45524602   56388657
       BLOCK:    1107314    1290437    1396335    1263193
BLOCK_IOPOLL:          0          0          0          0
     TASKLET:     444161     163638     518738     264470
       SCHED:   34176300   27939686   24531928   24977357
     HRTIMER:          0          0          0          0
         RCU:  152761131  170249352  156225062  158460201

在查看 /proc/softirqs 文件内容时,你要特别注意以下这两点。

  • 第一,要注意软中断的类型,也就是这个界面中第一列的内容。从第一列你可以看到,软中断包括了 10 个类别,分别对应不同的工作类型。比如 NET_RX 表示网络接收中断,而 NET_TX 表示网络发送中断。
  • 第二,要注意同一种软中断在不同 CPU 上的分布情况,也就是同一行的内容。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多。比如这个界面中,NET_RX 在 CPU0 和 CPU1 上的中断次数基本是同一个数量级,相差不大。

不过你可能发现,TASKLET 在不同 CPU 上的分布并不均匀。TASKLET 是最常用的软中断实现机制,每个 TASKLET 只运行一次就会结束 ,并且只在调用它的函数所在的 CPU 上运行。

因此,使用 TASKLET 特别简便,当然也会存在一些问题,比如说由于只在一个 CPU 上运行导致的调度不均衡,再比如因为不能在多个 CPU 上并行运行带来了性能限制。

另外,刚刚提到过,软中断实际上是以内核线程的方式运行的,每个 CPU 都对应一个软中断内核线程,这个软中断内核线程就叫做 ksoftirqd/CPU 编号。那要怎么查看这些线程的运行状况呢?

其实用 ps 命令就可以做到,比如执行下面的指令:

[root@master-1 ~]#  ps aux | grep softirq
root         6  0.1  0.0      0     0 ?        S    Sep25   8:32 [ksoftirqd/0]
root        14  0.2  0.0      0     0 ?        S    Sep25  11:55 [ksoftirqd/1]
root        19  0.1  0.0      0     0 ?        S    Sep25  10:48 [ksoftirqd/2]
root        24  0.2  0.0      0     0 ?        S    Sep25  13:36 [ksoftirqd/3]

注意,这些线程的名字外面都有中括号,这说明 ps 无法获取它们的命令行参数(cmline)。一般来说,ps 的输出中,名字括在中括号里的,一般都是内核线程。

小结

Linux 中的中断处理程序分为上半部和下半部:

  1. 上半部对应硬件中断,用来快速处理中断。
  2. 下半部对应软中断,用来异步处理上半部未完成的工作。

Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的运行情况。


最后修改 March 25, 2025: clearup (feb59d93)