Linux中的QOS分为入口(Ingress)部分和出口(Egress)部分,入口部分主要用于进行入口流量限速(policing),出口部分的QOS用于队列调度(queuing scheduling)。
以下分析所参考的linux内核版本为2.6.21。
1. Ingress QOS
IngressQOS在内核的入口点有两个,但是不能同时启用,这取决于内核编译选项。当打开了CONFIG_NET_CLS_ACT时,入口点在src/net/core/dev.c的netif_receive_skb函数中,代码片段如下:
#ifdef CONFIG_NET_CLS_ACT
当没有打开CONFIG_NET_CLS_ACT,而是打开了CONFIG_NET_CLS_POLICE和CONFIG_NETFILTER时,就会在netfilter的PREROUTING钩子点处调用ing_hook函数,该函数的代码片段如下:
.next = NULL,
.cl_ops = &ingress_class_ops,
.id ="ingress",
.priv_size = sizeof(struct ingress_qdisc_data),
.enqueue = ingress_enqueue,
.dequeue = ingress_dequeue,
.requeue = ingress_requeue,
.drop =ingress_drop,
.init = ingress_init,
.reset = ingress_reset,
.destroy = ingress_destroy,
.change = NULL,
.dump =ingress_dump,
.owner =THIS_MODULE,
};
所有的qdisc都会有这样的一个对象实例,在模块初始化时会调用register_qdisc函数将自己的struct Qdisc_ops结构实例注册到链表中,该链表头是qdisc_base,定义在sch_api.c文件中,static struct Qdisc_ops*qdisc_base。
当通过tc qdisc命令配置了ingress qdisc规则时,会调用到ingress_init函数,进入ingress_init函数代码片段如下:
#ifndef CONFIG_NET_CLS_ACT
#ifdef CONFIG_NETFILTER
if (!nf_registered) {
if (nf_register_hook(&ing_ops) < 0) {
printk("ingress qdisc registration error\n");
return -EINVAL;
}
nf_registered++;
if (nf_register_hook(&ing6_ops) < 0) {
printk("IPv6 ingress qdisc registration error," \
"disabling IPv6 support.\n");
} else
nf_registered++;
}
#endif
#endif
我们看到当没有定义CONFIG_NET_CLS_ACT,但是定义了CONFIG_NETFILTER时,会调用nf_register_hook函数注册ing_ops和ing6_ops结构实例,ing_ops和ing6_ops的定义如下:
static struct nf_hook_ops ing_ops = {
.hook = ing_hook,
.owner =THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_FILTER + 1,
};
static struct nf_hook_ops ing6_ops = {
.hook = ing_hook,
.owner =THIS_MODULE,
.pf = PF_INET6,
.hooknum = NF_IP6_PRE_ROUTING,
.priority = NF_IP6_PRI_FILTER + 1,
};
针对IPV4和IPV6协议,注册的hook函数都是ing_hook,hook点是在PREROUTING,优先级低于NF_IP_PRI_FILTER。
当前比较推荐第一种使用方法,即打开CONFIG_NET_CLS_ACT选项,在netif_receive_skb函数中进入ingress的处理流程。
link_p[RTM_DELQDISC-RTM_BASE].doit = tc_get_qdisc;
link_p[RTM_GETQDISC-RTM_BASE].doit = tc_get_qdisc;
link_p[RTM_GETQDISC-RTM_BASE].dumpit = tc_dump_qdisc;
link_p[RTM_NEWTCLASS-RTM_BASE].doit = tc_ctl_tclass;
link_p[RTM_DELTCLASS-RTM_BASE].doit = tc_ctl_tclass;
link_p[RTM_GETTCLASS-RTM_BASE].doit = tc_ctl_tclass;
link_p[RTM_GETTCLASS-RTM_BASE].dumpit = tc_dump_tclass;
tc filter配置命令对应的配置函数在src/net/sched/cls_api.c的tc_filter_init函数中进行了初始化注册,该函数也会在系统初始化的时候被调用到。代码片段如下:、
if (link_p) {
link_p[RTM_NEWTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
link_p[RTM_DELTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
link_p[RTM_GETTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
link_p[RTM_GETTFILTER-RTM_BASE].dumpit = tc_dump_tfilter;
}
通过以上注册的一系列函数,就可以完成tc filter的命令配置。
if (clid == TC_H_INGRESS)
q = qdisc_create(dev, tcm->tcm_parent, tca, &err);
else
q = qdisc_create(dev, tcm->tcm_handle, tca, &err);
其中,qdisc_create函数用于创建struct Qdisc结构实例,在qdisc_create函数中进行了几个关键操作:
q = rcu_dereference(dev->qdisc);
#ifdef CONFIG_NET_CLS_ACT
skb->tc_verd = SET_TC_AT(skb->tc_verd,AT_EGRESS);
#endif
/* Grab device queue */
spin_lock(&dev->queue_lock);
q = dev->qdisc;
if (q->enqueue) {
dev->qdisc_sleeping = &noop_qdisc;
INIT_LIST_HEAD(&dev->qdisc_list);
新注册一个接口后,dev->qdisc指针指向noop_qdisc结构实例,这是一个特殊的qdisc,它什么也不做。当创建好设备,用ifconfig up命令把设备拉起后,会调用到内核的src/net/core/dev.c中的dev_open函数,在dev_open函数中又会调用到src/net/sched/sch_generic.c中的dev_activate函数,代码片段如下:
if (dev->tx_queue_len) {
qdisc = qdisc_create_dflt(dev, &pfifo_fast_ops,
TC_H_ROOT);
if (qdisc == NULL) {
printk(KERN_INFO "%s: activation failed\n", dev->name);
return;
}
write_lock(&qdisc_tree_lock);
list_add_tail(&qdisc->list, &dev->qdisc_list);
write_unlock(&qdisc_tree_lock);
} else {
qdisc = &noqueue_qdisc;
}
write_lock(&qdisc_tree_lock);
dev->qdisc_sleeping = qdisc;
write_unlock(&qdisc_tree_lock);
}
……
rcu_assign_pointer(dev->qdisc, dev->qdisc_sleeping);
从上面的代码可以看出,当把设备拉起时给设备配置的默认root qdisc为pfifo_fast。之后我们可以在控制台调用tc qdisc add……命令配置其他的qdisc,配置过程与配置ingress qdisc的过程类似,在这里就不再赘述了。
下面我们以prio qdisc为例,看一下出口队列调度的大概流程。
假设我们已经通过tc qdisc命令在接口上配置了prio qdisc作为root qdisc,那么在dev_queue_xmit函数中调用了rc = q->enqueue(skb, q)后,就会调用到与prio qdisc对应的enqueue函数。对于prio qdisc来说,对应的enqueue函数是prio_enqueue(src/net/sched/sch_prio.c), 在prio_enqueue函数中代码片段如下:
qdisc = prio_classify(skb, sch, &ret); //进行分类选择,找出子qdisc
/*调用子qdisc的enqueue函数*/
if ((ret = qdisc->enqueue(skb, qdisc)) == NET_XMIT_SUCCESS) {
sch->bstats.bytes += skb->len;
sch->bstats.packets++;
sch->q.qlen++;
return NET_XMIT_SUCCESS;
}
在上面的代码中首先通过调用prio_classify函数查找子qdisc,然后再调用子qdisc对应的enqueue函数。在这里我们要补充一些概念,在Linux QOS中qidsc分为两种,一种是有分类(classful)的qdisc,另一种是无分类(classless)的qdisc,有分类和无分类的qdisc都可以做为设备的root qdisc,但是有分类的qdisc通过它的分类(class)又可以嫁接出子qdisc,子qdisc可以是有分类的qdisc也可以是无分类的qdisc,最后的叶子qdisc则必须是无分类的qdisc,一般常用pfifo/bfifo做为叶子qdisc。因此可以利用这种组合的特性利用有分类qdisc和无分类qdisc组合出复杂的调度方式。下图简单的描述了他们之间的关系。
if (q->queues[i] == &noop_qdisc) {
struct Qdisc *child;
child = qdisc_create_dflt(sch->dev, &pfifo_qdisc_ops,
TC_H_MAKE(sch->handle, i + 1));
if (child) {
sch_tree_lock(sch);
child = xchg(&q->queues[i], child);
if (child != &noop_qdisc) {
qdisc_tree_decrease_qlen(child,
child->q.qlen);
qdisc_destroy(child);
}
sch_tree_unlock(sch);
}
}
}
通过上面你的描述,我们了解了root qdisc和child qdisc之间的关系,下面我们继续描述prio_classify函数,代码片段如下所示:
struct prio_sched_data *q = qdisc_priv(sch);
u32 band = skb->priority;
struct tcf_result res;
*qerr = NET_XMIT_BYPASS;
if (TC_H_MAJ(skb->priority) != sch->handle) {
#ifdef CONFIG_NET_CLS_ACT
switch ( tc_classify(skb, q->filter_list, &res) ) {
case TC_ACT_STOLEN:
case TC_ACT_QUEUED:
*qerr = NET_XMIT_SUCCESS;
case TC_ACT_SHOT:
return NULL;
};
if (!q->filter_list ) {
#else
if (!q->filter_list || tc_classify(skb, q->filter_list, &res) ) {
#endif
if (TC_H_MAJ(band))
band = 0;
return q->queues[q->prio2band[band&TC_PRIO_MAX]];
}
band = res.classid ;
}
band = TC_H_MIN(band) - 1;
if (band > q->bands)
return q->queues[q->prio2band[0]];
return q->queues[band];
当找到子qdisc的指针后,就调用子qdisc的enqueue函数,如前文所述prio qidsc默认的子qdisc为pfifo qdisc,因此在这里会调用pfifo qdisc对应的enqueue函数pfifo_enqueue
(src/net/sched/sch_fifo.c),进入pfifo_enqueue函数,代码片段如下:
struct fifo_sched_data *q = qdisc_priv(sch);
if (likely(skb_queue_len(&sch->q) < q->limit))
return qdisc_enqueue_tail (skb, sch);
return qdisc_reshape_fail (skb, sch);
数据包入队操作完成后,接下来在dev_queue_xmit函数中会调用qdisc_run函数进行队列调度和出队列操作,在该函数中会调用__qdisc_run函数,在该函数中代码片段如下:
out:
clear_bit(__LINK_STATE_QDISC_RUNNING, &dev->state);
可以看到在这里循环调用了qdisc_restart函数(src/net/sched/sch_generic.c),直到函数返回值不为负值或设备状态处于队列停止状态为止。进入qdisc_restart函数,代码片段如下:
if (((skb = dev->gso_skb)) || (( skb = q->dequeue(q)) )) {
……
{
spin_unlock(&dev->queue_lock);
if (!netif_queue_stopped(dev)) {
int ret;
ret = dev_hard_start_xmit(skb, dev);
if (ret == NETDEV_TX_OK) {
if (!nolock) {
netif_tx_unlock(dev);
}
spin_lock(&dev->queue_lock);
return -1;
}
if (ret == NETDEV_TX_LOCKED && nolock) {
spin_lock(&dev->queue_lock);
goto collision;
}
}
……
spin_lock(&dev->queue_lock);
q = dev->qdisc;
}
requeue:
if (skb->next)
dev->gso_skb = skb;
else
q->ops->requeue(skb, q);
netif_schedule(dev);
return 1;
}
BUG_ON((int) q->q.qlen < 0);
return q->q.qlen;
struct sk_buff *skb;
struct prio_sched_data *q = qdisc_priv(sch);
int prio;
struct Qdisc *qdisc;
for (prio = 0; prio < q->bands; prio++) {
qdisc = q->queues[prio];
skb = qdisc->dequeue(qdisc);
if (skb) {
sch->q.qlen--;
return skb;
}
}
return NULL;
dequeue操作完成后,会调用dev_hard_start_xmit函数将数据包发送出去,如果发送成功则最终返回-1,在__qdisc_run函数中通过循环又会进入到qdisc_restart函数,直到队列为空为止。我们注意到在特殊情况下可能会有调用dev_hard_start_xmit发包不成功的情况,如果是因为发送设备忙碌造成的发送不成功则会进入requeue流程,将数据包重新缓冲到队列里,然后调用netif_schedule函数启用网络发包软中断处理流程,在软中断处理流程中会调用net_tx_action函数,最终又会调用qdisc_run进入队列调度流程。
3. 总结
以上对Ingress和Egress QOS的实现流程和框架进行了粗略分析,通过以上分析,希望读者可以从大的流程上了解QOS的实现框架。由于内核中实现了多种qdisc,在此不能一一赘述,有兴趣的读者可以参考本文的分析流程,切入到自己感兴趣的部分进行深入分析即可。但是万变不离其中,不论是哪种qdisc,都不会超出本文所描述的框架和流程,只不过在队列调度算法上会比较复杂罢了。