mm: improve folio refcount scalability
Subsystems (17)
v6 cover mm: improve folio refcount scalability
From: Gladyshev Ilya <[email protected]>
Recap
-----
This patchset addresses a scalability issue of a folio's add_unless() operation, noticeable during contended IO reads from the same page [folio_try_get()]. The main idea is to replace CAS loop with optimistic increment (and deal with failure later). This requires splitting refcount into counter and separate "dead/frozen" bit.
To allow for such modification, this patchset also slightly refactors page_ref API, consolidating all implementation logic inside mm headers. For more information, check individual commit messages. The original performance issue and previous attempts by other people can be found in [1][2].
Performance
-----------
To my regret, I don't have any access to high-core CPUs that I can benchmark on, and a 12 vcpu laptop isn't really a scalability test. So, here I can only paste my previous measurements on Linux 6.15. To be fair, none of the related code paths really changed, so I don't expect any changes in the numbers here.
Performance was measured using a simple custom benchmark based on will-it-scale[3]. This benchmark spawns N pinned threads/processes that execute the following loop:
``
char buf[]
fd = open(/* same file in tmpfs */);
while (true) {
pread(fd, buf, /* read size = */ 64, /* offset = 0 */)
}``
While this is a synthetic load, it does highlight existing issue and doesn't differ much from the benchmarking in patch [2].
This benchmark measures operations per second in the inner loop and the results across all workers. Performance was tested on top of v6.15 kernel on two platforms. Since threads and processes showed similar performance on both systems, only the thread results are provided below. The performance improvement scales linearly between the CPU counts shown.
Platform 1: 2 x E5-2690 v3, 12C/12T each [disabled SMT]
#threads | vanilla | patched | boost (%)
1 | 1343381 | 1344401 | +0.1
2 | 2186160 | 2455837 | +12.3
5 | 5277092 | 6108030 | +15.7
10 | 5858123 | 7506328 | +28.1
12 | 6484445 | 8137706 | +25.5
/* Cross socket NUMA */
14 | 3145860 | 4247391 | +35.0
16 | 2350840 | 4262707 | +81.3
18 | 2378825 | 4121415 | +73.2
20 | 2438475 | 4683548 | +92.1
24 | 2325998 | 4529737 | +94.7Platform 2: 2 x AMD EPYC 9654, 96C/192T each [enabled SMT]
#threads | vanilla | patched | boost (%)
1 | 1077276 | 1081653 | +0.4
5 | 4286838 | 4682513 | +9.2
10 | 1698095 | 1902753 | +12.1
20 | 1662266 | 1921603 | +15.6
49 | 1486745 | 1828926 | +23.0
97 | 1617365 | 2052635 | +26.9
/* Cross socket NUMA */
105 | 1368319 | 1798862 | +31.5
136 | 1008071 | 1393055 | +38.2
168 | 879332 | 1245210 | +41.6
/* SMT */
193 | 905432 | 1294833 | +43.0
289 | 851988 | 1313110 | +54.1
353 | 771288 | 1347165 | +74.7Changes since v4 (last significant checkpoint)
---
- Lower Google GVE pagecnt_bias
- VM_BUG_ON -> VM_WARN_ON_ONCE
- Refactor missing API calls in mm/memory-failure.c
- rebase
- Make __page_is_frozen() public and introduce folio_is_frozen() counterpart [3].
- Make commit messages more informative
Link to v4: https://lore.kernel.org/linux-mm/[email protected]/
[1]: https://lore.kernel.org/linux-mm/CAHk-=wj00-nGmXEkxY=-=Z_qP6kiGUziSFvxHJ9N-cLWry5zpA@mail.gmail.com/
[2]: https://lore.kernel.org/linux-mm/[email protected]/
[3]: https://lore.kernel.org/all/aqAIFV4nOGPbWiDS@thinkstation/
---
Ilya Gladyshev (3):
gve: reduce pagecnt_bias to USHRT_MAX
mm: drop page refcount zero state semantics
mm: implement page refcount locking via dedicated bit
.../ethernet/google/gve/gve_buffer_mgmt_dqo.c | 4 +- drivers/net/ethernet/google/gve/gve_rx.c | 8 +-- drivers/net/ethernet/google/gve/gve_utils.c | 6 +- drivers/net/ethernet/google/gve/gve_utils.h | 2 +- drivers/pci/p2pdma.c | 4 +- drivers/virtio/virtio_mem.c | 2 +- include/linux/mm.h | 2 +- include/linux/page-flags.h | 13 ++++ include/linux/page_ref.h | 69 ++++++++++++++++--- kernel/liveupdate/kexec_handover.c | 6 +- lib/test_hmm.c | 4 +- mm/hugetlb.c | 2 +- mm/internal.h | 2 +- mm/memory-failure.c | 8 +-- mm/memremap.c | 4 +- mm/mm_init.c | 6 +- mm/page_alloc.c | 10 +-- mm/page_frag_cache.c | 2 +- 18 files changed, 108 insertions(+), 46 deletions(-)
Signature · 1 line
2.55.0
PATCH v6 1/3 gve: reduce pagecnt_bias to USHRT_MAX
Google GVE driver bumps page refcount with INT_MAX ghost users instead of the usual USHRT_MAX like other drivers. This INT_MAX bump brings refcount dangerously close to an overflow.
This works fine for now, as refcount is treated as unsigned. However, following patches will reduce the usable range of refcount values, making signed int overflow critical.
--- drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c | 4 ++-- drivers/net/ethernet/google/gve/gve_rx.c | 8 ++++---- drivers/net/ethernet/google/gve/gve_utils.c | 6 +++--- drivers/net/ethernet/google/gve/gve_utils.h | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-)
drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c +2 -2
@@ -149,8 +149,8 @@ int gve_alloc_qpl_page_dqo(struct gve_rx_ring *rx, buf_state->last_single_ref_offset =0 ;/* The page already has 1 ref. */ -page_ref_add (buf_state->page_info.page,INT_MAX -1 ); - buf_state->page_info.pagecnt_bias =INT_MAX ; +page_ref_add (buf_state->page_info.page,USHRT_MAX -1 ); + buf_state->page_info.pagecnt_bias =USHRT_MAX ;return 0 ; }
drivers/net/ethernet/google/gve/gve_rx.c +4 -4
@@ -152,8 +152,8 @@ static void gve_setup_rx_buffer(struct gve_rx_ring *rx, page_info->buf_size = rx->packet_buffer_size; *slot_addr =cpu_to_be64 (addr);/* The page already has 1 ref */ -page_ref_add (page,INT_MAX -1 ); - page_info->pagecnt_bias =INT_MAX ; +page_ref_add (page,USHRT_MAX -1 ); + page_info->pagecnt_bias =USHRT_MAX ; } static int gve_rx_alloc_buffer(struct gve_priv *priv, struct device *dev,
@@ -230,8 +230,8 @@ static int gve_rx_prefill_pages(struct gve_rx_ring *rx, rx->qpl_copy_pool[j].buf_size = rx->packet_buffer_size;/* The page already has 1 ref. */ -page_ref_add (page,INT_MAX -1 ); - rx->qpl_copy_pool[j].pagecnt_bias =INT_MAX ; +page_ref_add (page,USHRT_MAX -1 ); + rx->qpl_copy_pool[j].pagecnt_bias =USHRT_MAX ; } }
drivers/net/ethernet/google/gve/gve_utils.c +3 -3
@@ -95,13 +95,13 @@ void gve_dec_pagecnt_bias(struct gve_rx_slot_page_info *page_info)if (page_info->pagecnt_bias ==0 ) {int pagecount =page_count (page_info->page); -/* If we have run out of bias - set it back up to INT_MAX +/* If we have run out of bias - set it back up to USHRT_MAX * minus the existing refs. */ - page_info->pagecnt_bias =INT_MAX - pagecount; + page_info->pagecnt_bias =USHRT_MAX - pagecount;/* Set pagecount back up to max. */ -page_ref_add (page_info->page,INT_MAX - pagecount); +page_ref_add (page_info->page,USHRT_MAX - pagecount); } }
drivers/net/ethernet/google/gve/gve_utils.h +1 -1
@@ -25,7 +25,7 @@ struct sk_buff *gve_rx_copy_data(struct net_device *dev, struct napi_struct *napstruct sk_buff *gve_rx_copy (struct net_device *dev,struct napi_struct *napi,struct gve_rx_slot_page_info *page_info,u16 len); -/* Decrement pagecnt_bias. Set it back to INT_MAX if it reached zero. */ +/* Decrement pagecnt_bias. Set it back to USHRT_MAX if it reached zero. */ void gve_dec_pagecnt_bias (struct gve_rx_slot_page_info *page_info); void gve_add_napi(struct gve_priv *priv, int ntfy_idx,
Signature · 1 line
2.55.0
PATCH v6 2/3 mm: drop page refcount zero state semantics
In reply to Ilya GladyshevSome call sites manipulate page refcount directly based on its own assumptions of the refcount int value. Instead of making such assumptions these call sites should use high-level API like set_frozen() or init_refcount().
This patch tries to consolidate all page refcount implementation details inside mm headers. The main reason for this is the following patch that will stop using zero value for frozen refcounts.
Refactor external refcount API:
- Introduce init/init_as_frozen functions and replace low-level refcount manipulations with them where applicable.
- Introduce page/folio_is_frozen().
While page_ref_count() will always return zero for frozen pages, those functions allow more efficient (later) and more obvious code [1].
- Rename _unless_zero() into _unless_frozen() to deprecate zero value assumption.
Introduce debug asserts:
- VM_WARN_ON_ONCE() to prevent following scenarios:
page = alloc_frozen_page() /* page is frozen */ page_ref_inc(page, 1) /* BUG: Increment on frozen page instead of init */
[1]: https://lore.kernel.org/all/aqAIFV4nOGPbWiDS@thinkstation/
--- drivers/pci/p2pdma.c | 4 +-- drivers/virtio/virtio_mem.c | 2 +- include/linux/mm.h | 2 +- include/linux/page_ref.h | 43 +++++++++++++++++++++++++----- kernel/liveupdate/kexec_handover.c | 6 ++--- lib/test_hmm.c | 4 +-- mm/hugetlb.c | 2 +- mm/internal.h | 2 +- mm/memory-failure.c | 8 +++--- mm/memremap.c | 4 +-- mm/mm_init.c | 6 ++--- mm/page_alloc.c | 10 +++---- mm/page_frag_cache.c | 2 +- 13 files changed, 62 insertions(+), 33 deletions(-)
drivers/pci/p2pdma.c +2 -2
@@ -148,7 +148,7 @@ static int p2pmem_alloc_mmap(struct file *filp, struct kobject *kobj,* using it. */ VM_WARN_ON_ONCE_PAGE (page_ref_count (page), page); -set_page_count (page,1 ); +init_page_count (page); ret =vm_insert_page (vma, vaddr, page);if (ret) {gen_pool_free (p2pdma->pool, (uintptr_t )kaddr, len);
@@ -158,7 +158,7 @@ static int p2pmem_alloc_mmap(struct file *filp, struct kobject *kobj,* because we don't want to trigger the * p2pdma_folio_free() path. */ -set_page_count (page,0 ); +set_page_count_frozen (page);percpu_ref_put (ref);return ret; }
drivers/virtio/virtio_mem.c +1 -1
@@ -1293,7 +1293,7 @@ static void virtio_mem_fake_offline_cancel_offline(unsigned long pfn,* when going offline. */ for (i =0 ; i < nr_pages; i++) -page_ref_inc (pfn_to_page (pfn + i)); +init_page_count (pfn_to_page (pfn + i)); } static void virtio_mem_online_page(struct virtio_mem *vm,
include/linux/mm.h +1 -1
@@ -1860,7 +1860,7 @@ static inline int folio_put_testzero(struct folio *folio)*/ static inline bool get_page_unless_zero (struct page *page) { -return page_ref_add_unless_zero (page,1 ); +return page_ref_add_unless_frozen (page,1 ); }static inline struct folio *folio_get_nontail_page (struct page *page)
include/linux/page_ref.h +36 -7
135 lines not on this page · raw message
kernel/liveupdate/kexec_handover.c +3 -3
@@ -487,7 +487,7 @@ EXPORT_SYMBOL_GPL(kho_radix_walk_tree);static void kho_init_pages (struct page *page,unsigned long nr_pages) {for (unsigned long i =0 ; i < nr_pages; i++) { -set_page_count (page + i,1 ); +init_page_count (page + i);/* Clear each page's codetag to avoid accounting mismatch. */ clear_page_tag_ref (page + i); }
@@ -498,13 +498,13 @@ static void kho_init_folio(struct page *page, unsigned int order)unsigned long nr_pages = (1 << order);/* Head page gets refcount of 1. */ -set_page_count (page,1 ); +init_page_count (page);/* Clear head page's codetag to avoid accounting mismatch. */ clear_page_tag_ref (page);/* For higher order folios, tail pages get a page count of zero. */ for (unsigned long i =1 ; i < nr_pages; i++) -set_page_count (page + i,0 ); +set_page_count_frozen (page + i);if (order >0 )prep_compound_page (page, order);
lib/test_hmm.c +2 -2
@@ -1844,7 +1844,7 @@ static void dmirror_devmem_folio_split(struct folio *head, struct folio *tail)if (tail ==NULL ) {folio_reset_order (rfolio); rfolio->mapping =NULL ; -folio_set_count (rfolio,1 ); +folio_init_count (rfolio);return ; }
@@ -1858,7 +1858,7 @@ static void dmirror_devmem_folio_split(struct folio *head, struct folio *tail)folio_page (tail,0 )->mapping =folio_page (head,0 )->mapping; tail->pgmap = head->pgmap; -folio_set_count (page_folio (rpage_tail),1 ); +folio_init_count (page_folio (rpage_tail)); }static const struct dev_pagemap_ops dmirror_devmem_ops = {
mm/hugetlb.c +1 -1
@@ -3173,7 +3173,7 @@ static void __init hugetlb_folio_init_tail_vmemmap(struct folio *folio,for (pfn = head_pfn + start_page_number; pfn < end_pfn; page++, pfn++) {__init_single_page (page, pfn, zone, nid);prep_compound_tail (page, &folio->page, order); -set_page_count (page,0 ); +set_page_count_frozen (page); } }
mm/internal.h +1 -1
@@ -661,7 +661,7 @@ static inline void set_page_refcounted(struct page *page) {VM_BUG_ON_PAGE (PageTail (page), page);VM_BUG_ON_PAGE (page_ref_count (page), page); -set_page_count (page,1 ); +init_page_count (page); }static inline void set_pages_refcounted (struct page *page,unsigned long nr_pages)
mm/memory-failure.c +4 -4
@@ -214,7 +214,7 @@ static bool page_handle_poison(struct page *page, bool hugepage_or_freepage, booSetPageHWPoison (page);if (release)put_page (page); -page_ref_inc (page); +init_page_count (page);num_poisoned_pages_inc (page_to_pfn (page));return true;
@@ -1166,7 +1166,7 @@ static int me_huge_page(struct page_state *ps, struct page *p)*/ folio_put (folio);if (__page_handle_poison (p) >0 ) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else { res =MF_FAILED ;
@@ -2132,7 +2132,7 @@ static int try_memory_failure_hugetlb(unsigned long pfn, int flags)if (res ==MF_HUGETLB_FREED ) {folio_unlock (folio);if (__page_handle_poison (p) >0 ) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else { res =MF_FAILED ;
@@ -2462,7 +2462,7 @@ int memory_failure(unsigned long pfn, int flags)case 0 :if (is_free_buddy_page (p)) {if (take_page_off_buddy (p)) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else {/* We lost the race, try again */
mm/memremap.c +2 -2
@@ -463,7 +463,7 @@ void free_zone_device_folio(struct folio *folio)* Reset the refcount to 1 to prepare for handing out the page * again. */ -folio_set_count (folio,1 ); +folio_init_count (folio);break ;case MEMORY_DEVICE_FS_DAX :
@@ -520,7 +520,7 @@ void zone_device_page_init(struct page *page, struct dev_pagemap *pgmap,* memunmap_pages(). */ WARN_ON_ONCE (!percpu_ref_tryget_many (&page_pgmap (page)->ref,1 << order)); -set_page_count (page,1 ); +init_page_count (page);lock_page (page);if (order)
mm/mm_init.c +3 -3
@@ -1013,7 +1013,7 @@ static void __ref __init_zone_device_page(struct page *page, unsigned long pfn,case MEMORY_DEVICE_PRIVATE :case MEMORY_DEVICE_COHERENT :case MEMORY_DEVICE_PCI_P2PDMA : -set_page_count (page,0 ); +set_page_count_frozen (page);break ;case MEMORY_DEVICE_GENERIC :
@@ -1066,7 +1066,7 @@ static void __ref memmap_init_compound(struct page *head,__init_zone_device_page (page, pfn, zone_idx, nid, pgmap);prep_compound_tail (page, head, order); -set_page_count (page,0 ); +set_page_count_frozen (page); }prep_compound_head (head, order); }
@@ -2165,7 +2165,7 @@ void __init init_cma_reserved_pageblock(struct page *page)do {__ClearPageReserved (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }while (++p, --i);init_pageblock_migratetype (page,MIGRATE_CMA , false);
mm/page_alloc.c +5 -5
@@ -1606,8 +1606,8 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,/* * When initializing the memmap, __init_single_page() sets the refcount -* of all pages to 1 ("allocated"/"not free"). We have to set the -* refcount of all involved pages to 0. +* of all pages to 1 ("allocated"/"not free"). We have to freeze the +* refcount of all involved pages. * * Note that hotplugged memory pages are initialized to PageOffline(). * Pages freed from memblock might be marked as reserved.
@@ -1617,14 +1617,14 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,for (loop =0 ; loop < nr_pages; loop++, p++) {VM_WARN_ON_ONCE (PageReserved (p));__ClearPageOffline (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }adjust_managed_page_count (page, nr_pages); }else {for (loop =0 ; loop < nr_pages; loop++, p++) {__ClearPageReserved (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }/* memblock adjusts totalram_pages() manually. */
@@ -6452,7 +6452,7 @@ void free_reserved_pages(struct page *page, unsigned int order)for (i =0 ; i < nr_pages; i++) {clear_page_tag_ref (page + i); -set_page_count (page + i,0 ); +set_page_count_frozen (page + i);ClearPageReserved (page + i); }adjust_managed_page_count (page, nr_pages);
mm/page_frag_cache.c +1 -1
@@ -143,7 +143,7 @@ void *__page_frag_alloc_align(struct page_frag_cache *nc, goto refill; } -/* OK, page count is 0, we can safely set it */ +/* OK, page is frozen, we can safely set count */ set_page_count (page,PAGE_FRAG_CACHE_MAX_SIZE +1 );/* reset page count bias and offset to start of new frag */
Signature · 1 line
2.55.0
Re: [PATCH v6 2/3] mm: drop page refcount zero state semantics
Quoted 19 lines · Ilya Gladyshev
Some call sites manipulate page refcount directly based on its own assumptions of the refcount int value. Instead of making such assumptions these call sites should use high-level API like
set_frozen()orinit_refcount().This patch tries to consolidate all page refcount implementation details inside mm headers. The main reason for this is the following patch that will stop using zero value for frozen refcounts.
Refactor external refcount API:
- Introduceinit/init_as_frozenfunctions and replace low-level refcount manipulations with them where applicable.
- Introduce page/folio_is_frozen().
Whilepage_ref_count()will always return zero for frozen pages, those functions allow more efficient (later) and more obvious code [1].
- Rename_unless_zero()into_unless_frozen()to deprecate zero value assumption.
Quoted 5 lines
Introduce debug asserts:
-VM_WARN_ON_ONCE()to prevent following scenarios:page =
alloc_frozen_page()/* page is frozen */page_ref_inc(page, 1)/* BUG: Increment on frozen page instead of init */
page_ref_inc() takes one argument.
Quoted 133 lines
[1]: https://lore.kernel.org/all/aqAIFV4nOGPbWiDS@thinkstation/
Reviewed-by Artem Kuzin <[email protected]>Co-developed-by Ivan Gorbunov <[email protected]>Signed-off-by Ivan Gorbunov <[email protected]>Signed-off-by Ilya Gladyshev <[email protected]>--- drivers/pci/p2pdma.c | 4 +-- drivers/virtio/virtio_mem.c | 2 +- include/linux/mm.h | 2 +- include/linux/page_ref.h | 43 +++++++++++++++++++++++++----- kernel/liveupdate/kexec_handover.c | 6 ++--- lib/test_hmm.c | 4 +-- mm/hugetlb.c | 2 +- mm/internal.h | 2 +- mm/memory-failure.c | 8 +++--- mm/memremap.c | 4 +-- mm/mm_init.c | 6 ++--- mm/page_alloc.c | 10 +++---- mm/page_frag_cache.c | 2 +- 13 files changed, 62 insertions(+), 33 deletions(-)drivers/pci/p2pdma.c +2
-2@@ -148,7 +148,7 @@ static int p2pmem_alloc_mmap(struct file *filp, struct kobject *kobj,* using it. */ VM_WARN_ON_ONCE_PAGE (page_ref_count (page), page); -set_page_count (page,1 ); +init_page_count (page); ret =vm_insert_page (vma, vaddr, page);if (ret) {gen_pool_free (p2pdma->pool, (uintptr_t )kaddr, len);@@ -158,7 +158,7 @@ static int p2pmem_alloc_mmap(struct file *filp, struct kobject *kobj,* because we don't want to trigger the * p2pdma_folio_free() path. */ -set_page_count (page,0 ); +set_page_count_frozen (page);percpu_ref_put (ref);return ret; }drivers/virtio/virtio_mem.c +1
-1@@ -1293,7 +1293,7 @@ static void virtio_mem_fake_offline_cancel_offline(unsigned long pfn,* when going offline. */ for (i =0 ; i < nr_pages; i++) -page_ref_inc (pfn_to_page (pfn + i)); +init_page_count (pfn_to_page (pfn + i)); } static void virtio_mem_online_page(struct virtio_mem *vm,include/linux/mm.h +1
-1@@ -1860,7 +1860,7 @@ static inline int folio_put_testzero(struct folio *folio)*/ static inline bool get_page_unless_zero (struct page *page) { -return page_ref_add_unless_zero (page,1 ); +return page_ref_add_unless_frozen (page,1 ); }static inline struct folio *folio_get_nontail_page (struct page *page)include/linux/page_ref.h +24
-2@@ -62,6 +62,21 @@ static inline void __page_ref_unfreeze(struct page *page, int v)#endif +static inline bool __page_count_is_frozen (int count) +{ +return count ==0 ; +} + +static inline bool page_is_frozen (const struct page *page) +{ +return __page_count_is_frozen (atomic_read (&page->_refcount)); +} + +static inline bool folio_is_frozen (const struct folio *folio) +{ +return page_is_frozen (&folio->page); +} +static inline int page_ref_count (const struct page *page) {return atomic_read (&page->_refcount);@@ -119,9 +134,9 @@ static inline void set_page_count(struct page *page, int v)__page_ref_set (page, v); } -static inline void folio_set_count (struct folio *folio,int v) +static inline void folio_init_count (struct folio *folio) { -set_page_count (&folio->page, v); +set_page_count (&folio->page,1 ); }/* @@ -133,8 +148,14 @@ static inline void init_page_count(struct page *page)set_page_count (page,1 ); } +static inline void set_page_count_frozen (struct page *page) +{ +set_page_count (page,0 ); +} +static inline void page_ref_add (struct page *page,int nr) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);atomic_add (nr, &page->_refcount);if (page_ref_tracepoint_active (page_ref_mod))__page_ref_mod (page, nr);@@ -147,6 +168,7 @@ static inline void folio_ref_add(struct folio *folio, int nr)static inline void page_ref_sub (struct page *page,int nr) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);atomic_sub (nr, &page->_refcount);if (page_ref_tracepoint_active (page_ref_mod))__page_ref_mod (page, -nr);
include/linux/page_ref.h static inline void folio_ref_sub(struct folio *folio, int nr) · line 188
Quoted 7 lines
include/linux/page_ref.h +1
-0@@ -160,6 +182,7 @@ static inline void folio_ref_sub(struct folio *folio, int nr)static inline int folio_ref_sub_return (struct folio *folio,int nr) {int ret =atomic_sub_return (nr, &folio->_refcount); +VM_WARN_ON_ONCE_FOLIO (__page_count_is_frozen (ret + nr), folio);if (page_ref_tracepoint_active (page_ref_mod_and_return))__page_ref_mod_and_return (&folio->page, -nr, ret);
Empty line has to be above VM_WARN_ON_ONCE_FOLIO(), not below.
The same applies to all functions below.
Quoted 17 lines
include/linux/page_ref.h +2
-0@@ -168,6 +191,7 @@ static inline int folio_ref_sub_return(struct folio *folio, int nr)static inline void page_ref_inc (struct page *page) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);atomic_inc (&page->_refcount);if (page_ref_tracepoint_active (page_ref_mod))__page_ref_mod (page,1 );@@ -180,6 +204,7 @@ static inline void folio_ref_inc(struct folio *folio)static inline void page_ref_dec (struct page *page) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);atomic_dec (&page->_refcount);if (page_ref_tracepoint_active (page_ref_mod))__page_ref_mod (page,-1 );
include/linux/page_ref.h static inline void folio_ref_dec(struct folio *folio) · line 223
Quoted 7 lines
include/linux/page_ref.h +1
-0@@ -192,6 +217,7 @@ static inline void folio_ref_dec(struct folio *folio)static inline int page_ref_sub_and_test (struct page *page,int nr) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);int ret =atomic_sub_and_test (nr, &page->_refcount);if (page_ref_tracepoint_active (page_ref_mod_and_test))
Declaration block has to be first in the function:
int ret; VM_WARN_ON_ONCE_PAGE(page_is_frozen(page), page);
ret = atomic_sub_and_test(nr, &page->_refcount);
Quoted 9 lines
include/linux/page_ref.h +1
-0@@ -207,6 +233,7 @@ static inline int folio_ref_sub_and_test(struct folio *folio, int nr)static inline int page_ref_inc_return (struct page *page) {int ret =atomic_inc_return (&page->_refcount); +VM_WARN_ON_ONCE_PAGE (__page_count_is_frozen (ret -1 ), page);if (page_ref_tracepoint_active (page_ref_mod_and_return))__page_ref_mod_and_return (page,1 , ret);
include/linux/page_ref.h static inline int folio_ref_inc_return(struct folio *folio) · line 252
Quoted 6 lines
include/linux/page_ref.h +1
-0@@ -220,6 +247,7 @@ static inline int folio_ref_inc_return(struct folio *folio)static inline int page_ref_dec_and_test (struct page *page) { +VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);int ret =atomic_dec_and_test (&page->_refcount);
The same as page_ref_sub_and_test().
Quoted 127 lines
include/linux/page_ref.h +6
-5if (page_ref_tracepoint_active (page_ref_mod_and_test))@@ -235,6 +263,7 @@ static inline int folio_ref_dec_and_test(struct folio *folio)static inline int page_ref_dec_return (struct page *page) {int ret =atomic_dec_return (&page->_refcount); +VM_WARN_ON_ONCE_PAGE (__page_count_is_frozen (ret +1 ), page);if (page_ref_tracepoint_active (page_ref_mod_and_return))__page_ref_mod_and_return (page,-1 , ret);@@ -246,7 +275,7 @@ static inline int folio_ref_dec_return(struct folio *folio)return page_ref_dec_return (&folio->page); } -static inline bool page_ref_add_unless_zero (struct page *page,int nr) +static inline bool page_ref_add_unless_frozen (struct page *page,int nr) {bool ret =atomic_add_unless (&page->_refcount, nr,0 );@@ -255,9 +284,9 @@ static inline bool page_ref_add_unless_zero(struct page *page, int nr)return ret; } -static inline bool folio_ref_add_unless_zero (struct folio *folio,int nr) +static inline bool folio_ref_add_unless_frozen (struct folio *folio,int nr) { -return page_ref_add_unless_zero (&folio->page, nr); +return page_ref_add_unless_frozen (&folio->page, nr); }/** @@ -273,12 +302,12 @@ static inline bool folio_ref_add_unless_zero(struct folio *folio, int nr)*/ static inline bool folio_try_get (struct folio *folio) { -return folio_ref_add_unless_zero (folio,1 ); +return folio_ref_add_unless_frozen (folio,1 ); }static inline bool folio_ref_try_add (struct folio *folio,int count) { -return folio_ref_add_unless_zero (folio, count); +return folio_ref_add_unless_frozen (folio, count); }static inline int page_ref_freeze (struct page *page,int count)kernel/liveupdate/kexec_handover.c +3
-3@@ -487,7 +487,7 @@ EXPORT_SYMBOL_GPL(kho_radix_walk_tree);static void kho_init_pages (struct page *page,unsigned long nr_pages) {for (unsigned long i =0 ; i < nr_pages; i++) { -set_page_count (page + i,1 ); +init_page_count (page + i);/* Clear each page's codetag to avoid accounting mismatch. */ clear_page_tag_ref (page + i); }@@ -498,13 +498,13 @@ static void kho_init_folio(struct page *page, unsigned int order)unsigned long nr_pages = (1 << order);/* Head page gets refcount of 1. */ -set_page_count (page,1 ); +init_page_count (page);/* Clear head page's codetag to avoid accounting mismatch. */ clear_page_tag_ref (page);/* For higher order folios, tail pages get a page count of zero. */ for (unsigned long i =1 ; i < nr_pages; i++) -set_page_count (page + i,0 ); +set_page_count_frozen (page + i);if (order >0 )prep_compound_page (page, order);lib/test_hmm.c +2
-2@@ -1844,7 +1844,7 @@ static void dmirror_devmem_folio_split(struct folio *head, struct folio *tail)if (tail ==NULL ) {folio_reset_order (rfolio); rfolio->mapping =NULL ; -folio_set_count (rfolio,1 ); +folio_init_count (rfolio);return ; }@@ -1858,7 +1858,7 @@ static void dmirror_devmem_folio_split(struct folio *head, struct folio *tail)folio_page (tail,0 )->mapping =folio_page (head,0 )->mapping; tail->pgmap = head->pgmap; -folio_set_count (page_folio (rpage_tail),1 ); +folio_init_count (page_folio (rpage_tail)); }static const struct dev_pagemap_ops dmirror_devmem_ops = {mm/hugetlb.c +1
-1@@ -3173,7 +3173,7 @@ static void __init hugetlb_folio_init_tail_vmemmap(struct folio *folio,for (pfn = head_pfn + start_page_number; pfn < end_pfn; page++, pfn++) {__init_single_page (page, pfn, zone, nid);prep_compound_tail (page, &folio->page, order); -set_page_count (page,0 ); +set_page_count_frozen (page); } }mm/internal.h +1
-1@@ -661,7 +661,7 @@ static inline void set_page_refcounted(struct page *page) {VM_BUG_ON_PAGE (PageTail (page), page);VM_BUG_ON_PAGE (page_ref_count (page), page); -set_page_count (page,1 ); +init_page_count (page); }static inline void set_pages_refcounted (struct page *page,unsigned long nr_pages)mm/memory-failure.c +0
-0
mm/memory-failure.c static bool page_handle_poison(struct page *page, bool hugepage_or_freepage, boo · line 220
Quoted 8 lines
mm/memory-failure.c +1
-1@@ -214,7 +214,7 @@ static bool page_handle_poison(struct page *page, bool hugepage_or_freepage, booSetPageHWPoison (page);if (release)put_page (page); -page_ref_inc (page); +init_page_count (page);num_poisoned_pages_inc (page_to_pfn (page));return true;
I don't think it is safe. Speculative folio_try_get() can bump refcount just before init_page_count() here. When speculative scanner gives the pin back, it will drop to 0.
Quoted 137 lines
mm/memory-failure.c +3
-3@@ -1166,7 +1166,7 @@ static int me_huge_page(struct page_state *ps, struct page *p)*/ folio_put (folio);if (__page_handle_poison (p) >0 ) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else { res =MF_FAILED ;@@ -2132,7 +2132,7 @@ static int try_memory_failure_hugetlb(unsigned long pfn, int flags)if (res ==MF_HUGETLB_FREED ) {folio_unlock (folio);if (__page_handle_poison (p) >0 ) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else { res =MF_FAILED ;@@ -2462,7 +2462,7 @@ int memory_failure(unsigned long pfn, int flags)case 0 :if (is_free_buddy_page (p)) {if (take_page_off_buddy (p)) { -page_ref_inc (p); +init_page_count (p); res =MF_RECOVERED ; }else {/* We lost the race, try again */ mm/memremap.c +2
-2@@ -463,7 +463,7 @@ void free_zone_device_folio(struct folio *folio)* Reset the refcount to 1 to prepare for handing out the page * again. */ -folio_set_count (folio,1 ); +folio_init_count (folio);break ;case MEMORY_DEVICE_FS_DAX :@@ -520,7 +520,7 @@ void zone_device_page_init(struct page *page, struct dev_pagemap *pgmap,* memunmap_pages(). */ WARN_ON_ONCE (!percpu_ref_tryget_many (&page_pgmap (page)->ref,1 << order)); -set_page_count (page,1 ); +init_page_count (page);lock_page (page);if (order)mm/mm_init.c +3
-3@@ -1013,7 +1013,7 @@ static void __ref __init_zone_device_page(struct page *page, unsigned long pfn,case MEMORY_DEVICE_PRIVATE :case MEMORY_DEVICE_COHERENT :case MEMORY_DEVICE_PCI_P2PDMA : -set_page_count (page,0 ); +set_page_count_frozen (page);break ;case MEMORY_DEVICE_GENERIC :@@ -1066,7 +1066,7 @@ static void __ref memmap_init_compound(struct page *head,__init_zone_device_page (page, pfn, zone_idx, nid, pgmap);prep_compound_tail (page, head, order); -set_page_count (page,0 ); +set_page_count_frozen (page); }prep_compound_head (head, order); }@@ -2165,7 +2165,7 @@ void __init init_cma_reserved_pageblock(struct page *page)do {__ClearPageReserved (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }while (++p, --i);init_pageblock_migratetype (page,MIGRATE_CMA , false);mm/page_alloc.c +5
-5@@ -1606,8 +1606,8 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,/* * When initializing the memmap, __init_single_page() sets the refcount -* of all pages to 1 ("allocated"/"not free"). We have to set the -* refcount of all involved pages to 0. +* of all pages to 1 ("allocated"/"not free"). We have to freeze the +* refcount of all involved pages. * * Note that hotplugged memory pages are initialized to PageOffline(). * Pages freed from memblock might be marked as reserved. @@ -1617,14 +1617,14 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,for (loop =0 ; loop < nr_pages; loop++, p++) {VM_WARN_ON_ONCE (PageReserved (p));__ClearPageOffline (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }adjust_managed_page_count (page, nr_pages); }else {for (loop =0 ; loop < nr_pages; loop++, p++) {__ClearPageReserved (p); -set_page_count (p,0 ); +set_page_count_frozen (p); }/* memblock adjusts totalram_pages() manually. */ @@ -6452,7 +6452,7 @@ void free_reserved_pages(struct page *page, unsigned int order)for (i =0 ; i < nr_pages; i++) {clear_page_tag_ref (page + i); -set_page_count (page + i,0 ); +set_page_count_frozen (page + i);ClearPageReserved (page + i); }adjust_managed_page_count (page, nr_pages);mm/page_frag_cache.c +1
-1@@ -143,7 +143,7 @@ void *__page_frag_alloc_align(struct page_frag_cache *nc, goto refill; } -/* OK, page count is 0, we can safely set it */ +/* OK, page is frozen, we can safely set count */ set_page_count (page,PAGE_FRAG_CACHE_MAX_SIZE +1 );/* reset page count bias and offset to start of new frag */ Signature · 1 line
2.55.0
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
PATCH v6 3/3 mm: implement page refcount locking via dedicated bit
In reply to Ilya GladyshevThe current page refcount implementation uses a single counter value (zero) as dead. So, to prevent incrementing a dead refcount in folio_try_get(), it fundamentally requires a CAS loop.
This CAS loop can act as a serialization point and can become a significant bottleneck during high-frequency file read operations [1][2].
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference count
In other words, refcount is now split into "dead bit" and a 31-bit counter.
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS to INT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.
The size of the dead zone allows performing an optimistic increment inside page_ref_add_unless_frozen(), replacing the previous read + CAS loop with a single RMW operation. This reduces cache line bouncing and improves scalability, especially in NUMA scenarios.
[1]: https://lore.kernel.org/all/[email protected]/
[2]: https://lore.kernel.org/all/CAHk-=wj00-nGmXEkxY=-=Z_qP6kiGUziSFvxHJ9N-cLWry5zpA@mail.gmail.com/
--- include/linux/page-flags.h | 13 +++++++++++++ include/linux/page_ref.h | 30 +++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-)
include/linux/page-flags.h +13 -0
@@ -196,6 +196,19 @@ enum pageflags {#define PAGEFLAGS_MASK ((1UL << NR_PAGEFLAGS) - 1) +/* Most significant bit in page refcount */ +#define PAGEREF_FROZEN_BIT BIT(31) + +/* Page reference counter can be in 3 logical states, +* which are described below with their value representation +* state | value +* (1) safe with owners | 1...INT_MAX +* (2) safe with no owners | 0 +* (3) frozen | INT_MIN....-1 +* +* State (2) can only temporarily occur inside dec_and_test. +*/ +#ifndef __GENERATING_BOUNDS_H/*
include/linux/page_ref.h +25 -5
@@ -64,7 +64,7 @@ static inline void __page_ref_unfreeze(struct page *page, int v)static inline bool __page_count_is_frozen (int count) { -return count ==0 ; +return count &PAGEREF_FROZEN_BIT ; }static inline bool page_is_frozen (const struct page *page)
@@ -79,7 +79,12 @@ static inline bool folio_is_frozen(const struct folio *folio)static inline int page_ref_count (const struct page *page) { -return atomic_read (&page->_refcount); +int val =atomic_read (&page->_refcount); + +if (unlikely (val &PAGEREF_FROZEN_BIT )) +return 0 ; + +return val; }/**
@@ -150,7 +155,7 @@ static inline void init_page_count(struct page *page)static inline void set_page_count_frozen (struct page *page) { -set_page_count (page,0 ); +set_page_count (page,PAGEREF_FROZEN_BIT ); }static inline void page_ref_add (struct page *page,int nr)
@@ -220,6 +225,9 @@ static inline int page_ref_sub_and_test(struct page *page, int nr)VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);int ret =atomic_sub_and_test (nr, &page->_refcount); +if (ret) + ret = !atomic_cmpxchg_relaxed (&page->_refcount,0 ,PAGEREF_FROZEN_BIT ); +if (page_ref_tracepoint_active (page_ref_mod_and_test))__page_ref_mod_and_test (page, -nr, ret);return ret;
@@ -250,6 +258,9 @@ static inline int page_ref_dec_and_test(struct page *page)VM_WARN_ON_ONCE_PAGE (page_is_frozen (page), page);int ret =atomic_dec_and_test (&page->_refcount); +if (ret) + ret = !atomic_cmpxchg_relaxed (&page->_refcount,0 ,PAGEREF_FROZEN_BIT ); +if (page_ref_tracepoint_active (page_ref_mod_and_test))__page_ref_mod_and_test (page,-1 , ret);return ret;
@@ -275,9 +286,18 @@ static inline int folio_ref_dec_return(struct folio *folio)return page_ref_dec_return (&folio->page); } +#define _PAGEREF_FROZEN_LIMIT ((1 << 30) | PAGEREF_FROZEN_BIT) +static inline bool page_ref_add_unless_frozen (struct page *page,int nr) { -bool ret =atomic_add_unless (&page->_refcount, nr,0 ); +int val =atomic_add_return (nr, &page->_refcount); +bool ret = !(val &PAGEREF_FROZEN_BIT ); + +/* Undo atomic_add() if counter is locked and scary big */ +while (unlikely ((unsigned int )val >= _PAGEREF_FROZEN_LIMIT)) { +if (atomic_try_cmpxchg_relaxed (&page->_refcount, &val,PAGEREF_FROZEN_BIT )) +break ; + }if (page_ref_tracepoint_active (page_ref_mod_unless))__page_ref_mod_unless (page, nr, ret);
@@ -312,7 +332,7 @@ static inline bool folio_ref_try_add(struct folio *folio, int count)static inline int page_ref_freeze (struct page *page,int count) { -int ret =likely (atomic_cmpxchg (&page->_refcount, count,0 ) == count); +int ret =likely (atomic_cmpxchg (&page->_refcount, count,PAGEREF_FROZEN_BIT ) == count);if (page_ref_tracepoint_active (page_ref_freeze))__page_ref_freeze (page, count, ret);
Signature · 1 line
2.55.0
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 48 lines · Ilya Gladyshev
The current page refcount implementation uses a single counter value (zero) as dead. So, to prevent incrementing a dead refcount in
folio_try_get(), it fundamentally requires a CAS loop.This CAS loop can act as a serialization point and can become a significant bottleneck during high-frequency file read operations [1][2].
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference countIn other words, refcount is now split into "dead bit" and a 31-bit counter.
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.The size of the dead zone allows performing an optimistic increment inside
page_ref_add_unless_frozen(), replacing the previous read + CAS loop with a single RMW operation. This reduces cache line bouncing and improves scalability, especially in NUMA scenarios.[1]: https://lore.kernel.org/all/[email protected]/
[2]: https://lore.kernel.org/all/CAHk-=wj00-nGmXEkxY=-=Z_qP6kiGUziSFvxHJ9N-cLWry5zpA@mail.gmail.com/Reviewed-by Artem Kuzin <[email protected]>Co-developed-by Ivan Gorbunov <[email protected]>Signed-off-by Ivan Gorbunov <[email protected]>Signed-off-by Ilya Gladyshev <[email protected]>Acked-by Linus Torvalds <[email protected]>--- include/linux/page-flags.h | 13 +++++++++++++ include/linux/page_ref.h | 30 +++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-)include/linux/page-flags.h +0
-0
include/linux/page-flags.h enum pageflags { · line 200
Quoted 5 lines
include/linux/page-flags.h +2
-0@@ -196,6 +196,19 @@ enum pageflags {#define PAGEFLAGS_MASK ((1UL << NR_PAGEFLAGS) - 1) +/* Most significant bit in page refcount */ +#define PAGEREF_FROZEN_BIT BIT(31)
I am not sure about the naming here. I would expect _BIT to be 31. Maybe we should name it _MASK?
#define PAGEREF_FROZEN_BIT 31
#define PAGEREF_FROZEN_MASK BIT(PAGEREF_FROZEN_BIT)
include/linux/page-flags.h enum pageflags { · line 211
Quoted 11 lines
include/linux/page-flags.h +11
-0+ +/* Page reference counter can be in 3 logical states, +* which are described below with their value representation +* state | value +* (1) safe with owners | 1...INT_MAX +* (2) safe with no owners | 0 +* (3) frozen | INT_MIN....-1 +* +* State (2) can only temporarily occur inside dec_and_test. +*/ +
Do we what to enforce your comment about (2)? VM_WARN() in page_ref_dec(), page_ref_sub() and _return() variants?
BTW, I wounder if all these VM_WARNs makes DEBUG_VM=y kernel too slow? Do we need a separate config option to debug refcounts?
Quoted 17 lines
include/linux/page-flags.h +0
-0#ifndef __GENERATING_BOUNDS_H/* include/linux/page_ref.h +1
-1@@ -64,7 +64,7 @@ static inline void __page_ref_unfreeze(struct page *page, int v)static inline bool __page_count_is_frozen (int count) { -return count ==0 ; +return count &PAGEREF_FROZEN_BIT ; }static inline bool page_is_frozen (const struct page *page)
include/linux/page_ref.h static inline bool folio_is_frozen(const struct folio *folio) · line 87
Quoted 10 lines
include/linux/page_ref.h +6
-1@@ -79,7 +79,12 @@ static inline bool folio_is_frozen(const struct folio *folio)static inline int page_ref_count (const struct page *page) { -return atomic_read (&page->_refcount); +int val =atomic_read (&page->_refcount); + +if (unlikely (val &PAGEREF_FROZEN_BIT )) +return 0 ; + +return val;
I was worried about the branch in the hot path and wanted to propose a branchless hack -- val & ~(val >> 31) -- but compilers seem to be good enough to generate CMOV. The hack produces one more instruction.
include/linux/page_ref.h static inline void init_page_count(struct page *page) · line 158
Quoted 9 lines
include/linux/page_ref.h +1
-1}/** @@ -150,7 +155,7 @@ static inline void init_page_count(struct page *page)static inline void set_page_count_frozen (struct page *page) { -set_page_count (page,0 ); +set_page_count (page,PAGEREF_FROZEN_BIT );
Are you sure we covered the cases that do frozen -> 1 transition with page_ref_inc()?
Patch 2 switches __init_zone_device_page() to set_page_count_frozen(). Who unfreezes them?
Looking dax_fault_iter(), it seems to be done by folio_ref_inc() there for FS_DAX case. Switching to PAGEREF_FROZEN_BIT would break it, no?
It seems some more ground work needed.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Zi Yan
reviewerRe: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
In reply to Ilya GladyshevQuoted 17 lines · Ilya Gladyshev
The current page refcount implementation uses a single counter value (zero) as dead. So, to prevent incrementing a dead refcount in
folio_try_get(), it fundamentally requires a CAS loop.This CAS loop can act as a serialization point and can become a significant bottleneck during high-frequency file read operations [1][2].
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference countIn other words, refcount is now split into "dead bit" and a 31-bit counter.
Quoted 7 lines
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.
I am not sure this works. David raised a concern on V4[1], where if a folio lands to page_frag_free()'s free_frozen_pages(), it will not be properly handled. "this page is their problem" only works if every folio/page free functions call a generic folio/page free function that checks the page type and does the proper handling.
Before your patches, only a speculative page getter can see any page and folio_put() handles the last reference drop. So it is safe today.
Let me know if I miss anything.
[1] https://lore.kernel.org/linux-mm/[email protected]/
Quoted 19 lines
The size of the dead zone allows performing an optimistic increment inside
page_ref_add_unless_frozen(), replacing the previous read + CAS loop with a single RMW operation. This reduces cache line bouncing and improves scalability, especially in NUMA scenarios.[1]: https://lore.kernel.org/all/[email protected]/
[2]: https://lore.kernel.org/all/CAHk-=wj00-nGmXEkxY=-=Z_qP6kiGUziSFvxHJ9N-cLWry5zpA@mail.gmail.com/Reviewed-by Artem Kuzin <[email protected]>Co-developed-by Ivan Gorbunov <[email protected]>Signed-off-by Ivan Gorbunov <[email protected]>Signed-off-by Ilya Gladyshev <[email protected]>Acked-by Linus Torvalds <[email protected]>--- include/linux/page-flags.h | 13 +++++++++++++ include/linux/page_ref.h | 30 +++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-)
Signature · 2 lines
Best Regards, Yan, Zi
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 43 lines · Zi Yan
Quoted 18 lines · Ilya Gladyshev
The current page refcount implementation uses a single counter value
(zero) as dead. So, to prevent incrementing a dead refcount infolio_try_get(), it fundamentally requires a CAS loop.This CAS loop can act as a serialization point and can become a significant bottleneck during high-frequency file read operations [1][2].
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference countIn other words, refcount is now split into "dead bit" and a 31-bit counter.
Quoted 8 lines
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.I am not sure this works. David raised a concern on V4[1], where if a folio lands to page_frag_free()'s
free_frozen_pages(), it will not be properly handled. "this page is their problem" only works if every folio/page free functions call a generic folio/page free function that checks the page type and does the proper handling.Quoted 7 lines
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.
Quoted 5 lines
I am not sure this works. David raised a concern on V4[1], where if a folio lands to page_frag_free()'s
free_frozen_pages(), it will not be properly handled. "this page is their problem" only works if every folio/page free functions call a generic folio/page free function that checks the page type and does the proper handling.
After some thinking, this scenario is indeed possible and problematic. The complexity of the required setup give me hope that there are some implicit barriers, however I wasn't able to find them. But I am not an mm guru, so maybe I missed something :shrug:
-- Below is a minimal buggy scenario --
Thread A: network code that calls page_frag_free()
Thread B: calls folio_try_get(). Maybe it is the DAMON paddr scanner,
maybe this folio was previously used in page cache (however this noticeably complicates the scenario)
Thread C: user of folio's reincarnation. It also requires destruction via
folio_put() and not via free_frozen_pages() -- for example hugetlb or memcg charged folio.
Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [ page_ref_dec_and_test() ]
B: INC 0 -> 1 [ folio_try_get() ] => success
B: DEC 1 -> 0 [ page_ref_dec_and_test() ]
B: CAS 0 -> FR [ page_ref_dec_and_test() ]
B: * deallocates via __folio_put() *
C: * re-allocates page *
C: DEC 1 -> 0 [ page_ref_dec_and_test() ]
A: CAS 0 -> FR [ page_ref_dec_and_test() ] => success
A: * deallocates directly via free_frozen_pages() * => BUG
This can be fixed with a s/page_frag_free/folio_put/-style patch, however it seems unreasonable...
Quoted 16 lines
Before your patches, only a speculative page getter can see any page and
folio_put()handles the last reference drop. So it is safe today.Let me know if I miss anything.
[1] https://lore.kernel.org/linux-mm/[email protected]/
Quoted 9 lines
The size of the dead zone allows performing an optimistic increment
insidepage_ref_add_unless_frozen(), replacing the previous read + CAS loop with a single RMW operation. This reduces cache line bouncing and improves scalability, especially in NUMA scenarios.[1]: https://lore.kernel.org/all/[email protected]/
[2]: https://lore.kernel.org/all/CAHk-=wj00-nGmXEkxY=-=Z_qP6kiGUziSFvxHJ9N-cLWry5zpA@mail.gmail.com/
Quoted 14 lines
Quoted 10 lines
Reviewed-by Artem Kuzin <[email protected]>Co-developed-by Ivan Gorbunov <[email protected]>Signed-off-by Ivan Gorbunov <[email protected]>Signed-off-by Ilya Gladyshev <[email protected]>Acked-by Linus Torvalds <[email protected]>---
include/linux/page-flags.h | 13 +++++++++++++ include/linux/page_ref.h | 30 +++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-)Signature · 2 lines
Best Regards, Yan, Zi
---
Ilya Gladyshev // foxido.dev
David Hildenbrand (Arm)
maintainerRe: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 75 lines · Ilya Gladyshev
Quoted 43 lines · Zi Yan
Quoted 18 lines · Ilya Gladyshev
The current page refcount implementation uses a single counter value
(zero) as dead. So, to prevent incrementing a dead refcount infolio_try_get(), it fundamentally requires a CAS loop.This CAS loop can act as a serialization point and can become a significant bottleneck during high-frequency file read operations [1][2].
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference countIn other words, refcount is now split into "dead bit" and a 31-bit counter.
Quoted 8 lines
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.I am not sure this works. David raised a concern on V4[1], where if a folio lands to page_frag_free()'s
free_frozen_pages(), it will not be properly handled. "this page is their problem" only works if every folio/page free functions call a generic folio/page free function that checks the page type and does the proper handling.Quoted 7 lines
Refcount decrement now works as follows:
1. Counter decrement
2. If it is now zero, try to put it deep inside the dead zone (CAS toINT_MIN). Or you can view it as "set up frozen bit and reset counter".
3. This CAS can fail only if someone grabbed a reference in-between -- that's okay, this page is their problem now.Quoted 5 lines
I am not sure this works. David raised a concern on V4[1], where if a folio lands to page_frag_free()'s
free_frozen_pages(), it will not be properly handled. "this page is their problem" only works if every folio/page free functions call a generic folio/page free function that checks the page type and does the proper handling.After some thinking, this scenario is indeed possible and problematic. The complexity of the required setup give me hope that there are some implicit barriers, however I wasn't able to find them. But I am not an mm guru, so maybe I missed something :shrug:
-- Below is a minimal buggy scenario --
Thread A: network code that calls
page_frag_free()
Thread B: callsfolio_try_get(). Maybe it is the DAMON paddr scanner,
maybe this folio was previously used in page cache (however this noticeably complicates the scenario)
Thread C: user of folio's reincarnation. It also requires destruction via
folio_put()and not viafree_frozen_pages()-- for example hugetlb or memcg charged folio.Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [
page_ref_dec_and_test()]
B: INC 0 -> 1 [folio_try_get()] => success
B: DEC 1 -> 0 [page_ref_dec_and_test()]
B: CAS 0 -> FR [page_ref_dec_and_test()]
B: * deallocates via__folio_put()*
Quoted 7 lines
C: * re-allocates page *
C: DEC 1 -> 0 [page_ref_dec_and_test()]
A: CAS 0 -> FR [page_ref_dec_and_test()] => success
A: * deallocates directly viafree_frozen_pages()* => BUGThis can be fixed with a
s/page_frag_free/folio_put/-style patch, however it seems unreasonable...
There needs to be a common page freeing function that dipatches stuff.
folio_put() is not the best future-proof thing, because not all pages will be folios sooninsh.
Signature · 3 lines
Cheers, David
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 2 lines · David Hildenbrand (Arm)
There needs to be a common page freeing function that dipatches stuff.
I think this all needs to be entirely generic to all pages. Agreed. Everybody needs to do it right.
But I think that in the scenario that Ilya mentioned:
Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [ page_ref_dec_and_test() ] B: INC 0 -> 1 [ folio_try_get() ] => success B: DEC 1 -> 0 [ page_ref_dec_and_test() ]
B: CAS 0 -> FR [ page_ref_dec_and_test() ]
B: * deallocates via __folio_put() *
C: * re-allocates page *
C: DEC 1 -> 0 [ page_ref_dec_and_test() ]
A: CAS 0 -> FR [ page_ref_dec_and_test() ] => success
A: * deallocates directly via free_frozen_pages() * => BUG
the big here is that the de-allocation was done not from the state of the *page*, but from the state of who finished it off.
So that
A: CAS 0 -> FR [ page_ref_dec_and_test() ] => success
is not a problem per se. It's a fine case of "we had one single unique final free".
The failure is that A then goes on to do that
"deallocates directly via free_frozen_pages"
because A mis-judged the page as having the old stale state that it had when *A* was using it.
IOW, I think the soluition is either:
(a) every user of put_page_testzero(page) agrees tro do exactly the
same thing and there is no difference between how that final "0->FROZEN" is dealt with, so it doesn't matter that A is freeing a page that C allocated for something else
or
(b) the CAS 0 -> FR state sequence always looks at the *page* to
decide what to do, never at the caller state (ie A will look at whatever C wrote when it allocated the page to know how to do that final free).
Hmm?
Linus
Zi Yan
reviewerRe: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 6 lines · Linus Torvalds
Quoted 2 lines · David Hildenbrand (Arm)
There needs to be a common page freeing function that dipatches stuff.
I think this all needs to be entirely generic to all pages. Agreed. Everybody needs to do it right.
It assumes all callers of page_ref_dec_and_test() are free page functions, but there are two exceptions:
1. compaction_free() uses folio_put_testzero(), which calls page_ref_dec_and_test() to put not used migration dst page back to isolated freelist instead of freeing them.
2. virtio_mem_fake_offline_going_offline() drops the last reference of fake-offline pages, so that they can be offline.
They will need to be converted to not use page_ref_dec_and_test() in addition to a common page freeing function. And any such future use needs to be banned.
Quoted 40 lines
But I think that in the scenario that Ilya mentioned:
Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [ page_ref_dec_and_test() ] B: INC 0 -> 1 [ folio_try_get() ] => success B: DEC 1 -> 0 [ page_ref_dec_and_test() ]B: CAS 0 -> FR [
page_ref_dec_and_test()]
B: * deallocates via__folio_put()*C: * re-allocates page *
C: DEC 1 -> 0 [page_ref_dec_and_test()]
A: CAS 0 -> FR [page_ref_dec_and_test()] => success
A: * deallocates directly viafree_frozen_pages()* => BUGthe big here is that the de-allocation was done not from the state of the *page*, but from the state of who finished it off.
So that
A: CAS 0 -> FR [
page_ref_dec_and_test()] => successis not a problem per se. It's a fine case of "we had one single unique final free".
The failure is that A then goes on to do that
"deallocates directly via
free_frozen_pages"because A mis-judged the page as having the old stale state that it had when *A* was using it.
IOW, I think the soluition is either:
(a) every user of
put_page_testzero(page)agrees tro do exactly the
same thing and there is no difference between how that final "0->FROZEN" is dealt with, so it doesn't matter that A is freeing a page that C allocated for something else
Quoted 9 lines
or
(b) the CAS 0 -> FR state sequence always looks at the *page* to
decide what to do, never at the caller state (ie A will look at whatever C wrote when it allocated the page to know how to do that final free).Hmm?
Just wonder if we could make 1 -> FROZEN atomic to get rid of the gap.
Signature · 2 lines
Best Regards, Yan, Zi
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 68 lines · Zi Yan
Quoted 9 lines · Linus Torvalds
Quoted 3 lines · David Hildenbrand (Arm)
There needs to be a common page freeing function that dipatches stuff.
I think this all needs to be entirely generic to all pages. Agreed. Everybody needs to do it right.
It assumes all callers of
page_ref_dec_and_test()are free page functions, but there are two exceptions:1.
compaction_free()usesfolio_put_testzero(), which callspage_ref_dec_and_test()to put not used migration dst page back to isolated freelist instead of freeing them.2.
virtio_mem_fake_offline_going_offline()drops the last reference of fake-offline pages, so that they can be offline.They will need to be converted to not use
page_ref_dec_and_test()in addition to a common page freeing function. And any such future use needs to be banned.Quoted 42 lines
But I think that in the scenario that Ilya mentioned:
Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [
page_ref_dec_and_test()]
B: INC 0 -> 1 [folio_try_get()] => success
B: DEC 1 -> 0 [page_ref_dec_and_test()]
B: CAS 0 -> FR [page_ref_dec_and_test()]
B: * deallocates via__folio_put()*C: * re-allocates page *
C: DEC 1 -> 0 [page_ref_dec_and_test()]
A: CAS 0 -> FR [page_ref_dec_and_test()] => success
A: * deallocates directly viafree_frozen_pages()* => BUGthe big here is that the de-allocation was done not from the state of the *page*, but from the state of who finished it off.
So that
A: CAS 0 -> FR [
page_ref_dec_and_test()] => successis not a problem per se. It's a fine case of "we had one single unique final free".
The failure is that A then goes on to do that
"deallocates directly via
free_frozen_pages"because A mis-judged the page as having the old stale state that it had when *A* was using it.
IOW, I think the soluition is either:
(a) every user of
put_page_testzero(page)agrees tro do exactly the same thing and there is no difference between how that final "0->FROZEN" is dealt with, so it doesn't matter that A is freeing a page that C allocated for something elseor
Quoted 8 lines
Quoted 7 lines
(b) the CAS 0 -> FR state sequence always looks at the *page* to decide what to do, never at the caller state (ie A will look at whatever C wrote when it allocated the page to know how to do that final free).
Hmm?
Just wonder if we could make 1 -> FROZEN atomic to get rid of the gap.
Hmmm, I’m afraid that since you need CAS for a safe 1->FR transition, it will result in a CAS loop for the decrement itself (like in atomic_sub_unless). And this will introduce scalability issues just like in folio_try_get(), but this time for everyone...
Unless there is a way to change the refcount encoding even further and fix this gap.
---
Ilya Gladyshev // foxido.dev
David Hildenbrand (Arm)
maintainerRe: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 70 lines · Ilya Gladyshev
Quoted 69 lines · Zi Yan
Quoted 5 lines · Linus Torvalds
I think this all needs to be entirely generic to all pages. Agreed. Everybody needs to do it right.
It assumes all callers of
page_ref_dec_and_test()are free page functions, but there are two exceptions:1.
compaction_free()usesfolio_put_testzero(), which callspage_ref_dec_and_test()to put not used migration dst page back to isolated freelist instead of freeing them.2.
virtio_mem_fake_offline_going_offline()drops the last reference of fake-offline pages, so that they can be offline.They will need to be converted to not use
page_ref_dec_and_test()in addition to a common page freeing function. And any such future use needs to be banned.Quoted 47 lines
But I think that in the scenario that Ilya mentioned:
Refcount operations as follows (FR = FROZEN):
A: DEC 1 -> 0 [
page_ref_dec_and_test()]
B: INC 0 -> 1 [folio_try_get()] => success
B: DEC 1 -> 0 [page_ref_dec_and_test()]
B: CAS 0 -> FR [page_ref_dec_and_test()]
B: * deallocates via__folio_put()*C: * re-allocates page *
C: DEC 1 -> 0 [page_ref_dec_and_test()]
A: CAS 0 -> FR [page_ref_dec_and_test()] => success
A: * deallocates directly viafree_frozen_pages()* => BUGthe big here is that the de-allocation was done not from the state of the *page*, but from the state of who finished it off.
So that
A: CAS 0 -> FR [
page_ref_dec_and_test()] => successis not a problem per se. It's a fine case of "we had one single unique final free".
The failure is that A then goes on to do that
"deallocates directly via
free_frozen_pages"because A mis-judged the page as having the old stale state that it had when *A* was using it.
IOW, I think the soluition is either:
(a) every user of
put_page_testzero(page)agrees tro do exactly the same thing and there is no difference between how that final "0->FROZEN" is dealt with, so it doesn't matter that A is freeing a page that C allocated for something elseor
(b) the CAS 0 -> FR state sequence always looks at the *page* to decide what to do, never at the caller state (ie A will look at whatever C wrote when it allocated the page to know how to do that final free).
Quoted 7 lines
Quoted 3 lines
Quoted 2 lines
Hmm?
Just wonder if we could make 1 -> FROZEN atomic to get rid of the gap.
Hmmm, I’m afraid that since you need CAS for a safe 1->FR transition, it will result in a CAS loop for the decrement itself (like in
atomic_sub_unless).
I was playing with exact this idea when I was reviewing one of the earlier versions; didn't manage to make it fly in a nice way :)
Signature · 3 lines
Cheers, David
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
In reply to Ilya GladyshevQuoted 5 lines · Ilya Gladyshev
Hmmm, I’m afraid that since you need CAS for a safe 1->FR transition, it will result in a CAS loop for the decrement itself (like in
atomic_sub_unless). And this will introduce scalability issues just like infolio_try_get(), but this time for everyone...
Note that we could make that CAS case be the thing that only the special cases do.
IOW, maybe only do that slow sequence in compaction_free() and the memory offlining.
So we'd have two different cases:
- the high-performance case is ready and willing to accept the "sees
zero" window and the extra 0->FR state that can race with somebody else taking an optimistic ref
- the unusual slow cases that are *not* willing to deal with
optimistic ref takers do the "CAS 1 -> FR" state atomically and always use compare-and-exchange for their freeing path
That actually sounds like a good approach to me.
But maybe I'm missing something obvious and it doesn't really solve the issue.
Linus
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 19 lines · Linus Torvalds
Quoted 5 lines · Ilya Gladyshev
Hmmm, I’m afraid that since you need CAS for a safe 1->FR transition, it will result in a CAS loop for the decrement itself (like in
atomic_sub_unless). And this will introduce scalability issues just like infolio_try_get(), but this time for everyone...Note that we could make that CAS case be the thing that only the special cases do.
IOW, maybe only do that slow sequence in
compaction_free()and the memory offlining.So we'd have two different cases:
- the high-performance case is ready and willing to accept the "sees
zero" window and the extra 0->FR state that can race with somebody else taking an optimistic ref
Quoted 5 lines
- the unusual slow cases that are *not* willing to deal with
optimistic ref takers do the "CAS 1 -> FR" state atomically and always use compare-and-exchange for their freeing pathThat actually sounds like a good approach to me.
Cool idea, thanks!
This, however, will still require converting all page_frag_free() callers to use the generic dispatching dtor (which is __folio_put() for now), right?
---
Beyond that, I see two trade-offs with my patchset. Neither seems important for the current kernel code, but I'd like to outline them anyway.
1. We lose the "high-performance + custom deallocation" scenario
For example, in cases where the current refcount allows you to write the following:
if (!put_page_testzero()) {
/* We failed to steal the last refcount. However, our refcount is
* already decremented, and therefore we can move forward -- the one
* who stole our page will free it via the generic dispatching dtor. */
}
The proposed refcount impl will require either a slow decrement or a proper (slow) deallocation when put_page_testzero() succeeds. So, basically, the compaction_free() scenario but with a performance requirement.
2. Page deallocation becomes unpredictable
With the current refcount impl, if you work with a page of "type X", you can be sure that folio_put() will perform X-specific actions on it, and you can make implicit assumptions based on that. This is no longer true for fast decrements.
---
Ilya Gladyshev // foxido.dev
David Hildenbrand (Arm)
maintainerRe: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
In reply to Zi YanQuoted 10 lines · Zi Yan
Quoted 6 lines · Linus Torvalds
Quoted 2 lines · David Hildenbrand (Arm)
There needs to be a common page freeing function that dipatches stuff.
I think this all needs to be entirely generic to all pages. Agreed. Everybody needs to do it right.
It assumes all callers of
page_ref_dec_and_test()are free page functions, but there are two exceptions:
Right, we'll have to sort these out. Which is the right thing to do either way :)
Quoted 7 lines
1.
compaction_free()usesfolio_put_testzero(), which callspage_ref_dec_and_test()to put not used migration dst page back to isolated freelist instead of freeing them.2.
virtio_mem_fake_offline_going_offline()drops the last reference of fake-offline pages, so that they can be offline.
I had a patch for that, where we keep the refcount untouched:
https://lore.kernel.org/linux-mm/[email protected]/
Long-term PG_offline pages will not have a refcount at all, and just have a frozen refcount. That's the right solution long-term: allocate+free them as frozen in virtio-mem. Needs some more work, though :)
Signature · 3 lines
Cheers, David
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
In reply to Ilya GladyshevQuoted 8 lines · Ilya Gladyshev
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference countIn other words, refcount is now split into "dead bit" and a 31-bit counter.
It seems to me that refcount overflow is now a problem.
We can deliberately increase the refcount on a page by stuffing it into a pipe. Over and over again. See merge commit 6b3a70773630 and the four commits on that branch: f958d7b528b1 88b1a17dfc3e 8fde12ca79af 15fab63e1e57
Unfortunately, I think the discussion that led to those commits was conducted off-list because security. I wish we had a way to declassify thse emails after the fact.
include/linux/page-flags.h enum pageflags { · line 207
Quoted 9 lines
include/linux/page-flags.h +9
-0+/* Most significant bit in page refcount */ +#define PAGEREF_FROZEN_BIT BIT(31) + +/* Page reference counter can be in 3 logical states, +* which are described below with their value representation +* state | value +* (1) safe with owners | 1...INT_MAX +* (2) safe with no owners | 0 +* (3) frozen | INT_MIN....-1
I think we need four states. The first two are the same.
(3) frozen: 0xc000'0000 - 0xffff'ffff
(4) temporarily overflown: 0x8000'0000 - 0xbfff'ffff
(we don't really need that much space for temporary overflow; we could have something like 0x8100'0000 as the boundary if that works out better)
include/linux/page_ref.h static inline void __page_ref_unfreeze(struct page *page, int v) · line 67
Quoted 4 lines
static inline bool
__page_count_is_frozen(int count){include/linux/page_ref.h +1
-1-return count ==0 ; +return count &PAGEREF_FROZEN_BIT ;
This probably becomes '(unsigned)count >> 30 == 3'. If we choose a different boundary then something like (unsigned)count >> 24 >= 0x81.
include/linux/page_ref.h static inline bool folio_is_frozen(const struct folio *folio) · line 85
Quoted 7 lines
static inline int
page_ref_count(const struct page *page){include/linux/page_ref.h +4
-1-return atomic_read (&page->_refcount); +int val =atomic_read (&page->_refcount); + +if (unlikely (val &PAGEREF_FROZEN_BIT )) +return 0 ;
... use __page_count_is_frozen() here?
and you need to adjust folio_ref_zero_or_close_to_overflow(). try_get_page() can stay as it is.
(Thanks to Pedro for asking me annoying questions about mapcount overflow which prompted me to look at this again)
Re: [PATCH v6 3/3] mm: implement page refcount locking via dedicated bit
Quoted 7 lines · Matthew Wilcox
Quoted 6 lines · Ilya Gladyshev
This patch reallocates the refcount value range:
(1) refcount < 0 means dead refcount (uninit / frozen)
(2) refcount = 0 allowed only as a temporary state (see below)
(3) refcount > 0 is a regular reference count
Quoted 8 lines
Quoted 2 lines
In other words, refcount is now split into "dead bit" and a 31-bit counter.
It seems to me that refcount overflow is now a problem.
We can deliberately increase the refcount on a page by stuffing it into a pipe. Over and over again. See merge commit
6b3a70773630and the four commits on that branch:f958d7b528b188b1a17dfc3e8fde12ca79af15fab63e1e57
Thanks for pointing that out!
Quoted 16 lines
Unfortunately, I think the discussion that led to those commits was conducted off-list because security. I wish we had a way to declassify thse emails after the fact.
include/linux/page-flags.h enum pageflags { · line 207
Quoted 9 lines
include/linux/page-flags.h +9
-0+/* Most significant bit in page refcount */ +#define PAGEREF_FROZEN_BIT BIT(31) + +/* Page reference counter can be in 3 logical states, +* which are described below with their value representation +* state | value +* (1) safe with owners | 1...INT_MAX +* (2) safe with no owners | 0 +* (3) frozen | INT_MIN....-1 I think we need four states. The first two are the same.
Quoted 2 lines
(3) frozen: 0xc000'0000 - 0xffff'ffff
(4) temporarily overflown: 0x8000'0000 - 0xbfff'ffff
Agree.
Quoted 2 lines
(we don't really need that much space for temporary overflow; we could have something like 0x8100'0000 as the boundary if that works out better)
Frozen state doesn't really need that much space either, so I guess the boundary with the simplest check in page_ref_count wins.
Quoted 8 lines
include/linux/page_ref.h static inline void __page_ref_unfreeze(struct page *page, int v) · line 67
Quoted 4 lines
static inline bool __page_count_is_frozen(int count) {include/linux/page_ref.h +1
-1-return count ==0 ; +return count &PAGEREF_FROZEN_BIT ;This probably becomes '(unsigned)count >> 30 == 3'. If we choose a different boundary then something like (unsigned)count >> 24 >=
0x81.
Quoted 9 lines
include/linux/page_ref.h static inline bool folio_is_frozen(const struct folio *folio) · line 85
Quoted 7 lines
static inline int
page_ref_count(const struct page *page){include/linux/page_ref.h +4
-1-return atomic_read (&page->_refcount); +int val =atomic_read (&page->_refcount); + +if (unlikely (val &PAGEREF_FROZEN_BIT )) +return 0 ;... use
__page_count_is_frozen()here?
Missed that, thanks!
Quoted 2 lines
and you need to adjust
folio_ref_zero_or_close_to_overflow().try_get_page()can stay as it is.
Agree.
Quoted 2 lines
(Thanks to Pedro for asking me annoying questions about mapcount overflow which prompted me to look at this again)
Andrew Morton
maintainerQuoted 16 lines · On Sat, 12 Sep 2026 22:50:07 +0300 Ilya Gladyshev <[email protected]>…
From: Gladyshev Ilya <[email protected]>
Recap
-----This patchset addresses a scalability issue of a folio's
add_unless()operation, noticeable during contended IO reads from the same page [folio_try_get()]. The main idea is to replace CAS loop with optimistic increment (and deal with failure later). This requires splitting refcount into counter and separate "dead/frozen" bit.To allow for such modification, this patchset also slightly refactors
page_refAPI, consolidating all implementation logic inside mm headers. For more information, check individual commit messages. The original performance issue and previous attempts by other people can be found in [1][2].
Quoted 8 lines
Performance
-----------To my regret, I don't have any access to high-core CPUs that I can benchmark on, and a 12 vcpu laptop isn't really a scalability test. So, here I can only paste my previous measurements on Linux 6.15. To be fair, none of the related code paths really changed, so I don't expect any changes in the numbers here.
From a quick eyeballing, this patchset seems to provide performance gains which are similar to Kiryl's ill-fated "mm/filemap: Implement fast short reads" [2]. Is that accurate?
Quoted 26 lines · Andrew Morton
Quoted 16 lines · On Sat, 12 Sep 2026 22:50:07 +0300 Ilya Gladyshev <[email protected]>…
Quoted 1 line
From: Gladyshev Ilya <[email protected]>
Recap
-----This patchset addresses a scalability issue of a folio's
add_unless()operation, noticeable during contended IO reads from the same page [folio_try_get()]. The main idea is to replace CAS loop with optimistic increment (and deal with failure later). This requires splitting refcount into counter and separate "dead/frozen" bit.To allow for such modification, this patchset also slightly refactors
page_refAPI, consolidating all implementation logic inside mm headers. For more information, check individual commit messages. The original performance issue and previous attempts by other people can be found in [1][2].Quoted 8 lines
Performance
-----------To my regret, I don't have any access to high-core CPUs that I can benchmark on, and a 12 vcpu laptop isn't really a scalability test. So, here I can only paste my previous measurements on Linux 6.15. To be fair, none of the related code paths really changed, so I don't expect any changes in the numbers here.
Quoted 3 lines
From a quick eyeballing, this patchset seems to provide performance gains which are similar to Kiryl's ill-fated "mm/filemap: Implement fast short reads" [2]. Is that accurate?
Yes, since both patches aim to optimize the same refcount CAS contention, just in different ways. This patch is probably less scalable than Kiryl's seqlock because there is still refcount cache-line bouncing. That said, I haven't analyzed the code deeply, and the benchmark numbers are very simular.