HTB分层令牌桶排队规则分析

之前通过《默认FIFO_FAST出口排队规则分析》、《ingress入口排队规则分析》分析,已经对排队规则的基础架框有了简单的了解。那两种排队规则都是无类的,这里选出可以分类的HTB排队规则进行分析。



当前实例分析的基本对象关联图

一、当前分析的配置范例
//在eth0设备上创建一个根HTB排队规则,当未匹配任何过滤器时,将报文放入ID为20
//的分类中
tc qdisc add dev eth0 root handle 1: htb default 20

//在根HTB排队规则上创建ID为1的分类
tc class add dev eth0 parent 1: classid 1:1 htb rate 6mbit burst 15k

//在ID为1的分类上分别创建两个ID为10、ID为20的分类
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit burst 15k
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 3mbit ceil 6mbit burst 15k

//创建两个U32分类器
U32="tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32"
$U32 match ip dport 80 0xffff flowid 1:10
$U32 match ip sport 25 0xffff flowid 1:20

二、创建根HTB排队规则
1、用户层分析
//初始化,获取每纳秒对应多少TICKET
tc_core_init();
fp = fopen("/proc/net/psched", "r");
fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clock_res);
fclose(fp);

if (clock_res == 1000000000)
t2us = us2t;

clock_factor  = (double)clock_res / TIME_UNITS_PER_SEC;
tick_in_usec = (double)t2us / us2t * clock_factor;

//创建一个ROUTE类型的netlink套接口
rtnl_open(&rth, 0)
rtnl_open_byproto(rth, subscriptions, NETLINK_ROUTE);
rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
setsockopt(rth->fd,SOL_SOCKET,SO_SNDBUF,&sndbuf,sizeof(sndbuf))
setsockopt(rth->fd,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof(rcvbuf))
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = subscriptions;	//0
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local))
rth->seq = time(NULL);

do_cmd(argc-1, argv+1);
if (matches(*argv, "qdisc") == 0)
//执行设置排队规则的命令
do_qdisc(argc-1, argv+1);
if (matches(*argv, "add") == 0)
tc_qdisc_modify(RTM_NEWQDISC, NLM_F_EXCL|NLM_F_CREATE, 
argc-1, argv+1);
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
req.n.nlmsg_flags = NLM_F_REQUEST|flags;
req.n.nlmsg_type = cmd;	//RTM_NEWQDISC
req.t.tcm_family = AF_UNSPEC;

while (argc > 0)
if (strcmp(*argv, "dev") == 0)
NEXT_ARG();
strncpy(d, *argv, sizeof(d)-1);		//eth0
else if (strcmp(*argv, "root") == 0)
req.t.tcm_parent = TC_H_ROOT;
else if (strcmp(*argv, "handle") == 0)
NEXT_ARG();
get_qdisc_handle(&handle, *argv)
req.t.tcm_handle = handle;	//0x00010000
else
//如果有/usr/lib/tc/htb.so动态库中则从中获
//取htb_qdisc_util符号结构,否则检测当前tc
//程序是否有htb_qdisc_util符号结构则从中获取
//,否则返回q 为空。
q = get_qdisc_kind(k);	

//在消息尾部追加KIND属性
//rta->rta_type = type; 	//TCA_KIND
//rta->rta_len = len;		
//属性值为 “htb”
addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);

//当前q为htb_qdisc_util,使用其中parse_qopt回调进行其它参数
//解析,当前回调函数为htb_parse_opt
q->parse_qopt(q, argc, argv, &req.n)
htb_parse_opt
//默认参数
opt.rate2quantum = 10;
opt.version = 3;

while (argc > 0)
if (matches(*argv, "default") == 0)
NEXT_ARG();
get_u32(&opt.defcls, *argv, 16)	//20

//添加扩展属性OPTIONS,标记后面都是htb的选项
addattr_l(n, 1024, TCA_OPTIONS, NULL, 0);

//添加扩展属性HTB_INIT
addattr_l(n, 2024, TCA_HTB_INIT, &opt, 
NLMSG_ALIGN(sizeof(opt)));

//根据接口名获取接口索引
if (d[0])
idx = ll_name_to_index(d)
req.t.tcm_ifindex = idx;

//给内核发送该netlink消息
rtnl_talk(&rth, &req.n, 0, 0, NULL)

rtnl_close(&rth);

2、内核层分析
用户侧发出RTM_NEWQDISC套接口消息后,在内核侧对应的处理回调函数为tc_modify_qdisc,该函数是在pktsched_init中初始化的。

tc_modify_qdisc
tcm = NLMSG_DATA(n);
clid = tcm->tcm_parent;	//当前用户侧传入值为  TC_H_ROOT

//根据设备索引获取设备对象,上面用户侧传入设备名为eth0
dev = __dev_get_by_index(tcm->tcm_ifindex)

if (clid)
if (clid != TC_H_ROOT)
//当前为根排队规则,不走此流程
else
q = dev->qdisc_sleeping;

//当前设备存储的是默认的排队规则,则忽略
if (q && q->handle == 0)
q = NULL;

if (!q || !tcm->tcm_handle || q->handle != tcm->tcm_handle)
if (tcm->tcm_handle)	//用户侧传入为特定的0x1
//当前设备的qdisc_list排队规则链表中不含有此规则,进行创建
if ((q = qdisc_lookup(dev, tcm->tcm_handle)) == NULL)
goto create_n_graft;

create_n_graft:

//创建排队规则
q = qdisc_create(dev, tcm->tcm_handle, tca, &err);
//从已经注册到qdisc_base链表中获取匹配排队规则,当前htb已经注册
//,则ops = htb_qdisc_ops
ops = qdisc_lookup_ops(kind);

sch = qdisc_alloc(dev, ops);
INIT_LIST_HEAD(&sch->list);
skb_queue_head_init(&sch->q);	//初始化规则中的SKB队列
sch->ops = ops;	//htb_qdisc_ops
sch->enqueue = ops->enqueue;	//ingress_enqueue
sch->dequeue = ops->dequeue;	//ingress_dequeue
sch->dev = dev;				//eth0设备对象
dev_hold(dev);				//设备对象引用递增
sch->stats_lock = &dev->queue_lock;
atomic_set(&sch->refcnt, 1);

sch->handle = handle;				//0x00010000

//使用排队规则中的初始化回调进行初始化,当前htb的回调函数为
//htb_init
ops->init(sch, tca[TCA_OPTIONS-1])
htb_init(tca[TCA_OPTIONS-1])
htb_sched *q = qdisc_priv(sch);

//HTB_INIT属性
gopt = RTA_DATA(tb[TCA_HTB_INIT - 1]);

//初始化根类链表
INIT_LIST_HEAD(&q->root);

for (i = 0; i < HTB_HSIZE; i++)
INIT_HLIST_HEAD(q->hash + i);

for (i = 0; i < TC_HTB_NUMPRIO; i++)
INIT_LIST_HEAD(q->drops + i);

init_timer(&q->timer);
skb_queue_head_init(&q->direct_queue);
q->direct_qlen = sch->dev->tx_queue_len;
if (q->direct_qlen < 2)
q->direct_qlen = 2;
q->timer.function = htb_timer;
q->timer.data = (unsigned long)sch;

//启动了速率定时器,每秒触发一下,其中htb_rate_timer函数在每秒
//触发会都会根据q->recmp_bucket索引来获取q->hash中的一个
//HASH链表,对该HASH链表所有条目进行速率计算,之后递增
//q->recmp_bucket索引,准备下一秒后对下一个HASH链表进行速
//率计算。当前计算方法也比较简单
//#define RT_GEN(D,R) R+=D-(R/HTB_EWMAC);D=0
//RT_GEN(cl->sum_bytes, cl->rate_bytes);
//RT_GEN(cl->sum_packets, cl->rate_packets);
init_timer(&q->rttim);
q->rttim.function = htb_rate_timer;
q->rttim.data = (unsigned long)sch;
q->rttim.expires = jiffies + HZ;
add_timer(&q->rttim);

if ((q->rate2quantum = gopt->rate2quantum) < 1)	//用户默认值为10
q->rate2quantum = 1;

q->defcls = gopt->defcls;					//20

//将当前排队规则加入到设备的qdisc_list链表中
qdisc_lock_tree(dev);
list_add_tail(&sch->list, &dev->qdisc_list);
qdisc_unlock_tree(dev);

//排队规则嫁接处理
qdisc_graft(dev, p, clid, q, &old_q);
if (parent == NULL)	//当前为根排队规则,未有父类
dev_graft_qdisc(dev, new);
//设备激活的情况下,先去激活
if (dev->flags & IFF_UP)
dev_deactivate(dev);

oqdisc = dev->qdisc_sleeping;

//假设当前仅使用的默认的fifo_fast排队规则,则当前这个老的排队规则
//已经存在,需要将老的排队规则进行复位,这里fifo_fast的reset回调函
//数为pfifo_fast_reset
if (oqdisc && atomic_read(&oqdisc->refcnt) <= 1)
qdisc_reset(oqdisc);
pfifo_fast_reset
//丢弃每个频道队列中的所有报文
for (prio = 0; prio < PFIFO_FAST_BANDS; prio++)
__qdisc_reset_queue(qdisc, list + prio);

qdisc->qstats.backlog = 0;
qdisc->q.qlen = 0;

//将新建的排队规则设置到qdisc_sleeping,qdisc的当前规则指向空规则
//noop_qdisc
dev->qdisc_sleeping = qdisc;
dev->qdisc = &noop_qdisc;

if (dev->flags & IFF_UP)
//设备激活
dev_activate(dev);
if (dev->qdisc_sleeping == &noop_qdisc)
//当前已经有根排队规则,不走此流程

//没有载波则直接返回
if (!netif_carrier_ok(dev))
return;

//当前使用的排队规则设置为已经选择的根排队规则
//启动看门狗。
rcu_assign_pointer(dev->qdisc, dev->qdisc_sleeping);
if (dev->qdisc != &noqueue_qdisc)
dev->trans_start = jiffies;
dev_watchdog_up(dev);

//发送netlink消息,告知添加成功,并且老的已经删除
qdisc_notify(skb, n, clid, old_q, q);

//将老的排队规则去除
qdisc_destroy(old_q);
//属于内建规则,或者还有其它模块引用,则不进行去除。
if (qdisc->flags & TCQ_F_BUILTIN || !atomic_dec_and_test(&qdisc->refcnt))
return;

//从设备的排队规则链表中去除
list_del(&qdisc->list);

//如果老的排队规则有reset、destroy回调,则进行处理
if (ops->reset)
ops->reset(qdisc);
if (ops->destroy)
ops->destroy(qdisc);

//资源销毁
module_put(ops->owner);
dev_put(qdisc->dev);
call_rcu(&qdisc->q_rcu, __qdisc_destroy);

三、创建ID为1的分类
1、用户层分析
//初始化,获取每纳秒对应多少TICKET
tc_core_init();
fp = fopen("/proc/net/psched", "r");
fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clock_res);
fclose(fp);

if (clock_res == 1000000000)
t2us = us2t;

clock_factor  = (double)clock_res / TIME_UNITS_PER_SEC;
tick_in_usec = (double)t2us / us2t * clock_factor;

//创建一个ROUTE类型的netlink套接口
rtnl_open(&rth, 0)
rtnl_open_byproto(rth, subscriptions, NETLINK_ROUTE);
rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
setsockopt(rth->fd,SOL_SOCKET,SO_SNDBUF,&sndbuf,sizeof(sndbuf))
setsockopt(rth->fd,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof(rcvbuf))
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = subscriptions;	//0
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local))
rth->seq = time(NULL);

do_cmd(argc-1, argv+1);
if (matches(*argv, "class") == 0)
do_class(argc-1, argv+1);
//创建新的类
tc_class_modify(RTM_NEWTCLASS, NLM_F_EXCL|NLM_F_CREATE, argc-1, 
argv+1);
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
req.n.nlmsg_flags = NLM_F_REQUEST|flags;
req.n.nlmsg_type = cmd;		//RTM_NEWTCLASS
req.t.tcm_family = AF_UNSPEC;

while (argc > 0)
if (strcmp(*argv, "dev") == 0)
NEXT_ARG();
strncpy(d, *argv, sizeof(d)-1);			//eth0
else if (strcmp(*argv, "parent") == 0)
NEXT_ARG();
get_tc_classid(&handle, *argv)
req.t.tcm_parent = handle;			//0x00010000
else if (strcmp(*argv, "classid") == 0)
NEXT_ARG();
get_tc_classid(&handle, *argv)
req.t.tcm_handle = handle;			//0x00010001
else
//如果有/usr/lib/tc/htb.so动态库中则从中获取htb_qdisc_util符
//号结构,否则检测当前tc程序是否有htb_qdisc_util符号结构则
//从中获取,否则返回q 为空。
q = get_qdisc_kind(k);

//添加KIND属性项,当前值为“htb”
addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);

//使用当前扩展排队规则的parse_copt回调去解析后续命令字符,当前
//htb的回调为htb_parse_class_opt
q->parse_copt(q, argc, argv, &req.n)
htb_parse_class_opt
mtu = 1600;

while (argc > 0)
if (strcmp(*argv, "rate") == 0)
NEXT_ARG();
get_rate64(&rate64, *argv)	//6 * 1000000 / 8
else if (matches(*argv, "burst") == 0)
NEXT_ARG();
//buffer = 15 * 1024
//cell_log = -1
get_size_and_cell(&buffer, &cell_log, *argv)
if (!ceil64)
ceil64 = rate64;

//超出32位最大值,则转换为全1?
opt.rate.rate = (rate64 >= (1ULL << 32)) ? ~0U : rate64;
opt.ceil.rate = (ceil64 >= (1ULL << 32)) ? ~0U : ceil64;

//如果cbuffer为设置,则取值为当前mtu值加上最大速率下
//每ticket单位的比特数
if (!cbuffer)
cbuffer = ceil64 / get_hz() + mtu;

opt.ceil.overhead = overhead;		//0
opt.rate.overhead = overhead;		//0

opt.ceil.mpu = mpu;	//0
opt.rate.mpu = mpu;	//0

//计算最小速率表
//cell_log = -1
//mtu = 1600
//linklayer = LINKLAYER_ETHERNET
tc_calc_rtable(&opt.rate, rtab, cell_log, mtu, linklayer)
//根据最大传输单元计算需要多少槽我理解是不可能每个
//字节都有准确速率,所以划定字节范围,从多少字节到
//多少字节的速率相同。
if (cell_log < 0)
cell_log = 0;
while ((mtu >> cell_log) > 255)
cell_log++;

for (i=0; i<256; i++)
//校正当前槽位的字节大小。这个算法比较简单,当前
//链路类型为以太网,则包根据原值处理,不会影响包
//大小。Mpu为最小包大小,如果槽位字节小于mpu,
//则校正为mpu的值。
sz = tc_adjust_size((i + 1) << cell_log, mpu, linklayer);

//根据当前槽位字节大小,及用户配置的速率,计算当
//前槽位所需ticket时间
rtab[i] = tc_calc_xmittime(bps, sz);

r->cell_align=-1;
r->cell_log=cell_log;
r->linklayer = (linklayer & TC_LINKLAYER_MASK);

//计算单包正常速率下的传送峰值,这里通过包大小转换成所
//需要的ticket时间
opt.buffer = tc_calc_xmittime(rate64, buffer);

//计算最大速率表,仅在发包速率大于最小速率后,租借模式下
//有效。
tc_calc_rtable(&opt.ceil, ctab, ccell_log, mtu, linklayer)

//计算单包在租借模速率作用下,传送的峰值。
opt.cbuffer = tc_calc_xmittime(ceil64, cbuffer);

//添加扩展属性OPTIONS,标记后面都是htb的选项
addattr_l(n, 1024, TCA_OPTIONS, NULL, 0);

//超出32位,则添加扩展属性HTB_RATE64
//超出32位,则添加扩展属性HTB_CEIL64
if (rate64 >= (1ULL << 32))
addattr_l(n, 1124, TCA_HTB_RATE64, &rate64, 
sizeof(rate64));
if (ceil64 >= (1ULL << 32))
addattr_l(n, 1224, TCA_HTB_CEIL64, &ceil64, 
sizeof(ceil64));

//添加扩展属性HTB_PARMS
addattr_l(n, 2024, TCA_HTB_PARMS, &opt, sizeof(opt));

//添加扩展属性HTB_RTAB
addattr_l(n, 3024, TCA_HTB_RTAB, rtab, 1024);

//添加扩展属性HTB_CTAB
addattr_l(n, 4024, TCA_HTB_CTAB, ctab, 1024);

//根据接口名获取接口索引
if (d[0])
idx = ll_name_to_index(d)
req.t.tcm_ifindex = idx;

//给内核发送该netlink消息
rtnl_talk(&rth, &req.n, 0, 0, NULL)

rtnl_close(&rth);

2、内核层分析
用户侧发出RTM_NEWTCLASS套接口消息后,在内核侧对应的处理回调函数为tc_ctl_tclass,该函数是在pktsched_init中初始化的。

tc_ctl_tclass
pid = tcm->tcm_parent			//父类ID			0x00010000
clid = tcm->tcm_handle;			//创建类ID		0x00010001
qid = TC_H_MAJ(clid);			//队列ID			0x00010000

//eth0设备对象
dev = __dev_get_by_index(tcm->tcm_ifindex))

if (pid != TC_H_ROOT)
pid = TC_H_MAKE(qid, pid);	//0x00010000

//从当前设备的qdisc_list链表中找到已经创建的排队规则,当前q为之前创建的HTB
q = qdisc_lookup(dev, qid)

//HTB的回调组为 cops = htb_class_ops
cops = q->ops->cl_ops;

clid = TC_H_MAKE(qid, clid);		//0x00010001

//get的回调函数为htb_get
cl = cops->get(q, clid);
htb_get
htb_class *cl = htb_find(classid, sch);
//使用类ID做HASH KEY在当前队列的HASH链表中查找已经创建的类
hlist_for_each_entry(cl, p, q->hash + htb_hash(handle), hlist)
if (cl->classid == handle)
return cl;
return NULL;

//如果找到则增加引用计数
if (cl)
cl->refcnt++;
return (unsigned long)cl;

//当前环境还没有该类存在,进行新类的属性设置
new_cl = cl;
//当前回调为 htb_change_class
cops->change(q, clid, pid, tca, &new_cl);/
htb_change_class
//opt指向扩展属性基值索引,后续rtattr_parse_nested进行属性查找都从该
//基值索引之外进行查找。
rtattr *opt = tca[TCA_OPTIONS - 1];

//查看枚举定义,TCA_HTB_RTAB值是最后一个,所以rtattr_parse_nested
//函数会把用户设置的所有扩展参数存储到临时变量tb中。
//enum
//{
	//	TCA_HTB_UNSPEC,
	//	TCA_HTB_PARMS,
	//	TCA_HTB_INIT,
	//	TCA_HTB_CTAB,
//	TCA_HTB_RTAB,
	//	__TCA_HTB_MAX,
//};
rtattr_parse_nested(tb, TCA_HTB_RTAB, opt)

//parentid = 0x00010000
//以类ID为HASH KEY向当前队规则的hash链表中查找父类,当前还未存在。
parent = htb_find(parentid, sch);
htb_sched *q = qdisc_priv(sch);
hlist_for_each_entry(cl, p, q->hash + htb_hash(handle), hlist)
if (cl->classid == handle)
return cl;
return NULL;

//取用户配置工具设置的HTB参数属性
hopt = RTA_DATA(tb[TCA_HTB_PARMS - 1]);

//将最小速率表加入到全局qdisc_rtab_list链表中
rtab = qdisc_get_rtab(&hopt->rate, tb[TCA_HTB_RTAB - 1]);

//将最大速率表加入到全局qdisc_rtab_list链表中
ctab = qdisc_get_rtab(&hopt->ceil, tb[TCA_HTB_CTAB - 1]);

//当前为新类进行创建
if (!cl)
cl = kzalloc(sizeof(*cl), GFP_KERNEL)
cl->refcnt = 1;
INIT_LIST_HEAD(&cl->sibling);
INIT_HLIST_NODE(&cl->hlist);
INIT_LIST_HEAD(&cl->children);
INIT_LIST_HEAD(&cl->un.leaf.drop_list);
RB_CLEAR_NODE(&cl->pq_node);

for (prio = 0; prio < TC_HTB_NUMPRIO; prio++)
RB_CLEAR_NODE(&cl->node[prio]);

//创建默认的pfifo排队规则,同时将该排队规则的父亲设置为当前类
//sch->parent = parentid;        0x00010001
new_q = qdisc_create_dflt(sch->dev, &pfifo_qdisc_ops, classid);

//当前新建的类的默认排队规则设置为pfifo
cl->un.leaf.q = new_q;

cl->classid = classid;				//0x00010001
cl->parent = parent;					//NULL

cl->tokens = hopt->buffer;			//正常速率下单包峰值
cl->ctokens = hopt->cbuffer;			//借用速率下单包峰值
cl->mbuffer = PSCHED_JIFFIE2US(HZ * 60);
PSCHED_GET_TIME(cl->t_c);
cl->cmode = HTB_CAN_SEND;     //初始时可以发送报文

//当新建的类加入到根排队规则的hash表中
hlist_add_head(&cl->hlist, q->hash + htb_hash(classid));

//将新建的类加入到根排队规则的root链表中
list_add_tail(&cl->sibling, &q->root);

if (!cl->level)
//在htb_init时,rate2quantum值默认为1
//设置quantum值,该值用于在进行带宽租借时的单位量
cl->un.leaf.quantum = rtab->rate.rate / q->rate2quantum;
if (!hopt->quantum && cl->un.leaf.quantum < 1000)
cl->un.leaf.quantum = 1000;
if (!hopt->quantum && cl->un.leaf.quantum > 200000)
cl->un.leaf.quantum = 200000;

cl->un.leaf.prio = hopt->prio;		//0

cl->quantum = cl->un.leaf.quantum;
cl->prio = cl->un.leaf.prio;

cl->buffer = hopt->buffer;				//正常速率下单包峰值
cl->cbuffer = hopt->cbuffer;				//借用速率下单包峰值

//设置当前类的最小、最大速率表
cl->rate = rtab;
cl->ceil = ctab;

//发送netlink消息,告知添加成功
tclass_notify(skb, n, q, new_cl, RTM_NEWTCLASS);

四、创建ID为20的分类
1、用户层分析
//初始化,获取每纳秒对应多少TICKET
tc_core_init();
fp = fopen("/proc/net/psched", "r");
fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clock_res);
fclose(fp);

if (clock_res == 1000000000)
t2us = us2t;

clock_factor  = (double)clock_res / TIME_UNITS_PER_SEC;
tick_in_usec = (double)t2us / us2t * clock_factor;

//创建一个ROUTE类型的netlink套接口
rtnl_open(&rth, 0)
rtnl_open_byproto(rth, subscriptions, NETLINK_ROUTE);
rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
setsockopt(rth->fd,SOL_SOCKET,SO_SNDBUF,&sndbuf,sizeof(sndbuf))
setsockopt(rth->fd,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof(rcvbuf))
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = subscriptions;	//0
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local))
rth->seq = time(NULL);

do_cmd(argc-1, argv+1);
if (matches(*argv, "class") == 0)
do_class(argc-1, argv+1);
//创建新的类
tc_class_modify(RTM_NEWTCLASS, NLM_F_EXCL|NLM_F_CREATE, argc-1, 
argv+1);
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
req.n.nlmsg_flags = NLM_F_REQUEST|flags;
req.n.nlmsg_type = cmd;		//RTM_NEWTCLASS
req.t.tcm_family = AF_UNSPEC;

while (argc > 0)
if (strcmp(*argv, "dev") == 0)
NEXT_ARG();
strncpy(d, *argv, sizeof(d)-1);			//eth0
else if (strcmp(*argv, "parent") == 0)
NEXT_ARG();
get_tc_classid(&handle, *argv)
req.t.tcm_parent = handle;			//0x00010001
else if (strcmp(*argv, "classid") == 0)
NEXT_ARG();
get_tc_classid(&handle, *argv)
req.t.tcm_handle = handle;			//0x00010014 (0x14 = 20)
else
//如果有/usr/lib/tc/htb.so动态库中则从中获取htb_qdisc_util符
//号结构,否则检测当前tc程序是否有htb_qdisc_util符号结构则
//从中获取,否则返回q 为空。
q = get_qdisc_kind(k);

//添加KIND属性项,当前值为“htb”
addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);

//使用当前扩展排队规则的parse_copt回调去解析后续命令字符,当前
//htb的回调为htb_parse_class_opt
q->parse_copt(q, argc, argv, &req.n)
htb_parse_class_opt
mtu = 1600;

while (argc > 0)
if (strcmp(*argv, "rate") == 0)
NEXT_ARG();
get_rate64(&rate64, *argv)	//3 * 1000000 / 8
else if (matches(*argv, "burst") == 0)
NEXT_ARG();
//buffer = 15 * 1024
//cell_log = -1
get_size_and_cell(&buffer, &cell_log, *argv)
else if (strcmp(*argv, "ceil") == 0)
NEXT_ARG();
get_rate64(&ceil64, *argv);	//6 * 1000000 / 8

//超出32位最大值,则转换为全1?
opt.rate.rate = (rate64 >= (1ULL << 32)) ? ~0U : rate64;
opt.ceil.rate = (ceil64 >= (1ULL << 32)) ? ~0U : ceil64;

//如果cbuffer为设置,则取值为当前mtu值加上最大速率下
//每ticket单位的比特数
if (!cbuffer)
cbuffer = ceil64 / get_hz() + mtu;

opt.ceil.overhead = overhead;		//0
opt.rate.overhead = overhead;		//0

opt.ceil.mpu = mpu;	//0
opt.rate.mpu = mpu;	//0

//计算最小速率表
//cell_log = -1
//mtu = 1600
//linklayer = LINKLAYER_ETHERNET
tc_calc_rtable(&opt.rate, rtab, cell_log, mtu, linklayer)
//根据最大传输单元计算需要多少槽我理解是不可能每个
//字节都有准确速率,所以划定字节范围,从多少字节到
//多少字节的速率相同。
if (cell_log < 0)
cell_log = 0;
while ((mtu >> cell_log) > 255)
cell_log++;

for (i=0; i<256; i++)
//校正当前槽位的字节大小。这个算法比较简单,当前
//链路类型为以太网,则包根据原值处理,不会影响包
//大小。Mpu为最小包大小,如果槽位字节小于mpu,
//则校正为mpu的值。
sz = tc_adjust_size((i + 1) << cell_log, mpu, linklayer);

//根据当前槽位字节大小,及用户配置的速率,计算当
//前槽位所需ticket时间
rtab[i] = tc_calc_xmittime(bps, sz);

r->cell_align=-1;
r->cell_log=cell_log;
r->linklayer = (linklayer & TC_LINKLAYER_MASK);

//计算单包正常速率下的传送峰值,这里通过包大小转换成所
//需要的ticket时间
opt.buffer = tc_calc_xmittime(rate64, buffer);

//计算最大速率表,仅在发包速率大于最小速率后,租借模式下
//有效。
tc_calc_rtable(&opt.ceil, ctab, ccell_log, mtu, linklayer)

//计算单包在租借模速率作用下,传送的峰值。
opt.cbuffer = tc_calc_xmittime(ceil64, cbuffer);

//添加扩展属性OPTIONS,标记后面都是htb的选项
addattr_l(n, 1024, TCA_OPTIONS, NULL, 0);

//超出32位,则添加扩展属性HTB_RATE64
//超出32位,则添加扩展属性HTB_CEIL64
if (rate64 >= (1ULL << 32))
addattr_l(n, 1124, TCA_HTB_RATE64, &rate64, 
sizeof(rate64));
if (ceil64 >= (1ULL << 32))
addattr_l(n, 1224, TCA_HTB_CEIL64, &ceil64, 
sizeof(ceil64));

//添加扩展属性HTB_PARMS
addattr_l(n, 2024, TCA_HTB_PARMS, &opt, sizeof(opt));

//添加扩展属性HTB_RTAB
addattr_l(n, 3024, TCA_HTB_RTAB, rtab, 1024);

//添加扩展属性HTB_CTAB
addattr_l(n, 4024, TCA_HTB_CTAB, ctab, 1024);

//根据接口名获取接口索引
if (d[0])
idx = ll_name_to_index(d)
req.t.tcm_ifindex = idx;

//给内核发送该netlink消息
rtnl_talk(&rth, &req.n, 0, 0, NULL)

rtnl_close(&rth);

2、内核层分析
用户侧发出RTM_NEWTCLASS套接口消息后,在内核侧对应的处理回调函数为tc_ctl_tclass,该函数是在pktsched_init中初始化的。

tc_ctl_tclass
pid = tcm->tcm_parent			//父类ID			0x00010001
clid = tcm->tcm_handle;			//创建类ID		0x00010014
qid = TC_H_MAJ(clid);			//队列ID			0x00010000

//eth0设备对象
dev = __dev_get_by_index(tcm->tcm_ifindex))

if (pid != TC_H_ROOT)
pid = TC_H_MAKE(qid, pid);	//0x00010001

//从当前设备的qdisc_list链表中找到已经创建的排队规则,当前q为之前创建的HTB
q = qdisc_lookup(dev, qid)

//HTB的回调组为 cops = htb_class_ops
cops = q->ops->cl_ops;

clid = TC_H_MAKE(qid, clid);		//0x00010014

//get的回调函数为htb_get
cl = cops->get(q, clid);
htb_get
htb_class *cl = htb_find(classid, sch);
//使用类ID做HASH KEY在当前队列的HASH链表中查找已经创建的类
hlist_for_each_entry(cl, p, q->hash + htb_hash(handle), hlist)
if (cl->classid == handle)
return cl;
return NULL;

//如果找到则增加引用计数
if (cl)
cl->refcnt++;
return (unsigned long)cl;

//当前环境还没有该类存在,进行新类的属性设置
new_cl = cl;
//当前回调为 htb_change_class
cops->change(q, clid, pid, tca, &new_cl);/
htb_change_class
//opt指向扩展属性基值索引,后续rtattr_parse_nested进行属性查找都从该
//基值索引之外进行查找。
rtattr *opt = tca[TCA_OPTIONS - 1];

//查看枚举定义,TCA_HTB_RTAB值是最后一个,所以rtattr_parse_nested
//函数会把用户设置的所有扩展参数存储到临时变量tb中。
//enum
//{
	//	TCA_HTB_UNSPEC,
	//	TCA_HTB_PARMS,
	//	TCA_HTB_INIT,
	//	TCA_HTB_CTAB,
//	TCA_HTB_RTAB,
	//	__TCA_HTB_MAX,
//};
rtattr_parse_nested(tb, TCA_HTB_RTAB, opt)

//parentid = 0x00010001
//以类ID为HASH KEY向当前队规则的hash链表中查找父类,之前已经创建。
parent = htb_find(parentid, sch);
htb_sched *q = qdisc_priv(sch);
hlist_for_each_entry(cl, p, q->hash + htb_hash(handle), hlist)
if (cl->classid == handle)
return cl;
return NULL;

//取用户配置工具设置的HTB参数属性
hopt = RTA_DATA(tb[TCA_HTB_PARMS - 1]);

//将最小速率表加入到全局qdisc_rtab_list链表中
rtab = qdisc_get_rtab(&hopt->rate, tb[TCA_HTB_RTAB - 1]);

//将最大速率表加入到全局qdisc_rtab_list链表中
ctab = qdisc_get_rtab(&hopt->ceil, tb[TCA_HTB_CTAB - 1]);

//当前为新类进行创建
if (!cl)
cl = kzalloc(sizeof(*cl), GFP_KERNEL)
cl->refcnt = 1;
INIT_LIST_HEAD(&cl->sibling);
INIT_HLIST_NODE(&cl->hlist);
INIT_LIST_HEAD(&cl->children);
INIT_LIST_HEAD(&cl->un.leaf.drop_list);
RB_CLEAR_NODE(&cl->pq_node);

for (prio = 0; prio < TC_HTB_NUMPRIO; prio++)
RB_CLEAR_NODE(&cl->node[prio]);

//创建默认的pfifo排队规则,同时将该排队规则的父亲设置为当前类
//sch->parent = parentid;        0x00010014
new_q = qdisc_create_dflt(sch->dev, &pfifo_qdisc_ops, classid);

//当前存在父类,该父类还未有孩子节点
if (parent && !parent->level)
//保留当前父类中排队规则的队列长度,在HTB队列机制仅叶子类
//含有队列,所以当父类有孩子类后,父类中的队列就要删除。
qlen = parent->un.leaf.q->q.qlen;

//调用父类中排队规则的reset回调,当前父类的排队规则为默认的
//pfifo,则回调为qdisc_reset_queue
qdisc_reset(parent->un.leaf.q);
ops = qdisc->ops;
ops->reset(qdisc);
//该函数的实现仅仅是删除队列中所有skb报文 
qdisc_reset_queue

//通知父排队规则,队列长度减少。
qdisc_tree_decrease_qlen(parent->un.leaf.q, qlen);
while ((parentid = sch->parent))
//当前排队规则为父类的pfifo规则,其中parent为父类
//的ID,TC_H_MAJ(parentid)对应的就是父类所在的排
//队规则,当前父类所在的排队规则即为最早创建的根
//排队规则HTB。
sch = __qdisc_lookup(sch->dev, TC_H_MAJ(parentid));

cops = sch->ops->cl_ops;

//htb_get
//在排队规则中获取指定ID的类对象,并增加引用计数
cl = cops->get(sch, parentid);
htb_get(sch, parentid)

//htb_qlen_notify
cops->qlen_notify(sch, cl);
htb_qlen_notify(sch, cl)
//如果当前父类中排队规则的队列长度为0,则进行
//去激活
if (cl->un.leaf.q->q.qlen == 0)
htb_deactivate(qdisc_priv(sch), cl);
htb_deactivate_prios(q, cl);
mask = cl->prio_activity;

while (cl->cmode == 
HTB_MAY_BORROW 
&& p && mask)
//当前还未有激活项,不处理

if (cl->cmode == HTB_CAN_SEND 
&& mask)
//当前还未有激活项,不处理

cl->prio_activity = 0;
list_del_init(&cl->un.leaf.drop_list);

//htb_put
//减小类引用计数,如果没有其它对象引用该类,则调用
//htb_destroy_class进行类的销毁
cops->put(sch, cl);
htb_put(sch, cl);

//子排队规则中队列长度减少,则父排队规则中的队列长度
//相应减少。
sch->q.qlen -= n;

//销毁父类中排队规则,当前父类的排队规则为pfifo类型
qdisc_destroy(parent->un.leaf.q);
list_del(&qdisc->list);

//qdisc_reset_queue
ops->reset(qdisc);
//删除队列中skb链表
qdisc_reset_queue

//递减相关对象引用计数,并释放排队规则占用的分配内存块
module_put(ops->owner);
dev_put(qdisc->dev);
call_rcu(&qdisc->q_rcu, __qdisc_destroy);

//父类中还有未激活项则去激活
if (parent->prio_activity)
htb_deactivate(q, parent);

//根类等级深度为最大值
parent->level = TC_HTB_MAXDEPTH) - 1; 	//7

memset(&parent->un.inner, 0, sizeof(parent->un.inner));

//当前新建的类的默认排队规则设置为pfifo
cl->un.leaf.q = new_q;

cl->classid = classid;				//0x00010014
cl->parent = parent;					//指向当前ID为1:1的父类

cl->tokens = hopt->buffer;			//正常速率下单包峰值
cl->ctokens = hopt->cbuffer;			//借用速率下单包峰值
cl->mbuffer = PSCHED_JIFFIE2US(HZ * 60);
PSCHED_GET_TIME(cl->t_c);
cl->cmode = HTB_CAN_SEND;     //初始时可以发送报文

//当新建的类加入到根排队规则的hash表中
hlist_add_head(&cl->hlist, q->hash + htb_hash(classid));

//将新建的类关联到当前父类上
list_add_tail(&cl->sibling, &parent->children);

if (!cl->level)
//在htb_init时,rate2quantum值默认为1
//设置quantum值,该值用于在进行带宽租借时的单位量
cl->un.leaf.quantum = rtab->rate.rate / q->rate2quantum;
if (!hopt->quantum && cl->un.leaf.quantum < 1000)
cl->un.leaf.quantum = 1000;
if (!hopt->quantum && cl->un.leaf.quantum > 200000)
cl->un.leaf.quantum = 200000;

cl->un.leaf.prio = hopt->prio;		//0

cl->quantum = cl->un.leaf.quantum;
cl->prio = cl->un.leaf.prio;

cl->buffer = hopt->buffer;				//正常速率下单包峰值
cl->cbuffer = hopt->cbuffer;				//借用速率下单包峰值

//设置当前类的最小、最大速率表
cl->rate = rtab;
cl->ceil = ctab;

//发送netlink消息,告知添加成功
tclass_notify(skb, n, q, new_cl, RTM_NEWTCLASS);

五、创建一个U32分类器
1、用户层分析
//初始化,获取每纳秒对应多少TICKET
tc_core_init();
fp = fopen("/proc/net/psched", "r");
fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clock_res);
fclose(fp);

if (clock_res == 1000000000)
t2us = us2t;

clock_factor  = (double)clock_res / TIME_UNITS_PER_SEC;
tick_in_usec = (double)t2us / us2t * clock_factor;

//创建一个ROUTE类型的netlink套接口
rtnl_open(&rth, 0)
rtnl_open_byproto(rth, subscriptions, NETLINK_ROUTE);
rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
setsockopt(rth->fd,SOL_SOCKET,SO_SNDBUF,&sndbuf,sizeof(sndbuf))
setsockopt(rth->fd,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof(rcvbuf))
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = subscriptions;	//0
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local))
rth->seq = time(NULL);

do_cmd(argc-1, argv+1);
if (matches(*argv, "filter") == 0)
do_filter(argc-1, argv+1);
tc_filter_modify(RTM_NEWTFILTER, NLM_F_EXCL|NLM_F_CREATE, 
argc-1, argv+1);
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
req.n.nlmsg_flags = NLM_F_REQUEST|flags;
req.n.nlmsg_type = cmd;		//RTM_NEWTFILTER
req.t.tcm_family = AF_UNSPEC;

while (argc > 0)
if (strcmp(*argv, "dev") == 0)
NEXT_ARG();
strncpy(d, *argv, sizeof(d)-1);		//eth0
else if (matches(*argv, "protocol") == 0)
NEXT_ARG();
ll_proto_a2n(&id, *argv)
protocol = id;					//ETH_P_IP
else if (strcmp(*argv, "parent") == 0)
NEXT_ARG();
get_tc_classid(&handle, *argv)
req.t.tcm_parent = handle;		//0x00010000
else if (matches(*argv, "priority") == 0)
NEXT_ARG();
get_u32(&prio, *argv, 0)			//1
Else
//如果有/usr/lib/tc/f_u32.so动态库中则从中获取u32_filter_util符	//号结构,否则检测当前tc程序是否有u32_filter_util符号结构则
//从中获取,否则返回q 为空。
q = get_filter_kind(k);

//prio = 1,左移到前两个字节
//protocol = ETH_P_IP,在低两个字节
req.t.tcm_info = TC_H_MAKE(prio<<16, protocol);

//增加KIND扩展属性,值为u32
addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);

//使用u32扩展过滤器解析后续规则,当前回调为u32_parse_opt
q->parse_fopt(q, fhandle, argc, argv, &req.n)
u32_parse_opt
//添加OPTIONS扩展属性,标识后续都属于u32的扩展项
addattr_l(n, MAX_MSG, TCA_OPTIONS, NULL, 0);

while (argc > 0)
if (matches(*argv, "match") == 0)
NEXT_ARG();
parse_selector(&argc, &argv, &sel.sel, n)
else if (matches(*argv, "ip") == 0)
NEXT_ARG();
parse_ip(&argc, &argv, sel);
else if (strcmp(*argv, "dport") == 0)
NEXT_ARG();

//sel->keys[hwm].val = key; htos(80)
//sel->keys[hwm].mask = mask;全1
//sel->keys[hwm].off = off; 20
//sel->keys[hwm].offmask=offmask; 0
//sel->nkeys++;              1
parse_u16(&argc, &argv, sel, 22,0);

sel_ok++;
else if (strcmp(*argv, "flowid") == 0)
NEXT_ARG();
//0x0001000A(0xA=10)
get_tc_classid(&handle, *argv)

//添加U32_CLASSID扩展属性
addattr_l(n, MAX_MSG, TCA_U32_CLASSID, &handle, 4);
//添加终止标记TC_U32_TERMINAL,内核代码会根据
//此标记判断当前处理完成。
sel.sel.flags |= TC_U32_TERMINAL;

if (sel_ok)
//添加U32_SEL扩展属性
addattr_l(n, MAX_MSG, TCA_U32_SEL, &sel,
sizeof(sel.sel)+sel.sel.nkeys*sizeof(struct tc_u32_key));

//根据接口名获取接口索引
if (d[0])
idx = ll_name_to_index(d)
req.t.tcm_ifindex = idx;

//给内核发送该netlink消息
rtnl_talk(&rth, &req.n, 0, 0, NULL)

rtnl_close(&rth);

2、内核层分析
用户侧发出RTM_NEWTFILTER套接口消息后,在内核侧对应的处理回调函数为tc_ctl_tfilter,该函数是在tc_filter_init中初始化的。

protocol = TC_H_MIN(t->tcm_info);	//ETH_P_IP
prio = TC_H_MAJ(t->tcm_info);		//1
nprio = prio;
parent = t->tcm_parent;				//0x00010000

dev = __dev_get_by_index(t->tcm_ifindex)	//eth0设备对象

//从设备的qdisc_list列表中查找排队规则,之前HTB排队规则已经加入到链表中,所以
//这里的q就等于HTB排队规则。
q = qdisc_lookup(dev, TC_H_MAJ(t->tcm_parent))

//前2个字节为排队规则索引,后2个字节为类索引,当前命令设置基于排队规则之上,后
//2个字节设置为0,该条件不满足,此时cl变量取值为初始的0。
if (TC_H_MIN(parent))
//不走此流程

//HTB排队规则中对应的回调为htb_find_tcf
//获取该排队规则中的过滤链表。
chain = cops->tcf_chain(q, cl);
htb_find_tcf
htb_sched *q = qdisc_priv(sch);
htb_class *cl = (struct htb_class *)arg;
//当前cl为0,则取的是排队规则中的过滤链表
tcf_proto **fl = cl ? &cl->filter_list : &q->filter_list;
return fl;

//查找待插入的位置,优先级的值越小表示越高。
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;

//新建过滤协议项
if (tp == NULL)
//从tcf_proto_base链表中查找u32分类器,当前tp_ops为cls_u32_ops
tp_ops = tcf_proto_lookup_ops(tca[TCA_KIND-1]);

tp->ops = tp_ops;			//cls_u32_ops
tp->protocol = protocol;		//ETH_P_IP
tp->prio = nprio;			//1
tp->q = q;				//指向根HTB类型排队规则
tp->classify = tp_ops->classify;	//u32_classify
tp->classid = parent;			//0x00010000

//当前u32类的初始化回调为fw_ini。
tp_ops->init(tp)
u32_init
//查找u32_list链表中是否存在相同的排队规则对象的过滤通用项
for (tp_c = u32_list; tp_c; tp_c = tp_c->next)
if (tp_c->q == tp->q)
break;

root_ht = kzalloc(sizeof(*root_ht), GFP_KERNEL);
root_ht->divisor = 0;
root_ht->refcnt++;
//当前在u32_list链表中还未存在相同的排队规则对象的过滤通用项,所以
//tp_c不为真。
root_ht->handle = tp_c ? gen_new_htid(tp_c) : 0x80000000;
root_ht->prio = tp->prio;			//1

//当前tp_c为空,创建一个并加入到u32_list链表中。
if (tp_c == NULL)
tp_c = kzalloc(sizeof(*tp_c), GFP_KERNEL);
tp_c->q = tp->q;
tp_c->next = u32_list;
u32_list = tp_c;

//将新建的root_ht与tc_c互相关联
tp_c->refcnt++;
root_ht->next = tp_c->hlist;
tp_c->hlist = root_ht;
root_ht->tp_c = tp_c;

//当前过滤对象的root指向新分配HASH节点对象,同时data指
//向新分配的过滤通用项
tp->root = root_ht;
tp->data = tp_c;

//将当前过滤器加入到当前排队规则的过滤链表中
tp->next = *back;
*back = tp;

//u32分类器的get回调为u32_get,当前在命令行里没有handle值,所以这里返回为0
fh = tp->ops->get(tp, t->tcm_handle);

//u32分类器的change回调为u32_change
tp->ops->change(tp, cl, t->tcm_handle, tca, &fh);
u32_change
//把用户配置的后续所有扩展属性提取到临时变量opt中
rtattr_parse_nested(tb, TCA_U32_MAX, opt)

if ((n = (struct tc_u_knode*)*arg) != NULL)
//当前在调用u32_get后,还不存在对应的hash节点项,所以不走此流程。

//如果命令接口设置了divisor,则表示创建一个新的hash表对象,该hash表含有
//divisor个数量的列表项,主要用于规则数量很大时,加快匹配速度。
if (tb[TCA_U32_DIVISOR-1])
unsigned divisor = *(unsigned*)RTA_DATA(tb[TCA_U32_DIVISOR-1]);

ht = kzalloc(sizeof(*ht) + divisor*sizeof(void*), GFP_KERNEL);
ht->tp_c = tp_c;		//当前hash表与过滤通用项关联
ht->refcnt = 0;
ht->divisor = divisor;
ht->handle = handle;
ht->prio = tp->prio;
//通用项中的hlist指向第一hash表,这里将新建的hash插入到通用项的hlist
//链头。
ht->next = tp_c->hlist;
tp_c->hlist = ht;

*arg = (unsigned long)ht;
return 0;

//当用户在命令接口指定了hash表的ID,则查找对应的hash表,否则如果在命令
//接口没有指定hash表ID,则使用过滤协议项中root指向的第一个hash链表。
if (tb[TCA_U32_HASH-1])
htid = *(unsigned*)RTA_DATA(tb[TCA_U32_HASH-1]);

if (TC_U32_HTID(htid) == TC_U32_ROOT)
ht = tp->root;
htid = ht->handle;
else
//tp->data指向过滤通用项,这里使用过滤通用项中hlist链表来进行hash
//表查找。
ht = u32_lookup_ht(tp->data, TC_U32_HTID(htid));
else
ht = tp->root;
htid = ht->handle;	//0x80000000,该值在上面u32_init中初始化

//当前命令行未设置handle,使用gen_new_kid生成hash节点id
if (handle)
//不走此流程
else
handle = gen_new_kid(ht, htid);
unsigned i = 0x7FF;

//handle = 0x80000000
//TC_U32_HASH(handle) = 0
//当前ht->ht[0]还未有值,所有这里返回值为 0x80000800
for (n=ht->ht[TC_U32_HASH(handle)]; n; n = n->next)
if (i < TC_U32_NODE(n->handle))
i = TC_U32_NODE(n->handle);
i++;
return handle|(i>0xFFF ? 0xFFF : i);

//指向选择项属性值
s = RTA_DATA(tb[TCA_U32_SEL-1]);

//当前用户配置接口在当前规则中仅设置了一个匹配项,则nkeys为1
//这里分配内存空间为一个struct tc_u_knode大小,加上一个struct tc_u32_key大小
n = kzalloc(sizeof(*n) + s->nkeys*sizeof(struct tc_u32_key), GFP_KERNEL);

//把用户配置设置匹配规则复制到当前新的hash关键节点的sel中
memcpy(&n->sel, s, sizeof(*s) + s->nkeys*sizeof(struct tc_u32_key));

//当前hash关键节点的ht_up指向当前使用的hash表
n->ht_up = ht;
n->handle = handle;	// 0x80000800

//当前s->hmask为0,所以i最后为0
u8 i = 0;
u32 mask = s->hmask
if (mask)
......
n->fshift = i;

//过滤节点参数设置
u32_set_parms(tp, base, ht, n, tb, tca[TCA_RATE-1]);
//当前命令行没有输入action或police,所以无扩展属性
tcf_exts_validate(tp, tb, est, &e, &u32_ext_map);

//如果用户接口输入了link,则进行链接处理,这里链接是指在当前过滤项
//匹配成功后,可以跳到其它hash表项中去处理,如果在其它hash表项中
//分类成功,则直接返回成功,否则在其它hash表项中分类失败,还可以
//回来原来的hash表项继续处理。
if (tb[TCA_U32_LINK-1])
u32 handle = *(u32*)RTA_DATA(tb[TCA_U32_LINK-1]);

//根据用户接口设置的link索引,查找当前通用项中hlist对应的hash
//表是否含有对应id的hash项。
ht_down = u32_lookup_ht(ht->tp_c, handle);

//没找到返回错误,找到则增加引用计数
if (ht_down == NULL)
goto errout;
ht_down->refcnt++;

//将hash关键节点的ht_down指向希望链接的hash表项,在进行分类处
//理里,如果当前hash关键节点匹配成功,会检测如果存在ht_down,则
//使用该hash表项的各hash关键节点项继续匹配处理。
ht_down = xchg(&n->ht_down, ht_down);

//如果之前ht_down已经关联了其它hash表项,当前已经不使用,则
//递减引用计数。
if (ht_down)
ht_down->refcnt--;

if (tb[TCA_U32_CLASSID-1])
//0x0001000A
n->res.classid = *(u32*)RTA_DATA(tb[TCA_U32_CLASSID-1]);

tcf_bind_filter(tp, &n->res, base);
//当前回调为htb_bind_filter
//查找绑定该hash关键节点对象的类
cl = tp->q->ops->cl_ops->bind_tcf(tp->q, base, r->classid);
htb_bind_filter
htb_sched *q = qdisc_priv(sch);

//当前classid为0x000100A
htb_class *cl = htb_find(classid, sch);

//找到匹配的类,则类的过滤计数递增
if (cl)
cl->filter_cnt++;
else
q->filter_cnt++;
return (unsigned long)cl;

//将找到的类对象关联到hash关键节点对象中,同时返回hash关键
//节点对象中原有的关联类,当前没有老的关联类,所以old_cl为空
cl = cls_set_class(tp, &r->class, cl);
old_cl = __cls_set_class(clp, cl);
old_cl = *clp;
*clp = cl;
return old_cl;
return old_cl;

//当前hash关键节点对象是新建的,没有老的关联类,不执行此流程。
if (cl)
tp->q->ops->cl_ops->unbind_tcf(tp->q, cl);

//扩展参数设置,当前命令行没有输入action或police,所以无扩展属性。
tcf_exts_change(tp, &n->exts, &e);

//把新建的hash关键节点加入到hash对象的ht链表中
for (ins = &ht->ht[TC_U32_HASH(handle)]; *ins; ins = &(*ins)->next)
if (TC_U32_NODE(handle) < TC_U32_NODE((*ins)->handle))
break;

n->next = *ins;
wmb();
*ins = n;

//告知添加成功
tfilter_notify(skb, n, tp, fh, RTM_NEWTFILTER);

六、发送报文
当上层发出报文,调用dev_queue_xmit,这里仅分析和出口排队规则相关的代码,其它详细
的代码分析可以参见《接口设备发包》
dev_queue_xmit
......

//如果当前使用出口排队规则
q = rcu_dereference(dev->qdisc);
if (q->enqueue)
spin_lock(&dev->queue_lock);
q = dev->qdisc;

if (q->enqueue)
//调用当前排队规则的入队回调,当前HTB类型排队规则回调为
//htb_enqueue
rc = q->enqueue(skb, q);
htb_enqueue
htb_sched *q = qdisc_priv(sch);

//分类处理
htb_class *cl = htb_classify(skb, sch, &ret);
//直报文的优先级字段与当前队列的句柄相同,则返回直接处理
if (skb->priority == sch->handle)
return HTB_DIRECT;

//使用报文的优先级字段做为类ID,在htb_find函数中,从排队
//规则的hash表中查找指定类ID的类对象,找到指定类对象后,
//根据cl->level判断当前类是否为叶子(0为叶子),如果是叶子
//类则返回。
if ((cl = htb_find(skb->priority, sch)) != NULL && cl->level == 0)
return cl;


*qerr = NET_XMIT_BYPASS;

tcf = q->filter_list;

//使用过滤对象进行分类,tc_classify函数在下面单独分析。
while (tcf && (result = tc_classify(skb, tcf, &res)) >= 0)
switch (result)
//这三种处理结果都会返回空类,在htb_enqueue对返回类
//为空的情况都会将报文丢弃。
case TC_ACT_QUEUED:
case TC_ACT_STOLEN:
*qerr = NET_XMIT_SUCCESS;
case TC_ACT_SHOT:
return NULL;

//如果返回结果的类不存在,则继续尝试
if ((cl = (void *)res.class) == NULL)
//如果返回结果就classid为排队规则句柄,也就是说之
//前用户在命令接口配置过滤规则时,最后匹配的流ID
//为排队规则句柄,则返回直接处理。
if (res.classid == sch->handle)
return HTB_DIRECT;

//此时根据用户在命令接口配置过滤规则时指定的
//流ID查找当前排队规则是否存在该类,如果不存
//在,则直接跳出当前while循环,使用之前创建
//HTB排队规则时设置的默认的类
if ((cl = htb_find(res.classid, sch)) == NULL)
break;

//如果找到了类,并且该类是叶子类,则返回,否则继续使
//用下一个过滤器进行查找。
if (!cl->level)
return cl;

//使用非叶子类中的过滤器进行分类
tcf = cl->filter_list;

//到达这里,表明所有过滤器都已经遍历完了,但还没有找到
//对应的叶子类。这里则使用之前创建HTB排队规则时设置的
//默认类ID进行查找。
cl = htb_find(TC_H_MAKE(TC_H_MAJ(sch->handle), q->defcls), 
sch);

//如果使用默认类ID,还没找到对应的类,或者找到的类是非
//叶子类型的,则返回直接处理。
if (!cl || cl->level)
return HTB_DIRECT;

//返回找到的默认类。
return cl;

//当前返回结果为直接处理
if (cl == HTB_DIRECT)
//如果当前直接发送队列还未达到直接发送限额,则将报文放入
//直接发送队列中,否则将报文丢弃处理。
if (q->direct_queue.qlen < q->direct_qlen)
__skb_queue_tail(&q->direct_queue, skb);
q->direct_pkts++;
else
kfree_skb(skb);
sch->qstats.drops++;
return NET_XMIT_DROP;
//使用叶子类中的排队规则的enqueue回调,将报文压入到该排队规
//则中,如果压入失败,则进行丢包统计。
else if (cl->un.leaf.q->enqueue(skb, cl->un.leaf.q) !=net_xmit_success)
sch->qstats.drops++;
cl->qstats.drops++;
return NET_XMIT_DROP;
else 
//包压入指定叶子类下的排队规则后,统计成功项,同时激活对
//应叶子类。
cl->bstats.packets++;
cl->bstats.bytes += skb->len;

//激活叶子类,当前HTB的机制在对激活的类做了划分。首先
//分了8层,叶子类为第0层,根类为第7层,中间类在父类的
//层数上减1,在进行出队处理时从第0层开始优先进行处理。
//同时每层又分为8个优先级,0为第高优先级,当被激活的类
//层次相同时,则按优先级最高的优先进行处理。当被激活的类
//层次也相同,优先级也相同,则被根据类ID为对比值放入一
//棵红黑树中,类ID越小优先级越高。激活类的数据结构大概
//如下图所示:


htb_activate(q, cl);
//设置当前类中被激活的优先级位图,这里cl->un.leaf.prio
//是在类被用户接口创建时设置的,如果在用户接口未输入
//prio则默认为0。
cl->prio_activity = 1 << (cl->un.leaf.aprio = cl->un.leaf.prio);

//根据类中层次及优先级位图激活类
htb_activate_prios(q, cl);
p = cl->parent;		//当前类的父类
mask = cl->prio_activity;	//当前类的优先级位图

//当前类如果已经进到了租借模式,同时该类存在父类
//则进行处理。这里会对父类进行激活,同时将当前类
//挂接到父类的feed供给树下。
while (cl->cmode == HTB_MAY_BORROW && p && 
mask)
m = mask;

while (m)
//将优先级位图取反,之后使用ffz查找第一
//为0的位为表示优先级索引。
int prio = ffz(~m);

//当前位处理后就去除。
m &= ~(1 << prio);

//当前父类如果已经在供给,则表明之前已经
//激活,则在mask中去除当前优先级位,否
//则下面会将cl变量指向父类,并对父类进行
//激活。
if (p->un.inner.feed[prio].rb_node)
mask &= ~(1 << prio);

//这里将当前子类加入到父类的供给树中
//(un.inner.feed),表明当前父类在给哪些
//子类供给。
htb_add_to_id_tree(p->un.inner.feed + prio, cl, 
prio);

//父类可能租借给多个子类使用,这里在父类中保
//存子类的优先级。开始对父类准备激活处理。
p->prio_activity |= mask;
cl = p;
p = cl->parent;

//如果当前类为可发送模式,则在HTB排队规则中激
//活该优先级类。
if (cl->cmode == HTB_CAN_SEND && mask)
htb_add_class_to_row(q, cl, mask);
//记载当前层次哪些优先级被激活
q->row_mask[cl->level] |= mask;

//遍历所有待处理的优先级位,将当前类插入
//到排队规则row指定位置的树中。插入位置
//的实现可以参考上图激活类的数据结构来
//加深理解。
while (mask)
int prio = ffz(~mask);
mask &= ~(1 << prio);
htb_add_to_id_tree(q->row[cl->level] + 
prio, cl, prio);

//将当前叶子类插入到HTB排队规则的drops对应优先级的
//列表中。
list_add_tail(&cl->un.leaf.drop_list,q->drops + 
cl->un.leaf.aprio);

//更新统计,返回分类成功。
sch->q.qlen++;
sch->bstats.packets++;
sch->bstats.bytes += skb->len;
return NET_XMIT_SUCCESS;

//队列处理
qdisc_run(dev);
//如果队列发送没有关闭,并且队列调度还没有运行,则运行队列调度
if (!netif_queue_stopped(dev) 
&&!test_and_set_bit(__LINK_STATE_QDISC_RUNNING, &dev->state))
__qdisc_run(dev);
//如果队列为noop_qdisc类型,则表明接口还未启动,则直接退
//出队列调度
if (unlikely(dev->qdisc == &noop_qdisc))
goto out;

//当还有待处理的数据,并且发送队列没有关闭,则循环从队列
//中取出数据,使用网卡驱动进行发送数据。该函数具体实现不
//详细列出。之前在《接口设备发包》中已经分析过,可以参考
//那节的分析。这里主要关注,从队列中取出数据是使用的队列
//的dequeue回调,当前HTB类型的排队规则回调为
//htb_dequeue,htb_dequeue在下面单独分析。
while (qdisc_restart(dev) < 0 && !netif_queue_stopped(dev))
;

out:
//去除正在进行队列调度标记
clear_bit(__LINK_STATE_QDISC_RUNNING, &dev->state);

spin_unlock(&dev->queue_lock);

rc = rc == NET_XMIT_BYPASS ? NET_XMIT_SUCCESS : rc;
goto out;

spin_unlock(&dev->queue_lock);

---------------------------------------------------------------------------------------------------------------------
//使用过滤器进行分类
tc_classify
protocol = skb->protocol;

//遍历所有过滤器
for ( ; tp; tp = tp->next)
//首先检测当前报文的协议与过滤器中的协议匹配,或者过滤器匹配任何协议类
//型,则进一步使用当前过滤器的classify回调进行分类处理,当前u32的回调
//函数为u32_classify,u32_classify在下面单独分析。
if ((tp->protocol == protocol ||tp->protocol == __constant_htons(ETH_P_ALL)) &&
(err = tp->classify(skb, tp, res)) >= 0)
//返回处理结果
return err;

//返回-1,表明分类失败。
return -1;

---------------------------------------------------------------------------------------------------------------------
使用u32分类器进行分类处理
u32_classify
//过滤协议对象的root,即首hash链表对象
tc_u_hnode *ht = (struct tc_u_hnode*)tp->root;
u8 *ptr = skb->nh.raw;		//报文的IP头位置
int sel = 0;

next_ht:

n = ht->ht[sel];

next_knode:

//判断当前hash关键节点是否存在
if (n)
struct tc_u32_key *key = n->sel.keys;	//hash关键节点对象的匹配项

//遍历当前hash关键节点的所有匹配项,如果有任何一项匹配失败,则使用
//goto进入下一个hash关键节点进行匹配。这里匹配项中的off就是对应项
//(比如dport)在报文IP文件位置的偏移。
for (i = n->sel.nkeys; i>0; i--, key++)
if ((*(u32*)(ptr+key->off+(off2&key->offmask))^key->val)&key->mask)
n = n->next;
goto next_knode;

//ht_down和命令行中link配置相关,用于多层嵌套,可以实现跳转到其它hash链
//节点进行查找,当前测试实例没有配置嵌套,这里ht_down 为NULL。
if (n->ht_down == NULL)
check_terminal:
//在用户侧命令接口已经对当前过滤选择项设置了U32_TERMINAL
if (n->sel.flags&TC_U32_TERMINAL)
*res = n->res;		//返回该过滤选择项的结果,这里含有绑定的类

//进行扩展属性处理,处理失败尝试下一个关键匹配点,当前命令接口
//没有设置相关扩展项,所以这里返回为0
r = tcf_exts_exec(skb, &n->exts, res);
if (r < 0)
n = n->next;
goto next_knode;

//当前匹配完成,返回结果。
return r;

//如果hash关键节点匹配项匹配完成,但不含U32_TERMINAL处理结束的标
//记,则尝试下一个hash关键节点。
n = n->next;
goto next_knode;

//n->ht_down不为空,表明有嵌套匹配处理

//先将当前hash关键节点及匹配指针压入临时变量stack栈中
stack[sdepth].knode = n;
stack[sdepth].ptr = ptr;
sdepth++;

//取出待跳转的hash链对象
ht = n->ht_down;

sel = 0;
//如果当前hash链对象含有divisor值,表明该hash链对象是之前通过命令接口
//使用divisor关键字段创建的含有多个列表项的hash链对象,则这里根据之
//前命令接口设置的散列因子相关的参数(比如根据源IP地址最后1个字节进行散
//列因子处理)进行计算,来确定当前的报文应该在当前hash链对象的哪个列表
//项中进行处理。
if (ht->divisor)
sel = ht->divisor&u32_hash_fold(*(u32*)(ptr+n->sel.hoff), &n->sel,n->fshift);

//如果当前hash关键节点的选择项中不含有任何偏移属性,则直接跳转到下一个
//hash链节点中进行查找。
if (!(n->sel.flags&(TC_U32_VAROFFSET|TC_U32_OFFSET|TC_U32_EAT)))
goto next_ht;


//走到此流程,表明当前hash关键节点中含有相关的偏移属性,则根据用户接口
//配置进行对应的偏移处理,如果处理完后报文合法,则跳转到下一个hash链节
//点中进行查找。
if (n->sel.flags&(TC_U32_OFFSET|TC_U32_VAROFFSET))
off2 = n->sel.off + 3;
if (n->sel.flags&TC_U32_VAROFFSET)
off2 += ntohs(n->sel.offmask & *(u16*)(ptr+n->sel.offoff)) >>n->sel.offshift;
off2 &= ~3;
if (n->sel.flags&TC_U32_EAT)
ptr += off2;
off2 = 0;
if (ptr < skb->tail)
goto next_ht;

//用户接口含有link关键字时,则进行嵌套处理,每进行一次嵌套则将前一次处理结果
//存储到临时变量stack中,当最后的嵌套处理没有匹配成功时,会走到此流程,如果
//当前确定是嵌套处理,则取出前一次的hash链对象继续进行处理。
if (sdepth--)
n = stack[sdepth].knode;
ht = n->ht_up;
ptr = stack[sdepth].ptr;
goto check_terminal;

//分类失败
return -1;

---------------------------------------------------------------------------------------------------------------------
//HTB数据包出队处理
htb_dequeue
q->jiffies = jiffies;			//记载当前滴答

//如果当前直接发送队列含有报文,则优先进行处理,直接将报文取出。
skb = __skb_dequeue(&q->direct_queue);
if (skb != NULL)
sch->flags &= ~TCQ_F_THROTTLED;
sch->q.qlen--;
return skb;

//队列无数据则返回为NULL
if (!sch->q.qlen)
goto fin;

//获取当前系统时间
PSCHED_GET_TIME(q->now);

min_delay = LONG_MAX;

q->nwc_hit = 0;

//从第0层开始遍历处理,第0层为叶子类层
for (level = 0; level < TC_HTB_MAXDEPTH; level++)
//当前流逝时间已经超过最近事件的缓冲时间
if (time_after_eq(q->jiffies, q->near_ev_cache[level]))
delay = htb_do_events(q, level);
//最多处理500个被阻塞的报文
for (i = 0; i < 500; i++)
//取当前层中第1个被阻塞的树节点
struct rb_node *p = rb_first(&q->wait_pq[level]);

//如果当前没有阻塞的报文,则返回0延时。
if (!p)
return 0;

//获取包含该树节点的HTB类对象
cl = rb_entry(p, struct htb_class, pq_node);

//如果当前类的阻塞时间还未到,则继续阻塞,返回还有多长时间会
//到的时间差。
if (time_after(cl->pq_key, q->jiffies))
return cl->pq_key - q->jiffies;

//当前类的阻塞时间已到。

//将树节点从阻塞树中删除。
htb_safe_rb_erase(p, q->wait_pq + level);

//计算当前类最后一次处理到当前的时间差,该时间差不能超过
//mbuffer值(在创建类时该值为1分钟)。
diff = PSCHED_TDIFF_SAFE(q->now, cl->t_c, (u32) cl->mbuffer);

//进行类的模式改变
htb_change_class_mode(q, cl, &diff);
new_mode = htb_class_mode(cl, diff);
//这里ctokens是租借模式下令牌数,经过一段时间后令牌
//数就会进行补充。待补充后仍然小于低水位线,则状态变
//为HTB_CANT_SEND(不能进行发送),这里htb_lowater
//低水位线根据当前类的模式不同而不同,如果当前类模式
//为HTB_CANT_SEND,则低水位线的值为-cl->cbuffer,也
//就是租借模式下单包最大可传达数据所需要的ticket,其它
//模块下低水位线的值为0。
if ((toks = (cl->ctokens + *diff)) < htb_lowater(cl))
*diff = -toks;	//返回还需要多少令牌
return HTB_CANT_SEND;

//这里tokens是非租借模式下令牌数,经过一段时间补充后,
//如果高于高水位线,则状态变为HTB_CAN_SEND
//(可以发送)
if ((toks = (cl->tokens + *diff)) >= htb_hiwater(cl))
return HTB_CAN_SEND;

//状态变为可租借,返回还需要多少令牌
*diff = -toks;
return HTB_MAY_BORROW;

//当前模式相同则直接返回。
if (new_mode == cl->cmode)
reutrn;

//如果当前类有激活,则根据新、老模式进行类的激活、去激活
//处理(之前为非不可发送模式,则将当前类去激活,该类后
//续暂时不能发包,当前新的模式为可发送模式,则将当前类
//激活,该类后续可以继续发包,当前新的模式为租借模式,则
//激活父类,并将当前类挂接到父类的feed供给树下)。
//否则当前类没有激活,则仅修改当前模式。
if (cl->prio_activity)
if (cl->cmode != HTB_CANT_SEND)
htb_deactivate_prios(q, cl);
cl->cmode = new_mode;
if (new_mode != HTB_CANT_SEND)
htb_activate_prios(q, cl);
else
cl->cmode = new_mode;

//如果当前模式不可发送,则将类插入到q->wait_pq[level]等待树中。
//同时更新类的cl->pq_key等待关键字的值为当前jitter加上diff,其
//中diff是在上面htb_change_class_mode中计算得到。
if (cl->cmode != HTB_CAN_SEND)
htb_add_to_wait_tree(q, cl, diff);

//在上面等待树中最多处理500个等待报文,超过则放到下轮再来处理,
//同时返回延时时间为100ms。
return HZ / 10;

//更新最近事件处理时间,如果没有等待处理的包,则delay为0,下一次事件
//处理则加长一些(1秒),否则还有等待处理的包,则下一次事件处理的时间
//设置为上面的delay值。
q->near_ev_cache[level] =q->jiffies + (delay ? delay : HZ);
else
//如果当前流逝时间还未到达下一次事件处理时间,则delay取值为下一次事
//件处理所需要的时间点。
delay = q->near_ev_cache[level] - q->jiffies;

//因为最终在下面进行延时处理是使用min_delay的值,这里对该值进行校正
if (delay && min_delay > delay)
min_delay = delay;

//row_mask[level]为当前层已经激活的类,这里取反是为了方便下面使用ffz进行
//查找第一个为0的位所在索引。
m = ~q->row_mask[level];

//优先级激活位图如果取反为全为1,则表明当前层的类都没有被激活。
while (m != (int)(-1))
//优先取最低优先级位进行处理。
int prio = ffz(m);

m |= 1 << prio;	//每处理一次,当前优先级位就清除,以免二次进入。

//指定层次、指定优先级的树中取出待发送的skb报文。
skb = htb_dequeue_tree(q, prio, level);
//q->row记载着激活的位图,仅类被激活后才可以发送报文,当子类未激
//活时,从父类中租借代宽,父类就会激活。
//q->ptr记载了最后处理的的类的树节点。
//q->last_ptr_id记载了最后处理的类ID。
//这里根据DDR算法,查找出可调度的HTB类。
//htb_lookup_leaf函数比较复杂,放在下面单独分析。

start = cl = htb_lookup_leaf(q->row[level] + prio, prio,q->ptr[level] + prio,
                         q->last_ptr_id[level] + prio);

do
next:
//由于代宽受限等原因导致当前没有可出队的报文。
if (!cl)
return NULL;

//如果当前类的队列为空
if (unlikely(cl->un.leaf.q->q.qlen == 0))
//该类去激活
htb_deactivate(q, cl);
//检查如果当前层、当前优先级已经没有激活的类,则返回为
//NULL,让上面函数继续下一层、下一优先级类的处理。
if ((q->row_mask[level] & (1 << prio)) == 0)
return NULL;

//当前调度的类没有队列,并且当前层、当前优先级下还有激活
//的类,则再次调用htb_lookup_leaf查找可调度的类。同时更新
//临时变量start、cl。
next = htb_lookup_leaf(q->row[level] + prio,
prio, q->ptr[level] + prio,q->last_ptr_id[level] + prio);

if (cl == start)
start = next;
cl = next;
goto next;

//使用当前类下的排队规则的dequeue回调进行将报文出队,这里
//dequeue回调实现取决于用户侧给该类下绑定什么排队规则,如
//果启用未设置,则默认HTB类下的排队规则为pfifo_qdisc,则
//对应的回调函数为qdisc_dequeue_head,该函数比较简单,仅仅
//是仅出双向链表中第一个节点的报文。
skb = cl->un.leaf.q->dequeue(cl->un.leaf.q);

//报文取出后直接跳出do while循环,返回。
if (likely(skb != NULL))
break;

//当该类下标明有队列,但取出失败,则标记告警。
if (!cl->warned)
cl->warned = 1;

q->nwc_hit++;	//统计不工作的错误类计数
//将之前保存记录的ptr(最后处理的的类的树节点)更新为下一个节
//点。
htb_next_rb_node((level ? cl->parent->un.inner.ptr : q->ptr[0]) + prio);

//再次调用htb_lookup_leaf查找可调度的类
cl = htb_lookup_leaf(q->row[level] + prio, prio,q->ptr[level] + prio,
q->last_ptr_id[level] + prio);

//走到这里,表明刚才调度类在出队报文时异常,又重新选择了新的调度
//类。重新进行出队处理。
while (cl != start)

//报文成功出队
if (likely(skb != null))
//htb_next_rb_node用来修改下一次使用htb_lookup_leaf选择调度
//类时先从哪个类进行处理,这里根据quantum值的限额来进行
//控制,可以防止htb_lookup_leaf在一段时间内始终使用相同的类
//进行调度。
if ((cl->un.leaf.deficit[level] -= skb->len) < 0)
cl->un.leaf.deficit[level] += cl->un.leaf.quantum;
htb_next_rb_node((level ? cl->parent->un.inner.ptr : q->ptr[0]) + 
prio);

//当前类下的排队规则中已经没有报文,则将该类去激活。
if (!cl->un.leaf.q->q.qlen)
htb_deactivate(q, cl);

//对该类进行收费计算。
htb_charge_class(q, cl, level, skb->len);
//临时定义了一个计帐功能宏,这里T可以是tokens或ctokens,
//B可以是buffer或cbuffer,R可以是rate或ceil。
//T表示租借模式或非租借模式下的令牌数。
//B表示租借模式或非租借模式下基于对应速率的单包峰值。
//R表示租借模式或非租借模式下不同速率。
//首先将根据流逝时间来对所剩的令牌数进行补充。
#define HTB_ACCNT(T,B,R) toks = diff + cl->T; \
//对令牌数进行上限峰值限制
if (toks > cl->B) toks = cl->B; \
//根据当前报文字节数,在速率表中查找符合当前字节范围
//之内所需消耗的单位值,之后从令牌总数中扣除消耗的单
//位值。
toks -= L2T(cl, cl->R, bytes); \
//如果令牌数小于最大负等待峰值时间(1分钟),则对令牌
//数修正为最大负等待峰值时间。
if (toks <= -cl->mbuffer) toks = 1-cl->mbuffer; \
//将剩余的令牌数保留,供下一次使用。
cl->T = toks

while (cl)
//计算当前HTB类最后一个工作到当前已经流逝的时间,
//不能超过mbuffer上限。
diff = PSCHED_TDIFF_SAFE(q->now, cl->t_c, (u32) 
cl->mbuffer);

//该函数第一次处理的cl类都是叶子类,之后在while
//循环中cl类依次改变为当前类的父类,而level在循环
//中是始终保持不变的。如果当前level是0,表明叶子一直
//未超过最低速率,始终是可发送的。如果当前level非0,
//表明叶子已经在借用父类的带宽。当cl->level >= level,则
//对当前类及父类都进行tokens计费处理,当
//cl->level < level则表明当前类是在对父类进行租借,不
//再对当前类进行tokens计费处理,仅对提供租借的父类
//进行tokens计费处理。但不管当前类是否在租借都会
//对ctokens进行计费处理。这里要理解tokens与ctokens
//的差别,tokens是当前类的速率限制,ctokens是可向父类
//借用的速率限制。如果使用当前类发送报文时小于tokens,
//则当前类速率还比较正常,可以继续处理,如果当前发送
//报文时大于tokens但小于ctokens,则表明当前类已经超出
//自己的速率限制,但还可以从父类借用带宽,如果当前发
//送报文时大于ctokens,则表明都超出了可借用速率,此时
//该类的模式会修改为不可发送模式,当前类暂时已经不能
//在发送任何报文。
if (cl->level >= level)
if (cl->level == level)
cl->xstats.lends++;
HTB_ACCNT(tokens, buffer, rate);
else
cl->xstats.borrows++;
cl->tokens += diff;
HTB_ACCNT(ctokens, cbuffer, ceil);

//记载当前类最后处理时间,用于下一个时间点来统计已经
//流逝的时间。
cl->t_c = q->now;

//根据当前类中tokens、ctokens剩余令牌数,使用
//htb_change_class_mode进行HTB类的模式切换处理。
//同时根据是否还是可发送模式而确定是否将类插入到
//HTB排队规则的等待树中。
old_mode = cl->cmode;
diff = 0;
htb_change_class_mode(q, cl, &diff);
if (old_mode != cl->cmode)
if (old_mode != HTB_CAN_SEND)
htb_safe_rb_erase(&cl->pq_node, q->wait_pq + 
cl->level);
if (cl->cmode != HTB_CAN_SEND)
htb_add_to_wait_tree(q, cl, diff);

//非叶子类的统计。
if (cl->level)
cl->bstats.bytes += bytes;
cl->bstats.packets++;

//将cl修改为当前类的父类,在while循环中依次对父类
//进行处理。
cl = cl->parent;

//如果成功取出报文,则HTB排队规则的队列个数递减,同时去除受限标记,
//将当前skb返回。
if (likely(skb != NULL))
sch->q.qlen--;
sch->flags &= ~TCQ_F_THROTTLED;
goto fin;

//当前HTB排队规则中有报文,但遍历完所有层、所有优先级的类后,没有成功取出
//报文,则进行延时处理。
htb_delay_by(sch, min_delay > 5 * HZ ? 5 * HZ : min_delay);
//修改当前HTB排队规则的timer定时器时间,该定时器是在创建HTB排队规则
//时,通过htb_init函数创建,该定时器的超时处理函数为htb_timer,该函数比较
//简单,仅仅是去除TCQ_F_THROTTLED受限标记,之后调用netif_schedule进行
//发送队列的重新调度。
mod_timer(&q->timer, q->jiffies + delay);

//加上受限标记,更新overlimits超出限速的统计。
sch->flags |= TCQ_F_THROTTLED;
sch->qstats.overlimits++;

-----------------------------------------------------------------------------------------------------------------
查找可调度的叶子类(虽然代码不多,但这里是整个HTB排队规则中最难的一部分)
htb_lookup_leaf(struct rb_root *tree, int prio,struct rb_node **pptr, u32 * pid)
//定义了8个元素的数组,初始sp指向第0个元素。
struct {
struct rb_node *root;
struct rb_node **pptr;
u32 *pid;
} stk[TC_HTB_MAXDEPTH], *sp = stk;

//当前第0个元素的初始值由传入的参数提供,root指向当前层次、当前优先级下待处
//理的HTB类所在的树节点。pptr指向HTB排队规则中记载的最后处理的树节点,
//注意这里使用了双层指针,在当前函数处理时就可以通过*pptr来对传入参数进行修
//改,更新HTB排队规则中记载的最后处理树节点,以方便下一次报文处理时使用。
//pid指向HTB排队规则中记载的最后处理的类ID,同样,这里可以通过*pid来对传
//入参数进行修改。
sp->root = tree->rb_node;
sp->pptr = pptr;
sp->pid = pid;

//尝试65535次处理
for (i = 0; i < 65535; i++)
//当树节点不存在,但类ID存在,则使用类ID尝试在当前待处理的树中进行查
//找树节点。通过查看代码,发现有一种情景会走此流程,如果一个HTB叶子
//类为租借模式,当其发送报文后,队列中已经没有数据,则触发htb_deactivate_prios
//函数,该函数会判断当前类为租借模式,并且正好匹配父类中un.inner.ptr
//记载的最后处理树节点,则将父类的un.inner.ptr设置为空,同时父类的
//un.inner.last_ptr_id保留此叶子类的ID,并将该类的树节点从父类的供应树中
//删除。之后立即有一个新的报文使用htb_enqueue函数对该叶子类又进行入队
//操作,则触发htb_activate_prios函数将此叶子类又加入到父类的供应树中。
//那么当使用htb_lookup_leaf查找可调度类时,如果q->ptr队列的最后树节点
//处理正好是这个父类,在此函数中会将sp->root指向父类的供应树,sp->pptr
//指向父类的un.inner.ptr,sp->pid指向父类的un.inner.last_ptr_id,此时正好满足
//当前IF的条件判断。
if (!*sp->pptr && *sp->pid)
*sp->pptr = htb_id_find_next_upper(prio, sp->root, *sp->pid);

//清除pid,保证上面的条件判断仅触发一次。
*sp->pid = 0;

//如果当前树节点不存在
if (!*sp->pptr)
//将树节点指向当前待处理的树根节点。一定要留意,使用了*sp也就意味着
//将sp->pptr之前指向的对象内容做了修改,即如果之前对象是指针类型,则
//把原对象的指针指向进行了修改。
*sp->pptr = sp->root;

//将pptr指向当前树中最小类ID的节点(生成该树时就是以类ID为索引进行
//的红黑树排序)。
while ((*sp->pptr)->rb_left)
*sp->pptr = (*sp->pptr)->rb_left;

//这里stk就是上面的临时数组,也相当于数组的第0元素指针,刚进入该
//函数时,sp是指向数组的第0个元素的,当下面发现找到的调度类
//非叶子类时,sp就会递增,同时sp将指向该类的供给类(也就是该
//类的孩子类)。如果出现这种情况,则sp--来退到当前类的父类,同时
//对父类的sp->pptr最后使用的树节点进行更新,htb_next_rb_node函数
//是将sp->pptr值设置为比sp->pptr更大一点的节点(如果有右子树,则
//取右子树中最小的树节点;否则如果有父亲则取父亲中最小的树节点;
//否则如果没有父子节点则取值为NULL)。
if (sp > stk)
sp--;
if (!*sp->pptr)
return NULL;
htb_next_rb_node(sp->pptr);

else
//pptr找到了树节点,则取出包含该树节点的类对象
cl = rb_entry(*sp->pptr, struct htb_class, node[prio]);

//检测如果当前是叶子类则表示查找成功,返回该叶子类。
if (!cl->level)
return cl;

//走到此流程表明找到的类是非叶子类型,此时递增sp,使其指向stk的下一
//个数组元素,同时将该类的相关信息存储到这个新的数组元素中。root指向
//当前类供应的子类树节点,pptr指向当前类的最后处理树节点,pid指向当前
//类的最后处理类ID。
(++sp)->root = cl->un.inner.feed[prio].rb_node;
sp->pptr = cl->un.inner.ptr + prio;
sp->pid = cl->un.inner.last_ptr_id + prio;

//经过这么多次查找,仍然没找到可调度的叶子类,则查找失败,返回NULL。
return NULL;
---------------------------------------------------------------------------------------------------------------------
(注意: 以下分析仅考虑htb_lookup_leaf函数配合类激活、去激活的流程分析,并未考虑数据报发送成功后,在其它条件下也会使用htb_next_rb_node进行下一个待处理的树节点修改的情况)。
一、到这里已经对htb_lookup_leaf怎样进行查找可调度的叶子类分析完成,但可能还是云里雾里,这里用一个实例来描述htb_lookup_leaf函数的晦涩算法机制。在如下实例中,我们假设仅创建了三个类(根父类、子类1、子类2),这里根类的层次为最高层7,叶子类的层次为最低层0,同时假设这三个类的优先级都为0级,所以可以看到下图中子类1与子类2在同层次、同优先级情况下构建了一棵树,并且假设子类1的ID比较小,所以子类1在红黑树的左侧。



1、最初时,子类1、与子类2都为可发送模式,并且随着报文进入对应类的队列,类分别被激活。
2、在出队时,根据第0层优先、0优先级优先的原则,此时会找到子类2的树对象上。
3、之后使用htb_lookup_leaf函数进行查找可调度的叶子类。
4、最初HTB排队规则的q->ptr最后处理的树节点为NULL,所以会走到if (!*sp->pptr)流程,在该流程中会更新q->ptr,使其指向子类2的树节点,之后又遍历该树中最小的类ID节点,所以q->ptr又被更新为子类1树节点。此时子类1是叶子类,查找成功,返回当前调度类为子类1。
5、后续由于q->ptr最后处理树节点指向了子类1,所有只要子类1一直有待发送的数据,则始终使用子类1进行调度。

二、当子类1不断发包,已经超出其带宽限制后。

1、子类1的模式由可发送模式变为租借模式,在模式变更时触发了两个关键动作,首先子类1进行去激活处理,在去激活处理过程中q->ptr指向了同层次、同优先级、比子类1略大一点的子类2上。其次,父类被激活,同时父类的供给树根节点指向了子类1的树节点。
2、在后续出队时,优先处理第0层、第0优先级的类,当前q->ptr已经指向子类2,所以后续始终用子类2进行调度。

三、当子类2不断发包,已经超出其带宽限制后。

1、子类2的模式由可发送模式变为租借模式,在模式变更时触发了两个关键动作,首先子类2进行去激活处理,在去激活处理过程中由于子类2已经是只含1个节点的树了,q->ptr指向了NULL。其次,子类2也被加入到了父类的供给树中,由于和子类1的优先级相同,所以子类2和子类1又成为一棵树上的节点。
2、此时第0层的类已经没有激活的,所以只能从当前只激活的第7层类中进行处理。首先检测当前q->ptr为NULL,则将q->ptr指向父类的树节点。(注意当前父类的树仅有一个节点,图中画的连线并非父节点的子节点,也就是说父类在HTB排队规则中指定层次、指定优先级的那棵树下仅自己一个节点,而父类自身有一个feed供给树,用来标识当前父类都给哪些孩子类提供带宽借用。)
3、之后判断当前父类并非叶子类,则将sp指向stk数组的第1个元素,第1个元素的
root指向当前父类feed供给树的根节点(子类1),第1个元素的pptr指向父类的cl->un.inner.ptr,第1个元素的pid指向父类的cl->un.inner.last_ptr_id。
4、由于最终父类的cl->un.inner.ptr还为NULL值,所有又走到if (!*sp->pptr)条件里去,在这里将父类的cl->un.inner.ptr指向了父类的供给树根节点(子类1)。
5、sp此时指向stk数组的第1个元素,所有if (sp > stk)条件成立,将sp指针递减回第0个数组元素,第0个数组元素在第2步中已经将q->ptr指向父类的树节点,由于父类树节点仅有一个树节点,所有在执行htb_next_rb_node(sp->pptr)时返回NULL,导致q->ptr又变成NULL值。
6、继续for循环处理,又将q->ptr指向父类,之后判断当前父类并非叶子类,则将sp指向stk数组的第1个元素,第1个元素的root指向当前父类feed供给树的根节点(子类1),第1个元素的pptr指向父类的cl->un.inner.ptr,第1个元素的pid指向父类的cl->un.inner.last_ptr_id。
7、此时父类的cl->un.inner.ptr已经指向子类1,所以判断子类1为叶子类,则对子类1进行调度。
8、后续只要子类1中始终有报文,则重复第6、7步对子类1进行调度处理。

你可能感兴趣的:(HTB分层令牌桶排队规则分析)