本文為原創,轉載請注明: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標志
- 在上層此舊頁就會被釋放掉。
