htb qdisc

htb qdisc是一种classfull qdisc,通过在htb qdisc下添加class和filter可以实现一些复杂的限速功能,本篇笔记中浅析一下htb enqueue/dequeue的过程,看看它是如何工作的。

htb工作原理

htb enqueue时,是根据filter或者数据包携带的优先级字段来选择一个class进行enqueue,这个过程比较简单。
相比enqueue而言,dequeue选择class的过程需要考虑class的层级、优先级、class当前的状态、parent class的状态等因素,会稍微复杂一些。接下来先简单看下htb的一些关键参数,看下它是如何使用这些参数的,然后再通过几个例子来感受htb选择class的过程。

htb class的关键参数

htb class有一些比较关键的参数,后续的描述中会提到它们,这里先大致给出它们的含义:

  • ceil: 最大速率,htb class的速率永远超不过ceil
  • rate: 保证速率,class有很多数据包要输出时,htb会保证class的速率能达到rate,class速率达到rate后再想提升就得看parent还有没有剩余速率,parent有剩余速率,就能借用parent的速率。(ceil值通常会设置得大于等于rate值,若ceil值设置得小于rate值,则htb只能保证class速率到达ceil)
  • prio: 优先级(0~7),htb在dequeue时会按照优先级来选择class
  • quantum: htb在 dequeue优先级相同的class时,会逐个选择class,class输出quantum字节后就切换下一个class,一直循环这个过程
  • level: htb class在树状结构中所处层级,最多有8层(0~7)
  • burst/cburst: htb 使用令牌桶来限速,每个htb class都有两个令牌桶,一个用于限制最大速率(ceil),一个用于限制保证速率(rate),burst/cburst分别是rate桶和ceil桶的桶尺寸

htb class的三种状态

htb class存在三种状态,htb class随着自身速率的变化会在这三种状态之间切换,htb会依据class的状态来判断它是否有资格dequeue,按照官方的解释,这三种状态的含义如下:

  • HTB_CANT_SEND:class的速率大于ceil
  • HTB_MAY_BORROW: class的速率大于 rate但小于ceil
  • HTB_CAN_SEND: class的速率小于 rate也小于ceil

为了方便描述,接下来的描述和图表中将用绿色代表HTB_CAN_SEND,黄色代表HTB_MAY_BORROW,红色代表HTB_CANT_SEND

htb 的关键结构

htb_class_level.png

htb class会形成树状的结构,如上图所示,这个树状结构中最多存在8个层级,每个层级都有一个对应的level值,最靠近htb qdisc的class的level为7,每向下一层,level就减少1,leaf class处于最下层,level为0。
htb qdisc会维护一个全局的表,如上图htb qdisc旁边的hlevel[8]数组所示,hlevel[0]~hlevel[7]分别对应着level 0~7,每个hlevel[*]中又会有一个包含8个成员的数组hprio[8],hprio[0]~hprio[7]分别对应着prio 0~7,每一个hprio[*]都是一个树结构,绿色的leaf class会在数据包enqueue后按照自身的[level,prio]值链入对应的树,绿色的inner class会在child class变为黄色时按照自身的[level,prio]值链入对应的树,为了方便描述,后面就称这个表为 hlevel表。
htb qdisc下的每个inner class也会维护一个表,如上图任何一个inner class旁边的 clprio[8]数组所示,clprio[0]~clprio[7]分别对应着prio 0~7,每一个 clprio[*]都是一个树结构,该class的child class变成黄色时,child class就会根据自身或者更下端leaf class的prio值链入对应的树,为了方便描述,后面就称这个表为 clprio表。

dequeue时,hlevel和clprio表是htb选择class的重要依据,htb会遵循这样的基本原则:先从hlevel表选中level和prio都最小且非空的树,然后遍历树上的class,若class是leaf class,则用该leaf class进行dequeue,若class是inner class则查看inner class的clprio表,从clprio表中查找prio最小且非空的树,然后遍历树上的class直到找到leaf class。

htb如何选择class来进行dequeue

接下来通过几个例子来感受htb选择class过程。

假设有下图所示的树状结构,图最右侧是hlevel表(因为图中只有3层,所以只画出了hlevel[7],hlevel[6],hlevel[0]),A/B/C/D是四个inner class,它们旁边的数组代表它们的clprio表,E/F/G/H/I/J 6个都是leaf class,它们的优先级如图中标注,此时还没有dequeue过任何数据包,所有class都是绿色。
htb_example0.png

假设此时htb qdisc开始enqueue了,6个leaf class都被逐渐被enqueue了数据包,随着enqueue的进行,E/F/G/H/I/J会根据自身的优先级链接到hlevel[0]对应的树上去,如下所示
htb_example1.png

htb qdisc开始dequeue,首先查找hlevel表,发现hlevel[0]上有非空的树,从优先级最高的开始,此时J就开始dequeue,E/F/G/H/I不会被dequeue,有一点需要留意一下,class在dequeue时,消耗自身带宽的同时也在消耗parent class的带宽。
直到J变成了黄色,G/H/I开始dequeue(按quantum值轮流dequeue),E/F此时不会被dequeue。J变成了黄色,J将从hlevel[0].hprio[0]上摘除,然后根据自身优先级挂入D的clprio[0],D则挂入hlevel[6].hprio[0]的树结构,如下所示:
htb_example2.png

G/H/I随着dequeue的进行逐渐变成黄色,I变成黄色时,I将从hlevel[0].hprio[2]上摘除,然后根据自身优先级挂入D的clprio[2],假设此时D仍是绿色,D则会挂入hlevel[6].hprio[2]的树结构(此时D既挂入了hlevel[6].hprio[0]也挂入了hlevel[6].hprio[2])。
G/H变成黄色时,将从hlevel[0].hprio[2]上摘除,然后根据自身优先级挂入C的clprio[2],假设此时C也变成了黄色,则C会挂入A的clprio[2](C的挂入的树是根据下端leaf class的优先级来确定的),而A也会挂入hlevel[7].hprio[2]。
E/F开始dequeue(按quantum值轮流dequeue),随着dequeue的进行,它们开始变黄色,E/F挂入B的clprio[3],假设此时B变成了红色, A仍然是绿色,此时各class的状态如下所示:
htb_example3.png

此时hlevel[0]已经没有非空的树,开始查找hlevel[6],hlevel[6].hprio[0]的D将被选中,D是inner class,查看它的clprio[0]上有没有黄色的class,J被选中,它开始借用D的速率,开始dequeue,直到D变黄色或者J变红色,假设D没有变黄色,J变成了红色,J将从D的clprio[0]上移除,D将从hlevel[6].hprio[0]上移除。
此时hlevel[6].hprio[1]的D将被选中,D是inner class,查看它的clprio[2]有没有黄色的class,I被选中,它开始借用D的速率,开始dequeue,直到D变黄色或者I变红色,假设D没有变黄色,I变成了红色,此时I将从D的clprio[2]上移除,D也将从hlevel[6].hprio[2]上移除,如下图所示:
htb_example4.png

此时此时hlevel[6]已经没有非空的树,开始查找hlevel[7],hlevel[7].hprio[2]的A被选中,A是inner class,查找它的clprio,clprio[2]的C被选中,C是inner class,继续查找C的clprio,G和H按quantum值轮流dequeue,此时G和H借用C的速率,而C也是黄色,C就借用A的速率。
随着G和H的dequeue,假如A变成了黄色,此时A将从hlevel[7].hprio[2]移除,因为A直连于htb qdisc,没有parent class了,所以它没有clprio[*]可加入,此时所有的class状态如下所示。这个时候htb qdisc无法再dequeue出任何数据包了,若仍然有很多的数据包enqueue进htb qdisc而htb class也达到了leaf qdisc缓存数据包的极限值,则htb class就会开始丢弃数据包。
htb_example5.png

随着时间的推移,有的class逐渐变回绿色,它们又可能会重新被放入hlevel表,htb qdisc则又可以继续dequeue了。

在上述的例子,class的状态更新是随dequeue的过程同步进行的,不是由htb启动定时任务来定期的更新,class每一次dequeue完,都会顺便更新一下自身的状态。
然而那些变红色的class它们无法被选去dequeue,它们的状态更新就依赖于htb 的等待队列,那些变黄或者变红的class会加入htb的等待队列,顺便告诉htb自己预计变绿的时间,htb dequeue过程中,在选择class前,会先看看等待队列有没有class需要更新状态,更新了它们的状态,然后再去选择class。

代码分析

接下来从代码上来看看htb qdisc和htb class的创建过程以及htb enqueue/dequeue的过程。

创建htb qdisc

内核使用struct htb_sched来描述htb qdisc,每创建一个htb qdisc,在内核中便会产生一个struct htb_sched类型的结构变量,它的组成大致如下所示:

// linux-5.4.246/net/shced/sch_htb.c
struct htb_sched {
	struct Qdisc_class_hash clhash; // 用于链接class的hash 表,该htb qdisc下所有的htb class都会链接过来
	......
	// 内核中filter存在block-chain-tcf_proto-filter这样的层级结构,按理说qdisc classify的时候需要从block开始向下逐级遍历所有filter,
	// 但实际情况不是这样的,这里的filter_list指针会指向chain0 的首个tcf_proto,qdisc classify的时候会通过filter_list指针来查找filter
	// 也就是说qdisc classify时只会直接匹配chain0上的filter
	struct tcf_proto __rcu	*filter_list;
	struct tcf_block	*block; // 指向block 的指针,输出方向上的qdisc一般只会有一个block
	......
	int			row_mask[TC_HTB_MAXDEPTH]; // TC_HTB_MAXDEPTH值为8,row_mask[*]不为0则表示level *有绿色的class
	struct htb_level	hlevel[TC_HTB_MAXDEPTH]; // 用于记录class的hlevel表,helevel[0]~hlevel[7]分别对应level 0~7,struct htb_level结构体里面还有包含一个hprio数组
};

struct htb_level {
	struct rb_root	wait_pq; // 当前level变黄或者变红的class会链接到wait_pq上
	struct htb_prio hprio[TC_HTB_NUMPRIO]; // TC_HTB_NUMPRIO值为8,hprio[0]~hprio[7]分别对应prio 0~7,一个struct htb_prio就代表一棵树
};

struct htb_prio {
	union {
		struct rb_root	row;
		struct rb_root	feed;
	}; // 根节点
	struct rb_node	*ptr; // 该指针记录着被选中用于dequeue的class的树节点地址
	u32		last_ptr_id; // 记录被选中用于dequeue的class的id
};

内核分配并初始化qdisc公共数据结构的部分就不看了,只看下内核是如何初始化htb_sched的。内核中,htb qdisc注册的Qdisc_ops和Qdisc_class_ops回调函数集如下所示:

// linux-5.4.246/net/shced/sch_htb.c
static const struct Qdisc_class_ops htb_class_ops = {
	.graft		=	htb_graft, // 将leaf qdisc与class建立联系
	.leaf		=	htb_leaf, // 获取class的leaf qdisc
	.qlen_notify	=	htb_qlen_notify,
	.find		=	htb_search, // 根据class id查找class
	.change		=	htb_change_class, // 新建 class 时调用,用于分配 class
	.delete		=	htb_delete, // 删除class
	.walk		=	htb_walk, // 遍历qdisc 下所有class, 对所有class调用 walker.fn
	.tcf_block	=	htb_tcf_block, // 用于从class或者qdisc获取block
	.bind_tcf	=	htb_bind_filter, // 在filter和class的绑定过程中调用
	.unbind_tcf	=	htb_unbind_filter,
	.dump		=	htb_dump_class,
	.dump_stats	=	htb_dump_class_stats,
};

static struct Qdisc_ops htb_qdisc_ops __read_mostly = {
	.cl_ops		=	&htb_class_ops,
	.id		=	"htb",
	.priv_size	=	sizeof(struct htb_sched),
	.enqueue	=	htb_enqueue, // enqueue时被调用
	.dequeue	=	htb_dequeue, // dequeue时被调用
	.peek		=	qdisc_peek_dequeued,
	.init		=	htb_init, // 创建qdisc时被调用
	.reset		=	htb_reset,
	.destroy	=	htb_destroy,
	.dump		=	htb_dump,
	.owner		=	THIS_MODULE,
};

创建htb qdisc时,htb_init函数将被调用,htb_init函数大致内容如下:

// linux-5.4.246/net/shced/sch_htb.c
int htb_init(struct Qdisc *sch, struct nlattr *opt, struct netlink_ext_ack *extack)
{
	struct htb_sched *q = qdisc_priv(sch); // 获取htb qdisc私有数据结构
	......
	
	//1 创建 block,将block地址赋值于q->block
	//2 注册回调,当chain0的首个tcf_proto发生变动时,回调会被调用并将首个tcf_proto的地址赋予q->filter_list
	err = tcf_block_get(&q->block, &q->filter_list, sch, extack);

	......
	
	//3 初始化用于链接class的hash table
	err = qdisc_class_hash_init(&q->clhash);
	......
}

创建htb class

应用层的tc工具添加class时将class的各个参数封装到一个结构体变量中,通过netlink传递到内核(通过rtnetlink RTM_NEWTCLASS消息来传递),这个结构体如下所示:

struct {
	struct nlmsghdr	n;
	struct tcmsg		t; // 部分参数会放置在tcmsg
	char			buf[4096]; // 部分参数以netLink attr的形式存放在buf中
}

添加class时,tc_class_modify函数(位于iproute2-5.11.0/tc/tc_class.c文件)将被调用,tc_class_modify函数会解析一些公共参数,比如classid/dev/parent等,这些命令行参数转化后如下所示:

tc class参数转化后传递到内核的参数
devreq.t.tcm_ifindex
classidreq.t.tcm_handle
rootreq.t.tcm_parent = TC_H_ROOT

应用层代码中每类qdisc都会向tc核心注册一个struct qdisc_util类型的结构体变量,提供回调函数来解析qdisc私有参数,以htb qdisc的qdisc_util如下:

iproute2-5.11.0/tc/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 class时,htb_parse_class_opt函数将被调用来解析私有参数,htb class的私有参数主要就是描述令牌桶的参数,比如rate/ceil/prio/burst/cburst/quantum 等,这些命令行参数转化后如下所示:

htb class私有参数转化后传递到内核的参数
rateopt.rate.rate =5mbit (当rate超过u32最大值时,就会使用TCA_HTB_RATE64来传递)
burstopt.rate.cell_log opt.buffer
ceilopt.ceil.rate (当ceil超过u32最大值时,就会使用TCA_HTB_CEIL64来传递)
cburstopt.ceil.cell_log opt.cbuffer
prioopt.prio
quantumopt.quantum

接下来看看内核中创建class 的代码,内核创建class的代码大致可以分为两部分:公共代码和私有代码。
公共代码部分负责解析classid/dev/parent等参数并完成class和filter的绑定工作,而私有代码部分负责解析私有参数,并分配和初始化描述class的数据结构。

创建class的公共代码分析

创建class时,应用层通过rtnetlink RTM_NEWTCLASS消息将请求传递给内核,内核收到消息时将会调用tc_ctl_tclass函数

// linux-5.4.246/net/shced/sch_api.c
int tc_ctl_tclass(struct sk_buff *skb, struct nlmsghdr *n, struct netlink_ext_ack *extack){
	struct net *net = sock_net(skb->sk);
	struct tcmsg *tcm = nlmsg_data(n);
	struct nlattr *tca[TCA_MAX + 1];
	struct net_device *dev;
	struct Qdisc *q = NULL;
	const struct Qdisc_class_ops *cops;
	unsigned long cl = 0;
	unsigned long new_cl;
	u32 portid;
	u32 clid; // classid
	u32 qid; // qid代表class所属qdisc的id
	int err;

	if ((n->nlmsg_type != RTM_GETTCLASS) &&
	    !netlink_ns_capable(skb, net->user_ns, CAP_NET_ADMIN))
		return -EPERM;
	
	// 1 解析应用层传递过来的参数并放置到tca数组中
	err = nlmsg_parse_deprecated(n, sizeof(*tcm), tca, TCA_MAX,
				     rtm_tca_policy, extack);
	if (err < 0)
		return err;

	// 2 获取dev
	dev = __dev_get_by_index(net, tcm->tcm_ifindex);
	if (!dev)
		return -ENODEV;

	portid = tcm->tcm_parent; // tcm->tcm_parent来自与应用层传递的`parent`参数或者`root`参数
	clid = tcm->tcm_handle; // tcm->tcm_handle 来自于应用层传递的`classid`参数
	qid = TC_H_MAJ(clid); // class所属qdisc的id初始值取自于major(classid)
	
	// 3 计算class所属qdisc的id,从下面的逻辑可以看出应用层传递的classid和parent参数的major部分是用来查找class所属qdisc的,major(classid)和major(parent)不能同时为0
	if (portid != TC_H_ROOT) { // 3.1 应用层传递了`parent` 参数,没有传递`root`参数
	
		u32 qid1 = TC_H_MAJ(portid);
		
		if (qid && qid1) {
			// 3.1.1 应用层同时传递了parent和classid两个参数,它们的major部分都不为0时应该相同,否则报错返回
			if (qid != qid1)
				return -EINVAL;
		} else if (qid1) {
			// 3.1.2 major(parent)不为0,major(classid)为0, 则qdisc的id取自 major(parent)
			qid = qid1;
		} else if (qid == 0)
			// 3.1.3 major(parent)为0,major(classid)为0, 则qdisc的id取自 root qdisc(使用root参数给网卡创建的首个qdisc)
			qid = rtnl_dereference(dev->qdisc)->handle;
		
		// 3.1.4 major(parent)为0,major(classid)不为0, 则qdisc的id保持major(classid)不变

		if (portid)
			portid = TC_H_MAKE(qid, portid);
	} else {
		// 3.2 应用层传递了`root` 参数,没有传递`parent`参数,则qdisc的id取自root qdisc
		if (qid == 0)
			qid = rtnl_dereference(dev->qdisc)->handle;
	}

	// 4 使用第3步中得出的qdisc id来查找class所属qdisc,不存在则报错返回,
	q = qdisc_lookup(dev, qid);
	if (!q)
		return -ENOENT;

	// 5 检查qdisc是否有 Qdisc_class_ops 这个操作函数集,Qdisc_class_ops中的函数在新建/删除/遍历class时会被调用,若没有Qdisc_class_ops则代表该qdisc不支持class
	cops = q->ops->cl_ops;
	if (cops == NULL)
		return -EINVAL;

	// 6 因为应用层传递的major(classid)可能为0,所以这里需要重新拼凑classid。
	if (clid == 0) {
		if (portid == TC_H_ROOT)
			clid = qid;
	} else
		clid = TC_H_MAKE(qid, clid);
	
	// 7 在qdisc中查找目标class
	if (clid)
		cl = cops->find(q, clid);
	
	if (cl == 0) {
		// 7.1 没找到 class,若应用层传递的命令不是新建class则报错
		err = -ENOENT;
		if (n->nlmsg_type != RTM_NEWTCLASS ||
		    !(n->nlmsg_flags & NLM_F_CREATE))
			goto out;
	} else {
		// 7.2 找到了class
		switch (n->nlmsg_type) {
		// 7.2.1 若应用层传递的命令是新建class而不是修改class则报错
		case RTM_NEWTCLASS:
			err = -EEXIST;
			if (n->nlmsg_flags & NLM_F_EXCL)
				goto out;
			break;
		// 7.2.2 若应用层传递的命令是删除class则调用Qdisc_class_ops中删除class的函数然后正常退出
		case RTM_DELTCLASS:
			err = tclass_del_notify(net, cops, skb, n, q, cl);
			/* Unbind the class with flilters with 0 */
			tc_bind_tclass(q, portid, clid, 0);
			goto out;
		// 7.2.3 若应用层传递的命令是获取class则正常退出
		case RTM_GETTCLASS:
			err = tclass_notify(net, skb, n, q, cl, RTM_NEWTCLASS);
			goto out;
		default:
			err = -EINVAL;
			goto out;
		}
	}
	// 7.3 新建class或者修改class的情形会继续执行下面的代码
	
	// 8 指定unshared block时应用层会通过TCA_INGRESS_BLOCK和TCA_EGRESS_BLOCK传递block id,而class不支持shared block,故报错
	if (tca[TCA_INGRESS_BLOCK] || tca[TCA_EGRESS_BLOCK]) {
		NL_SET_ERR_MSG(extack, "Shared blocks are not supported for classes");
		return -EOPNOTSUPP;
	}

	new_cl = cl;
	err = -EOPNOTSUPP;
	// 9 调用Qdisc_class_ops中的`change`函数来新建class或修改class
	if (cops->change)
		err = cops->change(q, clid, portid, tca, &new_cl, extack);
	if (err == 0) {
		tclass_notify(net, skb, n, q, new_cl, RTM_NEWTCLASS);
		// 10 对于新建class的情况,会尝试绑定class和filter
		// tc_bind_tclass 调用 Qdisc_class_ops中的`walk`回调函数遍历qdisc下所有class, 对它们调用 `tc_bind_class_walker` 函数, tc_bind_class_walker 又遍历 qdisc下所有 filter, 对filter调用 `tcf_node_bind` 函数, `tcf_node_bind`则进一步调用filter的来绑定 class
		if (cl != new_cl)
			tc_bind_tclass(q, portid, clid, new_cl);
	}
out:
	return err;
}

从上面的分析可以看到,tc_ctl_tclass主要的工作就是找到class所属qdisc并调用Qdisc_class_ops种的change函数来创建和修改class。
与qdisc的公共部分代码不同,tc_ctl_tclass函数没有分配结构体变量用来描述class,其实不同类型的qdisc描述class的结构体是不同的,class对应结构体变量的分配将由Qdisc_class_ops中的change函数来完成。

htb创建class的私有代码

htb 创建class是由htb_change_class函数来完成的,htb_change_class会分配一个struct htb_class类型的结构体变量并初始化它,htb_class的构造大致如下:

struct htb_class {
	struct Qdisc_class_common common;
	struct psched_ratecfg	rate; // 保证速率
	struct psched_ratecfg	ceil; // 最大速率
	s64			buffer, cbuffer; // 令牌桶大小
	s64			mbuffer;	/* max wait time */
	u32			prio;	// class优先级,值越小dequeue时优先级越高
	int			quantum; // dequeue时,优先级和level相同的class,按照quantum值轮流发送

	struct tcf_proto __rcu	*filter_list;	// htb class比较特殊,它也有filter,但一般只有inner class才会有,classify时会读取inner class的filter来确定目标class。filter_list是指向chain0首个tcf_proto的指针
	struct tcf_block	*block; // 指向block的指针
	int			filter_cnt;

	int			level;		// 当前class所处level
	unsigned int		children; // class的child个数
	struct htb_class	*parent;	// 指向parent class的指针
	......
	/* token bucket parameters */
	s64			tokens, ctokens; // 令牌桶里现有的令牌
	s64			t_c;		// 上一次补充令牌的时间

	union {
		struct htb_class_leaf {
			int		deficit[TC_HTB_MAXDEPTH]; // 优先级和level相同的class,按照quantum值轮流dequeue,这个deficit记录着leaf class在当前轮次输出了多少字节,输出达到quantum值后,选择下一个class开始输出
			struct Qdisc	*q; // 指向leaf qdisc的指针
		} leaf;
		struct htb_class_inner {
			struct htb_prio clprio[TC_HTB_NUMPRIO]; // inner class的clprio表
		} inner;
	};
	s64			pq_key;

	int			prio_activity;	// 表明该class在哪个优先级上被激活(class是绿色和黄色且挂于hlevel表或者clprio表就属于激活状态),比如说inner class因下端prio 0的class变黄而被挂入hlevel,那么inner class的prio_activity的bit0会被置1
	enum htb_cmode		cmode;	// 当前class的状态,HTB_CAN_SEND/HTB_MAY_BORROW/HTB_CANT_SEND
	struct rb_node		pq_node; // class变黄色或者红色后,会链接到一个等待队列上,pq_node是链接节点
	struct rb_node		node[TC_HTB_NUMPRIO];// class是黄色时,node用于链接到parent class的clprio表,class是绿色时,node用于链接到hlevel表
	......
	unsigned int		overlimits;
};

htb_change_class函数大致如下:

// linux-5.4.246/net/shced/sch_api.c
// 入参中, sch是指向qdisc公共结构的指针,通过它可以获取htb的私有数据结构htb_sched
static int htb_change_class(struct Qdisc *sch, u32 classid,
			    u32 parentid, struct nlattr **tca,
			    unsigned long *arg, struct netlink_ext_ack *extack)
{
	int err = -EINVAL;
	struct htb_sched *q = qdisc_priv(sch);
	struct htb_class *cl = (struct htb_class *)*arg, *parent;
	struct nlattr *opt = tca[TCA_OPTIONS];
	struct nlattr *tb[TCA_HTB_MAX + 1];
	struct Qdisc *parent_qdisc = NULL;
	struct tc_htb_opt *hopt;
	u64 rate64, ceil64;
	int warn = 0;

	// 1 解析应用层传递的那些netlink attr
	err = nla_parse_nested_deprecated(tb, TCA_HTB_MAX, opt, htb_policy,
					  NULL);
	......
	// 2 获取parent class
	parent = parentid == TC_H_ROOT ? NULL : htb_find(parentid, sch);
	
	// 3 hopt就是应用层传递的令牌桶参数,里面包含rate/ceil/prio/burst/quantum等参数
	hopt = nla_data(tb[TCA_HTB_PARMS]);
	......

	// htb_change_class 函数既负责新建class也负责修改class,cl为空时表示要新建class
	if (!cl) {	// 4 新建class 
		struct Qdisc *new_q;
		int prio;
		......

		// 4.1 检查classid的major部分和qdisc 的major部分是否相同,检查calssid是否已经被使用
		if (!classid || TC_H_MAJ(classid ^ sch->handle) || tb_find(classid, sch))
			goto failure;

		// 4.2 检查level是否会超过8层
		if (parent && parent->parent && parent->parent->level < 2) {
			pr_err("htb: tree is too deep\n");
			goto failure;
		}
		// 4.3 分配htb class
		cl = kzalloc(sizeof(*cl), GFP_KERNEL);
		......
		//4.4 给class创建 block,将block地址赋值于cl->block
		//注册回调,当block的chain0的首个tcf_proto发生变动时,回调会被调用并将首个tcf_proto的地址赋予cl->filter_list
		err = tcf_block_get(&cl->block, &cl->filter_list, sch, extack);
		......
	
		// 4.5 初始化class部分参数和指针
		cl->children = 0; // child个数置0
		RB_CLEAR_NODE(&cl->pq_node); // 初始化用于链接到等待队列的节点
		for (prio = 0; prio < TC_HTB_NUMPRIO; prio++)
			RB_CLEAR_NODE(&cl->node[prio]); // 初始化用于链接到hlevel表和clprio表的节点

		// 4.6 创建 leaf qdisc, 默认类型pfifo
		new_q = qdisc_create_dflt(sch->dev_queue, &pfifo_qdisc_ops, classid, NULL);
		sch_tree_lock(sch);
		// 4.7 如果parent原本是一个leaf class,则需要释放parent class的leaf qdisc
		if (parent && !parent->level) {
			// 4.7.1 将parent class的leaf qdisc上的数据包清除掉
			qdisc_purge_queue(parent->leaf.q);
			
			parent_qdisc = parent->leaf.q;
			// 4.7.2 将parent class给deactive,deactive就是把class从hlevel表和clprio表上移除
			if (parent->prio_activity)
				htb_deactivate(q, parent);

			// 4.7.3  将parent class从等待队列上移除
			if (parent->cmode != HTB_CAN_SEND) {
				htb_safe_rb_erase(&parent->pq_node, &q->hlevel[0].wait_pq);
				parent->cmode = HTB_CAN_SEND;
			}
			// 4.7.4 parent class从leaf class切换为inner class,则它的level就不能是0了,它的level变为parent->parent->level - 1
			parent->level = (parent->parent ? parent->parent->level
					 : TC_HTB_MAXDEPTH) - 1;
			// 4.7.5 初始化parent class的clprio表
			memset(&parent->inner, 0, sizeof(parent->inner));
		}
		// 4.8 初始化class部分参数和指针
		cl->leaf.q = new_q ? new_q : &noop_qdisc; // leaf qdisc
		cl->common.classid = classid; // classid
		cl->parent = parent; // parent class
		
		// 4.9 初始化令牌桶参数,装满令牌
		cl->tokens = PSCHED_TICKS2NS(hopt->buffer);
		cl->ctokens = PSCHED_TICKS2NS(hopt->cbuffer);
		cl->mbuffer = 60ULL * NSEC_PER_SEC;	/* 1min */
		cl->t_c = ktime_get_ns();
		// 4.10 class 状态切换为HTB_CAN_SEND(绿色)
		cl->cmode = HTB_CAN_SEND;

		// 4.11 将class链接到htb qdisc的hash table上
		qdisc_class_hash_insert(&q->clhash, &cl->common);
		// 4.12 增加parent class的child计数
		if (parent)
			parent->children++;
		// 4.13 将leaf qdisc链接到网卡的全局qdisc链表上去,但该leaf qdisc通过tc qdisc show dev xxxx仍然是观察不到的
		if (cl->leaf.q != &noop_qdisc)
			qdisc_hash_add(cl->leaf.q, true);
	} else{
		......
	}
	
	// 5 计算速率,赋值到htb class对应字段
	rate64 = tb[TCA_HTB_RATE64] ? nla_get_u64(tb[TCA_HTB_RATE64]) : 0;
	ceil64 = tb[TCA_HTB_CEIL64] ? nla_get_u64(tb[TCA_HTB_CEIL64]) : 0;
	psched_ratecfg_precompute(&cl->rate, &hopt->rate, rate64);
	psched_ratecfg_precompute(&cl->ceil, &hopt->ceil, ceil64);

	// 6 如果class是leaf class,则计算quantum和prio
	// quantum值可以通过tc class命令指定,也可以通过速率计算出来
	if (!cl->level) {
		// 6.1 通过速率计算quantum值, 一般是拿速率(Bytes/s)来除以10
		u64 quantum = cl->rate.rate_bytes_ps;

		do_div(quantum, q->rate2quantum);
		cl->quantum = min_t(u64, quantum, INT_MAX);

		if (!hopt->quantum && cl->quantum < 1000) {
			warn = -1;
			cl->quantum = 1000;
		}
		if (!hopt->quantum && cl->quantum > 200000) {
			warn = 1;
			cl->quantum = 200000;
		}
		// 6.2 如果应用层传递了quantum,就用应用层传递的quantum
		if (hopt->quantum)
			cl->quantum = hopt->quantum;
		// 6.3 应用层传递了优先级就用应用层传递的优先级,否则使用TC_HTB_NUMPRIO - 1(7)
		if ((cl->prio = hopt->prio) >= TC_HTB_NUMPRIO)
			cl->prio = TC_HTB_NUMPRIO - 1;
	}
	
	// 7 计算令牌桶大小,以时间为单位
	cl->buffer = PSCHED_TICKS2NS(hopt->buffer);
	cl->cbuffer = PSCHED_TICKS2NS(hopt->cbuffer);
	......
}

简单来看,htb_change_class函数完成的工作如下:

  • 分配htb_class并把应用层传递的令牌桶参数记录下来(ceil/rate/burst/cburst/prio/quantum)
  • 给htb class创建leaf qdisc,销毁parent class的leaf qdisc
  • 初始化htb class的状态(HTB_CAN_SEND)
    此时的htb class既没有挂入hlevel表也没有挂入clprio表,还不能用于dequeue,当有数据包enqueue到它时,它才会被挂入hlevel表。

active和deactive

htb enqueue/dequeue的流程中会频繁的涉及两个操作:active和deactive。
这里简单的解释下它们的含义:

  • active: 激活class,把class放到hlevel表或者clprio表上去,使得class可以被dequeue
  • deactive: 禁用class,把class从hlevel表和clprio表移除,使得class得不到dequeue的机会
    有两个函数分别对应于active/deactive,这里也先给出它们的内容,便于理解后面的enqueue/dequeue流程
// 一般class在从红色变成黄色或绿色,或者黄色变成绿色时会调用htb_activate_prios函数,需要特别注意的是调用htb_activate_prios时,cl->cmode已经是新状态了
static void htb_activate_prios(struct htb_sched *q, struct htb_class *cl)
{
	struct htb_class *p = cl->parent;
	// mask代表着cl指针指向的class要active的优先级(cl指针在下面的处理流程中是可能变化的)
	long m, mask = cl->prio_activity;
	
	// 1 如果class是黄色,那么它的active其实就是加入parent class的clprio表,
	// 在class active的同时,我们也得看看它的parent是不是黄色,需不需要把parent加入parent->pranet的clprio表
	while (cl->cmode == HTB_MAY_BORROW && p && mask) {
		// 在这个循环中,mask暂时代表着parent class要active的优先级,mask初始值是class要active的优先级
		m = mask;
		while (m) {
			unsigned int prio = ffz(~m);

			if (WARN_ON_ONCE(prio >= ARRAY_SIZE(p->inner.clprio)))
				break;
			m &= ~(1 << prio);
			
			// 1.1 如果parent在当前优先级上已经active了,那么就就把mask的prio为置0
			if (p->inner.clprio[prio].feed.rb_node)
				/* parent already has its feed in use so that
				 * reset bit in mask as parent is already ok
				 */
				mask &= ~(1 << prio);
			
			// 1.2 将class加入parent class的clprio表
			htb_add_to_id_tree(&p->inner.clprio[prio].feed, cl, prio);
		}
		// 1.3 mask代表着parent class要active的优先级,这里更新parent class处于active状态的优先级
		p->prio_activity |= mask;
		// 1.4 接下来在mask所表示的那些优先级上active parent, 这个循环将持续到parent不是黄色(绿色),或者没有parent了,或者mask为0(parent在child class active的优先级上已经有其他的child class处于黄色)
		cl = p;
		p = cl->parent;

	}
	// 2 到达这一步时,cl有可能还指向最初那个要active的class(如果最初那个class是绿色就不会经过上面步骤1中的流程),也有可能指向某个parent class了
	// 如果cl是绿色,且cl有要active的优先级(mask不为0),则调用htb_add_class_to_row函数,htb_add_class_to_row函数会把class 加入hlevel表,当然只是在mask所指示的那些优先级加入
	if (cl->cmode == HTB_CAN_SEND && mask)
		htb_add_class_to_row(q, cl, mask);
}

// 一般class在黄色或绿色变成红色时或者绿色变成黄色时,会调用htb_deactivate_prios函数,需要特别注意的是调用htb_deactivate_prios时,cl->cmode还是老的状态
static void htb_deactivate_prios(struct htb_sched *q, struct htb_class *cl)
{
	struct htb_class *p = cl->parent;
	// mask代表着cl指针指向的class要deactive的优先级(cl指针在下面的处理流程中是可能变化的)
	long m, mask = cl->prio_activity;

	// 1 如果class是黄色,那么它的deactive其实就是从parent class的clprio表移除,
	// 在deactive的同时,我们也得看看它的parent是不是黄色,需不需要从parent->pranet的clprio表移除
	while (cl->cmode == HTB_MAY_BORROW && p && mask) {
		// 在这个循环中,mask暂时代表着parent class要deactive的优先级
		m = mask;
		mask = 0;
		while (m) {
			int prio = ffz(~m);
			m &= ~(1 << prio);

			if (p->inner.clprio[prio].ptr == cl->node + prio) {
				/* we are removing child which is pointed to from
				 * parent feed - forget the pointer but remember
				 * classid
				 */
				p->inner.clprio[prio].last_ptr_id = cl->common.classid;
				p->inner.clprio[prio].ptr = NULL;
			}
			
			// 1.1 将class从parent class的clprio表中移除
			htb_safe_rb_erase(cl->node + prio, &p->inner.clprio[prio].feed);
			
			// 1.2 判断parent class在该优先级上还有没有其他的child class处于黄色
			if (!p->inner.clprio[prio].feed.rb_node)
				mask |= 1 << prio; // 如果没有了,将mask的prio位置1
		}
		
		// 1.3 mask代表着parent class要deactive的优先级,这里更新parent class处于active状态的优先级
		p->prio_activity &= ~mask;
		// 1.4 接下来在mask所表示的那些优先级上deactive parent, 这个循环将持续到parent不是黄色(绿色),或者没有parent了,或者mask为0(parent在child class deactive的优先级上还有其他的child class处于黄色)
		cl = p;
		p = cl->parent;

	}
	
	// 2 到达这一步时,cl有可能还指向最初那个要deactive的class(如果最初那个class是绿色就不会经过上面步骤1中的流程),也有可能指向某个parent class了
	// 如果cl是绿色,且cl有要deactive的优先级(mask不为0),则调用htb_remove_class_from_row函数,htb_remove_class_from_row函数会把class 从hlevel表上移除,当然只移除mask所指示的那些优先级
	if (cl->cmode == HTB_CAN_SEND && mask)
		htb_remove_class_from_row(q, cl, mask);
}

enqueue

对于htb qdisc而言, enqueue时,htb_enqueue函数将被调用,它的实现大致如下:

static int htb_enqueue(struct sk_buff *skb, struct Qdisc *sch,
		       struct sk_buff **to_free)
{
	int ret;
	unsigned int len = qdisc_pkt_len(skb);
	struct htb_sched *q = qdisc_priv(sch);
	// 1 classify,选择一个class用于enqueue
	struct htb_class *cl = htb_classify(skb, sch, &ret);

	......
	// 2 将skb enqueue到class的leaf qdisc
	else if ((ret = qdisc_enqueue(skb, cl->leaf.q, to_free)) != NET_XMIT_SUCCESS) {
		// enqueue 失败
	} else {
	// 3 enqueue成功,激活class
		htb_activate(q, cl);
	}
	
	// 增加htb qdisc的统计值,qlen代表着该qdisc总共有多少个skb
	sch->qstats.backlog += len;
	sch->q.qlen++;
	return NET_XMIT_SUCCESS;
}

htb_classify根据skb携带的信息来确定目标class,qdisc_enqueue则进一步调用leaf qdisc的enqueue函数。若leaf qdisc是默认的fifo qdisc,数据包就会直接缓存在fifo qdisc上,若leaf qdisc是其它类型的qdisc,则在leaf qdisc在enqueue过程中可能又会经历classify然后enqueue到更下级leaf qdisc的过程。
htb_classify会使用两个方式来确定目标class:

  • 根据skb携带的priority字段来确定class
  • 查找filter,根据filter指定的classid来确定目标class

htb_classify函数大致如下:

// linux-5.4.246/net/shced/sch_htb.c
static struct htb_class *htb_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
{
	struct htb_sched *q = qdisc_priv(sch); // q指向htb qdisc
	struct htb_class *cl; // cl将用于指向目标class
	struct tcf_result res;  // res用于存储classify的结果
	struct tcf_proto *tcf;
	int result;

	// 1 根据 skb->priority来查找class
	if (skb->priority == sch->handle)
		return HTB_DIRECT;	/* X:0 (direct flow) selected */
	cl = htb_find(skb->priority, sch);
	if (cl) {
		// 1.1 若class是leaf class,直接返回
		if (cl->level == 0)
			return cl;
		// 1.2 若class是inner class,则还需要通过inner class的filter_list上的filter来确定目标class(htb的class可以添加filter,所以这里使用class的filter_list来查找)
		tcf = rcu_dereference_bh(cl->filter_list);
	} else {
		// 2 使用skb->priority找不到class,则使用htb qdisc的filter_list上的filter来确定目标class
		tcf = rcu_dereference_bh(q->filter_list);
	}

	*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
	// 3 对filter_list调用tcf_classify函数,tcf_classify是一个通用函数,很多qdisc在classify的时候都会调用它
	// tcf_classify会遍历filter_list上的tcf_proto,使用skb与tcf_proto包含的filter进行匹配,并返回结果
	while (tcf && (result = tcf_classify(skb, tcf, &res, false)) >= 0) {
		......
		// 4 tcf_classify 会将匹配结果存放在res.class,这里判断查找到的class是否是leaf class,若是则直接返回
		cl = (void *)res.class;
		if (!cl) {
			if (res.classid == sch->handle)
				return HTB_DIRECT;	/* X:0 (direct flow) */
			cl = htb_find(res.classid, sch);
			if (!cl)
				break;	/* filter selected invalid classid */
		}
		if (!cl->level) // 4.1 tcf_classify的结果是leaf class,直接返回
			return cl;	/* we hit leaf; return it */

		// 4.2 tcf_classify的结果是inner class,则继续通过inner class的filter_list上的filter来确定目标class
		tcf = rcu_dereference_bh(cl->filter_list);
	}
	......
}

tcf_classify 函数如下:

// linux-5.4.246/net/shced/cls_api.c
int tcf_classify(struct sk_buff *skb, const struct tcf_proto *tp, struct tcf_result *res, bool compat_mode)
{
	......
	// 1 遍历filter_list上的所有tcf_proto,filter_list上的tcf_proto按优先级排列,优先级高的先被匹配
	for (; tp; tp = rcu_dereference_bh(tp->next)) {
	
		// 2 使用skb携带的protocol与tcf_proto的protocol进行对比
		__be16 protocol = skb_protocol(skb, false);
		int err;

		if (tp->protocol != protocol &&
		    tp->protocol != htons(ETH_P_ALL))
			continue;
		
		// 3 protocol相同,调用tcf_proto的classify回调来匹配skb与tcf_proto内部包含的filter。
		err = tp->classify(skb, tp, res);
		......
	}
	......
}

tcf_proto的classify回调是在创建filter时被赋值的,它的值与filter的类型相关,不同类型的filter的classify回调实现各不同,但它们的目的是相同的,它们都会遍历tcf_proto下的所有filter,按照自身的逻辑来匹配skb和filter,并返回被命中的filter指向的class。

htb_activate函数如下:

static inline void htb_activate(struct htb_sched *q, struct htb_class *cl)
{
	WARN_ON(cl->level || !cl->leaf.q || !cl->leaf.q->q.qlen);

	if (!cl->prio_activity) {
		// 给cl->prio_activity赋值,表示该leaf class在cl->prio优先级上被激活了,当然leaf class只可能在自己的优先级上被激活,inner class才可能多个优先级上被激活
		cl->prio_activity = 1 << cl->prio;
		// 此时的class是绿色的,htb_activate_prios函数会直接将他放入到hlevel表
		htb_activate_prios(q, cl);
	}
}

简单来看,htb enqueue的过程主要就是通过skb的priority或者通过filter来确定class,确定class后将skb enqueue到class的leaf qdisc,然后激活htb class使得它可以被dequeue。

dequeue

对于htb qdisc而言, dequeue时,htb_dequeue函数将被调用,它的实现大致如下:

// linux-5.4.246/net/shced/sch_htb.c
static struct sk_buff *htb_dequeue(struct Qdisc *sch)
{
	struct sk_buff *skb;
	struct htb_sched *q = qdisc_priv(sch);
	int level;
	s64 next_event;
	unsigned long start_at;

	......
	
	if (!sch->q.qlen)
		goto fin;
	q->now = ktime_get_ns();
	start_at = jiffies;

	next_event = q->now + 5LLU * NSEC_PER_SEC;
	
	// 从level 0开始,逐层查找绿色的class
	for (level = 0; level < TC_HTB_MAXDEPTH; level++) {
		int m;
		
		// 1 那些变黄色或者红色的class会被挂于等待队列上,这里的几行代码试图去更新那些class的状态
		// 加入该level下有几个class变了黄色或红色,near_ev_cache[level]保存的就是最先可能变绿色的class的状态切换时间点 
		s64 event = q->near_ev_cache[level];
		if (q->now >= event) {
			// 1.1 q->now >= event表示当前有class经过时间的累积,已经可有切换状态了
			// htb_do_events 负责对那些需要切换状态的class调用htb_charge_class函数来切换它们的状态,顺带更新它们的令牌
			event = htb_do_events(q, level, start_at);
			if (!event)
				event = q->now + NSEC_PER_SEC;
			q->near_ev_cache[level] = event;
		}

		if (next_event > event)
			next_event = event;
		
		// 2 遍历该level上处于激活状态的prio
		m = ~q->row_mask[level];
		while (m != (int)(-1)) {
			int prio = ffz(m);

			m |= 1 << prio;
			// 调用htb_dequeue_tree对从处于[level,prio]的class中选出一个,然后使用它来dequeue
			skb = htb_dequeue_tree(q, prio, level);
			if (likely(skb != NULL))
				goto ok;
		}
	}
	......
fin:
	return skb;
}

htb_dequeue_tree函数大致如下

static struct sk_buff *htb_dequeue_tree(struct htb_sched *q, const int prio,
					const int level)
{
	struct sk_buff *skb = NULL;
	struct htb_class *cl, *start;
	struct htb_level *hlevel = &q->hlevel[level];
	struct htb_prio *hprio = &hlevel->hprio[prio];

	// 1 从hlevel[level].hprio[prio]这个树结构中选出一个绿色class,若这个class是inner class,则还要从inner class的clprio[prio]树结构中选择一个黄色class 
	start = cl = htb_lookup_leaf(hprio, prio);

	do {
next:
		......
		
		// 2 如果选出的class上已经没有数据包了,那么禁用该class
		if (unlikely(cl->leaf.q->q.qlen == 0)) {
			struct htb_class *next;
			// 2.1 htb_deactivate 直接调用htb_deactivate_prios函数来禁用class
			htb_deactivate(q, cl);

			/* row/level might become empty */
			if ((q->row_mask[level] & (1 << prio)) == 0)
				return NULL;
			
			// 2.2 从树结构中选择其他class
			next = htb_lookup_leaf(hprio, prio);

			if (cl == start)	/* fix start if we just deleted it */
				start = next;
			cl = next;
			goto next;
		}
		
		// 3 从class的leaf qdisc去dequeue
		skb = cl->leaf.q->dequeue(cl->leaf.q);
		if (likely(skb != NULL))
			break;
		
		// 4 如果dequeue不出数据包(比如说leaf qdisc是一个tbf qdisc,它因为达到速率限制无法dequeue数据包),更新树结构记录class的指针,移向下一个节点
		htb_next_rb_node(level ? &cl->parent->inner.clprio[prio].ptr:
					 &q->hlevel[0].hprio[prio].ptr);
		// 从树结构中选择下一个class
		cl = htb_lookup_leaf(hprio, prio);

	} while (cl != start);

	if (likely(skb != NULL)) {
		// 5 level和prio相同的class按quantum值轮流输出
		// cl->leaf.deficit初始值是cl->quantum, 每dequeue多少kb的报文,它就减少多少,当小于0时,就轮到下一个class输出了
		cl->leaf.deficit[level] -= qdisc_pkt_len(skb);
		if (cl->leaf.deficit[level] < 0) {
			cl->leaf.deficit[level] += cl->quantum;
			// 更新树结构记录class的指针,移向下一个节点
			htb_next_rb_node(level ? &cl->parent->inner.clprio[prio].ptr :
						 &q->hlevel[0].hprio[prio].ptr);
		}
		
		// 6 如果class没有数据包了,禁用它
		if (!cl->leaf.q->q.qlen)
			htb_deactivate(q, cl);
		
		// 7 更新class的令牌桶和状态,扣除class令牌的过程中同步扣除parent class的令牌
		htb_charge_class(q, cl, level, skb);
	}
	return skb;
}

htb_lookup_leaf函数大致如下:

static struct htb_class *htb_lookup_leaf(struct htb_prio *hprio, const int prio)
{
	int i;
	struct {
		struct rb_node *root;
		struct rb_node **pptr;
		u32 *pid;
	} stk[TC_HTB_MAXDEPTH], *sp = stk;

	BUG_ON(!hprio->row.rb_node);
	sp->root = hprio->row.rb_node;
	sp->pptr = &hprio->ptr; // 1 hprio->ptr记录着树结构上一次用于dequeue的class,尝试继续使用该class
	sp->pid = &hprio->last_ptr_id; // hprio->ptr记录着树结构上一次用于dequeue的class id

	
	for (i = 0; i < 65535; i++) {
		if (!*sp->pptr && *sp->pid) {
			*sp->pptr = htb_id_find_next_upper(prio, sp->root, *sp->pid);
		}
		*sp->pid = 0;	
		// 2 *sp->pptr还为空,表示已经到达了树的最右侧的节点,此时应该把当前树结构的指针移动到最左侧节点,并且返回到上一层的树结构去选择下一个节点
		if (!*sp->pptr) {	/* we are at right end; rewind & go up */
			*sp->pptr = sp->root;
			while ((*sp->pptr)->rb_left)
				*sp->pptr = (*sp->pptr)->rb_left; // 2.1 把当前树结构的指针移动到最左侧节点
			if (sp > stk) {
				sp--; // 2.2 返回到上一层的树结构
				if (!*sp->pptr) {
					WARN_ON(1);
					return NULL;
				}
				htb_next_rb_node(sp->pptr);
			}
		} else {
		// 3 *sp->pptr不为空
			struct htb_class *cl;
			struct htb_prio *clp;
			// 判断该class是否是leaf class,若是则返回,若不是则查看inner class的clprio[prio]树
			cl = rb_entry(*sp->pptr, struct htb_class, node[prio]);
			if (!cl->level)
				return cl;
			clp = &cl->inner.clprio[prio];
			(++sp)->root = clp->feed.rb_node; // 移动到下层树结构,去查找leaf class
			sp->pptr = &clp->ptr;
			sp->pid = &clp->last_ptr_id;
		}
	}
	WARN_ON(1);
	return NULL;
}

htb_charge_class函数大致如下:

static void htb_charge_class(struct htb_sched *q, struct htb_class *cl,
			     int level, struct sk_buff *skb)
{
	int bytes = qdisc_pkt_len(skb);
	enum htb_cmode old_mode;
	s64 diff;

	// 这个循环中会扣除class和它parent以及parent的parenat的令牌(所有parent)
	while (cl) {
		// 1 计算新增的令牌数
		diff = min_t(s64, q->now - cl->t_c, cl->mbuffer);
		// 2 更新令牌
		if (cl->level >= level) {
			if (cl->level == level)
				cl->xstats.lends++;
			// 2.1 cl->level >= level表示cl指向的是被借用速率的parent class(绿色状态的parent)或者它的更上级parent,给它的rate令牌桶增加diff个令牌,同时扣除skb所需的令牌
			htb_accnt_tokens(cl, bytes, diff);
		} else {
			// 2.2 cl->level < level表示cl指向的是借用速率的class,给它的rate令牌桶增加diff个令牌
			// 这里没有扣除skb所需的令牌其实有点奇怪,因为class是否达到rate和ceil速率是通过判断rate和ceil两个令牌桶剩余的令牌数目来间接判断的
			// 这里增加了rate令牌桶的令牌会导致下面的htb_change_class_mode函数将class的状态判断为绿色,这会导致class在绿色和黄色之间反复切换
			cl->xstats.borrows++;
			cl->tokens += diff;	/* we moved t_c; update tokens */
		}
		// 2.3 给它的ceil令牌桶增加diff个令牌,但扣除skb所需的令牌
		htb_accnt_ctokens(cl, bytes, diff);
		cl->t_c = q->now;

		old_mode = cl->cmode;
		diff = 0;
		// 3 更新class的状态
		htb_change_class_mode(q, cl, &diff);
		if (old_mode != cl->cmode) {
			// 下面的几行代码根据class的状态来决定要不要把它加进等待队列,这里列出它们切换状态时会调用的函数,其实黄色和红色都会被链入等待队列
			// 红色->黄色: htb_safe_rb_erase 和 htb_add_to_wait_tree
			// 红色->绿色: htb_safe_rb_erase
			// 黄色->绿色: htb_safe_rb_erase
			// 黄色->红色: htb_add_to_wait_tree
			// 绿色->黄色: htb_add_to_wait_tree
			// 绿色->红色: htb_add_to_wait_tree
			if (old_mode != HTB_CAN_SEND)
				htb_safe_rb_erase(&cl->pq_node, &q->hlevel[cl->level].wait_pq); // 从等待队列移除
			if (cl->cmode != HTB_CAN_SEND)
				htb_add_to_wait_tree(q, cl, diff); // 加入等待队列
		}

		......

		// 4 cl指针指向parent,再去更新parent的令牌桶和状态
		cl = cl->parent; 
	}
}

htb_change_class_mode函数大致如下:

static void
htb_change_class_mode(struct htb_sched *q, struct htb_class *cl, s64 *diff)
{
	// 1 根据rate和ceil令牌桶令牌获取新状态
	enum htb_cmode new_mode = htb_class_mode(cl, diff);
	
	// 2 新旧状态相同,直接返回
	if (new_mode == cl->cmode)
		return;
	......
	
	// 3 如果class上有优先级被激活,则需要根据状态的变动情况来禁用或激活class
	if (cl->prio_activity)  {
		// 红色->黄色: htb_activate_prios
		// 红色->绿色: htb_activate_prios
		// 黄色->绿色: htb_deactivate_prios 和 htb_activate_prios
		// 黄色->红色: htb_deactivate_prios
		// 绿色->黄色: htb_deactivate_prios 和 htb_activate_prios
		// 绿色->红色: htb_deactivate_prios
		if (cl->cmode != HTB_CANT_SEND)
			htb_deactivate_prios(q, cl); // 在cl->prio_activity所指示的优先级上禁用class,如果有必要,禁用上层的parent class
		cl->cmode = new_mode;
		if (new_mode != HTB_CANT_SEND)
			htb_activate_prios(q, cl); // 在cl->prio_activity所指示的优先级上激活class,如果有必要,激活上层的parent class
	} else
		cl->cmode = new_mode;
}

htb_class_mode函数大致如下:

static inline enum htb_cmode
htb_class_mode(struct htb_class *cl, s64 *diff)
{
	s64 toks;
	
	// 1 ceil 桶的令牌不足时代表着class的dequeue速率超过了ceil,状态为红色
	if ((toks = (cl->ctokens + *diff)) < htb_lowater(cl)) {
		*diff = -toks;
		return HTB_CANT_SEND;
	}
	
	// 2 rate 桶的令牌充足时代表着class的dequeue速率没有超过rate,状态为绿色
	if ((toks = (cl->tokens + *diff)) >= htb_hiwater(cl))
		return HTB_CAN_SEND;
	
	// 3 速率在rate和ceil间,状态为黄色
	*diff = -toks;
	return HTB_MAY_BORROW;
}

dequeue时,htb 选择class的方式可以概括为:
1 在hlevel中选择level中最低和优先级最高的树,从树上选择一个节点
2 若节点对应的class是leaf class,则从该class输出quantum字节,然后切换到树结构的下一个节点
3 若节点对应的class是inner class,则从该class的clprio表中选择优先级最高的树,从树上选择一个节点
4 若节点对应的class是leaf class就重复步骤2的动作,若节点对应的class是inner class重复步骤3的动作

参考文档

http://luxik.cdi.cz/~devik/qos/htb/manual/theory.htm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值