F-Stack 2.0 前瞻 3 : F-Stack 用户态栈与内核栈自动双栈共存 – 一个 listen 同时服务 DPDK 网卡和本机回环

1. 这功能解决什么问题

F-Stack v2.0(预计 2026.10 正式 release)引入的内核栈共存能力,解决 F-Stack 最经典的一个使用痛点——DPDK 接管网卡后,本机 curl 自己监听的服务会被内核报 Connection refused。

F-Stack 把网卡绑定给 DPDK 后,这张网卡上的流量完全绕过 Linux 内核协议栈,直接进 F-Stack 用户态 FreeBSD 栈。后果是:你在 F-Stack 里 listen 了 80 端口,从同网络另一台机器 curl 你的网卡 IP 能通,但在本机 curl 127.0.0.1 或 curl 本机 IP 会报 Connection refused——因为本机请求走的是 Linux 内核栈,而内核对 F-Stack 的这个 80 端口一无所知。

这个现象在 issue 里被反复问,官方给的标准答案一直是”从别的机器测”或者”用 KNI 回灌”。issue #511/#585/#741/#849 全是同一件事。

我们做的就是把这件事从”手动 workaround”变成”默认行为”:同一个 socket、同一个 listen(80),同时跑在 F-Stack 用户态栈(DPDK 网卡,业务高速路径)和 Linux 内核栈(本机回环/管理面)上。远端 curl 网卡 IP 走 F-Stack,本机 curl 127.0.0.1 走内核,两条路同时通,而且两栈的事件在同一个 epoll/kqueue 事件循环里统一处理。

【注意】F-Stack 用户态栈始终在位、始终承担业务高速路径,内核栈只是”并行附加”的第二条栈,用来承接本机/管理面/客户端访问,绝不是在旁路或替代 F-Stack。

这里先说清楚它不是什么,免得理解偏差:

  • 不是把 socket 旁路到内核(早期有个错误实现就是这么干的,后文 4.1 详述,已回退)
  • 不是”整进程默认走内核栈”(那是反 F-Stack 的,明确不做)
  • 不是 KNI 报文回灌(那是另一套独立机制,跟本功能无关)
  • 不是连接迁移/透明代理(一个 TCP 连接物理上只存在于收到 SYN 的那一栈,无法”双栈”)

2. 主要适用场景

2.1 本机直访自己监听的服务

这是最直接的场景。开发和运维时,你总想在本机 curl 一下自己刚起的服务确认存活,而不是每次都要开另一台机器。开了双栈后,本机 curl 127.0.0.1:80 直接通。

2.2 服务端双向可达

同一个 listen(80):远端经 DPDK 网卡访问 <DPDK_NIC_IP>:80 走 F-Stack 高速路径,本机经 127.0.0.1:80 走内核栈,一个 socket 两用,不用为内核侧额外开一个 SOCK_KERNEL socket,也不用写任何 marker。

2.3 客户端连内核服务

作为客户端要连本机或外部的内核服务(比如本机的某个守护进程、管理面 API),用双栈 connect 或纯内核的 SOCK_KERNEL 都能做到,不用绕道。

2.4 需要内核可见性的监控场景

F-Stack 监听端口在 Linux 的 ss/netstat 里是看不到的(那是用户态栈)。开了双栈后,ss 能看到内核侧的 80 监听,监控和健康检查都方便了。issue #593/#594 里官方也拿 kernel_coexist 当”让端口内核可见”的答案。

2.5 不太适合的场景

  • 需要真·双工于两栈的单条连接(一条 fd 上的数据要么走 F-Stack 要么走内核,不可能同时两栈都收发)——默认双栈 connect 只是”两栈各建一条、F-Stack 主”,纯内核客户端请用 SOCK_KERNEL
  • 用 select/poll 做多路复用的场景(后文 4.6 详述,内核 fd 装不进 fd_set,不共存)
  • 想省掉 F-Stack 只留内核的场景(本功能不提供”整进程默认内核”,那是旁路)

3. 架构特征

3.1 一个 listen 双栈服务

3.2 fd 三态路由

这是整个功能的核心模型。一个 fd 进来,按数值区间和映射表分成三种形态:

三类 fd 空间互不冲突:F-Stack fd < 65536,内核 encode fd ≥ 0x40000000,host fd 受 RLIMIT_NOFILE。

【注意】关键设计是”热路径不查 map”。accept 出来的连接 fd 一定是单栈的——F-Stack 侧连接返回原始 fd,内核侧连接返回 encode fd。之后 recv/send 只做一次 ff_is_kernel_fd 判断就路由了,不查映射表,数据热路径零额外开销。双建/双驱动只发生在 socket/bind/listen/close 这些一次性操作上。

3.3 双层开关保证零回归

三重零回归保证:编译宏关、运行期开关关、SOCK_FSTACK marker,三者任一满足就退化成纯 F-Stack。

4. 改造工作与遇到的问题

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

4.1 走过的弯路:v3 纯内核旁路(已回退)

这个功能不是一步到位的,中间踩过一个方向性的大坑。

最早一版(v3,commit 0748eff94)把 ff_socket(SOCK_KERNEL) 直接接到纯宿主的 socket(),完全绕开 F-Stack。看似解决了”本机能通”,但这是根本性错误——它旁路了 F-Stack,违背了”F-Stack 始终在位”的铁律。性能基线报告也坐实了这个问题:v3 测的 A/B 两版本都是纯内核,根本没测”共存”。

回退后重新按正确范式做:应用跑在 F-Stack 上,per-fd 用 SOCK_KERNEL 附加走内核栈,两者同进程共存。

【注3】这个弯路值得记住:共存不是”要么 F-Stack 要么内核”的二选一,而是”F-Stack 为主、内核并行附加”。一旦做成旁路,性能基线、稳定性评估全都会失真。

4.2 v5:编译宏门控 + per-fd 二选一

回退后第一版正确实现是 v5(commit ba148589d),做了两件事:

  • 用编译宏 FF_KERNEL_COEXIST 把全部共存代码包裹起来,lib/Makefile 默认关,保证宏关时 libfstack.a 与原 F-Stack 逐字节一致(nm 比对共存符号为 0)
  • per-fd 二选一语义:带 SOCK_KERNEL 建内核 fd(编码成 ≥0x40000000 的高位 fd),否则走 F-Stack 原路径

这一版已经实测可用,但语义是”每个 fd 自己二选一”。想本机也通,得额外写一个 SOCK_KERNEL 的 socket,麻烦。

4.3 v6:默认升级为自动双栈

v6(commit 13b418191)把默认语义从”二选一”升级为”自动双栈”:

  • 默认(无 marker)的 ff_socket 同时建 F-Stack fd + 内核 host fd,登记 ff_native_fd_map[fstack_fd]=host_fd,返回 F-Stack 原始 fd
  • ff_bind/ff_listen/ff_close 对该双栈 fd 同时驱动两栈
  • marker 变成”单栈覆盖”:SOCK_KERNEL 仅内核、SOCK_FSTACK 仅 F-Stack

核心新增是那张 65536 项的无锁映射表 ff_native_fd_map(仿 adapter 的 fstack_kernel_fd_map),以及 accept 的”单栈归属”——双栈 listen fd accept 返回单栈连接 fd,F-Stack 侧连接返回原始 fd、内核侧连接返回 encode fd。

4.4 踩到的坑:头改动没 clean 重编导致 ABI 偏斜

性能基线实测时踩了个构建卫生的坑:给 struct ff_config 的 stack 子结构加了 int kernel_coexist 字段后,改变了它后面 log 子结构(含 log.f)的偏移。而 lib 的 Makefile 不跟踪头依赖,增量构建残留了混用新旧 ff_config.h 布局的 .o——ff_log.o 用旧偏移读 log.f,读到了新布局里别的非零字段,fclose 直接段错误。

修复很简单:清掉全部 .o 和 libfstack.a 全量重编。但这暴露了一个规律——对 ff_config.h 这类结构头的改动,必须 clean 后全量重编 lib,增量编译会掩盖 ABI 偏斜。

4.5 踩到的坑:kqueue 模型应用感知不到内核侧事件

R7 落地后自动双栈对 ff_epoll_* 是完整的,但直接用 ff_kqueue/ff_kevent 的应用(比如 example/main.c 用的就是 kqueue 模型)感知不到内核侧连接。实测:内核 TCP 完成了握手、GET 进了内核缓冲并被 ACK(抓包 ack 73 吻合),但应用永不被唤醒去 accept 内核 listen fd,本机 curl 127.0.0.1:80 返回 http_code 000(6 秒超时)。

根因:ff_kqueue/ff_kevent 完全没有 FF_KERNEL_COEXIST 路由,只把 F-Stack listen fd 注册进 F-Stack kqueue,双栈 listen 的内核侧 host fd 从未进入任何事件后端。

修复(R9,commit 03f244ac1):对称仿 ff_epoll 做 kqueue 共存——ff_kevent 的 changelist 里,ident 为内核 fd 或双栈 fd 的 EV_ADD/EV_DELETE 映射到 host epoll_ctl(EVFILT_READ↔EPOLLIN、EVFILT_WRITE↔EPOLLOUT),eventlist 先取内核就绪再合并 F-Stack 就绪。

4.6 踩到的坑:IPv6 双建端口冲突

还是 R9 发现的。开了 -DINET6 后,默认 ff_socket(AF_INET6) 双建,host 侧 IPv6 socket 绑 [::]:80 时,因为本机 net.ipv6.bindv6only=0,[::] 连带占用了 IPv4,跟同进程 host 侧的 0.0.0.0:80 冲突,实测 errno=98 EADDRINUSE,进程直接起不来。

修复:给 host 侧 IPv6 socket 设 IPV6_V6ONLY=1,让它只处理 IPv6、跟 host IPv4 同端口共存。

4.7 收口:补齐剩余接口的内核路由

R8(commit 55a84f313)补齐了 sendmsg/recvmsg/getpeername/getsockname/shutdown 的内核 fd 路由;R10(commit c6f5918b8 + 2422d12eb)又补齐了 readv/writev/ioctl/dup/dup2。其中有几个值得说的点:

  • ioctl 的 request 编码在 Linux 和 FreeBSD 不同源,内核 host fd 必须用原始 Linux request 直传 host libc,不能经 linux2freebsd_ioctl 翻译
  • dup2 一端内核 fd 一端 F-Stack fd 的”混栈”语义不成立,明确拒绝 errno=EINVAL,不臆造
  • select 不共存:encode 内核 fd(≥0x40000000)远超 fd_set 的 FD_SETSIZE(1024),装不进去,硬限制
  • poll 也不共存:合并复杂度高、回归风险大,保守降级为文档限制

内核 fd 想多路复用,用 ff_epoll_* 或 ff_kqueue 就行。

4.8 性能无回归

这是最关心的点,实测数据(v6 双栈口径,真机 wrk):

档位共存关 A0双栈开 A1Δ
T1 (-t2 -c10)28,21627,729−1.73%
T2 (-t4 -c100)202,805206,219+1.68%
T3 (-t8 -c500)120,702127,784+5.87%

吞吐差异全落在 trial 噪声内,p99 基本相等。逻辑上也说得通——双建成本只在 listen socket 建立时一次性付出,keep-alive 连接的数据热路径单栈、不查 map,所以 F-Stack 业务快路径无可测量回归。

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

5.1 编译

默认编译不含共存代码,要开启得编译时加宏:

cd f-stack/lib && make clean && make FF_KERNEL_COEXIST=1 -j$(nproc)

应用侧如果想用 marker(SOCK_KERNEL/SOCK_FSTACK),编译应用时也要加 -DFF_KERNEL_COEXIST(否则这两个宏不可见)。只用默认双栈则不需要。

5.2 配置

config.ini 的 [stack] 段加 kernel_coexist:

[stack]
# 0=禁用(纯 F-Stack),1=启用(默认 socket 自动双栈)
kernel_coexist = 1

注意:这个运行期开关只在编译宏开了 FF_KERNEL_COEXIST 时才生效。编译宏关,或者这里设 0,都是纯 F-Stack,零回归。

5.3 用法

默认双栈(什么都不用加):

int s = ff_socket(AF_INET, SOCK_STREAM, 0);   /* 双栈:F-Stack fd + 内核 host fd */
ff_bind(s, &addr80, sizeof(addr80));           /* 双驱动:两栈各 bind 80 */
ff_listen(s, backlog);                          /* 双驱动:两栈各 listen */
ff_epoll_ctl(ep, EPOLL_CTL_ADD, s, &ev);        /* 双注册:kqueue + 内核 epoll */

/* 远端 curl <DPDK_NIC_IP>:80 走 F-Stack,本机 curl 127.0.0.1:80 走内核,皆可达 */

需要单栈时用 marker 覆盖:

int konly = ff_socket(AF_INET, SOCK_STREAM | SOCK_KERNEL, 0);  /* 仅内核 */
int fonly = ff_socket(AF_INET, SOCK_STREAM | SOCK_FSTACK, 0);  /* 仅 F-Stack */

5.4 效果

功能正确性(真机实测,commit 13b418191):

  • 单 listen(80):本机 curl 127.0.0.1:80 = 200(内核侧),远端 ssh → <DPDK_NIC_IP>:80 = 200(F-Stack 侧)
  • ss 能看到内核侧 80 监听,ff_netstat 能看到 F-Stack 侧 80 监听

零回归(三层保证):

  • 编译宏关:libfstack.a 共存符号为 0,与原 F-Stack 逐字节一致
  • kernel_coexist=0:双建/双驱动运行期短路
  • SOCK_FSTACK marker:仅 F-Stack

性能(第 4.8 节):T1/T2/T3 三档吞吐差异全在噪声内,热路径不查 map。

5.5 已知限制

  • select 不支持内核 fd(encode fd 装不进 fd_set),poll 也不共存——内核 fd 用 epoll/kqueue
  • 单条连接不能真双工于两栈——默认双栈 connect 是”F-Stack 主 + 内核并发建连备援”,纯内核客户端用 SOCK_KERNEL
  • 可观测统计(ff_stack_get_stats)当前未实现,两栈 fd 数/事件数看不了
  • IPv6 依赖 host 侧 V6ONLY,本机 bindv6only 相关行为已在 R9 处理

6. 延伸阅读

  • 完整 spec:docs/kernel_event_support_spec/zh_cn/(00-10 + plan-r9/plan-r10)
  • 三层架构:docs/zh_cn/F-Stack_Architecture_Layer1_System_Overview.md
  • 知识图谱:docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md(2A 节 FF_KERNEL_COEXIST delta)
  • 相关 issue:#511/#585/#741/#849(本机 curl connection refused 场景)

F-Stack 2.0 前瞻 2 : F-Stack 主进程瘦身 primary_slim – 把 primary 单点故障从紧急事故降级为计划内维护

1. 这功能解决什么问题

primary_slim 是 F-Stack v2.0(预计 2026.10 正式 release)引入的一个运行开关,解决 DPDK 多进程模式下 primary 进程是单点的问题。

F-Stack 的标准多进程模式是 1 个 primary + N 个 secondary,每个进程独占一个 lcore 跑一份独立 FreeBSD 栈实例。secondary 崩了可以单独重启,不影响其他进程的队列和已建连接;但 primary 一旦异常退出,问题就大了——按照 DPDK 的设计,primary 是 IPC 唯一服务端、secondary 扩堆必须由 primary 代理、所有中断只在 primary 触发,所以传统认知是”primary 挂了整个进程组都得重启,所有连接受影响”。

这个诉求来自上游 issue #1078(https://github.com/F-Stack/f-stack/issues/1078),作者提的想法很直接:把 primary 独立出来,只做网卡队列设置、绑定、初始化这类控制面工作,收包和后续处理全部下移 secondary,这样 primary 就不那么容易异常,对连接的影响范围也小。

primary_slim 干的就是这件事:

【注意】primary_slim 把”primary 崩溃 → 必须立刻全组重启 + 约 1/N 连接立即中断”变成”primary 崩溃 → 数据面零损失、业务继续跑、崩掉的 secondary 可原地重启、集群进入控制面降级态、在计划内维护窗口择机全组重启”。

说白了,是把紧急故障转化为计划内维护,重启时机从被动变成可控。

这里先说清楚它不做什么,免得期待过高:

  • 不承诺”完全无需重启”——控制面降级态无法就地修复,最终还是要择机全组重启
  • 不承诺”省一个 CPU 核”——瘦身后的 primary 仍会空转占满一核,除非开空闲休眠
  • 不承诺 primary 不再是结构性单点——只是降低异常概率、缩小影响面
  • 不承诺 secondary 崩溃时连接不丢——每进程独立 FreeBSD 栈,没有连接迁移这回事

2. 主要适用场景

2.1 对 primary 单点稳定性敏感的生产部署

如果你的业务在多进程模式下跑,primary 一旦挂了要拖累全组,primary_slim 能让 primary 退出时数据面零损失。实测数据(后文详述):杀掉瘦身 primary 后,已建连接 12/12 零中断(26 轮探测无一次失败),新建连接 12/12 正常。

2.2 需要控制面与数据面解耦、缩小故障爆炸半径

多进程模式里,primary 名下如果持有 rx/tx 队列,它崩了这些队列就变成没人 poll 的孤儿队列,落到上面的流量全部黑洞化。primary_slim 让 primary 不持任何队列,数据面能力全部留在 secondary,primary 退出不带走任何数据面能力。

【注意】因为使用virtio创建的tap的限制,kni依然由主进程处理,primary挂了后kni会失效。

2.3 已经有外部守护/编排,想要分层恢复能力的场景

配合外部守护,可以做到:secondary 崩了原地重启 → primary 崩了进入降级态告警 → 择机计划内全组重启。这比”primary 一挂就紧急全组重启”从容得多。

2.4 不太适合的场景

单进程多线程模式(thread_mode=1)下没有独立 primary 进程,这个特性语义不存在,所以 primary_slim 与 thread_mode 是互斥的,配置校验会拦掉。如果只有 1 个进程(nb_procs < 2),也没有瘦身的意义,同样会被校验拦截。

3. 架构特征

3.1 改造前后对比

改之前(标准多进程):

改之后(primary_slim=1):

3.2 为什么队列能自然收缩、不产生孤儿队列

这是整个方案成立的关键,也是调研阶段最反直觉的发现。一开始担心”把 primary 摘出 lcore_list 会导致队列数减 1、queueid 错位、RSS reta 错位”,逐行坐实后发现根本不成立:

  • nb_queues 来源是该 port 的 lcore_list 长度,不是 nb_procs(lib/ff_dpdk_if.c:836)
  • queueid 是本进程 lcore 在 lcore_list 里的下标,与 proc_id 完全解耦(lib/ff_dpdk_if.c:487-491)
  • set_rss_table() 按同一个 nb_queues 重算 reta

所以把 primary 摘出 lcore_list 后,队列数、queueid、RSS reta、dispatch_ring 一致收缩,孤儿队列在代码层面自然消失。这正好是维护者(本人)提示词设想的”复用 lcore_list”路线的根本依据。

3.3 KNI 场景下的数据通路(最复杂的情况)

primary_slim=1 且 KNI 开启时,数据通路是三条线,涉及一个关键竞态修复(后文 4.3 详述)。最终稳定形态:

ICMP 请求入站:
client → 物理网卡(Port0) → secondary rx_burst(q0)
  → ff_kni_enqueue → KNI ring
  → primary kni_process_tx → rte_eth_tx_burst(Port1=virtio_user0)
  → veth0(tap) → kernel 协议栈 → 生成 ICMP reply

ICMP 回复出站(inject ring 路径):
kernel → veth0 → virtio_user0
  → primary kni_process_rx → enqueue 到 kni_inject_rp ring
  → owner secondary 用自己的 tx_queue 发出
  → 物理网卡 → client

核心分工:secondary 负责数据面收包入队 KNI ring,primary 独占 virtio_user vdev 的 TX/RX(因为 vdev 只有 primary 能合法操作),KNI 收到的回复包经 kni_inject_rp ring 转发给 owner secondary 用自己的队列发出。这样既绕开了”secondary 不能操作 virtio_user vdev”的 DPDK 硬约束,又消除了跨进程共享 TX queue 的竞态。

4. 改造工作与遇到的问题

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

4.1 PoC 阶段:3 行代码验证核心命题

正式立项前先做了个最小 PoC,只改 3 行代码(_poc_primary_slim.patch,2 hunk):

  • lib/ff_dpdk_if.c:508-510:rte_exit 条件追加 && rte_eal_process_type() != RTE_PROC_PRIMARY,解除 B1 阻塞点(”lcore %u has nothing to do”)
  • lib/ff_dpdk_if.c:535:mbuf 池 RX 项去掉对本进程队列数的依赖,解除 B3 阻塞点(池规模归零)

配置是 lcore_mask=3 + [port0] lcore_list=1(primary 的 lcore0 不在列表内)。

3 行代码 + 1 项配置就验证了核心命题:瘦身 primary 崩溃后已建连接 12/12 零中断、新建连接 12/12、性能无回归。这也直接说明了为什么这个改动值得做——代价极小,收益是质变。

【注意】PoC 用 rte_eal_process_type() != RTE_PROC_PRIMARY 无条件放宽只是为了最小化 diff,正式实现改成了 primary_slim && RTE_PROC_PRIMARY 有条件门控,避免影响默认路径。

4.2 正式实现:从”自动推导”到”显式开关 + 校验链”

PoC 能用但不严谨(靠”自动推导”而非显式开关),正式实现补了完整的开关和校验链。核心提交是 1c28aaa2d(M1)+ f7961b083(M2~M4)。

交付的东西:

  • [dpdk] primary_slim=0/1 显式开关,默认 0,关闭时零回归
  • 开启后 primary 在 init_lcore_conf 里 early return,不分配 rx/tx 队列,不进收发包主循环
  • 配套 primary_slim_idle_sleep(默认 1000us)解决 CPU 空转
  • 三道校验:primary lcore 不得在 lcore_list(V2)、与 thread_mode 互斥(V4)、nb_procs >= 2(V5)
  • ff_is_slim_primary() API

4.3 踩到的坑:CPU 空转

这是实测发现的第一个负面问题。瘦身后的 primary 不持队列、不收包,但 ps -o pid,pcpu 一看还是 99.8% 占满一核。原因是 main_loop 仍在紧密空转(跑 timer 和 msg_ring 处理),而 config.ini 的 idle_sleep=0 意味着无空闲休眠。

【注意】primary_slim 的收益是稳定性,不包含 CPU 节省。如果用户期望”瘦身 = 省一个核”,这个预期必须纠正。正式实现引入了 primary_slim_idle_sleep(默认 1000us),否则在核数紧张的部署里就是白烧一个核。

4.4 踩到的坑:KNI 场景下的跨进程 TX 队列竞态

这是 post-implementation 阶段发现的最棘手的问题,分两阶段修复。

第一阶段(f23f1a464):primary_slim=1 + KNI 开启时,KNI 的 runtime TX/RX 应该由 primary 执行(因为 virtio_user vdev 只能 primary 操作)。但这样一改,primary 的 kni_process_rx 里调 rte_eth_tx_burst(Port0, queue_id=0) 把 kernel 回复包注入物理网卡,而 primary_slim=1 时 queue0 归属第一个 secondary worker0——两个进程并发操作同一个 TX queue,desc ring 会损坏、mbuf 会泄漏。DPDK 多进程模型里每个 TX queue 只能一个进程独占,没有进程级锁。

第二阶段(f250ad1ea):引入 kni_inject_rp 共享 ring 彻底消除竞态。primary 收到 KNI 回复包后不直接 TX Port0,而是 enqueue 到 inject ring,由 owner secondary dequeue 后用自己的 tx_queue_id 发出。方案选型时排除了另外两个:

  • 预留专用 queue(nb_queues+1,KNI 用最后一个 queue):本地 virtio 不支持 RSS,无法用 RETA 隔离额外 rx queue,vhost backend 会向所有启用的 qp 分发,额外 rx queue 无人 poll 会 desc ring 满导致 backpressure,不可行
  • rx!=tx 不等队列:virtio PMD 按 max(rx,tx) 分配 vq,未 setup 的 rx queue 会被后端写入导致 crash,不可行

inject ring 是唯一可行的路径。

4.5 踩到的坑:primary_slim=0 + owner_proc_id 误伤 KNI

实现 commit 1c28aaa2d 把 KNI 调用条件从 ff_kni_is_owner_thread() 改成了 ff_kni_is_runtime_owner(),但没考虑到 primary_slim=0 时 owner_proc_id 生效会导致 primary 不再处理 KNI,反而误伤了默认模式。修复(e075e534f)改成条件分支:primary_slim 时用 runtime owner 判定,否则用 owner thread 判定。

4.6 一个遗留问题:primary 退出清理其实效果有限

文档 14 专门分析了这个。跳过 rte_eal_cleanup() 的设计意图是保护 secondary 依赖的共享资源,但逐函数坐实后结论是实际效果几乎为零——hugepage 用 MAP_SHARED,primary 退出内核只 munmap 自己的映射不影响 secondary;config 文件不在 cleanup 里删除;mp_socket unlink 不影响 secondary(有自己的 fd)。

真正缺失的是没有通知 secondary 退出,primary 退出后 secondary 变成孤儿进程进入控制面降级态。但按设计哲学 primary 本来就不该正常退出,这个路径是”万一”的兜底。

【注意】这里对文档 10 里”避免 rte_eal_cleanup() 拆除共享资源”的说法做了修正——经源码坐实,rte_eal_cleanup() 实际上不拆除 secondary 依赖的共享资源,这个说法不完全准确。skip cleanup 的真正价值是保守策略:避免 cleanup 过程中 pthread_cancel 等操作的未定义行为。

4.7 issue 原文两处判断被实测修正

这个值得单独说,因为它是整个调研最有价值的部分——初始回复的两个前提假设,实测后都不准确:

  • “primary 一旦异常退出,整个进程组都得重启” → 部分不成立。其余进程继续服务,崩掉的 secondary 可原地重启(只要还有进程持有 /dev/uioX,uio refcnt >= 1),无需立即全组重启。但 primary 本身不能原地拉回(新 primary 会 EBUSY 失败,这是 EAL 的设计意图),所以还是要择机全组重启。
  • “所有连接都会受到影响” → 不成立。传统模式下只有哈希到 primary 队列的那部分流量受影响(2 进程约 1/2,3 进程约 1/3),瘦身后零影响。
  • “secondary 异常了可以重启,不影响其他进程” → 成立,E2d 实测验证。

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

5.1 配置

在 config.ini 的 [dpdk] 段加 primary_slim,在 [kni] 段加 owner_proc_id:

[dpdk]
lcore_mask=f             # primary(lcore0) + 1 个 secondary(lcore1)
primary_slim=1           # 0(默认)=关闭,1=primary 瘦身
primary_slim_idle_sleep=1000   # 默认 1000us,避免 primary 空转占满一核,后续因为kni还需要主进程处理,可以根据需要适当调整该值

[port0]
lcore_list=1-3             # 关键:primary 的 lcore0 不在列表内,队列全归 secondary

[kni]
enable=1
method=reject
owner_proc_id=1           # primary_slim=1 + KNI 时指定 runtime owner secondary
tcp_port=1-65535

配置校验会自动拦截三种非法组合:primary_slim 与 thread_mode 互斥、nb_procs 必须 >= 2、primary lcore 不得在 lcore_list 里。

5.2 启动

启动方式不变,还是 start.sh 管多进程:

./start.sh -c config.ini -b ./example/helloworld

5.3 效果

功能正确性(文档 05 实测):

  • 杀瘦身 primary 后已建连接 12/12 零中断,26 个探测轮次无一次失败
  • 杀瘦身 primary 后新建连接 12/12 正常
  • QPS:slim primary 存活时 122.4k(相对单进程基线 122.3k,+0.08%);slim primary 崩溃后 127.7k(+4.5%),均 0 失败请求

KNI 场景(文档 13 实测):

  • ping 5/5 0% 丢包,rtt 0.512~1.329ms
  • HTTP 200

结项验证(文档 15):

  • 物理机功能测试、性能压测通过
  • 默认标准多进程模式 10 分钟以上大流量回归压测零回归
  • 多线程模式(thread_mode=1)10 分钟以上大流量回归压测零回归

5.4 已知边界

  • 全部实测在 virtio 设备 + igb_uio 的虚拟化环境完成,物理机功能/性能压测已补做通过,但 VFIO 下的 refcnt 语义与设备运行态保持行为仍标注为未充分验证
  • primary 不可原地拉回,控制面降级后需择机计划内全组重启
  • 优雅退出未实测,primary 退出建议走强杀而非 rte_eal_cleanup()
  • 长期稳定性(小时级)未充分观测,10 分钟以上大流量回归通过

6. 延伸阅读:

  • 完整调研与实现文档:docs/primary_slim_spec/zh_cn/(00-15)
  • 结项报告:docs/primary_slim_spec/zh_cn/15-结项报告.md
  • 三层架构:docs/zh_cn/F-Stack_Architecture_Layer1_System_Overview.md
  • issue 原文:https://github.com/F-Stack/f-stack/issues/1078

F-Stack 2.0 前瞻 1 : F-Stack 原生多线程支持 – 单进程多线程多协议栈实例

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

F-Stack 原生多线程支持(native-mt,thread_mode=1)是 F-Stack v2.0(预计2026.10正式release) 引入的一种新运行模式:在单进程内启动 N 个线程,每线程运行一份完全独立的 FreeBSD 协议栈实例,线程间 share-nothing、无锁,靠网卡 RSS 分队列做数据面分流,靠无锁 ring 做跨线程分发。

1.1 核心特点

特点说明
单进程多线程不再启动 primary + N 个 secondary 进程,改为一个进程内 N 个 lcore 线程
多协议栈实例每线程一份独立 vnet(VNET/VIMAGE 隔离),含独立 ifnet/PCB/路由表/端口分配
share-nothing 无锁数据面线程间无共享可变状态,无锁竞争,线性扩展
零回归 opt-inthread_mode=0(默认)时多进程模式逐字节不变
API 向后兼容ff_init/ff_run 签名不变,应用无需改动即可在新模式运行

1.2 与多进程模式的对比

维度多进程模式(默认)native-mt 模式
进程数1 primary + N secondary1 个进程
线程数每进程 1 个主线程N 个 lcore 线程
协议栈实例每进程一份每线程一份(VNET 隔离)
共享内存跨进程共享 hugepage/mempool同地址空间,直接访问
IPC 开销跨进程 IPC(如ring) 通信无跨进程 IPC,直接内存访问
进程管理需 start.sh 管理多进程单进程管理简单
容错隔离进程崩溃互不影响线程崩溃可能影响整个进程

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

2.1 单机多核部署的简化管理

多进程模型需要 start.sh 脚本启动 1 个 primary + N 个 secondary 进程,进程管理、信号处理、资源清理都较复杂。native-mt 模式只需启动一个进程,大幅简化部署运维。

2.2 减少多线程应用迁移成本

某些应用原本是多线程模式,迁移到原有的多进程 F-Stack 需要较大的迁移成本,native-mt 模式让应用在单进程内获得多核扩展能力,而不再需要 fork 或启动多个独立进程等迁移改造。

2.3 减少跨进程 IPC 开销

多进程模式下,某些应用的共享控制数据可能需要 IPC (如共享内存或ring等方式)才能在进行跨进程共享,native-mt 模式下这部分共享数据可直接在同进程内访问。

2.4 业界主流模型对齐

mTCP、Seastar 等高性能用户态网络框架普遍采用 thread-per-core + share-nothing 的单进程多线程模型。native-mt 让 F-Stack 与业界主流架构对齐。

3. 本功能的架构特征

3.1 整体架构

3.2 per-thread 隔离策略

native-mt 的核心挑战是:FreeBSD 协议栈有数百个全局变量,如何在单进程内让每线程拥有独立副本?答案是分四类隔离:

3.3 数据面无锁通信

跨线程通信使用 DPDK 的无锁 ring,保持多进程模式已有的语义:

  • dispatch/kni_ring:保持 RING_F_SC_DEQ(MP+SC),多 worker 写、单 owner 读
  • msg_ring:已数组化 msg_ring[RTE_MAX_LCORE],每线程用自己 lcore 索引,SP+SC
  • mempool:按 NUMA socket 共享,DPDK per-lcore cache 开箱 MT-safe,无需改造

4. 改造工作与遇到的问题

后续比较枯燥,不需要深入研究实现过程的可以跳过本节。

4.1 改造里程碑总览

native-mt 的改造按危险度递增分 8 个里程碑(CM0-CM7):

里程碑目标危险度关键 commit
CM0低垂果实 + 脚手架e79ceb9f0
CM1配置层 thread_mode 开关3c31cc540
CM2lcore_conf per-lcore 化 + DPDK 拉 N lcoree79ceb9f0
CM3底座全局 per-thread(pcpu/thread0/callout)e79ceb9f0
CM4VIMAGE 可行性 PoC(关键卡点)高/存疑86e0f76b0
CM5初始化重构(per-thread 栈实例 init)极高717843004 + 7495e70c0
CM6KNI owner 线程 + msg_ring per-thread6d74d59e0
CM7联调 + 回归 + 性能基线fea49af6d + be4233709

4.2 CM0-CM3:per-thread 化基础

问题1:msg_iov_tmp 全局变量数据错乱

ff_syscall_wrapper.cmsg_iov_tmp/msg_iovlen_tmp 是全局数组,多线程并发调用 ff_readv/ff_writev 会互相覆盖。这是最低垂的果实——恢复 __thread 即可修复,多进程模式亦无害。

问题2:lcore_conf 全局单例

ff_dpdk_if.c:123lcore_conf 是全局单例,30+ 引用点。改为 lcore_conf[RTE_MAX_LCORE] 数组,每线程用 rte_lcore_id() 索引自己的那份。同时引入 ff_cur_lcore_conf() 宏作为间接层,降低后续改动面。

问题3:pcpu/thread0/callout 底座全局

  • pcpupff_freebsd_init.c:69)→ 每线程一份 __thread struct pcpu
  • thread0/proc0ff_init_main.c)→ 每实例一份
  • cc_cpu callout(ff_kern_timeout.c:180)→ __thread struct callout_cpuCC_SELF() 返回本线程实例

4.3 CM4:VIMAGE 可行性 PoC(关键卡点)

这是整个项目最大的不确定项。VIMAGE 是 FreeBSD 原生的虚拟网络栈子系统,启用后 curvnet = curthread->td_vnet 按 per-thread 隔离数百个网络栈全局。但 f-stack 大量阉割了 FreeBSD 用户态代码,VIMAGE 能否跑通需实测验证。

PoC 结果:VIMAGE 可用(route B 验证通过)

  • opt_global.h#define VIMAGE 1
  • 每线程 vnet_alloc() 创建独立 vnet 并设 td_vnet
  • VNET_SYSINIT 每 vnet 各跑一份,V_tcbinfo/V_rt_tables 成功隔离

4.4 CM5:初始化重构

问题4:mi_startup/SYSINIT 一次性机制

ff_init_main.cmi_startup 跑完会打勾 SI_SUB_LAST,二次调用空转。需拆分:全局一次性 init(EAL/UMA/mutex/vnet0)+ per-thread 栈实例 init(vnet_alloc/td_vnet/VNET_SYSINIT/pcpu_i/thread0_i)。

问题5:worker cred 挂全局 prison0(R1 缺陷)

这是 native-mt 最隐蔽的 bug。症状:2 线程压测 0 req/s,client 反复重传 SYN,f-stack 从不回 SYN-ACK。

根因:worker 线程的 cred 挂在全局 prison0 上,而 FreeBSD socket 的 vnet 取自 cred 的 prison(CRED_TO_VNET(cred)),不是 curvnet。导致 worker 所有 socket/ifioctl 被静默重定向到 vnet0,worker vnet 无接口地址、无默认路由(ENETUNREACH),app listen 的 PCB 全建在 vnet0,而数据面在 worker vnet 查 PCB → 收到 SYN 不回 SYN-ACK。

修复:lib/ff_freebsd_init.c 新增 ff_worker_prison_init(),给每个 worker 分配独立 prison,使其 CRED_TO_VNET 指向自己的 vnet。

问题6:worker pcpu cpuid 越界(R2 缺陷)

修复 R1 后,worker vnet 的 PCB 表被真正使用,压测时 in_pcblookup_mbuf SIGSEGV。

根因:worker 的 pcpu_init()rte_lcore_id()(=2)作 cpuid,但本 build 非 SMP(MAXCPU==1)、mp_maxid==0,UMA/SMR per-cpu 数组只按 1 CPU 分配 → zpcpu_get() 越界。

修复:ff_pcpu_thread_init() 改传 0(当时方案)。后续在文档 17 的 SMP-aware 改造中彻底修复——每线程拥有稠密、独立的 pcpu 槽位。

4.5 CM6-CM7:KNI/工具链归属与联调

问题7:KNI 归属

多进程模式下 KNI 由 primary 进程独占(virtio_user vdev 只能由 primary 创建)。native-mt 无 primary/secondary 概念,KNI 改由单一指定线程(线程 0)独占持有。现有 kni_rp/bitmap 已按 rte_lcore_id() 命名,天然可 per-lcore,改动集中在门控判定。

问题8:worker 时钟缺口

症状:2 线程吞吐从修复前的 0 提升到 557 req/s,但仍远低于 1 线程的 ~209k req/s。

根因:worker 线程的 FreeBSD 时钟从未被驱动。init_clock() 仅在主线程调用一次,worker 的 freebsd_clock__thread)是零初始化(expire == 0),ff_hardclock_job 永不触发 → worker 的 callwheel 永不推进 → vnet_i 上 syncache 超时、TCP 重传、delayed ACK 全部瘫痪。

修复:新增 ff_hardclock_worker()(只推进本线程 callwheel,不触碰全局 ticks/timecounter)+ init_clock_worker()(worker 在 main_loop 中注册自己的定时器)。

4.6 后续优化:SMP-aware pcpu 视图与去全局锁

问题9:SMR per-cpu 槽位共享的 UAF 窗口

R2 修复后所有 worker 的 pc_zpcpu_offset 均为 0,共享同一份 SMR per-cpu 槽位。理论窗口:A 线程 smr_exitSMR_SEQ_INVALID 可能清掉 B 线程的 read section 标记 → smr_poll 误判无读者 → PCB 提前回收(UAF)。

修复(commit c7996a94f):定义 SMP,设置 mp_ncpus/mp_maxid/all_cpusnb_threads,每线程拥有稠密 pcpu id 和 per-thread curcpu,每线程独占不相交的 UMA/SMR per-cpu 槽位。

问题10:uma_crit_lock 全局自旋锁

f-stack 曾为保护 UMA per-cpu cache 加了全局自旋锁 uma_crit_locklib/include/vm/uma_int.h)。在每线程独占 per-cpu 槽位后,该锁已无必要。

修复(commit 57b612d16):移除 uma_crit_lockcritical_enter/critical_exit 变为 no-op,恢复 UMA per-cpu cache 的无锁快路径。

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

5.1 配置方法

config.ini[dpdk] 段新增 thread_mode 配置项:

[dpdk]
# lcore_mask 指定使用的 lcore 集合,thread_mode=1 时每个 bit 对应一个线程
lcore_mask=0xf           # 4 个 lcore = 4 个线程

# thread_mode: 0=多进程模式(默认),1=单进程多线程多栈实例
thread_mode=1

# 其他配置项与多进程模式相同
idle_sleep=20
hz=100

[port0]
addr=<DPDK_NIC_IP>
netmask=<NETMASK>
broadcast=<BROADCAST_IP>
gateway=<GATEWAY_IP>
lcore_list=0,1,2,3       # 与 lcore_mask 对应,可以忽略

5.2 启动方式

native-mt 模式下只需启动一个进程(不再需要 start.sh 脚本管理多进程):

# 编译(make clean 后全量重编)
cd f-stack/lib && make clean && make -j$(nproc)
cd f-stack/example && make clean && make

# 启动(单进程)
./example/helloworld --conf config.ini --proc-type=primary --proc-id=0

5.3 使用效果

性能基线实测(virtio 网卡环境)

配置线程/进程数req/s延迟 (avg)说明
thread_mode=1, 2 线程2233,380修复后多线程
thread_mode=0, 2 进程2231,570多进程对照

关键发现:

  1. 多线程线性扩展:2 线程 233k req/s,与 2 进程 231k 持平,验证 share-nothing 无锁模型的线性扩展能力
  2. 60 秒 soak 稳定:400 连接持续 60 秒达 497k req/s、2983 万请求零错误
  3. 【注意】thread_mode=0时单进程程数据确认为当时噪声,不在本文展示,实际性能单进程和单线程都差不多

零回归保证

thread_mode=0(默认)时所有路径走既有 primary/secondary 分支,字节级不变。已通过 10 分钟以上大流量回归压测验证。

5.4 注意事项与限制

限制说明
fd 不可跨线程共享每个线程的 fd 表独立(VNET 隔离),同 fd 在不同线程含义不同,与多进程模式时单进程内的fd语义限制相同
KNI 单线程独占KNI 由线程 0 独占持有,其他线程不处理 KNI(其他线程需要kni处理的包通过kni_rp转发至线程0,流程与多进程模式相同)
工具链兼容优先保留外部工具进程(--proc-type=secondary)attach 方式,向后兼容
VIMAGE 依赖启用 VIMAGE 需完整编译,部分 f-stack 阉割的 FreeBSD 子系统可能不完整
物理网卡 RSS多线程真实扩展性依赖网卡 RSS 能力,无 RSS 网卡多线程扩展受限,与多进程模式相同

6. 参考资料

  • 三层架构文档docs/zh_cn/F-Stack_Architecture_Layer1_System_Overview.md
  • native-mt specdocs/native_mt_spec/zh_cn/(00-17 完整设计文档)
  • 知识图谱docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md
  • issue 汇总docs/zh_cn/f-stack-issue-ana.md(#430/#571/#807/#855 等多线程相关 issue)
  • 代码提交e79ceb9f0(CM0-CM3)→ 86e0f76b0(CM4 VIMAGE)→ 7495e70c0(CM5 多栈实例)→ 6d74d59e0(CM6 KNI)→ 82b409faf(worker 时钟)→ ff09a17b2(prison 隔离)→ c7996a94f(SMP-aware pcpu)→ 57b612d16(去全局锁)

F-Stack 2.0 前瞻0 : F-Stack 使用 CodeBuddy 开发工作流清空代办列表的实践

本文介绍 F-Stack 工程,通过在 CodeBuddy 使用自建 Skill + 知识库 + spec + harness 多 agent 门禁体系,2026 年大功能全由 AI 辅助完成的实践,主要完成或完善的大功能后续则会有 F-Stack 2.0 前瞻一系列系列的文章分别单独介绍。

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

AI 发展到 25 年底 26 年初之后,在实际项目中逐步可用了,从现网业务的单元测试、小功能逐步扩展到大工程的复杂任务(非 F-Stack,如 DNS 支持 io_uring 代替 epoll 性能总体提升 25% 左右),而我们在 F-Stack 上也从 2026年5月后开始用 CodeBuddy 做 AI 辅助开发,到今天已经把「怎么让 AI 在这个大 C 语言项目里干活干得靠谱」沉淀成了一套可复用的工作流——三个自建 Skill(f-stack-dev-rule / f-stack-info-search / f-stack-issue-process)+ 知识库(架构文档/知识图谱) + spec 文档驱动 + harness 工程化多 agent 流程 + 全链路门禁体系。这篇把这套工作流捋一遍:由哪些部分组成、每部分解决什么问题、2026 年实战效果如何。

从 5 月中旬至今 8 月中旬的 3 个月时间里,我们通过这套基于 CodeBuddy 的工作流由 AI 辅助完成的大功能清单(数据来自项目 iwiki 工作清单),成功将限于人力原因历史一直积攒的待办列表近乎清空,也算是为这段经历划上一个还算可以的分号(放心,F-Stack 后续会一直参与进行维护),当然 token 消耗也是杠杠的,单 F-Stack 这些功能大约消耗了 5-60 亿左右的 token 吧,且 7-80% 是 opus-4.6 ~ opus-5.0,公司的 token 额度支持是必不可少的。

【注意】耗时仅是功能开始和结束时间统计,中间会穿插做很多其他事情,不代表单独本功能实际 AI 辅助开发的用时,实际用时要更少,所以抛开实际上线流程,单纯讲开发自测流程,AI 的提效真不是盖的。

功能完成时间耗时
LD_PRELOAD 信号量 → 无锁 ring IPC2026.05.2510 天
FreeBSD 13.0 → 15.0 协议栈升级2026.06.0910 天
DPDK 升级到 24.11(dev 分支)2026.06.101 天
单元测试框架(Unity/CMocka)2026.06.112 天
接收零拷贝 ff_zc_mbuf_read2026.06.111 天
发送零拷贝原生化(去掉 MAGIC 魔改)2026.06.121 天
lib 库本地 socket 访问,后续考虑会移植到 1.21(LTS)2026.06.184 天
io_uring 对标调研,最终结论 F-Stack 当前不会去做 io_uring 对标2026.06.181 天
ff_rss_check 优化(IPv6 + rte_thash),后续会考虑部分移植到 1.21(LTS),rte_thash(DPDK-19.11不支持)需进一步调研2026.07.163 天
MTU 修改支持(jumbo frame),后续考虑会移植到 1.21(LTS)2026.07.223 天
LRO 支持 + TSO 完善,后续考虑会移植到 1.21(LTS)2026.07.231 天
基于 VIMAGE/VNET 的原生单进程多线程多协议栈(native-mt)2026.08.055 天
issue 批量处理 320 → 0,完全可以修改下复用到其他项目issue/工单的处理中2026.08.10持续

这些功能的统一分工模式是:所有代码由 AI 完成,人工只做提示词、spec 文档、plan、纠正方向、结果审核和测试验收(开发的虚拟机环境由 CodeBuddy 根据工作流自动进行单元测试和运行测试进行验收,人工额外做物理机的测试验收)。几个关键词:

  • 规约先行:AI 在 F-Stack 里干活的第一件事不是写代码,是加载 f-stack-dev-rule——13 节强制规约零容忍,把”AI 容易翻车的点”(rm/kill/chmod、增量编译、config.ini 污染、真实 IP 泄漏、注释风格)全部前置成规则
  • skill 分层:三个 skill 各管一段——dev-rule 管”怎么改”、info-search 管”怎么搜”、issue-process 管”issue 怎么处理”,另有一批通用 skill(spec 驱动、harness 工程、C 语言开发、单测等)可以对应安装,这篇不展开
  • harness 多 agent 门禁:大任务强制走”调研出中文 spec → 人工审核 → 多 agent 实现 → 门禁审核 → 里程碑提交”的完整链路,写审分离、单步打回上限 3 次

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

2.1 大功能开发(调研 + spec + harness 实现 + 验收)

F-Stack 里凡是”一个新功能/一次大升级”级别的任务,都走 harness 流程:先多 agent 调研产出中文 spec 文档(放 docs/<FEATURE>_spec/zh_cn/,具体调研过程见 f-stack-info-search 这个skill),人工审核通过后另起多 agent 做实现和验收,按里程碑多次提交,验收完成后才翻译英文 spec,包括 freebsd 13→15 升级、native-mt、ff_rss_check 优化 等都是这么干的。

2.2 issue 分析与批量处理

f-stack-issue-process skill 定义了 issue 处理三步 SOP(读全文 → 搜资料 → 综合判断),配合五类结论模板。2026 年项目把 issue 从 320 个清理到 0 个,全程半自动化:AI 按 SOP 分析归档、人工确认或修复后回复关闭,处理了众多咨询类 issue,并对功能类 issue 修复和新增了很多问题或功能。

2.3 性能优化攻坚(需要对照实验的)

ring IPC 性能优化、RSS check 优化这类”优化”任务,AI 的价值不在写代码而在执行对照实验纪律:对照组先于优化、数据先于理论、每条假设配套可证伪的物理量。ring IPC 的 v1~v3.7 七轮迭代就是 AI 在规约约束下自我证伪的完整案例。

2.4 不太适合的场景

  • 一句话的小改动:直接说需求就行,别套 harness 流程(任务规模判断规约里小任务直接执行)
  • 需要物理环境验证且环境不齐的:规约要求”未实际执行不臆测”,环境不满足时 AI 只能如实标注「未坐实」,硬要结论反而有害
  • token 额度紧张时期:native-mt 这种”堪比重做一个小 f-stack”的任务消耗巨大(iwiki 原话),适合额度重置后再做

3. 本功能的架构特征

3.1 工作流总图

3.2 三个自建 Skill 的分工

f-stack-dev-rule(怎么改):F-Stack 工作区一切任务的强制规约合集,13 节零容忍。核心几条:Shell 操作必须走 rm_tmp_file.sh/kill_process.sh/chmod_modify.sh 三个脚本(严禁直接 rm/kill/chmod);改代码先 make clean 再完整编译(增量编译不作为依据);config.ini 本地测试值不入库;文档严禁真实 IP 用占位符;commit message 英文 1-3 句;lib 最小注释;多 agent 写审分离/bounce≤3/leader 轮询不提前退出;所有调研类任务必须用 info-search 搜资料;任务规模判断与 harness 流程;代码改动必须过单测和运行时回归测试。

【注意】即使有了这些限制规则,Agent 也是会经常跑偏,或者不遵守规则限制的,所以人工的值守和纠偏目前依然是不可或缺的

f-stack-info-search(怎么搜):一切提问/分析/调研类任务的资料搜索 SOP,五部分:查三层架构文档与知识图谱(docs/ 下 LAYER1/2/3 + KNOWLEDGE_GRAPH_WIKI)→ 查代码提交记录(本地 git log + DPDK 上游)→ 查关联 issue/PR(先查本地 issue 分析档案再 gh search)→ 查公开资料(DPDK Bugzilla/Patchwork/邮件列表)→ 内外网技术资料(博客/公众号/iWiki,中英双语关键词)。核心纪律是三方证据收敛:内部文档 + 外部资料 + 实际代码,不一致处以实际代码为准。

f-stack-issue-process(issue 怎么处理):三步 SOP——读 issue 全文(含全部评论,不可只看标题)→ 调 info-search 搜资料 → 综合判断。五类结论模板(已修复/有上游 patch 未合入/未修复/有 workaround/非 bug),关键约束是不可自动操作 issue:分析完必须人工确认后才能评论关闭,回复用英文只讲核心结论。每次处理后同步更新中英文 issue 分析档案(f-stack-issue-ana.md,可以不断增加新的知识点供后续的issue分析和问题调研使用)

3.3 门禁体系(AI 干活的刹车)

  • 写审分离铁律:写代码/文档与审核必须不同 agent,leader 严禁自写自审;纯调研/探测/汇总等单一角色可由 leader 兼任
  • leader 轮询:子 agent 全部完成前 leader 严禁提前退出;禁止无超时死等,旁路探测(读落盘文件、git 状态)优先于消息探测,否则 CodeBuddy 等 Agent 经常出现子进程因为各种原因异常退出导致的整个任务中断,需要人工继续任务的情况。
  • 异常回退:子 Agent 超时/卡死时 leader 接管或 spawn 新 agent 重做,但不得违反写审分离
  • 测试门禁:代码改动必须完成单测和运行时回归测试,同样受门禁规则限制

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

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

4.1 skill 体系是怎么长出来的(从零散规则到三个 Skill)

这套体系不是一次设计出来的,是被实战问题逼出来的。从最早的裸奔,在 AI 开发过程中逐步踩坑晚上,并在后期整理合并成 f-stack-dev-rule 一个 skill 统一加载(13 节),替代分散的十余条分条规约。info-search 和 issue-process 则是从 issue 处理实战中提炼的:issue 处理要先搜索资料再下结论、搜索有固定的五部分流程、结论有五类模板、档案要中英同步——这些”怎么做对”的经验固化成了 skill。

skill 的落地方式是”库 + 源”双份:安装到 ~/.codebuddy/skills/ 供 CodeBuddy 加载,同时在 docs/zh_cn/skills/ 保留一份源码,规约明确”安装前若 use_skill 不可用,直接读取 docs/zh_cn/skills/ 下对应 SKILL.md 全文作为规约执行”。零容忍条款一旦违反任务直接打回。

【注意】规约里有一条值得单独说:rm_tmp_file.sh/kill_process.sh/chmod_modify.sh 三个脚本不是形式主义——2026 年 6 月 ZC-recv 实测期间就发生过一次误用 rm -rf 的违规(M2 报告里记录并修复)。规约约束的是 AI 最常见的危险动作,脚本带审计日志(chmod 快照到 /tmp/.trash/、审计到 /tmp/.chmod_audit.log),出了问题可回查。

4.2 实战问题一:AI 会”看起来很对,实际全错”

本地 socket 访问功能(2026.06.18,4 天)是典型的反面案例——iwiki 原话:”本功能 AI 表现不好,人工打回返工多次才最终完成”。这说明流程里最值钱的是”人工审核”和”打回”这两个环节,而不是 AI 一次写对的能力。对应到规约里就是 bounce≤3 的打回链:门禁失败必须打回上一步修复,同一单步打回超 3 次立即停止转人工决策,不许带病放行。

4.3 实战问题二:AI 会臆测,必须用”实事求是规约”压住

规约第 9 节(实事求是)和第 10 节(资料搜索)专门治这个:所有行动必须实际执行,严禁未执行就猜测给结果;代码/文档/外部数据源交叉验证,不一致处以实际代码为准;无法静态坐实或环境不满足的项,如实标注「未坐实/未执行」。ring IPC 性能分析的 v1~v3.7 七轮迭代是这个规约的最好注脚:v1 的单边代码分析错误、v2 的未验证假设、v3.3 的方案 C 实测劣化 4% 当天回退——每一步错误的证伪都靠实测数据,最终收敛出”ring 无 net win、生产推荐 sem”这个反直觉但正确的结论。

4.4 实战问题三:编译卫生和增量陷阱

AI 写 C 代码最常见的坑是增量编译:FF_ZC_* 开关切换后 make 按时间戳跳过 .o 重编译,导致内核 hook 缺失、http=000 的诡异现象(ZC M2 报告)。规约把”改代码先 make clean、编译验证以 clean build 通过为准、增量编译通过不作为依据”写死,还附了本机两个已知坑的规避(IDE safe-delete hook 拦截 make clean 用 PATH 前缀规避、make -j16 竞态先 make machine_includes)。freebsd 13→15 升级踩的”宏改名丢包裹”坑(UMA_MD_SMALL_ALLOC → UMA_USE_DMAP)和”结构头改动未 clean 重编 ABI 偏斜”坑,也都沉淀成了规约条款。

4.5 实战问题四:token 消耗与任务切分

native-mt 的 iwiki 备注很诚实:”本功能超级复杂,堪比重做一个小 f-stack,消耗 token 较多”,残留风险如实记录后暂停,等 token 额度重置后继续。这说明 harness 流程还有一层现实约束:任务要按 token 预算切里程碑,做不完的宁可如实留档(残留风险清单),也不要糊弄收尾。issue 批量处理 320→0 则是另一个方向的实践——用内网版 OpenClaw/Hermes 按 SKILL 的 SOP 半自动化处理,穿插在 token 紧张期(因为内网版 GLM-5.2不计算额度)。

【注意】截止目前(2026.08.17)大任务初始的调研、架构文档、任务拆分等任务依然推荐使用高级模型(额度消耗非常非常大,需要特别注意,主要是目前其他部分模型在大任务的调研分析和架构设计上表现的依然一言难尽,就不实际举例了),完成任务拆分后实际的代码编写等小任务或具体任务可以交由较轻量模型(但也应使用 GLM-5.2 以上模型)执行。

4.6 2026 年各功能沉淀的文档资产

每个大功能都按 harness 流程留下了完整文档:freebsd_13_to_15_upgrade_spec、zc_stack_user_spec、ld_preload_ring_spec、mtu_change_spec、lro_tso_spec、native_mt_spec、rss check 优化 spec 等,加上三层架构文档(LAYER1/2/3)和知识图谱(KNOWLEDGE_GRAPH_WIKI)。这些文档反过来又成了 info-search 的搜索源——文档 → 代码 → 决策形成了闭环。

【注意】新功能更新后应该对应更新架构文档和知识图谱,否则 info-search 的搜索源会失真,后续调研拿到的是过期结论。更新可以靠流程约束(里程碑里带文档更新项),也可以靠自动化——比如 git 操作自动触发知识图谱重建(此前每次提交后 GitNexus 在后台更新知识图谱即是此类机制)。

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

5.1 Skill 安装与加载

三个 skill 都装在 ~/.codebuddy/skills/(f-stack-dev-rule / f-stack-info-search / f-stack-issue-process),CodeBuddy 会话中用 use_skill 按需加载。F-Stack 工作区的任何任务第一步先加载 f-stack-dev-rule;提问/分析/调研类任务必须加载 f-stack-info-search;issue 分析处理加载 f-stack-issue-process(其搜索环节内部调用 info-search)。

5.2 配套通用 Skill(对应安装,这篇不展开)

spec 驱动开发(spec-driven)、harness 工程(harness-engineering)、C 语言开发/单测(c-pro、c-unittest-expert 等)、C 精准外科手术式修改(c-precision-surgery)等通用 skill 与 F-Stack 自有三个 skill 组合使用——自有 skill 管”F-Stack 特有约束”,通用 skill 管”通用工程方法”。

5.3 使用效果(2026 年实测数据)

维度数据
AI 辅助完成大功能13 个
最大单任务FreeBSD 13→15 升级(编译矩阵 5 格全绿)
issue 清理320 → 0(2026.08.10 清零)
编译基线lib error 0 / warning 51(既有 baseline,新改动零新增 warning)
文档资产7 个功能 spec 目录 + 三层架构文档 + 知识图谱 + 中英双语 issue 档案
博客文章F-Stack 2.0 前瞻系列 8 篇(含中英文版),后续将陆续发出。

5.4 对使用者的建议

  • 先把规约读一遍再让 AI 动手:dev-rule 的 13 节每条都是实战换来的,AI 违反任何一条任务都会被人工打回
  • 大任务别跳过 spec 环节:中文 spec + 人工审核是成本最低的方向纠偏点,本地 socket 功能的返工证明”跳过调研直接写代码”更贵
  • 信任门禁不信单次输出:AI 写的东西必须过独立 agent 审核,bounce 是正常流程不是失败
  • 让 AI 如实说”不知道”:环境不满足就标「未坐实」,这比硬给结论值钱得多

6. 总结

虽然 CodeBuddy 还是存在不少问题吧,比如 Loop 总是中断,总是需要人工去提醒他继续,即使加了各种限制规则依然不能避免该问题,但是这个“Buddy”确实极大幅度的提升了我们功能开发的效率,还是需要非常非常感谢。

7. 延伸阅读:

  • 开发强制规约:docs/zh_cn/skills/f-stack-dev-rule/SKILL.md
  • 资料搜索技能:docs/zh_cn/skills/f-stack-info-search/SKILL.md
  • issue 处理 SOP:docs/zh_cn/skills/f-stack-issue-process/SKILL.md
  • 三层架构文档:docs/zh_cn/01-LAYER1-ARCHITECTURE.md
  • 知识图谱:docs/zh_cn/KNOWLEDGE_GRAPH_WIKI.md
  • 历史 issue 档案:docs/zh_cn/f-stack-issue-ana.md
  • 2026 年各功能 spec:docs/<FEATURE>_spec/(freebsd_13_to_15_upgrade_spec、zc_stack_user_spec、ld_preload_ring_spec、mtu_change_spec、lro_tso_spec、native_mt_spec 等)

AI 时代的一些个人想法 2

上一篇是 3 月底写的,这两个月又用 AI 干了几个比较大的项目,攒了一些新感受,继续零散记一下。

1. 这两个月在干啥

DNS业务一个(io_uring支持)、F-Stack 两个(主要是FreeBSD-13.0升级到FreeBSD-15.0)。这两个项目里,已经没有一行代码是手写了

用的是这几个月比较火的 Spec 驱动 + Harness 工程方式:先让 AI 把文档和知识图谱生成好,再进入实际开发。前期慢一点,后面快得吓人。

2. 效率提升 80%,但代码掌控力会变弱

以前需要忙一两个月的活,现在四五天到一两周就能搞完。如果不算部署上线那些老太太裹脚布的流程,效率提升基本都在80% 以上,而且达到的效果至少能通过我自己的验收。

代价是代码掌控力肯定会弱不少。我现在做的 review 主要就三件:

  • Spec 文档
  • Plan 工作计划
  • 最终的测试结果和验收

代码我也会瞄两眼,但说实话,AI 写的代码有些地方有些技巧是完全没必要的,简单直接、易懂、性能也不差就好。这点 AI 的风格我不是太喜欢,当然这也和一些skill的提示词会有相关性,但是提示词、记忆、规约即使写入了,也是经常被忽略的,没办法。

附带一个意外收获:以前古法编程的时候,文档基本都是事后补、能省则省;现在 Spec 驱动跑下来,详细文档自然就落下了,这部分时间也是白送的。这个效率账我前面那 80% 还没算进去。

3. AI 编程不要并行,老老实实串行

最开始那一两天我是并行干的——这边 AI 跑着代码,那边我顺手处理别的事。看上去事情办了一堆,效率好像翻倍。

但人会非常非常累。

大脑上下文一直在切,一天下来感觉比正常加班还废。强烈不建议这么干,串行就好。 看着像浪费时间,其实更稳。建议可以盯着AI的过程学习或者在AI跑歪时及时打断纠正,或者干点其他不是非常费脑的事情。

4. 35 岁老程序员 vs 年轻人,AI 到底向着谁

最近网上有个声音:AI 对 35+ 老程序员更友好,对应届生不太友好——年轻人少了项目里的实战锻炼,成长断档。

部分同意。

但年轻人不是完全没机会。串行盯着 AI 干活的时候,可以盯它的分析过程和思考过程。现在 AI 的思考链条已经挺接近人类程序员了——会试错、会回退、此路不通就换条路。年轻人跟着 AI 的思路看,其实是能学到东西的,只是手动调试那种深刻的印象会少一些。

老程序员也一样,借着 AI 的”出声思考”,能很快摸清一个新项目的架构和设计思路。所以年轻人不是没成长路径,只是路径变了,效果可能比传统古法编程要打个折扣。

5. DPDK 程序也能让 AI Agent 自动调试

之前只是内核协议栈的程序使用Agent自动调试,一个小变通就可以让DPDK程序也让Agent自动调试了,不用再自己测试出问题贴给Agent了,用Agent通过ssh自动在client执行命令并回流结果就好了。

6. 大项目要人工盯着,AI 会跑歪

虽然能自动跑,但大需求我不建议做完全自动化

原因是 AI 跑长链路任务的时候,会随时蹦出一些需要人工决策的点;更要命的是它会跑歪——一开始走错路,越走越远,最后试错回来虽然能修正,但 token 和时间都白烧了。

人工盯着 AI 干活,看到他思路开始歪了,赶紧终止纠正一下,再让他继续。这个动作很重要,对整个项目的理解也会更深。大项目的”AI + 人工监督”,比”AI 全自动 + 事后验收”靠谱得多。当然这也是我们这种老登的优势😂.

7. 小需求纯自动化基本是噱头

反过来说,那些”AI 7×24 全自动”的小需求演示,说得难听点儿,更多是噱头。

小需求本身就用不了多长时间,纯自动化看起来是省了你那几小时,但你真敢让它合并代码不看一眼?最后还是要人工 review 提交、看测试结果。审核的时间 + 自动跑的时间,跟你串行盯着分步执行的时间比,提升其实非常有限。目前个人对小需求的修改还是有很大一部分是手工改,AI辅助审核的方式。同时建议新人也尽量保留一部分小需求来手写,不要全部丢给AI.

大需求和小需求在自动化收益上的差距非常明显:

  • 大需求:80%+ 效率提升,文档白送,但要人工盯
  • 小需求:纯自动化看起来酷,但提升不明显,凑合用

8. 这就这样

这两个月最大的体感是:AI 已经能干很多脏活累活,但“让 AI 干”和”让 AI 自己干”是两回事——前者效率提升巨大,后者噱头偏多。

至于会不会取代谁,再过半年看吧。


本文是个人语音口述零散信息后,由 Workbuddy + Claude Code 模拟个人风格整理生成。

AI 时代的一些个人想法

最近用 AI 比较多,也有一些自己的感受,零散记一下。

1. 大模型、Agent 和平台

大模型就像发动机,Token 是汽油,Agent 智能体相当于汽车,各种 CI/CD 平台就是路和基础设施,OpenClaw 这类工具大概是汽车的智驾。不一定非常准确,大概这么个意思。

2. AI 写代码这件事,现在到底到哪了

春节前,AI 辅助写单元测试,能完成大概六七十。春节后,用上 plan 模式 + Claude Opus/Sonnet 4.6 之类,至少 80%,部分场景 85% 以上。

对于初中级程序员能干的事,AI 基本已经碾压了。简单的功能、函数、小模块,没什么问题。

但是,遇到复杂项目,多模块互相关联、调用链条复杂的那种,目前国内国际顶级模型都还不太行。这块儿目前是高级程序员最后的阵地。但以 AI 目前的迭代速度,这个阵地估计也撑不了太久。

下一个真正的护城河,可能是:对现网复杂问题的经验判断,行业 context 的深度积累。这些东西 AI 目前基本还是胡说八道的多。

3. 真正的护城河在哪

不是什么人文关怀、情感这些,这些 AI 是可以模拟的,而且会越来越好,对这方面的护城河持悲观态度。

真正难被替代的是:私有数据、行业经验、关系网络、运营资源、信息差。这些才是门槛,其他的比如做个 to-do list 应用、学英语 app 之类,基本没有任何护城河,扯淡。

4. 技术跟进节奏

不必追 .0 版本,等到 .1、.2,大版本相对稳定了再跟进。但也不能隔三五个大版本才去关注,那就太晚了。这个思路对 AI 新技术和传统开源软件都一样。

5. 关于 OpenClaw 的实际使用体验

用了一段时间,整体来说,选对模型 + 配好 skills,效果还不错。我这边主要用来:定时提醒股票、花粉等信息、处理 F-Stack 开源项目的 issue,效率比较好。

后续计划用来给 F-Stack 生成架构文档、规范文档,以及尝试用 SDD(Spec-Driven Development)模式开发或优化一些新功能,具体效果后续再看,有些功能对 Claude Code 来说确实也有点吃力。

对其他人的建议:任务复杂但选了弱模型,效果就很差,基本不可用。如果平台只能用自动分配的国产模型,建议换个可以手选模型的,至少选 GLM5、Kimi、MiniMax 这类相对好的。有条件的话还是上海外顶级模型,效果差距很明显。

Token 消耗大的话,可以按任务分级:简单任务用国产模型,复杂的工程化连续任务建议用海外顶级模型;定期做记忆压缩也有帮助;尽量使用Coding Plan。

当然,小龙虾目前这么火,肯定也少不了各种宣传导致的全民焦虑,以及卖铲子和盒饭的推波助澜。

就这些,比较零散,凑合看。


本文是个人提供零散信息后,由 OpenClaw + Claude Code Sonnet-4.6 模拟个人风格整理生成。

F-Stack “No probed ethernet devices” 问题分析与解决指南

整理自 GitHub F-Stack/f-stack 仓库 issue 及 DPDK 官方文档

涉及 issue:#1035、#837、#693、#663、#583、#581、#531、#386、#379 等

最后更新:2026-03-09


一、问题概述

在运行 F-Stack(helloworld、nginx、自定义应用等)时,常见以下报错:

EAL: Error - exiting with code: 1

Cause: No probed ethernet devices

或:

ff_init failed with error "No probed ethernet devices"

该错误的根本原因是 DPDK 无法检测到可用的以太网设备,即 rte_eth_dev_count_avail() 返回 0。

这是 F-Stack 社区中出现频率最高的问题之一,历史上超过 10 个相关 issue,跨越 2019~2025 年,具有高度重复性。


二、根因分类

“No probed ethernet devices” 错误通常由以下几类原因导致,需按顺序逐一排查。


原因 1:网卡未绑定到 DPDK 兼容驱动

最常见原因。 DPDK 需要网卡从内核驱动(如 ixgbevirtio)解绑,并重新绑定到 DPDK 专用驱动(igb_uiovfio-pci)。

诊断命令:

cd /data/f-stack/dpdk

python3 usertools/dpdk-devbind.py --status

如果网卡显示在 Network devices using kernel driver 而不是 Network devices using DPDK-compatible driver,则需要绑定驱动。

解决方案:使用 igb_uio 绑定

# 1. 加载 igb_uio 模块

modprobe uio

insmod /data/f-stack/dpdk/build/kernel/linux/igb_uio/igb_uio.ko

2. 查看网卡 PCI 地址

python3 usertools/dpdk-devbind.py --status

3. 绑定网卡(替换 0000:00:03.0 为你的 PCI 地址)

python3 usertools/dpdk-devbind.py --bind=igb_uio 0000:00:03.0

4. 验证绑定成功

python3 usertools/dpdk-devbind.py --status

应看到网卡出现在 "Network devices using DPDK-compatible driver" 下

解决方案:使用 vfio-pci 绑定(推荐用于现代系统/虚拟机)

# 1. 加载 vfio-pci 模块

modprobe vfio-pci

2. 绑定网卡

python3 usertools/dpdk-devbind.py --bind=vfio-pci 0000:00:03.0

3. 如需在非 IOMMU 环境使用

echo 1 > /sys/module/vfio/parameters/enable_unsafe_noiommu_mode


原因 2:config.ini 中 port_list 未正确配置

config.ini[dpdk] 段中必须配置 port_list,且其编号要与实际绑定的 DPDK 端口对应。

错误示例:

[dpdk]

port_list=0

配置了 port 0,但网卡未绑定或 PCI 白名单不匹配。

排查方法:

# 查看当前绑定的 DPDK 端口

python3 usertools/dpdk-devbind.py --status

运行 testpmd 验证 DPDK 可以看到设备

dpdk-testpmd -l 0-1 -n 4 -- -i

解决方案:

确认 pci_whitelist(旧版)或 allow(新版)参数与实际网卡 PCI 地址匹配:

[dpdk]

lcore_mask=1

channel=4

port_list=0

旧版 DPDK

pci_whitelist=0000:00:03.0

新版 DPDK (21.11+)

allow=0000:00:03.0


原因 3:Makefile 链接参数缺少 --whole-archive

在自定义应用中链接 F-Stack 时,如果没有使用 --whole-archive 标志,DPDK 的 NIC PMD(Poll Mode Driver)驱动库不会被完整链接进来,导致运行时找不到网卡。

错误写法(不完整):

LIBS += -lfstack -ldpdk

正确写法:

DPDK 19.11 及以下:

LIBS += -L${FF_PATH}/lib -Wl,--whole-archive,-lfstack,--no-whole-archive

LIBS += -L${FF_DPDK}/lib -Wl,--whole-archive,-ldpdk,--no-whole-archive

DPDK 20.11 / 21.11 及以上(使用 pkg-config):

CFLAGS  += $(shell pkg-config --cflags libdpdk)

LDFLAGS += $(shell pkg-config --libs libdpdk)

LDFLAGS += -Wl,--whole-archive,-lfstack,--no-whole-archive

参考 f-stack/example/Makefile 作为标准模板


原因 4:pkg-config 版本过低

pkg-config 版本低于 0.28 时,无法正确解析 DPDK 的 .pc 文件,导致驱动库链接不完整,运行时检测不到网卡。

诊断命令:

pkg-config --version

解决方案:升级 pkg-config 到 0.28+

cd /data

wget https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz

tar xzvf pkg-config-0.29.2.tar.gz

cd pkg-config-0.29.2

./configure --with-internal-glib

make && make install

mv /usr/bin/pkg-config /usr/bin/pkg-config.bak

ln -s /usr/local/bin/pkg-config /usr/bin/pkg-config

验证版本

pkg-config --version # 应显示 0.29.2


原因 5:Mellanox(MLX5)网卡缺少 glue 库

使用 Mellanox ConnectX 系列网卡时,DPDK 的 MLX5 PMD 需要额外的动态链接库支持。

错误日志:

net_mlx5: cannot load glue library: librte_pmd_mlx5_glue.so.xx.xx.x:

cannot open shared object file: No such file or directory

net_mlx5: cannot initialize PMD due to missing run-time dependency

on rdma-core libraries (libibverbs, libmlx5)

解决方案:

# 1. 安装 rdma-core 依赖

apt-get install rdma-core libibverbs-dev libmlx5-1 # Ubuntu

yum install rdma-core libibverbs libmlx5 # CentOS

2. 复制 glue 库到系统路径

cp /data/f-stack/dpdk/build/lib/librte_pmd_mlx5_glue.so.* /lib64/

ldconfig

3. 编译 DPDK 时启用 MLX5 支持

cd /data/f-stack/dpdk

make config T=x86_64-native-linuxapp-gcc

sed 's/CONFIG_RTE_LIBRTE_MLX5_PMD=n/CONFIG_RTE_LIBRTE_MLX5_PMD=y/g' \

-i build/.config

make clean && make && make install

4. 修改 config.ini 指定 PCI 地址

[dpdk]

pci_whitelist=0000:03:00.0


原因 6:在虚拟机中运行 / 无物理网卡

在笔记本、虚拟机或容器中没有 DPDK 兼容物理网卡时,需使用虚拟设备(vdev)。

使用 net_ring(内存环回设备):

[dpdk]

lcore_mask=1

channel=4

vdev=net_ring0

port_list=0

[port0]

addr=10.0.0.2

netmask=255.255.255.0

broadcast=10.0.0.255

gateway=10.0.0.1

注意: net_ring 仅用于测试/开发,不适用于生产环境。

在虚拟机中修复 igb_uio:

虚拟机中 pci_intx_mask_supported() 可能返回 false,需修改内核模块代码:

// 文件:f-stack/dpdk/kernel/linux/igb_uio/igb_uio.c 第 274 行

// 修改前:

if (pci_intx_mask_supported(udev->pdev)) {

// 修改后:

if (true || pci_intx_mask_supported(udev->pdev)) {

修改后重新编译 DPDK:

cd /data/f-stack/dpdk

meson -Denable_kmods=true build

ninja -C build && ninja -C build install


原因 7:使用 DPDK 共享库(.so)时驱动未加载

当使用 DPDK 动态库(CONFIG_RTE_BUILD_SHARED_LIB=y)时,PMD 驱动不会自动加载。

解决方案:

  • 切换到 dev 分支代码(已修复此问题)
  • 或改用静态库链接方式

原因 8:Rust binding 链接问题

在 Rust 项目中使用 F-Stack 绑定时,需要在构建脚本中明确链接 NIC 驱动库:

// build.rs

println!("cargo:rustc-link-lib=fstack");

println!("cargo:rustc-link-lib=rte_net_bond"); // 需要显式添加驱动库

// 使用 pkg-config 链接 DPDK

pkg_config::Config::new()

.print_system_libs(false)

.probe("libdpdk")

.unwrap();

注意: 确保 pkg-config 版本 ≥ 0.28,否则驱动库无法被正确识别。


三、快速排查流程

遇到 “No probed ethernet devices” 时,按以下步骤逐一排查:

Step 1: 检查网卡是否绑定 DPDK 驱动

└─ dpdk-devbind.py --status

├─ 未绑定 → 绑定 igb_uio 或 vfio-pci(见原因1)

└─ 已绑定 → 继续 Step 2

Step 2: 检查 config.ini 的 pci_whitelist/allow 是否正确

└─ 与 PCI 地址不匹配 → 修正 config.ini(见原因2)

└─ 匹配 → 继续 Step 3

Step 3: 检查是否自定义 Makefile / 应用

└─ 自定义 → 检查 --whole-archive 链接参数(见原因3)

└─ 使用示例 → 继续 Step 4

Step 4: 检查 pkg-config 版本

└─ < 0.28 → 升级 pkg-config(见原因4)

└─ ≥ 0.28 → 继续 Step 5

Step 5: 检查网卡类型

├─ Mellanox → 安装 rdma-core / 复制 glue 库(见原因5)

├─ 无物理网卡/虚拟机 → 使用 vdev 或修复 igb_uio(见原因6)

└─ 其他 → 确认网卡在 DPDK 支持列表中


四、验证步骤

修复后,按以下步骤验证:

# 1. 验证网卡绑定状态

python3 /data/f-stack/dpdk/usertools/dpdk-devbind.py --status

2. 用 testpmd 验证 DPDK 可以检测到设备

dpdk-testpmd -l 0-1 -n 4 -a 0000:00:03.0 -- -i

若看到 "Found 1 port(s)" 则正常

3. 运行 helloworld 验证 F-Stack 正常启动

cd /data/f-stack/example

./helloworld --conf /etc/f-stack.conf --proc-type=primary --proc-id=0

正常应显示 port 初始化信息,不出现 "No probed ethernet devices"


五、相关 issue 汇总

Issue 状态 关键场景 解决方案
#1035 Open 使用 vdev=net_ring0 但无效 检查 vdev 配置格式
#837 Open 阿里云服务器,eth0 仍使用内核驱动 绑定网卡到 DPDK 驱动
#693 Open 自定义应用中 ff_init 失败 Makefile 添加 –whole-archive
#663 Open Rust binding 运行失败 显式链接 PMD 驱动库
#583 Closed 使用 DPDK .so 动态库 切换到 dev 分支
#581 Closed helloworld 运行失败 升级 pkg-config ≥ 0.28
#531 Closed Mellanox MLX5 网卡 复制 glue 库到 /lib64
#386 Closed ixgbe 网卡,驱动未绑定 绑定 igb_uio 驱动
#379 Closed 自定义 PMD 驱动未链接 在 Makefile 中链接 .a 文件

六、参考资料


*本文档由 OpenClaw 自动整理,基于 F-Stack GitHub issue 历史数据和 DPDK 官方文档。*

过去的2025

今年的头疼比去年略微好一点,但是还是挺严重的,晚上的失眠也是依然非常严重,安眠药的效果已经很弱了,有时候用酒精麻痹下效果还好下,还好布洛芬止疼的效果目前还凑合,随意搜了下,这种状况还是挺多的,但是人的性格就是这样,基本是很难改变了,可以参考:https://www.zhihu.com/question/461572685/answer/3561814996、https://www.v2ex.com/t/1189014。另外发际线也在持续后退,M型越来越明显了。

M


有依然繁琐无用的流程问题令人十分百分千分厌烦,有依然存在的业务稳定性压力,本来是要提离职的,目前同意了部门给的一个选择,不再负责任何一项具体的业务,脱身出来只作为技术专家有问题时进行支持。后续看是否会有些好转吧,个人自身并没有太大的信心能放下,也只能说是常识一段时间看下。头疼和失眠问题是从24年中多了一堆乱七八糟的流程后几个月变得很严重的,在此之前2-3年只有业务稳定压力的时候头疼和失眠本身也会有,只是很偶发,没那么严重,后续个人如果能放下业务稳定性压力,也许会好转,当然前提时能放下。

此图像的alt属性为空;文件名为image.png
虽然不那么准确,但是可以看到趋势,这是在安眠药和酒精麻醉下才达到的,否则会差的更多

AI焦虑问题,实话实说,AI在25年进步还是很大的,从最基本的问答机器人进化到能做一些稍复杂的系列任务的智能体,比较简单或比较独立的代码和流程问题都处理的效果也很不错了,当然比较复杂的大型代码目前还是差强人意的程度也没有达到(比如claude-code-opus-4.5/4.6等效果依然不行),还是需要人工或其他方式不停的拆解任务或修改提示,才能达到比较好的效果。不过按照这两年的进化趋势,说不定26年就都能做得比较好了。


目前AI的使用上,除了一些华而不实的玩具外,主要还是先来搭个框架,做到了6、70分,人工再补充完善到7、80分还是可以的,还是能节省具体的写代码或搭框架的很多时间,只是将之前大部分边写边完善的步骤变换了下,变成了先写好比较完善的流程提示词,再和AI交互逐步完善,虽然AI的进一步完善很多时候的效果就不行了,最后这20%会很依赖人工来完善,如果继续依赖AI完善,浪费的时间和token实在是得不偿失。


总体来说目前AI极大缩小了初级牛马和中级牛马的差距,高级牛马的地位也岌岌可危。但是那个但是呢,作为牛马并不会变得更轻松,AI跑任务的时候,牛马就在并行干其他事情了,总体好像产出会变高,但是对个人的压力不会有任何的减轻,反而会更重了。

F-Stack ff_rss_check()优化介绍

1. 概述

本文档旨在介绍 F-Stack 中对 ff_rss_check() 函数的一项重要优化。该优化通过引入一个预计算的静态端口查找表,显著提升了应用程序作为客户端主动发起大量短连接时的性能,解决了原 ff_rss_check() 函数在高并发场景下可能成为性能瓶颈的问题。

核心优化提交:e54aa4317b5d81f9f8643e491d8ec0ec1e72282a

2. 背景与问题分析

ff_rss_check() 函数在 F-Stack 中负责一个关键任务:当应用程序作为客户端发起新的 TCP 连接时,为其分配合适的本地源端口。

  • 原有实现机制: 每次需要建立新连接时,都会动态调用 ff_rss_check()。该函数会根据目标服务的 IP 和端口(4元组信息),通过一个哈希计算来选择一个 RSS(Receive Side Scaling)友好的本地端口,以确保数据包能被高效地分发到正确的 CPU 核心上处理。
  • 性能瓶颈: 在需要频繁创建短连接(例如,处理 HTTP 请求且未开启 keep-alive)的场景下,每次连接建立都需要执行一次或多次端口选择和冲突检查。这个过程涉及多次的toeplitz_hash计算(平均每次耗时约300+tsc,平均次数至少为该端口的总队列(进程)数),在高并发压力下,会消耗可观的 CPU 资源,并成为限制连接建立速率的瓶颈。

3. 优化方案:静态 RSS 端口表

为了克服上述性能问题,优化方案的核心思想是:变“动态计算”为“静态查找”

  1. 预初始化静态表
    • 在应用程序启动阶段(ff_init() 时),根据配置文件中的预定义规则,预先计算并初始化一个静态的端口查找表(ff_rss_tbl),。
    • 该表包含了预先调用ff_rss_check()计算好的哪些本地端口发出去的包可以返回本进程对应的网卡队列,表中包含 {{远程地址,远程端口}:{本地地址, [所有可用本地端口]}}组合,以及一些辅助数据结构,如可用端口的起始、结束索引,上次选择的到的端口索引等。
  2. 高效的端口选择
    • 当需要建立新连接时,首先尝试从这个预先生成的静态表中进行查找。
    • 如果能找到一个与当前连接目标地址/端口匹配且未被占用的本地端口,则直接使用它。这个过程几乎是无锁且开销极低的(平均耗时约100-250tsc)
    • 最重要的是,静态查找表无需多次选择并计算RSS,只需要查找一次本地端口即可保证远程回包可以回到本进程的队列进行处理,极大提升了选择源端口的效率,进而提升总体的QPS性能。
  3. 优雅降级
    • 如果静态表中所有符合条件的端口都已被占用或该四元组未配置静态查找表,系统会自动降级到原有的 ff_rss_check() 动态计算流程,确保功能的正确性。

4. 配置说明

此功能需要通过配置文件(例如 config.ini)手动开启和定义。相关配置段如下:

# 启用或禁用静态 ff_rss_check 表。
# 若启用,F-Stack 将在 APP 启动时初始化该表。
# 此后当 APP 作为客户端连接服务器时,会首先尝试从该表中选择本地端口。
[ff_rss_check_tbl]
# 启用开关:0-禁用,1-启用。默认为 0。
enable = 1
# 定义端口分配规则(4元组)。
# 格式:<网卡端口ID> <本地地址 daddr> <远程地址 saddr> <远程端口 sport>
# 单个四元组内用空格分隔,多个四元组之间用分号分隔。
# 注意:saddr/sport 二元组最多支持 16 个,同一 saddr/sport 下最多支持 4 个 daddr。
# 因此,最多支持 64 种组合,超出的配置将被忽略。
rss_tbl=0 192.168.1.10 192.168.2.10 80;0 192.168.1.10 192.168.2.11 80;0 192.168.1.10 192.168.2.12 80

配置参数详解

  • enable: 总开关,必须设置为 1 才能启用此优化功能。
  • rss_tbl: 定义端口分配规则。每一条规则指定了网卡端口ID用于获取网卡的队列配置,对于特定的目标服务(saddr + sport),使用哪个预先分配好的本地地址(daddr)。

5. 性能提升

根据内部测试数据,该优化带来了显著的性能提升:

  • 测试场景: 客户端(wrk)向F-Stack Nginx发起http请求,使用长连接。F-Stack Nginx设置反向代理,作为客户端,频繁向多个远程服务器发起短 TCP 连接(未使用 keep-alive)。示例图如下所示:
  • 测试数据:如下表所示
  • 常规 QPS 提升约 2-6%左右,且提升的比列会随着进程数的增加而增加,因为进程数越多,原有的ff_rss_check()需要计算的平均次数就越多。
  • 某些特殊场景下提升可以达到35%以上,主要是因为在某些进程数的配置下,原始动态计算方式,随机选择源端口的次数会远超数学期望的总进程数+少量次数,导致需要大量额外调用ff_rss_check()进行计算,消耗了大量CPU资源。
    • 【注意】不同的upstream服务器配置(数量、IP、端口等)可能会导致不同的进程数存在类似问题,在本测试场景下配置8或16进程时,会出现该问题。导致该问题的具体原因则尚未完全明确,查看相关静态表、端口是否正在被占用和请求测试都正常。
    • 如下表所示分别为8和12进程Nginx调用ff_rss_check()次数的统计,可以看到8核时的随机选择次数明显超出了数学期望的(应该在8-12次左右),16核同理。
      • 其中randomtime表示内核参数net.inet.ip.portrange.randomtime的设置,因为F-Stack框架的特性,作为客户端选择源端口时此时完全随机选择效果更好,且F-Stack对随机性要求并不高,早前已经替换了性能好好很多的伪随机函数。
  • perf top截图

8进程间歇性随机(大部分不随机)时ff_rss_check()调用次数太多,热点很高

8进程完全随机,ff_rss_check()调用次数热点有一定降低

12进程,ff_rss_check()的热点则大幅下降

  • 8进程间歇随机和完全随机选择源端口QPS性能对比

6. 其他参数优化调整

F-Stack对应调整了部分参数,具体如下,其他业务如何配置可以根据自己业务的具体特点灵活调整

[freebsd.sysctl]
net.inet.tcp.delayed_ack=1 # 可以提升大并发的吞吐量
net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.finwait2_timeout=5000
net.inet.tcp.maxtcptw=128 # 尽快释放TIME_WAIT状态的端口,增加空闲端口,减少in_pcblookup_local()的调用次数,进而减少ff_rss_check()的调用次数
net.inet.ip.portrange.randomized=1
# Always do random while connect to remote server.
# In some scenarios of F-Stack application, the performance can be improved to a certain extent, ablout 5%.
net.inet.ip.portrange.randomtime=0 # 对某些特定配置条件下原动态计算ff_rss_check()方式有一定性能提升

7. 总结

本次对 ff_rss_check() 的优化是 F-Stack 追求性优化能的一个典型例子。它通过以下方式解决了核心瓶颈:

  1. 空间换时间: 使用预计算的静态表换取运行时动态计算的开销。
  2. 减少调用次数: 极大降低了端口选择时的ff_rss_check()和in_pcblookup_local()的重试调用次数,实现总体系统2-6%,特殊极限场景35%以上的总体系统性能提升。
  3. 保证兼容: 通过降级机制保障了在任何情况下的功能正确性。

建议启用此功能的场景: 所有使用 F-Stack 作为客户端、需要高频率创建新连接(特别是短连接)的应用程序。启用后,只需在配置文件中预先定义好常用的目标服务地址,即可获得显著的性能收益。

【AI使用声明】本文初始版本使用DeepSeek-V3.1生成,然后进行人工调整;示意图片由元宝根据人工提供的提示词生成和修改