Linux 下的快取和 TLB 重新整理¶
- 作者:
David S. Miller <davem@redhat.com>
本文件描述了 Linux VM 子系統呼叫的快取/TLB 重新整理介面。它列舉了每個介面,描述了其預期目的,以及呼叫介面後預期的副作用。
下面描述的副作用是針對單處理器實現的,以及在該單個處理器上會發生什麼。SMP 情況是一個簡單的擴充套件,即您只需擴充套件定義,以便特定介面的副作用發生在系統中的所有處理器上。不要讓這嚇到您,認為 SMP 快取/TLB 重新整理效率低下,這實際上是一個可以進行許多最佳化的地方。例如,如果可以證明使用者地址空間從未在某個 CPU 上執行(參見 mm_cpumask()),則無需在該 CPU 上對該地址空間執行重新整理。
首先是 TLB 重新整理介面,因為它們最簡單。“TLB”在 Linux 下被抽象為 CPU 用於快取從軟體頁表獲得的虛擬到物理地址轉換的東西。這意味著如果軟體頁表發生變化,這個“TLB”快取中可能存在過時的轉換。因此,當軟體頁表發生變化時,核心將在頁表變化發生_之後_呼叫以下重新整理方法之一。
void flush_tlb_all(void)所有重新整理中最嚴重的。在此介面執行後,任何以前的頁表修改都將對 CPU 可見。
這通常在核心頁表更改時呼叫,因為此類轉換本質上是“全域性的”。
void flush_tlb_mm(struct mm_struct *mm)此介面從 TLB 中重新整理整個使用者地址空間。執行後,此介面必須確保地址空間“mm”的任何以前的頁表修改都將對 CPU 可見。也就是說,執行後,TLB 中將不存在“mm”的任何條目。
此介面用於處理整個地址空間頁表操作,例如 fork 和 exec 期間發生的操作。
void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)在這裡,我們正在從 TLB 中重新整理特定範圍的(使用者)虛擬地址轉換。執行後,此介面必須確保地址空間“vma->vm_mm”中“start”到“end-1”範圍內的任何以前的頁表修改都將對 CPU 可見。也就是說,執行後,TLB 中將不存在“mm”的在“start”到“end-1”範圍內的虛擬地址條目。
“vma”是用於該區域的後備儲存。主要用於 munmap() 型別操作。
提供此介面是希望移植能夠找到一種足夠有效的方法來從 TLB 中刪除多個頁面大小的轉換,而不是讓核心為每個可能被修改的條目呼叫 flush_tlb_page(見下文)。
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)這次我們需要從 TLB 中刪除 PAGE_SIZE 大小的轉換。“vma”是 Linux 用於跟蹤程序的 mmap 區域的後備結構,地址空間可透過 vma->vm_mm 獲得。此外,可以透過測試 (vma->vm_flags & VM_EXEC) 來檢視此區域是否可執行(因此在分體式 TLB 設定中可能存在於“指令 TLB”中)。
執行後,此介面必須確保地址空間“vma->vm_mm”中使用者虛擬地址“addr”的任何以前的頁表修改都將對 CPU 可見。也就是說,執行後,TLB 中將不存在“vma->vm_mm”中虛擬地址“addr”的條目。
這主要在缺頁處理期間使用。
void update_mmu_cache_range(struct vm_fault *vmf, struct vm_area_struct *vma, unsigned long address, pte_t *ptep, unsigned int nr)在每次缺頁的末尾,都會呼叫此例程,以告知特定於架構的程式碼,現在在地址空間“vma->vm_mm”中的虛擬地址“address”處,存在連續“nr”個頁面的軟體頁錶轉換。
此例程也在傳遞 NULL“vmf”的各種其他位置被呼叫。
移植可以以任何它選擇的方式使用此資訊。例如,它可以利用此事件為軟體管理的 TLB 配置預載入 TLB 轉換。sparc64 移植目前就是這樣做的。
接下來,我們有快取重新整理介面。通常,當 Linux 將現有虛擬到物理對映更改為新值時,序列將採用以下形式之一:
1) flush_cache_mm(mm);
change_all_page_tables_of(mm);
flush_tlb_mm(mm);
2) flush_cache_range(vma, start, end);
change_range_of_page_tables(mm, start, end);
flush_tlb_range(vma, start, end);
3) flush_cache_page(vma, addr, pfn);
set_pte(pte_pointer, new_pte_val);
flush_tlb_page(vma, addr);
快取級別重新整理將始終是第一個,因為這允許我們正確處理快取嚴格且要求虛擬地址從快取中重新整理時該虛擬地址存在虛擬到物理轉換的系統。HyperSparc CPU 就是具有此屬性的此類 CPU 之一。
下面的快取重新整理例程只需要處理特定 CPU 所需的快取重新整理。大多數情況下,對於具有虛擬索引快取且在虛擬到物理轉換更改或刪除時必須重新整理的 CPU,必須實現這些例程。因此,例如,IA32 處理器的物理索引物理標記快取不需要實現這些介面,因為快取是完全同步的並且不依賴於轉換資訊。
以下是例程,逐一介紹:
void flush_cache_mm(struct mm_struct *mm)此介面將整個使用者地址空間從快取中重新整理。也就是說,執行後,將沒有與“mm”關聯的快取行。
此介面用於處理整個地址空間頁表操作,例如在退出和執行期間發生的操作。
void flush_cache_dup_mm(struct mm_struct *mm)此介面將整個使用者地址空間從快取中重新整理。也就是說,執行後,將沒有與“mm”關聯的快取行。
此介面用於處理整個地址空間頁表操作,例如在 fork 期間發生的操作。
此選項與 flush_cache_mm 分開,以允許對 VIPT 快取進行一些最佳化。
void flush_cache_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)在這裡,我們正在從快取中重新整理特定範圍的(使用者)虛擬地址。執行後,快取中將不存在“vma->vm_mm”的在“start”到“end-1”範圍內的虛擬地址條目。
“vma”是用於該區域的後備儲存。主要用於 munmap() 型別操作。
提供此介面是希望移植能夠找到一種足夠有效的方法來從快取中刪除多個頁面大小的區域,而不是讓核心為每個可能被修改的條目呼叫 flush_cache_page(見下文)。
void flush_cache_page(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn)這次我們需要從快取中刪除 PAGE_SIZE 大小的範圍。“vma”是 Linux 用於跟蹤程序的 mmap 區域的後備結構,地址空間可透過 vma->vm_mm 獲得。此外,可以透過測試 (vma->vm_flags & VM_EXEC) 來檢視此區域是否可執行(因此在“哈佛”型快取佈局中可能存在於“指令快取”中)。
“pfn”指示“addr”轉換到的物理頁幀(將此值左移 PAGE_SHIFT 可獲得物理地址)。應從快取中刪除的就是此對映。
執行後,快取中將不存在“vma->vm_mm”的虛擬地址“addr”轉換為“pfn”的條目。
這主要在缺頁處理期間使用。
void flush_cache_kmaps(void)只有當平臺使用高記憶體時,才需要實現此例程。它將在所有 kmap 失效之前被呼叫。
執行後,核心虛擬地址範圍 PKMAP_ADDR(0) 到 PKMAP_ADDR(LAST_PKMAP) 的快取中將沒有條目。
此路由應在 asm/highmem.h 中實現
void flush_cache_vmap(unsigned long start, unsigned long end)void flush_cache_vunmap(unsigned long start, unsigned long end)在這兩個介面中,我們正在從快取中重新整理特定範圍的(核心)虛擬地址。執行後,核心地址空間中將沒有在“start”到“end-1”範圍內的虛擬地址條目。
這兩個例程中的第一個在 vmap_range() 安裝頁表條目後被呼叫。第二個在 vunmap_range() 刪除頁表條目之前被呼叫。
還有另一類 CPU 快取問題,目前需要一套完全不同的接口才能妥善處理。最大的問題是處理器資料快取中的虛擬別名(virtual aliasing)。
您的移植是否容易受到 D-cache 中虛擬別名的影響?好吧,如果您的 D-cache 是虛擬索引的,大小大於 PAGE_SIZE,並且不阻止同一物理地址的多個快取行同時存在,那麼您就有這個問題。
如果您的 D-cache 有這個問題,首先請在 asm/shmparam.h 中正確定義 SHMLBA,它本質上應該是您的虛擬定址 D-cache 的大小(如果大小可變,則為最大可能大小)。此設定將強制 SYSv IPC 層只允許使用者程序將共享記憶體 mmap 到此值倍數的地址。
注意
這並不能解決共享 mmap 的問題,請檢視 sparc64 移植以瞭解一種解決方案(特別是 SPARC_FLAG_MMAPSHARED)。
接下來,您必須解決所有其他情況下的 D-cache 別名問題。請記住,對於對映到某些使用者地址空間中的給定頁面,總是至少存在一個附加對映,即核心在其從 PAGE_OFFSET 開始的線性對映中的對映。因此,一旦第一個使用者將給定物理頁面對映到其地址空間中,D-cache 別名問題就可能立即存在,因為核心已經將此頁面對映到其虛擬地址。
void copy_user_page(void *to, void *from, unsigned long addr, struct page *page)void clear_user_page(void *to, unsigned long addr, struct page *page)這兩個例程將資料儲存在使用者匿名或 COW 頁面中。它允許移植有效地避免使用者空間和核心之間的 D-cache 別名問題。
例如,移植可以在複製期間將“from”和“to”臨時對映到核心虛擬地址。這兩個頁面的虛擬地址選擇方式是,核心的載入/儲存指令發生到與頁面的使用者對映具有相同“顏色”的虛擬地址。例如,Sparc64 使用了這種技術。
“addr”引數告訴使用者最終將此頁面對映到的虛擬地址,而“page”引數提供指向目標頁面的 struct page 的指標。
如果 D-cache 別名不是問題,這兩個例程可以直接呼叫 memcpy/memset,無需做更多事情。
void flush_dcache_folio(struct folio *folio)在以下情況下必須呼叫此例程:
核心已寫入頁面快取頁面和/或高記憶體中的頁面。
核心即將從頁面快取頁面讀取,並且可能存在此頁面的使用者空間共享/可寫對映。請注意,{get,pin}_user_pages{_fast} 已經對使用者地址空間中找到的任何頁面呼叫了 flush_dcache_folio,因此驅動程式程式碼很少需要考慮這一點。
注意
此例程只需要為可能對映到使用者程序地址空間的頁面快取頁面呼叫。因此,例如,處理頁面快取中 vfs 符號連結的 VFS 層程式碼根本不需要呼叫此介面。
“核心寫入頁面快取頁面”這一短語特指核心執行儲存指令,在頁面的核心虛擬對映處弄髒該頁面中的資料。在這裡重新整理很重要,以處理 D-cache 別名,確保這些核心儲存對於該頁面的使用者空間對映可見。
推論情況同樣重要,如果存在對該檔案有共享+可寫對映的使用者,我們必須確保核心讀取這些頁面時會看到使用者最近的儲存。
如果 D-cache 別名不是問題,則此例程在該架構上可以簡單地定義為 nop。
folio->flags (PG_arch_1) 中有一個位被設定為“架構私有”。核心保證,對於頁快取頁面,當這樣的頁面首次進入頁快取時,它會清除此位。
這使得這些介面的實現效率更高。它允許“延遲”(可能無限期)實際重新整理,如果當前沒有使用者程序對映此頁面。請參閱 sparc64 的 flush_dcache_folio 和 update_mmu_cache_range 實現,瞭解如何執行此操作的示例。
想法是,首先在 flush_dcache_folio() 時間,如果
folio_flush_mapping()返回一個對映,並且在該對映上呼叫 mapping_mapped() 返回 %false,則只需標記架構私有頁面標誌位。稍後,在 update_mmu_cache_range() 中,會檢查此標誌位,如果設定,則執行重新整理並清除標誌位。重要提示
如果您延遲重新整理,通常重要的是實際刷新發生在與導致頁面髒的 CPU 儲存相同的 CPU 上。同樣,請參閱 sparc64,瞭解如何處理此問題的示例。
void copy_to_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)void copy_from_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)當核心需要將任意資料複製進出任意使用者頁面時(例如用於 ptrace()),它將使用這兩個例程。
任何必要的快取重新整理或其他一致性操作都應在此處發生。如果處理器的指令快取不監聽 CPU 儲存,則很可能您需要為 copy_to_user_page() 重新整理指令快取。
void flush_anon_page(struct vm_area_struct *vma, struct page *page, unsigned long vmaddr)當核心需要訪問匿名頁面的內容時,它會呼叫此函式(目前僅限 get_user_pages())。注意:flush_dcache_folio() 故意不適用於匿名頁面。預設實現是一個 nop(並且對於所有一致性架構都應保持如此)。對於非一致性架構,它應重新整理 vmaddr 處頁面的快取。
void flush_icache_range(unsigned long start, unsigned long end)當核心儲存到它將執行的地址中時(例如載入模組時),會呼叫此函式。
如果 I-cache 不監聽儲存,則此例程需要重新整理它。
void flush_icache_page(struct vm_area_struct *vma, struct page 板塊)flush_icache_page 的所有功能都可以在 flush_dcache_folio 和 update_mmu_cache_range 中實現。未來,希望完全移除此介面。
最後一類 API 是用於核心內部故意別名地址範圍的 I/O。此類別名透過使用 vmap/vmalloc API 進行設定。由於核心 I/O 透過物理頁面進行,I/O 子系統假定使用者對映和核心偏移對映是唯一的別名。對於 vmap 別名來說並非如此,因此核心中任何嘗試對 vmap 區域進行 I/O 的程式碼都必須手動管理一致性。它必須透過在執行 I/O 之前重新整理 vmap 範圍並在 I/O 返回後使其失效來完成此操作。
void flush_kernel_vmap_range(void *vaddr, int size)重新整理 vmap 區域中給定虛擬地址範圍的核心快取。這是為了確保核心在 vmap 範圍中修改的任何資料對物理頁面可見。設計旨在使此區域安全地執行 I/O。請注意,此 API _不_同時重新整理區域的偏移對映別名。
void invalidate_kernel_vmap_range(void *vaddr, int size) invalidate使 vmap 區域中給定虛擬地址範圍的快取失效,這可防止處理器在對物理頁面進行 I/O 時透過推測性讀取資料而使快取過期。這僅適用於資料讀取到 vmap 區域。