fs: Hole punch vs page cache filling races
Subsystems (12)
Jan Kara
maintainerv3 cover fs: Hole punch vs page cache filling races
Hello,
here is another version of my patches to address races between hole punching and page cache filling functions for ext4 and other filesystems. Since the last posting I've added more documentation and comments regarding the lock ordering of the new lock (i_mapping_sem) and how is the new lock supposed to be used. I've also added conversions of ext2, xfs, zonefs so that there is better idea how the conversion looks for most filesystems. Obviously, there are much more filesystems to convert but I don't want to do that work unless we have a concensus this is indeed the right approach. Also as a result of spelling out locking rules more precisely, I have realized there's no need to use i_mapping_sem in .page_mkwrite handlers so I've added a bonus patch removing those - not sure we want to actually do that together with the rest of the series (maybe we can do this cleanup later when the rest of the conversion has settled down).
Also when writing the documentation I came across one question: Do we mandate i_mapping_sem for truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to use i_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
What do people think about this?
Changes since v2:
* Added documentation and comments regarding lock ordering and how the lock is supposed to be used
* Added conversions of ext2, xfs, zonefs
* Added patch removing i_mapping_sem protection from .page_mkwrite handlers
Changes since v1:
* Moved to using inode->i_mapping_sem instead of aops handler to acquire appropriate lock
---
Motivation:
Amir has reported [1] a that ext4 has a potential issues when reads can race with hole punching possibly exposing stale data from freed blocks or even corrupting filesystem when stale mapping data gets used for writeout. The problem is that during hole punching, new page cache pages can get instantiated and block mapping from the looked up in a punched range after truncate_inode_pages() has run but before the filesystem removes blocks from the file. In principle any filesystem implementing hole punching thus needs to implement a mechanism to block instantiating page cache pages during hole punching to avoid this race. This is further complicated by the fact that there are multiple places that can instantiate pages in page cache. We can have regular read(2) or page fault doing this but fadvise(2) or madvise(2) can also result in reading in page cache pages through force_page_cache_readahead().
There are couple of ways how to fix this. First way (currently implemented by XFS) is to protect read(2) and *advise(2) calls with i_rwsem so that they are serialized with hole punching. This is easy to do but as a result all reads would then be serialized with writes and thus mixed read-write workloads suffer heavily on ext4. Thus this series introduces inode->i_mapping_sem and uses it when creating new pages in the page cache and looking up their corresponding block mapping. We also replace EXT4_I(inode)->i_mmap_sem with this new rwsem which provides necessary serialization with hole punching for ext4.
Honza
Previous versions:
Jan Kara
maintainerPATCH v3 1/7 mm: Fix comments mentioning i_mutex
inode->i_mutex has been replaced with inode->i_rwsem long ago. Fix comments still mentioning i_mutex.
--- mm/filemap.c | 10 +++++----- mm/madvise.c | 2 +- mm/memory-failure.c | 2 +- mm/rmap.c | 6 +++--- mm/shmem.c | 20 ++++++++++---------- mm/truncate.c | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-)
mm/filemap.c +5 -5
@@ -76,7 +76,7 @@* ->swap_lock (exclusive_swap_page, others) * ->i_pages lock * -* ->i_mutex +* ->i_rwsem * ->i_mmap_rwsem (truncate->unmap_mapping_range) * * ->mmap_lock
@@ -87,7 +87,7 @@* ->mmap_lock * ->lock_page (access_process_vm) * -* ->i_mutex (generic_perform_write) +* ->i_rwsem (generic_perform_write) * ->mmap_lock (fault_in_pages_readable->do_page_fault) * * bdi->wb.list_lock
@@ -3625,12 +3625,12 @@ EXPORT_SYMBOL(generic_perform_write);* modification times and calls proper subroutines depending on whether we * do direct IO or a standard buffered write. * -* It expects i_mutex to be grabbed unless we work on a block device or similar +* It expects i_rwsem to be grabbed unless we work on a block device or similar * object which does not need locking at all. * * This function does *not* take care of syncing data in case of O_SYNC write. * A caller has to handle it. This is mainly due to the fact that we want to -* avoid syncing under i_mutex. +* avoid syncing under i_rwsem. * * Return: * * number of bytes written, even for truncated writes
@@ -3718,7 +3718,7 @@ EXPORT_SYMBOL(__generic_file_write_iter);* * This is a wrapper around __generic_file_write_iter() to be used by most * filesystems. It takes care of syncing the file in case of O_SYNC file -* and acquires i_mutex as needed. +* and acquires i_rwsem as needed. * Return: * * negative error code if no data has been written at all of * vfs_fsync_range() failed for a synchronous write
mm/madvise.c +1 -1
@@ -853,7 +853,7 @@ static long madvise_remove(struct vm_area_struct *vma, + ((loff_t )vma->vm_pgoff <<PAGE_SHIFT );/* -* Filesystem's fallocate may need to take i_mutex. We need to +* Filesystem's fallocate may need to take i_rwsem. We need to * explicitly grab a reference because the vma (and hence the * vma's reference to the file) can go away as soon as we drop * mmap_lock.
mm/memory-failure.c +1 -1
@@ -704,7 +704,7 @@ static int me_pagecache_clean(struct page *p, unsigned long pfn)/* * Truncation is a bit tricky. Enable it per file system for now. * -* Open: to take i_mutex or not for this? Right now we don't. +* Open: to take i_rwsem or not for this? Right now we don't. */ return truncate_error_page (p, pfn, mapping); }
mm/rmap.c +3 -3
@@ -20,9 +20,9 @@/* * Lock ordering in mm: * -* inode->i_mutex (while writing or truncating, not reading or faulting) +* inode->i_rwsem (while writing or truncating, not reading or faulting) * mm->mmap_lock -* page->flags PG_locked (lock_page) * (see huegtlbfs below) +* page->flags PG_locked (lock_page) * (see hugetlbfs below) * hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share) * mapping->i_mmap_rwsem * hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
@@ -41,7 +41,7 @@* in arch-dependent flush_dcache_mmap_lock, * within bdi.wb->list_lock in __sync_single_inode) * -* anon_vma->rwsem,mapping->i_mutex (memory_failure, collect_procs_anon) +* anon_vma->rwsem,mapping->i_mmap_rwsem (memory_failure, collect_procs_anon) * ->tasklist_lock * pte map lock *
mm/shmem.c +10 -10
@@ -96,7 +96,7 @@ static struct vfsmount *shm_mnt;/* * shmem_fallocate communicates with shmem_fault or shmem_writepage via -* inode->i_private (with i_mutex making sure that it has only one user at +* inode->i_private (with i_rwsem making sure that it has only one user at * a time): we would prefer not to enlarge the shmem inode just for that. */ struct shmem_falloc {
@@ -774,7 +774,7 @@ static int shmem_free_swap(struct address_space *mapping,* Determine (in bytes) how many of the shmem object's pages mapped by the * given offsets are swapped out. * -* This is safe to call without i_mutex or the i_pages lock thanks to RCU, +* This is safe to call without i_rwsem or the i_pages lock thanks to RCU, * as long as the inode doesn't go away and racy results are not a problem. */ unsigned long shmem_partial_swap_usage(struct address_space *mapping,
@@ -806,7 +806,7 @@ unsigned long shmem_partial_swap_usage(struct address_space *mapping,* Determine (in bytes) how many of the shmem object's pages mapped by the * given vma is swapped out. * -* This is safe to call without i_mutex or the i_pages lock thanks to RCU, +* This is safe to call without i_rwsem or the i_pages lock thanks to RCU, * as long as the inode doesn't go away and racy results are not a problem. */ unsigned long shmem_swap_usage (struct vm_area_struct *vma)
@@ -1069,7 +1069,7 @@ static int shmem_setattr(struct user_namespace *mnt_userns,loff_t oldsize = inode->i_size;loff_t newsize = attr->ia_size; -/* protected by i_mutex */ +/* protected by i_rwsem */ if ((newsize < oldsize && (info->seals &F_SEAL_SHRINK )) || (newsize > oldsize && (info->seals &F_SEAL_GROW )))return -EPERM ;
@@ -2049,7 +2049,7 @@ static vm_fault_t shmem_fault(struct vm_fault *vmf)/* * Trinity finds that probing a hole which tmpfs is punching can * prevent the hole-punch from ever completing: which in turn -* locks writers out with its hold on i_mutex. So refrain from +* locks writers out with its hold on i_rwsem. So refrain from * faulting pages into the hole while it's being punched. Although * shmem_undo_range() does remove the additions, it may be unable to * keep up, as each new page needs its own unmap_mapping_range() call,
@@ -2060,7 +2060,7 @@ static vm_fault_t shmem_fault(struct vm_fault *vmf)* we just need to make racing faults a rare case. * * The implementation below would be much simpler if we just used a -* standard mutex or completion: but we cannot take i_mutex in fault, +* standard mutex or completion: but we cannot take i_rwsem in fault, * and bloating every shmem inode for this unlikely case would be sad. */ if (unlikely (inode->i_private)) {
@@ -2518,7 +2518,7 @@ shmem_write_begin(struct file *file, struct address_space *mapping,struct shmem_inode_info *info =SHMEM_I (inode);pgoff_t index = pos >>PAGE_SHIFT ; -/* i_mutex is held by caller */ +/* i_rwsem is held by caller */ if (unlikely (info->seals & (F_SEAL_GROW |F_SEAL_WRITE |F_SEAL_FUTURE_WRITE ))) {if (info->seals & (F_SEAL_WRITE |F_SEAL_FUTURE_WRITE ))
@@ -2618,7 +2618,7 @@ static ssize_t shmem_file_read_iter(struct kiocb *iocb, struct iov_iter *to)/* * We must evaluate after, since reads (unlike writes) -* are called without i_mutex protection against truncate +* are called without i_rwsem protection against truncate */ nr =PAGE_SIZE ; i_size =i_size_read (inode);
@@ -2688,7 +2688,7 @@ static loff_t shmem_file_llseek(struct file *file, loff_t offset, int whence)return -ENXIO ;inode_lock (inode); -/* We're holding i_mutex so we can access i_size directly */ +/* We're holding i_rwsem so we can access i_size directly */ offset =mapping_seek_hole_data (mapping, offset, inode->i_size, whence);if (offset >=0 ) offset =vfs_setpos (file, offset,MAX_LFS_FILESIZE );
@@ -2717,7 +2717,7 @@ static long shmem_fallocate(struct file *file, int mode, loff_t offset,loff_t unmap_end =round_down (offset + len,PAGE_SIZE ) -1 ;DECLARE_WAIT_QUEUE_HEAD_ONSTACK (shmem_falloc_waitq); -/* protected by i_mutex */ +/* protected by i_rwsem */ if (info->seals & (F_SEAL_WRITE |F_SEAL_FUTURE_WRITE )) { error = -EPERM ; goto out;
mm/truncate.c +4 -4
@@ -416,7 +416,7 @@ EXPORT_SYMBOL(truncate_inode_pages_range);* @mapping: mapping to truncate * @lstart: offset from which to truncate * -* Called under (and serialised by) inode->i_mutex. +* Called under (and serialised by) inode->i_rwsem. * * Note: When this function returns, there can be a page in the process of * deletion (inside __delete_from_page_cache()) in the specified range. Thus
@@ -433,7 +433,7 @@ EXPORT_SYMBOL(truncate_inode_pages);* truncate_inode_pages_final - truncate *all* pages before inode dies * @mapping: mapping to truncate * -* Called under (and serialized by) inode->i_mutex. +* Called under (and serialized by) inode->i_rwsem. * * Filesystems have to use this in the .evict_inode path to inform the * VM that this is the final truncate and the inode is going away.
@@ -766,7 +766,7 @@ EXPORT_SYMBOL(truncate_pagecache);* setattr function when ATTR_SIZE is passed in. * * Must be called with a lock serializing truncates and writes (generally -* i_mutex but e.g. xfs uses a different lock) and before all filesystem +* i_rwsem but e.g. xfs uses a different lock) and before all filesystem * specific block truncation has been performed. */ void truncate_setsize (struct inode *inode,loff_t newsize)
@@ -795,7 +795,7 @@ EXPORT_SYMBOL(truncate_setsize);* * The function must be called after i_size is updated so that page fault * coming after we unlock the page will already see the new i_size. -* The function must be called while we still hold i_mutex - this not only +* The function must be called while we still hold i_rwsem - this not only * makes sure i_size is stable but also that userspace cannot observe new * i_size value before we are prepared to store mmap writes at new inode size. */
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Re: [PATCH 1/7] mm: Fix comments mentioning i_mutex
Quoted 4 lines · Jan Kara
inode->i_mutexhas been replaced withinode->i_rwsemlong ago. Fix comments still mentioningi_mutex.Signed-off-by Jan Kara <[email protected]>
Looks good,
Jan Kara
maintainerPATCH v3 2/7 mm: Protect operations adding pages to page cache with i_mapping_lock
In reply to Jan KaraCurrently, serializing operations such as page fault, read, or readahead against hole punching is rather difficult. The basic race scheme is like:
fallocate(FALLOC_FL_PUNCH_HOLE) read / fault / ..
truncate_inode_pages_range()
<create pages in page
cache here>
<update fs block mapping and free blocks>
Now the problem is in this way read / page fault / readahead can instantiate pages in page cache with potentially stale data (if blocks get quickly reused). Avoiding this race is not simple - page locks do not work because we want to make sure there are *no* pages in given range. inode->i_rwsem does not work because page fault happens under mmap_sem which ranks below inode->i_rwsem. Also using it for reads makes the performance for mixed read-write workloads suffer.
So create a new rw_semaphore in the inode - i_mapping_sem - that protects adding of pages to page cache for page faults / reads / readahead.
--- Documentation/filesystems/locking.rst | 38 ++++++++++++++------ fs/inode.c | 3 ++ include/linux/fs.h | 2 ++ mm/filemap.c | 51 ++++++++++++++++++++++++--- mm/readahead.c | 2 ++ mm/rmap.c | 37 +++++++++---------- mm/truncate.c | 2 +- 7 files changed, 101 insertions(+), 34 deletions(-)
Documentation/filesystems/locking.rst +27 -11
@@ -266,19 +266,19 @@ prototypes:: locking rules: All except set_page_dirty and freepage may block -====================== ======================== ========= -ops PageLocked(page) i_rwsem -====================== ======================== ========= +====================== ======================== ========= =============== +ops PageLocked(page) i_rwsem i_mapping_sem +====================== ======================== ========= =============== writepage: yes, unlocks (see below) -readpage: yes, unlocks +readpage: yes, unlocks shared writepages: set_page_dirty no -readahead: yes, unlocks -readpages: no +readahead: yes, unlocks shared +readpages: no shared write_begin: locks the page exclusive write_end: yes, unlocks exclusive bmap: -invalidatepage: yes +invalidatepage: yes exclusive releasepage: yes freepage: yes direct_IO:
@@ -373,7 +373,10 @@ keep it that way and don't breed new callers. ->invalidatepage() is called when the filesystem must attempt to drop some or all of the buffers from the page when it is being truncated. It returns zero on success. If ->invalidatepage is zero, the kernel uses -block_invalidatepage() instead. +block_invalidatepage() instead. The filesystem should exclusively acquire +i_mapping_sem before invalidating page cache in truncate / hole punch path (and +thus calling into ->invalidatepage) to block races between page cache +invalidation and page cache filling functions (fault, read, ...). ->releasepage() is called when the kernel is about to try to drop the buffers from the page in preparation for freeing it. It returns zero to
@@ -567,6 +570,19 @@ in sys_read() and friends. the lease within the individual filesystem to record the result of the operation +->fallocate implementation must be really careful to maintain page cache +consistency when punching holes or performing other operations that invalidate +page cache contents. Usually the filesystem needs to call +truncate_inode_pages_range() to invalidate relevant range of the page cache. +However the filesystem usually also needs to update its internal (and on disk) +view of file offset -> disk block mapping. Until this update is finished, the +filesystem needs to block page faults and reads from reloading now-stale page +cache contents from the disk. VFS provides inode->i_mapping_sem for this and +acquires it in shared mode in paths loading pages from disk (filemap_fault(), +filemap_read(), readahead paths). The filesystem is responsible for taking this +lock in its fallocate implementation and generally whenever the page cache +contents needs to be invalidated because a block is moving from under a page. + dquot_operations ================
@@ -628,9 +644,9 @@ access: yes to be faulted in. The filesystem must find and return the page associated with the passed in "pgoff" in the vm_fault structure. If it is possible that the page may be truncated and/or invalidated, then the filesystem must lock -the page, then ensure it is not already truncated (the page lock will block -subsequent truncate), and then return with VM_FAULT_LOCKED, and the page -locked. The VM will unlock the page. +i_mapping_sem, then ensure the page is not already truncated (i_mapping_sem +will block subsequent truncate), and then return with VM_FAULT_LOCKED, and the +page locked. The VM will unlock the page. ->map_pages() is called when VM asks to map easy accessible pages. Filesystem should find and map pages associated with offsets from "start_pgoff"
fs/inode.c +3 -0
@@ -175,6 +175,9 @@ int inode_init_always(struct super_block *sb, struct inode *inode)init_rwsem (&inode->i_rwsem);lockdep_set_class (&inode->i_rwsem, &sb->s_type->i_mutex_key); +init_rwsem (&inode->i_mapping_sem); +lockdep_set_class (&inode->i_mapping_sem, + &sb->s_type->i_mapping_sem_key);atomic_set (&inode->i_dio_count,0 );
include/linux/fs.h +2 -0
@@ -660,6 +660,7 @@ struct inode {/* Misc */ unsigned long i_state;struct rw_semaphore i_rwsem; +struct rw_semaphore i_mapping_sem;unsigned long dirtied_when;/* jiffies of first dirtying */ unsigned long dirtied_time_when;
@@ -2351,6 +2352,7 @@ struct file_system_type {struct lock_class_key i_lock_key;struct lock_class_key i_mutex_key; +struct lock_class_key i_mapping_sem_key;struct lock_class_key i_mutex_dir_key; };
mm/filemap.c +47 -4
137 lines not on this page · raw message
mm/readahead.c +2 -0
@@ -192,6 +192,7 @@ void page_cache_ra_unbounded(struct readahead_control *ractl,*/ unsigned int nofs =memalloc_nofs_save (); +down_read (&mapping->host->i_mapping_sem);/* * Preallocate as many pages as we will need. */
@@ -236,6 +237,7 @@ void page_cache_ra_unbounded(struct readahead_control *ractl,* will then handle the error. */ read_pages (ractl, &page_pool, false); +up_read (&mapping->host->i_mapping_sem);memalloc_nofs_restore (nofs); }EXPORT_SYMBOL_GPL (page_cache_ra_unbounded);
mm/rmap.c +19 -18
@@ -22,24 +22,25 @@* * inode->i_rwsem (while writing or truncating, not reading or faulting) * mm->mmap_lock -* page->flags PG_locked (lock_page) * (see hugetlbfs below) -* hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share) -* mapping->i_mmap_rwsem -* hugetlb_fault_mutex (hugetlbfs specific page fault mutex) -* anon_vma->rwsem -* mm->page_table_lock or pte_lock -* swap_lock (in swap_duplicate, swap_info_get) -* mmlist_lock (in mmput, drain_mmlist and others) -* mapping->private_lock (in __set_page_dirty_buffers) -* lock_page_memcg move_lock (in __set_page_dirty_buffers) -* i_pages lock (widely used) -* lruvec->lru_lock (in lock_page_lruvec_irq) -* inode->i_lock (in set_page_dirty's __mark_inode_dirty) -* bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty) -* sb_lock (within inode_lock in fs/fs-writeback.c) -* i_pages lock (widely used, in set_page_dirty, -* in arch-dependent flush_dcache_mmap_lock, -* within bdi.wb->list_lock in __sync_single_inode) +* inode->i_mapping_sem (in filemap_fault) +* page->flags PG_locked (lock_page) * (see hugetlbfs below) +* hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share) +* mapping->i_mmap_rwsem +* hugetlb_fault_mutex (hugetlbfs specific page fault mutex) +* anon_vma->rwsem +* mm->page_table_lock or pte_lock +* swap_lock (in swap_duplicate, swap_info_get) +* mmlist_lock (in mmput, drain_mmlist and others) +* mapping->private_lock (in __set_page_dirty_buffers) +* lock_page_memcg move_lock (in __set_page_dirty_buffers) +* i_pages lock (widely used) +* lruvec->lru_lock (in lock_page_lruvec_irq) +* inode->i_lock (in set_page_dirty's __mark_inode_dirty) +* bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty) +* sb_lock (within inode_lock in fs/fs-writeback.c) +* i_pages lock (widely used, in set_page_dirty, +* in arch-dependent flush_dcache_mmap_lock, +* within bdi.wb->list_lock in __sync_single_inode) * * anon_vma->rwsem,mapping->i_mmap_rwsem (memory_failure, collect_procs_anon) * ->tasklist_lock
mm/truncate.c +1 -1
@@ -416,7 +416,7 @@ EXPORT_SYMBOL(truncate_inode_pages_range);* @mapping: mapping to truncate * @lstart: offset from which to truncate * -* Called under (and serialised by) inode->i_rwsem. +* Called under (and serialised by) inode->i_rwsem and inode->i_mapping_rwsem. * * Note: When this function returns, there can be a page in the process of * deletion (inside __delete_from_page_cache()) in the specified range. Thus
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Re: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2320
Quoted 4 lines
mm/filemap.c +1
-0if (error ==AOP_TRUNCATED_PAGE )put_page (page); +up_read (&mapping->host->i_mapping_sem);return error;
Please add an unlock_mapping label above this up_read and consolidate most of the other unlocks by jumping there (put_and_wait_on_page_locked probablt can't use it).
Review static int filemap_update_page(struct kiocb *iocb, · line 2324
Quoted 4 lines
truncated:
(quoted diff) +0
-0unlock_page(page);@@ -2309,6 +2324,7 @@ static int filemap_update_page(struct kiocb *iocb, return AOP_TRUNCATED_PAGE;
The trunated case actually seems to miss the unlock.
Similarly I think filemap_fault would benefit from a common unlock path.
Jan Kara
maintainerRe: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
Quoted 8 lines · Christoph Hellwig
mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2320
Quoted 4 lines
mm/filemap.c +1
-0if (error ==AOP_TRUNCATED_PAGE )put_page (page); +up_read (&mapping->host->i_mapping_sem);return error;Please add an
unlock_mappinglabel above thisup_readand consolidate most of the other unlocks by jumping there (put_and_wait_on_page_lockedprobablt can't use it).
Yeah, I've actually simplified the labels even a bit more like:
...
error = filemap_read_page(iocb->ki_filp, mapping, page);
goto unlock_mapping;
unlock:
unlock_page(page);
unlock_mapping:
up_read(&mapping->host->i_mapping_sem);
if (error == AOP_TRUNCATED_PAGE)
put_page(page);
return error;and everything now jumps to either unlock or unlock_mapping (except for put_and_wait_on_page_locked() case).
Quoted 9 lines
Review static int filemap_update_page(struct kiocb *iocb, · line 2324
Quoted 4 lines
truncated:
(quoted diff) +0
-0unlock_page(page);@@ -2309,6 +2324,7 @@ static int filemap_update_page(struct kiocb *iocb, return AOP_TRUNCATED_PAGE;The trunated case actually seems to miss the unlock.
Similarly I think
filemap_faultwould benefit from a common unlock path.
Right, thanks for catching that!
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
In reply to Jan KaraQuoted 23 lines · Jan Kara
Currently, serializing operations such as page fault, read, or readahead against hole punching is rather difficult. The basic race scheme is like:
fallocate(FALLOC_FL_PUNCH_HOLE)read / fault / ..
truncate_inode_pages_range()
<create pages in page
cache here>
<update fs block mapping and free blocks>Now the problem is in this way read / page fault / readahead can instantiate pages in page cache with potentially stale data (if blocks get quickly reused). Avoiding this race is not simple - page locks do not work because we want to make sure there are *no* pages in given range.
inode->i_rwsemdoes not work because page fault happens undermmap_semwhich ranks belowinode->i_rwsem. Also using it for reads makes the performance for mixed read-write workloads suffer.So create a new
rw_semaphorein the inode -i_mapping_sem- that protects adding of pages to page cache for page faults / reads / readahead.Signed-off-by Jan Kara <[email protected]>
....
mm/filemap.c line 81
Quoted 11 lines
mm/filemap.c +2
-1@@ -77,7 +77,8 @@* ->i_pages lock * * ->i_rwsem -* ->i_mmap_rwsem (truncate->unmap_mapping_range) +* ->i_mapping_sem (acquired by fs in truncate path) +* ->i_mmap_rwsem (truncate->unmap_mapping_range)
This is now officially a confusing mess.
We have inode->i_mapping that points to an address space, which has an internal i_mmap variable and an i_mmap_rwsem that serialises access to the i_mmap tree.
Now we have a inode->i_mapping_sem (which is actually a rwsem, not a sem) that sorta serialises additions to the mapping tree (inode->i_mapping->i_pages) against truncate, but it does not serialise accesses to the rest of the inode->i_mapping structure itself despite the similarlities in naming.
Then we have the inode_lock() API and the i_mmap_lock() API that wrap around the i_rwsem and i_mmap_rwsem, but there's no API for inode_mapping_lock()...
THen we have the mmap_lock in the page fault path as well, which is also an rwsem despite the name, and that protects something completely different to the i_mmap and the i_mapping.
IOWs, we have 4 layers of entwined structures and locking here that pretty much all have the same name but protect different things and not always the obvious thing the name suggests. This makes it really difficult to actually read the code and understand that the correct lock is being used in the correct place...
Quoted 14 lines
mm/filemap.c +2
-1* * ->mmap_lock * ->i_mmap_rwsem @@ -85,7 +86,8 @@* ->i_pages lock (arch-dependent flush_dcache_mmap_lock) * * ->mmap_lock -* ->lock_page (access_process_vm) +* ->i_mapping_sem (filemap_fault) +* ->lock_page (filemap_fault, access_process_vm) * * ->i_rwsem (generic_perform_write) * ->mmap_lock (fault_in_pages_readable->do_page_fault)
mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2286
Quoted 9 lines
mm/filemap.c +6
-0@@ -2276,16 +2278,28 @@ static int filemap_update_page(struct kiocb *iocb, {int error; +if (iocb->ki_flags &IOCB_NOWAIT ) { +if (!down_read_trylock (&mapping->host->i_mapping_sem)) +return -EAGAIN ; + }else { +down_read (&mapping->host->i_mapping_sem); + }
We really need a lock primitive for this. The number of times this exact lock pattern is being replicated all through the IO path is getting out of hand.
static inline bool down_read_try_or_lock(struct rwsem *sem, bool try)
{
if (try) {
if (!down_read_trylock(sem))
return false;
} else {
down_read(&mapping->host->i_mapping_sem);
}
return true;
}and the callers become:
if (!down_read_try_or_lock(sem, (iocb->ki_flags & IOCB_NOWAIT))) return -EAGAIN;
We can do the same with mutex_try_or_lock(), down_try_or_lock(), etc and we don't need to rely on cargo cult knowledge to propagate this pattern anymore. Because I'm betting relatively few people actually know why the code is written this way because the only place it is documented is in an XFS commit message....
Doing this is a separate cleanup, though, and not something that needs to be done in this patchset.
mm/readahead.c void page_cache_ra_unbounded(struct readahead_control *ractl, · line 198
Quoted 11 lines
index
c5b0457415be..ac5bb50b3a4c100644mm/readahead.c +1
-0@@ -192,6 +192,7 @@ void page_cache_ra_unbounded(struct readahead_control *ractl,*/ unsigned int nofs =memalloc_nofs_save (); +down_read (&mapping->host->i_mapping_sem);/* * Preallocate as many pages as we will need. */
I can't say I'm a great fan of having the mapping reach back up to the host to lock the host. THis seems the wrong way around to me given that most of the locking in the IO path is in "host locks mapping" and "mapping locks internal mapping structures" order...
I also come back to the naming confusion here, in that when we look at this in long hand from the inode perspective, this chain actually looks like:
lock(inode->i_mapping->inode->i_mapping_sem)
i.e. the mapping is reaching back up outside it's scope to lock itself against other inode->i_mapping operations. Smells of layering violations to me.
So, next question: should this truncate semanphore actually be part of the address space, not the inode? This patch is actually moving the page fault serialisation from the inode into the address space operations when page faults and page cache operations are done, so maybe the lock should also make that move? That would help clear up the naming problem, because now we can name it based around what it serialises in the address space, not the address space as a whole...
Cheers,
Dave.
Signature · 2 lines
Dave Chinner [email protected]
Jan Kara
maintainerRe: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
Quoted 46 lines · Dave Chinner
Quoted 14 lines · Jan Kara
*
* ->mmap_lock
* ->i_mmap_rwsemmm/filemap.c +2
-1@@ -85,7 +86,8 @@* ->i_pages lock (arch-dependent flush_dcache_mmap_lock) * * ->mmap_lock -* ->lock_page (access_process_vm) +* ->i_mapping_sem (filemap_fault) +* ->lock_page (filemap_fault, access_process_vm) * * ->i_rwsem (generic_perform_write) * ->mmap_lock (fault_in_pages_readable->do_page_fault) mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2286
Quoted 9 lines
mm/filemap.c +6
-0@@ -2276,16 +2278,28 @@ static int filemap_update_page(struct kiocb *iocb, {int error; +if (iocb->ki_flags &IOCB_NOWAIT ) { +if (!down_read_trylock (&mapping->host->i_mapping_sem)) +return -EAGAIN ; + }else { +down_read (&mapping->host->i_mapping_sem); + }We really need a lock primitive for this. The number of times this exact lock pattern is being replicated all through the IO path is getting out of hand.
static inline bool
down_read_try_or_lock(struct rwsem *sem, bool try){ if (try) { if (!down_read_trylock(sem)) return false; } else { down_read(&mapping->host->i_mapping_sem); } return true; }and the callers become:
if (!down_read_try_or_lock(sem, (iocb->ki_flags & IOCB_NOWAIT))) return -EAGAIN;
Quoted 8 lines
We can do the same with
mutex_try_or_lock(),down_try_or_lock(), etc and we don't need to rely on cargo cult knowledge to propagate this pattern anymore. Because I'm betting relatively few people actually know why the code is written this way because the only place it is documented is in an XFS commit message....Doing this is a separate cleanup, though, and not something that needs to be done in this patchset.
Yep, good idea but let's do it in a separate patch set.
Quoted 27 lines
mm/readahead.c void page_cache_ra_unbounded(struct readahead_control *ractl, · line 198
Quoted 11 lines
index
c5b0457415be..ac5bb50b3a4c100644mm/readahead.c +1
-0@@ -192,6 +192,7 @@ void page_cache_ra_unbounded(struct readahead_control *ractl,*/ unsigned int nofs =memalloc_nofs_save (); +down_read (&mapping->host->i_mapping_sem);/* * Preallocate as many pages as we will need. */ I can't say I'm a great fan of having the mapping reach back up to the host to lock the host. THis seems the wrong way around to me given that most of the locking in the IO path is in "host locks mapping" and "mapping locks internal mapping structures" order...
I also come back to the naming confusion here, in that when we look at this in long hand from the inode perspective, this chain actually looks like:
lock(inode->i_mapping->inode->i_mapping_sem)i.e. the mapping is reaching back up outside it's scope to lock itself against other
inode->i_mappingoperations. Smells of layering violations to me.
Quoted 7 lines
So, next question: should this truncate semanphore actually be part of the address space, not the inode? This patch is actually moving the page fault serialisation from the inode into the address space operations when page faults and page cache operations are done, so maybe the lock should also make that move? That would help clear up the naming problem, because now we can name it based around what it serialises in the address space, not the address space as a whole...
I think that moving the lock to address_space makes some sence although the lock actually protects consistency of inode->i_mapping->i_pages with whatever the filesystem has in its file_offset->disk_block mapping structures (which are generally associated with the inode). So it is not only about inode->i_mapping contents but I agree that struct address_space is probably a bit more logical place than struct inode.
Regarding the name: How about i_pages_rwsem? The lock is protecting invalidation of mapping->i_pages and needs to be held until insertion of pages into i_pages is safe again...
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Re: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
Quoted 37 lines · Jan Kara
Quoted 28 lines · Dave Chinner
mm/readahead.c void page_cache_ra_unbounded(struct readahead_control *ractl, · line 198
Quoted 11 lines · Jan Kara
index
c5b0457415be..ac5bb50b3a4c100644mm/readahead.c +1
-0@@ -192,6 +192,7 @@ void page_cache_ra_unbounded(struct readahead_control *ractl,*/ unsigned int nofs =memalloc_nofs_save (); +down_read (&mapping->host->i_mapping_sem);/* * Preallocate as many pages as we will need. */ I can't say I'm a great fan of having the mapping reach back up to the host to lock the host. THis seems the wrong way around to me given that most of the locking in the IO path is in "host locks mapping" and "mapping locks internal mapping structures" order...
I also come back to the naming confusion here, in that when we look at this in long hand from the inode perspective, this chain actually looks like:
lock(inode->i_mapping->inode->i_mapping_sem)i.e. the mapping is reaching back up outside it's scope to lock itself against other
inode->i_mappingoperations. Smells of layering violations to me.Quoted 7 lines
So, next question: should this truncate semanphore actually be part of the address space, not the inode? This patch is actually moving the page fault serialisation from the inode into the address space operations when page faults and page cache operations are done, so maybe the lock should also make that move? That would help clear up the naming problem, because now we can name it based around what it serialises in the address space, not the address space as a whole...
Quoted 4 lines
I think that moving the lock to
address_spacemakes some sence although the lock actually protects consistency ofinode->i_mapping->i_pageswith whatever the filesystem has in itsfile_offset->disk_blockmapping structures (which are generally associated with the inode).
Well, I look at is as a mechanism that the filesystem uses to ensure coherency of the page cache accesses w.r.t. physical layout changes. The layout is a property of the inode, but changes to the physical layout of the inode are serialised by other inode based mechanisms. THe page cache isn't part of the inode - it's part of the address space - but coherency with the inode is required. Hence inode operations need to be able to ensure coherency of the address space content and accesses w.r.t. physical layout changes of the inode, but the address space really knows nothing about the physical layout of the inode or how it gets changed...
Hence it's valid for the inode operations to lock the address space to ensure coherency of the page cache when making physical layout changes, but locking the address space, by itself, is not sufficient to safely serialise against physical changes to the inode layout.
Quoted 3 lines
So it is not
only aboutinode->i_mappingcontents but I agree that structaddress_spaceis probably a bit more logical place than struct inode.
Yup. Remember that the XFS_MMAPLOCK arose at the inode level because that was the only way the filesystem could acheive the necessary serialisation of page cache accesses whilst doing physical layout changes. So the lock became an "inode property" because of implementation constraints, not because it was the best way to implement the necessary coherency hooks.
Quoted 3 lines
Regarding the name: How about
i_pages_rwsem? The lock is protecting invalidation ofmapping->i_pagesand needs to be held until insertion of pages intoi_pagesis safe again...
I don't actually have a good name for this right now. :(
The i_pages structure has it's own internal locking, so i_pages_rwsem implies things that aren't necessarily true, and taking a read lock for insertion for something that is named like a structure protection lock creates cognitive dissonance...
I keep wanting to say "lock for invalidation" and "lock to exclude invalidation" because those are the two actions that we need for coherency of operations. But they are way too verbose for an actual API...
So I want to call this an "invalidation lock" of some kind (no need to encode the type in the name!), but haven't worked out a good shorthand for "address space invalidation coherency mechanism"...
Naming is hard. :/
Cheers,
Dave.
Signature · 2 lines
Dave Chinner [email protected]
Jan Kara
maintainerRe: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
Quoted 12 lines · Dave Chinner
Quoted 3 lines · Jan Kara
Regarding the name: How about
i_pages_rwsem? The lock is protecting invalidation ofmapping->i_pagesand needs to be held until insertion of pages intoi_pagesis safe again...I don't actually have a good name for this right now. :(
The
i_pagesstructure has it's own internal locking, soi_pages_rwsemimplies things that aren't necessarily true, and taking a read lock for insertion for something that is named like a structure protection lock creates cognitive dissonance...
Quoted 8 lines
I keep wanting to say "lock for invalidation" and "lock to exclude invalidation" because those are the two actions that we need for coherency of operations. But they are way too verbose for an actual API...
So I want to call this an "invalidation lock" of some kind (no need to encode the type in the name!), but haven't worked out a good shorthand for "address space invalidation coherency mechanism"...
So "invalidate_lock" was just next on my list of things to suggest so I'm fine with that name. Or maybe block_invalidate_lock, block_remove_lock, map_remove_lock, ... Dunno :).
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Matthew Wilcox
maintainerRe: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
In reply to Dave ChinnerQuoted 23 lines · Dave Chinner
mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2286
Quoted 6 lines
mm/filemap.c +6
-0+if (iocb->ki_flags &IOCB_NOWAIT ) { +if (!down_read_trylock (&mapping->host->i_mapping_sem)) +return -EAGAIN ; + }else { +down_read (&mapping->host->i_mapping_sem); + }We really need a lock primitive for this. The number of times this exact lock pattern is being replicated all through the IO path is getting out of hand.
static inline bool
down_read_try_or_lock(struct rwsem *sem, bool try){ if (try) { if (!down_read_trylock(sem)) return false; } else { down_read(&mapping->host->i_mapping_sem); } return true; }
Quoted 4 lines
and the callers become:
if (!down_read_try_or_lock(sem, (iocb->ki_flags & IOCB_NOWAIT))) return -EAGAIN;
I think that should be written:
if (!iocb_read_lock(iocb, &rwsem)) return -EAGAIN;
and implemented as:
static inline int iocb_read_lock(struct kiocb *iocb, struct rwsem *sem)
{
if (iocb->ki_flags & IOCB_NOWAIT)
return down_read_trylock(sem) ? 0 : -EAGAIN;
return down_read_killable(sem);
}Re: [PATCH 2/7] mm: Protect operations adding pages to page cache with i_mapping_lock
Quoted 34 lines · Matthew Wilcox
Quoted 23 lines · Dave Chinner
mm/filemap.c static int filemap_update_page(struct kiocb *iocb, · line 2286
Quoted 6 lines
mm/filemap.c +6
-0+if (iocb->ki_flags &IOCB_NOWAIT ) { +if (!down_read_trylock (&mapping->host->i_mapping_sem)) +return -EAGAIN ; + }else { +down_read (&mapping->host->i_mapping_sem); + }We really need a lock primitive for this. The number of times this exact lock pattern is being replicated all through the IO path is getting out of hand.
static inline bool
down_read_try_or_lock(struct rwsem *sem, bool try){ if (try) { if (!down_read_trylock(sem)) return false; } else { down_read(&mapping->host->i_mapping_sem); } return true; }Quoted 4 lines
and the callers become:
if (!down_read_try_or_lock(sem, (iocb->ki_flags & IOCB_NOWAIT))) return -EAGAIN;I think that should be written:
if (!iocb_read_lock(iocb, &rwsem)) return -EAGAIN;
Quoted 8 lines
and implemented as:
static inline int
iocb_read_lock(struct kiocb *iocb, struct rwsem *sem){ if (iocb->ki_flags & IOCB_NOWAIT) return down_read_trylock(sem) ? 0 : -EAGAIN; return down_read_killable(sem); }
Yup, we already have done that with xfs_ilock_iocb(), but my point is that this "non blocking try lock or lock" pattern is slowly being used in more places than just IOCB_NOWAIT situations. e.g. We use if for IOMAP_NOWAIT locking in XFS, too, and ISTR other places where optimisitic locking is used are replicating it, too.
Hence my suggestion that is moved up into the locking primitives, not merely have context specific wrappers added...
Cheers,
Dave.
Signature · 2 lines
Dave Chinner [email protected]
Jan Kara
maintainerPATCH v3 3/7 ext4: Convert to use inode->i_mapping_sem
In reply to Jan KaraConvert ext4 to use inode->i_mapping_sem instead of its private EXT4_I(inode)->i_mmap_sem. This is mostly search-and-replace. By this conversion we fix a long standing race between hole punching and read(2) / readahead(2) paths that can lead to stale page cache contents.
--- fs/ext4/ext4.h | 10 ---------- fs/ext4/extents.c | 18 +++++++++--------- fs/ext4/file.c | 12 ++++++------ fs/ext4/inode.c | 47 +++++++++++++++++----------------------------- fs/ext4/ioctl.c | 4 ++-- fs/ext4/super.c | 11 ++++------- fs/ext4/truncate.h | 4 ++-- 7 files changed, 40 insertions(+), 66 deletions(-)
fs/ext4/ext4.h +0 -10
@@ -1081,15 +1081,6 @@ struct ext4_inode_info {* by other means, so we have i_data_sem. */ struct rw_semaphore i_data_sem; -/* -* i_mmap_sem is for serializing page faults with truncate / punch hole -* operations. We have to make sure that new page cannot be faulted in -* a section of the inode that is being punched. We cannot easily use -* i_data_sem for this since we need protection for the whole punch -* operation and i_data_sem ranks below transaction start so we have -* to occasionally drop it. -*/ -struct rw_semaphore i_mmap_sem;struct inode vfs_inode;struct jbd2_inode *jinode;
@@ -2908,7 +2899,6 @@ extern int ext4_chunk_trans_blocks(struct inode *, int nrblocks);extern int ext4_zero_partial_blocks (handle_t *handle,struct inode *inode,loff_t lstart,loff_t lend);extern vm_fault_t ext4_page_mkwrite (struct vm_fault *vmf); -extern vm_fault_t ext4_filemap_fault (struct vm_fault *vmf);extern qsize_t *ext4_get_reserved_space (struct inode *inode);extern int ext4_get_projid (struct inode *inode,kprojid_t *projid);extern void ext4_da_release_space (struct inode *inode,int to_free);
fs/ext4/extents.c +9 -9
@@ -4553,17 +4553,17 @@ static long ext4_zero_range(struct file *file, loff_t offset,* Prevent page faults from reinstantiating pages we have * released from page cache. */ -down_write (&EXT4_I (inode)->i_mmap_sem); +down_write (&inode->i_mapping_sem); ret =ext4_break_layouts (inode);if (ret) { -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); goto out_mutex; } ret =ext4_update_disksize_before_punch (inode, offset, len);if (ret) { -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); goto out_mutex; }/* Now release the pages and zero block aligned part of pages */
@@ -4572,7 +4572,7 @@ static long ext4_zero_range(struct file *file, loff_t offset, ret =ext4_alloc_file_blocks (file, lblk, max_blocks, new_size, flags); -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem);if (ret) goto out_mutex; }
@@ -5267,7 +5267,7 @@ static int ext4_collapse_range(struct inode *inode, loff_t offset, loff_t len)* Prevent page faults from reinstantiating pages we have released from * page cache. */ -down_write (&EXT4_I (inode)->i_mmap_sem); +down_write (&inode->i_mapping_sem); ret =ext4_break_layouts (inode);if (ret)
@@ -5288,7 +5288,7 @@ static int ext4_collapse_range(struct inode *inode, loff_t offset, loff_t len)/* * Write data that will be shifted to preserve them when discarding * page cache below. We are also protected from pages becoming dirty -* by i_mmap_sem. +* by i_mapping_sem. */ ret =filemap_write_and_wait_range (inode->i_mapping, offset + len,LLONG_MAX );
@@ -5343,7 +5343,7 @@ static int ext4_collapse_range(struct inode *inode, loff_t offset, loff_t len)ext4_journal_stop (handle);ext4_fc_stop_ineligible (sb); out_mmap: -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); out_mutex:inode_unlock (inode);return ret;
@@ -5418,7 +5418,7 @@ static int ext4_insert_range(struct inode *inode, loff_t offset, loff_t len)* Prevent page faults from reinstantiating pages we have released from * page cache. */ -down_write (&EXT4_I (inode)->i_mmap_sem); +down_write (&inode->i_mapping_sem); ret =ext4_break_layouts (inode);if (ret)
@@ -5519,7 +5519,7 @@ static int ext4_insert_range(struct inode *inode, loff_t offset, loff_t len)ext4_journal_stop (handle);ext4_fc_stop_ineligible (sb); out_mmap: -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); out_mutex:inode_unlock (inode);return ret;
fs/ext4/file.c +6 -6
@@ -692,17 +692,17 @@ static vm_fault_t ext4_dax_huge_fault(struct vm_fault *vmf,if (write) {sb_start_pagefault (sb);file_update_time (vmf->vma->vm_file); -down_read (&EXT4_I (inode)->i_mmap_sem); +down_read (&inode->i_mapping_sem); retry: handle =ext4_journal_start_sb (sb,EXT4_HT_WRITE_PAGE ,EXT4_DATA_TRANS_BLOCKS (sb));if (IS_ERR (handle)) { -up_read (&EXT4_I (inode)->i_mmap_sem); +up_read (&inode->i_mapping_sem);sb_end_pagefault (sb);return VM_FAULT_SIGBUS ; } }else { -down_read (&EXT4_I (inode)->i_mmap_sem); +down_read (&inode->i_mapping_sem); } result =dax_iomap_fault (vmf, pe_size, &pfn, &error, &ext4_iomap_ops);if (write) {
@@ -714,10 +714,10 @@ static vm_fault_t ext4_dax_huge_fault(struct vm_fault *vmf,/* Handling synchronous page fault? */ if (result &VM_FAULT_NEEDDSYNC ) result =dax_finish_sync_fault (vmf, pe_size, pfn); -up_read (&EXT4_I (inode)->i_mmap_sem); +up_read (&inode->i_mapping_sem);sb_end_pagefault (sb); }else { -up_read (&EXT4_I (inode)->i_mmap_sem); +up_read (&inode->i_mapping_sem); }return result;
@@ -739,7 +739,7 @@ static const struct vm_operations_struct ext4_dax_vm_ops = {#endif static const struct vm_operations_struct ext4_file_vm_ops = { - .fault = ext4_filemap_fault, + .fault = filemap_fault, .map_pages = filemap_map_pages, .page_mkwrite = ext4_page_mkwrite, };
fs/ext4/inode.c +17 -30
131 lines not on this page · raw message
fs/ext4/ioctl.c +2 -2
@@ -147,7 +147,7 @@ static long swap_inode_boot_loader(struct super_block *sb, goto journal_err_out; } -down_write (&EXT4_I (inode)->i_mmap_sem); +down_write (&inode->i_mapping_sem); err =filemap_write_and_wait (inode->i_mapping);if (err) goto err_out;
@@ -255,7 +255,7 @@ static long swap_inode_boot_loader(struct super_block *sb,ext4_double_up_write_data_sem (inode, inode_bl); err_out: -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); journal_err_out:unlock_two_nondirectories (inode, inode_bl);iput (inode_bl);
fs/ext4/super.c +4 -7
@@ -90,11 +90,8 @@ static struct inode *ext4_get_journal_inode(struct super_block *sb,/* * Lock ordering * -* Note the difference between i_mmap_sem (EXT4_I(inode)->i_mmap_sem) and -* i_mmap_rwsem (inode->i_mmap_rwsem)! -* * page fault path: -* mmap_lock -> sb_start_pagefault -> i_mmap_sem (r) -> transaction start -> +* mmap_lock -> sb_start_pagefault -> i_mapping_sem (r) -> transaction start -> * page lock -> i_data_sem (rw) * * buffered write path:
@@ -103,8 +100,9 @@ static struct inode *ext4_get_journal_inode(struct super_block *sb,* i_data_sem (rw) * * truncate: -* sb_start_write -> i_mutex -> i_mmap_sem (w) -> i_mmap_rwsem (w) -> page lock -* sb_start_write -> i_mutex -> i_mmap_sem (w) -> transaction start -> +* sb_start_write -> i_mutex -> i_mapping_sem (w) -> i_mmap_rwsem (w) -> +* page lock +* sb_start_write -> i_mutex -> i_mapping_sem (w) -> transaction start -> * i_data_sem (rw) * * direct IO:
@@ -1349,7 +1347,6 @@ static void init_once(void *foo)INIT_LIST_HEAD (&ei->i_orphan);init_rwsem (&ei->xattr_sem);init_rwsem (&ei->i_data_sem); -init_rwsem (&ei->i_mmap_sem);inode_init_once (&ei->vfs_inode);ext4_fc_init_inode (&ei->vfs_inode); }
fs/ext4/truncate.h +2 -2
@@ -15,10 +15,10 @@ static inline void ext4_truncate_failed_write(struct inode *inode)* We don't need to call ext4_break_layouts() because the blocks we * are truncating were never visible to userspace. */ -down_write (&EXT4_I (inode)->i_mmap_sem); +down_write (&inode->i_mapping_sem);truncate_inode_pages (inode->i_mapping, inode->i_size);ext4_truncate (inode); -up_write (&EXT4_I (inode)->i_mmap_sem); +up_write (&inode->i_mapping_sem); }/*
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Jan Kara
maintainerPATCH v3 4/7 ext2: Convert to using i_mapping_sem
In reply to Jan KaraExt2 has its private dax_sem used for synchronizing page faults and truncation. Use inode->i_mapping_sem instead as it is meant for this purpose.
--- fs/ext2/ext2.h | 11 ----------- fs/ext2/file.c | 7 +++---- fs/ext2/inode.c | 12 ++++++------ fs/ext2/super.c | 3 --- 4 files changed, 9 insertions(+), 24 deletions(-)
fs/ext2/ext2.h +0 -11
@@ -671,9 +671,6 @@ struct ext2_inode_info {struct rw_semaphore xattr_sem;#endif rwlock_t i_meta_lock; -#ifdef CONFIG_FS_DAX -struct rw_semaphore dax_sem; -#endif /* * truncate_mutex is for serialising ext2_truncate() against
@@ -689,14 +686,6 @@ struct ext2_inode_info {#endif }; -#ifdef CONFIG_FS_DAX -#define dax_sem_down_write (ext2_inode) down_write(&(ext2_inode)->dax_sem) -#define dax_sem_up_write (ext2_inode) up_write(&(ext2_inode)->dax_sem) -#else -#define dax_sem_down_write (ext2_inode) -#define dax_sem_up_write (ext2_inode) -#endif -/* * Inode dynamic state flags */
fs/ext2/file.c +3 -4
@@ -81,7 +81,7 @@ static ssize_t ext2_dax_write_iter(struct kiocb *iocb, struct iov_iter *from)* * mmap_lock (MM) * sb_start_pagefault (vfs, freeze) -* ext2_inode_info->dax_sem +* inode->i_mapping_sem * address_space->i_mmap_rwsem or page_lock (mutually exclusive in DAX) * ext2_inode_info->truncate_mutex *
@@ -91,7 +91,6 @@ static ssize_t ext2_dax_write_iter(struct kiocb *iocb, struct iov_iter *from)static vm_fault_t ext2_dax_fault (struct vm_fault *vmf) {struct inode *inode =file_inode (vmf->vma->vm_file); -struct ext2_inode_info *ei =EXT2_I (inode);vm_fault_t ret;bool write = (vmf->flags &FAULT_FLAG_WRITE ) && (vmf->vma->vm_flags &VM_SHARED );
@@ -100,11 +99,11 @@ static vm_fault_t ext2_dax_fault(struct vm_fault *vmf)sb_start_pagefault (inode->i_sb);file_update_time (vmf->vma->vm_file); } -down_read (&ei->dax_sem); +down_read (&inode->i_mapping_sem); ret =dax_iomap_fault (vmf,PE_SIZE_PTE ,NULL ,NULL , &ext2_iomap_ops); -up_read (&ei->dax_sem); +up_read (&inode->i_mapping_sem);if (write)sb_end_pagefault (inode->i_sb);return ret;
fs/ext2/inode.c +6 -6
@@ -1175,7 +1175,7 @@ static void ext2_free_branches(struct inode *inode, __le32 *p, __le32 *q, int deext2_free_data (inode, p, q); } -/* dax_sem must be held when calling this function */ +/* inode->i_mapping_sem must be held when calling this function */ static void __ext2_truncate_blocks (struct inode *inode,loff_t offset) {__le32 *i_data =EXT2_I (inode)->i_data;
@@ -1192,7 +1192,7 @@ static void __ext2_truncate_blocks(struct inode *inode, loff_t offset) iblock = (offset + blocksize-1 ) >>EXT2_BLOCK_SIZE_BITS (inode->i_sb);#ifdef CONFIG_FS_DAX -WARN_ON (!rwsem_is_locked (&ei->dax_sem)); +WARN_ON (!rwsem_is_locked (&inode->i_mapping_sem));#endif n =ext2_block_to_path (inode, iblock, offsets,NULL );
@@ -1274,9 +1274,9 @@ static void ext2_truncate_blocks(struct inode *inode, loff_t offset)if (ext2_inode_is_fast_symlink (inode))return ; -dax_sem_down_write (EXT2_I (inode)); +down_write (&inode->i_mapping_sem);__ext2_truncate_blocks (inode, offset); -dax_sem_up_write (EXT2_I (inode)); +up_write (&inode->i_mapping_sem); }static int ext2_setsize (struct inode *inode,loff_t newsize)
@@ -1306,10 +1306,10 @@ static int ext2_setsize(struct inode *inode, loff_t newsize)if (error)return error; -dax_sem_down_write (EXT2_I (inode)); +down_write (&inode->i_mapping_sem);truncate_setsize (inode, newsize);__ext2_truncate_blocks (inode, newsize); -dax_sem_up_write (EXT2_I (inode)); +up_write (&inode->i_mapping_sem); inode->i_mtime = inode->i_ctime =current_time (inode);if (inode_needs_sync (inode)) {
fs/ext2/super.c +0 -3
@@ -206,9 +206,6 @@ static void init_once(void *foo)init_rwsem (&ei->xattr_sem);#endif mutex_init (&ei->truncate_mutex); -#ifdef CONFIG_FS_DAX -init_rwsem (&ei->dax_sem); -#endif inode_init_once (&ei->vfs_inode); }
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Jan Kara
maintainerPATCH v3 5/7 xfs: Convert to use i_mapping_sem
In reply to Jan KaraUse i_mapping_sem instead of XFS internal i_mmap_lock. The intended purpose of i_mapping_sem is exactly the same.
--- fs/xfs/xfs_file.c | 12 ++++++----- fs/xfs/xfs_inode.c | 51 +++++++++++++++++++++++++--------------------- fs/xfs/xfs_inode.h | 1 - fs/xfs/xfs_super.c | 2 -- 4 files changed, 35 insertions(+), 31 deletions(-)
fs/xfs/xfs_file.c +7 -5
@@ -1282,7 +1282,7 @@ xfs_file_llseek(* * mmap_lock (MM) * sb_start_pagefault(vfs, freeze) -* i_mmaplock (XFS - truncate serialisation) +* i_mapping_sem (XFS - truncate serialisation) * page_lock (MM) * i_lock (XFS - extent map serialisation) */
@@ -1303,24 +1303,26 @@ __xfs_filemap_fault(file_update_time (vmf->vma->vm_file); } -xfs_ilock (XFS_I (inode),XFS_MMAPLOCK_SHARED );if (IS_DAX (inode)) {pfn_t pfn; +xfs_ilock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); ret =dax_iomap_fault (vmf, pe_size, &pfn,NULL , (write_fault && !vmf->cow_page) ? &xfs_direct_write_iomap_ops : &xfs_read_iomap_ops);if (ret &VM_FAULT_NEEDDSYNC ) ret =dax_finish_sync_fault (vmf, pe_size, pfn); +xfs_iunlock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); }else { -if (write_fault) +if (write_fault) { +xfs_ilock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); ret =iomap_page_mkwrite (vmf, &xfs_buffered_write_iomap_ops); -else +xfs_iunlock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); + }else ret =filemap_fault (vmf); } -xfs_iunlock (XFS_I (inode),XFS_MMAPLOCK_SHARED );if (write_fault)sb_end_pagefault (inode->i_sb);
fs/xfs/xfs_inode.c +28 -23
111 lines not on this page · raw message
fs/xfs/xfs_inode.h +0 -1
@@ -40,7 +40,6 @@ typedef struct xfs_inode {/* Transaction and locking information. */ struct xfs_inode_log_item *i_itemp;/* logging information */ mrlock_t i_lock;/* inode lock */ -mrlock_t i_mmaplock;/* inode mmap IO lock */ atomic_t i_pincount;/* inode pin count */ /*
fs/xfs/xfs_super.c +0 -2
@@ -715,8 +715,6 @@ xfs_fs_inode_init_once(atomic_set (&ip->i_pincount,0 );spin_lock_init (&ip->i_flags_lock); -mrlock_init (&ip->i_mmaplock,MRLOCK_ALLOW_EQUAL_PRI |MRLOCK_BARRIER , -"xfsino" , ip->i_ino);mrlock_init (&ip->i_lock,MRLOCK_ALLOW_EQUAL_PRI |MRLOCK_BARRIER ,"xfsino" , ip->i_ino); }
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Re: [PATCH 5/7] xfs: Convert to use i_mapping_sem
Quoted 2 lines · Jan Kara
Use
i_mapping_seminstead of XFS internali_mmap_lock. The intended purpose ofi_mapping_semis exactly the same.
Might be worth mentioning here that the locking in __xfs_filemap_fault changes because filemap_fault already takes i_mapping_sem?
Quoted 4 lines
*
mmap_lock(MM)
*sb_start_pagefault(vfs, freeze)
- *i_mmaplock(XFS - truncate serialisation)
+ *i_mapping_sem(XFS - truncate serialisation)
This is sort of VFS now, isn't it?
Otherwise looks good:
Jan Kara
maintainerRe: [PATCH 5/7] xfs: Convert to use i_mapping_sem
Quoted 6 lines · Christoph Hellwig
Quoted 2 lines · Jan Kara
Use
i_mapping_seminstead of XFS internali_mmap_lock. The intended purpose ofi_mapping_semis exactly the same.Might be worth mentioning here that the locking in
__xfs_filemap_faultchanges becausefilemap_faultalready takesi_mapping_sem?
Sure, will add.
Quoted 7 lines
Quoted 5 lines
*
mmap_lock(MM)
*sb_start_pagefault(vfs, freeze)
- *i_mmaplock(XFS - truncate serialisation)
+ *i_mapping_sem(XFS - truncate serialisation)This is sort of VFS now, isn't it?
Right, I'll update the comment.
Quoted 3 lines
Otherwise looks good:
Reviewed-by Christoph Hellwig <[email protected]>
Thanks!
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Jan Kara
maintainerPATCH v3 6/7 zonefs: Convert to using i_mapping_sem
In reply to Jan KaraUse i_mapping_sem instead of zonefs' private i_mmap_sem. The intended purpose is exactly the same.
--- fs/zonefs/super.c | 23 +++++------------------ fs/zonefs/zonefs.h | 7 +++---- 2 files changed, 8 insertions(+), 22 deletions(-)
fs/zonefs/super.c +5 -18
@@ -462,7 +462,7 @@ static int zonefs_file_truncate(struct inode *inode, loff_t isize)inode_dio_wait (inode);/* Serialize against page faults */ -down_write (&zi->i_mmap_sem); +down_write (&inode->i_mapping_sem);/* Serialize against zonefs_iomap_begin() */ mutex_lock (&zi->i_truncate_mutex);
@@ -500,7 +500,7 @@ static int zonefs_file_truncate(struct inode *inode, loff_t isize) unlock:mutex_unlock (&zi->i_truncate_mutex); -up_write (&zi->i_mmap_sem); +up_write (&inode->i_mapping_sem);return ret; }
@@ -575,18 +575,6 @@ static int zonefs_file_fsync(struct file *file, loff_t start, loff_t end,return ret; } -static vm_fault_t zonefs_filemap_fault (struct vm_fault *vmf) -{ -struct zonefs_inode_info *zi =ZONEFS_I (file_inode (vmf->vma->vm_file)); -vm_fault_t ret; - -down_read (&zi->i_mmap_sem); - ret =filemap_fault (vmf); -up_read (&zi->i_mmap_sem); - -return ret; -} -static vm_fault_t zonefs_filemap_page_mkwrite (struct vm_fault *vmf) {struct inode *inode =file_inode (vmf->vma->vm_file);
@@ -607,16 +595,16 @@ static vm_fault_t zonefs_filemap_page_mkwrite(struct vm_fault *vmf)file_update_time (vmf->vma->vm_file);/* Serialize against truncates */ -down_read (&zi->i_mmap_sem); +down_read (&inode->i_mapping_sem); ret =iomap_page_mkwrite (vmf, &zonefs_iomap_ops); -up_read (&zi->i_mmap_sem); +up_read (&inode->i_mapping_sem);sb_end_pagefault (inode->i_sb);return ret; }static const struct vm_operations_struct zonefs_file_vm_ops = { - .fault = zonefs_filemap_fault, + .fault = filemap_fault, .map_pages = filemap_map_pages, .page_mkwrite = zonefs_filemap_page_mkwrite, };
@@ -1158,7 +1146,6 @@ static struct inode *zonefs_alloc_inode(struct super_block *sb)inode_init_once (&zi->i_vnode);mutex_init (&zi->i_truncate_mutex); -init_rwsem (&zi->i_mmap_sem); zi->i_wr_refcnt =0 ;return &zi->i_vnode;
fs/zonefs/zonefs.h +3 -4
@@ -70,12 +70,11 @@ struct zonefs_inode_info {* and changes to the inode private data, and in particular changes to * a sequential file size on completion of direct IO writes. * Serialization of mmap read IOs with truncate and syscall IO -* operations is done with i_mmap_sem in addition to i_truncate_mutex. -* Only zonefs_seq_file_truncate() takes both lock (i_mmap_sem first, -* i_truncate_mutex second). +* operations is done with i_mapping_sem in addition to +* i_truncate_mutex. Only zonefs_seq_file_truncate() takes both lock +* (i_mapping_sem first, i_truncate_mutex second). */ struct mutex i_truncate_mutex; -struct rw_semaphore i_mmap_sem;/* guarded by i_truncate_mutex */ unsigned int i_wr_refcnt;
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Jan Kara
maintainerPATCH v3 7/7 fs: Remove i_mapping_sem protection from .page_mkwrite handlers
In reply to Jan KaraUsing i_mapping_sem in .page_mkwrite handlers is generally unnecessary since these handlers do not create any new pages in the page cache. Thus if the handler locks the page and it is still attached to appropriate mapping, we are certain there cannot be truncate or hole punch in progress past its page cache invalidation step (remember that a page cannot be created in the page cache from the moment hole punch acquires i_mapping_sem and starts invalidating page cache to the moment the operation is finished and i_mapping_sem is released) and any page cache invalidation will wait until we release the page lock. So just remove i_mapping_sem usage from .page_mkwrite handlers.
--- fs/ext4/inode.c | 3 --- fs/xfs/xfs_file.c | 2 -- fs/zonefs/super.c | 3 --- 3 files changed, 8 deletions(-)
fs/ext4/inode.c +0 -3
@@ -6064,8 +6064,6 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)sb_start_pagefault (inode->i_sb);file_update_time (vma->vm_file); -down_read (&inode->i_mapping_sem); - err =ext4_convert_inline_data (inode);if (err) goto out_ret;
@@ -6177,7 +6175,6 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf) out_ret: ret =block_page_mkwrite_return (err); out: -up_read (&inode->i_mapping_sem);sb_end_pagefault (inode->i_sb);return ret; out_error:
fs/xfs/xfs_file.c +0 -2
@@ -1316,10 +1316,8 @@ __xfs_filemap_fault(xfs_iunlock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); }else {if (write_fault) { -xfs_ilock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); ret =iomap_page_mkwrite (vmf, &xfs_buffered_write_iomap_ops); -xfs_iunlock (XFS_I (inode),XFS_MMAPLOCK_SHARED ); }else ret =filemap_fault (vmf); }
fs/zonefs/super.c +0 -3
@@ -594,10 +594,7 @@ static vm_fault_t zonefs_filemap_page_mkwrite(struct vm_fault *vmf)sb_start_pagefault (inode->i_sb);file_update_time (vmf->vma->vm_file); -/* Serialize against truncates */ -down_read (&inode->i_mapping_sem); ret =iomap_page_mkwrite (vmf, &zonefs_iomap_ops); -up_read (&inode->i_mapping_sem);sb_end_pagefault (inode->i_sb);return ret;
Signature · 1 line
2.31.0.99.g0d91da736d9f.dirty
Quoted 9 lines
Also when writing the documentation I came across one question: Do we mandate
i_mapping_semfor truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to usei_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
I think all makes for much better to understand and document rules, so I'd shoot for that eventually.
Btw, what about locking for DAX faults? XFS seems to take the mmap sem for those as well currently.
Jan Kara
maintainerQuoted 10 lines · Christoph Hellwig
Quoted 9 lines
Also when writing the documentation I came across one question: Do we mandate
i_mapping_semfor truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to usei_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
Quoted 2 lines
I think all makes for much better to understand and document rules, so I'd shoot for that eventually.
OK.
Quoted 2 lines
Btw, what about locking for DAX faults? XFS seems to take the mmap sem for those as well currently.
Yes, I've mechanically converted all those uses to i_mapping_sem for XFS, ext4, and ext2 as well. Longer term we may be able to move some locking into generic DAX code now that the lock is in struct inode. But I want to leave that for later since DAX locking is different enough that it needs some careful thinking and justification...
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Matthew Wilcox
maintainerQuoted 9 lines · Jan Kara
Also when writing the documentation I came across one question: Do we mandate
i_mapping_semfor truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to usei_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
What do we actually get in return for supporting these complex fallocate operations? Someone added them for a reason, but does that reason actually benefit me? Other than running xfstests, how many times has holepunch been called on your laptop in the last week? I don't want to incur even one extra instruction per I/O operation to support something that happens twice a week; that's a bad tradeoff.
Can we implement holepunch as a NOP? Or return -ENOTTY? Those both seem like better solutions than adding an extra rwsem to every inode. Failing that, is there a bigger hammer we can use on the holepunch side (eg preventing all concurrent accesses while the holepunch is happening) to reduce the overhead on the read side?
Jan Kara
maintainerQuoted 11 lines · Matthew Wilcox
Quoted 9 lines · Jan Kara
Also when writing the documentation I came across one question: Do we mandate
i_mapping_semfor truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to usei_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
Quoted 6 lines
What do we actually get in return for supporting these complex fallocate operations? Someone added them for a reason, but does that reason actually benefit me? Other than running xfstests, how many times has holepunch been called on your laptop in the last week? I don't want to incur even one extra instruction per I/O operation to support something that happens twice a week; that's a bad tradeoff.
I agree hole punch is relatively rare compared to normal operations but when it is used, it is used rather frequently - e.g. by VMs to manage their filesystem images. So if we regress holepunch either by not freeing blocks or by slowing it down significantly, I'm pretty sure some people will complain. That being said I fully understand your reluctance to add lock to the read path but note that it is taken only when we need to fill data from the storage and it should be taken once per readahead request so I actually doubt the extra acquisition will be visible in the profiles. But I can profile it to be sure.
Quoted 2 lines
Can we implement holepunch as a NOP? Or return
-ENOTTY? Those both seem like better solutions than adding an extra rwsem to every inode.
We already have that rwsem there today for most major filesystems. This work just lifts it from fs-private inode area into the VFS inode. So in terms of memory usage we are not loosing that much.
Quoted 3 lines
Failing that, is there a bigger hammer we can use on the holepunch side (eg preventing all concurrent accesses while the holepunch is happening) to reduce the overhead on the read side?
I'm open to other solutions but frankly this was the best I could come up with. Holepunch already uses a pretty much big hammer approach - take all the locks there are on the inode in exclusive mode, block DIO, unmap everything and then do its dirty deeds... I don't think we want hole punch to block anything on fs-wide basis (that's a DoS recipe) and besides that I don't see how the hammer could be bigger ;).
Honza
Signature · 2 lines
Jan Kara <[email protected]> SUSE Labs, CR
Quoted 11 lines · Matthew Wilcox
Quoted 9 lines · Jan Kara
Also when writing the documentation I came across one question: Do we mandate
i_mapping_semfor truncate + hole punch for all filesystems or just for filesystems that support hole punching (or other complex fallocate operations)? I wrote the documentation so that we require every filesystem to usei_mapping_sem. This makes locking rules simpler, we can also add asserts when all filesystems are converted. The downside is that simple filesystems now pay the overhead of the locking unnecessary for them. The overhead is small (uncontended rwsem acquisition for truncate) so I don't think we care and the simplicity is worth it but I wanted to spell this out.
Quoted 4 lines
What do we actually get in return for supporting these complex fallocate operations? Someone added them for a reason, but does that reason actually benefit me? Other than running xfstests, how many times has holepunch been called on your laptop in the last week?
Quite a lot, actually.
Quoted 3 lines
I don't want to
incur even one extra instruction per I/O operation to support something that happens twice a week; that's a bad tradeoff.
Hole punching gets into all sorts of interesting places. For example, did you know that issuing fstrim (discards) or "write zeroes" on a file-backed loopback device will issue hole punches to the underlying file? nvmet does the same. Userspace iscsi server implementations (e.g. TGT) do the same thing and have for a long time. NFSv4 servers issue hole punching based on client side requests, too.
Then there's Kubernetes management tools. Samba. Qemu. Libvirt. Mysql. Network-Manager. Gluster. Chromium. RocksDB. Swift. Systemd. The list of core system infrastructure we have that uses hole punching is quite large...
So, really, hole punching is something that happens a lot and in many unexpected places. You can argue that your laptop doesn't use it, but that really doesn't matter in the bigger scheme of things. Hole punching is something applications expect to work and not corrupt data....
Quoted 2 lines
Can we implement holepunch as a NOP? Or return
-ENOTTY? Those both seem like better solutions than adding an extra rwsem to every inode.
We've already added this extra i_rwsem to ext4 and XFS - it's a sunk cost for almost every production machine out there in the wild. It needs to be made generic so we can optimise the implementation and not have to implement a unicorn in every filesystem to work around the fact the page cache and page faults have no internal serialisation mechanism against filesystem operations that directly manipulate and invalidate large ranges of the backing storage the page cache sits over.
Quoted 3 lines
Failing that, is there a bigger hammer we can use on the holepunch side (eg preventing all concurrent accesses while the holepunch is happening) to reduce the overhead on the read side?
That's what we currently do and what Jan is trying to refine....
Cheers,
Dave.
Signature · 2 lines
Dave Chinner [email protected]