本文描述了linux2.4.x内核中对QoS支持的设计与实现,但是对缺省的数据包调度机制PFIFO进行了详尽的剖析。
在传统的TCP/IP网路的路由器中,所有的IP数据包的传输都是采用FIFO(先进先出),尽最大努力传输的处理机制。在初期网路数据量和关键业务数据不多的时侯,并没有彰显出特别大的缺点,路由器简单的把数据报遗弃来处理串扰。并且随着计算机网路的发展,数据量的大幅下降,以及多媒体,VOIP数据等对延时要求高的应用的降低。路由器简单遗弃数据包的处理方式早已不再适宜当前的网路。单纯的降低网路带宽也不能从根本上解决问题。所以网路的开发者们提出了服务质量的概念。概括的说:就是针对各类不同需求,提供不同服务质量的网路服务功能。提供QoS能力将是对未来IP网路的基本要求。
1.Linux内核对QOS的支持
Linux内核网路合同栈从2.2.x开始,就实现了对服务质量的支持模块。具体的代码坐落net/sched/目录。在Linux上面,对这个功能模块的尊称是TrafficControl,简称TC。首先我们了解一下Linux网路合同栈在没有TC模块时发送数据包的大致流程。如图1。
注:上图的分层是根据Linux实现来画,并没有严格遵循OSI分层
从上图可以看出,没有TC的情况下,每位数据包的发送还会调用dev_queue_xmit,之后判定是否须要向AF_PACKET合同支持体传递数据包内容,最后直接调用网卡驱动注册的发送函数把数据包发送出去。发送数据包的机制就是本文开始提到的FIFO机制。一旦出现串扰linux安装教程,合同栈只是尽自己最大的努力去调用网卡发送函数。所以这些传统的处理方式存在着很大的症结。
为了支持QoS,Linux的设计者在发送数据包的代码中加入了TC模块。因而可以对数据包进行分类,管理,测量串扰和处理串扰。为了防止和先前的代码冲突,而且让用户可以选择是否使用TC。内核开发者在上图中的两个白色圆圈之间添加了TC模块。(实际上在TC模块中,发送数据包也实现对AF_PACKET合同的支持,本文为了描述便捷,把两个地方的AF_PACKET合同处理分开来了)。
下边从具体的代码中剖析一下对TC模块的支持。
net/core/dev.c:dev_queue_xmit函数中略了部份代码:
int dev_queue_xmit(struct sk_buff *skb)
{
……………….
q = dev->qdisc;
if (q->enqueue) {
/*如果这个设备启动了TC,那么把数据包压入队列*/
int ret = q->enqueue(skb, q);
/*启动这个设备发送*/
qdisc_run(dev);
return;
}
if (dev->flags&IFF_UP) {
………….
if (netdev_nit)
dev_queue_xmit_nit(skb,dev);
/*对AF_PACKET协议的支持*/
if (dev->hard_start_xmit(skb, dev) == 0) {
/*调用网卡驱动发送函数发送数据包*/
return 0;
}
}
………………
}
从里面的代码中可以看出,当q->enqueue为假的时侯,就不采用TC处理,而是直接发送这个数据包。倘若为真,则对这个数据包进行QoS处理。
2.TC的具体设计与实现
第一节描述了linux内核是怎样对QoS进行支持的,以及是怎样在先前的代码基础上添加了tc模块。本节将对TC的设计和实现进行详尽的描述。
QoS有好多的串扰处理机制,如FIFOQueueing(先入先出队列),PQ(优先队列),CQ(订制队列),WFQ(加权公正队列)等等。QoS还要求才能对每位插口分别采用不同的串扰处理。为了才能实现上述功能,Linux采用了基于对象的实现方式。
上图是一个数据发送队列管理机制的模型图。其中的QoS策略可以是各类不同的串扰处理机制。我们可以把这一种策略看成是一个类,策略类。在实现中,这个类有好多的实例对象,策略对象。使用者可以分别采用不同的对象来管理数据包。策略类有好多的方式。如入队列(enqueue),出队列(dequeue),重新入队列(requeue),初始化(init),撤消(destroy)等方式。在Linux中,用Qdisc_ops结构体来代表前面描述的策略类。
后面提及,每位设备可以采用不同的策略对象。所以在设备和对象之间须要有一个桥梁,使设备和设备采用的对象相关。在Linux中,起到桥梁作用的是Qdisc结构体。
通过前面的描述,整个TC的构架也就下来了。如右图:
加上TC以后,发送数据包的流程应当是这样的:
(1)下层合同开始发送数据包
(2)获得当前设备所采用的策略对象
(3)调用此对象的enqueue方式把数据包压入队列
(4)调用此对象的dequeue方式从队列中取出数据包
(5)调用网卡驱动的发送函数发送
接出来从代码上来剖析TC是怎样对每位设备安装策略对象的。
在网卡注册的时侯,还会调用register_netdevice,给设备安装一个Qdisc和Qdisc_ops。
int register_netdevice(struct net_device *dev)
{
………………….
dev_init_scheduler(dev);
………………….
}
void dev_init_scheduler(struct net_device *dev)
{
………….
/*安装设备的qdisc为noop_qdisc*/
dev->qdisc = &noop_qdisc;
………….
dev->qdisc_sleeping = &noop_qdisc;
dev_watchdog_init(dev);
}
此时,网卡设备刚注册,还没有UP,采用的是noop_qdisc,
struct Qdisc noop_qdisc =
{
noop_enqueue,
noop_dequeue,
TCQ_F_BUILTIN,
&noop_qdisc_ops,
};
noop_qdisc采用的数据包处理方法是noop_qdisc_ops,
struct Qdisc_ops noop_qdisc_ops =
{
NULL,
NULL,
"noop",
0,
noop_enqueue,
noop_dequeue,
noop_requeue,
};
从noop_enqueuelinux内核中网络协议的设计与实现,noop_dequeue,noop_requeue函数的定义可以看出,她们并没有对数据包进行任何的分类或则排队,而是直接释放掉skb。所以此时网卡设备还不能发送任何数据包。必须ifconfigup上去以后能够发送数据包。
调用ifconfigup来启动网卡设备会走到dev_open函数。
int dev_open(struct net_device *dev)
{
…………….
dev_activate(dev);
……………..
}
void dev_activate(struct net_device *dev)
{
…………. if (dev->qdisc_sleeping == &noop_qdisc) {
qdisc = qdisc_create_dflt(dev, &pfifo_fast_ops);
/*安装缺省的qdisc*/
}
……………
if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc) {
……………./*.安装特定的qdisc*/
}
……………..
}
设备启动以后,此时当前设备缺省的Qdisc->ops是pfifo_fast_ops。假如须要采用不同的ops,这么就须要为设备安装其他的Qdisc。本质上是替换掉dev->Qdisc表针。见sched/sch_api.c的dev_graft_qdisc函数。
static struct Qdisc *
dev_graft_qdisc(struct net_device *dev, struct Qdisc *qdisc)
{
……………
oqdisc = dev->qdisc_sleeping;
/* 首先删除掉旧的qdisc */
if (oqdisc && atomic_read(&oqdisc->refcnt) qdisc_sleeping = qdisc;
dev->qdisc = &noop_qdisc;
/*启动新安装的qdisc*/
if (dev->flags & IFF_UP)
dev_activate(dev);
…………………
}
从dev_graft_qdisc可以看出linux vps,假如须要使用新的Qdisc,这么首先须要删掉旧的,之后安装新的,使dev->qdisc_sleeping为新的qdisc,之后调用dev_activate函数来启动新的qdisc。结合dev_activate函数中的句子:
if ((dev->qdisc = dev->qdisc_sleeping) != &noqueue_qdisc)
可以看出,此时的dev->qdisc所指的就是新的qdisc。(注意,里面句子中右侧是一个形参句子。)
在网卡down掉的时侯,通过调用dev_close->dev_deactivate重新使设备的qdisc为noop_qdisc,停止发送数据包。
Linux中的所有的QoS策略最终都是通过前面这个方式来安装的。在sch_api.c中,对dev_graft_qdisc函数又封装了一层函数(register_qdisc),供模块来安装新的Qdisc。如RED(初期随后检查队列)模块,就调用register_qdisc来安装RED对象(net/sched/sch_red.c->init_module())。
3.Linux缺省策略对象pfifo_fast_ops剖析
在Linux中,假如设备启动以后,没有配置特定的QoS策略,内核对每位设备采用缺省的策略,pfifo_fast_ops。下边的pfifo_fast_ops进行详尽的剖析。
上图中的信息可以对应于pfifo_fast_ops结构体的每位部份:
static struct Qdisc_ops pfifo_fast_ops =
{
NULL,
NULL,
"pfifo_fast", /*ops名称*/
3 * sizeof(struct sk_buff_head), /*数据包skb队列*/
pfifo_fast_enqueue, /*入队列函数*/
pfifo_fast_dequeue, /*出队列函数*/
pfifo_fast_requeue, /*重新压入队列函数*/
NULL,
pfifo_fast_init, /*队列管理初始化函数*/
pfifo_fast_reset, /*队列管理重置函数*/
};
在注册pfifo_fast_ops的时侯首先会调用pfifo_fast_init来初始化队列管理,见qdisc_create_dflt函数。
static int pfifo_fast_init(struct Qdisc *qdisc, struct rtattr *opt)
{
………
for (i=0; i<3; i++)
skb_queue_head_init(list+i); /*初始化3个优先级队列*/
……….
}
init函数的作用就是初始化3个队列。
在注销一个Qdisc的时侯还会调用Qdisc的ops的reset函数。见dev_graft_qdisc函数。
static void
pfifo_fast_reset(struct Qdisc* qdisc)
{
…………..
for (prio=0; prio < 3; prio++)
skb_queue_purge(list+prio); /*释放3个优先级队列中的所有数据包*/
…………..
}
在数据包发送的时侯会调用Qdisc->enqueue函数(在qdisc_create_dflt函数中早已将Qdisc_ops的enqueue,dequeue,requeue函数分别形参于Qdisc分别对应的函数表针)。
int dev_queue_xmit(struct sk_buff *skb)
{
……………….
q = dev->qdisc;
if (q->enqueue) {
/* 对应于pfifo_fast_enqueue 函数*/
int ret = q->enqueue(skb, q);
/*启动这个设备的发送,这里涉及到两个函数pfifo_fast_dequeue ,pfifo_fast_requeue 稍后介绍*/
qdisc_run(dev);
return;
}
……………
}
入队列函数pfifo_fast_enqueue:
static int
pfifo_fast_enqueue(struct sk_buff *skb, struct Qdisc* qdisc)
{
…………..
list = ((struct sk_buff_head*)qdisc->data) +
prio2band[skb->priority&TC_PRIO_MAX];
/*首先确定这个数据包的优先级,决定放入的队列*/
if (list->qlen dev->tx_queue_len) {
__skb_queue_tail(list, skb); /*将数据包放入队列的尾部*/
qdisc->q.qlen++;
return 0;
}
……………..
}
在数据包装入队列以后,调用qdisc_run来发送数据包。
static inline void qdisc_run(struct net_device *dev)
{
while (!netif_queue_stopped(dev) &&
qdisc_restart(dev)<0)
/* NOTHING */;
}
在qdisc_restart函数中,首先从队列中取出一个数据包(调用函数pfifo_fast_dequeue)。之后调用网卡驱动的发送函数(dev->hard_start_xmit)发送数据包,假如发送失败,则须要将这个数据包重新压入队列(pfifo_fast_requeue),之后启动合同栈的发送软中断进行再度的发送。
static struct sk_buff *
pfifo_fast_dequeue(struct Qdisc* qdisc)
{
…………..
for (prio = 0; prio q.qlen--;
return skb;
}
}
……………….
}
从dequeue函数中可以看出linux内核中网络协议的设计与实现,pfifo的策略是:从高优先级队列中取出数据包,只有高优先级的队列为空,就会对下一优先级的队列进行处理。
requeue函数重新将数据包压入相应优先级队列的脸部。
static int
pfifo_fast_requeue(struct sk_buff *skb, struct Qdisc* qdisc)
{
struct sk_buff_head *list;
list = ((struct sk_buff_head*)qdisc->data) +
prio2band[skb->priority&TC_PRIO_MAX];
/*确定相应优先级的队列*/
__skb_queue_head(list, skb);/*将数据包压入队列的头部*/
qdisc->q.qlen++;
return 0;
}
总结
QoS是当前一个十分热门的话题,几乎所有低端的网路设备都支持QoS功能,但是这个功能也是当前网路设备之间竞争的一个关键技术。Linux为了在在高档服务器才能占有一席之地,从2.2.x内核开始就支持了QoS。本文在linux2.4.0的代码基础上对Linux怎样支持QoS进行了剖析。而且剖析了Linux内核的缺省队列处理方式PFIFO的实现。