diff mbox series

[02/16] huge tmpfs: fix split_huge_page() after FALLOC_FL_KEEP_SIZE

Message ID f3f6f5e7-6749-7830-5627-a1b6b68dc365@google.com (mailing list archive)
State New
Headers show
Series tmpfs: HUGEPAGE and MEM_LOCK fcntls and memfds | expand

Commit Message

Hugh Dickins July 30, 2021, 7:28 a.m. UTC
A successful shmem_fallocate() guarantees that the extent has been
reserved, even beyond i_size when the FALLOC_FL_KEEP_SIZE flag was used.
But that guarantee is broken by shmem_unused_huge_shrink()'s attempts to
split huge pages and free their excess beyond i_size; and by other uses
of split_huge_page() near i_size.

It's sad to add a shmem inode field just for this, but I did not find a
better way to keep the guarantee.  A flag to say KEEP_SIZE has been used
would be cheaper, but I'm averse to unclearable flags.  The fallocend
field is not perfect either (many disjoint ranges might be fallocated),
but good enough; and gains another use later on.

Fixes: 779750d20b93 ("shmem: split huge pages beyond i_size under memory pressure")
Signed-off-by: Hugh Dickins <hughd@google.com>
---
 include/linux/shmem_fs.h | 13 +++++++++++++
 mm/huge_memory.c         |  6 ++++--
 mm/shmem.c               | 15 ++++++++++++++-
 3 files changed, 31 insertions(+), 3 deletions(-)

Comments

Yang Shi July 30, 2021, 11:48 p.m. UTC | #1
On Fri, Jul 30, 2021 at 12:28 AM Hugh Dickins <hughd@google.com> wrote:
>
> A successful shmem_fallocate() guarantees that the extent has been
> reserved, even beyond i_size when the FALLOC_FL_KEEP_SIZE flag was used.
> But that guarantee is broken by shmem_unused_huge_shrink()'s attempts to
> split huge pages and free their excess beyond i_size; and by other uses
> of split_huge_page() near i_size.
>
> It's sad to add a shmem inode field just for this, but I did not find a
> better way to keep the guarantee.  A flag to say KEEP_SIZE has been used
> would be cheaper, but I'm averse to unclearable flags.  The fallocend
> field is not perfect either (many disjoint ranges might be fallocated),
> but good enough; and gains another use later on.
>
> Fixes: 779750d20b93 ("shmem: split huge pages beyond i_size under memory pressure")
> Signed-off-by: Hugh Dickins <hughd@google.com>

Reviewed-by: Yang Shi <shy828301@gmail.com>

> ---
>  include/linux/shmem_fs.h | 13 +++++++++++++
>  mm/huge_memory.c         |  6 ++++--
>  mm/shmem.c               | 15 ++++++++++++++-
>  3 files changed, 31 insertions(+), 3 deletions(-)
>
> diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
> index 8e775ce517bb..9b7f7ac52351 100644
> --- a/include/linux/shmem_fs.h
> +++ b/include/linux/shmem_fs.h
> @@ -18,6 +18,7 @@ struct shmem_inode_info {
>         unsigned long           flags;
>         unsigned long           alloced;        /* data pages alloced to file */
>         unsigned long           swapped;        /* subtotal assigned to swap */
> +       pgoff_t                 fallocend;      /* highest fallocate endindex */
>         struct list_head        shrinklist;     /* shrinkable hpage inodes */
>         struct list_head        swaplist;       /* chain of maybes on swap */
>         struct shared_policy    policy;         /* NUMA memory alloc policy */
> @@ -119,6 +120,18 @@ static inline bool shmem_file(struct file *file)
>         return shmem_mapping(file->f_mapping);
>  }
>
> +/*
> + * If fallocate(FALLOC_FL_KEEP_SIZE) has been used, there may be pages
> + * beyond i_size's notion of EOF, which fallocate has committed to reserving:
> + * which split_huge_page() must therefore not delete.  This use of a single
> + * "fallocend" per inode errs on the side of not deleting a reservation when
> + * in doubt: there are plenty of cases when it preserves unreserved pages.
> + */
> +static inline pgoff_t shmem_fallocend(struct inode *inode, pgoff_t eof)
> +{
> +       return max(eof, SHMEM_I(inode)->fallocend);
> +}
> +
>  extern bool shmem_charge(struct inode *inode, long pages);
>  extern void shmem_uncharge(struct inode *inode, long pages);
>
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index afff3ac87067..890fb73ac89b 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -2454,11 +2454,11 @@ static void __split_huge_page(struct page *page, struct list_head *list,
>
>         for (i = nr - 1; i >= 1; i--) {
>                 __split_huge_page_tail(head, i, lruvec, list);
> -               /* Some pages can be beyond i_size: drop them from page cache */
> +               /* Some pages can be beyond EOF: drop them from page cache */
>                 if (head[i].index >= end) {
>                         ClearPageDirty(head + i);
>                         __delete_from_page_cache(head + i, NULL);
> -                       if (IS_ENABLED(CONFIG_SHMEM) && PageSwapBacked(head))
> +                       if (shmem_mapping(head->mapping))
>                                 shmem_uncharge(head->mapping->host, 1);
>                         put_page(head + i);
>                 } else if (!PageAnon(page)) {
> @@ -2686,6 +2686,8 @@ int split_huge_page_to_list(struct page *page, struct list_head *list)
>                  * head page lock is good enough to serialize the trimming.
>                  */
>                 end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);
> +               if (shmem_mapping(mapping))
> +                       end = shmem_fallocend(mapping->host, end);
>         }
>
>         /*
> diff --git a/mm/shmem.c b/mm/shmem.c
> index 0cd5c9156457..24c9da6b41c2 100644
> --- a/mm/shmem.c
> +++ b/mm/shmem.c
> @@ -905,6 +905,9 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, loff_t lend,
>         if (lend == -1)
>                 end = -1;       /* unsigned, so actually very big */
>
> +       if (info->fallocend > start && info->fallocend <= end && !unfalloc)
> +               info->fallocend = start;
> +
>         pagevec_init(&pvec);
>         index = start;
>         while (index < end && find_lock_entries(mapping, index, end - 1,
> @@ -2667,7 +2670,7 @@ static long shmem_fallocate(struct file *file, int mode, loff_t offset,
>         struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
>         struct shmem_inode_info *info = SHMEM_I(inode);
>         struct shmem_falloc shmem_falloc;
> -       pgoff_t start, index, end;
> +       pgoff_t start, index, end, undo_fallocend;
>         int error;
>
>         if (mode & ~(FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE))
> @@ -2736,6 +2739,15 @@ static long shmem_fallocate(struct file *file, int mode, loff_t offset,
>         inode->i_private = &shmem_falloc;
>         spin_unlock(&inode->i_lock);
>
> +       /*
> +        * info->fallocend is only relevant when huge pages might be
> +        * involved: to prevent split_huge_page() freeing fallocated
> +        * pages when FALLOC_FL_KEEP_SIZE committed beyond i_size.
> +        */
> +       undo_fallocend = info->fallocend;
> +       if (info->fallocend < end)
> +               info->fallocend = end;
> +
>         for (index = start; index < end; ) {
>                 struct page *page;
>
> @@ -2750,6 +2762,7 @@ static long shmem_fallocate(struct file *file, int mode, loff_t offset,
>                 else
>                         error = shmem_getpage(inode, index, &page, SGP_FALLOC);
>                 if (error) {
> +                       info->fallocend = undo_fallocend;
>                         /* Remove the !PageUptodate pages we added */
>                         if (index > start) {
>                                 shmem_undo_range(inode,
> --
> 2.26.2
>
diff mbox series

Patch

diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index 8e775ce517bb..9b7f7ac52351 100644
--- a/include/linux/shmem_fs.h
+++ b/include/linux/shmem_fs.h
@@ -18,6 +18,7 @@  struct shmem_inode_info {
 	unsigned long		flags;
 	unsigned long		alloced;	/* data pages alloced to file */
 	unsigned long		swapped;	/* subtotal assigned to swap */
+	pgoff_t			fallocend;	/* highest fallocate endindex */
 	struct list_head        shrinklist;     /* shrinkable hpage inodes */
 	struct list_head	swaplist;	/* chain of maybes on swap */
 	struct shared_policy	policy;		/* NUMA memory alloc policy */
@@ -119,6 +120,18 @@  static inline bool shmem_file(struct file *file)
 	return shmem_mapping(file->f_mapping);
 }
 
+/*
+ * If fallocate(FALLOC_FL_KEEP_SIZE) has been used, there may be pages
+ * beyond i_size's notion of EOF, which fallocate has committed to reserving:
+ * which split_huge_page() must therefore not delete.  This use of a single
+ * "fallocend" per inode errs on the side of not deleting a reservation when
+ * in doubt: there are plenty of cases when it preserves unreserved pages.
+ */
+static inline pgoff_t shmem_fallocend(struct inode *inode, pgoff_t eof)
+{
+	return max(eof, SHMEM_I(inode)->fallocend);
+}
+
 extern bool shmem_charge(struct inode *inode, long pages);
 extern void shmem_uncharge(struct inode *inode, long pages);
 
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index afff3ac87067..890fb73ac89b 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2454,11 +2454,11 @@  static void __split_huge_page(struct page *page, struct list_head *list,
 
 	for (i = nr - 1; i >= 1; i--) {
 		__split_huge_page_tail(head, i, lruvec, list);
-		/* Some pages can be beyond i_size: drop them from page cache */
+		/* Some pages can be beyond EOF: drop them from page cache */
 		if (head[i].index >= end) {
 			ClearPageDirty(head + i);
 			__delete_from_page_cache(head + i, NULL);
-			if (IS_ENABLED(CONFIG_SHMEM) && PageSwapBacked(head))
+			if (shmem_mapping(head->mapping))
 				shmem_uncharge(head->mapping->host, 1);
 			put_page(head + i);
 		} else if (!PageAnon(page)) {
@@ -2686,6 +2686,8 @@  int split_huge_page_to_list(struct page *page, struct list_head *list)
 		 * head page lock is good enough to serialize the trimming.
 		 */
 		end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);
+		if (shmem_mapping(mapping))
+			end = shmem_fallocend(mapping->host, end);
 	}
 
 	/*
diff --git a/mm/shmem.c b/mm/shmem.c
index 0cd5c9156457..24c9da6b41c2 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -905,6 +905,9 @@  static void shmem_undo_range(struct inode *inode, loff_t lstart, loff_t lend,
 	if (lend == -1)
 		end = -1;	/* unsigned, so actually very big */
 
+	if (info->fallocend > start && info->fallocend <= end && !unfalloc)
+		info->fallocend = start;
+
 	pagevec_init(&pvec);
 	index = start;
 	while (index < end && find_lock_entries(mapping, index, end - 1,
@@ -2667,7 +2670,7 @@  static long shmem_fallocate(struct file *file, int mode, loff_t offset,
 	struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
 	struct shmem_inode_info *info = SHMEM_I(inode);
 	struct shmem_falloc shmem_falloc;
-	pgoff_t start, index, end;
+	pgoff_t start, index, end, undo_fallocend;
 	int error;
 
 	if (mode & ~(FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE))
@@ -2736,6 +2739,15 @@  static long shmem_fallocate(struct file *file, int mode, loff_t offset,
 	inode->i_private = &shmem_falloc;
 	spin_unlock(&inode->i_lock);
 
+	/*
+	 * info->fallocend is only relevant when huge pages might be
+	 * involved: to prevent split_huge_page() freeing fallocated
+	 * pages when FALLOC_FL_KEEP_SIZE committed beyond i_size.
+	 */
+	undo_fallocend = info->fallocend;
+	if (info->fallocend < end)
+		info->fallocend = end;
+
 	for (index = start; index < end; ) {
 		struct page *page;
 
@@ -2750,6 +2762,7 @@  static long shmem_fallocate(struct file *file, int mode, loff_t offset,
 		else
 			error = shmem_getpage(inode, index, &page, SGP_FALLOC);
 		if (error) {
+			info->fallocend = undo_fallocend;
 			/* Remove the !PageUptodate pages we added */
 			if (index > start) {
 				shmem_undo_range(inode,