nfs: fix NR_FILE_DIRTY underflow
Subsystems (4)
PATCH 1/1 nfs: fix NR_FILE_DIRTY underflow
Still testing this patch, but it looks good so far.
---
Just setting PG_dirty can cause NR_FILE_DIRTY to underflow which is bad (TM).
Use set_page_dirty() which will do the right thing.
--- fs/nfs/file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
fs/nfs/file.c +1 -1
@@ -321,7 +321,7 @@ static int nfs_release_page(struct pageif (!(gfp & __GFP_FS))return 0 ;/* Hack... Force nfs_wb_page() to write out the page */ -SetPageDirty (page); +set_page_dirty (page);return !nfs_wb_page (page->mapping->host, page); }
Quoted 7 lines · Peter Zijlstra
Still testing this patch, but it looks good so far.
---
Just settingPG_dirtycan causeNR_FILE_DIRTYto underflow which is bad (TM).Use
set_page_dirty()which will do the right thing.
Actually, I'd prefer to have it do the right thing by getting rid of that call to test_clear_page_dirty() inside invalidate_inode_pages2_range(). That is causing loss of data integrity, and is what is causing us to have to hack NFS in the first place.
Cheers
Trond
Quoted 20 lines
Signed-off-by Peter Zijlstra <[email protected]>--- fs/nfs/file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)Index:
linux-2.6-git2/fs/nfs/file.c
===================================================================fs/nfs/file.c +1
-1@@ -321,7 +321,7 @@ static int nfs_release_page(struct pageif (!(gfp & __GFP_FS))return 0 ;/* Hack... Force nfs_wb_page() to write out the page */ -SetPageDirty (page); +set_page_dirty (page);return !nfs_wb_page (page->mapping->host, page); }
- Sorry for possible duplicates, but I don't seem to be getting to lkml -
Quoted 7 lines · Trond Myklebust
Quoted 6 lines · Peter Zijlstra
Still testing this patch, but it looks good so far.
---
Just settingPG_dirtycan causeNR_FILE_DIRTYto underflow which is bad (TM).
Quoted 6 lines
Quoted 1 line
Use
set_page_dirty()which will do the right thing.Actually, I'd prefer to have it do the right thing by getting rid of that call to
test_clear_page_dirty()insideinvalidate_inode_pages2_range(). That is causing loss of data integrity, and is what is causing us to have to hack NFS in the first place.
Ah, I think I see what your problem is there.
How about this totally untested patch:
(little update - it seems to compile and run, now testing if it fixes
the problem too)
---
Delay clearing the dirty page state till after removing it from the mapping in invalidate_inode_pages2_range(). This will give try_to_release_pages() a shot to flush dirty data.
--- fs/nfs/file.c | 2 -- include/linux/page-flags.h | 2 ++ mm/page-writeback.c | 17 +++++++++++------ mm/truncate.c | 11 +++-------- 4 files changed, 16 insertions(+), 16 deletions(-)
fs/nfs/file.c +0 -2
@@ -320,8 +320,6 @@ static int nfs_release_page(struct page*/ if (!(gfp & __GFP_FS))return 0 ; -/* Hack... Force nfs_wb_page() to write out the page */ -SetPageDirty (page);return !nfs_wb_page (page_file_mapping (page)->host, page); }
include/linux/page-flags.h +2 -0
@@ -252,7 +252,9 @@ static inline void SetPageUptodate(struc#define ClearPageUncached (page) clear_bit(PG_uncached, &(page)->flags)struct page ;/* forward declaration */ +struct address_space ; +int __test_clear_page_dirty (struct address_space *mapping,struct page *page);int test_clear_page_dirty (struct page *page);int test_clear_page_writeback (struct page *page);int test_set_page_writeback (struct page *page);
mm/page-writeback.c +11 -6
@@ -850,13 +850,8 @@ int set_page_dirty_lock(struct page *pag }EXPORT_SYMBOL (set_page_dirty_lock); -/* -* Clear a page's dirty flag, while caring for dirty memory accounting. -* Returns true if the page was previously dirty. -*/ -int test_clear_page_dirty (struct page *page) +int __test_clear_page_dirty (struct address_space *mapping,struct page *page) { -struct address_space *mapping =page_mapping (page);unsigned long flags;if (!mapping)
@@ -880,6 +875,16 @@ int test_clear_page_dirty(struct page *pwrite_unlock_irqrestore (&mapping->tree_lock, flags);return 0 ; } + +/* +* Clear a page's dirty flag, while caring for dirty memory accounting. +* Returns true if the page was previously dirty. +*/ +int test_clear_page_dirty (struct page *page) +{ +struct address_space *mapping =page_mapping (page); +return __test_clear_page_dirty (mapping, page); +}EXPORT_SYMBOL (test_clear_page_dirty);/*
mm/truncate.c +3 -8
@@ -307,18 +307,12 @@ invalidate_complete_page2(struct addressreturn 0 ;write_lock_irq (&mapping->tree_lock); -if (PageDirty (page)) - goto failed; -BUG_ON (PagePrivate (page));__remove_from_page_cache (page);write_unlock_irq (&mapping->tree_lock);ClearPageUptodate (page);page_cache_release (page);/* pagecache ref */ return 1 ; -failed: -write_unlock_irq (&mapping->tree_lock); -return 0 ; }/**
@@ -386,12 +380,13 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } } - was_dirty =test_clear_page_dirty (page); + was_dirty =PageDirty (page);if (!invalidate_complete_page2 (mapping, page)) {if (was_dirty)set_page_dirty (page); ret = -EIO ; - } + }else +__test_clear_page_dirty (mapping, page);unlock_page (page); }pagevec_release (&pvec);
--
To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to [email protected]. For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
Quoted 7 lines · Trond Myklebust
Quoted 6 lines · Peter Zijlstra
Still testing this patch, but it looks good so far.
---
Just settingPG_dirtycan causeNR_FILE_DIRTYto underflow which is bad (TM).
Quoted 6 lines
Quoted 1 line
Use
set_page_dirty()which will do the right thing.Actually, I'd prefer to have it do the right thing by getting rid of that call to
test_clear_page_dirty()insideinvalidate_inode_pages2_range(). That is causing loss of data integrity, and is what is causing us to have to hack NFS in the first place.
Ah, I think I see what your problem is there.
How about this totally untested patch:
---
Delay clearing the dirty page state till after removing it from the mapping in invalidate_inode_pages2_range(). This will give try_to_release_pages() a shot to flush dirty data.
--- fs/nfs/file.c | 2 -- include/linux/page-flags.h | 2 ++ mm/page-writeback.c | 17 +++++++++++------ mm/truncate.c | 11 +++-------- 4 files changed, 16 insertions(+), 16 deletions(-)
fs/nfs/file.c +0 -2
@@ -320,8 +320,6 @@ static int nfs_release_page(struct page*/ if (!(gfp & __GFP_FS))return 0 ; -/* Hack... Force nfs_wb_page() to write out the page */ -SetPageDirty (page);return !nfs_wb_page (page_file_mapping (page)->host, page); }
include/linux/page-flags.h +2 -0
@@ -252,7 +252,9 @@ static inline void SetPageUptodate(struc#define ClearPageUncached (page) clear_bit(PG_uncached, &(page)->flags)struct page ;/* forward declaration */ +struct address_space ; +int __test_clear_page_dirty (struct address_space *mapping,struct page *page);int test_clear_page_dirty (struct page *page);int test_clear_page_writeback (struct page *page);int test_set_page_writeback (struct page *page);
mm/page-writeback.c +11 -6
@@ -850,13 +850,8 @@ int set_page_dirty_lock(struct page *pag }EXPORT_SYMBOL (set_page_dirty_lock); -/* -* Clear a page's dirty flag, while caring for dirty memory accounting. -* Returns true if the page was previously dirty. -*/ -int test_clear_page_dirty (struct page *page) +int __test_clear_page_dirty (struct address_space *mapping,struct page *page) { -struct address_space *mapping =page_mapping (page);unsigned long flags;if (!mapping)
@@ -880,6 +875,16 @@ int test_clear_page_dirty(struct page *pwrite_unlock_irqrestore (&mapping->tree_lock, flags);return 0 ; } + +/* +* Clear a page's dirty flag, while caring for dirty memory accounting. +* Returns true if the page was previously dirty. +*/ +int test_clear_page_dirty (struct page *page) +{ +struct address_space *mapping =page_mapping (page); +return __test_clear_page_dirty (mapping, page); +}EXPORT_SYMBOL (test_clear_page_dirty);/*
mm/truncate.c +3 -8
@@ -307,18 +307,12 @@ invalidate_complete_page2(struct addressreturn 0 ;write_lock_irq (&mapping->tree_lock); -if (PageDirty (page)) - goto failed; -BUG_ON (PagePrivate (page));__remove_from_page_cache (page);write_unlock_irq (&mapping->tree_lock);ClearPageUptodate (page);page_cache_release (page);/* pagecache ref */ return 1 ; -failed: -write_unlock_irq (&mapping->tree_lock); -return 0 ; }/**
@@ -386,12 +380,13 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } } - was_dirty =test_clear_page_dirty (page); + was_dirty =PageDirty (page);if (!invalidate_complete_page2 (mapping, page)) {if (was_dirty)set_page_dirty (page); ret = -EIO ; - } + }else +__test_clear_page_dirty (mapping, page);unlock_page (page); }pagevec_release (&pvec);
Quoted 118 lines · Peter Zijlstra
Quoted 7 lines · Trond Myklebust
Quoted 6 lines · Peter Zijlstra
Still testing this patch, but it looks good so far.
---
Just settingPG_dirtycan causeNR_FILE_DIRTYto underflow which is bad (TM).Quoted 6 lines
Quoted 1 line
Use
set_page_dirty()which will do the right thing.Actually, I'd prefer to have it do the right thing by getting rid of that call to
test_clear_page_dirty()insideinvalidate_inode_pages2_range(). That is causing loss of data integrity, and is what is causing us to have to hack NFS in the first place.Ah, I think I see what your problem is there.
How about this totally untested patch:---
Delay clearing the dirty page state till after removing it from the mapping ininvalidate_inode_pages2_range(). This will givetry_to_release_pages()a shot to flush dirty data.Signed-off-by Peter Zijlstra <[email protected]>--- fs/nfs/file.c | 2 -- include/linux/page-flags.h | 2 ++ mm/page-writeback.c | 17 +++++++++++------ mm/truncate.c | 11 +++-------- 4 files changed, 16 insertions(+), 16 deletions(-)Index:
linux-2.6-git/fs/nfs/file.c
===================================================================fs/nfs/file.c +0
-2@@ -320,8 +320,6 @@ static int nfs_release_page(struct page*/ if (!(gfp & __GFP_FS))return 0 ; -/* Hack... Force nfs_wb_page() to write out the page */ -SetPageDirty (page);return !nfs_wb_page (page_file_mapping (page)->host, page); }Index:
linux-2.6-git/include/linux/page-flags.h
===================================================================include/linux/page-flags.h +2
-0@@ -252,7 +252,9 @@ static inline void SetPageUptodate(struc#define ClearPageUncached (page) clear_bit(PG_uncached, &(page)->flags)struct page ;/* forward declaration */ +struct address_space ; +int __test_clear_page_dirty (struct address_space *mapping,struct page *page);int test_clear_page_dirty (struct page *page);int test_clear_page_writeback (struct page *page);int test_set_page_writeback (struct page *page);Index:
linux-2.6-git/mm/page-writeback.c
===================================================================mm/page-writeback.c +11
-6@@ -850,13 +850,8 @@ int set_page_dirty_lock(struct page *pag }EXPORT_SYMBOL (set_page_dirty_lock); -/* -* Clear a page's dirty flag, while caring for dirty memory accounting. -* Returns true if the page was previously dirty. -*/ -int test_clear_page_dirty (struct page *page) +int __test_clear_page_dirty (struct address_space *mapping,struct page *page) { -struct address_space *mapping =page_mapping (page);unsigned long flags;if (!mapping)@@ -880,6 +875,16 @@ int test_clear_page_dirty(struct page *pwrite_unlock_irqrestore (&mapping->tree_lock, flags);return 0 ; } + +/* +* Clear a page's dirty flag, while caring for dirty memory accounting. +* Returns true if the page was previously dirty. +*/ +int test_clear_page_dirty (struct page *page) +{ +struct address_space *mapping =page_mapping (page); +return __test_clear_page_dirty (mapping, page); +}EXPORT_SYMBOL (test_clear_page_dirty);/* Index:
linux-2.6-git/mm/truncate.c
===================================================================mm/truncate.c +0
-6@@ -307,18 +307,12 @@ invalidate_complete_page2(struct addressreturn 0 ;write_lock_irq (&mapping->tree_lock); -if (PageDirty (page)) - goto failed; -BUG_ON (PagePrivate (page));__remove_from_page_cache (page);write_unlock_irq (&mapping->tree_lock);ClearPageUptodate (page);page_cache_release (page);/* pagecache ref */ return 1 ; -failed: -write_unlock_irq (&mapping->tree_lock); -return 0 ; }/**
mm/truncate.c int invalidate_inode_pages2_range(struct · line 386
Quoted 8 lines
mm/truncate.c +1
-1@@ -386,12 +380,13 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } } - was_dirty =test_clear_page_dirty (page); + was_dirty =PageDirty (page);if (!invalidate_complete_page2 (mapping, page)) {if (was_dirty)set_page_dirty (page);
^^^^^^^^^^^^^^^^^^^^^
Why would you still need this?
mm/truncate.c int invalidate_inode_pages2_range(struct · line 392
Quoted 8 lines
mm/truncate.c +2
-1ret = -EIO ; - } + }else +__test_clear_page_dirty (mapping, page);unlock_page (page); }pagevec_release (&pvec);
Cheers
Trond
Quoted 13 lines · Trond Myklebust
Review int invalidate_inode_pages2_range(struct · line 386
Quoted 9 lines · Peter Zijlstra
(quoted diff) +1
-1@@ -386,12 +380,13 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } } - was_dirty = test_clear_page_dirty(page); + was_dirty = PageDirty(page); if (!invalidate_complete_page2(mapping, page)) { if (was_dirty) set_page_dirty(page);^^^^^^^^^^^^^^^^^^^^^
Quoted 1 line
Why would you still need this?
Hmm yeah, that doesn't make sense. What kind of twist did I get my brain into.
This should do.
Hmm, does this agree with the DIO path? I just noticed generic_file_direct_IO() also uses invalidate_inode_pages2_range(). I think it does, its looks like all they want is to ensure nothing remains in the pagecache after a write, which itself ensures nothing is dirty to begin with.
---
Delay clearing the dirty page state till after we've invalidated the page in invalidate_complete_page2(). This gives try_to_release_pages() a chance to flush dirty data.
--- fs/nfs/file.c | 2 -- mm/truncate.c | 14 ++------------ 2 files changed, 2 insertions(+), 14 deletions(-)
fs/nfs/file.c +0 -2
@@ -320,8 +320,6 @@ static int nfs_release_page(struct page*/ if (!(gfp & __GFP_FS))return 0 ; -/* Hack... Force nfs_wb_page() to write out the page */ -set_page_dirty (page);return !nfs_wb_page (page_file_mapping (page)->host, page); }
mm/truncate.c +2 -12
@@ -306,19 +306,14 @@ invalidate_complete_page2(struct addressif (PagePrivate (page) && !try_to_release_page (page,GFP_KERNEL ))return 0 ; +test_clear_page_dirty (page);write_lock_irq (&mapping->tree_lock); -if (PageDirty (page)) - goto failed; -BUG_ON (PagePrivate (page));__remove_from_page_cache (page);write_unlock_irq (&mapping->tree_lock);ClearPageUptodate (page);page_cache_release (page);/* pagecache ref */ return 1 ; -failed: -write_unlock_irq (&mapping->tree_lock); -return 0 ; }/**
@@ -350,7 +345,6 @@ int invalidate_inode_pages2_range(structfor (i =0 ; !ret && i <pagevec_count (&pvec); i++) {struct page *page = pvec.pages[i];pgoff_t page_index; -int was_dirty;lock_page (page);if (page->mapping != mapping) {
@@ -386,12 +380,8 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } } - was_dirty =test_clear_page_dirty (page); -if (!invalidate_complete_page2 (mapping, page)) { -if (was_dirty) -set_page_dirty (page); +if (!invalidate_complete_page2 (mapping, page)) ret = -EIO ; - }unlock_page (page); }pagevec_release (&pvec);
Quoted 35 lines · On Wed, 13 Dec 2006 19:48:34 +0100 Peter Zijlstra <[email protected]>…
mm/truncate.c +1
-7@@ -306,19 +306,14 @@ invalidate_complete_page2(struct addressif (PagePrivate (page) && !try_to_release_page (page,GFP_KERNEL ))return 0 ; +test_clear_page_dirty (page);write_lock_irq (&mapping->tree_lock); -if (PageDirty (page)) - goto failed; -BUG_ON (PagePrivate (page));__remove_from_page_cache (page);write_unlock_irq (&mapping->tree_lock);ClearPageUptodate (page);page_cache_release (page);/* pagecache ref */ return 1 ; -failed: -write_unlock_irq (&mapping->tree_lock); -return 0 ; }/** @@ -350,7 +345,6 @@ int invalidate_inode_pages2_range(structfor (i =0 ; !ret && i <pagevec_count (&pvec); i++) {struct page *page = pvec.pages[i];pgoff_t page_index; -int was_dirty;lock_page (page);if (page->mapping != mapping) {@@ -386,12 +380,8 @@ int invalidate_inode_pages2_range(struct PAGE_CACHE_SIZE, 0); } }
mm/truncate.c int invalidate_inode_pages2_range(struct · line 387
Quoted 10 lines
mm/truncate.c +1
-5- was_dirty =test_clear_page_dirty (page); -if (!invalidate_complete_page2 (mapping, page)) { -if (was_dirty) -set_page_dirty (page); +if (!invalidate_complete_page2 (mapping, page)) ret = -EIO ; - }unlock_page (page); }pagevec_release (&pvec);
a) we're now calling try_to_release_page() with a potentially-dirty page, whereas it was previously clean.
I wouldn't expect ->releasepage() implementations to go looking at PG_Dirty, because that's not what they're suppoed to be interested in. But they might do, dunno.
b) If invalidate_complete_page2() failed due to, say, dirty buffer_heads then we now have a clean page with dirty buffers. That is an illegal state and the page will leak permanently.
I _think_ that's what the was_dirty logic is in there for: to preserve the correct page-vs-buffers dirtiness coherency. But I'd need to do some 2.5.x changelog-dumpster-diving to be sure.
Sigh. I don't know what invalidate_inode_pages2() is *supposed to do*.
What are its semantics wrt unfreeable page metadata? Against page dirtiness?
It was written for direct-io and had one set of semantics for that, then NFS came along and seemed to require a slightly different set of semantics, even though the earlier semantics weren't really defined, leading to a belief that the present semantics are "wrong", without a definition of what semantics NFS actually desires.
So let's start again.
Trond, please define precisely and completely and without reference to the existing implementation: what behaviour does NFS want?
Quoted 6 lines · On Wed, 13 Dec 2006 17:29:21 -0800 Andrew Morton <[email protected]> wrote:
a) we're now calling
try_to_release_page()with a potentially-dirty page, whereas it was previously clean.I wouldn't expect ->releasepage() implementations to go looking at
PG_Dirty, because that's not what they're suppoed to be interested in. But they might do, dunno.
Still an issue, probably minor.
Quoted 7 lines
b) If
invalidate_complete_page2()failed due to, say, dirtybuffer_headsthen we now have a clean page with dirty buffers. That is an illegal state and the page will leak permanently.I _think_ that's what the
was_dirtylogic is in there for: to preserve the correct page-vs-buffers dirtiness coherency. But I'd need to do some 2.5.x changelog-dumpster-diving to be sure.
no, that's bs. The patch looks OK from that POV: try_to_release_page() will be able to clear clean buffers from a dirty page.
And in fact if it did that, it will then clean the page for us (see test_clear_page_dirty() in try_to_free_buffers()).
But we still need the clear_page_dirty() in invalidate_complete_page2() in case we didn't call try_to_release_page() at all.
Quoted 2 lines
Trond, please define precisely and completely and without reference to the existing implementation: what behaviour does NFS want?
But this would be nice.
Quoted 2 lines · Andrew Morton
Quoted 2 lines
Trond, please define precisely and completely and without reference to the existing implementation: what behaviour does NFS want?
Part of the behaviour is dictated by our needs for invalidate_inode_pages2(), part of the behaviour is dictated by the 2-stage NFS writeout mechanism.
Starting with invalidate_inode_pages2(). The goal here is to have a function that is guaranteed to force all currently cached pages to be read in from the server afresh. In other words, we'd like to throw out the old mapping of the file, and start with a new one. However, we also need to guarantee that any modifications that a user may have made to the file are preserved. Throwing out a dirty page is forbidden unless the data has been written to disk first.
What we're doing in try_to_release_page() in the current implementation of invalidate_inode_pages2() is therefore to fix races: the goal is to ensure that we write back any dirty data before the page is removed. This is made necessary by the fact that the VM may at any time override our call to unmap_mapping_range() and allow the page to be re-dirtied by the user.
Next: about the 2-stage NFS writeout mechanism. A client may want to allow the server to cache writeback data for a while, so that it can flush several WRITE RPC calls to disk at the same time, maximising I/O efficiency in terms of elevator algorithms etc.
It signals this to the server by means of the 'unstable' flag on the WRITE RPC call, which allows the former to just write the data into its pagecache. Before the client can actually free up the page it still needs to know that the data it wrote to the server has been flushed from the pagecache onto disk, and this is done by sending the COMMIT RPC request. The latter acts rather like fdatasync() does on local data: it guarantees that all file data within a specified range has been flushed to disk. Failure of the COMMIT request signals to the client that the server may have rebooted, and so the page needs to be written out again. Our second use of try_to_release_page() is therefore to ensure that the server has indeed flushed that dirty data from its pagecache to its disk before we allow the VM to release the page on the client. We don't send a COMMIT request for each and every page that is to be released, but we do ensure that at least one COMMIT has been sent that covers the data range represented by the page to be freed.
Cheers
Trond