Linux 网络:RPS 简介

文章目录

  • [1. 前言](#1. 前言)
  • [2. 什么是 RPS?](#2. 什么是 RPS?)
  • [3. RPS 的实现](#3. RPS 的实现)
  • [4. RPS 的使用](#4. RPS 的使用)
  • [5. RPS 的优化: RFS](#5. RPS 的优化: RFS)
  • [6. RPS 的测量](#6. RPS 的测量)
  • [7. 参考资料](#7. 参考资料)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 什么是 RPS?

RPSReceive Packet Steering 的缩写,该网络功能是在不支持网卡硬件多队列的情形下,用软件来模拟网卡多队列功能进行网络数据包的收取,这样可以充分利用 SMP 多 CPU 架构,不至于出现一核有难,多核围观的情况。该功能由 Google 工程师 Tom Herbert 引入。

3. RPS 的实现

以网卡 STMicro MAC 驱动为例,该网卡驱动使用 NAPI,从网卡 RX 中断开始:

c 复制代码
stmmac_interrupt()
	stmmac_dma_interrupt()
		__napi_schedule()
			/* 参数 1:当前 CPU 的 softnet_data */
			____napi_schedule(this_cpu_ptr(&softnet_data), n);
				/*
				 * 添加 网络数据接收工作 负载。
				 * 注意,这里没有做实际的数据接收工作。
				 */
				list_add_tail(&napi->poll_list, &sd->poll_list);
				/* 抛出 NET_RX_SOFTIRQ,在 softirq 中调用 net_rx_action() 实际的网络数据接收工作  */
				__raise_softirq_irqoff(NET_RX_SOFTIRQ);

上面做了两件工作:

  1. 添加网络数据接收工作负载。注意,这里没有做实际的数据接收工作。
  2. 抛出 NET_RX_SOFTIRQ softirq 事件,这样后续将在 softirq 中调用 net_rx_action() 做网络数据接收处理工作,将数据向上传递给协议栈。

接下来,在 softirq 上下文,将数据向上传递给协议栈,或者通过 RPS 功能派发给其它 CPU 处理:

c 复制代码
net_rx_action()
	napi_poll()
		stmmac_poll()
			stmmac_rx()
				napi_gro_receive()
					napi_skb_finish()
						netif_receive_skb_internal()

如果启用了 RPS 功能(CONFIG_RPS=y),在 netif_receive_skb_internal() 中,RPS 功能函数 get_rps_cpu() 决定到底是将数据向上传递给协议栈,还是派发给其它 CPU (的 softirq) 处理:

c 复制代码
static int netif_receive_skb_internal(struct sk_buff *skb)
{
	...

	rcu_read_lock();
#ifdef CONFIG_RPS
	if (static_key_false(&rps_needed)) {
		struct rps_dev_flow voidflow, *rflow = &voidflow;
		/* 模拟网卡硬件多队列, RPS 将数据分流到各个 cpu */
		int cpu = get_rps_cpu(skb->dev, skb, &rflow);

		if (cpu >= 0) {
			ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail); /* (1) */
			rcu_read_unlock();
			return ret; /* RPS: 派发给 remote CPU 处理的数据包 */
		}
	}
#endif
	ret = __netif_receive_skb(skb); /* (2) RPS: 给当前 CPU 处理的数据包 */
	rcu_read_unlock();
	return ret;
}

如果是将数据包向上传递给协议栈,走上面注释 (2) 处的 __netif_receive_skb();如果是将数据包派发给其它 CPU (的 softirq) 处理,则走 (1) 处的 enqueue_to_backlog() 将接收的 skb 挂接到目标 CPU (的 softnet_data),并记录接收数据 CPU 的 (softnet_data) 信息:

c 复制代码
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
			      unsigned int *qtail)
{
	struct softnet_data *sd;
	unsigned long flags;
	unsigned int qlen;

	sd = &per_cpu(softnet_data, cpu); /* 目标 CPU 的 sofnet_data */

	qlen = skb_queue_len(&sd->input_pkt_queue);
	/*
	 * 传统的、使用非 NAPI 收包方式的驱动:
	 * . qlen <= netdev_max_backlog: 当前 CPU 的 接收队列尚未满
	 * . !skb_flow_limit(skb, qlen): 指定了流量限制, 但尚未达到限流上限
	 */
	if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
		if (qlen) {
enqueue:
			/* 将网络设备收到的包放到 softnet_data 收包队列 input_pkt_queue */
			__skb_queue_tail(&sd->input_pkt_queue, skb);
			input_queue_tail_incr_save(sd, qtail);
			rps_unlock(sd);
			local_irq_restore(flags);
			return NET_RX_SUCCESS;
		}

		/* Schedule NAPI for backlog device
		 * We can use non atomic operation since we own the queue lock
		 */
		if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
			if (!rps_ipi_queued(sd))
				/* 数据目标 @cpu 就是当前 CPU */
				____napi_schedule(sd, &sd->backlog);
		}
		goto enqueue;
	}

	...
}

rps_ipi_queued() 记录要派发目标 CPU 的 (softnet_data) 信息:

c 复制代码
/*
 * Incoming packets are placed on per-CPU queues
 */
struct softnet_data {
	...
#ifdef CONFIG_RPS
	struct softnet_data	*rps_ipi_list;
#endif
	...
	#ifdef CONFIG_RPS
	/* input_queue_head should be written by cpu owning this struct,
	 * and only read by other cpus. Worth using a cache line.
	 */
	unsigned int		input_queue_head ____cacheline_aligned_in_smp;

	/* Elements below can be accessed between CPUs for RPS/RFS */
	call_single_data_t	csd ____cacheline_aligned_in_smp;
	struct softnet_data	*rps_ipi_next;
	unsigned int		cpu;
	unsigned int		input_queue_tail;
#endif
	...
	struct sk_buff_head	input_pkt_queue;
	...
};

/*
 * Check if this softnet_data structure is another cpu one
 * If yes, queue it to our IPI list and return 1
 * If no, return 0
 */
static int rps_ipi_queued(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
	struct softnet_data *mysd = this_cpu_ptr(&softnet_data);

	if (sd != mysd) { /* RPS 选择的数据目标 @cpu 就是当前 CPU */
		sd->rps_ipi_next = mysd->rps_ipi_list; /* 所有 RPS 需 派发给 remote CPU 处理的 sd 列表 */
		mysd->rps_ipi_list = sd; /* 记录 RPS 派发给 remote CPU 处理的 最新的 sd */

		__raise_softirq_irqoff(NET_RX_SOFTIRQ);
		return 1;
	}
#endif /* CONFIG_RPS */
	return 0; /* RPS 选择的数据目标 @cpu 就是当前 CPU */
}

然后在 net_rx_action() 检查 rps_ipi_queued() 记录的 remote CPU 数据派发信息,如果发现存在,则通知 remote CPU 处理数据:

c 复制代码
net_rx_action()
	// 1. 记录接收 skb 的 remote CPU 信息
	napi_poll()
		stmmac_poll()
			stmmac_rx()
				napi_gro_receive()
					napi_skb_finish()
						netif_receive_skb_internal()
							enqueue_to_backlog()
								rps_ipi_queued()
	// 2. 检查是否有需要 remote CPU 处理的 skb,如果有,通知它们处理数据
	net_rps_action_and_irq_enable(sd);
c 复制代码
static void net_rps_action_and_irq_enable(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
	struct softnet_data *remsd = sd->rps_ipi_list;

	if (remsd) {
		sd->rps_ipi_list = NULL;

		local_irq_enable();

		/* Send pending IPI's to kick RPS processing on remote cpus. */
		net_rps_send_ipi(remsd);
	} else
#endif
		local_irq_enable();
}

static void net_rps_send_ipi(struct softnet_data *remsd)
{
#ifdef CONFIG_RPS
	while (remsd) {
		struct softnet_data *next = remsd->rps_ipi_next;

		/*
		 * 在远程 CPU @remsd->cpu 上, 异步执行 rps_trigger_softirq(),
		 * 添加到 远程 CPU @remsd->cpu 的 数据接收工作队列.
		 */
		if (cpu_online(remsd->cpu))
			smp_call_function_single_async(remsd->cpu, &remsd->csd);
		remsd = next;
	}
#endif
}

smp_call_function_single_async()
	generic_exec_single()
		arch_send_call_function_single_ipi()
			smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC)
				...
				rps_trigger_softirq()

#ifdef CONFIG_RPS

...

// 在 remote 目标 CPU 上执行 rps_trigger_softirq()
/* Called from hardirq (IPI) context */
static void rps_trigger_softirq(void *data)
{
	struct softnet_data *sd = data;

	____napi_schedule(sd, &sd->backlog); /* 调度 remote CPU 的 rx 数据接收工作负载 */
	sd->received_rps++;
}

#endif /* CONFIG_RPS */

值得一提的是,rps_trigger_softirq() 函数是在网卡初始化接口 net_dev_init() 中设置:

c 复制代码
static int __init net_dev_init(void)
{
	int i, rc = -ENOMEM;

	...
	/* 每 CPU sofnet_data 初始化 */
	for_each_possible_cpu(i) {
		...
		struct softnet_data *sd = &per_cpu(softnet_data, i);

		...
#ifdef CONFIG_RPS
		sd->csd.func = rps_trigger_softirq;
		sd->csd.info = sd;
		sd->cpu = i;
#endif

		...
	}

	...
}

到此,RPSskb 接收处理的流程就已经分析完了,get_rps_cpu() 选择接收 skb 的目标 CPU 的算法,是整个 RPS 功能的核心之一,本文对此不做展开,对此感兴趣的读者可以参考 一文看懂linux 内核网络中 RPS/RFS 原理

4. RPS 的使用

从前面的代码分析观察到,在函数 netif_receive_skb_internal() 有一个 if (static_key_false(&rps_needed)) 的条件判断,rps_needed 要通过哪里设置?这里我们不讨论内核代码实现细节,只指出 rps_needed 用户空间设置接口导出代码和功能。

c 复制代码
/* net/core/net-sysfs.c */

static ssize_t store_rps_map(struct netdev_rx_queue *queue,
			     const char *buf, size_t len)
{
	...

	if (map)
		static_key_slow_inc(&rps_needed);
	if (old_map)
		static_key_slow_dec(&rps_needed);

	...
}

上面代码导出接口 /sys/class/net/<NIC>/queues/rx-N/rps_cpus,如:

c 复制代码
# ls /sys/class/net/eth0/queues/rx-0/ -l
total 0
-rw-r--r-- 1 root root 4096 Sep 19 17:57 rps_cpus
-rw-r--r-- 1 root root 4096 Sep 19 23:06 rps_flow_cnt

其作用是 RPS 派发 skb 目标 CPU 集合的掩码,如 RPS 派发 skb 到 CPU 0~3,则可以设置为:

bash 复制代码
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
c 复制代码
/* net/core/sysctl_net_core.c */

#ifdef CONFIG_RPS
static int rps_sock_flow_sysctl(struct ctl_table *table, int write,
				void __user *buffer, size_t *lenp, loff_t *ppos)
{
	...
	if (write) {
		...
		if (sock_table != orig_sock_table) {
			...
			if (sock_table) {
				static_key_slow_inc(&rps_needed);
				static_key_slow_inc(&rfs_needed);
			}
			if (orig_sock_table) {
				static_key_slow_dec(&rps_needed);
				static_key_slow_dec(&rfs_needed);
				synchronize_rcu();
				vfree(orig_sock_table);
			}
		}
	}
	...
}
#endif /* CONFIG_RPS */

上面代码导出接口 /proc/sys/net/core/rps_sock_flow_entries,该接口设置 RPS 全局流表条目数,相对于 /sys/class/net/<NIC>/queues/rx-N/rps_flow_cnt 每队列的流表条目数。

关于 RPS 更多配置的细节,看参考 Linux 内核官方文档:Scaling in the Linux Networking Stack

5. RPS 的优化: RFS

RPS 只考虑了将 skb 均衡到各 CPU,而没有考虑 skb 目标接收进程读取 cache 命中率的问题,假设接收进程 A 运行在 CPU 0,而 RPS 出于 skb 在 CPU 上均衡考量,将目的地为进程 A 的 skb 派发了给 CPU 1 处理,则导致了进程 skb 内存的 cache miss,RFS(Receive Flow Steering) 功能就是让 RPS 考虑 CPU skb 均衡的同时,也考虑接收目的进程 cache 目中率的问题。

Linux 内核的配置文件 net/Kconfig 中的 CONFIG_RFS_ACCEL 配置项启用 RFS 功能:

c 复制代码
config RFS_ACCEL
        bool
        depends on RPS
        select CPU_RMAP
        default y

可以看到,配置项 CONFIG_RFS_ACCEL 依赖于 CONFIG_RPS,可见,RFS 是对 RPS 的增强。对 RFS 的实现细节,苯二五年不做涉及,感兴趣得的读者可参考:一文看懂linux 内核网络中 RPS/RFS 原理

6. RPS 的测量

对 RPS 功能的性能测量,可参考 Linux内核 RPS/RFS功能详细测试分析

7. 参考资料

1\] [Receive packet steering](https://lwn.net/Articles/362339/ "Receive packet steering") \[2\] [rps: Receive packet steering](https://lwn.net/Articles/361440/ "rps: Receive packet steering") \[3\] [Scaling in the Linux Networking Stack](https://www.kernel.org/doc/Documentation/networking/scaling.txt "Scaling in the Linux Networking Stack")

相关推荐
Awkwardx2 小时前
Linux网络编程—五种IO模型与非阻塞IO
linux·服务器·网络
未来之窗软件服务2 小时前
幽冥大陆(六十五) PHP6.x SSL 文字解密—东方仙盟古法结界
网络·数据库·ssl·加解密·仙盟创梦ide·东方仙盟
小鹏linux2 小时前
【linux】进程与服务管理命令 - pkill
linux·运维·服务器
ChenXinBest2 小时前
一次firewalld和docker冲突问题排查
linux·docker
Henry Zhu1232 小时前
VPP中DHCP插件源码深度解析第二篇:DHCPv4客户端实现详解(下)
服务器·c语言·网络·计算机网络·云原生
墨白曦煜2 小时前
计算机组成原理:大端序与小端序的原理与权衡
linux·windows
写代码的橘子n3 小时前
IPv6协议深入学习指南(从易到难)
网络·计算机网络·ipv6
Knight_AL3 小时前
HTTP 状态码一览:理解 2xx、3xx、4xx 和 5xx 分类
网络·网络协议·http
网硕互联的小客服3 小时前
人工智能服务器是什么,人工智能服务器的有什么用?
运维·服务器·网络·安全