Linux网络之TC (Traffic Control) 网络流控子系统分析
基于 Linux Kernel v7.1.0-rc6 分析
一、概念总览
1.1 什么是 TC
TC(Traffic Control)是 Linux 内核中的网络流量控制框架,位于网络设备驱动层之上、协议栈之下。它在数据包的 发送路径(Egress)和接收路径(Ingress) 上提供了一个可编程的包处理管线,用于实现:
- • 流量整形(Shaping):限制发送速率,平滑突发流量
- • 流量 policing:对超过速率限制的包进行丢弃或标记
- • 包分类(Classification):根据包头字段(五元组、mark、接口等)将包分类到不同的流
- • 队列调度(Scheduling):决定哪个包先被发送,实现 QoS 优先级
- • 包标记/修改(Marking/Editing):修改包头字段(DSCP、VLAN tag 等)
- • 包重定向(Redirect):将包转发到其他设备
- • 包丢弃(Dropping):基于策略丢弃数据包
1.2 核心组件
TC 框架由三层组件构成,形成分层处理管线:
┌─────────────────────────────────────────────────┐
│ Classifier (分类器) - 识别包属于哪个流 │
│ 例: flower, u32, fw, bpf, matchall │
├─────────────────────────────────────────────────┤
│ Action (动作) - 对匹配的包执行操作 │
│ 例: mirred, gact, skbedit, pedit, nat, bpf │
├─────────────────────────────────────────────────┤
│ Qdisc (队列规则) - 包的排队与调度 │
│ 例: pfifo_fast, tbf, htb, fq_codel, prio │
└─────────────────────────────────────────────────┘
这三个组件通过 tcf_block(过滤块) 和 tcf_chain(过滤链) 组织在一起。
1.3 关键数据结构
| | |
|---|
struct Qdisc | include/net/sch_generic.h:69 | |
struct Qdisc_ops | include/net/sch_generic.h:307 | 队列规则的操作函数集(enqueue/dequeue/init/destroy) |
struct tcf_proto | include/net/sch_generic.h:426 | |
struct tcf_proto_ops | include/net/sch_generic.h:357 | 分类器的操作函数集(classify/init/destroy) |
struct tcf_result | include/net/sch_generic.h:345 | 分类结果,包含 classid 或 goto 目标 |
struct tcf_chain | include/net/sch_generic.h:469 | |
struct tcf_block | include/net/sch_generic.h:485 | |
struct qdisc_skb_head | include/net/sch_generic.h:62 | |
struct mini_Qdisc | include/net/sch_generic.h:1455 | 轻量级 Qdisc,用于 ingress/clsact 快速路径 |
二、设计原理
2.1 分层架构设计
TC 采用分层抽象设计,核心思路是将"包的分类"与"包的调度"解耦:
┌──────────────┐
│ 用户空间 │
│ tc 命令行 │
└──────┬───────┘
│ Netlink (RTM_NEWQDISC, RTM_NEWTFILTER)
▼
┌──────────────────────────────────────────────┐
│ 控制平面 (Control Plane) │
│ sch_api.c: tc_modify_qdisc() │
│ cls_api.c: tc_ctl_filter() │
│ → 创建/修改/删除 Qdisc、Filter、Action │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 数据平面 (Data Plane) │
│ 发送: __dev_queue_xmit() → __dev_xmit_skb()│
│ → q->enqueue() → q->dequeue() │
│ → sch_direct_xmit() → dev_hard_start_xmit() │
│ │
│ 接收: __netif_receive_skb_core() │
│ → sch_handle_ingress() → tc_run() │
│ → tcf_classify() │
└──────────────────────────────────────────────┘
2.2 Qdisc 树形层次结构
TC 支持 Qdisc 的层级嵌套(hierarchy),形成一棵树:
root qdisc (1:)
/ \
class (1:1) class (1:2)
/ \ |
qdisc(1:10) qdisc(1:20) qdisc(1:20)
(htb) (sfq) (tbf)
| | |
class class leaf
(1:10:1) (1:10:2) (1:20:1)
- • Root Qdisc:设备的根队列规则,每个 TX 队列一个
- • Class:Qdisc 内部的逻辑分类,用于带宽分配
- • Leaf Qdisc:挂在 class 下的子队列规则,真正缓存包
这种层次结构通过 struct Qdisc_class_ops 实现,其中的 graft 和 leaf 回调负责子 qdisc 的挂载和查询。
2.3 Classifier 链式匹配
分类器通过 struct tcf_proto 的 next 指针形成有序链表,按 prio(优先级)排序:
filter_chain → [u32 prio=1] → [flower prio=2] → [matchall prio=3] → NULL
匹配过程(__tcf_classify() in cls_api.c:1712):
- 2. 检查协议类型是否匹配(
tp->protocol vs skb->protocol) - 3. 调用具体分类器的
classify() 回调 - 4. 如果返回
TC_ACT_RECLASSIFY,则从头重新开始 - 5. 如果返回
TC_ACT_GOTO_CHAIN,则跳转到指定链 - 6. 返回分类结果(classid 或 action verdict)
2.4 快速路径 vs 慢速路径
TC 设计了**快速路径(Fast Path)**优化,避免在无分类器时的开销:
ingress/clsact 的快速路径(mini_Qdisc):
- • 使用
struct mini_Qdisc(sch_generic.h:1455),只包含 filter_list、block 和统计信息 - • 在
tc_run() 中直接通过 tcf_classify() 匹配,不经过完整 Qdisc 流程
TX 的 bypass 优化(TCQ_F_CAN_BYPASS):
- • 当 Qdisc 标记了
TCQ_F_CAN_BYPASS 且队列为空时 - • 包可以绕过入队/出队,直接调用
sch_direct_xmit() 发送
2.5 锁机制设计
TC 采用多级锁确保并发安全:
| | |
|---|
qdisc->q.lock | | qdisc_lock() |
dev->tx_global_lock | | HARD_TX_LOCK() |
chain->filter_chain_lock | | mutex_lock() |
tp->lock | | spin_lock() |
No-lock Qdisc(TCQ_F_NOLOCK)使用 seqlock + MISSED 标志位实现无锁快速路径:
- •
qdisc_run_begin() 尝试 spin_trylock(),失败则设置 __QDISC_STATE_MISSED - • 当前持有者在
qdisc_run_end() 中检查 MISSED,若有则调用 __netif_schedule() 重新调度
2.6 统计信息收集
Qdisc 通过 bstats(基础统计)和 qstats(队列统计)收集数据:
- • per-CPU 统计(
TCQ_F_CPUSTATS):每个 CPU 维护独立的统计计数器,避免锁竞争 - • 全局统计:直接更新
q->bstats 和 q->qstats - • 通过
gnet_stats 框架在 dump 时聚合
三、数据包处理流程
3.1 发送路径(Egress)
数据包从上层协议栈发出,经过 TC 处理后到达网卡驱动:
协议栈 (TCP/IP)
│
▼
dev_queue_xmit() // 公开 API 入口
│
▼
__dev_queue_xmit() // net/core/dev.c:4766
│
├── 1. sch_handle_egress() // Egress clsact 过滤 (dev.c:4807)
│ │
│ ├── tcx_run() // BPF egress 程序
│ └── tc_run() // TC 分类器执行
│ │
│ └── tcf_classify() // cls_api.c:1805
│ │
│ └── 遍历 tcf_proto 链表
│ 对每个 proto 调用 tp->ops->classify()
│ 返回 TC_ACT_OK/SHOT/REDIRECT/...
│
├── 2. 选择 TX 队列
│ netdev_core_pick_tx() → txq
│
├── 3. 获取该队列的 Qdisc
│ q = rcu_dereference_bh(txq->qdisc)
│
├── 4. 如果 Qdisc 有 enqueue(非 noqueue)
│ │
│ ▼
│ __dev_xmit_skb() // dev.c:4180
│ │
│ ├── [No-lock Qdisc 快速路径]
│ │ 如果 TCQ_F_CAN_BYPASS && 队列空 && 获得运行锁:
│ │ → sch_direct_xmit() 直接发送
│ │
│ ├── [普通路径 - TCQ_F_NOLOCK]
│ │ 使用 llist defer_list 延迟入队:
│ │ 1. llist_add() 无锁添加到 defer_list
│ │ 2. 获取 root_lock
│ │ 3. llist_del_all() 取出所有延迟包
│ │ 4. 如果 CAN_BYPASS 且队列空:
│ │ → sch_direct_xmit() 直接发送
│ │ 5. 否则: dev_qdisc_enqueue() 入队 + qdisc_run()
│ │
│ └── [传统路径 - 有锁 Qdisc]
│ 1. dev_qdisc_enqueue() → q->enqueue(skb, q, to_free)
│ 2. qdisc_run(q) → __qdisc_run()
│
└── 5. [无 Qdisc 的设备(loopback, tunnels)]
→ dev_hard_start_xmit() 直接发送
dev_qdisc_enqueue()(dev.c:4168):
static int dev_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *q,
struct sk_buff **to_free,
struct netdev_queue *txq)
{
rc = q->enqueue(skb, q, to_free) & NET_XMIT_MASK;
// 调用具体 Qdisc 的入队操作
// 例如 pfifo_fast_enqueue(), htb_enqueue(), tbf_enqueue()
}
__qdisc_run()(sch_generic.c:440):
void __qdisc_run(struct Qdisc *q)
{
int quota = READ_ONCE(net_hotdata.dev_tx_weight);
while (qdisc_restart(q, &packets, quota)) {
quota -= packets;
if (quota <= 0) {
// 配额用完,调度下次处理
if (q->flags & TCQ_F_NOLOCK)
set_bit(__QDISC_STATE_MISSED, &q->state);
else
__netif_schedule(q);
break;
}
}
}
出队 + 发送循环(sch_generic.c:420):
static bool qdisc_restart(struct Qdisc *q, int *packets, int quota)
{
skb = dequeue_skb(q, &validate, packets, budget);
// dequeue_skb 先检查 gso_skb(重发队列),
// 再调用 q->dequeue(q) 从实际队列出队
// 支持批量出队(bulk dequeue)
return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
// → validate_xmit_skb_list() (GSO/checksum)
// → dev_hard_start_xmit() (调用驱动 ndo_start_xmit)
}
3.2 接收路径(Ingress)
数据包从网卡驱动接收,经过 TC Ingress 处理后到达上层协议栈:
网卡驱动
│
▼
__netif_receive_skb_core() // net/core/dev.c:6010+
│
├── 1. ptype_all 处理(tcpdump 抓包点)
│
├── 2. sch_handle_ingress() // dev.c:4460
│ │
│ ├── tcx_run() // BPF ingress 程序
│ └── tc_run() // TC 分类器执行
│ │
│ ├── mini_qdisc_bstats_cpu_update()
│ │
│ └── tcf_classify(skb, miniq->block, miniq->filter_list, &res)
│ │
│ └── 遍历 tcf_proto 链表
│ 调用 tp->ops->classify() (如 fl_classify, u32_classify)
│ 返回 verdict
│
├── 3. 处理 verdict
│ TC_ACT_REDIRECT → skb_do_redirect() → 重定向到其他设备
│ TC_ACT_SHOT → kfree_skb_reason() → 丢弃
│ TC_ACT_STOLEN → consume_skb() → 消费(不传递上层)
│ TC_ACT_OK → 继续正常接收路径
│
└── 4. 继续正常接收(rx_handler → 协议分发 → socket)
Ingress 的特殊设计:
- • Ingress 使用
mini_Qdisc 而非完整 struct Qdisc,开销更小 - •
mini_Qdisc 直接持有 filter_list(tcf_proto 链表头),跳过 Qdisc 的 enqueue/dequeue 流程 - • 不需要排队,因为 ingress 的处理目标是分类 + 动作,而非整形
3.3 tc_run() 分类器执行流程
// net/core/dev.c:4386
static int tc_run(struct tcx_entry *entry, struct sk_buff *skb,
enum skb_drop_reason *drop_reason)
{
struct mini_Qdisc *miniq = rcu_dereference_bh(entry->miniq);
// 全局旁路检查
if (!static_branch_likely(&tcf_sw_enabled_key))
return TC_ACT_UNSPEC;
// 块级旁路检查
if (tcf_block_bypass_sw(miniq->block))
return TC_ACT_UNSPEC;
// 更新统计
mini_qdisc_bstats_cpu_update(miniq, skb);
// 核心:调用 tcf_classify() 遍历分类器链
ret = tcf_classify(skb, miniq->block, miniq->filter_list, &res, false);
// 处理 verdict
switch (ret) {
case TC_ACT_SHOT:
mini_qdisc_qstats_cpu_drop(miniq);
break;
case TC_ACT_OK:
case TC_ACT_RECLASSIFY:
skb->tc_index = TC_H_MIN(res.classid);
break;
}
return ret;
}
3.4 Flower 分类器匹配过程
以最常用的 cls_flower 为例(cls_flower.c:320):
int fl_classify(struct sk_buff *skb, const struct tcf_proto *tp,
struct tcf_result *res)
{
struct cls_fl_head *head = rcu_dereference_bh(tp->root);
// 遍历所有 mask(每个 mask 对应一组过滤规则)
list_for_each_entry_rcu(mask, &head->masks, list) {
// 1. 清零并提取 skb 关键字段
fl_clear_masked_range(&skb_key, mask);
// 2. 使用 dissector 解析包头
skb_flow_dissect_meta(skb, &mask->dissector, &skb_key);
skb_key.basic.n_proto = skb_protocol(skb, false);
skb_flow_dissect_tunnel_info(skb, &mask->dissector, &skb_key);
skb_flow_dissect(skb, &mask->dissector, &skb_key, ...);
// 3. 在 mask 的哈希表中查找匹配的过滤器
f = fl_mask_lookup(mask, &skb_key);
if (f && !tc_skip_sw(f->flags)) {
*res = f->res; // 设置分类结果(classid)
return tcf_exts_exec(skb, &f->exts, res);
// 执行过滤器关联的 actions(mirred, gact, etc.)
}
}
return -1; // 无匹配
四、Qdisc 创建与管理
4.1 初始化流程
dev_init_scheduler() // sch_generic.c:1478
│
├── dev->qdisc = &noop_qdisc // 根 qdisc 设为 noop
├── 每个 TX 队列: qdisc = noop_qdisc
└── ingress 队列: qdisc = noop_qdisc
dev_activate() // sch_generic.c:1264
│
├── attach_default_qdiscs() // 为设备挂载默认 qdisc
│ │
│ ├── 单队列设备 → attach_one_default_qdisc()
│ │ 创建 default_qdisc_ops (通常 pfifo_fast)
│ │
│ └── 多队列设备 → qdisc_create_dflt(&mq_qdisc_ops)
│ 创建 MQ qdisc,每个子队列挂载默认 qdisc
│
└── transition_one_qdisc() // 将 sleeping qdisc 切换为 active
rcu_assign_pointer(txq->qdisc, new_qdisc)
4.2 用户空间配置
通过 tc 命令行工具,经由 Netlink 与内核交互:
tc qdisc add dev eth0 root htb default 30
│
│ RTM_NEWQDISC
▼
tc_modify_qdisc() // sch_api.c:1795
│
└── __tc_modify_qdisc()
│
├── 查找父 qdisc (p)
├── 查找现有 qdisc (q) 或创建新的
│
└── qdisc_create() // sch_api.c:1244
│
├── qdisc_lookup_ops(kind) // 通过名字查找 Qdisc_ops
├── qdisc_alloc(dev_queue, ops) // 分配 Qdisc 实例
├── ops->init(sch, opt) // 初始化 Qdisc
└── qdisc_hash_add(sch) // 加入全局哈希表
tc filter add dev eth0 protocol ip parent 1:0 flower \
src_ip 10.0.0.1 dst_ip 10.0.0.2 ip_proto tcp action mirred egress redirect dev eth1
│
│ RTM_NEWTFILTER
▼
tc_ctl_filter() // cls_api.c
│
├── tcf_block_get() // 获取/创建 tcf_block
├── tcf_chain_lookup() // 查找 tcf_chain
└── tp->ops->change() // 创建过滤器规则
(例如 fl_change() 创建 flower 规则)
4.3 Qdisc 生命周期
创建: qdisc_create() → ops->init()
│
▼
激活: dev_activate() → transition_one_qdisc()
rcu_assign_pointer(txq->qdisc, qdisc)
│
▼
运行: __dev_xmit_skb() → enqueue/dequeue 循环
│
▼
停用: dev_deactivate() → qdisc_deactivate()
set_bit(__QDISC_STATE_DEACTIVATED)
rcu_assign_pointer(txq->qdisc, &noop_qdisc)
│
▼
销毁: qdisc_destroy() → ops->destroy()
qdisc_put() → kfree_rcu()
五、主要 Qdisc 实现
5.1 pfifo_fast(默认 Qdisc)
- • 算法:三优先级带 FIFO(3 bands),按
skb->priority 映射到 band - • 特性:支持
TCQ_F_CAN_BYPASS 快速路径 - • 入队:
pfifo_fast_enqueue() → skb_array_produce() 到对应 band - • 出队:
pfifo_fast_dequeue() → 从 band 0 开始依次尝试消费
5.2 TBF(Token Bucket Filter)
- • 外层 TBF(rate R, depth B):控制稳态速率和突发
- • 内层 TBF(peak rate P, depth M):限制短时间突发
- • 公式:任意时间窗口
[t_i, t_k] 内,发送总量 ≤ B + R × (t_k - t_i) - • 实现:维护
tokens(令牌)和 ptokens(峰值令牌),通过 hrtimer 定时补充
5.3 HTB(Hierarchical Token Bucket)
- •
HTB_CAN_SEND:有令牌,可以直接发送 - •
HTB_MAY_BORROW:自身无令牌,但可向父类借用 - •
HTB_CANT_SEND:无法发送也无法借用
- • 特点:支持 strict priority + rate limiting,适合 ISP/企业级 QoS
5.4 FQ-CoDel(Fair Queuing + Controlled Delay)
- • 算法:基于 CoDel 主动队列管理 + 多流公平排队
- • 按 flow hash 分流到多个子队列(公平性)
- • 目标延迟 5ms,检测到 overlimit 时丢包
- • 效果:解决 bufferbloat 问题,改善交互式应用延迟
5.5 其他常用 Qdisc
| | |
|---|
prio | | |
red | | Random Early Detection 主动丢弃 |
sfq | | Stochastic Fair Queuing 公平排队 |
netem | | |
cake | | Common Applications Kept Enhanced,综合优化 |
taprio | | Time-Aware Priority Shaper,TSN/802.1Qbv |
clsact | | |
六、Classifier 实现
6.1 主要分类器
| | |
|---|
flower | | 基于 L2-L4 流字段(五元组、VLAN、tunnel 等) |
u32 | | |
fw | | |
bpf | | |
matchall | | |
route | | |
basic | | |
6.2 Flower 分类器详解
Flower 是现代 Linux 中最常用的分类器,支持丰富的匹配字段:
// 五元组匹配
TCA_FLOWER_KEY_ETH_DST // 源/目的 MAC
TCA_FLOWER_KEY_IPV4_SRC/DST // 源/目的 IPv4
TCA_FLOWER_KEY_IPV6_SRC/DST // 源/目的 IPv6
TCA_FLOWER_KEY_TCP_SRC/DST // 源/目的端口
TCA_FLOWER_KEY_IP_PROTO // 协议号
// VLAN / Tunnel
TCA_FLOWER_KEY_VLAN_ID/PRIO // VLAN ID 和优先级
TCA_FLOWER_KEY_ENC_* // 隧道封装字段
// Conntrack
TCA_FLOWER_KEY_CT_STATE // 连接追踪状态
TCA_FLOWER_KEY_CT_MARK // 连接追踪标记
6.3 Action 系统
Action 是分类器匹配后执行的操作:
// 核心动作定义 (pkt_cls.h)
TC_ACT_OK // 通过,继续处理
TC_ACT_SHOT // 丢弃
TC_ACT_RECLASSIFY // 重新分类
TC_ACT_PIPE // 继续执行下一个 action
TC_ACT_STOLEN // 消费(不释放,但不再传递)
TC_ACT_QUEUED // 入队
TC_ACT_REDIRECT // 重定向
TC_ACT_TRAP // 陷阱到 CPU
常用 Action 实现:
- •
act_mirred(act_mirred.c):镜像或重定向包 - •
act_gact(act_gact.c):通用动作(丢弃/通过/随机) - •
act_skbedit(act_skbedit.c):修改 skb 字段(priority、mark、queue_mapping) - •
act_pedit(act_pedit.c):编辑包头任意字段 - •
act_nat(act_nat.c):NAT 操作 - •
act_bpf(act_bpf.c):运行 BPF 程序
七、硬件卸载(Offload)
TC 支持将分类和动作卸载到支持的网卡硬件中执行:
用户空间 tc filter add ... skip_sw
│
▼
tc_setup_flow_offload() // cls_api.c
│
├── tp->ops->reoffload() // 分类器卸载
│ 例如 fl_reoffload() → flow_indr_add_offload()
│
└── 设备驱动回调
ndo_setup_tc(TC_SETUP_CLSFLOWER)
→ 驱动将规则安装到硬件
卸载标志:
- •
TCA_CLS_FLAGS_SKIP_SW:不使用软件路径 - •
TCA_CLS_FLAGS_SKIP_HW:不卸载到硬件 - •
TCA_CLS_FLAGS_IN_HW:规则已卸载到硬件
八、关键流程总结
8.1 发送路径完整调用链
dev_queue_xmit(skb)
→ __dev_queue_xmit(skb)
→ sch_handle_egress(skb) // Egress 分类
→ tc_run(entry, skb)
→ tcf_classify(skb, block, tp, res)
→ tp->ops->classify(skb, tp, res) // 具体分类器
→ tcf_exts_exec(skb, exts, res) // 执行 actions
→ txq = netdev_core_pick_tx(dev, skb) // 选择 TX 队列
→ q = txq->qdisc // 获取 Qdisc
→ __dev_xmit_skb(skb, q, dev, txq)
→ dev_qdisc_enqueue(skb, q) // 入队
→ q->enqueue(skb, q, to_free) // Qdisc 入队操作
→ qdisc_run(q)
→ __qdisc_run(q)
→ qdisc_restart(q)
→ dequeue_skb(q) // 出队
→ q->dequeue(q) // Qdisc 出队操作
→ sch_direct_xmit(skb) // 发送到驱动
→ dev_hard_start_xmit(skb)
→ ndo_start_xmit(skb, dev) // 驱动发送
8.2 接收路径完整调用链
__netif_receive_skb_core(skb)
→ sch_handle_ingress(skb) // Ingress 分类
→ tc_run(entry, skb)
→ tcf_classify(skb, block, tp, res)
→ tp->ops->classify(skb, tp, res) // 具体分类器
→ switch(sch_ret):
TC_ACT_REDIRECT → skb_do_redirect(skb)
TC_ACT_SHOT → kfree_skb_reason(skb)
TC_ACT_OK → 继续
→ 协议分发 (ip_rcv, ipv6_rcv, etc.)
九、总结
Linux TC 是一个功能强大且高度可扩展的流量控制框架,其设计体现了以下核心原则:
- 1. 分层解耦:分类、动作、调度三者独立,可自由组合
- 2. 可扩展性:通过
Qdisc_ops、tcf_proto_ops、tcfa_act 等操作函数集实现插件化 - 3. 性能优化:快速路径 bypass、per-CPU 统计、无锁 Qdisc、硬件卸载
- 4. 层级化:支持 Qdisc 嵌套、class 层次、chain 组织,适应复杂 QoS 场景
- 5. RCU 友好:数据路径大量使用 RCU 读取,控制路径使用 RTNL 锁
- 6. 硬件协同:支持将分类和动作卸载到网卡,减少 CPU 开销