Linux 內存管理新特性 - Memory folios 解讀

一、folio [ˈfoʊlioʊ] 是什麼

1.1 folio 的定義

Add memory folios, a new type to represent either order-0 pages or the head page of a compound page.

folio 可以看成是 page 的一層包裝,沒有開銷的那種。folio 可以是單個頁,也可以是複合頁。

(圖片引用圍繞 HugeTLB 的極致優化)

上圖是 page 結構體的示意圖,64 字節管理 flags, lru, mapping, index, private, {ref_, map_}count, memcg_data 等信息。當 page 是複合頁的時候,上述 flags 等信息在 head page 中,tail page 則複用管理 compound_{head, mapcount, order, nr, dtor} 等信息。

struct folio {
        /* private: don't document the anon union */
        union {
                struct {
        /* public: */
                        unsigned long flags;
                        struct list_head lru;
                        struct address_space *mapping;
                        pgoff_t index;
                        void *private;
                        atomic_t _mapcount;
                        atomic_t _refcount;
                        unsigned long memcg_data;
        /* private: the union with struct page is transitional */
                struct page page;

folio 的結構定義中,flags, lru 等信息和 page 完全一致,因此可以和 page 進行 union。這樣可以直接使用 folio->flags 而不用 folio->page->flags。

#define page_folio(p)           (_Generic((p),                          \
        const struct page *:    (const struct folio *)_compound_head(p), \
        struct page *:          (struct folio *)_compound_head(p)))
#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n)    nth_page(&(folio)->page, n)

第一眼看 page_folio 可能有點懵,其實等效於:

switch (typeof(p)) {
  case const struct page *:
    return (const struct folio *)_compound_head(p);
  case struct page *:
    return (struct folio *)_compound_head(p)));


_Generic 是 C11 STANDARD - Generic selection( 特性,語法如下:

Generic selection
  _Generic ( assignment-expression , generic-assoc-list )
  generic-assoc-list , generic-association
  type-name : assignment-expression
  default : assignment-expression

page 和 folio 的相互轉換也很直接。不管 head,tail page,轉化爲 folio 時,意義等同於獲取 head page 對應的 folio;folio 轉化爲 page 時,folio->page 用於獲取 head page,folio_page(folio, n) 可以用於獲取 tail page。

問題是,本來 page 就能代表 base page,或者 compound page,爲什麼還需要引入 folio?

1.2 folio 能做什麼?

The folio type allows a function to declare that it's expecting only a head page. Almost incidentally, this allows us to remove various calls to VM_BUG_ON(PageTail(page)) and compound_head().

原因是,page 的含義太多了,可以是 base page,可以是 compound head page,還可以是 compound tail page。

如上述所說,page 元信息都存放在 head page(base page 可以看成是 head page)上,例如 page->mapping, page->index 等。但在 mm 路徑上,傳遞進來的 page 參數總是需要判斷是 head page 還是 tail page。由於沒有上下文緩存,mm 路徑上可能會存在太多重複的 compound_head 調用。

這裏以 mem_cgroup_move_account 函數調用舉例,一次 mem_cgroup_move_account 調用,最多能執行 7 次 compound_head。

static inline struct page *compound_head(struct page *page)
        unsigned long head = READ_ONCE(page->compound_head);
        if (unlikely(head & 1))
                return (struct page *) (head - 1);
        return page;

再以 page_mapping(page) 爲例具體分析,進入函數內部,首先執行 compound_head(page) 獲取 page mapping 等信息。另外還有一個分支 PageSwapCache(page) ,當執行這個分支函數的時候,傳遞的是 page,函數內部還需執行一次 compound_head(page) 來獲取 page flag 信息。

struct address_space *page_mapping(struct page *page)
        struct address_space *mapping;
        page = compound_head(page);
        /* This happens if someone calls flush_dcache_page on slab page */
        if (unlikely(PageSlab(page)))
                return NULL;
        if (unlikely(PageSwapCache(page))) {
                swp_entry_t entry;
                entry.val = page_private(page);
                return swap_address_space(entry);
        mapping = page->mapping;
        if ((unsigned long)mapping & PAGE_MAPPING_ANON)
                return NULL;
        return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);

當切換到 folio 之後,page_mapping(page) 對應 folio_mapping(folio) ,而 folio 隱含着 folio 本身就是 head page,因此兩個 compound_head(page) 的調用就省略了。mem_cgroup_move_account 僅僅是冰山一角,mm 路徑上到處是 compound_head 的調用。積少成多,不僅執行開銷減少了,開發者也能得到提示,當前 folio 一定是 head page,減少判斷分支。

1.3 folio 的直接價值

1)減少太多冗餘 compound_head 的調用。

2)給開發者提示,看到 folio,就能認定這是 head page。

3)修復潛在的 tail page 導致的 bug。

Here's an example where our current confusion between "any page"
and "head page" at least produces confusing behaviour, if not an
outright bug, isolate_migratepages_block():
        page = pfn_to_page(low_pfn);
        if (PageCompound(page) && !cc->alloc_contig) {
                const unsigned int order = compound_order(page);
                if (likely(order < MAX_ORDER))
                        low_pfn += (1UL << order) - 1;
                goto isolate_fail;
compound_order() does not expect a tail page; it returns 0 unless it's
a head page.  I think what we actually want to do here is:
        if (!cc->alloc_contig) {
            struct page *head = compound_head(page);
            if (PageHead(head)) {
                const unsigned int order = compound_order(head);
                low_pfn |= (1UL << order) - 1;
                goto isolate_fail;
Not earth-shattering; not even necessarily a bug.  But it's an example
of the way the code reads is different from how the code is executed,
and that's potentially dangerous.  Having a different type for tail
and not-tail pages prevents the muddy thinking that can lead to
tail pages being passed to compound_order().

1.4 folio-5.16 已經合入

This converts just parts of the core MM and the page cache.

willy/pagecache.git 共有 209 commit。這次 5.16 的合併窗口中,作者 Matthew Wilcox (Oracle) <[email protected]> 先合入 folio 基礎部分,即 Merge tag folio-5.16,其中包含 90 commits,74 changed files with 2914 additions and 1703 deletions。除了 folio 定義等基礎設施之外,這次改動主要集中在 memcg, filemap, writeback 部分。folio-5.16 用 folio 逐步取代 page 的過程,似乎值得一提。mm 路徑太多了,如果強迫症一次性替換完,就得 top-down 的方式,從 page 分配的地方改成 folio,然後一路改下去。這不現實,幾乎要修改整個 mm 文件夾了。folio-5.16 採用的是 bottom-up 的方式,在 mm 路徑的某個函數開始,將 page 替換成 folio,其內部所有實現都用 folio,形成一個“閉包”。然後修改其 caller function,用 folio 作爲參數調用該函數。直到所有 caller function 都改完了,那麼這個“閉包”又擴展了一層。有些函數的調用者很多,一時改不完,folio-5.16 就提供了一層 wrapper。這裏以 page_mapping/folio_mapping 爲例。

首先閉包裏是 folio_test_slab(folio),folio_test_swapcache(folio) 等基礎設施,然後向上擴展到 folio_mapping。page_mapping 的調用者很多,mem_cgroup_move_account 能順利地調用 folio_mapping,而 page_evictable 卻還是保留使用 page_mapping。那麼閉包在這裏停止擴展。

struct address_space *folio_mapping(struct folio *folio)
        struct address_space *mapping;
        /* This happens if someone calls flush_dcache_page on slab page */
        if (unlikely(folio_test_slab(folio)))
                return NULL;
        if (unlikely(folio_test_swapcache(folio)))
                return swap_address_space(folio_swap_entry(folio));
        mapping = folio->mapping;
        if ((unsigned long)mapping & PAGE_MAPPING_ANON)
                return NULL;
        return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
struct address_space *page_mapping(struct page *page)
        return folio_mapping(page_folio(page));
mem_cgroup_move_account(page, ...) {
  folio = page_folio(page);
  mapping = folio_mapping(folio);
page_evictable(page, ...) {
  ret = !mapping_unevictable(page_mapping(page)) && !PageMlocked(page);

folio 就這些嗎?

很多小夥伴看到這裏是不是和我有一樣的感受:就這些嗎?僅僅是 compound_head 的問題嗎?我不得不去學習 LWN: A discussion on folios,LPC 2021 - File Systems MC 大佬關於 folio 的討論。然後發現 Matthew Wilcox 的主題不是《The folio》,而是《Efficient buffered I/O》。事情並不簡單。

這次 folio-5.16 合入的都是 fs 相關的代碼,組裏大佬提到 “Linux-mm 社區大佬不同意全部把 page 替換成 folio,對於匿名頁和 slab,短期內還是不能替換”。於是我繼續翻閱 Linux-mm 郵件列表。

2.1 folio 的社區討論

2.1.1 命名

首先是 Linus,Linus 表示他不討厭這組 patch,因爲這組 patch 確實解決了 compound_head 的問題;但是他也不喜歡這組 patch,原因是 folio 聽起來不直觀。經過若干關於取名的討論,當然命名最後還是 folio。

2.1.2 FS 開發者的意見

目前 page cache 中都是 4K page,page cache 中的大頁也是隻讀的,例如代碼大頁特性。爲什麼 Transparent huge pages in the page cache 一直沒有實現,可以參考這篇 LWN。其中一個原因是,要實現 讀寫 file THP,基於 buffer_head 的 fs 對 page cache 的處理過於複雜。

  • buffer_head
    buffer_head 代表的是物理內存映射的塊設備偏移位置,一般一個 buffer_head 也是 4K 大小,這樣一個 buffer_head 正好對應一個 page。某些文件系統可能採用更小的block size,例如 1K,或者 512 字節。這樣一個 page 最多可以有 4 或者 8 個buffer_head 結構體來描述其內存對應的物理磁盤位置。
    這樣,在處理 multi-page 讀寫的時候,每個 page 都需要通過 get_block 獲取 page 和 磁盤偏移的關係,低效且複雜。
  • iomap
    iomap 最初是從 XFS 內部拿出來的,基於 extent,天然支持 multi-page。即在處理 multi-page 讀寫的時候,僅需一次翻譯就能獲取所有 page 和 磁盤偏移的關係。
    通過 iomap,文件系統與 page cache 隔離開來了,例如,它們在表示大小的時候都使用字節,而不是有多少 page。因此,Matthew Wilcox 建議任何直接使用 page cache 的文件系統都應該考慮要換到 iomap 或 netfs_lib 了。
    隔離 fs 與 page cache 的方式或許不止 folio,但是例如 scatter gather 是不被接受的,抽象太複雜。

這也是爲什麼 folio 先在 XFS/AFS 中落地了,因爲這兩個文件系統就是基於 iomap 的。這也是爲什麼 FS 開發者都強烈希望 folio 被合入,他們可以方便地在 page cache 中使用更大的 page,這個做法可以使文件系統的 I/O 更有效率。buffer_head 有一些功能是當前 iomap 仍然缺乏的。而 folio 的合入,能讓 iomap 得到推進,從而使 block-based 文件系統能夠改成使用 iomap。

2.1.3 MM 開發者的意見

最大的異議來自 Johannes Weiner,他承認 compound_head 的問題,但覺得修復該問題而引入這麼大的改動不值得;同時認爲 folio 對 fs 的所做的優化,anonymous page 不需要。

Unlike the filesystem side, this seems like a lot of churn for very little tangible value. And leaves us with an end result that nobody appears to be terribly excited about.But the folio abstraction is too low-level to use JUST for file cache and NOT for anon. It's too close to the page layer itself and would duplicate too much of it to be maintainable side by side.

最後在 Kirill A. Shutemov、Michal Hocko 等大佬的力挺 folio 態度下,Johannes Weiner 也妥協了。

2.1.4 達成一致

社區討論到最後,針對 folio 的反對意見在 folio-5.15 的代碼中都已經不存在了,但錯過了 5.15 的合併窗口,因此這次 folio-5.16 原封不動被合入了。

2.2 folio 的深層價值

I think the problem with folio is that everybody wants to read in her/his hopes and dreams into it and gets disappointed when see their somewhat related problem doesn't get magically fixed with folio.
Folio started as a way to relief pain from dealing with compound pages. It provides an unified view on base pages and compound pages. That's it.
It is required ground work for wider adoption of compound pages in page cache. But it also will be useful for anon THP and hugetlb.
Based on adoption rate and resulting code, the new abstraction has nice downstream effects. It may be suitable for more than it was intended for initially. That's great.
But if it doesn't solve your problem... well, sorry...
The patchset makes a nice step forward and cuts back on mess I created on the way to huge-tmpfs.
I would be glad to see the patchset upstream.
--Kirill A. Shutemov

大家都知道“struct page 相關的混亂”,但沒有人去解決,大家都在默默忍受這長期以來的困擾,在代碼中充斥着如下代碼。

if (compound_head(page)) // do A;
else                     // do B;

folio 並不完美,或許因爲大家期望太高,導致少數人對 folio 的最終實現表示失望。但多數人認爲 folio 是在正確方向上的重要一步。畢竟後續還有更多工作要實現。

folio 後續工作及其他

3.1 folio 開發計劃

For 5.17, we intend to convert various filesystems (XFS and AFS are ready; other filesystems may make it) and also convert more of the MM and page cache to folios. For 5.18, multi-page folios should be ready.

3.2 folio 還能提升性能

The 80% win is real, but appears to be an artificial benchmark (postgres startup, which isn't a serious workload). Real workloads (eg building the kernel, running postgres in a steady state, etc) seem to benefit between 0-10%.

folio-5.16 減少大量 compound_head 調用,在 sys 高的 micro benchmark 中應當有性能提升。未實測。folio-5.18 multi-page folios 支持之後,理論上 I/O 效率能提升,拭目以待。

3.3 我應該怎麼用 folio?

FS 開發者最應該做的就是把那些仍然使用 buffer head 的文件系統轉換爲使用 iomap 進行 I/O,至少對於那些 block-based 文件系統都應該這麼做。其他開發者欣然接受 folio 即可,基於 5.16+ 開發的新特性能用 folio 就用 folio,熟悉一下 API 即可,內存分配回收等 API 本質沒有改變。

點擊立即免費試用雲產品 開啓雲上實踐之旅!



還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.