Linux Fair Queue Packet Scheduler (FQ)公平队列报文调度器

简介

FQ主要用于本地流量调节,某一个套接字上的所有报文被认为是一条流,这条流的速度可以设置在skb->sk上,如果没设置则使用公用的另一套调度。本文关注基于流的调度。
代码中会使用一个flows结构体对应一个套接字,并挂接在红黑树上。
突发避免(也叫pacing)功能:
传输层可以设置sk->sk_pacing_rate,作为这条流的发包速度,这个报文调度器会根据速度在每个包之间添加延时。

重要接口

enqueue():

在红黑树上找到这条流,如果不存在则创建一条流并添加进红黑树。添加skb到每条流的skb队列(fifo)。

dequeue() :

服务于循环中的流。

函数分析

static int fq_enqueue(struct sk_buff *skb, struct Qdisc *sch,
		      struct sk_buff **to_free)
{
	struct fq_sched_data *q = qdisc_priv(sch);
	struct fq_flow *f;

	if (unlikely(sch->q.qlen >= sch->limit))
		return qdisc_drop(skb, sch, to_free);

	f = fq_classify(skb, q);
	if (unlikely(f->qlen >= q->flow_plimit && f != &q->internal)) {
		q->stat_flows_plimit++;
		return qdisc_drop(skb, sch, to_free);
	}

	f->qlen++;
	qdisc_qstats_backlog_inc(sch, skb);
	if (fq_flow_is_detached(f)) {
		struct sock *sk = skb->sk;

		fq_flow_add_tail(&q->new_flows, f);
		if (time_after(jiffies, f->age + q->flow_refill_delay))
			f->credit = max_t(u32, f->credit, q->quantum);
		if (sk && q->rate_enable) {
			if (unlikely(smp_load_acquire(&sk->sk_pacing_status) !=
				     SK_PACING_FQ))
				smp_store_release(&sk->sk_pacing_status,
						  SK_PACING_FQ);
		}
		q->inactive_flows--;
	}

	/* Note: this overwrites f->age */
	flow_queue_add(f, skb);

	if (unlikely(f == &q->internal)) {
		q->stat_internal_packets++;
	}
	sch->q.qlen++;

	return NET_XMIT_SUCCESS;
}

参数说明,skb就是要发送的数据包,sch就是对应的网卡设备上的队列,第三个参数不关注,这是有关锁而不能立即释放skb的需要。

Created with Raphaël 2.2.0 开始 网卡本身是否已经到达上限? 丢包报文 结束 流本身是否已经到达上限? 这条流是否是detached? 将这条流加入到新流上 当前jiffies是否大于这条流空闲的时刻加上可以开始重新填充数据的间隔(40ms) 设置credit = max(流的credit, 队列的quantum) 将skb加入到该流 yes no yes no yes no yes

简单来说,在条件允许的情况下,把skb加入对应的流。这里有个问题,为什么不直接把流挂在对应的套接字上,何必每次都去找。

static void flow_queue_add(struct fq_flow *flow, struct sk_buff *skb)
{
	struct rb_node **p, *parent;
	struct sk_buff *head, *aux;

	fq_skb_cb(skb)->time_to_send = skb->tstamp ?: ktime_get_ns();

	head = flow->head;
	if (!head ||
	    fq_skb_cb(skb)->time_to_send >= fq_skb_cb(flow->tail)->time_to_send) {
		if (!head)
			flow->head = skb;
		else
			flow->tail->next = skb;
		flow->tail = skb;
		skb->next = NULL;
		return;
	}

	p = &flow->t_root.rb_node;
	parent = NULL;

	while (*p) {
		parent = *p;
		aux = rb_to_skb(parent);
		if (fq_skb_cb(skb)->time_to_send >= fq_skb_cb(aux)->time_to_send)
			p = &parent->rb_right;
		else
			p = &parent->rb_left;
	}
	rb_link_node(&skb->rbnode, parent, p);
	rb_insert_color(&skb->rbnode, &flow->t_root);
}
Created with Raphaël 2.2.0 开始 设置skb的发送时间,tcp的话这个字段就是下一包的发送时间,ns级别 流是否为空,或者数据包的发送时间是否大于流最后一个包的发送时间 将skb加入流 结束 将该skb加入流的红黑树 yes no

重点,设置skb的发送时间,这是由tcp自己的算好的,至于加入的是红黑树还是链表,为了便于理解,认为加入链表就好。

static struct sk_buff *fq_dequeue(struct Qdisc *sch)
{
	struct fq_sched_data *q = qdisc_priv(sch);
	struct fq_flow_head *head;
	struct sk_buff *skb;
	struct fq_flow *f;
	unsigned long rate;
	u32 plen;
	u64 now;

	if (!sch->q.qlen)
		return NULL;

	skb = fq_dequeue_head(sch, &q->internal);
	if (skb)
		goto out;

	now = ktime_get_ns();
	fq_check_throttled(q, now);
begin:
	head = &q->new_flows;
	if (!head->first) {
		head = &q->old_flows;
		if (!head->first) {
			if (q->time_next_delayed_flow != ~0ULL)
				qdisc_watchdog_schedule_ns(&q->watchdog,
							   q->time_next_delayed_flow);
			return NULL;
		}
	}
	f = head->first;

	if (f->credit <= 0) {
		f->credit += q->quantum;
		head->first = f->next;
		fq_flow_add_tail(&q->old_flows, f);
		goto begin;
	}

	skb = fq_peek(f);
	if (skb) {
		u64 time_next_packet = max_t(u64, fq_skb_cb(skb)->time_to_send,
					     f->time_next_packet);

		if (now < time_next_packet) {
			head->first = f->next;
			f->time_next_packet = time_next_packet;
			fq_flow_set_throttled(q, f);
			goto begin;
		}
		if (time_next_packet &&
		    (s64)(now - time_next_packet - q->ce_threshold) > 0) {
			INET_ECN_set_ce(skb);
			q->stat_ce_mark++;
		}
	}

	skb = fq_dequeue_head(sch, f);
	if (!skb) {
		head->first = f->next;
		/* force a pass through old_flows to prevent starvation */
		if ((head == &q->new_flows) && q->old_flows.first) {
			fq_flow_add_tail(&q->old_flows, f);
		} else {
			fq_flow_set_detached(f);
			q->inactive_flows++;
		}
		goto begin;
	}
	prefetch(&skb->end);
	plen = qdisc_pkt_len(skb);
	f->credit -= plen;

	if (!q->rate_enable)
		goto out;

	rate = q->flow_max_rate;

	/* If EDT time was provided for this skb, we need to
	 * update f->time_next_packet only if this qdisc enforces
	 * a flow max rate.
	 */
	if (!skb->tstamp) {
		if (skb->sk)
			rate = min(skb->sk->sk_pacing_rate, rate);

		if (rate <= q->low_rate_threshold) {
			f->credit = 0;
		} else {
			plen = max(plen, q->quantum);
			if (f->credit > 0)
				goto out;
		}
	}
	if (rate != ~0UL) {
		u64 len = (u64)plen * NSEC_PER_SEC;

		if (likely(rate))
			len = div64_ul(len, rate);
		/* Since socket rate can change later,
		 * clamp the delay to 1 second.
		 * Really, providers of too big packets should be fixed !
		 */
		if (unlikely(len > NSEC_PER_SEC)) {
			len = NSEC_PER_SEC;
			q->stat_pkts_too_long++;
		}
		/* Account for schedule/timers drifts.
		 * f->time_next_packet was set when prior packet was sent,
		 * and current time (@now) can be too late by tens of us.
		 */
		if (f->time_next_packet)
			len -= min(len/2, now - f->time_next_packet);
		f->time_next_packet = now + len;
	}
out:
	qdisc_bstats_update(sch, skb);
	return skb;
}

参数说明,传入的参数是网卡队列,而不是某个流,返回的是一个skb。

Created with Raphaël 2.2.0 开始 优先返回优先队列上的数据 有关是否节流的操作,我也不太了解 新流链表上是否有节点? 取下新流链表上的一个流 流的信用是否小于0, 将流放入到旧流的队尾,并返回cond2 取下旧流链表上的一个流 预取下一个报文 该报文是否到了发送时间 发送 取下这个报文 这个流的分数减少报文字节数, 更新流的下一个包的发送时间 结束 返回到cond2,并且将这个流加入到红黑树,但是不移动它在链表上的位置 yes no yes no yes no

大致流程没有问题,超时加该流有剩余分数即可以发包。
下面讨论几个重要的疑点:

  • 红黑树的作用
  • 分数的作用
  • 公平体现在哪里

红黑树的作用

什么时候会往红黑树上添加节点

取下一个报文,但是该报文的发送时间还未超时,会将这个流取下,添加到红黑树上,排序根据是下一次允许发包时间。

什么时候把红黑树上的节点取下来

在函数fq_dequeue中的第一步,会顺序检查红黑树上的流,如果到了该流发包的时间,则把该流取下来,一直遍历到该某个流超时时间大于现在。

流下一次允许发包时间的计算

发完一个包之后,使用这个包的大小除以BtlBw,ns为单位,加上这个大小,就是下一次允许发包的时间。

		/* Account for schedule/timers drifts.
		 * f->time_next_packet was set when prior packet was sent,
		 * and current time (@now) can be too late by tens of us.
		 */
		if (f->time_next_packet)
			len -= min(len/2, now - f->time_next_packet);

在计算下一个超时时间时还有这么一段代码,意思是如果优先队列先发送了,那么这个队列发包就会延后了,这里给这个流加速。

分数的作用

什么时候减分

当一个包发完之后,分数需要相应地减去包大小的分数。

什么时候加分

当分数被减到0及以下之后,每次都会加一定的分数,大小是两倍的mtu。

公平体现在哪里

公平就是体现在分数,分数是以字节计算的,而不是包的个数计算的,比如,流1,每次发大包1500字节,流2,每次发小包500字节,假设流1排在流2前面,分数初始值相等,假设都为1500,每次增加1500,两者的速度也是相等的。

  • 第一轮,流1,共发送1500字节,分数减到0,流2,共发送0字节,分数1500,队列->流1->流2
  • 第二轮,流1,共发送1500字节,分数加到1500,流2,共发送500字节,分数1000,队列->流2->流1
  • 第三轮,。。。
  • 第四轮,。。。
  • 第五轮,流1,共发送3000字节,分数减到0,流2,共发送1500字节,分数1500,队列->流1->流2
    以上流程会一直循环下去,所以长时间来看,保证了两条流发送的总的字节数是相等的。

总结

  • 发包速率由套接字自己设定的速率控制,即BBR算法中或者其他算法,设置的BtlBw计算得到。
  • 公平体现在分数上,使用分数保证了上层设置的速度。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页