首先说说Buffer Management, 这里的buffer管理是针对当buffer overflow时,系统可能将会产生丢包情况时,如何处理新packet的buffer问题。实现Buffer管理的目标是
一般buffer管理是当可能要发生拥塞的时候主动通知发送方,或选择一定报文丢弃,来使发送方降低发送速率,从而降低整体丢包率,维持系统吞吐量。下面介绍几种典型的buffer管理算法。
首先是RED(Random Early Detection),这种方法对queue的长度进行监控,当长度超过一定限度后,将对于每个输入的packet按照queue长度计算一个概率值,根据此概率值决定是否丢掉此packet,packet被丢掉的概率随queue长度增加而增大。RED的实现分为两步,第一步需要估计队列的预期长度,一般方法为 avg_len = avg_len + w * (q_size – avg_len),第二步根据avg_len计算是否丢掉此packet。
基于RED还有WRED(weighted RED),用于处理不同优先级的packet问题,各个优先级有自己的queue,从而有自己的w。另外还有利用TCM算法判断一个packet是否是应有服务范围内,从而对应不同的队列长度要求和概率计算方法。
Packet Scheduling
所有的QoS服务都是基于各种调度方式,packet scheduling是包交换网络中QoS最核心的要素,因为调度方法负责决定现在应该对那个排队queue进行服务,对哪个packet进行传输。如何选择一个合适的调度算法,需要对自身的需求进行分析,另外不同调度方法提供的性能也不同,当然也有不同的复杂度。
评估一个调度算法的性能,除了一般算法的那些性能参数外,还有一个参数就是要评估调度的公平性(fairness),其中常用的公平标准,比如Jain’s fairness index:

,其中n是queue的数量,x_i是queue_i在单位时间内接受服务的百分比。
另外还有通过Fair Index(FI)和Service Fair Index(SFI)共同来判断调度公平性的方法,其中FI定义为

,其中Si(t1, t2)为queue_i在时间t1到t2内接受的服务的次数,SFI定义为SFI = |FI_i – FI_j|,即queue间FI的差值。如果一个调度算法的SFI始终为0,那么这个调度算法对于任意的queue都是公平的。另外还有一种比较好的描述公平性的方法是调度的最大延迟作为指标的方法(worst-case packet fair),比如如果是一个公平的调度算法,如果queue_i当前队列长度为Qi,其CIR为ri,那么如果是完全公平的调度算法,那么当前queue_i新接收的packet的得到调度最大延迟应该是Qi / ri,假设当前算法实际能保证的最大延迟是(Qi / ri + Ci),那么将各个queue的Ci归一化后Ci = Ci * ri / r,那个这个调度算法的WFI(Worst-case Fair Index)为WFI = max(Ci)。
下面介绍几种简单的调度算法。
First In, First Out
FIFO方法没有优先级的概念,完全按照packet进入系统的时间来决定的,对各个queue来说,也没有什么FI的概念,如下图所示:
Strict Priority
这个就是完全按照优先级进行调度,每次调度按照优先级顺序依次检查各个队列是否有packet要发送,也没有什么FI的概念,高优先级的queue永远阻塞低优先级的queue,如下图所示
Round Robin
这个也没有优先级,调度时依次对各个queue进行轮询,每次调度首先检查上次调度的下一个queue,如果有pkt要发送就选择此pkt,否则检查下一个queue。某种程度上,这个调度算法考虑了一定的公平性,但是完全忽略的pkt的优先级。如下图所示:
Weighted Round-Robin
综合考虑priority和RR就是WRR方法。WRR方法为每个queue指定一个weight,这个值和queue的优先级成正比;WRR调度时也是基于RR,但是对于一个queue每调度一次将此queue的weight值减一,如果此queue的weight已经为0,那么在此轮中此queue将不会被调度直到所有的queue的weight都为0,此轮结束;在一轮结束后将会把所有queue的weight值恢复到指定值。这样weight值高的queue在一轮中被调度的次数将多于weight低的queue,从而实现优先级。比如三个queue,A,B,C,各自的weight分别为1,2,3那么如果各个queue都始终有packet要发送的情况下,调度情况可能是ABCBCCABCBCC,也可能是ABBCCCABBCCC,也可能是ACBCBCACBCBC,很明显最后一个的调度的公平性较好。方法如下图所示:
上面介绍的各个调度算法都有很大局限性,没有对各个priority的queue的公平性进行很多考虑,下面介绍几种packet fair queueing方法
Packet Fair Queuing Scheduler
前面介绍的几种简单调度算法,都没有很好的考虑调度的公平性。比如在调度决策时没有考虑实际要发送的这个pkt的长度,如果在一个低优先级上一个长度很长的pkt,将会占用输出端口相对很长的时间,可能造成高优先级的pkt等待时间过长。对于不同优先级的流调度,最理想的情况就是将各个流都想象成为流体的形式,无论何时整个输出管道都可以同时为所有的输入流进行服务,不过不同优先级对输出带宽的占用比例不同,这种理想的调度一般成为Generalized Processor Sharing(GPS)。如下图所示:
上图可以看到,在基于流的模式下可以同时对多个pkt进行发送,而实际在于我们基于pkt的网络中这是不可能实现的,但是这种调度方式代表了一个理想的公平调度,可作为现实比较各种调度算法的标准,也可以给实际的调度算法的研究很多启发。
在上面的基于流的调度算法,在任意时刻t,queue_i收到的调度服务比例为:g_i(t) = r * w_i / sum(w_j, for j in B(t)),其中r为整个输出带宽,w_i是queue_i的weight,B(t)是在时间t所有的active queue。由于GPS无法用于pkt调度,因此只能利用模拟的方法,来使实际的调度算法来逼近GPS的性能。
在实际中,通过在系统中引入一个虚拟系统时间V(t)的概念,来模拟GPS中的流的概念。通常利用V(t) 模拟当采用GPS调度时,每个队列第一个pkt应当离开系统的时间(finish time, FT),然后按照调度时按照pkt的FT进行调度,这种调度策略一般称为Smallest virtual Finish time First (SFF)。因此对于调度器的设计转变为对V(t)函数的选择和对FT的计算方法的选择。首先我们考虑一个相对简单的情况,就是所有队列都有很多pkt在排队的情况,即不存在空的队列的情况。这种情况下,一个队首pkt的FT,在GPS调度的情况下可以计算为:F(i, k) = F(i, k-1) + L(i, k) / Ri,其中F(i, k)为queue_i的第k的pkt的理想结束时间,L(i, k)为这个pkt的长度,Ri是queue_i在总带宽中分配的带宽。因此queue_i中第k个pkt的结束时间就是第k-1个pkt的结束时间再加上利用queue_i的带宽发送第k个pkt的时间,与GPS中流的概念完全符合。但是,实际情况中queue总是有空的情况,这种情况下此queue中新来的pkt的FT如何进行计算就需要借助V(t)函数,V(t)是系统虚拟时间,函数必须满足时间的性质,即加入t1 > t2,那么V(t1) > V(t2)。此时对于F(i, k)的计算方式为:F(i, k) = max(V(a_k), F(i, k-1) )+ L(i, k) / Ri,其中a_k是队列queue_i中第k个pkt的到达时间,V(a_k)是其到达的系统虚拟时间,因此对于队列为空时,pkt的FT就是其到达时间再加上其发送时间,也符合GPS中流的概念。因此我们利用FT进行调度的方法应该可以完成对GPS算法的模拟,具体算法的调度公平性将很大取决于我们对V(t)函数的选择。
Deficit Round-Robin
DRR也是基于RR的一个调度算法,但是这个算法考虑了pkt length。对于DRR,每个queue都有一个deficit,而且queue的deficit值在每个单位时间都会增加,但是每发送一个pkt将会消耗此pkt length的deficit,而只有一个queue的deficit值大于发送queue head的那个pkt所需要的deficit时,此queue才会成为active queue,而一个active queue在发送完一个pkt后,造成当前的deficit不够完成后续pkt的发送时将会不再是active queue;DRR的调度就是对当前所有的active queue进行RR调度。具体如下图所示:
Weighted Fair Queuing (WFQ)
WFQ是一种基于SFF策略进行调度的算法。他定义其V(t)为
(1)V(0)= 0,
(2)V(t + dt) = V(t) + dt * r / sum(r_i, for i in B(t, t+dt)),
其中r为整个输出带宽,r_i为queue_i的输出带宽,B(t, t+dt)为[t, t+dt]时间内active queue的集合。
这个算法可以解释为,比如对于queue_i,第k个pkt的到达时间为t,第k+1个pkt的到达时间为(t+dt),那个k+1的pkt的虚拟到达时间就是V(t)再加上dt乘上一个倍数,这个倍数就是在dt时间内所有active queue的带宽和占总带宽的比例的倒数,如此计算是因为当active queue的带宽和小于总带宽的时候,就相当于各个queue受到的服务是多于其实际应有的比例的,因此也可以认为是系统的时间加快了,所以active queue在dt时间内获得了这个倍数比例的服务。
具体FT的计算和前面提到的方法相同,F(i, k) = max(V(a_k), F(i, k-1) )+ L(i, k) / Ri
WFQ是一种经典的packet fair queuing的调度算法,下面我们分析一下他的性能。一个调度算法性能主要从两个方面进行分析,计算复杂度和delay bound。首先对计算复杂度进行分析,通过上面的介绍,我们可以看出WFQ对每个pkt的FT的计算复杂度是O(1),其中需要维护系统中所有active queue的sum(r_i),这个采用增量更新的方式O(1)实现,并且保存每个queue最后一个pkt的FT,以及整个调度的最后一个pkt的VT,因此利用上面的结果O(1)可以完成对FT的计算。WFQ调度采用SFF策略,选择所有pkt中FT最小的pkt进行调度,由于每个class的queue都是FIFO,即对一个class当前队列中所有pkt的FT是单调递增的,因此也就是对所有queue的HOL pkt中选择最小的FT进行调度,这个操作的复杂度是O(logN)。因此WFQ的复杂度就是O(logN)。
对于scheduler的delay bound的定义是其调度时间相对于GPS调度时间的最大延迟时间:一个pkt在t1时间入队,如果采用GPS调度在t2时间被调度到,而实际scheduler在t2’时间才调度到,这个scheduler的delay bound就是max(t2’ – t2)。一个scheduler的delay bound越好,说明其对GPS的近似越好,因此公平性也越好。已有证明,WFQ的delay bound为N/2 * Lmax / r,其中N为queue的数目,Lmax为最大包长,r为链路发送速率。也就是说WFQ的delay bound与queue数目呈线性关系,这也是WFQ调度的一个主要缺陷,为此提出了WF2Q的调度方式,可以实现delay bound与queue数目无关,而只由Lmax和r决定。
接下来将会对Linux中QoS实现进行简单介绍。
QoS in Linux
前面介绍了QoS的各种参数,实现QoS的各种算法,下面结合Linux介绍一个系统如何QoS(当然,Linux系统和实际网络设备中运行的系统还是有很大差距,但是对于QoS的基本思想是一致的)。下面首先介绍一下Linux中queuing的软件架构,然后结合几个QoS算法介绍如何在Linux中实现自己的QoS算法。
Packet queuing in Linux
Linux中QoS的软件架构如下图所示:
从上图可以看出整个QoS的管理,从enqueue到dequeue,主要由三种元素连接而成,有Qdisc,Class和Filter。其中,Qdisc主要负责pkt队列的管理,Filter完成对pkt的分类,决定一个enqueue的pkt应该入队到哪个队列,class就是Filter分类后的结果,并对应有一组QoS参数,即这个class的CIR,PIR等等;dequeue时将由调度算法判断应该从哪个class对应的Qdisc中dequeue一个pkt发送。因此,一个pkt的从enqueue到dequeue的流程就是,首先Linux的QoS是针对一个网络设备的,每个网络设备本身有一个根Qdisc,在对此Qdisc enqueue的时候,首先需要对pkt进行分类,利用挂接在Qdisc下的filter链表分类到具体的class的Qdisc中;dequeue一般发生在发送DMA完成或发现DMA空闲但Qdisc中不为空时,此时将从根Qdisc中dequeue,然后调用到此设备所指定的调度算法完成对各个queue的dequeue工作。
从代码执行流程上看,如下图所示:
在Linux中实现一个调度
在Linux中,针对QoS有一个良好的框架,并对Qdisc,Class和Filter都有良好的接口定义。要在Linux中添加一个自己的调度算法需要时自己的算法按照已定义的接口实现,这样就可以无缝地加入到Linux中,并可以利用现成的tc工具进行配置。对于Qdisc定义如下:
struct Qdisc { int (*enqueue)(struct sk_buff *skb, struct Qdisc *dev); struct sk_buff *(*dequeue)(struct Qdisc *dev); unsigned flags; int padded; struct Qdisc_ops *ops; struct qdisc_size_table *stab; struct list_head list; u32 handle; u32 parent; atomic_t refcnt; struct gnet_stats_rate_est rate_est; int (*reshape_fail)(struct sk_buff *skb, struct Qdisc *q); void *u32_node; /* This field is deprecated, but it is still used by CBQ * and it will live until better solution will be invented. */ struct Qdisc *__parent; struct netdev_queue *dev_queue; struct Qdisc *next_sched; struct sk_buff *gso_skb; unsigned long state; struct sk_buff_head q; struct gnet_stats_basic_packed bstats; struct gnet_stats_queue qstats; } |
其中对于实现QoS算法最重要的struct Qdisc_ops定义如下:
struct Qdisc_ops { struct Qdisc_ops *next; const struct Qdisc_class_ops *cl_ops; char id[IFNAMSIZ]; int priv_size; int (*enqueue)(struct sk_buff *, struct Qdisc *); struct sk_buff * (*dequeue)(struct Qdisc *); struct sk_buff * (*peek)(struct Qdisc *); unsigned int (*drop)(struct Qdisc *); int (*init)(struct Qdisc *, struct nlattr *arg); void (*reset)(struct Qdisc *); void (*destroy)(struct Qdisc *); int (*change)(struct Qdisc *, struct nlattr *arg); … struct module *owner; }; |
在此结构中,enqueue和dequeue两个函数是整个QoS调度的入口函数。其中的Qdisc_class_ops用于对此Qdisc的filter list进行操作,添加删除等,通过对Qdisc添加fliter,而filter对enqueue到此Qdisc的pkt进行分类,从而归类到此Qdisc的子class中,而每个子class都有自己的Qdisc进行pkt queue的管理,因此实现一个树形的filter结构,当然可以多个filter将pkt分到同一个子class中,但对于根Qdisc和其下的子class的Qdisc来说还是一个树形的分层结构。
Qdisc_class_ops的定义如下:
struct Qdisc_class_ops { /* Child qdisc manipulation */ int (*graft)(struct Qdisc *, unsigned long cl, struct Qdisc*, struct Qdisc **); struct Qdisc * (*leaf)(struct Qdisc *, unsigned long cl); void (*qlen_notify)(struct Qdisc *, unsigned long); /* Class manipulation routines */ unsigned long (*get)(struct Qdisc *, u32 classid); void (*put)(struct Qdisc *, unsigned long); int (*change)(struct Qdisc *, u32, u32, struct nlattr **, unsigned long *); int (*delete)(struct Qdisc *, unsigned long); void (*walk)(struct Qdisc *, struct qdisc_walker * arg); /* Filter manipulation */ struct tcf_proto ** (*tcf_chain)(struct Qdisc *, unsigned long); unsigned long (*bind_tcf)(struct Qdisc *, unsigned long, u32 classid); void (*unbind_tcf)(struct Qdisc *, unsigned long); 。。。 }; |
对于filter由结构体tcf_proto定义,其函数tcf_proto_ops为:
struct tcf_proto_ops { struct tcf_proto_ops *next; char kind[IFNAMSIZ]; int (*classify)(struct sk_buff*, struct tcf_proto*, struct tcf_result *); int (*init)(struct tcf_proto*); void (*destroy)(struct tcf_proto*); unsigned long (*get)(struct tcf_proto*, u32 handle); void (*put)(struct tcf_proto*, unsigned long); int (*change)(struct tcf_proto*, unsigned long, u32 handle, struct nlattr **,unsigned long *); int (*delete)(struct tcf_proto*, unsigned long); void (*walk)(struct tcf_proto*, struct tcf_walker *arg); … } |
要在Linux中实现一个QoS算法,其入口函数就是struct Qdisc_ops,并通过register_qdisc 将自己的实现注册到系统中即可。
通过TC可以将QoS应用到网络设备上,命令如下:
> tc qdisc add dev eth1 root handle 1: htb > tc class add dev eth1 parent 1:0 classid 1:10 htb rate 100Mbit > tc class add dev eth1 parent 1:10 classid 1:20 htb rate 1Mbit > tc filter add dev eth1 protocol ip parent 1:0 prio 5 u32 match ipdst 192.168.1.200 flowid 1:20 |
以上命令,首先为eth1定义root qdisc,handle为1:0,其对应class带宽为100Mbps,class ID为1:10,然后在此class下定义子class带宽为1Mbps,class ID为1:20,并为此子class定义filter,filter定义为目标ip地址192.168.1.200
下面介绍DRR和HTB在Linux中的实现.
DRR在Linux中的实现
前面介绍了DRR算法,这里我们看看如何在Linux中实现DRR。从上面DRR介绍中,我们知道,对于DRR整个调度算法有一个active_queue,每个class都有一个deficit和weight。Linux中的DRR算法实现于net/sched/sch_drr.c,从中可以发现定义如下:
struct drr_sched { struct list_head active; struct tcf_proto *filter_list; struct Qdisc_class_hash clhash; }; struct drr_class { struct Qdisc_class_common common; unsigned int refcnt; unsigned int filter_cnt; struct gnet_stats_basic_packed bstats; struct gnet_stats_queue qstats; struct gnet_stats_rate_est rate_est; struct list_head alist; struct Qdisc *qdisc; u32 quantum; u32 deficit; }; |
在这里filter_list用于保存挂在这个Qdisc下面的filter,用于对子class进行进一步分类。每个class的weight用quantum表示,即每次class的deficit用完后一次添加的数目。
其enqueue函数如下(有删节):
static int drr_enqueue(struct sk_buff *skb, struct Qdisc *sch) { struct drr_sched *q = qdisc_priv(sch); struct drr_class *cl; cl = drr_classify(skb, sch, &err); /* 首先进行利用q->filter_list进行分类 */ len = qdisc_pkt_len(skb); err = qdisc_enqueue(skb, cl->qdisc); /* enqueue到子class的Qdisc中 */ if (unlikely(err != NET_XMIT_SUCCESS)) { 。。。 return err; } if (cl->qdisc->q.qlen == 1) { /* class qdisc原来为空 */ list_add_tail(&cl->alist, &q->active); /* 添加到active queue中*/ cl->deficit = cl->quantum; /* 同时更新deficit到最大 = quantum*/ } /* 更新统计信息 */ cl->bstats.packets++; cl->bstats.bytes += len; sch->bstats.packets++; sch->bstats.bytes += len; sch->q.qlen++; return err; } |
接下来看dequeue的实现 (有删节):
static struct sk_buff *drr_dequeue(struct Qdisc *sch) { struct drr_sched *q = qdisc_priv(sch); struct drr_class *cl; while (1) { /* 在此循环中对active queue进行RR调度 */ cl = list_first_entry(&q->active, struct drr_class, alist);/* 取出第一个class */ skb = cl->qdisc->ops->peek(cl->qdisc); if (skb == NULL) /* 没有pkt,退出 */ goto out; len = qdisc_pkt_len(skb); if (len <= cl->deficit) { /* 一个可调度的class */ cl->deficit -= len; /* 更新class的deficit */ skb = qdisc_dequeue_peeked(cl->qdisc); if (cl->qdisc->q.qlen == 0) /* 此class调度后为空,移出active queue */ list_del(&cl->alist); sch->q.qlen--; return skb; /* 返回这个pkt */ } cl->deficit += cl->quantum; /* class不可调度,顺便更新其下一轮的deficit */ list_move_tail(&cl->alist, &q->active); /* class移到active queue末尾,等待下一轮 */ } out: return NULL; } |
HTB在Linux中的实现
HTB(Hierarchical Token Bucket)是一种层次化的调度方式,调度器的结构如下图所示,树形结构中的每个一个节点都是一个class,由根节点通过filter将pkt分类到叶子节点中,每个pkt的enqueue都是enqueue到叶子节点的Qdisc中。对树中每个class都有自己的CIR和PIR,因此节点随着自身所余deficit的不同可能处于GREEN, YELLOW和RED三种状态,GREEN就是rate小于CIR,YELLOW就是rate已经大于CIR,但是还小于PIR,此时此节点将通过向其父节点借用deficit来获得调度,RED就是rate已经大于PIR,此时class将被置于waiting queue中等待deficit的更新才可能从RED变为可调度的状态。
HTB的调度策略是按照低level优先,高优先级优先的原则,由于叶子节点都位于level 0,所有在level 0上可调度的class都必然是GREEN状态,应当优先调度,然后在同一level上,选择优先级最高的进行调度。对于非叶子节点,只有当其有子节点向其借用deficit的时候才会被调度,因此可能有多个不同的priority的子节点同时向其借用deficit,因此非叶子节点可能有多个priority,但调度仍然以高priority为准,并在level的调度中也严格按照priority优先的形式。
通过上面说明,实际上HTB的enqueue和dequeue都十分简单。Enqueue就是将pkt分类到叶子节点,然后enqueue到叶子节点的Qdisc中,如果此叶子节点之前为idle状态,则activate之。Dequeue就是选择现有可调度的最低level,最高priority的class进行调度。如果是同时存在多个相同level,priority的情况,则按照DRR进行调度。
下面对Linux中HTB的实现详细演示。
在图中,我们可以看到一共有三个level,每个level有两个优先级,分别为红色和蓝色,其中红色为高优先级。5个class,其中C,D,E为叶子节点,A,B为非叶子节点,即pkt只能enqueue到CDE上。对于每个level都有三个队列,分别对应两个不同priority和waiting queue(白色),当此level中class有pkt在Qdisc中时,对于非叶子节点来说是有子class向其借用deficit时,即一个class需要被调度时,这个class将被enqueue到其所在level的对应队列中。如图1,所有class都没有pkt在Qdisc中,所有level的队列都没有class。图2中,两个class,D,E都有pkt,其中C为BLUE,D为RED。因此调度时,就是选择level0,搞priority的D进行调度。
由于D的priority较高,可以一直占用,如图3所示,随着他的rate超出CIR,将不得不向B借用deficit,此时D将处于YELLOW,被放到level 0的waiting queue中,并且在B的借用队列中排队,由于D本身为RED,因此B也将在level 1的RED queue中。此时调度时,将选择C进行调度,因为C的level为0。如图4所示,C的rate随着调度超过了其PIR,此时C将处于RED,无法向其父节点借用deficit,因此只能放到level 0的waiting queue中等待deficit的更新。此时调度将只能调度B,从而继续调度D中的pkt,但是如果也超出了B的CIR,那么B也将处于YELLOW的状态,从而被enqueue到level 1的waiting queue中,并且向其父节点A开始借用deficit,此时A将被activate,从而被放到level 2的可调度队列中,优先级仍为RED。因此在图4中,BCD都在各自level的waiting queue中,但是BD都未超出其PIR,可以向其父节点借用deficit,因此仍然会得到调度。
但是不幸,A很快超出其PIR了,此时A将处于不可调度的状态,从而被加入到level 2的等待队列中。并且此时class E被enqueue新的pkt,从而被activate,处于GREEN,因此被加入到level 0的可调度队列中。因此图5中调度将会对E进行调度。
图6演示了非叶子节点会有多个priority的情况,BCDE都是YELLOW,向A借用deficit,此时A有RED和BLUE两个priority,调度时将首先对A的RED的B进行调度,B对RED的D进行调度,因此首先是对D进行调度。D的Qdisc为空后,A将会按照DRR对BC进行调度,如果调度到B将间接调度到E。
整个HTB的工作流程如上所述。下面对Linux中的实现进行简单介绍。首先看htb的class的定义:
struct htb_class { struct Qdisc_class_common common; /* general class parameters */ struct gnet_stats_basic_packed bstats; struct gnet_stats_queue qstats; struct gnet_stats_rate_est rate_est; struct tc_htb_xstats xstats; /* our special stats */ int refcnt; /* usage count of this class */ /* topology */ int level; /* our level (see above) */ unsigned int children; struct htb_class *parent; /* parent class */ int prio; /* these two are used only by leaves... */ int quantum; /* but stored for parent-to-leaf return */ union { struct htb_class_leaf { struct Qdisc *q; int deficit[TC_HTB_MAXDEPTH]; struct list_head drop_list; } leaf; struct htb_class_inner { struct rb_root feed[TC_HTB_NUMPRIO]; /* feed trees */ struct rb_node *ptr[TC_HTB_NUMPRIO]; /* current class ptr */ u32 last_ptr_id[TC_HTB_NUMPRIO]; } inner; } un; struct rb_node node[TC_HTB_NUMPRIO]; struct rb_node pq_node; psched_time_t pq_key; int prio_activity; enum htb_cmode cmode; struct tcf_proto *filter_list; int filter_cnt; struct qdisc_rate_table *rate; struct qdisc_rate_table *ceil; long buffer, cbuffer; psched_tdiff_t mbuffer; long tokens, ctokens; psched_time_t t_c; }; |
还有htb_sched,负责管理htb整体的调度
struct htb_sched { struct Qdisc_class_hash clhash; struct list_head drops[TC_HTB_NUMPRIO];/* active leaves (fordrops) */ struct rb_root row[TC_HTB_MAXDEPTH][TC_HTB_NUMPRIO]; int row_mask[TC_HTB_MAXDEPTH]; struct rb_node *ptr[TC_HTB_MAXDEPTH][TC_HTB_NUMPRIO]; u32 last_ptr_id[TC_HTB_MAXDEPTH][TC_HTB_NUMPRIO]; struct rb_root wait_pq[TC_HTB_MAXDEPTH]; psched_time_t near_ev_cache[TC_HTB_MAXDEPTH]; int defcls; struct tcf_proto *filter_list; int rate2quantum; psched_time_t now; struct qdisc_watchdog watchdog; struct sk_buff_head direct_queue; int direct_qlen; long direct_pkts; #define HTB_WARN_TOOMANYEVENTS 0x1 unsigned int warned; struct work_struct work; }; |
下面看enqueue的实现:
static int htb_enqueue(struct sk_buff *skb, struct Qdisc *sch) { int uninitialized_var(ret); struct htb_sched *q = qdisc_priv(sch); struct htb_class *cl = htb_classify(skb, sch, &ret); /* 首先对pkt进行分类,找到叶子class */ if (cl == HTB_DIRECT) { /* enqueue to helper queue */ 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; } } else if ((ret = qdisc_enqueue(skb, cl->un.leaf.q)) != NET_XMIT_SUCCESS) { if (net_xmit_drop_count(ret)) { /* enqueue到叶子class,结束 */ sch->qstats.drops++; cl->qstats.drops++; } return ret; } else { cl->bstats.packets += skb_is_gso(skb)?skb_shinfo(skb)->gso_segs:1; cl->bstats.bytes += qdisc_pkt_len(skb); htb_activate(q, cl); /* 叶子class之前可能队列为空,activate之 */ } sch->q.qlen++; sch->bstats.packets += skb_is_gso(skb)?skb_shinfo(skb)->gso_segs:1; sch->bstats.bytes += qdisc_pkt_len(skb); return NET_XMIT_SUCCESS; } |
Dequeue的实现:htb的dequeue首先检查bitmap,找出最低level最高priority的class list,然后对这个class list调用htb_dequeue_tree来实现dequeue,下面是htb_dequeue_tree的实现。
static struct sk_buff *htb_dequeue_tree(struct htb_sched *q,int prio, int level) { struct sk_buff *skb = NULL; struct htb_class *cl, *start; /* look initial class up in the row */ start = cl = htb_lookup_leaf(q->row[level] + prio, prio, q->ptr[level] + prio, q->last_ptr_id[level] + prio); /* 此处考虑的DRR的调度 */ do { next: if (unlikely(!cl)) return NULL; /* class can be empty - it is unlikely but can be true if leaf qdisc drops packets in enqueue routine or if someone used graft operation on the leaf since last dequeue; simply deactivate and skip such class */ if (unlikely(cl->un.leaf.q->q.qlen == 0)) { struct htb_class *next; htb_deactivate(q, cl); /* 队列为空了??deactivate之 */ /* row/level might become empty */ if ((q->row_mask[level] & (1 << prio)) == 0) return NULL; next = htb_lookup_leaf(q->row[level] + prio, prio, q->ptr[level] + prio, q->last_ptr_id[level] + prio); /* DRR */ if (cl == start) /* fix start if we just deleted it */ start = next; cl = next; goto next; } skb = cl->un.leaf.q->dequeue(cl->un.leaf.q); /* 从queue中dequeue一个pkt */ if (likely(skb != NULL)) break; qdisc_warn_nonwc("htb", cl->un.leaf.q); htb_next_rb_node((level ? cl->parent->un.inner.ptr : q->ptr[0]) + prio); /* RR */ cl = htb_lookup_leaf(q->row[level] + prio, prio, q->ptr[level] + prio, q->last_ptr_id[level] + prio); /* DRR的实现 */ } while (cl != start); if (likely(skb != NULL)) { cl->un.leaf.deficit[level] -= qdisc_pkt_len(skb); /* 更新deficit */ if (cl->un.leaf.deficit[level] < 0) { /* deficit不够了 */ cl->un.leaf.deficit[level] += cl->quantum; htb_next_rb_node((level ? cl->parent->un.inner.ptr : q-> ptr[0]) + prio); /* 从此level的active queue中移除 */ } /* this used to be after charge_class but this constelation gives us slightly better performance */ if (!cl->un.leaf.q->q.qlen) htb_deactivate(q, cl); /* 队列为空,deactivate之 */ htb_charge_class(q, cl, level, skb); /* 检查此class是否发生超标*/ } return skb; } |
其中HTB层次化的实现主要体现在htb_activate, htb_deactivate, htb_charge_class, htb_change_class_mode等几个函数中,htb_activate_prios和htb_deactivate_prios完成子class向父class借用deficit的具体操作。