tc的基本工作原理

本篇中将对tbf qdisc的创建过程以及数据包enqueue/dequeue的过程进行分析,以此来观察tc的基本工作原理。

代码框架

应用层的tc工具由iproute2实现,iproute2是开源的,可以从这个地方下载源码:https://mirrors.edge.kernel.org/pub/linux/utils/net/iproute2/
写者阅读的是iproute2-5.11.0,tc的代码位于iproute2-5.11.0/tc/ 目录。tc有多种类型的qdisc,不同类型的qdisc所需要的参数不同,所以tc把qdisc的代码进行了分离,每种类型的qdisc都有一个q_*.c。

tc_iproute_code_framework.png

添加qdisc时,入口函数位于tc_qdisc.c文件的do_qdisc函数,tc_qdisc.c文件中的代码会解析一些公共的参数,比如handle,parent等参数。
而私有参数的解析则由q_*.c中的代码来完成,每个q_*.c都会构造一个 struct qdisc_util类型的结构体变量,提供一系列的回调,当添加qdisc时,tc_qdisc.c中的代码会根据qdisc的类型来查找struct qdisc_util结构体变量,并调用parse_qopt来解析qdisc私有参数。

struct qdisc_util {
	struct  qdisc_util *next;
	// id用于匹配qdisc
	const char *id;
	// 添加qdisc时,parse_qopt 用于解析qdisc私有参数
	int (*parse_qopt)(struct qdisc_util *qu, int argc, char **argv, struct nlmsghdr *n, const char *dev);
	// print_qopt 用于打印帮助信息
	int (*print_qopt)(struct qdisc_util *qu, FILE *f, struct rtattr *opt);
	int (*print_xstats)(struct qdisc_util *qu, FILE *f, struct rtattr *xstats);
	
	// 添加class时,parse_qopt 用于解析qdisc私有参数
	int (*parse_copt)(struct qdisc_util *qu, int argc, char **argv, struct nlmsghdr *n, const char *dev);
	// print_copt 用于打印class相关的帮助信息
	int (*print_copt)(struct qdisc_util *qu, FILE *f, struct rtattr *opt);
	int (*has_block)(struct qdisc_util *qu, struct rtattr *opt, __u32 block_idx, bool *p_has);
};

比如说,q_tbf.c中构造的struct qdisc_util如下所示,因为tbf不支持手动添加class,所以它没有提供parse_copt回调。

struct qdisc_util tbf_qdisc_util = {
	.id		= "tbf",
	.parse_qopt	= tbf_parse_opt,
	.print_qopt	= tbf_print_opt,
};

当我们想要了解某类qdisc的参数如何传递时,就去看它的对应的struct qdisc_utilparse_qopt函数。

在内核中,tc相关的代码位于linux-5.4.246/net/shced/目录, qdisc的核心代码和qdisc类型相关代码也是分离的,如下图所示:

tc_kenel_qdisc_code_framework.png

每个类型的qdisc都有一个sch_*.c文件,每种类型的qdisc都会构造一个struct Qdisc_ops和一个struct Qdisc_class_ops类型的结构体变量,并注册到tc的核心层,来自应用层的请求会先经过tc核心层的通用处理,随后就通过Qdisc_opsQdisc_class_ops提供的回调转接到具体qdisc模块,Qdisc_opsQdisc_class_ops的定义如下:

struct Qdisc_ops {
	struct Qdisc_ops	*next;
	const struct Qdisc_class_ops	*cl_ops; // 指向Qdisc_class_ops的指针,Qdisc_class_ops包含很多回调函数,操作class或者filter时都会调用其中的回调
	char			id[IFNAMSIZ]; // qdisc的类型,比如tbf,htb,sfq,这个id将会用于匹配Qdisc_ops
	int			priv_size; // tc核心层使用struct Qdisc来描述qdisc公共属性,为了描述qdisc的私有属性,不同种类的qdisc都声明了自己的私有结构体类型,tc核心层在分配struct Qdisc时会顺带分配qdisc的私有结构,这里的priv_size就是用来告知tc核心层私有结构需要占用多大的空间
	unsigned int		static_flags;

	// 输出路径上,数据包enqueue和dequeue时,就会调用enqueue和dequeue函数,qdisc的规则就是在这两个函数中实现的
	int 			(*enqueue)(struct sk_buff *skb,struct Qdisc *sch,struct sk_buff **to_free);
	struct sk_buff *	(*dequeue)(struct Qdisc *);
	// peek函数在dequeue过程中调用,它被用于从下级qdisc中预取数据包
	struct sk_buff *	(*peek)(struct Qdisc *);
	
	// tc核心层创建qdisc的过程中将调用init函数,通常情况下,各类型的qdisc会在init函数中解析自己的私有参数
	int			(*init)(struct Qdisc *sch, struct nlattr *arg, struct netlink_ext_ack *extack);
	void			(*reset)(struct Qdisc *);
	void			(*destroy)(struct Qdisc *);
	int			(*change)(struct Qdisc *sch,struct nlattr *arg,struct netlink_ext_ack *extack);
	// attatch函数
	void			(*attach)(struct Qdisc *sch);
	int			(*change_tx_queue_len)(struct Qdisc *, unsigned int);
	void			(*change_real_num_tx)(struct Qdisc *sch,
						      unsigned int new_real_tx);

	// 应用层调用tc qdisc show时,将会调用dump函数来获取qdisc的信息
	int			(*dump)(struct Qdisc *, struct sk_buff *);
	int			(*dump_stats)(struct Qdisc *, struct gnet_dump *);

	// 目前来看,ingress_block_set,egress_block_set,ingress_block_get,egress_block_get这四个函数是ingress和clsact这两个qdisc专用的,现在暂且不管它们的作用,以后分析ingress qdisc时再来分析它们
	void			(*ingress_block_set)(struct Qdisc *sch, u32 block_index);
	void			(*egress_block_set)(struct Qdisc *sch, u32 block_index);
	u32			(*ingress_block_get)(struct Qdisc *sch);
	u32			(*egress_block_get)(struct Qdisc *sch);

	struct module		*owner;
};


// Qdisc_class_ops 主要是提供一系列操作class和filter的回调,tc核心层在某些情况下会调用这些回调
struct Qdisc_class_ops {
	unsigned int		flags;
	/* Child qdisc manipulation */
	struct netdev_queue *	(*select_queue)(struct Qdisc *, struct tcmsg *);
	// graft 函数用于将新建的leaf qdisc与上级class建立联系
	int			(*graft)(struct Qdisc *, unsigned long cl,
					struct Qdisc *, struct Qdisc **,
					struct netlink_ext_ack *extack);
	// leaf函数用于返回class内部包含的leaf qdisc
	struct Qdisc *		(*leaf)(struct Qdisc *, unsigned long cl);
	void			(*qlen_notify)(struct Qdisc *, unsigned long);

	/* Class manipulation routines */
	// find函数用于查找qdisc下某个特定的class
	unsigned long		(*find)(struct Qdisc *, u32 classid);
	// change函数用于初始化class,当创建class时,change函数会被调用
	int			(*change)(struct Qdisc *, u32, u32,
					struct nlattr **, unsigned long *,
					struct netlink_ext_ack *);
	int			(*delete)(struct Qdisc *, unsigned long);
	// walk函数用于遍历qdisc下所有的class,对每个class调用qdisc_walker指定的回调函数。应用层执行tc class show dev ens333之类的命令时,就会调用到该walk回调来遍历qdisc下所有的class
	void			(*walk)(struct Qdisc *, struct qdisc_walker * arg);

	/* Filter manipulation */
	// tcf_block用于获取qdisc的filter block,在内核里面filter是按照block--chain--filter这样的层级来组织的,创建qdisc时filter block便会一同创建
	// 给qdisc添加filter时,内核首先会找到qdisc的filter block,然后再在filter block下创建chain和filter。若某个qdisc的tcf_block 函数指针为空,则就无法给这个qdisc添加filter
	struct tcf_block *	(*tcf_block)(struct Qdisc *sch,
					     unsigned long arg,
					     struct netlink_ext_ack *extack);
			
	// 因为有的filter会把流量导向class,在内核看来,这样的class和filter应该建立某些联系
	// 所以在新建class后,内核会尝试绑定class和filter,给class绑定filter时,就会调用bind_tcf函数
	unsigned long		(*bind_tcf)(struct Qdisc *, unsigned long,
					u32 classid);
	void			(*unbind_tcf)(struct Qdisc *, unsigned long);

	/* rtnetlink specific */
	// dump函数用于导出qdisc下某个class的信息
	int			(*dump)(struct Qdisc *, unsigned long,
					struct sk_buff *skb, struct tcmsg*);
	int			(*dump_stats)(struct Qdisc *, unsigned long,
					struct gnet_dump *);
};

tbf qdisc注册到tc核心层的struct Qdisc_ops如下所示:

static const struct Qdisc_class_ops tbf_class_ops = {
	.graft		=	tbf_graft,
	.leaf		=	tbf_leaf,
	.find		=	tbf_find,
	.walk		=	tbf_walk,
	.dump		=	tbf_dump_class,
};

static struct Qdisc_ops tbf_qdisc_ops __read_mostly = {
	.next		=	NULL,
	.cl_ops		=	&tbf_class_ops,
	.id		=	"tbf",
	.priv_size	=	sizeof(struct tbf_sched_data),
	.enqueue	=	tbf_enqueue,
	.dequeue	=	tbf_dequeue,
	.peek		=	qdisc_peek_dequeued,
	.init		=	tbf_init,
	.reset		=	tbf_reset,
	.destroy	=	tbf_destroy,
	.change		=	tbf_change,
	.dump		=	tbf_dump,
	.owner		=	THIS_MODULE,
};

当我们想要了解内核中某类qdisc的工作原理时,就从它对应的struct Qdisc_ops入手分析,想知道它如何初始化私有数据就看init函数,想知道它如何执行规则就看enqueuedequeue函数。

内核中qdisc的组织结构

内核中网络设备、网卡输出队列、qdisc之间的逻辑关系大概是这样的:
dev queue and qdisc.png

添加到网络设备的qdisc会被关联到网卡的队列上。输入方向上的队列只能添加ingress和clsact类型的qdisc,它们是classless的,下端无法添加其它qdisc,故输入方向上的队列只能关联一个qdisc 。而输出方向上队列因为可以添加classfull的qdisc,所以可以添加多个qdisc,添加多个qdisc后会形成类似上图的树状结构,数据包在enqueue进队列时,会从树状结构的根节点进入,往叶子节点的方向流动。
不难看出,一个qdisc包含class时,它下端才能才能添加其它qdisc,在上图描述的树状结构中,qdisc(20:0)通过class(10:1)级联在qdisc(10:0)下,对于qdisc(20:0)而言,class(10:1)是它的parent class,而qdisc(10:0)是它的parent qdisc。对于class(10:1)而言,qdisc(20:0)是它的leaf qdisc。

上面描绘的网络设备只存在一个输出队列,当网络设备存在多个输出队列时,网络设备、队列、qdisc之间的逻辑关系大概如下所示:
dev multi queue.png

如上图所示,多队列时,可能存在两种情形:

  • 一种是网络设备的每个输出队列都关联到同一个qdisc上,这种情况下qdisc控制的是整个网络设备的流量。
  • 另一种是网络设备的每个输出队列都关联到不同qdisc上,每个输出队列都有不同的流控规则,qdisc控制的是输出队列的流量。在使用mq/mqprio这类qdisc时才会产生这种情形。

内核使用struct netdev_queue来描述队列,使用struct Qdisc来描述qdisc,接下来简单看下这两个结构体的组成。
struct netdev_queue结构体成员如下(省略了部分结构体成员):

struct netdev_queue {
	struct net_device	*dev; // 指向网络设备的指针
	struct Qdisc __rcu	*qdisc; // 网络设备up后,qdisc指针将指向与队列关联的qdisc
	struct Qdisc		*qdisc_sleeping; // 网络设备没up前,qdisc_sleeping 指针将指向与队列关联的qdisc,上面那个qdisc指针为NULL
	......
	unsigned long		state; // state用于记录网络设备队列的状态,有的时候驱动的发送速度跟不上上层协议栈发送速度时,就会修改state,网络设备层会根据该state来判断是否应该继续往驱动送数据包
	......
} ____cacheline_aligned_in_smp

struct Qdisc结构体成员大致如下:

struct Qdisc {
	// 创建qdisc时,enqueue和dequeue两个函数指针会被赋值为Qdisc_ops中的enqueue和dequeue
	int 			(*enqueue)(struct sk_buff *skb,struct Qdisc *sch, struct sk_buff **to_free);
	struct sk_buff *	(*dequeue)(struct Qdisc *sch);
	unsigned int		flags;
        ......
	u32			limit;
	
	const struct Qdisc_ops	*ops; // 记录 Qdisc_ops的指针
	struct qdisc_size_table	__rcu *stab;
	
	struct hlist_node       hash; // hash是用于链接到hash table的节点。虽然从逻辑关系上来看,qdisc是与队列关联在一起的,但网络设备(net_device)也会使用hash table来组织属于本设备的qdisc
	u32			handle; // qdisc 的handle
	u32			parent; // 记录parent qdisc 的handle

	struct netdev_queue	*dev_queue; // dev_queue 是指向网络设备队列的指针

	struct net_rate_estimator __rcu *rate_est;
	struct gnet_stats_basic_cpu __percpu *cpu_bstats;
	struct gnet_stats_queue	__percpu *cpu_qstats;
	int			padded;
	refcount_t		refcnt;

	/*
	 * For performance sake on SMP, we put highly modified fields at the end
	 */
	struct sk_buff_head	gso_skb ____cacheline_aligned_in_smp; // gso_skb 用于缓存那些从当前qdisc enqueue出去但又因某些原因无法被送往驱动层而塞回来的数据包
	struct qdisc_skb_head	q; // 缓存数据包的队列。需要注意的是并非所有类型的qdisc都会用这个q来缓存数据包
	struct gnet_stats_basic_packed bstats;
	seqcount_t		running;
	struct gnet_stats_queue	qstats;
	unsigned long		state; // qdisc的状态,刚创建qdisc时,qdisc处于未激活状态,当网卡up后,qdisc才会进入激活状态
	struct Qdisc            *next_sched;
	struct sk_buff_head	skb_bad_txq;

	spinlock_t		busylock ____cacheline_aligned_in_smp;
	spinlock_t		seqlock;

	/* for NOLOCK qdisc, true if there are no enqueued skbs */
	bool			empty;
	struct rcu_head		rcu;
};

内核描述网络设备的数据结构是struct net_device, 这里简单记录一些与qdisc相关的结构体成员

net_device
	struct netdev_queue __rcu *ingress_queue; // ingress_queue 指针指向输入方向上的tc 队列
		
	struct netdev_queue	*_tx ____cacheline_aligned_in_smp; // _tx 指向一个指针数组,数组每个成员都是一个指向输出队列的指针
	unsigned int		num_tx_queues; // 输出队列的数目
	
	DECLARE_HASHTABLE	(qdisc_hash, 4); // 用于链接qdisc的hash table
	
	struct Qdisc __rcu	*qdisc // qdisc指向输出方向上的root qdisc,给网络设备添加qdisc时指定了root参数,该qdisc就是root qdisc,root qdisc往往是第一个添加的qdisc,一个网络设备只能有一个root qdisc,其余的qdisc会链接到上面的那个hash table, 而root qdisc不会。

qdisc创建过程

接下来以tbf qdisc为例来分析qdisc的创建过程,假设执行了如下指令:

tc qdisc add dev ens33 handle 10: root tbf rate 10mbps burst 1m limit 1m

tbf qdisc使用令牌桶来限制发送速率,上述命令中指定了三个参数:

  • rate是限制的发送速率
  • burst是桶的大小
  • limit是缓存于tbf qdisc上等待令牌的数据包的总大小,当缓存的数据包总量超过该值,tbf qdisc就会开始丢弃数据包

应用层tc工具创建qdsic的过程

tc qdisc的命令参数分为两个部分,一部分是添加所有类型qdisc都需要传递的公共参数(比如dev, handle),另一部分是qdisc的私有参数。
公共参数由tc_qdisc_modify函数处理,大致过程如下(省略了部分代码):

// tc_qdisc.c
int tc_qdisc_modify(int cmd, unsigned int flags, int argc, char **argv)
{
	struct qdisc_util *q = NULL; // 不同qdisc有不同的qdisc_util,q指针稍后会根据qdisc类型进行赋值
	char  d[IFNAMSIZ] = {}; // 用于暂存dev name
	char  k[FILTER_NAMESZ] = {}; // 用于暂存 qdisc name
	struct {
		struct nlmsghdr	n; // netlink消息头部,tc工具通过netlink将消息传递到内核层,添加qdisc时使用的netlink command为 RTM_NEWQDISC
		struct tcmsg		t; // dev,handle,parent这几个参数会通过这个struct tcmsg传递到内核
		char			buf[TCA_BUF_MAX]; // 其它参数则存放在buf中传递到内核
	} req ;

	......
	// 1 循环处理参数,将参数填充到tcmsg中
	while (argc > 0) {
		if (strcmp(*argv, "dev") == 0) {
			......
			// 1.1 临时记录下dev,稍后解析
			strncpy(d, *argv, sizeof(d)-1);
		} else if (strcmp(*argv, "handle") == 0) {
			......
			// 1.2 记录handle
			req.t.tcm_handle = handle;
		} else if (strcmp(*argv, "root") == 0) {
			......
			// 1.3 指定了root参数,则tcm_parent配置为一个特殊值 TC_H_ROOT
			req.t.tcm_parent = TC_H_ROOT;
		} else if (strcmp(*argv, "clsact") == 0) {
			......
			// 1.4 指定了clsact参数,则tcm_parent配置为一个特殊值 TC_H_CLSACT
			req.t.tcm_parent = TC_H_CLSACT;
			strncpy(k, "clsact", sizeof(k) - 1);
			// 查找clsact对应的qdisc_util
			q = get_qdisc_kind(k);
			// handle赋值为一个特殊值,ffff:0
			req.t.tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0);
			NEXT_ARG_FWD();
			break;
		} else if (strcmp(*argv, "ingress") == 0) {
			......
			// 1.4 指定了ingress参数,则tcm_parent配置为一个特殊值 TC_H_INGRESS
			req.t.tcm_parent = TC_H_INGRESS;
			// 查找ingress对应的qdisc_util
			strncpy(k, "ingress", sizeof(k) - 1);
			q = get_qdisc_kind(k);
			// handle赋值为一个特殊值,ffff:0
			req.t.tcm_handle = TC_H_MAKE(TC_H_INGRESS, 0);
			NEXT_ARG_FWD();
			break;
		} else if (strcmp(*argv, "parent") == 0) {
			// 1.5 指定了parent参数,则校验parent对应的class是否存在
			__u32 handle;
			if (get_tc_classid(&handle, *argv))
				invarg("invalid parent ID", *argv);
			req.t.tcm_parent = handle;
		}
		......
		else {
			// 1.6 根据qdisc名字查找qdisc_util
			strncpy(k, *argv, sizeof(k)-1);

			q = get_qdisc_kind(k);
			argc--; argv++;
			break;
		}
		......
		argc--; argv++;
	}
	
	// 1.7 qdisc类型通过 TCA_KIND 这个attr传递到内核
	if (k[0])
		addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);
	
	// 2 调用qdisc_util的parse_qopt函数来解析qdisc私有参数
	if (q) {
		if (q->parse_qopt) {
			if (q->parse_qopt(q, argc, argv, &req.n, d))
				return 1;
		}
	}
		
	// 1.8 dev参数转换为ifindex
	if (d[0])  {
		int idx;

		ll_init_map(&rth);
		idx = ll_name_to_index(d);
		......
		req.t.tcm_ifindex = idx;
	}
	
	// 3 发送netlink消息到内核
	rtnl_talk(&rth, &req.n, NULL)
}

tbf qdisc的私有参数由tbf_parse_opt函数解析,rate/burst/limit三个参数的解析过程如下:

// q_tbf.c
int tbf_parse_opt(struct qdisc_util *qu, int argc, char **argv,struct nlmsghdr *n, const char *dev){
	struct tc_tbf_qopt opt = {};
	unsigned buffer = 0;
	__u64 rate64 = 0;
	......
	// 1 循环读取参数
	while (argc > 0) {
		if (matches(*argv, "limit") == 0) {
			if (get_size(&opt.limit, *argv)) {
				explain1("limit", *argv);
				return -1;
			}
			ok++;
		}
		......
		else if (matches(*argv, "burst") == 0 ||
			strcmp(*argv, "buffer") == 0 ||
			strcmp(*argv, "maxburst") == 0) {

			if (get_size_and_cell(&buffer, &Rcell_log, *argv) < 0) {
				explain1(parm_name, *argv);
				return -1;
			}
			ok++;
		}
		......
		else if (strcmp(*argv, "rate") == 0) {
			......
			else if (get_rate64(&rate64, *argv)) {
				explain1("rate", *argv);
				return -1;
			}
			ok++;
		}
		
		argc--; argv++;
	}
	
	......
	// 2 将rate/burst/limit等参数放入netlink attr
	
	// 2.1 rate超过了u32就会放入TCA_TBF_RATE64这个netlink attr进行传递,否则通过TCA_TBF_PARMS这个netlink attr进行传递
	opt.rate.rate = (rate64 >= (1ULL << 32)) ? ~0U : rate64;
	if (rate64 >= (1ULL << 32))
		addattr_l(n, 2124, TCA_TBF_RATE64, &rate64, sizeof(rate64));
	
	// 2.2 使用rate将burst转换为时间,通过TCA_TBF_PARMS这个netlink attr进行传递
	opt.buffer = tc_calc_xmittime(opt.rate.rate, buffer);
	
	// 2.3 将rate(小于u32时)和burst(以时间为单位的buffer)放入了TCA_TBF_PARMS这个netlink attr
	addattr_l(n, 2024, TCA_TBF_PARMS, &opt, sizeof(opt));
	
	// 2.4 将未转换为时间的burst放入了TCA_TBF_BURST这个netlink attr
	addattr_l(n, 2124, TCA_TBF_BURST, &buffer, sizeof(buffer));
}

tbf_parse_opt函数中2.2将burst转换成时间的操作可能有点奇怪,其实是这样的:

按照常规的理解,令牌桶中令牌的单位是Byte,为了实现限速,令牌桶按照限定的速率(rate)来生成令牌,桶中的令牌数目会逐渐增多,总令牌数目受到桶大小(burst)的制约,输出数据包时,若桶中令牌足够,就从桶中取走与数据包大小等量的令牌,并放行数据包,若桶中令牌不足,就将数据包缓存起来等待令牌桶补充令牌。
把令牌的单位转换为时间,这样,每经过多长时间,令牌桶就增长相同时间的令牌,桶中的时间会逐渐增多,但也会受到桶大小(此时的桶大小是通过rate和burst计算出来的,桶的大小也以时间为单位)的制约,输出数据包时,会使用数据包的尺寸来计算rate速率下输出该数据包的时间,若桶中时间足够,则从桶中扣除一定时间并放行数据包,若桶中时间不够则将数据包缓存起来等待令牌桶补充时间。

内核创建qdisc的过程

应用层创建qdisc的请求传递到内核后,会先经过tc核心代码的处理,tc核心代码会负责分配qdisc对应的数据结构并处理parent/handle等公共参数。
内核中创建qdisc的入口函数是tc_modify_qdisc,接下来看看它的大致实现(省略了部分代码,另外需要注意tc_modify_qdisc也会处理修改qdisc的请求):

// linux-5.4.246/net/shced/sch_api.c
int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n, struct netlink_ext_ack *extack){
	struct tcmsg *tcm;
	struct nlattr *tca[TCA_MAX + 1];
	struct net_device *dev;
	u32 clid;
	
	struct Qdisc *q, *p;

	// 1 从netlink消息中取出应用层传递的参数
	nlmsg_parse_deprecated(n, sizeof(*tcm), tca, TCA_MAX, rtm_tca_policy, extack);
	tcm = nlmsg_data(n); // dev/handle/parent这三个参数都在tcm指向的数据结构中
	
	clid = tcm->tcm_parent;
	q = p = NULL;
	
	if (clid) { 
		// 2 clid不为0代表应用层传递了parent参数过来,接下来的代码根据应用层传递的parent参数查找parent qdisc和目标qdisc(要被修改或者被替换的那个qdisc,目标qdisc也可能不存在)
		// 下面的过程中p将指向parent qdisc,q将指向目标qdisc
		if (clid != TC_H_ROOT) {
			if (clid != TC_H_INGRESS) {
				// 2.1 parent既不是root也不是ingress的情形
				p = qdisc_lookup(dev, TC_H_MAJ(clid)); // p最终指向parent qdisc
				q = qdisc_leaf(p, clid); // q最终指向 parent class的leaf qdisc
			} else if (dev_ingress_queue_create(dev)) { 
				// 2.2 parent为ingress的情形, p为空,q的值则取决于 qdisc_sleeping, 如果输入方向已经添加过qdisc,则qdisc_sleeping不为空
				q = dev_ingress_queue(dev)->qdisc_sleeping;
			}
		} else {
			// 2.3 parent 为 root的情形,p为空,q的值则取决于是否已经添加过root qdisc
			q = rtnl_dereference(dev->qdisc);
		}
		
		// 3 下面的流程判断如何操作目标qidsc
		if (!q || !tcm->tcm_handle || q->handle != tcm->tcm_handle) {
			if (tcm->tcm_handle) {
				//3.1 应用层传递的handle不为0,则要使用handle为tcm->tcm_handle的qdisc来替换目标位置的qdisc
				q = qdisc_lookup(dev, tcm->tcm_handle); // 3.1 若不存在handle为tcm->tcm_handle的qdisc,则进入新建qdisc流程
				if (!q)
					goto create_n_graft;
				//3.2 若存在handle为tcm->tcm_handle的qdisc,则用它来替换目标qdisc
				goto graft;
			}else{
				// 3.3 应用层传递的handle为0,则表示可能会修改目标qdisc,
				if (!q) // 目标qdisc不存在,则在目标qdisc的位置上创建一个新的qdisc
					goto create_n_graft;
				// 3.4 目标qdisc存在,则进入修改qdisc的流程
			}
		}
		// 3.5 q(目标qdisc)不为空,应用层传递的handle不为0,目标qdisc的handle 与应用层传递的handle相等,则表示要修改目标qdisc,进入修改流程
	}else{
		// 4 应用层传递的parent参数为0,handle不为0,则表示要修改handle为tcm->tcm_handle的qdisc,进入修改流程
		q = qdisc_lookup(dev, tcm->tcm_handle);
	}
	
	// 5 调用Qdisc_ops的 change 回调来修改qdisc
	qdisc_change(q, tca, extack);
		sch->ops->change(sch, tca[TCA_OPTIONS], extack);
		
	// 6 创建新的qdisc
	create_n_graft:
		if (clid == TC_H_INGRESS) {
			// 6.1 创建作用于包输入方向上的 ingress/clsact qdisc
			if (dev_ingress_queue(dev)) { // dev_ingress_queue 获取ingress的netdev queue
				q = qdisc_create(dev, dev_ingress_queue(dev), p,tcm->tcm_parent, tcm->tcm_parent,tca, &err, extack);
			} 
		} else {
			// 6.2 创建作用于包输出方向上的qdisc
			struct netdev_queue *dev_queue;
			// 6.2.1 选择一个合适的输出队列
			if (p && p->ops->cl_ops && p->ops->cl_ops->select_queue)
				dev_queue = p->ops->cl_ops->select_queue(p, tcm); // 6.2.1.1若parent qdisc的Qdisc_class_ops有提供select_queue回调,则调用select_queue来选择队列,通常只有mq/mqprio这类qdisc才会提供select_queue回调
			else if (p)
				dev_queue = p->dev_queue; // 6.2.1.2 选择parent qdisc关联的队列
			else
				dev_queue = netdev_get_tx_queue(dev, 0); // 6.2.1.3 获取0号队列
			
			// 6.2.2 创建qdisc
			q = qdisc_create(dev, dev_queue, p,tcm->tcm_parent, tcm->tcm_handle,tca, &err, extack);
				
		}
	
	//7 将新建的qdisc或者用来替换目标qdisc的qdisc与 parent建立联系
graft:	
	qdisc_graft(dev, p, skb, n, clid, q, NULL, extack);
	
}

对于新建tbf qdisc的例子,会经过上述代码中1,2.3, 3.1, 6.2, 6.2.1.3, 6.2.2 7 所标注的流程。
tc_modify_qdisc调用两个比较核心的函数:qdisc_createqdisc_graft
qdisc_create负责分配描述qdisc的数据结构并初始化它,而qdisc_graft函数负责将新建的qidsc与它的parent建立联系。
qdisc_create函数的实现大致如下(省略了部分代码):

// linux-5.4.246/net/shced/sch_api.c
// dev_queue: 指向队列的指针
// p: 指向parent qdisc
// parent: parent的handle
// handle: 新建qdisc的handle
// tca: netlink attr
static struct Qdisc *qdisc_create(struct net_device *dev,
				  struct netdev_queue *dev_queue,
				  struct Qdisc *p, u32 parent, u32 handle,
				  struct nlattr **tca, int *errp,
				  struct netlink_ext_ack *extack){
	
	struct nlattr *kind = tca[TCA_KIND]; // 通过 TCA_KIND获取应用层传递的qdisc类型
	struct Qdisc *sch; // sch将指向新建的qdisc
	struct Qdisc_ops *ops;
	
	// 1 查找qdisc ops
	ops = qdisc_lookup_ops(kind); 
	
	// 2 分配描述qdisc的数据结构(struct Qdisc)并初始化它,每种类型的qidsc都有不同的私有数据结构,qdisc_alloc在分配struct Qdisc的同时会一并分配qdisc的私有数据结构
	sch = qdisc_alloc(dev_queue, ops, extack);
		// 计算需分配的内存大小,这里的ops->priv_size即是qdisc的私有数据尺寸,私有数据会被放置在struct Qdisc结构体变量尾部,以tbf为例,Qdisc尾部是 一个 tbf_sched_data类型的结构体变量
		size = QDISC_ALIGN(sizeof(*sch)) + ops->priv_size
		p = kzalloc_node(size, GFP_KERNEL, netdev_queue_numa_node_read(dev_queue));
		
		// 初始化用来缓存skb的链表头
		__skb_queue_head_init(&sch->gso_skb);
		qdisc_skb_head_init(&sch->q);
		
		sch->ops = ops; // 记录ops
		sch->flags = ops->static_flags;
		// 记录enqueue和dequeue函数
		sch->enqueue = ops->enqueue;
		sch->dequeue = ops->dequeue;
		// 记录队列
		sch->dev_queue = dev_queue;
		sch->empty = true;
		......
	// 3 记录qdisc的parent
	sch->parent = parent;
	
	// 4 记录qdisc的handle
	if (handle == TC_H_INGRESS)
		handle = TC_H_MAKE(TC_H_INGRESS, 0); // 4.1 ingress类型的qdisc其handle固定为ffff:0
	else
		if (handle == 0) 
			handle = qdisc_alloc_handle(dev); // 4.2 没指定handle时默认分配一个
	sch->handle = handle; // 4.3 记录qdisc 的handle
	
	// 5 设置block id, 对于ingress和clsact这两个类型的qdisc而言比较重要,其它类型不会使用
	qdisc_block_indexes_set(sch, tca, extack);
	
	// 6 调用Qdisc_ops的init回调,通常init回调会负责初始化qdisc的私有数据
	if (ops->init)
		ops->init(sch, tca[TCA_OPTIONS], extack);
		
	// 7 将新分配的qdisc链接到网络设备的hash table上
	qdisc_hash_add(sch, false); // qdisc_hash_add会进行一些逻辑判断,不是root qdisc也不是ingress/clsact qdisc时, 才会将qdisc链入网络设备的hash table
}

对于新建tbf qdisc的例子,会经过上述代码中1, 2, 3 ,4.3, 6, 7 所标注的流程。
qdisc_graft函数的实现大致如下(省略了部分代码):

// linux-5.4.246/net/shced/sch_api.c
// dev_queue: 指向队列的指针
// parent: 指向parent qdisc
// classid: parent class的classid
// new: 新建的qdisc
static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,
		       struct sk_buff *skb, struct nlmsghdr *n, u32 classid,
		       struct Qdisc *new, struct Qdisc *old,
		       struct netlink_ext_ack *extack){
	// 1 parent qdisc为空,则新建的qdisc是作用于输出方向上的root qdisc或者是作用于输入方向上的ingress/clsact qdisc,这种情况下qdisc是没有parent的,此时要做的就是把qdisc与队列建立联系
	if (parent == NULL) {
		unsigned int i, num_q, ingress;

		// 1.1 判断新建的qdisc是作用于输出方向上的root qdisc或者是作用于输入方向上的ingress/clsact qdisc
		ingress = 0; // ingress为1代表qdisc是作用于输入方向上的ingress/clsact qdisc
		num_q = dev->num_tx_queues; // num_q代表网络设备的输出队列数目
		if ((q && q->flags & TCQ_F_INGRESS) || (new && new->flags & TCQ_F_INGRESS)){ 
			num_q = 1;
			ingress = 1;
		}
		
		// 1.2 仅mq、mqprio这些qdisc才会有new->ops->attach,mq、mqprio比较特殊,它们会导致每个输出队列都关联到不同qdisc上
		if (new && new->ops->attach)
			goto skip;
		
		// 1.3 将新建的qdisc与队列建立联系,需要注意这种情况下每个输出队列其实都关联在同一个qdisc上
		for (i = 0; i < num_q; i++) {
			// 1.3.1 从dev->ingress_queue取得 netdev queue
			struct netdev_queue *dev_queue = dev_ingress_queue(dev);

			if (!ingress) // 1.3.2 若不是ingress,则从dev->_tx[index](输出队列) 取netdev queue
				dev_queue = netdev_get_tx_queue(dev, i);
			
			// 1.3.3 此时把新建的qdisc赋值给了 dev_queue->qdisc_sleeping,而dev_queue->qdisc暂时赋值为noop_qdisc
			old = dev_graft_qdisc(dev_queue, new);
				dev_queue->qdisc_sleeping = qdisc;
				dev_queue->qdisc = &noop_qdisc
		}
skip:	
		if (!ingress) {
			// 1.4 将root qdisc赋值给dev->qdisc
			rcu_assign_pointer(dev->qdisc, new ? : &noop_qdisc);

			// 1.5 若attach回调不为空,则调用attach回调,仅mq、mqprio
			if (new && new->ops->attach)
				new->ops->attach(new);
		}
		
		// 1.6 当netdev是up状态时,dev_activate函数会将qdisc赋值 dev_queue->qdisc,在发包流程中会通过dev_queue->qdisc来获取qdisc
		if (dev->flags & IFF_UP)
			dev_activate(dev); // dev_activate函数还会将qdisc的状态配置为激活状态
	}else{
		// 2 parent qdisc不为空,则表示新建的qdisc将要添加到某个class下
		
		// 2.1 获取parent的 Qdisc_class_ops
		const struct Qdisc_class_ops *cops = parent->ops->cl_ops;
		unsigned long cl;
		
		// 2.2 仅在parent qdisc有Qdisc_class_ops且提供了graft回调的情况下,才能在parent qdisc之下添加新的qdisc,支持class的qdisc都会提供graft回调
		if (!cops || !cops->graft)
			return -EOPNOTSUPP;
		
		// 2.3 在parent qdisc中查找parent class,查找不到则报错返回
		cl = cops->find(parent, classid);
		if (!cl) {
			return -ENOENT;
		}

		// 2.4 调用parent qidsc的graft回调来将新建的qdisc与class建立联系
		err = cops->graft(parent, cl, new, &old, extack);
	}			
}

对于新建tbf qdisc的例子,会经过上述代码中1.1, 1.3.1, 1.3.2, 1.3.3, 1.4 ,1.6所标注的流程。

tc_modify_qdisc 完成了公共部分的处理,简单来看,公共部分所做的事情其实就是:

  • 根据应用层传递的parent参数查找parent qdisc,以确认新建qdisc的插入位置
  • 根据应用层传递的qdisc类型查找 Qdisc_ops
  • 分配qdisc的公有数据结构和私有数据结构需要的内存空间,并初化qdisc的公有数据结构(把parent/handle/dev_queue/Qdisc_ops/enqueue/dequeue这些重要信息记录下来),调用Qdisc_ops的init回调来初始化qdisc的私有数据
  • 将qdisc与parent或者与队列dev_queue建立关联,以便后续enqueue/dequeue的过程中能从parent或者dev_queue找到qdisc
    私有部分的处理会在Qdisc_ops的init回调中进行,以tfb qdisc为例,tbf_init 函数会被调用,它的代码大致如下:
// linux-5.4.246/net/shced/sch_tbf.c
// sch是指向qdisc的指针
// opt是应用层传递的netlink attr
static int tbf_init(struct Qdisc *sch, struct nlattr *opt,struct netlink_ext_ack *extack){
	struct tbf_sched_data *q = qdisc_priv(sch); // 私有数据结构位于qdisc的尾部
	
	q->qdisc = &noop_qdisc;
	
	return tbf_change(sch, opt, extack);
}

tbf_init调用tbf_change函数来完成工作,为了方便理解tbf_change函数做的事情,先看一看描述tbf qdisc的私有数据的结构体变量

struct tbf_sched_data {
/* Parameters */
	u32		limit;	// 缓存在tbf qdisc上等待令牌的数据包总大小,对应应用层传递的limit参数
	u32		max_size;
	s64		buffer;		// 令牌桶大小,对应应用层传递的burst参数
	s64		mtu;
	struct psched_ratecfg rate; // 限定的速率,对应于应用层传递的burst参数
	struct psched_ratecfg peak;

/* Variables */
	s64	tokens;			// 令牌桶内现有的令牌数目
	s64	ptokens;	
	s64	t_c;			// 上一次补充令牌的时间
	struct Qdisc	*qdisc;	// qdisc是tbf qdisc的内部qdisc
	struct qdisc_watchdog watchdog;	/* Watchdog timer */
};

上述tbf_sched_data的qdisc成员是用来指向tbf qdisc的class的leaf qdisc的指针,这里涉及到了一些class相关的内容,简单介绍下。
class的概念其实有点抽象,与qdisc不同,内核没有为class构造一个公共的结构体,用什么结构体描述class完全取决于qdisc的实现。有的类型的qdisc可能会使用一些结构体变量来描述class,比如htb qdisc使用struct htb_class来描述class,而有的类型的qdisc甚至没有结构来描述class,比如tbf qdisc。
但从逻辑上看,tbf qdisc其实是有class,tbf qdisc的class有且仅有一个,这个class不能被修改也不能被删除,添加tbf qdisc后通过tc class show dev xxx也能观察到这个class,只能说这个class是tbf qdisc的为满足tc设计框架而臆想出来的一个没有实体的class。
一个qdisc能否在下端添加其它qidsc的一大必要条件就是它必须有class,tbf qdisc有了class,便可以在它下端添加其它qdisc了,tbf_sched_data结构体的qdisc成员将会指向下端的那个qdisc。
接下来看看tbf_change函数(省略了部分代码):

// linux-5.4.246/net/shced/sch_tbf.c
// sch是指向qdisc的指针
// opt是应用层传递的netlink attr
static int tbf_change(struct Qdisc *sch, struct nlattr *opt, struct netlink_ext_ack *extack){
	struct tbf_sched_data *q = qdisc_priv(sch);
	struct nlattr *tb[TCA_TBF_MAX + 1];
	struct Qdisc *child = NULL;
	struct psched_ratecfg rate;
	u64 max_size;
	s64 buffer, mtu;
	u64 rate64 = 0, prate64 = 0;

	// 1 解析应用层传递的netlink attr,保存到tb数组
	err = nla_parse_nested_deprecated(tb, TCA_TBF_MAX, opt, tbf_policy,
					  NULL);
	// 1.1 应用层传递的参数一部分位于qopt指向的结构体中,一部分则直接存储在netlink attr中
	qopt = nla_data(tb[TCA_TBF_PARMS]);
	
	// 2 从qopt->buffer取得令牌桶尺寸, qopt->buffer是转换为时间单位的令牌桶尺寸
	buffer = min_t(u64, PSCHED_TICKS2NS(qopt->buffer), ~0U);

	// 3 从TCA_TBF_RATE64或者qopt->rate获取限定速率
	if (tb[TCA_TBF_RATE64])
		rate64 = nla_get_u64(tb[TCA_TBF_RATE64]);
	psched_ratecfg_precompute(&rate, &qopt->rate, rate64);

	// 4 若传递了TCA_TBF_BURST,则从TCA_TBF_BURST这个netLink attr中获取令牌桶尺寸,不使用qopt->buffer
	if (tb[TCA_TBF_BURST]) {
		max_size = nla_get_u32(tb[TCA_TBF_BURST]); // max_size是以字节为单位的令牌桶尺寸
		buffer = psched_l2t_ns(&rate, max_size); // buffer是以ns为单位的令牌桶尺寸
	} else {
		max_size = min_t(u64, psched_ns_t2l(&rate, buffer), ~0U);
	}

	// 5 令牌桶尺寸小于网络设备的MTU,这样尺寸比较大的报文可能都永远等不到足够令牌,所以创建tbf qdisc时,需要选取一个比MTU大的桶大小
	if (max_size < psched_mtu(qdisc_dev(sch)))
		pr_warn_ratelimited("sch_tbf: burst %llu is lower than device %s mtu (%u) !\n",
				    max_size, qdisc_dev(sch)->name,
				    psched_mtu(qdisc_dev(sch)));

	if (!max_size) {
		err = -EINVAL;
		goto done;
	}
	
	// 6 因为创建和修改tbf qdisc两种情形下,tbf_change函数都会被调用,这里根据q->qdisc对两种情况进行了区分
	// 创建qdisc时q->qdisc会被设置为noop_qdisc,而修改qdisc时q->qdisc已经指向一个其它类型的qdisc
	if (q->qdisc != &noop_qdisc) {
		// 6.1 修改leaf qdisc
		err = fifo_set_limit(q->qdisc, qopt->limit);
	} else if (qopt->limit > 0) {
		// 6.2 创建leaf qdisc,默认leaf qdisc时fifo类型的qdisc。之所以使用fifo qdisc,是因为fifo qdisc在enqueue/dequeue仅发挥缓存数据包的作用,不会执行额外的逻辑。
		// 当然这个默认的leaf qdisc是可以通过tc命令替换为其它类型的qdisc的
		child = fifo_create_dflt(sch, &bfifo_qdisc_ops, qopt->limit, extack);
		// 6.3 将leaf qdisc添加到网络设备的hash table中,qdisc_hash_add的第二个参数为true,则通过`tc qdisc show`无法观测到该qdisc
		qdisc_hash_add(child, true);
	}

	// 7 记录leaf qdisc到tbf qdisc
	if (child) {
		q->qdisc = child;
	}
	// 8 记录limit/burst等令牌桶参数到tbf qdisc
	q->limit = qopt->limit;
	q->max_size = max_size;
	if (tb[TCA_TBF_BURST])
		q->buffer = buffer;
	else
		q->buffer = PSCHED_TICKS2NS(qopt->buffer);
	// 9 填满令牌桶
	q->tokens = q->buffer;
	
	// 10 记录限定速率到tbf qdisc
	memcpy(&q->rate, &rate, sizeof(struct psched_ratecfg));
}

tbf_inittbf_change函数做的事情可以简单归纳如下:

  • 将应用层传递的limit/burst/rate等参数保存到tbf_sched_data的某些成员上,稍后对tbf qdisc进行enqueue/dequeue时会根据这些参数来处理数据包
  • 创建一个fifo qdisc作为tbf qdisc的leaf qdisc,稍后该fifo qdisc将用于存储enqueue到tbf qdisc的数据包

使用tc qdisc add dev ens33 handle 10: root tbf rate 10mbps burst 1m limit 1m命令创建tbf qdisc后,最终的结果大致如下图所示:
tbf qdisc struct.png

enqueue和dequeue的过程

接下来从数据包enqueue/dequeue的流程来观察qdisc执行规则的过程。

数据包enqueue/dequeue的时机

上层的网络协议栈在确定了数据包的输出设备后会调用dev_queue_xmit函数来输出数据包,enqueue/dequeue的就是在dev_queue_xmit的调用期间进行的,dev_queue_xmit函数的大体流程如下:

// linux-5.4.246/net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb)
	__dev_queue_xmit(skb, NULL);
		txq = netdev_core_pick_tx(dev, skb, sb_dev); // 选择一个输出队列
		q = rcu_dereference_bh(txq->qdisc); // 获取与输出队列关联的qdisc
		if (q->enqueue) { // qdisc的enqueue回调不为空,才会进行enqueue/dequeue。有的网络设备是不需要执行qdisc的,比如lo网卡,它们的会被默认添加一个noqueue类型的qdisc,这种qdisc的enqueue回调为空
			rc = __dev_xmit_skb(skb, q, dev, txq); 
				// __dev_xmit_skb函数中有一系列加锁互斥的操作,比较复杂,不深入分析,最终__dev_xmit_skb会调用下面两个函数
				// 1 调用qdisc的enqueue回调来执行入队操作
				q->enqueue(skb, q, &to_free)
				// 2 调用__qdisc_run来间接调用qdisc的dequeue回调来执行出队操作
				__qdisc_run(q);
		}
		// 不需要执行qdisc的网络设备会直接调用驱动提供的ndo_start_xmit回调来输出数据包到驱动层

对于__dev_queue_xmit函数需要注意的是,__dev_xmit_skb会判段目标qdisc是否已经处于running状态,比如其他线程正在对该qdisc执行__qdisc_run函数时qdisc就会处于running状态,若是则__dev_xmit_skb函数不会去执行__qdisc_run函数,则当前线程不会立即调用qdisc的dequeue回调,当前线程enqueue进qdisc的数据包可能会在其他线程执行__qdisc_run时被dequeue,也有可能在sofirq执行__qdisc_run时被dequeue。
__qdisc_run 函数的实现如下:

// linux-5.4.246/net/shced/sch_generic.c
__qdisc_run(struct Qdisc *q)
{
	int quota = READ_ONCE(dev_tx_weight);
	int packets;
	// __qdisc_run会循环的调用qdisc_restart来从qdisc dequeue数据包,并将数据包送往驱动层
	while (qdisc_restart(q, &packets)) {
		/*
		 * Ordered by possible occurrence: Postpone processing if
		 * 1. we've exceeded packet quota
		 * 2. another process needs the CPU;
		 */
		quota -= packets;
		if (quota <= 0 || need_resched()) { // 直到系统给此次__qdisc_run的数据包配额消耗完毕或者有其它线程需要CPU时,__qdisc_run才会退出循环,并且__qdisc_run会raise softirq (NET_TX_SOFTIRQ),让softirq来继续dequeue数据包
			__netif_schedule(q); // __netif_schedule 负责raise softirq。NET_TX_SOFTIRQ的处理函数net_tx_action 位于linux-5.4.246/net/core/dev.c,net_tx_action也会调用__qdisc_run来dequeue数据包,此处不再深入分析
			break;
		}
	}
}

__qdisc_run调用的qdisc_restart 函数实现如下:

// linux-5.4.246/net/shced/sch_generic.c
static inline bool qdisc_restart(struct Qdisc *q, int *packets)
{
	spinlock_t *root_lock = NULL;
	struct netdev_queue *txq;
	struct net_device *dev;
	struct sk_buff *skb;
	bool validate;

	/* Dequeue packet */
	skb = dequeue_skb(q, &validate, packets); // dequeue数据包, dequeue_skb可能会dequeue出多个数据包,dequeue_skb会给packets赋值以表明dequeue了多少个数据包
	if (unlikely(!skb)) // 没有报文可出队时,返回false,停止上层的发包循环
		return false;

	if (!(q->flags & TCQ_F_NOLOCK))
		root_lock = qdisc_lock(q);

	dev = qdisc_dev(q);
	txq = skb_get_tx_queue(dev, skb);
	
	// 调用驱动提供的回调往驱动层输出数据包,需要注意的是,若驱动层此时因某种原因无法输出数据包(比如网卡太忙了),sch_direct_xmit会调用dev_requeue_skb函数来将数据包重新放回qdisc
	return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}

dequeue_skb 函数的实现如下:

// linux-5.4.246/net/shced/sch_generic.c
static struct sk_buff *dequeue_skb(struct Qdisc *q, bool *validate,int *packets)
{
	const struct netdev_queue *txq = q->dev_queue;
	struct sk_buff *skb = NULL;

	*packets = 1;
	// 1 因某种原因被重新放回qdisc的数据包不会再度enqueue到内部的qdisc,它们就被挂在gso_skb链表上, 这里判断gso_skb链表是否有数据包,有就优先输出它们
	if (unlikely(!skb_queue_empty(&q->gso_skb))) {
			......
			// 从gso_skb链表首取下数据包
			skb = __skb_dequeue(&q->gso_skb);
			// 减少qdisc的统计数目
			q->q.qlen--;
			......
		goto trace; // 跳转到trace直接返回skb
	}
	......
	// 2 gso_skb不为空则调用qdisc 的dequeue回调来获取数据包
	skb = q->dequeue(q); 
	if (skb) {
bulk:
		if (qdisc_may_bulk(q)) // try_bulk_dequeue_skb和try_bulk_dequeue_skb_slow也会调用qdisc的dequeue回调,它们可能会dequeue出多个数据包
			try_bulk_dequeue_skb(q, skb, txq, packets);
		else
			try_bulk_dequeue_skb_slow(q, skb, packets);
	}
	......
trace:
	return skb;
}

sch_direct_xmit函数往驱动层输出数据包失败时调用的dev_requeue_skb函数如下:

// linux-5.4.246/net/shced/sch_generic.c
static inline void dev_requeue_skb(struct sk_buff *skb, struct Qdisc *q)
{
	......

	while (skb) {
		struct sk_buff *next = skb->next;
		
		// 将数据包放置到gso_skb链表尾部
		__skb_queue_tail(&q->gso_skb, skb);

		......
			q->q.qlen++;

		skb = next;
	}
	......
	// raise softirq, 让softirq执行__qdisc_run来输出数据包
	__netif_schedule(q);
}

dev_requeue_skb做的事情其实就是把数据包链入qdisc的gso_skb链表,但不会再对数据包调用qdisc的enqueue回调了。

经过上述的分析,大致知道了上层网络协议栈在使用网卡输出数据包时,就会执行与网卡输出队列相关联的qdisc的enqueue/dequeue回调,这个过程中qdisc的enqueue回调是一定会被调用的,但qdisc的dequeue回调不一定会被马上调用。

tbf qdisc的enqueue,dequeue逻辑

tbf qdisc的enqueue回调为tbf_enqueue, 它的实现如下:

// linux-5.4.246/net/shced/sch_tbf.c
static int tbf_enqueue(struct sk_buff *skb, struct Qdisc *sch,
		       struct sk_buff **to_free)
{
	struct tbf_sched_data *q = qdisc_priv(sch); // 获取tbf qdisc私有数据
	unsigned int len = qdisc_pkt_len(skb); // 数据包长度
	int ret;

	// 1 如果报文长度比令牌桶还大,则它永远都等不到足够令牌,直接丢弃它
	if (qdisc_pkt_len(skb) > q->max_size) {
		......
		return qdisc_drop(skb, sch, to_free);
	}
	// 2 对leaf qdisc调用 qdisc_enqueue,qdisc_enqueue函数其实就是简单的调用qdisc的enqueue函数。
	ret = qdisc_enqueue(skb, q->qdisc, to_free);
	if (ret != NET_XMIT_SUCCESS) {
		if (net_xmit_drop_count(ret))
			qdisc_qstats_drop(sch);
		return ret;
	}
	
	// 3 增加统计数据
	sch->qstats.backlog += len;
	sch->q.qlen++;
	return NET_XMIT_SUCCESS;
}

从上述第2个注释处可以发现,enqueue到tbf qdisc的数据包其实还会进一步enqueue到它内部的leaf qdisc上去。
其实所有包含class的qdisc的enqueue行为都是类似的,它们会把数据包enqueue到内部class的leaf qdisc上,它们的leaf qdisc又会将数据包进一步enqueue到class的leaf qdisc, 如此反复直到遇见不包含class的qdisc。
对于本文tbf qdisc的例子,leaf qdisc是一个fifo类型的qdisc,它没有class,所以enqueue到tbf qdisc的数据包最终就缓存于内部的fifo qdisc上。
tbf and fifo qdisc.png

另外需要注意的是,即便最终数据包是enqueue到tbf qdisc的leaf qdisc上去了,但从整体上来说数据包依然是属于tbf qdisc的,tbf qdisc的统计数据仍然是要更新的。
简单的看一下fifo qdisc的enqueue行为:

// linux-5.4.246/net/shced/sch_fifo.c
// sch此时指向的是fifo qdisc,而不是tbf qdisc
static int bfifo_enqueue(struct sk_buff *skb, struct Qdisc *sch,
			 struct sk_buff **to_free)
{
	// sch->limit其实就是tbf qdisc的limit参数,tbf qdisc创建fifo qdisc时给它赋的值
	// 这里判断当前缓存的数据包总大小是否已超过limit,若没有则将数据包链入fifo qdisc的缓存链表(struct Qdisc的q成员)
	if (likely(sch->qstats.backlog + qdisc_pkt_len(skb) <= sch->limit))
		return qdisc_enqueue_tail(skb, sch);
	// 若超过limit则丢弃数据包
	return qdisc_drop(skb, sch, to_free);
}

tbf qdisc的dequeue回调为tbf_dequeue, 它的实现如下:

static struct sk_buff *tbf_dequeue(struct Qdisc *sch)
{
	struct tbf_sched_data *q = qdisc_priv(sch);
	struct sk_buff *skb;
	
	// 1 调用leaf qdisc的peek回调,它的作用是从leaf qdisc取回下一个要dequeue的skb的指针,不同类型的qdisc的peek实现可能会稍有不同
	// 有的leaf qdisc的peek回调可能只是简单的返回下一个要dequeue的skb的指针,比如fifo qdisc。
	// 而有的leaf qdisc在peek回调中会将skb直接dequeue出来,然后放置在自身的gso_skb链表上,tbf qdisc就是这样做的。
	// 无论leaf qdisc怎样peek,此时skb仍然还缓存于leaf qdisc。
	skb = q->qdisc->ops->peek(q->qdisc);

	if (skb) {
		s64 now;
		s64 toks;
		s64 ptoks = 0;
		// 2.1 skb数据长度
		unsigned int len = qdisc_pkt_len(skb);
		
		// 2.2 计算新增令牌数
		now = ktime_get_ns();
		// q->tc是上一次填充令牌桶的时间,从上一次填充令牌的时间到现在过去多长时间就填充多少时间的令牌,填充的令牌数最多不超过令牌桶的大小
		// toks就代表着新增令牌数
		toks = min_t(s64, now - q->t_c, q->buffer);

		......
		// 2.3 计算目前总的令牌, 总的令牌数不能超过桶的大小
		toks += q->tokens;
		if (toks > q->buffer)
			toks = q->buffer;
		
		// 2.4 计算限定速率下输出当前skb需要消耗多长时间,这个时间就是输出该skb需要消耗的令牌,从toks中扣除需要消耗的令牌
		toks -= (s64) psched_l2t_ns(&q->rate, len);
		
		// 3.1 toks若大于0,则表示可以输出当前数据包 
		if ((toks|ptoks) >= 0) {
			// 3.2 qdisc_dequeue_peeked 会真正的将skb从leaf qdisc取出来
			// qdisc_dequeue_peeked 会先尝试从leaf qdisc的gso_skb链表上去取数据包,若gso_skb链表为空则调用leaf qdisc的dequeue回调
			skb = qdisc_dequeue_peeked(q->qdisc); 
			if (unlikely(!skb))
				return NULL;
			
			// 3.3 更新时间令牌桶的参数,以及剩余的令牌
			q->t_c = now; // 令牌桶填充时间
			q->tokens = toks; // 记录剩余的令牌
			
			// 3.4 更新tbf qdisc的统计数据,然后返回skb到上层函数
			qdisc_qstats_backlog_dec(sch, skb);
			sch->q.qlen--;
			qdisc_bstats_update(sch, skb);
			return skb;
		}
	
		// 4 若令牌不足,则启动一个定时器,定时器的超时时长与不足的令牌值相同,定时器到期后会调用qdisc_watchdog函数,qdisc_watchdog函数会启动softirq来对qdisc执行dequeue
		qdisc_watchdog_schedule_ns(&q->watchdog,
					   now + max_t(long, -toks, -ptoks));

		......
	}
	return NULL;
}

从tbf qdisc的dequeue流程基本可以猜到,dequeue过程就是在满足qdisc dequeue条件的情况下对内部class的leaf qdisc进行dequeue,而leaf qdisc也会对自身内部class的leaf qdisc进行dequeue, 如此反复直到dequeue条件不满足或者遇到不包含class的qdisc为止。

参考文档

https://www.man7.org/linux/man-pages/man8/tc-tbf.8.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值