Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现

来源:互联网 发布:java中调用方法本身 编辑:程序博客网 时间:2024/04/30 15:50
本文描述了linux 2.4.x内核中对QoS支持的设计与实现,并且对缺省的数据包调度机制PFIFO进行了详细的分析。

在传统的TCP/IP网络的路由器中,所有的IP数据包的传输都是采用FIFO(先进先出),尽最大努力传输的处理机制。在早期网络数据量和关键业务数据不多的时候,并没有体现出非常大的缺点,路由器简单的把数据报丢弃来处理拥塞。但是随着计算机网络的发展, 数据量的急剧增长,以及多媒体,VOIP数据等对延时要求高的应用的增加。路由器简单丢弃数据包的处理方法已经不再适合当前的网络。单纯的增加网络带宽也不能从根本上解决问题。所以网络的开发者们提出了服务质量的概念。概括的说:就是针对各种不同需求,提供不同服务质量的网络服务功能。提供QoS能力将是对未来IP网络的基本要求。

1.Linux内核对QoS的支持

Linux内核网络协议栈从2.2.x开始,就实现了对服务质量的支持模块。具体的代码位于net/sched/目录。在Linux里面,对这个功能模块的称呼是Traffic Control ,简称TC。

首先我们了解一下Linux网络协议栈在没有TC模块时发送数据包的大致流程。如图1。
Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现(图一)

注:上图的分层是按照Linux实现来画,并没有严格遵守OSI分层

从上图可以看出,没有TC的情况下,每个数据包的发送都会调用dev_queue_xmit,然后判断是否需要向AF_PACKET协议支持体传递数据包内容,最后直接调用网卡驱动注册的发送函数把数据包发送出去。发送数据包的机制就是本文开始讲到的FIFO机制。一旦出现拥塞,协议栈只是尽自己最大的努力去调用网卡发送函数。所以这种传统的处理方法存在着很大的弊端。

为了支持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有很多的拥塞处理机制,如FIFO Queueing(先入先出队列),PQ(优先队列),CQ(定制队列),WFQ(加权公平队列)等等。QoS还要求能够对每个接口分别采用不同的拥塞处理。为了能够实现上述功能,Linux采用了基于对象的实现方法。

 

 

 

上图是一个数据发送队列管理机制的模型图。其中的QoS策略可以是各种不同的拥塞处理机制。我们可以把这一种策略看成是一个类,策略类。在实现中,这个类有很多的实例对象,策略对象。使用者可以分别采用不同的对象来管理数据包。策略类有很多的方法。如入队列(enqueue),出队列(dequeue),重新入队列(requeue),初始化(init),撤销(destroy)等方法。在Linux中,用Qdisc_ops结构体来代表上面描述的策略类。

前面提到,每个设备可以采用不同的策略对象。所以在设备和对象之间需要有一个桥梁,使设备和设备采用的对象相关。在Linux中,起到桥梁作用的是Qdisc结构体。

通过上面的描述,整个TC的架构也就出来了。如下图:


Linux 2.4.x 网络协议栈QoS模块(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_enqueue,noop_dequeue,noop_requeue函数的定义可以看出,他们并没有对数据包进行任何的分类或者排队,而是直接释放掉skb。所以此时网卡设备还不能发送任何数据包。必须ifconfig up起来之后才能发送数据包。

调用ifconfig up来启动网卡设备会走到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) <= 1)qdisc_reset(oqdisc);/*安装新的qdisc */if (qdisc == NULL)qdisc = &noop_qdisc;dev->qdisc_sleeping = qdisc;dev->qdisc = &noop_qdisc;/*启动新安装的qdisc*/if (dev->flags & IFF_UP)dev_activate(dev);…………………}

从dev_graft_qdisc可以看出,如果需要使用新的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())。

在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 voidpfifo_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 intpfifo_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 <= skb->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 < 3; prio++, list++) {skb = __skb_dequeue(list);if (skb) {qdisc->q.qlen--;return skb;}}……………….}

从dequeue函数中可以看出,pfifo的策略是:从高优先级队列中取出数据包,只有高优先级的队列为空,才会对下一优先级的队列进行处理。

requeue函数重新将数据包压入相应优先级队列的头部。

static intpfifo_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。本文在linux 2.4.0的代码基础上对Linux如何支持QoS进行了分析。并且分析了Linux内核的缺省队列处理方法PFIFO的实现。

 

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 3岁宝宝吃饭不香怎么办 胃ca吃饭反胃没食欲怎么办 12岁儿童脸色发黄怎么办 胃饿 但是没食欲不想吃饭怎么办 牙缝大经常塞西怎么办 吃肉老是塞牙缝怎么办 宝宝光喝奶粉不吃饭怎么办 九个月宝宝缺维c怎么办 九个月宝宝缺维d怎么办 9个月大宝宝缺锌怎么办 三周岁宝宝不爱吃饭怎么办 一周岁宝宝不爱吃饭怎么办 两岁半宝宝不自己吃饭怎么办 3岁宝宝不会吃饭怎么办 节食减肥胃疼怎么办呢 减肥不吃饭胃疼怎么办 过度节食伤了胃怎么办 3岁宝宝啥也不吃怎么办 1岁多宝宝不吃饭怎么办 胃口吃辣的难受怎么办 空腹吃辣椒胃疼怎么办 吃东西辣的胃口疼怎么办 吃辣的东西胃烧怎么办 吃母乳的宝宝不爱喝水怎么办 三个月宝宝不肯吃奶粉怎么办 三个月宝宝不肯喝奶粉怎么办 三个月的宝宝不肯喝奶粉怎么办 三个月的宝宝不肯吃奶粉怎么办 饿了还是没食欲怎么办 3岁半幼儿便秘怎么办 小孩字写得难看怎么办 小孩的字写的丑怎么办 小孩字写的太差怎么办 小孩很多字不会写怎么办 食欲不好吃不多怎么办 中班小孩子子不肯写字怎么办 1岁宝便秘该怎么办 两周的宝宝便秘怎么办 3岁宝宝不肯吃药怎么办 1岁婴儿不肯吃药怎么办 9月婴儿不肯吃药怎么办