1. 程式人生 > >Linux核心-記憶體-分割槽頁框分配器

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;
        }