数据结构
qdisc
qdisc是流量控制的核心。qdisc分为无类qdisc和有类qdisc两大类。
qdisc句柄
qdisc是32位,分为16位的主号码和16位的次号码。命令行表达时通常用"major:"的形式,其中major是十进制数。不指定次号码是因为次号码的范围要留给class,即使该qdisc是一个无类的qdisc。
有三个特殊的句柄号。0xFFFFFFFF(TC_H_ROOT)表示是根;句柄号0xFFFFFFF1(TC_H_INGRESS)表示接收方向qdisc;全0表示不设置,由内核自动选择一个。
qdisc对象: Qdisc
排队规则对应的数据类型的Qdisc,对于支持的各种qdisc,内核维护的实际是其操作集的实例:Qdisc_ops对象,只有当一个qdisc被配置到网络设备的队列时,内核才会根据注册的Qdisc_ops对象创建一个Qdisc对象,将其配置到队列中。
struct Qdisc
{
// 入队和出队操作
int (*enqueue)(struct sk_buff *skb, struct Qdisc *dev);
struct sk_buff *(*dequeue)(struct Qdisc *dev);
unsigned flags; // 取值见TCQ_F_XXX
#define TCQ_F_BUILTIN 1
#define TCQ_F_THROTTLED 2
#define TCQ_F_INGRESS 4
// qdisc第一个字段是指定字节对齐的,所以分配后首部可能会有padding,记录padding大小
int padded;
struct Qdisc_ops *ops; // 具体qdisc实现提供的操作集
struct qdisc_size_table *stab;
u32 handle; // 句柄,在同一个网络设备内挂接的qdisc需要保证句柄唯一
// 如果qdisc是队列的根qdisc,该值为TC_H_ROOT,否则为其parent class的class ID
u32 parent;
atomic_t refcnt; // 引用计数
unsigned long state; // qdsic的状态,发送过程中会使用
struct sk_buff *gso_skb;
struct sk_buff_head q;
struct netdev_queue *dev_queue; // 关联的设备队列
struct Qdisc *next_sched;
struct list_head list; // 将本qdisc和其它qdisc组织成链表
// 统计信息
struct gnet_stats_basic bstats;
struct gnet_stats_queue qstats;
struct gnet_stats_rate_est rate_est;
// 具有"整形"功能的qdisc在skb入队失败时会回调该实现
int (*reshape_fail)(struct sk_buff *skb, struct Qdisc *q);
void *u32_node; // u32过滤器专用,实际指向的是tc_u_common结构
/* This field is deprecated, but it is still used by CBQ
* and it will live until better solution will be invented.
*/
struct Qdisc *__parent; // 如注释: cbq算法专用
};
qdisc操作集: Qdisc_ops
struct Qdisc_ops
{
// 注册到内核的qdisc会用该字段把所有的qdisc操作集对象组织到全局链表qdisc_base中
struct Qdisc_ops *next;
// qdisc的类操作集,对于无类qdisc,该字段应该为NULL
const struct Qdisc_class_ops *cl_ops;
char id[IFNAMSIZ]; // qdisc的名称
// 具体qdisc实现可以扩展自己的私有数据区,创建qdisc时会多分配指定内存
int priv_size;
int (*enqueue)(struct sk_buff *, struct Qdisc *); // 入队和出队
struct sk_buff *(*dequeue)(struct Qdisc *);
// 从qdisc获取下一个出队的数据包,但是不将数据包从qdisc中删除
struct sk_buff *(*peek)(struct Qdisc *);
unsigned int (*drop)(struct Qdisc *); // 从qdisc中丢弃一个数据包
// 创建qdisc的时候调用该回调用于初始化,该实现是可选的
int (*init)(struct Qdisc *, struct nlattr *arg);
void (*reset)(struct Qdisc *); // 将qdisc复位,当qdisc和队列去关联时会回调
void (*destroy)(struct Qdisc *); // 和init()对应,qdisc被销毁时回调
// 用于修改qdisc的配置参数,不支持修改参数的qdisc可不实现该回调
int (*change)(struct Qdisc *, struct nlattr *arg);
int (*dump)(struct Qdisc *, struct sk_buff *);
int (*dump_stats)(struct Qdisc *, struct gnet_dump *);
struct module *owner;
};
qdisc类操作集: Qdisc_class_ops
有类qdisc需要提供类操作集,使得其可以管理内部的class。
// 如注释所示,操作集可以分为四大类
struct Qdisc_class_ops
{
/* Child qdisc manipulation */
int (*graft)(struct Qdisc *, unsigned long cl, struct Qdisc *, struct Qdisc **);
struct Qdisc *(*leaf)(struct Qdisc *, unsigned long cl);
void (*qlen_notify)(struct Qdisc *, unsigned long);
/* Class manipulation routines */
unsigned long (*get)(struct Qdisc *, u32 classid); // 根据classId获取句柄
void (*put)(struct Qdisc *, unsigned long); // 和get()对应
int (*change)(struct Qdisc *, u32, u32, struct nlattr **, unsigned long *);
int (*delete)(struct Qdisc *, unsigned long);
void (*walk)(struct Qdisc *, struct qdisc_walker * arg);
/* Filter manipulation */
struct tcf_proto ** (*tcf_chain)(struct Qdisc *, unsigned long);
unsigned long (*bind_tcf)(struct Qdisc *, unsigned long, u32 classid);
void (*unbind_tcf)(struct Qdisc *, unsigned long);
/* rtnetlink specific */
int (*dump)(struct Qdisc *, unsigned long, struct sk_buff *skb, struct tcmsg*);
int (*dump_stats)(struct Qdisc *, unsigned long, struct gnet_dump *);
};
qdisc相关操作:
- graft(): 将新的qdisc(参数3)和qdisc(参数1)的某个class(参数2)关联,并且返回原来的qdisc(参数4);
- leaf():对于分类qdisc,通常会为其配置class,当要修改某个class的孩子qdisc时,需要先找到该qdisc。该回调从qdisc(参数1)开始搜索,找到其某个类(参数2指定class ID),返回该类的qdisc;
class相关操作:
-
get():根据classid获取持有类的引用后返回其句柄,一般会返回该类的指针当作句柄,后续调用中的unsigned long类型参数均为该返回值;如果该classid不存在,则返回0。
-
put():和get()对应,释放类的引用;
-
change():在qdisc(参数1)下,修改或者创建class(参数2指定的calss ID如果存在则为修改,否则为新建);该class的parent用参数3指定;参数4指定了class的配置参数,修改后的或者新的class通过参数5返回;
filter相关操作:
- tcf_chain(): 获取qdisc(参数1)的,或者该qdisc中class(参数2)的filter列表指针;
注册qdisc: register_qdisc()
内核具体的qdisc模块向流控框架注册了qdisc操作集实例后,内核才能支持该qdisc。
/* Protects list of registered TC modules. It is pure SMP lock. */
static DEFINE_RWLOCK(qdisc_mod_lock);
// 保存系统中所有注册的排队规则
static struct Qdisc_ops *qdisc_base;
int register_qdisc(struct Qdisc_ops *qops)
{
struct Qdisc_ops *q, **qp;
int rc = -EEXIST;
write_lock(&qdisc_mod_lock);
// 遍历已注册排队规则链表,确保ID(排队规则名字)不重复。因此,
// 内核是用名字来唯一标识排队规则的
for (qp = &qdisc_base; (q = *qp) != NULL; qp = &q->next)
if (!strcmp(qops->id, q->id))
goto out;
// 如果没有指定入队回调,则指定一个丢包的入队操作
if (qops->enqueue == NULL)
qops->enqueue = noop_qdisc_ops.enqueue;
if (qops->peek == NULL) {
if (qops->dequeue == NULL) {
qops->peek = noop_qdisc_ops.peek;
} else {
rc = -EINVAL;
goto out;
}
}
// 如果没有指定出队回调,则指定一个空的出队操作
if (qops->dequeue == NULL)
qops->dequeue = noop_qdisc_ops.dequeue;
// 将排队规则安装到全局链表中
qops->next = NULL;
*qp = qops;
rc = 0;
out:
write_unlock(&qdisc_mod_lock);
return rc;
}
类似的,unregister_qdisc()为去注册接口。
class
对于有类qdisc,它内部会维护一颗class树。逻辑上,这些class是以树形结构组织的,但是存储结构上,这些class以哈希表的方式存储。
class的ID
class的ID和qdisc句柄类似,也是32位,分为16位的主号码和16位的次号码。由于class树一定是关联到某个有类qdisc的,对class的ID的指定有一定的约束,规则如下:
- 以qdisc为父亲的class的ID,其主号码必须是该qdisc的主号码,次号码自由分配;
- 以某个class为父亲的class的ID,其主号码必须是和父亲calss的主号码相同,次号码自由分配;
按照上述class ID的分配原则,同一颗calss树中的class的主号码肯定是相同的,和qdisc的主号码相同。
class哈希表: Qdisc_class_hash
struct Qdisc_class_hash
{
struct hlist_head *hash; // 哈希表指针
unsigned int hashsize; // 哈希表桶大小
unsigned int hashmask; // 哈希表桶大小-1
unsigned int hashelems; // 当前哈希表中元素个数
};
class哈希表的分配和初始化: qdisc_class_hash_init()
static struct hlist_head *qdisc_class_hash_alloc(unsigned int n)
{
unsigned int size = n * sizeof(struct hlist_head), i;
struct hlist_head *h;
if (size <= PAGE_SIZE)
h = kmalloc(size, GFP_KERNEL);
else
h = (struct hlist_head *)
__get_free_pages(GFP_KERNEL, get_order(size));
if (h != NULL) {
for (i = 0; i < n; i++)
INIT_HLIST_HEAD(&h[i]);
}
return h;
}
int qdisc_class_hash_init(struct Qdisc_class_hash *clhash)
{
unsigned int size = 4;
clhash->hash = qdisc_class_hash_alloc(size);
if (clhash->hash == NULL)
return -ENOMEM;
clhash->hashsize = size;
clhash->hashmask = size - 1;
clhash->hashelems = 0;
return 0;
}
类似的,针对该clas哈希表还有其它的增加、删除、扩展哈希表等操作,都很简单,不再赘述。
class公共信息: Qdisc_class_common
具体的qdisc会在Qdisc_class_common定义的基础上扩展定义符合自己要求的class,扩展时只需要在扩展类型的第一个字段放置一个该类型成员即可。
struct Qdisc_class_common
{
u32 classid; // class ID
struct hlist_node hnode; // 将class组织到哈希表中
};
filter
分类qdisc只有搭配filter之后,才能将期望的数据包导向不同的class,进而达到QoS目的。
filter对象: tcf_proto
filter被配置到内核后,最终是一个个的tcf_proto对象,这些对象会被组织到qdisc或者class的filter链表中。
struct tcf_proto
{
/* Fast access part */
struct tcf_proto *next; // 方便qdisc或者class将filter组织成一个单链表
void *root;
// 就是ops->classify()
int (*classify)(struct sk_buff*, struct tcf_proto*, struct tcf_result *);
__be16 protocol; // 该filter要过滤报文所属协议,可取ETH_P_IP之类的值
/* All the rest */
u32 prio; // 优先级
u32 classid; // parent class ID
// 如果filter属于qdisc,那么就是对应的qdisc对象;
// 如果filter属于class,那么是class所属qdisc;
struct Qdisc *q;
void *data;
struct tcf_proto_ops *ops; // filter操作集
};
filter操作集: tcf_proto_ops
和qdisc操作集类似,内核的filter模块要提前向流控框架注册一个filter操作集对象,这样内核才可以支持该filter。当filter对象被创建时,要找到它对应的filter操作集。
struct tcf_proto_ops
{
struct tcf_proto_ops *next; // 所有注册的filter操作集组织到全局tcf_proto_base表中
char kind[IFNAMSIZ]; // filter名字
// filter对数据包的分类判定回调,返回负数表示该filter无法决定数据包分类,否则分类结果保存在参数3中
int (*classify)(struct sk_buff*, struct tcf_proto*, struct tcf_result *);
int (*init)(struct tcf_proto*); // filter对象被创建时回调,必须提供
void (*destroy)(struct tcf_proto*); // filter对象被销毁时回调
unsigned long (*get)(struct tcf_proto*, u32 handle);
void (*put)(struct tcf_proto*, unsigned long);
int (*change)(struct tcf_proto*, unsigned long, u32 handle,
struct nlattr **, unsigned long *);
int (*delete)(struct tcf_proto*, unsigned long);
void (*walk)(struct tcf_proto*, struct tcf_walker *arg);
/* rtnetlink specific */
int (*dump)(struct tcf_proto*, unsigned long, struct sk_buff *skb, struct tcmsg*);
struct module *owner;
};
注册filter: register_tcf_proto_ops()
/* The list of all installed classifier types */
static struct tcf_proto_ops *tcf_proto_base __read_mostly;
/* Protects list of registered TC modules. It is pure SMP lock. */
static DEFINE_RWLOCK(cls_mod_lock);
int register_tcf_proto_ops(struct tcf_proto_ops *ops)
{
struct tcf_proto_ops *t, **tp;
int rc = -EEXIST;
// filter的名字要保持唯一
write_lock(&cls_mod_lock);
for (tp = &tcf_proto_base; (t = *tp) != NULL; tp = &t->next)
if (!strcmp(ops->kind, t->kind))
goto out;
ops->next = NULL;
*tp = ops;
rc = 0;
out:
write_unlock(&cls_mod_lock);
return rc;
}
类似的,unregister_tcf_proto_ops()为去注册接口。
分类过程: tc_classify()
class或者qdisc可以通过框架提供的tc_classify()判断某个filter链表是否可以对数据包的去向做出判定。
// 判定结果,一定是指向某个类,根据下面的action来解释该结构
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0
#define TC_ACT_RECLASSIFY 1
#define TC_ACT_SHOT 2
#define TC_ACT_PIPE 3
#define TC_ACT_STOLEN 4
#define TC_ACT_QUEUED 5
#define TC_ACT_REPEAT 6
#define TC_ACT_JUMP 0x10000000
// filter分类结果。如果class有效,那么应该指向对应的class指针,否则为NULL,
// 此时class ID有效,表明要分到的class的句柄
struct tcf_result
{
unsigned long class;
u32 classid;
};
// 返回负数表示分类失败,否则res中保存了分类结果
int tc_classify(struct sk_buff *skb, struct tcf_proto *tp, struct tcf_result *res)
{
int err = 0;
__be16 protocol;
#ifdef CONFIG_NET_CLS_ACT
struct tcf_proto *otp = tp;
reclassify:
#endif
protocol = skb->protocol; // 数据包所属协议
err = tc_classify_compat(skb, tp, res);
#ifdef CONFIG_NET_CLS_ACT
if (err == TC_ACT_RECLASSIFY) {
u32 verd = G_TC_VERD(skb->tc_verd);
tp = otp;
if (verd++ >= MAX_REC_LOOP) {
printk("rule prio %u protocol %02x reclassify loop, "
"packet dropped\n",
tp->prio&0xffff, ntohs(tp->protocol));
return TC_ACT_SHOT;
}
skb->tc_verd = SET_TC_VERD(skb->tc_verd, verd);
goto reclassify;
}
#endif
return err;
}
/* Main classifier routine: scans classifier chain attached
to this qdisc, (optionally) tests for protocol and asks
specific classifiers.
*/
int tc_classify_compat(struct sk_buff *skb, struct tcf_proto *tp, struct tcf_result *res)
{
__be16 protocol = skb->protocol;
int err = 0;
// 遍历filter链表,让协议匹配的filter对数据包做出判定,
// 如果判定结果大于等于0,则表示有了判定结论,结束整个判定过程
for (; tp; tp = tp->next) {
if ((tp->protocol == protocol || tp->protocol == htons(ETH_P_ALL)) &&
(err = tp->classify(skb, tp, res)) >= 0) { // 核心还是让filter自己判定
#ifdef CONFIG_NET_CLS_ACT
if (err != TC_ACT_RECLASSIFY && skb->tc_verd)
skb->tc_verd = SET_TC_VERD(skb->tc_verd, 0);
#endif
return err;
}
}
return -1;
}