本系列首先对网络QoS进行简单介绍,然后对不同调度算法的原理进行一定分析,最后简单介绍Linux中实现的几种调度算法。

整体目录为:
1 神马是QoS
2 神马是CoS
3 QoS的实现机制
4 Traffic Management
5 Packet Queuing
5.1 Buffer Management
5.2 Packet Scheduling
5.2.1 Packet Fair Queuing Scheduler
6 QoS in Linux
6.1 Packet queuing in Linux
6.2 如果在Linux中实现一个调度
6.3 DRR在Linux中的实现
6.4 层次化调度
6.5 HTB在Linux中的实现
神马是QoS
QoS,字面意思就是服务质量,QoS好就是服务质量好,QoS差就是服务质量差。那么服务质量是什么呢?针对网络设备而言,网络设备的功能就是数据包的转发,服务质量就是对数据包的转发行为符合预期的转发要求,客户很满意?。那么客户的转发预期是什么?就是设备的带宽,吞吐量能够保证,丢包率很低,整体延迟很小,而且抖动也很小,对于优先级高的包优先处理,对于不该转发的包就不转发,能够对各种不同类型的包做很好的流量整形,使整个网络有很好的可管理性。
因此对于QoS,有以下几个重要的性能参数:
  • Bandwidth and Throughput,带宽和吞吐量,在系统负载严重,各种feature都打开的时候,系统的吞吐量仍能获得保证
  • Packet Loss Rate,丢包率
  • Delay,包转发延迟
  • Jitter,也叫 Delay Variance,转发延迟的抖动
  • Bit Error Rate,误码率
实际系统中,管理员对不同的网络服务定义了不同的性能参数,那么一个网络设备的QoS的好坏就是实际的性能参数与定义的各个性能参数之间的差异了。
神马是CoS
CoS,就是Class of Service,即网络服务的类型。不同网络服务可能有不同的CoS值,为不同CoS定义这种服务类型的各个QoS参数。在layer2和layer3的协议中,这个值就是对应了packet的priority。
对于二层CoS,在802.1Q中定义如下:
可以看到,在VLAN头中有3bits的user priority字段,000为最低,111为最高。二层的priority用于LAN中,并且应该是反映layer3协议的CoS值。
对于三层的CoS,在IP头的TOS字段。ToS有8个bit,前面三个和最后一个bit一般不用,中间[3:6]定义如下:
但是在IPV6中,又进行了进一步的修订,利用了6个bits。
QoS的实现机制
要实现QoS,有很多工作要做,其中最重要的包括Admission Control(决定是否允许一个连接接入网络),traffic management(通过policing和shaping等手段,测量和调制数据流量)和packet queueing(对网络中的数据包决定是否丢包,是否缓冲,何时如何选择下一个转发的报文等等)。其中packet queueing又包括buffer management和packet scheduling。各个功能在系统中的位置如下所示:
对于Admission Control,在系统新接收一个connection时,需要确保系统中有足够的资源完成对此connect的服务,否则应当reject这个request,admission control会维护当前系统中资源的管理,包括connection的建立与释放。Classifier负责将input pkt分类到系统中的class中,通常一个class有自己的queue,也就是确定将input pkt放到哪个queue中。这里不对这两个模块做详细介绍。
下面将对Traffic Management和packet queuing做详细介绍。
Traffic Management
流量管理整体上基于一个简单的模型,就是token bucket模型。一个token bucket,有固定的容量和流出速度,数据包不断地流入到bucket中,然后流出,因为bucket有固定的流出速度,当流入速度大于流出速度时,将会在bucket中buffer,当所buffer的数据超过bucket容量时,将会造成丢包;即使平均流入速度小于流出速度,当来个一个突然的burst造成bucket溢出时,也会丢包。流量管理整体上就是基于这个简单模型,根据配置的CoS参数,完成QoS。
另外,还可以从另一个角度来看token bucket。如下图所示:
每个输入的数据包都需要一个token来可以被转发,系统以恒定的速度r将定时向bucket中添加token,因此当输入packet的流量超过r,将必然造成一定时间后没有足够的token给后续的packet,从而丢包;同样当虽然平均速率小于r,但是突然一个很大的burst时,也会造成bucket中没有token的情况,也会丢包。因此一个最简单的token bucket有两个参数(r, b)。而一个再复杂一点的token bucket如下图所示:
上图中有两个token bucket,一个参数为(p, 1),另一个参数为(r, b),其中 p > r,而且两个token bucket是串联状态,利用这个token bucket可以完成peak速率为p,平均速率r,token bucket size为b的流量管理。
对这个token bucket就可以用下面的方法简单实现:


current_deficit = current_deficit – r * (current_time – last_acc_time)
if (current_deficit > 0 && current_deficit + pkt_len > B) {
make the pkt as non-conforming; return;
} else if (current_deficit < 0) {
current_deficit = 0;
}
current_deficit += pkt_len
last_acc_time = current_time
make the packet as conforming;
return

另外还有一种将token bucket概念转变为时间概念进行流量管理的方法,叫做virtual scheduling,他根据当前允许的输出速率r算出完成一个pkt转发所应占用的时间,如果各个pkt间到达时间都大于其所应占用的时间则所有packet都是合法的,并且类似于token bucket,规定系统中能容忍提前占用的最大时间为L,因此系统参数为(r, L)。算法如下:


If (expected_next_time > current_time + L) {
make the pkt as non-conformed, return;
} else if (expected_next_time < current_time ) {
Expected_next_time = current_time
}
Expected_next_time += pkt_len / r
Make the pkt as conformed
Return

通过上面简单的token bucket进行流量管理,我们可以判断一个新输入的packet是符合流量要求的,什么时候流量超标。
通过上面简单的token bucket进行流量管理,我们可以判断一个新输入的packet是符合流量要求的,什么时候流量超标。在符合流量要求的时候,必须正确的将这个packet转发出去,而流量超标时如何处理这个packet就是可以配置的不同做出不同的选择,可以选择丢包,而当系统整体的负载不重时,也许我们也可以允许对这些超出的数据也进行一定的转发。因此我们有了TCM(three color markers)算法,他将所有输入的packet根据token bucket情况将其标记上三种颜色(green,yellow,red)。当然green就是这个pkt符合流量要求,进行转发,red就是不符合要求,丢掉,yellow是不符合要求,但是也可进行转发。通常green对应的是r,而yellow和red的计算根据系统不同配置而不同。下面介绍几种TCM变种算法。
srTCM (Single Rate Three Color Marker)
srTCM基于token bucket,方法有三个参数如下:
  • CIR (committed information rate),token的生成速率
  • CBS (committed burst size),burst小于此值将为green
  • EBS (excess burst size),burst小于此值,但大于CBS将为yellow,否则为red
具体算法可以如下描述:

// init : Tc = CBS, Te = EBS
If (Tc < CBS) { Tc += CIR * time_elapsed; }
else if (Te < EBS) { Te += CIR * time_elapsed; }

If (Tc >= pkt_size) { Tc -= pkt_size; make pkt as GREEN; }
else if (Te >= pkt_size) {Te -= pkt_size; mark pkt as YELLOW; }
else { Mark the pkt as RED; }

trTCM (Two Rate Three Color Marker)
trTCM基于前面的两个token bucket的方法,有四个参数如下:
  • CIR (committed information rate),committed bucket的token的生成速率
  • PIR (peak information rate),peak bucket的token的生成速率
  • CBS (committed burst size),burst小于此值将为green
  • PBS (peak burst size),burst小于此值,但大于CBS将为yellow,否则为red
具体算法可描述如下:

// init: Tc = CBS, Tp = PBS
If (Tc < CBS) { Tc += CIR * time_elapsed; }
If (Tp < PBS) { Tp += PIR * time_elapsed; }

If ( Tc >= pkt_size) { Tc -= pkt_size; Tp -= pkt_size; make pkt as GREEN; return }
If (Tp >= pkt_size) { Tp -= pkt_size; make the pkt as YELLOW; return; }
Make the pkt as RED; return;

tswTCM (Time Slicing Window Three Color Marker)
与前面两种基于token bucket的方法不同,tswTCM利用划窗进行速率估计的基于概率的标记方法。有三个参数:
  • CTR (committed target rate),估计速率低于此值为GREEN
  • PTR (peak target rate),估计速率低于此值,但高于CTR为YELLOW,否则为RED
  • AVG WIN SIZE,划窗窗口大小
算法描述如下:

// init: avg_rate = CTR
byte_count = pkt_len + avg_rate * AVG_WIN_SIZE
avg_rate = byte_count / (time_elapsed + AVG_WIN_SIZE)

if (avg_rate < CTR) { make the pkt GREEN; return; }
if (avg_rate < PTR) {
p0 = (avg_rate – CTR) / avg_rate;
mark the packet as YELLOW with probability p0, GREEN with (1 – p0)
} else {
P1 = (avg_rate – PTR) / avg_rate
P2 = (PTR – CTR) / avg_rate
Make the packet : RED(p1), YELLOW(p2), GREEN (1-p1-p2)
}

Policing and Shaping
利用前面提到的各种算法可以完成policing和shaping的功能,即对于GREEN和YELLOW的packet还是转发的策略,对于RED的packet可以丢弃,也可以将其进行排队直到满足条件再转发。
Packet Queuing
首先说说Buffer Management, 这里的buffer管理是针对当buffer overflow时,系统可能将会产生丢包情况时,如何处理新packet的buffer问题。实现Buffer管理的目标是
  • Minimal packet loss and queueing delay
  • The avoidance of global synchronization of packet sources
  • High link utilization
一般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的具体操作。
转载:http://blog.chinaunix.net/space.php?uid=7220314&do=blog&frmd=17002&view=me

网络QoS原理与实现相关推荐

  1. 云原生钻石课程 | 第6课:Kubernetes网络架构原理深度剖析(上)

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  2. 【云驻共创】华为云云原生之Kubernetes网络架构原理深度剖析(上)

    文章目录 前言 一.Kubernetes诞生背景 1.云原生的概念 2.云原生架构 3.Kubernetes(k8s) 二.Kubernetes基本网络模型剖析 1.概念厘清 1.1 二层桥接 VS ...

  3. 以太网及网络工作原理二

    以太网及网络工作原理二 2.以太网工作原理 2.2.以太网数据帧 2.3.数据帧传输 2.4.交换机的工作原理 2.5.网线的分类 2.6.千兆以太网 2.6.1.千兆位以太网标准主要四种类型的传输介 ...

  4. 网络工程原理与实践教程实验安排

    <网络工程原理与实践教程(第3版)>胜在超凡实验指导书和教材合为一体,"易学,易教,内容新"  第10章 实    验.... 235 实验1 水晶头的制作... 23 ...

  5. 充分掌握网络工作原理及底层实现 大家都做什么项目啊?

    张孝祥,张老师是软件编程语言培训和软件工程师速成培训专家.精通c/c++/vc++.vb.java.sql server.oracle.asp.jsp.j2ee.android等编程语言和数据库系统, ...

  6. Sniff网络基础原理和软件实现技巧详解

    Sniff网络基础原理和软件实现技巧详解 前言 SNIFF真是一个古老的话题,关于在网络上采用SNIFF来获取敏感信息已经不是什么新鲜事,也不乏很多成功的案例,那么,SNIFF究竟是什么呢? SNIF ...

  7. 网络编程原理进阶___TCP/IP(javaee)

    点击跳转 本章重点 网络编程原理进阶 应用层 DNS 传输层 UDP TCP面试重点 `TCP`原理 确认应答 超时重传 连接管理(面试重点) 3次握手 4次挥手 滑动窗口 流量控制 拥塞控制 延时应 ...

  8. 雷电网络(一):厘清雷电网络的原理

    闪电网络为解决比特币拥堵而生,当然也可以用于其他区块链项目,比如我们上篇说的OMG.不过,以太坊也有自己的"闪电网络",它的名字叫雷电网络.甚至,以太坊除了雷电网络,还有其他类似的 ...

  9. 【RDMA】RoCE网络QoS|应用层设置PFC等级|Tos|Priority|TC

    目录 1.什么是QoS 2.为什么RoCE网络需要QoS 3.为了实现每个流有不同的优先级,硬件层如何对流量分类 4. 应用层如何对流量分类 5. 应用层对流量的分类是怎么映射到硬件层上的分类 6.映 ...

  10. 一文读懂闪电网络工作原理

    一   引言 了解比特币的人都知道,比特币网络的拥堵问题由来已久,转账高手续费.速度缓慢严重制约了比特币的发展.关于扩容的争论喋喋不休,共识分歧严重,造成了多次比特币分叉.目前来看,社区共识无法达成一 ...

最新文章

  1. 新手提升JSP技术能力的一点建议-调试篇
  2. zabbix查看数据
  3. 调试.NET CORE代码
  4. jvm oracle sun,JVM - 常见的JVM种类
  5. javascript-流程控制-循环-分支-三元运算符
  6. mysql 改表面_CSS表面(outline)是什么【html5教程】,CSS
  7. java控制台输入汉字_给我一个JAVA控制台输入中文的实例
  8. Nginx日志分割处理
  9. 车辆轨迹跟踪算法---几何跟踪算法
  10. 单点登录 cas 设置回调地址_单点登录终极方案之 CAS 应用及原理
  11. HP SD2 DAT160小磁带机故障
  12. mic in、line inline out、speaker out、headphone out 区别
  13. Android应用停用
  14. 《指弹:Like a star》
  15. css font html里写,HTML,CSS,font
  16. exsi rh2288hv5 驱动_华为RH2288H服务器引导ServiceCD安装Windows Server操作系统
  17. Python项目:外星人入侵(汇总)
  18. 使用windows Server 2003搭建DHCP服务器
  19. 如何让谷歌google、百度baidu和雅虎yahoo收录我的网站
  20. 学校食堂外卖APP开发模板

热门文章

  1. python-整理--连接MSSQL
  2. zzulioj--1711--漂洋过海来看你(dfs+vector)
  3. 解决Charles Response 中文乱码
  4. 解决ASP.NET页面回车回发的问题
  5. 2013年阿里巴巴实习生笔试题
  6. Hibernate批量操作(一)
  7. 我为什么要使用Webpack?
  8. python之 模块与包
  9. socket简介 - 获取简单网页内容
  10. OSChina 周日乱弹 —— 感到孤单了怎么办?