多队列网卡介绍
多队列网卡顾名思义就是由原来的单网卡单队列变成了现在的单网卡多队列。多队列网卡是一种技术,最初是用来解决网络IO QoS (quality of service)问题的,后来随着网络IO的带宽的不断提升,单核CPU不能完全处满足网卡的需求,体现最为明显的就是单核CPU处理不了网卡大量的数据包请求(软中断)而造成大量丢包,其实当网卡收到数据包时会产生中断,通知内核有新数据包,然后内核调用中断处理程序进行响应,把数据包从网卡缓存拷贝到内存,因为网卡缓存大小有限,如果不及时拷出数据,后续数据包将会因为缓存溢出被丢弃,因此这一工作需要立即完成。剩下的处理和操作数据包的工作就会交给软中断,高负载的网卡是软中断产生的大户,很容易形成瓶颈。但通过多队列网卡驱动的支持,将各个队列通过中断绑定到不同的CPU核上,以满足网卡的需求,这就是多队列网卡的应用。
常见的有Intel的82575、82576,Boardcom的57711等,下面以公司的服务器使用较多的Intel 82575网卡为例,分析一下多队列网卡的硬件的实现以及Linux内核软件的支持。
一、多队列网卡硬件实现
图1.1是Intel 82575硬件逻辑图,有四个硬件队列。当收到报文时,通过hash包头的SIP、Sport、DIP、Dport四元组,将一条流总是收到相同的队列。同时触发与该队列绑定的中断。
Linux kernel 2.6.21前网卡驱动的实现
Linux kernel从2.6.21之前不支持多队列特性,一个网卡只能申请一个中断号,因此同一个时刻只有一个核在处理网卡收到的包。如图2.1,协议栈通过NAPI轮询收取各个硬件queue中的报文到图2.2的net_device数据结构中,通过QDisc队列将报文发送到网卡。
Linux kernel 2.6.21后网卡驱动的实现
Linux kernel 2.6.21开始支持多队列特性,当网卡驱动加载时,通过获取的网卡型号,得到网卡的硬件queue的数量,并结合CPU核的数量,最终通过Sum=Min(网卡queue,CPU core)得出所要激活的网卡queue数量(Sum),并申请Sum个中断号,分配给激活的各个queue。
如图3.1,当某个queue收到报文时,触发相应的中断,收到中断的核,将该任务加入到协议栈负责收包的该核的NET_RX_SOFTIRQ队列中(NET_RX_SOFTIRQ在每个核上都有一个实例),在NET_RX_SOFTIRQ中,调用NAPI的收包接口,将报文收到CPU中如图3.2的有多个netdev_queue的net_device数据结构中。这样,CPU的各个核可以并发的收包,就不会因为一个核不能满足需求,导致网络IO性能下降。
但当CPU可以平行收包时,就会出现不同的核收取了同一个queue的报文,这就会产生报文乱序的问题,解决方法是将一个queue的中断绑定到唯一的一个核上去,从而避免了乱序的问题。
二、查看网卡是否支持多队列
查看网卡是否支持多队列,使用lspci -vvv命令,找到Ethernet controller项:
如果有MSI-X, Enable+ 并且Count > 1,则该网卡是多队列网卡。
Message Signaled Interrupts(MSI)是PCI规范的一个实现,可以突破CPU 256条interrupt的限制,使每个设备具有多个中断线变成可能,多队列网卡驱动给每个queue申请了MSI。MSI-X是MSI数组,实际应用场景中,MSI方式的中断对多核cpu的利用情况不佳,网卡中断全部落在某一个cpu上,即使设置cpu affinity也没有作用,而MSI-X中断方式可以自动在多个cpu上分担中断。
中断是什么?
X86体系结构的计算机采用中断机制来协同处理器与其他设备的工作。当一个设备需要与处理器通信时,就会向处理器发出一个中断信号。例如敲击键盘时,键盘就会产生一个中断,通知操作系统有键被按下。在机器启动的时候,系统就已经识别了所有设备,并且也把相应的中断处理器加载到中断表中。所有的Linux操作系统都是基于中断驱动的,当我们在键盘上按下一个按键时,键盘就会对CPU说,一个键已经被按下。在这种情况下,键盘的IRQ线路中的电压就会发生一次变化,而这种电压的变化就是来自设备的请求,就相当于说这个设备有一个请求需要处理。
中断其实就是由硬件或软件所发送的一种称为IRQ(中断请求)的信号。中断允许让设备,如键盘,串口卡,并口等设备表明它们需要CPU。一旦CPU接收了中断请求,CPU就会暂时停止执行正在运行的程序,并且调用一个称为中断处理器或中断服务程序(interrupt service routine)的特定程序。中断服务程序或中断处理器可以在中断向量表中找到,而这个中断向量表位于内存中的固定地址中。中断被CPU处理后,就会恢复执行之前被中断的程序,整个流程如下图所示。
由于中断会频繁发生,因此要求中断处理程序执行要快速。为了实现快速执行,必须要将一些繁重且不非常紧急的任务从中断处理程序中剥离出来,这一部分Linux中称为下半部,有三种方法处理下半部——软中断、tasklet和工作队列。
以网卡为例,当网卡收到数据包时会产生中断,通知内核有新数据包,然后内核调用中断处理程序进行响应,把数据包从网卡缓存拷贝到内存,因为网卡缓存大小有限,如果不及时拷出数据,后续数据包将会因为缓存溢出被丢弃,因此这一工作需要立即完成。剩下的处理和操作数据包的工作就会交给软中断。高负载的网卡是软中断产生的大户,很容易形成瓶颈。
硬中断和软中断的区别
硬中断:
1)硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。
2)处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,可以同时处理多个中断。)。
3)硬中断可以直接中断CPU。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。
4)对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存在是为了让调度代码(或称为调度器)可以调度多任务。
软中断:
1)软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。
2)通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。
3)软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。
4)软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。有一个特殊的软中断是Yield调用,它的作用是请求内核调度器去查看是否有一些其他的进程可以运行。
问题解答:
1. 问:对于软中断,I/O操作是否是由内核中的I/O设备驱动程序完成?
答:对于I/O请求,内核会将这项工作分派给合适的内核驱动程序,这个程序会对I/O进行队列化,以可以稍后处理(通常是磁盘I/O),或如果可能可以立即执行它。通常,当对硬中断进行回应的时候,这个队列会被驱动所处理。当一个I/O请求完成的时候,下一个在队列中的I/O请求就会发送到这个设备上。
2. 问:软中断所经过的操作流程是比硬中断的少吗?换句话说,对于软中断就是:进程 ->内核中的设备驱动程序;对于硬中断:硬件->CPU->内核中的设备驱动程序?
答:是的,软中断比硬中断少了一个硬件发送信号的步骤。产生软中断的进程一定是当前正在运行的进程,因此它们不会中断CPU。但是它们会中断调用代码的流程。
如果硬件需要CPU去做一些事情,那么这个硬件会使CPU中断当前正在运行的代码。而后CPU会将当前正在运行进程的当前状态放到堆栈(stack)中,以至于之后可以返回继续运行。这种中断可以停止一个正在运行的进程;可以停止正处理另一个中断的内核代码;或者可以停止空闲进程。
网卡中断绑定配置
当CPU可以平行收包时,就会出现不同的核收取了同一个queue的报文,这就会产生报文乱序的问题,解决方法是将一个queue的中断绑定到唯一的一个核上去,从而避免了乱序问题,是的默认都是绑定到了CPU0上。但是如果网络流量大的时候,就需要将软中断均匀的分散到各个核上,避免CPU成为瓶颈。
首先查看网卡是否支持多队列,使用lspci -vvv命令,找到Ethernet controller项:
如果有MSI-X, Enable+ 并且Count > 1,则该网卡是多队列网卡。
然后可以查看是否打开了网卡多队列,使用命令cat /proc/interrupts,如果看到如下图信息表明多队列支持已经打开:
/proc/interrupts文件
在Linux的机器上,/proc/interrupts这个文件包含有关于哪些中断正在使用和每个处理器各被中断了多少次的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# cat /proc/interrupts
CPU0
CPU1
CPU2
CPU3
0
:
1825291229
0
0
0
IO
-
APIC
-
edge
timer
1
:
3
0
0
0
IO
-
APIC
-
edge
i8042
8
:
1
0
0
0
IO
-
APIC
-
edge
rtc
9
:
0
0
0
0
IO
-
APIC
-
level
acpi
12
:
4
0
0
0
IO
-
APIC
-
edge
i8042
50
:
54
859
164
0
IO
-
APIC
-
level
ehci_hcd
:
usb2
,
uhci_hcd
:
usb5
58
:
127
0
20
0
IO
-
APIC
-
level
uhci_hcd
:
usb3
66
:
0
0
0
0
IO
-
APIC
-
level
uhci_hcd
:
usb4
74
:
0
0
0
0
IO
-
APIC
-
level
uhci_hcd
:
usb6
82
:
221
26433417
125600672
28753602
IO
-
APIC
-
level
ata
_piix
122
:
1709
25131
3548386
117109
PCI
-
MSI
-
X
eth0
-
0
130
:
441
2167083
205623005
7779272
PCI
-
MSI
-
X
eth0
-
1
138
:
418
4803724
109660398
7188985
PCI
-
MSI
-
X
eth0
-
2
146
:
389
5473623
97942351
6893766
PCI
-
MSI
-
X
eth0
-
3
154
:
397
4999666
83224034
6996673
PCI
-
MSI
-
X
eth0
-
4
162
:
399
4542680
84930436
8034803
PCI
-
MSI
-
X
eth0
-
5
170
:
406
4429835
132193602
8443930
PCI
-
MSI
-
X
eth0
-
6
177
:
36677
73238285
160033165
71568233
IO
-
APIC
-
level
megasas
178
:
449
2881815
172147095
5454282
PCI
-
MSI
-
X
eth0
-
7
233
:
22
0
1
0
IO
-
APIC
-
level
ehci_hcd
:
usb1
NMI
:
1700174
2794161
5702230
2728583
LOC
:
1826804382
1826804332
1826804259
1826804177
|
对上面文件的输出,解释如下:
● 第一列表示IRQ号。
● 第二、三、四列表示相应的CPU核心被中断的次数。在上面的例子中,timer表示中断名称(为系统时钟)。1825291229表示CPU0被中断了1825291229次。i8042表示控制键盘和鼠标的键盘控制器。
● 对于像rtc(real time clock)这样的中断,CPU是不会被中断的。因为RTC存在于电子设备中,是用于追踪时间的。
● NMI和LOC是系统所使用的驱动,用户无法访问和配置。
IRQ号决定了需要被CPU处理的优先级,IRQ号越小意味着优先级越高。
例如,如果CPU同时接收了来自键盘和系统时钟的中断,那么CPU首先会服务于系统时钟,因为他的IRQ号是0。
● IRQ0 :系统时钟(不能改变)。
● IRQ1 :键盘控制器(不能改变)。
● IRQ3 :串口2的串口控制器(如有串口4,则其也使用这个中断)。
● IRQ4 :串口1的串口控制器(如有串口3,则其也使用这个中断)。
● IRQ5 :并口2和3或声卡。
● IRQ6 :软盘控制器。
● IRQ7 : 并口1,它被用于打印机或若是没有打印机,可以用于任何的并口。
而对于像操作杆(或称为游戏手柄)上的CPU,它并不会等待设备发送中断。因为操作杆主要用于游戏,操作杆的移动必须非常快,因此使用轮询的方式检测设备是否需要CPU的关注还是比较理想的。使用轮询方式的缺点是CPU就处于了忙等状态,因为CPU会不停的多次检查设备。但是需要注意的是在Linux中,这种处理信号的方式也是必不可少的。
最后确认每个队列是否绑定到不同的CPU核心上,cat /proc/interrupts查询到每个队列的中断号,对应的文件/proc/irq/${IRQ_NUM}/smp_affinity为中断号IRQ_NUM绑定的CPU核的情况。以十六进制表示,每一位代表一个CPU核:
(00000001)代表CPU0
(00000010)代表CPU1
(00000011)代表CPU0和CPU1
SMP是指”对称多处理器”,smp_affinity文件主要用于某个特定IRQ要绑定到哪个CPU核心上。在/proc/irq/IRQ_NUMBER/目录下都有一个smp_affinity文件,例如,网卡的中断号是:
1
2
3
4
5
6
7
8
9
|
$
grep
eth
/
proc
/
interrupts
122
:
1709
25131
3548386
117109
760075412
18094155
1164194590
25133609
PCI
-
MSI
-
X
eth0
-
0
130
:
441
2167083
205647340
7779272
134255850
18536504
102909662
21313619
PCI
-
MSI
-
X
eth0
-
1
138
:
418
4803724
109671264
7188985
78244984
10873510
64831982
12325705
PCI
-
MSI
-
X
eth0
-
2
146
:
389
5473623
97950876
6893766
75816940
9653979
64914232
12640764
PCI
-
MSI
-
X
eth0
-
3
154
:
397
4999666
83232819
6996673
69941153
8775666
59393814
10502229
PCI
-
MSI
-
X
eth0
-
4
162
:
399
4542680
84950941
8034803
68457494
8964770
59761538
11668785
PCI
-
MSI
-
X
eth0
-
5
170
:
406
4429835
132204416
8443930
95424200
7881102
77029769
19013968
PCI
-
MSI
-
X
eth0
-
6
178
:
449
2881815
172154888
5454282
100168246
18739016
85709743
18162463
PCI
-
MSI
-
X
eth0
-
7
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$
cat
/
proc
/
irq
/
122
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000040
$
cat
/
proc
/
irq
/
130
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000010
$
cat
/
proc
/
irq
/
138
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000004
$
cat
/
proc
/
irq
/
146
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000010
$
cat
/
proc
/
irq
/
154
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000004
$
cat
/
proc
/
irq
/
162
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000010
$
cat
/
proc
/
irq
/
170
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000040
$
cat
/
proc
/
irq
/
178
/
smp
_affinity
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000000
,
00000004
|
上面的十六进制对应的十进制是1,也就是说所有的和网卡驱动相关的中断都是有CPU0来提供服务的。我们可以通过手动改变smp_affinity文件中的值来将IRQ绑定到指定的CPU核心上,或者启用irqbalance服务来自动绑定IRQ到CPU核心上。
IRQ Balance
Irqbalance是一个Linux的实用程序,它主要是用于分发中断请求到CPU核心上,有助于性能的提升。它的目的是寻求省电和性能优化之间的平衡。你可以使用yum进行安装(CentOS系统一般默认安装):
1
2
|
$
rpm
-
qa
|
grep
irqbalance
irqbalance
-
0.55
-
15.el5
|
1
2
|
$
yum
search
irqbalance
$
yum
install
irqbalance
.x86_64
|
Irqbalance对于包含多个核心的系统来说是非常有用的,因为通常中断只被第一个CPU核心服务。
RPS/RFS
前面大量介绍了多队列网卡及中断绑定,但是在单网卡单队列的情况下要想负载做网卡软中断绑定怎么办呢?RPS/RFS就是为此而生的,RPS/RFS功能出现在Linux kernel 2.6.35中,由google的工程师提交的两个补丁,这两个补丁的出现主要功能是在单队列网卡的情况下,在系统层用模拟了多队列的情况,以便达到CPU的均衡。
RPS(Receive Packet Steering)主要是把软中断的负载均衡到各个cpu,简单来说,是网卡驱动对每个流生成一个hash标识,这个HASH值得计算可以通过四元组来计算(SIP,SPORT,DIP,DPORT),然后由中断处理的地方根据这个hash标识分配到相应的CPU上去,这样就可以比较充分的发挥多核的能力了。通俗点来说就是在软件层面模拟实现硬件的多队列网卡功能,如果网卡本身支持多队列功能的话RPS就不会有任何的作用。该功能主要针对单队列网卡多CPU环境,如网卡支持多队列则可使用SMP irq affinity直接绑定硬中断。
由于RPS只是单纯把数据包均衡到不同的cpu,这个时候如果应用程序所在的cpu和软中断处理的cpu不是同一个,此时对于cpu cache的影响会很大,那么RFS(Receive flow steering)确保应用程序处理的cpu跟软中断处理的cpu是同一个,这样就充分利用cpu的cache,这两个补丁往往都是一起设置,来达到最好的优化效果, 主要是针对单队列网卡多CPU环境。
这里只是提到一下RPS/RFS,如果有用到自行查询资料。