Skip to content

fix(sched,apic-timer): 原子区探针静默化 + rq 长临界区哨兵 + tmict 采样基线修正 - #2514

Closed
sparkzky wants to merge 3 commits into
DragonOS-Community:masterfrom
sparkzky:fix/probe-hygiene-irq-off-context
Closed

sparkzky wants to merge 3 commits into
DragonOS-Community:masterfrom
sparkzky:fix/probe-hygiene-irq-off-context

Conversation

@sparkzky

@sparkzky sparkzky commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

根因

R16 排查最贵的一课:console 输出被放进了原子区。

  • enqueue_task_on_cpu() 在持 rq 锁(self_lock(),1e6 次自旋忍耐度)的临界区里用串口级打印输出探针,单行数 ms,串口忙时更久;
  • program_deadline_delta() / deadline IRQ 路径在 IF=0 区间直写串口;
  • 结果:目标核在 tick/IPI 路径抢同一把 rq 锁时自旋超时 → self_lock spinout panic;定时器武装路径被拖慢 → 武装风暴与 BADHALT(idle 在 nr_running>0 时仍进入 halt)。

master 并不包含这些探针(因此"删除探针"在 master 上已自然满足)。本 PR 落地的是一份可长期保留的净增量:一个只会在安全上下文发声的回归哨兵 + 默认静默的采样探针。

修复要点

  1. rq 锁临界区哨兵(sched/mod.rs)

    • enqueue_task_on_cpu() 记录 rdtsc,显式 drop(guard) 之后才统计并告警;阈值 1ms(正常入队是微秒级)。
    • 告警前检查 is_irq_enabled():从 IRQ 里唤醒任务时只计数不打印——绝不把输出放回它正在测量的区域。
    • 只报前 16 次,哨兵自身不会变成抖动源。任何把 console/块 IO/嵌套等待塞进 rq 临界区的改动都会立刻在这里显形。
  2. set_next_task 去掉重复的 runtime 武装(sched/fair.rs)

    • pick_next_task() 在 set_next_task() 之后已经无条件调用 refresh_runtime_event();CompletelyFairScheduler::set_next_task() 里再调一次等于每次切换把同一个 deadline 事件重复 arm(now+remaining)。
    • 删除重复者。期限的权威武装点仍是 SchedClass::tick(真实计费之后),与 Linux 在 update_curr()/tick 点检查 runtime 耗尽一致——Linux 没有任何"随切换武装"的 runtime 定时器。
  3. TMICT 采样探针(默认静默)(arch/x86_64/driver/apic/apic_timer.rs)

    • per-CPU 基线:早期实现用单个 static 记录窗口/基线,被两个 CPU 交替 swap,相邻采样跨 CPU 混合,差分失真(曾因此误判 tsc_hz≈9.1GHz,把一个采样缺陷当成系统缺陷追了很久)。
    • PMTMR 24bit 扩展:ACPI PM Timer 是 24bit、约 4.7ms 回绕,直接跨窗口差分得到无意义值。改为 per-CPU 把回绕累加进 64bit,再与 TSC 配对交叉定频(tsc_hz = Δtsc/Δpmtmr × 3.579545e6)。
    • 编译期打印开关 TMICT_PRINT = false:探针由 program_deadline_delta() 在 IF=0 临界区调用,默认绝不输出;即使打开也有 1s 窗口/2048 次事件节流与 64 行总量上限。

Linux 对齐点

  • rq 锁临界区不可打印:Linux 调度器在 rq_lock 内只用 SCHED_WARN_ON/lockdep 这类无输出原语,输出统一在 __schedule() 之后。
  • runtime 带宽武装:Linux account_cfs_rq_runtime()/check_enqueue_throttle() 在计费点武装 hrtimer,切换路径(set_next_task_fair())只做 set_next_entity(),不武装定时器。
  • 探针采样基线 per-CPU:与 Linux tracepoint per-CPU 缓冲区同一动机(跨 CPU 共享基线必然产生混合样本)。

实测证据

  • 修复前:print_critical! 探针位于 rq 锁内/IF=0 区时,docker load 在 ~4 分钟处必挂,串口出现 cpu1 self_lock spinout panic 与 BADHALT 洪泛。
  • 修复后:净盘 + docker load 全程(>13 分钟)零 panic、零 sched: long rq section、零 rq spinout;Loaded image / LOAD_RC=0 达成。
  • TMICT 修正的取证价值:per-CPU 基线 + PMTMR 扩展后,tsc_hz 交叉定频回到真实量级;此前的 9.1GHz 结论被判定为采样缺陷而非系统缺陷。
  • 排查文档 docs/ocmanager-porting/status-overview.md §3.5("探针放入 IRQ 关闭 + 持 rq 锁的临界区")、§3.6 第 5 项。
  • 本分支 make fmt(kernel rustfmt + clippy --all-features)通过、make kernel BUILD=0。

拆分说明 / 交叉引用


Review 后更新(最终方案)

  • 仅保留 set_next_task() 重复武装 runtime event 的根因修复:pick_next_task() 已在 set_next_task() 后无条件武装。
  • 新增 selftest-only 不变量:每次 remaining > 0 的 refresh_runtime_event() 必须成功 arm,且 queued_deadline() == now + remaining;漏武装会在测试构建中直接失败。
  • 验证:normal/cgroup_cpu 20 个测试(runtime 计费、quota、period、ancestor/sibling、cgroup transition)本地通过;sched_*、test_tlb_shootdown 通过。
  • 诊断探针不在本 PR;跟踪:issue sched: 审计 runtime deadline 武装点并移除重复 arm #2545。

…entinel

The R16 investigation lost two days to console output placed inside
atomic regions: printk/console writes take milliseconds while holding the
runqueue lock or with interrupts disabled, which is itself the defect
that produced the runqueue self_lock spinout and the timer storms. master
has none of those probes, so the durable part of that lesson is (a) a
runtime sentinel that flags a long rq critical section and (b) probe code
that cannot print by default.

- enqueue_task_on_cpu(): keep the rq lock guard alive explicitly, time the
  critical section with rdtsc and report after the unlock only. Reporting
  requires interrupts to be enabled, so a wakeup from IRQ context can
  never print inside the region it is measuring.
- CompletelyFairScheduler::set_next_task(): drop the runtime deadline
  re-arm. pick_next_task() already calls refresh_runtime_event() right
  after set_next_task(), so this was a duplicate arm(now+remaining) on
  every switch; Linux arms runtime exhaustion at accounting points
  (update_curr/tick), not at switch-in.
- apic timer: add the TMICT sampling probe with two R16 corrections -
  every sampling baseline is per-CPU (a shared baseline mixed samples
  across CPUs and produced a bogus 9.1GHz TSC reading) and PMTMR is
  extended to 64 bits per CPU because the raw counter is 24 bits and wraps
  every ~4.7ms. Printing is behind a compile-time switch that defaults to
  off, since the probe runs in the deadline queue's IRQ-off region.

fslongjin commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

I reviewed the rebased patch and its call paths with an independent subagent. I am skipping this PR because the proposed instrumentation adds hot-path overhead and unreliable measurements, while the original debug-probe removal is already satisfied on master:

  • TMICT_PRINT=true restores synchronous IRQ-off console output and to_string allocation. False still adds multiple global atomic updates/cacheline contention on every timer arm.
  • A 24-bit ACPI PM timer at 3.579545 MHz wraps in about 4.69 seconds, not milliseconds. Sampling only at reporting windows cannot distinguish multiple missed wraps, so long-gap frequency estimates can be wrong.
  • rq timing begins before lock acquisition and ends after releasing it, including contention, possible migration and interrupt interference. It does not measure lock hold time. IF enabled after releasing this lock also does not prove no caller-owned lock conflicts with logging.
  • set_next_task() has callers outside pick_next_task: current-task policy/nice changes and CPU cgroup changes. The duplicate-arm argument must account for them. Those transitions may request rescheduling, so I have not established a user-visible quota regression and am not reporting one as confirmed. Any deduplication should target the proven duplicate path while preserving all entry contracts.

A narrowly validated duplicate-arm optimization could be submitted separately. No local rebase was pushed and this PR was not merged.

@fslongjin fslongjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. Please separate the underlying production problems into independent issues and focused PRs, and implement each feature or root-cause fix completely with concrete acceptance criteria. The debug probes described in the motivation are absent from master, so first identify the actual remaining defect or measured performance deficiency in the current tree.

A redundant-runtime-arm optimization needs a complete audit of scheduling, policy/nice, and cgroup-transition callers, with validation that every required event is still programmed. Diagnostic instrumentation should be a separate feature with correct measurements, bounded overhead, safe output contexts, and correct PM-timer wrap handling. The current global hot-path counters, lock timing that includes acquisition/interference, and sparse wrap sampling do not provide reliable evidence for the claimed fixes.

sparkzky added a commit to sparkzky/DragonOS that referenced this pull request Oct 24, 2026
pick_next_task() already calls refresh_runtime_event() unconditionally
right after set_next_task(). Arming again from set_next_task() re-arms
the same deadline at now+remaining, delaying it on every switch.

The authoritative arm points remain pick_next_task (all policy/nice and
cgroup-transition paths go through it) and SchedClass::tick (after real
accounting). Drop the diagnostic probes from this change; they are
tracked separately.

Refs: DragonOS-Community#2514, DragonOS-Community#2545
@sparkzky

Copy link
Copy Markdown
Member Author

已按 review 意见收敛为单一调度修复:

  • 移除诊断探针:rq 临界区哨兵、TMICT 全局热路径计数/采样全部从本 PR 移除,单独跟踪在 sched: 审计 runtime deadline 武装点并移除重复 arm #2545。
  • 保留并审计重复武装修复:pick_next_task() 在 set_next_task() 之后已经无条件调用 refresh_runtime_event();set_next_task() 内的第二次 arm 会用 now + remaining 把同一 deadline 往后推。审计确认权威武装点仍是 pick_next_task()(policy/nice 与 cgroup transition 都经过它)以及 SchedClass::tick(真实计费之后)。
  • 验证:本地用 CI 同款镜像通过 sched_affinity、sched_default_nice、sched_policy_semantics、sched_priority_syscall、sched_tracepoint、proc_stat_sched_fields,以及 test_tlb_shootdown(含 fork/COW 路径)。CI 已重新触发。

生产问题的拆分与后续验收项见 #2545。

pick_next_task() already calls refresh_runtime_event() unconditionally
right after set_next_task(). Arming again from set_next_task() re-arms
the same deadline at now+remaining, delaying it on every switch.

The authoritative arm points remain pick_next_task (all policy/nice and
cgroup-transition paths go through it) and SchedClass::tick (after real
accounting). Add a selftest-only invariant that every positive runtime
refresh actually arms the event at the requested deadline, so a missed
arm is caught instead of silently disabling throttling. Validated with
the cgroup CPU bandwidth suite (20 tests).

Refs: DragonOS-Community#2514, DragonOS-Community#2545
@sparkzky

Copy link
Copy Markdown
Member Author

本 PR 的 Build loongarch64 失败不是本 PR 内容导致的:失败在 kernel/src/filesystem/epoll/epitem.rs 的 const 断言(HeapPlan::prefers_virtual_layout),来自 master 45754bdc(#2554)。同一 commit 的 master Build Check 中该 job 同样 failure,x86_64/riscv64/格式检查通过。已记录 issue #2556。master 修复后重跑即可。

@sparkzky

Copy link
Copy Markdown
Member Author

已按 review 重做并补上验证:

  • 保留 set_next_task() 重复武装的根因修复:pick_next_task() 在 set_next_task() 后已无条件调用 refresh_runtime_event(),第二次 arm 会把同一 deadline 推到 now + remaining。
  • 新增 selftest-only 不变量:每次 remaining > 0 的 refresh_runtime_event() 必须成功 arm,且 queued_deadline() == now + remaining;否则 debug assert。这样“漏武装/武装失败”会在测试构建中立即暴露,而不是静默失去 throttling。
  • 用 normal/cgroup_cpu(20 个测试,覆盖 runtime 计费、quota、period、ancestor/sibling 和 cgroup transition)验证;本地通过。
  • 审计点:pick_next_task() 是 policy/nice 与 cgroup 迁移路径的统一武装点,SchedClass::tick 在真实计费后重新武装;诊断探针不在本 PR。

@sparkzky
sparkzky requested a review from fslongjin October 25, 2026 04:20
@fslongjin

Copy link
Copy Markdown
Member

Reviewed the latest, narrowed version (41a1384), not the earlier instrumentation patch, against current master ca991aa and Linux 6.6.139, with independent review.

The duplicate runtime-event arm was a valid issue in the older implementation. It has now been superseded by #2573 (9894665): master already removes the call from set_next_task() and removes the entire per-CPU runtime exhaustion event / refresh_runtime_event() mechanism, retaining normal tick/pick quota enforcement and settling bandwidth on entity switch-out. The new assertions in this PR test that removed mechanism, and the selftest-only queued_deadline() accessor has no remaining consumer. Restoring the old event just to retain those assertions would regress the newer architecture. After dropping the superseded commits, the rebase is empty.

I also inspected the failed LoongArch CI job (121062613318): it is the heap-layout E0080 documented here, already fixed by #2567 (32ef7a5), which aligns eligibility with actual virtual-heap backends. No additional source fix is needed for that historical failure.

Skipping this PR as superseded. No remote branch changes were pushed; this assessment does not claim a new build or guest test run.

@github-actions github-actions Bot added the test Unitest/User space test label Nov 3, 2026
@fslongjin fslongjin closed this Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Unitest/User space test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants