Skip to contentSkip to conversation

fs: range-based admission for generation-drained DIO

RFC PATCH New Vitali Lovich 1 participant 7 messages Nov 4

Subsystems (8)
lore
Share & tools
lore
#0 lore raw

cover fs: range-based admission for generation-drained DIO

Hi all,

This patch series attempts to resolve a major source of contention between concurrent readers and writers on the same inode, specifically when those writers are punching holes in the file or gapped appends provided that readers aren't overlapping these operations. There's some interface changes I needed just for this initial exclusive IOLOCK removal because DIO completion needs to cooperate and that lives in generic layers.

I believe the patch set to be complete and well-tested. The details performance numbers are available in my earlier XFS post:
https://lore.kernel.org/linux-xfs/[email protected]/T/#t
This patch series is identical, just widening the group.

Performance measurements on XFS available in the previous posting indicate substantial read throughput + P99 latency improvements under concurrent read/write contention with no negative impact to pure read scenarios. Fixing all sources of contention even manages to improve pure read workloads by ~2-5%.

Technical overview
------------------

To opt-in, the filesystem serializes compatible punch/gapped operations with a per-inode drain mutex (only for filesystems that opt-in). It publishes an exclusion range through a seqcount latch and flips the DIO generation counter, capturing the outstanding request count. Completion of old-generation requests retires that finite cohort to wake the drainer; this is similar semantics to today with the exception that reads can continue during this exclusive drain mutex hold provided they don't overlap the range (the writer doesn't wait for them to drain either since it's guaranteed they don't overlap). Overlapping reads queue up behind the drain mutex.

Filesystems not using the new interface don't pay a cost.

Not all cases are eligible for this fast-path and those degrade to the old exclusive IOLOCK + drain machinery, including DAX, realtime and unaligned punches; the goal is to remain conservative for now while correctly solving scaling bottlenecks for the common cases. I've tried to make sure there's good additional test coverage for the new code paths.

I expect the same concept would also benefit ext4 and Btrfs which similarly hold exclusive IOLOCKs; I have only double-checked that they broadly follow the similar pattern but I'm not 100% sure if the interface introduced here strictly applies the same to solve the same scaling problem. I could imagine small adjustments being necessary. Like XFS they have additional sources of contention present beyond what this specific patch series solves and like XFS I expect filesystem-specific optimizations will need to be written; it's possible there could be a shared mechanism to encode some of this but I think all contention sources for at least two or three filesystems need to be solved before generic interfaces are codified.

In particular, I would appreciate feedback on:

* Whether the generic inode DIO accounting and iomap interfaces, with filesystem-private lifetime management through ->dio_drain_complete is correct.
* The generation/count encoding and ordering guarantees.
* The XFS lock/revalidation contract and whether my analysis is correct that ext4/Btrfs would benefit from the same interface or if different hooks are needed.

The base is 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4. The patch ordering is generic accounting, iomap ownership, KUnit coverage, XFS admission, aligned hole punching, then post-EOF zeroing.

Thanks,
Vitali

Vitali Lovich (6):
fs: implement range-based generation-coordinated DIO draining
iomap: support generation accounting and caller-registered DIO
fs: add KUnit coverage for generation-based DIO draining
xfs: add range-drain state and latch-based DIO admission
xfs: use range-based DIO draining for aligned hole punches
xfs: use range-based DIO draining for post-EOF zeroing

 Documentation/filesystems/locking.rst         |  89 +-
 Documentation/filesystems/vfs.rst             |  41 +
 fs/Kconfig                                    |  13 +
 fs/Makefile                                   |   1 +
 fs/ext4/file.c                                |   2 +-
 fs/inode.c                                    | 120 ++-
 fs/iomap/direct-io.c                          |  56 +-
 fs/tests/dio_drain_kunit.c                    | 893 ++++++++++++++++++
 fs/xfs/Kconfig                                |   9 +
 fs/xfs/Makefile                               |   2 +
 fs/xfs/tests/xfs_dio_test.c                   | 190 ++++
 fs/xfs/xfs_bmap_util.c                        |   9 +-
 fs/xfs/xfs_dio_test.h                         |  32 +
 fs/xfs/xfs_file.c                             | 357 ++++++-
 fs/xfs/xfs_icache.c                           |   2 +
 fs/xfs/xfs_inode.c                            |  10 +
 fs/xfs/xfs_inode.h                            |   1 +
 fs/xfs/xfs_iops.c                             |   9 +
 fs/xfs/xfs_reflink.c                          |   2 +
 fs/xfs/xfs_reflink.h                          |   2 +-
 fs/xfs/xfs_super.c                            |   2 +
 include/linux/fs.h                            |  97 +-
 include/linux/iomap.h                         |  60 +-
 .../litmus-tests/DIO-register-first.litmus    |  36 +
 tools/testing/selftests/filesystems/Makefile  |   6 +
 .../selftests/filesystems/xfs_dio_admission.c | 380 ++++++++
 .../selftests/filesystems/xfs_dio_drain.c     | 392 ++++++++
 .../selftests/filesystems/xfs_dio_remap.c     | 391 ++++++++
 28 files changed, 3116 insertions(+), 88 deletions(-)
 create mode 100644 fs/tests/dio_drain_kunit.c
 create mode 100644 fs/xfs/tests/xfs_dio_test.c
 create mode 100644 fs/xfs/xfs_dio_test.h
 create mode 100644 tools/memory-model/litmus-tests/DIO-register-first.litmus
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_admission.c
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_drain.c
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_remap.c

base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4

Signature · 1 line
2.56.0
#1 lore raw

RFC PATCH 1/6 fs: implement range-based generation-coordinated DIO draining

Whole-inode direct I/O (DIO) draining currently requires filesystems to exclude all new submission requests, even those completely disjoint from the range being modified.

Add generation accounting and range admission to allow filesystems to drain a finite cohort of in-flight DIO while concurrently admitting new, disjoint DIO under a shared i_rwsem.

This reserves the top two bits of i_inode->i_dio_count for generation and draining state, utilizes seqcount_latch_t for lockless range snapshot validation, and introduces a new ->dio_drain_complete VFS operation to resolve private drain state lazily without bloating struct inode or per-request storage.

---
 Documentation/filesystems/locking.rst         |  89 ++++++++++-----
 Documentation/filesystems/vfs.rst             |  41 +++++++
 fs/inode.c                                    | 107 +++++++++++++++++-
 fs/xfs/xfs_reflink.h                          |   2 +-
 include/linux/fs.h                            |  92 ++++++++++++++-
 .../litmus-tests/DIO-register-first.litmus    |  36 ++++++
 6 files changed, 334 insertions(+), 33 deletions(-)
 create mode 100644 tools/memory-model/litmus-tests/DIO-register-first.litmus
Documentation/filesystems/locking.rst +59 -30
index f7363ad770c7..cfd4ce7ed563 100644
@@ -93,40 +93,44 @@ prototypes::
 	int (*fileattr_get)(struct dentry *dentry, struct file_kattr *fa);
 	struct posix_acl * (*get_acl)(const struct mnt_idmap *, struct dentry *, int);
 	struct offset_ctx *(*get_offset_ctx)(struct inode *inode);
+	void (*dio_drain_complete)(struct inode *inode);
 
 locking rules:
-	all may block
+	all may block except dio_drain_complete
 
-==============	==================================================
-ops		i_rwsem(inode)
-==============	==================================================
-lookup:		shared
-create:		exclusive
-link:		exclusive (both)
-mknod:		exclusive
-symlink:	exclusive
-mkdir:		exclusive
-unlink:		exclusive (both)
-rmdir:		exclusive (both)(see below)
-rename:		exclusive (both parents, some children)	(see below)
-readlink:	no
-get_link:	no
-setattr:	exclusive
-permission:	no (may not block if called in rcu-walk mode)
-get_inode_acl:	no
-get_acl:	no
-getattr:	no
-listxattr:	no
-fiemap:		no
-update_time:	no
-sync_lazytime:	no
-atomic_open:	shared (exclusive if O_CREAT is set in open flags)
-tmpfile:	no
-fileattr_get:	no or exclusive
-fileattr_set:	exclusive
-get_offset_ctx  no
-==============	==================================================
+==================  ==================================================
+ops                 i_rwsem(inode)
+==================  ==================================================
+lookup:             shared
+create:             exclusive
+link:               exclusive (both)
+mknod:              exclusive
+symlink:            exclusive
+mkdir:              exclusive
+unlink:             exclusive (both)
+rmdir:              exclusive (both)(see below)
+rename:             exclusive (both parents, some children) (see below)
+readlink:           no
+get_link:           no
+setattr:            exclusive
+permission:         no (may not block if called in rcu-walk mode)
+get_inode_acl:      no
+get_acl:            no
+getattr:            no
+listxattr:          no
+fiemap:             no
+update_time:        no
+sync_lazytime:      no
+atomic_open:        shared (exclusive if O_CREAT is set in open flags)
+tmpfile:            no
+fileattr_get:       no or exclusive
+fileattr_set:       exclusive
+get_offset_ctx      no
+dio_drain_complete  no
+==================  ==================================================
 
+->dio_drain_complete() does not require i_rwsem and must not block. It may
+run in interrupt context.
 
 	Additionally, ->rmdir(), ->unlink() and ->rename() have ->i_rwsem
 	exclusive on victim.
@@ -138,6 +142,31 @@ get_offset_ctx  no
 See Documentation/filesystems/directory-locking.rst for more detailed discussion
 of the locking scheme for directory operations.
 
+Direct I/O range draining
+=========================
+
+Range drainers acquire ``drain->lock`` before ``inode->i_rwsem`` and any
+filesystem locks needed by the protected operation. Hold the drain mutex
+through inode_dio_drain_finish(): it serializes range publication, cohort
+draining, the protected operation, and exclusion removal. Never wait for
+this mutex while holding i_rwsem, even shared.
+
+A drainer can drop i_rwsem and reacquire it exclusively during write
+checks, for example to strip privileges. Acquiring the mutex inside shared
+i_rwsem would permit this deadlock:
+
+1. Drainer A holds the drain mutex and shared i_rwsem.
+2. Drainer B holds shared i_rwsem and waits for A's drain mutex.
+3. A drops shared i_rwsem and tries to acquire it exclusively. B's shared
+   lock prevents A from proceeding, while B cannot release it until A
+   releases the mutex.
+
+Callers that already hold i_rwsem must drop it before waiting for the drain
+mutex, then reacquire i_rwsem and repeat their checks. DIO admission and
+completion do not acquire the drain mutex, so the old cohort can complete
+while a drainer holds it. Drain the old cohort before taking any filesystem
+lock that its I/O preparation or completion might need.
+
 xattr_handler operations
 ========================
 
Documentation/filesystems/vfs.rst +41 -0

53 lines not on this page · raw message

fs/inode.c +106 -1

122 lines not on this page · raw message

fs/xfs/xfs_reflink.h +1 -1
index 683c1841e640..c862b9a01c78 100644
@@ -20,7 +20,7 @@ xfs_can_free_cowblocks(struct xfs_inode *ip)
 	if ((inode_state_read_once(inode) & I_DIRTY_PAGES) ||
 	    mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY) ||
 	    mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK) ||
-	    atomic_read(&inode->i_dio_count))
+	    !inode_dio_finished(inode))
 		return false;
 	return true;
 }
include/linux/fs.h +91 -1

116 lines not on this page · raw message

tools/memory-model/litmus-tests/DIO-register-first.litmus new file +36 -0
new file mode 100644
index 000000000000..a7bca403bdfa
@@ -0,0 +1,36 @@
+C DIO-register-first
+
+(*
+ * Result: Never
+ *
+ * Model one drain cutover, with count bit 0 and generation bit 1.
+ * Publishing both latch copies (or the first initialized drain pointer)
+ * precedes the fully ordered generation RMW. An admission increment which
+ * follows the cutover must see the exclusion. A pre-cutover registration is
+ * instead accounted in the finite old cohort. Completion and subsequent
+ * latch reuse are deliberately outside this single-cutover ordering model.
+ *)
+
+{
+	atomic_t count = 0;
+	int active = 0;
+}
+
+P0(atomic_t *count, int *active)
+{
+	int r0;
+
+	WRITE_ONCE(*active, 1);
+	r0 = atomic_fetch_add(2, count);
+}
+
+P1(atomic_t *count, int *active)
+{
+	int r0;
+	int r1;
+
+	r0 = atomic_fetch_add(1, count);
+	r1 = READ_ONCE(*active);
+}
+
+exists (1:r0=2 /\ 1:r1=0)
Signature · 1 line
2.56.0
#2 lore raw

RFC PATCH 2/6 iomap: support generation accounting and caller-registered DIO

In reply to Vitali Lovich

Generation draining requires every DIO request on an opted-in inode to preserve its admission generation until completion. Convert both generic iomap DIO and the simple-read path to generation-aware accounting, reusing their existing internal flag fields.

Allow callers to register DIO before entering iomap with IOMAP_DIO_ACCOUNTED and pass the saved token in IOMAP_DIO_GENERATION. Iomap owns that registration and releases it on completion or any early return, including allocation failures, empty requests and -ENOTBLK. Callers retrying through the generic path must acquire a fresh admission. Callers without a registration retain internal iomap accounting.

Pass the flags into iomap_dio_read_simple() and update existing XFS and ext4 callers to pass zero. Preserve the token through mapping and submission errors, and read it before bio_put() frees the bio-backed simple-read struct. The completion lifetime still includes filesystem end_io work and post-write invalidation.

---
 fs/ext4/file.c        |  2 +-
 fs/iomap/direct-io.c  | 36 +++++++++++++++++++++-------
 fs/xfs/xfs_file.c     |  2 +-
 include/linux/iomap.h | 56 +++++++++++++++++++++++++++++--------------
 4 files changed, 67 insertions(+), 29 deletions(-)
fs/ext4/file.c +1 -1
index 50d3c92709c8..cb6d6722c18e 100644
@@ -91,7 +91,7 @@ static ssize_t ext4_dio_read_iter(struct kiocb *iocb, struct iov_iter *to)
 		return generic_file_read_iter(iocb, to);
 	}
 
-	ret = iomap_dio_read_simple(iocb, to, ext4_iomap_begin);
+	ret = iomap_dio_read_simple(iocb, to, ext4_iomap_begin, 0);
 	if (ret == -ENOTBLK)
 		ret = iomap_dio_rw(iocb, to, &ext4_iomap_ops, NULL, 0, NULL, 0);
 	inode_unlock_shared(inode);
fs/iomap/direct-io.c +27 -9
index ad041ac3252a..fffe6702c42f 100644
@@ -149,7 +149,8 @@ ssize_t iomap_dio_complete(struct iomap_dio *dio)
 	    !(dio->flags & IOMAP_DIO_NO_INVALIDATE))
 		kiocb_invalidate_post_direct_write(iocb, dio->size);
 
-	inode_dio_end(file_inode(iocb->ki_filp));
+	inode_dio_end_generation(file_inode(iocb->ki_filp),
+				dio->flags & IOMAP_DIO_GENERATION);
 
 	if (ret > 0) {
 		iocb->ki_pos += ret;
@@ -690,6 +691,12 @@ static int iomap_dio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
  * the correct total number of bytes transferred; this is essential for
  * completing partial requests asynchronously.
  *
+ * When @dio_flags includes IOMAP_DIO_ACCOUNTED, iomap takes ownership of the
+ * caller's registration from inode_dio_try_begin() on entry, with its saved
+ * generation token supplied in IOMAP_DIO_GENERATION. Iomap releases the
+ * registration on completion or any early error or NULL return, including
+ * -ENOTBLK. Callers retrying the request must acquire a fresh registration.
+ *
  * Returns -ENOTBLK In case of a page invalidation invalidation failure for
  * writes.  The callers needs to fall back to buffered I/O in this case.
  */
@@ -708,6 +715,8 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 	};
 	bool wait_for_completion =
 		is_sync_kiocb(iocb) || (dio_flags & IOMAP_DIO_FORCE_WAIT);
+	bool accounted = dio_flags & IOMAP_DIO_ACCOUNTED;
+	bool generation = dio_flags & IOMAP_DIO_GENERATION;
 	struct blk_plug plug;
 	struct iomap_dio *dio;
 	loff_t ret = 0;
@@ -715,11 +724,13 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 	trace_iomap_dio_rw_begin(iocb, iter, dio_flags, done_before);
 
 	if (!iomi.len)
-		return NULL;
+		goto out_account;
 
 	dio = kmalloc_obj(*dio);
-	if (!dio)
-		return ERR_PTR(-ENOMEM);
+	if (!dio) {
+		ret = -ENOMEM;
+		goto out_account;
+	}
 
 	dio->iocb = iocb;
 	atomic_set(&dio->ref, 1);
@@ -816,7 +827,10 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 			goto out_free_dio;
 	}
 
-	inode_dio_begin(inode);
+	if (!accounted)
+		generation = inode_dio_begin_generation(inode);
+	if (generation)
+		dio->flags |= IOMAP_DIO_GENERATION;
 
 	blk_start_plug(&plug);
 	while ((ret = iomap_iter(&iomi, ops)) > 0) {
@@ -901,6 +915,9 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 
 out_free_dio:
 	kfree(dio);
+out_account:
+	if (accounted)
+		inode_dio_end_generation(inode, generation);
 	if (ret)
 		return ERR_PTR(ret);
 	return NULL;
@@ -943,6 +960,7 @@ static ssize_t iomap_dio_simple_complete(struct iomap_dio_simple *sr)
 	struct bio *bio = &sr->bio;
 	struct kiocb *iocb = sr->iocb;
 	struct inode *inode = file_inode(iocb->ki_filp);
+	bool generation = sr->dio_flags & IOMAP_DIO_GENERATION;
 	ssize_t ret;
 
 	if (unlikely(bio->bi_status)) {
@@ -962,7 +980,7 @@ static ssize_t iomap_dio_simple_complete(struct iomap_dio_simple *sr)
 		bio_release_pages(bio, false);
 		bio_put(bio);
 	}
-	inode_dio_end(inode);
+	inode_dio_end_generation(inode, generation);
 	trace_iomap_dio_complete(iocb, ret < 0 ? ret : 0, ret);
 	return ret;
 }
@@ -980,7 +998,7 @@ static void iomap_dio_simple_end_io(struct bio *bio)
 }
 
 ssize_t __iomap_dio_read_simple(struct kiocb *iocb, struct iov_iter *iter,
-		struct iomap_iter *iomi)
+		struct iomap_iter *iomi, bool generation)
 {
 	gfp_t gfp = (iomi->flags & IOMAP_NOWAIT) ? GFP_NOWAIT : GFP_KERNEL;
 	struct iomap_dio_simple *sr;
@@ -1019,7 +1037,7 @@ ssize_t __iomap_dio_read_simple(struct kiocb *iocb, struct iov_iter *iter,
 	}
 	sr = container_of(bio, struct iomap_dio_simple, bio);
 	sr->iocb = iocb;
-	sr->dio_flags = 0;
+	sr->dio_flags = generation ? IOMAP_DIO_GENERATION : 0;
 
 	bio->bi_iter.bi_sector = iomap_sector(&iomi->iomap, iomi->pos);
 	bio->bi_ioprio = iocb->ki_ioprio;
@@ -1064,7 +1082,7 @@ ssize_t __iomap_dio_read_simple(struct kiocb *iocb, struct iov_iter *iter,
 out_bio_put:
 	bio_put(bio);
 out_dio_end:
-	inode_dio_end(iomi->inode);
+	inode_dio_end_generation(iomi->inode, generation);
 	return ret;
 }
 EXPORT_SYMBOL_GPL(__iomap_dio_read_simple);
fs/xfs/xfs_file.c +1 -1
index 7d0c8a197575..124a037cfa9f 100644
@@ -253,7 +253,7 @@ xfs_file_dio_read(
 		ret = iomap_dio_rw(iocb, to, &xfs_read_iomap_ops,
 				&xfs_dio_read_bounce_ops, 0, NULL, 0);
 	} else {
-		ret = iomap_dio_read_simple(iocb, to, xfs_read_iomap_begin);
+		ret = iomap_dio_read_simple(iocb, to, xfs_read_iomap_begin, 0);
 		if (ret == -ENOTBLK)
 			ret = iomap_dio_rw(iocb, to, &xfs_read_iomap_ops, NULL,
 					0, NULL, 0);
include/linux/iomap.h +38 -18

99 lines not on this page · raw message

Signature · 1 line
2.56.0
#3 lore raw

RFC PATCH 3/6 fs: add KUnit coverage for generation-based DIO draining

In reply to Vitali Lovich

Exercise admission before lazy allocation, rejection of overlapping DIO, disjoint new-generation requests and cohort rollover. Check that iomap consumes caller registrations on simple-read fallback and generic early returns, including an old-generation completion during an active drain.

Keep generation registration inline in normal builds. When DIO_DRAIN_KUNIT_TEST is enabled, provide an out-of-line definition in fs/inode.c with a static stub and one function address across translation units. Keep the KUnit stub headers out of linux/fs.h.

Use the stub to pause the real admission helper after its provisional increment and before its first pointer load. Publish the first drain allocation and verify that cutover completes while the reader is paused. The resumed overlapping admission must reject the request and withdraw its old-generation registration. Release both threads and any outstanding DIO count before reporting failures. Skip this race test without two online CPUs.

Check snapshot reads while a latch writer is interrupted at either copy update, and exercise repeated cutover and range reuse across both generations.

Inject iomap allocation and mapping failures with both saved generation values. Pause filesystem end_io in the real iomap completion path and verify that it remains in the old cohort while newer disjoint I/O finishes. Check successful short-read and failed completion accounting.

Exercise real bio-based writes on a hidden test block device, including a successful partial request, driver completion errors and block-layer submission rejection. Check both saved generation values, delayed filesystem completion, newer disjoint progress and exact token release.

---
 fs/Kconfig                 |  13 +
 fs/Makefile                |   1 +
 fs/inode.c                 |  13 +
 fs/iomap/direct-io.c       |  20 +-
 fs/tests/dio_drain_kunit.c | 893 +++++++++++++++++++++++++++++++++++++
 include/linux/fs.h         |   5 +
 include/linux/iomap.h      |   4 +
 7 files changed, 948 insertions(+), 1 deletion(-)
 create mode 100644 fs/tests/dio_drain_kunit.c
fs/Kconfig +13 -0
index 70a1d0760d28..e004f5a0d6f0 100644
@@ -428,4 +428,17 @@ config FDTABLE_KUNIT_TEST
 	  This builds the fdtable KUnit tests, which tests various aspects
 	  of the fdtable structure and allocation.
 
+config DIO_DRAIN_KUNIT_TEST
+	tristate "KUnit tests for direct I/O range draining" if !KUNIT_ALL_TESTS
+	depends on KUNIT && BLOCK
+	select FS_IOMAP
+	default KUNIT_ALL_TESTS
+	help
+	  Test direct I/O range admission, generation accounting, and
+	  seqcount_latch range snapshots across cohort cutovers, including
+	  iomap early-return ownership and delayed-admission races.
+	  The concurrent admission test requires at least two online CPUs.
+
+	  If unsure, say N.
+
 endmenu
fs/Makefile +1 -0
index 16f1108b64e1..8974a3bba893 100644
@@ -28,6 +28,7 @@ obj-$(CONFIG_SIGNALFD)		+= signalfd.o
 obj-$(CONFIG_TIMERFD)		+= timerfd.o
 obj-$(CONFIG_EVENTFD)		+= eventfd.o
 obj-$(CONFIG_AIO)               += aio.o
+obj-$(CONFIG_DIO_DRAIN_KUNIT_TEST) += tests/dio_drain_kunit.o
 obj-$(CONFIG_FS_DAX)		+= dax.o
 obj-$(CONFIG_FS_ENCRYPTION)	+= crypto/
 obj-$(CONFIG_FS_VERITY)		+= verity/
fs/inode.c +13 -0
index 7b027547b305..b31d5ac5bb96 100644
@@ -23,6 +23,9 @@
 #include <linux/rw_hint.h>
 #include <linux/seq_file.h>
 #include <linux/debugfs.h>
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+#include <kunit/static_stub.h>
+#endif
 #include <trace/events/writeback.h>
 #define CREATE_TRACE_POINTS
 #include <trace/events/timestamp.h>
@@ -2783,6 +2786,16 @@ bool inode_dio_finished(const struct inode *inode)
 }
 EXPORT_SYMBOL(inode_dio_finished);
 
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+bool inode_dio_begin_generation(struct inode *inode)
+{
+	KUNIT_STATIC_STUB_REDIRECT(inode_dio_begin_generation, inode);
+	return atomic_fetch_inc(&inode->i_dio_count) &
+		INODE_DIO_GENERATION;
+}
+EXPORT_SYMBOL_GPL(inode_dio_begin_generation);
+#endif
+
 /*
  * Allocated on the first draining operation, never on ordinary DIO. The
  * filesystem owns the pointer and retains the allocation until inode reclaim.
fs/iomap/direct-io.c +19 -1
index fffe6702c42f..2c840358abc8 100644
@@ -13,6 +13,9 @@
 #include <linux/init.h>
 #include "internal.h"
 #include "trace.h"
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+#include <kunit/static_stub.h>
+#endif
 
 #include "../internal.h"
 
@@ -674,6 +677,21 @@ static int iomap_dio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
 	}
 }
 
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+/* Fault injection is scoped to the task running the ownership test. */
+struct iomap_dio *iomap_dio_alloc(void)
+{
+	KUNIT_STATIC_STUB_REDIRECT(iomap_dio_alloc);
+	return kmalloc_obj(struct iomap_dio);
+}
+EXPORT_SYMBOL_GPL(iomap_dio_alloc);
+#else
+static inline struct iomap_dio *iomap_dio_alloc(void)
+{
+	return kmalloc_obj(struct iomap_dio);
+}
+#endif
+
 /*
  * iomap_dio_rw() always completes O_[D]SYNC writes regardless of whether the IO
  * is being issued as AIO or not.  This allows us to optimise pure data writes
@@ -726,7 +744,7 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 	if (!iomi.len)
 		goto out_account;
 
-	dio = kmalloc_obj(*dio);
+	dio = iomap_dio_alloc();
 	if (!dio) {
 		ret = -ENOMEM;
 		goto out_account;
fs/tests/dio_drain_kunit.c new file +893 -0

893 lines not on this page · raw message

include/linux/fs.h +5 -0
index 016f621345af..3eb62a40ea21 100644
@@ -3222,11 +3222,16 @@ static inline bool inode_dio_drain_overlap(struct inode *inode,
 	return range.active && last >= range.start && pos <= range.last;
 }
 
+/* Test builds need a single function address for the registration stub. */
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+bool inode_dio_begin_generation(struct inode *inode);
+#else
 static inline bool inode_dio_begin_generation(struct inode *inode)
 {
 	return atomic_fetch_inc(&inode->i_dio_count) &
 		INODE_DIO_GENERATION;
 }
+#endif
 
 static inline void inode_dio_end_generation(struct inode *inode, bool generation)
 {
include/linux/iomap.h +4 -0
index 611c37c19f82..f6652f5d8ac5 100644
@@ -723,6 +723,10 @@ struct iomap_dio_ops {
 ssize_t iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 		const struct iomap_ops *ops, const struct iomap_dio_ops *dops,
 		unsigned int dio_flags, void *private, size_t done_before);
+#if IS_ENABLED(CONFIG_DIO_DRAIN_KUNIT_TEST)
+struct iomap_dio *iomap_dio_alloc(void);
+#endif
+
 struct iomap_dio *__iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
 		const struct iomap_ops *ops, const struct iomap_dio_ops *dops,
 		unsigned int dio_flags, void *private, size_t done_before);
Signature · 1 line
2.56.0
#4 lore raw

RFC PATCH 4/6 xfs: add range-drain state and latch-based DIO admission

In reply to Vitali Lovich

Prepare XFS for range draining with lazily allocated inode-private state, retained across VFS inode recycling and freed at XFS inode reclamation. Provide ->dio_drain_complete to resolve the state for older requests.

Register provisionally before the final latch range snapshot for every direct read and write submission. Transfer the token to iomap, and obtain a fresh admission for simple-read fallback or write-check retries.

Overlapping IOCB_NOWAIT requests return -EAGAIN. Blocking requests drop IOLOCK before waiting for a nonoverlapping snapshot and contribute no DIO count while asleep. Writers reacquire through xfs_ilock_iocb_for_write(), repeat append and limit checks, and recheck unaligned COW eligibility because a clone can share blocks while IOLOCK is dropped.

Establish the final admission and ownership rules before enabling either range-drain consumer.

Provide optional root-only debugfs gates and counters for deterministic filesystem selftests of request revalidation. The controls pin the selected file and release paused tasks on close or timeout; normal builds omit them.

---
 fs/xfs/Kconfig              |   9 ++
 fs/xfs/Makefile             |   2 +
 fs/xfs/tests/xfs_dio_test.c | 190 ++++++++++++++++++++++++++++++++++++
 fs/xfs/xfs_dio_test.h       |  32 ++++++
 fs/xfs/xfs_file.c           | 162 ++++++++++++++++++++++++++++--
 fs/xfs/xfs_icache.c         |   2 +
 fs/xfs/xfs_inode.h          |   1 +
 fs/xfs/xfs_iops.c           |   9 ++
 fs/xfs/xfs_reflink.c        |   2 +
 fs/xfs/xfs_super.c          |   2 +
 10 files changed, 401 insertions(+), 10 deletions(-)
 create mode 100644 fs/xfs/tests/xfs_dio_test.c
 create mode 100644 fs/xfs/xfs_dio_test.h
fs/xfs/Kconfig +9 -0
index b99da294e9a3..9d36201b5cdc 100644
@@ -204,6 +204,15 @@ config XFS_WARN
 	  However, similar to XFS_DEBUG, it is only advisable to use this if you
 	  are debugging a particular problem.
 
+config XFS_DIO_TEST
+	bool "XFS direct I/O filesystem selftest synchronization"
+	depends on XFS_FS && DEBUG_FS
+	help
+	  Enable root-only debugfs synchronization and event counters used by
+	  direct I/O selftests on a mounted XFS filesystem. Tests can pause a
+	  selected inode at explicit events; closing the control file or a
+	  ten-second timeout releases paused tasks. Say N for production kernels.
+
 config XFS_DEBUG
 	bool "XFS Debugging support"
 	depends on XFS_FS
fs/xfs/Makefile +2 -0
index 399a207f2d0e..2c31202b6ba1 100644
@@ -110,6 +110,8 @@ xfs-y				+= xfs_aops.o \
 				   xfs_verify_media.o \
 				   xfs_xattr.o
 
+xfs-$(CONFIG_XFS_DIO_TEST) += tests/xfs_dio_test.o
+
 # low-level transaction/log code
 xfs-y				+= xfs_log.o \
 				   xfs_log_cil.o \
fs/xfs/tests/xfs_dio_test.c new file +190 -0

190 lines not on this page · raw message

fs/xfs/xfs_dio_test.h new file +32 -0
new file mode 100644
index 000000000000..178b668580d8
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __XFS_DIO_TEST_H__
+#define __XFS_DIO_TEST_H__
+
+struct inode;
+struct dentry;
+
+enum xfs_dio_test_event {
+	XFS_DIO_TEST_BATCH_LEADER,
+	XFS_DIO_TEST_BATCH_FOLLOWER,
+	XFS_DIO_TEST_BATCH_COLLECT,
+	XFS_DIO_TEST_BATCH_ZERO,
+	XFS_DIO_TEST_WAIT_UNLOCKED,
+	XFS_DIO_TEST_WRITE_CHECKS,
+	XFS_DIO_TEST_WRITE_ADMIT,
+	XFS_DIO_TEST_WRITE_REJECT,
+	XFS_DIO_TEST_UNALIGNED_RECHECK,
+	XFS_DIO_TEST_REMAP_ACTIVE,
+	XFS_DIO_TEST_REMAP_UPGRADE,
+	XFS_DIO_TEST_NR_EVENTS,
+};
+
+#ifdef CONFIG_XFS_DIO_TEST
+void xfs_dio_test_init(struct dentry *parent);
+void xfs_dio_test_event(struct inode *inode, enum xfs_dio_test_event event,
+		unsigned long value);
+#else
+static inline void xfs_dio_test_init(struct dentry *parent) { }
+static inline void xfs_dio_test_event(struct inode *inode,
+		enum xfs_dio_test_event event, unsigned long value) { }
+#endif
+#endif
fs/xfs/xfs_file.c +152 -10

266 lines not on this page · raw message

fs/xfs/xfs_icache.c +2 -0
index de8be344e987..e26df6894618 100644
@@ -110,6 +110,7 @@ xfs_inode_alloc(
 	ip->i_mount = mp;
 	memset(&ip->i_imap, 0, sizeof(struct xfs_imap));
 	ip->i_cowfp = NULL;
+	ip->i_dio_drain = NULL;
 	memset(&ip->i_af, 0, sizeof(ip->i_af));
 	ip->i_af.if_format = XFS_DINODE_FMT_EXTENTS;
 	memset(&ip->i_df, 0, sizeof(ip->i_df));
@@ -156,6 +157,7 @@ xfs_inode_free_callback(
 		xfs_inode_item_destroy(ip);
 	}
 
+	kfree(ip->i_dio_drain);
 	kmem_cache_free(xfs_inode_cache, ip);
 }
 
fs/xfs/xfs_inode.h +1 -0
index 75eff68c1e39..04907a7b431f 100644
@@ -41,6 +41,7 @@ typedef struct xfs_inode {
 	struct xfs_inode_log_item *i_itemp;	/* logging information */
 	struct rw_semaphore	i_lock;		/* inode lock */
 	atomic_t		i_pincount;	/* inode pin count */
+	struct inode_dio_drain	*i_dio_drain; /* lazily allocated DIO drain */
 	struct llist_node	i_gclist;	/* deferred inactivation list */
 
 	/*
fs/xfs/xfs_iops.c +9 -0
index d307ec97fac8..dec2139c0c6f 100644
@@ -1278,6 +1278,14 @@ xfs_vn_tmpfile(
 	return finish_open_simple(file, err);
 }
 
+static void
+xfs_vn_dio_drain_complete(
+	struct inode		*inode)
+{
+	/* The generation cutover orders i_dio_drain initialization before this callback. */
+	inode_dio_drain_complete(inode, READ_ONCE(XFS_I(inode)->i_dio_drain));
+}
+
 static const struct inode_operations xfs_inode_operations = {
 	.get_inode_acl		= xfs_get_acl,
 	.set_acl		= xfs_set_acl,
@@ -1289,6 +1297,7 @@ static const struct inode_operations xfs_inode_operations = {
 	.sync_lazytime		= xfs_vn_sync_lazytime,
 	.fileattr_get		= xfs_fileattr_get,
 	.fileattr_set		= xfs_fileattr_set,
+	.dio_drain_complete	= xfs_vn_dio_drain_complete,
 };
 
 static const struct inode_operations xfs_dir_inode_operations = {
fs/xfs/xfs_reflink.c +2 -0
index 104542b10d9e..3e1b7d628477 100644
@@ -12,6 +12,7 @@
 #include "xfs_mount.h"
 #include "xfs_defer.h"
 #include "xfs_inode.h"
+#include "xfs_dio_test.h"
 #include "xfs_trans.h"
 #include "xfs_bmap.h"
 #include "xfs_bmap_util.h"
@@ -1763,6 +1764,7 @@ xfs_reflink_remap_prep(
 	xfs_iflags_set(src, XFS_IREMAPPING);
 	if (inode_in != inode_out)
 		xfs_ilock_demote(src, XFS_IOLOCK_EXCL | XFS_MMAPLOCK_EXCL);
+	xfs_dio_test_event(inode_in, XFS_DIO_TEST_REMAP_ACTIVE, 0);
 
 	return 0;
 out_unlock:
fs/xfs/xfs_super.c +2 -0
index 5a06132aa384..a7e84ddd145b 100644
@@ -12,6 +12,7 @@
 #include "xfs_sb.h"
 #include "xfs_mount.h"
 #include "xfs_inode.h"
+#include "xfs_dio_test.h"
 #include "xfs_btree.h"
 #include "xfs_bmap.h"
 #include "xfs_alloc.h"
@@ -2677,6 +2678,7 @@ init_xfs_fs(void)
 		goto out_cleanup_procfs;
 
 	xfs_debugfs = xfs_debugfs_mkdir("xfs", NULL);
+	xfs_dio_test_init(xfs_debugfs);
 
 	xfs_kset = kset_create_and_add("xfs", NULL, fs_kobj);
 	if (!xfs_kset) {
Signature · 1 line
2.56.0
#5 lore raw

RFC PATCH 5/6 xfs: use range-based DIO draining for aligned hole punches

In reply to Vitali Lovich

Hole punching holds exclusive IOLOCK while draining DIO and modifying the range, blocking new submissions throughout the inode. Use shared IOLOCK and a latch-published range exclusion for eligible aligned data-device punches so disjoint new DIO can proceed.

Reject obvious ineligible requests before allocating drain state or taking the drain mutex. Obtain the drain mutex before IOLOCK and recheck eligibility under shared IOLOCK. A waiter must not hold shared IOLOCK while the mutex owner may drop and reacquire IOLOCK exclusively during write checks. Retain the existing path for DAX, realtime, privilege/capability stripping (!IS_NOSEC), reflink remapping, unaligned punches, layout breaking, or an unrepresentable rounded endpoint. Round the exclusion to the larger of the allocation unit and PAGE_SIZE, matching cache invalidation.

Drain the pre-cutover cohort before taking MMAPLOCK: an in-flight DIO can fault in a buffer backed by the same file. Skip unnecessary partial block zeroing for aligned data-device punches so the operation can retain shared IOLOCK. Keep the exclusion through extent removal and any required synchronous log force, and report allocation or drain-start errors.

Add a filesystem selftest that parks registered DIO in a userfaultfd fault, observes the active exclusion, and checks disjoint progress and overlapping NOWAIT write rejection. Race faulting same-inode buffers with punching, using synchronous and AIO extending and COW writes, and verify clone isolation.

Exercise repeated exclusion retries and append-position/limit changes while IOLOCK is dropped, including iterator expansion and a limit that changes DIO alignment.

Verify completed and in-progress reflink remaps during the unlocked window, including exact shared-extent evidence, unaligned COW fallback, exclusive lock reacquisition and clone isolation.

---
 fs/xfs/xfs_bmap_util.c                        |   9 +-
 fs/xfs/xfs_file.c                             |  88 ++++
 fs/xfs/xfs_inode.c                            |  10 +
 tools/testing/selftests/filesystems/Makefile  |   6 +
 .../selftests/filesystems/xfs_dio_admission.c | 380 +++++++++++++++++
 .../selftests/filesystems/xfs_dio_drain.c     | 371 +++++++++++++++++
 .../selftests/filesystems/xfs_dio_remap.c     | 391 ++++++++++++++++++
 7 files changed, 1253 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_admission.c
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_drain.c
 create mode 100644 tools/testing/selftests/filesystems/xfs_dio_remap.c
fs/xfs/xfs_bmap_util.c +7 -2
index 865aec3ae5ab..30759c6f94d3 100644
@@ -889,8 +889,8 @@ xfs_free_file_space(
 		return 0;
 
 	/*
-	 * Now AIO and DIO has drained we flush and (if necessary) invalidate
-	 * the cached range over the first operation we are about to run.
+	 * The caller has excluded and drained DIO over the affected range.
+	 * Flush and invalidate the cached range before removing its mappings.
 	 */
 	error = xfs_flush_unmap_range(ip, offset, len);
 	if (error)
@@ -917,6 +917,11 @@ xfs_free_file_space(
 		}
 	}
 
+	/* An aligned data-device punch needs no partial-block zeroing. */
+	if (!XFS_IS_REALTIME_INODE(ip) &&
+	    !((offset | len) & mp->m_blockmask))
+		return 0;
+
 	/*
 	 * Now that we've unmap all full blocks we'll have to zero out any
 	 * partial block at the beginning and/or end.  xfs_zero_range is smart
fs/xfs/xfs_file.c +88 -0

100 lines not on this page · raw message

fs/xfs/xfs_inode.c +10 -0
index 05a14da28031..b8119abab30a 100644
@@ -118,6 +118,16 @@ xfs_lock_flags_assert(
  *
  * i_rwsem -> invalidate_lock -> page_lock -> i_ilock
  *
+ * DIO range drainers add an outer mutex:
+ *
+ * i_dio_drain->lock -> i_rwsem -> invalidate_lock -> page_lock -> i_ilock
+ *
+ * Never wait for the drain mutex while holding IOLOCK: its owner may drop
+ * shared IOLOCK and reacquire it exclusively during write checks. A waiter
+ * holding shared IOLOCK would prevent that upgrade and deadlock both drainers.
+ * The mutex stays held through exclusion removal, including extent freeing
+ * and any synchronous log force. DIO admission and completion do not take it.
+ *
  * mmap_lock locking order:
  *
  * i_rwsem -> page lock -> mmap_lock
tools/testing/selftests/filesystems/Makefile +6 -0
index fbd5c27505ef..0c572f10fa71 100644
@@ -5,7 +5,13 @@ TEST_GEN_PROGS := devpts_pts anon_inode_test kernfs_test fclog ustat_test rw_hin
 TEST_GEN_PROGS += idmapped_tmpfile
 TEST_GEN_PROGS_EXTENDED := dnotify_test
 
+TEST_GEN_PROGS_EXTENDED += xfs_dio_drain xfs_dio_admission xfs_dio_remap
+
 include ../lib.mk
 
+$(OUTPUT)/xfs_dio_drain: LDLIBS += -pthread
+$(OUTPUT)/xfs_dio_admission: LDLIBS += -pthread
+$(OUTPUT)/xfs_dio_remap: LDLIBS += -pthread
+
 $(OUTPUT)/idmapped_tmpfile: LDLIBS += -lcap
 $(OUTPUT)/idmapped_tmpfile: utils.c
tools/testing/selftests/filesystems/xfs_dio_admission.c new file +380 -0

380 lines not on this page · raw message

tools/testing/selftests/filesystems/xfs_dio_drain.c new file +371 -0

371 lines not on this page · raw message

tools/testing/selftests/filesystems/xfs_dio_remap.c new file +391 -0

391 lines not on this page · raw message

Signature · 1 line
2.56.0
#6 lore raw

RFC PATCH 6/6 xfs: use range-based DIO draining for post-EOF zeroing

In reply to Vitali Lovich

Zeroing a gap before a direct write normally drains DIO with exclusive IOLOCK. Use range draining for block-aligned, non-atomic data-device writes entering write checks beyond EOF with shared IOLOCK.

Drop IOLOCK before obtaining the lazy drain state and its mutex, then reacquire through xfs_ilock_iocb_for_write() and repeat write checks. A drainer may need IOLOCK_EXCL during these checks; a second drainer must not hold shared IOLOCK while waiting for its mutex, or both would block. Publish the exclusion under shared IOLOCK, demoting first if required, and drain the pre-cutover cohort while admitting disjoint new DIO.

Round the exclusion outward to the larger of the filesystem block size and mapping_max_folio_size() to cover every folio touched by zeroing and invalidation. Recheck EOF after draining because completions can advance it, then zero any remaining gap under MMAPLOCK_EXCL.

Finish the exclusion before submitting the triggering write, which can share its final folio. Release the drain mutex on every write-check exit. Ineligible writes retain existing handling; allocation and drain-start errors propagate to the caller.

Extend the filesystem selftest to check the post-EOF drain consumer:
park an old registered request, observe the gap exclusion, and verify disjoint progress, overlapping NOWAIT rejection, and preserved data.

Also hold an old extending request while preparation drains, then verify its completed bytes survive the post-drain EOF recheck and only the remaining gap is zeroed.

---
 fs/xfs/xfs_file.c                             | 109 +++++++++++++++---
 .../selftests/filesystems/xfs_dio_drain.c     |  41 +++++--
 2 files changed, 125 insertions(+), 25 deletions(-)
fs/xfs/xfs_file.c +94 -15
index ad83328b7350..53b3d6266e03 100644
@@ -453,6 +453,55 @@ xfs_file_splice_read(
 	return ret;
 }
 
+/*
+ * Serialize zeroing with I/O to all cache folios that can cover the old EOF
+ * block and gap, not unrelated requests outside those folios. Clear the
+ * exclusion before submitting this write, which can share the final folio.
+ * Drain the finite cohort admitted before the generation cutover, including
+ * COW end-I/O remapping. New unrelated requests can proceed under shared IOLOCK.
+ */
+static int
+xfs_file_write_zero_eof_range(
+	struct kiocb		*iocb,
+	loff_t			isize,
+	unsigned int		*iolock,
+	struct inode_dio_drain	*drain)
+{
+	struct inode		*inode = file_inode(iocb->ki_filp);
+	struct xfs_inode	*ip = XFS_I(inode);
+	loff_t			granularity = max_t(loff_t, i_blocksize(inode),
+					mapping_max_folio_size(inode->i_mapping));
+	loff_t			start = round_down(isize, granularity);
+	loff_t			last = (iocb->ki_pos - 1) | (granularity - 1);
+	int			error = 0;
+
+	error = kiocb_modified(iocb);
+	if (error)
+		return error;
+	if (*iolock == XFS_IOLOCK_EXCL) {
+		xfs_ilock_demote(ip, XFS_IOLOCK_EXCL);
+		*iolock = XFS_IOLOCK_SHARED;
+	}
+	/* Folio invalidation is wider than zeroing, even for sub-page blocks. */
+	error = inode_dio_drain_start(inode, drain, start, last);
+	if (error)
+		return error;
+	inode_dio_drain_wait(inode, drain);
+	spin_lock(&ip->i_flags_lock);
+	isize = i_size_read(inode);
+	spin_unlock(&ip->i_flags_lock);
+	if (iocb->ki_pos > isize) {
+		trace_xfs_zero_eof(ip, isize, iocb->ki_pos - isize);
+		xfs_ilock(ip, XFS_MMAPLOCK_EXCL);
+		error = iomap_zero_range(inode, isize, iocb->ki_pos - isize,
+				NULL, &xfs_buffered_write_iomap_ops,
+				&xfs_iomap_write_ops, NULL);
+		xfs_iunlock(ip, XFS_MMAPLOCK_EXCL);
+	}
+	inode_dio_drain_finish(inode, drain);
+	return error;
+}
+
 /*
  * Take care of zeroing post-EOF blocks when they might exist.
  *
@@ -467,20 +516,17 @@ xfs_file_write_zero_eof(
 	unsigned int		*iolock,
 	size_t			count,
 	bool			*drained_dio,
-	struct xfs_zone_alloc_ctx *ac)
+	struct xfs_zone_alloc_ctx *ac,
+	struct inode_dio_drain	*drain)
 {
 	struct xfs_inode	*ip = XFS_I(iocb->ki_filp->f_mapping->host);
 	loff_t			isize;
 	int			error;
 
 	/*
-	 * We need to serialise against EOF updates that occur in IO completions
-	 * here. We want to make sure that nobody is changing the size while
-	 * we do this check until we have placed an IO barrier (i.e. hold
-	 * XFS_IOLOCK_EXCL) that prevents new IO from being dispatched.  The
-	 * spinlock effectively forms a memory barrier once we have
-	 * XFS_IOLOCK_EXCL so we are guaranteed to see the latest EOF value and
-	 * hence be able to correctly determine if we need to run zeroing.
+	 * Pair with EOF updates in DIO completion. The IOLOCK excludes truncate,
+	 * so EOF can only move forwards. Recheck after establishing either a
+	 * range exclusion or the exclusive IOLOCK and draining the relevant I/O.
 	 */
 	spin_lock(&ip->i_flags_lock);
 	isize = i_size_read(VFS_I(ip));
@@ -493,6 +539,9 @@ xfs_file_write_zero_eof(
 	if (iocb->ki_flags & IOCB_NOWAIT)
 		return -EAGAIN;
 
+	if (drain)
+		return xfs_file_write_zero_eof_range(iocb, isize, iolock, drain);
+
 	if (!*drained_dio) {
 		/*
 		 * If zeroing is needed and we are currently holding the iolock
@@ -543,23 +592,49 @@ xfs_file_write_checks(
 	struct xfs_zone_alloc_ctx *ac)
 {
 	struct inode		*inode = iocb->ki_filp->f_mapping->host;
+	struct inode_dio_drain	*drain = NULL;
 	size_t			count = iov_iter_count(from);
 	bool			drained_dio = false;
 	ssize_t			error;
 
+	/*
+	 * Another drainer may need IOLOCK_EXCL during write checks. Do not pin
+	 * shared IOLOCK while waiting for its drain mutex.
+	 */
+	if (*iolock == XFS_IOLOCK_SHARED &&
+	    (iocb->ki_flags & IOCB_DIRECT) &&
+	    !(iocb->ki_flags & IOCB_ATOMIC) &&
+	    !XFS_IS_REALTIME_INODE(XFS_I(inode)) &&
+	    IS_ALIGNED(iocb->ki_pos | count, i_blocksize(inode)) &&
+	    iocb->ki_pos > i_size_read(inode)) {
+		if (iocb->ki_flags & IOCB_NOWAIT)
+			return -EAGAIN;
+		xfs_iunlock(XFS_I(inode), *iolock);
+		drain = inode_dio_drain_get(&XFS_I(inode)->i_dio_drain);
+		if (IS_ERR(drain)) {
+			xfs_ilock(XFS_I(inode), *iolock);
+			return PTR_ERR(drain);
+		}
+		mutex_lock(&drain->lock);
+		error = xfs_ilock_iocb_for_write(iocb, iolock);
+		if (error) {
+			*iolock = 0;
+			goto out;
+		}
+	}
 restart:
 	/* Normalize append position and enforce limits before range admission. */
 	error = generic_write_checks(iocb, from);
 	xfs_dio_test_event(inode, XFS_DIO_TEST_WRITE_CHECKS,
 			iov_iter_count(from));
 	if (error <= 0)
-		return error;
+		goto out;
 
 	if (iocb->ki_flags & IOCB_DIRECT) {
 		error = xfs_dio_range_wait(iocb, iov_iter_count(from), iolock,
 				true);
 		if (error < 0)
-			return error;
+			goto out;
 		if (error) {
 			iov_iter_reexpand(from, count);
 			/* EOF and limits can change while IOLOCK is dropped. */
@@ -576,7 +651,7 @@ xfs_file_write_checks(
 	}
 
 	if (error)
-		return error;
+		goto out;
 
 	/*
 	 * For changing security info in file_remove_privs() we need i_rwsem
@@ -588,7 +663,7 @@ xfs_file_write_checks(
 		error = xfs_ilock_iocb(iocb, *iolock);
 		if (error) {
 			*iolock = 0;
-			return error;
+			goto out;
 		}
 		goto restart;
 	}
@@ -605,14 +680,18 @@ xfs_file_write_checks(
 	 */
 	if (iocb->ki_pos > i_size_read(inode)) {
 		error = xfs_file_write_zero_eof(iocb, from, iolock, count,
-				&drained_dio, ac);
+				&drained_dio, ac, drain);
 		if (error == 1)
 			goto restart;
 		if (error)
-			return error;
+			goto out;
 	}
 
-	return kiocb_modified(iocb);
+	error = kiocb_modified(iocb);
+out:
+	if (drain)
+		mutex_unlock(&drain->lock);
+	return error;
 }
 
 static ssize_t
tools/testing/selftests/filesystems/xfs_dio_drain.c +31 -10

96 lines not on this page · raw message

Signature · 1 line
2.56.0