pin_user_pages() 及相關呼叫¶
概述¶
本文件描述了以下函式
pin_user_pages()
pin_user_pages_fast()
pin_user_pages_remote()
FOLL_PIN 的基本描述¶
FOLL_PIN 和 FOLL_LONGTERM 是可以傳遞給 get_user_pages*()(“gup”)系列函式的標誌。FOLL_PIN 與 FOLL_LONGTERM 之間存在重要的互動和相互依賴關係,因此本文件同時涵蓋兩者。
FOLL_PIN 是 gup 內部的,這意味著它不應出現在 gup 呼叫點。這使得相關的封裝函式(pin_user_pages*() 及其他)能夠設定這些標誌的正確組合,並檢查問題。
另一方面,FOLL_LONGTERM *可以*在 gup 呼叫點設定。這是為了避免建立大量的封裝函式來涵蓋 get*()、pin*()、FOLL_LONGTERM 等所有組合。此外,pin_user_pages*() API 與 get_user_pages*() API 明顯不同,因此這是一個自然的劃分界限,也是進行單獨封裝呼叫的一個好地方。換句話說,對於 DMA 鎖定的頁面,使用 pin_user_pages*();對於其他情況,使用 get_user_pages*()。本文件後面描述了五種情況,以進一步闡明這一概念。
FOLL_PIN 和 FOLL_GET 在給定的 gup 呼叫中是互斥的。然而,多個執行緒和呼叫點可以自由地透過 FOLL_PIN 和 FOLL_GET 鎖定相同的 struct pages。只需要呼叫點選擇其中一個,而不是 struct page(s)。
FOLL_PIN 的實現與 FOLL_GET 幾乎相同,只是 FOLL_PIN 使用了不同的引用計數技術。
FOLL_PIN 是 FOLL_LONGTERM 的先決條件。換句話說,FOLL_LONGTERM 是 FOLL_PIN 的一個更具體、更嚴格的案例。
每個封裝函式設定的標誌¶
對於這些 pin_user_pages*() 函式,FOLL_PIN 會與呼叫者提供的任何 gup 標誌進行或運算。呼叫者需要傳入一個非空的 struct pages* 陣列,然後函式透過將每個頁面增加一個特殊值:GUP_PIN_COUNTING_BIAS 來鎖定頁面。
對於大型 folio,不使用 GUP_PIN_COUNTING_BIAS 方案。取而代之的是,利用 struct folio 中可用的額外空間直接儲存 pincount。
這種針對大型 folio 的方法避免了下面討論的計數上限問題。這些限制會因巨頁(huge pages)而嚴重加劇,因為每個尾頁(tail page)都會給頭頁(head page)增加一個引用計數。事實上,測試表明,如果沒有獨立的 pincount 欄位,在一些巨頁壓力測試中會出現引用計數溢位。
這也意味著巨頁和大型 folio 不會遇到下面提到的誤報問題。
Function
--------
pin_user_pages FOLL_PIN is always set internally by this function.
pin_user_pages_fast FOLL_PIN is always set internally by this function.
pin_user_pages_remote FOLL_PIN is always set internally by this function.
對於這些 get_user_pages*() 函式,FOLL_GET 可能甚至沒有被指定。行為比上面稍微複雜一些。如果 *未*指定 FOLL_GET,但呼叫者傳入了一個非空的 struct pages* 陣列,則函式會為您設定 FOLL_GET,並透過將每個頁面的引用計數增加 +1 來鎖定頁面。
Function
--------
get_user_pages FOLL_GET is sometimes set internally by this function.
get_user_pages_fast FOLL_GET is sometimes set internally by this function.
get_user_pages_remote FOLL_GET is sometimes set internally by this function.
跟蹤 DMA 鎖定頁¶
跟蹤 DMA 鎖定頁的一些關鍵設計約束和解決方案
需要為每個 struct page 提供一個實際的引用計數。這是因為多個程序可能會鎖定和解除鎖定一個頁面。
誤報(報告頁面被 DMA 鎖定,但實際上沒有)是可以接受的,但漏報則不允許。
為此,struct page 的大小不能增加,並且所有欄位都已使用。
鑑於上述情況,我們可以透過使用該欄位的某種上部位來儲存 DMA 鎖定計數,從而過載 page->_refcount 欄位。“某種”意味著,我們不是將 page->_refcount 劃分為位欄位,而是簡單地將一箇中等大小的值(GUP_PIN_COUNTING_BIAS,最初選擇為 1024:10 位)新增到 page->_refcount。這會提供模糊的行為:如果一個頁面被呼叫 get_page() 1024 次,那麼它將顯示有一個 DMA 鎖定計數。這同樣是可以接受的。
這也導致了限制:對於每次遞增 10 位的計數器,只有 31-10=21 位可用。
由於這一限制,在使用 FOLL_PIN 時,對零頁(zero pages)進行了特殊處理。我們只是假裝鎖定一個零頁——我們根本不改變它的引用計數或鎖定計數(它是永久的,所以沒有必要)。解除鎖定函式也不會對零頁做任何事情。這對呼叫者來說是透明的。
呼叫者必須明確請求“頁面的 DMA 鎖定跟蹤”。換句話說,僅僅呼叫 get_user_pages() 是不夠的;必須使用一組新的函式,pin_user_page() 及相關函式。
FOLL_PIN, FOLL_GET, FOLL_LONGTERM: 何時使用哪個標誌¶
感謝 Jan Kara、Vlastimil Babka 和其他一些 -mm 人員描述了這些類別
案例 1: 直接 IO (DIO)¶
存在對用作 DIO 緩衝區的頁面的 GUP 引用。這些緩衝區只需要相對較短的時間(因此它們不是“長期”的)。未提供與 folio_mkclean() 或 munmap() 的特殊同步。因此,在呼叫點設定的標誌是
FOLL_PIN
...但呼叫點不應直接設定 FOLL_PIN,而應使用設定 FOLL_PIN 的 pin_user_pages*() 例程之一。
案例 2: RDMA¶
存在對用作 DMA 緩衝區的頁面的 GUP 引用。這些緩衝區需要很長時間(“長期”)。未提供與 folio_mkclean() 或 munmap() 的特殊同步。因此,在呼叫點設定的標誌是
FOLL_PIN | FOLL_LONGTERM
注意:某些頁面,例如 DAX 頁面,不能用長期鎖定來鎖定。這是因為 DAX 頁面沒有單獨的頁面快取,因此“鎖定”意味著鎖定檔案系統塊,這尚未(或暫時)以這種方式支援。
案例 3: MMU 通知器註冊,無論是否有頁錯誤硬體¶
裝置驅動程式可以透過 get_user_pages*() 鎖定頁面,並註冊記憶體範圍的 MMU 通知器回撥。然後,在收到通知器“invalidate range”回撥後,停止裝置使用該範圍,並解除頁面鎖定。可能還有其他方案,例如明確地與掛起的 IO 進行同步,也能達到大致相同的目的。
或者,如果硬體支援可重播頁錯誤,那麼裝置驅動程式可以完全避免鎖定(這是理想情況),如下所示:如上所述註冊 MMU 通知器回撥,但不是在回撥中停止裝置和解除鎖定,而是簡單地從裝置的頁表中移除該範圍。
無論哪種方式,只要驅動程式在 MMU 通知器回撥時解除頁面鎖定,就能與檔案系統和 MM(folio_mkclean()、munmap() 等)進行適當的同步。因此,無需設定任何標誌。
案例 4: 僅用於 struct page 操作的鎖定¶
如果隻影響 struct page 資料(而不是頁面跟蹤的實際記憶體內容),則正常的 GUP 呼叫就足夠了,並且不需要設定任何標誌。
案例 5: 為寫入頁面內資料而鎖定¶
即使不涉及 DMA 或直接 IO,僅僅一個簡單的“鎖定、寫入頁面資料、解除鎖定”的案例也可能導致問題。案例 5 可以被視為案例 1 和案例 2 的超集,以及任何呼叫該模式的情況。換句話說,如果程式碼既不是案例 1 也不是案例 2,它仍然可能需要 FOLL_PIN,例如以下模式:
- 正確(使用 FOLL_PIN 呼叫)
pin_user_pages() 寫入頁面內資料 unpin_user_pages()
- 不正確(使用 FOLL_GET 呼叫)
get_user_pages() 寫入頁面內資料 put_page()
folio_maybe_dma_pinned(): 鎖定的全部意義¶
將 folio 標記為“DMA 鎖定”或“gup 鎖定”的全部意義在於能夠查詢“此 folio 是否被 DMA 鎖定?”這使得諸如 folio_mkclean()(以及一般的檔案系統回寫程式碼)之類的程式碼能夠做出明智的決定,當 folio 由於此類鎖定而無法解除對映時該怎麼做。
在這些情況下該怎麼做是長達數年的討論和辯論的主題(請參閱本文件末尾的參考文獻)。這是一個待辦事項:一旦確定了細節,請在此處填寫。同時,可以肯定地說,擁有此功能是
static inline bool folio_maybe_dma_pinned(struct folio *folio)
...是解決長期存在的 gup+DMA 問題的先決條件。
關於 FOLL_GET, FOLL_PIN 和 FOLL_LONGTERM 的另一種思考方式¶
另一種思考這些標誌的方式是將其視為一系列限制的遞進:FOLL_GET 用於 struct page 操作,而不影響 struct page 引用的資料。FOLL_PIN 是 FOLL_GET 的*替代*,用於對資料*將*被訪問的頁面進行短期鎖定。因此,FOLL_PIN 是一種“更嚴格”的鎖定形式。最後,FOLL_LONGTERM 是一個更具限制性的案例,它以 FOLL_PIN 為前提:這適用於將長期鎖定且其資料將被訪問的頁面。
單元測試¶
此檔案
tools/testing/selftests/mm/gup_test.c
包含以下新的呼叫來測試新的 pin*() 封裝函式
PIN_FAST_BENCHMARK (./gup_test -a)
PIN_BASIC_TEST (./gup_test -b)
您可以透過兩個新的 /proc/vmstat 條目監控系統啟動以來總共獲取和釋放了多少 DMA 鎖定頁面
/proc/vmstat/nr_foll_pin_acquired
/proc/vmstat/nr_foll_pin_released
在正常情況下,除非有任何長期 [R]DMA 鎖定存在,或在鎖定/解除鎖定轉換期間,這兩個值將相等。
nr_foll_pin_acquired: 這是系統通電以來獲取的邏輯鎖定數量。對於巨頁,巨頁中的每個頁面(頭頁和每個尾頁)都會鎖定頭頁一次。這與 get_user_pages() 對巨頁使用的行為類似:當 get_user_pages() 應用於巨頁時,巨頁中的每個尾頁或頭頁都會使頭頁的引用計數增加一次。
nr_foll_pin_released: 這是系統通電以來已釋放的邏輯鎖定數量。請注意,即使最初的鎖定應用於巨頁,頁面也會以 PAGE_SIZE 粒度釋放(解除鎖定)。由於上面“nr_foll_pin_acquired”中描述的鎖定計數行為,會計核算會平衡,因此在執行此操作後
pin_user_pages(huge_page); for (each page in huge_page) unpin_user_page(page);
...預計會發生以下情況
nr_foll_pin_released == nr_foll_pin_acquired
(...除非由於存在長期 RDMA 鎖定而導致其已經失衡。)
其他診斷¶
dump_page() 已略微增強,以處理這些新的計數字段,並更好地報告大型 folio。具體來說,對於大型 folio,會報告精確的鎖定計數 (pincount)。
參考文獻¶
John Hubbard,2019 年 10 月