|
| 1 | +# F-Stack LD_PRELOAD 无锁环形队列改造 — 需求规格文档 |
| 2 | + |
| 3 | +> **文档编号**: SPEC-001 |
| 4 | +> **版本**: v1.0 Draft |
| 5 | +> **日期**: 2026-03-27 |
| 6 | +> **状态**: 已审核 by fengbojiang 2026-03-27 ,大方向没问题,修改了一点细节 |
| 7 | +> **范围**: `/data/workspace/f-stack/adapter/syscall/` |
| 8 | +
|
| 9 | +--- |
| 10 | + |
| 11 | +## 1. 问题域分析 |
| 12 | + |
| 13 | +### 1.1 当前架构概述 |
| 14 | + |
| 15 | +F-Stack 的 LD_PRELOAD 模块(`libff_syscall.so`)通过劫持应用程序的 socket 系统调用,将其转发给 fstack 实例进程处理。APP 侧和 fstack 侧之间通过 **DPDK Hugepage 共享内存**中的 `ff_so_context` 结构进行 IPC 通信。 |
| 16 | + |
| 17 | +当前同步机制采用 **三层同步**: |
| 18 | + |
| 19 | +| 层 | 机制 | 位置 | |
| 20 | +|---|---|---| |
| 21 | +| 状态机 | `FF_SC_IDLE → FF_SC_REQ → FF_SC_REP → FF_SC_IDLE` | `ff_socket_ops.h:73-75` | |
| 22 | +| 互斥锁 | `rte_spinlock_t lock` | `ff_socket_ops.h:103` | |
| 23 | +| 信号量 | `sem_t wait_sem` (POSIX 跨进程信号量) | `ff_socket_ops.h:111` | |
| 24 | + |
| 25 | +### 1.2 信号量机制的性能瓶颈 |
| 26 | + |
| 27 | +#### 1.2.1 内核态系统调用开销 |
| 28 | + |
| 29 | +POSIX 信号量 `sem_wait()`/`sem_post()` 在 Linux 上通过 `futex` 系统调用实现。每次调用涉及: |
| 30 | +- **用户态 → 内核态切换**:约 100-200ns 开销 |
| 31 | +- **futex 唤醒路径**:`FUTEX_WAKE` 需唤醒等待队列中的线程,涉及内核调度器 |
| 32 | +- **跨进程信号量**(`sem_init(&sc->wait_sem, 1, 0)` 中 `pshared=1`)额外增加了共享内存 futex 的查找开销 |
| 33 | + |
| 34 | +在高频 syscall 场景下(如 Nginx 的 epoll_wait 短超时),这些开销被放大。 |
| 35 | + |
| 36 | +#### 1.2.2 sem_timedwait 超时精度问题 |
| 37 | + |
| 38 | +当前代码使用 `CLOCK_REALTIME` 计算超时(`ff_hook_syscall.c:2194-2207`): |
| 39 | +```c |
| 40 | +clock_gettime(CLOCK_REALTIME, &abs_timeout); |
| 41 | +abs_timeout.tv_sec += timeout / 1000; |
| 42 | +abs_timeout.tv_nsec += (timeout % 1000) * 1000 * 1000; |
| 43 | +``` |
| 44 | +- `CLOCK_REALTIME` 受 NTP 调时影响,可能导致意外超时或延迟 |
| 45 | +- `sem_timedwait` 的最小唤醒粒度受内核调度器 tick 影响(通常 1-4ms) |
| 46 | +- 超时后的竞态:注释明确指出(第 2224-2229 行)`sem_timedwait` 超时但 fstack 仍可能 `sem_post`,导致下次 `sem_timedwait` 立即返回,读取过期结果 |
| 47 | +
|
| 48 | +#### 1.2.3 O(n) 请求遍历 |
| 49 | +
|
| 50 | +`ff_handle_each_context()`(`ff_socket_ops.c:569-638`)在每次轮询循环中遍历 **所有** `ff_so_context`: |
| 51 | +```c |
| 52 | +for (i = 0; i < ff_so_zone->count; i++) { |
| 53 | + struct ff_so_context *sc = &ff_so_zone->sc[i]; |
| 54 | + if (ff_so_zone->inuse[i] == 0) continue; |
| 55 | + if (sc->status == FF_SC_REQ) { |
| 56 | + ff_handle_socket_ops(sc); |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | +当 `SOCKET_OPS_CONTEXT_MAX_NUM = 32` 时,每次循环都需脏读 32 个 sc 的 status 字段,即使绝大多数处于 IDLE 状态。 |
| 61 | + |
| 62 | +#### 1.2.4 alarm_event_sem 补偿机制的脆弱性 |
| 63 | + |
| 64 | +`alarm_event_sem()`(`ff_hook_syscall.c:3235-3252`)是一个补偿机制,用于处理 APP 进入 `sem_wait` 后 fstack 来不及 `sem_post` 的情况。但该机制仅在 `main_stack.c:75` 被真正调用,其余 4 个示例程序中均被注释掉,表明该机制不稳定且难以正确使用。 |
| 65 | + |
| 66 | +#### 1.2.5 模式分裂 |
| 67 | + |
| 68 | +当前存在两种互斥的 epoll_wait 实现路径: |
| 69 | +- **信号量模式**(默认):使用 `sem_wait`/`sem_post`,CPU 利用率低但延迟高 |
| 70 | +- **轮询模式**(`FF_PRELOAD_POLLING_MODE`):纯 busy-poll,延迟低但 CPU 100% |
| 71 | + |
| 72 | +两种模式通过编译宏分裂,无法在运行时切换,且轮询模式不支持 `FF_KERNEL_EVENT`。 |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## 2. 功能需求 |
| 77 | + |
| 78 | +### FR-001: 无锁请求队列 |
| 79 | + |
| 80 | +**描述**: 使用 DPDK `rte_ring`(SPSC 模式)替代 `sem_t wait_sem` 实现 APP → fstack 的请求通知。 |
| 81 | + |
| 82 | +**详细要求**: |
| 83 | +- APP 侧将请求的 `ff_so_context` 指针入队到请求 ring |
| 84 | +- fstack 侧从请求 ring 出队获取待处理请求(替代 O(n) 遍历) |
| 85 | +- ring 分配在 DPDK Hugepage 共享内存上,支持跨进程访问 |
| 86 | + |
| 87 | +**影响文件**: `ff_socket_ops.h`, `ff_so_zone.c`, `ff_hook_syscall.c`, `ff_socket_ops.c` |
| 88 | + |
| 89 | +### FR-002: 无锁响应队列 |
| 90 | + |
| 91 | +**描述**: 使用 DPDK `rte_ring`(SPSC 模式)替代 `sem_post`/`sem_wait` 实现 fstack → APP 的响应通知。 |
| 92 | + |
| 93 | +**详细要求**: |
| 94 | +- fstack 侧处理完请求后将 `ff_so_context` 指针入队到响应 ring |
| 95 | +- APP 侧从响应 ring 出队获取处理结果(替代 `sem_wait`/`sem_timedwait`) |
| 96 | +- 支持超时等待(基于 `rte_rdtsc()` 高精度计时) |
| 97 | + |
| 98 | +**影响文件**: `ff_hook_syscall.c`, `ff_socket_ops.c` |
| 99 | + |
| 100 | +### FR-003: 可配置等待策略 |
| 101 | + |
| 102 | +**描述**: 提供三种可配置的 APP 侧等待策略,替代当前的信号量阻塞和编译时轮询模式的二选一。 |
| 103 | + |
| 104 | +| 策略 | 描述 | 适用场景 | |
| 105 | +|---|---|---| |
| 106 | +| **busy-poll** | 持续调用 `rte_ring_dequeue` | 超低延迟,CPU 100% | |
| 107 | +| **yield-poll** | 每次 dequeue 失败后调用 `rte_pause()` / `sched_yield()` | 低延迟,CPU 占用适中 | |
| 108 | +| **eventfd** | dequeue 失败后通过 `eventfd` 阻塞等待 | 低 CPU 占用,延迟稍高 | |
| 109 | + |
| 110 | +**详细要求**: |
| 111 | +- 等待策略在运行时可配置(通过环境变量或配置),而非编译时宏 |
| 112 | +- 默认策略为 `yield-poll`,兼顾延迟和 CPU 占用 |
| 113 | + |
| 114 | +### FR-004: 替代 alarm_event_sem |
| 115 | + |
| 116 | +**描述**: 使用 ring 机制替代当前不稳定的 `alarm_event_sem()` 补偿机制。 |
| 117 | + |
| 118 | +**详细要求**: |
| 119 | +- 在 ring 模式下,fstack 侧在处理完阻塞型操作(kevent timeout=NULL, epoll_wait timeout=-1)后,无论是否有事件返回,均向响应 ring 入队 |
| 120 | +- 消除 `need_alarm_sem` 全局变量及其复杂的设置/清除逻辑 |
| 121 | + |
| 122 | +**影响文件**: `ff_hook_syscall.c`, `ff_socket_ops.c`, `ff_adapter.h`, `main_stack.c` |
| 123 | + |
| 124 | +### FR-005: ff_handle_each_context 优化 |
| 125 | + |
| 126 | +**描述**: 将 fstack 侧的 O(n) sc 遍历改为 O(1) ring 出队。 |
| 127 | + |
| 128 | +**详细要求**: |
| 129 | +- `ff_handle_each_context()` 改为从请求 ring 批量出队(`rte_ring_dequeue_burst`) |
| 130 | +- 消除对 `ff_so_zone->lock` 全局 spinlock 的持有(当前整个循环期间持锁) |
| 131 | +- 保持 `pkt_tx_delay` 时间窗口内多次轮询的行为 |
| 132 | + |
| 133 | +**影响文件**: `ff_socket_ops.c` |
| 134 | + |
| 135 | +### FR-006: 向后兼容 |
| 136 | + |
| 137 | +**描述**: 通过编译宏 `FF_USE_RING_IPC` 控制新旧方案切换。 |
| 138 | + |
| 139 | +**详细要求**: |
| 140 | +- 定义宏 `FF_USE_RING_IPC` 时使用无锁 ring 方案 |
| 141 | +- 未定义时保持当前信号量方案不变 |
| 142 | +- 后续稳定后可移除旧方案代码 |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## 3. 非功能需求 |
| 147 | + |
| 148 | +### NFR-001: 延迟 |
| 149 | + |
| 150 | +| 指标 | 当前基线 (sem) | 目标 (ring) | |
| 151 | +|---|---|---| |
| 152 | +| 单次普通 syscall RTT | 2-5 μs | < 1 μs | |
| 153 | +| epoll_wait 唤醒延迟 | 2-10 μs | < 2 μs | |
| 154 | +| 超时精度 | 毫秒级 | 微秒级 | |
| 155 | + |
| 156 | +### NFR-002: 吞吐量 |
| 157 | + |
| 158 | +- 在 Nginx 600B body 长连接基准测试中,ring 方案的 RPS 不低于 sem 方案 |
| 159 | +- 在相同 CPU 核数下,ring 方案的吞吐量提升 ≥ 10% |
| 160 | + |
| 161 | +### NFR-003: CPU 占用 |
| 162 | + |
| 163 | +- `yield-poll` 模式下,空闲时 CPU 占用不超过 sem 方案的 2 倍 |
| 164 | +- `eventfd` 模式下,空闲时 CPU 占用与 sem 方案持平 |
| 165 | +- `busy-poll` 模式下允许 CPU 100%(与现有 `FF_PRELOAD_POLLING_MODE` 行为一致) |
| 166 | + |
| 167 | +### NFR-004: 内存开销 |
| 168 | + |
| 169 | +- 每个 fstack 实例新增内存开销 < 64KB(2 个 ring × 64 entries × cache line) |
| 170 | +- ring 结构在 Hugepage 上分配,不增加常规内存使用 |
| 171 | + |
| 172 | +### NFR-005: 可维护性 |
| 173 | + |
| 174 | +- 新增代码量 < 500 行(不含测试和文档) |
| 175 | +- 保持现有代码风格(GNU C、4 空格缩进、BSD 函数命名) |
| 176 | +- 关键路径有注释说明 ring 操作语义 |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## 4. 约束条件 |
| 181 | + |
| 182 | +### C-001: 运行模式兼容性 |
| 183 | + |
| 184 | +新方案必须兼容以下 5 种运行模式: |
| 185 | + |
| 186 | +| 模式 | 编译宏 | 当前状态 | Ring 方案要求 | |
| 187 | +|---|---|---|---| |
| 188 | +| PIPELINE(默认) | 无 | 信号量同步 | 双 ring 替代 | |
| 189 | +| RTC (thread_socket) | `FF_THREAD_SOCKET` | 每线程独立 sc | 每线程独立 ring pair | |
| 190 | +| FF_KERNEL_EVENT | `FF_KERNEL_EVENT` | 同时调用 fstack + kernel epoll | ring 出队 + linux_epoll_wait | |
| 191 | +| FF_MULTI_SC | `FF_MULTI_SC` | 类 SO_REUSEPORT | 多 ring zone | |
| 192 | +| FF_PRELOAD_POLLING_MODE | `FF_PRELOAD_POLLING_MODE` | 纯轮询 | 统一为 busy-poll 策略 | |
| 193 | + |
| 194 | +### C-002: 共享内存要求 |
| 195 | + |
| 196 | +- ring 必须在 DPDK Hugepage 上分配(`rte_ring_create` 使用 `rte_memzone_reserve`) |
| 197 | +- ring name 必须全局唯一(格式: `ff_sc_req_ring_%d` / `ff_sc_rsp_ring_%d`) |
| 198 | +- APP 侧通过 `rte_ring_lookup` 查找已创建的 ring |
| 199 | + |
| 200 | +### C-003: DPDK 版本兼容 |
| 201 | + |
| 202 | +- 基于 F-Stack 当前使用的 DPDK 版本(目录 `dpdk/` 下的版本) |
| 203 | +- 使用 `rte_ring` 的 SPSC 模式(`RING_F_SP_ENQ | RING_F_SC_DEQ`) |
| 204 | + |
| 205 | +### C-004: 编译系统 |
| 206 | + |
| 207 | +- 新增编译宏 `FF_USE_RING_IPC` 在 `Makefile` 中控制 |
| 208 | +- 不引入新的外部依赖(仅使用已有的 DPDK 库) |
| 209 | + |
| 210 | +### C-005: 错误处理 |
| 211 | + |
| 212 | +- ring 满时(enqueue 失败):记录错误日志,等待重试 |
| 213 | +- ring 创建失败:回退到信号量方案(如果启用了 `FF_USE_RING_IPC`) |
| 214 | +- 进程异常退出:ring 中残留的 sc 指针需在 detach 时清理 |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## 5. 验收标准 |
| 219 | + |
| 220 | +### AC-001: 功能验收 |
| 221 | + |
| 222 | +- [ ] 5 个 helloworld 示例程序全部编译通过并正确运行 |
| 223 | + - `helloworld_stack` |
| 224 | + - `helloworld_stack_thread_socket` |
| 225 | + - `helloworld_stack_epoll` |
| 226 | + - `helloworld_stack_epoll_thread_socket` |
| 227 | + - `helloworld_stack_epoll_kernel` |
| 228 | +- [ ] 使用 `FF_USE_RING_IPC` 编译后,所有示例的 echo server 能正确响应客户端请求 |
| 229 | +- [ ] 不使用 `FF_USE_RING_IPC` 编译后,行为与修改前完全一致 |
| 230 | + |
| 231 | +### AC-002: 性能验收 |
| 232 | + |
| 233 | +- [ ] 单次 syscall RTT 在 ring 方案下 ≤ 1μs(yield-poll 模式) |
| 234 | +- [ ] Nginx 600B body 长连接 RPS ≥ sem 方案的性能 |
| 235 | +- [ ] 三种等待策略可正确切换,CPU 占用符合 NFR-003 |
| 236 | + |
| 237 | +### AC-003: 稳定性验收 |
| 238 | + |
| 239 | +- [ ] 连续运行 24 小时无内存泄漏(通过 DPDK memzone 统计验证) |
| 240 | +- [ ] 高并发(32 个 APP 线程)场景下无死锁、无数据错乱 |
| 241 | +- [ ] APP 进程异常退出后,fstack 能正确清理 ring 中的残留请求 |
| 242 | + |
| 243 | +### AC-004: 代码质量 |
| 244 | + |
| 245 | +- [ ] 无新增编译警告(`-Wall -Werror`) |
| 246 | +- [ ] 所有新增函数有注释说明参数和返回值 |
| 247 | +- [ ] 修改的函数保持原有代码风格 |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## 6. 信号量代码位置索引 |
| 252 | + |
| 253 | +> 以下表格列出所有需要在 ring 方案中修改或移除的信号量相关代码点。 |
| 254 | +
|
| 255 | +### 6.1 信号量 API 调用 |
| 256 | + |
| 257 | +| 调用 | 文件 | 行号 | 说明 | |
| 258 | +|---|---|---|---| |
| 259 | +| `sem_init` | `ff_so_zone.c` | 101 | 初始化跨进程信号量 | |
| 260 | +| `sem_timedwait` | `ff_hook_syscall.c` | 2265 | epoll_wait 超时等待 | |
| 261 | +| `sem_wait` | `ff_hook_syscall.c` | 2270 | epoll_wait 无限等待 | |
| 262 | +| `sem_timedwait` | `ff_hook_syscall.c` | 2555 | kevent 超时等待 | |
| 263 | +| `sem_wait` | `ff_hook_syscall.c` | 2558 | kevent 无限等待 | |
| 264 | +| `sem_post` | `ff_hook_syscall.c` | 3244 | alarm 补偿唤醒 | |
| 265 | +| `sem_post` | `ff_socket_ops.c` | 557 | fstack 侧处理完成通知 | |
| 266 | + |
| 267 | +### 6.2 信号量控制变量 |
| 268 | + |
| 269 | +| 变量 | 文件 | 行号 | 说明 | |
| 270 | +|---|---|---|---| |
| 271 | +| `sem_flag` | `ff_socket_ops.c` | 20 | 控制是否 sem_post | |
| 272 | +| `sem_flag` 赋值 | `ff_socket_ops.c` | 313-364 | epoll_wait/kevent 返回后设置 | |
| 273 | +| `sem_flag` 读取 | `ff_socket_ops.c` | 551, 555 | 决定是否唤醒 APP | |
| 274 | +| `need_alarm_sem` | `ff_hook_syscall.c` | 248 | alarm 补偿标志 | |
| 275 | +| `need_alarm_sem` 设置 | `ff_hook_syscall.c` | 2234, 2534 | 进入 sem_wait 前设置 | |
| 276 | +| `need_alarm_sem` 清除 | `ff_hook_syscall.c` | 2276, 2564, 3245 | sem_wait 返回/alarm 后清除 | |
| 277 | + |
| 278 | +### 6.3 结构体成员 |
| 279 | + |
| 280 | +| 成员 | 文件 | 行号 | 说明 | |
| 281 | +|---|---|---|---| |
| 282 | +| `sem_t wait_sem` | `ff_socket_ops.h` | 111 | 需替换为 ring 相关字段 | |
| 283 | +| `#include <semaphore.h>` | `ff_socket_ops.h` | 5 | ring 方案下可移除 | |
| 284 | + |
| 285 | +--- |
| 286 | + |
| 287 | +## 附录 A: 术语表 |
| 288 | + |
| 289 | +| 术语 | 说明 | |
| 290 | +|---|---| |
| 291 | +| **sc** | `ff_so_context` 的缩写,APP 与 fstack 之间的通信上下文 | |
| 292 | +| **SPSC** | Single Producer Single Consumer,单生产者单消费者模式 | |
| 293 | +| **RTT** | Round-Trip Time,请求到响应的完整往返时间 | |
| 294 | +| **futex** | Linux 内核提供的快速用户空间互斥原语,sem_wait 的底层实现 | |
| 295 | +| **Hugepage** | DPDK 使用的大页内存,跨进程共享 | |
| 296 | +| **rte_ring** | DPDK 提供的无锁环形队列实现 | |
| 297 | +| **drain_tsc** | fstack 轮询循环的时间窗口,由 `pkt_tx_delay` 参数控制 | |
| 298 | + |
| 299 | +## 附录 B: 参考文档 |
| 300 | + |
| 301 | +1. F-Stack adapter/syscall/README.md — 现有 LD_PRELOAD 模块文档 |
| 302 | +2. DPDK rte_ring API — https://doc.dpdk.org/api/rte__ring_8h.html |
| 303 | +3. VPP memif — https://docs.fd.io/vpp/21.06/dc/dea/libmemif_doc.html |
| 304 | +4. VPP svm_msg_q — FD.io VPP Session Layer 共享内存消息队列 |
| 305 | +5. F-Stack 三层架构文档 — `/data/workspace/f-stack/docs/` |
0 commit comments