linux流量控制

linux 流量控制

1 无类队列

能够接受数据和重新编排、延迟或丢弃数据包,最广泛使用的是pfifo_fast,它是网卡的缺省配置。

1.1 pfifo_fast

在这里插入图片描述
fifo,先进先出.pfifo_fast有三个“频道”,fifo规则应用于每一个频道,并且:如果0频道有数据包等待发送,1频道的包就不会被处理,1频道和2频道之间的关系也是如此。
内核遵照数据包的TOS值,把带有“最小延迟”标记的包放进0频道。

1.1.1 参数含义

priomap与TOS的关系
内核规定数据包的优先权情况,对应的频道。
TOS如下:
在这里插入图片描述
整个字段占用1个字节,表示如下:

   7    6    5    4    3    2    1    0
+----+----+----+----+----+----+----+----+
|              |                   |    |
|    优先权    |      TOS          |MBZ |
|              |                   |    |
+----+----+----+----+----+----+----+----+

TOS的四个bit位表示的含义如下:

二进制    十进制       含义
----------------------------------
1000      8            最小延迟(md)
0100      4            最大throughput(mt)
0010      2            最大可靠性(mr)
0001      1            最小成本(mmc)
0000      0            正常服务

因为这4个bit位之后还有1个bit,所以TOS的值实际上是上述值的2倍。
整个TOS字段的含义与频道的关系如下:

TOS      bits   意义              linux优先权        频道
---------------------------------------------------------
0x0      0      正常服务          0  最好效果        1
0x2      1      最小成本(mmc)     1  填充            2
0x4      2      最大可靠性(mr)    0  最好效果        1
0x6      3      mmc+mr            0  最好效果        1
0x8      4      最大吞吐量(mt)    2  大量传输        2
0xa      5      mmc+mt            2  大量传输        2
0xc      6      mr+mt             2  大量传输        2
0xe      7      mmc+mr+mt         2  大量传输        2
0x10     8      最小延迟(md)      6  交互            0
0x12     9      mmc+md            6  交互            0
0x14     10     mr+md             6  交互            0
0x16     11     mmc+mr+md         6  交互            0
0x18     12     mt+md             4  交互+大量传输   1
0x1a     13     mmc+mt+md         4  交互+大量传输   1
0x1c     14     mr+mt+md          4  交互+大量传输   1
0x1e     15     mmc+mr+mt+md      4  交互+大量传输   1

缺省的权限图如下:

1,2,2,2,1,2,0,0,1,1,1,1,1,1,1,1

比如,优先级全4将被映射到1频道。

1.1.2 linux内核的实现

static const u8 prio2band[TC_PRIO_MAX + 1] = {
	1, 2, 2, 2, 1, 2, 0, 0 , 1, 1, 1, 1, 1, 1, 1, 1
};

/* 3-band FIFO queue: old style, but should be a bit faster than
   generic prio+fifo combination.
 */

#define PFIFO_FAST_BANDS 3

/*
 * Private data for a pfifo_fast scheduler containing:
 * 	- queues for the three band
 * 	- bitmap indicating which of the bands contain skbs
 */
/*
	q:三个不同优先级的报文队列,数组下标越小优先级越高
	bitmap:记录三个优先级队列中有哪些报文需要发送
*/
struct pfifo_fast_priv {
	u32 bitmap;
	struct qdisc_skb_head q[PFIFO_FAST_BANDS];
};

/*
 * Convert a bitmap to the first band number where an skb is queued, where:
 * 	bitmap=0 means there are no skbs on any band.
 * 	bitmap=1 means there is an skb on band 0.
 *	bitmap=7 means there are skbs on all 3 bands, etc.
 */
 //根据priv->bitmap的值去处有报文要发送的队列
static const int bitmap2band[] = {-1, 0, 1, 0, 2, 0, 1, 0};

static inline struct qdisc_skb_head *band2list(struct pfifo_fast_priv *priv,
					     int band)
{
	return priv->q + band;
}

static int pfifo_fast_enqueue(struct sk_buff *skb, struct Qdisc *qdisc,
			      struct sk_buff **to_free)
{
	//如果队列没满,就缓存报文
	if (qdisc->q.qlen < qdisc_dev(qdisc)->tx_queue_len) {
		//根据skb的优先级找到队列策略对应的优先级队列
		int band = prio2band[skb->priority & TC_PRIO_MAX];
		struct pfifo_fast_priv *priv = qdisc_priv(qdisc);
		struct qdisc_skb_head *list = band2list(priv, band);
		//置为优先级队列对应的bitmap位
		priv->bitmap |= (1 << band);
		qdisc->q.qlen++;
		//把报文家务队列
		return __qdisc_enqueue_tail(skb, qdisc, list);
	}
	//如果队列已经满了,丢弃报文
	return qdisc_drop(skb, qdisc, to_free);
}

static struct sk_buff *pfifo_fast_dequeue(struct Qdisc *qdisc)
{
	struct pfifo_fast_priv *priv = qdisc_priv(qdisc);
	//找到有报文要发送的优先级最高的队列
	int band = bitmap2band[priv->bitmap];

	if (likely(band >= 0)) {
		struct qdisc_skb_head *qh = band2list(priv, band);
		//从队列中取一个skb
		struct sk_buff *skb = __qdisc_dequeue_head(qh);

		if (likely(skb != NULL)) {
			qdisc_qstats_backlog_dec(qdisc, skb);
			qdisc_bstats_update(qdisc, skb);
		}
	
		qdisc->q.qlen--;
		if (qh->qlen == 0)
			//如果队列为空,清除bitmap位
			priv->bitmap &= ~(1 << band);

		return skb;
	}
	//没有队列要发送报文,返回NULL
	return NULL;
}

设备默认发送队列规则:

root@ubuntu:/home/jerry# cat /proc/sys/net/core/default_qdisc 
htb

可以直接echo修改:

root@ubuntu:/home/jerry# echo "pfifo_fast" > /proc/sys/net/core/default_qdisc 
root@ubuntu:/home/jerry# cat /proc/sys/net/core/default_qdisc 
pfifo_fast

使用iptbables工具设置TOS的值:

iptables -t mangle -A POSTROUTING -p tcp -j TOS --set-tos 0x2

在这里插入图片描述
已成功设置

1.2 tbf-令牌桶过滤器

只允许不超过事先设定的速率的数据包通过,但可能允许短暂突发流量超过设定值。
tbf很精确,对于网络和处理的影响都很小。

TBF实现包括两部分:
1、A buffer(bucket):bucket最重要的参数是它的大小,即能容纳的token数量;
2、Tokens:token会已特定的速率填充bucket缓冲区;

当一个包到来时,会从bucket中拿到token,然后收集这个包的信息,最后从bucket中删除这个token。这个算法和token flow、data flow结合起来,会产生三种可能的场景:
1、数据速率==token速率:每个包都能找到一个对应的token,然后直接从队列出去,没有延时;
2、数据速率小于token速率:正常到来的数据都能及时发出去,然后删除一个token。由于token速率大于数据速率,会产生bucket积压,极端情况下会把bucket占满。如果数据速率突然高于token速率,就可以消耗这些积压的token。因此积压的token有一个额外的好处:能够容忍短时速率抖动(burst);
3、数据速率大于token速率:token很快就会用完,然后TBFhi关闭一会。这种情况称为overlimit。如果包还是源源不断的到来,就会产生丢包。

1.2.1 参数和使用

limit/latency:
limit:确定有多少字节在队列中等待可用令牌。
latency:参数确定了一个包在tbf中等待传输的最长等待时间。后者计算决定桶的大小、速率、峰值。
burst/buffer/maxburst:
桶的大小,以字节计算。这个参数指定了最多可以有多少个令牌能够立即被使用,即累积可用的token所支持的最大字节数。总体来说,越大的整流速率需要越大的缓冲区。如果缓冲区太小,可能会丢包,因为token到来太快导致无法放入bucket中。

mpu:
最小分组单位,一个包不会小于64字节。

rate:
速度操纵杆。
如果桶里存在令牌而且允许没有令牌。

peakrate:
指bucket发送数据的最快速度。

mtu/minburst:

tc qdisc add dev eth0 root tbf rate 220kbit latency 50ms burst 1540

1.2.2 tbf在内核中的实现
将入队的数据包缓存在队列中,同时按指定的速率产生令牌,只有拥有令牌才能数据包出队,目前netfilter的limit匹配实际上也是tbf算法。定义在net/sched/sch_tbf.c中。

//tbf属性结构
struct tc_tbf_qopt {
	struct tc_ratespec rate;//速率
	struct tc_ratespec peakrate;//峰值速率
	__u32		limit;//限制值
	__u32		buffer;//缓冲区大小
	__u32		mtu;//mtu参数
};
//tbf算法私有数据结构
struct tbf_sched_data {
/* Parameters */
//固定参数
	//流量限制值
	u32		limit;
	/* Maximal length of backlog: bytes */
	//允许的最大包长
	u32		max_size;
	//桶深,即缓冲区大小
	s64		buffer;		/* Token bucket depth/rate: MUST BE >= MTU/B */
	//网卡mtu
	s64		mtu;
	//允许的最大包长
	struct psched_ratecfg rate;
	struct psched_ratecfg peak;

/* Variables */
    //B型token的数量
	s64	tokens;			/* Current number of B tokens */
	//P型token的数量
	s64	ptokens;		/* Current number of P tokens */
	//时间
	s64	t_c;			/* Time check-point */
	//内部流控使用,缺省时使用bfifo
	struct Qdisc	*qdisc;		/* Inner qdisc, default - bfifo queue */
	//定时器
	struct
	qdisc_watchdog watchdog;	/* Watchdog timer */
};

tbf类别操作结构
static const struct Qdisc_class_ops tbf_class_ops = {
	.graft		=	tbf_graft,
	.leaf		=	tbf_leaf,
	.get		=	tbf_get,
	.put		=	tbf_put,
	.walk		=	tbf_walk,
	.dump		=	tbf_dump_class,
};
tbf算法流控操作结构
static struct Qdisc_ops tbf_qdisc_ops __read_mostly = {
	.next		=	NULL,
	.cl_ops		=	&tbf_class_ops,
	.id		=	"tbf",
	.priv_size	=	sizeof(struct tbf_sched_data),
	.enqueue	=	tbf_enqueue,
	.dequeue	=	tbf_dequeue,
	.peek		=	qdisc_peek_dequeued,
	.init		=	tbf_init,
	.reset		=	tbf_reset,
	.destroy	=	tbf_destroy,
	.change		=	tbf_change,
	.dump		=	tbf_dump,
	.owner		=	THIS_MODULE,
};

初始化
static int tbf_init(struct Qdisc *sch, struct nlattr *opt)
{
    //tbf私有数据
	struct tbf_sched_data *q = qdisc_priv(sch);
    //初始化定时器
	qdisc_watchdog_init(&q->watchdog, sch);
	//内部流控初始化为noop_qdisc
	q->qdisc = &noop_qdisc;

	if (opt == NULL)
		return -EINVAL;

	q->t_c = ktime_get_ns();
    //调用tbf_change设置流控结构参数
	return tbf_change(sch, opt);
}

//定时器函数,功能就是清楚阻塞标识,让网卡重新调度
static enum hrtimer_restart qdisc_watchdog(struct hrtimer *timer)
{
	struct qdisc_watchdog *wd = container_of(timer, struct qdisc_watchdog,
						 timer);

	rcu_read_lock();
	__netif_schedule(qdisc_root(wd->qdisc));
	rcu_read_unlock();

	return HRTIMER_NORESTART;
}

//入队函数
//tbf是根据数据包的长度而不是根据个数来济宁流控的,缺省内部流控结构是bfifo
static int tbf_enqueue(struct sk_buff *skb, struct Qdisc *sch,
		       struct sk_buff **to_free)
{
    //tbf私有数据
	struct tbf_sched_data *q = qdisc_priv(sch);
	int ret;
    //如果数据包长度超过tbf允许最大长度,丢包
	if (qdisc_pkt_len(skb) > q->max_size) {
		if (skb_is_gso(skb) && skb_gso_mac_seglen(skb) <= q->max_size)
			return tbf_segment(skb, sch, to_free);
		return qdisc_drop(skb, sch, to_free);
	}
	//入队操作
	ret = qdisc_enqueue(skb, q->qdisc, to_free);
	//入队失败,丢包
	if (ret != NET_XMIT_SUCCESS) {
		if (net_xmit_drop_count(ret))
			qdisc_qstats_drop(sch);
		return ret;
	}
    //入队成功增加相关统计量
	qdisc_qstats_backlog_inc(sch, skb);
	sch->q.qlen++;
	return NET_XMIT_SUCCESS;
}

//出队

static struct sk_buff *tbf_dequeue(struct Qdisc *sch)
{
	struct tbf_sched_data *q = qdisc_priv(sch);
	struct sk_buff *skb;
	//获取队列头部的skb
	skb = q->qdisc->ops->peek(q->qdisc);
	
	if (skb) {
		s64 now;
		s64 toks;
		s64 ptoks = 0;
		//数据包的长度
		unsigned int len = qdisc_pkt_len(skb);
		//获取当前时间
		now = ktime_get_ns();
		//q->t_c上一次调度的时间
		//q->buffer:缓冲区的大小,桶的大小
		toks = min_t(s64, now - q->t_c, q->buffer);
		//获取发送包时候的等待时间
		if (tbf_peak_present(q)) {
			ptoks = toks + q->ptokens;
			//大于mtu,则减为mtu
			if (ptoks > q->mtu)
				ptoks = q->mtu;
			ptoks -= (s64) psched_l2t_ns(&q->peak, len);
		}
		
		toks += q->tokens;
		if (toks > q->buffer)
			toks = q->buffer;
		toks -= (s64) psched_l2t_ns(&q->rate, len);

		if ((toks|ptoks) >= 0) {
			//数据包出队
			skb = qdisc_dequeue_peeked(q->qdisc);
			if (unlikely(!skb))
				return NULL;

			q->t_c = now;
			q->tokens = toks;
			q->ptokens = ptoks;
			qdisc_qstats_backlog_dec(sch, skb);
			sch->q.qlen--;
			qdisc_bstats_update(sch, skb);
			return skb;
		}
		//更新定时器下一次调度的时间
		qdisc_watchdog_schedule_ns(&q->watchdog,
					   now + max_t(long, -toks, -ptoks));

		/* Maybe we have a shorter packet in the queue,
		   which can be sent now. It sounds cool,
		   but, however, this is wrong in principle.
		   We MUST NOT reorder packets under these circumstances.

		   Really, if we split the flow into independent
		   subflows, it would be a very good solution.
		   This is the main idea of all FQ algorithms
		   (cf. CSZ, HPFQ, HFSC)
		 */

		qdisc_qstats_overlimit(sch);
	}
	return NULL;
}

//复位
static void tbf_reset(struct Qdisc *sch)
{
	struct tbf_sched_data *q = qdisc_priv(sch);
	//调用内部流控结构的复位函数
	qdisc_reset(q->qdisc);
	//
	sch->qstats.backlog = 0;
	//清空队列长度
	sch->q.qlen = 0;
	//获取当前时间
	q->t_c = ktime_get_ns();
	//B令牌为缓冲区大小
	q->tokens = q->buffer;
	//P令牌为MTU值
	q->ptokens = q->mtu;
	//删除定时器
	qdisc_watchdog_cancel(&q->watchdog);
}

1.3 sfq随机公平队列

精确性不如其他方法,需要的计算量很少。
sfq的关键是会话,主要针对一个TCP或者UDP流。流量被分成相当多数量的FIFO中,米格队列针对一个会话,会话按照简单轮转的方式发送,每个会话都按顺序得到发送机会。sfg使用一个三列算法,把所有的会话映射到有限的几个队列中去。因为使用了散列,所以可能有多个会话分配到同一个队列里,从而共享发包的机会,也就是共享带宽。为了不让这种效应太明显,sfg会频繁的改变散列算法。只有网卡确实已经挤满了的时候,sfg才会起作用。

1.3.1 参数和使用

perturb:
多少秒后重新配置一次散列算法,10应该是一个合适的值
quantum:
一个流至少要传输多少字节后才切换到下一个队列。
limit:
SFQ能缓存的最大包数(超过这个阈值将导丢包)。

tc qdisc add dev eth1 root sfq perturb 10
root@ubuntu:/# tc -s -d qdisc show dev eth1 
qdisc sfq 8002: root refcnt 2 limit 127p quantum 1514b depth 127 flows 128/1024 divisor 1024 perturb 10sec 
 Sent 107 bytes 1 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0
 8002:这个号码是系统自动分配的一个句柄好,limit有意思是这个队列中可以有127个数据包排队等待,一共可以有1024个散列目标用于速率审计,而其中128个可以同时激活,每隔10秒钟散列算法更换一次。

2 分类队列

2.1 cbq整形队列-基于类的排队规则

参数描述

avpkt:
平均包代销,以字节计。计算maxidle时需要,maxidle从maxburst得出。

bandwidth:
网卡的物理带宽,用来计算闲置时间

cell:
一个数据包被发送出去的时间可以是基于包长度而阶梯评价的。一个800字节的包和一个806字节的包可以认为耗费相同的时间。也就是说它设置时间粒度,通常设置为8,必须是2的整数次幂。

maxburst:
这个参数决定了计算maxidle所使用的数据包的个数。在avgidle跌落到0之前,这么多的数据包可以突发传输出去。这个值越高,越能够容纳突发传输。你无法直接设置maxidle的值,biubiu通过这个参数来控制。

minburst:
发生越限时,cbq会进制发包。该参数越大,整形越精确。

minidle:
如果avgidle降到0,也就是发生了越限,就需要等待,知道avgidle的值足够大才发送数据包包。盖值设置为10代表被先回在-10us上。

mpu:
最小包尺寸。

rate:
期望中的传输速率,也就是“油门”。

allot:
当从外部请求一个cbq发包的时候,它就会按照priority参数指定的顺序轮流尝试其内部的每一个类的队列规定。当轮到一个一个类发数据时,它只能发送一定量的数据,allot参数就是这个量的基值。

prio:
cbq可以像prio设备那样工作。其中prio值较低的类只要有数据就必须先服务,其他类要延后处理。

weight:
该参数控制WRR过程。每个类轮流取得发包的机会。如果其中一个类要求的带宽显著高于其他的类,就应该让它每次比其他的类发送更多的数据。

isolated:
使用该选项配置的类,就不会向其他兄弟类出借带宽。

sharing:
isolated的反义。

bounded:
使用该选项配置的类,意味着它不会向其他兄弟类借用带宽。

borrow:
bounded的反义。

配置范例
把WEB服务器的流量控制为5Mbps、SMTP流量控制在3Mbps上。而二者一共不得超过6Mbps,互相之间允许借用带宽。网卡带宽为100Mbps。

tc qdisc add dev eth0 root handle 1:0 cbq bandwidth 100Mbit avpkt 1000 cell 8
tc class add dev eth0 parent 1:0 classid 1:1 cbq bandwidth 100Mbit rate 6Mbit weight 0.6Mbos prio 8 allot 1514 cell 8 maxburst 20 avpkt 1000 bounded
以上两条设置了根为1:0,并且绑定了类1:1,也就是说整个带宽不能超过6Mbps。
tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 100Mbit rate 5Mbit weight 0.5Mbit prio 5 allot 1514 cell 8 maxburst 20 avpkt 1000
tc class add dev eth0 parent 1:1 classid 1:4 cbq bandwith 100Mbit rate 3Mbit weight 0.3Mbit prio 5 allot 1514 cell 8 maxburst 20 avpkt 1000
我们建立了两条类。注意weight的大小为rate的0.1倍。两个类都没有设置成bounded,但他们都连接到了类1:1,而1:1设置成了bounded,所以两个类的总带宽不会超过6Mbps,同一个cbq下面的子类的主号码必须与cbq自己的号码一致。
tc qdisc add dev eth0 parent 1:3 handle 30: sfq
tc qdisc add dev eth0 parent 1:3 handle 40: sfq
缺省情况下,两个类都有一个FIFO队规定,但是这里把它换成了sfq队列,以保证每个数据流都公平对待。

tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip sport 80 0xffff flowid 1:3
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip sport 25 0xffff flowid 1:4
这两条命令规定了根上的过滤器,保证数据流被送到正确的队列规定中去。

这里,先在根队列中创建了类,然后在类中创建队列,修改了队列方式。那些没有被分类的数据流,直接被1:0处理,没有限制。

2.2 HTB-层级令牌桶

在这里插入图片描述
使用与如下场景:
1、有一个固定总带宽,想将其分割成几个部分,分别用作不同目的;
2、每个部分的带宽是有保证的;
3、还可以指定每个部分向其他部分借带宽;
HTB的工作方式和CBQ类似,但不借助于计算空闲时间来实现整形。在内部,它其实是一个classful TBF-这也是它叫做层级令牌桶的原因。
参数:
default:
每个HTB队列的可选参数,默认值为0,其意义是任何未分类的流量以物理网卡的速率出队,完全绕过连接到根队列的任何分类。如果制定了default的值,未分类(不能和filter匹配)的流量(默认的)会被送到这个参数所制定的类中。

rate:
用来设置限制传输的流量速率。

ceil:
用来设置限制传输流量的最大所需速率。如何需要设置共享带宽,咋需要使用此参数。这个参数可以被认为相当于“可突发带宽”。可以达到的最大速率,即可以从空闲带宽中借用ceil-rate

burst:
桶的大小。就是令牌,用于处理传输数据时必要的参数。可以超出rate的值

quantum:
每轮当前的类能发送的字节数,这是用来控制租借带宽的关键参数。其默认计算quantum=rate/r2q。quantum必须大于1500小于60000.quantum只在class的流量超过了rate但是没有超过ceil时使用,它的值越小,共享带宽的效果越好。

配置范例

#这里的default后面跟的30就是minor编号,如果其他的都没有匹配中,则走minor为30的类
tc qdisc add dev eth0 root handle 1: htb default 30
#从根1:上分配1:1的类,速率为6mbit,可以超出15k
tc class add dev eth0 parent 1: classid 1:1 htb rate 6mbit burst 15k
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit burst 15k
#速率为3mbit,最大不能超过6mbit(意味着有空闲带宽时,可以借用3mbit)可以超出15k,
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 3mbit ceil 6mbit burst 15k
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 1kbit ceil 6mbit burst 15k

在1:10\1:20\1:30这三个类中放置sfq
#每10秒钟变换一次算法,保障不会出现某种网络连接因为某种算法优势一直传输的比其他连接快
tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 10
tc qdisc add dev eth0 parent 1:30 handle 30: sfq perturb 10
添加过滤器,直接把流量导向相应的流
#给eth0添加过滤器,针对的协议是ip协议,放置在根节点1:上,匹配的是目的端口为80,
#一旦数据包满足条件,就把包放到1:10的队列中去,也就是传输速率为5mbit的那个队列
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 25 0xffff flowid 1:20

常用的过滤命令一览:

tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32
u32匹配,可以匹配数据包的任意部分:

根据源/目的ip
match ip src 1.2.3.0/24
match ip dst 4.3.2.0/24

根据源/目的端口,所有ip协议:
match ip sport 80 0xffff
match ip dport 80 0xffff

根据ip协议:
match ip protocol 1 0xff

根据fwmark
使用iptables打上mark标记:
tc filter add dev eth1 protocol parent 1:0 prio 1 handle 6 fw flowid 1:1
iptables -A PREROUTING -t mangle -i eth0 -j MARK --set-mark 6

按TOS字段的值
选择交互和最小延迟的数据流:
tc filter add dev eth0 parent 1:0 protocol ip prio 10 u32 match ip tos 0x10 0xff flowid 1:4

2.3 fib用法

modprobe ifb
ip link list//显示ifb0和ifb1
ip link set dev ifb0 up
tc qdisc add dev eth0 ingress
tc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 flowid 1:1 action mirred egress redirect dev ifb0//通过eth的过滤器规则parent ffff:fff1,把数据重定向到device ifb0
tc qdisc add dev ifb0 root netem delay 750

带宽单位:

带宽或者流速单位: 
kbps
千字节/秒 
mbps
兆字节/秒 
kbit
KBits/秒 
mbit
MBits/秒 
bps或者一个无单位数字
字节数/秒 
数据的数量单位: 
kb或者k
千字节 
mb或者m
兆字节 
mbit
兆bit 
kbit
千bit 

2.4 fq_codel

在这里插入图片描述
the flow queue control delay。在fq_codel中有流的概念,一个flow是基于tuple5和随机数关联的概念。这样子每一个从远端发往你手机上的应用的数据都可以按tuple5划分成唯一的flow。

将数据按照flow区分开来,把数据缓存在该flow的queue中,同时引入DRR调度机制。deficit以字节为单位,可以理解为一个关于该flow是否可以发送的阈值判断,除非有大于0的deficit,否则这个flow缓存的数据不能够发送。

发往同一设备的flow组成一个队列,每次调度的时候,如果队列头的flow的deficit小于0,会放到队列的尾部并补充固定的deficit,然后继续iterate该队列知道找到deficit大于0的flow进行发送,并更新deficit(减掉发送字节数),flow也会被从队列头调度走,等待被调度flow的队列由new、old两个flow队列。

如果某个flow缓存的时间过程,就要依照codel的算法drop;如果缓存需要的总的buffer用完或者缓存的总的packets大于一定阈值,缓存最多数据的flow里面的skb也会被drop,直到有可用的buffer或者缓存总packets小于阈值为止。

这样调度,每个flow都不可能一直发送数据,让其他flow干等,按照发送字节数去调度,最大限度保证,每个flow都有机会发送数据。

在这里插入图片描述

2.5 PRIO-优先级排队规则

PRIO qdisc实际不会整形,只会根据设置的过滤器对流量进行分类。可以将PRIO qdisc理解为pfifo_fast的升级版,它也有多个band,但每个band都是一个独立的class,而不是简单的FIFO。
当一个包enqueue到PRIO dqisc之后,它会根据设置的filters选择一个class,并将包送到这个class。默认情况下会创建三个class。每个class默认情况下都包含一个纯FIFO qdisc,没有其他内部结构,但你可以用其他类型的qdisc替换掉FIFO。
当PRIO qdisc取出一个包时,会先尝试:1.只有lower bands/classes没有数据包可取时,才会尝试higher classes。
如果想就有tc filters而不仅仅是TOS做流量优先级分类时,这个qdisc会非常有用。还可以向这三个预置的classes添加额外的qdisc,毕竟pfifo_fast只能提供简单的FIFO qdisc。
由于没有流量整形功能,因此:
1、如果你的物理链路已经打满了,可以用PRIO qdisc,或者在外层嵌套一个classful qdisc,后者负责流量整形。

参数:
1、bands:需要创建的band数量。每个band实际上是一个class。如果改变这个配置,还需要同时修改priomap参数;
2、priomap:如果没有提供tc filters来指导如何对流量分类,那么PRIO qdisc将依据TC_PRIO优先级来决定优先级。
PRIO qdisc里面的band都是class,默认情况下名字分别为major:1、major:2、major:3,因此如果你的PRIO qdisc是12:,那么tc filter送到12:1的流量将具有更高的优先级。

配置实例:
高吞吐流量将送到30:,交互式流量将送到20:后者10:。

tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:1 handle 10 sfq
tc qdisc add dev eth0 parent 1:2 handle 20 tbf rate 20kbit buffer 1600 limit 3000 
tc qdisc add dev eth0 parent 1:3 handle 30: sfq

linux入口流量控制

业务需求:保证正常的网页浏览,FTP、STMP、POP3,对其他的所有应用加以限制,以免影响正常业务的使用

1、让交互数据包保持较低的延迟时间;
2、上传和下载期间有合理的速率用于网页浏览;
3、对FTP-DATA数据限速;
4、对SMTP、pop3限速;
5、对未分类的进行限制;
6、保证上传不会影响下载;
7、对每个IP额的下载速率进行限制;
8、取得空闲带宽的优先级别;

方法:

1、(eth0)使用HTB分成5类
+-------+
|root 1:|
+-------+
+---------+
|class 1:1|
+---------+
+----+ +----+ +----+ +----+ +----+
|1:11| |1:12| |1:13| |1:14| |1:15|
+----+ +----+ +----+ +----+ +----+

classid 1:11
1)这个类的优先权最高。用有最低的延迟并先取得空闲带宽,因此要设置这个类的峰值速率。ssh、telnet、dns、quake3、smtp命令和syn标记的都属于这一类;
2)为了保证上行数据不会伤害下行数据,还要把ACK包排队队列前面。这就是当发生大批量数据流的时候,双向传输均受到严重影响的原因。因为下行数据的ack必须同上行流量进行竞争,并在处理过程中被延迟;
3)限制上唇速率,把上传速率限制在比可用带宽稍小一些的位置上;
4)排除了下行队列(除了偶尔的突发),保证交互数据包永远排在上行队列的最前面;
classid 1:12 大量传输的类。主要用来处理80、443等;
classid 1:13 此类是用友最大吞吐TOS位的数据包
classid 1:14 这是TOS要求成本最小的数据流;
classid 1:15 最后经过NAT进行大批量传输的机器,以保证他们不会妨碍正常业务

限制下载速率:丢掉那些太快到来的数据包,不让他们导致tcp的速率低于我们期望的速率。因为我们不希望轻易丢掉数据包,所以我们要配置burst来容纳突发传输。

对每一个IP限制最高下载速率。

队列处理:

#用384kbps作为峰值速率,调整ceil为上行速率的75%。
DOWNLINK=2000
UPLINK=384
#清空已有的队列,并把出错消息清空
tc qdisc del dev eth0 root
tc qdisc del dev eth0 root
tc qdisc dev dev eth0 ingrerss
###############################uplink####################################
#建立HTB父类,默认数据走1:15这个类
tc qdisc add dev eth0 root handle 1: htb default 15
#设定uplink的最大速率
#main class
tc class add dev eth0 parent 1: classid 1:1 htb rate ${UPLINK}kbit ceil ${UPLINK} bit
#分类,1:11为最高优先级别,stmp、pop3,ftp_data次之,网页浏览再次之。并对每个类限制了最高速率
#high prio class 1:11
tc class add dev eth0 parent 1:1 classid 1:11 htb rate 128kbit ceil 128kbit prio 0
tc class add dev eth0 parent 1:1 classid 1:12 hbt rate 128kbit ceil ${UPLINK} kbit prio 2
tc class add dev eth0 parent 1:1 classid 1:13 htb rate 32kbit  ceil ${UPLINK}kbit prio 1
tc class add dev eth0 parent 1:1 classid 1:14 htb rate 32kbit ceil ${UPLINK}kbit prio 1
#bulk & default class 1:15-gets slightly less traffic,and a lower priority
tc class add dev eth0 parent 1:1 classid 1:15 htb rate 16kbit ceil ${UPLINK} kbit prio 3
#可以在类下面再附加一个队列规定,以保证带宽的公平使用:
#bost get stochastic fairness
tc qdisc add dev eth0 parent 1:12 handle 12: sfq perturb 10
tc qdisc add dev eth0 parent 1:13 handle 13: sfq perturb 10
tc qdisc add dev eth0 parent 1:13 handle 14: sfq perturb 10
tc qdisc add dev eth0 parent 1:13 handle 15: sfq perturb 10

#分类
#上面的队列处理中等于把所有发出的数据包都给了1:15(tc qdisc add dev eth0 root handle 1: htb default 15)
#TOS mininum Delay in (1:11)
tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle 1 fw classid 1:11
#80/8080/443 in 1:12
tc filter add dev eth0 parent 1:0 protocol ip prio 2 handle 2 fw classid 1:12
#ftp-data in 1:13
tc filter add dev eth0 parent 1:0 protocol ip prio 3 handle 3 fw classid 1:13
#smtp/pop3 in 1:14
tc filter add dev eth0 parent 1:0 protocol ip prio 4 handle 4 fw classid 1:14

tc filter add dev eth0 parent 1:0 protocol ip prio 5 handle 5 fw classid 1:15
#这样数据包会有一个特定的fwmark标记值(handle x fw),表明它应该送给哪个类(classid x)

htb代码梳理

graph TD
subgraph 配置流程
    A[tc_modify_qdisc]-->B[qdisc_create]
    B-->C[qdisc_lookup_ops]
    C-->D[qdisc_base]
end

qdisc_base中存放的是注册不同队列规则,例如htb、cbq等。

struct tcmsg {
	unsigned char	tcm_family;//对于tc,用于是AF_UNSPEC
	unsigned char	tcm__pad1;//填充字段,无异议
	unsigned short	tcm__pad2;
	int		tcm_ifindex;//关联的网络识别索引
	__u32		tcm_handle;//要操作的对象句柄,可以为0让内核选择
	__u32		tcm_parent;//父节点句柄
	__u32		tcm_info;//自定义使用
};

static int __net_init psched_net_init(struct net *net)
{
	struct proc_dir_entry *e;
    //创建/proc/net/psched文件
	e = proc_create("psched", 0, net->proc_net, &psched_fops);
	if (e == NULL)
		return -ENOMEM;

	return 0;
}

static int __init pktsched_init(void)
{
	int err;

	err = register_pernet_subsys(&psched_net_ops);
	if (err) {
		pr_err("pktsched_init: "
		       "cannot initialize per netns operations\n");
		return err;
	}
    //注册默认的排队规则
	register_qdisc(&fq_codel_qdisc_ops);
	register_qdisc(&pfifo_qdisc_ops);
	register_qdisc(&bfifo_qdisc_ops);
	register_qdisc(&pfifo_head_drop_qdisc_ops);
	register_qdisc(&mq_qdisc_ops);
    //在路由netlink协议中注册qdisc和class的操作函数
	rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc, tc_dump_qdisc, NULL);
	rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass, tc_dump_tclass, NULL);

	return 0;
}

tc命令的应用层实现

int do_qdisc(int argc, char **argv)
{
	if (argc < 1)
		return tc_qdisc_list(0, NULL);
	if (matches(*argv, "add") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_EXCL|NLM_F_CREATE, argc-1, argv+1);
	if (matches(*argv, "change") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, 0, argc-1, argv+1);
	if (matches(*argv, "replace") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_CREATE|NLM_F_REPLACE, argc-1, argv+1);
	if (matches(*argv, "link") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_REPLACE, argc-1, argv+1);
	if (matches(*argv, "delete") == 0)
		return tc_qdisc_modify(RTM_DELQDISC, 0,  argc-1, argv+1);
#if 0
	if (matches(*argv, "get") == 0)
		return tc_qdisc_get(RTM_GETQDISC, 0,  argc-1, argv+1);
#endif
	if (matches(*argv, "list") == 0 || matches(*argv, "show") == 0
	    || matches(*argv, "lst") == 0)
		return tc_qdisc_list(argc-1, argv+1);
	if (matches(*argv, "help") == 0) {
		usage();
		return 0;
        }
	fprintf(stderr, "Command \"%s\" is unknown, try \"tc qdisc help\".\n", *argv);
	return -1;
}
//RTM_NEWQDISC和内核注册时候就对应上了
int tc_qdisc_modify(int cmd, unsigned flags, int argc, char **argv)
{
	struct qdisc_util *q = NULL;
	struct tc_estimator est;
	struct {
		struct tc_sizespec	szopts;
		__u16			*data;
	} stab;
	char  d[16];//保存命令行的网络设备名称
	char  k[16];//保存qdisc的名称
	struct {
		struct nlmsghdr 	n;
		struct tcmsg 		t;
		char   			buf[TCA_BUF_MAX];
	} req;

	memset(&req, 0, sizeof(req));
	memset(&stab, 0, sizeof(stab));
	memset(&est, 0, sizeof(est));
	memset(&d, 0, sizeof(d));
	memset(&k, 0, sizeof(k));

	req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
	req.n.nlmsg_flags = NLM_F_REQUEST|flags;
	req.n.nlmsg_type = cmd;
	req.t.tcm_family = AF_UNSPEC;

	while (argc > 0) {
		if (strcmp(*argv, "dev") == 0) {//解析网络设备名称
			NEXT_ARG();
			if (d[0])
				duparg("dev", *argv);
			strncpy(d, *argv, sizeof(d)-1);
		} else if (strcmp(*argv, "handle") == 0) {//解析qdisc句柄
			__u32 handle;
			if (req.t.tcm_handle)
				duparg("handle", *argv);
			NEXT_ARG();
			if (get_qdisc_handle(&handle, *argv))
				invarg(*argv, "invalid qdisc ID");
			req.t.tcm_handle = handle;
		} else if (strcmp(*argv, "root") == 0) {//表示操作的是根qdisc
			if (req.t.tcm_parent) {
				fprintf(stderr, "Error: \"root\" is duplicate parent ID\n");
				return -1;
			}
			req.t.tcm_parent = TC_H_ROOT;
#ifdef TC_H_INGRESS
		} else if (strcmp(*argv, "ingress") == 0) {//表示要操作的是接收方向的qdisc
			if (req.t.tcm_parent) {
				fprintf(stderr, "Error: \"ingress\" is a duplicate parent ID\n");
				return -1;
			}
			req.t.tcm_parent = TC_H_INGRESS;
			strncpy(k, "ingress", sizeof(k)-1);
			q = get_qdisc_kind(k);
			req.t.tcm_handle = 0xffff0000;

			argc--; argv++;
			break;
#endif
		} else if (strcmp(*argv, "parent") == 0) {//解析parent句柄
			__u32 handle;
			NEXT_ARG();
			if (req.t.tcm_parent)
				duparg("parent", *argv);
			if (get_tc_classid(&handle, *argv))
				invarg(*argv, "invalid parent ID");
			req.t.tcm_parent = handle;
		} else if (matches(*argv, "estimator") == 0) {
			if (parse_estimator(&argc, &argv, &est))
				return -1;
		} else if (matches(*argv, "stab") == 0) {
			if (parse_size_table(&argc, &argv, &stab.szopts) < 0)
				return -1;
			continue;
		} else if (matches(*argv, "help") == 0) {
			usage();
		} else {
            //解析具体qdisc的名字
			strncpy(k, *argv, sizeof(k)-1);

			q = get_qdisc_kind(k);
			argc--; argv++;
			break;
		}
		argc--; argv++;
	}

	if (k[0])
		addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);
	if (est.ewma_log)
		addattr_l(&req.n, sizeof(req), TCA_RATE, &est, sizeof(est));

	if (q) {
        //让具体的qdisc实现解析其特定参数
		if (!q->parse_qopt) {
			fprintf(stderr, "qdisc '%s' does not support option parsing\n", k);
			return -1;
		}
		if (q->parse_qopt(q, argc, argv, &req.n))
			return 1;
	} else {
		if (argc) {
			if (matches(*argv, "help") == 0)
				usage();

			fprintf(stderr, "Garbage instead of arguments \"%s ...\". Try \"tc qdisc help\".\n", *argv);
			return -1;
		}
	}

	if (check_size_table_opts(&stab.szopts)) {
		struct rtattr *tail;

		if (tc_calc_size_table(&stab.szopts, &stab.data) < 0) {
			fprintf(stderr, "failed to calculate size table.\n");
			return -1;
		}

		tail = NLMSG_TAIL(&req.n);
		addattr_l(&req.n, sizeof(req), TCA_STAB, NULL, 0);
		addattr_l(&req.n, sizeof(req), TCA_STAB_BASE, &stab.szopts,
			  sizeof(stab.szopts));
		if (stab.data)
			addattr_l(&req.n, sizeof(req), TCA_STAB_DATA, stab.data,
				  stab.szopts.tsize * sizeof(__u16));
		tail->rta_len = (void *)NLMSG_TAIL(&req.n) - (void *)tail;
		if (stab.data)
			free(stab.data);
	}

	if (d[0])  {
		int idx;
        //根据网卡名字获取要操作的网络设备索引
 		ll_init_map(&rth);

		if ((idx = ll_name_to_index(d)) == 0) {
			fprintf(stderr, "Cannot find device \"%s\"\n", d);
			return 1;
		}
		req.t.tcm_ifindex = idx;
	}
    //和内核交互
 	if (rtnl_talk(&rth, &req.n, 0, 0, NULL, NULL, NULL) < 0)
		return 2;

	return 0;
}
//这里的AF_UNSPEC和内核注册的时候对应上了

static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n)
{
	struct net *net = sock_net(skb->sk);
	struct tcmsg *tcm;
	struct nlattr *tca[TCA_MAX + 1];//保存用户态设置的属性
	struct net_device *dev;
	u32 clid;//classid的缩写,代表qdisc的parent
	struct Qdisc *q, *p;
	int err;

	if (!netlink_capable(skb, CAP_NET_ADMIN))
		return -EPERM;

replay:
	/* Reinit, just in case something touches this. */
    //解析netlink消息属性,保存到tca数组中
    err = nlmsg_parse(n, sizeof(*tcm), tca, TCA_MAX, NULL);
	if (err < 0)
		return err;
    //解析netlink消息,找到tc配置信息
	tcm = nlmsg_data(n);
	clid = tcm->tcm_parent;
	q = p = NULL;
    //找到要配置的网络设备对象
	dev = __dev_get_by_index(net, tcm->tcm_ifindex);
	if (!dev)
		return -ENODEV;
    /*
        按照是否制定了parent classid分两种情况处理:
        1、如果没有制定,那么必须制定tcm_handle,表示需要修改网络设备上的跟qdisc
        2、如果制定了,表示要修改的qdisc属于某个类
        该值通常都非0,因为及时要修改跟qdisc,会制定root参数,此时clid就是TC_H_ROOT
    */
	if (clid) {
        //柑橘parent class id,找到要修改的qdisc
		if (clid != TC_H_ROOT) {//父类不是根qdisc
			if (clid != TC_H_INGRESS) {
                //用class id的主号码先查询父类对应的qdisc(就是父类qdisc的handle,这是由命名规则决定的)
				p = qdisc_lookup(dev, TC_H_MAJ(clid));
				if (!p)
					return -ENOENT;
                //这种情况要修改的qdisc一定是class的叶子qdisc,否则始终错误
				q = qdisc_leaf(p, clid);
			} else if (dev_ingress_queue_create(dev)) {
                //要修改的是接收队列qdisc
				q = dev_ingress_queue(dev)->qdisc_sleeping;
			}
		} else {
			q = dev->qdisc;
		}

		/* It may be default qdisc, ignore it */
		if (q && q->handle == 0)
			q = NULL;
        /*
            1、q为空表示当前配置的qdisc为默认qdisc pfifo_fast
            2、tcm->tcm_handle为0表示要让内核分配句柄并且要作为根qdisc
            3、q->handle != tcm->tcm_handle表示要修改制定的qdisc
        */
		if (!q || !tcm->tcm_handle || q->handle != tcm->tcm_handle) {
			if (tcm->tcm_handle) {
				if (q && !(n->nlmsg_flags & NLM_F_REPLACE))
					return -EEXIST;
				if (TC_H_MIN(tcm->tcm_handle))
					return -EINVAL;
				q = qdisc_lookup(dev, tcm->tcm_handle);
				if (!q)
					goto create_n_graft;
				if (n->nlmsg_flags & NLM_F_EXCL)
					return -EEXIST;
				if (tca[TCA_KIND] && nla_strcmp(tca[TCA_KIND], q->ops->id))
					return -EINVAL;
				if (q == p ||
				    (p && check_loop(q, p, 0)))
					return -ELOOP;
				atomic_inc(&q->refcnt);
				goto graft;
			} else {
				if (!q)
					goto create_n_graft;

				/* This magic test requires explanation.
				 *
				 *   We know, that some child q is already
				 *   attached to this parent and have choice:
				 *   either to change it or to create/graft new one.
				 *
				 *   1. We are allowed to create/graft only
				 *   if CREATE and REPLACE flags are set.
				 *
				 *   2. If EXCL is set, requestor wanted to say,
				 *   that qdisc tcm_handle is not expected
				 *   to exist, so that we choose create/graft too.
				 *
				 *   3. The last case is when no flags are set.
				 *   Alas, it is sort of hole in API, we
				 *   cannot decide what to do unambiguously.
				 *   For now we select create/graft, if
				 *   user gave KIND, which does not match existing.
				 */
				if ((n->nlmsg_flags & NLM_F_CREATE) &&
				    (n->nlmsg_flags & NLM_F_REPLACE) &&
				    ((n->nlmsg_flags & NLM_F_EXCL) ||
				     (tca[TCA_KIND] &&
				      nla_strcmp(tca[TCA_KIND], q->ops->id))))
					goto create_n_graft;
			}
		}
	} else {
	    //必须指定句柄以表明要修改的qdisc
		if (!tcm->tcm_handle)
			return -EINVAL;
        //从设备当前配置队列的qdisc中找到要修改的qdisc对象
		q = qdisc_lookup(dev, tcm->tcm_handle);
	}

	/* Change qdisc parameters */
	if (q == NULL)
		return -ENOENT;
	if (n->nlmsg_flags & NLM_F_EXCL)
		return -EEXIST;
	if (tca[TCA_KIND] && nla_strcmp(tca[TCA_KIND], q->ops->id))
		return -EINVAL;
    //修改qdisc配置参数
	err = qdisc_change(q, tca);
	if (err == 0)
		qdisc_notify(net, skb, n, clid, NULL, q);//发通知用户态调用者
	return err;

create_n_graft:
    //没有指定create标记,视为参数错误
	if (!(n->nlmsg_flags & NLM_F_CREATE))
		return -ENOENT;
	if (clid == TC_H_INGRESS) {
		if (dev_ingress_queue(dev))
			q = qdisc_create(dev, dev_ingress_queue(dev), p,
					 tcm->tcm_parent, tcm->tcm_parent,
					 tca, &err);//为接收队列新建并关联一个qdisc
		else
			err = -ENOENT;
	} else {
		struct netdev_queue *dev_queue;

		if (p && p->ops->cl_ops && p->ops->cl_ops->select_queue)
			dev_queue = p->ops->cl_ops->select_queue(p, tcm);
		else if (p)
			dev_queue = p->dev_queue;
		else
			dev_queue = netdev_get_tx_queue(dev, 0);
        //为发送队列新建并关联一个排队规则
		q = qdisc_create(dev, dev_queue, p,
				 tcm->tcm_parent, tcm->tcm_handle,
				 tca, &err);
	}
	if (q == NULL) {
		if (err == -EAGAIN)
			goto replay;
		return err;
	}

graft:
    //用新的qdisc替换老的qdisc
	err = qdisc_graft(dev, p, skb, n, clid, q, NULL);
	if (err) {
		if (q)
			qdisc_destroy(q);
		return err;
	}

	return 0;
}

/*
    @dev、dev_queue:关联的设备和队列
    @parent:新建qdisc的parent class id,对于跟qdisc,该值应该是TC_H_ROOT
    @handle:新建qdisc的句柄,如果为0,则内核自动分配一个
    @tca:新建qdisc参数
    @errp:返回值
*/
static struct Qdisc *
qdisc_create(struct net_device *dev, struct netdev_queue *dev_queue,
	     struct Qdisc *p, u32 parent, u32 handle,
	     struct nlattr **tca, int *errp)
{
	int err;
	struct nlattr *kind = tca[TCA_KIND];//新建qdisc的名称,内核根据名称查找全局qdisc_base表,找到对应模块
	struct Qdisc *sch;
	struct Qdisc_ops *ops;
	struct qdisc_size_table *stab;
    //查找全局qdisc操作表,找到对应的qdisc的操作函数集
	ops = qdisc_lookup_ops(kind);
#ifdef CONFIG_MODULES
	if (ops == NULL && kind != NULL) {
		char name[IFNAMSIZ];
		if (nla_strlcpy(name, kind, IFNAMSIZ) < IFNAMSIZ) {
			/* We dropped the RTNL semaphore in order to
			 * perform the module load.  So, even if we
			 * succeeded in loading the module we have to
			 * tell the caller to replay the request.  We
			 * indicate this using -EAGAIN.
			 * We replay the request because the device may
			 * go away in the mean time.
			 */
			rtnl_unlock();
			request_module("sch_%s", name);
			rtnl_lock();
			ops = qdisc_lookup_ops(kind);
			if (ops != NULL) {
				/* We will try again qdisc_lookup_ops,
				 * so don't keep a reference.
				 */
				module_put(ops->owner);
				err = -EAGAIN;
				goto err_out;
			}
		}
	}
#endif

	err = -ENOENT;
	if (ops == NULL)
		goto err_out;
    //分配一个qdisc对象
	sch = qdisc_alloc(dev_queue, ops);
	if (IS_ERR(sch)) {
		err = PTR_ERR(sch);
		goto err_out2;
	}
    //保存parent class句柄
	sch->parent = parent;
    //设置qdisc句柄
	if (handle == TC_H_INGRESS) {
		sch->flags |= TCQ_F_INGRESS;
		handle = TC_H_MAKE(TC_H_INGRESS, 0);
		lockdep_set_class(qdisc_lock(sch), &qdisc_rx_lock);
	} else {
        //调用者未制定,则由系统分配一个
		if (handle == 0) {
			handle = qdisc_alloc_handle(dev);
			err = -ENOMEM;
			if (handle == 0)
				goto err_out3;
		}
		lockdep_set_class(qdisc_lock(sch), &qdisc_tx_lock);
		if (!netif_is_multiqueue(dev))
			sch->flags |= TCQ_F_ONETXQUEUE;
	}

	sch->handle = handle;
    //调用qdisc的初始化函数
	if (!ops->init || (err = ops->init(sch, tca[TCA_OPTIONS])) == 0) {
		if (tca[TCA_STAB]) {
			stab = qdisc_get_stab(tca[TCA_STAB]);
			if (IS_ERR(stab)) {
				err = PTR_ERR(stab);
				goto err_out4;
			}
			rcu_assign_pointer(sch->stab, stab);
		}
        //处理rate属性
		if (tca[TCA_RATE]) {
			spinlock_t *root_lock;

			err = -EOPNOTSUPP;
			if (sch->flags & TCQ_F_MQROOT)
				goto err_out4;

			if ((sch->parent != TC_H_ROOT) &&
			    !(sch->flags & TCQ_F_INGRESS) &&
			    (!p || !(p->flags & TCQ_F_MQROOT)))
				root_lock = qdisc_root_sleeping_lock(sch);
			else
				root_lock = qdisc_lock(sch);
            //根据rate参数计算qdisc对象的bstats和rate_est参数
			err = gen_new_estimator(&sch->bstats, &sch->rate_est,
						root_lock, tca[TCA_RATE]);
			if (err)
				goto err_out4;
		}

		qdisc_list_add(sch);

		return sch;
	}
err_out3:
	dev_put(dev);
	kfree((char *) sch - sch->padded);
err_out2:
	module_put(ops->owner);
err_out:
	*errp = err;
	return NULL;

err_out4:
	/*
	 * Any broken qdiscs that would require a ops->reset() here?
	 * The qdisc was never in action so it shouldn't be necessary.
	 */
	qdisc_put_stab(rtnl_dereference(sch->stab));
	if (ops->destroy)
		ops->destroy(sch);
	goto err_out3;
}

//关联新的qdisc
static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,
		       struct sk_buff *skb, struct nlmsghdr *n, u32 classid,
		       struct Qdisc *new, struct Qdisc *old)
{
	struct Qdisc *q = old;
	struct net *net = dev_net(dev);
	int err = 0;

	if (parent == NULL) {
        //将新的qdisc设置为设备队列的根qdisc
		unsigned int i, num_q, ingress;

		ingress = 0;
		num_q = dev->num_tx_queues;
        //输入队列时调整参数
		if ((q && q->flags & TCQ_F_INGRESS) ||
		    (new && new->flags & TCQ_F_INGRESS)) {
			num_q = 1;
			ingress = 1;
			if (!dev_ingress_queue(dev))
				return -ENOENT;
		}
        //关闭设备
		if (dev->flags & IFF_UP)
			dev_deactivate(dev);

		if (new && new->ops->attach)
			goto skip;

		for (i = 0; i < num_q; i++) {
            //找到要操作的设备
			struct netdev_queue *dev_queue = dev_ingress_queue(dev);

			if (!ingress)
				dev_queue = netdev_get_tx_queue(dev, i);
            //将网络设备级别的qdisc关联
			old = dev_graft_qdisc(dev_queue, new);
			if (new && i > 0)
				atomic_inc(&new->refcnt);

			if (!ingress)
				qdisc_destroy(old);
		}

skip:
		if (!ingress) {
			notify_and_destroy(net, skb, n, classid,
					   dev->qdisc, new);
			if (new && !new->ops->attach)
				atomic_inc(&new->refcnt);
			dev->qdisc = new ? : &noop_qdisc;

			if (new && new->ops->attach)
				new->ops->attach(new);
		} else {
			notify_and_destroy(net, skb, n, classid, old, new);
		}

		if (dev->flags & IFF_UP)
			dev_activate(dev);
	} else {
		const struct Qdisc_class_ops *cops = parent->ops->cl_ops;
        //将新的qdisc与parent qdisc关联
		err = -EOPNOTSUPP;
		if (cops && cops->graft) {
            //调用类操作集的graft回调完成关联
			unsigned long cl = cops->get(parent, classid);
			if (cl) {
                //parent为为qdisc,new为要关联的新的还是qdisc
                //cl用于让class标识替换的孩子,old保存父qdisc原来的孩子指针
				err = cops->graft(parent, cl, new, &old);
				cops->put(parent, cl);
			} else
				err = -ENOENT;
		}
		if (!err)
			notify_and_destroy(net, skb, n, classid, old, new);
	}
	return err;
}

//将qdisc制定为队列的根,并返回队列原来的根qdisc
struct Qdisc *dev_graft_qdisc(struct netdev_queue *dev_queue,
			      struct Qdisc *qdisc)
{
	struct Qdisc *oqdisc = dev_queue->qdisc_sleeping;
	spinlock_t *root_lock;

	root_lock = qdisc_lock(oqdisc);
	spin_lock_bh(root_lock);
    //复位旧的qdisc
	/* Prune old scheduler */
	if (oqdisc && atomic_read(&oqdisc->refcnt) <= 1)
		qdisc_reset(oqdisc);

	/* ... and graft new one */
    //将新的qdisc制定为队列新的qdisc
    if (qdisc == NULL)
		qdisc = &noop_qdisc;
	dev_queue->qdisc_sleeping = qdisc;
	rcu_assign_pointer(dev_queue->qdisc, &noop_qdisc);

	spin_unlock_bh(root_lock);

	return oqdisc;
}

static int tc_ctl_tfilter(struct sk_buff *skb, struct nlmsghdr *n)
{
	struct net *net = sock_net(skb->sk);
	struct nlattr *tca[TCA_MAX + 1];
	spinlock_t *root_lock;
	struct tcmsg *t;
	u32 protocol;
	u32 prio;
	u32 nprio;
	u32 parent;
	struct net_device *dev;
	struct Qdisc  *q;
	struct tcf_proto **back, **chain;
	struct tcf_proto *tp;
	const struct tcf_proto_ops *tp_ops;
	const struct Qdisc_class_ops *cops;
	unsigned long cl;
	unsigned long fh;
	int err;
	int tp_created = 0;

	if ((n->nlmsg_type != RTM_GETTFILTER) && !netlink_capable(skb, CAP_NET_ADMIN))
		return -EPERM;

replay:
    //解析消息数据
	err = nlmsg_parse(n, sizeof(*t), tca, TCA_MAX, NULL);
	if (err < 0)
		return err;
    
	t = nlmsg_data(n);
    //提取优先级和协议字段
	protocol = TC_H_MIN(t->tcm_info);
	prio = TC_H_MAJ(t->tcm_info);
	nprio = prio;
	parent = t->tcm_parent;
	cl = 0;
    //prio为0表示要内核制定一个默认优先级
	if (prio == 0) {
		/* If no priority is given, user wants we allocated it. */
		if (n->nlmsg_type != RTM_NEWTFILTER ||
		    !(n->nlmsg_flags & NLM_F_CREATE))
			return -ENOENT;
		prio = TC_H_MAKE(0x80000000U, 0U);
	}

	/* Find head of filter chain. */

	/* Find link */
	dev = __dev_get_by_index(net, t->tcm_ifindex);
	if (dev == NULL)
		return -ENODEV;

	/* Find qdisc */
	if (!parent) {
        //未找到parent,那么默认将filter关联到根qdisc
		q = dev->qdisc;
		parent = q->handle;
	} else {
        //无论parent为qdisc或者class的句柄,其主号码肯定都是qdisc的句柄
		q = qdisc_lookup(dev, TC_H_MAJ(t->tcm_parent));
		if (q == NULL)
			return -EINVAL;
	}
    //无类qdisc是不允许关联filter的
	/* Is it classful? */
	cops = q->ops->cl_ops;
	if (!cops)
		return -EINVAL;

	if (cops->tcf_chain == NULL)
		return -EOPNOTSUPP;
    //parent的次号码不为0,说明filter关联的是一个class,找到该class对象
	/* Do we search for filter, attached to class? */
	if (TC_H_MIN(parent)) {
		cl = cops->get(q, parent);
		if (cl == 0)
			return -ENOENT;
	}
    //获取qdisc或其class的filter链表,注意,如果关联的是class,那么cl参数已经是该class对象了
	/* And the last stroke */
	chain = cops->tcf_chain(q, cl);
	err = -EINVAL;
	if (chain == NULL)
		goto errout;

	/* Check the chain for existence of proto-tcf with this priority */
    /*
        1、找到要操作的filter,并且找到要将其插入的位置,prio值小的排在开头
        2、优先级相同且协议相同的filter只能关联一个
        从逻辑可以看出,filter的句柄并不是用来标识filter对象的,内核是先从filter的parent
            找到filter链表,然后从中找到优先级相同且协议号相同的filter
    */
	for (back = chain; (tp = *back) != NULL; back = &tp->next) {
		if (tp->prio >= prio) {
			if (tp->prio == prio) {
				if (!nprio ||
				    (tp->protocol != protocol && protocol))
					goto errout;
			} else
				tp = NULL;
			break;
		}
	}

	root_lock = qdisc_root_sleeping_lock(q);
    //同一个filter链表下,相同优先级且协议号相同的filter内容会被组织到同一个tcf_proto
	if (tp == NULL) {
		/* Proto-tcf does not exist, create new one */
        //此时必须指定名字和协议
		if (tca[TCA_KIND] == NULL || !protocol)
			goto errout;
        
		err = -ENOENT;
		if (n->nlmsg_type != RTM_NEWTFILTER ||
		    !(n->nlmsg_flags & NLM_F_CREATE))
			goto errout;


		/* Create new proto tcf */
        //分配tcf_proto对象并对其进行初始化
		err = -ENOBUFS;
		tp = kzalloc(sizeof(*tp), GFP_KERNEL);
		if (tp == NULL)
			goto errout;
		err = -ENOENT;
        //根据filter名字找到filter操作集
		tp_ops = tcf_proto_lookup_ops(tca[TCA_KIND]);
		if (tp_ops == NULL) {
#ifdef CONFIG_MODULES
			struct nlattr *kind = tca[TCA_KIND];
			char name[IFNAMSIZ];

			if (kind != NULL &&
			    nla_strlcpy(name, kind, IFNAMSIZ) < IFNAMSIZ) {
				rtnl_unlock();
				request_module("cls_%s", name);
				rtnl_lock();
				tp_ops = tcf_proto_lookup_ops(kind);
				/* We dropped the RTNL semaphore in order to
				 * perform the module load.  So, even if we
				 * succeeded in loading the module we have to
				 * replay the request.  We indicate this using
				 * -EAGAIN.
				 */
				if (tp_ops != NULL) {
					module_put(tp_ops->owner);
					err = -EAGAIN;
				}
			}
#endif
			kfree(tp);
			goto errout;
		}
		tp->ops = tp_ops;
		tp->protocol = protocol;
        //可以让内核自动选择一个优先级
		tp->prio = nprio ? : TC_H_MAJ(tcf_auto_prio(*back));
		tp->q = q;
		tp->classify = tp_ops->classify;
		tp->classid = parent;
        //调用filter操作集的init回调
		err = tp_ops->init(tp);
		if (err != 0) {
			module_put(tp_ops->owner);
			kfree(tp);
			goto errout;
		}

		tp_created = 1;

	} else if (tca[TCA_KIND] && nla_strcmp(tca[TCA_KIND], tp->ops->kind))
		goto errout;
    //调用filter操作集get回调获取filter
	fh = tp->ops->get(tp, t->tcm_handle);

	if (fh == 0) {
		if (n->nlmsg_type == RTM_DELTFILTER && t->tcm_handle == 0) {
			spin_lock_bh(root_lock);
			*back = tp->next;
			spin_unlock_bh(root_lock);
            //删除filter
			tfilter_notify(net, skb, n, tp, fh, RTM_DELTFILTER);
			tcf_destroy(tp);
			err = 0;
			goto errout;
		}

		err = -ENOENT;
		if (n->nlmsg_type != RTM_NEWTFILTER ||
		    !(n->nlmsg_flags & NLM_F_CREATE))
			goto errout;
	} else {
		switch (n->nlmsg_type) {
		case RTM_NEWTFILTER:
			err = -EEXIST;
			if (n->nlmsg_flags & NLM_F_EXCL) {
				if (tp_created)
					tcf_destroy(tp);
				goto errout;
			}
			break;
		case RTM_DELTFILTER:
			err = tp->ops->delete(tp, fh);
			if (err == 0)
				tfilter_notify(net, skb, n, tp, fh, RTM_DELTFILTER);
			goto errout;
		case RTM_GETTFILTER:
			err = tfilter_notify(net, skb, n, tp, fh, RTM_NEWTFILTER);
			goto errout;
		default:
			err = -EINVAL;
			goto errout;
		}
	}
    //调用filter操作集change回调修改filter的配置参数,修改后的filter对象在fh中返回
	err = tp->ops->change(net, skb, tp, cl, t->tcm_handle, tca, &fh);
	if (err == 0) {
		if (tp_created) {
			spin_lock_bh(root_lock);
			tp->next = *back;
			*back = tp;
			spin_unlock_bh(root_lock);
		}
		tfilter_notify(net, skb, n, tp, fh, RTM_NEWTFILTER);
	} else {
		if (tp_created)
			tcf_destroy(tp);
	}

errout:
	if (cl)
		cops->put(q, cl);
	if (err == -EAGAIN)
		/* Replay the request. */
		goto replay;
	return err;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
目录 第1章 贡献 1 第2章 简介 2 2.1. 除外责任与许可 2 2.2. 预备知识 2 2.3. LINUX能为你做什么 3 2.4. 内务声明 3 2.5. 访问,CVS和提交更新 4 2.6. 邮件列表 4 2.7. 本文档的布局 4 第3章 介绍 IPROUTE2 6 3.1 为什么使用 IPROUTE2 6 3.2 IPROUTE2 概览 6 3.3 先决条件 6 3.4 浏览你的当前配置 7 3.4.1. 让ip显示我们的链路 7 3.4.2. 让ip显示我们的 IP 地址 7 3.4.3. 让ip显示路由 8 3.5. ARP 9 第4章 规则——路由策略数据库 11 4.1. 简单的源策略路由 11 4.2. 多重上连ISP的路由 12 4.2.1. 流量分割 13 4.2.2. 负载均衡 14 第5章 GRE 和其他隧道 15 5.1. 关于隧道的几点注释 15 5.2. IP-IN-IP 隧道 15 5.3. GRE 隧道 16 4 5.3.1. IPv4隧道 16 5.3.2. IPv6隧道 18 5.4. 用户级隧道 18 第6章 用CISCO和6BONE实现IPV6隧道 19 6.1. IPV6隧道 19 第7章 IPSEC:INTERNET上安全的IP 22 7.1. 从手动密钥管理开始 22 7.2. 自动密钥管理 25 7.2.1. 理论 26 7.2.2. 举例 26 7.2.3. 使用X.509证书进行自动密钥管理 29 7.3. IPSEC隧道 32 7.4. 其它IPSEC软件 33 7.5. IPSEC与其它系统的互操作 33 7.5.1. Windows 33 第8章 多播路由 34 第9章 带宽管理的队列规定 36 9.1. 解释队列和队列规定 36 9.2. 简单的无类队列规定 37 9.2.1. pfifo_fast 37 9.2.2. 令牌桶过滤器(TBF) 39 9.2.3. 随机公平队列(SFQ) 41 9.3. 关于什么时候用哪种队列的建议 42 9.4. 术语 43 9.5. 分类的队列规定 45 9.5.1. 分类的队列规定及其类中的数据流向 45 9.5.2. 队列规定家族:根,句柄,兄弟和父辈 45 9.5.3. PRIO队列规定 46 9.5.4. 著名的CBQ队列规定 48 9.5.5. HTB(Hierarchical Token Bucket, 分层的令牌桶) 54 5 9.6. 使用过滤器对数据包进行分类 55 9.6.1. 过滤器的一些简单范例 56 9.6.2. 常用到的过滤命令一览 57 9.7. IMQ(INTERMEDIATE QUEUEING DEVICE,中介队列设备) 58 9.7.1. 配置范例 58 第10章 多网卡的负载均衡 60 10.1. 告诫 61 10.2. 其它可能性 61 第11章 NETFILTER和IPROUTE——给数据包作标记 62 第12章 对包进行分类的高级过滤器 64 12.1. U32分类器 65 12.1.1. U32选择器 65 12.1.2. 普通选择器 66 12.1.3. 特殊选择器 67 12.2. 路由分类器 67 12.3. 管制分类器 68 12.3.1. 管制的方式 68 12.3.2. 越限动作 69 12.3.3. 范例 70 12.4. 当过滤器很多时如何使用散列表 70 第13章 内核网络参数 72 13.1. 反向路径过滤 72 13.2. 深层设置 73 13.2.1. ipv4一般设置 73 13.2.2. 网卡的分别设置 78 13.2.3. 邻居策略 79 13.2.4. 路由设置 80 第14章 不经常使用的高级队列规定 82 14.1. BFIFO/PFIFO 82 14.1.1. 参数与使用 82 6 14.2. CLARK-SHENKER-ZHANG算法 (CSZ) 82 14.3. DSMARK 83 14.3.1. 介绍 83 14.3.2. Dsmark与什么相关? 83 14.3.3. Differentiated Services指导 84 14.3.4. 使用Dsmark 84 14.3.5. SCH_DSMARK如何工作 84 14.3.6. TC_INDEX过滤器 85 14.4. 入口队列规定 87 14.4.1. 参数与使用 87 14.5. RED(RANDOM EARLY DETECTION,随机提前检测) 87 14.6. GRED(GENERIC RANDOM EARLY DETECTION,一般的随机提前检测) 88 14.7. VC/ATM模拟 89 14.8. WRR(WEIGHTED ROUND ROBIN,加权轮转) 89 第15章 方便菜谱 90 15.1. 用不同的SLA运行多个网站. 90 15.2. 防护SYN洪水攻击 90 15.3. 为防止DDOS而对ICMP限速 91 15.4. 为交互流量设置优先权 92 15.5. 使用NETFILTER,IPROUTE2和SQUID实现WEB透明代理 93 15.5.1. 实现之后的数据流图 96 15.6. 与PMTU发现有关的"基于路由的MTU设置" 96 15.6.1. 解决方案 97 15.7. 与PMTU发现有关的MSS箝位(给ADSL,CABLE,PPPOE和PPTP用户) 98 15.8. 终极的流量控制:低延迟,高速上/下载 98 15.8.1. 为什么缺省设置不让人满意 99 15.8.2. 实际的脚本(CBQ) 100 15.8.3. 实际的脚本(HTB) 102 15.9. 为单个主机或子网限速 103 15.10. 一个完全NAT和QOS的范例 104 7 15.10.1. 开始优化那不多的带宽 104 15.10.2. 对数据包分类 106 15.10.3. 改进设置 107 15.10.4. 让上面的设置开机时自动执行 108 第16章 构建网桥以及用ARP代理构建伪网桥 109 16.1. 桥接与IPTABLES的关系 109 16.2. 桥接与流量整形 109 16.3. 用ARP代理实现伪网桥 109 16.3.1. ARP和ARP代理 110 16.3.2. 实现 110 第17章 动态路由——OSPF和BGP 112 17.1. 用ZEBRA设置OSPF 112 17.1.1. 必要条件 113 17.1.2. 配置Zebra 113 17.1.3. 运行Zebra 115 第18章 其它可能性 117 第19章 进一步学习 119 第20章 鸣谢 120
Linux令牌桶流量控制是一种网络流量控制机制,用于限制进入或离开系统的数据包速率。它基于一个令牌桶模型,在这个模型中,一个固定数量的令牌以固定速率被添加到令牌桶中。每当有一个数据包到达时,系统将从令牌桶中取出一个令牌,只有当令牌桶中有足够的令牌时才允许数据包通过。如果令牌桶中没有足够的令牌,那么数据包就会被丢弃或延迟。 通过配置Linux内核参数和使用工具如tc(Traffic Control),可以实现令牌桶流量控制。以下是一些关键步骤: 1. 启用Linux内核的Traffic Control子系统。这可以通过加载“sch_htb”模块实现。 ``` $ modprobe sch_htb ``` 2. 使用tc命令创建一个类别(class)和队列(qdisc)来实现流量控制。例如,以下命令将创建一个带宽为1mbps、延迟为10ms的队列。 ``` $ tc qdisc add dev eth0 root handle 1: htb default 1 $ tc class add dev eth0 parent 1: classid 1:1 htb rate 1mbit ceil 1mbit $ tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 10ms ``` 3. 根据需求,可以添加过滤规则(filter)以便仅对特定的流量应用流量控制。例如,以下命令将对源IP为192.168.0.1的流量应用限制。 ``` $ tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip src 192.168.0.1 flowid 1:1 ``` 通过以上步骤,你可以实现针对特定网络接口、IP地址或其他条件的流量控制。你可以根据具体需求进行调整和配置,以满足你的流量控制需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值