F-Stack 2.0 前瞻8 :用户态/协议栈间的零拷贝,从魔改到 FreeBSD 原生 sosend 对称架构

1. 本功能主要作用和特点

本篇只讲 F-Stack 的用户态与协议栈之间一直有的”最后一公里”的内存拷贝问题,不涉及其他层的零拷贝,这篇把零拷贝从 2018 年到 2026 年的演进捋一遍——早期双向都有内存拷贝,2022 年有了发送方向的魔改方案,2026 年 6 月终于收敛成”双向对称、纯原生”的完整用户态零拷贝栈。

拷贝发生在哪先搞清楚。数据包在服务器的处理分接收和发送两个方向:

  • 接收方向:网卡 → DPDK hugepage 是 DMA,天然零拷贝;但应用调 ff_read() 把数据从协议栈 mbuf 搬到用户 buffer 有一次拷贝(issue #407 官方确认,这是设计如此)
  • 发送方向:有两处拷贝——应用层调用 socket 发送接口时数据从应用层拷到 FreeBSD 协议栈(应用 → 协议栈),以及协议栈把数据拷到 DPDK rte_mbuf(协议栈 → 网卡)

2026 年 6 月之前,F-Stack 零拷贝的版图是这样的:接收方向零拷贝(FF_ZC_RECV)只有空接口,未实际实现,在2026-06-11通过FreeBSD源生接口实现了接受方向零拷贝的实际实现(commit b87f5f0d2);发送方向零拷贝(FF_ZC_SEND)用的是 FSTACK_ZC_MAGIC 哨兵 + m_uiotombuf 内核魔改的 workaround——能跑但有 GPF 风险、大数据量发送 crash、FreeBSD 升级时维护成本高。2026-06-12 的原生化重构(commit b6ce5884c)把发送方向也换成了 FreeBSD 15.0 原生的 sosend(top) 路径,至此形成完全对称的双向零拷贝栈。几个关键词:

  • 双向对称:ZC-send(kern_zc_sendit + sosend top)与 ZC-recv(kern_zc_recvit + soreceive mp0)在内核入口、用户态 API、生命周期、错误路径、ABI 增量五个维度全镜像
  • 纯原生:直接复用 FreeBSD 15.0 上游能力——sosend(9) 手册原文就写着”Data may be sent as an mbuf chain via top, avoiding a data copy”,f-stack 只是补了一个把 top 贯通进 sosend 的入口
  • 拆除魔改:FSTACK_ZC_MAGIC 哨兵 + m_uiotombuf 魔改共 17 处触点全部拆除,m_uiotombuf 回归 vanilla 15.0 版本,内核 patch 从”5 处魔改”变成”只新增 1 个函数”

2. 本功能的主要适用场景

2.1 大块数据收发场景

这是零拷贝最实在的收益场景。M2 实测已经证明:小包 echo(256B 请求/438B 应答)下 ZC 与普通 read/write 持平——小请求的 uiomove 拷贝开销相对 TCP 处理 + syscall + 调度可以忽略,且 f-stack 用 UIO_SYSSPACE,uiomove 本就是同地址空间 memcpy,已经很廉价。真正的收益在 4KB/64KB/1MB 级 payload:大文件下载、大 body POST、块存储类业务。

2.2 代理转发场景(收到即转发)

转发类应用(代理、网关)收到数据后原样发出去,传统路径是 recv 拷进用户 buffer 再 send 拷回协议栈,两趟拷贝;ZC 路径下 recv 拿到 mbuf 链、send 直接投递 mbuf 链,全程零拷贝。这是 spec 里明确标注的优先收益场景。

2.3 与 FF_USE_PAGE_ARRAY 的关系(要说清)

FF_USE_PAGE_ARRAY 是 2020 年 PR #364 贡献的”协议栈 → DPDK”方向零拷贝(mmap + mlock + virt2phy 查找),与这篇讲的应用层零拷贝是两段不同的拷贝。它至今仍是实验性特性:issue 档案确认它在 i40e 下会静默丢包,官方不建议启用;Phase-2 实测还触发过 panic(F-A1,已修复为 soft drop)。三者开关完全独立,可任意组合。

2.4 不太适合的场景

  • 小包 echo 类负载:实测零收益,直接开还会多一层 API 复杂度
  • 依赖增量编译的快速迭代环境:FF_ZC_* 开关切换后必须 make clean,否则会踩 M2 的 http=000 坑(见 4.3)
  • 期待性能翻倍的:2022 年官方公众号文章就说过,性能提升”并不一定很明显,比如只有 2-3% 左右的提升”——零拷贝省的是 memcpy,不是协议栈本身的开销

3. 本功能的架构特征

3.1 三条发送路径对比(核心图)

新方案没有 magic、没有 uio_offset 透传、ff_write/ff_writev 不需要 opt-out。sosend 全段经过实测确认无任何 FSTACK_* 宏——这是 FreeBSD 上游原生能力。

3.2 接收方向对称结构

3.3 双向对称表(五维镜像)

维度ZC-recv(已落地)ZC-send(原生化后)
内核入口kern_zc_recvit → soreceive(mp0)kern_zc_sendit → sosend(top)
原生机制soreceive 的 mp0!=NULL 分支sosend 的 uio==NULL && top!=NULL 分支
长度来源uio->uio_residtop->m_pkthdr.len
绕过点uiomove 拷贝m_uiotombuf 分配+拷贝
用户态 APIff_zc_recv / ff_zc_mbuf_segment / ff_zc_recv_freeff_zc_send / ff_zc_mbuf_get / ff_zc_mbuf_write
ABI 增量+3 符号0 符号(只改实现)

两边共享同一个 struct ff_zc_mbuf(bsd_mbuf/bsd_mbuf_off/off/len 四字段),字段语义对称复用。

4. 具体做了哪些改造、遇到了哪些问题、怎么解决的

后续比较枯燥,不需要深入研究实现过程的可以跳过本节,直接看第 5 节怎么用。

4.1 早期方案回顾(2018~2022,最早期文档到现在版本的区别)

2018 年的基线(issue #407):接收方向网卡到 hugepage 零拷贝、但 ff_read() 有拷贝;发送方向”是拷贝的”。

2020 年 PR #364(@jinhao2)补了第一段:FF_USE_PAGE_ARRAY,协议栈 → DPDK 方向零拷贝。思路是初始化时 mmap 256MB(memsz_MB 可调)+ mlock 锁物理内存,维护虚拟地址→物理地址页表,协议栈 mbuf 数据地址落在池内时直接把物理地址填给 rte_mbuf 的 buf_addr/buf_physaddr,不拷贝;用长度等于 NIC tx_queue_length 的循环队列保证释放安全。这个方案对应用透明,但对 ixgbe 有效、i40e 下静默丢包,至今官方定位仍是”从未正式启用的实验特性”。

2022-04-15(commit e12886c/021aaded)补了第二段:FF_ZC_SEND,应用 → 协议栈方向零拷贝。ff_zc_mbuf_get() 用 m_getm2 分配 mbuf 链当应用缓存、ff_zc_mbuf_write() 直接写进 mbuf、ff_write(fd, zc_buf.bsd_mbuf, len) 时在 m_uiotombuf 里直接采用传入的 mbuf 链跳过拷贝。配套公众号文章《F-STACK发送零拷贝介绍》如实说明:预期性能提升约 2-3%,”需要结合实际场景测试”。

4.2 M8 重新点亮:FSTACK_ZC_MAGIC 哨兵方案(2026-06-08)

FreeBSD 13→15 升级的 Phase-2 把 FF_ZC_SEND 在 15.0 基线上重新点亮(commit add33a04a)。13.0 时代的直传方式在 15.0 下有了变化,落地成了哨兵方案:ff_zc_send 把 mbuf 链伪装成 char* 塞进 iov_base,同时往 uio->uio_offset 注入魔数 0xF8AC2C00F8AC2C00;dofilewrite 里加守护(非魔数才覆写 offset);m_uiotombuf 里加消费点(检测到魔数就把 iov_base 重新解释为 mbuf 链)。内核 5 处魔改,普通 ff_write/ff_writev 必须显式 uio_offset=0 opt-out 防止撞上魔数。

这个方案从落地第一天就带着三个包袱:M8 阶段曾因 opt-out 缺失直接 GPF(ff_syscall_wrapper.c:1146 的 RCA);issue #712(2022-11-01 至今 OPEN)大数据量发送 crash/hang 无解;每次 FreeBSD 升级都要重新审计 m_uiotombuf 周边约 120 行。

4.3 问题一:增量编译陷阱,http=000(M2 教训)

2026-06-11 做 ZC-recv 的 M2 实测时,首轮 curl 全部 http=000,一度误判为环境问题。实际调试定位到真凶:先做了无 flag 的 baseline build,再 FF_ZC_SEND=1 make,uipc_mbuf.c 没改过、它的 .o 比 .c 新,make 按时间戳跳过重编译——m_uiotombuf 里的 MAGIC 消费 hook 缺失(objdump 验证 magic 不在 uipc_mbuf.o 里)。后果:ff_zc_send 设了魔数但内核 hook 不识别,mbuf 链指针被当 char buffer 处理,应答崩坏。删 stale .o 重编译后 http=200 全通。

【注1】这个坑从此进了强制规约:变更 FF_ZC_* 等编译开关后必须 make clean 或删除受影响 .o,make 基于时间戳不感知 CFLAGS 变化。这也是哨兵方案的固有脆弱性——魔改文件越多,stale .o 的组合爆炸面越大。

4.4 ZC-recv 落地与调研转向(2026-06-11)

同一天 ZC-recv 落地(commit b87f5f0d2,M0+M1):新增 kern_zc_recvit 透传 soreceive 的原生 mp0 出参,用户态新增 ff_zc_recv/ff_zc_mbuf_segment/ff_zc_recv_free 三个 API。它只新增不魔改,soreceive 核心零改动。M2 实测(单核 A/B 三档 wrk):T1 −1.1%、T2 ≈ +3%、T3 −1.0%,小包 echo 下与 ff_read 持平(噪声内)——诚实的结论是收益在大 payload 场景,echo 负载体现不出来。

更重要的是,做 ZC-recv 的同时顺手调研了发送侧:FreeBSD 15.0 的 sosend() 签名里一直带着 top(mbuf 链)参数,uio==NULL && top!=NULL 时直接投递 mbuf 链、跳过 m_uiotombuf。调研实测确认 sosend 全段无任何 FSTACK_* 宏(git blame 显示 f-stack 的 delta 不在那里)——上游原生能力一直在,f-stack 的 MAGIC 方案是把 mbuf 伪装成 uio 绕行,属于”非必要重新发明”。缺的只是一个把 top 从用户态贯通进 sosend 的入口(收侧 mp0 被写死 NULL 完全同构,kern_zc_recvit 已经示范了怎么做)。

4.5 原生化重构:kern_zc_sendit(2026-06-12)

方案定了以后一天落地(commit b6ce5884c):新增 kern_zc_sendit(td, s, top, flags) 直调 sosend(so, NULL, NULL, top, NULL, flags, td),同时把魔改全部拆除——17 处触点里,mbuf.h 的 MAGIC 宏、m_uiotombuf 的 ZC 分支、dofilewrite 的守护、ff_write/ff_writev 的 opt-out 共 5 处 DELETE,m_uiotombuf 回归 vanilla 15.0 版本;ff_zc_send 重写为调用 kern_zc_sendit;ff_zc_mbuf_get/write 修复 M_PKTHDR 缺口。

M_PKTHDR 缺口是这个方案的技术前提,值得展开:sosend 的 uio==NULL 分支用 top->m_pkthdr.len 作为 resid(数据长度),而旧的 ff_zc_mbuf_get 用 m_getm2(..., flags=0) 分配的链头不带 M_PKTHDR,ff_zc_mbuf_write 里维护 pkthdr.len 的代码被注释掉了——直接走原生路径会 resid=0 什么都发不出去。修复是两行的事:分配时加 M_PKTHDR、写入时累加 pkthdr.len,但这两行是”原生能力可用”和”静默不发数据”的分水岭。

【注2】ABI 不变性是这次重构的合同:ff_zc_send/ff_zc_mbuf_get/ff_zc_mbuf_write 签名零修改、example/main_zc.c 调用序列零修改(G4 验收条款就是 diff 它 0 行变化)、FF_ZC_SEND 开关名保留但含义从”魔改启用”切换为”原生路径启用”、导出符号不增不减。对已部署项目唯一要求是 make clean 后重编译。

4.6 行为变化(对用户可感知的)

项旧(魔改)新(原生)
大数据(>SO_SNDBUF)发送crash/hang(issue #712)阻塞或非阻塞返 EWOULDBLOCK,调用方自分片
UDP 单包 > MTU行为不定明确返回 EMSGSIZE
普通 ff_write 与 ZC 共存需 opt-out 防误触,曾 GPF无需 opt-out
FreeBSD 升级维护重审计 m_uiotombuf 约 120 行只校验 sosend 签名是否变化

4.7 性能诚实边界

  • 2022 年官方文章:发送零拷贝预期提升约 2-3%,需结合场景实测
  • 2026-06 ZC-recv 单核实测:小包 echo 三档压测与普通 read 持平(噪声内),收益预期在大块数据/代理转发场景
  • ZC-send 原生化性能目标(spec 38):三档 wrk 下 vs baseline 和 vs 旧魔改均 Δ ≤ ±3%(噪声内)——原生化是”等价替换”不是”性能优化”,收益是架构债清零
  • 大 payload(4KB/64KB/1MB)专测是后续工作(spec 已列 P2-P4 场景,旧方案在 P3/P4 有 crash 风险,新方案必须数据完整)

5. 如何使用、如何配置、使用效果

5.1 编译开关(三个,完全独立可组合)

# lib/Makefile
#FF_ZC_SEND=1     # 发送零拷贝(原生 kern_zc_sendit 路径)
#FF_ZC_RECV=1     # 接收零拷贝(kern_zc_recvit 路径)
#FF_USE_PAGE_ARRAY=1 # 协议栈→DPDK 零拷贝(实验性,不建议生产启用)

切换开关后必须 make clean(4.3 的坑)。

5.2 发送零拷贝 API 用法

struct ff_zc_mbuf zc_buf;
​
ff_zc_mbuf_get(&zc_buf, buf_len);          // 申请 mbuf 链缓存(含 M_PKTHDR)
ff_zc_mbuf_write(&zc_buf, data, len);      // 直接写入 mbuf 链,可多次调用
ff_zc_send(fd, zc_buf.bsd_mbuf, buf_len);  // 投递 mbuf 链,跳过拷贝
​
// send 成功后 bsd_mbuf 已被协议栈接管,不得再访问;
// 复用 zc_buf 必须重新 ff_zc_mbuf_get

调用方代码审查要点(spec 39 迁移指南):单次 send 数据量别超过 SO_SNDBUF(否则 EWOULDBLOCK,需自分片);UDP 单包别超 MTU(否则 EMSGSIZE);send 后立刻丢弃 bsd_mbuf 指针(否则 UAF);nbytes 参数要 ≥ 实际 write 累计量(实发长度 = pkthdr.len)。

5.3 接收零拷贝 API 用法

struct ff_zc_mbuf zm;
ssize_t n = ff_zc_recv(fd, &zm, nbytes);
// 逐段读:ff_zc_mbuf_segment(&zm, &seg, &len)
ff_zc_recv_free(&zm);   // 用后必释,否则 mbuf 泄漏

5.4 使用效果

实测数据(2026-06-11,单核 lcore4,wrk 三档,A=普通 read / B=ZC-recv):

档位A req/sB req/sΔ
T1 (-t2 -c10)22,36322,115−1.1%
T2 (-t4 -c100)~31.1k~32.1k持平(噪声内)
T3 (-t8 -c500)28,61528,317−1.0%

小包 echo 下零拷贝没有可测收益——这是设计预期,不是失败。收益场景是大 payload 和转发。

5.5 对使用者的建议

  • 业务是大块数据/转发:开 FF_ZC_SEND + FF_ZC_RECV,用 ZC API 改造收发路径
  • 业务是小包 RPC/echo:别折腾,普通 ff_read/ff_write 就好,实测证明无差别
  • FF_USE_PAGE_ARRAY:官方定位实验性、i40e 丢包、Phase-2 实测触发过 panic(已修复为 soft drop)
  • 升级到新版本:make clean 后重编译,example/main_zc.c 不用改

6. 延伸阅读:

  • 用户态零拷贝栈 spec 总览:docs/zc_stack_user_spec/zh_cn/30-spec-overview.md
  • 现状测绘与拆除清单(17 处触点):docs/zc_stack_user_spec/zh_cn/31-current-state-and-removal.md
  • 对称架构设计(kern_zc_sendit ↔ kern_zc_recvit):docs/zc_stack_user_spec/zh_cn/32-native-arch-design.md
  • 原生 sosend(top) 调研:docs/zc_stack_user_spec/zh_cn/22-native-zc-send-research.md
  • 已部署项目迁移指南:docs/zc_stack_user_spec/zh_cn/39-migration-guide.md
  • ZC-recv M2 实测报告:docs/zc_stack_user_spec/zh_cn/21-m2-test-report.md
  • 初版发送零拷贝介绍(2022):https://mp.weixin.qq.com/s/j_b7pVOoFa6sqaWQD6eBpw
  • 相关 issue:#364(PR,协议栈→DPDK 零拷贝)、#407(接收侧零拷贝确认)、#712(大包发送 crash)、#467(零拷贝 API 讨论)
  • 三层架构文档:docs/zh_cn/01-LAYER1-ARCHITECTURE.md
  • 知识图谱:docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md

F-Stack 2.0 前瞻7 :MTU 修改和jumbo frame支持

1. 本功能主要作用和特点

F-Stack 的 DPDK 接管网卡之前只能锁死在 MTU 1500,这次把「减小 MTU 已可用、增大 MTU(jumbo frame)完全不可用」的现状,改造成「启动时按配置设置 1500~9000 任意 MTU、运行时通过标准 SIOCSIFMTU ioctl 动态修改、协议栈与 DPDK 硬件联动」的完整支持。本功能在dev分支和1.21分支都已经支持。

改造前的基线是这么个情况。2026-07 中旬做了一次结论性调研(见 docs/mtu_change_spec/zh_cn/00~15),三方证据交叉得出的结论是「部分支持」:

  • 减小 MTU(≤1500):开箱可用。ff_ioctl(SIOCSIFMTU) 走 FreeBSD ether_ioctl,对 ≤ETHERMTU 的值直接写入 if_mtu 即生效
  • 增大 MTU(>1500,jumbo):双重阻断。协议栈层 ether_ioctl 对 ifr_mtu > ETHERMTU(1500) 硬编码返回 EINVAL;DPDK 硬件层完全没有 MTU 接线——没有 rte_eth_dev_set_mtu 调用、mbuf 池固定 RTE_MBUF_DEFAULT_BUF_SIZE(2048 可用 dataroom)、rxmode 无 jumbo/scatter 配置

这个结论和 F-Stack 官方 issue #239/#490/#720 完全一致:jumbo 支持 issue 挂了很久一直是 OPEN,维护者明确回复「mtu 不能超过 1500」。所以这次任务不是打补丁,是把协议栈、DPDK 硬件层、配置体系三个断层全部接上。几个关键词:

  • 软硬联动:一个 MTU 三个视图——协议栈 if_mtu、DPDK 端口 MTU、mbuf 池承载能力,三者必须一致,任一不一致启动即失败
  • 多进程分工:DPDK 物理端口是共享状态,primary 唯一负责硬件(设软+硬),secondary 只设本进程软件 MTU,进程间零 IPC
  • 零回归承诺:新增 mtu_enable 总开关,不启用时旧配置的 MTU 1500、2176B mbuf、ioctl 行为完全不变

2. 本功能的主要适用场景

2.1 大包吞吐优化场景

这是最直接的动机。issue #1033 的历史结论里就有:大包场景性能下降的根本原因部分与 MTU 1500 导致 IP 分片有关。启用巨帧后 8500 字节的包不再被拆成 6 片,分片重组开销归零。适合内部高速网络、大数据块传输、存储网络这类能统一链路 MTU 的环境。

2.2 需要灵活调小 MTU 的隧道/叠加网络场景

减小 MTU 本来就能用,但这次改造后运行时动态修改统一走标准 SIOCSIFMTU 语义,ff_ifconfig f-stack-0 mtu <N> 一条命令改完协议栈+硬件。VXLAN/GRE/隧道叠加场景常需要把 MTU 压到 1400 级别给隧道头留空间。

2.3 多进程生产部署的巨帧统一

1 primary + N secondary 的经典部署下,每个进程各自设置一次 MTU 即可保持一致(primary 管硬件、secondary 管自己的软件视图),无需任何跨进程协调机制。

2.4 不太适合的场景

  • 链路对端不支持 jumbo:virtio 等 PMD 的 jumbo 能力有限,且需要底层网卡/vSwitch/对端统一支持,否则大帧中途被丢或分片——代码支持了,链路不支持也是白搭
  • KNI 与内核共存做深度 MTU 联动的场景:KNI veth 接口的 MTU 联动不在本期范围(只验证了互斥解除后各自独立工作)
  • 想要跨进程事务级 MTU 一致性:本期明确不做 IPC 协调,未设置的进程软件 MTU 保持旧值,这是已知使用约束

3. 本功能的架构特征

3.1 改造前的三层断层

调研阶段画出的根因图,这是理解所有改造的起点:

3.2 改造后的 SIOCSIFMTU 路径(按进程角色分流)

关键设计约束:ff_veth.c 不 include rte_ethdev.h,所有硬件操作走 ff_dpdk_if.h 的不透明接口 ff_dpdk_if_get_mtu/set_mtu/get_mtu_capability,DPDK 负 errno 经 ff_dpdk_errno_to_bsd() 统一转 BSD 正 errno。

3.3 mbuf 两种承载模式

large 模式 data_room_size = align(HEADROOM + max_mtu + L2_overhead),超过 UINT16_MAX 必须确定性失败,所以 large 模式 max_mtu=65535 不可用;scatter 模式保持标准 mbuf,可配到 65535 但必须满足 PMD 能力。

4. 具体做了哪些改造、遇到了哪些问题、怎么解决的

后续比较枯燥,不需要深入研究实现过程的可以跳过本节,直接看第 5 节怎么用。

4.1 调研结论直接转化为需求决策(D-MTU-01~06)

调研收尾时定了 6 条决策,全部贯穿实现:

ID决策
D-MTU-01mbuf 同时实现 large 与 scatter 两种模式,配置选择
D-MTU-02max_mtu 可配置,启用功能且缺省时为 9000
D-MTU-05不修改 freebsd/ 树,在 lib/ff_veth.c 拦截 SIOCSIFMTU 绕过 ether_ioctl 的 1500 硬校验
D-MTU-06mtu_enable=0 时行为与旧版本完全一致(1500 MTU、2176B mbuf、ioctl 不变)

其中 D-MTU-05 最值得说一句:ether_ioctl 的 ETHERMTU 硬校验是 FreeBSD 通用语义,f-stack 的选择不是在裁剪栈里改它,而是在 ff_veth 驱动层拦截 SIOCSIFMTU 自己处理。这样 freebsd/ 子树零改动,升级 FreeBSD 基线时不会引入新的 patch 维护负担。

4.2 代码实施(M1~M5,2026-07-21)

里程碑commit内容
M197452db34配置解析与校验:enum ff_mbuf_mode、ff_port_cfg.mtu、dpdk.{mtu_enable,max_mtu,mbuf_mode};严格 strtoul 解析;跨字段校验;8 个单测 +7 fixtures
M2eec178902DPDK 层:ff_mtu_data_room_size() 对齐计算、large 模式 data_room 尺寸、rxmode.mtu + PMD 能力检查 + scatter offload、set/get_mtu 回读
M30849f9f3aff_veth 集成:不透明 MTU API、DPDK errno→BSD errno 转换、SIOCSIFMTU 拦截(primary 软+硬 / secondary 仅软)、启动时从硬件读初始 if_mtu
M4ef20b1abfEBUSY 状态机:-EBUSY → stop/set_mtu/get_mtu/start;失败回滚 restore 旧 MTU + 重启端口
M5314df8a0a集成测试 + 架构文档更新
收尾0f8f6991e / 332abf997 / 4c30d118fmagic number 宏化、SIOCSIFMTU handler fall-through 去重、bool→uint8_t 干净编译修复

配置解析有一条铁律:禁止 atoi()。新增 ff_parse_u16/ff_parse_mbuf_mode 严格解析(strtoul + errno/endptr/范围检查),任一配置非法在启动阶段明确报错终止,绝不静默回退到 1500。mtu_enable=0 且配置 mtu>1500 直接解析失败并提示先启用功能。

4.3 问题一:IPv6 巨帧回包被按 1448 分片(最大的坑)

功能上线物理机验证时,出现一个诡异现象:IPv4 巨帧双向正常,IPv6 收 8500 字节 ping 正常、但回包被拆成 6 个 1448 字节分片。

抓包反推:6 片 = 1448×5 + 1268 = 8508,对应分片公式 len = (mtu - 40 - 8) & ~7,反解出来 mtu=1500——协议栈明明 if_mtu 已经设成 9000,IPv6 分片用的 MTU 却还是 1500。

根因链拉了 12 个 file:line(完整分析见 15-IPv6巨帧分片异常分析.md),总结下来就一句:

真正的坑:if_setmtu() 只写 ifp->if_mtu,不含协议族通知。标准 FreeBSD 内核路径 ifhwioctl → if_setmtu → if_notifymtu → nd6_setmtu + rt_updatemtu 会同步 IPv6 的 ndi->maxmtu 和路由 MTU,而 f-stack 为绕过 ether_ioctl 的 1500 硬校验走了”直接 if_setmtu”的捷径,把 if_notifymtu 这一步丢了。IPv4 输出路径直接用 ifp->if_mtu 判断分片所以没事,IPv6 走 IN6_LINKMTU 就中招。

修复(commit 0f25ac495,+163/-1):ff_veth.c 启动初始化和运行时 SIOCSIFMTU 两条路径的 if_setmtu 之后都补调 if_notifymtu(ifp),与标准内核语义对齐。物理机复测:ping6 -M do -s 8500 回包不再分片,IPv6 巨帧收发正常。

【注1】这个坑有普适价值:FreeBSD 里「改 if_mtu」和「通知协议族」是两件事,任何绕过标准 ifhwioctl 路径直接调 if_setmtu 的代码(驱动初始化、ioctl 拦截)都必须自己补 if_notifymtu,否则 IPv6 的 nd_ifinfo 和路由 nh_mtu 会永远停在旧值。顺带一提,当时还排查了次因 RA 通告压低 linkmtu 的可能性,运行时验证确认 linkmtu=0(未受 RA 影响),主因修复后即闭环。

4.4 问题二:KNI/MTU 互斥,先禁止后解除

需求规格 R-MTU-009 初版约定:检测到 mtu_enable=1 与 KNI/内核共存同时启用时,配置阶段以 EOPNOTSUPP 拒绝。理由是双栈 MTU 不一致风险。

物理机实测后推翻了这个保守决定(commit 989f1d2da,+1/-6):mtu_enable=1 与 kni.enable=1 共存完全正常——veth0 MTU=1500 时内核栈正常拆包收发,ifconfig veth0 mtu 9000 时 KNI 通路巨帧收发同样正常。互斥是个不必要的限制,解除后 KNI 用户也能用巨帧。

【注2】这个来回说明 spec 阶段的保守约束值得在实测后重新审视:EOPNOTSUPP 拒绝是最安全的写法,但如果有实测证据表明共存无问题,解除互斥比维持”纸面风险”对用户更友好。

4.5 其他工程细节

  • EBUSY 状态机:rte_eth_dev_set_mtu 对运行中端口可能返回 -EBUSY,M4 实现 primary 进程内的 stop/set_mtu/get_mtu/start,失败回滚 restore 旧 MTU + 重启端口;secondary 直接返回 0 不做硬件操作
  • 多进程一致性靠使用约定:进程间零 IPC、零事务、零消息环,每个进程各自触发一次 SIOCSIFMTU;未设置的进程软件 MTU 保持旧值(已知约束,spec 明示)
  • 门禁:-Werror 编译、freebsd/ 树 git diff 验证零改动、ff_veth.c 无 rte_eth* 引用、无 atoi、无 IPC 残留、mtu_enable=0 全路径零回归——六项全 PASS

5. 如何使用、如何配置、使用效果

5.1 配置

[dpdk]
mtu_enable=1         # 功能总开关,缺省 0(不启用时行为与旧版完全一致)
max_mtu=9000         # 运行时 MTU 上限 + large 模式 pool 预分配依据,缺省 9000
mbuf_mode=large       # large:单 mbuf 承载巨帧;scatter:标准 mbuf 多段链
​
[port0]
mtu=9000             # 端口启动时的协议栈+硬件 MTU,缺省 1500

约束速查:

  • mtu_enable=0 且配置 mtu>1500:解析失败,提示先启用 MTU 功能
  • large 模式 max_mtu 上限受 data_room_size <= UINT16_MAX 约束(65535 不可用);scatter 模式可到 65535 但受 PMD 能力限制
  • 非法配置启动即报错,不会静默回退

5.2 运行时修改

# primary 进程:软+硬件一起改(含 EBUSY→stop/set/start)
ff_ifconfig -p 0 f-stack-0 mtu 9000
​
# 查询
ff_ifconfig -p 0 f-stack-0
f-stack-0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 9000

多进程约定:primary 的修改同时更新软/硬件;每个 secondary 需各自触发一次(只改本进程软件视图)。这是使用约定不是 bug,跨进程 IPC 协调是范围外的后续里程碑。

5.3 使用效果(物理机实测)

测试项结果
IPv4 巨帧ping -M do -s 8972 双向 8500 字节收发正常,MTU=9000 不分片 ✅
IPv6 巨帧ping6 -M do -s 8500 收发正常(修复前回包被按 1448 分 6 片,补 if_notifymtu 后不分片)✅
KNI/MTU 共存mtu_enable=1 + kni.enable=1 共存正常;veth0 MTU 1500/9000 收发均正常 ✅
单元测试59 通过(含 8 个新增 MTU 测试),12 个测试二进制全 PASS ✅
门禁-Werror / freebsd 树零改动 / 无 rte_eth 泄漏 / 无 atoi / 无 IPC 残留 / 零回归 全 PASS ✅

单测覆盖:配置解析 8 个新用例(UT-CFG-01..08)+ 7 个 fixtures,重点打配置校验边界(缺省值、跨字段冲突、非法字符串、溢出)。

5.4 对使用者的建议

  • 要开巨帧先确认链路:物理网卡、vSwitch、对端全部支持 jumbo 才有意义,链路不支持时大帧中途被丢比 1500 更糟
  • 内存敏感场景选 scatter:large 模式 mbuf 池按 max_mtu 预分配,内存开销显著;scatter 省内存但要求 PMD 支持 RX_SCATTER + TX_MULTI_SEGS
  • 只想调小 MTU:不用开 mtu_enable,直接 ff_ifconfig ... mtu 1400 就是

6. 延伸阅读:

  • 调研结论总览与三方证据:docs/mtu_change_spec/zh_cn/00-调研结论总览.md
  • IPv6 巨帧分片异常根因链(12 个 file:line):docs/mtu_change_spec/zh_cn/15-IPv6巨帧分片异常分析.md
  • 实施报告(M0~M5 + Post-M5):docs/mtu_change_spec/zh_cn/14-实施报告.md
  • 接口与配置设计:docs/mtu_change_spec/zh_cn/08-接口与配置设计.md
  • 相关 issue:#239(set MTU in example)、#490(Why MTU MAX CONF is 1500)、#720(Enabling jumbo frames)、#1033(大包性能与 MTU)
  • 三层架构文档:docs/zh_cn/01-LAYER1-ARCHITECTURE.md
  • 知识图谱:docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md

F-Stack 2.0 前瞻 6 : LD_PRELOAD 无锁 Ring IPC 改造

1. 本功能主要作用和特点

F-Stack 的 LD_PRELOAD 模块(libff_syscall.so)从 2023-05 初版提交到现在演进三年了,这篇讲的是 2026 年上半年给它做的无锁环形队列改造(FF_USE_RING_IPC),顺带把这三年从初版到现在的优化和修改一起捋一遍。

F-Stack 的标准接入方式是改代码:应用调 ff_* 前缀 API + ff_run() 主循环,代码侵入大。为了降低存量应用的迁移门槛,2023 年社区在 dev 分支提交了 adapter/syscall 目录(commit 8f5f1dfbb,2023-05-03),提供 libff_syscall.so 动态库,通过 LD_PRELOAD 劫持 Linux socket 系统调用转发给 fstack 实例进程处理,应用零代码修改即可接入。初版发布时官方定位很克制:「目前功能尚不完善,仅供测试使用」。

三年后的今天,这个模块已经支持 fork、accept4、_FORTIFY_SOURCE 包装函数、epoll polling 模式,并且把 APP 与 fstack 之间的 IPC 从信号量三层同步演进到了 DPDK 无锁 rte_ring 双环 SPSC。几个关键点:

  • 免改代码接入:socket/bind/connect/read/write/epoll/kevent/select 全部劫持,存量应用(Nginx、netperf 等)零代码修改直接跑在 F-Stack 上
  • 双环 SPSC:FF_USE_RING_IPC 用 rte_ring 单生产者单消费者模式替换 sem_wait/sem_post,消灭全局锁和 O(n) 遍历
  • 诚实收敛:ring 改造经过 v1~v3.7 共七轮实测迭代,最终结论不是”ring 全面胜利”,而是”ring 在 LD_PRELOAD + FF_MULTI_SC 场景下没有性能优势,生产推荐 sem”——这是这篇最想说的:优化要有对照、有证伪、敢于否定自己

2. 本功能的主要适用场景

2.1 存量应用免改代码接入 F-Stack

这是 LD_PRELOAD 模块的根本定位。历史 issue 档案里,官方对”现有多线程/多进程/Go/netperf 类应用怎么移植”的回答,推荐方案第一条几乎都是它:不用改源码,先起 fstack 实例进程,再 LD_PRELOAD 拉起应用。适配的接口面包括 socket 全家桶、epoll 全家桶、kqueue/kevent、select、fork,以及 glibc _FORTIFY_SOURCE 编译的 recv_chk/read_chk/__recvfrom_chk 包装函数。

2.2 Nginx 免改代码集成

Nginx 官方默认携带 1.16.1,经 LD_PRELOAD 接入无需改任何代码,需要同时开 FF_KERNEL_EVENT + FF_MULTI_SC 两个编译开关,配置上 multi_accept on、listen reuseport。不适用场景明确:多实例做客户端(反向代理)暂不支持,需要像社区 @铁皮大爷 那样在 hook 层加 RSS 对称 hash 选实例。

2.3 fork 多进程模型应用

2025-05 的 fork 支持(commit 4891fabf5,PR #887)之后,每个 fork 出来的进程都拥有独立的 FreeBSD struct thread,行为对标 Linux 内核 fork。配合 FF_MULTI_SC,Nginx master fork worker 的经典模型可以跑通。

2.4 不太适合的场景

  • 追求极致性能:每组应用实例要 2 个 CPU 核(1 个 fstack + 1 个 APP),CPU 几乎翻倍,官方原话”性价比不高”。要极致性能还是直接改代码用标准 F-Stack
  • 8 核以上的短连接场景:LD_PRELOAD 性能不如标准 F-Stack,受 APP 与 fstack 匹配度影响

3. 本功能的架构特征

3.1 整体拓扑

核心通信载体是 DPDK 大页共享内存上的 ff_so_context(简称 sc)。APP 把要执行的 socket 操作填进 sc,fstack 实例主循环处理并把结果写回。fstack 实例和 APP 建议跑在同 NUMA 节点的不同物理核上。

3.2 初版同步机制:信号量三层同步

初版(2023-05 ~ 2026-04 默认路径)sc 的同步靠三层机制:

层机制位置
状态机FF_SC_IDLE → FF_SC_REQ → FF_SC_REP → FF_SC_IDLEff_socket_ops.h
互斥锁rte_spinlock_t lock(sc 级 + zone 级全局锁)ff_socket_ops.h
信号量sem_t wait_sem(POSIX 跨进程信号量,futex 实现)ff_socket_ops.h

一次 socket 调用的往返流程:

3.3 新同步机制:双 Ring SPSC

2026-04-09 的 commit d2b71ac89 引入 FF_USE_RING_IPC:用 DPDK rte_ring(SPSC 模式)替代 sem。请求环 APP 生产 fstack 消费,响应环 fstack 生产 APP 消费:

状态语义的等价迁移:sc 在 req_ring 中 = 旧 FF_SC_REQ;在 rsp_ring 中 = 旧 FF_SC_REP;两个 ring 都不在 = 空闲。spinlock 和状态机转换不再参与同步逻辑。

3.4 为什么选 rte_ring

spec 文档(SPEC-002)里给了四条理由:F-Stack 本身大量使用 rte_ring(dispatch_ring、msg_ring),零学习成本;DPDK 原生 SPSC 无需自研无锁算法;Hugepage + rte_memzone 已验证跨进程可见;比 VPP 的 svm_msg_q 更简单。APP 与 fstack 实例 1:1 绑定,天然满足 SPSC 约束。

4. 具体做了哪些改造、遇到了哪些问题、怎么解决的

后续比较枯燥,不需要深入研究实现过程的可以跳过本节,直接看第 5 节怎么用。

4.1 初版遗留问题清单(ring 改造的动机)

初版的信号量机制在 spec 文档(SPEC-001)里被列了 5 条性能瓶颈:

  1. 内核态切换:sem_wait/sem_post 走 futex 系统调用,每次 100-200ns,epoll_wait 短超时场景被放大
  2. 超时精度差:sem_timedwait 用 CLOCK_REALTIME,受 NTP 调时影响,唤醒粒度受调度器 tick(1-4ms)限制;还有个竞态:超时返回后 fstack 才 sem_post,导致下次 sem_timedwait 立即返回读到过期结果
  3. O(n) 遍历:ff_handle_each_context 每轮遍历全部 32 个 sc 的 status 字段,绝大多数处于 IDLE 也要脏读
  4. alarm_event_sem 补偿机制脆弱:5 个示例程序里只有 1 个真正调用它,其余全注释掉了
  5. 模式分裂:信号量模式(低 CPU 高延迟)和轮询模式 FF_PRELOAD_POLLING_MODE(低延迟 100% CPU)通过编译宏二选一,运行时不可切换

4.2 三年中间演进(2023-08 ~ 2026-03,ring 之前的沉淀)

ring 不是凭空出现的,中间这三年社区补了不少坑,按时间排:

时间commit内容
2023-080ee517ed0修 Ubuntu 22.04 / kernel 5.19 / gcc 11.4 编译错误(#777),pre-C99 声明问题
2025-035de6ec6d2新增 epoll polling 模式,改善 RTT 敏感场景延迟
2025-033c21f2253修 ff_hook_recvfrom sh_fromlen 未初始化导致返回 -1(PR #872)
2025-03e3766b423accept4 支持 + ff_socket 支持 LINUX_SOCK_CLOEXEC/NONBLOCK
2025-05111816e29hook recv_chk/read_chk/_recvfrom_chk,FORTIFY_SOURCE 编译的应用可用
2025-054891fabf5完整 fork 支持(PR #887),每个进程独立 FreeBSD struct thread
2026-03b36ce995d修 ioctl 可变参数签名冲突编译错误(#942,PR #1048)

其中 ioctl 那个坑值得展开一句:glibc 里 ioctl(2) 是可变参数签名 int ioctl(int, unsigned long, …),而 ff_declare_syscalls.h 按定参注册,新工具链一编就炸。这类”glibc 签名 vs hook 声明”的不一致,是 LD_PRELOAD 这类方案的天生雷区,只能靠社区逐一踩坑修复。

4.3 ring 初版落地(2026-04)

2026-03-27 先产出了 4 篇 spec(需求/架构/接口/测试),审核通过后 2026-04-09 一次性提交 d2b71ac89:双 rte_ring SPSC、FF_USE_RING_IPC 编译开关、三种等待策略(busy-poll/yield-poll/eventfd,默认 yield-poll)、rte_rdtsc 替代 sem_timedwait 做微秒级超时、alarm_event_sem 改为 sentinel 入队。设计上严格走”编译宏切换 + 旧路径不动”,可渐进迁移。

4.4 ring 性能优化攻坚战(2026-05,七轮迭代的教训全集)

初版 ring 一上实测就出问题:短连接 QPS 9.1w,比 sem 基线 10.5w 低 12.4%。于是有了 v1~v3.7 的七轮迭代,这段过程是这篇最有价值的部分——不是”怎么修好的”,而是”怎么一步步证伪自己的错误假设”。

第一轮假设(H10/H11,v1):只读了 ff_socket_ops.c 的 ring 分支,下结论”sem 模式没有 30us drain 强制空轮询”。被证伪——对照 else 分支发现 sem 同样有 if (diff_tsc >= drain_tsc) break 循环。教训:单边代码分析,不对照另一分支就下结论。

第二轮假设(H15,v2):基于”sem 有 nb_handled 旁路而 ring 没有”,假设 ring 的 dequeue burst 跨核读 prod.tail 导致 LLC miss 暴增。perf stat 实测证伪:ring 的 LLC miss 反而比 sem 少 23%(6.8K/s 量级,不构成瓶颈)。教训:没先验证可证伪的物理量就把假设写进文档。

第三轮(v3.1):把”每 256 次 PAUSE 触发 sched_yield”按纯算术推成次因(H18),实测三组配置(yield 阈值 0xFF/0x1FFF/busy-poll)QPS 全部 9.2w,voluntary_ctxt_switches 增长约等于 0——spin_count 是局部变量,短连接场景单次 wait 根本到不了 256 次。教训:频率类估算先做 10 秒采样,别做 30 分钟的弯路。

第四轮(v3.2):perf top callgraph 锁定真因——ff_ring_process_requests Self 15.44%(每次主循环 spin 都走完整 dequeue burst 函数调用栈,即使 nb=0)+ ff_ring_send_response Self 3.33%(每次响应写 rsp_ring->prod.tail,触发 APP 核 cache invalidate)。同期还证伪了自己两条假设:H21 把 wrk 延迟差 +190us 当成单 syscall 开销(差 3 个数量级,真因是 CPU 占用挤压);H24 在 perf 已给明确证据后还发散”跨 NUMA”环境假设(同环境对照实验的唯一变量是 IPC 实现本身)。

方案 C 的失败(v3.3,H25):想用 atomic pending_count 旁路复刻 sem 的 nb_handled 快速跳过,结果 QPS 反劣化 4%(9.1w → 8.7w)。根因:APP 每次 syscall 多写 atomic 2-4 次,跨核 cache 乒乓比省掉的 dequeue 路径更贵。这个方案当天实施当天回退,代码随后彻底删除(f344d945f)。教训写进了文档的”对称评估表”方法论:任何优化必须同时列出”消除的物理量”和”新引入的物理量”,后者数量级必须明显小于前者才允许实施。

D2 的成功(v3.4,+9.7%):放弃响应 ring,fstack 处理完直接写 sc->completion(sc cache line 0 上本来就预留的字段),APP 端 spin completion 标志。写频率与 sem 基线完全一致,零新跨核字段。QPS 9.1w → 10.0w,ff_ring_send_response 从 perf top 消失。

D5(+1.3%):ff_ring_process_requests 改为先 rte_ring_empty() 内联快速空判断,空时不展开函数调用栈。Self 18.98% → 4.53%,但 QPS 只涨 1.3%——省下的 CPU 立刻被 main loop spin 填满。这是关键的洞察起点:fstack lcore 100% CPU + 30us drain 设计下,IPC 省下的 CPU 不会 1:1 变 QPS。

D6(+0.9%):ff_handle_socket_ops_ring 标 static inline,主循环直接函数名调用(去函数指针),与 sem 模式的 ff_handle_socket_ops 对齐。10.13w → 10.22w。

最终收敛:ring 单 worker QPS 10.22w,距 sem 的 10.5w 还差 2.7%。perf top 显示两边 main loop CPU 占比已完全一致(50.13% vs 50.41%),剩余差距来自 rte_ring_sc_dequeue_burst 的 acquire fence 和 ring 元数据维护——sem 的 dirty read 是 plain load 无 fence。这是 SPSC 架构的物理代价,单 worker 单 lcore 场景不可消除。

4.5 意外收获:sem 路径的启动饥饿修复

ring 攻坚过程中顺带修了一个老问题(8125beece,2026-05-25)。现象:sem 模式 + FF_MULTI_SC + config.ini 里 idle_sleep=0 时,多 worker 启动、还没任何流量,nginx 第二个 worker 卡死在 ff_attach_so_context 的 rte_spinlock_lock,gdb 堆栈和死锁一模一样。

但它是饥饿不是死锁:fstack secondary lcore 专核紧自旋,每 50-100us 释锁一次但释锁窗口 <<1us(idle_sleep=0 不让 CPU),nginx worker 是普通调度进程,cmpxchg 永远慢一拍,命中窗口概率趋近 0。修复只改了 13+/5- 行:进入 while 前快照 tmp(in-use sc 数),unlikely(!tmp) 时 unlock → pause → lock 让出窗口,有负载时零影响。最优补丁是”条件性让出”不是”无条件让出”。

【注1】这条饥饿现象同时说明:跨进程自旋锁竞争里,专核 DPDK lcore 永远胜过普通调度进程,这是物理机制不是代码 bug。能用配置(idle_sleep 改 1)消除的现象,根因 99% 是时间窗/调度类问题。

4.6 多核长连接实测:最终证伪 ring 的设计目标(v3.5~v3.7)

单 worker 收敛后转向多核对照。结果比预想残酷:

场景结论
多核短连接(1/2/4 核)ring ≡ sem(差距 -1.9%/0%/-0.3%,噪声范围)
多核长连接(1/2/4 核)ring 稳定劣于 sem 2.4%~4.5%,方向一致且大于噪声

长连接把 ring 的劣势放大而不是抵消:FF_MULTI_SC 下每个 fstack lcore 与 nginx worker 是 1:1 同 zone,sem 的 zone 锁是 fstack lcore 独占持有(cache line 一直 exclusive,近乎零成本),而 ring 每次 dequeue 都要 acquire fence 同步内存系统,高 QPS 下线性放大。当初”长连接下 sem 全程持锁压力升高、ring lock-free 优势将显现”的预测被彻底证伪。

v3.7 终版结论(2026-05-25 晚):

  1. 性能层面:ring 在任何已测场景下均无 net win,sem 仍是 LD_PRELOAD + FF_MULTI_SC 的最优配置
  2. 鲁棒性层面:ring 主循环 lock-free 的理论价值(启动饥饿免疫)已被 8125beece 在 sem 源头修复
  3. 架构层面:ring 代码保留(FF_USE_RING_IPC + D2/D5/D6 作为默认行为,c62d56a3b),作为”多线程同进程共享 sc”和”多进程间共享 sc(worker 数 > fstack 实例数)”的未来预留能力,当前 fork 多进程场景默认不启用

【注2】这个收敛结论值得反复看:一个设计良好、实现了完整 spec、优化了七轮的方案,最终被对照实验判定”不如老方案”。如果只有实现没有对照基线,这个结论永远不会出现。性能优化工作里,”对照组先于优化”比”数据先于理论”更值钱。

4.7 ring 之后还在继续(2026-05 ~ 08)

收敛之后模块没有停:README 全面刷新(bc7c380b2,2026-05-26);ff_hook_bind 增加 addrlen 边界检查(8762e05c1,2026-06-08,#1068);新增 UDP echo server 示例(2215dc4f2,2026-08-07,#1063)。

5. 如何使用、如何配置、使用效果

5.1 编译

export FF_PATH=/data/f-stack
export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig
​
cd /data/f-stack/adapter/syscall
# Nginx 无缝接入:同时开 FF_KERNEL_EVENT + FF_MULTI_SC
export FF_KERNEL_EVENT=1
export FF_MULTI_SC=1
# 可选:启用无锁 ring IPC(默认关闭,见 5.4 推荐配置)
#export FF_USE_RING_IPC=1
make clean; make all

编译产物:fstack(实例程序)、libff_syscall.so(劫持库)、5 个 helloworld 示例 + main_stack_udp 示例。FF_USE_RING_IPC 与 FF_PRELOAD_POLLING_MODE 互斥,Makefile 有编译期检查,同时定义直接报错。

5.2 运行

# 1. 先起 fstack 实例(lcore_mask 等配置同标准 F-Stack)
bash ./start.sh -b adapter/syscall/fstack
​
# 2. 再以 LD_PRELOAD 方式拉起应用
export LD_PRELOAD=/data/f-stack/adapter/syscall/libff_syscall.so
export FF_NB_FSTACK_INSTANCE=4        # 与 nginx worker 数 1:1
/usr/local/nginx/sbin/nginx

运行时环境变量:FF_NB_FSTACK_INSTANCE(实例数,默认 1)、FF_INITIAL_LCORE_ID(起始核,默认 0x4)、FF_PROC_ID(进程号,配合 CPU 绑定)。应用能自己绑核的(如 nginx worker_cpu_affinity)优先自己绑。

5.3 Nginx 配置要点(免改代码,但要调配置)

worker_processes  4;
worker_cpu_affinity 10000 100000 1000000 10000000;
​
events {
   worker_connections 1024;
   multi_accept on;      # F-Stack epoll 是 kqueue 封装,必须开
   use epoll;
}
​
http {
   access_log off;
   sendfile   off;      # 用 F-Stack 必须关
   keepalive_timeout 0;
​
   server {
       listen       80 reuseport;   # 每个 fd 对接不同 fstack 实例的 sc
       location / {
           return 200 "0123456789abcdefghijklmnopqrstuvwxyz";
      }
  }
}

注意 kern.maxfiles 不应大于 65536(保证 epoll fd 到内核 fd 的正确映射)。

5.4 使用效果与推荐配置

性能数据(E5-2670 v3 双路 / 10G X540,v3.7 终版实测):

场景SemRing差距
多核短连接 1/2/4 核(万 QPS)10.4 / 20.8 / 35.910.2 / 20.8 / 35.8≤2%(噪声)
多核长连接 1/2/4 核(万 QPS)33.3 / 65.9 / 130.531.8 / 64.3 / 127.0-2.4%~-4.5%
单 worker 优化轨迹—9.1w → 10.22w(+12.3%)距 sem 10.5w 差 2.7%

生产推荐配置(2026-05-25 终版):

# LD_PRELOAD + nginx 多 worker 推荐(默认,不开 ring)
make FF_KERNEL_EVENT=1 FF_MULTI_SC=1
# config.ini: idle_sleep = 0(8125beece 之后安全),pkt_tx_delay = 50(短连接)/ 100(长连接)

ring 路径仅在以下任一情况启用:单进程内多线程需共享 sc;多进程间共享 sc(worker 数 > fstack 实例数);或者接受 -2.4%~-4.5% 的性能损失换取主循环 lock-free 设计。

对 F-Stack 使用者的整体建议:LD_PRELOAD 是”少改代码”的折中,不是”性能最优”的路径。要追求极致还是标准 F-Stack 改代码接入;只想快速迁移存量应用,LD_PRELOAD + sem 默认配置就够用,ring 等未来多对多共享场景再启。

6. 延伸阅读:

  • ring 需求/架构/接口/测试 spec:docs/ld_preload_ring_spec/zh_cn/01~04
  • v3.7 终版性能分析(七轮迭代完整复盘):docs/ld_preload_ring_spec/zh_cn/ring_ipc_perf_offline_analysis.md
  • 模块官方文档(含 2023-05 ~ 2026-05 全部 feature updates):adapter/syscall/README.md
  • 初版公众号介绍(2023-05):https://mp.weixin.qq.com/s/hmxCEu0kOzp5X5TEB7r3OQ
  • 三层架构文档:docs/zh_cn/01-LAYER1-ARCHITECTURE.md
  • 知识图谱:docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md
  • 历史 issue 档案:docs/zh_cn/f-stack-issue-ana.md