HTB相关TC命令以及内核实现介绍
前言... 3
关于此文档... 3
参考资料... 3
第一章 HTB 介绍... 4
1.0 HTB 命令介绍... 5
2.0 Rate ceiling 速率限度... 12
第二章 HTB 程序实现... 13
2.0 用户传递消息的格式... 14
2.1 HTB 命令的解析... 16
举例消息解析过程... 17
2.2 HTB 内核程序的实现... 18
2.2.1 数据包进队... 19
2.2.2 数据包的出队... 21
本文档是本人学习LINUX 流量控制的过程中的学习总结。
主要讲述了HTB的原理以及内核的实现部分,不过重点讲的是原理部分,以及如何根据需要给数据流分类,控制不同数据流的速度。
网络资源。
HTB相关TC命令以及内核实现介绍
在介绍HTB 前我们看看在TC下的速度换算:
tc 采用如下规定来描述带宽:
mbps = 1024 kbps = 1024 * 1024 bps => byte/s
mbit = 1024 kbit => kilo bit/s.
mb = 1024 kb = 1024 * 1024 b => byte
mbit = 1024 kbit => kilo bit.
内定:数字以bps和b方式储存。
但当tc输出速率时,使用如下表示:
1Mbit = 1024 Kbit = 1024 * 1024 bps => byte/s
HTB 意味着是一个更好理解更容易掌握的可以快速替换LINUX CBQ 队列规定的队列, CBQ和HTB都可以帮助你限制你的链路上的出口带宽,但是CBQ配置很复杂而且精度又不够,在HTB问世后,HTB就逐渐的代替CBQ,成为人们进行流量控制的工具。 他允许你把一条物理链路模拟成几条更慢的链路,或者是把发出的不同类型的流量模拟成不同的连接,在他们的实际应用中, 你必须指定怎么分配物理链路给各种不同的带宽应用并且如何判断每种不同的应用的数据包是怎么样被发送的。应用HTB我们可以很好的规划我们的带宽,根据不同的需要为网络中的主机或者本机上的不同业务分配不同的带宽。
案例: 我们有两不同的用户A和B, 都通过网卡 eth0 连接到 internet ,我们想分配 60kbps的带宽给A 和 40 kbps的带宽给B。
HTB 可以保障提供给每个类带宽的数量是它所需求的最小需求或者等于分配给它的数量.当一个类需要的带宽少于分配的带宽时,剩余的带宽被分配给其他需要服务的类.
注: 这里这种情况被称为”借用”剩余带宽, 我们以后将用这个术语, 但无论如何,好像很不好因为这个”借用”是没有义务偿还的.
为了此目的,我们利用HTB来为我们服务:服务模型如下:
我们使用的 TC 命令如下:
tc qdisc add dev ifbr0 root handle 1: htb default 12
这个命令建立QDISC 的根类型。默认情况下选择的类为12.
tc class add dev ifbr0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps
这个命令在根的基础上建立一个类,速度为 100Kbps起到总速度限制的作用。
tc class add dev ifbr0 parent 1:1 classid 1:10 htb rate 30kbps ceil100kbps
tc class add dev ifbr0 parent 1:1 classid 1:11 htb rate 10kbps ceil100kbps
tc class add dev ifbr0 parent 1:1 classid 1:12 htb rate 60kbps ceil100kbps
这两命令是进行分流作用,在前面建立的底下建立三个类,速度控制分别为 30kbps ,10kbps和60,并且都设置 ceil 为100kbps 至于ceil我们稍后讨论。接下来我们使用分类器进行设置使不同的数据包分发到不同的类进行发送数据。
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip src 192.168.1.164
match ip dport 80 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip src 192.168.1.164
match ip dport 90 0xffff
flowid 1:11
这样来自ip 192.168.1.164 且目的端口号为80的数据包都会发送到类1:10 所对应的队列中,自ip 192.168.1.164 且目的端口号为90的数据包都会发送到类1:11 所对应的队列中,其他不匹配的数据包都会发送到类1:12所对应的队列中,这是默认的队列,前面已经定义。
当然这只是简单的配置,我们也可以根据自己的需要配置成更为复杂的队列规定,接下来我们举如下的例子:
假如内网中的用户通过网卡eth0与外界通讯,这样我们可能需要配置我们的通讯规则。
我们可能有如下的需求:
我们希望制定以上的控制规则,以便保证不同用户的不同需求。
当然带宽比例是相对的,也就是说在忙的时候会趋向于以上的带宽比例。不忙的时候忙的服务可以借用不忙用户的带宽。
假如我们总带宽为 2048Mbps。这样我们分配给A,B的带宽分别为1228Mbps,820Mbps。带宽分配如下所示:
好了按照以上的模型我们开始建立规则。
首先我们需要为网卡建立一个根类型(QDISC)这个是必须的步骤。
接下来我们在根类型下建立子类,控制总速度如图示。
按照上图的步骤我们应该在这个类的底下建立两个子类,来进行速度的分发。
我们继续根据上图建立如下子类:
CLASS 1:2的子类:
CLASS 1:3的子类:
接下来我们为叶子节点挂载pfifo的队列规定,当然你也可以根据自己的需要定义自己的队列规定。命令如下:
这样我们根据需要框架是已经搭建完成了,不过我们还不知道哪些包进入哪个类进行处理。接下来我们讲根据需求建立分类器。当然我们使用的是功能强大的U32分类器,
首先我们为A网络分配规则:
接下来我们为B 网络匹配规则:
这样我们就大功告成了。
我们看到了上面配置过程中一直有这个参数,我们接下来讨论下这个参数的用途。
参数ceil指定了一个类可以用的最大带宽, 用来限制类可以借用多少带宽.缺省的ceil是和速率一样。我们看上面的例子:
我们在分配A和B的速度的时候我们设置了cell 都为2048。 也就是说虽然A与B的rate值不一样但是他们峰值的速度是一样的。假如在某段时间内A用户网络流量很少或者几乎是没有的,但是这时候B用户的流量很大,这样B用户将会借用A用户的网络流量,当然B用户的总速度是不会超过cell 定义的速度的。
注: ceil的数值应该至少和它所在的类的速率一样高, 也就是说ceil应该至少和它的任何一个子类一样高。
对于HTB的内核实现包括两部分
第一部分为:用户空间命令的解析以及传递消息到内核空间。
第二部分为HTB流量控制算法的实现。
我们对消息的封装使用如下的格式:首先是消息头然后接下来就是消息的数据部分了。数据部分是按照 类型、 长度、 参数值 的格式来填充数据包的。如下所示:
消息头 |
消息模板TC_MSG |
传递的数据 |
其中消息头有如下的参数
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
系统中为支持QOS 定义了如下的消息类型:在文件RTNETLINK.H中。
RTM_NEWQDISC = 36,
#define RTM_NEWQDISC RTM_NEWQDISC
RTM_DELQDISC,
#define RTM_DELQDISC RTM_DELQDISC
RTM_GETQDISC,
#define RTM_GETQDISC RTM_GETQDISC
RTM_NEWTCLASS = 40,
#define RTM_NEWTCLASS RTM_NEWTCLASS
RTM_DELTCLASS,
#define RTM_DELTCLASS RTM_DELTCLASS
RTM_GETTCLASS,
#define RTM_GETTCLASS RTM_GETTCLASS
TC_MSG结构如下:
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;
};
数据部分的格式有如下:
struct rtattr
{
unsigned short rta_len;
unsigned short rta_type;
};
nlmsg_len |
|
nlmsg_type |
nlmsg_flags |
nlmsg_seq |
|
nlmsg_pid |
|
消息模板TC_MSG |
|
rta_type 16 bits |
rta_len 16 bits |
Values 32 bits |
其中数据部分的rta_type就是传递的参数类型:我们根据HTB可能的参数类型,定义如下的参数类型:定义在文件PKT_SCHED.H中。(我们这里省略一些定义的结构体可以去看看源文件)
/* HTB section */
#define TC_HTB_NUMPRIO 8
#define TC_HTB_MAXDEPTH 8
#define TC_HTB_PROTOVER 3/* the same as HTB and
enum
{
TCA_HTB_UNSPEC,
TCA_HTB_PARMS,
TCA_HTB_INIT,
TCA_HTB_CTAB,
TCA_HTB_RTAB,
__TCA_HTB_MAX,
};
由于TC 关于QOS 方面的命令基本格式如下:
Usage: tc [ OPTIONS ]OBJECT { COMMAND | help }\n"
"tc [-force] -batch file\n"
"where OBJECT := { qdisc | class | filter | action}\n"
" OPTIONS := {-s[tatistics] | -d[etails] | -r[aw]
可以看出TC 命令中OBJECT := { qdisc | class | filter | action }
也就是 用于建立QDISC以及建立分类CLASS, 建立过滤器FILTER或者是其他的动作ACTION。LINUX 内核中为这不同的命令分别建立了相应的命令解析结构,大致如下:
struct qdisc_util
{
struct qdisc_util *next;
constchar *id;
int (*parse_qopt)(struct qdisc_util *qu, int argc,char **argv, struct nlmsghdr *n);
int (*print_qopt)(struct qdisc_util *qu, FILE *f,struct rtattr *opt);
int (*print_xstats)(struct qdisc_util *qu,FILE *f, struct rtattr *xstats);
int (*parse_copt)(struct qdisc_util *qu, int argc,char **argv, struct nlmsghdr *n);
int (*print_copt)(structqdisc_util *qu, FILE *f, struct rtattr *opt);
};
我们只列出qdisc_util部分其他的 如FILTER以及ACTION 的解析结构请参照tc_util.c这个文件。可以看出qdisc_util 这个结构是个链表。对于不同的策略我们都可以编译然后插入到这个链表当中。当然其中的函数指针根据不同的解析类型指向不同的函数。比如我们讲的HTB:这个文件在q_htb.c中
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,
};
htb_parse_opt,用于解析qdisc htb参数部分的命令的。
htb_parse_class_opt,用于解析class htb参数部分的命令的。
一个命令:tc qdisc add dev ifbr0 roothandle 1: htb default 12
这个命令为某个网络接口eth0增加一个qdisc。
命令首先在用户空间被iproute2分析:
分析tc:main(int argc, char **argv)被调用,此函数在tc/tc.c中;
分析tc qdisc:do_qdisc(argc-2, argv+2);被调用,此函数在tc/tc_qdisc.c中;
分析tc qdisc add: tc_qdisc_modify(RTM_NEWQDISC,NLM_F_EXCL|NLM_F_CREATE, argc-1, argv+1); 被调用,此函数在tc/tc_qdisc.c中,在这个函数中,将分析完这一行tc的命令,
分析 ……. htb default 12:前面分析完后,接下来就开始分析后面的部分了,程序发现是用htb,所以就根据这个标记找到htb_qdisc_util这个结构,然后用其中的函数htb_parse_opt对其后面的参数进行解析,并且根据上面提到的格式填充数据包。最后返回tc_qdisc_modify 中,tc_qdisc_modify这个函数最终完成发送消息到内核空间,用于控制相关的操作。其他的命令基本上也是一样的。只是不同的命令走的分支不一样。
HTB关键是对不同数据包进行不同的流量控制,以达到控制的目的。对于流量的控制使用的是令牌桶的算法如下图:
对于一个数据包在内核中总的操作是入队和出队,入队只是根据分类把数据包放入不同的队列中,如果队列满了就要根据策略丢弃数据包。出队的过程中就是根据策略从队列中取出数据包,当然HTB和TBF一样速度的限制是在出队的时候才进行。
我们首先来看看进队的操作:
static inthtb_enqueue(struct sk_buff *skb, struct Qdisc *sch)
{
struct htb_class *cl =htb_classify(skb,sch,&ret);
else if (cl->un.leaf.q->enqueue(skb,cl->un.leaf.q) != NET_XMIT_SUCCESS) {
sch->stats.drops++;
cl->stats.drops++;
return NET_XMIT_DROP;
} else {
cl->stats.packets++; cl->stats.bytes += skb->len;
htb_activate (q,cl);
}
return NET_XMIT_SUCCESS;
}
进队首先调用的是分类器,查看数据包要放到那个队列中,然后再进行进队操作。至于分类过程我们看看下图:
当然这些结果是用户传递进来的信息,我们只是进行匹配然后找到分类结果
Tcf_result。当然分类结果有时候我们只保存了分类的CLASSID 但是我们有时候也保存了CLASS 根据不同的策略有时候是不同的:我们也看看另外一种的分类吧:
这种分类结果比较特殊,直接就有一个指针指向所定义的类,程序就不需要再根据ID来查找相应的类了。直接根据其结果的指针进行必要的操作就可以了。
出队操作比较复杂整个过程掉的函数如下:
HTB的出队是个非常复杂的处理过程, 函数调用过程为:
htb_dequeue
è __skb_dequeue
è ->htb_do_events
è ->htb_safe_rb_erase
è ->htb_change_class_mode
è ->htb_add_to_wait_tree
è ->htb_dequeue_tree
è ->htb_lookup_leaf
è ->htb_deactivate
è ->q->dequeue
è ->htb_next_rb_node
è ->htb_charge_class
è ->htb_change_class_mode
è ->htb_safe_rb_erase
è ->htb_add_to_wait_tree
è-> htb_delay_by
对于流量的控制总的思想如下:
首先从队列中取出数据包,然后根据数据包的长度算出要消耗的令牌数函数如下:
/* TODO: maybe compute rate when sizeis too large .. or drop ? */
// 将长度转换为令牌数
static inline long L2T(struct htb_class*cl, struct qdisc_rate_table *rate,
int size)
{
// 根据大小计算合适的槽位
int slot = size >> rate->rate.cell_log;
// 如果超过了255, 限制为255
if (slot > 255) {
cl->xstats.giants++;
slot = 255;
}
return rate->data[slot];
}
计算从上次发送数据包到现在的时间里生成的令牌数
toks = PSCHED_TDIFF_SAFE(now,q->t_c, q->buffer);
然后把toks加上原先桶中的令牌数就为总的令牌数,当然总的令牌数是不能比桶大的。
这时候总令牌数-数据包长度转换过来的令牌数, 如果大于0则允许数据包通过,如过小于0则看看能够借用其他类的速度,否则就让数据包重新入队。这就是限速的基本思想。