天天看點

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  其實一句話可以概括頁面遷移時是如何進行同步的,就是:我要開始對這個頁進行頁面遷移處理了,你們這些通路此頁的程序都給我加入等待隊列等着,我處理完了就會喚醒你們。

  如果需要詳細些說,那就是記憶體壓縮中即将對一個頁進行遷移工作,首先會對這個舊頁上鎖(置位此頁的PG_locked标志),然後建立一個頁表項資料,這個頁表項資料屬于swap類型,這個頁表項資料映射的是這個舊頁,然後把這個頁表項資料寫入所有映射了此舊頁的程序頁表項中,将舊頁資料和參數複制到新頁裡,再建立一個頁表項資料,這個頁表項資料就是正常的頁表項資料,這個頁表項資料映射的是這個新頁,然後把這個頁表項資料寫入到之前所有映射了舊頁的程序頁表項中,釋放鎖,喚醒等待的程序。

  使用一張圖就可以說明整個過程:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這裡我們簡單說一下什麼叫做swap類型的頁表項資料,我們知道,頁表項中儲存的一個重要的資料就是頁内偏移量,還有一個重要标志位是此頁在不在記憶體中,當我們将一個匿名頁寫入swap分區時,會将此匿名頁在記憶體中占用的頁框進行釋放,而這樣,映射了此匿名頁的程序就沒辦法通路到處于磁盤上的匿名頁了,核心需要提供一些手段,讓這些程序能夠有辦法知道此匿名頁不在記憶體中,然後嘗試把這個匿名頁放入記憶體中。核心提供的手段就是将映射了此頁的程序頁表項修改成一個特殊的頁表項,當程序通路此頁時,此特殊的頁表項就會造成缺頁異常,在缺頁異常中,此特殊頁表項會引領走到相應處理的地方。這個特殊的頁表項就是swap類型的頁表項,對于swap類型的頁表項,它又分為2種,一種是它會表示此頁不在記憶體中,并且頁表項偏移量是匿名頁所在swap分區頁槽的索引。這種swap類型頁表項能夠正确引領缺頁異常将應該換入的匿名頁換入記憶體。而另一種,就是我們需要使用的頁面遷移類型的頁表項,它頁會表示此頁不在記憶體中,并且頁表項偏移量是舊頁的頁框号,同樣,這種頁表項也會引領缺頁異常将目前發生缺頁異常的程序加入到此舊頁的等待PG_locked清除的等待隊列中。

  

  接下來我們可以直接上源碼了,因為之前的文章也分析了很多,這篇我們隻講當一個頁開始進行頁面遷移時,核心的處理,我們可以直接從__unmap_and_move()函數看,此函數已經從上級函數中傳入了待移動的頁框和準備移入的頁框的描述符,并且提供了記憶體壓縮模式,這裡的記憶體壓縮模式幾乎不會對我們本次分析造成實質性的影響,但是這裡還是要說說幾種差別:

異步模式:不會進行任何阻塞操作,嘗試移動的頁都是MIGRATE_MOVABLE和MIGRATE_CMA類型的頁框

輕同步模式:會進行阻塞操作(比如裝置繁忙,會等待一小會,鎖繁忙,會阻塞直到拿到鎖為止),但是不會阻塞在等待頁面回寫完成的路徑上,會直接跳過正在回寫的頁,嘗試移動的頁是MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA類型的頁框

同步模式:在輕同步模式的基礎上,會阻塞在等待頁面回寫完成,然後再對此頁進行處理。如果需要,也會對髒檔案頁進行回寫,回寫完成後再對此頁進行移動(這種情況視檔案系統而定)

  待移動的頁框,一定是MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA類型中的一種(檔案頁和匿名頁),而準備移入的頁框,肯定是一個空閑頁框,并且相對于待移動的頁框,它更靠近zone的末尾。

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這段代碼一前一後的兩個上鎖,就是之前說的頁面遷移時同步的重點,而且通過代碼也可以看到,這個鎖是一定要擷取,才能夠繼續進行頁面遷移的。當處于異步模式時,如果沒擷取到鎖,就直接跳出,取消對此頁的處理了。而輕同步和同步模式時,就會對此鎖不拿到不死心。對于這個函數主要的函數入口就兩個,一個try_to_unmap(),一個是move_to_new_page()。

  try_to_unmap()函數是對此頁進行反向映射,對每一個映射了此頁的程序頁表進行處理,注意TTU_MIGRATION标志,代表着這次反向映射是為了頁面遷移而進行的,而TTU_IGNORE_MLOCK标志,也代表着記憶體壓縮是可以對mlock在記憶體中的頁框進行的。如之前所說,在try_to_unmap()函數中,主要工作就是一件事情,生成一個swap類型的頁表項資料,将此頁表項資料設定為頁面遷移使用的資料,然後将此頁表項資料寫入到每一個映射了此待移動頁的程序頁表項中。我們進入此函數看看:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  此函數很長,原因是把所有可能進行反向映射unmap操作的情況都寫進去了,比如說記憶體回收和我們現在說的頁面遷移。需要注意,此函數一開始第一件事情,就是判斷此vma是否映射了此頁,通過page_check_address()進行判斷,判斷條件也很簡單,通過page->index儲存的虛拟頁框号,與此vma起始的虛拟頁框号相減,得到一個以頁為機關的偏移量,這個偏移量與vma起始線性位址相加,就得到了此頁在此程序位址空間的線性位址,然後通過線性位址找到對應的頁表項,頁表項中映射的實體頁框号是否與此頁的實體頁框号相一緻,一緻則說明此vma映射了此頁。其實對我們頁面遷移來說,涉及到的代碼并不多,如下:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這裡的代碼就将檔案頁和匿名頁的頁面遷移的情況都包括了,這裡是通過make_migration_entry()生成了之前說的用于頁面遷移的swap類型頁表項,然後通過set_pte_at()寫入到程序對應的頁表項中。經過這裡的處理,這個舊頁裡的資料已經沒有程序能夠通路到了,當程序此時嘗試通路此頁框時,就會被加入到等待消除此頁PG_locked的等待隊列中。這裡注意:是根據映射了此舊頁的程序頁表項而生成一個遷移使用的swap類型的頁表項,也就是程序頁表項中一些标志會儲存到了swap類型頁表項中。并且檔案頁和非檔案頁都會生成一個遷移使用的swap類型的頁表項。而在記憶體回收過程中,也會使用這個swap類型的頁表項,但是不是遷移類型的,并且隻會是用于非檔案頁。

  好的,這時候所有的程序都沒辦法通路這個舊頁了,下面的工作就是建立一個新頁,将舊頁的資料參數移動到新頁上,這個工作是由move_to_new_page()函數來做,在調用move_to_new_page()前會通過page_mapped(page)判斷這個舊頁還有沒有程序映射了它,沒有才能進行,這裡我們直接看move_to_new_page()函數:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這裡有兩個重要函數,一個是檔案系統對應的migrate_page()函數,一個就是後面的remove_migration_ptes()函數,對于migrate_page()函數,實質就是将舊頁的參數和資料複制到新頁中,而remove_migration_ptes()函數,是對新頁進行一次反向映射(新頁已經從舊頁中複制好了,新的的反向映射效果和舊頁的反向映射效果一模一樣),然後将所有被修改為swap類型的程序頁表項都重新設定為映射了新頁的頁表項。

  我們先看migrate_page(),這裡隻拿匿名頁的migrate_page()函數進行說明,因為比較清晰易懂:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這裡面又有兩個函數,migrate_page_move_mapping()和migrate_page_copy(),先看第一個,migrate_page_move_mapping()的作用是将舊頁在address_space的基樹結點中的資料替換為新頁:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  而migrate_page_copy()則非常簡單,通過memcpy()将舊頁的資料拷貝到新頁中,然後将一些舊頁的參數也拷貝到新頁的頁描述符中:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  好了,到這裡,實際上整個新頁已經設定好了,隻不過因為頁表項的關系,也沒有程序能夠通路這個新頁,最後一個處理過程,就是重新将那些程序的頁表項設定為映射到新頁上,這個工作在之前列出的move_to_new_page()中的remove_migration_ptes()函數中進行,在remove_migration_ptes()中,也是進行了一次反向映射:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  這裡就直接看remove_migration_pte()函數了,也不難,直接看:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  好的,到這裡整個流程就理了一遍,不過我們發現,這整個流程都沒有将舊頁框釋放的過程,實際上,這個舊頁框釋放過程在最開始看的函數__unmap_and_move()的上一級,因為此舊頁是從lru中隔離出來的。是以在它已經遷移到新頁後,它的page->_count為1,當從隔離狀态放回lru時,這個page->_count會--,這時候系統會發現此頁框的page->_count為0,就直接釋放到夥伴系統中了。

  最後我們再簡單看看當程序設定了swap類型的頁面遷移頁表項時,在缺頁中斷中走的路徑,由于前面的路徑太長,我主要把後面的路徑列出來,而前面的路徑是:do_page_fault() -> __do_page_fault() -> handle_mm_fault() -> __handle_mm_fault() -> handle_pte_fault() -> do_swap_page();最後到do_swap_page()就可以看到是怎麼處理,這裡隻截一部分代碼:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  好了最後會有migration_entry_wait()函數進行處理:

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  再往下看__migration_entry_wait():

linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移
linux記憶體源碼分析 - 記憶體壓縮(同步關系)概述 頁面遷移

  看到後面的wait_on_page_locked(page):

  現在知道為什麼頁面遷移類型的頁表項需要拿舊頁作為頁表項偏移量了吧,是為了這個友善擷取舊頁的頁描述符,然後加入到這個等待PG_locked清除的等待隊列中。

最後總結這個流程:

置位舊頁的PG_locked

對舊頁進行反向映射對每個映射了此頁的程序頁表項進行處理

根據舊頁的程序頁表項生成一個遷移使用的swap類型的頁表項(檔案頁和非檔案頁都會配置設定),這裡需要使用到舊頁的程序頁表項,相當于将舊頁的程序頁表項中一些标志也儲存到了這個swap類型的頁表項中

将此遷移使用的swap類型的頁表項寫入到所用映射了此頁的程序頁表項中。

調用遷移函數實作遷移,将舊頁的頁描述符資料和頁内資料複制到新頁中,對于不同狀态的頁,還有不同的處理

沒有加入到address_space中的頁,使用預設遷移函數,直接複制

加入到address_space中的頁,不使用預設遷移函數,而是使用address_space中的遷移函數,主要會更新舊頁在address_space中對應slot,讓其指向新頁

對新頁進行反向映射,将之前修改為可遷移類型的swap頁表項的程序頁表項重新映射到新頁。由于新頁的頁描述符中的資料與舊頁一緻,可以進行反向映射,然後通過此頁在不同程序vma中的線性位址,可以找到對應的頁表項,然後判斷此頁表項是否為可遷移swap類型的頁表項,并且指向的是否是舊頁(這裡并不是映射到舊頁),就可以判斷此程序的vma在遷移前是否映射了舊頁。

清除舊頁的PG_locked标志

在上層此舊頁就會被釋放掉。

繼續閱讀