RFS特性

核心思想

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
...
}

参考资料

  1. RFS补丁;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值