天天看點

linux記憶體源碼分析

直接記憶體回收中的等待隊列

  記憶體回收詳解見linux記憶體源碼分析 - 記憶體回收(整體流程),在直接記憶體回收過程中,有可能會造成目前需要配置設定記憶體的程序被加入一個等待隊列,當整個node的空閑頁數量滿足要求時,由kswapd喚醒它重新擷取記憶體。這個等待隊列頭就是node結點描述符pgdat中的pfmemalloc_wait。如果目前程序加入到了pgdat->pfmemalloc_wait這個等待隊列中,那麼程序就不會進行直接記憶體回收,而是由kswapd喚醒後直接進行記憶體配置設定。

  直接記憶體回收執行路徑是:

  __alloc_pages_slowpath() -> __alloc_pages_direct_reclaim() -> __perform_reclaim() -> try_to_free_pages() -> do_try_to_free_pages() -> shrink_zones() -> shrink_zone()

  在__alloc_pages_slowpath()中可能喚醒了所有node的kswapd核心線程,也可能沒有喚醒,每個node的kswapd是否在__alloc_pages_slowpath()中被喚醒有兩個條件:

  1. 配置設定标志中沒有__GFP_NO_KSWAPD,隻有在透明大頁的配置設定過程中會有這個标志。
  2. node中有至少一個zone的空閑頁框沒有達到 空閑頁框數量 >= high閥值 + 1 << order + 保留記憶體,或者有至少一個zone需要進行記憶體壓縮,這兩種情況node的kswapd都會被喚醒。

  而在kswapd中會對node中每一個不平衡的zone進行記憶體回收,直到所有zone都滿足 zone配置設定頁框後剩餘的頁框數量 > 此zone的high閥值 + 此zone保留的頁框數量。kswapd就會停止記憶體回收,然後喚醒在等待隊列的程序。

  之後程序由于記憶體不足,對zonelist進行直接回收時,會調用到try_to_free_pages(),在這個函數内,決定了程序是否加入到node結點的pgdat->pfmemalloc_wait這個等待隊列中,如下:

linux記憶體源碼分析
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
                gfp_t gfp_mask, nodemask_t *nodemask)
{
    unsigned long nr_reclaimed;    struct scan_control sc = {        /* 打算回收32個頁框 */
        .nr_to_reclaim = SWAP_CLUSTER_MAX,
        .gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)),        /* 本次記憶體配置設定的order值 */
        .order = order,        /* 允許進行回收的node掩碼 */
        .nodemask = nodemask,        /* 優先級為預設的12 */
        .priority = DEF_PRIORITY,        /* 與/proc/sys/vm/laptop_mode檔案有關
         * laptop_mode為0,則允許進行回寫操作,即使允許回寫,直接記憶體回收也不能對髒檔案頁進行回寫
         * 不過允許回寫時,可以對非檔案頁進行回寫         */
        .may_writepage = !laptop_mode,        /* 允許進行unmap操作 */
        .may_unmap = 1,        /* 允許進行非檔案頁的操作 */
        .may_swap = 1,
    };    /*
     * Do not enter reclaim if fatal signal was delivered while throttled.
     * 1 is returned so that the page allocator does not OOM kill at this
     * point.     */
    /* 當zonelist中擷取到的第一個node平衡,則傳回,如果擷取到的第一個node不平衡,則将目前程序加入到pgdat->pfmemalloc_wait這個等待隊列中 
     * 這個等待隊列會在kswapd進行記憶體回收時,如果讓node平衡了,則會喚醒這個等待隊列中的程序
     * 判斷node平衡的标準:
     * 此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁框數量 是否大于 此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數量,大于則說明node平衡
     * 加入pgdat->pfmemalloc_wait的情況
     * 1.如果配置設定标志禁止了檔案系統操作,則将要進行記憶體回收的程序設定為TASK_INTERRUPTIBLE狀态,然後加入到node的pgdat->pfmemalloc_wait,并且會設定逾時時間為1s 
     * 2.如果配置設定标志沒有禁止了檔案系統操作,則将要進行記憶體回收的程序加入到node的pgdat->pfmemalloc_wait,并設定為TASK_KILLABLE狀态,表示允許 TASK_UNINTERRUPTIBLE 響應緻命信号的狀态 
     * 傳回真,表示此程序加入過pgdat->pfmemalloc_wait等待隊列,并且已經被喚醒
     * 傳回假,表示此程序沒有加入過pgdat->pfmemalloc_wait等待隊列     */
    if (throttle_direct_reclaim(gfp_mask, zonelist, nodemask))        return 1;

    trace_mm_vmscan_direct_reclaim_begin(order,
                sc.may_writepage,
                gfp_mask);    /* 進行記憶體回收,有三種情況到這裡 
     * 1.目前程序為核心線程
     * 2.最優node是平衡的,目前程序沒有加入到pgdat->pfmemalloc_wait中
     * 3.目前程序接收到了kill信号     */
    nr_reclaimed = do_try_to_free_pages(zonelist, &sc);

    trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);    return nr_reclaimed;
}      
linux記憶體源碼分析

  主要通過throttle_direct_reclaim()函數判斷是否加入到pgdat->pfmemalloc_wait等待隊列中,主要看此函數:

linux記憶體源碼分析
  throttle_direct_reclaim(gfp_t gfp_mask,  zonelist *zonelist,
                    nodemask_t *nodemask)
{     zoneref *z;     zone *zone;
    pg_data_t *pgdat = NULL;    
     (current->flags & PF_KTHREAD)         ;    
     (fatal_signal_pending(current))         ;

    
    for_each_zone_zonelist_nodemask(zone, z, zonelist,
                    gfp_mask, nodemask) {        
         (zone_idx(zone) > ZONE_NORMAL)            ;        
        pgdat = zone->zone_pgdat;        
         (pfmemalloc_watermark_ok(pgdat))             ;        ;
    }     (!pgdat)         ;

    count_vm_event(PGSCAN_DIRECT_THROTTLE);     (!(gfp_mask & __GFP_FS)) {        
        wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
            pfmemalloc_watermark_ok(pgdat), HZ);         check_pending;
    }    
    
    wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
        pfmemalloc_watermark_ok(pgdat));

check_pending:    
    
    
     (fatal_signal_pending(current))         ;:     ;
}      
linux記憶體源碼分析

  有四點需要注意:

  1. 目前程序已經接收到kill信号,則不會将其加入到pgdat->pfmemalloc_wait中。
  2. 隻擷取第一個node,也就是目前程序最希望從此node中配置設定到記憶體。
  3. 判斷一個node是否平衡的條件是:此node的ZONE_NORMAL和ZONE_DMA兩個區的空閑頁框數量 > 此node的ZONE_NORMAL和ZONE_DMA兩個區的平均min閥值。如果不平衡,則加入到pgdat->pfmemalloc_wait等待隊列中,如果平衡,則直接傳回,并由目前程序自己進行直接記憶體回收。
  4. 如果目前程序配置設定記憶體時使用的标志沒有__GFP_FS,則加入pgdat->pfmemalloc_wait中會有一個逾時限制,為1s。并且加入後的狀态是TASK_INTERRUPTABLE。
  5. 其他情況的程序加入到pgdat->pfmemalloc_wait中沒有逾時限制,并且狀态是TASK_KILLABLE。

  如果程序加入到了node的pgdat->pfmemalloc_wait等待隊列中。在此node的kswapd進行記憶體回收後,會通過再次判斷此node是否平衡來喚醒這些程序,如果node平衡,則喚醒這些程序,否則不喚醒。實際上,不喚醒也說明了node沒有平衡,kswapd還是會繼續進行記憶體回收,最後kswapd實在沒辦法讓node達到平衡水準下,會在kswapd睡眠前,将這些程序全部進行喚醒。

zone的保留記憶體

  之前很多地方說明到,判斷一個zone是否達到閥值,主要通過zone_watermark_ok()函數實作的,而在此函數中,又以 zone目前空閑記憶體 >= zone閥值(min/low/high) + 1 << order + 保留記憶體 這個公式進行判斷的,而對于zone閥值和1<<order都很好了解,這裡主要說說最後的那個保留記憶體。我們知道,如果打算從ZONE_HIGHMEM進行記憶體配置設定時,使用的zonelist是ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA,當ZONE_HIGHMEM沒有足夠記憶體時,就會去ZONE_NORMAL和ZONE_DMA進行配置設定,這樣會造成一種情況,有可能ZONE_NORMAL和ZONE_DMA的記憶體都被本應該從ZONE_HIGHMEM配置設定的記憶體占完了,特定需要從ZONE_NORMAL和ZONE_DMA配置設定的記憶體則無法進行配置設定,是以核心設計了一個功能,就是讓其他ZONE記憶體不足時,可以在本ZONE進行記憶體配置設定,但是必須保留一些頁框出來,不能讓你所有都用完,而應該從本ZONE進行配置設定的時候則沒辦法擷取頁框,這個值就儲存在struct zone中:

linux記憶體源碼分析
struct zone
{
    ......    long lowmem_reserve[MAX_NR_ZONES];

    ......
}      
linux記憶體源碼分析

  注意是以數組儲存這個必須保留的頁框數量的,而數組長度是node中zone的數量,為什麼要這樣組織,其實很好了解,對于對不同的zone進行的記憶體配置設定,如果因為目标zone的記憶體不足導緻從本zone進行配置設定時,會因為目标zone的不同而保留的頁框數量不同,比如說,一次記憶體配置設定本來打算在ZONE_HIGHMEM進行配置設定,然後記憶體不足,最後到了ZONE_DMA進行配置設定,這時候ZONE_DMA使用的保留記憶體數量就是lowmem_reserve[ZONE_HIGHMEM];而如果一次記憶體配置設定是打算在ZONE_NORMAL進行配置設定,因為記憶體不足導緻到了ZONE_DMA進行配置設定,這時候ZONE_DMA使用的保留記憶體數量就是lowmem_reserve[ZONE_NORMAL]。這樣,對于這兩次記憶體配置設定過程中,當對ZONE_DMA調用zone_watermark_ok()進行閥值判斷能否從ZONE_DMA進行記憶體配置設定時,公式就會變為 zone目前空閑記憶體 >= zone閥值(min/low/high) + 1 << order + lowmem_reserve[ZONE_HIGHMEM] 和 zone目前空閑記憶體 >= zone閥值(min/low/high) + 1 << order + lowmem_reserve[ZONE_NORMAL]。這樣就可能會因為lowmem_reserve[ZONE_HIGHMEM]和lowmem_reserve[ZONE_NORMAL]的不同,導緻一種能夠順利從ZONE_DMA配置設定到記憶體,另一種不能夠。而對于本來就打算從本zone進行記憶體配置設定時,比如本來就打算從ZONE_DMA進行記憶體配置設定,就會使用lowmem_reserve[ZONE_DMA],而由于zone本來就是ZONE_DMA,是以ZONE_DMA的lowmem_reserve[ZONE_DMA]為0,也就是,當打算從ZONE_DMA進行記憶體配置設定時,會使用zone_watermark_ok()判斷ZONE_DMA是否達到閥值,而判斷公式中的保留記憶體lowmem_reverve[ZONE_DMA]是為0的。同理,當本來就打算從ZONE_NORMAL進行記憶體配置設定,并通過zone_watermark_ok()對ZONE_NORMAL進行閥值判斷時,會使用ZONE_NORMAL區的lowmem_reserve[ZONE_NORAML],這個值也是0。對于ZONE_NORMAL區而言,它的lowmem_reserve[ZONE_DMA]和lowmem_reserve[ZONE_NORMAL]為0,因為需要從ZONE_DMA進行記憶體配置設定時,即使記憶體不足也不會到ZONE_NORMAL進行配置設定,而由于自己又是ZONE_NORMAL區,所有這兩個數為0;而對于ZONE_HIGHMEM,它的lowmem_reserve[]中所有值都為0,它不必為其他zone限制保留記憶體,因為其他zone當記憶體不足時不會到ZONE_HIGHMEM中進行嘗試配置設定記憶體。

  可以通過cat /proc/zoneinfo檢視這個數組中的值為多少:

linux記憶體源碼分析

  這個是我的ZONE_DMA的區的參數,可以看到,對應ZONE_DMA就是為0,然後ZONE_NORMAL和ZONE_HIGHMEM都為1854,最後一個是虛拟的zone,叫ZONE_MOVABLE。

  對于zone保留記憶體的多少,可以通過/proc/sys/vm/lowmem_reserve_ratio進行修改。具體可見核心文檔Documentation/sysctl/vm.txt,我的系統預設的lowmem_reserve_ratio如下:

linux記憶體源碼分析

  這個256和32代表的是1/256和1/32。而第一個256用于代表DMA區的,第二個256代表NORMAL區的,第三個32代表HIGHMEM區的,計算公式是:

ZONE_DMA對于ZONE_NORMAL配置設定需要保留的記憶體:

  zone_dma->lowmem_reserve[ZONE_NORMAL] = zone_normal.managed / lowmem_reserve_ratio[ZONE_DMA]

ZONE_DMA對于ZONE_HIGHMEM配置設定需要保留的記憶體:

  zone_dma->lowmem_reserve[ZONE_HIGHMEM] = zone_highmem.managed / lowmem_reserve_ratio[ZONE_DMA]

drop_caches

  在/proc/sys/vm/目錄下有個drop_caches檔案,可以通過将1,2,3寫入此檔案達到記憶體回收的效果,這三個值會達到不同的效果,效果如下:

  • echo 1 > /proc/sys/vm/drop_caches:對幹淨的pagecache進行記憶體回收
  • echo 2 > /proc/sys/vm/drop_caches:對空閑的slab進行記憶體回收
  • echo 3 > /proc/sys/vm/drop_caches:對幹淨的pagecache和slab進行記憶體回收

  注意隻會對幹淨的pagecache和空閑的slab進行回收。幹淨的pagecache就是頁中的資料與磁盤對應檔案一緻,沒有被修改過的頁,對于髒的pagecache,是不會進行回收的。而空閑的slab指的就是沒有被配置設定給核心子產品使用的slab。

  先看看/proc/meminfo檔案:

linux記憶體源碼分析

  主要注意Buffers、Cached、Slab、Shmem這四行。

  再看看free -k指令:

linux記憶體源碼分析

  我們主要關注shared和buff/cache這兩列。

  • shared:記錄的是目前系統中共享記憶體使用的記憶體數量,對應meminfo的Shmem行。
  • buff/cache:記錄的是目前系統中buffers、cached、slab總共使用的記憶體數量。對應于meminfo中的Buffers + Cached + Slab。

  然後我們通過free -k看看目前系統在使用echo 3 > /proc/sys/vm/drop_caches清空了pagecache和slab之後的情況:

linux記憶體源碼分析

  這裡為什麼buff/cache在echo 3 > /proc/sys/vm/drop_caches後沒有被清0,因為之前也說了,drop_caches隻回收空閑的pagecache和空閑的slab。

  之後使用shmem建立一個100M的shmem共享記憶體,再通過echo 3 > /proc/sys/vm/drop_caches清空pagecache和slab之後的情況:

linux記憶體源碼分析

  很明顯看出來,兩次的shared相差102400,兩次的buff/cache相差102364。也就是這段shmem共享記憶體沒有被drop_caches回收,實際上,mmap共享記憶體也是與shmem相同的情況,也就是說,共享記憶體是不會被drop_caches進行回收的。實際總體上drop_caches不會回收以下記憶體:

  1. 正在使用的slab,一些空閑的slab會被drop_caches回收
  2. 匿名mmap共享記憶體和檔案映射mmap共享記憶體使用的頁,不會被drop_caches回收,但是在記憶體不足時會被記憶體回收換出
  3. shmem共享記憶體使用的頁,不會被drop_caches回收,但是在記憶體不足時會被記憶體回收換出
  4. tmpfs使用的頁
  5. 被mlock鎖在記憶體中的pagecache
  6. 髒的pagecache

  有些人會很好奇,為什麼shmem和mmap共享記憶體使用的頁都算成了pagecache,而在linux記憶體源碼分析 - 記憶體回收(lru連結清單)中明确說明了,shmem和匿名mmap共享記憶體使用的頁會加到匿名頁lru連結清單中的,在匿名頁lru連結清單中的頁有個特點,在要被換出時,會加入到swapcache中,然後再進行換出。實際上,這兩種類型的共享記憶體,在建立時,系統會為它們配置設定一個沒有映射到磁盤上的inode結點,當然也會有inode對應pagecache,但是它們會設定PG_swapbacked,并加入到匿名頁lru連結清單中,當對它們進行換出時,它們會加入到swapcache中進行換出。也就是在沒有被換出時,這些沒有指定磁盤檔案的共享記憶體,是有屬于自己的inode和pagecache的,是以系統也将它們算作了pagecache。而對于檔案映射mmap共享記憶體,它本來就有對應的檔案inode和pagecache,是以算到pagecache中也理所應當了。

  以上說了為什麼shmem和匿名mmap被系統算作pagecache,這裡再說說為什麼drop_caches沒有對它們這種pagecache進行釋放。首先,drop_caches的實作原理時,周遊系統中所有挂載了的檔案系統的超級塊struct super_block,對每個檔案系統的超級塊中的每個檔案的inode進行周遊,然後再對每個檔案inode中的所有page進行周遊,然後将能夠回收的回收掉,實際上,嚴格地判斷pagecache能否回收的條件如下:

  1. 标記了PG_dirty的髒pagecache不能回收
  2. 有程序頁表項映射的pagecache不能回收
  3. 被mlock鎖在記憶體中的pagecache不能回收
  4. 标記了PG_writeback的正在回寫的pagecache不能回收
  5. 沒有buffer_head并且page->_count不等于2的pagecache不能進行回收
  6. 有buffer_head并且page->_count不等于3的pagecache不能進行回收

  到這裡,實際上已經很清晰了,對于隻有write、read進行讀寫的檔案,隻要它是幹淨并且沒有被mlock鎖在記憶體中,基本上都是能夠回收的,因為write、read系統調用不會讓程序頁表映射此pagecache。而相反,mmap和shmem都會讓程序頁表映射到pagecache,這樣當某個pagecache被使用了mmap或shmem的程序映射時,這個pagecache就不能夠進行回收了。而shmem相對于mmap還有所不同,mmap在程序退出時,會取消映射,這時候這些被mmap取消映射的pagecache是可以進行回收的,但是當一個程序使用shmem進行共享記憶體時,然後程序退出,shmem共享記憶體使用的pagecache也還是不能夠被drop_caches進行回收,原因是shmem共享記憶體使用的pagecache的page->_count為4,不為上面的2或者3的情況,具體shmem共享記憶體的pagecache被哪個子產品引用了,還待排查。總的來說,mmap使用的頁不能夠回收是因為有程序映射了此頁,shmem使用的頁不能夠回收是因為有其他子產品引用了此頁。

  再說說tmpfs,tmpfs中的檔案頁也是不能夠被drop_caches回收的,原因是tmpfs中的檔案頁生來就是髒頁,而又因為它們在磁盤上沒有對應的具體磁盤檔案,是以tmpfs中的檔案頁不會被回寫到磁盤,又因為生來是髒頁,是以tmpfs的檔案頁在記憶體充足的情況下,它的整個生命周期都為髒頁,是以不會被drop_caches回收。

繼續閱讀