核心思想
RPS的核心思想是将不同流的数据包映射到不同的CPU上进行接收,进而发挥多核的优势。数据包到达L4后会放入接收buffer中等待应用程序读取,如果能够更近一步,接收数据包的CPU和应用程序读取数据的CPU是同一个,那么就可以更好的提高cache命中率,进而能有更高的接收速率,这就是RFS补丁要做的事情。
数据结构
rps_sock_flow_table
首先,引入了一个全局的哈希表来记录应用程序上一次读写socket时所运行的cpu id,我们下面称这个表为rps_sock_flow_table,相关数据结构和全局变量如下:
/*
* The rps_sock_flow_table contains mappings of flows to the last CPU
* on which they were processed by the application (set in recvmsg).
*/
struct rps_sock_flow_table {
// 哈希表掩码值,即如果ents数组长度为0x100,那么mask就是0xFF
unsigned int mask;
u16 ents[0];
};
#define RPS_SOCK_FLOW_TABLE_SIZE(_num) (sizeof(struct rps_sock_flow_table) + (_num * sizeof(u16)))
#define RPS_NO_CPU 0xffff
/* One global table that all flow-based protocols share. */
struct rps_sock_flow_table *rps_sock_flow_table;
EXPORT_SYMBOL(rps_sock_flow_table)
和哈希表rps_sock_flow_table关联了一个系统配置参数:/proc/sys/net/core/rps_sock_flow_entries,该参数指定了哈希表中ents数组的大小,哈希表也是在向该节点写值的时候创建的。
rps_sock_flow_table的创建
static int rps_sock_flow_sysctl(ctl_table *table, int write, void __user *buffer, size_t *lenp, loff_t *ppos)
{
unsigned int orig_size, size;
int ret, i;
ctl_table tmp = {
.data = &size,
.maxlen = sizeof(size),
.mode = table->mode
};
struct rps_sock_flow_table *orig_sock_table, *sock_table;
static DEFINE_MUTEX(sock_flow_mutex);
mutex_lock(&sock_flow_mutex);
// 记录了原始表的大小和指针
orig_sock_table = rps_sock_flow_table;
size = orig_size = orig_sock_table ? orig_sock_table->mask + 1 : 0;
// 获取要配置的新参数值,保存到tmp中
ret = proc_dointvec(&tmp, write, buffer, lenp, ppos);
if (write) {
// 只处理写操作
if (size) {
if (size > 1<<30) {
// 表大小上限检查
mutex_unlock(&sock_flow_mutex);
return -EINVAL;
}
// 表大小向上对齐到2的整数次幂
size = roundup_pow_of_two(size);
if (size != orig_size) {
// 表大小有变更,建立新表来替换旧表
sock_table = vmalloc(RPS_SOCK_FLOW_TABLE_SIZE(size));
if (!sock_table) {
mutex_unlock(&sock_flow_mutex);
return -ENOMEM;
}
// 设置掩码
sock_table->mask = size - 1;
} else
// 大小相等则不替换表,但是注意下面的清零逻辑,即使不替换,也会将当前所有记录的映射信息全部清零
sock_table = orig_sock_table;
// 初始化为RPS_NO_CPU
for (i = 0; i < size; i++)
sock_table->ents[i] = RPS_NO_CPU;
} else
// 指定size为0则会导致空的rps_sock_flow_table,这相当于关闭RFS功能
sock_table = NULL;
// 表有变化,替换全局表rps_sock_flow_table并释放原来的表
if (sock_table != orig_sock_table) {
rcu_assign_pointer(rps_sock_flow_table, sock_table);
synchronize_rcu();
vfree(orig_sock_table);
}
}
mutex_unlock(&sock_flow_mutex);
return ret;
}
struct inet_sock
struct inet_sock中新增一个成员rxhash,用来记录该socket的哈希值,socket的哈希值来源于skb->rxhash,而skb->rxhash值的计算在RPS中已经介绍过了。
struct inet_sock {
...
#ifdef CONFIG_RPS
__u32 rxhash;
#endif
}
inet_sk->rxhash的更新是在L4协议处理下行数据时完成的,具体是在tcp_v4_do_rcv()和__udp_queue_rcv_skb()中,都是调用inet_rps_save_rxhash()函数进行设置的,下面只以__udp_queue_rcv_skb()为例说明。
static inline void inet_rps_save_rxhash(const struct sock *sk, u32 rxhash)
{
#ifdef CONFIG_RPS
if (unlikely(inet_sk(sk)->rxhash != rxhash)) {
// 可见,不仅仅会设置rxhash,还会将rps_sock_flow_table中的指定位置设置为RPS_NO_CPU
inet_rps_reset_flow(sk);
inet_sk(sk)->rxhash = rxhash;
}
#endif
}
static int __udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
int rc;
// 调用inet_rps_save_rxhash()将skb->rxhash设置到inet_sk->rxhash
if (inet_sk(sk)->inet_daddr)
inet_rps_save_rxhash(sk, skb->rxhash);
rc = sock_queue_rcv_skb(sk, skb);
...
}
struct netdev_rx_queue
在引入RPS补丁时,为网络设备添加了该RPS接收队列,引入RFS补丁时,对该结构进行了扩充。
/*
* The rps_dev_flow structure contains the mapping of a flow to a CPU and the
* tail pointer for that CPU's input queue at the time of last enqueue.
*/
struct rps_dev_flow {
// 目标cpu,最初来自rps_sock_flow_table,当后者变化时,该值也会相应的做更新
u16 cpu;
u16 fill;
// 上一次数据包放入input_pkt_queue时的位置
unsigned int last_qtail;
};
/*
* The rps_dev_flow_table structure contains a table of flow mappings.
*/
struct rps_dev_flow_table {
// rps_dev_flow_table掩码,即如果flows数组大小为0x100,那么mask就是0xFF
unsigned int mask;
struct rcu_head rcu;
// 使用工作队列来释放rps_dev_flow_table
struct work_struct free_work;
struct rps_dev_flow flows[0];
};
#define RPS_DEV_FLOW_TABLE_SIZE(_num) (sizeof(struct rps_dev_flow_table) + (_num * sizeof(struct rps_dev_flow)))
/* This structure contains an instance of an RX queue. */
struct netdev_rx_queue {
...
// 每个RPS接收队列都有一个rps_flow_table
struct rps_dev_flow_table *rps_flow_table;
...
} ____cacheline_aligned_in_smp;
引入rps_dev_flow_table时,在/sys/class/net/<device>/queues/rx-<n>/rps_flow_cnt属性文件,用于指定特定RPS接收队列的rps_dev_flow_table哈希表的大小。同样,该哈希表也是在该属性文件的写操作中分配创建的。
ssize_t store_rps_dev_flow_table_cnt(struct netdev_rx_queue *queue, struct rx_queue_attribute *attr,
const char *buf, size_t len)
{
unsigned int count;
char *endp;
struct rps_dev_flow_table *table, *old_table;
static DEFINE_SPINLOCK(rps_dev_flow_lock);
if (!capable(CAP_NET_ADMIN))
return -EPERM;
// 要设置的rps_dev_flow_table表的大小
count = simple_strtoul(buf, &endp, 0);
if (endp == buf)
return -EINVAL;
if (count) {
int i;
// 大小上限检查
if (count > 1<<30) {
/* Enforce a limit to prevent overflow */
return -EINVAL;
}
// 向上对齐到2的整数次幂,并且分配内存
count = roundup_pow_of_two(count);
table = vmalloc(RPS_DEV_FLOW_TABLE_SIZE(count));
if (!table)
return -ENOMEM;
// 设置mask值,并且初始化表
table->mask = count - 1;
for (i = 0; i < count; i++)
table->flows[i].cpu = RPS_NO_CPU;
} else
// count指定为0,那么table为NULL,这会导致RFS功能关闭
table = NULL;
// 设置新表
spin_lock(&rps_dev_flow_lock);
old_table = queue->rps_flow_table;
rcu_assign_pointer(queue->rps_flow_table, table);
spin_unlock(&rps_dev_flow_lock);
// 释放旧表
if (old_table)
call_rcu(&old_table->rcu, rps_dev_flow_table_release);
return len;
}
struct softnet_data
在CPU的接收队列中增加一个计数器,用于记录input_pkt_queue队列上数据包的处理进程。
struct softnet_data {
...
unsigned int input_queue_head;
...
}
在process_backlog()函数中,会对该变量进行更新。
static inline void incr_input_queue_head(struct softnet_data *queue)
{
#ifdef CONFIG_RPS
queue->input_queue_head++;
#endif
}
static int process_backlog(struct napi_struct *napi, int quota)
{
...
incr_input_queue_head(queue);
...
}
可见,CPU每处理一个input_pkt_queue队列中的数据包,计数器input_queue_head就加1。
rps_sock_flow_table的更新
当应用程序读写socket时,到了内核时inet_recvmsg()和inet_sendmsg(),这两个函数都是进程上下文,当前cpu就可以作为应用程序最后一次操作socket时的cpu,在这两个函数中会更新rps_sock_flow_talbe。这两个函数调用的都是同一个函数,下面只看inet_sendmsg()。
static inline void rps_record_sock_flow(struct rps_sock_flow_table *table, u32 hash)
{
// table和hash必须都有效时才会更新
if (table && hash) {
// index是更新位置,其值就是hash与哈希表掩码相与后的结果
unsigned int cpu, index = hash & table->mask;
// 如果和之前记录的不一致,则更新
cpu = raw_smp_processor_id();
if (table->ents[index] != cpu)
table->ents[index] = cpu;
}
}
static inline void inet_rps_record_flow(const struct sock *sk)
{
#ifdef CONFIG_RPS
struct rps_sock_flow_table *sock_flow_table;
rcu_read_lock();
// 持有全局哈希表指针
sock_flow_table = rcu_dereference(rps_sock_flow_table);
// 第二个参数来自socket结构中的rxhash
rps_record_sock_flow(sock_flow_table, inet_sk(sk)->rxhash);
rcu_read_unlock();
#endif
}
int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg...)
{
...
// 更新rps_sock_flow_table
inet_rps_record_flow(sk);
}
类似的,当socket关闭时,在inet_release()中也会将对应位置的映射关系清除。
static inline void rps_reset_sock_flow(struct rps_sock_flow_table *table, u32 hash)
{
// 将对应位置设置为RPS_NO_CPU
if (table && hash)
table->ents[hash & table->mask] = RPS_NO_CPU;
}
static inline void inet_rps_reset_flow(const struct sock *sk)
{
#ifdef CONFIG_RPS
struct rps_sock_flow_table *sock_flow_table;
rcu_read_lock();
// 持有全局rps_sock_flow_table指针
sock_flow_table = rcu_dereference(rps_sock_flow_table);
rps_reset_sock_flow(sock_flow_table, inet_sk(sk)->rxhash);
rcu_read_unlock();
#endif
}
int inet_release(struct socket *sock)
{
...
inet_rps_reset_flow(sk);
}
显然,rps_sock_flow_table没有处理冲突,如果两个socket的rxhash哈希到同一个位置时,如果这两个socket属于同一个应用程序,那么没什么问题,但是如果属于不同的应用程序,那么可能会出现两个应用程序不断的互相更新rps_sock_flow_table的情况(这两个应用程序还总是在不同的cpu上被调度的情况)。
netif_receive_skb()
在netif_receive_skb()的实现中,流程变更如下:
int netif_receive_skb(struct sk_buff *skb)
{
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu, ret;
rcu_read_lock();
// 增加了入参rflow
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu >= 0) {
// 增加了入参rflow->last_qtail
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
} else {
rcu_read_unlock();
ret = __netif_receive_skb(skb);
}
return ret;
}
get_rps_cpu()
get_rps_cpu()的核心流程变更如下:
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb, struct rps_dev_flow **rflowp)
{
...
+ struct rps_dev_flow_table *flow_table;
+ struct rps_sock_flow_table *sock_flow_table;
int cpu = -1;
...
// 前面确定RPS接收队列以及计算skb->rxhash的逻辑没什么变化
got_hash:
// 持有rps_dev_flow_talbe和rps_sock_flow_table指针
flow_table = rcu_dereference(rxqueue->rps_flow_table);
sock_flow_table = rcu_dereference(rps_sock_flow_table);
if (flow_table && sock_flow_table) {
u16 next_cpu;
struct rps_dev_flow *rflow;
// 首先将rps_dev_flow_table中保存的cpu作为目标cpu
rflow = &flow_table->flows[skb->rxhash & flow_table->mask];
tcpu = rflow->cpu;
// 在rps_sock_flow_table中找到映射的cpu
next_cpu = sock_flow_table->ents[skb->rxhash & sock_flow_table->mask];
/*
* If the desired CPU (where last recvmsg was done) is
* different from current CPU (one in the rx-queue flow
* table entry), switch if one of the following holds:
* - Current CPU is unset (equal to RPS_NO_CPU).
* - Current CPU is offline.
* - The current CPU's queue tail has advanced beyond the
* last packet that was enqueued using this table entry.
* This guarantees that all previous packets for the flow
* have been dequeued, thus preserving in order delivery.
*/
if (unlikely(tcpu != next_cpu) &&
(tcpu == RPS_NO_CPU || !cpu_online(tcpu) ||
((int)(per_cpu(softnet_data, tcpu).input_queue_head - rflow->last_qtail)) >= 0)) {
// 以rps_sock_flow_table中cpu为准,将数据包交给该cpu接收
tcpu = rflow->cpu = next_cpu;
if (tcpu != RPS_NO_CPU)
rflow->last_qtail = per_cpu(softnet_data, tcpu).input_queue_head;
}
// 如果经过上面的决策,目标cpu ok,那么返回cpu id,作为后续处理该数据包的cpu
if (tcpu != RPS_NO_CPU && cpu_online(tcpu)) {
// 这里会将将*rflowp指向rps_dev_flow_table中的rflow,所以后续enqueue_to_backlog()更新的是表中的数据
*rflowp = rflow;
cpu = tcpu;
goto done;
}
}
// 经过上面RPS和RFS的综合决策后,也没有确定目标cpu,那么根据rps_map确定一个。引入RFS后,该rps_map的作用还在吗?
map = rcu_dereference(rxqueue->rps_map);
if (map) {
tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32];
if (cpu_online(tcpu))
cpu = tcpu;
}
done:
return cpu;
}
enqueue_to_backlog()
将数据包放入目标cpu的input_pkt_queue中时,将当时该队列中最后一个数据包的位置记录到rps_dev_flow_table中的qtail变量中。
static int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
...
if (queue->input_pkt_queue.qlen) {
enqueue:
__skb_queue_tail(&queue->input_pkt_queue, skb);
#ifdef CONFIG_RPS
*qtail = queue->input_queue_head + queue->input_pkt_queue.qlen;
#endif
...
}