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会形成树状的结构,如上图所示,这个树状结构中最多存在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 qdisc开始enqueue了,6个leaf class都被逐渐被enqueue了数据包,随着enqueue的进行,E/F/G/H/I/J会根据自身的优先级链接到hlevel[0]对应的树上去,如下所示
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]的树结构,如下所示:
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的状态如下所示:
此时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]上移除,如下图所示:
此时此时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就会开始丢弃数据包。
随着时间的推移,有的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参数 | 转化后传递到内核的参数 |
---|---|
dev | req.t.tcm_ifindex |
classid | req.t.tcm_handle |
root | req.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私有参数 | 转化后传递到内核的参数 |
---|---|
rate | opt.rate.rate =5mbit (当rate超过u32最大值时,就会使用TCA_HTB_RATE64来传递) |
burst | opt.rate.cell_log opt.buffer |
ceil | opt.ceil.rate (当ceil超过u32最大值时,就会使用TCA_HTB_CEIL64来传递) |
cburst | opt.ceil.cell_log opt.cbuffer |
prio | opt.prio |
quantum | opt.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