tc tbf qdisc
本文记录tc tbf qdisc使用netlink与内核通信的参数格式及其数据含义与tc命令中tbf相关实现与内核中相关实现。
tc命令中tbf qdisc实现位于iproute2项目文件iproute2/tc/q_tbf.c
内核中实现位于文件net/sched/sch_tbf.c
参考内核版本:4.18.0-193.el8.x86_64
传输参数
tbf qdic使用netlink与内核交互的传输参数类型
1 | enum { |
TCA_TBF_PARMS
TCA_TBF_PARMS对应的数据结构为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct tc_tbf_qopt {
struct tc_ratespec rate; // 限速速率相关
struct tc_ratespec peakrate; // 峰值限速速率。因为累积的token,也就是buffer的存在,会有突发数据,单纯的rate无法限制该突发。
// 因此使用peakrate进一步限制突发速率
__u32 limit; // 数据包队列可以缓存的最大字节数。
// tc命令行参数中latency也是作用于该参数,因为发送速率限制的原因,缓存最大字节数与最大延迟时间直接相关
__u32 buffer; // 该数据结构中buffer存储的是队列中允许累积的最大token数所对应的ticks值。
// 这个ticks是一个虚拟的概念,与内核的时钟tick不同
// 这个值也会经过换算体现为允许突发的数据字节数,突发字节数需要不小于设备实际mtu。
__u32 mtu; // minburst,这个可以看作peakrate所对应的buffer,也就是peakrate的突发,意义相同。如果mtu设置小于设备实际mtu,会发生数据包无法发送的问题,直接被丢弃。
// 一般mtu设置小于buffer,peakrate大于rate,达到削峰效果。
};
struct tc_ratespec {
unsigned char cell_log;
__u8 linklayer; /* lower 4 bits */ // 链路层类型。3种值TC_LINKLAYER_UNAWARE、TC_LINKLAYER_ETHERNET、TC_LINKLAYER_ATM
unsigned short overhead; // 数据包在链路传播需要额外占用的字节数。
short cell_align;
unsigned short mpu; // 数据包最小尺寸字节数。比如以太网是64字节,小于该尺寸的数据包也会被填充到该尺寸再发送到链路中,这个跟冲突检测有关。但是没有看懂怎么使用的该字段……
__u32 rate; // 限速速率。如果限速每秒字节数小于1<<32,也就是不超过32位,使用该字段。否则该字段置~0U(也就是所有位置1),并使用TCA_TBF_RATE64及TCA_TBF_PRATE64。
};
这里提到的tc_tbf.qopt.buffer
和tc_tbf_qopt.mtu
存储的是虚拟ticks数,但是tc命令输入参数是字节数,换算过程如下:
- 将字节数size转换为发送所需要的时间,
size / rate * USEC_PER_SEC
,得到发送size字节需要的微秒数time,这里USEC_PER_SEC
在iproute2项目中实际使用的宏为TIME_UNITS_PER_SEC - 计算微秒数time对应的ticks数,
time * tick_in_usec
其中tick_in_usec
在函数tc_core_init
中被预先计算出了,这个函数中对读取值存入的变量命名会引起误解,因此下面使用内核代码说明。
读取/proc/net/psched文件,该文件输出4个值,实现为内核代码中函数psched_show
,位于文件net/sched/sch_api.c
- 第一个值为NSEC_PER_USEC,一微秒包含多少纳秒,值为1000
- 第二个值为PSCHED_TICKS2NS(1),该宏实现为左移6位。一个tick包含多少纳秒,这里的tick为模拟的假tick,只用于数据包调度整形使用,最终值为64
- 第三个值写死为1000000。
- 第四个值应该与时钟分辨率有关,tc用不到,不管了
tick_in_usec
的计算方式为 值1 / 值2 * 值3 / TIME_UNITS_PER_SEC
由于值3与TIME_UNITS_PER_SEC相等,time_in_usec
最终即为一微秒包含的ticks数
TCA_TBF_RTAB/TCA_TBF_PTAB
这两个目前的内核中已经不再使用。本意应该是加速通过数据包字节数换算需要的token数(其实就是时间纳秒数),以避免使用除法。现有内核使用乘法与偏移加速该换算
TCA_TBF_RATE64/TCA_TBF_PRATE64
当限速每秒字节数大于等于1<<32,也就是超过32位表示时,使用这两个参数,不再使用tc_ratespec.rate字段
TCA_TBF_BURST
这里对应的也是tc_tbf_qopt.buffer,但是这里存储的是字节数,而不是虚拟tick数。
TCA_TBF_PBURST
这里对应的也是tc_tbf_qopt.mtu,但是这里存储的是字节数,而不是虚拟tick数
内核实现
配置 tbf_change
内核使用数据结构tbf_sched_data
保存配置参数,内核源码中该结构的注释记录了tbf的计算原理,这里不重复了
这个结构中有些成员记录的是字节数,有些是时间,但是这里的时间又不是传输中使用的虚拟ticks,而是经过换算后的实际的纳秒数
1 | struct tbf_sched_data { |
1 | struct psched_ratecfg { |
使用函数psched_ratecfg_precompute
保存及计算限速速率及用于加速字节数到对应时间长度的算法参数,存储到结构体psched_ratecfg
中,overhead、linklayer均在这里被保存
- 普通的计算方式为
time_in_ns = (NSEC_PER_SEC * len) / rate_bps
- 加速的计算方式为
time_in_ns = (len * mult) >> shift
入队列 tbf_enqueue
入队列出队列都比较简单,简略记录一下
对于外发数据包来说,__dev_queue_xmit
中调用函数qdisc_pkt_len_init
初始化了qdisc_skb_cb(skb)->pkt_len = skb->len
- 入队列时获取该长度,与max_size比较,并综合判断gso相关字段
- 符合条件会调用
qdisc_enqueue
- 调用
qdisc_calculate_pkt_len
,尝试使用Qdisc->stab更新qdisc_skb_cb(skb)->pkt_len
。没有看到stab哪里初始化的,就当不存在吧 - 调用Qdisc.enqueue函数指针,这里应该是
bfifo_enqueue
函数。判断数据包队列是否会超出limit,不超出则入队列
- 调用
- 更新统计信息
出队列 tbf_dequeue
- 获取队列首包
qdisc_pkt_len(skb)
获取数据包长度- 获取当前时间纳秒值
- 当前时间纳秒值与上次检查点时间纳秒值做减法,并与
tbf_sched_data.buffer
做比较、取较小值赋值给toks。含义是取得当前累积的token数量,该数量不能超过突发量。可以看到token数量等价于纳秒值 - 如果有配置峰值速率,获取峰值ptoks,注意这里mtu相当于峰值限速的buffer,因此ptoks不能大于mtu。ptoks减掉当前数据包长度需要消耗的纳秒值(这里使用了overhead及linklayer)
- 限制toks同样不能大于buffer
- toks减去当前数据包需要消耗的纳秒值(这里使用了overhead及linklayer)
- 如果toks与ptoks都大于0,说明当前资源满足发送该数据包
- 更新检查点为当前时间
- 更新tbf_sched_data的tokens与ptokens成员
- 更新统计信息
- 返回skb
- 如果不满足发包要求
- 设置定时器
- 更新超限速统计信息