本文记录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
2
3
4
5
6
7
8
9
10
11
12
enum {                   
TCA_TBF_UNSPEC,      
TCA_TBF_PARMS,       
TCA_TBF_RTAB,        
TCA_TBF_PTAB,        
TCA_TBF_RATE64,      
TCA_TBF_PRATE64,     
TCA_TBF_BURST,       
TCA_TBF_PBURST,      
TCA_TBF_PAD,         
__TCA_TBF_MAX,       
};

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
25
struct 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.buffertc_tbf_qopt.mtu存储的是虚拟ticks数,但是tc命令输入参数是字节数,换算过程如下:

  1. 将字节数size转换为发送所需要的时间,size / rate * USEC_PER_SEC,得到发送size字节需要的微秒数time,这里USEC_PER_SEC在iproute2项目中实际使用的宏为TIME_UNITS_PER_SEC
  2. 计算微秒数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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct tbf_sched_data {                                                     
/* Parameters */
u32 limit; /* Maximal length of backlog: bytes */
/* 对应传输进来的limit,数据队列字节数。生效于qdisc成员包含的bfifo队列中 */

u32 max_size; /* 突发数据字节数,实际运行时便于比较。取buffer和mtu对应的字节数的较小值。如果数据包尺寸超过该值将会直接丢弃 */

s64 buffer; /* Token bucket depth/rate: MUST BE >= MTU/B */
/* rate对应突发数据限额对应的纳秒数 */

s64 mtu; /* prate对应的突发数据限额对应的纳秒数 */

struct psched_ratecfg rate;
struct psched_ratecfg peak;

/* Variables */
s64 tokens; /* Current number of B tokens */
s64 ptokens; /* Current number of P tokens */
s64 t_c; /* Time check-point */
struct Qdisc *qdisc; /* Inner qdisc, default - bfifo queue */
struct qdisc_watchdog watchdog; /* Watchdog timer */
};
1
2
3
4
5
6
7
struct psched_ratecfg {                            
u64 rate_bytes_ps; /* bytes per second */
u32 mult; /* 用于加速计算字节数到纳秒数的计算 */
u16 overhead; /* 见上文 */
u8 linklayer; /* 见上文 */
u8 shift; /* 用于加速计算字节数到纳秒数的计算 */
};

使用函数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
  • 如果不满足发包要求
    • 设置定时器
    • 更新超限速统计信息