1. 程式人生 > >記憶體管理---匿名頁面管理

記憶體管理---匿名頁面管理

一、匿名頁面

這些記憶體頁面儲存了一些通常所說的機動性最強的內容,或者可以認為是銀行的活期存款,這些記憶體可以隨時被使用,隨時被歸還。例如使用者通過malloc–>>mmap申請的記憶體,或者通過brk/sbrk擴大的堆空間。相對於mmap檔案、檔案系統元資料之類的內容,這些空間對使用者來說最為順手,也最為常見。但是管理起來也比較複雜,因為這裡涉及到這些頁面如果被同時使用了很多,系統記憶體負載將會變的很重,此時系統的虛擬記憶體就要起作用,將這些頁面換到慢速二級儲存裝置,例如硬碟上。但是此時同樣會涉及到一個問題,因為一個頁面可以被幾個程序共享,例如fork出的子程序和父程序可能會共享相同的頁面,此時將一個頁面swap到二級儲存裝置上之後,此時每個程序的pte項都需要做相應的一次性修改。這些pte項指向的頁面即將被週轉做其它用處,而他們指向的真正資料內容將會被虛擬到二級儲存裝置上。 這裡有兩個需要直面的基本問題: 第一個問題就、是如何找到一個頁面是被哪些(所有的)程序的pte指向。找到這些之後,需要將這些pte逐一修改,並讓它們指向換出的二級儲存裝置上。 第二個問題是將哪些頁面換出去?系統中可能有很多的匿名頁面,可能有些正在鎂光燈下呼風喚雨,有些可能在角落裡被人默默遺忘。比如說當前使用者正在打WAR3,那麼可能這個程式使用的大量記憶體資源都會在一段時間內別頻繁使用(一局比賽正常來說10–40mins);相反的,一個週期性執行的後臺任務可能只有每隔一段時間才執行一段時間,例如某些客戶端的後臺更新任務。此時一個優秀的系統應該能夠準確的將長時間不用的頁面置換出去,最近長被使用的保留在記憶體中,這就是Latest Recently Used演算法(最近最少被使用),也就是核心中最為常見的LRU縮寫的來源。

二、匿名空間的申請

和IP地址空間一樣,一個程序的地址空間也是一個重要的資源。最近據說IPV4地址已經告罄,就像之前大家覺得IPV4地址足夠使用一樣,在沒有使用大型軟體之前,可能也是覺得4G邏輯地址空間是足夠使用的,但是我還是有幸遇到了地址空間被用完而導致的分配失敗問題,作為無意義的測試,可以嘗試不斷mmap,直到自己地址被使用完。 廢話了一段,大致的意思是想說,地址作為一種資源,你如果想使用的話,就必須先申請。你申請到了也可以不用,但是系統不能再將這些地址分配其它人,除非你通過munmap告訴系統這個地址空間已經使用完畢,此時才可以再次分配給其它人。雖然說大部分使用者態程式設計都是使用malloc來分配,但是malloc的底層在linux下正是通過mmap來實現,通過strace可以明顯的看到對於mmap的系統呼叫:

[[email protected] malloc]# cat malloc.c 
\#include <stdlib.h>
int main()
{
    printf("Hello\n");這裡新增兩個列印是為了在strace中起到定界的作用,從而可以知道malloc使用的系統呼叫
     malloc(0x100000);
    printf("world\n");
}
[[email protected] malloc]# strace ./a.out 
write(1, "Hello\n", 6Hello
)                  = 6
mmap2(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77af000
write(1, "world\n", 6world
)                  = 6
exit_group(6)                           = ?

核心中該系統呼叫的實現位於linux-2.6.21\arch\i386\kernel\sys_i386.c sys_mmap2-->>do_mmap_pgoff …… addr = get_unmapped_area(file, addr, len, pgoff, flags);這裡是邏輯地址分配,也就是從4GB(當然使用者態通常只能使用3G,核心使用1G)地址空間申請長度為len的地址空間。如果記憶體壓力已經很大,這裡也可能返回失敗。 …… vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);開始分配vma結構,也就是核心中表示某個記憶體區間已經被佔用的控制結構,所有的這種結構表示了一個程序的邏輯地址空間的佔用情況。 …… if (file) {這個分支對於匿名頁面對映來說是不會走到的,但是對於其他的檔案mmap來說非常之重要,而且從該流程中可以看看到如何確定一個頁面是在一個匿名對映地址空間中,例如通過mmalloc分配的地址空間。

        error = -EINVAL;
        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
            goto free_vma;
        if (vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;
        get_file(file);
        error = file->f_op->mmap(file, vma); /* 這裡呼叫檔案使用的mmap函式指標,對於ext2檔案系統,其mmap操作為
        generic_file_mmap函式,其中最為重要的操作就是安裝vma->vm_ops成員,這個將會在按需調頁中使用,
        ext2檔案系統中該值初始化為generic_file_vm_ops。*/
        if (error)
            goto unmap_and_free_vma;
    } else if (vm_flags & VM_SHARED) {

三、匿名頁面的分配

可以看到,mmap操作執行的實質性操作非常少,我們甚至認為它和檔案系統中的open系統呼叫類似,它只是進行一些最為必要的基礎性初始化工作,而真正的實質性操作都是通過read/write來執行的,只是記憶體的read/write核心執行是不需要使用者態關係的,只要使用者態訪問或者寫入了區間內容,那麼這個按需調頁就會被觸發。 正如剛才所說,mmap只是完成了初始化工作,頁面沒有分配,而且這片記憶體使用的PTE也沒有初始化,當之後真正使用這片地址的時候,將會觸發訪問異常,這些需要CPU硬體支援,對於常見的386系統,這個功能應該是在386時開始支援。 對於386系統,它的缺頁異常處理函式在linux-2.6.21\arch\i386\kernel\traps.c set_intr_gate(14,&page_fault)設定訪問異常的處理函式為page_fault linux-2.6.21\arch\i386\kernel\entry.S KPROBE_ENTRY(page_fault) RING0_EC_FRAME pushl $do_page_fault linux-2.6.21\arch\i386\mm\fault.c fastcall void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) 從這個函式開始,執行的相關呼叫鏈為 handle_mm_fault--->>>__handle_mm_fault--->>>handle_pte_fault,在該函式中

            if (vma->vm_ops) {由於記憶體頁面中沒有初始化vma的這個成員,所以不會走這個流程。
                if (vma->vm_ops->nopage)
                    return do_no_page(mm, vma, address,
                              pte, pmd,
                              write_access);
                if (unlikely(vma->vm_ops->nopfn))
                    return do_no_pfn(mm, vma, address, pte,
                             pmd, write_access);
            }
            return do_anonymous_page(mm, vma, address,對於malloc申請的頁面將會進入這個分支。
                         pte, pmd, write_access);

其中

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, pte_t *page_table, pmd_t *pmd,
        int write_access)
    if (write_access) {這裡如果是寫操作,此時就需要分配新的頁面。
        /* Allocate our own private page. */
        pte_unmap(page_table);
        if (unlikely(anon_vma_prepare(vma))) /*該函式中可能會動態分配一個struct anon_vma結構,並將其地址賦值給struct 
        vm_area_struct結構的struct anon_vma *anon_vma成員,明顯地,這個結構是一個佇列頭結構,它引導一個vm_area_struct結構
        連結串列(通過vm_area_struct的struct list_head anon_vma_node;成員連線在一起)。至於什麼樣的vm_area_struct連線在一起,它們
        連線在一起有什麼作用,將在之後說明。*/
            goto oom;
        page = alloc_zeroed_user_highpage(vma, address);這裡開始分配真正的頁面,也就是觸發了按需頁面分配的真正分配動作。
        if (!page)
            goto oom;
        entry = mk_pte(page, vma->vm_page_prot);
        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (!pte_none(*page_table))
            goto release;
        inc_mm_counter(mm, anon_rss);

        lru_cache_add_active(page); /* 將新分配的頁面新增到cache連結串列中,這個可以為之後的頁面LRU提供參考,
        當系統物理頁面週轉緊張而需要將一些頁面swap到二級儲存裝置上的時候,可以參考這個連結串列中的內容。*/
        page_add_new_anon_rmap(page, vma, address); /*這裡函式表示這個頁面是被匿名映射了,並且初始化其對映個數為0(-1表示
        未對映),並且將頁面結構中的page->mapping成員初始化為anon_vma_prepare函式中分配的anon_vma結構的地址在偏移一個
        位元組。這樣充分使用了地址的低兩個bits,因為核心中結構的大小是4位元組對齊的,所以正常情況下一個struct page中的mapping
        指向的地址應該是4位元組對齊,它的低兩個bit值為零。在判斷一個page時候是匿名對映的時候,可以判斷其struct page結構中的
        mapping最低一個bit是否為1,如果為1,則是匿名對映,並且將這個數值減一就可以得到所有映射了這個頁面的vm_area_struct
        結構連結串列。當需要將這個頁面swap出去的時候,就可以遍歷這個連結串列中所有的vm_area_struct結構,並更新它們的pte結構。*/

四、匿名頁面的換出

假設說同一個頁面被不同程序的PTE指向,而此時又需要將這個頁面swap到二級快取中,此時就需要逐一修改所有程序指向該頁面的pte,讓他們更新到新的正確位置。但是既然是匿名頁面,此時其它的程序如何能訪問到呢?最為直觀的就是當一個程序執行了fork的時候,也就是執行fork的時候,對應的函式呼叫結構為 do_fork--->>copy_process--->>>copy_mm--->>dup_mm--->>>dup_mmap--->>>anon_vma_link(tmp);

void anon_vma_link(struct vm_area_struct *vma)
{
    struct anon_vma *anon_vma = vma->anon_vma;在dup_mmap函式中可以看到,tmp被賦值為父程序的vm_area_struct,所以新的tmp結構的anon_vma和父程序指向相同的anon_vma結構。
    if (anon_vma) {
        spin_lock(&anon_vma->lock);
        list_add_tail(&vma->anon_vma_node, &anon_vma->head);這裡通過vm_area_struct結構中嵌入的anon_vma_node結構將新的vm_area_struct結構新增到父程序anon_vma引導的連結串列中。
        validate_anon_vma(vma);
        spin_unlock(&anon_vma->lock);
    }
}

當需要把這個匿名頁面swap出去,或者其它原因釋放這個頁面的時候,此時需要將所有對映入該頁面的vm_area_struct的pte做相應的更新。 shrink_page_list--->>>try_to_unmap---->>>try_to_unmap_anon--->>>try_to_unmap_one 在該函式中,將會完成各個vm_area_struct結構中引用了該頁面的PTE更新,

if (PageAnon(page)) {
        swp_entry_t entry = { .val = page_private(page) };
……
        set_pte_at(mm, address, pte, swp_entry_to_pte(entry));

至於其中的page_private成員,應該是在shrink_page_list函式中通過add_to_swap--->>>__add_to_swap_cache--->>>set_page_private(page, entry.val)函式設定的。對於所有使用了該頁面的PTE的遍歷則在try_to_unmap_anon函式中通過下面的迴圈完成

    list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {
        ret = try_to_unmap_one(page, vma, migration);
        if (ret == SWAP_FAIL || !page_mapped(page))
            break;
    }

五、頁面老化

對於選擇什麼樣的頁面換出的問題,在do_anonymous_page中通過lru_cache_add_active函式表示了自己願意被老化,所以就新增到了系統中該頁面所屬的zone的active連結串列中: lru_cache_add_active--->>>__pagevec_lru_add_active--->>>add_page_to_active_list(zone, page);

static inline void
add_page_to_active_list(struct zone *zone, struct page *page)
{
    list_add(&page->lru, &zone->active_list);將頁面新增到一個區的活動頁面連結串列中,之後頁面回收或者swap的時候將從這個連結串列中進行操作。
    __inc_zone_state(zone, NR_ACTIVE);
}
在shrink_active_list
    while (!list_empty(&l_hold)) {
        cond_resched();
        page = lru_to_page(&l_hold);
……
        list_add(&page->lru, &l_inactive);這裡將會把頁面從活躍連結串列移動到非活躍連結串列中。
    }

shrink_inactive_list--->>>shrink_page_list--->>try_to_unmap/pageout 來完成將非活躍連結串列中的頁面從記憶體中移除,可能是放入swap區(對於那些使用匿名對映並且配置了swap),對於那些是檔案內容在記憶體對映的頁面,可以將修改內容寫入( mapping->a_ops->writepage(page, &wbc))