从“取外卖”看中断

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

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

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

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

这里的"打电话",其实就是一个中断。没接到电话的时候,你可以做其他的事情;只有接到了电话(也就是发生中断),你才要进行另一个动作:取外卖这个例子你就可以发现,中断其实就是一种异步的事件处理机制,可以提高系统的并发处理能力

由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序需要尽可能快的运行。如由于中断本身要做的事情不多,那么处理起来也不会有太大的问题;但如果 中断要处理的事情很多,中断服务程序就有可能要运行很长时间

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

假如你定了2分外卖,一份主食和一份饮料,并且是由2个不同的配送员来配送。两份外卖都约定了电话取外卖的方式,但是问题又来了,当第一份外卖送到的,配送员给你打了个长长的电话,商量发票的处理方式,与此同时,第二个配送员也到了,也想给你打电话,但是很明显,因为电话占线(也就是关闭了中断响应),第二个配送员的电话是打不通的。所以,第二个配送员很可能试了几次后就走掉了(也就是丢失了一次中断)。

软中断

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

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

这两个阶段你可以这样理解:

上半部分直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行

而下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行

实际上,上半部会打断CPU正在执行的任务,然后立即执行中断处理程序,而下半部以内核线程的方式执行,并且每个CPU都对应一个软中断

内核线程,名字为"ksoftirqd/CPU 编号" 比如0号CPU对应的软中断内核线程的名字就是ksoftirqd/0

注意事项:

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

查看软中断和内核线程

不知道你还记不记得,前面提到过的proc文件系统。她是一种内核空间和用户空间进行通信的机制,可以用来查看内核的数据结构,或者用来动态修改内核的配置。其中:

/proc/softirqs提供了软中断的运行情况
/proc/interrupts提供了硬中断的运行情况
# cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
          HI:          1          0          0          0          0          0          0          0
       TIMER:   91465557   75078967   99549386   74936020   92918225   59243402   92919973   58735897
      NET_TX:          7          9          8         10          8         11          5          8
      NET_RX:   10462869   10402563   10232492   50570620   10442656   49829946   10726373   89459403
       BLOCK:       4831          0   11032160          0          0          0          0          0
BLOCK_IOPOLL:          0          0          0          0          0          0          0          0
     TASKLET:          5          3         26          4          2          5          2          4
       SCHED:   40910608   31925972   51753270   18248274   39602169   16387318   39118080   16326508
     HRTIMER:          0          0          0          0          0          0          0          0
         RCU:   56137399   38856963   59327894   27917659   56945439   29177149   56783540   29259905

TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU锁)

1、TASKLET在不同CPU上的分布并不均匀,TASKLET是最常用的软中断实现机制,每TASKLET个只运行一次就会结束,并且只在调用它的函数所在的CPU上运行。因为使用TASKLET特别简便,当然也会存在一些问题,比如说由于只在一个cpu上运行导致调度不均衡,再比如因为不能在多个CPU上并行运行带来了性能限制。

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

# ps aux |grep softirq
root         6  0.0  0.0      0     0 ?        S     2023   0:10 [ksoftirqd/0]
root        14  0.0  0.0      0     0 ?        S     2023   0:10 [ksoftirqd/1]
root        19  0.0  0.0      0     0 ?        S     2023   0:11 [ksoftirqd/2]
root        24  0.0  0.0      0     0 ?        S     2023   0:11 [ksoftirqd/3]
root        29  0.0  0.0      0     0 ?        S     2023   0:10 [ksoftirqd/4]
root        34  0.0  0.0      0     0 ?        S     2023   0:13 [ksoftirqd/5]
root        39  0.0  0.0      0     0 ?        S     2023   0:10 [ksoftirqd/6]
root        44  0.0  0.0      0     0 ?        S     2023   0:15 [ksoftirqd/7]