* [PATCH 0/3] NFS/localio: issue IO inline when not reclaiming memory
@ 2026-07-06 16:05 Mike Snitzer
2026-07-06 16:05 ` [PATCH 1/3] NFS/localio: issue IO inline when not in a memory-reclaim context Mike Snitzer
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Mike Snitzer @ 2026-07-06 16:05 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker
Cc: Tejun Heo, Lai Jiangshan, linux-nfs, linux-kernel
Hi,
Every NFS LOCALIO read, write and commit is currently issued indirectly: it
is queued onto the dedicated !WQ_MEM_RECLAIM nfslocaliod_workqueue rather
than being submitted to the underlying filesystem in the calling context.
That intermediate hop was introduced by commit b9f5dd57f4a5 ("nfs/localio:
use dedicated workqueues for filesystem read and write") because:
LOCALIO submits IO directly into a stacked local filesystem (e.g. XFS)
which may in turn flush its own !WQ_MEM_RECLAIM workqueue. Doing so from a
WQ_MEM_RECLAIM worker -- most importantly writeback's wb_workfn running on
bdi_wq -- or from an explicit PF_MEMALLOC reclaim task trips
check_flush_dependency() and risks a forward-progress deadlock.
However, that hazard only exists when the submitting context is itself a
memory-reclaim context. For ordinary application/task submission -- e.g.
O_DIRECT, or an fsync-driven commit -- the workqueue hop buys nothing: it
just adds a context switch and scheduling latency per IO and throws away the
NFS client's inherent application-context parallelism.
This series makes the hop conditional. It adds a small workqueue-core
helper, current_is_workqueue_mem_reclaim(), that reports whether %current is
a WQ_MEM_RECLAIM worker using exactly the predicate check_flush_dependency()
warns on. LOCALIO uses it (together with the existing PF_MEMALLOC test) in a
new nfs_local_defer_io() helper to decide per-IO whether it must defer to
nfslocaliod_workqueue or may issue the IO inline. Reclaim contexts still
defer and are unaffected; everything else runs inline.
Patch 1 adds current_is_workqueue_mem_reclaim() and applies the gating to
the read and write paths.
Patch 2 removes never-taken FLUSH_SYNC handling from nfs_local_commit()
(every caller supplies a FLUSH_SYNC-stripped "how"), which also
drops the sole user of the ctx->done completion plumbing and the
now-unused "how" argument. No functional change.
Patch 3 extends the same gating to the commit (fsync) path. Note the
writeback-triggered commit does reach here in reclaim context --
nfs_write_inode() (the ->write_inode super_op) runs under wb_workfn
on the WQ_MEM_RECLAIM bdi_wq -- so that case correctly keeps
deferring; only app-context commits run inline.
Patch 1 touches the workqueue core (kernel/workqueue.c, include/linux/
workqueue.h), hence the Cc to Tejun Heo, Lai Jiangshan and LKML. The new
export mirrors the existing current_is_workqueue_rescuer()/current_work()
context-introspection helpers.
Additional note for reviewers:
- The inline path now stacks the NFS pgio path plus the underlying
filesystem's ->write_iter/->read_iter (and vfs_fsync_range) in task
context. The reclaim-context stack-depth concern of b9f5dd57f4a5 (2) is
avoided for the deferred paths; feedback on inline stack headroom under
deeply-stacking filesystems (XFS especially) is welcome.
All review appreciated, thanks.
Mike
Mike Snitzer (3):
NFS/localio: issue IO inline when not in a memory-reclaim context
NFS/localio: remove dead FLUSH_SYNC handling from nfs_local_commit
NFS/localio: issue commit inline when not in a memory-reclaim context
fs/nfs/internal.h | 4 +--
fs/nfs/localio.c | 56 ++++++++++++++++++++++++++++++---------
fs/nfs/write.c | 2 +-
include/linux/workqueue.h | 1 +
kernel/workqueue.c | 24 +++++++++++++++++
5 files changed, 71 insertions(+), 16 deletions(-)
--
2.44.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/3] NFS/localio: issue IO inline when not in a memory-reclaim context
2026-07-06 16:05 [PATCH 0/3] NFS/localio: issue IO inline when not reclaiming memory Mike Snitzer
@ 2026-07-06 16:05 ` Mike Snitzer
2026-07-06 16:05 ` [PATCH 2/3] NFS/localio: remove dead FLUSH_SYNC handling from nfs_local_commit Mike Snitzer
2026-07-06 16:05 ` [PATCH 3/3] NFS/localio: issue commit inline when not in a memory-reclaim context Mike Snitzer
2 siblings, 0 replies; 4+ messages in thread
From: Mike Snitzer @ 2026-07-06 16:05 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker
Cc: Tejun Heo, Lai Jiangshan, linux-nfs, linux-kernel
Every LOCALIO read and write is currently bounced through the dedicated
!WQ_MEM_RECLAIM nfslocaliod_workqueue. That bounce is only actually
required when the submitting context is a memory-reclaim context: LOCALIO
issues IO directly into a stacked local filesystem (e.g. XFS) which may in
turn flush its own !WQ_MEM_RECLAIM workqueue. Doing that from a
WQ_MEM_RECLAIM worker (most importantly writeback's wb_workfn on bdi_wq) or
an explicit PF_MEMALLOC reclaim task trips check_flush_dependency() and
risks a forward-progress deadlock, which is why commit b9f5dd57f4a5
("nfs/localio: use dedicated workqueues for filesystem read and write")
introduced the intermediate workqueue.
Outside of reclaim context -- ordinary application/task submission such as
O_DIRECT or fsync-driven writeback -- the workqueue hop buys nothing and
merely adds a context switch and scheduling latency per IO while discarding
the NFS client's inherent application-context parallelism.
Add current_is_workqueue_mem_reclaim(), which reports whether %current is a
WQ_MEM_RECLAIM worker using the same predicate check_flush_dependency()
warns on. Use it, together with the PF_MEMALLOC check, in the new
nfs_local_defer_io() helper to decide per-IO whether nfs_local_do_read()
and nfs_local_do_write() must defer to nfslocaliod_workqueue or may issue
the IO inline. Buffered writeback continues to bounce (wb_workfn is a
WQ_MEM_RECLAIM worker); O_DIRECT and app-context submission now run inline.
Running nfs_local_call_write() inline is safe: it already saves and
restores current->flags around the PF_LOCAL_THROTTLE|PF_MEMALLOC_NOIO it
sets and scopes the file opener's creds. The async O_DIRECT completion
path is likewise unaffected: when the underlying filesystem returns
-EIOCBQUEUED, the kiocb ki_complete callback (nfs_local_read_aio_complete /
nfs_local_write_aio_complete) can run in bottom-half context and so must
still defer the pgio completion (nfs_local_pgio_release -> rpc_call_done) to
nfsiod_workqueue via nfs_local_pgio_aio_complete(). That completion hop is
independent of how the IO was submitted, and this change leaves it as-is;
only the submission side stops unconditionally hopping through
nfslocaliod_workqueue.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
fs/nfs/localio.c | 33 +++++++++++++++++++++++++++++++--
include/linux/workqueue.h | 1 +
kernel/workqueue.c | 24 ++++++++++++++++++++++++
3 files changed, 56 insertions(+), 2 deletions(-)
diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c
index e55c5977fcc3..d3e480888eb1 100644
--- a/fs/nfs/localio.c
+++ b/fs/nfs/localio.c
@@ -699,6 +699,29 @@ static void nfs_local_call_read(struct work_struct *work)
}
}
+/*
+ * Decide whether LOCALIO must defer submission to the dedicated
+ * !WQ_MEM_RECLAIM nfslocaliod_workqueue rather than issue the IO inline.
+ *
+ * LOCALIO issues IO directly into a stacked local filesystem (e.g. XFS),
+ * which may in turn flush its own !WQ_MEM_RECLAIM workqueue. Doing so from a
+ * memory-reclaim context -- either a WQ_MEM_RECLAIM worker (most importantly
+ * writeback's wb_workfn running on bdi_wq) or an explicit reclaim task
+ * (PF_MEMALLOC) -- would trip check_flush_dependency() and risks a
+ * forward-progress deadlock; see commit b9f5dd57f4a5 ("nfs/localio: use
+ * dedicated workqueues for filesystem read and write"). In that case defer
+ * to nfslocaliod_workqueue.
+ *
+ * Otherwise (ordinary application/task context, e.g. O_DIRECT or fsync-driven
+ * submission) issue the IO inline: this preserves the NFS client's inherent
+ * application-context parallelism and avoids the per-IO workqueue hop.
+ */
+static inline bool nfs_local_defer_io(void)
+{
+ return (current->flags & PF_MEMALLOC) ||
+ current_is_workqueue_mem_reclaim();
+}
+
static void nfs_local_do_read(struct nfs_local_kiocb *iocb,
const struct rpc_call_ops *call_ops)
{
@@ -711,7 +734,10 @@ static void nfs_local_do_read(struct nfs_local_kiocb *iocb,
hdr->res.eof = false;
INIT_WORK(&iocb->work, nfs_local_call_read);
- queue_work(nfslocaliod_workqueue, &iocb->work);
+ if (nfs_local_defer_io())
+ queue_work(nfslocaliod_workqueue, &iocb->work);
+ else
+ nfs_local_call_read(&iocb->work);
}
static void
@@ -929,7 +955,10 @@ static void nfs_local_do_write(struct nfs_local_kiocb *iocb,
nfs_set_local_verifier(hdr->inode, hdr->res.verf, hdr->args.stable);
INIT_WORK(&iocb->work, nfs_local_call_write);
- queue_work(nfslocaliod_workqueue, &iocb->work);
+ if (nfs_local_defer_io())
+ queue_work(nfslocaliod_workqueue, &iocb->work);
+ else
+ nfs_local_call_write(&iocb->work);
}
static struct nfs_local_kiocb *
diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index bc1ccdfbfb1d..3d2e426bcf27 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -659,6 +659,7 @@ extern void workqueue_set_min_active(struct workqueue_struct *wq,
int min_active);
extern struct work_struct *current_work(void);
extern bool current_is_workqueue_rescuer(void);
+extern bool current_is_workqueue_mem_reclaim(void);
extern bool workqueue_congested(int cpu, struct workqueue_struct *wq);
extern unsigned int work_busy(struct work_struct *work);
extern __printf(1, 2) void set_worker_desc(const char *fmt, ...);
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 03d9588e16d7..4add75c621da 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -6169,6 +6169,30 @@ bool current_is_workqueue_rescuer(void)
return worker && worker->rescue_wq;
}
+/**
+ * current_is_workqueue_mem_reclaim - is %current a %WQ_MEM_RECLAIM worker?
+ *
+ * Determine whether %current is a workqueue worker executing on a workqueue
+ * created with %WQ_MEM_RECLAIM. This mirrors the condition that
+ * check_flush_dependency() warns on: flushing (or otherwise waiting on) a
+ * !WQ_MEM_RECLAIM workqueue from such a context breaks the forward-progress
+ * guarantee and can deadlock. Callers that may recurse into such a flush --
+ * e.g. NFS LOCALIO submitting into a stacked filesystem that flushes its own
+ * !WQ_MEM_RECLAIM workqueue -- can use this to decide whether they must defer
+ * the work to a !WQ_MEM_RECLAIM workqueue rather than run it inline.
+ *
+ * Return: %true if %current is a %WQ_MEM_RECLAIM worker. %false otherwise.
+ */
+bool current_is_workqueue_mem_reclaim(void)
+{
+ struct worker *worker = current_wq_worker();
+
+ return worker &&
+ ((worker->current_pwq->wq->flags &
+ (WQ_MEM_RECLAIM | __WQ_LEGACY)) == WQ_MEM_RECLAIM);
+}
+EXPORT_SYMBOL_GPL(current_is_workqueue_mem_reclaim);
+
/**
* workqueue_congested - test whether a workqueue is congested
* @cpu: CPU in question
--
2.44.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 2/3] NFS/localio: remove dead FLUSH_SYNC handling from nfs_local_commit
2026-07-06 16:05 [PATCH 0/3] NFS/localio: issue IO inline when not reclaiming memory Mike Snitzer
2026-07-06 16:05 ` [PATCH 1/3] NFS/localio: issue IO inline when not in a memory-reclaim context Mike Snitzer
@ 2026-07-06 16:05 ` Mike Snitzer
2026-07-06 16:05 ` [PATCH 3/3] NFS/localio: issue commit inline when not in a memory-reclaim context Mike Snitzer
2 siblings, 0 replies; 4+ messages in thread
From: Mike Snitzer @ 2026-07-06 16:05 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker
Cc: Tejun Heo, Lai Jiangshan, linux-nfs, linux-kernel
nfs_local_commit() is reached only through nfs_initiate_commit(), and every
path that supplies its "how" argument has already cleared FLUSH_SYNC:
__nfs_commit_inode() strips it (how &= ~FLUSH_SYNC) before dispatch and does
its own waiting via wait_on_commit(), while the O_DIRECT path passes how=0.
filelayout issues its DS commit with a NULL localio, so it never enters
nfs_local_commit() at all. The FLUSH_SYNC branch has therefore been dead
since it was introduced with commit 70ba381e1a43 ("nfs: add LOCALIO
support").
Remove the never-taken FLUSH_SYNC branch along with the completion plumbing
it was the sole user of: the struct nfs_local_fsync_ctx::done member, its
initialization, and the complete() call in nfs_local_fsync_work(). With the
branch gone the "how" parameter is unused, so drop it from nfs_local_commit()
and its callers. No functional change.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
fs/nfs/internal.h | 4 ++--
fs/nfs/localio.c | 15 ++-------------
fs/nfs/write.c | 2 +-
3 files changed, 5 insertions(+), 16 deletions(-)
diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h
index 0338603e9674..66dbc9befdbb 100644
--- a/fs/nfs/internal.h
+++ b/fs/nfs/internal.h
@@ -476,7 +476,7 @@ extern int nfs_local_doio(struct nfs_client *,
const struct rpc_call_ops *);
extern int nfs_local_commit(struct nfsd_file *,
struct nfs_commit_data *,
- const struct rpc_call_ops *, int);
+ const struct rpc_call_ops *);
extern bool nfs_server_is_local(const struct nfs_client *clp);
#else /* CONFIG_NFS_LOCALIO */
@@ -498,7 +498,7 @@ static inline int nfs_local_doio(struct nfs_client *clp,
}
static inline int nfs_local_commit(struct nfsd_file *localio,
struct nfs_commit_data *data,
- const struct rpc_call_ops *call_ops, int how)
+ const struct rpc_call_ops *call_ops)
{
return -EINVAL;
}
diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c
index d3e480888eb1..acbc2bddcf81 100644
--- a/fs/nfs/localio.c
+++ b/fs/nfs/localio.c
@@ -52,7 +52,6 @@ struct nfs_local_fsync_ctx {
struct nfsd_file *localio;
struct nfs_commit_data *data;
struct work_struct work;
- struct completion *done;
};
static bool localio_enabled __read_mostly = true;
@@ -1100,8 +1099,6 @@ nfs_local_fsync_work(struct work_struct *work)
status = nfs_local_run_commit(nfs_to->nfsd_file_file(ctx->localio),
ctx->data);
nfs_local_commit_done(ctx->data, status);
- if (ctx->done != NULL)
- complete(ctx->done);
nfs_local_fsync_ctx_free(ctx);
current->flags = old_flags;
@@ -1117,14 +1114,13 @@ nfs_local_fsync_ctx_alloc(struct nfs_commit_data *data,
ctx->localio = localio;
ctx->data = data;
INIT_WORK(&ctx->work, nfs_local_fsync_work);
- ctx->done = NULL;
}
return ctx;
}
int nfs_local_commit(struct nfsd_file *localio,
struct nfs_commit_data *data,
- const struct rpc_call_ops *call_ops, int how)
+ const struct rpc_call_ops *call_ops)
{
struct nfs_local_fsync_ctx *ctx;
@@ -1136,14 +1132,7 @@ int nfs_local_commit(struct nfsd_file *localio,
}
nfs_local_init_commit(data, call_ops);
-
- if (how & FLUSH_SYNC) {
- DECLARE_COMPLETION_ONSTACK(done);
- ctx->done = &done;
- queue_work(nfslocaliod_workqueue, &ctx->work);
- wait_for_completion(&done);
- } else
- queue_work(nfslocaliod_workqueue, &ctx->work);
+ queue_work(nfslocaliod_workqueue, &ctx->work);
return 0;
}
diff --git a/fs/nfs/write.c b/fs/nfs/write.c
index 0d7f2c2e599c..3afe243597fb 100644
--- a/fs/nfs/write.c
+++ b/fs/nfs/write.c
@@ -1663,7 +1663,7 @@ int nfs_initiate_commit(struct rpc_clnt *clnt, struct nfs_commit_data *data,
dprintk("NFS: initiated commit call\n");
if (localio)
- return nfs_local_commit(localio, data, call_ops, how);
+ return nfs_local_commit(localio, data, call_ops);
task = rpc_run_task(&task_setup_data);
if (IS_ERR(task))
--
2.44.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 3/3] NFS/localio: issue commit inline when not in a memory-reclaim context
2026-07-06 16:05 [PATCH 0/3] NFS/localio: issue IO inline when not reclaiming memory Mike Snitzer
2026-07-06 16:05 ` [PATCH 1/3] NFS/localio: issue IO inline when not in a memory-reclaim context Mike Snitzer
2026-07-06 16:05 ` [PATCH 2/3] NFS/localio: remove dead FLUSH_SYNC handling from nfs_local_commit Mike Snitzer
@ 2026-07-06 16:05 ` Mike Snitzer
2 siblings, 0 replies; 4+ messages in thread
From: Mike Snitzer @ 2026-07-06 16:05 UTC (permalink / raw)
To: Trond Myklebust, Anna Schumaker
Cc: Tejun Heo, Lai Jiangshan, linux-nfs, linux-kernel
Extend the memory-reclaim-context test used for LOCALIO reads and writes to
the commit (fsync) path. As with data IO, bouncing every commit through the
dedicated !WQ_MEM_RECLAIM nfslocaliod_workqueue is only required when the
submitting context is a memory-reclaim context: nfs_local_run_commit() calls
vfs_fsync_range(), which may flush the underlying filesystem's own
!WQ_MEM_RECLAIM workqueue, and doing so from a WQ_MEM_RECLAIM worker or a
PF_MEMALLOC task trips check_flush_dependency().
The writeback path does exercise this: nfs_write_inode() (the ->write_inode
super_op) runs under wb_workfn on the WQ_MEM_RECLAIM bdi_wq and reaches
nfs_local_commit() via __nfs_commit_inode(), so that case must keep
deferring. Application-context commits -- fsync (nfs_file_fsync), O_DIRECT
(nfs_direct), and copy/clone (nfs42) -- are not in a reclaim context and now
run the fsync inline via nfs_local_defer_io(), avoiding the per-commit
workqueue hop.
Completion (nfs_commit_release_pages -> nfs_commit_end) then runs
synchronously in the submitting context; higher layers already cope with
this, as __nfs_commit_inode() dispatches the commit async and waits for it
separately via wait_on_commit().
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
fs/nfs/localio.c | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c
index acbc2bddcf81..f42b6112a613 100644
--- a/fs/nfs/localio.c
+++ b/fs/nfs/localio.c
@@ -1132,7 +1132,19 @@ int nfs_local_commit(struct nfsd_file *localio,
}
nfs_local_init_commit(data, call_ops);
- queue_work(nfslocaliod_workqueue, &ctx->work);
+
+ /*
+ * Run the commit (fsync) inline when not in a memory-reclaim context,
+ * rather than bouncing through nfslocaliod_workqueue; see
+ * nfs_local_defer_io(). Completion (nfs_commit_release_pages ->
+ * nfs_commit_end) then runs synchronously, which higher layers cope
+ * with: __nfs_commit_inode() dispatches async and waits via
+ * wait_on_commit().
+ */
+ if (nfs_local_defer_io())
+ queue_work(nfslocaliod_workqueue, &ctx->work);
+ else
+ nfs_local_fsync_work(&ctx->work);
return 0;
}
--
2.44.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-06 16:05 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-06 16:05 [PATCH 0/3] NFS/localio: issue IO inline when not reclaiming memory Mike Snitzer
2026-07-06 16:05 ` [PATCH 1/3] NFS/localio: issue IO inline when not in a memory-reclaim context Mike Snitzer
2026-07-06 16:05 ` [PATCH 2/3] NFS/localio: remove dead FLUSH_SYNC handling from nfs_local_commit Mike Snitzer
2026-07-06 16:05 ` [PATCH 3/3] NFS/localio: issue commit inline when not in a memory-reclaim context Mike Snitzer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox