Introduce shmem_insert_folio(), which transfers an isolated folio zero-copy into a shmem file's page cache. The folio is charged to memcg, inserted into the address space, and placed on the anon LRU for normal reclaim. An optional writeback parameter requests immediate swap writeback. Higher-order folios are promoted to compound before insertion, enabling THP-sized swap entries with CONFIG_THP_SWAP=y. On failure the folio is returned to its original state and the caller retains ownership. Assisted-by: GitHub_Copilot:claude-sonnet-4.6 Signed-off-by: Thomas Hellström --- include/linux/mm.h | 1 + include/linux/shmem_fs.h | 2 + mm/page_alloc.c | 21 ++++++++ mm/shmem.c | 105 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) diff --git a/include/linux/mm.h b/include/linux/mm.h index af23453e9dbd..e2e7b0c0998b 100644 --- a/include/linux/mm.h +++ b/include/linux/mm.h @@ -1676,6 +1676,7 @@ struct mmu_gather; struct inode; extern void prep_compound_page(struct page *page, unsigned int order); +extern void undo_compound_page(struct page *page); static inline unsigned int folio_large_order(const struct folio *folio) { diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h index 93a0ba872ebe..2dc9355757fd 100644 --- a/include/linux/shmem_fs.h +++ b/include/linux/shmem_fs.h @@ -175,6 +175,8 @@ int shmem_get_folio(struct inode *inode, pgoff_t index, loff_t write_end, struct folio **foliop, enum sgp_type sgp); struct folio *shmem_read_folio_gfp(struct address_space *mapping, pgoff_t index, gfp_t gfp); +int shmem_insert_folio(struct file *file, struct folio *folio, unsigned int order, + pgoff_t index, bool writeback, gfp_t folio_gfp); static inline struct folio *shmem_read_folio(struct address_space *mapping, pgoff_t index) diff --git a/mm/page_alloc.c b/mm/page_alloc.c index 227d58dc3de6..db82825a3348 100644 --- a/mm/page_alloc.c +++ b/mm/page_alloc.c @@ -705,6 +705,27 @@ void prep_compound_page(struct page *page, unsigned int order) prep_compound_head(page, order); } +/** + * undo_compound_page() - Reverse the effect of prep_compound_page(). + * @page: The head page of a compound page to demote. + * + * Returns the pages to non-compound state as if prep_compound_page() + * had never been called. split_page() must NOT have been called on + * the compound page; tail refcounts must be 0. The caller must ensure + * no other users hold references to the compound page. + */ +void undo_compound_page(struct page *page) +{ + unsigned int i, nr = 1U << compound_order(page); + + page[1].flags.f &= ~PAGE_FLAGS_SECOND; + for (i = 1; i < nr; i++) { + page[i].mapping = NULL; + clear_compound_head(&page[i]); + } + ClearPageHead(page); +} + static inline void set_buddy_order(struct page *page, unsigned int order) { set_page_private(page, order); diff --git a/mm/shmem.c b/mm/shmem.c index 3b5dc21b323c..45e80a74f77c 100644 --- a/mm/shmem.c +++ b/mm/shmem.c @@ -937,6 +937,111 @@ int shmem_add_to_page_cache(struct folio *folio, return 0; } +/** + * shmem_insert_folio() - Insert an isolated folio into a shmem file. + * @file: The shmem file created with shmem_file_setup(). + * @folio: The folio to insert. Must be isolated (not on LRU), unlocked, + * have exactly one reference (the caller's), have no page-table + * mappings, and have folio->mapping == NULL. + * @order: The allocation order of @folio. If @order > 0 and @folio is + * not already a large (compound) folio, it will be promoted to a + * compound folio of this order inside this function. This requires + * the standard post-alloc state: head refcount == 1, tail + * refcounts == 0 (i.e. split_page() must NOT have been called). + * On failure the promotion is reversed and the folio is returned + * to its original non-compound state. + * @index: Page-cache index at which to insert. Must be aligned to + * (1 << @order) and within the file's size. + * @writeback: If true, attempt immediate writeback to swap after insertion. + * Best-effort; failure is silently ignored. + * @folio_gfp: The GFP flags to use for memory-cgroup charging. + * + * The folio is inserted zero-copy into the shmem page cache and placed on + * the anon LRU, where it participates in normal kernel reclaim (written to + * swap under memory pressure). Any previous content at @index is discarded. + * On success the caller should release their reference with folio_put() and + * track the (@file, @index) pair for later recovery via shmem_read_folio() + * and release via shmem_truncate_range(). + * + * Return: 0 on success. On failure the folio is returned to its original + * state and the caller retains ownership. + */ +int shmem_insert_folio(struct file *file, struct folio *folio, unsigned int order, + pgoff_t index, bool writeback, gfp_t folio_gfp) +{ + struct address_space *mapping = file->f_mapping; + struct inode *inode = mapping->host; + bool promoted; + long nr_pages; + int ret; + + promoted = order > 0 && !folio_test_large(folio); + if (promoted) + prep_compound_page(&folio->page, order); + nr_pages = folio_nr_pages(folio); + + VM_BUG_ON_FOLIO(folio_test_lru(folio), folio); + VM_BUG_ON_FOLIO(folio_mapped(folio), folio); + VM_BUG_ON_FOLIO(folio_test_swapcache(folio), folio); + VM_BUG_ON_FOLIO(folio->mapping, folio); + VM_BUG_ON(index != round_down(index, nr_pages)); + + folio_lock(folio); + __folio_set_swapbacked(folio); + folio_mark_uptodate(folio); + + folio_gfp &= GFP_RECLAIM_MASK; + ret = mem_cgroup_charge(folio, NULL, folio_gfp); + if (ret) + goto err_unlock; + + ret = shmem_add_to_page_cache(folio, mapping, index, NULL, folio_gfp); + if (ret == -EEXIST) { + shmem_truncate_range(inode, + (loff_t)index << PAGE_SHIFT, + ((loff_t)(index + nr_pages) << PAGE_SHIFT) - 1); + ret = shmem_add_to_page_cache(folio, mapping, index, NULL, + folio_gfp); + } + if (ret) + goto err_uncharge; + + folio_mark_dirty(folio); + + ret = shmem_inode_acct_blocks(inode, nr_pages); + if (ret) { + filemap_remove_folio(folio); + goto err_uncharge; + } + + shmem_recalc_inode(inode, nr_pages, 0); + + if (writeback) { + ret = shmem_writeout(folio, NULL, NULL); + if (ret == AOP_WRITEPAGE_ACTIVATE) { + /* No swap slot available; reclaim will retry. */ + folio_add_lru(folio); + folio_unlock(folio); + } + /* ret == 0 or ret < 0: folio unlocked by shmem_writeout */ + } else { + folio_add_lru(folio); + folio_unlock(folio); + } + + return 0; + +err_uncharge: + mem_cgroup_uncharge(folio); +err_unlock: + __folio_clear_swapbacked(folio); + folio_unlock(folio); + if (promoted) + undo_compound_page(&folio->page); + return ret; +} +EXPORT_SYMBOL_GPL(shmem_insert_folio); + /* * Somewhat like filemap_remove_folio, but substitutes swap for @folio. */ -- 2.54.0