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