流量控制机制概述

本文深入探讨Linux网络流量控制机制,包括qdisc、class和filter。qdisc是流量控制核心,分为无类和有类两种,有特殊句柄如TC_H_ROOT表示根。class在有类qdisc中以树形结构组织,而filter负责数据包分类。文章详细介绍了相关数据结构和操作,如qdisc对象、class哈希表和filter操作集。
摘要由CSDN通过智能技术生成

数据结构

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的指定有一定的约束,规则如下:

  1. 以qdisc为父亲的class的ID,其主号码必须是该qdisc的主号码,次号码自由分配;
  2. 以某个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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值