
switch_mm是進程切換context_switch中負責切換虛擬內存地址空間的核心函數。它的本質工作就是把新進程的頁表基地址加載到硬件寄存器x86 上是CR3并處理好 TLB 刷新的相關事宜。核心職責加載新頁表并管理 TLB當調度器決定運行一個新進程時switch_mm(prev, next, tsk)會被調用。它的核心邏輯圍繞幾個關鍵點展開加載next-pgd到CR3這是最根本的一步。next-pgd是next進程頁全局目錄的虛擬地址。執(zhí)行l(wèi)oad_cr3()后CPU 的地址翻譯單元就開始使用新進程的頁表了。維護mm_cpumask這是一個位掩碼記錄了當前哪些 CPU 正在使用這個mm。switch_mm會把自己所在的 CPU 加入到next的掩碼中并從prev的掩碼中移除。這確保了當頁表發(fā)生變化時能準確地通知到所有正在使用該頁表的 CPU 去刷新 TLB。處理 TLB 的“懶惰”策略如果prev next比如內核線程切換時借用同一個mmswitch_mm會走快速路徑只更新 CPU 的 TLB 狀態(tài)而不會重新加載CR3從而避免了昂貴的 TLB 刷新開銷。代碼邏輯與關鍵細節(jié)雖然不同內核版本的實現細節(jié)有差異但核心骨架是清晰的。一個經典的switch_mm實現以較新版本為例包含以下關鍵步驟void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { unsigned cpu smp_processor_id(); if (likely(prev ! next)) { // 1. 設置當前CPU的TLB狀態(tài)為OK并記錄活躍的mm this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); this_cpu_write(cpu_tlbstate.active_mm, next); // 2. 將當前CPU加入next的mm_cpumask cpumask_set_cpu(cpu, mm_cpumask(next)); // 3. 加載新頁表到CR3這是關鍵的同步點 // load_cr3 是序列化指令充當了全屏障的作用 load_cr3(next-pgd); // 4. 從prev的mm_cpumask中移除當前CPU cpumask_clear_cpu(cpu, mm_cpumask(prev)); // 5. 加載與mm相關的CR4狀態(tài)如PCID、SMEP等 load_mm_cr4(next); } }這里有一個微妙的并發(fā)同步問題體現在mm_cpumask和load_cr3的順序上。注釋中詳細解釋了原因switch_mm必須保證cpumask_set_cpu的寫入先于任何可能從新頁表加載 TLB 條目的操作。否則另一個 CPU 在修改頁表并檢查mm_cpumask時可能看不到當前 CPU從而漏發(fā) TLB 刷新 IPI導致當前 CPU 的 TLB 中殘留過期的映射。幸運的是load_cr3()指令本身具有序列化效果天然充當了所需的全內存屏障。與其他核心機制的聯動switch_mm并非孤立存在它和你之前問過的許多概念都緊密相連與context_switchswitch_mm是context_switch的第一步負責“換地址空間”之后的switch_to才負責“換寄存器?!?。與active_mm/ 惰性 TLB當切換到內核線程next-mm NULL時調度器不會調用switch_mm而是借用前一個進程的active_mm并進入“惰性 TLB”模式。與 PCID/ASID現代 x86 的switch_mm不會簡單地往CR3里寫next-pgd而是通過choose_new_asid等邏輯結合kern_pcid計算出包含 PCID 的完整CR3值并決定是否需要刷新 TLB。你之前看到的load_new_mm_cr3就是實際執(zhí)行這一步的函數。與 TLB 刷新 IPImm_cpumask的維護是 TLB shootdown 的基礎。當一個 CPU 修改了next的頁表后它會遍歷mm_cpumask找到所有正在使用該頁表的 CPU并向它們發(fā)送 IPI 進行 TLB 刷新。