Linux內存管理--內存回收
內存的回收在Linux內存管理中占據非常重要的地位,系統的內存畢竟是有限的,跑的進程成百上千,系統內存越來越小,我們必須選擇一些內存進行回收,以滿足別的任務的需求。在內存回收過程中,有哪些內存可以回收,什么時候進行回收,回收內存時如何盡可能的減少對系統性能的影響,回收內存的策略,這些是我們著重要關注的問題,也是本文主要闡述的重點。
1.1 內存回收的目標
不是所有的物理內存都可以參與回收的,比如要是把內核代碼段的內存給回收了,系統就無法正常運行了,一般內核代碼段,數據段,內核kmalloc()出來的內存,內核線程占用的內存等都是不可以回收的,除此之外的內存都是我們要回收的目標。
回收的內存主要是由用戶態進程占用的內存和內核自己在運行時所使用的一些內存組成。用戶態進程占用的內存主要是我們常見的進程代碼段,數據段,堆棧等,內核運行使用的內存主要是磁盤高速緩存(如索引節點,目錄項高速緩存),頁面高速緩存(訪問文件時系統生成的頁面cache),mmap()文件時所用的有名映射所使用的物理內存。后面的這些內才能雖然也是內核管理使用的內存,但對其進行回收的時候,頂多影響內核的性能,而不會導致系統無法運行。
1.2 內存回收的時機
1、內存緊缺回收:grow_buffers()無法獲取緩沖區頁,alloc_page_buffers()無法獲取頁臨時緩沖區首部,__alloc_pages()無法再給定的內存區分配一組連續頁框。
2、周期回收:必要時,激活相應內核線程執行內存回收算法:kswapd()內核線程,檢查某個內存管理區的空閑頁框數是否已低于pages_high值的標高。events內核線程,一個工作者線程,回收位于高速內存緩存中的所有空閑的slab。
1.3 內存回收的策略
1.3.1 內存回收的分類
內存回收主要是要回收兩類內存:最近最少使用的內存以及高速內存緩存中空閑的slab。前者主要包括用戶態進程的代碼段,數據段,堆棧,文件映射內存,頁高速內存,后者主要包括磁盤高速緩存及一些其他的空閑內存高速緩存。
最近最少使用內存存放在一個lru鏈表上,每個內存管理區zone都有一個lru結構,里面含有active和inactive兩個鏈表頭,active鏈表上記錄當前的活躍的報文,inactive用來記錄當前不活躍的報文。一般我們回首lru上的inactive鏈表上的內存頁。同時,在內存回收的過程中,會從active鏈表向inactive鏈表上補充對應的最近最少使用內存頁。每個內存頁的內核數據結構page上有一個標記位PG_referenced,該標記位使得一個頁從"不活動“狀態轉為”活動“狀態的時間加倍,反之亦然。比如:一個頁面可能1個小時內沒人反應,不能因為偶然的一次訪問就認為它是活躍的,得兩次才認為它是一個活躍的頁面。下面是頁面在inactive和active鏈表上轉移的變化圖。
Slab內存高速緩存中經常會有一些完全空閑的slab,這些是我們回收的另一個目標。
1.3.2 反向映射
對于可以通過用戶態線性地址空間可以直接訪問到的物理頁來說,可以分為匿名頁和文件映射頁兩類,匿名頁指的是不與具體文件對應映射的物理頁,比如代碼段,堆棧等使用的物理頁,映射頁指的是映射到文件某一部分的物理頁,通常使用mmap()來進行相關的映射。
對于匿名映射和文件映射來說,可能一段物理內存會在多個進程的頁表中使用,比如對于匿名映射,fork()一個進程,一開始會共用父進程的物理內存,對于文件映射,多個進程可能同時映射到一個文件的同一部分文件。所以在頁面回收時,需要將該頁面在所有的頁表引用中給去除掉。這種手段稱為反向映射。想要找到使用這些物理頁的頁表項的話,需要先找到引用他們的頁表,而頁表的地址記錄在每個進程的內存描述符里面,同時用來描述進程用戶態地址空間的每個vm_area_struct都記錄了一個指針,指向所屬的內存描述符。因此只要通過物理頁找到引用他們的vm_area_struct,就能找到內存描述符,從而找到頁表,找到對應的頁表項。
匿名頁的反向映射:
對于匿名頁來說,每個頁面的mapping字段指向一個anon_vma描述符,anon_vma描述符中存在一個鏈表頭,所有引用該頁面的vm_area_struct都存放在里面。page,anon_vma,vm_area_struct這些數據結構的關系如下圖所示:
對于匿名頁來說,其被別的地址空間引用,基本上都是因為fork()進程時,子進程復制父進程的地址空間,從而被引用的。各個vm_area_struct加入anon_vma的鏈表的過程如下:
假設當前一個進程p,后來fork出一個子進程c。
1、當進程P為某個vm_area_struct加入***個物理頁時,比如說發生了缺頁異常,動態分配一個anon_vma的數據結構,將vm_area_struct加入該anon_vma所管理的鏈表,vm_area_struct結構中的anon_vma字段指向該anon_vma,同時把該頁面中的mapping字段賦值為該anon_vma.對于后續為該vm_area_struct申請的物理頁面,其mapping字段都賦值為該anon_vma。
2、當該進程p執行fork()時,在fork的處理過程中,會調用dup_mmap()來復制進程p的線性地址空間,在dup_mmap()會復制進程p的每一個vm_area_struct,加入到自己的地址空間中,并將vm_area_struct加入到anon_vma所管理的鏈表中,參看anon_vma_link()。此時為進程p申請的頁面被進程c共享,通過頁面的mapping字段可以找到anon_vma,從anon_vma可以遍歷進程p,c。
3、考慮一個問題,在進程c中才觸發缺頁異常被申請的內存頁,其mmapping被賦值為所屬vm_area_struct的vma_anon,但進程p并沒有使用到該頁,所以一個物理頁mapping字段指向的vma_anon所下掛的vm_area_struct可能并不包含該物理頁。
文件映射頁的反向映射:
對于每個文件映射頁,其page mapping字段指向的是對應文件的address_space數據結構,address_space中有個 struct prio_tree_root i_mmap 字段,指向一個優先樹,優先樹里面會把所有映射該文件內容的vm_area_struct 給組織起來。在該樹中,其樹的節點基地址和堆地址分別是映射的文件內容的起始地址和結束地址,要是多個進程同時映射該地址段,會用鏈表在該節點上將vm_area_struct串起來。
1.3.3 內存回收流程介紹
睡眠回收我們不關注,主要介紹內存緊缺回收及周期回收:
1、內存緊缺回收主要函數是try_to_free_pages(),該函數會執行一個循環,按照優先級從12到0,依次調用shrink_caches(),shrink_slab()來回收頁面,直到回收至少32個內存頁面。
依次調用以下輔助函數:
shrink_caches():調用shrink_zone()對傳入的zone鏈表中的每個zone,進行lru上面的頁面回收。
shrink_slab():對磁盤索引節點cache和目錄項索引節點等磁盤高速緩存進行回收,由于磁盤索引節點和目錄項索引節點都是從slab高速緩存中分配的,這樣就會導致空閑slab的產生,空閑slab后續會在周期性回收的cache_reap工作隊列中被回收。估計也就是因為最終會清零空閑slab,才會起這么一個函數名。^_^
shrink_zone():對內存管理區上的lru鏈表中的非活躍頁面進行回收,在非活躍頁面不足的時候,調用refill_inactive_zone()對lru上的inactive鏈表補充非活躍頁面,同時shrink_zone()調用shrink_cache()來進行頁面的回收,該函數的具體解析可以參照下面的源碼淺析。
shrink_list():該輔助函數在shrink_cache()中被調用,該函數對在shrink_cache()中傳入的非活躍page列表進行遍歷,對每個頁面進行回收工作,該函數的具體解析可以參考下面的源碼解析。
refill_inactie_zone():該輔助函數根據一定的規則將處于lru active鏈表上的活躍頁面移動到inactive鏈表上,以補充可以回收的頁面,在lru鏈表里有兩類頁,一類是屬于用戶態空間的頁,比如用戶態進程的代碼段,數據段,一類是在頁高速緩存中的頁,系統為了降低對應用程序的影響,將要優先將頁高速緩存頁進行回收,同時為了系統整體性能也會適當回收用戶態進程頁。按照如下經驗公式進行選擇:
交換傾向值=映射比率/2+負荷值+交換值
2、kswapd進程一般會在系統中睡眠,但當__alloc_page()發現各個管理區的剩余頁面都低于警告值(由內存管理描述符的pages_low字段和protection字段推算出來)時,會激活kswapd進程進行頁面回收,直到回收的頁面使得管理區的剩余頁面高于zone->pages_high時才停止回收,本質上也是調用了shrink_zone()和shrink_slab()。
3、cache_reap工作隊列定期運行來回收slab高速緩存中空閑的slab占用的頁。
#p#
1.4 相關源代碼的淺析
- static void
- shrink_zone(struct zone *zone, struct scan_control *sc)
- {
- unsigned long nr_active;
- unsigned long nr_inactive;
- //根據優先級,得到可以掃描的頁面數,優先級越高,
- //代表越不急迫,可以掃描的頁面數也最少
- zone->nr_scan_active += (zone->nr_active >> sc->priority) + 1;
- nr_active = zone->nr_scan_active;
- if (nr_active >= SWAP_CLUSTER_MAX)
- zone->nr_scan_active = 0;
- else
- nr_active = 0;
- zone->nr_scan_inactive += (zone->nr_inactive >> sc->priority) + 1;
- nr_inactive = zone->nr_scan_inactive;
- //非活動頁比較少的話,可以先忽略過去,將跳過的頁面記錄到nr_scan_inactive中
- //留待下一次再處理
- if (nr_inactive >= SWAP_CLUSTER_MAX)
- zone->nr_scan_inactive = 0;
- else
- nr_inactive = 0;
- //設置需要回收的頁面數為32個
- sc->nr_to_reclaim = SWAP_CLUSTER_MAX;
- //開始回收頁面,每次掃描32個頁面,多了不干噢!!!
- while (nr_active || nr_inactive) {
- if (nr_active) {
- //設置每次要掃描的非活動頁面數,需要將其放
- //入到inactive list里面
- sc->nr_to_scan = min(nr_active,
- (unsigned long)SWAP_CLUSTER_MAX);
- nr_active -= sc->nr_to_scan;
- //補充inactive list中的頁面
- refill_inactive_zone(zone, sc);
- }
- if (nr_inactive) {
- //設置每次將要掃描的頁面,最多也就32個頁面
- sc->nr_to_scan = min(nr_inactive,
- (unsigned long)SWAP_CLUSTER_MAX);
- nr_inactive -= sc->nr_to_scan;
- //開始正式回收inactive list中的頁面
- shrink_cache(zone, sc);
- //32個頁面被回收完畢,大功告成了!!!
- if (sc->nr_to_reclaim <= 0)
- break;
- }
- }
- }
- static int shrink_list(struct list_head *page_list, struct scan_control *sc)
- {
- LIST_HEAD(ret_pages);
- struct pagevec freed_pvec;
- int pgactivate = 0;
- int reclaimed = 0;
- //有進程需要調度,先進行調度
- cond_resched();
- pagevec_init(&freed_pvec, 1);
- //對于page_list 鏈表上的每一個頁面試圖進行回收
- while (!list_empty(page_list)) {
- struct address_space *mapping;
- struct page *page;
- int may_enter_fs;
- int referenced;
- //獲取一個頁面
- page = lru_to_page(page_list);
- //從lru上摘除
- list_del(&page->lru);
- //page被鎖定,不能回收
- if (TestSetPageLocked(page))//page is locked?
- goto keep;
- BUG_ON(PageActive(page));
- //page正在被writeback,不能回收
- if (PageWriteback(page))//page is writeback?
- goto keep_locked;
- sc->nr_scanned++;
- /* Double the slab pressure for mapped and swapcache pages */
- if (page_mapped(page) || PageSwapCache(page))
- sc->nr_scanned++;
- //查看最近該頁面有無被訪問過
- referenced = page_referenced(page, 1, sc->priority <= 0);
- /* In active use or really unfreeable? Activate it. */
- //1頁面被訪問過,2頁面在用戶態空間,頁面是文件映射頁面,
- //頁面在交換高速緩存中,同時滿足這兩個條件的話,頁面不被回收
- if (referenced && page_mapping_inuse(page))
- goto activate_locked;
- #ifdef CONFIG_SWAP
- //page is anon and page has not been add to swapcache
- //該頁面是匿名映射的頁面,且該頁面不在swapcache中
- if (PageAnon(page) && !PageSwapCache(page)) {
- //將頁面加入到swap cache中
- if (!add_to_swap(page))
- goto activate_locked;
- }
- #endif /* CONFIG_SWAP */
- //得到對應的address_space,有可能是對應文件的address_space,或者是
- //swap cache的address_space
- mapping = page_mapping(page);
- may_enter_fs = (sc->gfp_mask & __GFP_FS) ||
- (PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));
- //該頁面被映射到某個用戶頁表中
- if (page_mapped(page) && mapping) {
- //將該頁面在用戶頁表中的頁表項通通清除
- switch (try_to_unmap(page)) {
- case SWAP_FAIL:
- goto activate_locked;
- case SWAP_AGAIN:
- goto keep_locked;
- case SWAP_SUCCESS:
- ; /* try to free the page below */
- }
- }
- //頁面是臟的,哈哈,準備往文件或swapcache里面寫硬盤吧
- if (PageDirty(page)) {
- if (referenced)
- goto keep_locked;
- if (!may_enter_fs)
- goto keep_locked;
- if (laptop_mode && !sc->may_writepage)
- goto keep_locked;
- /* Page is dirty, try to write it out here */
- //往磁盤上寫頁面
- switch(pageout(page, mapping)) {
- case PAGE_KEEP:
- goto keep_locked;
- case PAGE_ACTIVATE:
- goto activate_locked;
- case PAGE_SUCCESS:
- if (PageWriteback(page) || PageDirty(page))
- goto keep;
- if (TestSetPageLocked(page))
- goto keep;
- if (PageDirty(page) || PageWriteback(page))
- goto keep_locked;
- mapping = page_mapping(page);
- case PAGE_CLEAN:
- ; /* try to free the page below */
- }
- }
- //若頁面是緩沖區頁面,將對應的buffer_head給釋放掉
- if (PagePrivate(page)) {
- if (!try_to_release_page(page, sc->gfp_mask))
- goto activate_locked;
- if (!mapping && page_count(page) == 1)
- goto free_it;
- }
- if (!mapping)
- goto keep_locked;
- /* truncate got there first */
- spin_lock_irq(&mapping->tree_lock);
- //頁面為臟頁面或者page的引用計數為2,都是不可以回收的
- if (page_count(page) != 2 || PageDirty(page)) {
- spin_unlock_irq(&mapping->tree_lock);
- goto keep_locked;
- }
- #ifdef CONFIG_SWAP
- //到達這里,說明該page只被swap cache或者頁高速緩存及
- //fpra所共有,需要將其從swap cache上或者頁高速緩存上刪除。
- if (PageSwapCache(page)) {
- swp_entry_t swap = { .val = page->private };
- //從swap cache上進行刪除
- __delete_from_swap_cache(page);
- spin_unlock_irq(&mapping->tree_lock);
- swap_free(swap);
- __put_page(page);
- /* The pagecache ref */
- goto free_it;
- }
- #endif /* CONFIG_SWAP */
- //從頁面高速緩存中將該頁面刪除
- __remove_from_page_cache(page);
- spin_unlock_irq(&mapping->tree_lock);
- __put_page(page);
- free_it:
- unlock_page(page);
- reclaimed++;
- if (!pagevec_add(&freed_pvec, page))
- __pagevec_release_nonlru(&freed_pvec);
- continue;
- activate_locked:
- //將頁面設為active頁面,等回去將其放入lru的active鏈表
- SetPageActive(page);
- pgactivate++;
- keep_locked:
- //保持頁面的狀態不變,放入對應的lru active或inactive鏈表中
- unlock_page(page);
- keep:
- //將該無法回收的頁面,放入到ret_pages鏈表中
- list_add(&page->lru, &ret_pages);
- BUG_ON(PageLRU(page));
- }
- //此處將無法回收的頁面放入page_list中,在函數返回后,去其進行處理
- list_splice(&ret_pages, page_list);
- //此處將可以釋放的頁面通通給釋放掉,回收了^_^
- if (pagevec_count(&freed_pvec))
- __pagevec_release_nonlru(&freed_pvec);
- mod_page_state(pgactivate, pgactivate);
- sc->nr_reclaimed += reclaimed;
- return reclaimed;
- }