linux 网络软中断 softirq 底层机制及并发优化

本文详细探讨了Linux网络软中断的底层机制,包括软中断定义、调用过程、触发及执行。重点讲述了网络收发包软中断的执行,以及并行优化策略,如RSS、RPS、RFS和XPS,旨在解决高负载下服务器性能瓶颈问题。通过对这些机制的理解,可以在系统设计中提前做好优化准备。
摘要由CSDN通过智能技术生成

目录

1 软中断

2 网络软中断定义

3 软中断调用(__napi_schedule)

3.1 唤醒中断 ( __raise_softirq_irqoff )

4 触发软中断

4.1 软中断线程 [ksoftirqd/%u]

5 收发包软中断执行

6 并行优化

6.1  RSS/RPS/RFS/XPS

6.1.1 RSS (Receive Side Scaling ) (接收侧的缩放) 

6.1.2 RPS Receive Packet Steering (接收端包的控制) 

6.1.3 RFS Receive Flow Steering (接收端流的控制) :

6.1.4 加速RFS

6.1.5 XPS Transmit Packet Steering(发送端包的控制)

6.2 最后几个优化手段


在实际生产系统环境中,我们经常碰到过高的软中断导致 CPU 的负载偏高,从而导致性能服务器性能出现瓶颈。而这种瓶颈出现的时候往往是在业务高峰期,此时很多优化手段不敢轻易去上,只能祈祷平稳度过。但是如果能从底层去了解网络软中断,就可以在事前将优化做充足。

1 软中断

软中断(softirq)表示可延迟函数的所有种类, linux 上使用的软中断个数是有限的,linux 最多注册 32 个,目前使用了10个左右,在 include/linux/interrupt.h 中定义,如下。

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

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上,因此软中断是可重入函数必须使用自旋锁保护其数据结构。一个软中断不会去抢占另外一个软中断。特别适合网络后半段的处理。

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/core/dev.c)。

/*
 *	Initialize the DEV module. At boot time this walks the device list and
 *	unhooks any devices that fail to initialise (normally hardware not
 *	present) and leaves us with a valid list of present and active devices.
 *
 */

/*
 *       This is called single threaded during boot, so no need
 *       to take the rtnl semaphore.
 */
static int __init net_dev_init(void)
{
	int i, rc = -ENOMEM;

	BUG_ON(!dev_boot_phase);

	if (dev_proc_init())
		goto out;

	if (netdev_kobject_init())
		goto out;

	INIT_LIST_HEAD(&ptype_all);
	for (i = 0; i < PTYPE_HASH_SIZE; i++)
		INIT_LIST_HEAD(&ptype_base[i]);

	INIT_LIST_HEAD(&offload_base);

	if (register_pernet_subsys(&netdev_net_ops))
		goto out;

	/*
	 *	Initialise the packet receive queues.
	 */

	for_each_possible_cpu(i) {
		struct work_struct *flush = per_cpu_ptr(&flush_works, i);
		struct softnet_data *sd = &per_cpu(softnet_data, i);

		INIT_WORK(flush, flush_backlog);

		skb_queue_head_init(&sd->input_pkt_queue);
		skb_queue_head_init(&sd->process_queue);
		INIT_LIST_HEAD(&sd->poll_list);
		sd->output_queue_tailp = &sd->output_queue;
		
#ifdef CONFIG_RPS
		sd->csd.func = rps_trigger_softirq;
		sd->csd.info = sd;
		sd->cpu = i;
#endif

		sd->backlog.poll = process_backlog;
		sd->backlog.weight = weight_p;
	}

	dev_boot_phase = 0;

	/* The loopback device is special if any other network devices
	 * is present in a network namespace the loopback device must
	 * be present. Since we now dynamically allocate and free the
	 * loopback device ensure this invariant is maintained by
	 * keeping the loopback device as the first device on the
	 * list of network devices.  Ensuring the loopback devices
	 * is the first device that appears and the last network device
	 * that disappears.
	 */
	if (register_pernet_device(&loopback_net_ops))
		goto out;

	if (register_pernet_device(&default_device_ops))
		goto out;

	open_softirq(NET_TX_SOFTIRQ, net_tx_action);
	open_softirq(NET_RX_SOFTIRQ, net_rx_action);

	rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
				       NULL, dev_cpu_dead);
	WARN_ON(rc < 0);
	rc = 0;
out:
	return rc;
}

//软中断注册
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}


//软中断向量表
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

3 软中断调用(__napi_schedule)

这需要回到网卡的中断函数中(位于驱动代码中),在网卡驱动的中断函数中(如果是 e1000,则是 e1000_intr 函数),其会调用 __napi_schedule 函数(net/core/dev.c),其调用 ____napi_schedule,该函数会设置 NET_RX_SOFTIRQ。

/**
 * __napi_schedule - schedule for receive
 * @n: entry to schedule
 *
 * The entry's receive function will be scheduled to run.
 * Consider using __napi_schedule_irqoff() if hard irqs are masked.
 */
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);
}
EXPORT_SYMBOL(__napi_schedule);


/* 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);
}

3.1 唤醒中断 ( __raise_softirq_irqoff )

// kernel/softirq.c
void __raise_softirq_irqoff(unsigned int nr) // 触发nr号软中断
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

include/linux/interrupt.h 文件中:

// 将本地cpu的软中断挂起位图与x做或运算
#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状态等。

/*
 * CPU type, hardware bug flags, and per-CPU state.  Frequently used
 * state comes earlier:
 */
struct cpuinfo_ia64 {
	unsigned int softirq_pending;
	unsigned long itm_delta;	/* # of clock cycles between clock ticks */
	unsigned long itm_next;		/* interval timer mask value to use for next clock tick */
	unsigned long nsec_per_cyc;	/* (1000000000<<IA64_NSEC_PER_CYC_SHIFT)/itc_freq */
	unsigned long unimpl_va_mask;	/* mask of unimplemented virtual address bits (from PAL) */
	unsigned long unimpl_pa_mask;	/* mask of unimplemented physical address bits (from PAL) */
	unsigned long itc_freq;		/* frequency of ITC counter */
	unsigned long proc_freq;	/* frequency of processor */
	unsigned long cyc_per_usec;	/* itc_freq/1000000 */
	unsigned long ptce_base;
	unsigned int ptce_count[2];
	unsigned int ptce_stride[2];
	struct task_struct *ksoftirqd;	/* kernel softirq daemon for this CPU */

#ifdef CONFIG_SMP
	unsigned long loops_per_jiffy;
	int cpu;
	unsigned int socket_id;	/* physical processor socket id */
	unsigned short core_id;	/* core id */
	unsigned short thread_id; /* thread id */
	unsigned short num_log;	/* Total number of logical processors on
				 * this socket that were successfully booted */
	unsigned char cores_per_socket;	/* Cores per processor socket */
	unsigned char threads_per_core;	/* Threads per core */
#endif

	/* CPUID-derived information: */
	unsigned long ppn;
	unsigned long features;
	unsigned char number;
	unsigned char revision;
	unsigned char model;
	unsigned char family;
	unsigned char archrev;
	char vendor[16];
	char *model_name;

#ifdef CONFIG_NUMA
	struct ia64_node_data *node_data;
#endif
};

DECLARE_PER_CPU(struct cpuinfo_ia64, ia64_cpu_info);

这样就可以看到,跟软中断相关的字段是每个CPU都有一个64位(32位机器就是32位)掩码的字段。他描述挂起的软中断。每一位对应相应的软中断。比如0位代表HI_SOFTIRQ。明白了or_softirq_pending函数设置了CPU中NET_RX_SOFTIRQ,表示软中断挂起。

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 )标记。现在系统有挂起的软中断了,那么谁去运行呢?

4 触发软中断

  1. 当调用 local_bh_enable() 函数激活本地CPU的软中断时。条件满足就调用do_softirq() 来处理软中断。
  2. 当 do_IRQ() 完成硬中断处理时调用 irq_exit() 时会唤醒 ksoftirq 来处理软中断。
  3. 当内核线程 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。

//获取是否有挂起的软中断
#define local_softirq_pending()         (local_cpu_data->softirq_pending)

asmlinkage __visible void do_softirq(void)
{
	__u32 pending;
	unsigned long flags;

	if (in_interrupt())
		return;

	local_irq_save(flags);
    
	pending = local_softirq_pending();

	if (pending && !ksoftirqd_running(pending))
		do_softirq_own_stack();

	local_irq_restore(flags);
}

static inline void do_softirq_own_stack(void)
{
	__do_softirq();
}

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC s current task context is borrowed for the
	 * softirq. A softirq handled such as network RX might set PF_MEMALLOC
	 * again if the socket is related to swap
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending();
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);

	local_irq_enable();

	h = softirq_vec;//软中断向量表

	while ((softirq_bit = ffs(pending))) {
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1;

		vec_nr = h - softirq_vec;
		prev_count = preempt_count();

		kstat_incr_softirqs_this_cpu(vec_nr);

		trace_softirq_entry(vec_nr);
		h->action(h);
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {
			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count);
		}
		h++;
		pending >>= softirq_bit;
	}

	rcu_bh_qs();
	local_irq_disable();

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)
			goto restart;

		wakeup_softirqd();
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);
	WARN_ON_ONCE(in_interrupt());
	current_restore_flags(old_flags, PF_MEMALLOC);
}

4.1 软中断线程 [ksoftirqd/%u]

每个CPU下都有一个内核函数进程,他叫做 ksoftirq/k,如果是第0个CPU,则进程的名字叫做 ksoftirq/0。

static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,
	.thread_comm		= "ksoftirqd/%u",
};

static __init int spawn_ksoftirqd(void)
{
	cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
				  takeover_tasklets);
	BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

	return 0;
}

static void run_ksoftirqd(unsigned int cpu)
{
	local_irq_disable();
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();
		local_irq_enable();
		cond_resched_rcu_qs();
		return;
	}
	local_irq_enable();
}

真正的软中断处理函数 net_rx_action 和 net_tx_action 做什么呢?

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 使用特定于分组类型的处理程序。

6 并行优化

到此我们对软中断的整个流程有了清晰的认识,下面开始针对几个细节进行学习并探究如何在系统中去优化软中断并发。

网线收到帧(包处理后为帧)后,会将帧拷贝到网卡内部的FIFO缓冲区(一般现在网卡都支持DMA,如果支持则放到DMA内存中),然后触发硬件中断。硬件中断函数属于网卡驱动,在网卡驱动中实现。

中断处理函数会在一个CPU上运行,如果绑定了一个核就在绑定的核上运行。硬中断处理函数构建sk_buff,把frame从网卡FIFO拷贝到内存skb中,然后触发软中断。如果软中断不及时处理内核缓存中的帧,也会导致丢包。这个过程要注意的是,如果网卡中断时绑定在CPU0上处理硬中断的,那么其触发的软中断也是在CPU0上的,因为修改的NET_RX_SOFTIRQ是cpu-per的变量,只有其上的ksoftirq进程会去读取及执行。

多队列网卡由原来的单网卡单队列变成了现在的单网卡多队列。通过多队列网卡驱动的支持,将各个队列通过中断绑定到不同的CPU核上,以满足网卡的需求,这就是多队列网卡的应用。

因此,加大队列数量可以优化系统网络性能,例如10GE的82599网卡,最大可以增加到64个网卡队列。

6.1  RSS/RPS/RFS/XPS

6.1.1 RSS (Receive Side Scaling ) (接收侧的缩放) 

把不同的流分散的不同的网卡多列中,就是多队列的支持,在2.6.36中引入。网卡多队列的驱动提供了一个内核模块参数,用来指定硬件队列个数。每个接收队列都有一个单独的IRQ(中断号),PCIe设备使用MSI-x来路由每个中断到CPU,有效队列的IRQ的映射由/proc/interrupts来指定的。一个终端能被任何一个CPU处理。一些系统默认运行irqbalance来优化中断(但是在NUMA架构下不太好,不如手动绑定到制定的CPU)。

6.1.2 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将会是有效的。

6.1.3 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被指定到每个设备的每个硬件接收队列。

6.1.4 加速RFS

加速RFS需要内核编译CONFIG_RFS_ACCEL, 需要NIC设备和驱动都支持。加速RFS是一个硬件加速的负载平衡机制。要启用加速RFS,网络协议栈调用ndo_rx_flow_steer驱动函数为数据包通讯理想的硬件队列,这个队列匹配数据流。当rps_dev_flow_table中的每个流被更新了,网络协议栈自动调用这个函数。驱动轮流地使用一种设备特定的方法指定NIC去控制数据包。如果想用RFS并且NIC支持硬件加速,都需要开启硬件加速RFS。

6.1.5 XPS Transmit Packet Steering(发送端包的控制)

XPS要求内核编译了CONFIG_XPS,根据当前处理软中断的cpu选择网卡发包队列, XPS主要是为了避免cpu由RX队列的中断进入到TX队列的中断时发生切换,导致cpu cache失效损失性能

6.2 最后几个优化手段

  1. 对于开了超线程的系统,一个中断只绑定到其中一个。
  2. 对于一个多队列的系统,多列队已经支持。那么RPS就是多余、不必要的。如果只有很少的硬件中断队列(比CPU个数少),每个队列的rps_cpus 指向的CPU列表与这个队列的中断CPU共享相同的内存域,那RPS将会是有效的。
  3. RFS主要是为了避免cpu由内核态进入到用户态的时候发生切换,导致cpu cache失效损失性能。
  4. 不管什么时候,想用RFS并且NIC支持硬件加速,都开启硬件加速RFS。
     

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值