linux网络软中断softirq底层机制及并发优化
在实际生产系统环境中,我们经常碰到过高的软中断导致CPU的si负载偏高,从而导致性能服务器性能出现瓶颈。而这种瓶颈出现的时候往往是在业务高峰期,此时很多优化手段不敢轻易去上,只能祈祷平稳度过。但是如果能从底层去了解网络软中断,就可以在事前将优化做充足。
1.1.1 软中断
软中断(softirq)表示可延迟函数的所有种类, linux上使用的软中断个数是有限的,linux最多注册32个,目前使用了10个左右,在include/linux/interrupt.h中定义,如下。
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
软中断(即使同一类型的软中断)可以并发运行在多个CPU上,因此软中断是可重入函数必须使用自旋锁保护其数据结构。一个软中断不会去抢占另外一个软中断。特别适合网络后半段的处理。
1.1.2 网络软中断定义
软中断通过open_softirq函数(定义在kernel/softirq.c文件中)来注册的。open_softirq注册一个软中断处理函数,即在软中断向量表softirq_vec数组中添加新的软中断处理action函数。
我们可以从start_kernel函数开始,该函数定义在init/main.c中。会调用softirq_init(),该函数会调用open_softirq函数来注册相关的软中断,但是并没有注册网络相关的软中断:
该函数如下:
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
那么网络相关的软中断在哪里呢?其也是在startup_kernel函数中的中,调用链路如下:
startup_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup(); 而do_basic_setup函数会进行驱动设置。会通过调用net_dev_init函数。
net_dev_init函数(定义在net/core/dev.c),最注册软中断,如下:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
定义在:kernel/softirq.c文件中
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
这个就是网络接收和发送的软中断,并关联两个函数net_tx_action和net_rx_action。软中断由softirq_action结构体表示,
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
定义有了,那何时会去调用呢?
1.1.3 软中断调用
这需要回到网卡的中断函数中(位于驱动代码中)
在网卡驱动的中断函数中(如果是e1000,则是e1000_intr函数),其会调用__napi_schedule函数,其调用____napi_schedule,该函数会设置NET_RX_SOFTIRQ。
net/core/dev.c
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}
/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
kernel/softirq.c文件
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
include/linux/interrupt.h文件中:
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
arch/ia64/include/asm/hardirq.h文件中:
#define local_softirq_pending() (local_cpu_data->softirq_pending)
arch/ia64/include/asm/processor.h文件中:
#define local_cpu_data (&__ia64_per_cpu_var(ia64_cpu_info))
ia64_cpu_info的结构体为cpuinfo_ia64,定义在在文件arch/ia64/include/asm/processor.h中,定义了。其中定义了CPU类型,硬件BUG标志, CPU状态等。
struct cpuinfo_ia64 {
unsigned int softirq_pending;
………
DECLARE_PER_CPU(struct cpuinfo_ia64, ia64_cpu_info);
而__ia64_per_cpu_var是取变量地址。
这样就可以看到,跟软中断相关的字段是每个CPU都有一个64位(32位机器就是32位)掩码的字段
他描述挂起的软中断。每一位对应相应的软中断。比如0位代表HI_SOFTIRQ.
明白了or_softirq_pending函数设置了CPU中NET_RX_SOFTIRQ,表示软中断挂起。
PS:可以参考Linux协议栈(6)——初始化及链路层实现 文件描述。
netif_rx该函数(net/core/dev.c)不特定于网络驱动程序,主要实现从驱动中获取包并丢到缓存队列中,等待其被处理。有些驱动(例如arch/ia64/hp/sim/simeth.c),在中断函数中调用, netif_rx, 而netif_rx函数调用enqueue_to_backlog函数,最后也会调用____napi_schedule函数。而e1000驱动则是直接调用了__napi_schedule函数.
NET_RX_SOFTIRQ(include/linux/interrupt.h)标记。
现在系统有挂起的软中断了,那么谁去运行呢?
1.1.4 触发软中断
l  当调用local_bh_enable()函数激活本地CPU的软中断时。条件满足就调用do_softirq() 来处理软中断。
l  当do_IRQ()完成硬中断处理时调用irq_exit()时会唤醒ksoftirq来处理软中断。
l  当内核线程ksoftirq/n被唤醒时,处理软中断。
以上几点在不同版本中会略有变化,比如某个函数放被包含在另一个函数里面了。在不影响大局理解的前提下,暂时不用去关心这个。
先来看下do_IRQ函数,该函数(arch/x86/kernel/irq.c文件)处理普通设备的中断。该函数会调用irq_exit()函数。irq_exit函数在kernel/softirq.c文件中定义,该函数会调用local_softirq_pending(),如果有挂起的软中断,就调用invoke_softirq函数,如果ksoftirq在运行就返回,如果没有运行就调用wakeup_softirqd唤醒ksoftirq。
执行软中断函数do_softirq 参见于kernel/softirq.c文件,如果有待处理的软中断,会调用__do_softirq()函数, 然后执行相应软中断处理函数,注册两个函数net_tx_action和net_rx_action。
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
}
函数中有pending = local_softirq_pending();
#define local_softirq_pending() (local_cpu_data->softirq_pending)
用于获取是否有挂起的软中断。
每个CPU下都有一个内核函数进程,他叫做ksoftirq/k,如果是第0个CPU,则进程的名字叫做ksoftirq/0。
真正的软中断处理函数net_rx_action和net_tx_action做什么呢?
1.1.5 软中断执行
net_rx_action(net/core/dev.c)用作软中断的处理程序,net_rx_action调用设备的poll方法(默认为process_backlog),process_backlog函数循环处理所有分组。调用__skb_dequeue从等待队列移除一个套接字缓冲区。
调用__netif_receive_skb(net/core/dev.c)函数,分析分组类型、处理桥接,然后调用deliver_skb(net/core/dev.c),该函数调用packet_type->func使用特定于分组类型的处理程序。
1.1.6 并行优化
到此我们对软中断的整个流程有了清晰的认识,下面开始针对几个细节进行学习并探究如何在系统中去优化软中断并发。
网线收到帧(包处理后为帧)后,会将帧拷贝到网卡内部的FIFO缓冲区(一般现在网卡都支持DMA,如果支持则放到DMA内存中),然后触发硬件中断。硬件中断函数属于网卡驱动,在网卡驱动中实现。
中断处理函数会在一个CPU上运行,如果绑定了一个核就在绑定的核上运行。硬中断处理函数构建sk_buff,把frame从网卡FIFO拷贝到内存skb中,然后触发软中断。如果软中断不及时处理内核缓存中的帧,也会导致丢包。这个过程要注意的是,如果网卡中断时绑定在CPU0上处理硬中断的,那么其触发的软中断也是在CPU0上的,因为修改的NET_RX_SOFTIRQ是cpu-per的变量,只有其上的ksoftirq进程会去读取及执行。
多队列网卡由原来的单网卡单队列变成了现在的单网卡多队列。通过多队列网卡驱动的支持,将各个队列通过中断绑定到不同的CPU核上,以满足网卡的需求,这就是多队列网卡的应用。
因此,加大队列数量可以优化系统网络性能,例如10GE的82599网卡,最大可以增加到64个网卡队列。
1.1.6.1 RSS/RPS/RFS/XPS
RSS (Receive Side Scaling ) (接收侧的缩放)
把不同的流分散的不同的网卡多列中,就是多队列的支持,在2.6.36中引入。网卡多队列的驱动提供了一个内核模块参数,用来指定硬件队列个数。每个接收队列都有一个单独的IRQ(中断号),PCIe设备使用MSI-x来路由每个中断到CPU,有效队列的IRQ的映射由/proc/interrupts来指定的。一个终端能被任何一个CPU处理。一些系统默认运行irqbalance来优化中断(但是在NUMA架构下不太好,不如手动绑定到制定的CPU)。
RPS Receive Packet Steering (接收端包的控制)
逻辑上以软件方式实现RSS,适合于单队列网卡或者虚拟网卡,把该网卡上的数据流让多个cpu处理。在netif_rx() 函数和netif_receive_skb()函数中调用get_rps_cpu (定义在net/core/dev.c),来选择应该执行包的队列。基于包的地址和端口(有的协议是2元组,有的协议是4元组)来计算hash值。hash值是由硬件来提供的,或者由协议栈来计算的。hash值保存在skb->rx_hash中,该值可以作为流的hash值可以被使用在栈的其他任何地方。每一个接收硬件队列有一个相关的CPU列表,RPS就可以将包放到这个队列中进行处理,也就是指定了处理的cpu.最终实现把软中断的负载均衡到各个cpu。需要配置了才能使用,默认数据包由中断的CPU来处理的。
对于一个多队列的系统,如果RSS已经配置了,导致一个硬件接收队列已经映射到每一个CPU。那么RPS就是多余的和不必要的。如果只有很少的硬件中断队列(比CPU个数少),每个队列的rps_cpus 指向的CPU列表与这个队列的中断CPU共享相同的内存域,那RPS将会是有效的。
RFS Receive Flow Steering (接收端流的控制) :
RPS依靠hash来控制数据包,提供了好的负载平衡,只是单纯把数据包均衡到不同的cpu,如果应用程序所在的cpu和软中断处理的cpu不是同一个,那么对于cpu cache会有影响。
RFS依靠RPS的机制插入数据包到指定CPU队列,并唤醒该CPU来执行。
数据包并不会直接的通过数据包的hash值被转发,但是hash值将会作为流查询表的索引。这个表映射数据流与处理这个流的CPU。流查询表的每条记录中所记录的CPU是上次处理数据流的CPU。如果记录中没有CPU,那么数据包将会使用RPS来处理。多个记录会指向相同的CPU。
rps_sock_flow_table是一个全局的数据流表,sock_rps_record_flow()来记录rps_sock_flow_table表中每个数据流表项的CPU号。
RFS使用了第二个数据流表来为每个数据流跟踪数据包:rps_dev_flow_table被指定到每个设备的每个硬件接收队列。
加速RFS
加速RFS需要内核编译CONFIG_RFS_ACCEL, 需要NIC设备和驱动都支持。加速RFS是一个硬件加速的负载平衡机制。要启用加速RFS,网络协议栈调用ndo_rx_flow_steer驱动函数为数据包通讯理想的硬件队列,这个队列匹配数据流。当rps_dev_flow_table中的每个流被更新了,网络协议栈自动调用这个函数。驱动轮流地使用一种设备特定的方法指定NIC去控制数据包。如果想用RFS并且NIC支持硬件加速,都需要开启硬件加速RFS。
XPS Transmit Packet Steering(发送端包的控制)
XPS要求内核编译了CONFIG_XPS,根据当前处理软中断的cpu选择网卡发包队列, XPS主要是为了避免cpu由RX队列的中断进入到TX队列的中断时发生切换,导致cpu cache失效损失性能
最后几个优化手段:
l  对于开了超线程的系统,一个中断只绑定到其中一个。
l  对于一个多队列的系统,多列队已经支持。那么RPS就是多余、不必要的。如果只有很少的硬件中断队列(比CPU个数少),每个队列的rps_cpus 指向的CPU列表与这个队列的中断CPU共享相同的内存域,那RPS将会是有效的。
l  RFS主要是为了避免cpu由内核态进入到用户态的时候发生切换,导致cpu cache失效损失性能。
l  不管什么时候,想用RFS并且NIC支持硬件加速,都开启硬件加速RFS。