天天看點

Linux記憶體管理之頁面回收【轉】

請求調頁機制,隻要使用者态程序繼續執行,他們就能獲得頁框,然而,請求調頁沒有辦法強制程序釋放不再使用的頁框。是以,遲早所有空閑記憶體将被配置設定給程序和高速緩存,Linux核心的頁面回收算法(PFRA)采取從使用者程序和核心高速緩存“竊取”頁框的辦法不從夥伴系統的空閑塊清單。

       實際上,在用完所有空閑記憶體之前,就必須執行頁框回收算法。否則,核心很可能陷入一種記憶體請求的僵局中,并導緻系統崩潰。也就是說,要釋放一個頁框,核心就必須把頁框的資料寫入磁盤;但是,為了完成這一操作,核心卻要請求另一個頁框(例如,為I/O資料傳送配置設定緩沖區首部)。因為不存在空閑頁框,是以,不可能釋放頁框。

       頁框算法的目标之一就是儲存最少的空閑頁框并使核心可以安全地從“記憶體緊缺”的情形中恢複過來。

選擇目标頁

       PFRA的目标就是獲得頁框并使之空閑。PFRA按照頁框所含内容,以不同的方式處理頁框。我們将他們區分成:不可回收頁、可交換頁、可同步頁和可丢棄頁:

頁類型

說明

回收操作

不可回收頁

空閑頁(包含子夥伴系統清單中)

保留頁(PG_reserved标志置位)

核心動态配置設定頁

程序核心态堆棧頁

臨時鎖定頁(PG_locked标志置位)

記憶體鎖定頁(在先行區中且VM_LOCKED标志置位)

不允許也無需回收

可回收頁

使用者太位址空間的匿名頁

Tmpfs檔案系統的映射頁(如IPC共享記憶體的頁)

将頁的内容儲存在交換區

可同步頁

使用者态位址空間的映射頁

存有磁盤檔案資料且在頁高速緩存中的頁

塊裝置緩沖區頁

某些磁盤高速緩存的頁(如索引節點高速緩存)

必要時,與磁盤鏡像同步這些頁

可丢棄頁

記憶體高速緩存中的未使用頁(如slab配置設定器高速緩存)

目錄想高速緩存的未使用頁

無需操作

        Linux 作業系統使用如下這兩種機制檢查系統記憶體的使用情況,進而确定可用的記憶體是否太少進而需要進行頁面回收。

周期性的檢查:這是由背景運作的守護程序 kswapd 完成的。該程序定期檢查目前系統的記憶體使用情況,當發現系統内空閑的實體頁面數目少于特定的門檻值時,該程序就會發起頁面回收的操作。

“記憶體嚴重不足”事件的觸發:在某些情況下,比如,作業系統忽然需要通過夥伴系統為使用者程序配置設定一大塊記憶體,或者需要建立一個很大的緩沖區,而當時系統中的記憶體沒有辦法提供足夠多的實體記憶體以滿足這種記憶體請求,這時候,作業系統就必須盡快進行頁面回收操作,以便釋放出一些記憶體空間進而滿足上述的記憶體請求。這種頁面回收方式也被稱作“直接頁面回收”。

睡眠回收,在進入suspend-to-disk狀态時,核心必須釋放記憶體。

       如果作業系統在進行了記憶體回收操作之後仍然無法回收到足夠多的頁面以滿足上述記憶體要求,那麼作業系統隻有最後一個選擇,那就是使用 OOM( out of memory )killer,它從系統中挑選一個最合适的程序殺死它,并釋放該程序所占用的所有頁面。

       上面介紹的記憶體回收機制主要依賴于三個字段:pages_min,pages_low 以及 pages_high。每個記憶體區域( zone )都在其區域描述符中定義了這樣三個字段,這三個字段的具體含義如下表 所示。

字段含義

名稱

字段描述

pages_min

區域的預留頁面數目,如果空閑實體頁面的數目低于 pages_min,那麼系統的壓力會比較大,此時,記憶體區域中急需空閑的實體頁面,頁面回收的需求非常緊迫。

pages_low

控制進行頁面回收的最小門檻值,如果空閑實體頁面的數目低于 pages_low,那麼作業系統核心會開始進行頁面回收。

pages_high

控制進行頁面回收的最大門檻值,如果空閑實體頁面的數目多于 pages_high,則記憶體區域的狀态是理想的。

PFRA設計

設計總則

1.       首先釋放“無害”頁,即必須線回收沒有被任何程序使用的磁盤與記憶體高速緩存中的頁;

2.       将使用者态程序和所有頁定為可回首頁,FPRA必須能夠竊得人任何使用者态程序頁,包括匿名頁。這樣,睡眠較長時間的程序将逐漸失去所有頁;

3.       同時取消引用一個共享頁的所有頁表項的映射,就可以回收該共享頁;

4.       隻回收“未用”頁,使用LRU算法。Linux使用每個頁表項中的通路标志位,在頁被通路時,該标志位由銀獎自動置位;而且,頁年齡由頁描述符在連結清單(兩個不同的連結清單之一)中的位置來表示。

是以,頁框回收算法是集中啟發式方法的混合:

1.  謹慎選擇檢查高速緩存的順序;

2.  基于頁年齡的變化排序;

3.  差別對待不同狀态的頁;

反向映射

        PFRA的目标之一是能釋放共享頁框。為達到這個目地。Linux核心能夠快速定為指向同一頁框的所有頁表項。這個過程就叫做反向映射。Linux 作業系統為實體頁面建立一個連結清單,用于指向引用了該實體頁面的所有頁表項。

基本思想如下圖:

Linux記憶體管理之頁面回收【轉】

       Linux采用“面向對象的反向映射”技術。實際上,對任何可回收的使用者态頁,核心保留系統中該頁所在所有現行區(“對象”)的反向連結,每個線性區描述符( vm_area_struct 結構)存放一個指針指向一個記憶體描述符( mm_struct 結構),而該記憶體描述符又包含一個指針指向一個頁全局目錄(PGD)。是以,這些反向連結使得PFRA能夠檢索引用某頁的所有頁表項。因為線性區描述符比頁描述符少,是以更新共享頁的反向連結就比較省時間。下面是具體的實作:

資料結構

       首先,PFRA必須要确定待回收頁是共享的還是非共享的,以及是映射頁或是匿名頁。為做到這一點,核心要檢視頁描述符的兩個字段:_mapcount和mapping。_mapcount字段存放引用頁框的頁表項數目,确定其是否共享;mapping字段用于确定頁是映射的或是匿名的:為空表示該頁屬于交換高速緩存;非空,且最低位是1,表示該頁為匿名頁,同時mapping字段中存放的是指向anon_vma描述符的指針;如果mapping字段非空,且最低位是0,表示該頁為映射頁;同時mapping字段指向對應檔案的address_space對象。

struct page   

{   

     atomic_t _mapcount;          

     union {   

        ……  

        struct {   

        ……        

         struct address_space *mapping;       

        };   

 };   

        Linux的address_space對象在RAMA中是對其的,是以其起始位址是4的倍數。是以其mapping字段的最低位可以用作一個标志位來表示該字段的指針是指向address_space對象還是anon_vma描述符。PageAnon檢查mapping最低位。

/*檢查頁是否為匿名頁,低位為1時為匿名頁*/  

static inline int PageAnon(struct page *page)  

{  

    return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;  

}  

        匿名頁面和檔案映射頁面分别采用了不同的底層資料結構去存放與頁面相關的虛拟記憶體區域。對于匿名頁面來說,與該頁面相關的虛拟記憶體區域存放在結構 anon_vma 中定義的雙向連結清單中。結構 anon_vma 定義很簡單,如下所示:

struct anon_vma   

     spinlock_t lock;     

     struct list_head head;       

};  

匿名頁的面向對象反向映射如下圖:

Linux記憶體管理之頁面回收【轉】

       可以通過頁面的mapping找到anon_vma然後找到映射該頁面的所有線性區域(vm_area_struct結構)。

而對于基于檔案映射的頁面來說,與匿名頁面不同的是,與該頁面相關的虛拟記憶體區域的存放是利用了優先級搜尋樹這種資料結構的。這是因為對于匿名頁面來說,頁面雖然可以是共享的,但是一般情況下,共享匿名頁面的使用者的數目不會很多;而對于基于檔案映射的頁面來說,共享頁面的使用者的數目可能會非常多,使用優先級搜尋樹這種結構可以更加快速地定位那些引用了該頁面的虛拟記憶體區域。作業系統會為每一個檔案都建立一個優先級搜尋樹,其根節點可以通過結構 address_space 中的 i_mmap 字段擷取。

struct address_space {   

  ……  

 struct prio_tree_root   i_mmap;          

……  

 }   

       Linux中使用 (radix,size,heap) 來表示優先級搜尋樹中的節點。其中,radix 表示記憶體區域的起始位置,heap 表示記憶體區域的結束位置,size 與記憶體區域的大小成正比。在優先級搜尋樹中,父節點的 heap 值一定不會小于子節點的 heap 值。在樹中進行查找時,根據節點的 radix 值進行。程式可以根據 size 值區分那些具有相同 radix 值的節點。

      在用于表示虛拟記憶體區域的結構 vm_area_struct 中,與上邊介紹的雙向連結清單和優先級搜尋樹相關的字段如下所示:

struct vm_area_struct {   

 struct mm_struct * vm_mm;    

 union {   

     struct {   

         struct list_head list;   

         void *parent;    

         struct vm_area_struct *head;   

     } vm_set;   

     struct raw_prio_tree_node prio_tree_node;   

 } shared;   

 struct list_head anon_vma_node;      

 struct anon_vma *anon_vma;       

};   

       與匿名頁面的雙向連結清單相關的字段是 anon_vma_node 和 anon_vma。union shared 則與檔案映射頁面使用的優先級搜尋樹相關。字段 anon_vma 指向 anon_vma 表;字段 anon_vma_node 将映射該頁面的所有虛拟記憶體區域連結起來;union shared 中的 prio_tree_node 結構用于表示優先級搜尋樹的一個節點;在某些情況下,比如不同的程序的記憶體區域可能映射到了同一個檔案的相同部分,也就是說這些記憶體區域具有相同的(radix,size,heap)值,這個時候 Linux 就會在樹上相應的節點(樹上原來那個具有相同(radix,size,heap) 值的記憶體區域)上接一個雙向連結清單用來存放這些記憶體區域,這個連結清單用 vm_set.list 來表示;樹上那個節點指向的連結清單中的第一個節點是表頭,用 vm_set.head 表示;vm_set.parent 用于表示是否是樹結點。下邊給出一個小圖示簡單說明一下 vm_set.list 和 vm_set.head。

vm_set.list 和 vm_set.head

Linux記憶體管理之頁面回收【轉】
Linux記憶體管理之頁面回收【轉】

       通過結構 vm_area_struct 中的 vm_mm 字段可以找到對應的 mm_struct 結構,在該結構中找到頁全局目錄,進而定位所有相關的頁表項。

反向映射實作

       在進行頁面回收的時候,Linux的 shrink_page_list() 函數中調用 try_to_unmap() 函數去更新所有引用了回收頁面的頁表項。其代碼流程如下所示:

Linux記憶體管理之頁面回收【轉】

       函數 try_to_unmap() 分别調用了兩個函數 try_to_unmap_anon() 和 try_to_unmap_file(),其目的都是檢查并确定都有哪些頁表項引用了同一個實體頁面,但是,由于匿名頁面和檔案映射頁面分别采用了不同的資料結構,是以二者采用了不同的方法。

       函數 try_to_unmap_anon() 用于匿名頁面,該函數掃描相應的 anon_vma 表中包含的所有記憶體區域,并對這些記憶體區域分别調用 try_to_unmap_one() 函數。

       函數 try_to_unmap_file() 用于檔案映射頁面,該函數會在優先級搜尋樹中進行搜尋,并為每一個搜尋到的記憶體區域調用 try_to_unmap_one() 函數。

       兩條代碼路徑最終彙合到 try_to_unmap_one() 函數中,更新引用特定實體頁面的所有頁表項的操作都是在這個函數中實作的。

代碼如下,對關鍵部分做了注釋:

static int try_to_unmap_anon(struct page *page, enum ttu_flags flags)  

    struct anon_vma *anon_vma;  

    struct vm_area_struct *vma;  

    unsigned int mlocked = 0;  

    int ret = SWAP_AGAIN;  

    int unlock = TTU_ACTION(flags) == TTU_MUNLOCK;  

    if (MLOCK_PAGES && unlikely(unlock))  

        ret = SWAP_SUCCESS; /* default for try_to_munlock() */  

    /*如果該頁面為匿名映射,傳回該頁面對應的匿名結構*/  

    anon_vma = page_lock_anon_vma(page);  

    if (!anon_vma)  

        return ret;  

    /*這裡可以看出,vma的anon_vma_node字段連結到 

    anon_vma的head字段*/  

    /*掃描線性區描述符的anon_vma連結清單*/  

    list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {  

        if (MLOCK_PAGES && unlikely(unlock)) {  

            if (!((vma->vm_flags & VM_LOCKED) &&  

                  page_mapped_in_vma(page, vma)))  

                continue;  /* must visit all unlocked vmas */  

            ret = SWAP_MLOCK;  /* saw at least one mlocked vma */  

        } else {  

            /*對anon_vma連結清單中的每一個vma線性區描述符 

            調用該函數*/  

            ret = try_to_unmap_one(page, vma, flags);  

            if (ret == SWAP_FAIL || !page_mapped(page))  

                break;  

        }  

        if (ret == SWAP_MLOCK) {  

            mlocked = try_to_mlock_page(page, vma);  

            if (mlocked)  

                break;  /* stop if actually mlocked page */  

    }  

    page_unlock_anon_vma(anon_vma);  

    if (mlocked)  

        ret = SWAP_MLOCK;   /* actually mlocked the page */  

    else if (ret == SWAP_MLOCK)  

        ret = SWAP_AGAIN;   /* saw VM_LOCKED vma */  

    return ret;  

}  

/* 

 * Subfunctions of try_to_unmap: try_to_unmap_one called 

 * repeatedly from either try_to_unmap_anon or try_to_unmap_file. 

 */  

 /** 

    *page是一個指向目标頁描述符的指針; 

    *vma是指向線性區描述符的指針 

    */  

static int try_to_unmap_one(struct page *page, struct vm_area_struct *vma,  

                enum ttu_flags flags)  

    struct mm_struct *mm = vma->vm_mm;  

    unsigned long address;  

    pte_t *pte;  

    pte_t pteval;  

    spinlock_t *ptl;  

    /*計算出待回收頁的線性位址*/  

    address = vma_address(page, vma);  

    if (address == -EFAULT)  

        goto out;  

    /*擷取線性位址對應的頁表項位址*/  

    pte = page_check_address(page, mm, address, &ptl, 0);  

    if (!pte)  

    /* 

     * If the page is mlock()d, we cannot swap it out. 

     * If it's recently referenced (perhaps page_referenced 

     * skipped over this mm) then we should reactivate it. 

     */  

     /*下面為判斷是否可以被回收*/  

    if (!(flags & TTU_IGNORE_MLOCK)) {  

        if (vma->vm_flags & VM_LOCKED) {  

            ret = SWAP_MLOCK;  

            goto out_unmap;  

    if (!(flags & TTU_IGNORE_ACCESS)) {  

        if (ptep_clear_flush_young_notify(vma, address, pte)) {  

            ret = SWAP_FAIL;  

    /* Nuke the page table entry. */  

    flush_cache_page(vma, address, page_to_pfn(page));  

    /*更新頁表項并沖刷相應的TLB*/  

    pteval = ptep_clear_flush_notify(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 */  

    /*更新mm的hiwater_rss*/  

    update_hiwater_rss(mm);  

    if (PageHWPoison(page) && !(flags & TTU_IGNORE_HWPOISON)) {  

        if (PageAnon(page))  

            dec_mm_counter(mm, anon_rss);  

        else  

            dec_mm_counter(mm, file_rss);  

        set_pte_at(mm, address, pte,  

                swp_entry_to_pte(make_hwpoison_entry(page)));  

    } else if (PageAnon(page)) {/*如果是匿名頁*/  

        swp_entry_t entry = { .val = page_private(page) };  

        if (PageSwapCache(page)) {  

            /* 

             * Store the swap location in the pte. 

             * See handle_pte_fault() ... 

             */  

             /*儲存換出位置*/  

            swap_duplicate(entry);  

            if (list_empty(&mm->mmlist)) {  

                spin_lock(&mmlist_lock);  

                if (list_empty(&mm->mmlist))  

                    /*添加到init_mm的相應連結清單,從這裡可以 

                    看出mm->mmlist為交換用的連結清單*/  

                    list_add(&mm->mmlist, &init_mm.mmlist);  

                spin_unlock(&mmlist_lock);  

            }  

        } else if (PAGE_MIGRATION) {  

             * Store the pfn of the page in a special migration 

             * pte. do_swap_page() will wait until the migration 

             * pte is removed and then restart fault handling. 

            BUG_ON(TTU_ACTION(flags) != TTU_MIGRATION);  

            entry = make_migration_entry(page, pte_write(pteval));  

        set_pte_at(mm, address, pte, swp_entry_to_pte(entry));  

        BUG_ON(pte_file(*pte));  

    } else if (PAGE_MIGRATION && (TTU_ACTION(flags) == TTU_MIGRATION)) {  

        /* Establish migration entry for a file page */  

        swp_entry_t entry;  

        entry = make_migration_entry(page, pte_write(pteval));  

    } else  

        dec_mm_counter(mm, file_rss);  

    /*斷開頁表項和實體頁面的關系*/  

    page_remove_rmap(page);  

    /*釋放所配置設定的緩存*/  

    page_cache_release(page);  

out_unmap:  

    pte_unmap_unlock(pte, ptl);  

out:  

        對于給定的實體頁面來說,該函數會根據計算出來的線性位址找到對應的頁表項位址,并更新頁表項。對于匿名頁面來說,換出的位置必須要被儲存下來,以便于該頁面下次被通路的時候可以被換進來。并非所有的頁面都是可以被回收的,比如被 mlock() 函數設定過的記憶體頁,或者最近剛被通路過的頁面,等等,都是不可以被回收的。一旦遇上這樣的頁面,該函數會直接跳出執行并傳回錯誤代碼。如果涉及到頁緩存中的資料,需要設定頁緩存中的資料無效,必要的時候還要置位頁面辨別符以進行資料回寫。該函數還會更新相應的一些頁面使用計數器,比如前邊提到的 _mapcount 字段,還會相應地更新程序擁有的實體頁面數目等。

PFRA具體實作

LRU 連結清單

        在 Linux 中,作業系統對 LRU 的實作主要是基于一對雙向連結清單:active 連結清單和 inactive 連結清單,這兩個連結清單是 Linux 作業系統進行頁面回收所依賴的關鍵資料結構,每個記憶體區域都存在一對這樣的連結清單。顧名思義,那些經常被通路的處于活躍狀态的頁面會被放在 active 連結清單上,而那些雖然可能關聯到一個或者多個程序,但是并不經常使用的頁面則會被放到 inactive 連結清單上。頁面會在這兩個雙向連結清單中移動,作業系統會根據頁面的活躍程度來判斷應該把頁面放到哪個連結清單上。頁面可能會從 active 連結清單上被轉移到 inactive 連結清單上,也可能從 inactive 連結清單上被轉移到 active 連結清單上,但是,這種轉移并不是每次頁面通路都會發生,頁面的這種轉移發生的間隔有可能比較長。那些最近最少使用的頁面會被逐個放到 inactive 連結清單的尾部。進行頁面回收的時候,Linux 作業系統會從 inactive 連結清單的尾部開始進行回收。

用于描述記憶體區域的 struct zone() 中關于這兩個連結清單以及相關的關鍵字段的定義如下所示:

struct zone {   

   ……  

 spinlock_t             lru_lock;     

 struct list_head    active_list;   

 struct list_head    inactive_list;   

 unsigned long       nr_active;   

 unsigned long       nr_inactive;   

}   

各字段含義如下所示:

lru_lock:active_list 和 inactive_list 使用的自旋鎖。

active_list:管理記憶體區域中處于活躍狀态的頁面。

inactive_list:管理記憶體區域中處于不活躍狀态的頁面。

nr_active:active_list 連結清單上的頁面數目。

nr_inactive:inactive_list 連結清單上的頁面數目。

如何在兩個LRU 連結清單之間移動頁面

       Linux 引入了兩個頁面标志符 PG_active 和 PG_referenced 用于辨別頁面的活躍程度,進而決定如何在兩個連結清單之間移動頁面。PG_active 用于表示頁面目前是否是活躍的,如果該位被置位,則表示該頁面是活躍的。PG_referenced 用于表示頁面最近是否被通路過,每次頁面被通路,該位都會被置位。Linux 必須同時使用這兩個标志符來判斷頁面的活躍程度,假如隻是用一個标志符,在頁面被通路時,置位該标志符,之後該頁面一直處于活躍狀态,如果作業系統不清除該标志位,那麼即使之後很長一段時間内該頁面都沒有或很少被通路過,該頁面也還是處于活躍狀态。為了能夠有效清除該标志位,需要有定時器的支援以便于在逾時時間之後該标志位可以自動被清除。然而,很多 Linux 支援的體系結構并不能提供這樣的硬體支援,是以 Linux 中使用兩個标志符來判斷頁面的活躍程度。

Linux 2.6 中這兩個标志符密切合作,其核心思想如下所示:

如果頁面被認為是活躍的,則将該頁的 PG_active 置位;否則,不置位。

當頁面被通路時,檢查該頁的 PG_referenced 位,若未被置位,則置位之;若發現該頁的 PG_referenced 已經被置位了,則意味着該頁經常被通路,這時,若該頁在 inactive 連結清單上,則置位其 PG_active 位,将其移動到 active 連結清單上去,并清除其 PG_referenced 位的設定;如果頁面的 PG_referenced 位被置位了一段時間後,該頁面沒有被再次通路,那麼 Linux 作業系統會清除該頁面的 PG_referenced 位,因為這意味着這個頁面最近這段時間都沒有被通路。

PG_referenced 位同樣也可以用于頁面從 active 連結清單移動到 inactive 連結清單。對于某個在 active 連結清單上的頁面來說,其 PG_active 位被置位,如果 PG_referenced 位未被置位,給定一段時間之後,該頁面如果還是沒有被通路,那麼該頁面會被清除其 PG_active 位,挪到 inactive 連結清單上去。

Linux 中實作在 LRU 連結清單之間移動頁面的關鍵函數如下所示(本文涉及的源代碼均是基于 Linux 2.6.18.1 版本的):

mark_page_accessed():當一個頁面被通路時,則調用該函數相應地修改 PG_active 和 PG_referenced。

page_referenced():當作業系統進行頁面回收時,每掃描到一個頁面,就會調用該函數設定頁面的 PG_referenced 位。如果一個頁面的 PG_referenced 位被置位,但是在一定時間内該頁面沒有被再次通路,那麼該頁面的 PG_referenced 位會被清除。

activate_page():該函數将頁面放到 active 連結清單上去。

shrink_active_list():該函數将頁面移動到 inactive 連結清單上去。

LRU 緩存

        前邊提到,頁面根據其活躍程度會在 active 連結清單和 inactive 連結清單之間來回移動,如果要将某個頁面插入到這兩個連結清單中去,必須要通過自旋鎖以保證對連結清單的并發通路操作不會出錯。為了降低鎖的競争,Linux 提供了一種特殊的緩存:LRU 緩存,用以批量地向 LRU 連結清單中快速地添加頁面。有了 LRU 緩存之後,新頁不會被馬上添加到相應的連結清單上去,而是先被放到一個緩沖區中去,當該緩沖區緩存了足夠多的頁面之後,緩沖區中的頁面才會被一次性地全部添加到相應的 LRU 連結清單中去。Linux 采用這種方法降低了鎖的競争,極大地提升了系統的性能。

LRU 緩存用到了 pagevec 結構,如下所示 :

struct pagevec {   

     unsigned long nr;   

     unsigned long cold;   

     struct page *pages[PAGEVEC_SIZE];   

 };  

         pagevec 這個結構就是用來管理 LRU 緩存中的這些頁面的。該結構定義了一個數組,這個數組中的項是指向 page 結構的指針。一個 pagevec 結構最多可以存在 14 個這樣的項(PAGEVEC_SIZE 的預設值是 14)。當一個 pagevec 的結構滿了,那麼該 pagevec 中的所有頁面會一次性地被移動到相應的 LRU 連結清單上去。

用來實作 LRU 緩存的兩個關鍵函數是 lru_cache_add() 和 lru_cache_add_active()。前者用于延遲将頁面添加到 inactive 連結清單上去,後者用于延遲将頁面添加到 active 連結清單上去。這兩個函數都會将要移動的頁面先放到頁向量 pagevec 中,當 pagevec 滿了(已經裝了 14 個頁面的描述符指針),pagevec 結構中的所有頁面才會被一次性地移動到相應的連結清單上去。

下圖概括總結了上文介紹的如何在兩個連結清單之間移動頁面,以及 LRU 緩存在其中起到的作用:

Linux記憶體管理之頁面回收【轉】

       其中,1 表示函數 mark_page_accessed(),2 表示函數 page_referenced(),3 表示函數 activate_page(),4 表示函數 shrink_active_list()。

        PFRA必須處理多種屬于使用者态程序、磁盤高速緩存和記憶體高速緩存的頁,而且必須遵照幾條試探法準則。PFRA的大部分函數如下:

Linux記憶體管理之頁面回收【轉】

       如上圖在配置設定VFS緩沖區或緩沖區首部時,核心調用free_more_memory();而當從夥伴系統配置設定一個或多個頁框時,調用try_to_free_pages()。

頁面回收關鍵代碼流程圖

Linux記憶體管理之頁面回收【轉】

       上文提到 Linux 中頁面回收主要是通過兩種方式觸發的,一種是由“記憶體嚴重不足”事件觸發的;一種是由背景程序 kswapd 觸發的,該程序周期性地運作,一旦檢測到記憶體不足,就會觸發頁面回收操作。對于第一種情況,系統會調用函數 try_to_free_pages() 去檢查目前記憶體區域中的頁面,回收那些最不常用的頁面。對于第二種情況,函數 balance_pgdat() 是入口函數。

        當 NUMA 上的某個節點的低記憶體區域調用函數 try_to_free_pages() 的時候,該函數會反複調用 shrink_zones() 以及 shrink_slab() 釋放一定數目的頁面,預設值是 32 個頁面。如果在特定的循環次數内沒有能夠成功釋放 32 個頁面,那麼頁面回收會調用 OOM killer 選擇并殺死一個程序,然後釋放它占用的所有頁面。函數 shrink_zones() 會對記憶體區域清單中的所有區域分别調用 shrink_zone() 函數,後者是從記憶體回收最近最少使用頁面的入口函數。

        對于定期頁面檢查并進行回收的入口函數 balance_pgdat() 來說,它主要調用的函數是 shrink_zone() 和 shrink_slab()。從上圖中我們也可以看出,進行頁面回收的兩條代碼路徑最終彙合到函數 shrink_zone() 和函數 shrink_slab() 上。

函數 shrink_zone()

       其中,shrink_zone() 函數是 Linux 作業系統實作頁面回收的最核心的函數之一,它實作了對一個記憶體區域的頁面進行回收的功能,該函數主要做了兩件事情:

将某些頁面從 active 連結清單移到 inactive 連結清單,這是由函數 shrink_active_list() 實作的。

從 inactive 連結清單中標明一定數目的頁面,将其放到一個臨時連結清單中,這由函數 shrink_inactive_list() 完成。該函數最終會調用 shrink_page_list() 去回收這些頁面。

       函數 shrink_page_list() 傳回的是回收成功的頁面數目。概括來說,對于可進行回收的頁面,該函數主要做了這樣幾件事情,其代碼流程圖如下所示:

Linux記憶體管理之頁面回收【轉】

對于匿名頁面來說,在回收此類頁面時,需要将其資料寫入到交換區。如果尚未為該頁面配置設定交換區槽位,則先配置設定一個槽位,并将該頁面添加到交換緩存。同時,将相關的 page 執行個體加入到交換區,這樣,對該頁面的處理就可以跟其他已經建立映射的頁面一樣;

如果該頁面已經被映射到一個或者多個程序的頁表項中,那麼必須找到所有引用該頁面的程序,并更新頁表中與這些程序相關的所有頁表項。在這裡,Linux 2.6 作業系統會利用反向映射機制去檢查哪些頁表項引用了該頁面,關于反向映射的内容在後邊會有介紹;

如果該頁面中的資料是髒的,那麼資料必須要被回寫;

釋放頁緩存中的幹淨頁面。

函數 shrink_slab()

       函數 shrink_slab() 是用來回收磁盤緩存所占用的頁面的。Linux 作業系統并不清楚這類頁面是如何使用的,是以如果希望作業系統回收磁盤緩存所占用的頁面,那麼必須要向作業系統核心注冊 shrinker 函數,shrinker 函數會在記憶體較少的時候主動釋放一些該磁盤緩存占用的空間。函數 shrink_slab() 會周遊 shrinker 連結清單,進而對所有注冊了 shrinker 函數的磁盤緩存進行處理。

       從實作上來看,shrinker 函數和 slab 配置設定器并沒有固定的聯系,隻是目前主要是 slab 緩存使用 shrinker 函數最多。

       注冊 shrinker 是通過函數 set_shrinker() 實作的,解除 shrinker 注冊是通過函數 remove_shrinker() 實作的。目前,Linux 作業系統中主要的 shrinker 函數有如下幾種:

shrink_dcache_memory():該 shrinker 函數負責 dentry 緩存。

shrink_icache_memory():該 shrinker 函數負責 inode 緩存。

mb_cache_shrink_fn():該 shrinker 函數負責用于檔案系統中繼資料的緩存。

具體的源代碼實作細節有時間再做分析。後面将談論交換。

參考:

ULK3

<a href="http://www.ibm.com/developerworks/cn/linux/l-cn-pagerecycle/">http://www.ibm.com/developerworks/cn/linux/l-cn-pagerecycle/</a>

<dl><dt>頂</dt></dl>

【新浪微網誌】 張昺華--sky

【twitter】 @sky2030_

【facebook】 張昺華 zhangbinghua

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.