文章目录
- 1 无类队列
-
- 1.1 pfifo_fast
-
- 1.1.1 参数含义
- 1.1.2 linux内核的实现
- 1.2 tbf-令牌桶过滤器
-
- 1.2.1 参数和使用
- 1.3 sfq随机公平队列
-
- 1.3.1 参数和使用
- 2 分类队列
-
- 2.1 cbq整形队列-基于类的排队规则
- 2.2 HTB-层级令牌桶
- 2.3 fib用法
- 2.4 fq_codel
- 2.5 PRIO-优先级排队规则
- 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 thangeneric 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);//从队列中取一个skbstruct 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;}//没有队列要发送报文,返回NULLreturn 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 *///网卡mtus64 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 *///内部流控使用,缺省时使用bfifostruct Qdisc *qdisc; /* Inner qdisc, default - bfifo queue *///定时器structqdisc_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_qdiscq->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;//获取队列头部的skbskb = 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,则减为mtuif (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 independentsubflows, 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 08002:这个号码是系统自动分配的一个句柄好,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:14tc 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_UNSPECunsigned 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 0if (matches(*argv, "get") == 0)return tc_qdisc_get(RTM_GETQDISC, 0, argc-1, argv+1);
#endifif (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) {//表示操作的是根qdiscif (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) {//表示要操作的是接收方向的qdiscif (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的parentstruct 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,表示需要修改网络设备上的跟qdisc2、如果制定了,表示要修改的qdisc属于某个类该值通常都非0,因为及时要修改跟qdisc,会制定root参数,此时clid就是TC_H_ROOT*/if (clid) {//柑橘parent class id,找到要修改的qdiscif (clid != TC_H_ROOT) {//父类不是根qdiscif (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)) {//要修改的是接收队列qdiscq = 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_fast2、tcm->tcm_handle为0表示要让内核分配句柄并且要作为根qdisc3、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 {//必须指定句柄以表明要修改的qdiscif (!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);//为接收队列新建并关联一个qdiscelseerr = -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;elsedev_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替换老的qdiscerr = 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_MODULESif (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;}}}
#endiferr = -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);elseroot_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设置为设备队列的根qdiscunsigned 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);} elseerr = -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制定为队列新的qdiscif (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关联到根qdiscq = 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;} elsetp = NULL;break;}}root_lock = qdisc_root_sleeping_lock(q);//同一个filter链表下,相同优先级且协议号相同的filter内容会被组织到同一个tcf_protoif (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_MODULESstruct 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;}}
#endifkfree(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回调获取filterfh = 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);//删除filtertfilter_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;
}