全网口架构防卡死约束 (p4 驱动 / p5 服务 落地必读)
审查智能体: 全网口架构防卡死审查 (只读审查 + 落档) 日期: 2026-08-14 范围:
Darra_Pnet_Kernel\Windows\Pnet\(驱动) +Darra_Pnet_Service\(服务) +Darra_Pnet_Core\main\core\platform\pnal_port.c/pnet_stack.c(native 数据面) 背景: 用户裁定全网口架构 (驱动全口 RX + 按源回发 TX + 服务不再单卡绑定), 同时重申"防止卡死"。p4 (驱动) 与 p5 (服务) 并行实施中, 本文档给出现行代码的卡死风险点与必须遵守的约束清单。 方法: 逐文件 Read + 跨仓 grep, 全部结论附 文件:行 证据。本 agent 未改任何代码。
一、现存卡死风险点清单 (证据行)
风险 1 [P0·服务] 同步 IOCTL 全链无超时 — 历史"启动尝试无完成"家族的结构性未修
PnetDriverBridge.cs:1815DeviceIoControl(..., IntPtr.Zero)— lpOverlapped 恒为 NULL = 同步无超时。驱动一旦在 IOCTL 路径挂住 (CyclicStop/Quiesce 未来回归、多口新路径死锁), 调用线程永久阻塞。PnetRuntime.cs:343/:1160— StartCoreFresh/StopCore 全程持_lifecycleGate, 且PnetApiServer的 /api/start、/api/stop、/api/diag 全部经同一锁 (PnetRuntime.cs:283/318/1259/1468/1516/1561)。IOCTL 挂起 → 生命周期锁永久被占 → 整个服务 API 面 + SCM 停止全部卡死。- 历史对照:
docs\本地自连验证.md:97-105记录了 05:25 等时段反复出现的BindAdapterIOCTL 失败 (1168) 与数据面降级; "启动尝试 32 分钟无完成 = IOCTL 挂起" 即本风险的实测形态。目前代码没有任何超时/取消兜底, 全网口扩大 IOCTL 面后此风险放大。 - pnal 侧同款:
pnal_port.c:1124(SEND_CONTROL) /:1149(RECEIVE_CONTROL) /:1218(MAP_SHARED) 三处DeviceIoControl(..., NULL)同步无超时。
风险 2 [P0·native] INFINITE join 依赖"内部全有界"假设
pnal_port.c:1849-1853rx 线程WaitForSingleObject(handle->rx_thread, INFINITE)— rx 主循环内就是RECEIVE_CONTROL轮询 (pnal_port.c:1612-1622, 1~2ms 节奏)。若驱动侧该 IOCTL 出现任何阻塞 (全网口后控制环/锁改坏), rx 线程永挂 → join 永挂 →pnet_stack_stop永挂 → 服务停止卡死。pnet_stack.c:1240pnet_stack_stop周期线程同样INFINITEjoin, 注释自述前提是"收发均为有界调用" (pnet_stack.c:1238-1240)。p4 若给 rx/周期路径加任何可阻塞调用, 必须先打破此前提。
风险 3 [P1·驱动] 单口 RX 门 — 全网口移除处的防护必须完整保留
DarraRT_Pnet_Nic.c:301-310现行 RX 门:adapter != pnet->SelectedAdapter→ 立即返还 NBL。p4 移除该门后, 每口仍必须保留同处的Closing/pnet->Stopping/BindingHandle判空 (Nic.c:304-308) — 这些是 0x3B/0xD1 家族 (卸载竞态返还 NBL) 的防线, 不能随门一起删。- 全局
RxActive/RxIdleEvent计数 (Nic.c:294-296, 395-397) 是全口共用的 Quiesce 等待源, 多口仍保持全局计数即可, 但 Quiesce 等待必须覆盖所有口 (见约束 C11)。
风险 4 [P1·驱动] SelectedAdapter 单点耦合 — 全网口每个引用点都是潜在卡死/丢帧/蓝屏源
单点指针被 7 处语义使用, 全网口必须逐处换成"每口状态 + 源口路由", 漏一处即回归历史家族:
| 引用点 | 证据 | 全网口影响 |
|---|---|---|
| TX LinkUp 门 | Nic.c:975 adapter->LinkUp == 0 拒发 | 必须改为源口 LinkUp; 沿用单口会把任意口掉线放大为全停发 |
| PPM 源 MAC | Iocr.c:223-224 extension->SelectedAdapter->Mac | 每口 MAC 不同, 模板必须按口建 (或每口一份模板) |
| 控制帧目的 MAC 门 | Iocr.c:513-517 只认 SelectedAdapter MAC | 多口必须认所有启用口 MAC + 广播 + 组播 AR MAC (见风险 5) |
| Status/PnP ForceSafe | Nic.c:492/522 仅 SelectedAdapter 掉线才转 SAFE | 多口必须定义"承载 AR 的口掉线"策略 (见约束 C9) |
| CLOSE 自愈复位 | Entry.c:102-103 只 CyclicStop + NicDeselect 单口 | 服务崩溃后多口残留半死 = 1168/半死家族重触发 (见约束 C11) |
| NicSelect 单 Selected 置位 | Nic.c:931-933 | 多口 BIND 流程需重定义 (见约束 C12) |
| unbind 清理 | Nic.c:219-230 只清 SelectedAdapter | 陈旧 Selected=1 放行是历史 0x3B 家族真因 (注释即证据), 多口清理必须逐口归零 |
风险 5 [P1·驱动] 控制环 64 槽共享 + 防灌门只覆盖单口 — 全网口必重触发"控制面软卡死"
- 控制环仅 64 槽 (
Internal.h:64), 入环丢帧判定(write-read) >= 64(Iocr.c:469)。2026-08-13 的目的 MAC 门 (Iocr.c:494-532) 就是为了防"同网段其它 PROFINET 设备的周期组播帧灌满环 → 本设备真实 connect/alarm/RPC 帧被丢 → 控制器连不上设备" (注释原话)。 - 全网口后: 每个口的外来 0x8892 组播/DCP 广播都会进同一 64 槽环, 灌满概率 × N。DCP Identify 广播 (0x8892, FrameID 0xFEFD, 目的 01-0E-CF-00-00-00, 见
docs\本地自连验证.md:125) 正是广播 — 多口下必到所有口。 - DCP EtherType 0xFEFE 当前未注册 (
Nic.c:127-128FrameTypeArray 仅 0x8892/0x88CC)。p4 若为 DCP 注册新帧型, 必须同时把防灌门覆盖 DCP, 否则等于打开新灌入面。 - LLDP (0x88CC) 现行不判目的 MAC (
Iocr.c:577-579直接入环) — 多口下每口邻居 LLDP 全部入环, 需评估是否每口限速。
风险 6 [P1·ABI] SEND_CONTROL/RECEIVE_CONTROL 无口字段 — 按源回发/DCP 应答缺路由依据
Internal.h:287-293DARRT_PNET_CONTROL_FRAME { Header, Kind, Length, TimestampTsc, Data[1522] }— 无端口字段。pnal 发送控制帧pnal_port.c:1104-1128只能走驱动"当前单口"。全网口后: DCP 应答、connect/RPC 应答必须从入口出; 要么扩 ABI 加口字段 (三侧: 驱动 Internal.h / 服务 PnetDriverBridge / native pnal_port.c 同步 + 版本门), 要么驱动侧记录"AR/会话 → 源口"映射。任何一侧先改不版本门 = 静默 ABI 断裂 (历史上 0xB2x access 位错位已栽过一次, 见docs\全栈审计_驱动层.md:79)。
风险 7 [P1·驱动] TX 槽全局 per-IOCR — 全网口按源回发需每口独立槽组
- 现行每 IOCR 双缓冲 Tx[2] + 控制帧 ControlTx[16] (
Internal.h:380-415),NdisSendNetBufferLists绑定单一adapter->BindingHandle(Nic.c:1010)。同一 NBL 不能同时在两个口在途 — 全网口按源回发 = 每 IOCR × 每口 × 双缓冲 独立槽组。 SendComplete的 NBL 匹配循环 (Nic.c:424-439) 必须同步扩到全口槽, 否则 InFlight 清不掉 → 槽耗尽 → 周期发送永久STATUS_DEVICE_BUSY丢帧 (软卡死)。InFlight三态语义 (-1/0/1) 已有审计指出脆弱 (docs\全栈审计_驱动层.md:178), 扩槽组时顺带统一置位语义 (直置 1), 防新代码踩雷。
风险 8 [P1·语义] 主站切换/双主站场景的现行拒绝路径
- IOCR 配置单一 DestinationMac/SourceMac (
Internal.h:276-277); CPM 收帧校验源 MAC (Iocr.c:343-348) 与配置期目的 MAC = SelectedAdapter MAC 校验 (Iocr.c:697-698)。第二主站 (S2 failover / 双口双主站) 的帧会被结构校验拒绝 — 不是卡死但表现为"数据面无反应", 联调时会被误诊为卡死。 - 序号前跳已对齐 p-net (
MaxCycleAdvance=61440,Iocr.c:738-745), 主站切换不误拒序号 — 但RxCycleCounterValid单值 + 全局 SAFE 恢复 (Iocr.c:398-431) 在多口下没有"按口"语义。 - 控制帧目的 MAC 门 (
Iocr.c:513-517) 在 SelectedAdapter MAC 全零时整门跳过 (Iocr.c:517-520) — 多口下任一启用口 MAC 未提供时不能整门跳过, 否则灌入面全开。
风险 9 [P1·语义] 看门狗/Connected 全局单头 — 多口归约规则未定义
- 共享内存头单一
Connected/Heartbeat/IocrActive/IocrHealthy(Internal.h:125-163); 喂狗与 SAFE 判定全按全局掩码 (Iocr.c:907-960)。全网口后: 一口健康一口超时, 全局该 RUNNING 还是 SAFE? 规则不定义 = 虚假 SAFE 卡死 (输出清 0, 控制器判故障) 或虚假 RUNNING (超时口输出不冻结)。 - 现行 PPM-only 卡 SAFE 自愈 (
Iocr.c:939-957) 是单口逻辑 — 多口需要等价的自愈路径, 否则任一链路抖动永久卡 SAFE (历史教训原注释)。
风险 10 [P2·服务] BIND 仅 STOPPED 态 + 单名解析 — 多口流程未定义
- 驱动 BIND_ADAPTER 要求
State == STOPPED(Entry.c:226-229)。全网口若支持运行态加口, 需要新 IOCTL 或"全停重绑" — 至少当前实现必须维持"全停重绑", 禁止周期 DPC 运行中改口表 (模板构建/槽组并发竞态)。 - 服务单网卡名解析
ResolveNdisAdapterName(PnetDriverBridge.cs:1660-1758) 与单口BindAdapter(:766-832) — p5 需多口清单逐口 BIND; 一口 1168 失败现行直接整体 ConfigError 降级 (PnetRuntime.cs:499-527) — 多口应"该口降级、它口继续" (约束 C13)。
风险 11 [P2·服务] StopDataPlaneLoop 3s join 超时后继续 Dispose
PnetRuntime.cs:1772thread.Join(3s)超时后流程继续 (解除映射/回收), 数据面线程可能在解映射后访问视图 — 服务层审计已列 L1 (docs\全栈审计_服务层.md:67)。全网口后数据面路径更多, 此窗口风险上升, 需兜底 (见约束 C2)。
风险 12 [已验证安全] 驱动 RX/TX 路径 IRQL 现状 — 必须保持
- RX 回调 (
Nic.c:265-398) 在 DISPATCH 级仅取 spinlock (IocrRxLocks/StateLock/ControlRingLock/RxScratchLock), 零等待、零同步等待用户态 — 审查未发现 DISPATCH 级等待潜伏。 - 锁序铁律已成立:
StateLock > IocrRxLocks > ControlRingLock, 反向嵌套禁止 (Internal.h:453-459); IOCTL 路径持ControlMutex(FastMutex, APC_LEVEL) 不与上述 spinlock 反向嵌套; NDIS unbind 回调 PASSIVE 级取 ControlMutex 合法 (Nic.c:218)。 - 全网口新增代码不得破坏这三条 (见约束 C14)。
二、历史卡死档案对照 (家族 → 修法状态 → 全网口是否重触发)
| # | 家族 | 证据 | 状态 | 全网口重触发风险 |
|---|---|---|---|---|
| 1 | close-after-unload 悬垂 | Entry.c:76-127 CLOSE 对称 rundown + 最后句柄置位 LastCloseEvent; Entry.c:500-513 Unload 有界 3s 等待 | ✅ 已修 (2026-08-12) | 中: CLOSE 自愈复位必须扩到全部口, 否则半口残留 |
| 2 | 卸载无限等待 (ExWaitForRundownProtectionRelease 永久 / 100 次重试) | Entry.c:445-475 有界 3s 句柄轮询 + 重试 100→5 (最坏 ~22s < SCM 30s) | ✅ 已修 (2026-08-12/13) | 中: 多口使每次 CyclicStop 内含的 Quiesce 等待 ×N 口, 5 次重试预算可能 >30s — 必须并行等待或总预算封顶 (约束 C11) |
| 3 | STOP_PENDING / Stopping 锁存 → 后续 IOCTL 全 170 / create 5 | Entry.c:324-336 STOP 幂等化; Timer.c:218/231/251 出口统一清 Stopping | ✅ 已修 (2026-08-12 修复3) | 中: 多口停止路径必须同样每口幂等 + 出口统一清 Stopping |
| 4 | NicQuiesce 无界等 NBL 完成回调 (停止/卸载挂死) | Nic.c:733-775 2000ms + NdisCancel + 500ms 有界化 | ✅ 已修 (2026-08-12 修复2) | 高: 多口必须等所有口 SendIdle/RxIdle, 且 2s+500ms 有界不变、不 ×N 线性膨胀 |
| 5 | DPC 尾重臂写已释放 KTIMER/KDPC (0x3B 家族) | Timer.c:85-108 Stopping + TimerActive 双复查 | ✅ 已修 (2026-08-12) | 低: 多口不引入新重臂点即可 |
| 6 | 1168 半死绑定家族 (注册名/友好名/RequireExclusiveBinding) | Nic.c:636-687 BindForm 注册名; PnetDriverBridge.cs:1660-1758 三级名解析 | ✅ 代码已修 (roundPNETCOMPAT2, 2026-08-13) | 高: 开发机当前仍是半死绑定残留 (docs\本地自连验证.md:59-63 协议侧无 Linkage\Bind, sc restart 待授权); 全网口绑定面扩大后, BIND_CALLBACK/SELECT_RESULT 埋点 (Nic.c:85-104/869-890) 必须覆盖每口 |
| 7 | IOCTL 挂起家族 (05:25 启动尝试无完成) | docs\本地自连验证.md:97-105; 结构性根因 = 同步 DeviceIoControl 无超时 (风险 1) | ❌ 结构性未修 | 必然放大: 全网口 IOCTL 面变大, 必须修 (约束 C1) |
| 8 | 控制环灌满 → connect/alarm 帧被丢 → 控制器连不上 (控制面软卡死) | Iocr.c:494-532 目的 MAC 门 (2026-08-13) | ✅ 单口已修 | 必然重触发: 多口外来帧 ×N, 防灌门必须随 p4 同步扩口 (约束 C7) |
| 9 | 0x3B/0xD1 蓝屏家族 (NULL 参数/回环/双注册/陈旧 Selected=1 放行) | Nic.c:78-124/191-207/285-398; 陈旧 Selected 放行真因注释 Nic.c:224-229 | ✅ 已修 (2026-08-12) | 高: 全网口若把 Selected 语义改坏 (陈旧放行/清理漏口), 此家族直接重触发 |
| 10 | 虚假 SAFE / 永久卡 SAFE (PPM-only 无恢复路径) | Iocr.c:939-957 自愈 (2026-08-13) | ✅ 单口已修 | 高: 多口需要等价"按口自愈"路径 (约束 C9/C10) |
三、给 p4/p5 的防卡死约束清单 (硬约束, 违反 = 回滚)
C1 [P0] 同步 IOCTL 必须超时化 — 全链 (服务桥 14 个 + pnal 3 处)
PnetDriverBridge.SendIoControl(:1806-1839) 改为 OVERLAPPED +WaitForSingleObject(超时 5s)+ 超时CancelIoEx; 或至少把 START/STOP/BIND/GET_DIAGNOSTICS 移到_lifecycleGate之外 + 独立超时看门狗线程。- 判据: grep
DeviceIoControl全部调用点 (服务桥 +pnal_port.c:1124/1149/1218) lpOverlapped 非 NULL 或包超时; 注入挂起模拟 (驱动测试桩) 后 /api/start 5s 内返回错误而非永久挂。
C2 [P0] 生命周期锁内禁止无界调用
_lifecycleGate(PnetRuntime.cs:343) 临界区内 (StartCoreFresh/StartCoreReuse/StopCore/GetStackStats) 只允许有界调用。配合 C1 落地。StopDataPlaneLoop3s join 超时后 (PnetRuntime.cs:1772) 必须加兜底: 超时时不解除映射或强制线程退出标记 + 二次等待, 消除 use-after-unmap 窗口 (服务层审计 L1)。
C3 [P0] INFINITE join 前提条款
pnal_port.c:1853rx join 与pnet_stack.c:1240周期线程 join 保持 INFINITE 的唯一前提 = 环内调用全有界。p4 若给 rx 环/周期路径加任何可阻塞调用, 必须先同步把两处 join 改有界 + 强制退出 (TerminateThread 禁 — 用停止标志 + 有界等)。- 判据: 新增阻塞调用 diff 与两处 join 改动出现在同一 wave。
C4 [P1] 每口独立状态 — SelectedAdapter 单点拆除
SelectedAdapter(7 处引用, 见风险 4 表) 拆为每口状态:{LinkUp, Closing, Present, 启用标志, 每口 TX 槽组, 源口 AR 映射}。- 判据: 全仓 grep
SelectedAdapter仅剩兼容读取/迁移注释; 每口 MAC/名称/绑定句柄不共享。
C5 [P1] RX 门移除保留防护
- 移除单口 RX 门 (
Nic.c:301-310) 时, 每口的Closing/Stopping/BindingHandle判空与 NBL 返还路径逐字保留;RxActive/RxIdleEvent全局计数语义不变。
C6 [P1] 按源回发 TX — 每口独立槽组
- CPM 收帧记录 ingress 口 → PPM/控制帧应答从同口出; TX 槽 = 每 IOCR × 每口 × 双缓冲;
SendComplete匹配循环 (Nic.c:424-439) 覆盖全口槽;InFlight置位语义统一为直置 1 (顺带收掉docs\全栈审计_驱动层.md:178的脆弱语义)。 - 判据: 双口并发收 CPM 时两口的 PPM 均正常发、无 STATUS_DEVICE_BUSY 累积 (DroppedFrames 不增长)。
C7 [P1] 防灌门随全网口同步扩 (DCP/外来 0x8892)
- 目的 MAC 门 (
Iocr.c:494-532) 扩为: 任一启用口 MAC / 广播 / 任一已配置 IOCR 组播 AR MAC 放行; 任一启用口 MAC 全零不得整门跳过 (改为该口单独跳过)。 - 若 p4 注册 DCP EtherType 0xFEFE, 防灌门必须覆盖 DCP; 建议每口独立控制环或入环前按口限速。
- LLDP 每口入环需按口限速评估。
- 判据: 多口灌入外来 0x8892 组播/DCP 广播 (模拟) 时, 本设备 connect/alarm 帧 0 丢 (DroppedFrames 不因环满增长)。
C8 [P1] ABI 变更必须版本门 (口字段)
DARRT_PNET_CONTROL_FRAME(Internal.h:287-293) 加口字段时: 驱动 Size/AbiVersion 校验 (Entry.c:35-42) 已有, 服务桥与 pnal 三侧同一 wave 同步; 或驱动侧记"AR→源口"免 ABI 改 (推荐后者优先, 改面最小)。- 判据: 旧服务 + 新驱动 / 新服务 + 旧驱动组合实测 — 显式版本错报, 不静默。
C9 [P1] 安全态按口归约 — 防虚假 SAFE / 永久卡 SAFE
- 定义归约规则 (建议): 任一承载 AR 的口不健康 → 该口输出进 SAFE (清零); 全局 SAFE 仅当全部启用口不健康; 每口链路恢复 → 该口自愈回 RUNNING (对齐
Iocr.c:939-957PPM-only 自愈哲学)。 - 主站切换: 每口允许重新同步序号 (清该口 RxCycleCounterValid, 对齐
Iocr.c:129-137进 SAFE 清位语义); 源 MAC 校验 (Iocr.c:343-348) 按口放宽到"该口 AR 的 MAC 集合"。 - 判据: 双口单口拔线 → 全局 Connected 仍 1 (它口健康); 两口全断 → SAFE; 单口插回 → 该口自愈, 无永久卡 SAFE。
C10 [P1] 看门狗每口语义
- 共享内存头加
PerPortConnected/PerPortLinkUp(ABI 兼容: 复用 Reserved 尾段, 对齐Internal.h:159-163CpmCycleCounter 复用先例), 全局 Connected = 每口归约值; 服务/api/status单一权威口径不变 (对齐PnetDriverBridge.cs:554-558E2 注释)。
C11 [P1] 停止/卸载预算封顶 — 不因多口线性膨胀
CyclicStop→NicQuiesce(Nic.c:712-796) 多口等待必须并行/总预算 ≤ 现有 2s + 500ms, 而非 ×N; 卸载 5 次重试总预算保持 < SCM 30s (Entry.c:469-475的 22s 界限)。- CLOSE 自愈复位 (
Entry.c:96-122) 扩为复位全部启用口 (每口 Deselect + 状态镜像刷 STOPPED)。 - 判据: N=2 口卸载实测 < 30s; 服务崩溃 (句柄关闭) 后驱动自动全口复位, Probe 不出现 err=5 残留。
C12 [P1] 运行态禁改口表
- 维持 BIND 仅 STOPPED 态 (
Entry.c:226-229); 多口加口 = 全停重绑 (或未来新 IOCTL 走独立状态机)。禁止周期 DPC 运行中改口表/模板/槽组。 - 判据: 运行中 UNBIND/BIND 任一启用口 → 驱动明确拒绝 (STATUS_INVALID_DEVICE_STATE), 无竞态路径。
C13 [P2] 服务多口部分降级
- 一口解析失败/1168 → 该口降级 (ConfigError 文案带口名), 它口继续启动; 只有全部口失败才整体降级。现行整体降级路径 (
PnetRuntime.cs:499-527) 按此改造。 UnbindAdapter的 ERROR_BUSY 重试 (10×100ms,PnetDriverBridge.cs:857-888) 保持有界 + 幂等, 多口 UNBIND 序列逐口同口径。
C14 [P2] 锁序与 IRQL 铁律保持
- 锁序
StateLock > IocrRxLocks > ControlRingLock反向禁止;ControlMutex不与上述 spinlock 反向嵌套 (Internal.h:453-459); RX/TX 回调 (DISPATCH 级) 零等待、零同步等待用户态 — 多口新代码逐条自检。 - 判据: 新增代码 grep 无 DISPATCH 上下文
KeWaitForSingleObject/ExAcquireFastMutex/NdisScheduleWorkItem后等待。
四、联调验证门 (p4+p5 合流后必过)
| 门 | 场景 | 判据 |
|---|---|---|
| V1 | 双口拔线/插回 (承载 AR 口 / 非 AR 口分别) | 非 AR 口拔线不触发全局 SAFE; AR 口插回按口自愈, 无永久卡 SAFE |
| V2 | 主站从 A 口切到 B 口 (双主站/重连) | 序号重同步成功, 不误拒不虚假 SAFE |
| V3 | DCP/外来 0x8892 组播双口灌入 | 本设备 connect/alarm 0 丢, 控制环不灌满 |
| V4 | 驱动 IOCTL 挂注入 (测试桩) + 服务停止/卸载 | /api 5s 超时返回; 卸载 < 30s (SCM 不 STOP_PENDING) |
| V5 | 服务进程崩溃 (句柄关闭) 后重启 | 驱动全口自愈复位, Probe OK (无 err=5 残留), 无半口半死绑定 |
五、落档守则自检
- ✅ 0 代码文件 Edit/Write; 仅新建本 md
- ✅ 0 git 操作
- ✅ 全部结论附 文件:行 证据
- ✅ 风险分级 (P0 结构性 2 项 / P1 必修 8 项 / P2 3 项) + 历史档案对照 10 家族 + 约束 14 条 + 验证门 5 项