Add buffered write-through support to iomap & xfs
Subsystems (5)
v2 cover Add buffered write-through support to iomap & xfs
Hi all,
This is the v2 RFC to add buffered writethrough support to iomap and xfs. The changes made are mostly to get the writethrough implementation more inline with how dio handles writes.
** Changes since RFC v1 [3] **
1. In v1, even the non-aio writethrough syscall returned after IO submission but before waiting for IO to be finished. However, upon revisiting some of the discussions, we feel that it's more cleaner to keep the behavior similar to dio ie non-aio variant should only return after the IO completes and report any issues upon return. Hence v2 now follows the exact pattern of dio where non-aio writethrough waits for write to finish whereas aio writethrough returns after submission. This is inline with the discussion here [2].
2. Instead of submitting a bio per folio, we now submit a bio per iomap. Only once all IO are complete, we call the completion function to invoke FS specific ->end_io()
3. Instead of reusing dio code, we have open coded the IO submission and completion. Althrough this is heavily inspired by dio, trying to hammer buffered writethrough handling in iomap_dio_rw() was resulting in ugly if elses and hard to follow code. The open coded variant is clean and easier to follow however ideally we should try to factor out common parts of dio code to have a cleaner interface.
4. Support for aio and DSYNC writethrough is added, which utilizes FUA optimizations if available.
5. Added a new ->writethrough_submit() operation which allows FSes to perform tasks before IO submission. Like converting COW mappins to written. The motivation is explained in patch 3
6. Refactored folio_clear_dirty_for_io() so it can be reused without having to call folio_mkclean(). This is because writethrough mkcleans the folio in all the cases but only clears dirty bit if the whole folio is about to become clean.
[2] https://lore.kernel.org/all/aZUQKx_C3-qyU4PJ@dread/
[3] https://lore.kernel.org/linux-xfs/[email protected]/
*** Original Cover ***
Hi all,
This patchset implements an early design prototype of buffered I/O write-through semantics in linux.
This idea mainly picked up traction to enable RWF_ATOMIC buffered IO [1], 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
The implementation of write-through combines the buffered IO frontend with dio backend, which leads to some interesting interactions. I've added most of the design notes in respective patches. Please note that this is an initial RFC to iron out any early design issues. This is largely based on suggestions from Dave an Jan in [1] so thanks for the pointers!
* Testing Notes (UPDATED) *
- I've added support for RWF_WRITETHROUGH to fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.
* Design TODOs (UPDATED) *
- Evaluate if we need to tag page cache dirty bit in xarray, since PG_Writeback is already set on the folio.
- Look into a better way to refactor writethrough path by reusing common parts of dio code.
* Future work (once design is finalized) (UPDATED) *
- Add RWF_ATOMIC support for buffered IO via write-through path
- Add support of other RWF_ flags for write-through buffered I/O path
- Benchmarking numbers and more thorough testing needed.
- ext4 support for writethrough
- Utilize writethrough for normal buffered DSYNC path to get truly async semantincs for DSYNC
- Look into folio batching support.
As usual, thoughts and suggestions are welcome.
[1] https://lore.kernel.org/all/[email protected]/
Regards,
ojaswin
Ojaswin Mujoo (5):
mm: Refactor folio_clear_dirty_for_io()
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 writethrough
fs/iomap/buffered-io.c | 420 ++++++++++++++++++++++++++++++++++++++++ fs/xfs/xfs_file.c | 53 ++++- include/linux/fs.h | 7 + include/linux/iomap.h | 45 +++++ include/linux/pagemap.h | 1 + include/uapi/linux/fs.h | 5 +- mm/page-writeback.c | 18 +- 7 files changed, 540 insertions(+), 9 deletions(-)
Signature · 1 line
2.53.0
RFC PATCH v2 1/5 mm: Refactor folio_clear_dirty_for_io()
Add 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 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-)
mm/page-writeback.c +10 -2
@@ -2847,8 +2847,11 @@ EXPORT_SYMBOL(__folio_cancel_dirty);* * This incoherency between the folio's dirty flag and xarray tag is * unfortunate, but it only exists while the folio is locked. +* +* 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;
@@ -2885,7 +2888,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
@@ -2908,6 +2911,11 @@ bool folio_clear_dirty_for_io(struct folio *folio) }return folio_test_clear_dirty (folio); } + +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.53.0
Re: [RFC PATCH v2 1/5] mm: Refactor folio_clear_dirty_for_io()
mm/page-writeback.c EXPORT_SYMBOL(__folio_cancel_dirty); · line 2853
Quoted 8 lines · Ojaswin Mujoo
mm/page-writeback.c +3
-0@@ -2847,8 +2847,11 @@ EXPORT_SYMBOL(__folio_cancel_dirty);* * This incoherency between the folio's dirty flag and xarray tag is * unfortunate, but it only exists while the folio is locked. +* +* 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. */
The kerneldoc should move down to the exported version.
Re: [RFC PATCH v2 1/5] mm: Refactor folio_clear_dirty_for_io()
Quoted 11 lines · Christoph Hellwig
mm/page-writeback.c EXPORT_SYMBOL(__folio_cancel_dirty); · line 2853
Quoted 8 lines · Ojaswin Mujoo
mm/page-writeback.c +3
-0@@ -2847,8 +2847,11 @@ EXPORT_SYMBOL(__folio_cancel_dirty);* * This incoherency between the folio's dirty flag and xarray tag is * unfortunate, but it only exists while the folio is locked. +* +* 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. */ The kerneldoc should move down to the exported version.
Hey Christoph, sure thanks I'll take care of it in next version.
Regards,
ojaswin
Quoted 2 lines
RFC PATCH v2 2/5 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 delibrately open coded the IO submission and completion flow (inspired by dio) rather than reusing the dio functions as accomodating buffered writethrough logic in dio code was polluting it with too many if else conditionals and special cases.
--- fs/iomap/buffered-io.c | 352 ++++++++++++++++++++++++++++++++++++++++ include/linux/fs.h | 7 + include/linux/iomap.h | 38 +++++ include/linux/pagemap.h | 1 + include/uapi/linux/fs.h | 5 +- mm/page-writeback.c | 6 + 6 files changed, 408 insertions(+), 1 deletion(-)
fs/iomap/buffered-io.c +352 -0
@@ -9,6 +9,7 @@#include <linux/swap.h> #include <linux/migrate.h> #include <linux/fserror.h> +#include <linux/rmap.h> #include "internal.h" #include "trace.h"
@@ -1096,6 +1097,276 @@ 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->dops && wt_ctx->dops->end_io) { +int err = wt_ctx->dops->end_io (iocb, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags); +if (err) + ret = err; + } + +mapping_clear_stable_writes (inode->i_mapping); + +if (!ret) { + ret = wt_ctx->written; + iocb->ki_pos = wt_ctx->pos + 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); +return ; +} + +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 void +iomap_writethrough_submit_bio (struct iomap_writethrough_ctx *wt_ctx, +struct iomap *iomap, +const struct iomap_writethrough_ops *wt_ops) +{ +struct bio *bio; +unsigned int i; +u64 len =0 ; + +if (!wt_ctx->nr_bvecs) +return ; + +for (i =0 ; i < wt_ctx->nr_bvecs; i++) + len += wt_ctx->bvec[i].bv_len; + +if (wt_ops->writethrough_submit) + wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, + len); + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,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); + +atomic_inc (&wt_ctx->ref); +submit_bio (bio); + wt_ctx->nr_bvecs =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 ; +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 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 ; + +do { +struct folio *folio; +size_t offset;/* Offset into folio */ +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; + + 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 and don't bother submitting more bios. +*/ +if (unlikely (data_race (wt_ctx->error))) { + wt_ctx->nr_bvecs =0 ; +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 ; + +if (!written) + goto put_folio; + + off_aligned =round_down (offset, bs); + len_aligned =round_up (offset + written, bs) - off_aligned; + +iomap_folio_prepare_writethrough (folio, off_aligned, + len_aligned); + +if (!wt_ctx->nr_bvecs) + wt_ctx->bio_pos =round_down (pos, bs); + +bvec_set_folio (&wt_ctx->bvec[wt_ctx->nr_bvecs], folio, + len_aligned, off_aligned); + wt_ctx->nr_bvecs++; + wt_ctx->written += written; + +if (pos + written > wt_ctx->new_i_size) + wt_ctx->new_i_size = pos + written; + +if (wt_ctx->nr_bvecs == wt_ctx->max_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +put_folio: +__iomap_put_folio (iter, wt_ops->write_ops, written, folio); + +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; +iomap_iter_advance (iter, written); + } + }while (iov_iter_count (i) &&iomap_length (iter)); + +if (wt_ctx->nr_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +return total_written ?0 : status; +} +static int iomap_write_iter (struct iomap_iter *iter,struct iov_iter *i,const struct iomap_write_ops *write_ops) {
@@ -1232,6 +1503,87 @@ iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i, }EXPORT_SYMBOL_GPL (iomap_file_buffered_write); +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; + + +/* +* For now we don't support any other flag with WRITETHROUGH +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) +return -EINVAL ; +if (iocb->ki_flags & (IOCB_NOWAIT |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 +*/ + max_bvecs =DIV_ROUND_UP ( +iov_iter_count (i), +PAGE_SIZE <<mapping_min_folio_order (inode->i_mapping)) +1 ; + +if (max_bvecs >BIO_MAX_VECS ) + max_bvecs =BIO_MAX_VECS ; +if (!max_bvecs) + max_bvecs =1 ; + + 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->dops = wt_ops->dops; + wt_ctx->pos = iocb->ki_pos; + wt_ctx->new_i_size =i_size_read (inode); + wt_ctx->max_bvecs = max_bvecs; +atomic_set (&wt_ctx->ref,1 ); + wt_ctx->waiter = current; + +mapping_set_stable_writes (inode->i_mapping); + +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); + } +if (ret <0 ) +cmpxchg (&wt_ctx->error,0 , ret); + +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 ); + } + +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 +7 -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)
@@ -1985,6 +1986,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 *,
@@ -3434,6 +3437,10 @@ 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 ; kiocb_flags |= (__force int ) (flags &RWF_SUPPORTED );if (flags &RWF_SYNC ) kiocb_flags |=IOCB_DSYNC ;
include/linux/iomap.h +38 -0
@@ -209,6 +209,7 @@ struct iomap_write_ops {#endif /* CONFIG_FS_DAX */ #define IOMAP_ATOMIC (1 << 9)/* torn-write protection */ #define IOMAP_DONTCACHE (1 << 10) +#define IOMAP_WRITETHROUGH (1 << 11)struct iomap_ops {/*
@@ -475,6 +476,27 @@ struct iomap_writepage_ctx {void *wb_ctx;/* pending writeback context */ }; +struct iomap_writethrough_ctx { +struct kiocb *iocb; +const struct iomap_dio_ops *dops; +struct inode *inode; +loff_t new_i_size; +loff_t pos; +size_t written; +atomic_t ref; +unsigned int flags; +int error; + +/* used during submission and for non-aio completion */ +struct task_struct *waiter; + +loff_t bio_pos; +unsigned int nr_bvecs; +unsigned int max_bvecs; +struct bio_vec bvec[]; + +}; +struct iomap_ioend *iomap_init_ioend (struct inode *inode,struct bio *bio,loff_t file_offset,u16 ioend_flags); struct iomap_ioend *iomap_split_ioend(struct iomap_ioend *ioend,
@@ -599,6 +621,22 @@ struct iomap_dio *__iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,ssize_t iomap_dio_complete (struct iomap_dio *dio);void iomap_dio_bio_end_io (struct bio *bio); +/* +* In writethrough, we copy user data to folio first and then send the folio +* to writeback via dio path. To achieve this, we need callbacks from iomap_ops, +* iomap_write_ops and iomap_dio_ops. This struct packs them together. +*/ +struct iomap_writethrough_ops { +const struct iomap_ops *ops; +const struct iomap_write_ops *write_ops; +const struct iomap_dio_ops *dops; +int (*writethrough_submit)(struct inode *inode,struct iomap *iomap, +loff_t offset,u64 len); +}; +ssize_t iomap_file_writethrough_write (struct kiocb *iocb,struct iov_iter *i, +const struct iomap_writethrough_ops *wt_ops, +void *private); +#ifdef CONFIG_SWAP struct file ;struct swap_info_struct ;
include/linux/pagemap.h +1 -0
@@ -1260,6 +1260,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
@@ -435,10 +435,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 +6 -0
@@ -2918,6 +2918,12 @@ bool folio_clear_dirty_for_io(struct folio *folio) }EXPORT_SYMBOL (folio_clear_dirty_for_io); +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);
Signature · 1 line
2.53.0
Jan Kara
maintainerRe: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 33 lines · Ojaswin Mujoo
This adds initial support for performing buffered non-aio
RWF_WRITETHROUGHwrite. 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 theiomap_iter()loop. For each iomap:
3.1. Acquire folio andfolio_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 towt_ctx->bvecandfolio_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 delibrately open coded the IO submission and completion flow (inspired by dio) rather than reusing the dio functions as accomodating buffered writethrough logic in dio code was polluting it with too many if else conditionals and special cases.
Quoted 5 lines
Suggested-by Jan Kara <[email protected]>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]>
Overall this looks good to me. Just a few smaller things below:
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1249
Quoted 13 lines
fs/iomap/buffered-io.c +13
-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 ; +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 DIO handles these flags */
^ missing 'how' here
Quoted 28 lines
fs/iomap/buffered-io.c +28
-0+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; + + +/* +* For now we don't support any other flag with WRITETHROUGH +*/ +if (!(iocb->ki_flags &IOCB_WRITETHROUGH )) +return -EINVAL ; +if (iocb->ki_flags & (IOCB_NOWAIT |IOCB_DONTCACHE )) +return -EINVAL ; +if (iocb_is_dsync (iocb)) +/* D_SYNC support not implemented yet */ +return -EOPNOTSUPP ; +if (!is_sync_kiocb (iocb))
fs/iomap/buffered-io.c iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i, · line 1543
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+/* aio support not implemented yet */ +return -EOPNOTSUPP ; + +/* +* +1 to max bvecs to account for unaligned write spanning multiple +* folios +*/ + max_bvecs =DIV_ROUND_UP ( +iov_iter_count (i), +PAGE_SIZE <<mapping_min_folio_order (inode->i_mapping)) +1 ;
Can this overflow? iov_iter_count() returns size_t which is ulong.
fs/iomap/buffered-io.c iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i, · line 1548
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+ +if (max_bvecs >BIO_MAX_VECS ) + max_bvecs =BIO_MAX_VECS ; +if (!max_bvecs) + max_bvecs =1 ;
I don't think 0 is possible here since we do +1 in max_bvecs computation above.
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+ + wt_ctx =kzalloc (struct_size (wt_ctx, bvec, max_bvecs),GFP_NOFS ); +if (!wt_ctx) +return -ENOMEM ; + + wt_ctx->iocb = iocb;
fs/iomap/buffered-io.c iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i, · line 1564
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ wt_ctx->inode = inode; + wt_ctx->dops = wt_ops->dops; + wt_ctx->pos = iocb->ki_pos; + wt_ctx->new_i_size =i_size_read (inode); + wt_ctx->max_bvecs = max_bvecs; +atomic_set (&wt_ctx->ref,1 ); + wt_ctx->waiter = current; + +mapping_set_stable_writes (inode->i_mapping); +
We should check if mapping is already marked as requiring stable pages avoid messing with (in particular clearing) the flag in that case.
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+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); + } +if (ret <0 ) +cmpxchg (&wt_ctx->error,0 , ret); + +if (!atomic_dec_and_test (&wt_ctx->ref)) { +for (;;) {
fs/iomap/buffered-io.c iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i, · line 1584
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+set_current_state (TASK_UNINTERRUPTIBLE ); +if (!READ_ONCE (wt_ctx->waiter)) +break ; +blk_io_schedule (); + } +__set_current_state (TASK_RUNNING ); + } + +return iomap_writethrough_complete (wt_ctx); +}
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Jan Kara
maintainerRe: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
In reply to Ojaswin MujooSome more thoughs... :)
Quoted 4 lines
fs/iomap/buffered-io.c +0
-0@@ -1096,6 +1097,276 @@ 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); }
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1109
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+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->dops && wt_ctx->dops->end_io) { +int err = wt_ctx->dops->end_io (iocb, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags);
It's a bit odd to use only ->end_io from dops also because we don't really use direct IO submission path. So perhaps you can have just an end_io handler pointer in iomap_writethrough_ops similarly as you have a submission one?
Quoted 14 lines
fs/iomap/buffered-io.c +14
-0+if (err) + ret = err; + } + +mapping_clear_stable_writes (inode->i_mapping); + +if (!ret) { + ret = wt_ctx->written; + iocb->ki_pos = wt_ctx->pos + ret; + } + +kfree (wt_ctx); +return ret; +}
...
Quoted 88 lines
fs/iomap/buffered-io.c +88
-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 ; +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 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 ; + + do { + struct folio *folio; + size_t offset; /* Offset into folio */ + 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; + + 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 and don't bother submitting more bios. + */ + if (unlikely(data_race(wt_ctx->error))) { + wt_ctx->nr_bvecs = 0; + 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; + + if (!written) + goto put_folio; + + off_aligned = round_down(offset, bs);
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1334
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ len_aligned =round_up (offset + written, bs) - off_aligned; + +iomap_folio_prepare_writethrough (folio, off_aligned, + len_aligned); + +if (!wt_ctx->nr_bvecs) + wt_ctx->bio_pos =round_down (pos, bs); + +bvec_set_folio (&wt_ctx->bvec[wt_ctx->nr_bvecs], folio, + len_aligned, off_aligned);
Shouldn't we zero out the tail of the folio if we are submitting partial folio for write?
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1339
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+ wt_ctx->nr_bvecs++; + wt_ctx->written += written; + +if (pos + written > wt_ctx->new_i_size) + wt_ctx->new_i_size = pos + written;
I'm probably missing something here but where is i_size update handled? I don't see new_i_size used anywhere? Also why is it OK to not call pagecache_isize_extended() but that goes with the i_size update...
Quoted 13 lines
fs/iomap/buffered-io.c +13
-0+ +if (wt_ctx->nr_bvecs == wt_ctx->max_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +put_folio: +__iomap_put_folio (iter, wt_ops->write_ops, written, folio); + +cond_resched (); +if (unlikely (written ==0 )) { +iomap_write_failed (iter->inode, pos, bytes); +iov_iter_revert (i, copied); + +if (chunk >PAGE_SIZE )
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1362
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ chunk /=2 ; +if (copied) { + bytes = copied; + goto retry; + } + }else { + total_written += written; +iomap_iter_advance (iter, written); + } + }while (iov_iter_count (i) &&iomap_length (iter));
Overall the differences of this function from iomap_write_iter() seem relatively small so maybe it would be possible to just extend iomap_write_iter() to support writethrough IO as well? Basically once we've copied data into the folio and called iomap_write_end() we can have "if writethrough, call function to prepare & submit the folio for IO".
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1369
Quoted 7 lines
fs/iomap/buffered-io.c +7
-0+ +if (wt_ctx->nr_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +return total_written ?0 : status; +} +
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 1 line · Jan Kara
Some more thoughs... :)
Hi Jan,
Thanks for the review!
Quoted 5 lines
Quoted 5 lines
fs/iomap/buffered-io.c +0
-0@@ -1096,6 +1097,276 @@ 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); }
Quoted 15 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1109
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+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->dops && wt_ctx->dops->end_io) { +int err = wt_ctx->dops->end_io (iocb, wt_ctx->written, + wt_ctx->error, + wt_ctx->flags);It's a bit odd to use only ->end_io from dops also because we don't really use direct IO submission path. So perhaps you can have just an
end_iohandler pointer iniomap_writethrough_opssimilarly as you have a submission one?
Yes makes sense, I can change that in v3.
Quoted 111 lines
Quoted 15 lines
fs/iomap/buffered-io.c +14
-0+if (err) + ret = err; + } + +mapping_clear_stable_writes (inode->i_mapping); + +if (!ret) { + ret = wt_ctx->written; + iocb->ki_pos = wt_ctx->pos + ret; + } + +kfree (wt_ctx); +return ret; +}...
Quoted 93 lines
fs/iomap/buffered-io.c +93
-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 ; +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 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 ; + + do { + struct folio *folio; + size_t offset; /* Offset into folio */ + 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; + + 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 and don't bother submitting more bios. + */ + if (unlikely(data_race(wt_ctx->error))) { + wt_ctx->nr_bvecs = 0; + 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; + + if (!written) + goto put_folio; + + off_aligned = round_down(offset, bs); + len_aligned = round_up(offset + written, bs) - off_aligned; + + iomap_folio_prepare_writethrough(folio, off_aligned, + len_aligned); +
Quoted 8 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1334
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+if (!wt_ctx->nr_bvecs) + wt_ctx->bio_pos =round_down (pos, bs); + +bvec_set_folio (&wt_ctx->bvec[wt_ctx->nr_bvecs], folio, + len_aligned, off_aligned);Shouldn't we zero out the tail of the folio if we are submitting partial folio for write?
Hmm, so for the folio range we zeroout if needed in __iomap_write_begin(). I think that should take care of this right?
Quoted 9 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1339
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0+ wt_ctx->nr_bvecs++; + wt_ctx->written += written; + +if (pos + written > wt_ctx->new_i_size) + wt_ctx->new_i_size = pos + written;I'm probably missing something here but where is
i_sizeupdate handled? I don't seenew_i_sizeused anywhere?
So the i_size update happens in endio(), similar to dio. We initially had the update in iomap_writethrough_iter in v1 however based on Dave's feedback [1], moved it to the endio. The idea is for writethrough semantics to be closer to dio hence we either update isize when we succeffuly write, or return an error to user without update isize.
[1] https://lore.kernel.org/linux-fsdevel/aa--rBKQG7ck5nuM@dread/
Quoted 2 lines
Also why is it OK to not call
pagecache_isize_extended()but that goes with thei_sizeupdate...
As for pagecache_isize_extended(), (this might be a bit tangential from your comment but) after this email, I started diggin a bit more into why it is needed. As per my understanding, it tackles 2 things:
Problem 1. mkclean's the old EOF folio so that the FS can fault again. This allows us to allocate new blocks which previously might not be allocated if bs < ps.
Problem 2. Since mmap writes can dirty data beyond EOF, we zero the range from old EOF to end of that folio so that readers dont read junk data after isize extension.
Another thing I noticed is that most users of iomap_file_buffered_write() do their own eof zeroing in the FS layer (eg, xfs_file_write_zero_eof(), ext4's new changes, ntfs_extend_initialized_size() etc).
I think this FS level zerooing should take care of mkcleaning the eof folio (problem 1), as they call iomap_zero_range() which would flush the eof range anyways. So am I right in assuming that for FSes that do their own zeroing, 1. is already taken care of?
As for 2, I think after the EOF zeroing of the FS, there might be a window before iomap_write_iter() where an mmap writer can still dirty EOF blocks, hence the pagecache_isize_extended() would be needed here. But doesn't that then make the eof zeroing in the FS layer redundant? Am I missing something here?
Regardless, for our case I think we will also need to do the pagecache_isize_extended(), mainly to take care of problem 2, but where exactly should we do it now? We currently change the isize in endio() but for aio, it can run outside inode or folio lock. I think this function needs to be called under inode lock(). Hmm.. its a bit late here so I'll revisit this tomorrow with a fresh mind :)
Quoted 22 lines
Quoted 22 lines
fs/iomap/buffered-io.c +21
-0+ +if (wt_ctx->nr_bvecs == wt_ctx->max_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +put_folio: +__iomap_put_folio (iter, wt_ops->write_ops, written, folio); + +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; +iomap_iter_advance (iter, written);
Quoted 8 lines
Review
Quoted 2 lines
(quoted diff) +2
-0+ } + } while (iov_iter_count(i) && iomap_length(iter));Overall the differences of this function from
iomap_write_iter()seem relatively small so maybe it would be possible to just extendiomap_write_iter()to support writethrough IO as well? Basically once we've copied data into the folio and callediomap_write_end()we can have "if writethrough, call function to prepare & submit the folio for IO".
Right, we ideally want to avoid as much duplication of the code as possible, but in v1 where we did use iomap_write_iter() and iomap_dio_rw(), we ended up with a lot of if else logic making it messy.
I think just using iomap_write_iter() with writethrough specific IO submission functions is worth a try though, but Im not sure how ugly passing the extra writethrough params will look there :) Anyways, thanks for the suggesting, I'll give it a try and see.
Quoted 13 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1369
Quoted 8 lines
fs/iomap/buffered-io.c +7
-0+ +if (wt_ctx->nr_bvecs) +iomap_writethrough_submit_bio (wt_ctx, &iter->iomap, wt_ops); + +return total_written ?0 : status; +} +Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Jan Kara
maintainerRe: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 104 lines · Ojaswin Mujoo
Quoted 94 lines · Jan Kara
Quoted 94 lines
fs/iomap/buffered-io.c +93
-0@@ -1096,6 +1097,276 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, +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 ; +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 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 ; + + do { + struct folio *folio; + size_t offset; /* Offset into folio */ + 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; + + 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 and don't bother submitting more bios. + */ + if (unlikely(data_race(wt_ctx->error))) { + wt_ctx->nr_bvecs = 0; + 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; + + if (!written) + goto put_folio; + + off_aligned = round_down(offset, bs); + len_aligned = round_up(offset + written, bs) - off_aligned; + + iomap_folio_prepare_writethrough(folio, off_aligned, + len_aligned); +Quoted 8 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1334
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+if (!wt_ctx->nr_bvecs) + wt_ctx->bio_pos =round_down (pos, bs); + +bvec_set_folio (&wt_ctx->bvec[wt_ctx->nr_bvecs], folio, + len_aligned, off_aligned);Shouldn't we zero out the tail of the folio if we are submitting partial folio for write?
Quoted 2 lines
Hmm, so for the folio range we zeroout if needed in
__iomap_write_begin(). I think that should take care of this right?
Yeah, right, that seems to do it.
Quoted 24 lines
Quoted 8 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1339
Quoted 5 lines
fs/iomap/buffered-io.c +5
-0+ wt_ctx->nr_bvecs++; + wt_ctx->written += written; + +if (pos + written > wt_ctx->new_i_size) + wt_ctx->new_i_size = pos + written;I'm probably missing something here but where is
i_sizeupdate handled? I don't seenew_i_sizeused anywhere?So the
i_sizeupdate happens inendio(), similar to dio. We initially had the update iniomap_writethrough_iterin v1 however based on Dave's feedback [1], moved it to the endio. The idea is for writethrough semantics to be closer to dio hence we either update isize when we succeffuly write, or return an error to user without update isize.[1] https://lore.kernel.org/linux-fsdevel/aa--rBKQG7ck5nuM@dread/
Quoted 2 lines
Also why is it OK to not call
pagecache_isize_extended()but that goes with thei_sizeupdate...As for
pagecache_isize_extended(), (this might be a bit tangential from your comment but) after this email, I started diggin a bit more into why it is needed. As per my understanding, it tackles 2 things:
Quoted 7 lines
Problem 1. mkclean's the old EOF folio so that the FS can fault again. This allows us to allocate new blocks which previously might not be allocated if bs < ps.
Problem 2. Since mmap writes can dirty data beyond EOF, we zero the range from old EOF to end of that folio so that readers dont read junk data after isize extension.
Correct.
Quoted 8 lines
Another thing I noticed is that most users of
iomap_file_buffered_write()do their own eof zeroing in the FS layer (eg,xfs_file_write_zero_eof(), ext4's new changes,ntfs_extend_initialized_size()etc).
I think this FS level zerooing should take care of mkcleaning the eof folio (problem 1), as they calliomap_zero_range()which would flush the eof range anyways. So am I right in assuming that for FSes that do their own zeroing, 1. is already taken care of?
Well, I don't see anything that would writeprotect the old tail page in iomap_zero_range(). I think iomap_zero_range() calls are there mostly to address 2. Not only due to mmap but also possibly to clear whatever junk there can be in the blocks after EOF.
Quoted 5 lines
As for 2, I think after the EOF zeroing of the FS, there might be a window before
iomap_write_iter()where an mmap writer can still dirty EOF blocks, hence thepagecache_isize_extended()would be needed here. But doesn't that then make the eof zeroing in the FS layer redundant? Am I missing something here?
Hmm, I agree the zeroing looks duplicit (for some users of pagecache_isize_extended()). And yes, doing the zeroing from xfs_file_write_zero_eof() is somewhat racy (mmap writer can still come and write non-zeros before we update i_size) but I'd have hard time to argue it really practically matters - you are racing mmap writes with buffered writes so any kind of write atomicity guarantees are not there.
Quoted 6 lines
Regardless, for our case I think we will also need to do the
pagecache_isize_extended(), mainly to take care of problem 2, but where exactly should we do it now? We currently change the isize inendio()but for aio, it can run outside inode or folio lock. I think this function needs to be called under inodelock(). Hmm.. its a bit late here so I'll revisit this tomorrow with a fresh mind :)
I think mainly to take care of problem 1... You are correct about inode_lock but since we are updating i_size, we should be better holding it, shouldn't we?
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 8 lines · Jan Kara
Quoted 7 lines · Ojaswin Mujoo
Quoted 6 lines · Jan Kara
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0@@ -1096,6 +1097,276 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, +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) + +{
<...>
Quoted 22 lines
Quoted 10 lines
your comment but) after this email, I started diggin a bit more into why it is needed. As per my understanding, it tackles 2 things:
Problem 1. mkclean's the old EOF folio so that the FS can fault again. This allows us to allocate new blocks which previously might not be allocated if bs < ps.
Problem 2. Since mmap writes can dirty data beyond EOF, we zero the range from old EOF to end of that folio so that readers dont read junk data after isize extension.
Correct.
Quoted 8 lines
Another thing I noticed is that most users of
iomap_file_buffered_write()do their own eof zeroing in the FS layer (eg,xfs_file_write_zero_eof(), ext4's new changes,ntfs_extend_initialized_size()etc).
I think this FS level zerooing should take care of mkcleaning the eof folio (problem 1), as they calliomap_zero_range()which would flush the eof range anyways. So am I right in assuming that for FSes that do their own zeroing, 1. is already taken care of?
Quoted 4 lines
Well, I don't see anything that would writeprotect the old tail page in
iomap_zero_range(). I thinkiomap_zero_range()calls are there mostly to address 2. Not only due to mmap but also possibly to clear whatever junk there can be in the blocks after EOF.
Well I was thinking more like if the EOF page was mmap'd it would be dirty and blocks beyond EOF would be unmapped, so iomap_zero_range() will write it back which shall mkclean() the folio.
But I think the same race we discussed for problem 2 can also occur here.
Thread 1 (extending write) Thread 2 (mmap writer)
iomap_zero_range()filemap_write_and_wait_range()
// mmaps & writes EOF range
iomap_write_iter()
isize = new_size
// pagecache_isize_extended() is
needed to mkclean() old EOF page.
Quoted 7 lines
Quoted 6 lines
As for 2, I think after the EOF zeroing of the FS, there might be a window before
iomap_write_iter()where an mmap writer can still dirty EOF blocks, hence thepagecache_isize_extended()would be needed here. But doesn't that then make the eof zeroing in the FS layer redundant? Am I missing something here?
Quoted 6 lines
Hmm, I agree the zeroing looks duplicit (for some users of
pagecache_isize_extended()). And yes, doing the zeroing fromxfs_file_write_zero_eof()is somewhat racy (mmap writer can still come and write non-zeros before we updatei_size) but I'd have hard time to argue it really practically matters - you are racing mmap writes with buffered writes so any kind of write atomicity guarantees are not there.
Yeah, seems like it is not enough to take care of either 1 or 2 and pagecache_isize_extended() should maybe be enough. I was just wondering if we could optimize it away even for normal extend path (no racing mmap), we can avoid the expensive folio_zero_range() calls.
Regardless, Ive not looked at this more closely and its a separate issue so we can revisit it later. For now I wanted some clarity around pagecache_isize_extended() so thanks for that.
Quoted 11 lines
Quoted 7 lines
Regardless, for our case I think we will also need to do the
pagecache_isize_extended(), mainly to take care of problem 2, but where exactly should we do it now? We currently change the isize inendio()but for aio, it can run outside inode or folio lock. I think this function needs to be called under inodelock(). Hmm.. its a bit late here so I'll revisit this tomorrow with a fresh mind :)I think mainly to take care of problem 1... You are correct about
inode_lockbut since we are updatingi_size, we should be better holding it, shouldn't we?
Yes you are correct. In the aio writethrough codepath, the inode update is happening without the inode lock which is wrong. I overlooked the fact that even aio dio uses IOMAP_DIO_FORCE_WAIT to force isize update under inode lock, and we should do something similar as well.
So in v3, I make the change that for extending writes we shall always finish them in "sync" fashion so ->endio() runs under inode lock. Then, after ->endio() in iomap_dio_complete(), I will call pagecache_isize_extended() to take care of this. Just like isize update right now, the isize_extension only runs when the IO was successful otherwise we return an error to the user. This gives us semantics like dio while handling extension properly.
Does that sound okay?
Regards,
ojaswin
Quoted 5 lines
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Jan Kara
maintainerRe: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 48 lines · Ojaswin Mujoo
Quoted 8 lines · Jan Kara
Quoted 7 lines · Ojaswin Mujoo
Quoted 6 lines · Jan Kara
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0@@ -1096,6 +1097,276 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, +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) + +{<...>
Quoted 22 lines
Quoted 10 lines
your comment but) after this email, I started diggin a bit more into why it is needed. As per my understanding, it tackles 2 things:
Problem 1. mkclean's the old EOF folio so that the FS can fault again. This allows us to allocate new blocks which previously might not be allocated if bs < ps.
Problem 2. Since mmap writes can dirty data beyond EOF, we zero the range from old EOF to end of that folio so that readers dont read junk data after isize extension.
Correct.
Quoted 8 lines
Another thing I noticed is that most users of
iomap_file_buffered_write()do their own eof zeroing in the FS layer (eg,xfs_file_write_zero_eof(), ext4's new changes,ntfs_extend_initialized_size()etc).
I think this FS level zerooing should take care of mkcleaning the eof folio (problem 1), as they calliomap_zero_range()which would flush the eof range anyways. So am I right in assuming that for FSes that do their own zeroing, 1. is already taken care of?Quoted 4 lines
Well, I don't see anything that would writeprotect the old tail page in
iomap_zero_range(). I thinkiomap_zero_range()calls are there mostly to address 2. Not only due to mmap but also possibly to clear whatever junk there can be in the blocks after EOF.Well I was thinking more like if the EOF page was mmap'd it would be dirty and blocks beyond EOF would be unmapped, so
iomap_zero_range()will write it back which shallmkclean()the folio.But I think the same race we discussed for problem 2 can also occur here.
Thread 1 (extending write) Thread 2 (mmap writer)
Quoted 7 lines
iomap_zero_range()filemap_write_and_wait_range()// mmaps & writes EOF range
iomap_write_iter()
isize =new_size
//pagecache_isize_extended()is
needed tomkclean()old EOF page.
Yes, this race exists and unlike in the case of zeroing where it is mostly harmless not guranteeing calling page_mkwrite() with updated i_size can lead to filesystem tripping on assertions, data loss or similar.
Quoted 13 lines
Quoted 6 lines
Quoted 5 lines
As for 2, I think after the EOF zeroing of the FS, there might be a window before
iomap_write_iter()where an mmap writer can still dirty EOF blocks, hence thepagecache_isize_extended()would be needed here. But doesn't that then make the eof zeroing in the FS layer redundant? Am I missing something here?Quoted 6 lines
Hmm, I agree the zeroing looks duplicit (for some users of
pagecache_isize_extended()). And yes, doing the zeroing fromxfs_file_write_zero_eof()is somewhat racy (mmap writer can still come and write non-zeros before we updatei_size) but I'd have hard time to argue it really practically matters - you are racing mmap writes with buffered writes so any kind of write atomicity guarantees are not there.
Quoted 8 lines
Yeah, seems like it is not enough to take care of either 1 or 2 and
pagecache_isize_extended()should maybe be enough. I was just wondering if we could optimize it away even for normal extend path (no racing mmap), we can avoid the expensivefolio_zero_range()calls.Regardless, Ive not looked at this more closely and its a separate issue so we can revisit it later. For now I wanted some clarity around
pagecache_isize_extended()so thanks for that.
Well, but pagecache_isize_extended() doesn't guarantee on disk blocks are zeroed out as well as it doesn't dirty the page. Also xfs_file_write_zero_eof() potentially handles zeroing of more than a tail page. So you cannot simply drop one of these.
Quoted 7 lines
Quoted 7 lines
Quoted 6 lines
Regardless, for our case I think we will also need to do the
pagecache_isize_extended(), mainly to take care of problem 2, but where exactly should we do it now? We currently change the isize inendio()but for aio, it can run outside inode or folio lock. I think this function needs to be called under inodelock(). Hmm.. its a bit late here so I'll revisit this tomorrow with a fresh mind :)
Quoted 8 lines
Quoted 3 lines
I think mainly to take care of problem 1... You are correct about
inode_lockbut since we are updatingi_size, we should be better holding it, shouldn't we?Yes you are correct. In the aio writethrough codepath, the inode update is happening without the inode lock which is wrong. I overlooked the fact that even aio dio uses
IOMAP_DIO_FORCE_WAITto force isize update under inode lock, and we should do something similar as well.
Yes.
Quoted 9 lines
So in v3, I make the change that for extending writes we shall always finish them in "sync" fashion so ->endio() runs under inode lock. Then, after ->endio() in
iomap_dio_complete(), I will callpagecache_isize_extended()to take care of this. Just like isize update right now, theisize_extensiononly runs when the IO was successful otherwise we return an error to the user. This gives us semantics like dio while handling extension properly.Does that sound okay?
Yep, sounds fine.
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 57 lines · Jan Kara
Quoted 48 lines · Ojaswin Mujoo
Quoted 8 lines · Jan Kara
Quoted 7 lines · Ojaswin Mujoo
Quoted 6 lines · Jan Kara
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0@@ -1096,6 +1097,276 @@ static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, +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) + +{<...>
Quoted 22 lines
Quoted 10 lines
your comment but) after this email, I started diggin a bit more into why it is needed. As per my understanding, it tackles 2 things:
Problem 1. mkclean's the old EOF folio so that the FS can fault again. This allows us to allocate new blocks which previously might not be allocated if bs < ps.
Problem 2. Since mmap writes can dirty data beyond EOF, we zero the range from old EOF to end of that folio so that readers dont read junk data after isize extension.
Correct.
Quoted 8 lines
Another thing I noticed is that most users of
iomap_file_buffered_write()do their own eof zeroing in the FS layer (eg,xfs_file_write_zero_eof(), ext4's new changes,ntfs_extend_initialized_size()etc).
I think this FS level zerooing should take care of mkcleaning the eof folio (problem 1), as they calliomap_zero_range()which would flush the eof range anyways. So am I right in assuming that for FSes that do their own zeroing, 1. is already taken care of?Quoted 4 lines
Well, I don't see anything that would writeprotect the old tail page in
iomap_zero_range(). I thinkiomap_zero_range()calls are there mostly to address 2. Not only due to mmap but also possibly to clear whatever junk there can be in the blocks after EOF.Well I was thinking more like if the EOF page was mmap'd it would be dirty and blocks beyond EOF would be unmapped, so
iomap_zero_range()will write it back which shallmkclean()the folio.But I think the same race we discussed for problem 2 can also occur here.
Thread 1 (extending write) Thread 2 (mmap writer)
Quoted 7 lines
iomap_zero_range()filemap_write_and_wait_range()// mmaps & writes EOF range
iomap_write_iter()
isize =new_size
//pagecache_isize_extended()is
needed tomkclean()old EOF page.
Quoted 3 lines
Yes, this race exists and unlike in the case of zeroing where it is mostly harmless not guranteeing calling
page_mkwrite()with updatedi_sizecan lead to filesystem tripping on assertions, data loss or similar.
Right.
Quoted 19 lines
Quoted 19 lines
Quoted 7 lines
Quoted 6 lines
As for 2, I think after the EOF zeroing of the FS, there might be a window before
iomap_write_iter()where an mmap writer can still dirty EOF blocks, hence thepagecache_isize_extended()would be needed here. But doesn't that then make the eof zeroing in the FS layer redundant? Am I missing something here?Quoted 6 lines
Hmm, I agree the zeroing looks duplicit (for some users of
pagecache_isize_extended()). And yes, doing the zeroing fromxfs_file_write_zero_eof()is somewhat racy (mmap writer can still come and write non-zeros before we updatei_size) but I'd have hard time to argue it really practically matters - you are racing mmap writes with buffered writes so any kind of write atomicity guarantees are not there.Yeah, seems like it is not enough to take care of either 1 or 2 and
pagecache_isize_extended()should maybe be enough. I was just wondering if we could optimize it away even for normal extend path (no racing mmap), we can avoid the expensivefolio_zero_range()calls.
Quoted 8 lines
Quoted 3 lines
Regardless, Ive not looked at this more closely and its a separate issue so we can revisit it later. For now I wanted some clarity around
pagecache_isize_extended()so thanks for that.Well, but
pagecache_isize_extended()doesn't guarantee on disk blocks are zeroed out as well as it doesn't dirty the page. Alsoxfs_file_write_zero_eof()potentially handles zeroing of more than a tail page. So you cannot simply drop one of these.
Hmm yea true, xfs_file_write_zero_eof() does take of zeroing any mapped eof blocks on disk as well.
Quoted 12 lines
Quoted 12 lines
Quoted 11 lines
Quoted 7 lines
Regardless, for our case I think we will also need to do the
pagecache_isize_extended(), mainly to take care of problem 2, but where exactly should we do it now? We currently change the isize inendio()but for aio, it can run outside inode or folio lock. I think this function needs to be called under inodelock(). Hmm.. its a bit late here so I'll revisit this tomorrow with a fresh mind :)I think mainly to take care of problem 1... You are correct about
inode_lockbut since we are updatingi_size, we should be better holding it, shouldn't we?
Quoted 10 lines
Quoted 4 lines
Yes you are correct. In the aio writethrough codepath, the inode update is happening without the inode lock which is wrong. I overlooked the fact that even aio dio uses
IOMAP_DIO_FORCE_WAITto force isize update under inode lock, and we should do something similar as well.Yes.
Quoted 3 lines
So in v3, I make the change that for extending writes we shall always finish them in "sync" fashion so ->endio() runs under inode lock. Then, after ->endio() in
iomap_dio_complete(), I will call
( I meant iomap_writethrough_complete(), here)
Quoted 8 lines
Quoted 6 lines
pagecache_isize_extended()to take care of this. Just like isize update right now, theisize_extensiononly runs when the IO was successful otherwise we return an error to the user. This gives us semantics like dio while handling extension properly.Does that sound okay?
Yep, sounds fine.
Got it, thanks for the review Jan!
Regards,
ojaswin
Quoted 5 lines
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
In reply to Ojaswin Mujoo<snip>
Quoted 3 lines
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
<snip>
Quoted 25 lines
fs/iomap/buffered-io.c +25
-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); +
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1217
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+/* +* 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);
I get the point where you want to prevent mmap writes before the current writeback completes. It is not very obvious to me why folio_mkclean and folio_mark_dirty had to be moved out and then we call the folio_clear_dirty_for_writethrough wrapper. Anyway those calls are made after we do folio_wait_writeback right?
Is to cover the cases where we do partial write in which case we don't end up calling folio_clear_dirty_for_io? If that is the case, then a small comment on top of folio_mkclean would make it a bit more clear.
--
Pankaj
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 41 lines · Pankaj Raghav (Samsung)
<snip>
Quoted 3 lines
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<snip>
Quoted 25 lines
fs/iomap/buffered-io.c +25
-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); +fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1217
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+/* +* 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);
Quoted 8 lines
I get the point where you want to prevent mmap writes before the current writeback completes. It is not very obvious to me why
folio_mkcleanandfolio_mark_dirtyhad to be moved out and then we call thefolio_clear_dirty_for_writethroughwrapper. Anyway those calls are made after we dofolio_wait_writebackright?
Is to cover the cases where we do partial write in which case we don't end up callingfolio_clear_dirty_for_io? If that is the case, then a small comment on top offolio_mkcleanwould make it a bit more clear.
Hey Pankaj,
Yes you are right, we need to call folio_wait_writeback() and mkclean() for partial or complete writes. However, clearing of foliodirty bit and accounting shall only be done when the complete folio becomes clean, and hence we call folio_clear_dirty_for_writethrough() only for those cases.
I will add a more clear comment for this in v3.
Thanks,
Ojaswin
Quoted 3 lines
--
Pankaj
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
In reply to Ojaswin Mujoofs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1169
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+ +if (wt_ops->writethrough_submit) + wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, + len); + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,GFP_NOFS );
We might want to check if bio_alloc succeeded here.
Quoted 14 lines
fs/iomap/buffered-io.c +14
-0+ 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); + +atomic_inc (&wt_ctx->ref); +submit_bio (bio); + wt_ctx->nr_bvecs =0 ; +} +
<snip>
Quoted 50 lines
fs/iomap/buffered-io.c +50
-0+ +/** +* 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 ; +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 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 ; + + do { + struct folio *folio; + size_t offset; /* Offset into folio */ + 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; + + bytes = iov_iter_count(i); +retry: + offset = iter->pos & (chunk - 1); + bytes = min(chunk - offset, bytes);
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1280
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ status =balance_dirty_pages_ratelimited_flags (mapping, + bdp_flags); +if (unlikely (status)) +break ; + +/* +* If completions already occurred and reported errors, give up +* now and don't bother submitting more bios. +*/ +if (unlikely (data_race (wt_ctx->error))) {
In the unlikely scenario where we encounter an error, do we have to also clear the writeback flag on all the folios that is part of this bvec until now?
Something like explicitly iterate over wt_ctx->bvec[0] through wt_ctx->bvec[nr_bvecs - 1], manually call folio_end_writeback(bvec[i].bv_page) on them, and then discard the bvecs by setting the nr_bvecs = 0;
I am wondering if the folios that were processed until now will be in PG_WRITEBACK state which can affect reclaim as we never clear the flag.
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1284
Quoted 4 lines
fs/iomap/buffered-io.c +4
-0+ wt_ctx->nr_bvecs =0 ; +break ; + } +
--
Pankaj
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 8 lines · Pankaj Raghav (Samsung)
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1169
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+ +if (wt_ops->writethrough_submit) + wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, + len); + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,GFP_NOFS );We might want to check if
bio_allocsucceeded here.
Hi Pankaj, so we pass GFP_NOFS which has GFP_DIRECT_RECLAIM and according to comment over bio_alloc()
* If %__GFP_DIRECT_RECLAIM is set then bio_alloc will always be able to
* allocate a bio. This is due to the mempool guarantees. To make this work,
* callers must never allocate more than 1 bio at a time from the general pool.
And we seem to be following this.
Quoted 81 lines
Quoted 15 lines
fs/iomap/buffered-io.c +14
-0+ 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); + +atomic_inc (&wt_ctx->ref); +submit_bio (bio); + wt_ctx->nr_bvecs =0 ; +} +<snip>
Quoted 50 lines
fs/iomap/buffered-io.c +50
-0+ +/** +* 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 ; +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 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 ; + + do { + struct folio *folio; + size_t offset; /* Offset into folio */ + 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; + + bytes = iov_iter_count(i); +retry: + offset = iter->pos & (chunk - 1); + bytes = min(chunk - offset, bytes);fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1280
Quoted 10 lines
fs/iomap/buffered-io.c +10
-0+ status =balance_dirty_pages_ratelimited_flags (mapping, + bdp_flags); +if (unlikely (status)) +break ; + +/* +* If completions already occurred and reported errors, give up +* now and don't bother submitting more bios. +*/ +if (unlikely (data_race (wt_ctx->error))) {In the unlikely scenario where we encounter an error, do we have to also clear the writeback flag on all the folios that is part of this bvec until now?
Quoted 6 lines
Something like explicitly iterate over wt_ctx->bvec[0] through wt_ctx->bvec[nr_bvecs - 1], manually call
folio_end_writeback(bvec[i].bv_page)on them, and then discard the bvecs by setting thenr_bvecs= 0;I am wondering if the folios that were processed until now will be in
PG_WRITEBACKstate which can affect reclaim as we never clear the flag.
Hey Pankaj, yes you are right. I think the error handling is a bit buggy and Sashiko has also pointed some of these. I'll take care of this in v3, thanks for pointing this out.
Regards,
ojaswin
Quoted 8 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1284
Quoted 5 lines
fs/iomap/buffered-io.c +4
-0+ wt_ctx->nr_bvecs =0 ; +break ; + } +--
Pankaj
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 10 lines · Ojaswin Mujoo
Quoted 8 lines · Pankaj Raghav (Samsung)
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1169
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+ +if (wt_ops->writethrough_submit) + wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, + len); + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,GFP_NOFS );We might want to check if
bio_allocsucceeded here.
Quoted 9 lines
Hi Pankaj, so we pass
GFP_NOFSwhich hasGFP_DIRECT_RECLAIMand according to comment overbio_alloc()* If %__GFP_DIRECT_RECLAIM is set then
bio_allocwill always be able to
* allocate a bio. This is due to the mempool guarantees. To make this work,
* callers must never allocate more than 1 bio at a time from the general pool.And we seem to be following this.
Makes sense. Thanks for the clarification.
Quoted 14 lines
Quoted 14 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1174
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0+ 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++)In the unlikely scenario where we encounter an error, do we have to also clear the writeback flag on all the folios that is part of this bvec until now?
Something like explicitly iterate over wt_ctx->bvec[0] through wt_ctx->bvec[nr_bvecs - 1], manually call
folio_end_writeback(bvec[i].bv_page)on them, and then discard the bvecs by setting thenr_bvecs= 0;
Quoted 7 lines
Quoted 2 lines
I am wondering if the folios that were processed until now will be in
PG_WRITEBACKstate which can affect reclaim as we never clear the flag.Hey Pankaj, yes you are right. I think the error handling is a bit buggy and Sashiko has also pointed some of these. I'll take care of this in v3, thanks for pointing this out.
FWIW, I got the following panic on xfs/011 (not reproducible all the time) when I was running the xfstests with 16k block size with the writethrough patches:
[76313.736356] INFO: task fsstress:1845687 blocked for more than 122 seconds. [76313.738751] Not tainted 7.0.0-08885-g97cbd56b7479 #43 [76313.740650] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. [76313.743311] task:fsstress state:D stack:0 pid:1845687 tgid:1845687 ppid:1845685 task_flags:0x400140 flags:0x00080000 [76313.747137] Call Trace: [76313.748000] <TASK> [76313.748830] __schedule+0xcc2/0x3c40 [76313.750129] ? __pfx___schedule+0x10/0x10 [76313.751479] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.753214] schedule+0x78/0x2e0 [76313.754334] io_schedule+0x92/0x100 [76313.755597] folio_wait_bit_common+0x26a/0x6f0 [76313.757156] ? __pfx_folio_wait_bit_common+0x10/0x10 [76313.758873] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.760508] ? xas_load+0x19/0x260 [76313.761693] ? __pfx_wake_page_function+0x10/0x10 [76313.763386] ? __pfx_filemap_get_entry+0x10/0x10 [76313.764948] folio_wait_writeback+0x58/0x190 [76313.766499] __filemap_get_folio_mpol+0x56d/0x800 [76313.768085] ? kvm_read_and_reset_apf_flags+0x4a/0x70 [76313.769899] iomap_write_begin+0xea7/0x1e90 [76313.771304] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.773016] ? asm_exc_page_fault+0x22/0x30 [76313.774427] ? __pfx_iomap_write_begin+0x10/0x10 [76313.776100] ? fault_in_readable+0x80/0xe0 [76313.777476] ? __pfx_fault_in_readable+0x10/0x10 [76313.779106] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.780765] ? balance_dirty_pages_ratelimited_flags+0x549/0xcb0 [76313.782861] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.784457] ? fault_in_iov_iter_readable+0xe5/0x250 [76313.786221] iomap_file_writethrough_write+0x9fd/0x1ce0 [76313.787978] ? __pfx_iomap_file_writethrough_write+0x10/0x10 [76313.789991] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.791589] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.793314] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.794932] ? current_time+0x73/0x2b0 [76313.796132] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.797276] ? xfs_file_write_checks+0x420/0x900 [xfs] [76313.798786] xfs_file_buffered_write+0x195/0xae0 [xfs] [76313.800243] ? __pfx_xfs_file_buffered_write+0x10/0x10 [xfs] [76313.801775] ? kasan_save_track+0x14/0x40 [76313.802843] ? kasan_save_free_info+0x3b/0x70 [76313.803908] ? __kasan_slab_free+0x4f/0x80 [76313.804894] ? vfs_fstatat+0x55/0xa0 [76313.805835] ? __do_sys_newfstatat+0x7b/0xe0 [76313.806899] ? do_syscall_64+0x5b/0x540 [76313.807829] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.809052] ? xfs_file_write_iter+0x22e/0xa80 [xfs] [76313.810451] do_iter_readv_writev+0x453/0xa70
I have a feeling this has to do with the error handling as we are stuck waiting for writeback to complete. It is not reproducible because it might be dependent on the state of the system before this triggers. Let me see if I can find a way to reliably reproduce this so that we have something to verify against once we make these changes.
--
Pankaj
Re: [RFC PATCH v2 2/5] iomap: Add initial support for buffered RWF_WRITETHROUGH
Quoted 40 lines · Pankaj Raghav
Quoted 10 lines · Ojaswin Mujoo
Quoted 8 lines · Pankaj Raghav (Samsung)
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1169
Quoted 6 lines
fs/iomap/buffered-io.c +6
-0+ +if (wt_ops->writethrough_submit) + wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, + len); + + bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,GFP_NOFS );We might want to check if
bio_allocsucceeded here.Quoted 9 lines
Hi Pankaj, so we pass
GFP_NOFSwhich hasGFP_DIRECT_RECLAIMand according to comment overbio_alloc()* If %__GFP_DIRECT_RECLAIM is set then
bio_allocwill always be able to
* allocate a bio. This is due to the mempool guarantees. To make this work,
* callers must never allocate more than 1 bio at a time from the general pool.And we seem to be following this.
Makes sense. Thanks for the clarification.
Quoted 17 lines
Quoted 16 lines
fs/iomap/buffered-io.c static bool iomap_write_end(struct iomap_iter *iter, size_t len, size_t copied, · line 1174
Quoted 6 lines
fs/iomap/buffered-io.c +5
-0+ 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++)In the unlikely scenario where we encounter an error, do we have to also clear the writeback flag on all the folios that is part of this bvec until now?
Something like explicitly iterate over wt_ctx->bvec[0] through wt_ctx->bvec[nr_bvecs - 1], manually call
folio_end_writeback(bvec[i].bv_page)on them, and then discard the bvecs by setting thenr_bvecs= 0;I am wondering if the folios that were processed until now will be in
PG_WRITEBACKstate which can affect reclaim as we never clear the flag.
Quoted 8 lines
Quoted 4 lines
Hey Pankaj, yes you are right. I think the error handling is a bit buggy and Sashiko has also pointed some of these. I'll take care of this in v3, thanks for pointing this out.
FWIW, I got the following panic on xfs/011 (not reproducible all the time) when I was running the xfstests with 16k block size with the writethrough patches:
Good point. I don't think so we tested large blocksize path currently.
Ojaswin has been running fsx and fsstress and we didn't hit this scenario. So yes looks to be some corner path missed.
Thanks for testing that.
Quoted 25 lines
[76313.736356] INFO: task fsstress:1845687 blocked for more than 122 seconds. [76313.738751] Not tainted 7.0.0-08885-g97cbd56b7479 #43 [76313.740650] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. [76313.743311] task:fsstress state:D stack:0 pid:1845687 tgid:1845687 ppid:1845685 task_flags:0x400140 flags:0x00080000 [76313.747137] Call Trace: [76313.748000] <TASK> [76313.748830] __schedule+0xcc2/0x3c40 [76313.750129] ? __pfx___schedule+0x10/0x10 [76313.751479] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.753214] schedule+0x78/0x2e0 [76313.754334] io_schedule+0x92/0x100 [76313.755597] folio_wait_bit_common+0x26a/0x6f0 [76313.757156] ? __pfx_folio_wait_bit_common+0x10/0x10 [76313.758873] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.760508] ? xas_load+0x19/0x260 [76313.761693] ? __pfx_wake_page_function+0x10/0x10 [76313.763386] ? __pfx_filemap_get_entry+0x10/0x10 [76313.764948] folio_wait_writeback+0x58/0x190 [76313.766499] __filemap_get_folio_mpol+0x56d/0x800 [76313.768085] ? kvm_read_and_reset_apf_flags+0x4a/0x70 [76313.769899] iomap_write_begin+0xea7/0x1e90 [76313.771304] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.773016] ? asm_exc_page_fault+0x22/0x30
Quoted 8 lines
[76313.774427] ? __pfx_iomap_write_begin+0x10/0x10 [76313.776100] ? fault_in_readable+0x80/0xe0 [76313.777476] ? __pfx_fault_in_readable+0x10/0x10 [76313.779106] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.780765] ? balance_dirty_pages_ratelimited_flags+0x549/0xcb0 [76313.782861] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.784457] ? fault_in_iov_iter_readable+0xe5/0x250 [76313.786221] iomap_file_writethrough_write+0x9fd/0x1ce0
Looks like, while we were in iomap_writethrough_iter(), we ended up looping over the same folio twice w/o submitting the bio.
So this could be a short copy case (written < bytes). I guess, if we have a short copy, then too we should submit the prepared bio in iomap_writethrough_iter(), otherwise we will deadlock when we iterate over the same folio twice. (because previously we changed the folio state to writeback)
Quoted 19 lines
[76313.787978] ? __pfx_iomap_file_writethrough_write+0x10/0x10 [76313.789991] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.791589] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.793314] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.794932] ? current_time+0x73/0x2b0 [76313.796132] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.797276] ? xfs_file_write_checks+0x420/0x900 [xfs] [76313.798786] xfs_file_buffered_write+0x195/0xae0 [xfs] [76313.800243] ? __pfx_xfs_file_buffered_write+0x10/0x10 [xfs] [76313.801775] ? kasan_save_track+0x14/0x40 [76313.802843] ? kasan_save_free_info+0x3b/0x70 [76313.803908] ? __kasan_slab_free+0x4f/0x80 [76313.804894] ? vfs_fstatat+0x55/0xa0 [76313.805835] ? __do_sys_newfstatat+0x7b/0xe0 [76313.806899] ? do_syscall_64+0x5b/0x540 [76313.807829] ? srso_alias_return_thunk+0x5/0xfbef5 [76313.809052] ? xfs_file_write_iter+0x22e/0xa80 [xfs] [76313.810451] do_iter_readv_writev+0x453/0xa70
Quoted 6 lines
I have a feeling this has to do with the error handling as we are stuck waiting for writeback to complete. It is not reproducible because it might be dependent on the state of the system before this triggers. Let me see if I can find a way to reliably reproduce this so that we have something to verify against once we make these changes.
Maybe we can add a WARN_ON() too to detect and confirm that this only happens when we have a short copy case.
We will give this a try too at our end. Also the error handling pointed by you and Ojaswin needs review & fixing in the next revision, to catch any remaining paths where we may end up like this.
Quoted 2 lines
--
Pankaj
Thanks Pankaj for giving this a try at your setup.
-ritesh
RFC PATCH v2 3/5 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 wirte ->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 | 53 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-)
fs/xfs/xfs_file.c +47 -6
@@ -988,6 +988,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, + .dops = &xfs_dio_write_ops, + .writethrough_submit = &xfs_writethrough_submit +}; + + STATIC ssize_t xfs_file_buffered_write( struct kiocb *iocb,
@@ -1010,9 +1043,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
@@ -1047,8 +1084,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; }
@@ -2042,7 +2083,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.53.0
RFC PATCH v2 4/5 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 | 53 ++++++++++++++++++++++++++++++++++++------ include/linux/iomap.h | 10 ++++++-- 2 files changed, 54 insertions(+), 9 deletions(-)
fs/iomap/buffered-io.c +46 -7
@@ -1113,6 +1113,9 @@ static ssize_t iomap_writethrough_complete(struct iomap_writethrough_ctx *wt_ctxmapping_clear_stable_writes (inode->i_mapping); +if (wt_ctx->is_aio) +inode_dio_end (inode); +if (!ret) { ret = wt_ctx->written; iocb->ki_pos = wt_ctx->pos + ret;
@@ -1122,12 +1125,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);return ; }
@@ -1530,9 +1548,6 @@ 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 ;/* * +1 to max bvecs to account for unaligned write spanning multiple
@@ -1557,11 +1572,32 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i, wt_ctx->pos = iocb->ki_pos; wt_ctx->new_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_set_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_clear_stable_writes (inode->i_mapping); +kfree (wt_ctx); +return ret; + } + } +while ((ret =iomap_iter (&iter, wt_ops->ops)) >0 ) {WARN_ON (iter.iomap.type !=IOMAP_UNWRITTEN && iter.iomap.type !=IOMAP_MAPPED );
@@ -1571,6 +1607,9 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i,cmpxchg (&wt_ctx->error,0 , ret);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 +8 -2
@@ -486,9 +486,15 @@ struct iomap_writethrough_ctx {atomic_t ref;unsigned int flags;int error; +bool is_aio; -/* used during submission and for non-aio completion */ -struct task_struct *waiter; +union { +/* used during submission and for non-aio completion */ +struct task_struct *waiter; + +/* used during aio completion */ +struct work_struct aio_work; + };loff_t bio_pos;unsigned int nr_bvecs;
Signature · 1 line
2.53.0
RFC PATCH v2 5/5 iomap: Add DSYNC support to 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 | 37 +++++++++++++++++++++++++++++++++---- include/linux/iomap.h | 1 + 2 files changed, 34 insertions(+), 4 deletions(-)
fs/iomap/buffered-io.c +33 -4
@@ -1119,6 +1119,14 @@ static ssize_t iomap_writethrough_complete(struct iomap_writethrough_ctx *wt_ctxif (!ret) { ret = wt_ctx->written; iocb->ki_pos = wt_ctx->pos + ret; + +/* +* 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); }kfree (wt_ctx);
@@ -1173,6 +1181,7 @@ iomap_writethrough_submit_bio(struct iomap_writethrough_ctx *wt_ctx,struct bio *bio;unsigned int i;u64 len =0 ; +blk_opf_t opf =REQ_OP_WRITE ;if (!wt_ctx->nr_bvecs)return ;
@@ -1184,7 +1193,10 @@ iomap_writethrough_submit_bio(struct iomap_writethrough_ctx *wt_ctx, wt_ops->writethrough_submit (wt_ctx->inode, iomap, wt_ctx->bio_pos, len); - bio =bio_alloc (iomap->bdev, wt_ctx->nr_bvecs,REQ_OP_WRITE ,GFP_NOFS ); +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; bio->bi_private = wt_ctx;
@@ -1273,6 +1285,19 @@ static int iomap_writethrough_iter(struct iomap_writethrough_ctx *wt_ctx,if (!(iter->flags &IOMAP_WRITETHROUGH ))return -EINVAL ; +/* +* If we realise that cache flush is neccessary (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 */
@@ -1545,9 +1570,6 @@ ssize_t iomap_file_writethrough_write(struct kiocb *iocb, struct iov_iter *i,return -EINVAL ;if (iocb->ki_flags & (IOCB_NOWAIT |IOCB_DONTCACHE ))return -EINVAL ; -if (iocb_is_dsync (iocb)) -/* D_SYNC support not implemented yet */ -return -EOPNOTSUPP ;/* * +1 to max bvecs to account for unaligned write spanning multiple
@@ -1575,6 +1597,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
@@ -487,6 +487,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.53.0
Thanks for the patches.
Quoted 8 lines
The implementation of write-through combines the buffered IO frontend with dio backend, which leads to some interesting interactions. I've added most of the design notes in respective patches. Please note that this is an initial RFC to iron out any early design issues. This is largely based on suggestions from Dave an Jan in [1] so thanks for the pointers!
Quoted 5 lines
* Testing Notes (UPDATED) *
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
Could you point me to your xfstests repo where you have added this support? :)
I have been traveling the whole week. I can give the patches a spin in my setup in the coming days.
Quoted 11 lines
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.
* Design TODOs (UPDATED) *
- Evaluate if we need to tag page cache dirty bit in xarray, since
PG_Writebackis already set on the folio.
- Look into a better way to refactor writethrough path by reusing common parts of dio code.
Quoted 12 lines · Pankaj Raghav (Samsung)
Thanks for the patches.
Quoted 10 lines
The implementation of write-through combines the buffered IO frontend with dio backend, which leads to some interesting interactions. I've added most of the design notes in respective patches. Please note that this is an initial RFC to iron out any early design issues. This is largely based on suggestions from Dave an Jan in [1] so thanks for the pointers!
* Testing Notes (UPDATED) *
Quoted 8 lines
Quoted 3 lines
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.Could you point me to your xfstests repo where you have added this support? :)
I have been traveling the whole week. I can give the patches a spin in my setup in the coming days.
Hi Pankaj, you can find the xfstests support here:
https://github.com/OjaswinM/xfstests/tree/iomap-buf-writethrough2
For fsstress, I was using generic/013 modified to run more operations. For fsx, any of the run_fsx stressing tests would do, or you could use the following command (writethrough should auto enable if kernel supports it):
/home/ojas/workspace/xfstests-dev/ltp/fsx \
-N 100000 \
-o 65536 \
-l 500000 \
-r 4096 \
-t 512 \
-w 512 \
--record-ops=rwf_rt.fsxops \
$MNT/junk
Regards,
Ojaswin
Quoted 12 lines
Quoted 12 lines
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.
* Design TODOs (UPDATED) *
- Evaluate if we need to tag page cache dirty bit in xarray, since
PG_Writebackis already set on the folio.
- Look into a better way to refactor writethrough path by reusing common parts of dio code.
Quoted 9 lines
* Testing Notes (UPDATED) *
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.
I am hitting a very strange error in generic/127. I deconstructed it in to two commands that can reproduce the issue:
``` $ dd if=/dev/zero of=/media/test/fsx_lite_nommap bs=262144 count=1 $ /root/home/xfstests/ltp/fsx -l 262144 -o 65536 -S 191110531 -N 100000 -L -R -W /media/test/fsx_lite_nommap mapped writes DISABLED Seed set to 191110531 main: filesystem does not support exchange range, disabling! main: atomic writes need O_DIRECT (-Z), disabling! short read: 0x0 bytes instead of 0x6c7f LOG DUMP (1 total operations): 1( 1 mod 256): READ 0x39381 thru 0x3ffff (0x6c7f bytes) Log of operations saved to "/media/test/fsx_lite_nommap.fsxops"; replay with --replay-ops fsx: save_buffer: .fsxgood file too short... will save 0x0 bytes instead of 0x40000 : Operation not supported Correct content saved for comparison (maybe hexdump "/media/test/fsx_lite_nommap" vs "/media/test/fsx_lite_nommap.fsxgood") ```
This is on a bs == ps (4k block size) xfs filesystem.
```
$ xfs_info /media/test/
meta-data=/dev/nvme1n1 isize=512 agcount=4, agsize=2097152 blks
= sectsz=4096 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=0
= reflink=1 bigtime=1 inobtcount=1 nrext64=0
data = bsize=4096 blocks=8388608, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0, ftype=1
log =internal log bsize=4096 blocks=16384, version=2
= sectsz=4096 sunit=1 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
```The same test passes if I pass -G to fsx parameters(disabling writethrough).
The error is coming when we do a read operation which feels very strange. Could you run this in your setup and see if you can reproduce them? I still do not know where the issue is coming from or it is because of my test setup.
--
Pankaj
Quoted 46 lines · Pankaj Raghav (Samsung)
Quoted 9 lines
* Testing Notes (UPDATED) *
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.I am hitting a very strange error in generic/127. I deconstructed it in to two commands that can reproduce the issue:
``` $ dd if=/dev/zero of=/media/test/fsx_lite_nommap bs=262144 count=1 $ /root/home/xfstests/ltp/fsx -l 262144 -o 65536 -S 191110531 -N 100000 -L -R -W /media/test/fsx_lite_nommap mapped writes DISABLED Seed set to 191110531 main: filesystem does not support exchange range, disabling! main: atomic writes need O_DIRECT (-Z), disabling! short read: 0x0 bytes instead of 0x6c7f LOG DUMP (1 total operations): 1( 1 mod 256): READ 0x39381 thru 0x3ffff (0x6c7f bytes) Log of operations saved to "/media/test/fsx_lite_nommap.fsxops"; replay with --replay-ops fsx: save_buffer: .fsxgood file too short... will save 0x0 bytes instead of 0x40000 : Operation not supported Correct content saved for comparison (maybe hexdump "/media/test/fsx_lite_nommap" vs "/media/test/fsx_lite_nommap.fsxgood") ```This is on a bs == ps (4k block size) xfs filesystem.
``` $ xfs_info /media/test/ meta-data=/dev/nvme1n1 isize=512 agcount=4, agsize=2097152 blks = sectsz=4096 attr=2, projid32bit=1 = crc=1 finobt=1, sparse=1, rmapbt=0 = reflink=1 bigtime=1 inobtcount=1 nrext64=0 data = bsize=4096 blocks=8388608, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0, ftype=1 log =internal log bsize=4096 blocks=16384, version=2 = sectsz=4096 sunit=1 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 ```
Quoted 6 lines
The same test passes if I pass
-Gto fsx parameters(disabling writethrough).The error is coming when we do a read operation which feels very strange. Could you run this in your setup and see if you can reproduce them? I still do not know where the issue is coming from or it is because of my test setup.
Hey Pankaj, I think the xfstests branch I shared might have been missing one small fix that can cause this issue. I've update the branch with the fix, can you give it a try:
https://github.com/OjaswinM/xfstests/tree/iomap-buf-writethrough2
Regards,
Ojaswin
Quoted 3 lines
--
Pankaj
Quoted 54 lines · Ojaswin Mujoo
Quoted 46 lines · Pankaj Raghav (Samsung)
Quoted 9 lines
* Testing Notes (UPDATED) *
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.I am hitting a very strange error in generic/127. I deconstructed it in to two commands that can reproduce the issue:
``` $ dd if=/dev/zero of=/media/test/fsx_lite_nommap bs=262144 count=1 $ /root/home/xfstests/ltp/fsx -l 262144 -o 65536 -S 191110531 -N 100000 -L -R -W /media/test/fsx_lite_nommap mapped writes DISABLED Seed set to 191110531 main: filesystem does not support exchange range, disabling! main: atomic writes need O_DIRECT (-Z), disabling! short read: 0x0 bytes instead of 0x6c7f LOG DUMP (1 total operations): 1( 1 mod 256): READ 0x39381 thru 0x3ffff (0x6c7f bytes) Log of operations saved to "/media/test/fsx_lite_nommap.fsxops"; replay with --replay-ops fsx: save_buffer: .fsxgood file too short... will save 0x0 bytes instead of 0x40000 : Operation not supported Correct content saved for comparison (maybe hexdump "/media/test/fsx_lite_nommap" vs "/media/test/fsx_lite_nommap.fsxgood") ```This is on a bs == ps (4k block size) xfs filesystem.
``` $ xfs_info /media/test/ meta-data=/dev/nvme1n1 isize=512 agcount=4, agsize=2097152 blks = sectsz=4096 attr=2, projid32bit=1 = crc=1 finobt=1, sparse=1, rmapbt=0 = reflink=1 bigtime=1 inobtcount=1 nrext64=0 data = bsize=4096 blocks=8388608, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0, ftype=1 log =internal log bsize=4096 blocks=16384, version=2 = sectsz=4096 sunit=1 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 ```Quoted 6 lines
The same test passes if I pass
-Gto fsx parameters(disabling writethrough).The error is coming when we do a read operation which feels very strange. Could you run this in your setup and see if you can reproduce them? I still do not know where the issue is coming from or it is because of my test setup.
Quoted 6 lines
Hey Pankaj, I think the xfstests branch I shared might have been missing one small fix that can cause this issue. I've update the branch with the fix, can you give it a try:
https://github.com/OjaswinM/xfstests/tree/iomap-buf-writethrough2
Ohk, just noticed your reply, after sending a response...
Thinking it again, I agree we should just always ftruncate it back to the original file_size. Because we might have changed to file size while testing the writethrough support.
Thanks for fixing it! This fix looks right to me.
~# git diff
ltp/fsx.c +1 -1
@@ -2095,7 +2095,7 @@ test_writethrough_io(void)return 0 ; } -if (ftruncate (fd,0 ) !=0 ) { +if (ftruncate (fd, file_size) !=0 ) {perror ("test_writethrough_io: failed to ftruncate to 0\n" );exit (132 ); }
-ritesh
Quoted 6 lines
Regards,
OjaswinQuoted 3 lines
--
Pankaj
Quoted 15 lines
Ohk, just noticed your reply, after sending a response...
Thinking it again, I agree we should just always ftruncate it back to the original
file_size. Because we might have changed to file size while testing the writethrough support.Thanks for fixing it! This fix looks right to me.
~# git diff
ltp/fsx.c +0
-0
ltp/fsx.c test_writethrough_io(void) · line 2101
Quoted 8 lines
ltp/fsx.c +1
-1@@ -2095,7 +2095,7 @@ test_writethrough_io(void)return 0 ; } -if (ftruncate (fd,0 ) !=0 ) { +if (ftruncate (fd, file_size) !=0 ) {perror ("test_writethrough_io: failed to ftruncate to 0\n" );exit (132 ); }
Yes, this fixes it. I knew it was some issue with fsx itself.
--
Pankaj
Quoted 22 lines · Pankaj Raghav (Samsung)
Quoted 9 lines
* Testing Notes (UPDATED) *
- I've added support for
RWF_WRITETHROUGHto fsx and fsstress in xfstests and these patches survive fsx with integrity verification as well as fsstress parallel stressing.
- -g quick with blocks size == page size and blocksize < pagesize shows no new regressions.I am hitting a very strange error in generic/127. I deconstructed it in to two commands that can reproduce the issue:
``` $ dd if=/dev/zero of=/media/test/fsx_lite_nommap bs=262144 count=1 $ /root/home/xfstests/ltp/fsx -l 262144 -o 65536 -S 191110531 -N 100000 -L -R -W /media/test/fsx_lite_nommap mapped writes DISABLED Seed set to 191110531 main: filesystem does not support exchange range, disabling! main: atomic writes need O_DIRECT (-Z), disabling! short read: 0x0 bytes instead of 0x6c7f
Quoted 8 lines
LOG DUMP (1 total operations): 1( 1 mod 256): READ 0x39381 thru 0x3ffff (0x6c7f bytes) Log of operations saved to "/media/test/fsx_lite_nommap.fsxops"; replay with --replay-ops fsx: save_buffer: .fsxgood file too short... will save 0x0 bytes instead of 0x40000 : Operation not supported Correct content saved for comparison (maybe hexdump "/media/test/fsx_lite_nommap" vs "/media/test/fsx_lite_nommap.fsxgood") ```
This looks like fsx has a problem where while testing writethrough support we ftruncate the file size to 0 instead of file_size.
@Ojaswin,
I think we should have it like this I guess.
ltp/fsx.c +1 -1
@@ -2095,7 +2095,7 @@ test_writethrough_io(void)return 0 ; } -if (ftruncate (fd,0 ) !=0 ) { +if (!lite &&ftruncate (fd, file_size) !=0 ) {perror ("test_writethrough_io: failed to ftruncate to 0\n" );exit (132 ); }
-ritesh
Quoted 25 lines
This is on a bs == ps (4k block size) xfs filesystem.
``` $ xfs_info /media/test/ meta-data=/dev/nvme1n1 isize=512 agcount=4, agsize=2097152 blks = sectsz=4096 attr=2, projid32bit=1 = crc=1 finobt=1, sparse=1, rmapbt=0 = reflink=1 bigtime=1 inobtcount=1 nrext64=0 data = bsize=4096 blocks=8388608, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0, ftype=1 log =internal log bsize=4096 blocks=16384, version=2 = sectsz=4096 sunit=1 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 ```The same test passes if I pass
-Gto fsx parameters(disabling writethrough).The error is coming when we do a read operation which feels very strange. Could you run this in your setup and see if you can reproduce them? I still do not know where the issue is coming from or it is because of my test setup.
--
Pankaj
Quoted 7 lines · Ojaswin Mujoo
Hi all,
This is the v2 RFC to add buffered writethrough support to iomap and xfs. The changes made are mostly to get the writethrough implementation more inline with how dio handles writes.
I gave these series a run with kdevops and fstests using the following
profiles: xfs-crc-logdev, xfs-crc, xfs-reflink-logdev, xfs-reflink.
Note that I applied all patches on top of v7.1-rc1 and xfstest-dev patches from Ojaswin have been rebased on latest master [1].
Link:
https://github.com/dagomez137/xfstests-dev/tree/iomap-buf-writethrough2 [1].
In general, some of the results here seem flaky. But let me know what you think. I'll try to confirm the possible regressions with another full run.
Summary per profile:
* xfs-crc-logdev: no regressions or fixes found
* xfs-crc:
* generic/753 fixed. This test failed on 7.1.0-rc1 run but succeeded
on 7.1.0-rc1-00005-gc6a39600d3c3.
* xfs-reflink-logdev:
* generic/415 failed on 7.1.0-rc1-00005-gc6a39600d3c3. I tried to
replicate the issue by just running generic/415 (not the entire test suite), and I couldn't replicate it.
* xfs-reflink:
* generic/457 failed on 7.1.0-rc1-00005-gc6a39600d3c3.
* generic/561 failed on 7.1.0-rc1 but not on
7.1.0-rc1-00005-gc6a39600d3c3.
Here the full report list for each A/B profile test run:
Quoted 25 lines · Daniel Gomez
Quoted 7 lines · Ojaswin Mujoo
Hi all,
This is the v2 RFC to add buffered writethrough support to iomap and xfs. The changes made are mostly to get the writethrough implementation more inline with how dio handles writes.
I gave these series a run with kdevops and fstests using the following
profiles: xfs-crc-logdev, xfs-crc, xfs-reflink-logdev, xfs-reflink.Note that I applied all patches on top of v7.1-rc1 and xfstest-dev patches from Ojaswin have been rebased on latest master [1].
Link:
https://github.com/dagomez137/xfstests-dev/tree/iomap-buf-writethrough2 [1].In general, some of the results here seem flaky. But let me know what you think. I'll try to confirm the possible regressions with another full run.
Summary per profile:
Quoted 12 lines
* xfs-crc-logdev: no regressions or fixes found
* xfs-crc:
* generic/753 fixed. This test failed on 7.1.0-rc1 run but succeeded
on 7.1.0-rc1-00005-gc6a39600d3c3.
* xfs-reflink-logdev:
* generic/415 failed on 7.1.0-rc1-00005-gc6a39600d3c3. I tried to
replicate the issue by just running generic/415 (not the entire test suite), and I couldn't replicate it.
* xfs-reflink:
* generic/457 failed on 7.1.0-rc1-00005-gc6a39600d3c3.
* generic/561 failed on 7.1.0-rc1 but not on
7.1.0-rc1-00005-gc6a39600d3c3.
Hey Daniel,
thanks so much for testing the patches. I'm back this week from travel so still catching up to my emails. I'll go trhough thi and get back to you soon.
Regards,
Ojaswin
Quoted 16 lines
Here the full report list for each A/B profile test run:
Quoted 39 lines · Ojaswin Mujoo
Quoted 25 lines · Daniel Gomez
Quoted 7 lines · Ojaswin Mujoo
Hi all,
This is the v2 RFC to add buffered writethrough support to iomap and xfs. The changes made are mostly to get the writethrough implementation more inline with how dio handles writes.
I gave these series a run with kdevops and fstests using the following
profiles: xfs-crc-logdev, xfs-crc, xfs-reflink-logdev, xfs-reflink.Note that I applied all patches on top of v7.1-rc1 and xfstest-dev patches from Ojaswin have been rebased on latest master [1].
Link:
https://github.com/dagomez137/xfstests-dev/tree/iomap-buf-writethrough2 [1].In general, some of the results here seem flaky. But let me know what you think. I'll try to confirm the possible regressions with another full run.
Summary per profile:
Quoted 12 lines
* xfs-crc-logdev: no regressions or fixes found
* xfs-crc:
* generic/753 fixed. This test failed on 7.1.0-rc1 run but succeeded
on 7.1.0-rc1-00005-gc6a39600d3c3.
* xfs-reflink-logdev:
* generic/415 failed on 7.1.0-rc1-00005-gc6a39600d3c3. I tried to
replicate the issue by just running generic/415 (not the entire test suite), and I couldn't replicate it.
* xfs-reflink:
* generic/457 failed on 7.1.0-rc1-00005-gc6a39600d3c3.
* generic/561 failed on 7.1.0-rc1 but not on
7.1.0-rc1-00005-gc6a39600d3c3.
Quoted 8 lines
Hey Daniel,
thanks so much for testing the patches. I'm back this week from travel so still catching up to my emails. I'll go trhough thi and get back to you soon.
Regards,
Ojaswin
Hi Daniel,
So this week I spent some time looking into the failure of g/457. I was able to replicate it in my setup and after analysing it, this is actually an issue in my xfstests patch. The issue is that to verify RWF_WRITETHROUGH support I do a 4kb write on the fd during the test prep phase. However "out-of-band" write makes the actual contents of the file different from what fsx expected (It reads it in before the problematic write), causing the test failure.
The below snippet fixes it:
ltp/fsx.c +19 -1
@@ -2077,16 +2077,34 @@ test_dontcache_io(void)return 1 ; } +/* +* Use a temporary file to test RWF_WRITETHROUGH so as to not modify the +* original. This is so that fsx expected contents and the actual contents +* of the original file don't go out of sync +*/ int test_writethrough_io (void ) {char buf[4096 ]; +char tmpname[PATH_MAX ]; +int tmp_fd;struct iovec iov = { .iov_base = buf, .iov_len =sizeof (buf) };int ret, e; +snprintf (tmpname,sizeof (tmpname),"%s.fsx.XXXXXX" , fname); + + tmp_fd =mkstemp (tmpname); +if (fd <0 ) { +perror ("test_writethrough_io: mkstemp() failed\n" ); +exit (132 ); + } +memset (buf,'\0' ,sizeof (buf)); - ret =pwritev2 (fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); + ret =pwritev2 (tmp_fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); e = ret <0 ? errno :0 ; + +close (tmp_fd); +if (e ==EOPNOTSUPP ) {if (!quiet) fprintf(stderr,
Im unable to replicate the 415 failure, but this one is strange because it doesn't involve any writethrough IO. In you previous runs have you seen this be flaky?
Admittedly, I've not yet started thorough testing since this is in early RFC and the design is still getting finalized but I appriciate the help in testing this.
Thanks again,
ojaswin
Quoted 17 lines
Quoted 17 lines
Here the full report list for each A/B profile test run:
Quoted 8 lines · Ojaswin Mujoo
So this week I spent some time looking into the failure of g/457. I was able to replicate it in my setup and after analysing it, this is actually an issue in my xfstests patch. The issue is that to verify
RWF_WRITETHROUGHsupport I do a 4kb write on the fd during the test prep phase. However "out-of-band" write makes the actual contents of the file different from what fsx expected (It reads it in before the problematic write), causing the test failure.
Why does this get out of sync? One of the arguments for RWF_WRITETHROUGH vs direct I/O was that it avoids the content getting out of sync.
Quoted 10 lines · Christoph Hellwig
Quoted 8 lines · Ojaswin Mujoo
So this week I spent some time looking into the failure of g/457. I was able to replicate it in my setup and after analysing it, this is actually an issue in my xfstests patch. The issue is that to verify
RWF_WRITETHROUGHsupport I do a 4kb write on the fd during the test prep phase. However "out-of-band" write makes the actual contents of the file different from what fsx expected (It reads it in before the problematic write), causing the test failure.
Quoted 3 lines
Why does this get out of sync? One of the arguments for
RWF_WRITETHROUGHvs direct I/O was that it avoids the content getting out of sync.
Hey Christoph, I think I was a bit vague in my explanation. So this issue is not in the kernel but in fsx. Basically fsx does something like:
1. Read in the file contents and store it in a local good_buffer
2. Detect support for various IO types (writethrough, dontcache etc) 2.1 To detect writethrough support, I perform a
pwrite("4kb of 0s", file, flag=RWF_WRITETHROUGH) and look out for EOPNOTSUPP.
3. Start running the test
3.1 Whenever we perform a write operation in fsx we use fsx write
wrappers which also update the good_buffer before writing to file.
4. During fsync, we write the good_buffer to a golden output file and also set a dm_logwrite mark. Later, we replay the dm_logwrites mark and then check if the file matches the golden o/p file.
The issue here is that fsx's golden output was not correct cause in step 2.1 I was using pwritev2() directly instead of using fsx wrappers to do the write which was not updating the good_buffer. Hence, good_buffer went out of sync from what was actually supposed to be in the file. g/457 thought this was a corruption however it was a bug in my fsx writethrough support patch.
Regards,
ojaswin
Quoted 34 lines · Ojaswin Mujoo
Quoted 2 lines · Ojaswin Mujoo
Quoted 1 line · Daniel Gomez
On 08/04/2026 20.45, Ojaswin Mujoo wrote:
ltp/fsx.c +15
-0@@ -2077,16 +2077,34 @@ test_dontcache_io(void)return 1 ; } +/* +* Use a temporary file to test RWF_WRITETHROUGH so as to not modify the +* original. This is so that fsx expected contents and the actual contents +* of the original file don't go out of sync +*/ int test_writethrough_io (void ) {char buf[4096 ]; +char tmpname[PATH_MAX ]; +int tmp_fd;struct iovec iov = { .iov_base = buf, .iov_len =sizeof (buf) };int ret, e; +snprintf (tmpname,sizeof (tmpname),"%s.fsx.XXXXXX" , fname); + + tmp_fd =mkstemp (tmpname); +if (fd <0 ) { +perror ("test_writethrough_io: mkstemp() failed\n" ); +exit (132 ); + } +
ltp/fsx.c test_dontcache_io(void) · line 2110
Quoted 10 lines
ltp/fsx.c +4
-1memset (buf,'\0' ,sizeof (buf)); - ret =pwritev2 (fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); + ret =pwritev2 (tmp_fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); e = ret <0 ? errno :0 ; + +close (tmp_fd); +if (e ==EOPNOTSUPP ) {if (!quiet) fprintf(stderr,
I think this is missing:
@@ -2158,12 +2175,6 @@ test_writethrough_io(void)
return 0;
}- if (ftruncate(fd, file_size) != 0) {
- perror("test_writethrough_io: failed to ftruncate to 0\n");
- exit(132);
- }
-
-
The fix works for me after a stress loop test on generic/457.
Quoted 4 lines
Im unable to replicate the 415 failure, but this one is strange because it doesn't involve any writethrough IO. In you previous runs have you seen this be flaky?
I couldn't confirm back then, so I think we can ignore it. Also, unrelated.
Quoted 4 lines
Admittedly, I've not yet started thorough testing since this is in early RFC and the design is still getting finalized but I appriciate the help in testing this.
No problem. IMHO, testing makes a design more robust, once the path is agreed on. That said, do you think the path needs changing? Reading LWN and the patch series threads, I don't see any blocker. You can cc me for the next iteration and I'll follow up with more testing.
Quoted 60 lines · Daniel Gomez
Quoted 34 lines · Ojaswin Mujoo
Quoted 2 lines · Ojaswin Mujoo
Quoted 1 line · Daniel Gomez
On 08/04/2026 20.45, Ojaswin Mujoo wrote:
ltp/fsx.c +15
-0@@ -2077,16 +2077,34 @@ test_dontcache_io(void)return 1 ; } +/* +* Use a temporary file to test RWF_WRITETHROUGH so as to not modify the +* original. This is so that fsx expected contents and the actual contents +* of the original file don't go out of sync +*/ int test_writethrough_io (void ) {char buf[4096 ]; +char tmpname[PATH_MAX ]; +int tmp_fd;struct iovec iov = { .iov_base = buf, .iov_len =sizeof (buf) };int ret, e; +snprintf (tmpname,sizeof (tmpname),"%s.fsx.XXXXXX" , fname); + + tmp_fd =mkstemp (tmpname); +if (fd <0 ) { +perror ("test_writethrough_io: mkstemp() failed\n" ); +exit (132 ); + } +ltp/fsx.c test_dontcache_io(void) · line 2110
Quoted 10 lines
ltp/fsx.c +4
-1memset (buf,'\0' ,sizeof (buf)); - ret =pwritev2 (fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); + ret =pwritev2 (tmp_fd, &iov,1 ,0 ,RWF_WRITETHROUGH ); e = ret <0 ? errno :0 ; + +close (tmp_fd); +if (e ==EOPNOTSUPP ) {if (!quiet) fprintf(stderr,I think this is missing:
(quoted diff) +0
-6@@ -2158,12 +2175,6 @@ test_writethrough_io(void) return 0; } - if (ftruncate(fd, file_size) != 0) { - perror("test_writethrough_io: failed to ftruncate to 0\n"); - exit(132); - } - -
Quoted 1 line
The fix works for me after a stress loop test on generic/457.
Awesome! and yes since we use a different file now, we can remove the ftruncate() call.
Quoted 7 lines
Quoted 5 lines
Im unable to replicate the 415 failure, but this one is strange because it doesn't involve any writethrough IO. In you previous runs have you seen this be flaky?
I couldn't confirm back then, so I think we can ignore it. Also, unrelated.
Okay.
Quoted 11 lines
Quoted 5 lines
Admittedly, I've not yet started thorough testing since this is in early RFC and the design is still getting finalized but I appriciate the help in testing this.
No problem. IMHO, testing makes a design more robust, once the path is agreed on. That said, do you think the path needs changing? Reading LWN and the patch series threads, I don't see any blocker. You can cc me for the next iteration and I'll follow up with more testing.
Yes definitely and really appreciate your help in testing this. As for the design, the current design suffer when many writers are writing to the same file, because the inode lock critical section is increased. So I'm trying out a few optimizations that will lead to some non-trivial design changes (like deferring the IO to outside the inode lock).
I'm in process of preparing and benchmarking the new design and would hopefully post it soon. I'll keep you in loop, thanks.
Regards,
ojaswin