程序地址

使用者空間記憶體範圍由核心透過虛擬記憶體區域或 ‘VMA’ 的型別 struct vm_area_struct 跟蹤。

每個 VMA 描述了一個具有相同屬性的虛擬連續記憶體範圍,每個範圍由一個 struct vm_area_struct 物件描述。 除了相鄰的堆疊 VMA 可以擴充套件到包含訪問的地址的情況外,對 VMA 之外的使用者空間訪問是無效的。

所有 VMA 都包含在一個且僅一個虛擬地址空間中,該空間由 struct mm_struct 物件描述,該物件被所有共享虛擬地址空間的任務(即執行緒)引用。 我們將其稱為 mm

每個 mm 物件都包含一個 maple 樹資料結構,該結構描述了虛擬地址空間中的所有 VMA。

注意

此規則的一個例外是 ‘gate’ VMA,它由使用 vsyscall 的體系結構提供,並且是一個不屬於任何特定 mm 的全域性靜態物件。

鎖定

核心被設計為針對 VMA 元資料 的併發讀取操作具有高度可擴充套件性,因此需要一組複雜的鎖來確保不會發生記憶體損壞。

注意

鎖定 VMA 的元資料不會對其描述的記憶體或對映它們的頁表產生任何影響。

術語

  • mmap 鎖 - 每個 MM 都有一個讀/寫訊號量 mmap_lock,它在程序地址空間粒度上鎖定,可以透過 mmap_read_lock()mmap_write_lock() 及其變體獲取。

  • VMA 鎖 - VMA 鎖位於 VMA 粒度(當然),在實踐中充當讀/寫訊號量。 VMA 讀鎖透過 lock_vma_under_rcu() 獲取(並透過 vma_end_read() 解鎖),寫鎖透過 vma_start_write() 獲取(所有 VMA 寫鎖在釋放 mmap 寫鎖時自動解鎖)。 要獲取 VMA 寫鎖,您必須已經獲取了 mmap_write_lock()

  • rmap 鎖 - 當嘗試透過反向對映經由 struct address_spacestruct anon_vma 物件(可從 folio 透過 folio->mapping 訪問)訪問 VMA 時。 VMA 必須透過 anon_vma_[try]lock_read()anon_vma_[try]lock_write()(對於匿名記憶體)和 i_mmap_[try]lock_read()i_mmap_[try]lock_write()(對於檔案支援的記憶體)進行穩定。 我們將這些鎖稱為反向對映鎖,或簡稱 ‘rmap 鎖’。

我們將在下面的專用部分中單獨討論頁表鎖。

這些鎖中的任何一個要實現的第一個目標是穩定 MM 樹中的 VMA。 也就是說,保證 VMA 物件不會在您不知情的情況下被刪除或修改(除了下面描述的一些特定欄位)。

穩定 VMA 還會保留它描述的地址空間。

鎖的使用

如果您想讀取 VMA 元資料欄位或只是保持 VMA 穩定,則必須執行以下操作之一

  • 透過 mmap_read_lock()(或合適的變體)在 MM 粒度上獲取 mmap 讀鎖,並在處理完 VMA 後使用匹配的 mmap_read_unlock() 解鎖,或者

  • 嘗試透過 lock_vma_under_rcu() 獲取 VMA 讀鎖。 這會嘗試原子地獲取鎖,因此可能會失敗,在這種情況下,需要回退邏輯來獲取 mmap 讀鎖(如果它返回 NULL),或者

  • 在遍歷鎖定的間隔樹(無論是匿名的還是檔案支援的)以獲取所需的 VMA 之前,獲取 rmap 鎖。

如果您想寫入 VMA 元資料欄位,那麼情況會有所不同,具體取決於欄位(我們將在下面詳細探討每個 VMA 欄位)。 對於大多數,您必須

  • 透過 mmap_write_lock()(或合適的變體)在 MM 粒度上獲取 mmap 寫鎖,並在處理完 VMA 後使用匹配的 mmap_write_unlock() 解鎖,並且

  • 透過 vma_start_write() 為您希望修改的每個 VMA 獲取 VMA 寫鎖,該鎖將在呼叫 mmap_write_unlock() 時自動釋放。

  • 如果您想能夠寫入任何欄位,您還必須透過獲取 rmap 寫鎖來隱藏來自反向對映的 VMA。

VMA 鎖很特殊,您必須首先獲取 mmap 鎖才能獲取 VMA 鎖。 但是,可以在沒有任何其他鎖的情況下獲取 VMA 鎖(lock_vma_under_rcu() 將獲取然後釋放 RCU 鎖以查詢 VMA)。

這限制了寫入器對讀取器的影響,因為寫入器可以與一個 VMA 互動,而讀取器可以同時與另一個 VMA 互動。

注意

VMA 讀鎖的主要使用者是頁面錯誤處理程式,這意味著如果沒有 VMA 寫鎖,頁面錯誤將與您正在執行的任何操作同時執行。

檢查所有有效的鎖定狀態

mmap 鎖

VMA 鎖

rmap 鎖

穩定?

讀取?

寫入大多數?

寫入全部?

-

-

-

-

R

-

-

-

R/W

R/W

-/R

-/R/W

W

W

-/R

W

W

W

警告

雖然可以在持有 mmap 讀鎖的同時獲取 VMA 鎖,但嘗試反向操作是無效的,因為它可能導致死鎖 - 如果另一個任務已經持有 mmap 寫鎖並嘗試獲取 VMA 寫鎖,則會在 VMA 讀鎖上死鎖。

實際上,所有這些鎖都充當讀/寫訊號量,因此您可以為每個鎖獲取讀鎖或寫鎖。

注意

一般來說,讀/寫訊號量是一類允許併發讀取器的鎖。 但是,只有在所有讀取器都離開臨界區之後才能獲取寫鎖(並且掛起的讀取器會被強制等待)。

這使得讀/寫訊號量上的讀鎖與其他讀取器併發,而寫鎖則排斥所有其他持有訊號量的人。

VMA 欄位

我們可以按其用途細分 struct vm_area_struct 欄位,這使得探索其鎖定特性更容易

注意

我們在此處排除 VMA 鎖特定的欄位以避免混淆,因為這些實際上是內部實現細節。

虛擬佈局欄位

欄位

描述

寫鎖

vm_start

VMA 描述的範圍的包含性起始虛擬地址。

mmap 寫,VMA 寫,rmap 寫。

vm_end

VMA 描述的範圍的互斥結束虛擬地址。

mmap 寫,VMA 寫,rmap 寫。

vm_pgoff

描述檔案中的頁面偏移量、虛擬地址空間中的原始頁面偏移量(在任何 mremap() 之前)或 PFN(如果是 PFN 對映且體系結構不支援 CONFIG_ARCH_HAS_PTE_SPECIAL)。

mmap 寫,VMA 寫,rmap 寫。

這些欄位描述了 VMA 的大小、起始和結束位置,因此必須先將其從反向對映中隱藏才能進行修改,因為這些欄位用於在反向對映間隔樹中定位 VMA。

核心欄位

欄位

描述

寫鎖

vm_mm

包含 mm_struct。

無 - 在初始對映時寫入一次。

vm_page_prot

從 VMA 標誌確定的特定於體系結構的頁表保護位。

mmap 寫,VMA 寫。

vm_flags

對 VMA 標誌的只讀訪問,這些標誌描述 VMA 的屬性,與私有可寫 __vm_flags 聯合。

不適用

__vm_flags

對 VMA 標誌欄位的私有可寫訪問,由 vm_flags_*() 函式更新。

mmap 寫,VMA 寫。

vm_file

如果 VMA 是檔案支援的,則指向描述底層檔案的 struct file 物件,如果是匿名的,則為 NULL

無 - 在初始對映時寫入一次。

vm_ops

如果 VMA 是檔案支援的,則驅動程式或檔案系統會提供一個 struct vm_operations_struct 物件,該物件描述要在 VMA 生命週期事件中呼叫的回撥。

無 - 由 f_ops->mmap() 在初始對映時寫入一次。

vm_private_data

一個 void * 欄位,用於驅動程式特定的元資料。

由驅動程式處理。

這些是描述 VMA 所屬的 MM 及其屬性的核心欄位。

特定於配置的欄位

欄位

配置選項

描述

寫鎖

anon_name

CONFIG_ANON_VMA_NAME

用於儲存 struct anon_vma_name 物件的欄位,該物件為匿名對映提供名稱,如果未設定名稱或 VMA 是檔案支援的,則為 NULL。 底層物件是引用計數的,可以跨多個 VMA 共享以實現可擴充套件性。

mmap 寫,VMA 寫。

swap_readahead_info

CONFIG_SWAP

交換機制用於執行預讀的元資料。 此欄位是原子訪問的。

mmap 讀,交換特定的鎖。

vm_policy

CONFIG_NUMA

mempolicy 物件,描述 VMA 的 NUMA 行為。 底層物件是引用計數的。

mmap 寫,VMA 寫。

numab_state

CONFIG_NUMA_BALANCING

vma_numab_state 物件,描述 NUMA 平衡與此 VMA 相關的當前狀態。 由 task_numa_work() 在 mmap 讀鎖下更新。

mmap 讀,numab 特定的鎖。

vm_userfaultfd_ctx

CONFIG_USERFAULTFD

型別為 vm_userfaultfd_ctx 的 Userfaultfd 上下文包裝器物件,如果 userfaultfd 被停用,則為零大小,或者包含指向底層 userfaultfd_ctx 物件的指標,該物件描述 userfaultfd 元資料。

mmap 寫,VMA 寫。

這些欄位是否存在取決於是否設定了相關的核心配置選項。

反向對映欄位

欄位

描述

寫鎖

shared.rb

一個紅/黑樹節點,如果對映是檔案支援的,則用於將 VMA 放置在 struct address_space->i_mmap 紅/黑間隔樹中。

mmap 寫,VMA 寫,i_mmap 寫。

shared.rb_subtree_last

用於管理間隔樹的元資料(如果 VMA 是檔案支援的)。

mmap 寫,VMA 寫,i_mmap 寫。

anon_vma_chain

指向 fork/CoW 的 anon_vma 物件和 vma->anon_vma 的指標列表(如果它是非 NULL)。

mmap 讀,anon_vma 寫。

anon_vma

anon_vma 物件,由專門對映到此 VMA 的匿名 folio 使用。 最初由 anon_vma_prepare() 設定,由 page_table_lock 序列化。 這會在任何頁面被分頁到記憶體中後立即設定。

NULL 且設定為非 NULL 時:mmap 讀,page_table_lock。

當非 NULL 且設定為 NULL 時:mmap 寫,VMA 寫,anon_vma 寫。

這些欄位用於將 VMA 放置在反向對映中,以及對於匿名對映,能夠訪問相關的 struct anon_vma 物件和 struct anon_vma,其中專門對映到此 VMA 的 folio 應駐留。

注意

如果使用 MAP_PRIVATE 設定對映檔案支援的對映,則它可以同時位於 anon_vmai_mmap 樹中,因此可以一次性使用所有這些欄位。

頁表

我們不會詳盡地討論該主題,但廣義地說,頁表透過一系列頁表將虛擬地址對映到物理地址,每個頁表都包含具有下一頁表級別的物理地址的條目(以及標誌),並且在葉級別包含底層物理資料頁面的物理地址或特殊條目(例如交換條目、遷移條目或其他特殊標記)。 這些頁面的偏移量由虛擬地址本身提供。

在 Linux 中,這些分為五個級別 - PGD、P4D、PUD、PMD 和 PTE。 大頁面可能會消除其中一個或兩個級別,但如果出現這種情況,我們通常將葉級別稱為 PTE 級別。

注意

如果體系結構支援的頁表少於五個,則核心會巧妙地“摺疊”頁表級別,即存根與跳過的級別相關的函式。 這使我們可以在概念上表現得好像總是有五個級別,即使編譯器實際上可能會消除與丟失的級別相關的任何程式碼。

通常對頁表執行四個關鍵操作

  1. 遍歷頁表 - 只需讀取頁表即可遍歷它們。 這隻需要保持 VMA 穩定,因此建立此狀態的鎖足以進行遍歷(還有無鎖變體,甚至消除了此要求,例如 gup_fast())。

  2. 安裝頁表對映 - 無論是建立新對映還是以更改其身份的方式修改現有對映。 這要求透過 mmap 或 VMA 鎖(顯式地不是 rmap 鎖)保持 VMA 穩定。

  3. Zapping/取消對映頁表條目 - 這是核心在僅清除葉級別的頁表對映,同時保留所有頁表的情況下呼叫的操作。 這是核心中非常常見的操作,在檔案截斷、透過 madvise()MADV_DONTNEED 操作以及其他操作中執行。 這是透過多個函式執行的,包括 unmap_mapping_range()unmap_mapping_pages()。 只需要為該操作保持 VMA 穩定。

  4. 釋放頁表 - 當核心最終從使用者空間程序中刪除頁表時(通常透過 free_pgtables()),必須格外小心以確保安全地完成此操作,因為此邏輯最終會釋放指定範圍內的所有頁表,而忽略現有的葉條目(它假定呼叫者已 zap 了該範圍並阻止了其中的任何進一步的故障或修改)。

注意

在 rmap 鎖下執行用於回收或遷移的對映修改,因為它像 zapping 一樣,不會從根本上修改正在對映的內容的標識。

可以持有上面術語部分中描述的任何鎖(即 mmap 鎖、VMA 鎖或任何反向對映鎖)來執行遍歷zapping 範圍。

也就是說 - 只要您保持相關 VMA 穩定 - 您就可以繼續對頁表執行這些操作(儘管在內部,執行寫入的核心操作也會獲取內部頁表鎖以進行序列化 - 有關更多詳細資訊,請參見頁表實現細節部分)。

安裝頁表條目時,必須持有 mmap 或 VMA 鎖以保持 VMA 穩定。 我們將在下面的頁表鎖定細節部分中探討為什麼會這樣。

警告

通常僅在 VMA 覆蓋的區域中遍歷頁表。 如果您想在可能未被 VMA 覆蓋的區域中遍歷頁表,則需要更重的鎖定。 有關詳細資訊,請參見 walk_page_range_novma()

釋放頁表完全是內部記憶體管理操作,具有特殊要求(有關更多詳細資訊,請參見下面的頁面釋放部分)。

警告

釋放頁表時,必須無法透過反向對映訪問包含這些頁表對映到的範圍的 VMA。

free_pgtables() 函式從反向對映中刪除相關的 VMA,但不允許訪問任何其他 VMA 並跨越指定的範圍。

鎖的順序

由於我們在核心中有多個鎖,這些鎖可能會也可能不會與顯式 mm 或 VMA 鎖同時獲取,因此我們必須注意鎖反轉,並且獲取和釋放鎖的順序變得非常重要。

注意

當兩個執行緒需要獲取多個鎖時,但這樣做會無意中導致相互死鎖時,會發生鎖反轉。

例如,考慮執行緒 1 持有鎖 A 並嘗試獲取鎖 B,而執行緒 2 持有鎖 B 並嘗試獲取鎖 A。

現在兩個執行緒都相互死鎖了。 但是,如果他們嘗試以相同的順序獲取鎖,則一個執行緒會等待另一個執行緒完成其工作,並且不會發生死鎖。

mm/rmap.c 中的開頭註釋詳細描述了記憶體管理程式碼中所需的鎖的順序

inode->i_rwsem        (while writing or truncating, not reading or faulting)
  mm->mmap_lock
    mapping->invalidate_lock (in filemap_fault)
      folio_lock
        hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share, see hugetlbfs below)
          vma_start_write
            mapping->i_mmap_rwsem
              anon_vma->rwsem
                mm->page_table_lock or pte_lock
                  swap_lock (in swap_duplicate, swap_info_get)
                    mmlist_lock (in mmput, drain_mmlist and others)
                    mapping->private_lock (in block_dirty_folio)
                        i_pages lock (widely used)
                          lruvec->lru_lock (in folio_lruvec_lock_irq)
                    inode->i_lock (in set_page_dirty's __mark_inode_dirty)
                    bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty)
                      sb_lock (within inode_lock in fs/fs-writeback.c)
                      i_pages lock (widely used, in set_page_dirty,
                                in arch-dependent flush_dcache_mmap_lock,
                                within bdi.wb->list_lock in __sync_single_inode)

mm/filemap.c 的頂部也有一個特定於檔案系統的鎖排序註釋

->i_mmap_rwsem                        (truncate_pagecache)
  ->private_lock                      (__free_pte->block_dirty_folio)
    ->swap_lock                       (exclusive_swap_page, others)
      ->i_pages lock

->i_rwsem
  ->invalidate_lock                   (acquired by fs in truncate path)
    ->i_mmap_rwsem                    (truncate->unmap_mapping_range)

->mmap_lock
  ->i_mmap_rwsem
    ->page_table_lock or pte_lock     (various, mainly in memory.c)
      ->i_pages lock                  (arch-dependent flush_dcache_mmap_lock)

->mmap_lock
  ->invalidate_lock                   (filemap_fault)
    ->lock_page                       (filemap_fault, access_process_vm)

->i_rwsem                             (generic_perform_write)
  ->mmap_lock                         (fault_in_readable->do_page_fault)

bdi->wb.list_lock
  sb_lock                             (fs/fs-writeback.c)
  ->i_pages lock                      (__sync_single_inode)

->i_mmap_rwsem
  ->anon_vma.lock                     (vma_merge)

->anon_vma.lock
  ->page_table_lock or pte_lock       (anon_vma_prepare and various)

->page_table_lock or pte_lock
  ->swap_lock                         (try_to_unmap_one)
  ->private_lock                      (try_to_unmap_one)
  ->i_pages lock                      (try_to_unmap_one)
  ->lruvec->lru_lock                  (follow_page_mask->mark_page_accessed)
  ->lruvec->lru_lock                  (check_pte_range->folio_isolate_lru)
  ->private_lock                      (folio_remove_rmap_pte->set_page_dirty)
  ->i_pages lock                      (folio_remove_rmap_pte->set_page_dirty)
  bdi.wb->list_lock                   (folio_remove_rmap_pte->set_page_dirty)
  ->inode->i_lock                     (folio_remove_rmap_pte->set_page_dirty)
  bdi.wb->list_lock                   (zap_pte_range->set_page_dirty)
  ->inode->i_lock                     (zap_pte_range->set_page_dirty)
  ->private_lock                      (zap_pte_range->block_dirty_folio)

請檢查這些註釋的當前狀態,自本文件編寫之時起可能已更改。

鎖定實現細節

警告

PTE 級別的頁表的鎖定規則與其他級別的頁表的鎖定規則截然不同。

頁表鎖定細節

除了上面術語部分中描述的鎖之外,我們還有專用於頁表的其他鎖

  • 更高級別的頁表鎖 - 更高級別的頁表(即 PGD、P4D 和 PUD)在修改時都使用程序地址空間粒度的 mm->page_table_lock 鎖。

  • 細粒度的頁表鎖 - PMD 和 PTE 各自都有細粒度的鎖,這些鎖要麼儲存在描述頁表的 folio 中,要麼分配並由 folio 指向(如果設定了 ALLOC_SPLIT_PTLOCKS)。 PMD 自旋鎖透過 pmd_lock() 獲取,但是 PTE 被對映到更高的記憶體中(如果是 32 位系統),並透過 pte_offset_map_lock() 小心鎖定。

這些鎖代表與每個頁表級別互動所需的最低要求,但還有其他要求。

重要的是,請注意,在頁表的遍歷中,有時不會獲取此類鎖。 但是,在 PTE 級別,至少必須防止併發頁表刪除(使用 RCU),並且頁表必須對映到高記憶體中,請參見下文。

是否謹慎讀取頁表條目取決於體系結構,請參見下面的原子性部分。

鎖定規則

我們建立了與頁表互動的基本鎖定規則

  • 更改頁表條目時,必須持有該頁表的頁表鎖,除非您可以安全地假設沒有人可以併發訪問頁表(例如,在呼叫 free_pgtables() 時)。

  • 對頁表條目的讀取和寫入必須適當地具有原子性。 有關詳細資訊,請參見下面的原子性部分。

  • 填充先前為空的條目需要持有 mmap 或 VMA 鎖(讀取或寫入),僅使用 rmap 鎖這樣做會很危險(請參見下面的警告)。

  • 如前所述,可以在保持 VMA 穩定的同時執行 Zapping,即持有 mmap 鎖、VMA 鎖或任何 rmap 鎖。

警告

填充先前為空的條目是危險的,因為在取消對映 VMA 時,vms_clear_ptes() 在 zapping(透過 unmap_vmas())和釋放頁表(透過 free_pgtables())之間存在一個時間視窗,在此期間 VMA 仍然在 rmap 樹中可見。free_pgtables() 假定 zap 已執行並無條件地刪除 PTE(以及已釋放範圍內的所有其他頁表),因此安裝新的 PTE 條目可能會洩漏記憶體,並導致其他意外和危險的行為。

移動頁表時還有其他適用規則,我們將在下面有關此主題的部分中進行討論。

PTE 級別的頁表與其他級別的頁表不同,並且訪問它們還有其他要求

  • 在 32 位體系結構上,它們可能位於高記憶體中(這意味著需要將它們對映到核心記憶體中才能訪問)。

  • 當為空時,可以在保持用於讀取的 mmap 鎖或 rmap 鎖以及 PTE 和 PMD 頁表鎖的情況下取消連結並 RCU 釋放它們。 特別是,當處理 MADV_COLLAPSE 時,這種情況發生在 retract_page_tables() 中。 因此,訪問 PTE 級別的頁表至少需要持有 RCU 讀鎖; 但這僅足以讓可以容忍與併發頁表更新競爭的讀取器觀察到空的 PTE(在實際上已經分離並標記為 RCU 釋放的頁表中),而另一個新的頁表已安裝在同一位置並填充了條目。 寫入器通常需要獲取 PTE 鎖並重新驗證 PMD 條目是否仍然引用同一 PTE 級別的頁表。 如果寫入器不在乎是否是同一 PTE 級別的頁表,則它可以獲取 PMD 鎖並重新驗證 pmd 條目的內容是否仍然滿足要求。 特別是,當處理 MADV_COLLAPSE 時,這種情況也發生在 retract_page_tables() 中。

要訪問 PTE 級別的頁表,可以使用諸如 pte_offset_map_lock()pte_offset_map() 之類的輔助函式,具體取決於穩定性要求。 如果需要,這些函式會將頁表對映到核心記憶體中,獲取 RCU 鎖,並且根據變體,還可以查詢或獲取 PTE 鎖。 請參見 __pte_offset_map_lock() 上的註釋。

原子性

無論頁表鎖如何,MMU 硬體都會併發更新訪問位和髒位(也許更多,具體取決於體系結構)。 此外,並行執行的頁表遍歷操作(儘管保持 VMA 穩定)和諸如 GUP-fast 之類的功能會無鎖地遍歷(即讀取)頁表,甚至根本不保持 VMA 穩定。

當執行頁表遍歷並保持 VMA 穩定時,是否必須執行一次且僅執行一次讀取取決於體系結構(例如,x86-64 不需要任何特殊預防措施)。

如果要執行寫入,或者如果讀取通知是否發生寫入(例如,在 __pud_install() 中安裝頁表條目時),則必須始終格外小心。 在這些情況下,我們永遠不能假設頁表鎖會賦予我們完全獨佔的訪問許可權,並且必須只獲取頁表條目一次。

如果我們要讀取頁表條目,那麼我們只需要確保編譯器不會重新排列我們的載入。 這是透過 pXXp_get() 函式實現的 - pgdp_get()p4dp_get()pudp_get()pmdp_get()ptep_get()

每個函式都使用 READ_ONCE() 來保證編譯器僅讀取一次頁表條目。

但是,如果我們希望操作現有的頁表條目並關心先前儲存的資料,則我們必須走得更遠,並使用硬體原子操作,例如,在 ptep_get_and_clear() 中。

同樣,不依賴於保持 VMA 穩定的操作(例如 GUP-fast(請參見 gup_fast() 及其各種頁表級別處理程式,例如 gup_fast_pte_range()))必須非常小心地與頁表條目互動,使用諸如 ptep_get_lockless() 之類的函式以及更高級別的頁表級別的等效函式。

對頁表條目的寫入也必須是適當原子的,如 set_pXX() 函式所建立的 - set_pgd()set_p4d()set_pud()set_pmd()set_pte()

同樣,清除頁表條目的函式也必須是適當原子的,如 pXX_clear() 函式中所述 - pgd_clear()p4d_clear()pud_clear()pmd_clear()pte_clear()

頁表安裝

頁表安裝是在讀寫模式下由 mmap 或 VMA 鎖顯式保持 VMA 穩定的情況下執行的(有關詳細資訊,請參見鎖定規則部分中的警告,以瞭解原因)。

當分配 P4D、PUD 或 PMD 並在上面的 PGD、P4D 或 PUD 中設定相關條目時,必須持有 mm->page_table_lock。 這是分別在 __p4d_alloc()__pud_alloc()__pmd_alloc() 中獲取的。

注意

__pmd_alloc() 實際上依次呼叫 pud_lock()pud_lockptr(),但是在編寫本文時,它最終引用 mm->page_table_lock

分配 PTE 將使用 mm->page_table_lock,或者,如果定義了 USE_SPLIT_PMD_PTLOCKS,則使用嵌入在 PMD 物理頁面元資料中的鎖,以 struct ptdesc 的形式,透過從 pmd_lock() 呼叫的 pmd_ptdesc() 和最終的 __pte_alloc() 獲取。

最後,修改 PTE 的內容需要特殊處理,因為每當我們想要穩定和獨佔地訪問 PTE 中包含的條目時,都必須獲取 PTE 頁表鎖,尤其是在我們希望修改它們時。

這是透過 pte_offset_map_lock() 執行的,它仔細檢查以確保 PTE 沒有在我們不知情的情況下發生更改,最終呼叫 pte_lockptr() 以獲取 PTE 粒度的自旋鎖,該自旋鎖包含在與物理 PTE 頁面關聯的 struct ptdesc 中。該鎖必須透過 pte_unmap_unlock() 釋放。

注意

對此有一些變體,例如 pte_offset_map_rw_nolock(),當我們知道我們持有穩定的 PTE 時,但為了簡潔起見,我們不對此進行探討。有關更多詳細資訊,請參閱 __pte_offset_map_lock() 的註釋。

當修改範圍中的資料時,我們通常只希望在必要時分配更高的頁表,使用這些鎖來避免競爭或覆蓋任何內容,並根據需要設定/清除 PTE 級別的資料(例如,在頁面錯誤或 zapping 時)。

在遍歷頁表條目以安裝新對映時,通常採用的模式是樂觀地確定上面表中的頁表條目是否為空,如果是,則僅在獲取頁表鎖並再次檢查以檢視它是否在我們不知情的情況下被分配。

這允許在僅在需要時才獲取頁表鎖的情況下進行遍歷。一個例子是 __pud_alloc()

在葉頁表(即 PTE)上,我們不能完全依賴此模式,因為我們有單獨的 PMD 和 PTE 鎖,並且例如 THP 摺疊可能已經從我們不知情的情況下消除了 PMD 條目以及 PTE。

這就是為什麼 __pte_offset_map_lock() 在獲取特定於 PTE 的鎖之前,以無鎖方式檢索 PTE 的 PMD 條目,仔細檢查它是否如預期,然後再次檢查 PMD 條目是否如預期。

如果發生 THP 摺疊(或類似情況),則會獲取兩個頁面上的鎖,因此我們可以確保在持有 PTE 鎖時防止這種情況。

以這種方式安裝條目可確保寫入時的互斥。

頁表釋放

拆除頁表本身是一件需要非常小心的事情。必須確保併發任務無法遍歷或引用指定要刪除的頁表。

僅僅持有 mmap 寫鎖和 VMA 鎖(這將防止競爭性錯誤和 rmap 操作)是不夠的,因為檔案支援的對映可能會在 struct address_space->i_mmap_rwsem 下被截斷。

因此,沒有可以透過反向對映訪問的 VMA(透過 struct anon_vma->rb_rootstruct address_space->i_mmap 間隔樹),可以拆除其頁表。

該操作通常透過 free_pgtables() 執行,該函式假定已獲取 mmap 寫鎖(如其 mm_wr_locked 引數所指定),或者 VMA 已經是不可訪問的。

它小心地從所有反向對映中刪除 VMA,但重要的是,沒有新的對映與這些對映重疊,也沒有任何路由保留以允許訪問正在拆除其頁表的範圍內的地址。

此外,它假定已經執行了 zap 操作,並且已經採取了措施來確保在 zap 和 free_pgtables() 的呼叫之間不會安裝任何進一步的頁表條目。

由於假定已採取所有此類步驟,因此在沒有頁表鎖的情況下清除頁表條目(在 pgd_clear()p4d_clear()pud_clear()pmd_clear() 函式中)。

注意

可以獨立於其上方的頁表拆除葉頁表,如 retract_page_tables() 所做的那樣,該函式在 i_mmap 讀鎖、PMD 和 PTE 頁表鎖下執行,而沒有這種程度的謹慎。

頁表移動

一些函式操作 PMD 以上的頁表級別(即 PUD、P4D 和 PGD 頁表)。其中最著名的是 mremap(),它可以移動更高級別的頁表。

在這些情況下,需要獲取所有鎖,即 mmap 鎖、VMA 鎖和相關的 rmap 鎖。

您可以在 mremap() 實現中觀察到這一點,在函式 take_rmap_locks()drop_rmap_locks() 中,它們執行鎖獲取的 rmap 方面,最終由 move_page_tables() 呼叫。

VMA 鎖內部

概述

VMA 讀鎖完全是樂觀的 - 如果鎖被爭用或競爭性寫入已開始,那麼我們不會獲得讀鎖。

VMA 鎖透過 lock_vma_under_rcu() 獲得,它首先呼叫 rcu_read_lock() 以確保在 RCU 臨界區中查詢 VMA,然後嘗試透過 vma_start_read() 鎖定它,然後在透過 rcu_read_unlock() 釋放 RCU 鎖之前。

在使用者已經持有 mmap 讀鎖的情況下,可以使用 vma_start_read_locked()vma_start_read_locked_nested()。這些函式不會因鎖爭用而失敗,但呼叫者仍應檢查它們的返回值,以防它們因其他原因而失敗。

VMA 讀鎖在其持續時間內遞增 vma.vm_refcnt 引用計數器,並且 lock_vma_under_rcu() 的呼叫者必須透過 vma_end_read() 釋放它。

VMA 鎖透過 vma_start_write() 獲得,在 VMA 即將被修改的情況下,與 vma_start_read() 不同,該鎖總是被獲取。mmap 寫鎖必須在 VMA 寫鎖的持續時間內持有,釋放或降級 mmap 寫鎖也會釋放 VMA 寫鎖,因此沒有 vma_end_write() 函式。

請注意,當寫鎖定 VMA 鎖時,vma.vm_refcnt 會被臨時修改,以便讀者可以檢測到寫作者的存在。一旦用於序列化的 vma 序列號更新,引用計數器就會恢復。

這確保了我們需要的語義 - VMA 寫鎖提供對 VMA 的獨佔寫訪問。

實現細節

VMA 鎖機制旨在成為避免使用高度爭用的 mmap 鎖的輕量級手段。它是使用屬於包含 struct mm_struct 和 VMA 的引用計數器和序列號的組合來實現的。

讀鎖透過 vma_start_read() 獲得,這是一個樂觀的操作,即它嘗試獲取讀鎖,但如果無法獲取則返回 false。在讀取操作結束時,呼叫 vma_end_read() 以釋放 VMA 讀鎖。

呼叫 vma_start_read() 需要首先呼叫 rcu_read_lock(),以確保我們在 VMA 讀鎖獲取時處於 RCU 臨界區中。一旦獲取,RCU 鎖可以被釋放,因為它僅用於查詢。這由 lock_vma_under_rcu() 抽象出來,它是使用者應該使用的介面。

寫入需要 mmap 被寫鎖定,並且 VMA 鎖透過 vma_start_write() 獲得,但是寫鎖透過 mmap 寫鎖的終止或降級來釋放,因此不需要 vma_end_write()

所有這些都是透過使用 per-mm 和 per-VMA 序列計數來實現的,這些序列計數用於降低複雜性,特別是對於一次寫鎖定多個 VMA 的操作。

如果 mm 序列計數 mm->mm_lock_seq 等於 VMA 序列計數 vma->vm_lock_seq,則 VMA 被寫鎖定。如果它們不同,則不是。

每次在 mmap_write_unlock()mmap_write_downgrade() 中釋放 mmap 寫鎖時,都會呼叫 vma_end_write_all(),它還透過 mm_lock_seqcount_end() 遞增 mm->mm_lock_seq

這樣,我們確保,無論 VMA 的序列號如何,都不會錯誤地指示寫鎖,並且當我們釋放 mmap 寫鎖時,我們會有效地同時釋放 mmap 中包含的所有 VMA 寫鎖。

由於 mmap 寫鎖與其他持有者互斥,因此在其釋放時自動釋放任何 VMA 鎖是有意義的,因為您永遠不會希望在完全獨立的寫入操作中保持 VMA 鎖定。它還保持了正確的鎖排序。

每次獲取 VMA 讀鎖時,我們遞增 vma.vm_refcnt 引用計數器,並檢查 VMA 的序列計數是否與 mm 的序列計數不匹配。

如果匹配,則讀鎖失敗並刪除 vma.vm_refcnt。如果不匹配,我們保持引用計數器升高,排除寫作者,但允許其他讀者,他們也可以在 RCU 下獲得此鎖。

重要的是,在 lock_vma_under_rcu() 中執行的 maple 樹操作也是 RCU 安全的,因此保證整個讀取鎖定操作能夠正常執行。

在寫入方面,我們在 vma.vm_refcnt 中設定一個位,讀者無法修改該位,並等待所有讀者刪除其引用計數。一旦沒有讀者,VMA 的序列號就會設定為與 mm 的序列號匹配。在整個操作過程中,mmap 寫鎖被持有。

這樣,如果有任何讀鎖生效,vma_start_write() 將休眠直到這些完成並實現互斥。

在設定 VMA 的序列號後,清除 vma.vm_refcnt 中指示寫作者的位。從此刻開始,VMA 的序列號將指示 VMA 的寫鎖定狀態,直到 mmap 寫鎖被刪除或降級。

引用計數器和序列計數的這種巧妙組合允許基於 RCU 的快速 per-VMA 鎖獲取(尤其是在頁面錯誤時,但在其他地方也被利用),並且鎖排序的複雜性最小。

mmap 寫鎖降級

當持有 mmap 寫鎖時,一個人對 mmap 中的資源具有獨佔訪問權(通常需要 VMA 寫鎖才能避免與持有 VMA 讀鎖的任務發生競爭)。

然後可以透過 mmap_write_downgrade() 從寫鎖降級到讀鎖,它類似於 mmap_write_unlock(),隱式地透過 vma_end_write_all() 終止所有 VMA 寫鎖,但重要的是在降級時不會放棄 mmap 鎖,因此保持鎖定的虛擬地址空間穩定。

一個有趣的後果是,降級的鎖與其他擁有降級鎖的任務互斥(因為競爭任務必須首先獲取寫鎖才能降級它,並且降級的鎖會阻止在原始鎖釋放之前獲得新的寫鎖)。

為了清楚起見,我們將讀 (R)/降級寫 (D)/寫 (W) 鎖相互對映,顯示哪些鎖排除其他鎖

鎖互斥性

R

D

W

R

D

W

此處,Y 表示匹配的行/列中的鎖是互斥的,N 表示它們不是互斥的。

堆疊擴充套件

堆疊擴充套件丟擲了額外的複雜性,因為我們不允許存在競爭性頁面錯誤,因此我們在 expand_downwards()expand_upwards() 中呼叫 vma_start_write() 以防止這種情況。