本文為原創,轉載請注明: http://www.cnblogs.com/tolimit/
概述
最近在看記憶體回收,記憶體回收在進行同步的一些情況非常複雜,然後就想,不會記憶體壓縮的頁面遷移過程中的同步關系也那麼複雜吧,帶着好奇心就把頁面遷移的源碼都大緻看了一遍,還好,不複雜,也容易了解,這裡我們就說說在頁面遷移過程中是如何進行同步的。不過首先可能沒看過的朋友需要先看看
linux記憶體源碼分析 - 記憶體壓縮(一),因為會涉及裡面的一些知識。
其實一句話可以概括頁面遷移時是如何進行同步的,就是:我要開始對這個頁進行頁面遷移處理了,你們這些通路此頁的程序都給我加入等待隊列等着,我處理完了就會喚醒你們。
如果需要詳細些說,那就是記憶體壓縮中即将對一個頁進行遷移工作,首先會對這個舊頁上鎖(置位此頁的PG_locked标志),然後建立一個頁表項資料,這個頁表項資料屬于swap類型,這個頁表項資料映射的是這個舊頁,然後把這個頁表項資料寫入所有映射了此舊頁的程序頁表項中,将舊頁資料和參數複制到新頁裡,再建立一個頁表項資料,這個頁表項資料就是正常的頁表項資料,這個頁表項資料映射的是這個新頁,然後把這個頁表項資料寫入到之前所有映射了舊頁的程序頁表項中,釋放鎖,喚醒等待的程序。
使用一張圖就可以說明整個過程:
這裡我們簡單說一下什麼叫做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的末尾。
static int __unmap_and_move(struct page *page, struct page *newpage,
int force, enum migrate_mode mode)
{
int rc = -EAGAIN;
int remap_swapcache = 1;
struct anon_vma *anon_vma = NULL;
/* 擷取這個page鎖(PG_locked标志)是關鍵,是整個回收過程中同步的保證
* 當我們對所有映射了此頁的程序進行unmap操作時,會給它們一個特殊的頁表項
* 當這些程序再次通路此頁時,會由于通路了這個特殊的頁表項進入到缺頁異常,然後在缺頁異常中等待此頁的這個鎖釋放
* 當此頁的這個鎖釋放時,頁面遷移已經完成了,這些程序的此頁表項已經在釋放鎖前就映射到了新頁上,這時候已經可以喚醒這些等待着此頁的鎖的程序
* 這些程序下次通路此頁表項時,就是通路到了新頁
*/
if (!trylock_page(page)) {
/* 異步此時一定需要拿到鎖,否則就傳回,因為下面還有一個lock_page(page)擷取鎖,這個有可能會導緻阻塞等待 */
if (!force || mode == MIGRATE_ASYNC)
goto out;
if (current->flags & PF_MEMALLOC)
goto out;
/* 同步和輕同步的情況下,都有可能會為了拿到這個鎖而阻塞在這 */
lock_page(page);
}
/* 此頁正在回寫到磁盤 */
if (PageWriteback(page)) {
/* 異步和輕同步模式都不會等待 */
if (mode != MIGRATE_SYNC) {
rc = -EBUSY;
goto out_unlock;
}
if (!force)
goto out_unlock;
/* 同步模式下,等待此頁回寫完成 */
wait_on_page_writeback(page);
}
/* 匿名頁并且不使用于ksm的情況 */
if (PageAnon(page) && !PageKsm(page)) {
/* 擷取匿名頁所指向的anon_vma,如果是檔案頁,則傳回NULL */
anon_vma = page_get_anon_vma(page);
if (anon_vma) {
/*
* 此頁是匿名頁,不做任何處理
*/
} else if (PageSwapCache(page)) {
/* 此頁是已經加入到swapcache,并且進行過unmap的匿名頁(因為anon_vma為空,才到這裡,說明進行過unmap了),現在已經沒有程序映射此頁 */
remap_swapcache = 0;
} else {
goto out_unlock;
}
}
/* balloon使用的頁 */
if (unlikely(isolated_balloon_page(page))) {
rc = balloon_page_migrate(newpage, page, mode);
goto out_unlock;
}
/* page->mapping為空的情況,有兩種情況
* 1.此頁是已經加入到swapcache,并且進行過unmap的匿名頁,現在已經沒有程序映射此頁
* 2.一些特殊的頁,這些頁page->mapping為空,但是page->private指向一個buffer_head連結清單(日志緩沖區使用的頁?)
*/
if (!page->mapping) {
VM_BUG_ON_PAGE(PageAnon(page), page);
/* page->private有buffer_head */
if (page_has_private(page)) {
/* 釋放此頁所有的buffer_head,之後此頁将被回收 */
try_to_free_buffers(page);
goto out_unlock;
}
goto skip_unmap;
}
/* Establish migration ptes or remove ptes */
/* umap此頁,會為映射了此頁的程序建立一個遷移使用的swp_entry_t,這個swp_entry_t指向的頁就是此page
* 将此swp_entry_t替換映射了此頁的頁表項
* 然後對此頁的頁描述符的_mapcount進行--操作,表明反向映射到的一個程序取消了映射
*/
try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
skip_unmap:
/* 将page的内容複制到newpage中,會進行将newpage重新映射到page所屬程序的pte中 */
if (!page_mapped(page))
rc = move_to_new_page(newpage, page, remap_swapcache, mode);
/* 當在move_to_new_page()中進行remove_migration_ptes()失敗時,這裡才會執行
* 這裡是将所有映射了舊頁的程序頁表項再重新映射到舊頁上,也就是本次記憶體遷移失敗了。
*/
if (rc && remap_swapcache)
remove_migration_ptes(page, page);
/* Drop an anon_vma reference if we took one */
if (anon_vma)
put_anon_vma(anon_vma);
out_unlock:
/* 釋放此頁的鎖(PG_locked清除)
* 在unmap後,所有通路此頁的程序都會阻塞在這裡,等待此鎖釋放
* 這裡釋放後,所有通路此頁的程序都會被喚醒
*/
unlock_page(page);
out:
return rc;
}
這段代碼一前一後的兩個上鎖,就是之前說的頁面遷移時同步的重點,而且通過代碼也可以看到,這個鎖是一定要擷取,才能夠繼續進行頁面遷移的。當處于異步模式時,如果沒擷取到鎖,就直接跳出,取消對此頁的處理了。而輕同步和同步模式時,就會對此鎖不拿到不死心。對于這個函數主要的函數入口就兩個,一個try_to_unmap(),一個是move_to_new_page()。
try_to_unmap()函數是對此頁進行反向映射,對每一個映射了此頁的程序頁表進行處理,注意TTU_MIGRATION标志,代表着這次反向映射是為了頁面遷移而進行的,而TTU_IGNORE_MLOCK标志,也代表着記憶體壓縮是可以對mlock在記憶體中的頁框進行的。如之前所說,在try_to_unmap()函數中,主要工作就是一件事情,生成一個swap類型的頁表項資料,将此頁表項資料設定為頁面遷移使用的資料,然後将此頁表項資料寫入到每一個映射了此待移動頁的程序頁表項中。我們進入此函數看看:
int try_to_unmap(struct page *page, enum ttu_flags flags)
{
int ret;
/* 反向映射控制結構 */
struct rmap_walk_control rwc = {
/* 對一個vma所屬頁表進行unmap操作
* 每次擷取一個vma就會對此vma調用一次此函數,在函數裡第一件事就是判斷擷取的vma有沒有映射此page
*/
.rmap_one = try_to_unmap_one,
.arg = (void *)flags,
/* 對一個vma進行unmap後會執行此函數 */
.done = page_not_mapped,
.file_nonlinear = try_to_unmap_nonlinear,
/* 用于對整個anon_vma的紅黑樹進行上鎖,用讀寫信号量,鎖是aon_vma的rwsem */
.anon_lock = page_lock_anon_vma_read,
};
VM_BUG_ON_PAGE(!PageHuge(page) && PageTransHuge(page), page);
if ((flags & TTU_MIGRATION) && !PageKsm(page) && PageAnon(page))
rwc.invalid_vma = invalid_migration_vma;
/* 裡面會對所有映射了此頁的vma進行周遊,具體見反向映射 */
ret = rmap_walk(page, &rwc);
/* 沒有vma要求此頁鎖在記憶體中,并且page->_mapcount為-1了,表示沒有程序映射了此頁 */
if (ret != SWAP_MLOCK && !page_mapped(page))
ret = SWAP_SUCCESS;
return ret;
}
反向映射原理具體見
linux記憶體源碼分析 - 記憶體回收(匿名頁反向映射),這裡就不詳細說明了,說說這個函數,這個函數有一個最重要的函數指針,就是rmap_one,它指向try_to_unmap_one()函數,這個函數在每通路一個vma時,就會調用一次,無論此vma有沒有映射此頁,而反向映射走的流程都在rmap_walk中,這裡我們就不看了,主要看try_to_unmap_one()函數:
/*
* 對vma進行unmap操作,并對此頁的page->_mapcount--,這裡面的頁可能是檔案頁也可能是匿名頁
* page: 目标page
* vma: 擷取到的vma
* address: page在vma所屬的程序位址空間中的線性位址
*/
static int try_to_unmap_one(struct page *page, struct vm_area_struct *vma,
unsigned long address, void *arg)
{
struct mm_struct *mm = vma->vm_mm;
pte_t *pte;
pte_t pteval;
spinlock_t *ptl;
int ret = SWAP_AGAIN;
enum ttu_flags flags = (enum ttu_flags)arg;
/* 先檢查此vma有沒有映射此page,有則傳回此page在此程序位址空間的頁表項 */
/* 檢查page有沒有映射到mm這個位址空間中
* address是page在此vma所屬程序位址空間的線性位址,擷取方法: address = vma->vm_pgoff + page->pgoff << PAGE_SHIFT;
* 通過線性位址address擷取對應在此程序位址空間的頁表項,然後通過頁表項映射的頁框号和page的頁框号比較,則知道頁表項是否映射了此page
* 會對頁表上鎖
*/
pte = page_check_address(page, mm, address, &ptl, 0);
/* pte為空,則說明page沒有映射到此mm所屬的程序位址空間,則跳到out */
if (!pte)
goto out;
/* 如果flags沒有要求忽略mlock的vma */
if (!(flags & TTU_IGNORE_MLOCK)) {
/* 如果此vma要求裡面的頁都鎖在記憶體中,則跳到out_mlock */
if (vma->vm_flags & VM_LOCKED)
goto out_mlock;
/* flags标記了對vma進行mlock釋放模式,則跳到out_unmap,因為這個函數中隻對vma進行unmap操作 */
if (flags & TTU_MUNLOCK)
goto out_unmap;
}
/* 忽略頁表項中的Accessed */
if (!(flags & TTU_IGNORE_ACCESS)) {
/* 清除頁表項的Accessed标志 */
if (ptep_clear_flush_young_notify(vma, address, pte)) {
/* 清除失敗,發生在清除後檢查是否為0 */
ret = SWAP_FAIL;
goto out_unmap;
}
}
/* Nuke the page table entry. */
/* 空函數 */
flush_cache_page(vma, address, page_to_pfn(page));
/* 擷取頁表項内容,儲存到pteval中,然後清空頁表項 */
pteval = ptep_clear_flush(vma, address, pte);
/* Move the dirty bit to the physical page now the pte is gone. */
/* 如果頁表項标記了此頁為髒頁 */
if (pte_dirty(pteval))
/* 設定頁描述符的PG_dirty标記 */
set_page_dirty(page);
/* Update high watermark before we lower rss */
/* 更新程序所擁有的最大頁框數 */
update_hiwater_rss(mm);
/* 此頁是被标記為"壞頁"的頁,這種頁用于核心糾正一些錯誤,是否用于邊界檢查? */
if (PageHWPoison(page) && !(flags & TTU_IGNORE_HWPOISON)) {
/* 非大頁 */
if (!PageHuge(page)) {
/* 是匿名頁,則mm的MM_ANONPAGES-- */
if (PageAnon(page))
dec_mm_counter(mm, MM_ANONPAGES);
else
/* 此頁是檔案頁,則mm的MM_FILEPAGES-- */
dec_mm_counter(mm, MM_FILEPAGES);
}
/* 設定頁表項新的内容為 swp_entry_to_pte(make_hwpoison_entry(page)) */
set_pte_at(mm, address, pte,
swp_entry_to_pte(make_hwpoison_entry(page)));
} else if (pte_unused(pteval)) {
/* 一些架構上會有這種情況,X86不會調用到這個判斷中 */
if (PageAnon(page))
dec_mm_counter(mm, MM_ANONPAGES);
else
dec_mm_counter(mm, MM_FILEPAGES);
} else if (PageAnon(page)) {
/* 此頁為匿名頁處理 */
/* 擷取page->private中儲存的内容,調用到try_to_unmap()前會把此頁加入到swapcache,然後配置設定一個以swap頁槽偏移量為内容的swp_entry_t */
swp_entry_t entry = { .val = page_private(page) };
pte_t swp_pte;
/* 對于記憶體回收,基本都是這種情況,因為page在調用到這裡之前已經被移動到了swapcache
* 而對于記憶體壓縮,
*/
if (PageSwapCache(page)) {
/* 檢查entry是否有效
* 并且增加entry對應頁槽在swap_info_struct的swap_map的數值,此數值标記此頁槽的頁有多少個程序引用
*/
if (swap_duplicate(entry) < 0) {
/* 檢查失敗,把原來的頁表項内容寫回去 */
set_pte_at(mm, address, pte, pteval);
/* 傳回值為SWAP_FAIL */
ret = SWAP_FAIL;
goto out_unmap;
}
/* entry有效,并且swap_map中目标頁槽的數值也++了 */
/* 這個if的情況是此vma所屬程序的mm沒有加入到所有程序的mmlist中(init_mm.mmlist) */
if (list_empty(&mm->mmlist)) {
spin_lock(&mmlist_lock);
if (list_empty(&mm->mmlist))
list_add(&mm->mmlist, &init_mm.mmlist);
spin_unlock(&mmlist_lock);
}
/* 減少此mm的匿名頁統計 */
dec_mm_counter(mm, MM_ANONPAGES);
/* 增加此mm的頁表中标記了頁在swap的頁表項的數量 */
inc_mm_counter(mm, MM_SWAPENTS);
} else if (IS_ENABLED(CONFIG_MIGRATION)) {
/* 執行到這裡,就是對匿名頁進行頁面遷移工作(記憶體壓縮時使用) */
/* 如果flags沒有标記此次是在執行頁面遷移操作 */
BUG_ON(!(flags & TTU_MIGRATION));
/* 為此匿名頁建立一個頁遷移使用的swp_entry_t,此swp_entry_t指向此匿名頁 */
entry = make_migration_entry(page, pte_write(pteval));
}
/*
* 這個entry有兩種情況,儲存在page->private中的以在swap中頁槽偏移量為資料的swp_entry_t
* 另一種是一個遷移使用的swp_entry_t
*/
/* 将entry轉為一個頁表項 */
swp_pte = swp_entry_to_pte(entry);
/* 頁表項有一位用于_PAGE_SOFT_DIRTY,用于kmemcheck */
if (pte_soft_dirty(pteval))
swp_pte = pte_swp_mksoft_dirty(swp_pte);
/* 将配置好的新的頁表項swp_pte寫入頁表項中 */
set_pte_at(mm, address, pte, swp_pte);
/* 如果頁表項表示映射的是一個檔案,則是一個bug。因為這裡處理的是匿名頁,主要檢查頁表項中的_PAGE_FILE位 */
BUG_ON(pte_file(*pte));
} else if (IS_ENABLED(CONFIG_MIGRATION) &&
(flags & TTU_MIGRATION)) {
/* 本次調用到此是對檔案頁進行頁遷移操作的,會為映射了此檔案頁的程序建立一個swp_entry_t,這個swp_entry_t指向此檔案頁 */
/* Establish migration entry for a file page */
swp_entry_t entry;
/* 建立一個遷移使用的swp_entry_t,用于檔案頁遷移 */
entry = make_migration_entry(page, pte_write(pteval));
/* 将此頁表的pte頁表項寫入entry轉為的頁表項内容 */
set_pte_at(mm, address, pte, swp_entry_to_pte(entry));
} else
/* 此頁是檔案頁,僅對此mm的檔案頁計數--,檔案頁不需要設定頁表項,隻需要對頁表項進行清空 */
dec_mm_counter(mm, MM_FILEPAGES);
/* 如果是匿名頁,上面的代碼已經将匿名頁對應于此程序的頁表項進行修改了 */
/* 主要對此頁的頁描述符的_mapcount進行--操作,當_mapcount為-1時,表示此頁已經沒有頁表項映射了 */
page_remove_rmap(page);
/* 每個程序對此頁進行了unmap操作,此頁的page->_count--,并判斷是否為0,如果為0則釋放此頁,一般這裡不會為0 */
page_cache_release(page);
out_unmap:
pte_unmap_unlock(pte, ptl);
if (ret != SWAP_FAIL && !(flags & TTU_MUNLOCK))
mmu_notifier_invalidate_page(mm, address);
out:
return ret;
out_mlock:
pte_unmap_unlock(pte, ptl);
if (down_read_trylock(&vma->vm_mm->mmap_sem)) {
if (vma->vm_flags & VM_LOCKED) {
mlock_vma_page(page);
ret = SWAP_MLOCK;
}
up_read(&vma->vm_mm->mmap_sem);
}
return ret;
}
此函數很長,原因是把所有可能進行反向映射unmap操作的情況都寫進去了,比如說記憶體回收和我們現在說的頁面遷移。需要注意,此函數一開始第一件事情,就是判斷此vma是否映射了此頁,通過page_check_address()進行判斷,判斷條件也很簡單,通過page->index儲存的虛拟頁框号,與此vma起始的虛拟頁框号相減,得到一個以頁為機關的偏移量,這個偏移量與vma起始線性位址相加,就得到了此頁在此程序位址空間的線性位址,然後通過線性位址找到對應的頁表項,頁表項中映射的實體頁框号是否與此頁的實體頁框号相一緻,一緻則說明此vma映射了此頁。其實對我們頁面遷移來說,涉及到的代碼并不多,如下:
。。。。。。
} else if (IS_ENABLED(CONFIG_MIGRATION)) {
/* 執行到這裡,就是對匿名頁進行頁面遷移工作(記憶體壓縮時使用) */
/* 如果flags沒有标記此次是在執行頁面遷移操作 */
BUG_ON(!(flags & TTU_MIGRATION));
/* 為此匿名頁建立一個頁遷移使用的swp_entry_t,此swp_entry_t指向此匿名頁 */
entry = make_migration_entry(page, pte_write(pteval));
}
/*
* 這個entry有兩種情況,儲存在page->private中的以在swap中頁槽偏移量為資料的swp_entry_t
* 另一種是一個遷移使用的swp_entry_t
*/
/* 将entry轉為一個頁表項 */
swp_pte = swp_entry_to_pte(entry);
/* 頁表項有一位用于_PAGE_SOFT_DIRTY,用于kmemcheck */
if (pte_soft_dirty(pteval))
swp_pte = pte_swp_mksoft_dirty(swp_pte);
/* 将配置好的新的頁表項swp_pte寫入頁表項中 */
set_pte_at(mm, address, pte, swp_pte);
/* 如果頁表項表示映射的是一個檔案,則是一個bug。因為這裡處理的是匿名頁,主要檢查頁表項中的_PAGE_FILE位 */
BUG_ON(pte_file(*pte));
} else if (IS_ENABLED(CONFIG_MIGRATION) &&
(flags & TTU_MIGRATION)) {
/* 本次調用到此是對檔案頁進行頁遷移操作的,會為映射了此檔案頁的程序建立一個swp_entry_t,這個swp_entry_t指向此檔案頁 */
/* Establish migration entry for a file page */
swp_entry_t entry;
/* 建立一個遷移使用的swp_entry_t,用于檔案頁遷移 */
entry = make_migration_entry(page, pte_write(pteval));
/* 将此頁表的pte頁表項寫入entry轉為的頁表項内容 */
set_pte_at(mm, address, pte, swp_entry_to_pte(entry));
} else
/* 此頁是檔案頁,僅對此mm的檔案頁計數--,檔案頁不需要設定頁表項,隻需要對頁表項進行清空 */
dec_mm_counter(mm, MM_FILEPAGES);
/* 如果是匿名頁,上面的代碼已經将匿名頁對應于此程序的頁表項進行修改了 */
/* 主要對此頁的頁描述符的_mapcount進行--操作,當_mapcount為-1時,表示此頁已經沒有頁表項映射了 */
page_remove_rmap(page);
/* 每個程序對此頁進行了unmap操作,此頁的page->_count--,并判斷是否為0,如果為0則釋放此頁,一般這裡不會為0 */
page_cache_release(page);
out_unmap:
pte_unmap_unlock(pte, ptl);
if (ret != SWAP_FAIL && !(flags & TTU_MUNLOCK))
mmu_notifier_invalidate_page(mm, address);
out:
return ret;
out_mlock:
pte_unmap_unlock(pte, ptl);
if (down_read_trylock(&vma->vm_mm->mmap_sem)) {
if (vma->vm_flags & VM_LOCKED) {
mlock_vma_page(page);
ret = SWAP_MLOCK;
}
up_read(&vma->vm_mm->mmap_sem);
}
return ret;
}
這裡的代碼就将檔案頁和匿名頁的頁面遷移的情況都包括了,這裡是通過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()函數:
static int move_to_new_page(struct page *newpage, struct page *page,
int remap_swapcache, enum migrate_mode mode)
{
struct address_space *mapping;
int rc;
/* 對新頁上鎖,這裡應該100%上鎖成功,因為此頁是新的,沒有任何程序和子產品使用 */
if (!trylock_page(newpage))
BUG();
/* Prepare mapping for the new page.*/
/* 将舊頁的index、mapping和PG_swapbacked标志複制到新頁
* 對于複制index和mapping有很重要的意義
* 通過index和mapping,就可以對新頁進行反向映射了,當新頁配置好後,對新頁進行反向映射,找到的就是映射了舊頁的程序,然後将它們的對應頁表項映射到新頁
*/
newpage->index = page->index;
newpage->mapping = page->mapping;
if (PageSwapBacked(page))
SetPageSwapBacked(newpage);
/* 擷取舊頁的mapping */
mapping = page_mapping(page);
/* 如果mapping為空,則執行預設的migrate_page()
* 注意到這裡時,映射了此頁的程序已經對此頁進行了unmap操作,而程序對應的頁表項被設定為了指向page(而不是newpage)的swp_entry_t
*/
if (!mapping)
/* 未加入到swapcache中的匿名頁會在這裡進行頁面遷移 */
rc = migrate_page(mapping, newpage, page, mode);
else if (mapping->a_ops->migratepage)
/* 檔案頁,和加入到swapcache中的匿名頁,都會到這裡
* 對于匿名頁,調用的是swap_aops->migrate_page()函數,而這個函數,實際上就是上面的migrate_page()函數
* 根據檔案系統的不同,這裡可能會對髒檔案頁造成回寫,隻有同步模式才能進行回寫
*/
rc = mapping->a_ops->migratepage(mapping,
newpage, page, mode);
else
/* 當檔案頁所在的檔案系統沒有支援migratepage()函數時,會調用這個預設的函數,裡面會對髒檔案頁進行回寫,隻有同步模式才能進行 */
rc = fallback_migrate_page(mapping, newpage, page, mode);
if (rc != MIGRATEPAGE_SUCCESS) {
newpage->mapping = NULL;
} else {
mem_cgroup_migrate(page, newpage, false);
/* 這個remap_swapcache預設就是1
* 這裡做的工作就是将之前映射了舊頁的頁表項,統統改為映射到新頁,會使用到反向映射
*/
if (remap_swapcache)
remove_migration_ptes(page, newpage);
page->mapping = NULL;
}
/* 釋放newpage的PG_locked标志 */
unlock_page(newpage);
return rc;
}
這裡有兩個重要函數,一個是檔案系統對應的migrate_page()函數,一個就是後面的remove_migration_ptes()函數,對于migrate_page()函數,實質就是将舊頁的參數和資料複制到新頁中,而remove_migration_ptes()函數,是對新頁進行一次反向映射(新頁已經從舊頁中複制好了,新的的反向映射效果和舊頁的反向映射效果一模一樣),然後将所有被修改為swap類型的程序頁表項都重新設定為映射了新頁的頁表項。
我們先看migrate_page(),這裡隻拿匿名頁的migrate_page()函數進行說明,因為比較清晰易懂:
/* 未加入到swapcache和加入到swapcache中的匿名頁都會在這裡進行頁面遷移 */
int migrate_page(struct address_space *mapping,
struct page *newpage, struct page *page,
enum migrate_mode mode)
{
int rc;
/* 頁都沒加入到swapcache,更不可能會正在進行回寫 */
BUG_ON(PageWriteback(page)); /* Writeback must be complete */
/* 此函數主要工作就是如果舊頁有加入到address_space的基樹中,那麼就用新頁替換這個舊頁的slot,新頁替換舊頁加入address_space的基樹中
* 并且會同步舊匿名頁的PG_swapcache标志和private指針内容到新頁
* 對舊頁會page->_count--(從基樹中移除)
* 對新頁會page->_count++(加入到基樹中)
*/
rc = migrate_page_move_mapping(mapping, newpage, page, NULL, mode, 0);
if (rc != MIGRATEPAGE_SUCCESS)
return rc;
/* 将page頁的内容複制的newpage
* 再對一些标志進行複制
*/
migrate_page_copy(newpage, page);
return MIGRATEPAGE_SUCCESS;
}
這裡面又有兩個函數,migrate_page_move_mapping()和migrate_page_copy(),先看第一個,migrate_page_move_mapping()的作用是将舊頁在address_space的基樹結點中的資料替換為新頁:
/* 此函數主要工作就是如果舊頁有加入到address_space的基樹中,那麼就用新頁替換這個舊頁的slot,新頁替換舊頁加入address_space的基樹中
* 并且會同步舊匿名頁的PG_swapcache标志和private指針内容到新頁
* 對于未加入到swapcache中的匿名頁,head = NULL,extra_count = 0
*/
int migrate_page_move_mapping(struct address_space *mapping,
struct page *newpage, struct page *page,
struct buffer_head *head, enum migrate_mode mode,
int extra_count)
{
int expected_count = 1 + extra_count;
void **pslot;
/* 這裡主要判斷未加入swapcache中的舊匿名頁(page)
* 對于未加入到swapcache中的舊匿名頁,隻要page->_count為1,就說明可以直接進行遷移
* page->_count為1說明隻有隔離函數對此進行了++,其他地方沒有引用此頁
* page->_count為1,直接傳回MIGRATEPAGE_SUCCESS
*/
if (!mapping) {
/* Anonymous page without mapping */
if (page_count(page) != expected_count)
return -EAGAIN;
return MIGRATEPAGE_SUCCESS;
}
/* 以下是對page->mapping不為空的情況 */
/* 對mapping中的基樹上鎖 */
spin_lock_irq(&mapping->tree_lock);
/* 擷取此舊頁所在基樹中的slot */
pslot = radix_tree_lookup_slot(&mapping->page_tree,
page_index(page));
/* 對于加入了address_space的基樹中的舊頁
* 判斷page->_count是否為2 + page_has_private(page)
* 如果正确,則往下一步走
* 如果不是,可能此舊頁被某個程序映射了
*/
expected_count += 1 + page_has_private(page);
if (page_count(page) != expected_count ||
radix_tree_deref_slot_protected(pslot, &mapping->tree_lock) != page) {
spin_unlock_irq(&mapping->tree_lock);
return -EAGAIN;
}
/* 這裡再次判斷,這裡就隻判斷page->_count是否為2 + page_has_private(page)了
* 是的話就繼續往下走
* 如果不是,可能此舊頁被某個程序映射了
*/
if (!page_freeze_refs(page, expected_count)) {
spin_unlock_irq(&mapping->tree_lock);
return -EAGAIN;
}
if (mode == MIGRATE_ASYNC && head &&
!buffer_migrate_lock_buffers(head, mode)) {
page_unfreeze_refs(page, expected_count);
spin_unlock_irq(&mapping->tree_lock);
return -EAGAIN;
}
/* 如果走到這,上面的代碼得出一個結論,page是處于page->mapping指向的address_space的基樹中的,并且沒有程序映射此頁
* 是以以下要做的,就是用新頁(newpage)資料替換舊頁(page)資料所在的slot
*/
/* 新的頁的newpage->_count++,因為後面要把新頁替換舊頁所在的slot */
get_page(newpage);
/* 如果是匿名頁,走到這,此匿名頁必定已經加入了swapcache */
if (PageSwapCache(page)) {
/* 設定新頁也在swapcache中,後面會替換舊頁,新頁就會加入到swapcache中 */
SetPageSwapCache(newpage);
/* 将舊頁的private指向的位址複制到新頁的private
* 對于加入了swapcache中的頁,這項儲存的都是以swap分區頁槽為索引的swp_entry_t
* 這裡注意與在記憶體壓縮時unmap時寫入程序頁表項的swp_entry_t的差別,在記憶體壓縮時,寫入程序頁表項的swp_entry_t是以舊頁(page)為索引
*/
set_page_private(newpage, page_private(page));
}
/* 用新頁資料替換舊頁的slot */
radix_tree_replace_slot(pslot, newpage);
/* 設定舊頁的page->_count為expected_count - 1
* 這個-1是因為此舊頁已經算是從address_space的基樹中拿出來了
*/
page_unfreeze_refs(page, expected_count - 1);
/* 統計,注意,加入到swapcache中的匿名頁,也算作NR_FILE_PAGES的數量 */
__dec_zone_page_state(page, NR_FILE_PAGES);
__inc_zone_page_state(newpage, NR_FILE_PAGES);
if (!PageSwapCache(page) && PageSwapBacked(page)) {
__dec_zone_page_state(page, NR_SHMEM);
__inc_zone_page_state(newpage, NR_SHMEM);
}
spin_unlock_irq(&mapping->tree_lock);
return MIGRATEPAGE_SUCCESS;
}
而migrate_page_copy()則非常簡單,通過memcpy()将舊頁的資料拷貝到新頁中,然後将一些舊頁的參數也拷貝到新頁的頁描述符中:
/* 将page頁的内容複制的newpage
* 再對一些标志進行複制
*/
void migrate_page_copy(struct page *newpage, struct page *page)
{
int cpupid;
if (PageHuge(page) || PageTransHuge(page))
/* 大頁調用 */
copy_huge_page(newpage, page);
else
/* 普通頁調用,主要就是通過永久映射配置設定給兩個頁核心的線性位址,然後做memcpy,将舊頁内容拷貝到新頁
* 對于64位機器,就沒必要使用永久映射了,直接memcpy
*/
copy_highpage(newpage, page);
/* 對頁标志的複制 */
if (PageError(page))
SetPageError(newpage);
if (PageReferenced(page))
SetPageReferenced(newpage);
if (PageUptodate(page))
SetPageUptodate(newpage);
if (TestClearPageActive(page)) {
VM_BUG_ON_PAGE(PageUnevictable(page), page);
SetPageActive(newpage);
} else if (TestClearPageUnevictable(page))
SetPageUnevictable(newpage);
if (PageChecked(page))
SetPageChecked(newpage);
if (PageMappedToDisk(page))
SetPageMappedToDisk(newpage);
/* 如果頁标記了髒頁 */
if (PageDirty(page)) {
/* 清除舊頁的髒頁标志 */
clear_page_dirty_for_io(page);
/* 設定新頁的髒頁标志 */
if (PageSwapBacked(page))
SetPageDirty(newpage);
else
__set_page_dirty_nobuffers(newpage);
}
/* 還是複制一些标志 */
cpupid = page_cpupid_xchg_last(page, -1);
page_cpupid_xchg_last(newpage, cpupid);
/* 這裡也是做一些标志的複制
* 主要是PG_mlocked和ksm的stable_node
*/
mlock_migrate_page(newpage, page);
ksm_migrate_page(newpage, page);
/* 清除舊頁的幾個标志,這幾個标志在之前都賦給了新頁了 */
ClearPageSwapCache(page);
ClearPagePrivate(page);
set_page_private(page, 0);
/*
* If any waiters have accumulated on the new page then
* wake them up.
*/
/* 這裡主要用于喚醒等待新頁的等待者 */
if (PageWriteback(newpage))
end_page_writeback(newpage);
}
好了,到這裡,實際上整個新頁已經設定好了,隻不過因為頁表項的關系,也沒有程序能夠通路這個新頁,最後一個處理過程,就是重新将那些程序的頁表項設定為映射到新頁上,這個工作在之前列出的move_to_new_page()中的remove_migration_ptes()函數中進行,在remove_migration_ptes()中,也是進行了一次反向映射:
static void remove_migration_ptes(struct page *old, struct page *new)
{
/* 反向映射控制結構 */
struct rmap_walk_control rwc = {
/* 每擷取一個vma就會調用一次此函數 */
.rmap_one = remove_migration_pte,
/* rmap_one的最後一個參數為舊的頁框 */
.arg = old,
.file_nonlinear = remove_linear_migration_ptes_from_nonlinear,
};
/* 反向映射周遊vma函數 */
rmap_walk(new, &rwc);
}
這裡就直接看remove_migration_pte()函數了,也不難,直接看:
static int remove_migration_pte(struct page *new, struct vm_area_struct *vma,
unsigned long addr, void *old)
{
struct mm_struct *mm = vma->vm_mm;
swp_entry_t entry;
pmd_t *pmd;
pte_t *ptep, pte;
spinlock_t *ptl;
/* 新的頁是大頁(新的頁與舊的頁大小一樣,說明舊的頁也是大頁) */
if (unlikely(PageHuge(new))) {
ptep = huge_pte_offset(mm, addr);
if (!ptep)
goto out;
ptl = huge_pte_lockptr(hstate_vma(vma), mm, ptep);
} else {
/* 新的頁是普通4k頁 */
/* 這個addr是new和old在此程序位址空間中對應的線性位址,new和old會有同一個線性位址,因為new是old複制過來的 */
/* 擷取線性位址addr對應的頁中間目錄項 */
pmd = mm_find_pmd(mm, addr);
if (!pmd)
goto out;
/* 根據頁中間目錄項和addr,擷取對應的頁表項指針 */
ptep = pte_offset_map(pmd, addr);
/* 擷取頁中間目錄項的鎖 */
ptl = pte_lockptr(mm, pmd);
}
/* 上鎖 */
spin_lock(ptl);
/* 擷取頁表項内容 */
pte = *ptep;
/* 頁表項内容不是swap類型的頁表項内容(頁遷移頁表項屬于swap類型的頁表項),則準備跳出 */
if (!is_swap_pte(pte))
goto unlock;
/* 根據頁表項記憶體轉為swp_entry_t類型 */
entry = pte_to_swp_entry(pte);
/* 如果這個entry不是頁遷移類型的entry,或者此entry指向的頁不是舊頁,那就說明有問題,準備跳出 */
if (!is_migration_entry(entry) ||
migration_entry_to_page(entry) != old)
goto unlock;
/* 新頁的page->_count++ */
get_page(new);
/* 根據新頁new建立一個新的頁表項内容 */
pte = pte_mkold(mk_pte(new, vma->vm_page_prot));
/* 這個好像跟numa有關,先不用理,無傷大雅 */
if (pte_swp_soft_dirty(*ptep))
pte = pte_mksoft_dirty(pte);
/* Recheck VMA as permissions can change since migration started */
/* 如果擷取的entry标記了映射頁可寫 */
if (is_write_migration_entry(entry))
/* 給新頁的頁表項增加可寫标志 */
pte = maybe_mkwrite(pte, vma);
#ifdef CONFIG_HUGETLB_PAGE
/* 大頁的情況,先不看 */
if (PageHuge(new)) {
pte = pte_mkhuge(pte);
pte = arch_make_huge_pte(pte, vma, new, 0);
}
#endif
flush_dcache_page(new);
/* 将設定好的新頁的頁表項内容寫到對應頁表項中,到這裡,此頁表項原來映射的是舊頁,現在變成映射了新頁了 */
set_pte_at(mm, addr, ptep, pte);
/* 大頁,先不看 */
if (PageHuge(new)) {
if (PageAnon(new))
hugepage_add_anon_rmap(new, vma, addr);
else
page_dup_rmap(new);
/* 針對匿名頁 */
} else if (PageAnon(new))
/* 主要對page->_count++,因為多了一個程序映射此頁 */
page_add_anon_rmap(new, vma, addr);
else
/* 針對檔案頁,同樣,也是對page->_count++,因為多了一個程序映射此頁 */
page_add_file_rmap(new);
/* No need to invalidate - it was non-present before */
/* 重新整理tlb */
update_mmu_cache(vma, addr, ptep);
unlock:
/* 釋放鎖 */
pte_unmap_unlock(ptep, ptl);
out:
return SWAP_AGAIN;
}
好的,到這裡整個流程就理了一遍,不過我們發現,這整個流程都沒有将舊頁框釋放的過程,實際上,這個舊頁框釋放過程在最開始看的函數__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()就可以看到是怎麼處理,這裡隻截一部分代碼:
static int do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
spinlock_t *ptl;
struct page *page, *swapcache;
struct mem_cgroup *memcg;
swp_entry_t entry;
pte_t pte;
int locked;
int exclusive = 0;
int ret = 0;
if (!pte_unmap_same(mm, pmd, page_table, orig_pte))
goto out;
entry = pte_to_swp_entry(orig_pte);
/* 這個entry不是swap類型的entry,但是此頁表項是swap類型的頁表項 */
if (unlikely(non_swap_entry(entry))) {
/* 是頁面遷移類型的entry */
if (is_migration_entry(entry)) {
/* 進入處理 */
migration_entry_wait(mm, pmd, address);
} else if (is_hwpoison_entry(entry)) {
ret = VM_FAULT_HWPOISON;
} else {
print_bad_pte(vma, address, orig_pte, NULL);
ret = VM_FAULT_SIGBUS;
}
goto out;
}
。。。。。。
好了最後會有migration_entry_wait()函數進行處理:
void migration_entry_wait(struct mm_struct *mm, pmd_t *pmd,
unsigned long address)
{
/* 擷取鎖(并不是上鎖) */
spinlock_t *ptl = pte_lockptr(mm, pmd);
/* 擷取發生缺頁異常的對應頁表項 */
pte_t *ptep = pte_offset_map(pmd, address);
/* 處理 */
__migration_entry_wait(mm, ptep, ptl);
}
再往下看__migration_entry_wait():
static void __migration_entry_wait(struct mm_struct *mm, pte_t *ptep,
spinlock_t *ptl)
{
pte_t pte;
swp_entry_t entry;
struct page *page;
/* 上鎖 */
spin_lock(ptl);
/* 頁表項對應的頁表項内容 */
pte = *ptep;
/* 不是對應的swap類型的頁表項内容,則是錯誤的
* 注意,頁面遷移的頁表項内容是屬于swap類型
* 但是頁面遷移的entry類型是不屬于swap類型
*/
if (!is_swap_pte(pte))
goto out;
/* 頁表項内容轉為swp_entry_t */
entry = pte_to_swp_entry(pte);
/* 如果不是頁面遷移的entry類型,則錯誤 */
if (!is_migration_entry(entry))
goto out;
/* entry指定的頁描述符,這個頁是舊頁的,也就是即将被移動的頁 */
page = migration_entry_to_page(entry);
/* 此頁的page->_count++ */
if (!get_page_unless_zero(page))
goto out;
/* 釋放鎖 */
pte_unmap_unlock(ptep, ptl);
/* 如果此頁的PG_locked置位了,則加入此頁的等待隊列,等待此位被清除 */
wait_on_page_locked(page);
/* 經過一段時間的阻塞,到這裡PG_locked被清除了,page->_count-- */
put_page(page);
return;
out:
pte_unmap_unlock(ptep, ptl);
}
看到後面的wait_on_page_locked(page):
static inline void wait_on_page_locked(struct page *page)
{
if (PageLocked(page))
wait_on_page_bit(page, PG_locked);
}
現在知道為什麼頁面遷移類型的頁表項需要拿舊頁作為頁表項偏移量了吧,是為了這個友善擷取舊頁的頁描述符,然後加入到這個等待PG_locked清除的等待隊列中。
最後總結這個流程:
- 置位舊頁的PG_locked
- 對舊頁進行反向映射對每個映射了此頁的程序頁表項進行處理
- 根據舊頁的程序頁表項生成一個遷移使用的swap類型的頁表項(檔案頁和非檔案頁都會配置設定),這裡需要使用到舊頁的程序頁表項,相當于将舊頁的程序頁表項中一些标志也儲存到了這個swap類型的頁表項中
- 将此遷移使用的swap類型的頁表項寫入到所用映射了此頁的程序頁表項中。
- 調用遷移函數實作遷移,将舊頁的頁描述符資料和頁内資料複制到新頁中,對于不同狀态的頁,還有不同的處理
- 沒有加入到address_space中的頁,使用預設遷移函數,直接複制
- 加入到address_space中的頁,不使用預設遷移函數,而是使用address_space中的遷移函數,主要會更新舊頁在address_space中對應slot,讓其指向新頁
- 對新頁進行反向映射,将之前修改為可遷移類型的swap頁表項的程序頁表項重新映射到新頁。由于新頁的頁描述符中的資料與舊頁一緻,可以進行反向映射,然後通過此頁在不同程序vma中的線性位址,可以找到對應的頁表項,然後判斷此頁表項是否為可遷移swap類型的頁表項,并且指向的是否是舊頁(這裡并不是映射到舊頁),就可以判斷此程序的vma在遷移前是否映射了舊頁。
- 清除舊頁的PG_locked标志
- 在上層此舊頁就會被釋放掉。