本文是一篇译文,原文的地址为:http://www.alexonlinux.com/smp-affinity-and-proper-interrupt-handling-in-linux
Introduction
硬件中断是非常昂贵的,因为它会消耗大量的CPU处理能力。硬件和软件工程师已经做了很多工作试图改变这种状况,但硬件中断仍然会消耗大量CPU。
硬件中断对桌面系统的影响并不明显,具体信息可以通过/proc/interrupts文件来查看。这个文件中记录了所有的硬件设备以及它们在每个CPU上收到的中断数目。如果是在一个常规的桌面系统上,将会看到系统中处理的中断数目是相对较小的。即使是每秒处理百万数据包的超强服务器,一秒内也只需处理数万个中断。即便如此,通过恰当的方式降低中断所带来的CPU开销仍将毫无疑问的帮助提升系统性能。
But really, what can we do about interrupts?
事实上,还有很多事情可以做。许多Linux的发行版中都包含了能够明显改善这种情况的修改。一种有效的技术就是NAPI,它显著的降低了中断的数目和开销,因此现代服务器无需再忍受1Gbps的以太网链路。NAPI现在已经成为了内核的一部分。其他的技术还有像中断合并等。
在本文中,我将把注意力放在一个最有力的中断处理优化技术上。
SMP affinity
术语“SMP亲和性” 或 “处理器亲和性”有着广泛的含义,需要首先来解释一下。“亲和性”是指在多处理器系统中,将特定的任务放到特定的处理器上执行。例如,当进程Y运行在处理器X上时,它们之间就具有了亲缘关系,处理器会将进程的部分内存内容缓存在cache中,因此频繁的将进程调度到不同的处理器上是一种低效的方法。
考虑到中断的情况,“SMP 亲和性”是指特定的中断由哪个处理器来处理。与进程的情况不同,将中断绑定到特定的CPU上将会引起性能下降。下面就来说说为什么会这样。中断处理程序通常都非常小,因此它所占用的内存也很小,也就是说将中断保持在同一个CPU上不会改善缓存命中的情况。相反,过多的中断会使一个核心过载,而其他的核心却处在相对空闲的状态。调度程序却对此一无所知,它会假设我们的中断处理核心与其他核心一样繁忙,由此带来的结果就是你可能会面临一个瓶颈,这是由于某些进程或线程运行在了只有90%可用处理能力的核心上所造成的。
事情还可能会变得更加糟糕,因为在默认情况下core0会被用来处理所有中断。在一个繁忙的系统上,所有的中断加起来可能会消耗掉core0 30%的处理能力。假设所有核心都具有相同的处理能力,我们会发现我们的软件系统只能有效地利用CPU全部处理能力的70%。
Who's responsible
APIC or Advanced Programmable Interrupt Controller 已经集成到所有现代x86体系架构的系统中 --- 包括 SP(single-processor)和MP。APIC负责递交中断,并决定中断的去向(到哪个核心)。
默认情况下,APIC将所有中断递交到core0,这就是为什么在主流现代Linux操作系统上/proc/interrupts文件的输出内容看起来如下的原因:
CPU0 CPU1 CPU2 CPU3 0: 123357 0 0 0 IO-APIC-edge timer 8: 0 0 0 0 IO-APIC-edge rtc 11: 0 0 0 0 IO-APIC-level acpi 169: 0 0 0 0 IO-APIC-level uhci_hcd:usb1 177: 0 0 0 0 IO-APIC-level qla2xxx 185: 0 0 0 0 IO-APIC-level qla2xxx 193: 12252 0 0 0 IO-APIC-level ioc0 209: 0 0 0 0 IO-APIC-level uhci_hcd:usb2 217: 468 0 0 0 IO-APIC-level eth0 225: 285 0 0 0 IO-APIC-level eth1 NMI: 120 66 76 45 LOC: 123239 123220 123187 123065 ERR: 0 MIS: 0
看到了吗,CPU0处理了所有的硬件中断,这就是你将在一个错误配置了中断SMP亲和性的系统上看到的情形。
Simple solution for the problem
随着APIC的引入,这个问题已经有了多种解决方案。APIC包含了若干中断传递和指定模式,包括物理模式和逻辑模式,固定模式和低优先级模式,等等。一个重要的事实是它有能力将中断递送到任何一个核上,甚至在它们之间进行负载均衡。
APIC也有配置限制,它只能配置在前8个核心上。如果你有多于8个的核心,不要指望序号大于7的核心能够收到中断。
默认情况下,它的操作是physical/fixed的。也就是说,它会将特定的中断传递到特定的核心上(默认是core0),但你也可以方便的更改接收指定中断的核心号。
对于/proc/interrupts文件第一列中的每个IRQ号,在/proc/irq/目录下都有一个对应的子目录,其中包含了一个名为smp_affinity的文件,它可以用来修改处理这个中断的核心号。读这个文件可以获得一个由十六进制表示的掩码,其中的每一位代表一个核心。当特定位置位时,APIC会将这个中断传递到对应的核心。
让我们来看一个例子:
# # cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 19599546 0 0 0 IO-APIC-edge timer 8: 0 0 0 0 IO-APIC-edge rtc 11: 0 0 0 0 IO-APIC-level acpi 169: 0 0 0 0 IO-APIC-level uhci_hcd:usb1 177: 0 0 0 0 IO-APIC-level qla2xxx 185: 0 0 0 0 IO-APIC-level qla2xxx 193: 95337 0 0 0 IO-APIC-level ioc0 209: 0 0 0 0 IO-APIC-level uhci_hcd:usb2 217: 100778 0 0 0 IO-APIC-level eth0 225: 56651 0 0 0 IO-APIC-level eth1 NMI: 466 393 422 372 LOC: 19600453 19600434 19600401 19600279 ERR: 0 MIS: 0 # # # echo "2" > /proc/irq/217/smp_affinity # cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 19606722 0 0 0 IO-APIC-edge timer 8: 0 0 0 0 IO-APIC-edge rtc 11: 0 0 0 0 IO-APIC-level acpi 169: 0 0 0 0 IO-APIC-level uhci_hcd:usb1 177: 0 0 0 0 IO-APIC-level qla2xxx 185: 0 0 0 0 IO-APIC-level qla2xxx 193: 95349 0 0 0 IO-APIC-level ioc0 209: 0 0 0 0 IO-APIC-level uhci_hcd:usb2 217: 101027 49 0 0 IO-APIC-level eth0 225: 56655 0 0 0 IO-APIC-level eth1 NMI: 466 393 422 372 LOC: 19607629 19607610 19607577 19607455 ERR: 0 MIS: 0 #
可以看到,当我们输入了这个神奇的命令后,CPU1将代替CPU0,开始从eth0接收中断。用于完成这件事情的echo命令非常有趣,如果我们将“4”写入这个文件中,那么CPU2将代替CPU1来处理eth0的中断,就像之前已经提到的,它采用了位掩码的方式,每一位与一个单独的CPU关联。
那么写入“3”又会怎么样呢?理论上,这时APIC应该将中断传递到CPU0和CPU1上,不幸的是,这里的情况会更复杂一些。具体的行为依赖于APIC是否工作在 physical "destination mode" 以及 low priority "delivery mode"模式下。如果是的话,那么你不太可能看到CPU0处理所有中断,这是因为当APIC工作在 physical / low priority模式下的时候,会自动在前8个核心之间进行负载均衡。
因此,如果在你的系统上CPU0默认处理了全部的中断,这可能暗示了APIC的配置问题。
Ultimate solution
首先,不幸的是,别无选择,除非替换内核。配置APIC是内核的工作,如果我们想改变现状就只能在内核里修改它,那么唯一的问题就是,用什么来替换内核?
我在OpenSuSE 10.2(内核版本2.6.18)上进行了测试,使用OpenSuSE的默认内核配置(/proc/config.gz)安装2.6.24.3版本的内核(当时最新的版本)来修正这个问题。在这个内核上,事情看起来正确了:
# cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 728895 728796 728624 728895 IO-APIC-edge timer 8: 0 0 0 0 IO-APIC-edge rtc 11: 0 0 0 0 IO-APIC-fasteoi acpi 16: 0 0 0 0 IO-APIC-fasteoi uhci_hcd:usb1 19: 0 0 0 0 IO-APIC-fasteoi uhci_hcd:usb2 24: 14090 14090 14327 14056 IO-APIC-fasteoi ioc0 49: 7 9 7 8 IO-APIC-fasteoi qla2xxx 50: 8 12 11 10 IO-APIC-fasteoi qla2xxx 77: 2849 2759 2841 2827 IO-APIC-fasteoi eth0 78: 25072 25138 24996 24980 IO-APIC-fasteoi eth1 NMI: 0 0 0 0 LOC: 2915270 2915256 2915228 2915092 ERR: 0
看起来不错不是吗?所有的核心都在处理中断,工作效率也因此得到了最大化。
还有一个内核的配置选项横在我们的道路上,一旦移除掉它就能够在所有晚于2.6.10版本的内核上获得类似的效果。这个选项就是CONFIG_HOTPLUG_CPU,它增加了对CPU热插拔的支持。关掉这个选项,并正确的配置APIC。
实际上这很好理解,APIC被告知使用哪个CPU来接收中断,你需要增加一段代码来告知APIC如何处理CPU的移除事件 --- 而CPU的移除事件正是CONFIG_HOTPLUG_CPU允许你做的事,我假定这个功能在较早时被内核遗忘了,并在2.6.24.3版本之后被添加了进来。
Conclusion
我们已经看到,通过修改内核的配置可以获得实实在在的改进。在一个繁忙的系统中,通过一个小小的改动就可以成倍的提升服务器的生产力。
希望你能够认同这篇文章的内容,并将这些技术用到实际中去。