Files
wandering1 f1d0b5c2d9 docs(规范,门禁): 卷锁五态去裸形——本仓第一次真正还第二根护栏的债,并写清还债的算法
最反直觉的是结果里的差值:36 处裸形态号全部清零,绝对数只从 1,162 降到 1,156(−6,余量 4 → 10 格)。
作者没把差值糊过去而是查到底:check_ids 只在字面量**落不进任何已登记族**时才计不可判定,撞进已登记族的
23 处本来就记在可判定侧,删它们分子分母同缩、比例反而从 86.74% 涨到 86.77%;真正在盲区里的只有族外
7 处,其中 1 处还被变更记录行豁免 ⇒ 账面 −6,算式对平(±1 属同窗别道在飞)。**这四条机制我写进
docs/门禁说明/id-check.md §6.10 并署名作者**——它是「怎么还这根护栏的债」的第一份操作口径。

改动面:App/fs/vol_lock.c 10 处、.h 6 处、vol_lock_test.c 14 处、卷锁单元测试说明.md 3 处 ⇒ 同口径
改后 0 处。四态语义只留 vol_lock.h 的 vol_lock_stats_t 注释一处,其余三处只描述断言并指回头文件,
不抄第二真源。除 4 行声明外全是注释文字,代码行为零改动。顺带消掉一个**同形两义**的撞号源。

实测:1,162(03:15:08)→ 1,156(03:21:56),写变更记录后复量仍 1,156(本笔没吃余量);--strict exit
0;vol_lock 套件 45/45;**上限未动、刻意不跑 --update-baseline**(作者反对成立:重锚会把只减不增的线
按这次 −6 重画,白丢一次真还债的账面记录);未改 id_registry.json / 台账,未跑 --all-files。
2026-10-27 05:14:11 +08:00

128 lines
7.6 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Volume Lock(卷锁)
* 模块功能:NAND FatFs 卷的跨任务串行化锁——FatFs 卷状态(FS 窗口缓冲/FAT
* 缓存)与 FTL 核心状态(单页 RAM 缓存 + dhara journal)均非线程安全;
* defaultTask(syslog 落盘/配置引擎/存储 bring-up)与 ftpTask(lftpd
* 文件操作)对卷的访问必须在本锁下串行执行
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2)
* 作者:王建锋
* 创建日期:2026-10-02
* 修改记录:
* v1.0 2026-10-02 王建锋 创建(2026-10-02 台架实证:defaultTask 与
* ftpTask 并发 FTL 写 → dhara journal 竞争 →
* 长耗时恢复旋转饥饿 defaultTask 主循环(喂狗点)
* → IWDG 误复位;单写者卷属主假设在多任务 FatFs
* 调用下不成立,须显式串行化)
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 修契约——vol_lock.c 原 :38 的
* 「返回值」把 fail-open 归给 0,而代码(:46)与
* TC-VOLLOCK-008 都是返 1;现在 .h/.c 两处都按代码写实,
* 并显式警告"返 1 也可能是零互斥";② 新增 5 态计数与
* vol_lock_get_stats() 只读口(不取锁、任意上下文可调);
* ③ 使用约定补「可观测性」段(为何计数在锁层、为何不得
* 在失败分支里取锁打日志)
*
* 使用约定(铁律):
* - 锁跨越"一次完整文件操作序列"(open...close / opendir...closedir /
* 单发 stat/unlink/原子写),不得只锁单个 f_read/f_write;
* - defaultTask 调用方一律用非阻塞 try(vol_lock_wait(0)):defaultTask 是
* 看门狗喂狗点(task_wd_poll),阻塞等锁 = 主循环停摆 = 停喂 → IWDG 误复位;
* try 失败 = 本轮跳过(syslog 条目滞留队列/配置保存待下轮重试);
* - ftpTask 调用方用 100ms 分片阻塞等待 + 每片心跳(lftpd_task_wd_beat),
* 防长等待导致 FTP 心跳位缺席误复位;
* - 锁在内核启动后由 defaultTask 存储 bring-up 创建(vol_lock_init 幂等)。
*
* 可观测性(道G task-17):锁层自带 5 态计数 + `vol_lock_get_stats()` 只读口。
* 为什么计数必须在**锁层**:唯一能把事写下来的通道(`/log/wdev.log` 等)自己也要
* 取这把锁,而卷锁**非递归**——被卷忙挡住时既写不出日志,也不许在失败分支里再取
* 同一把锁。所以锁层只自增计数;一次性留痕交给 defaultTask(syslog 唯一写者)按边沿
* 补投自检记录。调用方 13 处 try 的分支与返回值一字不动(不动分支 = 不动行为)。
*/
#ifndef VOL_LOCK_H
#define VOL_LOCK_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 函数功能:创建卷锁(幂等)
* 入口参数:无
* 返回值:0 - 成功/已创建;-1 - 创建失败 int
* 限定条件:内核启动后调用(defaultTask 存储 bring-up 首行)
*/
int vol_lock_init(void);
/*
* 函数功能:获取卷锁
* 入口参数:timeout_ms - 等待超时(0 = 非阻塞 try) uint32_t
* 返回值:1 - 已取得互斥,**或锁不存在的 fail-open(此时并未串行化)**;
* 0 - 未取得(try 超时,或分片等待超时) int
* 限定条件:见文件头使用约定(defaultTask 必须 timeout_ms=0)
* 函数说明:⚠ 道G task-17 修掉的契约缺口——本函数只有 0/1 两值,而"返 1"有两种
* 成因:真取到锁,与 `osMutexNew` 失败时的 fail-open(返 1 但对卷的访问
* **零互斥**)。要求 13 处调用方各自区分是不现实的(它们只关心"能不能干活"),
* 判据请改用 `vol_lock_get_stats()` 的 `failopen` 计数:累计 > 0 =
* 本 boot 内至少发生过一次"无互斥读写卷"。
* 本函数内只**自增计数**:不打日志、不投记录、不阻塞(理由见 .c 计数块)。
*/
int vol_lock_wait(uint32_t timeout_ms);
/*
* 卷锁观测计数(道G task-17「卷锁 5 态可观测性」)。四个字段就是四态,语义**不得**合并:
* `acquired` 持锁态 = 真取到互斥的次数
* `try_timeout` try 超时态 = timeout=0 的 try 失败次数(卷被别的任务占住;
* defaultTask/uiTask 的喂狗与绘制路径只有这一态会发生)
* `blk_timeout` 分片退让态 = timeout>0 的分片等待超时次数(ftpTask 100 ms 分片正常
* 退让;与 try 超时态语义不同:那是"等了一会儿没等到",
* 这是"压根没等")
* `failopen` 零互斥态 = 锁不存在却返 1 的次数(**谎报成功**那一态,见 wait 函数头)
* 「未调用」这一态不占计数:三个计数全 0 就说明这条路径没跑过——区分"没跑过"与
* "跑了但一直忙"正是本卡的目的(此前两者在日志上完全一样)。
* ⚠ 本层**刻意不记时刻**:记时刻就得读 tick,等于给 defaultTask 的取锁链添一个
* callee 与几字节帧,而这条链的余量我们整天都在省着花(诊断不值那几个字节)。
* 代价是"什么时候开始忙"只能由消费方观测,见下面 get_stats 的第② 条告诫。
*/
typedef struct {
uint32_t acquired;
uint32_t try_timeout;
uint32_t blk_timeout;
uint32_t failopen;
} vol_lock_stats_t;
/*
* 函数功能:读卷锁观测计数(纯拷字段)
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 vol_lock_stats_t *
* 返 回 值:无 void
* 限定条件:任意上下文可调——**不取卷锁**、不关中断、不阻塞(卷被长期占住时正是最
* 需要读这几位的时刻,观测口再抢锁就把观测做成故障的一部分)
* 函数说明:① 四个字段各是 32 位对齐量,Cortex-M4 上单字读写原子,但**不保证是同一
* 瞬间的四元组快照**(诊断够用)。计数本身跨任务读-改-写,竞态下可能
* **少计不会多计** → 判据用「是否 > 0 / 与上一轮比是否增长」,别拿精确值当账。
* ② ⚠ **给消费方的告诫**(消费方 = 看门狗/自检记录道,接口按此写就不会错):
* 本层不产出时刻。消费方若自行盖时间戳,那是「**被观测到的时刻**」,不是
* 「**发生的时刻**」——真实发生可以早到一整个轮询周期之前。命名请用
* `obs_first_ms` / `obs_last_ms`(观测时刻)这类,**别用** `first_ms`/`last_ms`,
* 文案里也别写「首次/末次发生」(那就是新的谎报)。
* 一次性留痕自己做边沿:照 `sys_log.h` 的 `syslog_post_dropped()` 先例(唯一
* 写者按差值补发,一个 episode 一条),锁层自身绝不产日志、不投记录。
* ③ 本函数零外部调用、零局部变量(除出参),因此不改变任何调用方的栈最坏链。
*/
void vol_lock_get_stats(vol_lock_stats_t *p_out);
/*
* 函数功能:释放卷锁(获取方任务释放)
* 入口参数:无
* 返回值:无
* 限定条件:必须与 vol_lock_wait 成对;未获取时调用无害(内部判空)
*/
void vol_lock_release(void);
#ifdef __cplusplus
}
#endif
#endif /* VOL_LOCK_H */