[PATCHv2] sched: Account I/O waits independently of delayed dequeue
From: Christian Loehle
Date: Thu Oct 15 2026 - 07:38:48 EST
Starting blkio delay accounting and incrementing rq->nr_iowait in
__block_task() ties both to removal from the runqueue. With EEVDF delayed
dequeue, this misses the initial part of an I/O sleep, or the entire
sleep if the task wakes before it is dequeued. Such tasks are also
missing from procs_blocked while their dequeue is delayed. Final dequeue
can run in another task's context, causing delayacct_blkio_start() to
timestamp the wrong task.
Start both forms of accounting in try_to_block_task(), when the current
task commits to blocking, before delayed dequeue or proxy execution can
retain it on the runqueue. End both when clearing is_blocked, including
queued wakeups and the proxy execution paths that let the current task
continue. Checking is_blocked excludes wakeups that precede blocking.
Save the CPU charged at the start of the wait. Delayed tasks can migrate
while still queued, and proxy execution can also move blocked tasks.
Decrement the saved CPU's counter on unblock, regardless of the task's
current placement, and remove the old dequeue-based accounting.
CPU iowait continues to classify idle time using the counter on the
charged CPU; it does not accumulate the full per-task blocked duration.
Reported-by: Zhan Xusheng <zhanxusheng@xxxxxxxxxx>
Closes: https://lore.kernel.org/r/20261014081731.1068794-1-zhanxusheng@xxxxxxxxxx
Fixes: 152e11f6df29 ("sched/fair: Implement delayed dequeue")
Signed-off-by: Christian Loehle <christian.loehle@xxxxxxx>
---
include/linux/sched.h | 2 ++
kernel/sched/core.c | 33 ++++++++++++++++++++-------------
kernel/sched/sched.h | 5 -----
3 files changed, 22 insertions(+), 18 deletions(-)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index d35ae49a991f..d558816cc05a 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -880,6 +880,8 @@ struct task_struct {
*/
int recent_used_cpu;
int wake_cpu;
+ /* CPU charged while is_blocked && in_iowait. */
+ int iowait_cpu;
int prio;
int static_prio;
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 1fe40de6ebe3..85eba26f00d6 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -636,7 +636,7 @@ int task_llc(const struct task_struct *p)
*
* p->is_blocked <- { 0, 1 }:
*
- * is set by try_to_block_task() and cleared by ttwu_do_wakeup() and tracks
+ * is set by try_to_block_task() and cleared by unblock_task() and tracks
* if the task is blocked. Traditionally this would mirror p->on_rq, however
* due things like DELAY_DEQUEUE and PROXY_EXEC, this can diverge.
*
@@ -3737,12 +3737,22 @@ ttwu_stat(struct task_struct *p, int cpu, int wake_flags)
__schedstat_inc(p->stats.nr_wakeups_sync);
}
+static inline void unblock_task(struct task_struct *p)
+{
+ if (p->is_blocked && p->in_iowait) {
+ delayacct_blkio_end(p);
+ atomic_dec(&cpu_rq(p->iowait_cpu)->nr_iowait);
+ }
+
+ p->is_blocked = 0;
+}
+
/*
* Mark the task runnable.
*/
static inline void ttwu_do_wakeup(struct task_struct *p)
{
- p->is_blocked = 0;
+ unblock_task(p);
WRITE_ONCE(p->__state, TASK_RUNNING);
trace_sched_wakeup(p);
}
@@ -3839,10 +3849,6 @@ ttwu_do_activate(struct rq *rq, struct task_struct *p, int wake_flags,
en_flags |= ENQUEUE_RQ_SELECTED;
if (wake_flags & WF_MIGRATED)
en_flags |= ENQUEUE_MIGRATED;
- else if (p->in_iowait) {
- delayacct_blkio_end(p);
- atomic_dec(&task_rq(p)->nr_iowait);
- }
activate_task(rq, p, en_flags);
wakeup_preempt(rq, p, wake_flags);
@@ -4415,11 +4421,6 @@ int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
cpu = select_task_rq(p, p->wake_cpu, &wake_flags);
if (task_cpu(p) != cpu) {
- if (p->in_iowait) {
- delayacct_blkio_end(p);
- atomic_dec(&task_rq(p)->nr_iowait);
- }
-
wake_flags |= WF_MIGRATED;
psi_ttwu_dequeue(p);
set_task_cpu(p, cpu);
@@ -6762,6 +6763,12 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
p->is_blocked = 1;
+ if (p->in_iowait) {
+ p->iowait_cpu = cpu_of(rq);
+ atomic_inc(&rq->nr_iowait);
+ delayacct_blkio_start();
+ }
+
/*
* We check should_block after signal_pending because we
* will want to wake the task in that case. But if
@@ -6934,7 +6941,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
if (!mutex) {
clear_task_blocked_on(p, mutex);
if (task_current(rq, p)) {
- p->is_blocked = 0;
+ unblock_task(p);
return p;
}
goto deactivate;
@@ -6970,7 +6977,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
*/
__clear_task_blocked_on(p, NULL);
if (task_current(rq, p)) {
- p->is_blocked = 0;
+ unblock_task(p);
return p;
}
goto deactivate;
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 4c25fbe84fb5..eaad943a27a0 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -3072,11 +3072,6 @@ static inline void __block_task(struct rq *rq, struct task_struct *p)
if (p->sched_contributes_to_load)
rq->nr_uninterruptible++;
- if (p->in_iowait) {
- atomic_inc(&rq->nr_iowait);
- delayacct_blkio_start();
- }
-
ASSERT_EXCLUSIVE_WRITER(p->on_rq);
/*
--
2.34.1