Linux TLB 刷新的懶惰模式【轉】

轉自:https://blog.csdn.net/Henzox/article/details/41963271

 我們都知道,在切換頁表時會刷新 TLB,這樣就可以使用新的地址空間,那什麼是 TLB 刷新的懶惰模式呢?

        TLB 是什麼這裏不作多的解釋,可以簡單理解爲,爲了加快 MMU 對虛擬地址的轉換而增加的緩存,它記錄了一個虛擬地址對應的內存頁的物理地址。其實就是根據虛擬地址的前 20 位,來建立一個個條目,對應記錄通過查找頁表來記錄的內存頁的物理地址。 
        既然有緩存,那麼被緩存的內容改變時,就涉及到緩存的刷新,就是 TLB 的刷新問題,當一個頁表結構發生變化時,使用該頁表節構的 CPU 就應該刷新自己的 TLB。
        顯然進程切換時,由於地址空間發生了變化,TLB 應該得到刷新,然而,內核進程只訪問內核空間的地址範圍,而每個進程的內核空間的地址範圍相同,所以如果 CPU 從一個用戶進程切換到一個內核進程,由於用戶進程和內核進程的內核地址空間部分相同,其實是不用切換頁表的,內核進程依然可以使用前一個用戶進程的內核部分的地址空間。這樣就省去了刷新 TLB 帶來的性能損耗。其實內核進程並不擁有自己的頁表集。
        想像着是挺完美的,但是在 SMP 構架下,這將帶來一些問題,例如,在某一核上 CPU0 剛從一用戶進程切換到內核進程,該內核進程沿用該用戶進程的地址空間,但它只訪問內核空間部分,這不會有問題,然而,如果該用戶進程在另一個 CPU1 核上被調度,並且在 CPU0 用它的地址空間時,它在 CPU1 上修改了自己內核空間的頁表,此時,若 CPU0 如果訪問它的地址空間是非常危險的,不管是被緩存的地址還是未被緩存的地址都將可能帶來意想不到的嚴重後果。
        那麼,難道這種美好的事情就要被上面的情況的發生扼殺,而每次都要刷新 TLB,重新加載頁表麼。顯然還是有補救辦法的,如果在 CPU0 上的內核進程執行期間,它所引用的用戶進程的地址空間沒有被調度並執行完畢的情況還是非常多的,這種不刷新 TLB 帶來的性能提升還是可以利用一下的,誰讓 Linux 是一個精打細算的內核呢。
        如何辦到這一點,其實很簡單,當內核空間頁表集在其它 CPU 上更改時,會調用 flush_tlb_all,它會讓每個 CPU 去刷新自己的 TLB。

        那麼當用戶空間的頁表集在其它 CPU 上更改時,由於 CPU0 雖然引用了相同的地址空間,但由於它是一個內核進程,它不會去訪問用戶空間的地址,那麼,那些失效和 TLB 項也不會造成危險,也就是它可以不去立即刷新 TLB。所以此時其它的 CPU 往往會發送一個 IPI 給其它引用該地址空間的 CPU,以通知他們自己更改了用戶空間的頁表,其它  CPU 就會根據自己的狀態作出相應的處理,如果是懶惰模式,就不用刷新 TLB。

        這就引入了 TLB 刷新的懶惰模式。
        Linux 爲每一個 CPU 創建了一個節構,它是一個每 CPU 數據,所以不需要加鎖,每個 CPU 只訪問自己的節構,它記錄了該 CPU 的狀態,TLBSTATE_OK 表示非懶惰模式, TLBSTATE_LAZY 表示懶惰模式。它還記錄該 CPU 引用的地址空間節構,是一個 mm_struct 類型的節構體指針,它記錄了一個進程的地址空間的所有信息,mm_struct 有一個成員 cpu_vm_mask, 是一個默認 32 位的掩碼,如果某個 CPU 在使用這個地址空間,則相應位置會被置位,顯然,它將支持最多 32 個 CPU。這樣情況就簡單了,當一個 CPU 從一個用戶進程調出,調用一個內核進程時,它會設置自己的進入 TLBSTATE_LAZY 模式,並且把它引用的用戶進程的 mm_struct 中相應的位置位,此時並不切換頁表節構,即不加載內核空間的頁目錄,而如果其它 CPU 更改了用戶空間的頁表,它會發送一個 IPI 消息,此時使用該地址空間的 CPU 都會收到這個消息,消息的響應函數爲 smp_invalidate_interrupt,代碼如下:

void smp_invalidate_interrupt(struct pt_regs *regs)
{
    unsigned long cpu;
 
    cpu = get_cpu();
 
    if (!cpu_isset(cpu, flush_cpumask))
        goto out;
        /*
         * This was a BUG() but until someone can quote me the
         * line from the intel manual that guarantees an IPI to
         * multiple CPUs is retried _only_ on the erroring CPUs
         * its staying as a return
         *
         * BUG();
         */
 
    if (flush_mm == per_cpu(cpu_tlbstate, cpu).active_mm) {
        if (per_cpu(cpu_tlbstate, cpu).state == TLBSTATE_OK) {
            if (flush_va == TLB_FLUSH_ALL)
                local_flush_tlb();
            else
                __flush_tlb_one(flush_va);
        } else
            leave_mm(cpu);
    }
    ack_APIC_irq();
    smp_mb__before_clear_bit();
    cpu_clear(cpu, flush_cpumask);
    smp_mb__after_clear_bit();
out:
    put_cpu_no_resched();
    __get_cpu_var(irq_stat).irq_tlb_count++;
}

        其它 CPU 會比較,看自己引用的地址空間是否是正在銷燬的地址空間,如果引用了相同地址空間,再判斷自己的狀態,如果不是懶惰模式,說明它在運行一個用戶進程,此時需要刷新 TLB 以同步用戶空間的頁表,如果是懶惰模式,那麼查看自己是否是懶惰模式,如果是懶惰模式,則調用 leave_mm ,代碼如下:
void leave_mm(int cpu)
{
    if (per_cpu(cpu_tlbstate, cpu).state == TLBSTATE_OK)
        BUG();
    cpu_clear(cpu, per_cpu(cpu_tlbstate, cpu).active_mm->cpu_vm_mask);
    load_cr3(swapper_pg_dir);
}

        它會把自己從地址空間的掩碼中清除,這樣就不會再次接收到第二次 IPI 消息。也不用刷新 TLB。但你可能發現,最後調用了 load_cr3,它會引起 tlb 的刷新,其實在 x86 上的這種實現與 intel 的預取隊列有關,在這裏加載 cr3 雖然感覺沒有實現完全的懶惰,但是不會有任何問題的,詳細原因這裏不談。
————————————————
版權聲明:本文爲CSDN博主「Henzox」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/Henzox/article/details/41963271

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章