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")

相关推荐
qq_4112624210 分钟前
用 ESP32-C3 直接连 Starlink 路由器/热点并完成配网
网络·智能路由器
郝亚军21 分钟前
ubuntu-18.04.6-desktop-amd64安装步骤
linux·运维·ubuntu
Konwledging41 分钟前
kernel-devel_kernel-headers_libmodules
linux
Web极客码42 分钟前
CentOS 7.x如何快速升级到CentOS 7.9
linux·运维·centos
一位赵1 小时前
小练2 选择题
linux·运维·windows
代码游侠2 小时前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
LucDelton2 小时前
Java 读取无限量文件读取的思路
java·运维·网络
Lw老王要学习2 小时前
CentOS 7.9达梦数据库安装全流程解析
linux·运维·数据库·centos·达梦
CRUD酱2 小时前
CentOS的yum仓库失效问题解决(换镜像源)
linux·运维·服务器·centos
Wasim4043 小时前
【渗透测试】SQL注入
网络·数据库·sql