fs: range-based admission for generation-drained DIO
Subsystems (8)
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
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
@@ -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
@@ -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
@@ -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
RFC PATCH 2/6 iomap: support generation accounting and caller-registered DIO
In reply to Vitali LovichGeneration 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
@@ -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
@@ -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
@@ -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
RFC PATCH 3/6 fs: add KUnit coverage for generation-based DIO draining
In reply to Vitali LovichExercise 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
RFC PATCH 4/6 xfs: add range-drain state and latch-based DIO admission
In reply to Vitali LovichPrepare 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
RFC PATCH 5/6 xfs: use range-based DIO draining for aligned hole punches
In reply to Vitali LovichHole 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
@@ -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
@@ -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
@@ -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
RFC PATCH 6/6 xfs: use range-based DIO draining for post-EOF zeroing
In reply to Vitali LovichZeroing 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
@@ -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