土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】Traffic Control 深度拆解:qdisc、class 与 filter

文章导航

分类入口
linuxnetworking
标签入口
#tc#qdisc#traffic-control#htb#fq-codel#tbf#edt#pacing#tc-bpf#multiqueue#bpftrace

目录

发包路径全解 一文中,我们看到 dev_queue_xmit() 在把包交给驱动之前,经过了 qdisc 的排队和调度。那篇文章给出了发包路径的全景——本文深入 Traffic Control 框架本身。

TC 的核心问题:当多个应用同时发包,谁先发、发多快、怎么限速?这不是简单的先进先出——内核需要支持带宽分配、优先级调度、延迟控制、公平排队等复杂策略。TC 框架通过三层抽象实现:

   应用层:tc qdisc / tc filter / tc class 命令
   ───────────────────────────────────────────
   内核层:
   ┌──────────────────────────────────────────┐
   │ filter(分类器)                           │
   │  └── tcf_proto: 根据包特征选择 class       │
   │       ├── u32: 通用字段匹配               │
   │       ├── flower: 流表匹配                │
   │       └── bpf: BPF 程序分类               │
   ├──────────────────────────────────────────┤
   │ class(分类)                              │
   │  └── 仅分层 qdisc 有:HTB class、CBQ class│
   │       每个 class 可挂自己的子 qdisc        │
   ├──────────────────────────────────────────┤
   │ qdisc(排队规则)                          │
   │  └── Qdisc_ops: enqueue → dequeue → 驱动  │
   │       ├── pfifo_fast: 三优先级 FIFO       │
   │       ├── fq_codel: 公平排队 + 延迟控制   │
   │       ├── htb: 分层令牌桶                  │
   │       └── fq: 公平排队 + pacing            │
   └──────────────────────────────────────────┘

一、struct Qdisc:排队规则对象

核心结构

struct Qdiscinclude/net/sch_generic.h:73)是 TC 框架的核心——每个发送队列(netdev_queue)上挂载一个 qdisc 实例:

struct Qdisc {
    int (*enqueue)(struct sk_buff *skb,
                   struct Qdisc *sch,
                   struct sk_buff **to_free);  /* 入队 */
    struct sk_buff *(*dequeue)(struct Qdisc *sch); /* 出队 */
    unsigned int    flags;               /* TCQ_F_* 标志 */

    const struct Qdisc_ops  *ops;        /* 操作表 */
    struct netdev_queue     *dev_queue;   /* 所属发送队列 */

    struct qdisc_skb_head    q;          /* 内部包队列 */
    struct gso_skb           gso_skb;    /* GSO 大包暂存 */

    unsigned long            state;       /* 运行状态 */
    struct Qdisc            *parent;     /* 父 qdisc */

    /* 统计 */
    struct gnet_stats_basic_sync bstats;  /* 基本统计 */
    struct gnet_stats_queue  qstats;      /* 队列统计 */

    /* 并发控制 */
    seqcount_t               running;     /* 无锁运行标记 */
    spinlock_t               busylock;    /* 并发保护 */
    spinlock_t               seqlock;     /* 序列锁 */

    long                     privdata[] ____cacheline_aligned;
};

关键标志

标志 含义
TCQ_F_BUILTIN 内建 qdisc(不可删除)
TCQ_F_CAN_BYPASS 队列空时可跳过排队直接发送
TCQ_F_MQROOT MQ 多队列根 qdisc
TCQ_F_NOLOCK 无锁 qdisc(fq、fq_codel 等)

Qdisc_ops:操作表

struct Qdisc_opssch_generic.h:287)定义 qdisc 实现的接口:

struct Qdisc_ops {
    struct Qdisc_ops        *next;       /* 链表 */
    const struct Qdisc_class_ops *cl_ops;/* 分类操作(分层 qdisc 才有) */
    char                    id[IFNAMSIZ];/* 名称:"pfifo_fast"、"htb" */
    int                     priv_size;   /* 私有数据大小 */
    unsigned int            static_flags;/* 默认标志 */

    int  (*enqueue)(struct sk_buff *, struct Qdisc *,
                    struct sk_buff **to_free);
    struct sk_buff *(*dequeue)(struct Qdisc *);
    struct sk_buff *(*peek)(struct Qdisc *);

    int  (*init)(struct Qdisc *, struct nlattr *arg,
                 struct netlink_ext_ack *);
    void (*reset)(struct Qdisc *);
    void (*destroy)(struct Qdisc *);
    int  (*change)(struct Qdisc *, struct nlattr *arg,
                   struct netlink_ext_ack *);
    int  (*dump)(struct Qdisc *, struct sk_buff *);
    int  (*dump_stats)(struct Qdisc *, struct gnet_dump *);
};

二、发送路径中的 TC

dev_queue_xmit 到 qdisc

回顾发包路径中 TC 的位置(详见 发包路径全解):

dev_queue_xmit(skb)
    → __dev_queue_xmit(skb, sb_dev)
        → 选择 TX 队列:netdev_pick_tx()
        → 获取队列的 qdisc:rcu_dereference(txq->qdisc)
        → __dev_xmit_skb(skb, q, dev, txq)

__dev_xmit_skb:快路径与慢路径

/* 简化逻辑 */
static inline int __dev_xmit_skb(struct sk_buff *skb,
                                  struct Qdisc *q, ...)
{
    /* 快路径:队列空且支持 bypass */
    if ((q->flags & TCQ_F_CAN_BYPASS) && qdisc_empty(q) &&
        qdisc_run_begin(q)) {
        /* 跳过排队,直接发送 */
        sch_direct_xmit(skb, q, dev, txq, root_lock);
        qdisc_run_end(q);
        return NET_XMIT_SUCCESS;
    }

    /* 慢路径:入队 */
    rc = q->enqueue(skb, q, &to_free);
    if (qdisc_run_begin(q)) {
        __qdisc_run(q);  /* 出队循环 */
        qdisc_run_end(q);
    }
    return rc;
}

CAN_BYPASS 优化:当 qdisc 队列为空(没有积压)且标志允许,直接跳过 enqueue/dequeue 流程,走 sch_direct_xmit() 发送。对于低负载场景,这避免了不必要的排队和调度开销。pfifo_fast、fq_codel 等默认都支持 bypass。

__qdisc_run:出队循环

void __qdisc_run(struct Qdisc *q)
{
    int quota = dev_tx_weight;  /* 默认 64 */

    while (qdisc_restart(q, &packets)) {
        quota -= packets;
        if (quota <= 0) {
            /* 配额用尽,调度 NET_TX_SOFTIRQ 稍后继续 */
            __netif_schedule(q);
            break;
        }
    }
}

qdisc_restart() 每次调用 q->dequeue() 取一个包,交给 sch_direct_xmit() 发送到驱动。dev_tx_weight(默认 64)限制每次软中断处理的包数——防止单个 qdisc 独占 CPU。

无锁 qdisc(TCQ_F_NOLOCK)

传统 qdisc 用 qdisc->root_lock 保护——所有 CPU 竞争同一把锁。现代 qdisc(fq、fq_codel)使用 TCQ_F_NOLOCK 标志,配合 seqlockQDISC_STATE_MISSED 标志实现无锁并发:

static inline bool qdisc_run_begin(struct Qdisc *qdisc)
{
    if (qdisc->flags & TCQ_F_NOLOCK) {
        if (spin_trylock(&qdisc->seqlock))
            return true;
        /* 竞争失败:设置 MISSED 标志,通知持有者 */
        set_bit(__QDISC_STATE_MISSED, &qdisc->state);
        /* 再试一次(避免 MISSED 丢失) */
        if (spin_trylock(&qdisc->seqlock))
            return true;
        return false;
    }
    /* 传统 qdisc:测试并设置 RUNNING 位 */
    return !__test_and_set_bit(__QDISC_STATE2_RUNNING, ...);
}

MISSED 标志的作用:CPU-A 持有锁正在出队,CPU-B 入队后发现拿不到锁——设置 MISSED。CPU-A 在 qdisc_run_end() 检查 MISSED,如果有则重新调度出队——保证 CPU-B 入队的包不会被遗忘。

三、常用 qdisc 实现

pfifo_fast:默认 qdisc

pfifo_fast 维护三个优先级 band(band 0 最高),根据 TOS/DSCP 映射选择 band:

Band 0(高优先级)← TOS: Minimize-Delay
Band 1(普通)    ← 大多数流量
Band 2(低优先级)← TOS: Maximize-Throughput

dequeue 时从 band 0 开始——只有高优先级 band 为空才取下一级。

特点:实现极简、零开销,但无公平排队——一个大流量可以饿死同 band 的其他流。

fq_codel:公平排队 + 延迟控制

fq_codel(Fair Queuing Controlled Delay)是现代 Linux 的推荐 qdisc——解决 bufferbloat 问题:

入队:
skb → hash(五元组) → 分配到 1024 个桶之一
    → 桶内 FIFO

出队:
DRR(Deficit Round Robin)→ 轮流从各桶出队
    → CoDel AQM → 检测排队延迟
        → 延迟 > target(5ms) 持续 > interval(100ms)
            → 开始丢包(概率递增)

CoDel 算法的核心洞察:排队延迟才是 bufferbloat 的信号,而不是队列长度。CoDel 测量每个包在队列中停留的时间——如果排队延迟持续超过 target(默认 5ms),开始概率性丢包,强迫发送端降速。

fq 与 fq_codel 的区别:fq 支持 pacing——根据 TCP 的 EDT(Earliest Departure Time)时间戳控制每个包的精确发送时间。BBR 拥塞控制依赖 fq 的 pacing 能力。

HTB:分层令牌桶

HTB(Hierarchical Token Bucket)支持树形带宽分配:

tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30mbit ceil 60mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 50mbit ceil 80mbit
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 20mbit ceil 100mbit

每个 class 有两个参数: - rate:保证带宽(即使其他 class 满载也能获得) - ceil:最大带宽(可以借用父 class 的空闲带宽)

HTB 在内核中用红黑树管理 class 层级,令牌桶算法控制每个 class 的发送速率。当 class 用完自己的令牌但 ceil 未满时,可以从父 class”借”令牌。

TBF:简单令牌桶

TBF(Token Bucket Filter)是最简单的限速 qdisc:

# 限速 10Mbps,突发 32KB
tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 400ms

令牌以 rate 速率填充桶,每发一个包消耗对应令牌。桶满时丢弃新到的令牌(不超过 burst)。无令牌时包在队列中等待,超过 latency 则丢弃。

四、EDT(Earliest Departure Time)

EDT 是现代 TC 的调度模型——不再问”每秒发多少包”,而是为每个包标记”这个包应该在什么时间发出”。

工作原理

TCP 层:
    skb->tstamp = ktime_get() + pacing_delay
    (BBR/CUBIC pacing 计算精确发送时间)
        ↓
fq qdisc:
    按 skb->tstamp 排序
    只有 tstamp ≤ 当前时间的包才出队
        ↓
驱动:
    支持 EDT 的网卡可进一步延迟到精确时间

EDT 的优势: 1. 精确 pacing:BBR 可以精确控制每个包的发送间隔 2. 消除排队:包在 qdisc 中按时间排序,不需要大队列 3. 端到端协同skb->tstamp 从 TCP 层传递到驱动,全路径感知

# 启用 fq qdisc(支持 EDT pacing)
tc qdisc replace dev eth0 root fq

五、filter(分类器)

tcf_proto:分类器实例

struct tcf_protosch_generic.h:406)是单个分类器:

struct tcf_proto {
    struct tcf_proto __rcu *next;
    void __rcu      *root;          /* 分类器特定的数据 */
    int (*classify)(struct sk_buff *,
                    const struct tcf_proto *,
                    struct tcf_result *);
    __be16           protocol;      /* 匹配的协议(ETH_P_IP 等) */
    const struct tcf_proto_ops *ops;/* 分类器类型操作 */
    struct tcf_chain *chain;        /* 所属链 */
    u32              prio;          /* 优先级 */
};

常用分类器

分类器 用途 特点
u32 通用字段匹配 可匹配任意包头偏移
flower 流表匹配 支持硬件卸载
bpf BPF 程序 最灵活,直接返回 classid
fw fwmark 匹配 配合 iptables MARK
route 路由匹配 基于路由 realm
basic 基础匹配 ematch 表达式

TC BPF:direct-action 模式

传统 TC 流程:filter 分类 → 选择 class → class 的 qdisc 处理。TC BPF 的 direct-action 模式跳过中间步骤——BPF 程序直接返回 TC_ACT 动作:

/* BPF 程序返回值 */
TC_ACT_OK      (0)  /* 放行 */
TC_ACT_SHOT    (2)  /* 丢弃 */
TC_ACT_PIPE    (3)  /* 传递给下一个 filter */
TC_ACT_REDIRECT(7)  /* 重定向到其他设备 */

内核对 TC BPF 有 retpoline 优化——tc_wrapper.h 中为 cls_bpf_classify 提供直接调用路径,避免间接函数调用的 Spectre 缓解开销:

/* tc_wrapper.h 中的快路径 */
if (IS_BUILTIN(CONFIG_NET_CLS_BPF)) {
    if (tp->classify == cls_bpf_classify)
        return cls_bpf_classify(skb, tp, res);  /* 直接调用 */
}
# 挂载 TC BPF 程序(direct-action 模式)
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj prog.o sec classifier

六、MQ 多队列 qdisc

netdev_queue 与 qdisc 的关系

现代网卡有多个 TX 队列——每个队列是一个 struct netdev_queueinclude/linux/netdevice.h:640):

struct netdev_queue {
    struct Qdisc __rcu *qdisc;         /* 活跃 qdisc */
    struct Qdisc __rcu *qdisc_sleeping;/* 非活跃 qdisc */
    struct net_device  *dev;
    spinlock_t          _xmit_lock;     /* TX 锁 */
    unsigned long       trans_start;
    struct dql           dql;           /* BQL(动态队列限制) */
};

MQ 根 qdisc

多队列网卡默认使用 MQ qdisc 作为根——它本身不排队,只是把包分发给各队列的子 qdisc:

root: mq (TCQ_F_MQROOT)
├── queue 0: fq_codel (子 qdisc)
├── queue 1: fq_codel
├── queue 2: fq_codel
└── queue 3: fq_codel

每个子 qdisc 独立运行——不同 TX 队列的入队/出队可以并行,无锁竞争。

BQL(Byte Queue Limits)

struct dqlstruct netdev_queue 的成员)动态控制驱动层队列的深度——防止驱动 ring buffer 积攒过多包导致延迟增加:

BQL 算法:
1. 记录每次 xmit 提交的字节数
2. 记录驱动完成(interrupt)的字节数
3. 动态调整 limit = f(inflight, completions)
4. inflight > limit 时暂停提交,等驱动消化

BQL 与 qdisc 配合——qdisc 控制”怎么排队”,BQL 控制”排多深”。

七、可观测性实战

qdisc 状态查看

# 查看所有 qdisc 统计
tc -s qdisc show dev eth0

# 查看 class 统计(HTB)
tc -s class show dev eth0

# 查看 filter
tc filter show dev eth0

qdisc 丢包追踪

# 追踪 qdisc 丢包——enqueue 返回非零
bpftrace -e '
kretprobe:htb_enqueue /retval != 0/ {
    printf("htb enqueue DROP ret=%d\n", retval);
    @drops = count();
}
interval:s:10 { print(@drops); clear(@drops); }'

fq_codel 延迟监控

# fq_codel 的 CoDel 丢包事件
bpftrace -e '
tracepoint:qdisc:qdisc_dequeue {
    if (args->qdisc_handle == 0x00010000) {
        @dequeue_bytes = hist(args->skbaddr);
    }
}'

# 查看 fq_codel 内部统计
tc -s qdisc show dev eth0 | grep -A5 fq_codel

TC BPF 程序性能

# 查看 TC BPF 程序统计
tc filter show dev eth0 ingress

# BPF 程序执行时间
bpftool prog show
bpftool prog profile id <ID> duration 5

__qdisc_run 耗时

# 测量 qdisc 出队循环耗时
bpftrace -e '
kprobe:__qdisc_run { @start[tid] = nsecs; }
kretprobe:__qdisc_run /@start[tid]/ {
    @qdisc_run_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

八、关键参数速查

参数 默认值 含义 调优建议
net.core.default_qdisc pfifo_fast 默认 qdisc 推荐 fq 或 fq_codel
net.core.dev_weight 64 每次 softirq 的出队包数 高吞吐可增大
net.core.dev_weight_tx_bias 1 TX/RX 权重比 发送密集型增大
fq_codel target 5ms CoDel 目标延迟 低延迟场景减小
fq_codel interval 100ms CoDel 检测间隔 通常不需调整
fq_codel quantum MTU DRR 调度量 通常不需调整
fq_codel flows 1024 哈希桶数 高并发可增大
fq pacing 1 启用 pacing BBR 必须开启
fq initial_quantum 10×MTU 新流首包配额 通常不需调整
HTB r2q 10 rate-to-quantum 转换系数 低速 class 可调

参考文献

  1. Linux 内核源码,include/net/sch_generic.h,6.8(struct Qdisc 于第 73 行,Qdisc_ops 于第 287 行)
  2. Linux 内核源码,include/net/pkt_sched.h,6.8(qdisc_run 于第 120 行)
  3. Linux 内核源码,include/net/pkt_cls.h,6.8(tcf_exts 于第 214 行)
  4. Linux 内核源码,include/net/tc_wrapper.h,6.8(cls_bpf_classify 快路径于第 144 行)
  5. Kathleen Nichols, Van Jacobson, “Controlling Queue Delay”, ACM Queue, 2012(CoDel 算法)
  6. Linux 内核文档,Documentation/networking/tc-actions-env-rules.rst

上一篇Netfilter 内核实现:钩子、conntrack 与 NAT

下一篇网络命名空间:内核级网络隔离的实现

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-22 · linux / networking

【Linux 网络子系统深度拆解】eBPF 网络钩子全景:TC/XDP/socket/cgroup

从内核源码全面拆解 eBPF 在网络子系统中的所有挂载点:TC BPF direct-action 模式与 bpf_mprog 多程序链、XDP 驱动级钩子回顾、socket ops 回调与 TCP 生命周期事件、cgroup BPF 策略控制、sk_msg/sk_skb 的 sockmap 重定向引擎、struct_ops 实现自定义拥塞控制,以及 bpftrace 可观测实战。

2026-04-27 · linux / networking

【Linux 网络子系统深度拆解】多队列与流量分发:RSS/RPS/RFS/XPS

单队列网卡的时代早已过去,但多队列本身只是起点——如何把包分到正确的 CPU 上,才是性能的关键。本文从 Linux 6.6 内核源码拆解多队列网络的完整流量分发体系:RSS 硬件哈希与 Toeplitz 算法、RPS 软件多队列模拟与 get_rps_cpu() 路径、RFS 应用感知的 rps_sock_flow_table 机制、XPS 发送端 CPU/队列亲和、aRFS 硬件流表加速,以及 netdev_pick_tx() 发送队列选择逻辑。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】TCP 内核实现(下):数据传输与拥塞控制

tcp_sendmsg 把用户数据拷到 sk_buff 就完事了?远没有。后面还有 Nagle 合并、TSQ 限流、cwnd/rwnd 双窗口门控、RACK-TLP 丢包检测、拥塞状态机五态跳转、sk_pacing_rate 软件限速。本文从 Linux 6.6 内核源码拆解 TCP 数据传输的完整路径——从 send() 到 ACK 处理——以及拥塞控制框架 tcp_congestion_ops 的可插拔架构。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】发包路径全解:从 send() 到网线

一个用户态 send() 调用要走过 TCP 分段、IP 路由、Netfilter 钩子、Qdisc 排队、GSO 分段、驱动 DMA 映射六个阶段才能把数据送上网线。本文从 Linux 6.6 内核源码出发,逐函数拆解完整的 TX 发包路径,深入 TSQ 限流、Qdisc 调度、BQL 防膨胀、GSO/TSO 分段决策等核心机制。


By .