/* * 模块名称: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 #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 */