
简介本资源为WFQ加权公平队列算法的C/C实现工程面向网络协议学习者、路由与流量调度方向的开发者及课程设计人员用于理解并复现多路复用网络中按权重公平分配带宽的调度机制。工程同时覆盖发送端、接收端与路由器转发三部分将数据包分类、权重分配、队列调度与循环监控等关键环节落地为可编译运行的代码并与FIFO策略形成对照便于分析公平性与服务质量差异。压缩包共344个文件约14.98MB以cpp与c源码、h头文件为主体辅以obj、exe、pdb等编译产物及dsp、dsw工程文件另有rc资源、txt说明与doc文档结构完整、便于直接打开工程调试。目前已有958人学习下载适合作为网络调度算法入门实践与二次开发的参考样例。1. WFQ算法实现c/c从排队论到可运行代码的完整落地很多开发者第一次接触 QoS 调度时都会被 WFQWeighted Fair Queueing加权公平队列这个名字唬住以为它只是网络设备里的黑匣子。实际上WFQ 的核心思想非常朴素给每个数据流分配一个权重按权重比例分配带宽同时保证低权重流不会被高权重流彻底饿死。这套 C/C 实现把 WFQ 从论文公式拉到了可编译、可调试、可改造的工程代码层面适合正在做网络协议栈、流量调度、嵌入式网关或软交换二次开发的工程师。你拿到手就能跑改几个参数就能看到不同权重下的调度差异不用先去啃 RFC 文档。2. 项目架构拆解WFQ 调度器的模块划分与数据结构2.1 整体架构与文件组织这份 WFQ 实现采用经典的 C/C 混合风格核心逻辑用 C 写对外接口和测试用例用 C 封装。目录结构大致如下wfq/ ├── include/ │ ├── wfq.h # 对外接口初始化、入队、出队、权重设置 │ ├── packet.h # 数据包结构定义 │ └── queue.h # 队列节点与链表操作 ├── src/ │ ├── wfq.c # WFQ 核心调度算法 │ ├── queue.c # 队列管理入队、出队、排序 │ └── packet.c # 数据包构造与销毁 ├── test/ │ ├── test_wfq.cpp # 单元测试权重比例验证 │ └── demo.cpp # 演示程序模拟多流调度 ├── Makefile └── README.md这种分层的好处是wfq.c只关心调度逻辑queue.c只关心数据结构packet.c只关心数据包生命周期。二次开发时你要换调度策略只动wfq.c要换底层队列实现比如从链表换成环形缓冲区只动queue.c。常见做法是把虚拟时间计算和队列选择分开这样调试时能单独打印每个流的虚拟时间快速定位是权重算错了还是队列排序错了。2.2 核心数据结构数据包与流队列WFQ 的调度精度取决于两个东西数据包的元信息是否完整以及每个流的队列是否维护了正确的虚拟时间。先看数据包结构// include/packet.h typedef struct Packet { int flow_id; // 流标识用于区分不同队列 int length; // 包长字节影响虚拟时间增量 int weight; // 该流对应的权重 double virtual_time; // 该包到达时的虚拟时间 struct Packet *next; // 链表指针 } Packet;flow_id是流的唯一标识调度器靠它找到对应队列。length直接参与虚拟时间计算——包越长占用带宽越多虚拟时间推进越快。weight决定该流在总带宽中的占比。virtual_time是 WFQ 的灵魂字段每个包入队时都会根据当前流的虚拟时间和包长算出一个新的虚拟时间出队时选虚拟时间最小的包。再看流队列结构// include/queue.h typedef struct FlowQueue { int flow_id; int weight; double virtual_time; // 该流当前虚拟时间 Packet *head; // 队头 Packet *tail; // 队尾 int packet_count; // 队列长度用于统计 } FlowQueue;每个流一个FlowQueue里面维护自己的virtual_time。这个虚拟时间不是全局时钟而是该流“应该”推进到的位置。权重越大虚拟时间推进越慢因为同样的包长除以更大的权重增量更小。这样高权重流的包虚拟时间更小更容易被优先调度但低权重流也不会永远排不上——因为它的虚拟时间也在推进只是慢一点。2.3 虚拟时间计算WFQ 的数学核心WFQ 的公平性全靠虚拟时间公式virtual_time_new virtual_time_old packet_length / weight这个公式的含义是包长越大虚拟时间增量越大权重越大增量越小。调度器每次从所有非空队列中选虚拟时间最小的队头包出队。// src/wfq.c double calc_virtual_time(FlowQueue *fq, int packet_len) { // 虚拟时间增量 包长 / 权重 // 权重越大增量越小该流越容易被调度 double increment (double)packet_len / (double)fq-weight; return fq-virtual_time increment; }参数说明fq是流队列指针packet_len是当前包长。返回值是该包的新虚拟时间。注意这里用double而不是int因为权重比可能是 1:3:5 这种非整数关系用整数会丢精度导致调度结果和理论值偏差越来越大。我一般会在调试时把每个包的虚拟时间打印出来和手算结果对一遍确认公式没写错。2.4 入队与出队调度器的两个关键动作入队逻辑负责把包挂到对应流队列并更新该流的虚拟时间// src/wfq.c int wfq_enqueue(WFQ_Scheduler *sched, Packet *pkt) { FlowQueue *fq find_flow_queue(sched, pkt-flow_id); if (!fq) return -1; // 流不存在需要先注册 // 计算该包的虚拟时间 pkt-virtual_time calc_virtual_time(fq, pkt-length); // 更新流队列的虚拟时间 fq-virtual_time pkt-virtual_time; // 按虚拟时间插入队列保持有序 insert_packet_sorted(fq, pkt); fq-packet_count; return 0; }出队逻辑遍历所有流队列选虚拟时间最小的队头包// src/wfq.c Packet* wfq_dequeue(WFQ_Scheduler *sched) { FlowQueue *best_fq NULL; double min_vt 1e18; // 初始化为一个极大值 for (int i 0; i sched-flow_count; i) { FlowQueue *fq sched-flows[i]; if (fq-head NULL) continue; // 空队列跳过 if (fq-head-virtual_time min_vt) { min_vt fq-head-virtual_time; best_fq fq; } } if (!best_fq) return NULL; // 所有队列为空 Packet *pkt best_fq-head; best_fq-head pkt-next; if (best_fq-head NULL) best_fq-tail NULL; best_fq-packet_count--; return pkt; }这里有个细节insert_packet_sorted保证每个流队列内部按虚拟时间有序这样出队时只需要比较各队头不用遍历整个队列。如果流数量很多比如上千个流每次出队都遍历所有流会变成性能瓶颈。常见优化是用最小堆维护各流队头的虚拟时间把 O(n) 降到 O(log n)。这份代码为了可读性先用线性扫描二次开发时可以替换。3. 编译与运行从源码到可执行 Demo 的完整步骤3.1 环境准备与编译命令这份代码依赖很少Linux 下只需要 gcc/g 和 make。Windows 下可以用 MinGW 或 WSL。先确认工具链gcc --version g --version make --version进入项目根目录直接执行make clean makeMakefile 里默认编译出两个可执行文件test_wfq和demo。test_wfq跑单元测试验证权重比例是否正确demo模拟三个流不同权重的调度过程打印每个包的出队顺序。如果编译报错undefined reference to calc_virtual_time说明wfq.c没被链接进去检查 Makefile 的SRCS变量是否包含src/wfq.c。这是最常见的翻车点尤其是手动改过目录结构之后。3.2 运行 Demo 与结果解读编译成功后运行./demo输出大致如下Flow 1 (weight1): packet 1, vt100.00 Flow 2 (weight2): packet 1, vt50.00 Flow 3 (weight3): packet 1, vt33.33 --- dequeue order --- Flow 3 packet 1 (vt33.33) Flow 2 packet 1 (vt50.00) Flow 1 packet 1 (vt100.00)这个结果说明权重 3 的流虚拟时间最小最先出队权重 1 的流虚拟时间最大最后出队。如果三个流持续有包长期来看出队包的数量比会接近 3:2:1这就是 WFQ 的加权公平性。你可以改demo.cpp里的权重值重新编译运行观察调度顺序变化。比如把 Flow 1 的权重改成 10它会变成最先出队的流。这种即时反馈对理解 WFQ 非常有用。3.3 单元测试验证权重比例test_wfq.cpp里构造了固定包长的多流场景统计一段时间内各流出队包数和理论比例对比// test/test_wfq.cpp void test_weight_proportion() { WFQ_Scheduler sched; wfq_init(sched); wfq_add_flow(sched, 1, 1); // flow 1, weight 1 wfq_add_flow(sched, 2, 2); // flow 2, weight 2 wfq_add_flow(sched, 3, 3); // flow 3, weight 3 // 每个流各入队 100 个包 for (int i 0; i 100; i) { wfq_enqueue(sched, make_packet(1, 1000)); wfq_enqueue(sched, make_packet(2, 1000)); wfq_enqueue(sched, make_packet(3, 1000)); } int count[4] {0}; Packet *pkt; while ((pkt wfq_dequeue(sched)) ! NULL) { count[pkt-flow_id]; free(pkt); } // 理论比例 1:2:3允许 5% 误差 assert(abs(count[1] - 50) 5); assert(abs(count[2] - 100) 5); assert(abs(count[3] - 150) 5); }这个测试跑通说明虚拟时间计算和出队选择逻辑是对的。如果断言失败先检查calc_virtual_time里的除法是不是写成了整数除法再检查insert_packet_sorted有没有正确排序。4. 避坑与常见问题WFQ 实现里最容易翻车的几个点4.1 虚拟时间溢出或精度丢失现象跑长时间仿真后调度顺序开始混乱高权重流反而排不过低权重流。原因virtual_time用float而不是double累加几千次后精度不够或者用int存虚拟时间包长除以权重后直接截断成整数。解决统一用double并且在每次入队时检查虚拟时间是否超过阈值比如 1e12超过就所有流的虚拟时间同时减去一个基准值防止溢出。这个操作叫“虚拟时间重归一化”生产环境必须做。4.2 权重为 0 导致除零崩溃现象程序在calc_virtual_time里直接 segfault。原因注册流的时候没校验权重传了 0 进去除法直接崩。解决wfq_add_flow里加断言或返回错误码权重必须大于 0。如果业务上确实需要“暂停”某个流应该把它从调度器里移除而不是设权重为 0。4.3 队列排序不稳定导致同虚拟时间包顺序错乱现象两个包虚拟时间相同出队顺序随机测试结果不稳定。原因insert_packet_sorted用的是而不是导致相同虚拟时间的包插到了前面破坏了 FIFO 语义。解决排序时用严格小于相同虚拟时间的包按入队顺序排列。WFQ 本身不保证相同虚拟时间的顺序但工程实现里保持 FIFO 更符合直觉也方便调试。4.4 流数量多时出队性能急剧下降现象流数量从 10 增加到 1000 后出队耗时从微秒级涨到毫秒级。原因wfq_dequeue每次遍历所有流找最小虚拟时间复杂度 O(n)。解决引入最小堆或红黑树维护各流队头虚拟时间出队复杂度降到 O(log n)。这份代码为了可读性先用线性扫描二次开发时如果流数量超过 100建议直接换堆实现。4.5 内存泄漏出队后忘记释放包现象长时间运行后 RSS 持续上涨最终 OOM。原因wfq_dequeue返回的Packet*是堆分配的调用方用完必须free。Demo 里释放了但集成到实际项目时经常漏掉。解决在wfq_dequeue内部不释放由调用方负责或者提供wfq_dequeue_and_free接口把释放逻辑封装进去。我一般会在测试里用 valgrind 跑一遍确认没有 definitely lost。5. 二次开发与改造把 WFQ 塞进你自己的项目5.1 替换底层队列从链表到环形缓冲区这份代码用链表实现流队列好处是插入删除 O(1)坏处是缓存不友好。如果你的场景是固定包长、高吞吐可以把FlowQueue里的链表换成环形缓冲区typedef struct RingBuffer { Packet *buf; int capacity; int head; int tail; int count; } RingBuffer;入队时tail (tail 1) % capacity出队时head (head 1) % capacity。注意环形缓冲区满了之后要么丢包要么阻塞WFQ 本身不处理拥塞需要你在上层加策略。5.2 支持动态权重调整实际网络里流的权重可能随时变化。你可以在FlowQueue里加一个pending_weight字段入队时如果发现权重变了先按旧权重把当前队列的虚拟时间重算一遍再切到新权重。重算逻辑void update_flow_weight(FlowQueue *fq, int new_weight) { if (new_weight 0) return; double ratio (double)fq-weight / (double)new_weight; fq-virtual_time * ratio; // 按比例缩放虚拟时间 fq-weight new_weight; }这个缩放保证权重切换时不会出现虚拟时间跳变调度顺序平滑过渡。5.3 验证改造是否正确三个必跑测试改完代码后至少跑这三个测试测试项目的预期结果权重比例测试验证调度公平性出队包数比接近权重比空队列测试验证边界条件所有队列为空时返回 NULL不崩溃长时间运行测试验证虚拟时间不溢出跑 100 万包后调度顺序仍正确我一般会在 CI 里挂一个 10 万包的回归测试每次提交都跑一遍。有一次改排序逻辑把写成单元测试没覆盖到相同虚拟时间的场景结果上线后调度顺序全乱。从那以后我每次改insert_packet_sorted都强制走一遍相同虚拟时间的用例确认 FIFO 语义没丢。希望帮到你。本文还有配套的精品资源点击获取