Add RWF_WRITETHROUGH support to iomap & xfs
Subsystems (5)
v4 cover Add RWF_WRITETHROUGH support to iomap & xfs
This is the v4 RFC of RWF_WRITETHROUGH patches. Mostly the design is the same as v3 with some changes based on Pankaj's review (thanks) and Sashiko's comments. Further, the error handling is improved and more defined. This series survives xfstests -g quick and fsx/fsstress stressing, I'll do more rigorous testing once the design is stablized.
I'll quote part of the original cover:
Hi all,
This patchset implements buffered write-through IO in linux.
This idea mainly picked up traction to enable RWF_ATOMIC buffered IO, however write-through path can have many use cases beyond atomic writes,
- such as enabling truly async AIO buffered I/O when issued with O_DSYNC
- better scalability for buffered I/O
==============================================
** Changes since rfc v3 ** [4]
==============================================
Patch 1: xfs: reexpand iter to original count when upgrading to excl ILOCK
* Small fix for xfs write path found while working on similar code in writethrough
Patch 5: iomap: Add initial support for buffered RWF_WRITETHROUGH
* iomap: Prevent duplicate bvec additions for same block across short copies in writethrough
* Error handling changes to ensure address space error is set when we have inconsistent folio state
Patch 6: xfs: Add RWF_WRITETHROUGH support to xfs
* Add a comment on lockless ip->i_disk_size usage
* Proper error handling in xfs_writethrough_end_io
* Restart writethrough checks and re-expand iter when upgrading to exclusive lock
Patch 7: iomap: Add aio support to RWF_WRITETHROUGH
* Propagate IOCB_NOWAIT correctly
Patch 9: Introduce RWF_NOSERIAL flag to indicate parallel reads/writes
* NOSERIAL can be passed by user and isn't default on with writethrough. It needs to be explicitly passed (as per discussion in v3)
Patch 11: iomap: Avoid folio dirtying in case of RWF_WRITETHROUGH
* We use iomap_start/end_writeback() instead of just setting the writeback bit like before, because we need the writeback xarray tags.
Adding some snippets from the original v3 cover letter (updated) below as it goes through some important bits of the design:
1. Introduce RWF_NOSERIAL to allow parallel writes
--------------------------------------------------
As per our discussions in LSFMM 2026, we noticed that writethrough (v2 design) suffered from a big regression (~65%) in workloads with multiple writers writing to a single file. This is because writethrough submits IO within the inode lock and since buffered IO has an exclusive lock in write, this hurt massively. In LSFMM, we discussed 3 approaches:
a) Defer IO submission outside inode lock
b) Avoid folio dirty - clear cycle
c) Shared lock for writes
We tried a) by only staging prepared folios in a list under inode lock and then submitting them outside. However, the initial implementation still showed around ~30% regression even though the code complexity was significantly higher. As the ROI was not worth it, we dropped this idea (if there is interest I can share a github link for these patches). So we finally decided to go with b) and c).
With b), we can avoid cycling folio through dirty and clear as we immediately submit the IO. This cuts back on xa_lock contention.
With c), the NOSERIAL flag allows us to do writes under a shared lock if possible. This violates guarantees that XFS has historically provided however its expected to be an advanced feature that should be used by applications who know what they are doing. If used correctly, it can give a very good performance boost to parallel workloads. This is something that was also discussed at LSFMM 2026 [3]. Based on previous discussions we've kept the RWF_NOSERIAL flag serparate to RWF_WRITETHROUGH but, for now, it is only supported with RWF_WRITETHROUGH.
With b & c, we are able to see a good performance improvement in almost all of the cases that were regressing. More details and performance numbers specific to the NOSERIAL flag can be found in the respective patches.
2. Use REQ_SYNC | REQ_IDLE like dio
-----------------------------------
Writethrough doesn't go via the writeback mechanism and it's IO characteristics are similar to dio. Hence we pass REQ_SYNC | REQ_IDLE in the bio, just like dio, which allows us to bypass writeback throttling.
3. Move inode i_size update from completion path to write path
--------------------------------------------------------------
As per Dave's suggestion we originally wanted to update isize in completion like dio however this resulted in a big regression in extending IO because we end up holding the exclusive lock throughout the IO. To avoid this, we can just take the buffered IO approach of updating i_size in write path so that we can safely drop the inode lock and allow completion to finish outside the lock. This brings back the append IO performance in par with buffered IO.
4. There's a deadock in v2 that is fixed in the last patch. If needed this can be squashed in but I've kept it separate for now for easier review.
===================================================================
Performance Comparison Tables (with fio snippet) (Writethrough IO = RWF_WRITETHROUGH + RWF_NOSERIAL)
===================================================================
Table 1: Extending writes using (libaio + O_DSYNC) - All writers on single file
+----------+-------------------+---------------------+
| numjobs | Buffered IO | Writethrough IO |
+----------+-------------------+---------------------+
| 1 | 133 MiB/s | 133 MiB/s (+0.0%) |
| 2 | 179 MiB/s | 243 MiB/s (+35.8%) |
| 4 | 253 MiB/s | 358 MiB/s (+41.5%) |
| 8 | 366 MiB/s | 376 MiB/s (+2.7%) |
| 16 | 474 MiB/s | 449 MiB/s (-5.3%) |
+----------+-------------------+---------------------+
(fio --ioengine=libaio --writethrough=0/1 --bs=4k --rw=write \
--iodepth=32 --sync=dsync--file_append=1)Table 2: Random pure overwrites (libaio + O_DSYNC) - All writers on single file
+----------+-------------------+---------------------+
| numjobs | Buffered IO | Writethrough IO |
+----------+-------------------+---------------------+
| 1 | 131 MiB/s | 391 MiB/s (+198.5%) |
| 2 | 377 MiB/s | 791 MiB/s (+109.8%) |
| 4 | 695 MiB/s | 1591 MiB/s (+128.9%)|
| 8 | 1217 MiB/s | 1846 MiB/s (+51.7%) |
| 16 | 1197 MiB/s | 1844 MiB/s (+54.1%) |
+----------+-------------------+---------------------+
(fio --ioengine=libaio --bs=4k --size=2G --sync=dsync --writethrough=0/1 \
--overwrite=1--rw=randwrite --iodepth=32)Table 3: Random pure overwrites (libaio + O_DSYNC) - Each write writes own file
+----------+-------------------+---------------------+
| numjobs | Buffered IO | Writethrough IO |
+----------+-------------------+---------------------+
| 1 | 187 MiB/s | 389 MiB/s (+108.0%) |
| 2 | 373 MiB/s | 781 MiB/s (+109.4%) |
| 4 | 706 MiB/s | 1568 MiB/s (+122.1%)|
| 8 | 1222 MiB/s | 1796 MiB/s (+47.0%) |
+----------+-------------------+---------------------+
(fio --ioengine=libaio --bs=4k --size=2G --filename_format=.test_file.\$jobnum \
--sync=dsync --writethrough=0/1 --overwrite=1 --rw=randwrite --iodepth=32)Table 4: Random 16KB overwrites with sync_file_range:16 - Single file (Roughly mimics postgresql IO pattern)
+----------+-------------------+---------------------+
| numjobs | Buffered IO | Writethrough IO |
+----------+-------------------+---------------------+
| 1 | 1323 MiB/s | 1275 MiB/s (-3.6%) |
| 2 | 1779 MiB/s | 2434 MiB/s (+36.8%) |
| 4 | 2092 MiB/s | 2519 MiB/s (+20.4%) |
| 8 | 2272 MiB/s | 2517 MiB/s (+10.8%) |
| 16 | 2328 MiB/s | 2525 MiB/s (+8.5%) |
+----------+-------------------+---------------------+
(fio --ioengine=libaio --bs=16k --size=5G --writethrough=0/1 --overwrite=1\
--sync_file_range=wait_before,write:16 --rw=randwrite --iodepth=32)* Environment details *
CPU : IBM Power 11 LPAR
Memory : 62Gi
Storage : Samsung PM173-series Enterprise NVMe SSD
Kernel : Linux 7.2-rc1
Filesystem : XFS (4k block size)
Thoughts and suggestions welcome!
Regards,
ojaswin
[1] https://lore.kernel.org/linux-xfs/[email protected]/
[2] https://github.com/OjaswinM/xfstests/tree/iomap-buf-writethrough2
[3] https://lwn.net/Articles/1072019
[4] https://lore.kernel.org/linux-xfs/[email protected]/
Ojaswin Mujoo (12):
xfs: reexpand iter to original count when upgrading to excl ILOCK
fs: Add counter to track inflight writes that need stable pages
mm: Refactor folio_clear_dirty_for_io()
iomap: Add helper to revert iomap iter
iomap: Add initial support for buffered RWF_WRITETHROUGH
xfs: Add RWF_WRITETHROUGH support to xfs
iomap: Add aio support to RWF_WRITETHROUGH
iomap: Add DSYNC support to RWF_WRITETHROUGH
fs: Introduce RWF_NOSERIAL flag to indicate parallel reads/writes
xfs: Implement RWF_NOSERIAL to parallelize RWF_WRITETHROUGH writes
iomap: Avoid folio dirtying in case of RWF_WRITETHROUGH
iomap: Handle deadlock due to repeating folios in RWF_WRITETHROUGH
fs/iomap/buffered-io.c | 658 +++++++++++++++++++++++++++++++++- fs/iomap/iter.c | 10 + fs/xfs/xfs_file.c | 155 +++++++- include/linux/fs.h | 22 +- include/linux/iomap.h | 51 +++ include/linux/pagemap.h | 15 +- include/uapi/linux/fs.h | 9 +- mm/page-writeback.c | 73 +++- tools/include/uapi/linux/fs.h | 8 +- 9 files changed, 964 insertions(+), 37 deletions(-)
Signature · 1 line
2.55.0
RFC PATCH v4 1/12 xfs: reexpand iter to original count when upgrading to excl ILOCK
In xfs_file_write_checks() when we upgrade to an exclusive lock, we correctly redo the write checks, but we should also reexpand the iter to the original count incase it was trimmed. This is because a parallel truncation/expansion, in the window where inode was unlocked, could have changed the isize so its good to recalculate limits based on the original count.
Also, s/count/orig_count/ while we are at it to better indicate its purpose
--- fs/xfs/xfs_file.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-)
fs/xfs/xfs_file.c +3 -2
@@ -450,7 +450,7 @@ xfs_file_write_checks( struct xfs_zone_alloc_ctx *ac) {struct inode *inode = iocb->ki_filp->f_mapping->host; -size_t count =iov_iter_count (from); +size_t orig_count =iov_iter_count (from);bool drained_dio = false;ssize_t error;
@@ -482,6 +482,7 @@ xfs_file_write_checks( *iolock =0 ;return error; } +iov_iter_reexpand (from, orig_count); goto restart; }
@@ -496,7 +497,7 @@ xfs_file_write_checks(* the slow path when we are at or beyond the current EOF. */ if (iocb->ki_pos >i_size_read (inode)) { - error =xfs_file_write_zero_eof (iocb, from, iolock, count, + error =xfs_file_write_zero_eof (iocb, from, iolock, orig_count, &drained_dio, ac);if (error ==1 ) goto restart;
Signature · 1 line
2.55.0
RFC PATCH v4 2/12 fs: Add counter to track inflight writes that need stable pages
In reply to Ojaswin MujooThe current flag-style stable write implementation uses idempotent set and clear functions. This is okay because the users for the most part just want to set or clear it once based on factors like underlying device support.
However, this scheme doesn't play well when we have parallel users wanting to temporarily set and unset stable writes. For example, the upcoming RWF_WRITETHROUGH patches need stable writes to be enabled for the duration of the IO. The current scheme can lead to bugs like:
RWF_WRITETHROUGH write 1 RWF_WRITETHROUGH write 2
enable stable write enable stable write
submit IO
disable stable write <---- WRONG
submit IO disable stable write
The 2nd write loses the stable write guarantee midway which is not correct. Fix this by introducing a new inflight_stable_write counter which can be used by parallel users safely.
Unfortunately, due to the way the current users are designed, we cannot directly migrate them to the counter approach hence for now we will have to keep both methods till all the users adapt to the counters.
--- include/linux/fs.h | 1 + include/linux/pagemap.h | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-)
include/linux/fs.h +1 -0
@@ -482,6 +482,7 @@ struct address_space {errseq_t wb_err;spinlock_t i_private_lock;struct rw_semaphore i_mmap_rwsem; +atomic_t inflight_stable_writes_count; } __attribute__((aligned(sizeof(long)))) __randomize_layout;/* * On most architectures that alignment is already the case; but
include/linux/pagemap.h +13 -1
@@ -306,7 +306,8 @@ static inline void mapping_clear_release_always(struct address_space *mapping)static inline bool mapping_stable_writes (const struct address_space *mapping) { -return test_bit (AS_STABLE_WRITES , &mapping->flags); +return test_bit (AS_STABLE_WRITES , &mapping->flags) || +atomic_read (&mapping->inflight_stable_writes_count) >0 ; }static inline void mapping_set_stable_writes (struct address_space *mapping)
@@ -319,6 +320,17 @@ static inline void mapping_clear_stable_writes(struct address_space *mapping)clear_bit (AS_STABLE_WRITES , &mapping->flags); } +static inline void mapping_inc_inflight_stable_writes (struct address_space *mapping) +{ +atomic_inc (&mapping->inflight_stable_writes_count); +} + +static inline void mapping_dec_inflight_stable_writes (struct address_space *mapping) +{ +WARN_ON_ONCE (atomic_dec_if_positive ( + &mapping->inflight_stable_writes_count) <0 ); +} +static inline void mapping_set_inaccessible (struct address_space *mapping) {/*
Signature · 1 line
2.55.0
RFC PATCH v4 3/12 mm: Refactor folio_clear_dirty_for_io()
In reply to Ojaswin MujooAdd a new __folio_clear_dirty_for_io() helper which takes an extra parameter to indicate folio_mkclean() is needed. This is in preparation of buffered writethrough support where we already do folio_mkclean() before calling into this function.
--- mm/page-writeback.c | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-)
mm/page-writeback.c +25 -14
@@ -2850,20 +2850,12 @@ void __folio_cancel_dirty(struct folio *folio)EXPORT_SYMBOL (__folio_cancel_dirty);/* -* Clear a folio's dirty flag, while caring for dirty memory accounting. -* Returns true if the folio was previously dirty. -* -* This is for preparing to put the folio under writeout. We leave -* the folio tagged as dirty in the xarray so that a concurrent -* write-for-sync can discover it via a PAGECACHE_TAG_DIRTY walk. -* The ->writepage implementation will run either folio_start_writeback() -* or folio_mark_dirty(), at which stage we bring the folio's dirty flag -* and xarray dirty tag back into sync. -* -* This incoherency between the folio's dirty flag and xarray tag is -* unfortunate, but it only exists while the folio is locked. +* Internal helper to take care of clearing dirty bit on a folio in preparation +* of an IO. For some cases we might not want to do mkclean, eg, if we've +* already taken care of it, hence pass the should_mkclean flag to indicate if +* its needed. */ -bool folio_clear_dirty_for_io (struct folio *folio) +static bool __folio_clear_dirty_for_io (struct folio *folio,bool should_mkclean) {struct address_space *mapping =folio_mapping (folio);bool ret = false;
@@ -2900,7 +2892,7 @@ bool folio_clear_dirty_for_io(struct folio *folio)* as a serialization point for all the different * threads doing their things. */ -if (folio_mkclean (folio)) +if (should_mkclean &&folio_mkclean (folio))folio_mark_dirty (folio);/* * We carefully synchronise fault handlers against
@@ -2925,6 +2917,25 @@ bool folio_clear_dirty_for_io(struct folio *folio) }return folio_test_clear_dirty (folio); } + +/* +* Clear a folio's dirty flag, while caring for dirty memory accounting. +* Returns true if the folio was previously dirty. +* +* This is for preparing to put the folio under writeout. We leave +* the folio tagged as dirty in the xarray so that a concurrent +* write-for-sync can discover it via a PAGECACHE_TAG_DIRTY walk. +* The ->writepage implementation will run either folio_start_writeback() +* or folio_mark_dirty(), at which stage we bring the folio's dirty flag +* and xarray dirty tag back into sync. +* +* This incoherency between the folio's dirty flag and xarray tag is +* unfortunate, but it only exists while the folio is locked. +*/ +bool folio_clear_dirty_for_io (struct folio *folio) +{ +return __folio_clear_dirty_for_io (folio, true); +}EXPORT_SYMBOL (folio_clear_dirty_for_io);static void wb_inode_writeback_start (struct bdi_writeback *wb)
Signature · 1 line
2.55.0
RFC PATCH v4 4/12 iomap: Add helper to revert iomap iter
In reply to Ojaswin MujooAdd helper to revert back the iomap iter by count bytes. This will be used by upcoming RWF_WRITETHROUGH feature to revert the state of the iomap in case an error is encountered.
Writethrough can process and park folios so that they can be batched together for IO later, this also advances the iomap iter. However if we encounter any error, we would want to cancel the IO on those processed folios. In this case, we want to revert the iter back so the upper layers can know how much we were actually able to write before failure.
--- fs/iomap/iter.c | 10 ++++++++++ include/linux/iomap.h | 1 + 2 files changed, 11 insertions(+)
fs/iomap/iter.c +10 -0
@@ -32,6 +32,16 @@ int iomap_iter_advance(struct iomap_iter *iter, u64 count)return 0 ; } +int iomap_iter_revert (struct iomap_iter *iter,u64 count) +{ +if (WARN_ON_ONCE (iter->pos < count || + iter->pos - count < iter->iter_start_pos)) +return -EIO ; + iter->pos -= count; + iter->len += count; +return 0 ; +} +static inline void iomap_iter_done (struct iomap_iter *iter) {WARN_ON_ONCE (iter->iomap.offset > iter->pos);
include/linux/iomap.h +1 -0
@@ -275,6 +275,7 @@ struct iomap_iter {int iomap_iter (struct iomap_iter *iter,const struct iomap_ops *ops);int iomap_iter_advance (struct iomap_iter *iter,u64 count); +int iomap_iter_revert (struct iomap_iter *iter,u64 count);/** * iomap_length_trim - trimmed length of the current iomap iteration
Signature · 1 line
2.55.0
RFC PATCH v4 5/12 iomap: Add initial support for buffered RWF_WRITETHROUGH
In reply to Ojaswin MujooThis adds initial support for performing buffered non-aio RWF_WRITETHROUGH write. The rough flow for a writethrough write is as follows:
1. Acquire inode lock
2. initialize writethrough context (wt_ctx) and mark
mapping as stable.
3. Start the iomap_iter() loop. For each iomap:
3.1. Acquire folio and folio_lock.
3.2. perform memcpy from user buffer to the folio and mark it
dirty
3.3. Wait for any current writeback to complete and then call
folio_mkclean() to prevent mmap writes from changing it.
3.4. Start writeback on the folio
3.5. Add the folio range under write to wt_ctx->bvec and folio_unlock() 3.6. If bvec is full, submit the current bvecs for IO.
3.7. Repeat 3.2 to 3.6 till the whole iomap is processed. Submit
the final set of bvecs for IO.
4. Repeat step 3 till we have no more data to write.
5. Finally, sleep in the syscall thread till all the IOs are completed (refcount == 0). Once that happens, the end io handler will wake us up.
6. Upon waking up, call fs ->end_io() callback (which updates inode size), record any errors and return.
7. inode_unlock()
This design gives buffered writethrough the same semantics as dio and any error in the IO is directly returned to the caller. The design has deliberately open coded the IO submission and completion flow (inspired by dio) rather than reusing the dio functions as accommodating buffered writethrough logic in dio code was polluting it with too many if else conditionals and special cases. For notes on error handling, check the comment above iomap_file_writethrough_write()
--- fs/iomap/buffered-io.c | 495 ++++++++++++++++++++++++++++++++++ include/linux/fs.h | 11 + include/linux/iomap.h | 42 +++ include/linux/pagemap.h | 1 + include/uapi/linux/fs.h | 5 +- mm/page-writeback.c | 10 + tools/include/uapi/linux/fs.h | 5 +- 7 files changed, 567 insertions(+), 2 deletions(-)
fs/iomap/buffered-io.c +495 -0
@@ -10,6 +10,7 @@#include <linux/migrate.h> #include <linux/fserror.h> #include <linux/fsverity.h> +#include <linux/rmap.h> #include "internal.h" #include "trace.h"
@@ -1185,6 +1186,384 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied,return __iomap_write_end (iter->inode, pos, len, copied, folio); } +static ssize_t iomap_writethrough_complete (struct iomap_writethrough_ctx *wt_ctx) +{ +struct kiocb *iocb = wt_ctx->iocb; +struct inode *inode = wt_ctx->inode; +ssize_t ret = wt_ctx->error; + +if (wt_ctx->end_io) { +int err = wt_ctx->end_io (wt_ctx, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags); +if (err) + ret = err; + } + +mapping_dec_inflight_stable_writes (inode->i_mapping); + +if (!ret) { + ret = wt_ctx->written; + iocb->ki_pos += ret; + }else +mapping_set_error (inode->i_mapping, ret); + +kfree (wt_ctx); +return ret; +} + +static void iomap_writethrough_done (struct iomap_writethrough_ctx *wt_ctx) +{ +struct task_struct *waiter = wt_ctx->waiter; + +WRITE_ONCE (wt_ctx->waiter,NULL ); +blk_wake_io_task (waiter); +} + +static void iomap_writethrough_bio_end_io (struct bio *bio) +{ +struct iomap_writethrough_ctx *wt_ctx = bio->bi_private; +struct folio_iter fi; + +if (bio->bi_status) +cmpxchg (&wt_ctx->error,0 , +blk_status_to_errno (bio->bi_status)); +bio_for_each_folio_all (fi, bio) +folio_end_writeback (fi.folio); + +bio_put (bio); +if (atomic_dec_and_test (&wt_ctx->ref)) +iomap_writethrough_done (wt_ctx); +} + +static int +iomap_writethrough_submit_bio (struct iomap_writethrough_ctx *wt_ctx, +struct iomap *iomap, +const struct iomap_writethrough_ops *wt_ops,int error) +{ +struct bio *bio; +unsigned int i; +u64 len =0 ; +blk_opf_t opf =REQ_OP_WRITE |REQ_SYNC |REQ_IDLE ; + +if (!wt_ctx->nr_bvecs) + goto exit; + +for (i =0 ; i < wt_ctx->nr_bvecs; i++) + len += wt_ctx->bvec[i].bv_len; + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs, opf,GFP_NOFS ); + bio->bi_iter.bi_sector =iomap_sector (iomap, wt_ctx->bio_pos); + bio->bi_end_io = iomap_writethrough_bio_end_io; + bio->bi_private = wt_ctx; + +for (i =0 ; i < wt_ctx->nr_bvecs; i++) +__bio_add_page (bio, wt_ctx->bvec[i].bv_page, + wt_ctx->bvec[i].bv_len, + wt_ctx->bvec[i].bv_offset); + +if (!error && wt_ops->writethrough_submit) + error = wt_ops->writethrough_submit (wt_ctx->inode, iomap, + wt_ctx->bio_pos, len); + + +atomic_inc (&wt_ctx->ref); + +/* +* In case of error we still need the I/O completion to run so we can +* release references and end writeback on the folios. +*/ +if (error) { + bio->bi_status =errno_to_blk_status (error); +bio_endio (bio); +return error; + } + +submit_bio (bio); + wt_ctx->nr_bvecs =0 ; + +exit: +return 0 ; +} + +/* +* Submit any pending bvecs as a bio and account the written bytes. +* On failure, iomap_writethrough_submit_bio() has already called the +* endio completion handler to record the error. +*/ +static int +iomap_writethrough_try_submit (struct iomap_writethrough_ctx *wt_ctx, +struct iomap *iomap, +const struct iomap_writethrough_ops *wt_ops, +ssize_t *pending) +{ +int ret =iomap_writethrough_submit_bio (wt_ctx, iomap, wt_ops,0 ); + +if (ret <0 ) +return ret; + wt_ctx->written += *pending; + *pending =0 ; +return 0 ; +} + +/** +* iomap_writethrough_begin - prepare the various structures for writethrough +* @folio: folio to prepare for writethrough +* @off: offset of write within folio +* @len: len of write within folio +* +* This function does the major preparation work needed before starting the +* writethrough. The main task is to prepare folio for writeththrough by blocking +* mmap writes and setting writeback on it. Further, we must clear the write range +* to non-dirty. If this results in the complete folio becoming non-dirty, then we +* need to clear the master dirty bit. +*/ +static void iomap_folio_prepare_writethrough (struct folio *folio,size_t off, +size_t len) +{ +bool fully_written; +u64 zero =0 ; + +if (folio_test_writeback (folio)) +folio_wait_writeback (folio); + +if (folio_mkclean (folio)) +folio_mark_dirty (folio); + +/* +* We might either write through the complete folio or a partial folio +* writethrough might result in all blocks becoming non-dirty, so we need to +* check and mark the folio clean if that is the case. +*/ + fully_written = (off ==0 && len ==folio_size (folio)); +iomap_clear_range_dirty (folio, off, len); +if (fully_written || + !iomap_find_dirty_range (folio, &zero,folio_size (folio))) +folio_clear_dirty_for_writethrough (folio); + +folio_start_writeback (folio); +} + +/** +* iomap_writethrough_iter - perform RWF_WRITETHROUGH buffered write +* @wt_ctx: writethrough context +* @iter: iomap iter holding mapping information +* @i: iov_iter for write +* @wt_ops: the fs callbacks needed for writethrough +* +* This function copies the user buffer to folio similar to usual buffered +* IO path, with the difference that we immediately issue the IO. For this we +* utilize IO submission and completion mechanism that is inspired by dio. +* +* Folio handling note: We might be writing through a partial folio so we need +* to be careful to not clear the folio dirty bit unless there are no dirty blocks +* in the folio after the writethrough. +*/ +static int iomap_writethrough_iter (struct iomap_writethrough_ctx *wt_ctx, +struct iomap_iter *iter,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops) + +{ +ssize_t total_written =0 , pending =0 ; +loff_t submit_start_pos; +int status =0 ; +struct address_space *mapping = iter->inode->i_mapping; +size_t chunk =mapping_max_folio_size (mapping); +unsigned int bdp_flags = (iter->flags &IOMAP_NOWAIT ) ?BDP_ASYNC :0 ; +unsigned int bs =i_blocksize (iter->inode); + +/* copied over based on how DIO handles these flags */ +if (iter->iomap.type ==IOMAP_UNWRITTEN ) + wt_ctx->flags |=IOMAP_DIO_UNWRITTEN ; +if (iter->iomap.flags &IOMAP_F_SHARED ) + wt_ctx->flags |=IOMAP_DIO_COW ; + +if (!(iter->flags &IOMAP_WRITETHROUGH )) +return -EINVAL ; + +/* +* IOMAP_INLINE mappings have NULL bdev and would cause +* iomap_sector() to dereference invalid memory. Reject them. +*/ +if (iter->iomap.type ==IOMAP_INLINE ) +return -EINVAL ; + +do { +struct folio *folio; +size_t offset;/* Offset into folio */ +loff_t old_size; +u64 bytes;/* Bytes to write to folio */ +size_t copied;/* Bytes copied from user */ +u64 written;/* Bytes have been written */ +loff_t pos; +size_t off_aligned, len_aligned, pos_aligned; +size_t prev_pos, prev_len; + + bytes =iov_iter_count (i); +retry: + offset = iter->pos & (chunk -1 ); + bytes =min (chunk - offset, bytes); + status =balance_dirty_pages_ratelimited_flags (mapping, + bdp_flags); +if (unlikely (status)) +break ; + +/* +* If completions already occurred and reported errors, give up +* now +*/ + status =data_race (wt_ctx->error); +if (unlikely (status)) +break ; + +if (bytes >iomap_length (iter)) + bytes =iomap_length (iter); + +/* +* Bring in the user page that we'll copy from _first_. +* Otherwise there's a nasty deadlock on copying from the +* same page as we're writing to, without it being marked +* up-to-date. +* +* For async buffered writes the assumption is that the user +* page has already been faulted in. This can be optimized by +* faulting the user page. +*/ +if (unlikely (fault_in_iov_iter_readable (i, bytes) == bytes)) { + status = -EFAULT ; +break ; + } + + status =iomap_write_begin (iter, wt_ops->write_ops, &folio, + &offset, &bytes); +if (unlikely (status)) { +iomap_write_failed (iter->inode, iter->pos, bytes); +break ; + } +if (iter->iomap.flags &IOMAP_F_STALE ) +break ; + + pos = iter->pos; + +if (mapping_writably_mapped (mapping)) +flush_dcache_folio (folio); + + copied =copy_folio_from_iter_atomic (folio, offset, bytes, i); + written =iomap_write_end (iter, bytes, copied, folio) ? + copied :0 ; + + old_size = iter->inode->i_size; +if (pos + written > old_size) +i_size_write (iter->inode, pos + written); + +if (!written) + goto put_folio; + + off_aligned =round_down (offset, bs); + pos_aligned =round_down (pos, bs); + len_aligned =round_up (offset + written, bs) - off_aligned; + +/* +* Because we round down to the block size, a short copy in prev +* iteration can result in processing the exact same fs block +* again. Hence, we need to be careful to not add the repeating +* blocks in the bvec again. +*/ +if (wt_ctx->nr_bvecs && prev_pos + prev_len > pos_aligned) { +size_t delta = prev_pos + prev_len - pos_aligned; + +/* Everything already added to bvec, nothing to do */ +if (delta >= len_aligned) + goto put_folio; + + pos_aligned += delta; + off_aligned += delta; + len_aligned -= delta; + } + + prev_pos = off_aligned; + prev_len = len_aligned; + +iomap_folio_prepare_writethrough (folio, off_aligned, + len_aligned); + +if (!wt_ctx->nr_bvecs) { + wt_ctx->bio_pos =round_down (pos, bs); + submit_start_pos = pos; + } + +bvec_set_folio (&wt_ctx->bvec[wt_ctx->nr_bvecs], folio, + len_aligned, off_aligned); + wt_ctx->nr_bvecs++; + +put_folio: +__iomap_put_folio (iter, wt_ops->write_ops, written, folio); + +if (old_size < pos) +pagecache_isize_extended (iter->inode, old_size, pos); + +cond_resched (); +if (unlikely (written ==0 )) { +iomap_write_failed (iter->inode, pos, bytes); +iov_iter_revert (i, copied); + +if (chunk >PAGE_SIZE ) + chunk /=2 ; +if (copied) { + bytes = copied; + goto retry; + } + }else { + total_written += written; + pending += written; +iomap_iter_advance (iter, written); + } + +/* +* If we fail to submit the bio, we immediately call the +* IO completion handler that records the error. We +* shall not retry anymore cause this could lead to +* infinite loops in case of non-transient errors. +*/ +if (wt_ctx->nr_bvecs == wt_ctx->max_bvecs) { + status =iomap_writethrough_try_submit (wt_ctx, + &iter->iomap, wt_ops, &pending); +if (status) + goto submit_failed; + } + + }while (iov_iter_count (i) &&iomap_length (iter)); + +if (wt_ctx->nr_bvecs) { +int ret; + + ret =iomap_writethrough_try_submit (wt_ctx, + &iter->iomap, wt_ops, &pending); +if (ret) { + status = ret; + goto submit_failed; + } + } + +return status; + +submit_failed: +WARN_ON (!status); +cmpxchg (&wt_ctx->error,0 , status); + +/* +* In case of an error, we only consider the bytes we were actually able +* to submit IO for as valid data and revert the iters accordingly +*/ +if (pending) { +iomap_write_failed (iter->inode, submit_start_pos, pending); +iomap_iter_revert (iter, pending); +iov_iter_revert (i, pending); + } + +return status; +} +static int iomap_write_iter (struct iomap_iter *iter,struct iov_iter *i,const struct iomap_write_ops *write_ops) {
@@ -1345,6 +1724,122 @@ int iomap_fsverity_write(struct file *file, loff_t pos, size_t length, }EXPORT_SYMBOL_GPL (iomap_fsverity_write); +/** +* iomap_file_writethrough_write - perform a writethrough write +* @iocb: kiocb for the write +* @i: iov_iter for the write +* @wt_ops: writethrough related callbacks +* @private: optional private data +* +* Perform a writethrough write by first copying data to the page cache and then +* immediately sending it for IO. This is similar to dio but which maintaining +* the page cache coherency. +* +* There are 2 types of errors RWF_WRITETHROUGH can face: +* +* 1. Errors that don't leave page cache inconsistent. These are errors that +* happen before we have copied any data to folio or where we are able to +* successfully submit everything in folio before the error was encountered. +* Here the disk contents and page cache are in sync. In this case we return +* num. of bytes successfully written. If nothing is written, return the error. +* +* 2. Errors that leave page cache inconsistent: Any errors that either prevent +* us to submit data copied to folio or endio errors fall in this category. +* Basically, these errors happen after the folio is made uptodate and unlocked. +* Being unable to submit it means applications can't be sure if the content of +* the folio are consistent to the disk anymore. In essence these are similar to +* writeback failures. Hence, in this case we always mark the error in the +* address space and return it to user. +* +* Return: number of bytes written on success, -EIOCBQUEUED if the operation +* was submitted asynchronously, or a negative error code on failure. +*/ +ssize_t iomap_file_writethrough_write (struct kiocb *iocb,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops, +void *private) +{ +struct inode *inode = iocb->ki_filp->f_mapping->host; +struct iomap_iter iter = { + .inode = inode, + .pos = iocb->ki_pos, + .len =iov_iter_count (i), + .flags =IOMAP_WRITE |IOMAP_WRITETHROUGH , + .private = private, + }; +struct iomap_writethrough_ctx *wt_ctx; +unsigned int max_bvecs; +ssize_t ret; +struct blk_plug plug; +size_t min_folio_bytes =PAGE_SIZE + <<mapping_min_folio_order (inode->i_mapping); + +/* +* For now we don't support any other flag with WRITETHROUGH +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) +return -EINVAL ; +if (iocb->ki_flags & (IOCB_DONTCACHE )) +return -EINVAL ; +if (iocb_is_dsync (iocb)) +/* D_SYNC support not implemented yet */ +return -EOPNOTSUPP ; +if (!is_sync_kiocb (iocb)) +/* aio support not implemented yet */ +return -EOPNOTSUPP ; + +/* +* +1 to max bvecs to account for unaligned write spanning multiple +* folios. Guard against overflow since iov_iter_count() returns size_t. +*/ + max_bvecs = (unsigned int )min_t ( + size_t,DIV_ROUND_UP (iov_iter_count (i), min_folio_bytes) +1 , +BIO_MAX_VECS ); + + wt_ctx =kzalloc (struct_size (wt_ctx, bvec, max_bvecs),GFP_NOFS ); +if (!wt_ctx) +return -ENOMEM ; + + wt_ctx->iocb = iocb; + wt_ctx->inode = inode; + wt_ctx->end_io = wt_ops->end_io; + wt_ctx->old_i_size =i_size_read (inode); + wt_ctx->max_bvecs = max_bvecs; +atomic_set (&wt_ctx->ref,1 ); + wt_ctx->waiter = current; + +mapping_inc_inflight_stable_writes (inode->i_mapping); + +blk_start_plug (&plug); + +while ((ret =iomap_iter (&iter, wt_ops->ops)) >0 ) { +WARN_ON (iter.iomap.type !=IOMAP_UNWRITTEN && + iter.iomap.type !=IOMAP_MAPPED ); + iter.status =iomap_writethrough_iter (wt_ctx, &iter, i, wt_ops); + } + +blk_finish_plug (&plug); + +if (!atomic_dec_and_test (&wt_ctx->ref)) { +for (;;) { +set_current_state (TASK_UNINTERRUPTIBLE ); +if (!READ_ONCE (wt_ctx->waiter)) +break ; +blk_io_schedule (); + } +__set_current_state (TASK_RUNNING ); + } + +/* +* we encountered an error before submitting any data and there are no +* mapping error. We can simply return the error. +*/ +if (ret && !wt_ctx->written && !wt_ctx->error) +return ret; + +return iomap_writethrough_complete (wt_ctx); +} +EXPORT_SYMBOL_GPL (iomap_file_writethrough_write); +static void iomap_write_delalloc_ifs_punch (struct inode *inode,struct folio *folio,loff_t start_byte,loff_t end_byte,struct iomap *iomap,iomap_punch_t punch)
include/linux/fs.h +11 -0
@@ -344,6 +344,7 @@ struct readahead_control;#define IOCB_ATOMIC (__force int) RWF_ATOMIC#define IOCB_DONTCACHE (__force int) RWF_DONTCACHE#define IOCB_NOSIGNAL (__force int) RWF_NOSIGNAL +#define IOCB_WRITETHROUGH (__force int) RWF_WRITETHROUGH/* non-RWF related bits - start at 16 */ #define IOCB_EVENTFD (1 << 16)
@@ -1979,6 +1980,8 @@ struct file_operations {#define FOP_ASYNC_LOCK ((__force fop_flags_t)(1 << 6))/* File system supports uncached read/write buffered IO */ #define FOP_DONTCACHE ((__force fop_flags_t)(1 << 7)) +/* File system supports write through buffered IO */ +#define FOP_WRITETHROUGH ((__force fop_flags_t)(1 << 8))/* Wrap a directory iterator that needs exclusive inode access */ int wrap_directory_iterator(struct file *, struct dir_context *,
@@ -3478,6 +3481,14 @@ static inline int kiocb_set_rw_flags(struct kiocb *ki, rwf_t flags,if (IS_DAX (ki->ki_filp->f_mapping->host))return -EOPNOTSUPP ; } +if (flags &RWF_WRITETHROUGH ) { +/* file system must support it */ +if (!(ki->ki_filp->f_op->fop_flags &FOP_WRITETHROUGH )) +return -EOPNOTSUPP ; +/* DAX mappings not supported */ +if (IS_DAX (ki->ki_filp->f_mapping->host)) +return -EOPNOTSUPP ; + } kiocb_flags |= (__force int ) (flags &RWF_SUPPORTED );if (flags &RWF_SYNC ) kiocb_flags |=IOCB_DSYNC ;
include/linux/iomap.h +42 -0
60 lines not on this page · raw message
include/linux/pagemap.h +1 -0
@@ -1333,6 +1333,7 @@ static inline void folio_cancel_dirty(struct folio *folio)__folio_cancel_dirty (folio); }bool folio_clear_dirty_for_io (struct folio *folio); +bool folio_clear_dirty_for_writethrough (struct folio *folio);bool clear_page_dirty_for_io (struct page *page);void folio_invalidate (struct folio *folio,size_t offset,size_t length);bool noop_dirty_folio (struct address_space *mapping,struct folio *folio);
include/uapi/linux/fs.h +4 -1
@@ -451,10 +451,13 @@ typedef int __bitwise __kernel_rwf_t;/* prevent pipe and socket writes from raising SIGPIPE */ #define RWF_NOSIGNAL ((__force __kernel_rwf_t)0x00000100) +/* buffered IO that is asynchronously written through to disk after write */ +#define RWF_WRITETHROUGH ((__force __kernel_rwf_t)0x00000200) +/* mask of flags supported by the kernel */ #define RWF_SUPPORTED (RWF_HIPRI | RWF_DSYNC | RWF_SYNC | RWF_NOWAIT |\ RWF_APPEND | RWF_NOAPPEND | RWF_ATOMIC |\ - RWF_DONTCACHE | RWF_NOSIGNAL) + RWF_DONTCACHE | RWF_NOSIGNAL | RWF_WRITETHROUGH)#define PROCFS_IOCTL_MAGIC 'f'
mm/page-writeback.c +10 -0
@@ -2938,6 +2938,16 @@ bool folio_clear_dirty_for_io(struct folio *folio) }EXPORT_SYMBOL (folio_clear_dirty_for_io); +/* +* Clear folio dirty in preparation of writethrough. Note that for writethrough +* we have already done folkio_mkclean so we avoid it here +*/ +bool folio_clear_dirty_for_writethrough (struct folio *folio) +{ +return __folio_clear_dirty_for_io (folio, false); +} +EXPORT_SYMBOL (folio_clear_dirty_for_writethrough); +static void wb_inode_writeback_start (struct bdi_writeback *wb) {atomic_inc (&wb->writeback_inodes);
tools/include/uapi/linux/fs.h +4 -1
@@ -344,10 +344,13 @@ typedef int __bitwise __kernel_rwf_t;/* buffered IO that drops the cache after reading or writing data */ #define RWF_DONTCACHE ((__force __kernel_rwf_t)0x00000080) +/* buffered IO that is asynchronously written through to disk after write */ +#define RWF_WRITETHROUGH ((__force __kernel_rwf_t)0x00000200) +/* mask of flags supported by the kernel */ #define RWF_SUPPORTED (RWF_HIPRI | RWF_DSYNC | RWF_SYNC | RWF_NOWAIT |\ RWF_APPEND | RWF_NOAPPEND | RWF_ATOMIC |\ - RWF_DONTCACHE) + RWF_DONTCACHE | RWF_WRITETHROUGH)#define PROCFS_IOCTL_MAGIC 'f'
Signature · 1 line
2.55.0
Re: [RFC PATCH v4 05/12] iomap: Add initial support for buffered RWF_WRITETHROUGH
One general comment, all your commit headers are starting with uppercase.
iomap: Add initial support for buffered RWF_WRITETHROUGH
s/Add/add
Overall this looks good to me. I will run some tests via fio. Do you have a branch with WRITETHROUGH by any chance?
Quoted 16 lines · Ojaswin Mujoo
fs/iomap/buffered-io.c +16
-0+static int iomap_writethrough_iter (struct iomap_writethrough_ctx *wt_ctx, +struct iomap_iter *iter,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops) + +{ +ssize_t total_written =0 , pending =0 ; +loff_t submit_start_pos; +int status =0 ; +struct address_space *mapping = iter->inode->i_mapping; +size_t chunk =mapping_max_folio_size (mapping); +unsigned int bdp_flags = (iter->flags &IOMAP_NOWAIT ) ?BDP_ASYNC :0 ; +unsigned int bs =i_blocksize (iter->inode); + +/* copied over based on how DIO handles these flags */ +if (iter->iomap.type ==IOMAP_UNWRITTEN ) + wt_ctx->flags |=IOMAP_DIO_UNWRITTEN ;
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1387
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+if (iter->iomap.flags &IOMAP_F_SHARED ) + wt_ctx->flags |=IOMAP_DIO_COW ; + +if (!(iter->flags &IOMAP_WRITETHROUGH )) +return -EINVAL ; + +/* +* IOMAP_INLINE mappings have NULL bdev and would cause +* iomap_sector() to dereference invalid memory. Reject them. +*/
Nit:
I noticed that gfs2 sets the bdev even for IOMAP_INLINE. A better filesystem agnostic comment might be something like this?
/*
* Inline data lives in the inode's metadata buffer, so it cannot be
* written via a bio built from the folio.
*/
Quoted 9 lines
fs/iomap/buffered-io.c +9
-0+if (iter->iomap.type == IOMAP_INLINE) +return -EINVAL; + +do { +* blocks in the bvec again. +*/ +if (wt_ctx->nr_bvecs && prev_pos + prev_len > pos_aligned) { +size_t delta = prev_pos + prev_len - pos_aligned; +
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1484
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+/* Everything already added to bvec, nothing to do */ +if (delta >= len_aligned) + goto put_folio; + + pos_aligned += delta; + off_aligned += delta; + len_aligned -= delta; + } + + prev_pos = off_aligned;
I think this is a mistake?prev_pos = pos_aligned; ?
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1491
Quoted 7 lines
fs/iomap/buffered-io.c +7
-0+ prev_len = len_aligned; + +iomap_folio_prepare_writethrough (folio, off_aligned, + len_aligned); + +if (!wt_ctx->nr_bvecs) { + wt_ctx->bio_pos =round_down (pos, bs);
We could reuse pos_aligned variable here instead of recalculating?
Quoted 9 lines
fs/iomap/buffered-io.c +9
-0+ submit_start_pos = pos; +ssize_t iomap_file_writethrough_write (struct kiocb *iocb,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops, +void *private) +{ +struct inode *inode = iocb->ki_filp->f_mapping->host; +struct iomap_iter iter = { + .inode = inode, + .pos = iocb->ki_pos,
fs/iomap/buffered-io.c int iomap_fsverity_write(struct file *file, loff_t pos, size_t length, · line 1774
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ .len = iov_iter_count(i), + .flags = IOMAP_WRITE | IOMAP_WRITETHROUGH, + .private = private, + }; +struct iomap_writethrough_ctx *wt_ctx; +unsigned int max_bvecs; +ssize_t ret; +struct blk_plug plug; +size_t min_folio_bytes =PAGE_SIZE + <<mapping_min_folio_order (inode->i_mapping);
min_folio_nr_bytes could be used here.
Quoted 15 lines
fs/iomap/buffered-io.c +15
-0+ +/* +* For now we don't support any other flag with WRITETHROUGH +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) +return -EINVAL ; +if (iocb->ki_flags & (IOCB_DONTCACHE )) +return -EINVAL ; +if (iocb_is_dsync (iocb)) +/* D_SYNC support not implemented yet */ +return -EOPNOTSUPP ; +if (!is_sync_kiocb (iocb)) +/* aio support not implemented yet */ +return -EOPNOTSUPP ; +
Signature · 1 line
Pankaj
Re: [RFC PATCH v4 05/12] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 6 lines · Pankaj Raghav (Samsung)
One general comment, all your commit headers are starting with uppercase.
iomap: Add initial support for buffered
RWF_WRITETHROUGHs/Add/add
Hi Pankaj, thanks for the review. Sure I'll take care of this.
Quoted 3 lines
Overall this looks good to me. I will run some tests via fio. Do you have a branch with WRITETHROUGH by any chance?
yes, I've this branch:
https://github.com/OjaswinM/xfstests/tree/iomap-buf-writethrough2
The patches are a bit rough but it has writethrough support for fsx and fsstress so we can stress the feature.
Quoted 33 lines · Ojaswin Mujoo
Quoted 16 lines
fs/iomap/buffered-io.c +16
-0+static int iomap_writethrough_iter (struct iomap_writethrough_ctx *wt_ctx, +struct iomap_iter *iter,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops) + +{ +ssize_t total_written =0 , pending =0 ; +loff_t submit_start_pos; +int status =0 ; +struct address_space *mapping = iter->inode->i_mapping; +size_t chunk =mapping_max_folio_size (mapping); +unsigned int bdp_flags = (iter->flags &IOMAP_NOWAIT ) ?BDP_ASYNC :0 ; +unsigned int bs =i_blocksize (iter->inode); + +/* copied over based on how DIO handles these flags */ +if (iter->iomap.type ==IOMAP_UNWRITTEN ) + wt_ctx->flags |=IOMAP_DIO_UNWRITTEN ;fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1387
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+if (iter->iomap.flags &IOMAP_F_SHARED ) + wt_ctx->flags |=IOMAP_DIO_COW ; + +if (!(iter->flags &IOMAP_WRITETHROUGH )) +return -EINVAL ; + +/* +* IOMAP_INLINE mappings have NULL bdev and would cause +* iomap_sector() to dereference invalid memory. Reject them. +*/ Nit:
I noticed that gfs2 sets the bdev even forIOMAP_INLINE. A better filesystem agnostic comment might be something like this?
Quoted 4 lines
/*
* Inline data lives in the inode's metadata buffer, so it cannot be
* written via a bio built from the folio.
*/
Yes that sounds better, thanks.
Quoted 15 lines
Quoted 15 lines
fs/iomap/buffered-io.c +14
-0+if (iter->iomap.type == IOMAP_INLINE) +return -EINVAL; + +do { +* blocks in the bvec again. +*/ +if (wt_ctx->nr_bvecs && prev_pos + prev_len > pos_aligned) { +size_t delta = prev_pos + prev_len - pos_aligned; + +/* Everything already added to bvec, nothing to do */ +if (delta >= len_aligned) + goto put_folio; + + pos_aligned += delta;
Quoted 8 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1484
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+ off_aligned += delta; + len_aligned -= delta; + } + + prev_pos = off_aligned;I think this is a mistake?
prev_pos=pos_aligned; ?
oh right, I guess I didn't hit this in my testing. Thanks, I'll fix it.
Quoted 10 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1491
Quoted 8 lines
fs/iomap/buffered-io.c +7
-0+ prev_len = len_aligned; + +iomap_folio_prepare_writethrough (folio, off_aligned, + len_aligned); + +if (!wt_ctx->nr_bvecs) { + wt_ctx->bio_pos =round_down (pos, bs);We could reuse
pos_alignedvariable here instead of recalculating?
Yes, I will fix it.
Quoted 14 lines
Quoted 14 lines
fs/iomap/buffered-io.c +13
-0+ submit_start_pos = pos; +ssize_t iomap_file_writethrough_write (struct kiocb *iocb,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops, +void *private) +{ +struct inode *inode = iocb->ki_filp->f_mapping->host; +struct iomap_iter iter = { + .inode = inode, + .pos = iocb->ki_pos, + .len =iov_iter_count (i), + .flags =IOMAP_WRITE |IOMAP_WRITETHROUGH , + .private = private, + };
Quoted 8 lines
fs/iomap/buffered-io.c int iomap_fsverity_write(struct file *file, loff_t pos, size_t length, · line 1774
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+struct iomap_writethrough_ctx *wt_ctx; +unsigned int max_bvecs; +ssize_t ret; +struct blk_plug plug; +size_t min_folio_bytes =PAGE_SIZE + <<mapping_min_folio_order (inode->i_mapping);
min_folio_nr_bytescould be used here.
Noted.
Quoted 18 lines
Quoted 15 lines
fs/iomap/buffered-io.c +15
-0+ +/* +* For now we don't support any other flag with WRITETHROUGH +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) +return -EINVAL ; +if (iocb->ki_flags & (IOCB_DONTCACHE )) +return -EINVAL ; +if (iocb_is_dsync (iocb)) +/* D_SYNC support not implemented yet */ +return -EOPNOTSUPP ; +if (!is_sync_kiocb (iocb)) +/* aio support not implemented yet */ +return -EOPNOTSUPP ; +Signature · 1 line
Pankaj
Re: [RFC PATCH v4 05/12] iomap: Add initial support for buffered RWF_WRITETHROUGH
In reply to Ojaswin MujooHi Ojaswin,
I was comparing normal buffered IO scenario and writethrough.
In a normal buffered IO scenario, for unwritten extents or CoW extents, we first convert them before ending the writeback on the folio. xfs_end_io calls xfs_end_ioend_write().
static void
xfs_end_ioend_write(
struct iomap_ioend *ioend)
{
<snip>
/*
* Success: commit the COW or unwritten blocks if needed.
*/
if (is_zoned)
error = xfs_zoned_end_io(ip, offset, size, ioend->io_sector,
ioend->io_private, NULLFSBLOCK);
else if (ioend->io_flags & IOMAP_IOEND_SHARED)
error = xfs_reflink_end_cow(ip, offset, size);
else if (ioend->io_flags & IOMAP_IOEND_UNWRITTEN)
error = xfs_iomap_write_unwritten(ip, offset, size, false);
if (!error &&
!(ioend->io_flags & IOMAP_IOEND_DIRECT) &&
xfs_ioend_is_append(ioend))
error = xfs_setfilesize(ip, offset, size);
done:
if (is_zoned)
xfs_ioend_put_open_zones(ioend);
iomap_finish_ioends(ioend, error); // #1
memalloc_nofs_restore(nofs_flag);
}#1iomap_finish_ioends()
iomap_finish_ioend_buffered_write()
iomap_finish_folio_write()
folio_end_writeback()
Quoted 18 lines · Ojaswin Mujoo
fs/iomap/buffered-io.c +14
-0@@ -1185,6 +1186,384 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied,return __iomap_write_end (iter->inode, pos, len, copied, folio); } +static ssize_t iomap_writethrough_complete (struct iomap_writethrough_ctx *wt_ctx) +{ +struct kiocb *iocb = wt_ctx->iocb; +struct inode *inode = wt_ctx->inode; +ssize_t ret = wt_ctx->error; + +if (wt_ctx->end_io) { +int err = wt_ctx->end_io (wt_ctx, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags); +if (err) + ret = err; + } +
<snip>
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+static void iomap_writethrough_bio_end_io (struct bio *bio) +{ +struct iomap_writethrough_ctx *wt_ctx = bio->bi_private; +struct folio_iter fi; + +if (bio->bi_status)
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1238
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+cmpxchg (&wt_ctx->error,0 , +blk_status_to_errno (bio->bi_status)); +bio_for_each_folio_all (fi, bio) +folio_end_writeback (fi.folio); + +bio_put (bio); +if (atomic_dec_and_test (&wt_ctx->ref)) +iomap_writethrough_done (wt_ctx); +} +
But in the writethrough patches, we first end the writeback on the folio and then call xfs_dio_write_end_io(), which converts unwritten or CoW extents.
Is there a race possible as we are doing the other way? What if folios are reclaimed between ending the writeback and conversion? A new read in that area will return zero filled folios as XFS still sees IOMAP_UNWRITTEN?
I don't have a reproducer, but my hunch says we can run into this if we have a lot of AIO as the end_io will be run via a workqueue?
Do we do something similar to buffered IO where we do folio_end_writeback after extents conversion?
--
Pankaj
Re: [RFC PATCH v4 05/12] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 89 lines · Pankaj Raghav (Samsung)
Hi Ojaswin,
I was comparing normal buffered IO scenario and writethrough.
In a normal buffered IO scenario, for unwritten extents or CoW extents, we first convert them before ending the writeback on the folio.
xfs_end_iocallsxfs_end_ioend_write().static void
xfs_end_ioend_write(
structiomap_ioend*ioend)
{
<snip>/* * Success: commit the COW or unwritten blocks if needed. */ if (is_zoned) error = xfs_zoned_end_io(ip, offset, size, ioend->io_sector, ioend->io_private, NULLFSBLOCK); else if (ioend->io_flags & IOMAP_IOEND_SHARED) error = xfs_reflink_end_cow(ip, offset, size); else if (ioend->io_flags & IOMAP_IOEND_UNWRITTEN) error = xfs_iomap_write_unwritten(ip, offset, size, false); if (!error && !(ioend->io_flags & IOMAP_IOEND_DIRECT) && xfs_ioend_is_append(ioend)) error = xfs_setfilesize(ip, offset, size); done: if (is_zoned) xfs_ioend_put_open_zones(ioend); iomap_finish_ioends(ioend, error); // #1 memalloc_nofs_restore(nofs_flag); }#1
iomap_finish_ioends()
iomap_finish_ioend_buffered_write()
iomap_finish_folio_write()
folio_end_writeback()Quoted 18 lines · Ojaswin Mujoo
fs/iomap/buffered-io.c +14
-0@@ -1185,6 +1186,384 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied,return __iomap_write_end (iter->inode, pos, len, copied, folio); } +static ssize_t iomap_writethrough_complete (struct iomap_writethrough_ctx *wt_ctx) +{ +struct kiocb *iocb = wt_ctx->iocb; +struct inode *inode = wt_ctx->inode; +ssize_t ret = wt_ctx->error; + +if (wt_ctx->end_io) { +int err = wt_ctx->end_io (wt_ctx, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags); +if (err) + ret = err; + } +<snip>
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+static void iomap_writethrough_bio_end_io (struct bio *bio) +{ +struct iomap_writethrough_ctx *wt_ctx = bio->bi_private; +struct folio_iter fi; + +if (bio->bi_status)fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1238
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+cmpxchg (&wt_ctx->error,0 , +blk_status_to_errno (bio->bi_status)); +bio_for_each_folio_all (fi, bio) +folio_end_writeback (fi.folio); + +bio_put (bio); +if (atomic_dec_and_test (&wt_ctx->ref)) +iomap_writethrough_done (wt_ctx); +} +But in the writethrough patches, we first end the writeback on the folio and then call
xfs_dio_write_end_io(), which converts unwritten or CoW extents.Is there a race possible as we are doing the other way? What if folios are reclaimed between ending the writeback and conversion? A new read in that area will return zero filled folios as XFS still sees
IOMAP_UNWRITTEN?
Quoted 5 lines
I don't have a reproducer, but my hunch says we can run into this if we have a lot of AIO as the
end_iowill be run via a workqueue?Do we do something similar to buffered IO where we do
folio_end_writebackafter extents conversion?
Hey Pankaj, thanks for pointing this out. I agree that this can open up some races like you mentioned. I believe the right things will be to have an buffered endio like flow however we will need to store the folios somewhere till the whole conversion is done, like buffered endio does in struct ioend->io_bio. Hmm I think it would be ideal if we could somehow reuse some of the logic there. I'll see if its possible.
Thanks,
ojaswin
Quoted 3 lines
--
Pankaj
RFC PATCH v4 6/12 xfs: Add RWF_WRITETHROUGH support to xfs
In reply to Ojaswin MujooAdd the boilerplate needed to start supporting RWF_WRITETHROUGH in XFS. We use the direct write ->iomap_begin() functions to ensure the range under write through always has a real non-delalloc extent. We reuse the xfs dio's end IO function to perform extent conversion and i_size handling for us.
*Note on COW extent over DATA hole case*
In case of an unmapped COW extent over a DATA hole
(due to COW preallocations), leave the extent unmapped until we are just about to send IO. At that time, use the ->writethrough_submit() call back to convert the COW extent to written.
We initially tried converting during iomap_begin() time (like dio does) but that results in a stale data exposure as follows:
1. iomap_begin() - converts COW extent over DATA hole to written and marks IOMAP_F_NEW to handle zeroing.
2. During iomap_write_begin() -> realise extent is stale and return back without zeroing.
3. iomap_begin() - Again sees the same COW extent but it's written this time so we don't mark IOMAP_F_NEW
4. Since IOMAP_F_NEW is unmarked, we never zeroout and hence expose stale data.
To avoid the above, take the buffered IO approach of converting the extent just before IO, when we are sure to have zeroed out the folio.
--- fs/xfs/xfs_file.c | 95 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-)
fs/xfs/xfs_file.c +89 -6
@@ -690,6 +690,48 @@ static const struct iomap_dio_ops xfs_dio_write_ops = { .end_io = xfs_dio_write_end_io, }; +static int +xfs_writethrough_end_io ( +struct iomap_writethrough_ctx *wt_ctx, +ssize_t size, +int error, +unsigned int flags) +{ +struct xfs_inode *ip =XFS_I (wt_ctx->inode); +xfs_off_t offset = wt_ctx->iocb->ki_pos; + +if (unlikely (error)) + goto error; + +/* +* writethrough completions are handled same as dio with the exception +* that we need to explicitly change the i_disk_size. This is because +* unlike dio, we have already updated the i_size and hence the +* (i_disk_size < i_size) check will fail in dio code +*/ + error =xfs_dio_write_end_io (wt_ctx->iocb, size, error, flags); +if (error) + goto error; +if (!size) + goto out; + +/* Fast lockless check similar to how we do in buffered endio */ +if (offset + size > ip->i_disk_size) { + error =xfs_setfilesize (ip, offset, size); +if (error) + goto error; + } + +return 0 ; + +error: +if (wt_ctx->flags &IOMAP_DIO_COW ) +xfs_reflink_cancel_cow_range (ip, offset, size, true); + +out: +return error; +} + static void xfs_dio_zoned_submit_io( const struct iomap_iter *iter,
@@ -1021,6 +1063,39 @@ xfs_file_dax_write(return ret; } +static int +xfs_writethrough_submit ( +struct inode *inode, +struct iomap *iomap, +loff_t offset, +u64 count) +{ +int error =0 ; +unsigned int nofs_flag; + +/* +* Convert CoW extents to regular. +* +* We are under writethrough context with folio lock possibly held. To +* avoid memory allocation deadlocks, set the task-wide nofs context. +*/ +if (iomap->flags &IOMAP_F_SHARED ) { + nofs_flag =memalloc_nofs_save (); + error =xfs_reflink_convert_cow (XFS_I (inode), offset, count); +memalloc_nofs_restore (nofs_flag); + } + +return error; +} + +const struct iomap_writethrough_ops xfs_writethrough_ops = { + .ops = &xfs_direct_write_iomap_ops, + .write_ops = &xfs_iomap_write_ops, + .end_io = xfs_writethrough_end_io, + .writethrough_submit = &xfs_writethrough_submit +}; + + STATIC ssize_t xfs_file_buffered_write( struct kiocb *iocb,
@@ -1043,9 +1118,13 @@ xfs_file_buffered_write( goto out;trace_xfs_file_buffered_write (iocb, from); - ret =iomap_file_buffered_write (iocb, from, - &xfs_buffered_write_iomap_ops, &xfs_iomap_write_ops, -NULL ); +if (iocb->ki_flags &IOCB_WRITETHROUGH ) { + ret =iomap_file_writethrough_write (iocb, from, + &xfs_writethrough_ops,NULL ); + }else + ret =iomap_file_buffered_write (iocb, from, + &xfs_buffered_write_iomap_ops, + &xfs_iomap_write_ops,NULL );/* * If we hit a space limit, try to free up some lingering preallocated
@@ -1080,8 +1159,12 @@ xfs_file_buffered_write(if (ret >0 ) {XFS_STATS_ADD (ip->i_mount, xs_write_bytes, ret); -/* Handle various SYNC-type writes */ - ret =generic_write_sync (iocb, ret); +/* +* Handle various SYNC-type writes. +* For writethrough, we handle sync during completion. +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) + ret =generic_write_sync (iocb, ret); }return ret; }
@@ -2176,7 +2259,7 @@ const struct file_operations xfs_file_operations = { .remap_file_range = xfs_file_remap_range, .fop_flags =FOP_MMAP_SYNC |FOP_BUFFER_RASYNC |FOP_BUFFER_WASYNC |FOP_DIO_PARALLEL_WRITE | - FOP_DONTCACHE, + FOP_DONTCACHE | FOP_WRITETHROUGH, .setlease = generic_setlease, };
Signature · 1 line
2.55.0
RFC PATCH v4 7/12 iomap: Add aio support to RWF_WRITETHROUGH
In reply to Ojaswin MujooWith aio the only thing we need to be careful off is that writethrough can be in progress even after dropping inode and folio lock. Due to this, we need a way to synchronise with other paths where stable write is not enough, example:
1. Truncate to 0 in xfs sets i_size = 0 before waiting for writeback to complete. In case of writethrough, the end io completion can again push the i_size to a non-zero value.
2. Dio reads might race with aio writethrough ->end_io() and read 0s if unwritten conversion is yet to happen.
Hence use the dio begin/end as it gives us the required guarantees.
--- fs/iomap/buffered-io.c | 57 ++++++++++++++++++++++++++++++++++++------ include/linux/iomap.h | 11 ++++++-- 2 files changed, 59 insertions(+), 9 deletions(-)
fs/iomap/buffered-io.c +50 -7
@@ -1202,6 +1202,9 @@ static ssize_t iomap_writethrough_complete(struct iomap_writethrough_ctx *wt_ctxmapping_dec_inflight_stable_writes (inode->i_mapping); +if (wt_ctx->is_aio) +inode_dio_end (inode); +if (!ret) { ret = wt_ctx->written; iocb->ki_pos += ret;
@@ -1212,12 +1215,27 @@ static ssize_t iomap_writethrough_complete(struct iomap_writethrough_ctx *wt_ctxreturn ret; } +static void iomap_writethrough_complete_work (struct work_struct *work) +{ +struct iomap_writethrough_ctx *wt_ctx = + container_of(work, struct iomap_writethrough_ctx, aio_work); +struct kiocb *iocb = wt_ctx->iocb; + + iocb->ki_complete (iocb,iomap_writethrough_complete (wt_ctx)); +} +static void iomap_writethrough_done (struct iomap_writethrough_ctx *wt_ctx) { -struct task_struct *waiter = wt_ctx->waiter; +if (!wt_ctx->is_aio) { +struct task_struct *waiter = wt_ctx->waiter; -WRITE_ONCE (wt_ctx->waiter,NULL ); -blk_wake_io_task (waiter); +WRITE_ONCE (wt_ctx->waiter,NULL ); +blk_wake_io_task (waiter); +return ; + } + +INIT_WORK (&wt_ctx->aio_work, iomap_writethrough_complete_work); +queue_work (wt_ctx->inode->i_sb->s_dio_done_wq, &wt_ctx->aio_work); }static void iomap_writethrough_bio_end_io (struct bio *bio)
@@ -1783,9 +1801,9 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i,if (iocb_is_dsync (iocb))/* D_SYNC support not implemented yet */ return -EOPNOTSUPP ; -if (!is_sync_kiocb (iocb)) -/* aio support not implemented yet */ -return -EOPNOTSUPP ; + +if (iocb->ki_flags &IOCB_NOWAIT ) + iter.flags |=IOMAP_NOWAIT ;/* * +1 to max bvecs to account for unaligned write spanning multiple
@@ -1804,11 +1822,33 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i, wt_ctx->end_io = wt_ops->end_io; wt_ctx->old_i_size =i_size_read (inode); wt_ctx->max_bvecs = max_bvecs; + wt_ctx->is_aio = !is_sync_kiocb (iocb);atomic_set (&wt_ctx->ref,1 ); - wt_ctx->waiter = current; + +if (!wt_ctx->is_aio) + wt_ctx->waiter = current; +else +/* +* With aio, writethrough can be in progress even after dropping +* inode and folio lock. Due to this, we need a way to +* synchronise with other paths where stable write is not enough +* (example truncate). Hence use the dio begin/end as it gives +* us the required guarantees. +*/ +inode_dio_begin (inode);mapping_inc_inflight_stable_writes (inode->i_mapping); +if (wt_ctx->is_aio && !inode->i_sb->s_dio_done_wq) { + ret =sb_init_dio_done_wq (inode->i_sb); +if (ret <0 ) { +mapping_dec_inflight_stable_writes (inode->i_mapping); +inode_dio_end (inode); +kfree (wt_ctx); +return ret; + } + } +blk_start_plug (&plug);while ((ret =iomap_iter (&iter, wt_ops->ops)) >0 ) {
@@ -1820,6 +1860,9 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i,blk_finish_plug (&plug);if (!atomic_dec_and_test (&wt_ctx->ref)) { +if (wt_ctx->is_aio) +return -EIOCBQUEUED ; +for (;;) {set_current_state (TASK_UNINTERRUPTIBLE );if (!READ_ONCE (wt_ctx->waiter))
include/linux/iomap.h +9 -2
@@ -569,9 +569,16 @@ struct iomap_writethrough_ctx {atomic_t ref;unsigned int flags;int error; +bool is_aio; + +union { +/* used during submission and for non-aio completion */ +struct task_struct *waiter; + +/* used during aio completion */ +struct work_struct aio_work; + }; -/* used during submission and for non-aio completion */ -struct task_struct *waiter;int (*end_io)(struct iomap_writethrough_ctx *wt_ctx,ssize_t size,int error,unsigned int flags);
Signature · 1 line
2.55.0
RFC PATCH v4 8/12 iomap: Add DSYNC support to RWF_WRITETHROUGH
In reply to Ojaswin MujooAdd DSYNC support to writethrough buffered writes. Unlike the usual buffered writes where we call generic_write_sync() inline during the syscall path, for writethrough we instead sync the data during IO completion path, just like dio.
This allows aio writethrough to be truly async where the syscall can return after IO submission and the sync can then be done asynchronously during IO completion time.
Further, just like dio, we utilize the FUA optimization, if available, to avoid syncing the data for DSYNC operations.
--- fs/iomap/buffered-io.c | 35 ++++++++++++++++++++++++++++++++++- include/linux/iomap.h | 1 + 2 files changed, 35 insertions(+), 1 deletion(-)
fs/iomap/buffered-io.c +34 -1
@@ -1208,7 +1208,17 @@ static ssize_t iomap_writethrough_complete(struct iomap_writethrough_ctx *wt_ctxif (!ret) { ret = wt_ctx->written; iocb->ki_pos += ret; - }else + +/* +* If this is a DSYNC write and we couldn't optimize it, make +* sure we push it to stable storage now that we've written +* data. +*/ +if (iocb_is_dsync (wt_ctx->iocb) && !wt_ctx->use_fua) + ret =generic_write_sync (iocb, ret); + } + +if (ret <0 )mapping_set_error (inode->i_mapping, ret);kfree (wt_ctx);
@@ -1270,6 +1280,9 @@ iomap_writethrough_submit_bio(struct iomap_writethrough_ctx *wt_ctx,for (i =0 ; i < wt_ctx->nr_bvecs; i++) len += wt_ctx->bvec[i].bv_len; +if (wt_ctx->use_fua) + opf |=REQ_FUA ; + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs, opf,GFP_NOFS ); bio->bi_iter.bi_sector =iomap_sector (iomap, wt_ctx->bio_pos); bio->bi_end_io = iomap_writethrough_bio_end_io;
@@ -1406,6 +1419,19 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx,if (iter->iomap.type ==IOMAP_INLINE )return -EINVAL ; +/* +* If we realise that cache flush is necessary (eg FUA is not present +* or we need metadata updates) then we turn off the optimization. +*/ +if (wt_ctx->use_fua) { +if (iter->iomap.type !=IOMAP_MAPPED || + (iter->iomap.flags & + (IOMAP_F_NEW |IOMAP_F_SHARED |IOMAP_F_DIRTY )) || + (bdev_write_cache (iter->iomap.bdev) && + !bdev_fua (iter->iomap.bdev))) + wt_ctx->use_fua = false; + } + do { struct folio *folio; size_t offset; /* Offset into folio */
@@ -1825,6 +1851,13 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i, wt_ctx->is_aio = !is_sync_kiocb (iocb);atomic_set (&wt_ctx->ref,1 ); +/* +* Similar to dio, we optimistically set use_fua=true to avoid explicit +* sync. In case we later realise cache flush is needed we set it back +* to false. +*/ + wt_ctx->use_fua =iocb_is_dsync (iocb) && !(iocb->ki_flags &IOCB_SYNC ); +if (!wt_ctx->is_aio) wt_ctx->waiter = current;else
include/linux/iomap.h +1 -0
@@ -570,6 +570,7 @@ struct iomap_writethrough_ctx {unsigned int flags;int error;bool is_aio; +bool use_fua;union {/* used during submission and for non-aio completion */
Signature · 1 line
2.55.0
Re: [RFC PATCH v4 08/12] iomap: Add DSYNC support to RWF_WRITETHROUGH
Quoted 9 lines · Ojaswin Mujoo
Add DSYNC support to writethrough buffered writes. Unlike the usual buffered writes where we call
generic_write_sync()inline during the syscall path, for writethrough we instead sync the data during IO completion path, just like dio.This allows aio writethrough to be truly async where the syscall can return after IO submission and the sync can then be done asynchronously during IO completion time.
Quoted 7 lines
Further, just like dio, we utilize the FUA optimization, if available, to avoid syncing the data for DSYNC operations.
Suggested-by Dave Chinner <[email protected]>Co-developed-by Ritesh Harjani (IBM) <[email protected]>Signed-off-by Ritesh Harjani (IBM) <[email protected]>Signed-off-by Ojaswin Mujoo <[email protected]>
I made a small mistake here while doing the final squash. This patch also removes the EOPNOTSUPP on dsync in iomap_file_writethrough_iter();
@@ -1798,9 +1824,6 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i,
return -EINVAL;
if (iocb->ki_flags & (IOCB_DONTCACHE))
return -EINVAL;
- if (iocb_is_dsync(iocb))
- /* D_SYNC support not implemented yet */
- return -EOPNOTSUPP;
if (iocb->ki_flags & IOCB_NOWAIT)
Regards,
ojaswin
RFC PATCH v4 9/12 fs: Introduce RWF_NOSERIAL flag to indicate parallel reads/writes
In reply to Ojaswin MujooIntroduce RWF_NOSERIAL flag to indicate that the application is okay with its reads and writes going in parallel to other read/writes. This flag will allow writes and read to go in parallel (for eg, under a shared lock) increasing performance at the cost of losing the (loosely implemented) POSIX guarantees wrt to R/W serialization that various filesystems provide.
In this patch we just introduce the flag and in upcoming patches we will use it to implement parallel writes to increase performance of RWF_WRITETHROUGH
--- include/linux/fs.h | 10 +++++++++- include/uapi/linux/fs.h | 6 +++++- tools/include/uapi/linux/fs.h | 5 ++++- 3 files changed, 18 insertions(+), 3 deletions(-)
include/linux/fs.h +9 -1
@@ -345,6 +345,7 @@ struct readahead_control;#define IOCB_DONTCACHE (__force int) RWF_DONTCACHE#define IOCB_NOSIGNAL (__force int) RWF_NOSIGNAL#define IOCB_WRITETHROUGH (__force int) RWF_WRITETHROUGH +#define IOCB_NOSERIAL (__force int) RWF_NOSERIAL/* non-RWF related bits - start at 16 */ #define IOCB_EVENTFD (1 << 16)
@@ -375,7 +376,8 @@ struct readahead_control; { IOCB_NOIO, "NOIO" }, \ { IOCB_ALLOC_CACHE, "ALLOC_CACHE" }, \ { IOCB_AIO_RW, "AIO_RW" }, \ - { IOCB_HAS_METADATA, "AIO_HAS_METADATA" } + { IOCB_HAS_METADATA, "AIO_HAS_METADATA" }, \ + { IOCB_NOSERIAL, "IOCB_NOSERIAL" } struct kiocb {struct file *ki_filp;
@@ -3499,6 +3501,12 @@ static inline int kiocb_set_rw_flags(struct kiocb *ki, rwf_t flags, ki->ki_flags &= ~IOCB_APPEND ; } +/* +* Currently, only writethrough supports noserial IO. +*/ +if ((flags &RWF_NOSERIAL ) && !(flags &RWF_WRITETHROUGH )) +return -EOPNOTSUPP ; + ki->ki_flags |= kiocb_flags;return 0 ; }
include/uapi/linux/fs.h +5 -1
@@ -454,10 +454,14 @@ typedef int __bitwise __kernel_rwf_t;/* buffered IO that is asynchronously written through to disk after write */ #define RWF_WRITETHROUGH ((__force __kernel_rwf_t)0x00000200) +/* buffered IO writes that are non sequential because they use a shared lock */ +#define RWF_NOSERIAL ((__force __kernel_rwf_t)0x00000400) +/* mask of flags supported by the kernel */ #define RWF_SUPPORTED (RWF_HIPRI | RWF_DSYNC | RWF_SYNC | RWF_NOWAIT |\ RWF_APPEND | RWF_NOAPPEND | RWF_ATOMIC |\ - RWF_DONTCACHE | RWF_NOSIGNAL | RWF_WRITETHROUGH) + RWF_DONTCACHE | RWF_NOSIGNAL | RWF_WRITETHROUGH |\ + RWF_NOSERIAL)#define PROCFS_IOCTL_MAGIC 'f'
tools/include/uapi/linux/fs.h +4 -1
@@ -347,10 +347,13 @@ typedef int __bitwise __kernel_rwf_t;/* buffered IO that is asynchronously written through to disk after write */ #define RWF_WRITETHROUGH ((__force __kernel_rwf_t)0x00000200) +/* buffered IO writes that are non sequential because they use a shared lock */ +#define RWF_NOSERIAL ((__force __kernel_rwf_t)0x00000400) +/* mask of flags supported by the kernel */ #define RWF_SUPPORTED (RWF_HIPRI | RWF_DSYNC | RWF_SYNC | RWF_NOWAIT |\ RWF_APPEND | RWF_NOAPPEND | RWF_ATOMIC |\ - RWF_DONTCACHE | RWF_WRITETHROUGH) + RWF_DONTCACHE | RWF_WRITETHROUGH | RWF_NOSERIAL)#define PROCFS_IOCTL_MAGIC 'f'
Signature · 1 line
2.55.0
RFC PATCH v4 10/12 xfs: Implement RWF_NOSERIAL to parallelize RWF_WRITETHROUGH writes
In reply to Ojaswin MujooIn xfs, buffered writethrough writes take an exclusive inode lock similar to regular buffered write path. However, since writethrough also submits the write under the inode lock, the increased critical section really hurts performance when we have parallel writers writing to the same file.
To mitigate this, implement RWF_NOSERIAL flag which allows us to perform the write under a shared inode lock, instead of an exclusive lock. This gives significant performance boost to single file, multiple writer workloads at the cost of losing write-write serialization and read-write serialization guarantees which XFS has historically provided.
Let's look at each of the guarantees and what changes with the NOSERIAL flag:
Write-write guarantee:
Image 2 writers trying to write the same 3 folios. One is write As to all 3 folios (denoted by AAA) and the other is writing BBB. Then under exclusive lock the final state of the 3 folios could only be either AAA or BBB.
However, with NOSERIAL writes, we could end up with mixed data in the 3 folios, like AAB, ABA, BAA etc. Note that this mixing will always happen at the inter folio level, data contained within the same folio will not be mixed as it is protected by the folio lock.
Read-write guarantee:
In XFS, reads also take a shared lock, however they don't take a folio lock when copying from the folio to the user buffer. Due to the shared lock of read and exclusive lock of write, we are able to guarantee that the read always reads either completely old data or completely new data.
However, with the NOSERIAL flag, writethrough will take a shared lock for writes, ie a read can race with a write which is still in middle of copying data to the folio, hence the read can read a mix of old and new data.
Despite the above changes in behavior, there might be applications who would be okay to lose the guarantees because of the nature of their workloads for example, if they never have multiple readers/writes doing IO to the same range in the file. Such applications would benefit significantly by using NOSERIAL writes.
Below are some fio performance numbers of RWF_WRITETHROUGH with and without NOSERIAL writes.
** Multiple writes, single file (Pure overwrites, DSYNC) **
Fio Workload:
libaio, buffered randwrite, bs=4k, size=2G (pre written), O_DSYNC iodepth=32
numjobs baseline BW RWF_NOSERIAL BW Δ%
1 350 392 +12.0%
2 526 798 +51.7%
4 597 1591 +166.5%
8 630 1839 +191.9%
16 570 1836 +222.1%** Multiple writes, single file (Preallocated file, sync_file_range) **
Fio Workload:
libaio, buffered randwrite, bs=16k, size=5G (fallocated) sync_file_range=wait_before,write:16
numjobs baseline BW RWF_NOSERIAL BW Δ%
1 1099 1279 +16.4%
2 1482 2456 +65.7%
4 2065 2479 +20.1%
8 1778 2500 +40.6%
16 1787 2508 +40.3%* Multiple writes, single file (Truncated file, DSYNC) * Fio Workload:
libaio, buffered randwrite, bs=4k, size=6.5G (truncated), O_DSYNC iodepth=32
numjobs baseline BW RWF_NOSERIAL BW Δ%
1 78 80 +2.6%
2 97 88 -9.3%
4 100 96 -4.0%
8 109 97 -11.0%
16 106 107 +0.9%--- fs/xfs/xfs_file.c | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-)
fs/xfs/xfs_file.c +51 -6
@@ -508,6 +508,45 @@ xfs_file_write_checks(return kiocb_modified (iocb); } +STATIC ssize_t +xfs_file_writethrough_checks ( +struct kiocb *iocb, +struct iov_iter *from, +unsigned int *iolock, +struct xfs_zone_alloc_ctx *ac) +{ +struct inode *inode = iocb->ki_filp->f_mapping->host; +size_t orig_count =iov_iter_count (from); +xfs_fsize_t isize =i_size_read (inode); +ssize_t error; + +restart: + error =xfs_file_write_checks (iocb, from, iolock, ac); +if (error <0 ) +return error; + +if (*iolock ==XFS_IOLOCK_EXCL ) +return 0 ; + +/* Extending IO needs exclusive lock for i_size change */ +if (iocb->ki_pos +iov_iter_count (from) > isize) + goto upgrade_excl; + +return 0 ; + +upgrade_excl: +xfs_iunlock (XFS_I (inode), *iolock); + *iolock =XFS_IOLOCK_EXCL ; + error =xfs_ilock_iocb (iocb, *iolock); +if (error) { + *iolock =0 ; +return error; + } +iov_iter_reexpand (from, orig_count); + goto restart; + +} + static ssize_t xfs_zoned_write_space_reserve( struct xfs_mount *mp,
@@ -1108,23 +1147,29 @@ xfs_file_buffered_write(unsigned int iolock; write_retry: - iolock =XFS_IOLOCK_EXCL ; +if (iocb->ki_flags &IOCB_NOSERIAL ) + iolock =XFS_IOLOCK_SHARED ; +else + iolock =XFS_IOLOCK_EXCL ; ret =xfs_ilock_iocb (iocb, iolock);if (ret)return ret; - ret =xfs_file_write_checks (iocb, from, &iolock,NULL ); -if (ret) - goto out; -trace_xfs_file_buffered_write (iocb, from);if (iocb->ki_flags &IOCB_WRITETHROUGH ) { + ret =xfs_file_writethrough_checks (iocb, from, &iolock,NULL ); +if (ret) + goto out; ret =iomap_file_writethrough_write (iocb, from, &xfs_writethrough_ops,NULL ); - }else + }else { + ret =xfs_file_write_checks (iocb, from, &iolock,NULL ); +if (ret) + goto out; ret =iomap_file_buffered_write (iocb, from, &xfs_buffered_write_iomap_ops, &xfs_iomap_write_ops,NULL ); + }/* * If we hit a space limit, try to free up some lingering preallocated
Signature · 1 line
2.55.0
RFC PATCH v4 11/12 iomap: Avoid folio dirtying in case of RWF_WRITETHROUGH
In reply to Ojaswin MujooRWF_WRITETHROUGH dirties the folio only to send it for IO immediately, in the same context. Due to this, we can optimize away the folio dirtying and clearing step usually seen in buffered IO. Althrough we can do away with most of the accounting there are a couple of counters we need to take care of which we do during IO submission/completion.
Further, now folio_start/end_writeback() can find an inode with no wb attached and hence ensure its not null before using it.
--- fs/iomap/buffered-io.c | 75 ++++++++++++++++++++++++++++++++++++------ mm/page-writeback.c | 24 +++++++++++--- 2 files changed, 84 insertions(+), 15 deletions(-)
fs/iomap/buffered-io.c +65 -10
@@ -11,6 +11,7 @@#include <linux/fserror.h> #include <linux/fsverity.h> #include <linux/rmap.h> +#include <linux/task_io_accounting_ops.h> #include "internal.h" #include "trace.h"
@@ -1162,6 +1163,34 @@ static bool iomap_write_end_inline(const struct iomap_iter *iter,return true; } +/* +* __iomap_writethrough_end() is almost same as __iomap_write_end() but with the difference +* that we don't mark folio dirty since we are about to issue it for IO anyways. +* Consequently, most of the accounting is skipped. +*/ +static bool __iomap_writethrough_end (struct inode *inode,loff_t pos,size_t len, +size_t copied,struct folio *folio) +{ +flush_dcache_folio (folio); + +/* +* The blocks that were entirely written will now be up-to-date, so we +* don't have to worry about a read_folio reading them and overwriting a +* partial write. However, if we've encountered a short write and only +* partially written into a block, it will not be marked up-to-date, so a +* read_folio might come in and destroy our partial write. +* +* Do the simplest thing and just treat any short write to a +* non-uptodate page as a zero-length write, and force the caller to +* redo the whole thing. +*/ +if (unlikely (copied < len && !folio_test_uptodate (folio))) +return false; +iomap_set_range_uptodate (folio,offset_in_folio (folio, pos), len); +return true; +} + +/* * Returns true if all copied bytes have been written to the pagecache, * otherwise return false.
@@ -1183,7 +1212,10 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied,return bh_written == copied; } -return __iomap_write_end (iter->inode, pos, len, copied, folio); +if (iter->flags &IOMAP_WRITETHROUGH ) +return __iomap_writethrough_end (iter->inode, pos, len, copied, folio); +else +return __iomap_write_end (iter->inode, pos, len, copied, folio); }static ssize_t iomap_writethrough_complete (struct iomap_writethrough_ctx *wt_ctx)
@@ -1302,9 +1334,11 @@ iomap_writethrough_submit_bio(struct iomap_writethrough_ctx *wt_ctx,/* * In case of error we still need the I/O completion to run so we can -* release references and end writeback on the folios. +* release references, handle accounting and end writeback on the +* folios. */ if (error) { +task_io_account_cancelled_write (len); bio->bi_status =errno_to_blk_status (error);bio_endio (bio);return error;
@@ -1352,8 +1386,10 @@ iomap_writethrough_try_submit(struct iomap_writethrough_ctx *wt_ctx,static void iomap_folio_prepare_writethrough (struct folio *folio,size_t off,size_t len) { -bool fully_written; +bool needs_cleardirty = false, fully_written = false;u64 zero =0 ; +u64 tmp_off = off; +struct iomap_folio_state *ifs = folio->private;if (folio_test_writeback (folio))folio_wait_writeback (folio);
@@ -1362,16 +1398,35 @@ static void iomap_folio_prepare_writethrough(struct folio *folio, size_t off,folio_mark_dirty (folio);/* -* We might either write through the complete folio or a partial folio -* writethrough might result in all blocks becoming non-dirty, so we need to -* check and mark the folio clean if that is the case. +* For writethrough, we don't mark the write range dirty but we still +* need clear the dirty range if someone else has dirtied it before. +* Further, if the clearing results in folio becoming completely clean, +* then we need to take care of accounting. If we have ifs, we can +* simply use that to ensure this. If we don't have ifs, that implies we +* did a complete overwrite of the folio, in which case we can just +* check if the folio was dirty earlier */ fully_written = (off ==0 && len ==folio_size (folio)); -iomap_clear_range_dirty (folio, off, len); -if (fully_written || - !iomap_find_dirty_range (folio, &zero,folio_size (folio))) -folio_clear_dirty_for_writethrough (folio); +if (ifs) { +if (iomap_find_dirty_range (folio, &tmp_off, tmp_off + len)) { +iomap_clear_range_dirty (folio, off, len); + +/* +* iomap_find_dirty_range() only works for folios with ifs +*/ +if (!iomap_find_dirty_range (folio, &zero, +folio_size (folio))) + needs_cleardirty = true; + } + }else { +WARN_ON (!fully_written); +if (folio_test_dirty (folio)) + needs_cleardirty = true; + } +if (needs_cleardirty) +folio_clear_dirty_for_writethrough (folio); +task_io_account_write (len);folio_start_writeback (folio); }
mm/page-writeback.c +19 -5
@@ -2986,11 +2986,18 @@ bool __folio_end_writeback(struct folio *folio)__xa_clear_mark (&mapping->i_pages, folio->index,PAGECACHE_TAG_WRITEBACK ); +/* +* With RWF_WRITETHROUGH, we might not have a writeback +* associated with the inode +*/ wb =inode_to_wb (inode); -wb_stat_mod (wb,WB_WRITEBACK , -nr); -__wb_writeout_add (wb, nr); +if (wb) { +wb_stat_mod (wb,WB_WRITEBACK , -nr); +__wb_writeout_add (wb, nr); + }if (!mapping_tagged (mapping,PAGECACHE_TAG_WRITEBACK )) { -wb_inode_writeback_end (wb); +if (wb) +wb_inode_writeback_end (wb);if (mapping->host)sb_clear_inode_writeback (mapping->host); }
@@ -3030,10 +3037,17 @@ void __folio_start_writeback(struct folio *folio, bool keep_write) on_wblist =mapping_tagged (mapping,PAGECACHE_TAG_WRITEBACK );xas_set_mark (&xas,PAGECACHE_TAG_WRITEBACK ); + +/* +* With RWF_WRITETHROUGH, we might not have a writeback +* associated with the inode +*/ wb =inode_to_wb (inode); -wb_stat_mod (wb,WB_WRITEBACK , nr); +if (wb) +wb_stat_mod (wb,WB_WRITEBACK , nr);if (!on_wblist) { -wb_inode_writeback_start (wb); +if (wb) +wb_inode_writeback_start (wb);/* * We can come through here when swapping anonymous * folios, so we don't necessarily have an inode to
Signature · 1 line
2.55.0
RFC PATCH v4 12/12 iomap: Handle deadlock due to repeating folios in RWF_WRITETHROUGH
In reply to Ojaswin MujooIn iomap_writethrough_iter() we might encounter repeating folios across multiple iterations. Repeating folios can occur if, example, copy_folio_from_iter_atomic() does a short copy due to userspace pages not faulted in. This is an issue because a previous loop might have started writeback on them but not yet issued the IO. In the next iteration trying to get the same folio with FGP_STABLE will result in a deadlock. Since repeating folios will always be encountered back to back, we can just use a simple cur != prev check to detect them.
Use this to avoid waiting for writeback or starting writeback on folios we have already processed. Note that in ->endio() we might end up calling folio_end_writethrough() twice on the same folio which can cause issues with folio_xor_flags_has_waiters(). For simplicity, just change the folio_xor_flags_has_waiters() call to an idempotent variant.
--- fs/iomap/buffered-io.c | 46 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-)
fs/iomap/buffered-io.c +38 -8
@@ -802,6 +802,13 @@ struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len) {fgf_t fgp =FGP_WRITEBEGIN ; +/* +* For writethrough, we open code the FGP_STABLE logic directly in +* iomap_writhrethrough_iter() so disable it here.. See +* iomap_writethrough_iter() for details. +*/ +if (iter->flags &IOMAP_WRITETHROUGH ) + fgp &= ~FGP_STABLE ;if (iter->flags &IOMAP_NOWAIT ) fgp |=FGP_NOWAIT ;if (iter->flags &IOMAP_DONTCACHE )
@@ -1284,12 +1291,16 @@ static void iomap_writethrough_bio_end_io(struct bio *bio) {struct iomap_writethrough_ctx *wt_ctx = bio->bi_private;struct folio_iter fi; +struct folio *prev_folio =NULL ;if (bio->bi_status)cmpxchg (&wt_ctx->error,0 ,blk_status_to_errno (bio->bi_status)); -bio_for_each_folio_all (fi, bio) -folio_end_writeback (fi.folio); +bio_for_each_folio_all (fi, bio) { +if (fi.folio != prev_folio) +folio_end_writeback (fi.folio); + prev_folio = fi.folio; + }bio_put (bio);if (atomic_dec_and_test (&wt_ctx->ref))
@@ -1384,16 +1395,13 @@ iomap_writethrough_try_submit(struct iomap_writethrough_ctx *wt_ctx,* need to clear the master dirty bit. */ static void iomap_folio_prepare_writethrough (struct folio *folio,size_t off, -size_t len) +size_t len,bool already_prepared) {bool needs_cleardirty = false, fully_written = false;u64 zero =0 ;u64 tmp_off = off;struct iomap_folio_state *ifs = folio->private; -if (folio_test_writeback (folio)) -folio_wait_writeback (folio); -if (folio_mkclean (folio))folio_mark_dirty (folio);
@@ -1427,7 +1435,8 @@ static void iomap_folio_prepare_writethrough(struct folio *folio, size_t off,if (needs_cleardirty)folio_clear_dirty_for_writethrough (folio);task_io_account_write (len); -folio_start_writeback (folio); +if (!already_prepared) +folio_start_writeback (folio); }/**
@@ -1444,6 +1453,17 @@ static void iomap_folio_prepare_writethrough(struct folio *folio, size_t off,* Folio handling note: We might be writing through a partial folio so we need * to be careful to not clear the folio dirty bit unless there are no dirty blocks * in the folio after the writethrough. +* +* **A corner case to be careful about** +* +* For writethrough, we open code the stable write behavior to handle the case +* where we encounter a folio that we already started writeback on but have not +* yet submitted. In that case we must not wait for writeback again to avoid +* deadlocking. Repeating folios can occur if, example, +* copy_folio_from_iter_atomic() does a short copy due to userspace pages not +* faulted in. Also, repeating folios will always be encountered back to back so +* we can just use a simple cur != prev check to detect them. +*/ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx, struct iomap_iter *iter, struct iov_iter *i,
@@ -1457,6 +1477,7 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx,size_t chunk =mapping_max_folio_size (mapping);unsigned int bdp_flags = (iter->flags &IOMAP_NOWAIT ) ?BDP_ASYNC :0 ;unsigned int bs =i_blocksize (iter->inode); +struct folio *prev_folio =NULL ;/* copied over based on how DIO handles these flags */ if (iter->iomap.type ==IOMAP_UNWRITTEN )
@@ -1547,6 +1568,10 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx,if (mapping_writably_mapped (mapping))flush_dcache_folio (folio); +/* Open coding stable write behavior, see comment on top. */ +if (prev_folio != folio) +folio_wait_writeback (folio); + copied =copy_folio_from_iter_atomic (folio, offset, bytes, i); written =iomap_write_end (iter, bytes, copied, folio) ? copied :0 ;
@@ -1584,7 +1609,10 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx, prev_len = len_aligned;iomap_folio_prepare_writethrough (folio, off_aligned, - len_aligned); + len_aligned, + prev_folio == folio); + + prev_folio = folio;if (!wt_ctx->nr_bvecs) { wt_ctx->bio_pos =round_down (pos, bs);
@@ -1629,6 +1657,8 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx, &iter->iomap, wt_ops, &pending);if (status) goto submit_failed; + + prev_folio =NULL ; } }while (iov_iter_count (i) &&iomap_length (iter));
Signature · 1 line
2.55.0
Re: [RFC PATCH v4 12/12] iomap: Handle deadlock due to repeating folios in RWF_WRITETHROUGH
Quoted 9 lines · Ojaswin Mujoo
In
iomap_writethrough_iter()we might encounter repeating folios across multiple iterations. Repeating folios can occur if, example,copy_folio_from_iter_atomic()does a short copy due to userspace pages not faulted in. This is an issue because a previous loop might have started writeback on them but not yet issued the IO. In the next iteration trying to get the same folio withFGP_STABLEwill result in a deadlock. Since repeating folios will always be encountered back to back, we can just use a simple cur != prev check to detect them.
Quoted 5 lines
Use this to avoid waiting for writeback or starting writeback on folios we have already processed. Note that in ->endio() we might end up calling
folio_end_writethrough()twice on the same folio which can cause issues withfolio_xor_flags_has_waiters(). For simplicity, just change thefolio_xor_flags_has_waiters()call to an idempotent variant.
Is this an outdated comment?
Quoted 2 lines
Reported-by Pankaj Raghav <[email protected]>
Thanks for fixing it. I think I was able to trigger this in a VM with limited memory where the chances of short copy is probably higher. The fixes makes sense to me.
I will run some tests this week and report back if there is an issue.
--
Pankaj
Re: [RFC PATCH v4 12/12] iomap: Handle deadlock due to repeating folios in RWF_WRITETHROUGH
Quoted 10 lines · Pankaj Raghav (Samsung)
Quoted 9 lines · Ojaswin Mujoo
In
iomap_writethrough_iter()we might encounter repeating folios across multiple iterations. Repeating folios can occur if, example,copy_folio_from_iter_atomic()does a short copy due to userspace pages not faulted in. This is an issue because a previous loop might have started writeback on them but not yet issued the IO. In the next iteration trying to get the same folio withFGP_STABLEwill result in a deadlock. Since repeating folios will always be encountered back to back, we can just use a simple cur != prev check to detect them.
Quoted 7 lines
Quoted 5 lines
Use this to avoid waiting for writeback or starting writeback on folios we have already processed. Note that in ->endio() we might end up calling
folio_end_writethrough()twice on the same folio which can cause issues withfolio_xor_flags_has_waiters(). For simplicity, just change thefolio_xor_flags_has_waiters()call to an idempotent variant.Is this an outdated comment?
Yes, thanks. I'll change this. We just use the folio_end_writethrough() now.
Quoted 12 lines
Quoted 3 lines
Reported-by Pankaj Raghav <[email protected]>Thanks for fixing it. I think I was able to trigger this in a VM with limited memory where the chances of short copy is probably higher. The fixes makes sense to me.
I will run some tests this week and report back if there is an issue.
--
Pankaj