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 等)