| @@ -0,0 +1,295 @@ |
| +# MCTP 端点重试的取消与生命周期 |
| + |
| +公共实现位于 `libraries/mctp/mctp.h` 和 `libraries/mctp/mctp.cpp`。本次修改解决三个相连的问题:端点查询的旧回调可能在停止期间重新启动业务 timer;取消仅等待当前 timer,不能覆盖初次同步查询及已经替换 timer 句柄的活动调用;框架同步等待可能在同一线程推进独立 stop,不能把同线程都当作允许立即清成员的回调自身重入。本文的“宿主”指持有 MCTP 和协议接口的设备对象。消费者清理保证适用于采用 `cancel_endpoint_retry(cleanup)` 的调用方;不能把公共接口的修改解释为所有厂商驱动都已完成生命周期整改。 |
| + |
| +## 1. 原来的停止顺序为何不够 |
| + |
| +修改前的 `mctp::destroy_transport_obj()` 开头是实际代码: |
| + |
| +```cpp |
| +if (m_retry_timer) { |
| + m_retry_timer->stop(); |
| + m_retry_timer.reset(); |
| +} |
| +``` |
| + |
| +`m_retry_timer` 是项目封装 `mc::shared_ptr<mc::timer>` 的成员句柄。`->stop()` 调用项目定时器的停止函数;`reset()` 只释放该成员持有的一份引用。排队任务或其他对象可能仍持有 timer,不能把 `reset()` 等同于 C 的“立即 free 定时器及其所有回调”。当前框架 `stop()` 会等待其他线程已经进入的该 timer 回调,但不会替调用方查找其他 timer,也不知道哪次同步查询还在运行。 |
| + |
| +修改前的异常路径会替换该成员,节选如下: |
| + |
| +```cpp |
| +if (m_retry_timer) { |
| + m_retry_timer->stop(); |
| +} |
| +m_retry_timer = mc::timer::single_shot(mc::milliseconds(1000), this, |
| + [this, alive, retry_count, cb]() { |
| + if (alive->load()) { |
| + try_get_endpoint_object(retry_count + 1, cb); |
| + } |
| + }); |
| +``` |
| + |
| +`single_shot` 是项目函数,创建并启动一次性 timer。`[this, alive, retry_count, cb]` 是 C++ lambda 的值捕获清单:框架保存的回调对象会复制这四项。`this` 只复制对象地址,类似 C 回调保存的 `void *ctx`;`cb` 是保存业务函数及其捕获内容的 `std::function`。`alive` 的标准库共享指针只延长标志内存寿命,不能延长 `this` 指向对象的寿命,也不能排空已经越过标志检查的回调。 |
| + |
| +初次 `try_get_endpoint_object()` 会直接查询并调用 `cb()`,此时可以完全没有重试 timer。另一方面,某个 timer 的回调可能已创建下一只 timer,当前成员句柄因此不再代表所有活动调用。 |
| + |
| +设备清理的错误顺序可写成下面的 C 教学伪代码: |
| + |
| +```c |
| +stop_all_consumer_timers(card); |
| +stop_endpoint_retry(card->mctp); /* 里面的 callback 仍可能再次 start 消费者 */ |
| +unregister_transport(card->mctp); |
| +``` |
| + |
| +```text |
| +清理线程:停止业务 timer ───────── 等待端点回调 ── 注销 transport ── 返回 |
| +回调线程: 创建新业务 timer ── 返回 |
| + ↑ 停止完成后仍可能留下新 timer |
| +``` |
| + |
| +这个时序支持“业务 timer 被重新启动”的判断;由于清理线程随后等待回调返回,不能再据此断言等待之后的 vector 清空与同一个已结束回调并发。未复现的业务崩溃、端口状态变化也不能由这张图直接归因。 |
| + |
| +## 2. 先登记访问,再允许清理等待 |
| + |
| +当前共享状态的核心成员如下,省略了方法体: |
| + |
| +```cpp |
| +struct mctp::endpoint_retry_state { |
| + std::mutex mutex; |
| + mc::runtime::condition_variable finished; |
| + uint64_t generation{0}; |
| + bool cancelled{false}; |
| + size_t active{0}; |
| + std::unordered_map<std::thread::id, size_t> active_threads; |
| + std::vector<std::function<void()>> deferred_cleanups; |
| + bool cleanup_running{false}; |
| + std::thread::id cleanup_thread; |
| + // 另有首次调用建立的 weak self/parent 与 managed 状态,见下文。 |
| +}; |
| +``` |
| + |
| +可按 C 的 `struct retry_state` 理解:`mutex` 保护状态、活动计数及 `m_retry_timer` 的发布/移出;`generation` 是本轮查询编号;`cancelled` 表示这轮已停止;`active` 是已经获准访问 MCTP、尚未返回的调用数量。线程计数表用于识别当前线程是否正在查询或回调中,防止重入取消等待自己。它不是 MCTP 所有业务字段的通用锁。 |
| + |
| +`std::mutex` 是标准库互斥量。`mc::runtime::condition_variable` 是项目等待工具,框架 worker 等待时可以继续推进运行时事件;不能不加分析就换成阻塞该 worker 的普通等待。调用 `wait(lock, predicate)` 时会释放传入的锁,让其他线程完成工作;醒来后重新持锁检查条件。 |
| + |
| +当前 timer lambda 的关键代码是: |
| + |
| +```cpp |
| +[this, state, generation, retry_count, cb]() { |
| + endpoint_retry_activity activity(state, generation); |
| + if (activity) { |
| + try_get_endpoint_object_impl(retry_count + 1, cb, state, generation); |
| + } |
| +} |
| +``` |
| + |
| +`state` 是 `std::shared_ptr<endpoint_retry_state>`:复制它保留的是独立同步状态,而不是宿主对象。`endpoint_retry_activity` 的构造函数在状态锁内确认代次未失效,再为原本受框架管理的 MCTP 和宿主取得强引用,成功后增加活动计数;`if (activity)` 调用该类型的布尔转换,判断登记是否成功。只有成功之后才通过 `this` 调用成员函数,因此晚到回调可以只读取仍有效的共享状态并返回。 |
| + |
| +`activity` 是栈上的 C++ 对象。离开作用域时自动执行其析构函数,在锁内减少计数并通知等待者;正常返回和 C++ 异常展开均经过这里。这种模式常称 RAII,在这里相当于 C 中每个返回分支都必须走同一个 `out:` 清理出口。当前 guard 还持有本次活动的 MCTP 和宿主强引用,直到延后清理结束才释放;它不单独保证线程安全,业务成员的清理仍须服从取消和排空协议。 |
| + |
| +当前保活的关键实际代码是: |
| + |
| +```cpp |
| +m_self = m_state->self.lock(); |
| +m_parent = m_state->parent.lock(); |
| +if ((m_state->managed_self && !m_self) || (m_state->managed_parent && !m_parent)) { |
| + return; |
| +} |
| +``` |
| + |
| +这里是项目的 `mc::weak_ptr` 和 `mc::object_ptr`,不是 `std::weak_ptr`/`std::shared_ptr`。弱引用只允许稍后尝试取得所有权,不长期阻止设备析构;`lock()` 成功才获得一份强引用。首次有效 `try_get_endpoint_object()` 在调用者保证对象有效的入口记录 `is_managed()`,只为原本受 mc 管理的对象建立弱引用。以后 timer 到期从独立 state 取得这些引用,不先解引用可能已失效的裸 `this`。如果原本 managed 的对象已无法 lock,必须拒绝活动,不能当成未托管对象继续访问。 |
| + |
| +用 C 表示,可把对象强引用理解为已有引用计数系统的 `object_get_if_alive(ctx)` / `object_put(ctx)`;不是向任意 `void *ctx` 新加一套所有权。栈对象和 `unique_ptr` 对象不因此变成受 mc 管理,调用者仍负责使其活到全部查询、回调及清理结束。 |
| + |
| +对应的 C 教学伪代码如下,省略线程登记和异常处理: |
| + |
| +```c |
| +lock(state->mutex); |
| +if (state->cancelled || state->generation != captured_generation) |
| + goto reject; |
| +self_ref = lock_weak(state->self); |
| +owner_ref = lock_weak(state->owner); |
| +if ((state->managed_self && !self_ref) || |
| + (state->managed_owner && !owner_ref)) |
| + goto reject; |
| +++state->active; |
| +unlock(state->mutex); |
| + |
| +query_and_maybe_call_user(ctx); /* 不持状态锁;强引用保内存,排空协议保业务成员 */ |
| + |
| +lock(state->mutex); |
| +--state->active; |
| +if (!state->active && state->cleanup_queue_not_empty) { |
| + state->cleanup_running = true; |
| + take_cleanup_queue(state, &local_queue); |
| +} |
| +unlock(state->mutex); |
| +run_cleanup_queue_without_lock(&local_queue); |
| +/* 实际实现还处理清理期间新排队的工作,再清 cleanup_running 并通知等待者。 */ |
| +object_put(self_ref); |
| +object_put(owner_ref); |
| +return; |
| +reject: |
| +unlock(state->mutex); |
| +/* 释放可能已取得的引用,不调用业务函数。 */ |
| +``` |
| + |
| +初次同步查询也使用同样的 guard,不能只保护 timer lambda。保活只是保证对象内存存在,不保证设备仍注册在 service 中,也不允许继续使用已经被其他停止路径清空的成员。 |
| + |
| +## 3. 取消与重装在同一处交接 |
| + |
| +`cancel_endpoint_retry()` 先在锁内执行以下实际操作: |
| + |
| +```cpp |
| +state->cancelled = true; |
| +++state->generation; |
| +timer = std::move(m_retry_timer); |
| +reentrant = state->active_threads.count(std::this_thread::get_id()) != 0; |
| +``` |
| + |
| +前两行让捕获旧编号的调用失效。`std::move` 在此把共享句柄从成员转移到局部变量,类似 C 中取走一个指针并将原槽清空;不是移动 timer 对象的内存,也不是停止动作。它发生在与重装相同的锁内,因此取消者能取得已发布的 timer,重装者也无法无条件把新句柄写回已取消的链。 |
| + |
| +非重入取消随后在锁外断开 timer 槽、调用 `stop()`,最后等待: |
| + |
| +```cpp |
| +std::unique_lock<std::mutex> lock(state->mutex); |
| +state->finished.wait(lock, [&state]() { |
| + return state->active == 0 && !state->cleanup_running; |
| +}); |
| +``` |
| + |
| +最后的等待覆盖全部登记活动,包含初次同步查询、已替换句柄的旧 timer,以及已经接管的延后清理。不能在 `active` 刚减到零时就允许外部 stop 返回,因为延后清理此时可能仍在停止消费者或释放协议。停止期间用户回调可能已经进入;取消不会强行中止它,而是等待它返回。调用方必须允许回调依赖的 I/O 继续推进。 |
| + |
| +异常路径准备重装时,先持同一把锁检查: |
| + |
| +```cpp |
| +if (state->cancelled || state->generation != generation) { |
| + return; |
| +} |
| +// 持锁创建、连接、发布并启动下一轮 timer。 |
| +// 旧 timer 移到局部变量,解锁后才 stop/disconnect。 |
| +``` |
| + |
| +查询、用户 `cb()` 和旧 timer 的 `stop()` 都不跨越状态锁,否则它们返回时所需的活动登记清理可能被这把锁阻塞。新的显式 `create_transport_and_endpoint()` 先取消旧链,再开启新编号;普通 `try_get_endpoint_object()` 不自动解除取消。只用一个布尔值不够:新 create 已把布尔值重新开放时,旧调用栈仍可能异常返回,必须靠编号拒绝其重装。 |
| + |
| +## 4. 调用方必须先停启动者 |
| + |
| +调用方把原有停止函数体提取为不再调用外层取消入口的 cleanup 函数,再由局部 lambda 调用;下面内联该函数体,是去除设备字段差异后的教学代码: |
| + |
| +```cpp |
| +auto cleanup = [this]() { |
| + stop_consumer_timers(); |
| + release_protocol_dependencies(); |
| + if (m_mctp_object) { |
| + m_mctp_object->destroy_transport_obj(); |
| + m_mctp_object.reset(); |
| + } |
| +}; |
| +if (m_mctp_object && !m_mctp_object->cancel_endpoint_retry(cleanup)) { |
| + return; // 本次清理已入队,不能在这里再次执行。 |
| +} |
| +cleanup(); // true 表示当前线程已经排空,可由调用方执行原函数体。 |
| +``` |
| + |
| +`[this]` 只复制设备地址,类似 C 的 `void *ctx`;这里的有效寿命来自活动 guard 持有的宿主引用,不能单靠 lambda 捕获保证。`cancel_endpoint_retry(cleanup)` 接收 `std::function<void()>`,相当于把 C 的函数指针和 ctx 打包保存。返回 `true` 时接口不会调用传入函数,由调用方执行一次;返回 `false` 时函数已排队,最后一个活动退出后执行。无 MCTP 时照常直接清理。 |
| + |
| +普通跨线程路径: |
| + |
| +```text |
| +清理线程:取消旧代 ── 等待活动及延后清理 ── true ── 清理消费者/注销 |
| +回调线程:已进入的回调完成 ── 返回 |
| +晚到回调:检查旧代失败 → 返回,不使用 this |
| +失败重装:检查旧代失败 → 返回,不发布新 timer |
| +``` |
| + |
| +restart 必须把“清理旧协议 → 建立新协议”整个序列交给同一取消判断,不能仍写成 `stop_protocol(); start_protocol();`:stop 可能因为嵌套等待返回 `false` 而尚未清理,此时立刻 start 会与旧清理交错。教学代码如下: |
| + |
| +```cpp |
| +auto restart = [this]() { |
| + cleanup_protocol(); // 已排空后的函数体,不重新调用外层 stop/cancel。 |
| + start_protocol(); |
| +}; |
| +if (m_mctp_object && !m_mctp_object->cancel_endpoint_retry(restart)) { |
| + return; |
| +} |
| +restart(); |
| +``` |
| + |
| +`create_transport_and_endpoint()` 本身只开启新代,不会根据取消的 `false` 自动把启动操作排队。已有“回调内显式 create 使旧代失效”的测试只验证代次,不等于任意设备 restart 都能直接这样调用。设备必须在上面的完整 transition 中安排清理与启动,不能跳过尚未执行的消费者清理。 |
| + |
| +不能机械地在所有 `stop_update_task()` 中增加取消:某些业务回调本来就调用这些局部停止函数来替换旧消费者,错误放置会取消正在正常建立的协议。`destroy_transport_obj()` 保持旧有同步注销契约,并不替旧调用方延后整个业务清理;安全入口是消费者停止之前的 `cancel_endpoint_retry(cleanup)`。析构中的取消也不是允许任意线程直接 `delete` 的协议。 |
| + |
| +## 5. 同线程嵌套、延后清理和注册身份 |
| + |
| +框架 `mc::future::get()` 在 worker 上等待同步请求时可以推进池里的其他任务。因此下面不是“业务回调自己调用 stop”,而是独立的 stop 被嵌套调度到同一 OS 线程。真实框架 probe 已复现原先 `void cancel_endpoint_retry()` 过早返回;没有通过释放非法对象来制造崩溃。 |
| + |
| +```text |
| +同一线程调用栈: |
| + 端点业务回调 → 同步请求等待 → 推进独立 stop |
| + │ |
| +旧行为:按 thread::id 认作重入 → cancel 返回 → 清空消费者/协议 |
| + │ |
| + stop 返回 → 请求完成 → 原业务回调继续使用成员/启动 timer |
| + |
| +新行为:标记旧代失效 → 将 cleanup(ctx) 入队 → false,stop 不清成员 |
| + │ |
| + stop 返回 → 请求完成 → 原业务回调自然结束 |
| + │ |
| + 最后 activity → 锁外 cleanup → 释放保活引用 |
| +``` |
| + |
| +该独立 stop 不能在内层等待外层调用栈退出,否则会等待自己。代次检查能拦截查询返回后的旧工作,但业务 `cb()` 已进入并在其中再次同步等待时,不能靠检查代次撤销它;此时必须保留依赖,等整个 cb 返回再清理。 |
| + |
| +`active_threads` 只用于识别“当前线程不能同步排空”的情形,不声称识别了某个唯一业务任务。实现还记录 `cleanup_running` 和 `cleanup_thread`:延后清理期间的同步等待若再次推进独立 stop,带清理函数的取消仍排队并返回 `false`。执行队列不持有状态锁;最后一个活动保留对象引用,待队列全部处理完才清除 cleanup 状态、通知外部等待者。传入的函数应是实际清理函数体,不能重新调用外层 stop,否则会不断重新入队。 |
| + |
| +不传清理函数的取消仍可用于只使旧代失效的场景;同线程返回 `false` 不代表可以立即销毁对象。显式 start/create 与 stop/destroy 仍须由调用方串行发起,这不是任意并发重建协议的接口。非 mc 管理对象仍由调用者负责有效寿命;直接销毁栈对象、重置 `unique_ptr` 或手工 `delete` 不会被这个 guard 自动延后。清理函数异常会被记录,不能把异常日志当成清理成功。 |
| + |
| +`destroy_transport_obj()` 保持原有同步注销时机,实际代码为: |
| + |
| +```cpp |
| +void mctp::destroy_transport_obj() |
| +{ |
| + cancel_endpoint_retry(); |
| + destroy_transport_obj_impl(); |
| +} |
| +``` |
| + |
| +这里无回调参数的取消在同线程仍可能返回 `false`,但旧的 void destroy 接口继续同步注销。它不等于“消费者已经排空”,也不承诺让原本有并发清理问题的旧驱动自动安全。本轮整改的 Broadcom、Emulex、Huawei NVMe 和 Samsung NVMe 必须先调用带完整 cleanup 的取消入口,只有排空后才在清理体中停止消费者、释放协议依赖、调用 destroy。 |
| + |
| +保持同步注销也有兼容原因:其他旧调用方仍可能在 destroy 返回后立即重建相同路径。若仅把 destroy 改为异步排队,新对象会发现旧路径仍在而跳过注册;随后旧队列又删除该路径,导致新协议没有自己的注册对象。兼容回归分别检查 destroy 后表项数为 0、重建后为 1、旧 callback 返回后仍为 1;另一个用例验证新调用方的延后 cleanup 内执行同样的注销与重建顺序。 |
| + |
| +还有一个独立的生命周期细节:框架注销宿主时会执行 `set_service(nullptr)`,强引用只保内存,不保注册身份。原先 `destroy_transport_obj()` 通过 `m_parent->get_service()` 注销独立 transport,延后清理时可能已取不到服务。因此现在优先使用 transport 自己的注册服务: |
| + |
| +```cpp |
| +auto service = m_transport_obj->get_service(); |
| +if (service == nullptr && m_parent != nullptr) { |
| + service = m_parent->get_service(); |
| +} |
| +``` |
| + |
| +`m_transport_obj` 是另外注册的对象,未挂在宿主的 engine 子对象树中。它自身的 `get_service()` 才能在宿主注销后仍定位应移除的对象表;正常停止时的宿主服务保留为后备。 |
| + |
| +## 6. 回归及证据范围 |
| + |
| +`tests/libraries/mctp/test_mctp.cpp` 中的 `endpoint_cancel_*` 使用生产 MCTP 查询逻辑和真实框架 timer,测试服务提供端点响应,不访问实际硬件。用例覆盖: |
| + |
| +| 用例后缀 | 行为 | |
| +| --- | --- | |
| +| `drains_initial_callback_before_stopping_consumer` | 初次同步回调阻塞时清理必须等待,随后清理真实消费者 timer | |
| +| `from_callback_prevents_exception_rearm` | 首次回调自身取消后抛异常,不得重新创建 retry | |
| +| `is_idempotent_and_explicit_create_reopens_query` | 重复取消、保留旧 timer、普通查询不复活、显式 create 可复用 | |
| +| `old_generation_cannot_rearm_after_reentrant_create` | 新代已启动时,旧调用栈的异常仍不能重装 | |
| +| `drains_retry_callback_and_prevents_exception_rearm` | 真正 retry timer 到期进入阻塞回调,外部取消等待,随后异常不得重装 | |
| +| `from_retry_callback_detaches_pending_timer` | 真实 retry 回调自身取消时断槽、摘链且不自等待 | |
| +| `independent_stop_nested_on_same_worker` | 真实单 worker 的等待推进独立 stop,清理必须在 cb 结束后执行 | |
| +| `waits_for_deferred_cleanup_and_queues_nested_cleanup` | 清理期间的第二个嵌套 stop 排队,外部取消等待整个清理队列 | |
| +| `endpoint_activity_keeps_managed_owner_and_transport_until_cleanup`(完整用例名) | 注销宿主并释放最后外部引用后,活动及清理仍可访问对象,最终引用可释放 | |
| +| `destroy_transport_after_owner_unregister_removes_independent_registration`(完整用例名) | 宿主 service 已清空仍能注销独立 transport | |
| +| `deferred_cleanup_unregisters_transport_before_same_path_restart`(完整用例名) | 延后清理内同步注销旧 transport,再建同路径对象不会被旧队列删除 | |
| +| `destroy_inside_callback_preserves_synchronous_registration_cleanup`(完整用例名) | 旧接口在活动 callback 内仍同步注销,随后同路径重建的注册保留;不宣称旧消费者已安全 | |
| + |
| +前两条在未修改公共实现时已经执行并得到预期 RED:旧清理过早返回,旧异常路径又创建 retry。同线程独立 stop 的 probe 也已在改为延后清理之前得到预期 RED;同路径重建和旧接口兼容用例在 destroy 仍追加到清理队尾时同样得到预期 RED(旧注册未立即移除、旧队列结束后新路径消失)。其他用例是新增覆盖,是否 GREEN 必须依据本轮实际运行记录,不把“已编写”写成“已通过”。测试中保留旧 timer 后直接触发其信号,只验证旧槽已拆除;真实调度验证由另外两条 retry 到期用例承担。 |
| + |
| +设备入口测试可以将受控业务回调放入真实 timer,以单独检验“端点取消在消费者停止之前”的顺序;它们不能替代公共查询代码的代次测试。协议编码测试中的 MCTP 替身没有真正的查询链,取消接口直接返回 `true` 且不调用传入清理函数,不能作为生产取消逻辑通过的证据。 |
| + |
| +这些用例不等于完整热拔插、固件升级、长时间压力测试或全部驱动动态覆盖。提交对应的测试日志、所用框架库及实际目标应随验证记录保存;本说明不固定会随修订变化的通过总数。 |
⚠️ 警告:
protocol_lifecycle::pump的异常可能在异步线程上被重新抛出第 92 行
std::rethrow_exception(error)会把本次pump循环中捕获的首个异常,在该循环最终走到!current时抛出。而pump既会由调用方线程(同步enqueue)触发,也会由异步完成回调done()(例如 MCTP worker/timer 线程)触发。后果有二:
done()排空的,异常会在那个框架 worker/timer 线程上被抛出;该上下文若无捕获,会变成未捕获异常导致进程终止。注意:
error是pump的局部变量,每次调用都会复位,因此这个行为逐次成立。修复建议:在由
done()驱动的异步排空路径中不要向外重抛异常(记录日志并继续),只在同步enqueue路径向调用方传播;或显式区分调用来源。