高記憶體處理

作者:Peter Zijlstra <a.p.zijlstra@chello.nl>

什麼是高記憶體?

當物理記憶體的大小接近或超過虛擬記憶體的最大大小時,將使用高記憶體(highmem)。那時,核心不可能始終保持對映所有可用的物理記憶體。這意味著核心需要開始使用物理記憶體片段的臨時對映,以便訪問它們。

沒有永久對映覆蓋的那部分(物理)記憶體,我們稱之為“高記憶體”。對於邊界的確切位置,存在各種體系結構相關的約束。

例如,在 i386 架構中,我們選擇將核心對映到每個程序的 VM 空間中,這樣我們就不必為核心進入/退出而付出完整的 TLB 失效成本。這意味著可用的虛擬記憶體空間(在 i386 上為 4GiB)必須在使用者空間和核心空間之間劃分。

使用此方法的體系結構的傳統拆分是 3:1,3GiB 用於使用者空間,頂部 1GiB 用於核心空間

+--------+ 0xffffffff
| Kernel |
+--------+ 0xc0000000
|        |
| User   |
|        |
+--------+ 0x00000000

這意味著核心最多可以一次對映 1GiB 的物理記憶體,但由於我們需要虛擬地址空間用於其他事物(包括訪問其餘物理記憶體的臨時對映),因此實際的直接對映通常會更少(通常約為 ~896MiB)。

具有 mm 上下文標記 TLB 的其他體系結構可以具有單獨的核心和使用者對映。但是,某些硬體(例如某些 ARM)在使用 mm 上下文標記時,虛擬空間有限。

臨時虛擬對映

核心包含幾種建立臨時對映的方法。以下列表按使用偏好順序列出它們。

  • kmap_local_page(), kmap_local_folio() - 這些函式用於建立短期對映。 它們可以從任何上下文(包括中斷)呼叫,但是這些對映只能在獲取它們的上下文中使用。 它們之間唯一的區別在於,第一個函式獲取指向 struct page 的指標,第二個函式獲取指向 struct folio 的指標以及標識該頁面在對開本中的位元組偏移量。

    應該始終使用這些函式,而 kmap_atomic()kmap() 已被棄用。

    這些對映是執行緒本地和 CPU 本地的,這意味著該對映只能從此執行緒訪問,並且該執行緒在對映處於活動狀態時繫結到 CPU。 儘管此函式永遠不會停用搶佔,但在釋放對映之前,無法透過 CPU 熱插拔從系統中拔出 CPU。

    在本地 kmap 區域中獲取頁面錯誤是有效的,除非獲取本地對映的上下文不允許出於其他原因這樣做。

    如前所述,頁面錯誤和搶佔永遠不會被停用。 無需停用搶佔,因為當上下文切換到另一個任務時,傳出任務的對映會被儲存,傳入任務的對映會被恢復。

    kmap_local_page() 以及 kmap_local_folio() 始終返回有效的虛擬核心地址,並且假定 kunmap_local() 永遠不會失敗。

    在 CONFIG_HIGHMEM=n 核心和低記憶體頁面上,它們返回直接對映的虛擬地址。 只有真正的高記憶體頁面被臨時對映。 因此,使用者可以為已知不是來自 ZONE_HIGHMEM 的頁面呼叫普通的 page_address()。 但是,始終可以安全地使用 kmap_local_{page,folio}() / kunmap_local()

    雖然它們比 kmap() 快得多,但對於高記憶體情況,它們對指標有效性有侷限性。 與 kmap() 對映相反,本地對映僅在呼叫者的上下文中有效,不能傳遞給其他上下文。 這意味著使用者必須絕對確保將返回地址的使用範圍限定為對映它的執行緒。

    大多數程式碼都可以設計為使用執行緒本地對映。 因此,使用者應嘗試設計其程式碼以避免使用 kmap(),方法是在將使用地址的同一執行緒中對映頁面,並優先使用 kmap_local_page()kmap_local_folio()

    允許在一定程度上巢狀 kmap_local_page()kmap_atomic() 對映(最多 KMAP_TYPE_NR),但它們的呼叫必須嚴格排序,因為對映實現基於堆疊。 有關如何管理巢狀對映的詳細資訊,請參見 kmap_local_page() kdocs(包含在“函式”部分中)。

  • kmap_atomic()。 此函式已被棄用; 請改用 kmap_local_page()

    注意:轉換為 kmap_local_page() 必須注意遵循施加於 kmap_local_page() 的對映限制。 此外,對 kmap_atomic()kunmap_atomic() 的呼叫之間的程式碼可能隱式地依賴於原子對映的副作用,即停用頁面錯誤或搶佔,或兩者。 在這種情況下,必須將對 pagefault_disable() 或 preempt_disable() 或兩者的顯式呼叫與 kmap_local_page() 一起使用。

    [舊文件]

    這允許單個頁面的非常短時間的對映。 由於對映僅限於發出它的 CPU,因此效能良好,但是因此要求發出任務停留在該 CPU 上直到完成,以免其他任務替換其對映。

    kmap_atomic() 也可以由中斷上下文使用,因為它不會休眠,並且呼叫者在呼叫 kunmap_atomic() 之後也可能不會休眠。

    核心中對 kmap_atomic() 的每次呼叫都會建立一個不可搶佔的部分並停用頁面錯誤。 這可能是造成不必要延遲的來源。 因此,使用者應首選 kmap_local_page() 而不是 kmap_atomic()

    假定 k[un]map_atomic() 不會失敗。

  • kmap()。 此函式已被棄用; 請改用 kmap_local_page()

    注意:轉換為 kmap_local_page() 必須注意遵循施加於 kmap_local_page() 的對映限制。 特別是,有必要確保核心虛擬記憶體指標僅在獲取它的執行緒中有效。

    [舊文件]

    這應該用於對單個頁面進行短時間對映,而對搶佔或遷移沒有任何限制。 它具有開銷,因為對映空間受到限制,並且受到全域性鎖的保護以進行同步。 當不再需要對映時,必須使用 kunmap() 釋放頁面對映到的地址。

    必須在所有 CPU 上傳播對映更改。 當 kmap 的池環繞時,kmap() 還需要全域性 TLB 失效,並且當對映空間被完全利用直到有空閒插槽時,它可能會阻塞。 因此,kmap() 只能從可搶佔的上下文中呼叫。

    如果對映必須持續相對較長的時間,則以上所有工作都是必要的,但是核心中大部分高記憶體對映都是短期的,並且僅在一個地方使用。 這意味著在這些情況下,kmap() 的成本在很大程度上被浪費了。 kmap() 並非旨在用於長期對映,但它已朝著該方向發展,並且強烈建議在新程式碼中不要使用它,而應首選前面這組函式。

    在 64 位系統上,對 kmap_local_page(), kmap_atomic()kmap() 的呼叫沒有實際工作要做,因為 64 位地址空間足以定址所有頁面已永久對映的物理記憶體。

  • vmap()。 這可用於將多個物理頁面長時間對映到連續的虛擬空間中。 它需要全域性同步才能取消對映。

臨時對映的成本

建立臨時對映的成本可能很高。 該架構必須操作核心的頁表、資料 TLB 和/或 MMU 的暫存器。

如果未設定 CONFIG_HIGHMEM,則核心將嘗試建立一個僅使用一些演算法的對映,該演算法會將頁面結構地址轉換為指向頁面內容的指標,而不是對映about。 在這種情況下,取消對映操作可能為空操作。

如果未設定 CONFIG_MMU,則不能有臨時對映,也不能有高記憶體。 在這種情況下,也將使用算術方法。

i386 PAE

在某些情況下,i386 架構將允許您將最多 64GiB 的 RAM 插入 32 位計算機中。 這會產生許多後果

  • Linux 需要系統中的每個頁面的頁面幀結構,並且頁面幀需要位於永久對映中,這意味著

  • 您最多可以擁有 896M/sizeof(struct page) 頁面幀; struct page 為 32 位元組,最終大約為 112G 的頁面; 但是,核心需要在該記憶體中儲存不僅僅是頁面幀...

  • PAE 使您的頁表更大 - 這會降低系統速度,因為需要訪問更多資料才能在 TLB 填充等方面進行遍歷。 一個優點是 PAE 具有更多的 PTE 位,可以提供高階功能,例如 NX 和 PAT。

一般的建議是您在 32 位計算機上不要使用超過 8GiB 的記憶體 - 儘管對於您和您的工作負載來說,更多記憶體可能會起作用,但您幾乎是孤身一人 - 如果出現問題,不要期望核心開發人員真正關心。

函式

void *kmap(struct page *page)

對映頁面以供長期使用

引數

struct page *page

指向要對映的頁面

返回

對映的虛擬地址

描述

只能從可搶佔的任務上下文中呼叫,因為在啟用了 CONFIG_HIGHMEM 的 32 位系統上,此函式可能會休眠。

對於 CONFIG_HIGHMEM=n 的系統以及低記憶體區域中的頁面,這將返回直接核心對映的虛擬地址。

返回的虛擬地址是全域性可見的,並且有效,直到透過 kunmap() 取消對映為止。 指標可以傳遞給其他上下文。

對於 32 位系統上的高記憶體頁面,這可能很慢,因為對映空間受到限制並且受到全域性鎖的保護。 如果沒有可用的對映插槽,該函式將阻塞,直到透過 kunmap() 釋放插槽為止。

void kunmap(struct page *page)

取消對映由 kmap() 對映的虛擬地址

引數

struct page *page

指向由 kmap() 對映的頁面

描述

對應於 kmap()。 對於 CONFIG_HIGHMEM=n 以及低記憶體區域中頁面的對映,為 NOOP。

struct page *kmap_to_page(void *addr)

獲取 kmap'ed 地址的頁面

引數

void *addr

要查詢的地址

返回

對映到 addr 的頁面。

void kmap_flush_unused(void)

重新整理所有未使用的 kmap 對映,以刪除錯誤的對映

引數

void

無引數

void *kmap_local_page(struct page *page)

對映頁面以供臨時使用

引數

struct page *page

指向要對映的頁面

返回

對映的虛擬地址

描述

可以從任何上下文呼叫,包括中斷。

在巢狀多個對映時需要小心處理,因為對映管理基於堆疊。 取消對映必須與對映操作的順序相反

addr1 = kmap_local_page(page1); addr2 = kmap_local_page(page2); ... kunmap_local(addr2); kunmap_local(addr1);

在 addr2 之前取消對映 addr1 是無效的,會導致故障。

kmap() 對映相反,該對映僅在呼叫者的上下文中有效,不能傳遞給其他上下文。

在 CONFIG_HIGHMEM=n 核心和低記憶體頁面上,這將返回直接對映的虛擬地址。 只有真正的高記憶體頁面被臨時對映。

雖然 kmap_local_page()kmap() 快得多,但對於高記憶體情況,它對指標有效性有侷限性。

在啟用 HIGHMEM 的系統上,對映高記憶體頁面具有停用遷移的副作用,以便在搶佔期間保持虛擬地址穩定。 kmap_local_page() 的任何呼叫者都不能依賴此副作用。

void *kmap_local_folio(struct folio *folio, size_t offset)

對映此對開本中的頁面以供臨時使用

引數

struct folio *folio

包含頁面的對開本。

size_t offset

標識頁面的對開本中的位元組偏移量。

描述

在巢狀多個對映時需要小心處理,因為對映管理基於堆疊。 取消對映必須與對映操作的順序相反

addr1 = kmap_local_folio(folio1, offset1);
addr2 = kmap_local_folio(folio2, offset2);
...
kunmap_local(addr2);
kunmap_local(addr1);

在 addr2 之前取消對映 addr1 是無效的,會導致故障。

kmap() 對映相反,該對映僅在呼叫者的上下文中有效,不能傳遞給其他上下文。

在 CONFIG_HIGHMEM=n 核心和低記憶體頁面上,這將返回直接對映的虛擬地址。 只有真正的高記憶體頁面被臨時對映。

雖然它比 kmap() 快得多,但對於高記憶體情況,它對指標有效性有侷限性。

在啟用 HIGHMEM 的系統上,對映高記憶體頁面具有停用遷移的副作用,以便在搶佔期間保持虛擬地址穩定。 kmap_local_folio() 的任何呼叫者都不能依賴此副作用。

上下文

可以從任何上下文呼叫。

返回

offset 的虛擬地址。

void *kmap_atomic(struct page *page)

原子地對映一個頁面用於臨時使用 - 已棄用!

引數

struct page *page

指向要對映的頁面

返回

對映的虛擬地址

描述

實際上是 kmap_local_page() 的一個包裝器,它也停用了缺頁中斷,並且根據 PREEMPT_RT 配置,也停用了 CPU 遷移和搶佔。因此,使用者不應依賴後兩個副作用。

對映應該總是透過 kunmap_atomic() 釋放。

不要在新程式碼中使用。請改用 kmap_local_page()

當代碼想要訪問可能從高階記憶體(參見 __GFP_HIGHMEM)分配的頁面的內容時,例如在頁面快取中的頁面,它將在原子上下文中使用。該 API 有兩個函式,它們可以以類似於以下的方式使用

// Find the page of interest.
struct page *page = find_get_page(mapping, offset);

// Gain access to the contents of that page.
void *vaddr = kmap_atomic(page);

// Do something to the contents of that page.
memset(vaddr, 0, PAGE_SIZE);

// Unmap that page.
kunmap_atomic(vaddr);

請注意,kunmap_atomic() 呼叫採用 kmap_atomic() 呼叫的結果,而不是引數。

如果您需要對映兩個頁面,因為您想從一個頁面複製到另一個頁面,您需要嚴格巢狀 kmap_atomic 呼叫,例如

vaddr1 = kmap_atomic(page1); vaddr2 = kmap_atomic(page2);

memcpy(vaddr1, vaddr2, PAGE_SIZE);

kunmap_atomic(vaddr2); kunmap_atomic(vaddr1);

struct folio *vma_alloc_zeroed_movable_folio(struct vm_area_struct *vma, unsigned long vaddr)

為一個 VMA 分配一個零填充的頁面。

引數

struct vm_area_struct *vma

要為其分配頁面的 VMA。

unsigned long vaddr

頁面將被插入到的虛擬地址。

描述

此函式將分配一個適合插入到此 VMA 在此虛擬地址的頁面。它可能從高階記憶體或可移動區域分配。架構可以提供自己的實現。

返回

包含一個已分配和零填充頁面的 folio,如果記憶體不足,則為 NULL。

void memcpy_from_folio(char *to, struct folio *folio, size_t offset, size_t len)

從 folio 複製一個位元組範圍。

引數

char *to

要複製到的記憶體。

struct folio *folio

要從中讀取的 folio。

size_t offset

要讀取的 folio 中的第一個位元組。

size_t len

要複製的位元組數。

void memcpy_to_folio(struct folio *folio, size_t offset, const char *from, size_t len)

將一個位元組範圍複製到 folio。

引數

struct folio *folio

要寫入的 folio。

size_t offset

要儲存到的 folio 中的第一個位元組。

const char *from

要從中複製的記憶體。

size_t len

要複製的位元組數。

void *folio_zero_tail(struct folio *folio, size_t offset, void *kaddr)

將 folio 的尾部置零。

引數

struct folio *folio

要置零的 folio。

size_t offset

folio 中開始置零的位元組偏移量。

void *kaddr

當前 folio 對映到的地址。

描述

如果您已經使用 kmap_local_folio() 來對映一個 folio,向它寫入一些資料,現在需要將 folio 的末尾置零(並重新整理 dcache),您可以使用此函式。如果您沒有對 folio 進行 kmap(例如,folio 已被 DMA 部分填充),請改用 folio_zero_range()folio_zero_segment()

返回

可以傳遞給 kunmap_local() 的地址。

void folio_fill_tail(struct folio *folio, size_t offset, const char *from, size_t len)

將一些資料複製到 folio 並用零填充。

引數

struct folio *folio

目標 folio。

size_t offset

開始複製的 **folio** 中的偏移量。

const char *from

要複製的資料。

size_t len

要複製的位元組數。

描述

此函式對於支援內聯資料的檔案系統最有用。當他們想將資料從 inode 複製到頁面快取中時,此函式會為他們完成所有操作。它支援即使在 HIGHMEM 配置上也能使用大型 folios。

size_t memcpy_from_file_folio(char *to, struct folio *folio, loff_t pos, size_t len)

從檔案 folio 複製一些位元組。

引數

char *to

目標緩衝區。

struct folio *folio

要從中複製的 folio。

loff_t pos

檔案中的位置。

size_t len

要複製的最大位元組數。

描述

從此 folio 複製最多 **len** 個位元組。如果 folio 來自 HIGHMEM,則可能受到 PAGE_SIZE 的限制,並且受到 folio 大小的限制。

返回

從 folio 複製的位元組數。

void folio_zero_segments(struct folio *folio, size_t start1, size_t xend1, size_t start2, size_t xend2)

將 folio 中的兩個位元組範圍置零。

引數

struct folio *folio

要寫入的 folio。

size_t start1

要置零的第一個位元組。

size_t xend1

比第一個範圍中的最後一個位元組大一。

size_t start2

第二個範圍中要置零的第一個位元組。

size_t xend2

比第二個範圍中的最後一個位元組大一。

void folio_zero_segment(struct folio *folio, size_t start, size_t xend)

將 folio 中的一個位元組範圍置零。

引數

struct folio *folio

要寫入的 folio。

size_t start

要置零的第一個位元組。

size_t xend

比要置零的最後一個位元組大一。

void folio_zero_range(struct folio *folio, size_t start, size_t length)

將 folio 中的一個位元組範圍置零。

引數

struct folio *folio

要寫入的 folio。

size_t start

要置零的第一個位元組。

size_t length

要置零的位元組數。

void folio_release_kmap(struct folio *folio, void *addr)

取消對映一個 folio 並刪除一個引用計數。

引數

struct folio *folio

要釋放的 folio。

void *addr

先前由呼叫 kmap_local_folio() 返回的地址。

描述

通常,例如在目錄處理中,對一個 folio 進行 kmap。此函式取消對映 folio 並刪除被保持的引用計數,以在我們訪問它時保持 folio 活動。

void *kmap_high(struct page *page)

將一個 highmem 頁面對映到記憶體中

引數

struct page *page

要對映的 struct page

描述

返回頁面的虛擬記憶體地址。

我們不能從中斷中呼叫它,因為它可能會阻塞。

void *kmap_high_get(struct page *page)

將一個 highmem 頁面固定到記憶體中

引數

struct page *page

要固定的 struct page

描述

返回頁面的當前虛擬記憶體地址,如果不存在對映,則返回 NULL。如果且僅當返回一個非空地址時,才需要匹配呼叫 kunmap_high()

這可以從任何上下文中呼叫。

void kunmap_high(struct page *page)

將一個 highmem 頁面取消對映到記憶體中

引數

struct page *page

要取消對映的 struct page

描述

如果未定義 ARCH_NEEDS_KMAP_HIGH_GET,則只能從使用者上下文中呼叫此函式。

void *page_address(const struct page *page)

獲取頁面的對映虛擬地址

引數

const struct page *page

要獲取虛擬地址的 struct page

描述

返回頁面的虛擬地址。

void set_page_address(struct page *page, void *virtual)

設定頁面的虛擬地址

引數

struct page *page

要設定的 struct page

void *virtual

要使用的虛擬地址

kunmap_atomic

kunmap_atomic (__addr)

取消對映由 kmap_atomic() 對映的虛擬地址 - 已棄用!

引數

__addr

要取消對映的虛擬地址

描述

取消對映先前由 kmap_atomic() 對映的地址,並重新啟用缺頁中斷。 根據 PREEMP_RT 配置,還會重新啟用遷移和搶佔。 使用者不應依賴這些副作用。

對映應該按照它們被對映的相反順序取消對映。 有關巢狀的詳細資訊,請參閱 kmap_local_page()

**__addr** 可以是對映頁面中的任何地址,因此無需減去已新增的任何偏移量。 與 kunmap() 相比,此函式採用從 kmap_atomic() 返回的地址,而不是傳遞給它的頁面。 如果您傳遞頁面,編譯器會發出警告。

kunmap_local

kunmap_local (__addr)

取消對映透過 kmap_local_page() 對映的頁面。

引數

__addr

對映頁面中的一個地址

描述

**__addr** 可以是對映頁面中的任何地址。 通常它是從 kmap_local_page() 返回的地址,但它也可以包括偏移量。

取消對映應該按照對映的相反順序進行。 詳情請參考 kmap_local_page()