根據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_NEWQDISC和RTM_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/)