Skip to content

[kernel][ipc] Fix timeout wakeup ownership race#11631

Open
illustriousness wants to merge 2 commits into
RT-Thread:masterfrom
illustriousness:fix/ipc-timeout-ready-race
Open

[kernel][ipc] Fix timeout wakeup ownership race#11631
illustriousness wants to merge 2 commits into
RT-Thread:masterfrom
illustriousness:fix/ipc-timeout-ready-race

Conversation

@illustriousness

Copy link
Copy Markdown
Contributor

拉取/合并请求描述:(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 cleared sched_flag_ttmr_set even 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)

  • Keep sched_flag_ttmr_set set when rt_timer_stop() fails, and clear it when _thread_timeout() completes timeout ownership.
  • Only consume event bits and report event delivery after rt_sched_thread_ready() succeeds.
  • Make rt_susp_list_dequeue() skip waiters already owned by their timeout callbacks and try the next eligible waiter.
  • Preserve a released semaphore token when no eligible waiter can be resumed.
  • Add 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:

CONFIG_RT_USING_UTEST=y
CONFIG_RT_USING_UTESTCASES=y
CONFIG_RT_UTEST_SCHEDULER=y
CONFIG_RT_USING_SEMAPHORE=y
CONFIG_RT_USING_EVENT=y
CONFIG_RT_USING_MAILBOX=y
CONFIG_RT_USING_MESSAGEQUEUE=y
  • Verification:
scons -j8 --strict
utest_run core.scheduler_timeout_race          PASSED
utest_run core.scheduler_timeout_race 50       50/50 PASSED
utest_run core.scheduler_timed_sem             PASSED (1000 iterations)
  • action: GitHub Actions for this PR

当前拉取/合并请求的状态 Intent for your PR

必须选择一项 Choose one (Mandatory):

  • 本拉取/合并请求是一个草稿版本 This PR is for a code-review and is intended to get feedback
  • 本拉取/合并请求是一个成熟版本 This PR is mature, and ready to be integrated into the repo

代码质量 Code Quality:

我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:

  • 已经仔细查看过代码改动的对比 Already check the difference between PR and old code
  • 代码风格正确,包括缩进空格,命名及其他风格 Style guide is adhered to, including spacing, naming and other styles
  • 没有垃圾代码,代码尽量精简,不包含#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up
  • 所有变更均有原因及合理的,并且不会影响到其他软件组件代码或BSP All modifications are justified and not affect other components or BSP
  • 对难懂代码均提供对应的注释 I've commented appropriately where code is tricky
  • 代码是高质量的 Code in this PR is of high quality
  • 已经使用clang-format 源码格式化工具确保格式符合RT-Thread代码规范 This PR has been formatted with clang-format and complies with RT-Thread code specification
  • 如果是新增bsp, 已经添加ci检查到.github/ALL_BSP_COMPILE.json 详细请参考链接BSP自查

@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  • 将目标分支设置为 \ Set the target branch to:fix/ipc-timeout-ready-race
  • 设置PR number为 \ Set the PR number to:11631
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将自动推送至你的分支。
    The formatted code will be automatically pushed to your branch.

完成后,提交将自动更新至 fix/ipc-timeout-ready-race 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the fix/ipc-timeout-ready-race branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@github-actions github-actions Bot added the Kernel PR has src relate code label Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: kernel

Reviewers: @GorrayLi @ReviewSun @hamburger-os @lianux-mm @wdfk-prog @xu18838022837

Changed Files (Click to expand)
  • src/ipc.c
  • src/scheduler_comm.c
  • src/thread.c
  • src/utest/SConscript
  • src/utest/sched_timeout_race_tc.c

📊 Current Review Status (Last Updated: 2026-07-17 14:40 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

at32f415-start

  • ROM: .text +36 B (+0.1%, 65,300 B / 262,144 B, total: 25% used)

esp32-c3

  • drom0_0_seg: .eh_frame +8 B (+0.0%, 410,996 B / 8,388,576 B, total: 5% used)

gd32105r-start

  • CODE: .text +36 B (+0.1%, 69,332 B / 262,144 B, total: 26% used)

hc32f334

  • FLASH: .text +36 B (+0.0%, 99,532 B / 131,072 B, total: 76% used)

hpmicro-hpm5301evklite

  • ILM: .fast +48 B (+0.1%, 38,144 B / 131,072 B, total: 29% used)
  • XPI0: .fast +48 B (+0.0%, 124,928 B / 1,048,576 B, total: 12% used)

infineon-psoc6

  • flash: .text +36 B (+0.0%, 116,176 B / 262,144 B, total: 44% used)

k230

  • SRAM: .eh_frame +32 B (+0.0%, 1,122,715 B / 268,300,288 B, total: 0% used)

loongson-ls1cdev

  • Code: .text +72 B (+0.0%, 362,744 B)

nordic-nrf51822

  • FLASH: .text +48 B (+0.1%, 51,952 B / 262,144 B, total: 20% used)

nuvoton-m487

  • CODE: .text -8 B (-0.0%, 355,772 B / 524,288 B, total: 68% used)

qemu-virt64-aarch64

  • Code: .text +64 B (+0.0%, 1,056,452 B)

raspberry-pico-rp2040

  • FLASH: .text +56 B (+0.0%, 114,192 B / 2,097,152 B, total: 5% used)

renesas-ra2l1

  • FLASH: .text +32 B (+0.0%, 98,228 B / 262,144 B, total: 37% used)

simulator

  • Code: .rela.dyn +336 B, .rodata +32 B, .text +322 B (+0.0%, 2,633,554 B)
  • Data: .data +384 B (+0.0%, 1,582,852 B)

stm32f407-rt-spark

  • CODE: .text +36 B (+0.0%, 85,800 B / 1,048,576 B, total: 8% used)

stm32l475-atk-pandora-llvm

  • ROM: .text +44 B (+0.1%, 83,492 B / 524,288 B, total: 16% used)

wch-ch32v208w-r0

  • FLASH: .text +56 B (+0.0%, 159,480 B / 491,520 B, total: 32% used)

x86

  • Code: .text +48 B (+0.0%, 198,381 B)

xuantie-e901plus

  • ISRAM: .rodata +32 B, .text +32 B (+0.1%, 46,360 B / 131,072 B, total: 35% used)
    No memory changes detected for:
  • nxp-lpc1114

@illustriousness

Copy link
Copy Markdown
Contributor Author

CI follow-up after c2bbd80:

  • Fixed the SMP-only utest assertion by taking the scheduler lock around internal thread-state inspection and timeout-ownership setup.
  • A9-smp kernel/kernel_basic.cfg now passes. Its QEMU log shows all five core.scheduler_timeout_race units ran and passed, followed by 29/29 total testcases passing: https://github.com/RT-Thread/rt-thread/actions/runs/29560681199/job/87822300508
  • A9 kernel/kernel_basic.cfg and A9-smp kernel/ipc.cfg also pass.
  • Local two-core QEMU ran core.scheduler_timeout_race for 50 loops successfully.

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

@CYFS3

CYFS3 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

我觉得这个pr有待商榷。需要测试一下性能相关的问题,而且这个bug触发的条件比较极限,是否应该考虑应用层设计的有问题?

@illustriousness

Copy link
Copy Markdown
Contributor Author

@CYFS3 感谢关注,这两个问题都值得讨论。补充一下实际复现场景和我们的判断。

这个问题最初是在 GD32F303 实机上通过 J-Link/GDB 抓到的,不是单纯构造出来的理论窗口:

  1. 接收线程通过带有限超时的 IPC 接口进入 suspend,同时启动 thread timer。
  2. SysTick 的 _timer_check() 已经把到期 timer 从 timer list 摘除并重新开放中断,但还没有执行 _thread_timeout();此时线程仍是 suspend,sched_flag_ttmr_set 仍表示 timeout path 拥有该线程。
  3. 更高优先级的 USART ISR 在这个窗口内连续产生两次 event wakeup。第一次 rt_sched_thread_ready() 因 timer 已被摘除而 stop 失败;旧代码却仍然清除了 sched_flag_ttmr_set
  4. 第二次 wakeup 因 ownership flag 已被错误清零,把同一线程改成 READY。返回 SysTick 后 _thread_timeout() 继续执行,最终触发 rt_sched_thread_is_suspended(thread) 断言。

issue 中 Rbb666 也已在 STM32 上复现,并确认 Event、Semaphore、Queue、Mailbox 存在同类现象。

关于应用层:非阻塞 IPC producer 在 ISR 中使用是 RT-Thread 支持的场景,这里 ISR 只负责通知,没有在中断里等待 IPC。连续通知确实让极小窗口更容易暴露,但调整中断频率或优先级只能降低概率,无法保证合法 API 调用不会破坏线程状态。我们的判断是,应用层可以优化负载,但内核仍应保证同一 suspended thread 只能由 timeout 或 producer 中的一方完成唤醒,不能出现重复 ready、断言或资源丢失。

关于性能:这个担忧合理。当前 rt_susp_list_dequeue() 的正常路径仍然只尝试队头一次,成功后立即退出;只有队头已被 timeout path 接管、rt_sched_thread_ready() 失败时才继续检查后续 waiter。最坏情况会在 scheduler lock 下扫描连续的 timeout-owned waiter,复杂度是 O(k)。这样做是为了避免队头 A 已归 timeout、后面的 B 明明可以消费资源却永远无法被 producer 唤醒。

目前 2-core QEMU 50 轮和 CI 的 UP/SMP 测试验证的是正确性和压力稳定性,不等同于性能 benchmark。性能方面可以继续补充正常队头、timeout-owned 队头加一个有效 waiter,以及多个 timeout-owned waiter 三种路径的 cycle 数据,再据此判断是否需要调整实现。

@Rbb666

Rbb666 commented Jul 17, 2026

Copy link
Copy Markdown
Member

@CYFS3 感谢关注,这两个问题都值得讨论。补充一下实际复现场景和我们的判断。

这个问题最初是在 GD32F303 实机上通过 J-Link/GDB 抓到的,不是单纯构造出来的理论窗口:

  1. 接收线程通过带有限超时的 IPC 接口进入 suspend,同时启动 thread timer。
  2. SysTick 的 _timer_check() 已经把到期 timer 从 timer list 摘除并重新开放中断,但还没有执行 _thread_timeout();此时线程仍是 suspend,sched_flag_ttmr_set 仍表示 timeout path 拥有该线程。
  3. 更高优先级的 USART ISR 在这个窗口内连续产生两次 event wakeup。第一次 rt_sched_thread_ready() 因 timer 已被摘除而 stop 失败;旧代码却仍然清除了 sched_flag_ttmr_set
  4. 第二次 wakeup 因 ownership flag 已被错误清零,把同一线程改成 READY。返回 SysTick 后 _thread_timeout() 继续执行,最终触发 rt_sched_thread_is_suspended(thread) 断言。

issue 中 Rbb666 也已在 STM32 上复现,并确认 Event、Semaphore、Queue、Mailbox 存在同类现象。

关于应用层:非阻塞 IPC producer 在 ISR 中使用是 RT-Thread 支持的场景,这里 ISR 只负责通知,没有在中断里等待 IPC。连续通知确实让极小窗口更容易暴露,但调整中断频率或优先级只能降低概率,无法保证合法 API 调用不会破坏线程状态。我们的判断是,应用层可以优化负载,但内核仍应保证同一 suspended thread 只能由 timeout 或 producer 中的一方完成唤醒,不能出现重复 ready、断言或资源丢失。

关于性能:这个担忧合理。当前 rt_susp_list_dequeue() 的正常路径仍然只尝试队头一次,成功后立即退出;只有队头已被 timeout path 接管、rt_sched_thread_ready() 失败时才继续检查后续 waiter。最坏情况会在 scheduler lock 下扫描连续的 timeout-owned waiter,复杂度是 O(k)。这样做是为了避免队头 A 已归 timeout、后面的 B 明明可以消费资源却永远无法被 producer 唤醒。

目前 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 恢复中断,所以这个问题没有体现出来。

@wdfk-prog

wdfk-prog commented Jul 18, 2026

Copy link
Copy Markdown
Contributor
  • 对这个问题,当前 PR使用while的解决方案是比较小的改动了,但是会引入while.另外问题的根源并不在这里,这个pr的修改只是填补问题发生后的兜底措施,不是一个根治的方案
  • 长期重构:
    • 线程阻塞超时从 generic rt_timer 中分离
    • scheduler 专用 timeout queue
    • wait ownership 状态机
    • timeout claim 与 IPC 摘链在 scheduler lock 内原子完成
    • 最终可以把 rt_susp_list_dequeue() 恢复成严格 O(1):
    thread = first_waiter;
    RT_ASSERT(thread->wait.state == RT_THREAD_WAIT_PENDING);
    rt_wait_complete_by_resource(thread);
    return thread;
  • 所以是:可以不要 while,但必须把线程超时提升为 scheduler 管理的同步等待机制;仅修改当前 callback 或增加 EXPIRING 枚举做不到。
  • 如下是AI给出的建议,仅供参考😄

建议:由 Scheduler 统一管理线程 IPC 等待超时

建议将线程 IPC 等待超时从通用 rt_timer callback 中拆分出来,由 Scheduler 统一管理等待状态和超时队列。

架构

每个线程增加等待上下文:

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 挂起队列继续使用现有线程链表节点,新增 timeout_node 加入 Scheduler 的线程等待超时队列。

统一锁顺序:

IPC object lock -> scheduler lock

等待流程

rt_sem_take(timeout)
-> 获取 semaphore lock
-> 获取 scheduler lock
-> 设置线程状态为 SUSPENDED
-> wait.state = PENDING
-> 加入 semaphore suspend list
-> 加入 scheduler timeout queue
-> 释放 scheduler lock
-> 释放 semaphore lock
-> 执行调度

资源到达流程

rt_sem_release()
-> 获取 semaphore lock
-> 获取 scheduler lock
-> 取得 suspend list 队首线程
-> PENDING -> RESOURCE
-> 从 timeout queue 摘除
-> 从 semaphore suspend list 摘除
-> result = RT_EOK
-> 插入 ready queue
-> RESOURCE -> IDLE
-> 释放 scheduler lock
-> 释放 semaphore lock

资源完成接口示意:

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;
}

超时流程

scheduler timeout process
-> 获取 scheduler lock
-> 取得已经到期的线程
-> PENDING -> TIMEOUT
-> 从 timeout queue 摘除
-> 从 IPC suspend list 摘除
-> result = -RT_ETIMEOUT
-> 插入 ready queue
-> TIMEOUT -> IDLE
-> 释放 scheduler lock

超时完成接口示意:

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;
}

资源路径和超时路径通过同一把 scheduler lock 竞争:

PENDING -> RESOURCE
或
PENDING -> TIMEOUT

只有先取得 scheduler lock 的路径能够完成状态转换、摘链和线程恢复。

主要修改点

  1. 在线程结构中增加 wait state、timeout node、deadline 和 result。
  2. Scheduler 增加线程等待专用 timeout queue。
  3. IPC 阻塞时,同时加入 IPC suspend list 和 Scheduler timeout queue。
  4. IPC 唤醒时,在 scheduler lock 内完成状态转换、两个队列摘除和 ready。
  5. Timeout processor 在 scheduler lock 内直接完成超时状态转换和线程恢复。
  6. 普通软件定时器继续使用现有 rt_timer callback,不受影响。

为什么这样修改

当前线程等待超时的处理被拆成两个阶段:

timer 到期并摘除
-> 释放 timer lock
-> 执行 timeout callback
-> callback 再从 IPC suspend list 摘除线程

在 timer 摘除和 IPC 摘链之间,资源释放路径仍可能观察并操作同一个等待线程。

新设计将以下操作放入同一个 scheduler lock 临界区:

确定完成路径
-> 修改 wait state
-> 从 timeout queue 摘除
-> 从 IPC suspend list 摘除
-> 设置等待结果
-> 插入 ready queue

因此等待完成权、线程状态和链表状态始终保持一致,不再依赖 rt_timer_stop() 的返回值推断所有权。

内存与性能

复用现有 IPC 挂起节点时,每个线程主要增加:

1 个 timeout list node
1 个 deadline
1 个 result
1 个 wait state

32 位平台预计增加约 16~24 字节/线程,具体取决于结构体对齐。

资源唤醒路径只处理队首线程,复杂度为 O(1)

超时处理复杂度与本次到期线程数量成正比。timeout queue 可以复用现有 timer 的有序结构或 skip list,避免简单链表插入带来的较大开销。

解决的问题

该设计统一解决:

  • timeout 与 IPC release 并发完成同一线程等待;
  • 同一线程被重复插入 ready queue;
  • timer flag 与 timer 实际状态不一致;
  • timer 已到期但 callback 尚未执行的竞态窗口;
  • ISR 和 SMP 场景下等待完成权不明确;
  • IPC 挂起链表中残留已失去等待资格的线程。

核心职责划分:

IPC 负责资源语义
-> Scheduler 负责线程等待生命周期
-> Wait state 负责完成权竞争
-> Scheduler lock 保证状态与链表原子一致

@illustriousness

Copy link
Copy Markdown
Contributor Author

这个while处的改动 甚至都非必须

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Kernel PR has src relate code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] rt_event_send can race with an in-flight thread timeout and resume a thread twice

4 participants