Tc用于Linux内核的流量控制,流量控制包括以下几种方式:
SHAPING(限制)
当流量被限制,它的传输速率就被控制在某个值以下。限制值可以大大小于有效带宽,这样可以平滑突发数据流量,使网络更为稳定。shaping(限制)只适用于向外的流量。
SCHEDULING(调度)
通过调度数据包的传输,可以在带宽范围内,按照优先级分配带宽。SCHEDULING(调度)也只适于向外的流量。
POLICING(策略)
SHAPING用于处理向外的流量,而POLICIING(策略)用于处理接收到的数据。
DROPPING(丢弃)
如果流量超过某个设定的带宽,就丢弃数据包,不管是向内还是向外。
所谓的递归控制就是分层次地控制,而对于每个层次,控制方式都是一致的。流量的处理由三种对象控制,它们是:qdisc(排队规则)、class(类别)和filter(过滤器),按照Qdisc –class –filter 的树型组织模式.
qdisc 队列规则(queueing discipline):
用来实现控制网络的收发速度.通过队列,linux可以将网络数据包缓存起来,然后根据用户的设置,在尽量不中断连接(如 tcp)的前提下来平滑网络流量.需要注意的是,linux 对接收队列的控制不够好,所以我们一般只用发送队列,即"控发不控收".它封装了其他两个主要 tc 组件(类和分类器).内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的 qdisc 队列规则把数据包加入队列.然后,内核会尽可能多地从 qdisc里面取出数据包,把它们交给网络适配器驱动模块。
最简单的 QDisc 是 pfifo 它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列.不过,它会保存网络接口一时无法处理的数据包.常有的队列规则包括 FIFO 先进先出,RED 随机早期探测,SFQ 随机公平队列和令牌桶 Token Bucket,类基队列 CBQ,CBQ 是一种超级队列,即它能够包含其它队列,甚至其它CBQ。
Class 类
class 用来表示控制策略.很显然,很多时候,我们很可能要对不同的IP实行不同的流量控制策略,这时候我们就得用不同的class来表示不同的控制策略了。
Filter 规则
filter 用来将用户划入到具体的控制策略中
目前,tc可以使用的过滤器有:fwmark分类器,u32 分类器,基于路由的分类器和 RSVP 分类器(分别用于IPV6、IPV4)等;其中,fwmark 分类器允许我们使用 Linux netfilter 代码选择流量,而 u32 分类器允许我们选择基于 ANY 头的流量 .需要注意的是,filter (过滤器)是在QDisc 内部,它们不能作为主体。
操作原理:
类(Class)组成一个树,每个类都只有一个父类,而一个类可以有多个子类。某些QDisc(例如:CBQ和HTB)允许在运行时动态添加类,而其它的QDisc(例如:PRIO)不允许动态建立类。
允许动态添加类的QDisc可以有零个或者多个子类,由它们为数据包排队。
此外,每个类都有一个叶子QDisc,默认情况下,这个叶子QDisc使用pfifo的方式排队,我们也可以使用其它类型的QDisc代替这个默认的QDisc。而且,这个叶子叶子QDisc有可以分类,不过每个子类只能有一个叶子QDisc。
当一个数据包进入一个分类QDisc,它会被归入某个子类。我们可以使用以下三种方式为数据包归类,不过不是所有的QDisc都能够使用这三种方式。
tc过滤器(tc filter)
如果过滤器附属于一个类,相关的指令就会对它们进行查询。过滤器能够匹配数据包头所有的域,也可以匹配由ipchains或者iptables做的标记。
服务类型(Type of Service)
某些QDisc有基于服务类型(Type of Service,ToS)的内置的规则为数据包分类。
skb->priority
用户空间的应用程序可以使用SO_PRIORITY选项在skb->priority域设置一个类的ID。
树的每个节点都可以有自己的过滤器,但是高层的过滤器也可以直接用于其子类。
如果数据包没有被成功归类,就会被排到这个类的叶子QDisc的队中。相关细节在各个QDisc的手册页中。
命名规则
所有的QDisc、类和过滤器都有ID。ID可以手工设置,也可以有内核自动分配。
ID由一个主序列号和一个从序列号组成,两个数字用一个冒号分开。
QDISC
一个QDisc会被分配一个主序列号,叫做句柄(handle),然后把从序列号作为类的命名空间。句柄采用象10:一样的表达方式。习惯上,需要为有子类的QDisc显式地分配一个句柄。
类(CLASS)
在同一个QDisc里面的类分享这个QDisc的主序列号,但是每个类都有自己的从序列号,叫做类识别符(classid)。类识别符只与父QDisc有关,和父类无关。类的命名习惯和QDisc的相同。
过滤器(FILTER)
过滤器的ID有三部分,只有在对过滤器进行散列组织才会用到。
为了支持QoS,Linux的设计者在发送数据包的代码中加入了TC模块。从而可以对数据包进行分类,管理,检测拥塞和处理拥塞。为了避免和以前的代码冲突,并且让用户可以选择是否使用TC。内核开发者在上图中的两个红色圆圈之间添加了TC模块。(实际上在TC模块中,发送数据包也实现对AF_PACKET协议的支持,本文为了描述方便,把两个地方的AF_PACKET协议处理分开来了)。
下面从具体的代码中分析一下对TC模块的支持。
net/core/dev.c: dev_queue_xmit函数中略了部分代码:
int dev_queue_xmit(struct sk_buff *skb)
{
……………….
q = dev->qdisc;
if (q->enqueue) {
/*如果这个设备启动了TC,那么把数据包压入队列*/
int ret = q->enqueue(skb, q);
/*启动这个设备发送*/
qdisc_run(dev);
return;
}
发送数据包的流程应该是这样的:
(1) 上层协议开始发送数据包
(2) 获得当前设备所采用的策略对象
(3) 调用此对象的enqueue方法把数据包压入队列
(4) 调用此对象的dequeue方法从队列中取出数据包
(5) 调用网卡驱动的发送函数发送
int register_netdevice(struct net_device *dev)
{
………………….
dev_init_scheduler(dev);
………………….
}
void dev_init_scheduler(struct net_device *dev)
{
………….
/*安装设备的qdisc为noop_qdisc*/
dev->qdisc = &noop_qdisc;
………….
dev->qdisc_sleeping = &noop_qdisc;
dev_watchdog_init(dev);
}
/ 此时,网卡设备刚注册,还没有UP,采用的是noop_qdisc /
struct Qdisc noop_qdisc =
{
noop_enqueue,
noop_dequeue,
TCQ_F_BUILTIN,
&noop_qdisc_ops,
};
noop_qdisc采用的数据包处理方法是noop_qdisc_ops,
struct Qdisc_ops noop_qdisc_ops =
{
NULL,
NULL,
"noop",
0,
noop_enqueue,
noop_dequeue,
noop_requeue,
};
从noop_enqueue,noop_dequeue,noop_requeue函数的定义可以看出,他们并没有对数据包进行任何的分类或者排队,而是直接释放掉skb。所以此时网卡设备还不能发送任何数据包。必须ifconfig up起来之后才能发送数据包。
调用ifconfig up来启动网卡设备会走到dev_open函数。
int dev_open(struct net_device *dev)
{
…………….
dev_activate(dev);
……………..
}
void dev_activate(struct net_device *dev)
{
…………. if (dev->qdisc_sleeping == &noop_qdisc) {
qdisc = qdisc_create_dflt(dev, &pfifo_fast_ops);
/*安装缺省的qdisc*/
}
……………
if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc) {
……………./.安装特定的qdisc/
}
……………..
}
设备启动之后,此时当前设备缺省的Qdisc->ops是pfifo_fast_ops。如果需要采用不同的ops,那么就需要为设备安装其他的Qdisc。本质上是替换掉dev->Qdisc指针。见sched/sch_api.c 的dev_graft_qdisc函数。
static struct Qdisc *
dev_graft_qdisc(struct net_device dev, struct Qdisc qdisc)
{
……………
oqdisc = dev->qdisc_sleeping;
/ 首先删除掉旧的qdisc /
if (oqdisc && atomic_read(&oqdisc->refcnt) <= 1)
qdisc_reset(oqdisc);
/*安装新的qdisc */
if (qdisc == NULL)
qdisc = &noop_qdisc;
dev->qdisc_sleeping = qdisc;
dev->qdisc = &noop_qdisc;
/*启动新安装的qdisc*/
if (dev->flags & IFF_UP)
dev_activate(dev);
…………………
}
从dev_graft_qdisc可以看出,如果需要使用新的Qdisc,那么首先需要删除旧的,然后安装新的,使dev->qdisc_sleeping 为新的qdisc,然后调用dev_activate函数来启动新的qdisc。结合dev_activate函数中的语句:
if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc)
可以看出,此时的dev->qdisc所指的就是新的qdisc。(注意,上面语句中左边是一个赋值语句。)
在网卡down掉的时候,通过调用dev_close -> dev_deactivate重新使设备的qdisc为noop_qdisc,停止发送数据包。
Linux中的所有的QoS策略最终都是通过上面这个方法来安装的。在sch_api.c中,对dev_graft_qdisc函数又封装了一层函数(register_qdisc),供模块来安装新的Qdisc。如RED(早期随即检测队列)模块,就调用register_qdisc来安装RED对象(net/sched/sch_red.c->init_module())。
首先需要了解的是,TC作为一个应用工具,它又是如何与内核去实现通讯的?很简单,消息机制,所借助的工具则是Netlink,而所使用的协议正是NETLINK_ROUTE。
在此可以说明下TC源代码中是如何初始化rtnetlink(可以理解为专门为路由设计的netlink)socket的。
struct rtnl_handle
{
int fd;
struct sockaddr_nl local;
struct sockaddr_nl peer;
__u32 seq;
__u32 dump;
};
struct rtnl_handle *rth
rth->fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
...
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = 0;
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local);
下面主要以TC工具对qdisc操作(包括增加,修改,取代等等)的实现。对qdisc规则解析代码是在tc_qdisc_modify函数中完成的,然后通过消息机制交给内核相关模块去处理。下面是其中一段消息初始化代码片段:
struct {
struct nlmsghdr n;
struct tcmsg t;
char buf[TCA_BUF_MAX];
} req;
struct tcmsg
{
unsigned char tcm_family;
unsigned char tcm__pad1;
unsigned short tcm__pad2;
int tcm_ifindex;
__u32 tcm_handle;
__u32 tcm_parent;
__u32 tcm_info;
};
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
req.n.nlmsg_flags = NLM_F_REQUEST|flags;
req.n.nlmsg_type = RTM_NEWQDISC;
req.t.tcm_family = AF_UNSPEC;
有一点值得注意的是,因为针对各种不同的调度机制,有着不一样的参数选项,如sfq所对应的参数就有quantum, perturb, limit等,而htb则有r2q, default,在TC工具中针对这些不同的调度机制,定义了不一样的解析函数。如sfq和htb中的定义如下:
struct qdisc_util htb_qdisc_util = {
.id = "htb",
.parse_qopt = htb_parse_opt,
.print_qopt = htb_print_opt,
.print_xstats = htb_print_xstats,
.parse_copt = htb_parse_class_opt,
.print_copt = htb_print_opt,
};
struct qdisc_util sfq_qdisc_util = {
.id = "sfq",
.parse_qopt = sfq_parse_opt,
.print_qopt = sfq_print_opt,
};
而在tc_qdisc_modify函数中则是首先get_qdisc_kind去获取对应的调度机制名,然后调用跟此种调度机制对应的解析参数函数去执行,对应代码片段如下:
q = get_qdisc_kind(k);
...
if (q->parse_qopt(q, argc, argv, &req.n))
return 1;
所有的参数均解析完成之后,接下来就是将消息发给内核(接着内核将会处理所收到的消息请求),并及时接受内核的回复消息。
当内核接收到请求消息后,按照消息的什么内容去完成消息的处理呢?消息的类型!前面总结了tc工具在不同的规则下有着对应的消息类型,例如,add, change, replace等操作所对应的消息类型则是RTM_NEWQDISC,因此,内核在收到此种消息类型之后会调用相应的模块去进行处理。这些消息处理模块全部放在了sch_api.c文件中,相关代码如下:
static int __init pktsched_init(void)
{
register_qdisc(&pfifo_qdisc_ops);
register_qdisc(&bfifo_qdisc_ops);
proc_net_fops_create(&init_net, "psched", 0, &psched_fops);
rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL);
rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL);
rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc, tc_dump_qdisc);
rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL);
rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL);
rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass, tc_dump_tclass);
return 0;
}
从上面这段代码可以看出,模块中注册了消息类型以及与处理函数的对应关系。此处以RTM_NEWQDISC消息类型为例,此时需要调用tc_modify_qdisc函数去处理。处理的基本思想是这样的:因为不同的规则可能对应着相同的消息类型(如RTM_NEWQDISC),此时就需要再通过消息的标志量做进一步的操作,最后通过调用内核中有关qdisc的API函数去完成。
从上面的片段中可以看出,根据不同的标志量,调用不同的API函数去完成最后的功能,如qdisc_change用于去修改原qdisc规则,修改完成之后然后调用qdisc_notify去回复响应TC,qdisc_create用于去重新创建一个新的qdisc队列规则,qdisc_graft函数用于去将qdisc移植到某个对象上去。
以上以TC工具对Qdisc操作为例简单地阐述了TC工具是如何与内核进行交互的,以及内核又是如何响应请求并作出处理的,下节将探讨在ATM设备上如何设置Qos。
在Linux中,如果设备启动之后,没有配置特定的QoS策略,内核对每个设备采用缺省的策略,pfifo_fast_ops。下面的pfifo_fast_ops进行详细的分析。
上图中的信息可以对应于pfifo_fast_ops结构体的每个部分:
static struct Qdisc_ops pfifo_fast_ops =
{
NULL,
NULL,
"pfifo_fast", /*ops名称*/
3 * sizeof(struct sk_buff_head), /*数据包skb队列*/
pfifo_fast_enqueue, /*入队列函数*/
pfifo_fast_dequeue, /*出队列函数*/
pfifo_fast_requeue, /*重新压入队列函数*/
NULL,
pfifo_fast_init, /*队列管理初始化函数*/
pfifo_fast_reset, /*队列管理重置函数*/
};
在注册pfifo_fast_ops的时候首先会调用pfifo_fast_init来初始化队列管理,见qdisc_create_dflt函数。
static int pfifo_fast_init(struct Qdisc qdisc, struct rtattr opt),init函数的作用就是初始化3个队列。
{
………
for (i=0; i<3; i++)
skb_queue_head_init(list+i); /*初始化3个优先级队列*/
……….
}
在注销一个Qdisc的时候都会调用Qdisc的ops的reset函数。见dev_graft_qdisc函数。
static void
pfifo_fast_reset(struct Qdisc* qdisc)
{
…………..
for (prio=0; prio < 3; prio++)
skb_queue_purge(list+prio); /*释放3个优先级队列中的所有数据包*/
…………..
}
在数据包发送的时候会调用Qdisc->enqueue函数(在qdisc_create_dflt函数中已经将Qdisc_ops的enqueue,dequeue,requeue函数分别赋值于Qdisc分别对应的函数指针)。
int dev_queue_xmit(struct sk_buff *skb)
{
……………….
q = dev->qdisc;
if (q->enqueue) {
/* 对应于pfifo_fast_enqueue 函数*/
int ret = q->enqueue(skb, q);
/*启动这个设备的发送,这里涉及到两个函数pfifo_fast_dequeue ,pfifo_fast_requeue 稍后介绍*/
qdisc_run(dev);
return;
}
……………
}
入队列函数pfifo_fast_enqueue:
static int
pfifo_fast_enqueue(struct sk_buff skb, struct Qdisc qdisc)
{
…………..
list = ((struct sk_buff_head*)qdisc->data) +
prio2band[skb->priority&TC_PRIO_MAX];
/*首先确定这个数据包的优先级,决定放入的队列*/
if (list->qlen <= skb->dev->tx_queue_len) {
__skb_queue_tail(list, skb); /*将数据包放入队列的尾部*/
qdisc->q.qlen++;
return 0;
}
……………..
}
在数据包放入队列之后,调用qdisc_run来发送数据包。
static inline void qdisc_run(struct net_device *dev)
{
while (!netif_queue_stopped(dev) &&
qdisc_restart(dev)<0)
/* NOTHING */;
}
在qdisc_restart函数中,首先从队列中取出一个数据包(调用函数pfifo_fast_dequeue)。然后调用网卡驱动的发送函数(dev->hard_start_xmit)发送数据包,如果发送失败,则需要将这个数据包重新压入队列(pfifo_fast_requeue),然后启动协议栈的发送软中断进行再次的发送。
static struct sk_buff *
pfifo_fast_dequeue(struct Qdisc* qdisc)
{
…………..
for (prio = 0; prio < 3; prio++, list++) {
skb = __skb_dequeue(list);
if (skb) {
qdisc->q.qlen--;
return skb;
}
}
……………….
}
从dequeue函数中可以看出,pfifo的策略是:从高优先级队列中取出数据包,只有高优先级的队列为空,才会对下一优先级的队列进行处理。
requeue函数重新将数据包压入相应优先级队列的头部。
static int
pfifo_fast_requeue(struct sk_buff skb, struct Qdisc qdisc)
{
struct sk_buff_head *list;
list = ((struct sk_buff_head*)qdisc->data) +
prio2band[skb->priority&TC_PRIO_MAX];
/*确定相应优先级的队列*/
__skb_queue_head(list, skb);/*将数据包压入队列的头部*/
qdisc->q.qlen++;
return 0;
}
tc class [ add | change | replace ] dev DEV parent qdisc-id [ classid class-id ] qdisc [ qdisc specific parameters ]
tc filter [ add | change | replace ] dev DEV [ parent qdisc-id | root ] protocol protocol prio priority filtertype [ filtertype specific parameters ] flowid flow-id
tc [-s | -d ] qdisc show [ dev DEV ]
tc [-s | -d ] class show dev DEV tc filter show dev DEV
tc qdisc del dev eth0 root 2>/dev/null 清除 eth0 所有队列规则
tc可以使用以下命令对QDisc、类和过滤器进行操作:
add
在一个节点里加入一个QDisc、类或者过滤器。添加时,需要传递一个祖先作为参数,传递参数时既可以使用ID也可以直接传递设备的根。如果要建立一个
QDisc或者过滤器,可以使用句柄(handle)来命名;如果要建立一个类,可以使用类识别符(classid)来命名。
remove
删除有某个句柄(handle)指定的QDisc,根QDisc(root)也可以删除。被删除QDisc上的所有子类以及附属于各个类的过滤器都会被自动删除。
change
以替代的方式修改某些条目。除了句柄(handle)和祖先不能修改以外,change命令的语法和add命令相同。换句话说,change命令不能一定节点的位置。
replace
对一个现有节点进行近于原子操作的删除/添加。如果节点不存在,这个命令就会建立节点。
link
只适用于DQisc,替代一个现有的节点。
CLASSLESS QDisc(不可分类QDisc)
无类别QDISC包括:
[p|b]fifo
使用最简单的qdisc,纯粹的先进先出。只有一个参数:limit,用来设置队列的长度,pfifo是以数据包的个数为单位;bfifo是以字节数为单位。
pfifo_fast
在编译内核时,如果打开了高级路由器(Advanced outer)编译选项,pfifo_fast就是系统的标准QDISC。它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。而三个波段(band)的优先级也不相同,band 0的优先级最高,band 2的最低。如果band里面有数据包,系统就不会处理band1里面的数据包,band 1和band 2之间也是一样。数据包是按照服务类型(Type ofService,TOS)被分配多三个波段(band)里面的。
red
red是Random Early Detection(随机早期探测)的简写。如果使用这种QDISC,当带宽的占用接近于规定的带宽时,系统会随机地丢弃一些数据包。它非常适合高带宽应用。
sfq
sfq是Stochastic Fairness Queueing的简写。它按照会话(session--对应于每个TCP连接或者UDP流)为流量进行排序,然后循环发送每个会话的数据包。
tbf
tbf是Token Bucket Filter的简写,适合于把流速降低到某个值。
如果没有可分类QDisc,不可分类QDisc只能附属于设备的根。它们的用法如下:
tc qdisc add dev DEV root QDISC QDISC-PARAMETERS
要删除一个不可分类QDisc,需要使用如下命令:
tc qdisc del dev DEV root
一个网络接口上如果没有设置QDisc,pfifo_fast就作为缺省的QDisc。
CLASSFUL QDISC(分类QDisc)
可分类的QDisc包括:
CBQ
CBQ 是Class Based
Queueing(基于类别排队)的缩写。它实现了一个丰富的连接共享类别结构,既有限制(shaping)带宽的能力,也具有带宽优先级管理的能力。带
宽限制是通过计算连接的空闲时间完成的。空闲时间的计算标准是数据包离队事件的频率和下层连接(数据链路层)的带宽。
HTB
HTB是 Hierarchy Token
Bucket的缩写。通过在实践基础上的改进,它实现了一个丰富的连接共享类别体系。使用HTB可以很容易地保证每个类别的带宽,虽然它也允许特定的类可
以突破带宽上限,占用别的类的带宽。HTB可以通过TBF(Token Bucket Filter)实现带宽限制,也能够划分类别的优先级。
PRIO
PRIO QDisc不能限制带宽,因为属于不同类别的数据包是顺序离队的。使用PRIO
QDisc可以很容易对流量进行优先级管理,只有属于高优先级类别的数据包全部发送完毕,才会发送属于低优先级类别的数据包。为了方便管理,需要使用
iptables或者ipchains处理数据包的服务类型(Type Of Service,ToS)。
6 应用举例
在Linux中,流量控制都是通过TC这个工具来完成的。通常,要对网卡进行流量控制的配置,需要进行如下的步骤:
◆ 为网卡配置一个队列;
◆ 在该队列上建立分类;
◆ 根据需要建立子队列和子分类;
◆ 为每个分类建立过滤器。
在Linux中,可以配置很多类型的队列,比如CBQ、HTB等,其中CBQ 比较复杂,不容易理解。HTB(Hierarchical Token Bucket)是一个可分类的队列, 与其他复杂的队列类型相比,HTB具有功能强大、配置简单及容易上手等优点。在TC中,使用"major:minor"这样的句柄来标识队列和类别,其中major和minor都是数字。
对于队列来说,minor总是为0,即"major:0"这样的形式,也可以简写为"major: "比如,队列1:0可以简写为1:。需要注意的是,major在一个网卡的所有队列中必须是惟一的。对于类别来说,其major必须和它的父类别或父队列的major相同,而minor在一个队列内部则必须是惟一的(因为类别肯定是包含在某个队列中的)。举个例子,如果队列2:包含两个类别,则这两个类别的句柄必须是2:x这样的形式,并且它们的x不能相同,比如2:1和2:2。
简单环境举例:
以HTB队列为主,结合需求来讲述TC的使用。假设eth0出口有100mbit/s的带宽,分配给WWW、E-mail和Telnet三种数据流量,其中分配给WWW的带宽为40Mbit/s,分配给Email的带宽为40Mbit/s,分配给Telnet的带宽为20Mbit/S。
需要注意的是,在TC 中使用下列的缩写表示相应的带宽:
◆ Kbps : kilobytes per second,千字节每秒 ;
◆ Mbps : megabytes per second,兆字节每秒 ,
◆ Kbit : kilobits per second,千比特每秒 ;
◆ Mbit : megabits per second, 兆比特每秒 。
创建HTB队列
有关队列的TC命令的一般形式为:
首先,需要为网卡eth0配置一个HTB队列,使用下列命令:
#tc qdisc add dev eth0 root handle 1:htb default 11
这里,命令中的”add”表示要添加,”dev eth0”表示要操作的网卡为eth0。”root”表示为网卡eth0添加的是一个根队列。”handle 1:”表示队列的句柄为1: 。”htb”表示要添加的队列为HTB队列。命令最后的”default 11”是htb特有的队列参数,意思是所有未分类的流量都将分配给类别1:11。
为根队列创建相应的类别
有关类别的TC 命令的一般形式为:
可以利用下面这三个命令为根队列1创建三个类别,分别是1:1 1、1:12和1:13,它们分别占用40、40和20mb[t的带宽。
#tc class add dev eth0 parent 1: classid 1:1 htb rate 40mbit ceil 40mbit
#tc class add dev eth0 parent 1: classid 1:12 htb rate 40mbit ceil 40mbit
#tc class add dev eth0 parent 1: cllassid 1:13 htb rate 20mbit ceil 20mbit
命令中,”parent 1:”表示类别的父亲为根队列1: 。”classid1:11”表示创建一个标识为1:11的类别,”rate 40mbit”表示系统将为该类别确保带宽40mbit,”ceil 40mbit”,表示该类别的最高可占用带宽为40mbit。
为各个类别设置过滤器
有关过滤器的TC 命令的一般形式为:
由于需要将WWW、E-mail、Telnet三种流量分配到三个类别,即上述1:11、1:12和1:13,因此,需要创建三个过滤器,如下面的三个命令:
#tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:11
#tc filter add dev eth0 prtocol ip parent 1:0 prio 1 u32 match ip dport 25 0xffff flowid 1:12
#tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 23 oxffff flowid 1:13
这里,”protocol ip”表示该过滤器应该检查报文分组的协议字段。”prio 1” 表示它们对报文处理的优先级是相同的,对于不同优先级的过滤器,系统将按照从小到大的优先级顺序来执行过滤器,对于相同的优先级,系统将按照命令的先后顺序执行。这几个过滤器还用到了u32选择器(命令中u32后面的部分)来匹配不同的数据流。以第一个命令为例,判断的是dport字段,如果该字段与Oxffff进行与操作的结果是8O,则”flowid 1:11”表示将把该数据流分配给类别1:1 1。更加详细的有关TC的用法可以参考TC的手册页。
复杂环境举例
在上面的例子中, 三种数据流(www、Email、Telnet)之间是互相排斥的。当某个数据流的流量没有达到配额时,其剩余的带宽并不能被其他两个数据流所借用。在这里将涉及如何使不同的数据流可以共享一定的带宽。
首先需要用到HTB的一个特性, 即对于一个类别中的所有子类别,它们将共享该父类别所拥有的带宽,同时,又可以使得各个子类别申请的各自带宽得到保证。这也就是说,当某个数据流的实际使用带宽没有达到其配额时,其剩余的带宽可以借给其他的数据流。而在借出的过程中,如果本数据流的数据量增大,则借出的带宽部分将收回,以保证本数据流的带宽配额。
下面考虑这样的需求,同样是三个数据流WWW、E-mail和Telnet, 其中的Telnet独立分配20Mbit/s的带宽。另一方面,WWW 和SMTP各自分配40Mbit/s的带宽。同时,它们又是共享的关系,即它们可以互相借用带宽。如图3所示。
需要的TC命令如下:
#tc qdisc add dev eth0 root handle 1: htb default 21
#tc class add dev eth0 partent 1: classid 1:1 htb rate 20mbit ceil 20mbit
#tc class add dev eth0 parent 1: classid 1:2 htb rate 80mbit ceil 80mbit
#tc class add dev eth0 parent 1: classid 1:21 htb rate 40mbit ceil 20mbit
#tc class add dev eth0 parent 1:2 classid 1:22 htb rate 40mbit ceil 80mbit
#tc filter add dev eth0 protocol parent 10 prio 1 u32 match ip dport 80 0xffff flowid 1:21
#tc filter add dev eth0 protocol parent 1:0 prio 1 u32 match ip dport 25 0xffff flowid 1:22
#tc filter add dev eth0 protocol parent 1:0 prio 1 u32 match ip dport 23 0xffff flowid 1:1
这里为根队列1创建两个根类别,即1:1和1:2,其中1:1对应Telnet数据流,1:2对应80Mbit的数据流。然后,在1:2中,创建两个子类别1:21和1:22,分别对应WWW和E-mail数据流。由于类别1:21和1:22是类别1:2的子类别,因此他们可以共享分配的80Mbit带宽。同时,又确保当需要时,自己的带宽至少有40Mbit。