[kernel][ipc] Fix timeout wakeup ownership race#11631
Conversation
|
👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread! 为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。 🛠 操作步骤 | Steps
完成后,提交将自动更新至 如有问题欢迎联系我们,再次感谢您的贡献!💐 |
📌 Code Review Assignment🏷️ Tag: kernelReviewers: @GorrayLi @ReviewSun @hamburger-os @lianux-mm @wdfk-prog @xu18838022837 Changed Files (Click to expand)
📊 Current Review Status (Last Updated: 2026-07-17 14:40 CST)
📝 Review Instructions
|
|
|
CI follow-up after c2bbd80:
The remaining riscv-none static-build failure is pre-existing and unrelated to this patch: gd32vw553h-eval devices.wlan dist links the same lwIP mem.o twice. The base commit 5363e4e fails at the identical target and symbol on master: https://github.com/RT-Thread/rt-thread/actions/runs/29548097346/job/87784825849 |
|
我觉得这个pr有待商榷。需要测试一下性能相关的问题,而且这个bug触发的条件比较极限,是否应该考虑应用层设计的有问题? |
|
@CYFS3 感谢关注,这两个问题都值得讨论。补充一下实际复现场景和我们的判断。 这个问题最初是在 GD32F303 实机上通过 J-Link/GDB 抓到的,不是单纯构造出来的理论窗口:
issue 中 Rbb666 也已在 STM32 上复现,并确认 Event、Semaphore、Queue、Mailbox 存在同类现象。 关于应用层:非阻塞 IPC producer 在 ISR 中使用是 RT-Thread 支持的场景,这里 ISR 只负责通知,没有在中断里等待 IPC。连续通知确实让极小窗口更容易暴露,但调整中断频率或优先级只能降低概率,无法保证合法 API 调用不会破坏线程状态。我们的判断是,应用层可以优化负载,但内核仍应保证同一 suspended thread 只能由 timeout 或 producer 中的一方完成唤醒,不能出现重复 ready、断言或资源丢失。 关于性能:这个担忧合理。当前 目前 2-core QEMU 50 轮和 CI 的 UP/SMP 测试验证的是正确性和压力稳定性,不等同于性能 benchmark。性能方面可以继续补充正常队头、timeout-owned 队头加一个有效 waiter,以及多个 timeout-owned waiter 三种路径的 cycle 数据,再据此判断是否需要调整实现。 |
当前问题的引入是来自 2023 年的性能优化提交 3283f54:硬定时器回调被移到 timer spinlock 和 irqsave 临界区之外,以降低中断延迟并支持 SMP。之前的处理是在:https://github.com/RT-Thread/rt-thread/blob/v5.0.x/src/timer.c#L661 关闭中断,执行回调后才在:https://github.com/RT-Thread/rt-thread/blob/v5.0.x/src/timer.c#L711 恢复中断,所以这个问题没有体现出来。 |
建议:由 Scheduler 统一管理线程 IPC 等待超时建议将线程 IPC 等待超时从通用 架构每个线程增加等待上下文: enum rt_thread_wait_state
{
RT_THREAD_WAIT_IDLE, /* 当前没有等待 IPC 资源 */
RT_THREAD_WAIT_PENDING, /* 已挂起,等待资源或超时 */
RT_THREAD_WAIT_RESOURCE, /* 资源路径已取得完成权 */
RT_THREAD_WAIT_TIMEOUT, /* 超时路径已取得完成权 */
RT_THREAD_WAIT_ABORTED, /* 等待被删除、复位或信号终止 */
};
struct rt_thread_wait_ctx
{
enum rt_thread_wait_state state; /* 当前等待状态 */
rt_list_t timeout_node; /* Scheduler 超时队列节点 */
rt_tick_t deadline; /* 绝对超时时刻 */
rt_err_t result; /* 最终等待结果 */
};IPC 挂起队列继续使用现有线程链表节点,新增 统一锁顺序: 等待流程资源到达流程资源完成接口示意: static rt_err_t rt_thread_wait_complete_resource(rt_thread_t thread,
rt_err_t result)
{
RT_SCHED_DEBUG_IS_LOCKED;
/* 只有仍在等待的线程才能由资源路径完成 */
if (thread->wait.state != RT_THREAD_WAIT_PENDING)
{
return -RT_EINVAL;
}
/* 资源路径取得本次等待的完成权 */
thread->wait.state = RT_THREAD_WAIT_RESOURCE;
/* 线程已由资源唤醒,不再参与超时处理 */
rt_sched_timeout_dequeue(thread);
/* 从当前 IPC 对象的挂起队列中删除 */
rt_list_remove(&RT_THREAD_LIST_NODE(thread));
/* 保存等待结果并加入就绪队列 */
thread->wait.result = result;
rt_sched_insert_thread(thread);
/* 本次等待已经完成 */
thread->wait.state = RT_THREAD_WAIT_IDLE;
return RT_EOK;
}超时流程超时完成接口示意: static rt_err_t rt_thread_wait_complete_timeout(rt_thread_t thread)
{
RT_SCHED_DEBUG_IS_LOCKED;
/* 只有仍在等待的线程才能由超时路径完成 */
if (thread->wait.state != RT_THREAD_WAIT_PENDING)
{
return -RT_EINVAL;
}
/* 超时路径取得本次等待的完成权 */
thread->wait.state = RT_THREAD_WAIT_TIMEOUT;
/* 从 Scheduler 超时队列中删除 */
rt_sched_timeout_dequeue(thread);
/* 同一临界区内从 IPC 挂起队列中删除 */
rt_list_remove(&RT_THREAD_LIST_NODE(thread));
/* 设置超时结果并加入就绪队列 */
thread->wait.result = -RT_ETIMEOUT;
rt_sched_insert_thread(thread);
/* 本次等待已经完成 */
thread->wait.state = RT_THREAD_WAIT_IDLE;
return RT_EOK;
}资源路径和超时路径通过同一把 只有先取得 主要修改点
为什么这样修改当前线程等待超时的处理被拆成两个阶段: 在 timer 摘除和 IPC 摘链之间,资源释放路径仍可能观察并操作同一个等待线程。 新设计将以下操作放入同一个 因此等待完成权、线程状态和链表状态始终保持一致,不再依赖 内存与性能复用现有 IPC 挂起节点时,每个线程主要增加: 32 位平台预计增加约 资源唤醒路径只处理队首线程,复杂度为 超时处理复杂度与本次到期线程数量成正比。timeout queue 可以复用现有 timer 的有序结构或 skip list,避免简单链表插入带来的较大开销。 解决的问题该设计统一解决:
核心职责划分: |
|
这个while处的改动 甚至都非必须 |
拉取/合并请求描述:(PR description)
为什么提交这份PR (why to submit this PR)
Fixes #11622.
When a thread timer has expired,
_timer_check()removes and deactivates it before invoking_thread_timeout(). A higher-priority ISR can enter that window and try to wake the same IPC waiter.rt_sched_thread_timer_stop()previously clearedsched_flag_ttmr_seteven when stopping the timer failed, allowing a later producer to resume the thread before the in-flight timeout callback. This can resume one thread twice and trigger_thread_timeout()'s suspended-state assertion.The same timeout ownership race is shared by event, semaphore, mailbox, and message queue waiters. It can also consume an event or semaphore token even though the timeout path already owns the waiter.
你的解决方案是什么 (what is your solution)
sched_flag_ttmr_setset whenrt_timer_stop()fails, and clear it when_thread_timeout()completes timeout ownership.rt_sched_thread_ready()succeeds.rt_susp_list_dequeue()skip waiters already owned by their timeout callbacks and try the next eligible waiter.core.scheduler_timeout_race, a deterministic utest that reproduces the timer-removed/callback-pending state and covers Event, Semaphore, Mailbox, and Message Queue resource semantics. It also verifies that a timeout-owned queue head does not block the next waiter.请提供验证的bsp和config (provide the config and bsp)
BSP:
bsp/qemu-vexpress-a9, QEMU ARM, 2 CPUs.config:
当前拉取/合并请求的状态 Intent for your PR
必须选择一项 Choose one (Mandatory):
代码质量 Code Quality:
我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:
#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up