文章目录
- [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?
RPS 是 Receive 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);
上面做了两件工作:
- 添加
网络数据接收工作负载。注意,这里没有做实际的数据接收工作。 - 抛出
NET_RX_SOFTIRQsoftirq 事件,这样后续将在 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
...
}
...
}
到此,RPS 的 skb 接收处理的流程就已经分析完了,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")