Skip to content

sched: fix local current/runqueue migration races - #108

Merged
FengHeting merged 2 commits into
ViudiraTech:developfrom
JiTianYu391:codex/fix-sched-local-races
Oct 22, 2026
Merged

FengHeting merged 2 commits into
ViudiraTech:developfrom
JiTianYu391:codex/fix-sched-local-races

Conversation

@JiTianYu391

Copy link
Copy Markdown
Contributor

5 vCPU 调度、等待队列和 pipe 压力下,曾出现 stale wait state on task 0 (swapper/N) 刷屏、zombie task resumed after exit、Attempt to kill init! 和损坏返回地址缺页。交接中的修前复现为 3 次启动全部崩溃;本地保留 repro1.log、base5*.log 及修后 fix2-long.log。

根因有两处同类窗口:local_current 先取 CPU 号再读对应 rq->curr,中间迁移会拿到旧 CPU 上的任务;sched_switch 和 sched_maybe_preempt 在关中断前解析本地 rq,也可能操作旧 CPU 的队列和上下文。current 改为单次 GS 相对读;解析 rq 放到关中断之后。

复现/验证:共同测试覆盖层中引导 kbench=8,QEMU -enable-kvm -cpu host -smp 5 -m 2G,反复跑 yield/wait/pipe/lifecycle;本分支另外完成独立构建。

稳定性验证 修前 修后
交接中的 5 vCPU 压测 3/3 启动崩溃 8 轮无错误
本轮 heap A/B 的相同 sched 覆盖层 — 10 次启动、共 30 轮无 panic/stale wait

修前会崩溃,不能提供可信耗时比;本 PR 只修竞态,不改变偷取策略。

验证环境:5 vCPU、KVM、2 GiB、每次 QEMU 超时 60 秒。所有独立分支直接从 origin/develop@153578b5 开出,目标均为 develop;每个分支均独立完成 GCC 16.2 / LTO 构建,构建源只取 git ls-files '*.c',切分支后清除对应 .o/.d。按已知基线问题临时关闭 CONFIG_VIRTIO_GPU。除 strcpy PR 自身外,独立构建在链接命令临时加 -Wl,-u,strcpy,绕过原始 develop 的已复现 LTO 缺陷;没有把其他 PR 的源码带进分支。

运行基准时明确使用独立 sched 修复、strcpy 修复、kbench 和 kthread 绑定的共同测试覆盖层,以免把已知调度崩溃或实际 CPU 不符误算成待测修改的效果。覆盖层仅用于测试镜像,不构成提交依赖。

已否决方案:把偷取限制为源 rq 至少两个任务,曾使 ping-pong 约 3 µs 恶化到 7.8 µs;轮询 idle 无明显收益。这些改动均未采用。宿主负载高,仅把交替多次中位数中超过 20% 的差异视为性能结论。

🤖 Generated with ClaudeCode

local_current() read the CPU number from %gs and then loaded
cpu_rqs[cpu].curr: two loads that a preemption can split.
wait_queue_sleep(), wait_queue_wait_timed() and wait_queue_cancel() call
it with interrupts enabled, typically right after spin_unlock() of the
wait-queue lock, which is exactly where a pending reschedule IPI fires.
If the task is migrated between the two loads it gets whatever now runs
on its old CPU - usually that CPU's idle task - and marks that task
TASK_BLOCKED.  Read the current task with the single GS-relative load
that sched_switch() keeps in lockstep with rq->curr instead.

sched_switch() had the same race in a more dangerous form: it resolved
local_rq() before spin_lock_irqsave() disabled interrupts, and
sched_yield() is entered with interrupts enabled.  A task migrated in
that gap locked its old CPU's runqueue, treated the task running there
as prev, re-enqueued it while it was still executing, and saved its own
registers into that task's context.  Disable interrupts first and only
then resolve the runqueue.  sched_maybe_preempt() is fixed the same way
so a migrated caller can no longer consume another CPU's need_resched.

Symptoms under multi-core load: "sched: stale wait state on task 0
(swapper/N) state=2" floods, idle tasks running on foreign CPUs, page
faults on corrupted return addresses, "zombie task resumed after exit"
and "Attempt to kill init!" panics.

Reproduction: build the tracked kernel sources with virtio-gpu disabled,
boot a 5-vCPU KVM guest with kbench=8 and repeat the scheduler/wait/pipe
suite. Before the fix, the prior reproduction crashed in 3/3 boots;
afterwards the prior 8-round run had no stale-idle, zombie-resume or panic.
The retained logs are build/kbench/repro1.log and fix2-long.log.

Standalone validation uses an origin/develop-only branch. The existing
baseline strcpy LTO defect is bypassed with -Wl,-u,strcpy in the build
command, without adding another PR's code. Runtime stress images may
include the independent kbench instrumentation and strcpy retention fix.

No runqueue-stealing policy is changed. Requiring two source tasks was
already measured to worsen ping-pong from about 3 to 7.8 microseconds and
is deliberately excluded.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
sched_maybe_preempt() runs on every interrupt return, but only needs the
runqueue lock when this CPU has a pending reschedule.  Check the
GS-relative CPU and need_resched first and return without touching the
lock on the common path; interrupts are still disabled before resolving
the local runqueue, so a migration cannot make the caller consume
another CPU's request.

Fold the rq->curr and GS-relative current updates into
local_rq_set_curr(), named after the invariant it enforces: the runqueue
must belong to the CPU executing the store, because the per-CPU GS
window cannot be written remotely.

Restore the call-site interrupt state through one helper after
context_switch(), and unlock the runqueue with interrupts still off in
sched_switch() so a tick cannot observe rq->curr before the stack switch.

Signed-off-by: MicroFish <[email protected]>
@FengHeting
FengHeting merged commit acf075b into ViudiraTech:develop Oct 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants