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/)










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章