Liunx下Qos功能实现简析

    根据OSI参考模型来分,Qos可以应用在如下两层:即上层协议(主要是应用层)与链路层以及物理层网卡发出数据处。前者是通过TC工具对上层协议数据实施Qos,原理就是首先在应用层对要处理的包或者流打上mark,然后利用TC工具多不同的流量实施不同的功能处理,如流量整形,优先级设置,调度与过滤等等,值得说明的是TC工具实质是一套中间件,功能最后均由内核去负责实现;至于后者的Qos,就是在网卡驱动处设置Qos,具体实现与TC工具类似,最后也是由内核去负责实现。

一、上层协议Qos以及TC工具原理分析:

      TC是一个在上层协议处添加Qos功能的工具,原理上看,它实质是专门供用户利用内核Qos调度模块去定制Qos的中间件,本节主要是阐述TC工具是如何去队列规则的,以及内部是如何实现的。

       首先需要了解的是,TC作为一个应用工具,它又是如何与内核去实现通讯的?很简单,消息机制,所借助的工具则是Netlink,而所使用的协议正是NETLINK_ROUTE,更加详细的Netlink相关的知识,请参考《linux内核与用户之间的通信方式——虚拟文件系统、ioctl以及netlink》。不过在此可以说明下TC源代码中是如何初始化rtnetlink(可以理解为专门为路由设计的netlink)socket的。

struct rtnl_handle
{
    int         fd;
    struct sockaddr_nl  local;
    struct sockaddr_nl  peer;
    __u32           seq;
    __u32           dump;	
};
struct rtnl_handle *rth
rth->fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
...
rth->local.nl_family = AF_NETLINK;
rth->local.nl_groups = 0;
bind(rth->fd, (struct sockaddr*)&rth->local, sizeof(rth->local);

       下面主要以TC工具对qdisc操作(包括增加,修改,取代等等)的实现。对qdisc规则解析代码是在tc_qdisc_modify函数中完成的,然后通过消息机制交给内核相关模块去处理。下面是其中一段消息初始化代码片段:

    struct {
        struct nlmsghdr     n;
        struct tcmsg        t;
        char            buf[TCA_BUF_MAX];
    } req;
    struct tcmsg
    {
        unsigned char   tcm_family;
        unsigned char   tcm__pad1;
        unsigned short  tcm__pad2;
        int     tcm_ifindex;
        __u32       tcm_handle;
        __u32       tcm_parent;
        __u32       tcm_info;
    };
    req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
    req.n.nlmsg_flags = NLM_F_REQUEST|flags;
    req.n.nlmsg_type = RTM_NEWQDISC;
    req.t.tcm_family = AF_UNSPEC;

       需要解释的是,tcmsg结构体定义了跟流量控制相关的消息内容,nlmsghdr则定义了消息头,消息头中附带了消息的类型以及标志量(主要用来区分各种不同的消息),常见的消息类型有(只是针对qdisc而言,若是class或者filter,肯定会有差别):RTM_NEWQDISCRTM_DELQDISC;常见的标志量有:NLM_F_REQUEST,NLM_F_CREATE,NLM_F_REPLACE,NLM_F_EXCL,分别意味着该消息时一个请求类的消息,进行创建或者取代操作,若存在则不予处理。Qdisc有关的各种操作所对应的消息类型以及标志量总结如下表:

                             
      有一点值得注意的是,因为针对各种不同的调度机制,有着不一样的参数选项,如sfq所对应的参数就有quantum, perturb, limit等,而htb则有r2q, default,在TC工具中针对这些不同的调度机制,定义了不一样的解析函数。如sfq和htb中的定义如下:
    struct qdisc_util htb_qdisc_util = {
        .id         = "htb",
        .parse_qopt = htb_parse_opt,
        .print_qopt = htb_print_opt,
        .print_xstats   = htb_print_xstats,
        .parse_copt = htb_parse_class_opt,
        .print_copt = htb_print_opt,
    };
    struct qdisc_util sfq_qdisc_util = {
        .id     = "sfq",
        .parse_qopt = sfq_parse_opt,
        .print_qopt = sfq_print_opt,
    };
    而在 tc_qdisc_modify函数中则是首先get_qdisc_kind去获取对应的调度机制名,然后调用跟此种调度机制对应的解析参数函数去执行,对应代码片段如下:
    q = get_qdisc_kind(k);
    ...
    if (q->parse_qopt(q, argc, argv, &req.n))
        return 1;

     所有的参数均解析完成之后,接下来就是将消息发给内核(接着内核将会处理所收到的消息请求),并及时接受内核的回复消息。下面着重阐述内核在收到消息请求之后是如何进行处理的呢?首先需要明白的是,当内核接收到请求消息后,按照消息的什么内容去完成消息的处理呢?消息的类型!前面总结了tc工具在不同的规则下有着对应的消息类型,例如,add, change, replace等操作所对应的消息类型则是RTM_NEWQDISC,因此,内核在收到此种消息类型之后会调用相应的模块去进行处理。OK,这些消息处理模块全部放在了sch_api.c文件中,相关代码如下:

    static int __init pktsched_init(void)
    {
        register_qdisc(&pfifo_qdisc_ops);
        register_qdisc(&bfifo_qdisc_ops);
        proc_net_fops_create(&init_net, "psched", 0, &psched_fops);

        rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL);
        rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL);
        rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc, tc_dump_qdisc);
        rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL);
        rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL);
        rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass, tc_dump_tclass);
        return 0;
    }

       从上面这段代码可以看出,模块中注册了消息类型以及与处理函数的对应关系。此处以RTM_NEWQDISC消息类型为例,此时需要调用tc_modify_qdisc函数去处理。处理的基本思想是这样的:因为不同的规则可能对应着相同的消息类型(如RTM_NEWQDISC),此时就需要再通过消息的标志量做进一步的操作,最后通过调用内核中有关qdisc的API函数去完成,相关代码片段如下:

    static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n, void *arg)
    {
        ......

        err = nlmsg_parse(n, sizeof(*tcm), tca, TCA_MAX, NULL);
        if (err < 0)
            return err;

        if (clid) {
            .......
            if (!q || !tcm->tcm_handle || q->handle != tcm->tcm_handle) {
                if (tcm->tcm_handle) {
                    ......
                    if ((q = qdisc_lookup(dev, tcm->tcm_handle)) == NULL)
                        goto create_n_graft;
                    ......
                    atomic_inc(&q->refcnt);
                    goto graft;
                } else {
                    if (q == NULL)
                        goto create_n_graft;
                        if ((n->nlmsg_flags&NLM_F_CREATE) &&
                        (n->nlmsg_flags&NLM_F_REPLACE) &&
                        ((n->nlmsg_flags&NLM_F_EXCL) ||
                         (tca[TCA_KIND] &&
                          nla_strcmp(tca[TCA_KIND], q->ops->id))))
                        goto create_n_graft;
                }
            }
        ......
        /* Change qdisc parameters */
        ......
        err = qdisc_change(q, tca);
        if (err == 0)
            qdisc_notify(skb, n, clid, NULL, q);
        return err;

    create_n_graft:
        if (!(n->nlmsg_flags&NLM_F_CREATE))
            return -ENOENT;
        if (clid == TC_H_INGRESS)
            q = qdisc_create(dev, &dev->rx_queue,
                     tcm->tcm_parent, tcm->tcm_parent,
                     tca, &err);
        else
            q = qdisc_create(dev, netdev_get_tx_queue(dev, 0),
                     tcm->tcm_parent, tcm->tcm_handle,
                     tca, &err);
        ......
    graft:
        err = qdisc_graft(dev, p, skb, n, clid, q, NULL);
        ......
        return 0;
    }

       从上面的片段中可以看出,根据不同的标志量,调用不同的API函数去完成最后的功能,如qdisc_change用于去修改原qdisc规则,修改完成之后然后调用qdisc_notify去回复响应TC,qdisc_create用于去重新创建一个新的qdisc队列规则,qdisc_graft函数用于去将qdisc移植到某个对象上去。

       以上以TC工具对Qdisc操作为例简单地阐述了TC工具是如何与内核进行交互的,以及内核又是如何响应请求并作出处理的,下节将探讨在ATM设备上如何设置Qos。


二、ATM设备的Qos:

     本节结合Broadcom代码分析ATM设备上的Qos是如何被设置的。在讨论此问题之前,需要明白ATM设备是如何创建的,当用户配置通过ADSL拨号方式上网时,此时将会生成一个ATM设备接口,具体的创建过程代码片段如下:

static int bcmxtmcfg_ioctl( struct inode *inode, struct file *flip,unsigned int command, unsigned long arg )
{
    int ret = 0;
    unsigned int cmdnr = _IOC_NR(command);

    FN_IOCTL IoctlFuncs[] = {DoInitialize, DoUninitialize, DoGetTrafficDescrTable, DoSetTrafficDescrTable, DoGetInterfaceCfg,
        DoSetInterfaceCfg, DoGetConnCfg, DoSetConnCfg, DoGetConnAddrs,
        DoGetInterfaceStatistics, DoSetInterfaceLinkInfo, DoSendOamCell,
        DoCreateNetworkDevice, DoDeleteNetworkDevice, DoReInitialize, DoGetBondingInfo, NULL};

    if( cmdnr >= 0 && cmdnr < MAX_XTMCFGDRV_IOCTL_COMMANDS &&
        IoctlFuncs[cmdnr] != NULL )
    {
        (*IoctlFuncs[cmdnr]) (arg);
    }
……
}

       Bcmxtmcfg_ioctl在收到来自于用户请求需要创建一个XTM(ATM或者PTM)时,接着调用DoCreateNetworkDevice函数,最后向bcmxtmrt驱动发送创建设备的请求信息XTMRT_CMD_CREATE_DEVICE,相关代码片段如下:

int bcmxtmrt_request( XTMRT_HANDLE hDev, UINT32 ulCommand, void *pParm )
{
    PBCMXTMRT_DEV_CONTEXT pDevCtx = (PBCMXTMRT_DEV_CONTEXT) hDev;
    int nRet = 0;

    switch( ulCommand )
    {
    .......
    
    case XTMRT_CMD_CREATE_DEVICE:
        nRet = DoCreateDeviceReq( (PXTMRT_CREATE_NETWORK_DEVICE) pParm );
        break;

    .......
}

       接着进入DoCreateDeviceReq接口函数去创建设备,相关代码片段如下:

static int DoCreateDeviceReq( PXTMRT_CREATE_NETWORK_DEVICE pCnd )
{
    ......

    if( pGi->ulDrvState != XTMRT_UNINITIALIZED &&
        (dev = alloc_netdev( sizeof(BCMXTMRT_DEV_CONTEXT),
         pCnd->szNetworkDeviceName, ether_setup )) != NULL )
    {
        dev_alloc_name(dev, dev->name);
        SET_MODULE_OWNER(dev);

 #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,30)
        pDevCtx = (PBCMXTMRT_DEV_CONTEXT) netdev_priv(dev);
 #else
        pDevCtx = (PBCMXTMRT_DEV_CONTEXT) dev->priv;
 #endif
        memset(pDevCtx, 0x00, sizeof(BCMXTMRT_DEV_CONTEXT));
        memcpy(&pDevCtx->Addr, &pCnd->ConnAddr, sizeof(XTM_ADDR));
        if(( pCnd->ConnAddr.ulTrafficType & TRAFFIC_TYPE_ATM_MASK ) == TRAFFIC_TYPE_ATM )
            pDevCtx->ulHdrType = pCnd->ulHeaderType;
        else
            pDevCtx->ulHdrType = HT_PTM;

        if (pDevCtx->ulHdrType == HT_PTM) {
           if (pGi->bondConfig.sConfig.ptmBond == BC_PTM_BONDING_ENABLE)
              pDevCtx->ulTrafficType = TRAFFIC_TYPE_PTM_BONDED ;
           else
              pDevCtx->ulTrafficType = TRAFFIC_TYPE_PTM ;
        }
        else {
           if (pGi->bondConfig.sConfig.atmBond == BC_ATM_BONDING_ENABLE)
              pDevCtx->ulTrafficType = TRAFFIC_TYPE_ATM_BONDED ;
           else
              pDevCtx->ulTrafficType = TRAFFIC_TYPE_ATM ;
        }
	   ......
        /* format the mac id */
        i = strcspn(dev->name, "0123456789");
        if (i > 0)
           unit = simple_strtoul(&(dev->name[i]), (char **)NULL, 10);

        if (pDevCtx->ulHdrType == HT_PTM)
           macId = MAC_ADDRESS_PTM;
        else
           macId = MAC_ADDRESS_ATM;
        /* set unit number to bit 20-27 */
        macId |= ((unit & 0xff) << 20);

        kerSysGetMacAddress(dev->dev_addr, macId);
        ......
        dev->netdev_ops = &bcmXtmRt_netdevops; //控制接口(包括设备相关的ioctl函数)
#else
        /* Setup the callback functions. */
        dev->open               = bcmxtmrt_open;
        dev->stop               = bcmxtmrt_close;
        dev->hard_start_xmit    = (HardStartXmitFuncP) bcmxtmrt_xmit;
        dev->tx_timeout         = bcmxtmrt_timeout;
        dev->set_multicast_list = NULL;
        dev->do_ioctl           = &bcmxtmrt_ioctl;
        dev->poll               = bcmxtmrt_poll;
        dev->weight             = 64;
        dev->get_stats          = bcmxtmrt_query;
#endif
#if defined(CONFIG_MIPS_BRCM) && defined(CONFIG_BLOG)
        dev->clr_stats          = bcmxtmrt_clrStats;
#endif
        dev->watchdog_timeo     = SAR_TIMEOUT;

        /* identify as a WAN interface to block WAN-WAN traffic */
        dev->priv_flags |= IFF_WANDEV;

        switch( pDevCtx->ulHdrType )
        {
        ......
        nRet = register_netdev(dev);
        ........
}
       从上面这段代码可以看出,主要是完成新建设备的一些初始化工作,包括控制接口、操作回调函数等,其中最主要的就是在 register_netdev(register_netdevice)中,它是在内核中完成的,其中完成的一项工作就是队列规则的初始化,相关代码片段如下:
    void dev_init_scheduler(struct net_device *dev)
    {
        netdev_for_each_tx_queue(dev, dev_init_scheduler_queue, &noop_qdisc);
        dev_init_scheduler_queue(dev, &dev->rx_queue, &noop_qdisc);

        setup_timer(&dev->watchdog_timer, dev_watchdog, (unsigned long)dev);
    }

       从代码中可以看到初始化时给设备加载的是noop_qdisc规则,而通过此规则对应的回调函数可以看出,实质上他并没有给队列加载任何规则,只是做了释放空间的工作。以noop_enquene为例,它负责对入队列加载规则,但是在noop_enqueue函数仅仅进行了数据的释放。

struct Qdisc noop_qdisc = {
    .enqueue    =   noop_enqueue,
    .dequeue    =   noop_dequeue,
    .flags      =   TCQ_F_BUILTIN,
    .ops        =   &noop_qdisc_ops,
    .list       =   LIST_HEAD_INIT(noop_qdisc.list),
    .q.lock     =   __SPIN_LOCK_UNLOCKED(noop_qdisc.q.lock),
    .dev_queue  =   &noop_netdev_queue,
};
static int noop_enqueue(struct sk_buff *skb, struct Qdisc * qdisc)
{
    kfree_skb(skb);
    return NET_XMIT_CN;
}

       OK,前面很长篇幅阐述了broadcom代码中是如何去生成一个XTM设备以及是如何去完成它的初始化的,同时也知道了对新创建的设备并没有加载任何的Qos规则,那么要想对刚创建的设备增加Qos功能,该如何去实现呢?首先在rutQos_qMgmtQueueConfig函数中完成了对QMgmtQueueObject对象的相关Qos参数的设置,之后调用devCtl_xtmSetConnCfg函数试图将所配置的参数写进ATM设备中,之后进入bcmxtmcfg_ioctl中的DoSetConnCfg函数,然后是BcmXtm_SetConnCfg函数,一次类推,最后是通过DoSetTxQueue函数完成最后的配置,整个逻辑流程如下:

ATM TC:

Rut_qos.c(rutQos_qMgmtQueueConfig-->devCtl_xtmSetConnCfg)bcmxtmcfg_ioctl-->DoSetConnCfg-->BcmXtm_SetConnCfg-->SetConnCfg-->SetCfg->CheckTransmitQueues->bcmxtmrt_request-àDoSetTxQueue

      

 

参考文献:

1 Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现(http://www.ibm.com/developerworks/cn/linux/kernel/l-qos/)










  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值