On 12.08.19 23:33, Alexander Duyck wrote: > From: Alexander Duyck <alexander.h.duyck@xxxxxxxxxxxxxxx> > > In order to pave the way for free page reporting in virtualized > environments we will need a way to get pages out of the free lists and > identify those pages after they have been returned. To accomplish this, > this patch adds the concept of a Reported Buddy, which is essentially > meant to just be the Uptodate flag used in conjunction with the Buddy > page type. > > It adds a set of pointers we shall call "boundary" which represents the > upper boundary between the unreported and reported pages. The general idea > is that in order for a page to cross from one side of the boundary to the > other it will need to go through the reporting process. Ultimately a > free_list has been fully processed when the boundary has been moved from > the tail all they way up to occupying the first entry in the list. > > Doing this we should be able to make certain that we keep the reported > pages as one contiguous block in each free list. This will allow us to > efficiently manipulate the free lists whenever we need to go in and start > sending reports to the hypervisor that there are new pages that have been > freed and are no longer in use. > > An added advantage to this approach is that we should be reducing the > overall memory footprint of the guest as it will be more likely to recycle > warm pages versus trying to allocate the reported pages that were likely > evicted from the guest memory. > > Since we will only be reporting one zone at a time we keep the boundary > limited to being defined for just the zone we are currently reporting pages > from. Doing this we can keep the number of additional pointers needed quite > small. To flag that the boundaries are in place we use a single bit > in the zone to indicate that reporting and the boundaries are active. > > The determination of when to start reporting is based on the tracking of > the number of free pages in a given area versus the number of reported > pages in that area. We keep track of the number of reported pages per > free_area in a separate zone specific area. We do this to avoid modifying > the free_area structure as this can lead to false sharing for the highest > order with the zone lock which leads to a noticeable performance > degradation. > > Signed-off-by: Alexander Duyck <alexander.h.duyck@xxxxxxxxxxxxxxx> > --- > include/linux/mmzone.h | 40 +++++ > include/linux/page-flags.h | 11 + > include/linux/page_reporting.h | 138 ++++++++++++++++++ > mm/Kconfig | 5 + > mm/Makefile | 1 > mm/memory_hotplug.c | 1 > mm/page_alloc.c | 136 +++++++++++++++++- > mm/page_reporting.c | 308 ++++++++++++++++++++++++++++++++++++++++ > 8 files changed, 632 insertions(+), 8 deletions(-) > create mode 100644 include/linux/page_reporting.h > create mode 100644 mm/page_reporting.c > > diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h > index 2f2b6f968ed3..b8ed926552b1 100644 > --- a/include/linux/mmzone.h > +++ b/include/linux/mmzone.h > @@ -462,6 +462,14 @@ struct zone { > seqlock_t span_seqlock; > #endif > > +#ifdef CONFIG_PAGE_REPORTING > + /* > + * Pointer to reported page tracking statistics array. The size of > + * the array is MAX_ORDER - PAGE_REPORTING_MIN_ORDER. NULL when > + * unused page reporting is not present. > + */ > + unsigned long *reported_pages; > +#endif > int initialized; > > /* Write-intensive fields used from the page allocator */ > @@ -537,6 +545,14 @@ enum zone_flags { > ZONE_BOOSTED_WATERMARK, /* zone recently boosted watermarks. > * Cleared when kswapd is woken. > */ > + ZONE_PAGE_REPORTING_REQUESTED, /* zone enabled page reporting and has > + * requested flushing the data out of > + * higher order pages. > + */ > + ZONE_PAGE_REPORTING_ACTIVE, /* zone enabled page reporting and is > + * activly flushing the data out of > + * higher order pages. > + */ > }; > > static inline unsigned long zone_managed_pages(struct zone *zone) > @@ -757,6 +773,8 @@ static inline bool pgdat_is_empty(pg_data_t *pgdat) > return !pgdat->node_start_pfn && !pgdat->node_spanned_pages; > } > > +#include <linux/page_reporting.h> > + > /* Used for pages not on another list */ > static inline void add_to_free_list(struct page *page, struct zone *zone, > unsigned int order, int migratetype) > @@ -771,10 +789,16 @@ static inline void add_to_free_list(struct page *page, struct zone *zone, > static inline void add_to_free_list_tail(struct page *page, struct zone *zone, > unsigned int order, int migratetype) > { > - struct free_area *area = &zone->free_area[order]; > + struct list_head *tail = get_unreported_tail(zone, order, migratetype); > > - list_add_tail(&page->lru, &area->free_list[migratetype]); > - area->nr_free++; > + /* > + * To prevent the unreported pages from being interleaved with the > + * reported ones while we are actively processing pages we will use > + * the head of the reported pages to determine the tail of the free > + * list. > + */ > + list_add_tail(&page->lru, tail); > + zone->free_area[order].nr_free++; > } > > /* Used for pages which are on another list */ > @@ -783,12 +807,22 @@ static inline void move_to_free_list(struct page *page, struct zone *zone, > { > struct free_area *area = &zone->free_area[order]; > > + /* > + * Clear Hinted flag, if present, to avoid placing reported pages > + * at the top of the free_list. It is cheaper to just process this > + * page again than to walk around a page that is already reported. > + */ > + clear_page_reported(page, zone); > + > list_move(&page->lru, &area->free_list[migratetype]); > } > > static inline void del_page_from_free_list(struct page *page, struct zone *zone, > unsigned int order) > { > + /* Clear Reported flag, if present, before resetting page type */ > + clear_page_reported(page, zone); > + > list_del(&page->lru); > __ClearPageBuddy(page); > set_page_private(page, 0); > diff --git a/include/linux/page-flags.h b/include/linux/page-flags.h > index f91cb8898ff0..759a3b3956f2 100644 > --- a/include/linux/page-flags.h > +++ b/include/linux/page-flags.h > @@ -163,6 +163,9 @@ enum pageflags { > > /* non-lru isolated movable page */ > PG_isolated = PG_reclaim, > + > + /* Buddy pages. Used to track which pages have been reported */ > + PG_reported = PG_uptodate, > }; > > #ifndef __GENERATING_BOUNDS_H > @@ -432,6 +435,14 @@ static inline bool set_hwpoison_free_buddy_page(struct page *page) > #endif > > /* > + * PageReported() is used to track reported free pages within the Buddy > + * allocator. We can use the non-atomic version of the test and set > + * operations as both should be shielded with the zone lock to prevent > + * any possible races on the setting or clearing of the bit. > + */ > +__PAGEFLAG(Reported, reported, PF_NO_COMPOUND) > + > +/* > * On an anonymous page mapped into a user virtual memory area, > * page->mapping points to its anon_vma, not to a struct address_space; > * with the PAGE_MAPPING_ANON bit set to distinguish it. See rmap.h. > diff --git a/include/linux/page_reporting.h b/include/linux/page_reporting.h > new file mode 100644 > index 000000000000..498bde6ea764 > --- /dev/null > +++ b/include/linux/page_reporting.h > @@ -0,0 +1,138 @@ > +/* SPDX-License-Identifier: GPL-2.0 */ > +#ifndef _LINUX_PAGE_REPORTING_H > +#define _LINUX_PAGE_REPORTING_H > + > +#include <linux/mmzone.h> > +#include <linux/jump_label.h> > +#include <linux/pageblock-flags.h> > +#include <asm/pgtable_types.h> > + > +#define PAGE_REPORTING_MIN_ORDER pageblock_order > +#define PAGE_REPORTING_HWM 32 > + > +#ifdef CONFIG_PAGE_REPORTING > +struct page_reporting_dev_info { > + /* function that alters pages to make them "reported" */ > + void (*report)(struct page_reporting_dev_info *phdev, > + unsigned int nents); > + > + /* scatterlist containing pages to be processed */ > + struct scatterlist *sg; > + > + /* > + * Upper limit on the number of pages that the react function > + * expects to be placed into the batch list to be processed. > + */ > + unsigned long capacity; > + > + /* work struct for processing reports */ > + struct delayed_work work; > + > + /* > + * The number of zones requesting reporting, plus one additional if > + * processing thread is active. > + */ > + atomic_t refcnt; > +}; > + > +extern struct static_key page_reporting_notify_enabled; > + > +/* Boundary functions */ > +struct list_head *__page_reporting_get_boundary(unsigned int order, > + int migratetype); > +void page_reporting_del_from_boundary(struct page *page, struct zone *zone); > +void page_reporting_add_to_boundary(struct page *page, struct zone *zone, > + int migratetype); > + > +/* Hinted page accessors, defined in page_alloc.c */ > +struct page *get_unreported_page(struct zone *zone, unsigned int order, > + int migratetype); > +void put_reported_page(struct zone *zone, struct page *page); > + > +void __page_reporting_request(struct zone *zone); > +void __page_reporting_free_stats(struct zone *zone); > + > +/* Tear-down and bring-up for page reporting devices */ > +void page_reporting_shutdown(struct page_reporting_dev_info *phdev); > +int page_reporting_startup(struct page_reporting_dev_info *phdev); > +#endif /* CONFIG_PAGE_REPORTING */ > + > +static inline struct list_head * > +get_unreported_tail(struct zone *zone, unsigned int order, int migratetype) > +{ > +#ifdef CONFIG_PAGE_REPORTING > + if (order >= PAGE_REPORTING_MIN_ORDER && > + test_bit(ZONE_PAGE_REPORTING_ACTIVE, &zone->flags)) > + return __page_reporting_get_boundary(order, migratetype); > +#endif > + return &zone->free_area[order].free_list[migratetype]; > +} > + > +static inline void clear_page_reported(struct page *page, > + struct zone *zone) > +{ > +#ifdef CONFIG_PAGE_REPORTING > + if (likely(!PageReported(page))) > + return; > + > + /* push boundary back if we removed the upper boundary */ > + if (test_bit(ZONE_PAGE_REPORTING_ACTIVE, &zone->flags)) > + page_reporting_del_from_boundary(page, zone); > + > + __ClearPageReported(page); > + > + /* page_private will contain the page order, so just use it directly */ > + zone->reported_pages[page_private(page) - PAGE_REPORTING_MIN_ORDER]--; > +#endif > +} > + > +/* Free reported_pages and reset reported page tracking count to 0 */ > +static inline void page_reporting_reset(struct zone *zone) > +{ > +#ifdef CONFIG_PAGE_REPORTING > + if (zone->reported_pages) > + __page_reporting_free_stats(zone); > +#endif > +} > + > +/** > + * page_reporting_notify_free - Free page notification to start page processing > + * @zone: Pointer to current zone of last page processed > + * @order: Order of last page added to zone > + * > + * This function is meant to act as a screener for __page_reporting_request > + * which will determine if a give zone has crossed over the high-water mark > + * that will justify us beginning page treatment. If we have crossed that > + * threshold then it will start the process of pulling some pages and > + * placing them in the batch list for treatment. > + */ > +static inline void page_reporting_notify_free(struct zone *zone, int order) > +{ > +#ifdef CONFIG_PAGE_REPORTING > + unsigned long nr_reported; > + > + /* Called from hot path in __free_one_page() */ > + if (!static_key_false(&page_reporting_notify_enabled)) > + return; > + > + /* Limit notifications only to higher order pages */ > + if (order < PAGE_REPORTING_MIN_ORDER) > + return; > + > + /* Do not bother with tests if we have already requested reporting */ > + if (test_bit(ZONE_PAGE_REPORTING_REQUESTED, &zone->flags)) > + return; > + > + /* If reported_pages is not populated, assume 0 */ > + nr_reported = zone->reported_pages ? > + zone->reported_pages[order - PAGE_REPORTING_MIN_ORDER] : 0; > + > + /* Only request it if we have enough to begin the page reporting */ > + if (zone->free_area[order].nr_free < nr_reported + PAGE_REPORTING_HWM) > + return; > + > + /* This is slow, but should be called very rarely */ > + __page_reporting_request(zone); > +#endif > +} > +#endif /*_LINUX_PAGE_REPORTING_H */ > diff --git a/mm/Kconfig b/mm/Kconfig > index 1c9698509273..daa8c45e2af4 100644 > --- a/mm/Kconfig > +++ b/mm/Kconfig > @@ -237,6 +237,11 @@ config COMPACTION > linux-mm@xxxxxxxxx. > > # > +# support for unused page reporting > +config PAGE_REPORTING > + bool > + > +# > # support for page migration > # > config MIGRATION > diff --git a/mm/Makefile b/mm/Makefile > index d0b295c3b764..1e17ba0ed2f0 100644 > --- a/mm/Makefile > +++ b/mm/Makefile > @@ -105,3 +105,4 @@ obj-$(CONFIG_PERCPU_STATS) += percpu-stats.o > obj-$(CONFIG_ZONE_DEVICE) += memremap.o > obj-$(CONFIG_HMM_MIRROR) += hmm.o > obj-$(CONFIG_MEMFD_CREATE) += memfd.o > +obj-$(CONFIG_PAGE_REPORTING) += page_reporting.o > diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c > index 5b8811945bbb..bd40beac293b 100644 > --- a/mm/memory_hotplug.c > +++ b/mm/memory_hotplug.c > @@ -1612,6 +1612,7 @@ static int __ref __offline_pages(unsigned long start_pfn, > if (!populated_zone(zone)) { > zone_pcp_reset(zone); > build_all_zonelists(NULL); > + page_reporting_reset(zone); > } else > zone_pcp_update(zone); > > diff --git a/mm/page_alloc.c b/mm/page_alloc.c > index 4b5812c3800e..d0d3fb12ba54 100644 > --- a/mm/page_alloc.c > +++ b/mm/page_alloc.c > @@ -68,6 +68,7 @@ > #include <linux/lockdep.h> > #include <linux/nmi.h> > #include <linux/psi.h> > +#include <linux/page_reporting.h> > > #include <asm/sections.h> > #include <asm/tlbflush.h> > @@ -915,7 +916,7 @@ static inline struct capture_control *task_capc(struct zone *zone) > static inline void __free_one_page(struct page *page, > unsigned long pfn, > struct zone *zone, unsigned int order, > - int migratetype) > + int migratetype, bool reported) > { > struct capture_control *capc = task_capc(zone); > unsigned long uninitialized_var(buddy_pfn); > @@ -990,11 +991,20 @@ static inline void __free_one_page(struct page *page, > done_merging: > set_page_order(page, order); > > - if (is_shuffle_order(order) ? shuffle_add_to_tail() : > - buddy_merge_likely(pfn, buddy_pfn, page, order)) > + if (reported || > + (is_shuffle_order(order) ? shuffle_add_to_tail() : > + buddy_merge_likely(pfn, buddy_pfn, page, order))) > add_to_free_list_tail(page, zone, order, migratetype); > else > add_to_free_list(page, zone, order, migratetype); > + > + /* > + * No need to notify on a reported page as the total count of > + * unreported pages will not have increased since we have essentially > + * merged the reported page with one or more unreported pages. > + */ > + if (!reported) > + page_reporting_notify_free(zone, order); > } > > /* > @@ -1305,7 +1315,7 @@ static void free_pcppages_bulk(struct zone *zone, int count, > if (unlikely(isolated_pageblocks)) > mt = get_pageblock_migratetype(page); > > - __free_one_page(page, page_to_pfn(page), zone, 0, mt); > + __free_one_page(page, page_to_pfn(page), zone, 0, mt, false); > trace_mm_page_pcpu_drain(page, 0, mt); > } > spin_unlock(&zone->lock); > @@ -1321,7 +1331,7 @@ static void free_one_page(struct zone *zone, > is_migrate_isolate(migratetype))) { > migratetype = get_pfnblock_migratetype(page, pfn); > } > - __free_one_page(page, pfn, zone, order, migratetype); > + __free_one_page(page, pfn, zone, order, migratetype, false); > spin_unlock(&zone->lock); > } > > @@ -2183,6 +2193,122 @@ struct page *__rmqueue_smallest(struct zone *zone, unsigned int order, > return NULL; > } > > +#ifdef CONFIG_PAGE_REPORTING > +/** > + * get_unreported_page - Pull an unreported page from the free_list > + * @zone: Zone to draw pages from > + * @order: Order to draw pages from > + * @mt: Migratetype to draw pages from > + * > + * This function will obtain a page from the free list. It will start by > + * attempting to pull from the tail of the free list and if that is already > + * reported on it will instead pull the head if that is unreported. > + * > + * The page will have the migrate type and order stored in the page > + * metadata. While being processed the page will not be avaialble for > + * allocation. > + * > + * Return: page pointer if raw page found, otherwise NULL > + */ > +struct page *get_unreported_page(struct zone *zone, unsigned int order, int mt) > +{ > + struct list_head *tail = get_unreported_tail(zone, order, mt); > + struct free_area *area = &(zone->free_area[order]); > + struct list_head *list = &area->free_list[mt]; > + struct page *page; > + > + /* zone lock should be held when this function is called */ > + lockdep_assert_held(&zone->lock); > + > + /* Find a page of the appropriate size in the preferred list */ > + page = list_last_entry(tail, struct page, lru); > + list_for_each_entry_from_reverse(page, list, lru) { > + /* If we entered this loop then the "raw" list isn't empty */ > + > + /* If the page is reported try the head of the list */ > + if (PageReported(page)) { > + page = list_first_entry(list, struct page, lru); > + > + /* > + * If both the head and tail are reported then reset > + * the boundary so that we read as an empty list > + * next time and bail out. > + */ > + if (PageReported(page)) { > + page_reporting_add_to_boundary(page, zone, mt); > + break; > + } > + } > + > + del_page_from_free_list(page, zone, order); > + > + /* record migratetype and order within page */ > + set_pcppage_migratetype(page, mt); > + set_page_private(page, order); Can you elaborate why you (similar to Nitesh) have to save/restore the migratetype? I can't yet follow why that is necessary. You could simply set it to MOVABLE (like e.g., undo_isolate_page_range() would do via alloc_contig_range()). If a pageblock is completely free, it might even make sense to set it to MOVABLE. (mainly wondering if this is required here and what the rational is) Now a theoretical issue: You are allocating pages from all zones, including the MOVABLE zone. The pages are currently unmovable (however, only temporarily allocated). del_page_from_free_area() will clear PG_buddy, and leave the refcount of the page set to zero (!). has_unmovable_pages() will skip any pages either on the MOVABLE zone or with a refcount of zero. So all pages that are being reported are detected as movable. The isolation of allocated pages will work - which is not bad, but I wonder what the implications are. As far as I can follow, alloc_contig_range() -> __alloc_contig_migrate_range() -> isolate_migratepages_range() -> isolate_migratepages_block() will choke on these pages ((!PageLRU(page) -> !__PageMovable(page) -> fail), essentially making alloc_contig_range() range fail. Same could apply to offline_pages(). I would have thought that there has to be something like a reported_pages_drain_all(), that waits until all reports are over (or even signals to the hypervisor to abort reporting). As the pages are isolated, they won't be allocated for reporting again. I have not yet understood completely the way all the details work. I am currently looking into using alloc_contig_range() in a different context, which would co-exist with free-page-reporting. -- Thanks, David / dhildenb