Linux內核-內存-分區頁框分配器


簡介

分區頁框分配器處理對連續頁框組的內存分配請求,主要組成如下圖所示:

這里寫圖片描述

管理區分配器接受動態內存分配與釋放的請求,它首先從每CPU頁框高速緩存中請求頁框,若無法滿足才從伙伴系統中請求分配。


源碼分析(Linux2.6/mm/page_alloc.c)

  1. buffered_rmqueue()

    此函數從指定內存管理區中分配頁框,對應上圖中的分配機制。

     static struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
    {
    unsigned long flags;
    struct page *page = NULL;
    int cold = !(gfp_flags & __GFP_COLD);

    /**
    * 如果order!=0,則每CPU頁框高速緩存就不能被使用,因為每CPU頁框高速緩存是對單個頁框的管理
    */

    if (order == 0) {
    struct per_cpu_pages *pcp;

    /**
    * 檢查由__GFP_COLD標志所標識的內存管理區本地CPU高速緩存是否需要被補充。
    * 其count字段小於或者等於low
    */

    pcp = &zone->pageset[get_cpu()].pcp[cold];
    local_irq_save(flags);
    /**
    * 當前緩存中的頁框數低於low,需要從伙伴系統中補充頁框。
    * 調用rmqueue_bulk函數從伙伴系統中分配batch個單一頁框
    * rmqueue_bulk反復調用__rmqueue,直到緩存的頁框達到low。
    */

    if (pcp->count <= pcp->low)
    pcp->count += rmqueue_bulk(zone, 0,
    pcp->batch, &pcp->list);
    /**
    * 如果count為正,函數從高速緩存鏈表中獲得一個頁框。
    * count減1
    */

    if (pcp->count) {
    page = list_entry(pcp->list.next, struct page, lru);
    list_del(&page->lru);
    pcp->count--;
    }
    local_irq_restore(flags);
    /**
    * 沒有和get_cpu配對使用呢?
    * 這就是內核,外層一定調用了get_cpu。這種代碼看起來頭疼。
    */

    put_cpu();
    }

    /**
    * 內存請求沒有得到滿足,或者是因為請求跨越了幾個連續頁框,或者是因為被選中的頁框高速緩存為空。
    * 調用__rmqueue函數(因為已經保護了,直接調用__rmqueue即可)從伙伴系統中分配所請求的頁框(見伙伴系統一文)
    */

    if (page == NULL) {
    spin_lock_irqsave(&zone->lock, flags);
    page = __rmqueue(zone, order);
    spin_unlock_irqrestore(&zone->lock, flags);
    }

    /**
    * 如果內存請求得到滿足,函數就初始化(第一個)頁框的頁描述符
    */

    if (page != NULL) {
    BUG_ON(bad_range(zone, page));
    /**
    * 將第一個頁清除一些標志,將private字段置0,並將頁框引用計數器置1。
    */

    mod_page_state_zone(zone, pgalloc, 1 << order);
    prep_new_page(page, order);

    /**
    * 如果__GFP_ZERO標志被置位,則將被分配的區域填充0。
    */

    if (gfp_flags & __GFP_ZERO)
    prep_zero_page(page, order, gfp_flags);

    if (order && (gfp_flags & __GFP_COMP))
    prep_compound_page(page, order);
    }
    return page;
    }
  2. __alloc_pages()

    __alloc_pages()函數是管理區分配器的核心,請求頁框的函數,比如alloc_page()是對__alloc_pages()的封裝,該函數比較長,但是不算難理解,耐心點看完吧。該函數接收3個參數:

    • gfp_mask:在內存分配請求中指定的標志
    • order: 連續分配的頁框數量的對數(實際分配的是2^order個連續的頁框)
    • zonelist: zonelist數據結構的指針。該結構按優先次序描述了適於內存分配的內存管理區。它實際上是指向zone數組,存放本節點和其他節點的同類型管理區

    代碼如下:

    struct page * fastcall
    __alloc_pages(unsigned int gfp_mask, unsigned int order,
    struct zonelist *zonelist)
    {
    const int wait = gfp_mask & __GFP_WAIT;
    struct zone **zones, *z;
    struct page *page;
    struct reclaim_state reclaim_state;
    struct task_struct *p = current;
    int i;
    int classzone_idx;
    int do_retry;
    int can_try_harder;
    int did_some_progress;

    might_sleep_if(wait);

    /**
    * if語句在匯編中的實現:如果條件滿足就執行下一條指令,如果不滿足則跳轉,unlikely是為了使程序順序執行而避免跳轉
    * rt_task判斷是否為實時進程,如果是實時進程且不可中斷或者不可等待
    * 那么can_try_harder被置位,閾值將會再次減少(見下面zone_watermark_ok()函數)
    */


    can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;

    zones = zonelist->zones;

    if (unlikely(zones[0] == NULL)) {
    return NULL;
    }

    classzone_idx = zone_idx(zones[0]);

    restart:

    /**
    * 掃描包含在zonelist數據結構中的每個內存管理區
    */

    for (i = 0; (z = zones[i]) != NULL; i++) {
    /**
    *對於每個內存管理區,該函數將空閑頁框的個數與一個閥值進行比較
    *該值取決於內存分配標志、當前進程的類型及管理區被函數檢查的次數。
    *實際上,如果空閑內存不足,那么每個內存管理區一般會被檢查幾次。
    *每一次在所請求的空閑內存最低量的基礎上使用更低的值進行掃描。
    * 因此,這段循環代碼會被復制幾次,而變化很小。
    */


    /**
    *zone_watermark_ok輔助函數接收幾個參數,它們決定內存管理區中空閑頁框個數的閥值min。
    *這是對內存管理區的第一次掃描,在第一次掃描中,閥值設置為z->pages_low
    */

    if (!zone_watermark_ok(z, order, z->pages_low,
    classzone_idx, 0, 0))
    continue;

    page = buffered_rmqueue(z, order, gfp_mask);
    if (page)
    goto got_pg;
    }

    /**
    * 一般來說,應當在上一次掃描時得到內存。
    * 運行到此,表示內存已經緊張了(沒有連續的頁框可供分配了)
    * 就喚醒kswapd內核線程來異步的開始回收頁框。
    */

    for (i = 0; (z = zones[i]) != NULL; i++)
    wakeup_kswapd(z, order);

    /**
    * 執行對內存管理區的第二次掃描,將值z->pages_min作為閥值傳入。這個值已經在上一步的基礎上降低了。
    * 當然,實際的min值還是要由can_try_harder和gfp_high確定。z->pages_min僅僅是一個參考值而已。
    */

    for (i = 0; (z = zones[i]) != NULL; i++) {
    if (!zone_watermark_ok(z, order, z->pages_min,
    classzone_idx, can_try_harder,
    gfp_mask & __GFP_HIGH))
    continue;

    page = buffered_rmqueue(z, order, gfp_mask);
    if (page)
    goto got_pg;
    }

    /**
    * 上一步都還沒有獲得內存,系統內存肯定是不足了。
    */


    /**
    * 如果產生內存分配的內核控制路徑不是一個中斷處理程序或者可延遲函數,
    * 並且它試圖回收頁框(PF_MEMALLOC,TIF_MEMDIE標志被置位),那么才對內存管理區進行第三次掃描。
    */

    if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) {

    for (i = 0; (z = zones[i]) != NULL; i++) {
    /**
    *本次掃描就不調用zone_watermark_ok,它忽略閥值,這樣才能從預留的頁中分配頁。
    *允許這樣做,因為是這個進程想要歸還頁框,那就暫借一點給它吧(舍不得孩子套不到狼)。
    */

    page = buffered_rmqueue(z, order, gfp_mask);
    if (page)
    goto got_pg;
    }

    /**
    * 運行到這里來,實在是沒有內存了。
    *不論是高端內存區還是普通內存區、還是DMA內存區,甚至這些管理區中保留的內存都沒有了。
    * 意味着我們的家底都完了。
    */

    goto nopage;
    }

    /**
    * 如果gfp_mask的__GFP_WAIT標志沒有被置位,函數就返回NULL。
    */

    if (!wait)
    goto nopage;

    rebalance:
    /**
    * 如果當前進程能夠被阻塞,調用cond_resched檢查是否有其他進程需要CPU
    */

    cond_resched();

    /*
    * 設置PF_MEMALLOC標志來表示進程已經准備好執行內存回收。
    */

    p->flags |= PF_MEMALLOC;
    reclaim_state.reclaimed_slab = 0;
    /**
    * 將reclaim_state數據結構指針存入reclaim_state。這個結構只包含一個字段reclaimed_slab,初始值為0
    */

    p->reclaim_state = &reclaim_state;

    /**
    * 調用try_to_free_pages尋找一些頁框來回收。
    * 這個函數可能會阻塞當前進程。一旦返回,就重設PF_MEMALLOC,並再次調用cond_resched
    */

    did_some_progress = try_to_free_pages(zones, gfp_mask, order);

    p->reclaim_state = NULL;
    p->flags &= ~PF_MEMALLOC;

    cond_resched();

    /**
    * 如果已經回收了一些頁框,那么執行第二遍掃描類似的操作。
    */

    if (likely(did_some_progress)) {
    for (i = 0; (z = zones[i]) != NULL; i++) {
    if (!zone_watermark_ok(z, order, z->pages_min,
    classzone_idx, can_try_harder,
    gfp_mask & __GFP_HIGH))
    continue;

    page = buffered_rmqueue(z, order, gfp_mask);
    if (page)
    goto got_pg;
    }
    } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
    /**
    *沒有釋放任何頁框,說明內核遇到很大麻煩了。因為內存少又不能釋放頁框。
    *如果允許殺死進程:__GFP_FS被置位並且__GFP_NORETRY標志為0。
    * 那就開始准備殺死進程吧。
    */


    /**
    * 再掃描一次內存管理區。
    *這樣做有點莫名其妙,既然申請少一點的內存都不行,為什么還要傳入z->pages_high??它看起來更不會成功。
    *其實這樣做還是有道理的:實際上,只有另一個內核控制路徑已經殺死一個進程來回收它的內存后,這步才會成功。
    * 因此,這步避免了兩個(而不是一個)無辜的進程被殺死。
    */

    for (i = 0; (z = zones[i]) != NULL; i++) {
    if (!zone_watermark_ok(z, order, z->pages_high,
    classzone_idx, 0, 0))
    continue;

    page = buffered_rmqueue(z, order, gfp_mask);
    if (page)
    goto got_pg;
    }

    /**
    * 還是不行,就殺死一些進程再試吧。
    */

    out_of_memory(gfp_mask);
    goto restart;
    }

    /**
    * 如果內存分配請求不能被滿足,那么函數決定是否應當繼續掃描內存管理區。
    * 如果__GFP_NORETRY被清除,並且內存分配請求跨越了多達8個頁框或者__GFP_REPEAT被置位,或者__GFP_NOFAIL被置位。
    */

    do_retry = 0;
    if (!(gfp_mask & __GFP_NORETRY)) {
    if ((order <= 3) || (gfp_mask & __GFP_REPEAT))
    do_retry = 1;
    if (gfp_mask & __GFP_NOFAIL)
    do_retry = 1;
    }
    /**
    * 要重試,就調用blk_congestion_wait使進程休眠一會。再跳到rebalance重試。
    */

    if (do_retry) {
    blk_congestion_wait(WRITE, HZ/50);
    goto rebalance;
    }
    /**
    * 既然不用重試,那就執行到nopage返回NULL了。
    */

    nopage:
    if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
    printk(KERN_WARNING "%s: page allocation failure."
    " order:%d, mode:0x%x\n",
    p->comm, order, gfp_mask);
    dump_stack();
    }
    return NULL;
    got_pg:
    zone_statistics(zonelist, z);
    return page;
    }

    zone_watermark_ok()函數如下:

    int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int classzone_idx, int can_try_harder, int gfp_high)
    {
    long min = mark, free_pages = z->free_pages - (1 << order) + 1;
    int o;

    /**
    * 如果gfp_high標志被置位。則base除2。
    * 注意這里不是:min /= 2(min為奇偶數的差別)
    * 一般來說,如果gfp_mask的__GFP_WAIT標志被置位,那么這個標志就會為1
    * 換句話說,就是指從高端內存中分配。
    */

    if (gfp_high)
    min -= min / 2;
    /**
    * 如果作為參數傳遞的can_try_harder標志被置位,這個值再減少1/4
    * can_try_harder=1一般是當:gfp_mask中的__GFP_WAIT標志被置位,或者當前進程是一個實時進程並且在進程上下文中已經完成了內存分配。
    */

    if (can_try_harder)
    min -= min / 4;

    if (free_pages <= min + z->lowmem_reserve[classzone_idx])
    return 0;
    /*
    * for函數作用:除了被分配的頁框外,在order至少為k的塊中起碼還有min/2^k個空閑頁框
    */

    for (o = 0; o < order; o++) {

    free_pages -= z->free_area[o].nr_free << o;

    /* Require fewer higher order pages to be free */
    min >>= 1;

    if (free_pages <= min)
    return 0;
    }
    return 1;
    }

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com