Linux的QoS实现.docx
- 文档编号:9397081
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:23
- 大小:455.13KB
Linux的QoS实现.docx
《Linux的QoS实现.docx》由会员分享,可在线阅读,更多相关《Linux的QoS实现.docx(23页珍藏版)》请在冰豆网上搜索。
Linux的QoS实现
Linux网络流量控制实现
文档编号:
00-6201-100
当前版本:
1.0.0.0
创建日期:
2011-6-13
编写作者:
ganjingwei
Linux网络流量控制实现
摘要
Linux提供了一套丰富的流量控制功能。
这篇文章介绍了各个内核代码的设计,描述了其结构,并通过描述一个新的排队规则来说明新元素的加入。
1介绍
最近发行的Linux内核提供了很多类型的流量控制功能。
流量控制的核心部分,以及一些用来控制它们的用户空间程序已被AlexeyKuznetsov
这项工作的灵感来自于[1]中描述的概念,但它也包括支持IETF“集成服务”团队开发的架构所需机制[2],并且将作为支持更近期的“区分服务”的基础[3]。
另见[4]中关于集成服务和区分服务如何相关的进一步细节。
这篇文章介绍了底层架构,并描述如何增添新的流量控制功能到Linux内核。
我们使用的内核版本是2.2.6。
图1大致说明了内核进程如何从网络接收数据,以及如何生成新的在网络上发送的数据:
传入的数据包被检查,然后要么直接转发到网络(例如:
在不同的接口,在机器充当路由或者网桥的时候),要么提交给协议栈中的更高层(例如:
移交给像UDP或TCP这样的传输层协议)来进行进一步的处理。
这些高层也可能产生他们自己的数据并把数据提交给低层,以完成诸如封装、路由以及最终的传输这样的任务。
“转发”包括输出接口的选择,下一跳路由的选择,封装等。
这一切都完成了以后,数据包在各自的输出接口排队。
这里便是流量控制工作的地方。
流量控制可以在做其他事之前决定数据包要排队还是丢弃(例如:
如果该队列已达到一定长度的限制,或者超过一定的流量限速),可以决定数据包的发送顺序(例如:
给某些数据流优先级),可以延迟数据包的发送(例如:
限制输出流量速率),等等。
一旦流量控制给出一个数据包准备发送,网卡驱动马上得到这个数据包并把它发到网络上。
第2部分至第4部分给出一个概述,并解释一些术语。
第5部分至第8部分更详细地描述了Linux内核中流量控制的元素。
第9部分描述了一个已被笔者实现的排队规则。
2概述
Linux内核中的流量控制代码主要包括以下概念:
●队列规则
●类(对应一种队列规则)
●分离器
●策略
图1:
网络数据处理流程
每个网络设备都有一个与它关联的队列规则,以控制进入队列的数据包如何被操作。
一个非常简单的队列规则可以只包含一个单一的队列,所有的数据包将存放在其中,并按照进队的顺序排序,设备尽自己的努力取出它们并发送。
图2展示了一个没有外部可见的内部结构队列规则。
图2:
一个不分类的队列规则
更复杂的队列规则可能会用分离器来区分不同类别的数据包并按照特定的方式处理每个类,例如:
给一个类高于其他类的优先级。
图3展示了一个队列规则的例子。
注意,多个分离器可以映射到同一个类。
队列规则和类是紧密联系的:
类和类的含义是队列规则的基本属性。
相反地,分离器可以与队列规则和类任意组合,只要队列规则和类对应就行。
但是灵活性还在——类一般不管如何保存它们这个类别的数据包,而是通过队列规则来做到这一点。
可以通过设置可用的队列规则来任意选择这个队列规则,它很可能已经有类,反过来又包含队列规则,等等。
图4展示了这样一个堆栈的例子:
首先,这里有一个含有两个延迟优先级的队列规则。
被分离器挑选的数据包进入高优先级的类,其他包进入低优先级的类。
只要高优先级队列中有数据包,就先发送高优先级的包(例如:
sch_prio队列就是这样工作的)。
为了防止低优先级的流量完全得不到发送,我们使用一个令牌桶过滤器(TBF),使得速率最高1Mbps。
最后,低优先级队列的数据包通过先进先出队列规则来排队。
注意,有更好的方法来做到我们这里做到的结果,例如:
使用基于类的队列(CBQ)[5]。
数据包像这样进入队列:
当一个队列规则的入队函数被调用,它运行一个又一个分离器直到其中一个表示匹配。
然后它按照相应的类给这个包排序,一般是通过请求这个类“拥有”的队列规则的入队函数。
不匹配任何分离器的数据包都被归到默认类中。
通常情况下每个类“拥有”一个队列,但是原则上也可能若干个类共用同一个队列,或者甚至一个队列被各自的队列规则的所有类使用。
但是要注意,数据包不包含关于这个包属于哪个类的任何明确信息。
如果“内部”队列被共享,当数据包出队的时候改变每个类的信息的队列规则(例如:
CBQ)可能因此不能正确地工作,除非他们能重新被分类或者是通过其他方式在进队到出队的过程中跳过分类的结果。
通常,当数据包进队列的时候,符合的数据流能够被处理,例如:
丢弃超过流速的数据包。
我们将不会再试图引进新的术语来区分算法,更不会有他们的实现,实例。
而是在本文整个过程中使用队列规则,类,以及分离器这些术语。
用来同时指代三个抽象级别。
3源代码
Linux流量控制的实现分散在许多同样大量的源文件中。
请注意,所有路径名都关联着各自部分的目录。
例如:
分离器,在内核中是/usr/src/linux/,在tc应用程序中是iproute2/tc/。
图3:
一个含有多种类别的队列规则
图4:
结合了优先级算法、令牌桶算法和先进先出算法的队列规则
TC是一个用来控制单个流量控制元素的用户空间程序。
它的源代码在iproute2-version.tar.gz中,你可以在ftp:
//linux.wauug.org/pub/net/ip-routing/中获得它。
TC的内核代码主要在目录net/sched/中。
在内核和用户空间调用之间的接口在include/linux/pkt_cls.handinclude/linux/pkt_sched.h中申明。
只在内核中使用的申明和一些内联函数的定义,可以在include/net/pkt_cls.h和include/net/pkt_sched.h中找到。
用于在用户空间和内核空间通信的rtnetlink机制在net/core/rtnetlink.c和include/linux/rtnetlink.h中实现。
rtnetlink是基于netlink的,netlink可以在net/netlink/和include/linux/netlink.h中找到。
内核代码可以从那些普通的知名的地方获得,例如:
ftp:
//ftp.kernel.org/pub/linux/kernel/v2.2/。
第9部分的示例在Linux发布的ATM中包含,它可以从http:
//icawww1.epfl.ch/linux-atm/dist.html.中下载。
Linux项目的差异化服务(http:
//icawww1.epfl.ch/linux-diffserv/)已经发开了更长远的示例,以便扩充Linux流量控制及其应用。
4专业术语
不幸的是,用于描述流量控制的术语与以往文献中的相差甚远。
Linux流量控制甚至也发生了一些变动。
这一部分的目的是介绍环境背景。
图5显示了IETF团队“集成服务”[6]和“差异化服务”[7,8]中使用的框架模型和术语,也显示了Linux流量控制的元素是如何与他们关联的。
请注意,类扮演了一个含糊不定的角色,因为它决定了最后的分类结果,它可以是一个实现了一个确定的队列操作或者调度行为的机制的一部分。
表1概括了TC命令行中使用的关键字,以及内核中使用的文件名,还有TC源代码中使用的文件名。
图5:
Linux流量控制中集成服务和差异化服务框架的元素之间的关联
表1:
流量控制中的元素使用的关键字和文件名
5队列规则
每个队列规则提供以下的函数定义来控制它的运作(参见include/net/pkt_sched.h中的structQdisc_ops):
enqueue根据队列规则将数据包入队,如果这个队列规则有对应的类,enqueue函数首先搜索一个类,然后请求匹配的队列规则的enqueue函数执行更长远的入队操作。
dequeue取出下一个满足发送条件的数据包。
如果队列规则没有数据包发送(例如:
因为队列为空或者因为数据包都还没有被调度到可以发送),dequeue返回NULL。
requeue在一个数据包用dequeue函数出队以后,再次把它放入队列。
这个函数与enqueue函数的区别在于数据包应该恰好在它被dequeue函数删除的位置入队,而且它不能被当做已经通过队列的流量累积统计下来,因为这个统计已经在enqueue函数中执行过了。
drop丢弃一个队列中的数据包。
init初始化并配置队列规则。
change改变一个队列规则的配置。
reset让一个队列规则恢复到一开始的状态。
所有的队列被清空,计时器停止,等等。
同样的,所有与该队列规则的类有关的队列规则的reset函数都被调用。
destroy删除一个队列规则。
它删除所有类,也许也是所有分离器,取消所有等待事件,并释放该队列规则申请的所有资源(特别是描述队列规则自己的数据结构)。
dump返回用于维护的诊断数据。
通常情况下,dump函数返回足够重要的配置信息和状态变量。
为了调用这些函数,队列规则被一个指向适当的Qdisc结构体的指针引用。
一个数据包通过一个接口(net/core/dev.c中的dev_queue_xmit)入队的时候,设备的队列规则(include/linux/netdevice.c中structdevice的qdisc成员)的enqueue函数被调用。
然后,dev_queue_xmit函数调用设备的include/net/pkt_sched.h中的qdisc_wakeup函数来尝试发送刚刚入队的数据包。
qdisc_wakeup函数立刻呼叫net/sched/sch_generic.c中的qdisc_restart函数,这是轮询队列规则并且发送数据包的主要函数。
qdisc_restart函数首先尝试从设备的队列规则中获得一个数据包,如果成功获得,qdisc_restart函数将调用设备的hard_start_xmit函数来实际发送这个数据包。
如果因为某些原因发送失败,数据包将通过requeue函数交还给队列规则。
当一个队列规则发现一个数据包应当被发送时,qdisc_wakeup函数也可以被这个队列规则调用,例如:
计时器提示过期。
TBF就是这样的一个队列规则。
qdisc_restart函数也通过qdisc_run_queues函数由在net/core/dev.c中的net_bh调用。
net_bh是网络处理栈的下半部处理程序,只要数据包入队进行更长远的操作,它就会被执行。
图6说明了流程。
为了简单起见,队列规则的请求呼叫(例如:
请求分类)没有被标出。
请注意,队列规则从来不直接调用发送函数,而是必须等待直到他们被轮询。
如果一个队列规则被编译到内核,它必须在net/sched/sch_api.c中的pktsched_init函数中注册。
另外,它也可以在其他地方用register_qdisc函数进行注册,例如:
如果队列规则被编译成模块,在init_module函数中可以注册。
图6:
入队和发包时的函数调用
当一个队列规则的实例被创建或更改的时候,一个选项的载体(在include/linux/rtnetlink.h中申明的structrtattr*类型)被传递给初始化函数。
每个选项都按照它的类型、长度、值来编码(例如:
0或者更多的字节)。
选项类型和值所使用的数据结构在include/linux/pkt_sched.h中申明。
选项载体的解析通过调用rtattr_parse函数来完成,rtattr_parse函数返回一个由指向每个元素的指针组成的数组,用来索引选项类型。
选项的长度和内容可以分别在宏RTA_PAYLOAD和RTA_DATA中读取到。
选项载体通过rtnetlink机制在用户空间和内核空间传递。
解释rtnetlink和底层netlink超出了本文的范畴。
每个源文件的定位在第三部分说明了。
队列规则的实例被32位数字标记,这32位被分成主次两个数字。
通常的标记方法是主标记号:
次标记号。
对于队列规则,次标记号总是0。
请注意,这里的主次号和设备文件使用的设备号没有关系。
6类
类可以用两种方式标记:
(1)通过类ID,由用户指定;
(2)通过内部ID,由队列规则指定。
后者在给出的队列规则中必须是独一无二的,并且,可以是一个索引也就是一个指针,等等。
请注意,0这个值是特殊的,如果get函数返回0,则表示“没有找到”。
类ID是一个U32类型的,而内部ID是一个unsignedlong类型。
在内核中,通常找到一个类的方式是通过内部ID。
只有get函数和change函数使用类ID。
请注意,多个类ID可以映射到同一个内部ID。
在这种情况下,类ID传递更多的信息从分类器到队列规则或者类。
类ID的结构像队列规则ID一样,有一个对应队列规则实例的主号,和一个标记实例中的类得次号。
含有类的队列规则提供了以下函数族来控制类(参见include/net/pkt_sched.h中的structQdisc_class_ops):
●graft给一个类附加一个新的队列规则,并返回先前使用的队列规则。
●leaf返回类的队列规则。
●get通过类ID查找类,并返回它的内部ID。
如果这个类支持一个可用的计数器,get函数应该增加计数器。
●put在一个之前和get函数一起提到的类被提取的时候调用。
如果这个类支持一个可用的计数器,put函数应该减少计数器。
如果这个计数器达到零,put函数将删除这个类。
●change改变类的属性。
change函数也用于创建新类,适用于那些含有一个始终不变的号的,在队列规则初始化时被创建的类的队列规则。
●delete处理删除类的请求。
它检查这个类是否在使用中,在这种情况下停用并删除它。
●walk遍历队列规则的所有类并为每个类调用一个回调函数。
这用于获得队列规则的每个类的特征数据。
●tcf_chain返回一个指向与类关联的分离器的链表节点的指针。
这用于操作分离器链表。
●bind_tcf给一个类绑定一个分离器的实例。
bind_tcf函数通常和get函数等同,除了当队列规则需要能够明确拒绝类的删除操作的时候。
(例如:
sch_cbq拒绝删除那些被分离器提及的类。
)
●unbind_tcf从类中删除分离器的实例。
unbind_tcf函数通常等同于put函数。
●dump_class返回特征数据,就像dump函数对队列规则做的一样。
类在队列规则的入队函数中被选择,通常是通过调用include/net/pkt_cls.h中的tc_classify函数,这个函数返回一个包含类ID(classid)也可能包含内部ID(class)的结构体structtcf_result(在include/net/pkt_cls.h中申明),参见第7部分。
tc_classify函数的返回值不是1(TC_POLICE_UNSPEC)就是分离器返回的策略决定(参见第8部分)。
tc_classify函数的返回值在include/linux/pkt_cls.h中申明。
还有一个为本地流量分类的捷径:
如果skb->priority包含了当前队列规则的类的ID,这个类直接被使用,不再尝试其他分类。
当本地生成一个数据包时,skb->priority(include/linux/skbuff.h中的structsk_buff)被设置为sk->priority(include/net/sock.h中的structsock)。
sk->priority可以被设置为SO_PRIORITY套接字选项(net/core/sock.c中的sock_setsockopt)。
这种分类可以被用于实现像Arequipa提供的那种功能。
请注意,2.2.3以上的内核限制与SO_PRIORITY设置的那个值在范围0~7内,因此这个分类的捷径失效了。
然而,所有的队列规则都支持它。
也要注意,skb->priority可以包含其他优先值,例如:
从IPv4头中TOS字段获得的优先级。
所有这些值都低于最小有效类号65536。
选择完类以后,各自内部队列规则的入队函数被调用。
队列规则在与类关联的数据结构中存储方式可以在队列规则的实现中多样变化。
传递给change函数的选项载体和传递给队列规则初始化函数的载体是同一个结构体。
相应的声明也在include/linux/pkt_sched.h中。
7分离器
分离器被队列规则用于分配传入的数据包给其中一个类。
这个过程发生在队列规则的如对操作期间。
图7:
分离器的结构,第一个分离器有一个链表的元素,第二个没有内部结构
分离器被保存在分离器链表中,链表被每个队列规则或者每个类维持着,依赖于队列规则的设计。
分离器链表按照优先级排列,递增的顺序。
此外,分离器的入口用他们申请的协议来当钥匙。
那些协议号在skb->protocol被使用,在include/linux/if_ether.h被定义。
在同一个分离器链表上的相同协议的分离器必须具有不同的优先级。
分离器也可以有一个内部结构:
它可以控制内部元素,之后被32位句柄引用。
这些句柄和类ID很类似,但是他们没有被拆分为主次号。
0句柄总是指向这个分离器自己。
像类一样,分离器也有内部ID,可以通过get函数获得。
分离器的内部结构可以是随意的。
图7展示了一个含有一个内部元素链表的分离器。
图8展示了分离器和分离器的元素被检查的顺序。
一个顺序处理的链表当然在分离器许多可能的内部结构中是唯一的。
图8:
寻找匹配
分离器通过以下函数族来控制(参见include/net/pkt_cls.h中的structtcf_proto_ops):
●classify完成分类并返回TC_POLICE_...这些值中的一个,这些值将在第8部分描述。
如果结果不是TC_POLICE_UNSPEC,它也返回找到的类ID或者也可选择返回structtcf_result中被res指向的内部类ID。
如果内部类ID是缺省的,res->class的值必须为0。
●init初始化分离器。
●destroy删除一个分离器。
队列规则sch_cbq和sch_atm在删除类的时候也使用destroy函数来删除过时的分离器。
如果分离器或者任何它的元素与类一起注册,将会通过调用unbind_tcf函数来撤销注册。
●get按照句柄查找一个分离器元素并返回其内部分离器ID。
●put当一个之前被get函数引用的分离器元素不在使用时被调用。
●change配置一个新的分离器或改变一个已经存在的分离器的属性。
配置参数传递的机制和队列规则、类的传递机制是一样的。
change函数通过调用bind_tcf函数来注册添加一个新的分离器或者是分离器元素给一个类。
●delete删除一个分离器的元素。
为了删除整个分离器,destroy函数必须被使用。
这一点在net/sched/cls_api:
tc_ctl_tfilter中被区分,并且对用户透明。
如果这个分离器元素已经和类一起注册,则调用unbind_tcf函数来撤销注册。
●walk遍历一个分离器的所有元素,并对每一个元素调用一个回调函数。
这用于获得特征数据。
●dump返回一个分离器或他的元素的特征数据。
请注意,RSVP分离器的代码在cls_rsvp.h中。
cls_rsvp.c和cls_rsvp6.c只包含了文件包含的正确设置,并设置一些参数(主要是RSVP_DST_LEN),这些参数控制cls_rsvp.h中产生的分离器类型。
图9:
通用分离器
分离器在分离器的实例可以分类的数据包的范围中变换:
当使用cls_fw和cls_route分离器的时候,每个队列规则一个实例可以把所有类的数据包分类。
这些分离器从数据包描述符中得到类ID,类ID在存放到其他协议栈中的实体中之前存放在数据包描述符中,例如:
cls_fw使用防火墙编码中的标记功能。
我们叫这些分离器为通用分离器。
笔者将在第9部分阐述他们。
图10:
特定的分离器,含有一个像内部类ID一样被使用的指针,指向类
另一个类型的分离器(cls_rsvpandcls_u32)对于每个类需要一个或多个分离器实例或分离器的内部元素。
我们叫这些分离器特定分离器。
这种分离器(或者是它的元素)的多个实例存放在同一个分离器列表上(例如:
对于同一个类),他们靠一个分离器内部ID来区分,就像类使用的内部ID一样。
然而,不像类那样,分离器没有“分离器ID”。
取而代之的是,根据他们对应注册的队列规则或类,以及他们在这些分离器中的优先级可以识别他们。
因为特定分离器中对每个类至少一个实例或元素,特定分离器很自然地储存了类的内部ID,并把他们当做分类的结果。
这就支持了队列规则对类信息的快速检索。
图10展示了这个情况,指向类结构体的指针的位置就是像内部ID一样使用的地方。
不幸的是,通用分离器没有办法提供这个信息。
因此,通用分离器设置tcf_result结构体中的class成员为0,并把查找操作交给队列规则。
从2.2.5版本的内核开始,通用分离器cls_fw和cls_route也可以变成特定分离器。
这个配置的改变发生在明确绑定类给他们的时候。
8策略
策略的目的是确保流量不超出某个界限。
为了简单起见,我们将假设一个广义的策略,并把它当做包含了所有类型的基于流量体积的流量控制手段。
我们考虑四种策略机制:
(1)分离器决策,
(2)拒绝数据包入队,(3)从“内部”队列规则丢包,和(4)当新数据包入队时丢弃一个数据包。
图11到15展示了这四种机制。
第一种动作由分离器决定(图11)。
一个分离器的分类函数可能返回三种值来指示决策(这些值在include/linux/pkt_cls.h中声明):
●TC_POLICE_OK:
没有特别的对待请求。
●TC_POLICE_RECLASSIFY:
数据包被分离器选择,但是它超出了某个界限,因此需要被重新分类(参见下文)。
●TC_POLICE_SHOT:
数据包被分离器选择,并被发现触犯了边界,因此应该被丢弃。
目前,cls_rsvp,cls_rsvp6和cls_u32分离器支持策略。
策略信息通过tc_classify函数(在include/net/pkt_cls.h中)返回给队列规则的入队函数。
然后就提交给队列规则以执行适当的动作。
sch_cbq和sch_atm队列规则处理TC_POLICE_RECLASSIFY和TC_POLICE_SHOT。
sch_prio队列规则忽略任何tc_classify函数返回的策略信息。
分离器可以使用tcf_police函数(在net/sched/police.c中)来决定找到的流是否符合一个令牌桶。
桶的参数(在include/linux/pkt_cls.h中的structtc_police中申明,然后存放在include/net/pkt_sched.h中的structtcf_police中)大致和TBF相同:
最大传输单元(MTU),平均传输速率(rate),传输速率峰值(peakrate),以及桶大小(burst)。
action成员包含了当接收到的数据包超出限制时的决策编码的返回值。
如果数据包可以被接收,tcf_police更新长度并返回结果中存放的决策编码。
图11:
入队的策略;分离器所做的决策。
如果没有找到匹配的分离器,tc_classify函数返回TC_POLICE_UNSPEC。
在这种情况下,队列规则通常丢弃数据包或者把它放到低优先级。
有时,给重要的流量多于一个的令牌桶是可取的,例如:
把流量分成“低”、“高”和“越界”数据包。
为了组建这些配置,多个策略
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux QoS 实现