Zoned Allocator -3- (Watermark)

Zone 워터마크 관련

zone_watermark_ok()

mm/page_alloc.c

bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
                      int classzone_idx, int alloc_flags)
{
        return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags,
                                        zone_page_state(z, NR_FREE_PAGES));
}

해당 zone에서 order 페이지를 처리하기 위해 free page가 워터마크 값의 기준 보다 충분한지 확인한다.

 

__zone_watermark_ok()

mm/page_alloc.c

static bool __zone_watermark_ok(struct zone *z, unsigned int order,
                        unsigned long mark, int classzone_idx, int alloc_flags,
                        long free_pages)
{
        /* free_pages may go negative - that's OK */
        long min = mark;
        int o;
        long free_cma = 0;

        free_pages -= (1 << order) - 1;
        if (alloc_flags & ALLOC_HIGH)
                min -= min / 2;
        if (alloc_flags & ALLOC_HARDER)
                min -= min / 4;
#ifdef CONFIG_CMA
        /* If allocation can't use CMA areas don't use free CMA pages */
        if (!(alloc_flags & ALLOC_CMA))
                free_cma = zone_page_state(z, NR_FREE_CMA_PAGES);
#endif

        if (free_pages - free_cma <= min + z->lowmem_reserve[classzone_idx])
                return false;
        for (o = 0; o < order; o++) {
                /* At the next order, this order's pages become unavailable */
                free_pages -= z->free_area[o].nr_free << o;

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

                if (free_pages <= min)
                        return false;
        }
        return true;
}

해당 zone에서 order 페이지를 처리하기 위해 free page가 워터마크 값의 기준 보다 충분한지 확인한다.

  • free_pages -= (1 << order) – 1;
    • free_pages를 2^order-1 페이지 만큼 감소시킨다.
  • if (alloc_flags & ALLOC_HIGH) min -= min / 2;
    • ALLOC_HIGH 플래그가 설정된 경우 min 값을 절반으로 줄인다.
  • if (alloc_flags & ALLOC_HARDER) min -= min / 4;
    • ALLOC_HARDER 플래그가 설정된 경우 min 값을 1/4로 줄인다.
  • if (!(alloc_flags & ALLOC_CMA)) free_cma = zone_page_state(z, NR_FREE_CMA_PAGES);
    • ALLOC_CMA 플래그가 설정되지 않은 경우 free_cma 카운터를 알아온다.
  • if (free_pages – free_cma <= min + z->lowmem_reserve[classzone_idx]) return false;
    • free_cma 페이지를 제외한 free_pages가 classzone_idx로 지정한 lwmem_reserve를 포함한 min 값을 초과하는 경우 false를 반환한다.
  • for (o = 0; o < order; o++) { free_pages -= z->free_area[o].nr_free << o; min >>= 1; if (free_pages <= min) return false; }
    • 0부터 요청 order까지 루프를 돌며 등록된 페이지들의 수를 free_pages에서 감소시키고, min 값을 절반으로 감소시킨 후 free_pages가 min 값 이하인 경우 compaction 또는 reclaim에 문제가 발생할 가능성이 있어 false를 반환한다.
      • 이 알고리즘은 루프 수행 중 특정 order의 nr_free가 0일 때에도 min을 절반씩 줄이는 등 계산이 정확하지 않다.
      •  high order 페이지 할당 요청 시 워터마크 이하의 영역을 이용하여 compaction이 가능한지 여부를 정확히 계산하기 위해 이 부분의 알고리즘이 커널 v4.4-rc1에서 수정되었다.

 

다음 그림은 zone 워터마크 설정에 따른 할당과 페이지 회수에 따른 관계를 보여준다.

zone-watermark-1b

 

다음 그림은 요청한 워터마크와 free size를 order 루프만큼 비교하여 free size가 충분한 경우 ok가 되는 모습을 보여준다.

  • 위에서 소개하였듯이 이 루틴의 정확도가 떨어져 커널 4.4-rc1에서 수정되었다.

zone_watermark_ok-1a

 

워터마크 초기값 설정

init_per_zone_wmark_min()

mm/page_alloc.c

/*
 * Initialise min_free_kbytes.
 *
 * For small machines we want it small (128k min).  For large machines
 * we want it large (64MB max).  But it is not linear, because network
 * bandwidth does not increase linearly with machine size.  We use
 *
 *      min_free_kbytes = 4 * sqrt(lowmem_kbytes), for better accuracy:
 *      min_free_kbytes = sqrt(lowmem_kbytes * 16)
 *
 * which yields
 *
 * 16MB:        512k
 * 32MB:        724k
 * 64MB:        1024k
 * 128MB:       1448k
 * 256MB:       2048k
 * 512MB:       2896k
 * 1024MB:      4096k
 * 2048MB:      5792k
 * 4096MB:      8192k
 * 8192MB:      11584k
 * 16384MB:     16384k
 */
int __meminit init_per_zone_wmark_min(void)
{
        unsigned long lowmem_kbytes;
        int new_min_free_kbytes;

        lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
        new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);

        if (new_min_free_kbytes > user_min_free_kbytes) {
                min_free_kbytes = new_min_free_kbytes;
                if (min_free_kbytes < 128)
                        min_free_kbytes = 128;
                if (min_free_kbytes > 65536)
                        min_free_kbytes = 65536;
        } else {
                pr_warn("min_free_kbytes is not updated to %d because user defined value %d is preferred\n",
                                new_min_free_kbytes, user_min_free_kbytes);
        }
        setup_per_zone_wmarks();
        refresh_zone_stat_thresholds();
        setup_per_zone_lowmem_reserve();
        setup_per_zone_inactive_ratio();
        return 0;
}
module_init(init_per_zone_wmark_min)

메타 데이터(lowmem에서의 mem_map 및 lowmem_reserve)를 제외한 lowmem 크기에 따른 전역 min_free_kbytes 값을 초기화한다. 그 외에 각 zone의 워터마크, lowmem_reserve와 inactive_ratio 등을 설정한다.

  • lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
    • 현재 노드의 zonelist에서 ZONE_NORMAL 이하의 zone들을 대상으로 high 워터마크 페이지를 제외한 페이지 수를 kbyte 단위로 바꾼다.
      • 커널이 초기화될 때 이외에도 hotplug 메모리를 사용하여 메모리가 hot add/del 될 때마다 이 초기화 루틴이 수행된다.
      • 커널이 처음 초기화될 때에는 각 zone의 워터마크 초기값은 0이다.
  •  new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
    • 메타 데이터(lowmem에서의 mem_map 및 lowmem_reserve)를 제외한 lowmem 크기 * 16을 한 후 제곱근(루트)한 수를 얻어온다.
      • 예) sqrt(512M * 16) -> 2M
  • if (new_min_free_kbytes > user_min_free_kbytes) { min_free_kbytes = new_min_free_kbytes;
    • 산출한 값이 user_min_free_kbytes(초기값: -1)를 초과한 경우 전역 min_free_kbytes를 갱신한다.
  • if (min_free_kbytes < 128) min_free_kbytes = 128; if (min_free_kbytes > 65536) min_free_kbytes = 65536; }
    • 전역 min_free_kbytes 값을 128 ~ 65536 범위로 clamp한다.
  • setup_per_zone_wmarks();
    • 각 zone에 대한 threshold들을 갱신한다.
      • 각 cpu의 zone->pageset->stat_threshold 갱신
      • zone->percpu_drift_mark 갱신
  • setup_per_zone_lowmem_reserve();
    • zone->lowmem_reserve 갱신
    • 전역 dirty_balance_reserve, totalreserve_pages 갱신
  • setup_per_zone_inactive_ratio();
    • zone별 inactive anon 비율을 산출한다.
      • zone->inactive_ratio 갱신

 

다음 그림은 전역 min_free_kbytes 값이 산출되는 과정을 보여준다.

init_per_zone_wmark_min-1

 

nr_free_buffer_pages()

mm/page_alloc.c

/**
 * nr_free_buffer_pages - count number of pages beyond high watermark
 *
 * nr_free_buffer_pages() counts the number of pages which are beyond the high
 * watermark within ZONE_DMA and ZONE_NORMAL.
 */
unsigned long nr_free_buffer_pages(void)
{
        return nr_free_zone_pages(gfp_zone(GFP_USER));
}
EXPORT_SYMBOL_GPL(nr_free_buffer_pages);

현재 노드의 zonelist에서 ZONE_NORMAL 이하의 zone들을 대상으로 high 워터마크 페이지를 제외한 페이지 수를 반환한다.

  • 참고로 처음 커널 설정을 위해 호출될 때에는 high 워터마크가 0이다.

 

nr_free_zone_pages()

mm/page_alloc.c

/**
 * nr_free_zone_pages - count number of pages beyond high watermark
 * @offset: The zone index of the highest zone
 *
 * nr_free_zone_pages() counts the number of counts pages which are beyond the
 * high watermark within all zones at or below a given zone index.  For each
 * zone, the number of pages is calculated as:
 *     managed_pages - high_pages
 */
static unsigned long nr_free_zone_pages(int offset)
{
        struct zoneref *z;
        struct zone *zone;

        /* Just pick one node, since fallback list is circular */
        unsigned long sum = 0;

        struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);

        for_each_zone_zonelist(zone, z, zonelist, offset) {
                unsigned long size = zone->managed_pages;
                unsigned long high = high_wmark_pages(zone);
                if (size > high)
                        sum += size - high;
        }

        return sum;
}

현재 노드의 zonelist에서 offset이하의 zone들을 대상으로 managed_pages – high 워터마크 페이지를 모두 더한 수를 반환한다.

 

setup_per_zone_wmarks()

mm/page_alloc.c

/**
 * setup_per_zone_wmarks - called when min_free_kbytes changes
 * or when memory is hot-{added|removed}
 *
 * Ensures that the watermark[min,low,high] values for each zone are set
 * correctly with respect to min_free_kbytes.
 */
void setup_per_zone_wmarks(void)
{
        mutex_lock(&zonelists_mutex);
        __setup_per_zone_wmarks();
        mutex_unlock(&zonelists_mutex);
}

zone별 워터마크 값과 totalreserve 값을 산출한다.

 

__setup_per_zone_wmarks()

mm/page_alloc.c

static void __setup_per_zone_wmarks(void)
{
        unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
        unsigned long lowmem_pages = 0;
        struct zone *zone;
        unsigned long flags;

        /* Calculate total number of !ZONE_HIGHMEM pages */
        for_each_zone(zone) {
                if (!is_highmem(zone))
                        lowmem_pages += zone->managed_pages;
        }

        for_each_zone(zone) {
                u64 tmp;

                spin_lock_irqsave(&zone->lock, flags);
                tmp = (u64)pages_min * zone->managed_pages;
                do_div(tmp, lowmem_pages);
                if (is_highmem(zone)) {
                        /*
                         * __GFP_HIGH and PF_MEMALLOC allocations usually don't
                         * need highmem pages, so cap pages_min to a small
                         * value here.
                         *
                         * The WMARK_HIGH-WMARK_LOW and (WMARK_LOW-WMARK_MIN)
                         * deltas controls asynch page reclaim, and so should
                         * not be capped for highmem.
                         */
                        unsigned long min_pages;

                        min_pages = zone->managed_pages / 1024;
                        min_pages = clamp(min_pages, SWAP_CLUSTER_MAX, 128UL);
                        zone->watermark[WMARK_MIN] = min_pages;
                } else {
                        /*
                         * If it's a lowmem zone, reserve a number of pages
                         * proportionate to the zone's size.
                         */
                        zone->watermark[WMARK_MIN] = tmp;
                }

                zone->watermark[WMARK_LOW]  = min_wmark_pages(zone) + (tmp >> 2);
                zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1);

                __mod_zone_page_state(zone, NR_ALLOC_BATCH,
                        high_wmark_pages(zone) - low_wmark_pages(zone) -
                        atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]));

                setup_zone_migrate_reserve(zone);
                spin_unlock_irqrestore(&zone->lock, flags);
        }

        /* update totalreserve_pages */
        calculate_totalreserve_pages();
}

zone별 워터마크 값과 totalreserve 값을 산출한다.

  • unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT – 10);
    • 전역 min_free_kbytes를 읽어서 page 수 단위로 바꾼다.
  • for_each_zone(zone) { if (!is_highmem(zone)) lowmem_pages += zone->managed_pages; }
    • ZONE_HIGHMEM이 아닌 경우 managed_pages를 합산하여 lowmem_pages에 대입한다.
      • managed_pages
        • highmem 미만의 zone의 present_pages 에서 해당 zone이 사용하는 크기 만큼의 mem_map을 뺀 값이며 dma zone에서는 dma_reserve 값도 추가로 뺀다.
        • highmem zone에서는 highmem zone의 present page와 동일하다.
        • 참고: free_area_init_node() | 문c
  • for_each_zone(zone) { tmp = (u64)pages_min * zone->managed_pages; do_div(tmp, lowmem_pages);
    • 각 zone을 루프를 돌며 tmp에 pages_min(min_free_kbytes)을 현재 zone의 비율만큼 나눈 페이지 수
  • if (is_highmem(zone)) { min_pages = zone->managed_pages / 1024; min_pages = clamp(min_pages, SWAP_CLUSTER_MAX, 128UL); zone->watermark[WMARK_MIN] = min_pages;
    • highmem zone인 경우 실제 present page를 1024로 나눈 값을 32 ~ 128까지의 범위로 clamp한 후 min 워터마크에 저장한다.
  • } else { zone->watermark[WMARK_MIN] = tmp; }
    • highmem이 아닌 경우 min 워터마크에 tmp 값을 저장한다.
  • zone->watermark[WMARK_LOW] = min_wmark_pages(zone) + (tmp >> 2);
    • low 워터마크에 min 워터마크의 125%를 대입한다.
  • zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1);
    • high 워터마크에 min 워터마크의 150%를 대입한다.
  • __mod_zone_page_state(zone, NR_ALLOC_BATCH, high_wmark_pages(zone) – low_wmark_pages(zone) – atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]));
    • NR_ALLOC_BATCH에 high-low간 워터마크 값의 변경된 차이 값만 추가한다.
  • setup_zone_migrate_reserve(zone);
    • min_free_kbytes: 0xdac (3500) ->

 

다음 그림은 min_free_kbytes 값과 각 zone의 managed_pages를 가지고 워터마크 값을 산출해내는 모습을 보여준다.

__setup_per_zone_wmarks-1

 

setup_zone_migrate_reserve()

mm/page_alloc.c

/*
 * Mark a number of pageblocks as MIGRATE_RESERVE. The number
 * of blocks reserved is based on min_wmark_pages(zone). The memory within
 * the reserve will tend to store contiguous free pages. Setting min_free_kbytes
 * higher will lead to a bigger reserve which will get freed as contiguous
 * blocks as reclaim kicks in
 */
static void setup_zone_migrate_reserve(struct zone *zone)
{
        unsigned long start_pfn, pfn, end_pfn, block_end_pfn;
        struct page *page;
        unsigned long block_migratetype;
        int reserve;
        int old_reserve;

        /*
         * Get the start pfn, end pfn and the number of blocks to reserve
         * We have to be careful to be aligned to pageblock_nr_pages to
         * make sure that we always check pfn_valid for the first page in
         * the block.
         */
        start_pfn = zone->zone_start_pfn;
        end_pfn = zone_end_pfn(zone);
        start_pfn = roundup(start_pfn, pageblock_nr_pages);
        reserve = roundup(min_wmark_pages(zone), pageblock_nr_pages) >>
                                                        pageblock_order;

        /*
         * Reserve blocks are generally in place to help high-order atomic
         * allocations that are short-lived. A min_free_kbytes value that
         * would result in more than 2 reserve blocks for atomic allocations
         * is assumed to be in place to help anti-fragmentation for the
         * future allocation of hugepages at runtime.
         */
        reserve = min(2, reserve);
        old_reserve = zone->nr_migrate_reserve_block;

        /* When memory hot-add, we almost always need to do nothing */
        if (reserve == old_reserve)
                return;
        zone->nr_migrate_reserve_block = reserve;

워터마크 min 이하의 페이지 블럭들에 대해 버디 시스템의 free page들과 pageblock의 migrate 타입을 reserve 타입으로 구성한다.

  • 워터마크 min 이하의 페이지 블럭의 movable 타입과 버디 시스템의 free page들은 모두 reserve 타입으로 변경
  • 워타마크 min을 초과한 페이지 블럭의 reserve 타입과 버디 시스템의 free page들은 movable 타입으로 변경.

 

  • reserve = roundup(min_wmark_pages(zone), pageblock_nr_pages) >> pageblock_order;
    • min 워터마크 페이지가 필요한 페이지 블럭의 수를 산출한다.
      • min 워터마크 페이지 수를 페이지 블럭 단위로 round up한 후 pageblock_order 만큼 나눈다.
      • 예) min_free_kbytes: 0xdac -> 1개의 페이지 블럭
  • reserve = min(2, reserve); old_reserve = zone->nr_migrate_reserve_block;
    • 2와 reserve 값 중 작은 값을 선택하고, 기존 reserve 값을 백업한다.
  • if (reserve == old_reserve) return;
    • reserve 값이 변동이 없는 경우 처리를 중지하고 빠져나간다.
  • zone->nr_migrate_reserve_block = reserve;
    • reserve 값을 저장한다.

 

        for (pfn = start_pfn; pfn < end_pfn; pfn += pageblock_nr_pages) {
                if (!pfn_valid(pfn))
                        continue;
                page = pfn_to_page(pfn);

                /* Watch out for overlapping nodes */
                if (page_to_nid(page) != zone_to_nid(zone))
                        continue;

                block_migratetype = get_pageblock_migratetype(page);

                /* Only test what is necessary when the reserves are not met */
                if (reserve > 0) {
                        /*
                         * Blocks with reserved pages will never free, skip
                         * them.
                         */
                        block_end_pfn = min(pfn + pageblock_nr_pages, end_pfn);
                        if (pageblock_is_reserved(pfn, block_end_pfn))
                                continue;

                        /* If this block is reserved, account for it */
                        if (block_migratetype == MIGRATE_RESERVE) {
                                reserve--;
                                continue;
                        }

                        /* Suitable for reserving if this block is movable */
                        if (block_migratetype == MIGRATE_MOVABLE) {
                                set_pageblock_migratetype(page,
                                                        MIGRATE_RESERVE);
                                move_freepages_block(zone, page,
                                                        MIGRATE_RESERVE);
                                reserve--;
                                continue;
                        }
                } else if (!old_reserve) {
                        /*
                         * At boot time we don't need to scan the whole zone
                         * for turning off MIGRATE_RESERVE.
                         */
                        break;
                }

                /*
                 * If the reserve is met and this is a previous reserved block,
                 * take it back
                 */
                if (block_migratetype == MIGRATE_RESERVE) {
                        set_pageblock_migratetype(page, MIGRATE_MOVABLE);
                        move_freepages_block(zone, page, MIGRATE_MOVABLE);
                }
        }
}
  • for (pfn = start_pfn; pfn < end_pfn; pfn += pageblock_nr_pages) {
    • start_pfn ~ end_pfn 까지 페이지 블럭 단위로 증가시키며 루프를 돈다.
      • start_pfn은 페이지 블럭단위로 정렬되어 있다.
  • if (!pfn_valid(pfn)) continue;
    • 유효 메모리 페이지가 아니면 skip
  • if (page_to_nid(page) != zone_to_nid(zone)) continue;
    • 페이지의 노드와 zone의 노드가 다른 경우 skip
  • block_migratetype = get_pageblock_migratetype(page);
    • 지정된 페이지에 해당하는 페이지 블럭의 migrate 타입을 알아온다.
  • if (reserve > 0) { block_end_pfn = min(pfn + pageblock_nr_pages, end_pfn);
    • reserve가 0보다 크면 블럭의 끝 pfn을 알아온다.
  • if (pageblock_is_reserved(pfn, block_end_pfn)) continue;
    • pfn ~ block_end_pfn 범위내의 페이지에 유효 페이지가 없거나 reserved 페이지가 존재하는 경우 skip한다.
  • if (block_migratetype == MIGRATE_RESERVE) { reserve–; continue; }
    • 블럭의 migrate 타입이 MIGRATE_RESERVE인 경우 reserve를 감소시키고 skip한다.
  • } else if (!old_reserve) { break; }
    • reserve가 0이하 이면서 기존 reserve가 0인 경우 break 한다.
      • 커널 초기화 시에는 MIGRATE_RESERVE를 끄기 위해 전체 zone을 스캔할 필요 없다.
  • if (block_migratetype == MIGRATE_RESERVE) { set_pageblock_migratetype(page, MIGRATE_MOVABLE); move_freepages_block(zone, page, MIGRATE_MOVABLE); }
    • 블럭 migrate 타입이 MIGRATE_RESERVE인 경우 MIGRATE_MOVABLE로 변경하고 블럭의 free page들을 모두 MIGRATE_MOVABLE로 이동시킨다.

 

다음 그림은 워터마크 min 을 기준으로 하는 페이지 블럭 단위로 버디 시스템의 free page들을 movable 과 reserve 타입으로 변경하는 모습을 보여준다.

setup_zone_migrate_reserve-1a

 

pageblock_is_reserved()

mm/page_alloc.c

/*
 * Check if a pageblock contains reserved pages
 */
static int pageblock_is_reserved(unsigned long start_pfn, unsigned long end_pfn)
{
        unsigned long pfn;

        for (pfn = start_pfn; pfn < end_pfn; pfn++) {
                if (!pfn_valid_within(pfn) || PageReserved(pfn_to_page(pfn)))
                        return 1;
        }
        return 0;
}

시작 pfn ~ 끝 pfn에 유효 메모리가 없거나 reserved 페이지가 포함된 경우 true를 반환한다.

 

move_freepages_block()

mm/page_alloc.c

int move_freepages_block(struct zone *zone, struct page *page,
                                int migratetype)
{
        unsigned long start_pfn, end_pfn;
        struct page *start_page, *end_page;

        start_pfn = page_to_pfn(page);
        start_pfn = start_pfn & ~(pageblock_nr_pages-1);
        start_page = pfn_to_page(start_pfn);
        end_page = start_page + pageblock_nr_pages - 1;
        end_pfn = start_pfn + pageblock_nr_pages - 1;

        /* Do not cross zone boundaries */
        if (!zone_spans_pfn(zone, start_pfn))
                start_page = page;
        if (!zone_spans_pfn(zone, end_pfn))
                return 0;

        return move_freepages(zone, start_page, end_page, migratetype);
}

지정된 zone의 요청 page가 있는 pageblock내의 모든 free page들을 요청 migrate 타입으로 변경하고 버디 시스템에서 요청 migrate 타입으로 이동하고 이동된 페이지 수를 반환한다.

  • 요청 zone의 시작 페이지가 pageblock 단위로 정렬되지 않은 경우에도 zone의 시작 주소부터 move 적용가능 (partial)
  • 요청 zone의 끝 페이지가 pageblock 단위로 정렬되지 않은 경우 그 pageblock은 사용할 수 없음

 

아래 그림은 지정된 zone의 페이지블럭에 있는 free page들의 migrate type을 요청한 타입으로 변경하는 모습을 보여준다.

move_freepages_block-1

 

zone_spans_pfn()

include/linux/mmzone.h

static inline bool zone_spans_pfn(const struct zone *zone, unsigned long pfn)
{
        return zone->zone_start_pfn <= pfn && pfn < zone_end_pfn(zone);
}

pfn이 zone의 영역에 들어있는지 여부를 반환한다.

 

move_freepages()

mm/page_alloc.c

/*
 * Move the free pages in a range to the free lists of the requested type.
 * Note that start_page and end_pages are not aligned on a pageblock
 * boundary. If alignment is required, use move_freepages_block()
 */
int move_freepages(struct zone *zone,
                          struct page *start_page, struct page *end_page,
                          int migratetype)
{
        struct page *page;
        unsigned long order;
        int pages_moved = 0;

#ifndef CONFIG_HOLES_IN_ZONE
        /*
         * page_zone is not safe to call in this context when
         * CONFIG_HOLES_IN_ZONE is set. This bug check is probably redundant
         * anyway as we check zone boundaries in move_freepages_block().
         * Remove at a later date when no bug reports exist related to
         * grouping pages by mobility
         */
        VM_BUG_ON(page_zone(start_page) != page_zone(end_page));
#endif

        for (page = start_page; page <= end_page;) {
                /* Make sure we are not inadvertently changing nodes */
                VM_BUG_ON_PAGE(page_to_nid(page) != zone_to_nid(zone), page);

                if (!pfn_valid_within(page_to_pfn(page))) {
                        page++;
                        continue;
                }

                if (!PageBuddy(page)) {
                        page++;
                        continue;
                }

                order = page_order(page);
                list_move(&page->lru,
                          &zone->free_area[order].free_list[migratetype]);
                set_freepage_migratetype(page, migratetype);
                page += 1 << order;
                pages_moved += 1 << order;
        }

        return pages_moved;
}

요청 zone의 시작 페이지부터 끝 페이지까지 모든 free page들에 대해 migrate 타입을 변경하고 버디 시스템의 migrate 타입도 변경하고 변경된 페이지 수를 반환한다.

 

다음 그림은 요청한 범위에서 버디 시스템의 free page들을 찾아 MIGRATE_UNMOVABLE 타입으로 이동시키는 모습을 보여준다.

move_freepages-1

 

calculate_totalreserve_pages()

mm/page_alloc.c

/*
 * calculate_totalreserve_pages - called when sysctl_lower_zone_reserve_ratio
 *      or min_free_kbytes changes.
 */
static void calculate_totalreserve_pages(void)
{
        struct pglist_data *pgdat;
        unsigned long reserve_pages = 0;
        enum zone_type i, j;

        for_each_online_pgdat(pgdat) {
                for (i = 0; i < MAX_NR_ZONES; i++) {
                        struct zone *zone = pgdat->node_zones + i;
                        long max = 0;

                        /* Find valid and maximum lowmem_reserve in the zone */
                        for (j = i; j < MAX_NR_ZONES; j++) {
                                if (zone->lowmem_reserve[j] > max)
                                        max = zone->lowmem_reserve[j];
                        }

                        /* we treat the high watermark as reserved pages. */
                        max += high_wmark_pages(zone);

                        if (max > zone->managed_pages)
                                max = zone->managed_pages;
                        reserve_pages += max;
                        /*
                         * Lowmem reserves are not available to
                         * GFP_HIGHUSER page cache allocations and
                         * kswapd tries to balance zones to their high
                         * watermark.  As a result, neither should be
                         * regarded as dirtyable memory, to prevent a
                         * situation where reclaim has to clean pages
                         * in order to balance the zones.
                         */
                        zone->dirty_balance_reserve = max;
                }
        }
        dirty_balance_reserve = reserve_pages;
        totalreserve_pages = reserve_pages;
}

zone별 dirty_balance_reserve 값과 전역 dirty_balance_reserve 및 totalreserve_pages 값을 산출한다.

  • for_each_online_pgdat(pgdat) {
    • 모든 online 노드에 대하여 루프를 돈다.
  • for (i = 0; i < MAX_NR_ZONES; i++) {
    • 모든 zone에 대하여 루프를 돈다.
  • for (j = i; j < MAX_NR_ZONES; j++) { if (zone->lowmem_reserve[j] > max) max = zone->lowmem_reserve[j]; }
    • 지정된 zone을 포함한 상위 z->lowmem_reserve[] 배열에서 최대 값 max를 알아온다.
  • max += high_wmark_pages(zone);
    • max 값에 high 워터마크 값을 더한다.
  • if (max > zone->managed_pages) max = zone->managed_pages;
    • max 값이 zone의 managed_pages 값보다 큰 경우 max 값을 managed_pages로 제한한다.
  • reserve_pages += max;
    • 최종 전역 변수 갱신을 위해 모든 reserve_pages 값을 더한다.
  • zone->dirty_balance_reserve = max;
    • 각 zone의 dirty_balance_reserve 값에 max 값을 대입한다.
  • dirty_balance_reserve = reserve_pages; totalreserve_pages = reserve_pages;
  • 마지막으로 전역 dirty_balance_reserve 및 totalreserve_pages 값에 전체 reserve_pages 값을 대입한다.

 

다음 그림은 zone별 dirty_balance_reserve 값과 전역 dirty_balance_reserve 및 totalreserve_pages 값을 산출하는 과정을 보여준다.

calculate_totalreserve_pages-1a

 

refresh_zone_stat_thresholds()

mm/vmstat.c

/*
 * Refresh the thresholds for each zone.
 */
void refresh_zone_stat_thresholds(void)
{
        struct zone *zone;
        int cpu;
        int threshold;

        for_each_populated_zone(zone) {
                unsigned long max_drift, tolerate_drift;

                threshold = calculate_normal_threshold(zone);

                for_each_online_cpu(cpu) 
                        per_cpu_ptr(zone->pageset, cpu)->stat_threshold
                                                        = threshold;

                /* 
                 * Only set percpu_drift_mark if there is a danger that
                 * NR_FREE_PAGES reports the low watermark is ok when in fact
                 * the min watermark could be breached by an allocation
                 */
                tolerate_drift = low_wmark_pages(zone) - min_wmark_pages(zone);
                max_drift = num_online_cpus() * threshold;
                if (max_drift > tolerate_drift)
                        zone->percpu_drift_mark = high_wmark_pages(zone) +
                                        max_drift;
        }
}

모든 zone의 zone 통계 스레졸드 값을 구해 업데이트한다.

  • zone 카운터들은 zone에 대략의 카운터와 각 per-cpu에 구성된 카운터를 더해야 정확한 값을 알아낼 수 있다. 따라서 어느 일정 free 페이지 미만에 대해 정확히 검사하려고 하는 경우에는 각 cpu의 zone 통계 값들을 다 더해야하는데, 이렇게 매번 계산을 하는 경우 성능을 떨어뜨리므로 일정 스레졸드 이상인 경우는 zone에 있는 카운터 값만으로 판단하고, 스레졸드 기준 이하가 되면 각 cpu의 zone 통계를 구하도록 유도하여 성능을 유지할 수 있다.
  • percpu_drift_mark
    • compaction 등의 상황에서 워터마크 비교에 대해 더 정확한 판단이 필요할 때 free 페이지가 이 스레졸드 값보다 떨어지는 경우 zone 카운터에 per-cpu 카운터까지 합하여 정밀한 카운터를 구해 비교한다.

 

  • for_each_populated_zone(zone) { unsigned long max_drift, tolerate_drift; threshold = calculate_normal_threshold(zone);
    • 전체 zone에 대해 루프를 돌며 해당 zone에 대한 stat 스레졸드 값을 구해온다
  • for_each_online_cpu(cpu)  per_cpu_ptr(zone->pageset, cpu)->stat_threshold = threshold;
    • 해당 zone에 있는 per-cpu 페이지 프레임 캐시의 모든 cpu에 대해 산출한 스레졸드 값을 저장한다
  • tolerate_drift = low_wmark_pages(zone) – min_wmark_pages(zone);
    • tolerate_drift에 low 워터마크 – min 워터마크 값을 대입한다.
  • max_drift = num_online_cpus() * threshold;
    • max_drift에 스레졸드 * online cpu 수를 대입한다
  • if (max_drift > tolerate_drift) zone->percpu_drift_mark = high_wmark_pages(zone) + max_drift;
    • tolerate_drift보다 max_drift 값이 더 큰 경우 해당 zone의 percpu_drift_mark 값에 high 워터마크에 max_drift 값을 더한 값을 저장한다

 

아래는 zone stat 스레졸드를 이용한 zone_watermark_ok_safe() 함수의 사용예를 보여준다.

zone_watermark_ok_safe-1

 

 

calculate_normal_threshold()

mm/vmstat.c

int calculate_normal_threshold(struct zone *zone)
{
        int threshold;          
        int mem;        /* memory in 128 MB units */

        /*
         * The threshold scales with the number of processors and the amount
         * of memory per zone. More memory means that we can defer updates for
         * longer, more processors could lead to more contention.
         * fls() is used to have a cheap way of logarithmic scaling.
         *
         * Some sample thresholds:
         *
         * Threshold    Processors      (fls)   Zonesize        fls(mem+1)
         * ------------------------------------------------------------------
         * 8            1               1       0.9-1 GB        4
         * 16           2               2       0.9-1 GB        4
         * 20           2               2       1-2 GB          5
         * 24           2               2       2-4 GB          6
         * 28           2               2       4-8 GB          7
         * 32           2               2       8-16 GB         8
         * 4            2               2       <128M           1
         * 30           4               3       2-4 GB          5
         * 48           4               3       8-16 GB         8
         * 32           8               4       1-2 GB          4
         * 32           8               4       0.9-1GB         4
         * 10           16              5       <128M           1
         * 40           16              5       900M            4
         * 70           64              7       2-4 GB          5
         * 84           64              7       4-8 GB          6
         * 108          512             9       4-8 GB          6
         * 125          1024            10      8-16 GB         8
         * 125          1024            10      16-32 GB        9
         */

        mem = zone->managed_pages >> (27 - PAGE_SHIFT);

        threshold = 2 * fls(num_online_cpus()) * (1 + fls(mem));

        /*
         * Maximum threshold is 125
         */
        threshold = min(125, threshold);

        return threshold;
}

요청 zone의 managed_pages 크기에 비례하여 스레졸드 값을 구하는데 최소 125값부터 시작한다.

  • 요청 zone의 managed_pages에 대해 128M 단위로 round down 한 값을 mem에 대입한다.
  • 2 x (온라인cpu 수를 2의 차수 단위로 round down) x (mem을 2의 차수 단위로 round down + 1) 한 값으로 스레졸드를 구하고 이 값은 최대 125로 제한한다.
    • 예) 4 cpu이고, zone이 600M인 경우
      • threshold=24(2 x 3 x 4)

 

setup_per_zone_lowmem_reserve()

mm/page_alloc.c

/*
 * setup_per_zone_lowmem_reserve - called whenever
 *      sysctl_lower_zone_reserve_ratio changes.  Ensures that each zone
 *      has a correct pages reserved value, so an adequate number of
 *      pages are left in the zone after a successful __alloc_pages().
 */
static void setup_per_zone_lowmem_reserve(void)
{
        struct pglist_data *pgdat;
        enum zone_type j, idx;

        for_each_online_pgdat(pgdat) {
                for (j = 0; j < MAX_NR_ZONES; j++) {
                        struct zone *zone = pgdat->node_zones + j;
                        unsigned long managed_pages = zone->managed_pages;

                        zone->lowmem_reserve[j] = 0;

                        idx = j;
                        while (idx) {
                                struct zone *lower_zone;

                                idx--;

                                if (sysctl_lowmem_reserve_ratio[idx] < 1)
                                        sysctl_lowmem_reserve_ratio[idx] = 1;

                                lower_zone = pgdat->node_zones + idx;
                                lower_zone->lowmem_reserve[j] = managed_pages /
                                        sysctl_lowmem_reserve_ratio[idx];
                                managed_pages += lower_zone->managed_pages;
                        }
                }
        }

        /* update totalreserve_pages */
        calculate_totalreserve_pages();
}

zone별 lowmem_reserve[] 값을 산출한다.

  • for_each_online_pgdat(pgdat) {
    • 전체 online 노드만큼 루프를 돈다.
  • for (j = 0; j < MAX_NR_ZONES; j++) {
    • 전체 zone 갯수만큼 루프를 돈다.
  • idx = j; while (idx) { struct zone *lower_zone; idx–;
    • 가장 처음 zone을 제외하고 루프 카운터로 지정된 zone부터 밑으로 루프를 돈다.
  • if (sysctl_lowmem_reserve_ratio[idx] < 1) sysctl_lowmem_reserve_ratio[idx] = 1;
    • sysctl_lowmem_reserve_ratio[idx] 값이 1보다 작으면 1로 대입한다.
  • lower_zone = pgdat->node_zones + idx;
    • 변경하고자 하는 하위 zone을 선택한다.
  • lower_zone->lowmem_reserve[j] = managed_pages / sysctl_lowmem_reserve_ratio[idx];
    • 선택된 하위 zone의 lowmem_reserve[j] 값에 managed_pages / 비율로 나누어 대입한다.
      • sysctl_lowmem_reserve_ratio 비율
        • CONFIG_DMA 및 CONFIG_DMA32의 경우에만 0x100, 그 외 0x20
        • rpi2: CONFIG_NORMAL만 사용하므로 0x20 (32)로 설정되어 있다.
    • 참고: lowmem_reserve_ratio | barrios
  •  managed_pages += lower_zone->managed_pages;
    • 가장 밑에 있는 zone에 managed_pages를 중첩시키기 위해 보관한다.
  •  calculate_totalreserve_pages();
    • zone별 lowmem_reserve[] 값이 산출 완료되었으므로 totalreserve_pages 값을 산출한다.

 

다음 그림은 zone별 lowmem_reserve[]를 산출하는 과정을 보여준다.

setup_per_zone_lowmem_reserve-1

 

setup_per_zone_inactive_ratio()

mm/page_alloc.c

static void __meminit setup_per_zone_inactive_ratio(void)
{
        struct zone *zone;

        for_each_zone(zone)
                calculate_zone_inactive_ratio(zone);
}

모든 zone의 inactive anon 비율을 산출한다.

  • zone->managed_pages가 256k pages (1GB) 미만인 경우 zone->inactive_ratio가 1이되어 active anon과 inactive anon의 비율을 1:1로 설정한다. 만일 managed_pages가 256k pages (1GB)를 초과한 경우 inactive_ratio 값은 3 이상이 되면서 inactive anon의 비율이 1/3 이하로 줄어든다.

 

다음 그림은 각 zone의 managed_pages  크기에 따라 inactive_ratio 값이 산출되는 과정을 보여준다.

setup_per_zone_inactive_ratio-1

 

calculate_zone_inactive_ratio()

mm/page_alloc.c

/*
 * The inactive anon list should be small enough that the VM never has to
 * do too much work, but large enough that each inactive page has a chance
 * to be referenced again before it is swapped out.
 *
 * The inactive_anon ratio is the target ratio of ACTIVE_ANON to
 * INACTIVE_ANON pages on this zone's LRU, maintained by the
 * pageout code. A zone->inactive_ratio of 3 means 3:1 or 25% of
 * the anonymous pages are kept on the inactive list.
 *
 * total     target    max
 * memory    ratio     inactive anon
 * -------------------------------------
 *   10MB       1         5MB
 *  100MB       1        50MB
 *    1GB       3       250MB
 *   10GB      10       0.9GB
 *  100GB      31         3GB
 *    1TB     101        10GB
 *   10TB     320        32GB
 */
static void __meminit calculate_zone_inactive_ratio(struct zone *zone)
{
        unsigned int gb, ratio;

        /* Zone size in gigabytes */
        gb = zone->managed_pages >> (30 - PAGE_SHIFT);
        if (gb)
                ratio = int_sqrt(10 * gb);
        else
                ratio = 1;

        zone->inactive_ratio = ratio;
}

지정된 zone의 inactive anon 비율을 산출한다.

  • managed 페이지가 1G 미만인 경우 ratio는 1이된다.
    • inactive anon=1 : active anon=1
  • managed 페이지가 1G 이상인 경우 * 10한 후 제곱근(루트)을 하여 ratio를 설정한다. (inactive = 1 / ratio)
    • 예) managed pages=1G
      • inactive_ratio=3
        • inactive anon=1 : active anon=3
          • inactive anon=250M : active anon=750M

 

워터마크 관련 값

  • min_free_kbytes
    • free page가 부족해지면 kswapd가 페이지 회수에 나서는데 최소 free page 이상을 확보한다.
      • 이 페이지 수를 만족시키지 못하는 경우 OOM이 발생하게 된다.
  • zone->watermark[WMARK_MIN]
    • free pages가 이 값 미만이 되는 경우 페이지 할당 시 필요 시마다 페이지 회수 작업을 한다.
  • zone->watermark[WMARK_LOW]
    • free pages가 이 값 미만이 되는 경우 kswapd가 깨어나 페이지 회수 작업을 한다.
    • zone->watermark[WMARK_MIN] * 125%
  • zone->watermark[WMARK_HIGH]
    • free pages가 이 값을 초과하는 경우 kswapd가 슬립한다.
    • zone->watermark[WMARK_MIN] * 150%

 

참고

Zoned Allocator -2- (물리 페이지 할당-Slowpath)

 

Slowpath

NUMA의 경우 처음 주어진 메모리 정책에 따른 zonelist에서 할당이 실패한 경우 __GFP_IO 및 GFP_FS를 제거하고 slowpath 단계를 진행한다. slowpath 단계를 진행하는 동안 __GFP_NO_SWAPD 요청이 없으면 kswapd를 깨워 백그라운드에서 페이지 회수도 동시에 진행한다. 페이지 할당으 시도하는 동안 __GFP_WAIT 요청이 있는 경우 page compaction이나 direct reclaim을 통해 할당 할 수 있는 페이지를 확보한다. 마지막으로 __GFP_NOFAIL 이나 __GFP_REPEAT 등이 허용되는 경우 페이지 할당이 성공할 때까지 재시도를 한다

 

alloc_pages-2a

 

 

__alloc_pages_slowpath()

mm/page_alloc.c

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                                                struct alloc_context *ac)
{
        const gfp_t wait = gfp_mask & __GFP_WAIT;
        struct page *page = NULL;
        int alloc_flags;
        unsigned long pages_reclaimed = 0;
        unsigned long did_some_progress;
        enum migrate_mode migration_mode = MIGRATE_ASYNC;
        bool deferred_compaction = false;
        int contended_compaction = COMPACT_CONTENDED_NONE;

        /*
         * In the slowpath, we sanity check order to avoid ever trying to
         * reclaim >= MAX_ORDER areas which will never succeed. Callers may
         * be using allocators in order of preference for an area that is
         * too large.
         */
        if (order >= MAX_ORDER) {
                WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
                return NULL;
        }

        /*
         * GFP_THISNODE (meaning __GFP_THISNODE, __GFP_NORETRY and
         * __GFP_NOWARN set) should not cause reclaim since the subsystem
         * (f.e. slab) using GFP_THISNODE may choose to trigger reclaim
         * using a larger set of nodes after it has established that the
         * allowed per node queues are empty and that nodes are
         * over allocated.
         */
        if (IS_ENABLED(CONFIG_NUMA) &&
            (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
                goto nopage;
  • const gfp_t wait = gfp_mask & __GFP_WAIT;
    • __GFP_WAIT 플래그를 요청했는지 여부를 보관한다.
  • enum migrate_mode migration_mode = MIGRATE_ASYNC;
    • migration_mode를 MIGRATE_ASYNC로 설정하여 block 되지 않도록 한다.
  • if (order >= MAX_ORDER) { return NULL; }
    • MAX_order 이상을 요구하는 경우 할당을 포기하고 리턴한다.
  • if (IS_ENABLED(CONFIG_NUMA) && (gfp_mask & GFP_THISNODE) == GFP_THISNODE) goto nopage;
    • NUMA 시스템이면서 현재 노드로 제한 요청을 한 경우 페이지 할당을 포기한다.

 

retry:
        if (!(gfp_mask & __GFP_NO_KSWAPD))
                wake_all_kswapds(order, ac);

        /*
         * OK, we're below the kswapd watermark and have kicked background
         * reclaim. Now things get more complex, so set up alloc_flags according
         * to how we want to proceed.
         */
        alloc_flags = gfp_to_alloc_flags(gfp_mask);

        /*
         * Find the true preferred zone if the allocation is unconstrained by
         * cpusets.
         */
        if (!(alloc_flags & ALLOC_CPUSET) && !ac->nodemask) {
                struct zoneref *preferred_zoneref;
                preferred_zoneref = first_zones_zonelist(ac->zonelist,
                                ac->high_zoneidx, NULL, &ac->preferred_zone);
                ac->classzone_idx = zonelist_zone_idx(preferred_zoneref);
        }

        /* This is the last chance, in general, before the goto nopage. */
        page = get_page_from_freelist(gfp_mask, order,
                                alloc_flags & ~ALLOC_NO_WATERMARKS, ac);
        if (page)
                goto got_pg;

        /* Allocate without watermarks if the context allows */
        if (alloc_flags & ALLOC_NO_WATERMARKS) {
                /*
                 * Ignore mempolicies if ALLOC_NO_WATERMARKS on the grounds
                 * the allocation is high priority and these type of
                 * allocations are system rather than user orientated
                 */
                ac->zonelist = node_zonelist(numa_node_id(), gfp_mask);

                page = __alloc_pages_high_priority(gfp_mask, order, ac);

                if (page) {
                        goto got_pg;
                }
        }

        /* Atomic allocations - we can't balance anything */
        if (!wait) {
                /*
                 * All existing users of the deprecated __GFP_NOFAIL are
                 * blockable, so warn of any new users that actually allow this
                 * type of allocation to fail.
                 */
                WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL);
                goto nopage;
        }

        /* Avoid recursion of direct reclaim */
        if (current->flags & PF_MEMALLOC)
                goto nopage;

        /* Avoid allocations with no watermarks from looping endlessly */
        if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL))
                goto nopage;
  • retry:
    • 페이지 할당을 재시도할 때 retry 레이블로 이동되어 온다.
  • if (!(gfp_mask & __GFP_NO_KSWAPD)) wake_all_kswapds(order, ac);
    • __GFP_NO_KSWAPD 플래그가 사용되지 않은 경우 zonelist 중 현재 alloc context에 관계된 노드와 zone의 모든 kswpad들을 모두 깨운다.
  • alloc_flags = gfp_to_alloc_flags(gfp_mask);
    • gfp_mask에 따라 다음 항목을 alloc_flags에 추가할 수 있다.
      • ALLOC_WMARK_MIN 또는 ALLOC_NO_WATERMARKS
      • ALLOC_CPUSET
      • __GFP_HIGH
      • ALLOC_HARDER
      • ALLOC_CMA
  • if (!(alloc_flags & ALLOC_CPUSET) && !ac->nodemask) {
    • cpuset을 사용하지 않으면서 전체 노드에 대한 요청인 경우
  • preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->high_zoneidx, NULL, &ac->preferred_zone); ac->classzone_idx = zonelist_zone_idx(preferred_zoneref);
    • zonelist에서 모든 노드와 high_zoneidx 이하의 zone에 대해 처음 발견된 zone
  • page = get_page_from_freelist(gfp_mask, order, alloc_flags & ~ALLOC_NO_WATERMARKS, ac);
    • alloc_flags에서 ALLOC_NO_WATERMARKS를 제거하고 페이지 할당을 요청한다.
  • if (page) goto got_pg;
    • 할당이 성공된 경우 got_pg로 이동한다.
  • if (alloc_flags & ALLOC_NO_WATERMARKS) {
    • 페이지 할당이 실패한 경우 ALLOC_NO_WATERMARKS 플래그가 설정된 경우
  • ac->zonelist = node_zonelist(numa_node_id(), gfp_mask); page = __alloc_pages_high_priority(gfp_mask, order, ac);
    • 워터 마크 제한 없이 페이지 할당을 시도하며 실패한 경우 __GFP_NOFAIL 요청을 사용한 경우 20ms씩 쉬어가며 성공할 때까지 시도한다.
  • if (!wait) { goto nopage; }
    • 요청이 sleep을 허용하지 않는 경우 compaction 등 다음 단계를 수행하지 않고 페이지 할당이 실패되어 함수를 빠져나간다.
  • if (current->flags & PF_MEMALLOC) goto nopage;
    • 현재 태스크가 PF_MEMALLOC 플래그를 가진 경우 compaction 등 다음 단계를 수행하지 않고 페이지 할당이 실패되어 함수를 빠져나간다.
  • if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL)) goto nopage;
    • 현재 태스크가 TIF_MEMDIE 플래그를 가진 경우 compaction 등 다음 단계를 수행하지 않고 페이지 할당이 실패되어 함수를 빠져나간다.

 

        /*
         * Try direct compaction. The first pass is asynchronous. Subsequent
         * attempts after direct reclaim are synchronous
         */
        page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                                        migration_mode,
                                        &contended_compaction,
                                        &deferred_compaction);
        if (page)
                goto got_pg;

        /* Checks for THP-specific high-order allocations */
        if ((gfp_mask & GFP_TRANSHUGE) == GFP_TRANSHUGE) {
                /*
                 * If compaction is deferred for high-order allocations, it is
                 * because sync compaction recently failed. If this is the case
                 * and the caller requested a THP allocation, we do not want
                 * to heavily disrupt the system, so we fail the allocation
                 * instead of entering direct reclaim.
                 */
                if (deferred_compaction)
                        goto nopage;

                /*
                 * In all zones where compaction was attempted (and not
                 * deferred or skipped), lock contention has been detected.
                 * For THP allocation we do not want to disrupt the others
                 * so we fallback to base pages instead.
                 */
                if (contended_compaction == COMPACT_CONTENDED_LOCK)
                        goto nopage;

                /*
                 * If compaction was aborted due to need_resched(), we do not
                 * want to further increase allocation latency, unless it is
                 * khugepaged trying to collapse.
                 */
                if (contended_compaction == COMPACT_CONTENDED_SCHED
                        && !(current->flags & PF_KTHREAD))
                        goto nopage;
        }
  • page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac, migration_mode, &contended_compaction, &deferred_compaction);
    • page compaction 수행 후 페이지 할당을 시도한다.
  • if ((gfp_mask & GFP_TRANSHUGE) == GFP_TRANSHUGE) {
    • GFP_TRANSHUGE 플래그가 사용된 경우
  • if (deferred_compaction) goto nopage;
    • 최근에 high order 페이지 요청에 대해 compaction이 한 번 실패되어 compaction이 유예된 경우 할당을 포기하고 함수를 빠져나간다.
  • if (contended_compaction == COMPACT_CONTENDED_LOCK) goto nopage;
    • 모든 zone들이 compaction이 시도 되면서 lock contension(혼잡)이 발생하면 후보(fallback) zone의 사용을 포기하고 함수를 빠져나간다.
  • if (contended_compaction == COMPACT_CONTENDED_SCHED && !(current->flags & PF_KTHREAD)) goto nopage;
    • compaction 중 리스케쥴링되었던 경우 이면서 현재 태스크가 커널 스레드가 아닌 경우 할당을 포기하고 함수를 빠져나간다.

 

        /*
         * It can become very expensive to allocate transparent hugepages at
         * fault, so use asynchronous memory compaction for THP unless it is
         * khugepaged trying to collapse.
         */
        if ((gfp_mask & GFP_TRANSHUGE) != GFP_TRANSHUGE ||
                                                (current->flags & PF_KTHREAD))
                migration_mode = MIGRATE_SYNC_LIGHT;

        /* Try direct reclaim and then allocating */
        page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                                                        &did_some_progress);
        if (page)
                goto got_pg;

        /* Check if we should retry the allocation */
        pages_reclaimed += did_some_progress;
        if (should_alloc_retry(gfp_mask, order, did_some_progress,
                                                pages_reclaimed)) {
                /*
                 * If we fail to make progress by freeing individual
                 * pages, but the allocation wants us to keep going,
                 * start OOM killing tasks.
                 */
                if (!did_some_progress) {
                        page = __alloc_pages_may_oom(gfp_mask, order, ac,
                                                        &did_some_progress);
                        if (page)
                                goto got_pg;
                        if (!did_some_progress)
                                goto nopage;
                }
                /* Wait for some write requests to complete then retry */
                wait_iff_congested(ac->preferred_zone, BLK_RW_ASYNC, HZ/50);
                goto retry;
        } else {
                /*
                 * High-order allocations do not necessarily loop after
                 * direct reclaim and reclaim/compaction depends on compaction
                 * being called after reclaim so call directly if necessary
                 */
                page = __alloc_pages_direct_compact(gfp_mask, order,
                                        alloc_flags, ac, migration_mode,
                                        &contended_compaction,
                                        &deferred_compaction);
                if (page)
                        goto got_pg;
        }

nopage:
        warn_alloc_failed(gfp_mask, order, NULL);
got_pg:
        return page;
}
  • if ((gfp_mask & GFP_TRANSHUGE) != GFP_TRANSHUGE || (current->flags & PF_KTHREAD)) migration_mode = MIGRATE_SYNC_LIGHT;
    • GFP_TRANSHUGE 요청이 아니거나 현재 태스크가 커널 스레드인 경우 migration 모드를 MIGRATE_SYNC_LIGHT로 설정하여 dirty page를 제외하고 reclaim을 시도할 수 있도록 설정한다.
  • page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac, &did_some_progress);
    • 일단 먼저 direct reclaim을 통해 페이지 회수 후 할당을 시도한다.
  • pages_reclaimed += did_some_progress; if (should_alloc_retry(gfp_mask, order, did_some_progress, pages_reclaimed)) {
    • 재시도 할 수 있는지 확인한다.
  • if (!did_some_progress) { page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    • 회수된 페이지가 없으면 마지막으로 할당을 다시 시도하고 없는 경우 oom을 발생시킨다.
  • if (page) goto got_pg; if (!did_some_progress) goto nopage;
    • 페이지가 할당된 경우 성공적으로 완료되며 그렇지 않고 페이지 회수가 전혀 되지 않은 경우 할당 실패로 함수를 빠져나간다.
  • wait_iff_congested(ac->preferred_zone, BLK_RW_ASYNC, HZ/50); goto retry;
    • 회수된 페이지가 일부 있었던 경우 할당에 각 태스크들이 경쟁하여 혼잡한 경우 약 20ms 정도 잠시 쉬었다가 다시 시도한다.
  • } else { page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac, migration_mode, &contended_compaction, &deferred_compaction);
    • 최종적으로 direct compact 후에 페이지 할당을 시도한다. 할당이 실패하면 함수를 빠져나간다.

 

gfp_to_alloc_flags()

mm/page_alloc.c

static inline int
gfp_to_alloc_flags(gfp_t gfp_mask)
{
        int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
        const bool atomic = !(gfp_mask & (__GFP_WAIT | __GFP_NO_KSWAPD));

        /* __GFP_HIGH is assumed to be the same as ALLOC_HIGH to save a branch. */
        BUILD_BUG_ON(__GFP_HIGH != (__force gfp_t) ALLOC_HIGH);

        /*
         * The caller may dip into page reserves a bit more if the caller
         * cannot run direct reclaim, or if the caller has realtime scheduling
         * policy or is asking for __GFP_HIGH memory.  GFP_ATOMIC requests will
         * set both ALLOC_HARDER (atomic == true) and ALLOC_HIGH (__GFP_HIGH).
         */
        alloc_flags |= (__force int) (gfp_mask & __GFP_HIGH);

        if (atomic) {
                /*
                 * Not worth trying to allocate harder for __GFP_NOMEMALLOC even
                 * if it can't schedule.
                 */
                if (!(gfp_mask & __GFP_NOMEMALLOC))
                        alloc_flags |= ALLOC_HARDER;
                /*
                 * Ignore cpuset mems for GFP_ATOMIC rather than fail, see the
                 * comment for __cpuset_node_allowed().
                 */
                alloc_flags &= ~ALLOC_CPUSET;
        } else if (unlikely(rt_task(current)) && !in_interrupt())
                alloc_flags |= ALLOC_HARDER;

        if (likely(!(gfp_mask & __GFP_NOMEMALLOC))) {
                if (gfp_mask & __GFP_MEMALLOC)
                        alloc_flags |= ALLOC_NO_WATERMARKS;
                else if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
                        alloc_flags |= ALLOC_NO_WATERMARKS;
                else if (!in_interrupt() &&
                                ((current->flags & PF_MEMALLOC) ||
                                 unlikely(test_thread_flag(TIF_MEMDIE))))
                        alloc_flags |= ALLOC_NO_WATERMARKS;
        }
#ifdef CONFIG_CMA
        if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
                alloc_flags |= ALLOC_CMA;
#endif
        return alloc_flags;
}

gfp_mask에 따라 alloc_flags를 설정한다. 추가 설정될 수 있는 할당 플래그는 다음과 같다.

  • ALLOC_WMARK_MIN 또는 ALLOC_NO_WATERMARKS
  • ALLOC_CPUSET
  • __GFP_HIGH
  • ALLOC_HARDER
  • ALLOC_CMA

 

  • int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
    • alloc_flags에 min 워터마크 할당과 cpuset 사용을 하도록 한다.
  • const bool atomic = !(gfp_mask & (__GFP_WAIT | __GFP_NO_KSWAPD));
    • __GFP_WAIT 또는 __GFP_NO_KSWAPD 요청이 없는 경우 atomic을 true로 설정한다.
  •  alloc_flags |= (__force int) (gfp_mask & __GFP_HIGH);
    • gfp_mask에 __GFP_HIGH가 요청된 경우 alloc_flags에도 추가한다.
  • if (atomic) { if (!(gfp_mask & __GFP_NOMEMALLOC)) alloc_flags |= ALLOC_HARDER; alloc_flags &= ~ALLOC_CPUSET;
    • atomic 처리가 가능한 경우이면 ALLOC_CPUSET을 제거하고 또한 __GFP_NOMEMALLOC 요청이 없으면 alloc_flags에 ALLOC_HARDER를 추가한다.
  • } else if (unlikely(rt_task(current)) && !in_interrupt()) alloc_flags |= ALLOC_HARDER;
    • atomic 처리가 가능한 경우가 아니지만 적은 확률로 현재 태스크가 100이하의 높은 우선 순위를 가진 태스이면서 인터럽트 핸들러에서 호출된 경우가 아니면 ALLOC_HARDER를 추가한다.
  • if (likely(!(gfp_mask & __GFP_NOMEMALLOC))) {
    • 높은 확률로 __GFP_NOMEMALLOC 요청이 없는 경우
  • if (gfp_mask & __GFP_MEMALLOC) alloc_flags |= ALLOC_NO_WATERMARKS;
    • __GFP_MEMALLOC 요청이 있는 경우 ALLOC_NO_WATERMARKS를 추가하여 워터마크를 사용하지 않게 한다.
  • else if (in_serving_softirq() && (current->flags & PF_MEMALLOC)) alloc_flags |= ALLOC_NO_WATERMARKS;
    • softirq에서 호출된 경우이면서 PF_MEMALLOC 요청이 있는 경우 ALLOC_NO_WATERMARKS를 추가하여 워터마크를 사용하지 않게 한다.
  • else if (!in_interrupt() && ((current->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))) alloc_flags |= ALLOC_NO_WATERMARKS;
    • 인터럽트 핸들러에서 호출된 경우가 아니면서 현재 태스크의 플래그에 PF_MEMALLOC를 가진 경우 또는 작은 확률로 현재 태스크가 TIF_MEMDIE 플래그를 가진 경우 ALLOC_NO_WATERMARKS를 추가하여 워터마크를 사용하지 않게 한다.
  • if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE) alloc_flags |= ALLOC_CMA;
    • gfp 플래그로 변환한 migrate 타입이 MIGRATE_MOVABLE인 경우 alloc_flags에 ALLOC_CMA를 추가한다.

 

gfpflags_to_migratetype()

include/linux/gfp.h

/* Convert GFP flags to their corresponding migrate type */
static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{
        WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);

        if (unlikely(page_group_by_mobility_disabled))
                return MIGRATE_UNMOVABLE;

        /* Group based on mobility */
        return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
                ((gfp_flags & __GFP_RECLAIMABLE) != 0);
}

GFP 플래그에 대응하는 migrate 타입을 다음 중 하나로 변환한다.

  • MIGRATE_UNMOVABLE
  • MIGRATE_RECLAIMABLE
  • MIGRATE_MOVABLE

 

  • if (unlikely(page_group_by_mobility_disabled)) return MIGRATE_UNMOVABLE;
    • 적은 확률로 mobility 타입별로 페이지 그루핑을 하지 않는 경우 MIGRATE_UNVMOVABLE 한 가지만을 사용한다.
  • return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | ((gfp_flags & __GFP_RECLAIMABLE) != 0);
    • gfp_flags의 mobility 타입으로 migrate 타입을 다음 중 하나로 결정한다.
      • MIGRATE_UNMOVABLE
      • MIGRATE_RECLAIMABLE
      • MIGRATE_MOVABLE

 

__alloc_pages_high_priority()

mm/page_alloc.c

/*
 * This is called in the allocator slow-path if the allocation request is of
 * sufficient urgency to ignore watermarks and take other desperate measures
 */
static inline struct page *
__alloc_pages_high_priority(gfp_t gfp_mask, unsigned int order,
                                const struct alloc_context *ac)
{
        struct page *page;

        do {
                page = get_page_from_freelist(gfp_mask, order,
                                                ALLOC_NO_WATERMARKS, ac);

                if (!page && gfp_mask & __GFP_NOFAIL)
                        wait_iff_congested(ac->preferred_zone, BLK_RW_ASYNC,
                                                                        HZ/50);
        } while (!page && (gfp_mask & __GFP_NOFAIL));

        return page;
}

워터 마크 제한 없이 페이지 할당을 시도하며 실패한 경우 __GFP_NOFAIL 요청을 사용한 경우 20ms씩 쉬어가며 성공할 때까지 시도한다.

  • page = get_page_from_freelist(gfp_mask, order, ALLOC_NO_WATERMARKS, ac);
    • 워터 마크 제한 없이 페이지 할당을 시도한다.
  • if (!page && gfp_mask & __GFP_NOFAIL) wait_iff_congested(ac->preferred_zone, BLK_RW_ASYNC, HZ/50);
    • 페이지가 할당되지 않고 __GFP_NOFAIL 옵션이 있는 경우 잠시 20ms 정도 쉰다.
  • } while (!page && (gfp_mask & __GFP_NOFAIL));
    • 페이지가 할당되지 않고 __GFP_NOFAIL 옵션이 있는 경우 다시 시도한다.

 

should_alloc_retry()

mm/page_alloc.c

static inline int
should_alloc_retry(gfp_t gfp_mask, unsigned int order,
                                unsigned long did_some_progress,
                                unsigned long pages_reclaimed)
{
        /* Do not loop if specifically requested */
        if (gfp_mask & __GFP_NORETRY)
                return 0;

        /* Always retry if specifically requested */
        if (gfp_mask & __GFP_NOFAIL)
                return 1;

        /*
         * Suspend converts GFP_KERNEL to __GFP_WAIT which can prevent reclaim
         * making forward progress without invoking OOM. Suspend also disables
         * storage devices so kswapd will not help. Bail if we are suspending.
         */
        if (!did_some_progress && pm_suspended_storage())
                return 0;

        /*
         * In this implementation, order <= PAGE_ALLOC_COSTLY_ORDER
         * means __GFP_NOFAIL, but that may not be true in other
         * implementations.
         */
        if (order <= PAGE_ALLOC_COSTLY_ORDER)
                return 1;

        /*
         * For order > PAGE_ALLOC_COSTLY_ORDER, if __GFP_REPEAT is
         * specified, then we retry until we no longer reclaim any pages
         * (above), or we've reclaimed an order of pages at least as
         * large as the allocation's order. In both cases, if the
         * allocation still fails, we stop retrying.
         */
        if (gfp_mask & __GFP_REPEAT && pages_reclaimed < (1 << order))
                return 1;

        return 0;
}

할당 실패 시 재시도 가능한지 여부를 알아온다. 1=재시도 가능, 0=재시도 금지

  • if (gfp_mask & __GFP_NORETRY) return 0;
    • 재시도 금지 요청이 있으므로 0을 반환한다.
  • if (gfp_mask & __GFP_NOFAIL) return 1;
    • 실패를 허용치 않게 하라는 요청이 있으므로 재시도를 위해 1을 반환한다.
  • if (!did_some_progress && pm_suspended_storage()) return 0;
    • 회수된 페이지가 없으면서 할당에 storage 요청을 금지한 경우 0을 반환한다.
  • if (order <= PAGE_ALLOC_COSTLY_ORDER) return 1;
    • order 페이지 확보에 cost가 저렴한 편이면 재시도를 위해 1을 반환한다.
      • PAGE_ALLOC_COSTLY_ORDER=3
  • if (gfp_mask & __GFP_REPEAT && pages_reclaimed < (1 << order)) return 1;
    • 반복 요청이 있으면서 누적된 회수 페이지가 요청 order 보다 작아 더 확보를 하여야 하는 경우 재시도를 위해 1을 반환한다.

 

pm_suspended_storage()

mm/page_alloc.c

bool pm_suspended_storage(void)
{
        if ((gfp_allowed_mask & GFP_IOFS) == GFP_IOFS)
                return false;
        return true;
}

할당에 storage 요청을 금지했는지 여부를 알아온다.

  • 전역 gfp_allowed_mask에 GFP_IOFS 플래그가 설정된 경우 false를 반환한다.
  • 참고로 커널 초기화 중에는 GFP_IOFS를 사용하지 않는다.

 

참고

Zoned Allocator -1- (물리 페이지 할당-Fastpath)

 

zone-1a

 

관련 커널 옵션

FAIL_PAGE_ALLOC

alloc_pages()에 대한 디버그 기능으로 fault-injection 처리를 위한 코드가 추가된다.

  • “fail_page_alloc=<interval>,<probability>,<space>,<times>” 커널 옵션을 사용하여 FAULT-INJECTION에 대한 4가지 속성을 변경할 수 있다.

 

물리 페이지 할당(alloc)

 

다음 그림은 페이지 할당을 하는 흐름을 보여준다.

alloc_pages-1

 

alloc_pages()

include/linux/gfp.h

#ifdef CONFIG_NUMA
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
        return alloc_pages_current(gfp_mask, order);
}
#else
#define alloc_pages(gfp_mask, order) \
                alloc_pages_node(numa_node_id(), gfp_mask, order)
#endif

버디 시스템을 통하여 order 페이지만큼 할당 받는다.

  • NUMA 시스템인 경우 메모리 정책에 따라 노드를 선택하고 버디 시스템을 통하여 order 페이지만큼 할당 받는다.
  • NUMA 시스템이 아닌 경우 현재 노드에서 버디 시스템을 통하여 order 페이지만큼 할당 받는다.

 

alloc_pages_current()

mm/mempolicy.c

/**
 *      alloc_pages_current - Allocate pages.
 *
 *      @gfp:
 *              %GFP_USER   user allocation,
 *              %GFP_KERNEL kernel allocation,
 *              %GFP_HIGHMEM highmem allocation,
 *              %GFP_FS     don't call back into a file system.
 *              %GFP_ATOMIC don't sleep.        
 *      @order: Power of two of allocation size in pages. 0 is a single page.
 *
 *      Allocate a page from the kernel page pool.  When not in
 *      interrupt context and apply the current process NUMA policy.
 *      Returns NULL when no page can be allocated.
 *
 *      Don't call cpuset_update_task_memory_state() unless
 *      1) it's ok to take cpuset_sem (can WAIT), and
 *      2) allocating for current task (not interrupt).
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
        struct mempolicy *pol = &default_policy;
        struct page *page;
        unsigned int cpuset_mems_cookie;
                        
        if (!in_interrupt() && !(gfp & __GFP_THISNODE))
                pol = get_task_policy(current);

retry_cpuset:
        cpuset_mems_cookie = read_mems_allowed_begin();

        /*
         * No reference counting needed for current->mempolicy
         * nor system default_policy
         */
        if (pol->mode == MPOL_INTERLEAVE)
                page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
        else
                page = __alloc_pages_nodemask(gfp, order,
                                policy_zonelist(gfp, pol, numa_node_id()),
                                policy_nodemask(gfp, pol));

        if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
                goto retry_cpuset;
                                          
        return page;
}
EXPORT_SYMBOL(alloc_pages_current);

NUMA 시스템에서 메모리 정책에 따라 노드를 선택하고 버디 시스템을 통하여 order 페이지만큼 할당 받는다.

  • struct mempolicy *pol = &default_policy;
  • if (!in_interrupt() && !(gfp & __GFP_THISNODE)) pol = get_task_policy(current);
    • 인터럽트 핸들러에서 호출되어 수행 중이거나 현재 노드에서만 할당하라는 플래그 요청이 있는 경우 디폴트 메모리 정책을 사용하여 로컬 노드에서 할당하게 한다.
    • 그렇지 않은 경우 현재 태스크의 메모리 정책을 알아온다.
  • retry_cpuset: cpuset_mems_cookie = read_mems_allowed_begin();
    • 시퀀스 락에 의해 재시도 되는 경우 재 호출될 레이블이다.
  • if (pol->mode == MPOL_INTERLEAVE) page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
    • MPOL_INTERLEAVE 메모리 정책 을 사용하는 경우 페이지를 노드별로 돌아가며 할당하게 한다.
  • else page = __alloc_pages_nodemask(gfp, order, policy_zonelist(gfp, pol, numa_node_id()), policy_nodemask(gfp, pol));
    • 그 외 다른 메모리 정책을 사용하는 경우 현재노드의 정책에 따른 zonelist에서 order 페이지를 할당 받는다.
  • if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) goto retry_cpuset;
    • 작은 확률로 페이지 할당이 실패하고 사용한 시퀀스 락이 실패하는 경우 할당을 재시도한다.

 

alloc_pages_node()

include/linux/gfp.h

static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
                                                unsigned int order)
{
        /* Unknown node is current node */
        if (nid < 0)
                nid = numa_node_id();

        return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

지정된 노드의 zonelist에서 order 페이지를 할당 받는다. 만일 알 수 없는 노드가 지정된 경우 현재 노드의 zonelist에서 할당 받는다.

 

__alloc_pages()

include/linux/gfp.h

static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order,
                struct zonelist *zonelist)
{
        return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

지정된 zonelist에서 order 페이지를 할당 받는다.

 

메모리 정책(Policy) 관련

default_policy 전역 객체

mm/mempolicy.c

/*
 * run-time system-wide default policy => local allocation
 */
static struct mempolicy default_policy = {
        .refcnt = ATOMIC_INIT(1), /* never free it */
        .mode = MPOL_PREFERRED,
        .flags = MPOL_F_LOCAL,
};
  • 사용 메모리 정책은 MPOL_PREFERED로 하고 로컬 노드에서 할당을 하는 것을 권장한다.
    • MPOL_F_LOCAL
      • 로컬 노드 할당

 

get_task_policy()

mm/mempolicy.c

struct mempolicy *get_task_policy(struct task_struct *p)
{
        struct mempolicy *pol = p->mempolicy;
        int node;

        if (pol)
                return pol;

        node = numa_node_id();
        if (node != NUMA_NO_NODE) {
                pol = &preferred_node_policy[node];
                /* preferred_node_policy is not initialised early in boot */
                if (pol->mode)
                        return pol;
        }

        return &default_policy;
}

현재 태스크의 메모리 정책을 아래의 케이스에 따라 반환한다.

  • 현재 태스크의 메모리 정책이 지정되어 있는 경우 그 값을 반환한다.
  • 만일 노드가 지정되고 현재 태스크의 메모리 정책이 지정되지 않은 경우 노드별 권장 메모리 정책이 반환된다.
  • 그 외의 케이스에서는 디폴트 메모리 정책을 반환한다.
    • 로컬 노드에서 할당

 

interleave_nodes()

mm/mempolicy.c

/* Do dynamic interleaving for a process */
static unsigned interleave_nodes(struct mempolicy *policy)
{
        unsigned nid, next;
        struct task_struct *me = current;

        nid = me->il_next;
        next = next_node(nid, policy->v.nodes);
        if (next >= MAX_NUMNODES) 
                next = first_node(policy->v.nodes);
        if (next < MAX_NUMNODES)
                me->il_next = next;
        return nid;
}

currnet->il_next에 기억된 노드를 반환한다. 그 동안 다음에 배정할 노드를 current->il_next에 기억해놓는다.

  • il_next에는 메모리 정책이 MPOL_INTERLEAVE로 설정된 경우 사용할 노드를 interleave(round robin) 방식에 의해 배정한다.
    • next_node()
      • 노드 비트맵에 대하여 지정된 노드의 다음노드를 알아온다. 못 찾은 경우 MAX_NUMNODES를 반환한다.
    • first_node()
      • 노드 비트맵의 처음에 위치한 노드를 알아온다.

 

alloc_page_interleave()

mm/mempolicy.c

/* Allocate a page in interleaved policy.
   Own path because it needs to do special accounting. */
static struct page *alloc_page_interleave(gfp_t gfp, unsigned order,
                                        unsigned nid)
{
        struct zonelist *zl;
        struct page *page;

        zl = node_zonelist(nid, gfp);
        page = __alloc_pages(gfp, order, zl);
        if (page && page_zone(page) == zonelist_zone(&zl->_zonerefs[0]))
                inc_zone_page_state(page, NUMA_INTERLEAVE_HIT);
        return page;
}

지정된 노드의 zonelist를 알아와서 order 페이지를 할당 받는다.

  • zl = node_zonelist(nid, gfp);
    • 지정된 노드와 플래그를 통해 zonelist를 가져온다.
      • gfp 플래그에 따라  전체 노드 zonelist인 node_zonelist[0]을 가져오거나 지정된 노드 zone만을 담은 node_zonelist[1]을 가져온다.
  • page = __alloc_pages(gfp, order, zl);
    • zonelist에서 order 페이지를 할당 받는다.
  • if (page && page_zone(page) == zonelist_zone(&zl->_zonerefs[0])) inc_zone_page_state(page, NUMA_INTERLEAVE_HIT);
    • 만일 페이지 할당이 성공하고 할당 받은 page의 zone이 zonelist의 첫 zone인 경우 NUMA_INTERLEAVE_HIT stat을 증가시킨다.

 

__alloc_pages_nodemask()

mm/page_alloc.c

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
                        struct zonelist *zonelist, nodemask_t *nodemask)
{
        struct zoneref *preferred_zoneref;
        struct page *page = NULL;
        unsigned int cpuset_mems_cookie;
        int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
        gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
        struct alloc_context ac = {
                .high_zoneidx = gfp_zone(gfp_mask),
                .nodemask = nodemask,
                .migratetype = gfpflags_to_migratetype(gfp_mask),
        };

        gfp_mask &= gfp_allowed_mask;

        lockdep_trace_alloc(gfp_mask);

        might_sleep_if(gfp_mask & __GFP_WAIT);

        if (should_fail_alloc_page(gfp_mask, order))
                return NULL;

        /*
         * Check the zones suitable for the gfp_mask contain at least one
         * valid zone. It's possible to have an empty zonelist as a result
         * of GFP_THISNODE and a memoryless node
         */
        if (unlikely(!zonelist->_zonerefs->zone))
                return NULL;

        if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE)
                alloc_flags |= ALLOC_CMA;

지정된 노드마스크, zonelist 및 flags 설정을 참고하여 노드와 zone을 선택한 후 order 페이지 할당을 한다.

 

  • int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
    • 할당 플래그에 워터마크 하한을 설정하고 cpuset이 허용하는 노드 및 공정한 할당을 요청하는 플래그를 설정한다.
      • ALLOC_WMARK_LOW
        • 워터마크 하한까지 페이지 할당 제한
      • ALLOC_CPUSET
        • 현재 태스크에서 cgroup의 cpuset 서브 시스템에서 설정한 정책 사용
      • ALLOC_FAIR
        • NUMA 시스템에서 공정한 노드별 할당을 사용
  • .high_zoneidx = gfp_zone(gfp_mask),
    • ZONE 비트 정보가 포함된 flags를 사용하여 zone_type (based 0)을 리턴한다.
  • .migratetype = gfpflags_to_migratetype(gfp_mask),
    • gfp_flags에서 migrate type을 알아내어 반환한다.
      • gfp 플래그에 __GFP_MOVABLE 사용 시 MIGRATE_MOVABLE
      • gfp 플래그에 __GFP_RECLAIMABLE을 사용하는 경우 MIGRATE_RECLAIMABLE
      • 예외적으로 메모리가 적은 시스템의 경우에는 전역 page_group_by_mobility_disabled  변수가 설정되어 __GFP_UNMOVABLE만 사용된다.
      • 그 외의 migrate 타입은 gfp 플래그에 사용되지 않는다
  • gfp_mask &= gfp_allowed_mask;
    • 커널 부트업 프로세스가 처리되는 동안은 페이지 할당을 위해 io 처리를 위한 드라이버나 파일 시스템이 준비되지 않으며 따라서 메모리 회수 시스템도 구동되지 않고 있는 상태이다. 따라서 이러한 요청들이 발생할 때 이의 기능을 사용하지 못하게 막기 위해  gfp 플래그에서 __GFP_RECLAIM, __GFP_IO 및 |__GFP_FS 비트를 제거한다.
  • might_sleep_if(gfp_mask & __GFP_WAIT);
    • __GFP_WAIT 설정이 있는 경우 preemption point를 실행한다.
    • preemption point
      • 주로 데스크 탑 용도로 사용되는 리눅스 커널의 경우 CONFIG_PREEMPT_VOLUNTARY 커널 옵션을 설정하여 사용하는데 현재 태스크보다 급하게 리스케쥴링 요청하는 태스크가 있는 경우 이를 지원하여 리스케쥴링되고 현재 프로세스는 sleep 상태가 된다.
  • if (should_fail_alloc_page(gfp_mask, order)) return NULL;
    • page alloc에 대한 FAULT-INJECTION이 동작하여 fault 조건에서 동작하지 않도록 루틴을 빠져나간다.
  • if (unlikely(!zonelist->_zonerefs->zone)) return NULL;
    • zonelist에 zone이 없는 경우 루틴을 빠져나간다.
  •  if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) alloc_flags |= ALLOC_CMA;
    • CONFIG_CMA 커널 옵션이 사용되면서 MIGRATE_MOVABLE 타입 요청인 alloc_flags에 ALLOC_CMA를 추가하여 CMA 영역도 할당 영역에 포함시킨다.
    • CMA 영역에 movable 타입 페이지의 할당을 허락하는 이유
      • CMA 타입으로 free 페이지 요청 시 버디 시스템에 등록된 CMA 페이지가 부족한 경우 해당 CMA 영역에서 이미 사용중인 movable 페이지들을 다른 영역으로 강제로 이주시켜서라도 free 페이지를 확보하여 할당할 수 있다.
      • 또한 리눅스 시스템에서 CMA 메모리 요청은 빈번하지 않으므로 CMA 영역내의 메모리를 놀리지 않고 효율적으로 사용할 수 있게 하기 위해 이러한 방법으로 운용한다.

 

retry_cpuset:
        cpuset_mems_cookie = read_mems_allowed_begin();

        /* We set it here, as __alloc_pages_slowpath might have changed it */
        ac.zonelist = zonelist;
        /* The preferred zone is used for statistics later */
        preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx,
                                ac.nodemask ? : &cpuset_current_mems_allowed,
                                &ac.preferred_zone);
        if (!ac.preferred_zone)
                goto out;
        ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);

        /* First allocation attempt */
        alloc_mask = gfp_mask|__GFP_HARDWALL;
        page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
        if (unlikely(!page)) {
                /*
                 * Runtime PM, block IO and its error handling path
                 * can deadlock because I/O on the device might not
                 * complete.
                 */
                alloc_mask = memalloc_noio_flags(gfp_mask);

                page = __alloc_pages_slowpath(alloc_mask, order, &ac);
        }

        if (kmemcheck_enabled && page)
                kmemcheck_pagealloc_alloc(page, order, gfp_mask);

        trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);

out:
        /*
         * When updating a task's mems_allowed, it is possible to race with
         * parallel threads in such a way that an allocation can fail while
         * the mask is being updated. If a page allocation is about to fail,
         * check if the cpuset changed during allocation and if so, retry.
         */
        if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
                goto retry_cpuset;

        return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);
  • retry_cpuset: cpuset_mems_cookie = read_mems_allowed_begin();
    • cpuset 관련한 리드 시퀀스 값을 알아온다. 페이지 할당을 실패하는 동안 cpuset에 변경이 일어나는 경우에 한하여 이 레이블 위치에서 재시도한다.
  • ac.zonelist = zonelist;
    • alloc context에 zonlist 대입
    • slowpath 할당에서 ac.zonelist가 변동이 일어날 수 있어서 다시 인수로 요청했었던 오리지널 zonelist를 대입한다.
  • preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx, ac.nodemask ? : &cpuset_current_mems_allowed, &ac.preferred_zone);
    • 현재 zonelist의 사용할 수 있는 가장 첫 zone을 preferred_zoneref에 저장하고 이 값은 나중에 통계에서 사용되어질 예정이다
      • ac.nodemask가 지정되지 않아 null인 경우 현재 태스크에 cpuset으로 지정된 노드마스크를 사용한다
  • if (!ac.preferred_zone) goto out;
    • 첫 zone이 없는 경우는 페이지 할당을 할 수 없으므로 out 레이블로 이동하여 함수를 빠져나간다
  • ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);
    • preferred zone의 타입 인덱스(0~3)

fastpath

  • alloc_mask = gfp_mask|__GFP_HARDWALL; page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
    • 이제 첫 할당 성공을 위해 시도하는 fastpath이다. 이 때 gfp 마스크에 __GFP_HARDWALL을 사용하여 현재 태스크에서 페이지 할당을 할 때 상위 cpuset을 찾지못하게 막고 현재 cpuset에 지정된 설정만을 사용하게 하여 페이지 할당을 시도한다
    • __GFP_HARDWALL은 fastpath 페이지 할당에서도 사용되고, 사용자 태스크에서 페이지 할당 요청 및 slab 페이지 할당 등에서 사용된다.
      • #define GFP_USER   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
    • 메모리 부족으로 인해 slowpath 할당을 시도할 때 사용자 태스크에서 페이지 할당을 요청한 경우가 아니면 커널은 slab 할당 등의 특수한 경우를 제외하고 일반적인 경우 __GFP_HARDWALL 플래그를  사용하지 않으며 이 경우에는 hardwall 또는 exclusive 설정이 된 현재 cpuset을 포함한 가장 가까운 상위 cpuset을 찾아 설정을 사용할 수 있다.

slowpath

  • if (unlikely(!page)) {
    • 만일 낮은 확률로 fastpath 페이지 할당이 실패한 경우
  • alloc_mask = memalloc_noio_flags(gfp_mask);
    • slowpath용 alloc_mask에 낮은 확률로 현재 태스크의 플래그에 PF_MEMALLOC_NOIO 요청이 있는 경우에만 gfp 플래그에서 __GFP_IO 및 __GFP_FS를 제거한 값을 설정하여 페이지 할당 시 디스크 캐시의 사용이나 페이지의 회수 등을 이유로 io 및 파일 시스템의 이용을 금지한다.
  •  page = __alloc_pages_slowpath(alloc_mask, order, &ac);
    • slowpath 페이지 할당을 시도한다.
    • slowpath에서는 fastpath가 사용한 __GFP_HARDWALL을 추가하지 않으므로 처음 부터 gfp 플래그에 __GFP_HARDWALL 요청을 하지 않은 경우이므로 현재 태스크가 현재 cpuset을 포함하여 가장 가까운 hardwall 이나 exclusive 부모 cpuset 을 선택하여 그 설정을 사용하여 할당을 시도한다.
  • if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) goto retry_cpuset;
    • 페이지 할당이 실패하였으면서 현재 태스크에 대한 cpuset 설정이 바뀐 경우(/sys/fs/cpuset 디렉토리에 있는 설정) 페이지 할당을 다시 시도하면 성공할 확률이 있으므로 재시도하게 한다

 

should_fail_alloc_page()

mm/page_alloc.c

static bool should_fail_alloc_page(gfp_t gfp_mask, unsigned int order)
{
        if (order < fail_page_alloc.min_order)
                return false;
        if (gfp_mask & __GFP_NOFAIL)
                return false;
        if (fail_page_alloc.ignore_gfp_highmem && (gfp_mask & __GFP_HIGHMEM))
                return false;
        if (fail_page_alloc.ignore_gfp_wait && (gfp_mask & __GFP_WAIT))
                return false;

        return should_fail(&fail_page_alloc.attr, 1 << order);
}

CONFIG_FAIL_PAGE_ALLOC 커널 옵션을 사용하는 경우 디버깅 목적으로 사용되며 fail_page_alloc 전역 객체의 조건을 벗어나는 gfp_mask를 가진 경우 false를 반환한다. 또한 __GFP_NOFAIL 옵션을 사용하는 경우에도 false를 반환한다.

 

read_mems_allowed_begin()

include/linux/cpuset.h

/*
 * read_mems_allowed_begin is required when making decisions involving
 * mems_allowed such as during page allocation. mems_allowed can be updated in
 * parallel and depending on the new value an operation can fail potentially
 * causing process failure. A retry loop with read_mems_allowed_begin and
 * read_mems_allowed_retry prevents these artificial failures.
 */
static inline unsigned int read_mems_allowed_begin(void)
{
        return read_seqcount_begin(&current->mems_allowed_seq);
}

현재 태스크의 mems_allowed_seq 시퀀스 락 값을 알아온다.

  • 현재 태스크에 대한 cpuset 설정이 바뀐 경우(/sys/fs/cpuset 디렉토리에 있는 설정) current->mems_allowed_seq 값이 변경된다.

 

Fastpath 페이지 할당

아래의 함수는 페이지 할당 시 Fastpath와 Slowpath 두 곳에서 호출되어 사용되는데 Fastpath에서 호출될 때에는 인수 gfp_mask에 __GFP_HARDWALL을 추가하여 호출한다.

  • GFP_USER
    • (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
  • GFP_KERNEL
    • (__GFP_WAIT | __GFP_IO | __GFP_FS)
  • user space에서 페이지를 할당 시 __GFP_HARDWALL을 설정하여 현재 태스크의 cpuset이 허락하는 메모리 노드가 아닌 곳에서 할당되는 것을 허용하지 않게 한다.

 

다음 그림은 페이지 할당 시 사용되는 Fastpath 루틴과 Slowpath 루틴에서 사용되는 함수를 구분하여 보여준다.

  • 단 get_page_from_freelist() 함수가 __alloc_pages_slowpath() 함수에서 호출되는 경우에는 Slowpath의 일부분이다.

alloc_pages-2a

 

get_page_from_freelist()

mm/page_alloc.c

/*
 * get_page_from_freelist goes through the zonelist trying to allocate
 * a page.
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                                                const struct alloc_context *ac)
{
        struct zonelist *zonelist = ac->zonelist;
        struct zoneref *z;
        struct page *page = NULL;
        struct zone *zone;
        nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
        int zlc_active = 0;             /* set if using zonelist_cache */
        int did_zlc_setup = 0;          /* just call zlc_setup() one time */
        bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                                (gfp_mask & __GFP_WRITE);
        int nr_fair_skipped = 0;
        bool zonelist_rescan;

zonelist_scan:
        zonelist_rescan = false;

        /*
         * Scan zonelist, looking for a zone with enough free.
         * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
         */
        for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,
                                                                ac->nodemask) {
                unsigned long mark;

                if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                        !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                continue;
                if (cpusets_enabled() &&
                        (alloc_flags & ALLOC_CPUSET) &&
                        !cpuset_zone_allowed(zone, gfp_mask))
                                continue;
                /*
                 * Distribute pages in proportion to the individual
                 * zone size to ensure fair page aging.  The zone a
                 * page was allocated in should have no effect on the
                 * time the page has in memory before being reclaimed.
                 */
                if (alloc_flags & ALLOC_FAIR) {
                        if (!zone_local(ac->preferred_zone, zone))
                                break;
                        if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                                nr_fair_skipped++;
                                continue;
                        }
                }

zonelist에서 high_zoneidx 이하의 zone과 지정된 노드 마스크에 대해서 순서대로 zone을 조사하여 zlc 및 워터마크 관련 조건들을 만족하는 경우 그 zone에서 페이지 할당을 한다. 요청 zone에서 할당이 실패된 경우 조건에 따라 zonelist를 다시 구성하여 재시도를 한다.

  • bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) && (gfp_mask & __GFP_WRITE);
    • alloc_flags에 ALLOC_WMARK_LOW가 있고 gfp_mask에 __GFP_WRITE가 있는 경우 true
  • zonelist_scan: zonelist_rescan = false;
    • zonelist_rescan이 설정되는 경우 다시 이 레이블에서 시작한다.
  • for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx, ac->nodemask) {
    • zonelist에서 highest_zoneidx 이하의 zone이면서 nodemask에 설정된 노드인 zone 엔트리들에 대하여 루프를 돈다.
  • if (IS_ENABLED(CONFIG_NUMA) && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes)) continue;
    • NUMA 시스템을 지원하면서 zlc(Zone List Cache)가 활성화된 경우 zlc로 알아봐서 해당 zone이 full된 경우 skip 한다.
      • 첫 시도 시 allowednodes는 null로 모든 노드를 대상으로 한다.
  • if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) && !cpuset_zone_allowed(zone, gfp_mask)) continue;
    • cpusets이 활성화된 경우이면서 ALLOC_CPUSET 플래그가 설정된 경우 현재 태스크에 지정된 노드에 해당 zone 으로 할당할 수 없는 경우 skip 한다.
  • if (alloc_flags & ALLOC_FAIR) {
    • ALLOC_FAIR 플래그가 설정된 경우 페이지 할당 시 preferred_zone의 노드에서 할당을 시도하는데 실패하는 경우 ALLOC_FAIR 플래그를 제거하고 다시 시도한다. 이 때에는 현재 처리하려는 zone의 노드가 권장 zone의 노드와 다르더라도 할당 시도를 하게 한다
    • fastpath 할당 과정에서 보통 권장 zone이 있는 노드에서 할당이 되곤 하므로 균등하게 할당되지만 메모리 부족으로 문제가 일어나게 되면 slowpath 루틴에서 페이지 회수 매커니즘 들이 동작하기 전에 리모트 zone에서도 할당을 시도해 본다
  • if (!zone_local(ac->preferred_zone, zone)) break;
    • 권장 zone의 노드와 현재 zone의 노드가 서로 다른 경우 더 이상 처리할 필요가 없으므로 루프를 탈출한다.
  • if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) { nr_fair_skipped++; continue; }
    • 역시 fastpath 할당 과정에서 권장 zone의 노드와 현재 처리하고자 하는 zone의 노드가 같은 경우 이면서 이 zone에 ZONE_FAIR_DEPLETED 플래그가 설정된 경우 free 페이지가 없어 페이지 할당을 할 수 없는 zone 상태를 의미하므로 nr_fair_skipped를 증가시키고 skip한다.

 

                /*
                 * When allocating a page cache page for writing, we
                 * want to get it from a zone that is within its dirty
                 * limit, such that no single zone holds more than its
                 * proportional share of globally allowed dirty pages.
                 * The dirty limits take into account the zone's
                 * lowmem reserves and high watermark so that kswapd
                 * should be able to balance it without having to
                 * write pages from its LRU list.
                 *
                 * This may look like it could increase pressure on
                 * lower zones by failing allocations in higher zones
                 * before they are full.  But the pages that do spill
                 * over are limited as the lower zones are protected
                 * by this very same mechanism.  It should not become
                 * a practical burden to them.
                 *
                 * XXX: For now, allow allocations to potentially
                 * exceed the per-zone dirty limit in the slowpath
                 * (ALLOC_WMARK_LOW unset) before going into reclaim,
                 * which is important when on a NUMA setup the allowed
                 * zones are together not big enough to reach the
                 * global limit.  The proper fix for these situations
                 * will require awareness of zones in the
                 * dirty-throttling and the flusher threads.
                 */
                if (consider_zone_dirty && !zone_dirty_ok(zone))
                        continue;

                mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
                if (!zone_watermark_ok(zone, order, mark,
                                       ac->classzone_idx, alloc_flags)) {
                        int ret;

                        /* Checked here to keep the fast path fast */
                        BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
                        if (alloc_flags & ALLOC_NO_WATERMARKS)
                                goto try_this_zone;

                        if (IS_ENABLED(CONFIG_NUMA) &&
                                        !did_zlc_setup && nr_online_nodes > 1) {
                                /*
                                 * we do zlc_setup if there are multiple nodes
                                 * and before considering the first zone allowed
                                 * by the cpuset.
                                 */
                                allowednodes = zlc_setup(zonelist, alloc_flags);
                                zlc_active = 1;
                                did_zlc_setup = 1;
                        }

                        if (zone_reclaim_mode == 0 ||
                            !zone_allows_reclaim(ac->preferred_zone, zone))
                                goto this_zone_full;
  • if (consider_zone_dirty && !zone_dirty_ok(zone)) continue;
    • 파일을 쓰기(dirty-buffered write) 목적으로 할당되는 페이지 캐시가 증가하여 현재 zone의 dirty limit 기준치를 초과하는 경우 skip 한다.
  • mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
    • zone에 설정된 3가지 high, low, min 워터마크 값 중 하나를 가져온다. 3가지 워터마크의 선택은 다음 alloc 플래그에 의해 구분된다.
      • ALLOC_WMARK_MIN, ALLOC_WMARK_LOW, ALLOC_WMARK_HIGH
  • if (!zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags)) {
    • 해당 zone의 free page가 워터마크 값의 기준을 벗어난 경우
  • if (alloc_flags & ALLOC_NO_WATERMARKS) goto try_this_zone;
    • 해당 zone의 free page가 워터마크 기준을 벗어난 경우라도 alloc 플래그에서 3 가지 워터마크 기준에 대해 사용하지 않은 경우 그냥 현재 zone에서 페이지 할당을 시도하러 try_this_zone 레이블로 이동한다.
  • if (IS_ENABLED(CONFIG_NUMA) && !did_zlc_setup && nr_online_nodes > 1) { allowednodes = zlc_setup(zonelist, alloc_flags); zlc_active = 1; did_zlc_setup = 1; }
    • NUMA 시스템이고 zlc_setup() 함수를 호출하지 않았으면서 online 노드가 1개 이상인 경우 zlc_setup() 함수를 호출하여 태스크에 허락된 노드를 알아온다.
  • if (zone_reclaim_mode == 0 || !zone_allows_reclaim(ac->preferred_zone, zone)) goto this_zone_full;
    • NUMA 시스템에서 zone_reclaim_mode가 0이거나 현재 zone의 노드와 권장 zone의 노드 간 거리가 기준(30) 이상으로 회수하기에 너무 먼 경우 현재 zone을 reclaim 처리하지 않고 다음 zone으로 skip 한다.
      • zone_reclaim_mode
        • NUMA 시스템에서 “proc/sys/fs/zone_reclaim_mode” 파일 값으로 설정한다
          • 0=워터마크 기준 이하인 경우 현재 zone의 회수 처리 없이 다음 zone으로 skip
          • 1=워터마크 기준과 관련 없이 현재 zone에 대해 회수 처리한다
        • NUMA 시스템이 아닌 경우 항상 0이다.
    • NUMA 시스템에서 노드 간 거리가 30을 초과하는 경우 회수하기에 적합하지 않다고 판단한다

 

                        /*
                         * As we may have just activated ZLC, check if the first
                         * eligible zone has failed zone_reclaim recently.
                         */
                        if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                                !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                continue;

                        ret = zone_reclaim(zone, gfp_mask, order);
                        switch (ret) {
                        case ZONE_RECLAIM_NOSCAN:
                                /* did not scan */
                                continue;
                        case ZONE_RECLAIM_FULL:
                                /* scanned but unreclaimable */
                                continue;
                        default:
                                /* did we reclaim enough */
                                if (zone_watermark_ok(zone, order, mark,
                                                ac->classzone_idx, alloc_flags))
                                        goto try_this_zone;

                                /*
                                 * Failed to reclaim enough to meet watermark.
                                 * Only mark the zone full if checking the min
                                 * watermark or if we failed to reclaim just
                                 * 1<<order pages or else the page allocator
                                 * fastpath will prematurely mark zones full
                                 * when the watermark is between the low and
                                 * min watermarks.
                                 */
                                if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
                                    ret == ZONE_RECLAIM_SOME)
                                        goto this_zone_full;

                                continue;
                        }
                }

try_this_zone:
                page = buffered_rmqueue(ac->preferred_zone, zone, order,
                                                gfp_mask, ac->migratetype);
                if (page) {
                        if (prep_new_page(page, order, gfp_mask, alloc_flags))
                                goto try_this_zone;
                        return page;
                }
this_zone_full:
                if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
                        zlc_mark_zone_full(zonelist, z);
        }
  • if (IS_ENABLED(CONFIG_NUMA) && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes)) continue;
    • 해당 zone이 zlc에서 full되어 있는 경우 skip한다.
  • ret = zone_reclaim(zone, gfp_mask, order);
    • 해당 zone에 대한 free page 회수를 시행한다.
  • case ZONE_RECLAIM_NOSCAN: continue;
    • 페이지 회수를 위해 scan을 하지 않았을 경우 skip한다.
  • case ZONE_RECLAIM_FULL: continue;
    • 페이지 회수를 시도했지만 회수 되지 않은 경우 skip한다.
  • default: if (zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags)) goto try_this_zone;
    • 페이지를 회수 해서 다시 워터마크 이내에 들어간 경우 페이지 할당을 시도한다.
  • if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) || ret == ZONE_RECLAIM_SOME) goto this_zone_full;
    • 워터마크 이내에 들어가지 못한 경우 ALLOC_WMARK_MIN으로 설정되었거나 페이지 회수 결과가 일부만 된 경우 zone을 full 상태로 한다.
  • try_this_zone: page = buffered_rmqueue(ac->preferred_zone, zone, order, gfp_mask, ac->migratetype);
    • 해당 zone의 버디 시스템에서 order 페이지를 할당받는다.
  • if (page) { if (prep_new_page(page, order, gfp_mask, alloc_flags)) goto try_this_zone; return page; }
    • 페이지가 할당되었지만 매핑이 안되었거나 _count 및 _mapcount 등이 0으로 초기화되지 않은 경우 문제가 있다고 판단하여 다시 할당을 시도한다.
  • this_zone_full: if (IS_ENABLED(CONFIG_NUMA) && zlc_active) zlc_mark_zone_full(zonelist, z); }
    • zlc의 fullzones에서 지정된 zone의 비트를 설정하여 지정된 zone에서 할당을 할 수 없도록 막는다.

 

        /*
         * The first pass makes sure allocations are spread fairly within the
         * local node.  However, the local node might have free pages left
         * after the fairness batches are exhausted, and remote zones haven't
         * even been considered yet.  Try once more without fairness, and
         * include remote zones now, before entering the slowpath and waking
         * kswapd: prefer spilling to a remote zone over swapping locally.
         */
        if (alloc_flags & ALLOC_FAIR) {
                alloc_flags &= ~ALLOC_FAIR;
                if (nr_fair_skipped) {
                        zonelist_rescan = true;
                        reset_alloc_batches(ac->preferred_zone);
                }
                if (nr_online_nodes > 1)
                        zonelist_rescan = true;
        }

        if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
                /* Disable zlc cache for second zonelist scan */
                zlc_active = 0;
                zonelist_rescan = true;
        }

        if (zonelist_rescan)
                goto zonelist_scan;

        return NULL;
}
  • if (alloc_flags & ALLOC_FAIR) { alloc_flags &= ~ALLOC_FAIR;
    • alloc_flags에서 ALLOC_FAIR 설정이 된 경우 제거한다.
    • 처음 페이지 할당 시도 시 항상 ALLOC_FAIR 플래그를 설정하여 권장 zone의 노드에서만 할당을 시도하는데 실패하게 되면 ALLOC_FAIR 플래그를 제거하고 다른 노드에서도 할당을 할 수 있도록 하게 한다.
  • if (nr_fair_skipped) { zonelist_rescan = true; reset_alloc_batches(ac->preferred_zone); }
    • zone에 free 페이지가 없어서 nr_fair_skipped된 적이 있으면 리스캔 플래그를  설정하고 권장 zone이 있는 노드의 모든 zone에 NR_ALLOC_BATCH stat을 초기화하고 zone의 플래그에서 ZONE_FAIR_DEPLETED를 제거한 후 리스캔하게 한다.
    • NR_ALLOC_BATCH 카운터에 약간의 페이지가 있는 것으로 위장한 후 재 시도를 하는 방법으로 이를 통해 재성공 확률이 높기 때문에 수행하였다.
    • fair zone 정책은 NUMA 시스템의 메모리 정책을 사용하면서 불필요하게 되어 최근 커널 v4.8-rc1에서 제거되었다.
  • if (nr_online_nodes > 1) zonelist_rescan = true;
    • online 노드가 1개를 초과하는 경우 리스캔 플래그를 설정한다.
  • if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) { zlc_active = 0; zonelist_rescan = true; }
    • 누마가 설정되고 zlc_active 중인 경우 lzc_active를 끄고 리스캔 플래그를 설정한다.
  • if (zonelist_rescan) goto zonelist_scan;
    • 리스캔 플래그가 설정된 경우 다시 zonelist 스캔을 한다.

 

CPUSET 관련

cpuset_zone_allowed()

include/linux/cpuset.h

static inline int cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask)
{
        return cpuset_node_allowed(zone_to_nid(z), gfp_mask);
}

요청 zone의 노드가 현재 cpu가 지원하는 노드인 경우 true를 반환한다. 그 외에 우선되는 경우는 인터럽트 수행중에 호출되었거나 __GFP_THISNODE 플래그가 설정되었거나 현재 태스크가 이미 허락하는 노드이거나 태스크가 TIF_MEMDIE 플래그 또는 PF_EXITING 플래그가 설정된 경우는 true를 반환하고 __GFP_HARDWALL이 설정된 경우 현재 태스크의 cpuset이 허락한 메모리 노드가 아닌 노드에 기회를 주지않게 하기 위해 false를 반환한다.

 

cpuset_node_allowed()

include/linux/cpuset.h

static inline int cpuset_node_allowed(int node, gfp_t gfp_mask)
{
        return nr_cpusets() <= 1 || __cpuset_node_allowed(node, gfp_mask);
}

아래 함수와 동일

 

__cpuset_node_allowed()

kernel/cpuset.c

/**
 * cpuset_node_allowed - Can we allocate on a memory node?
 * @node: is this an allowed node?
 * @gfp_mask: memory allocation flags
 *
 * If we're in interrupt, yes, we can always allocate.  If __GFP_THISNODE is
 * set, yes, we can always allocate.  If node is in our task's mems_allowed,
 * yes.  If it's not a __GFP_HARDWALL request and this node is in the nearest
 * hardwalled cpuset ancestor to this task's cpuset, yes.  If the task has been
 * OOM killed and has access to memory reserves as specified by the TIF_MEMDIE
 * flag, yes.
 * Otherwise, no.
 *
 * The __GFP_THISNODE placement logic is really handled elsewhere,
 * by forcibly using a zonelist starting at a specified node, and by
 * (in get_page_from_freelist()) refusing to consider the zones for
 * any node on the zonelist except the first.  By the time any such
 * calls get to this routine, we should just shut up and say 'yes'.
 *
 * GFP_USER allocations are marked with the __GFP_HARDWALL bit,
 * and do not allow allocations outside the current tasks cpuset
 * unless the task has been OOM killed as is marked TIF_MEMDIE.
 * GFP_KERNEL allocations are not so marked, so can escape to the
 * nearest enclosing hardwalled ancestor cpuset.
 *
 * Scanning up parent cpusets requires callback_lock.  The
 * __alloc_pages() routine only calls here with __GFP_HARDWALL bit
 * _not_ set if it's a GFP_KERNEL allocation, and all nodes in the
 * current tasks mems_allowed came up empty on the first pass over
 * the zonelist.  So only GFP_KERNEL allocations, if all nodes in the
 * cpuset are short of memory, might require taking the callback_lock.
 *
 * The first call here from mm/page_alloc:get_page_from_freelist()
 * has __GFP_HARDWALL set in gfp_mask, enforcing hardwall cpusets,
 * so no allocation on a node outside the cpuset is allowed (unless
 * in interrupt, of course).
 *
 * The second pass through get_page_from_freelist() doesn't even call
 * here for GFP_ATOMIC calls.  For those calls, the __alloc_pages()
 * variable 'wait' is not set, and the bit ALLOC_CPUSET is not set
 * in alloc_flags.  That logic and the checks below have the combined
 * affect that:
 *      in_interrupt - any node ok (current task context irrelevant)
 *      GFP_ATOMIC   - any node ok
 *      TIF_MEMDIE   - any node ok
 *      GFP_KERNEL   - any node in enclosing hardwalled cpuset ok
 *      GFP_USER     - only nodes in current tasks mems allowed ok.
 */

 

int __cpuset_node_allowed(int node, gfp_t gfp_mask)
{
        struct cpuset *cs;              /* current cpuset ancestors */
        int allowed;                    /* is allocation in zone z allowed? */
        unsigned long flags;

        if (in_interrupt() || (gfp_mask & __GFP_THISNODE))
                return 1;
        if (node_isset(node, current->mems_allowed))
                return 1;
        /*
         * Allow tasks that have access to memory reserves because they have
         * been OOM killed to get memory anywhere.
         */
        if (unlikely(test_thread_flag(TIF_MEMDIE)))
                return 1;
        if (gfp_mask & __GFP_HARDWALL)  /* If hardwall request, stop here */
                return 0;

        if (current->flags & PF_EXITING) /* Let dying task have memory */
                return 1;

        /* Not hardwall and node outside mems_allowed: scan up cpusets */
        spin_lock_irqsave(&callback_lock, flags);

        rcu_read_lock();
        cs = nearest_hardwall_ancestor(task_cs(current));
        allowed = node_isset(node, cs->mems_allowed);
        rcu_read_unlock();

        spin_unlock_irqrestore(&callback_lock, flags);
        return allowed;
}

요청 노드가 현재 cpu가 지원하는 노드인 경우 true를 반환한다. 그 외에 우선되는 경우는 인터럽트 수행중에 호출되었거나 __GFP_THISNODE 플래그가 설정되었거나 현재 태스크가 이미 허락하는 노드이거나 태스크가 TIF_MEMDIE 플래그 또는 PF_EXITING 플래그가 설정된 경우는 true를 반환하고 __GFP_HARDWALL이 설정된 경우 현재 태스크의 cpuset이 허락한 메모리 노드가 아닌 노드에 기회를 주지않게 하기 위해 false를 반환한다.

  • user space(GFP_USER)에서 요청 시 __GFP_HARDWALL이 포함되어 있다.

 

  • if (in_interrupt() || (gfp_mask & __GFP_THISNODE)) return 1;
    • 인터럽트 핸들러에서 호출되었거나 __GFP_THISNODE 플래그가 설정된 경우 true를 반환한다.
  • if (node_isset(node, current->mems_allowed)) return 1;
    • 현재 태스크가 허락하는 노드인 경우 true를 반환한다.
  • if (unlikely(test_thread_flag(TIF_MEMDIE))) return 1;
    • 낮은 확률로 현재 태스크에 TIF_MEMDIE 플래그가 설정된 경우 true를 반환한다.
  • if (gfp_mask & __GFP_HARDWALL) return 0;
    • __GFP_HARDWALL이 설정된 경우 false를 반환한다.
  • if (current->flags & PF_EXITING) return 1;
    • 현재 태스크가 PF_EXITING 플래그가 설정된 경우 true를 반환한다.
  • cs = nearest_hardwall_ancestor(task_cs(current));
    • 현재 태스크의 cpuset에서 가장 가까운 hardwall 부모 cpuset을 알아온다.
  • allowed = node_isset(node, cs->mems_allowed);
    • cpuset에 허락된 메모리 노드의 여부를 반환한다.

 

nearest_hardwall_ancestor()

kernel/cpuset.c

/*
 * nearest_hardwall_ancestor() - Returns the nearest mem_exclusive or
 * mem_hardwall ancestor to the specified cpuset.  Call holding
 * callback_lock.  If no ancestor is mem_exclusive or mem_hardwall
 * (an unusual configuration), then returns the root cpuset.
 */
static struct cpuset *nearest_hardwall_ancestor(struct cpuset *cs)
{
        while (!(is_mem_exclusive(cs) || is_mem_hardwall(cs)) && parent_cs(cs))
                cs = parent_cs(cs);
        return cs;
}

cpuset이 메모리를 베타적으로 사용하거나 부모 cpuset이 hardwall인 경우 cpuset을 반환한다. 조건을 만족하지 못하면 만족할 때 까지 부모 cpuset을 계속 찾는다.

 

Zone Dirty 관련

zone_dirty_ok()

mm/page-writeback.c

/**
 * zone_dirty_ok - tells whether a zone is within its dirty limits
 * @zone: the zone to check
 *
 * Returns %true when the dirty pages in @zone are within the zone's
 * dirty limit, %false if the limit is exceeded.
 */
bool zone_dirty_ok(struct zone *zone)
{
        unsigned long limit = zone_dirty_limit(zone);

        return zone_page_state(zone, NR_FILE_DIRTY) +
               zone_page_state(zone, NR_UNSTABLE_NFS) +
               zone_page_state(zone, NR_WRITEBACK) <= limit;
}

zone이 사용할 수 있는 제한된 dirty(write buffer) 영역의 크기가 NR_FILE_DRITY + NRUNSTABLE_NFS + WRITEBACK 이상인 경우 true를 반환한다.

  • zone 카운터들 대한 수치 확인은 “cat /proc/zoneinfo” 명령을 통해 간단히 확인할 수 있다.

 

zone_dirty_limit()

mm/page-writeback.c

/**
 * zone_dirty_limit - maximum number of dirty pages allowed in a zone
 * @zone: the zone
 *
 * Returns the maximum number of dirty pages allowed in a zone, based
 * on the zone's dirtyable memory.                      
 */ 
static unsigned long zone_dirty_limit(struct zone *zone)
{                                 
        unsigned long zone_memory = zone_dirtyable_memory(zone);
        struct task_struct *tsk = current; 
        unsigned long dirty;
        
        if (vm_dirty_bytes)
                dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE) *
                        zone_memory / global_dirtyable_memory();
        else
                dirty = vm_dirty_ratio * zone_memory / 100;

        if (tsk->flags & PF_LESS_THROTTLE || rt_task(tsk))
                dirty += dirty / 4;

        return dirty;
}

zone에 허락된 캐시(dirty) 가능한 페이지 수의 일정 비율만큼으로 제한한 페이지 수를 반환한다. 만일 태스크에 PF_LESS_THROTTLE가 설정되어 있거나 우선 순위가 user task보다 높은 태스크인 경우 25%를 추가한다.

  • vm_dirty_bytes가 설정된 경우 zone이 사용하는 dirty 페이지의 비율만큼 배정한다.
  • vm_dirty_bytes가 설정되지 않은 경우 vm_dirty_ratio 백분율 만큼으로 배정한다.
  • 태스크 우선 순위
    • 0~139중 rt_task는 100이하의 높은 우선 순위를 가진다.
      • 100~139는 user space 태스크의 우선순위를 가진다.
      • 낮은 숫자가 가장 높은 우선순위를 가진다.

 

아래 그림은 zone_dirty_limit()을 산출 시 zone_dirty_bytes 또는 zone_dirty_ratio를 사용할 때 달라지는 모습을 보여준다.

zone_dirty_limit-1a

 

zone_dirtyable_memory()

mm/page-writeback.c

/*
 * In a memory zone, there is a certain amount of pages we consider
 * available for the page cache, which is essentially the number of
 * free and reclaimable pages, minus some zone reserves to protect
 * lowmem and the ability to uphold the zone's watermarks without
 * requiring writeback.
 *
 * This number of dirtyable pages is the base value of which the
 * user-configurable dirty ratio is the effictive number of pages that
 * are allowed to be actually dirtied.  Per individual zone, or
 * globally by using the sum of dirtyable pages over all zones.
 *
 * Because the user is allowed to specify the dirty limit globally as
 * absolute number of bytes, calculating the per-zone dirty limit can
 * require translating the configured limit into a percentage of
 * global dirtyable memory first.
 */

/**
 * zone_dirtyable_memory - number of dirtyable pages in a zone
 * @zone: the zone
 *
 * Returns the zone's number of pages potentially available for dirty
 * page cache.  This is the base value for the per-zone dirty limits.
 */
static unsigned long zone_dirtyable_memory(struct zone *zone)
{
        unsigned long nr_pages;

        nr_pages = zone_page_state(zone, NR_FREE_PAGES);
        nr_pages -= min(nr_pages, zone->dirty_balance_reserve);

        nr_pages += zone_page_state(zone, NR_INACTIVE_FILE);
        nr_pages += zone_page_state(zone, NR_ACTIVE_FILE);

        return nr_pages;
}

해당 zone의 캐시 가능한 페이지 수를 반환한다.

  • zone->free 페이지 + zone->file 캐시로 사용 중인 페이지 – zone->dirty_balance_reserve 값을 반환한다.

 

global_dirtyable_memory()

mm/page-writeback.c

/**
 * global_dirtyable_memory - number of globally dirtyable pages
 *
 * Returns the global number of pages potentially available for dirty
 * page cache.  This is the base value for the global dirty limits.
 */
static unsigned long global_dirtyable_memory(void)
{
        unsigned long x;

        x = global_page_state(NR_FREE_PAGES);
        x -= min(x, dirty_balance_reserve);

        x += global_page_state(NR_INACTIVE_FILE);
        x += global_page_state(NR_ACTIVE_FILE);

        if (!vm_highmem_is_dirtyable)
                x -= highmem_dirtyable_memory(x);

        return x + 1;   /* Ensure that we never return 0 */
}

시스템에서 캐시 가능한 페이지 수를 반환한다.

  • free 페이지 + file 캐시로 사용 중인 페이지 – dirty_balance_reserve 값을 반환한다.
  • 만일 highmem을 캐시로 사용되지 못하게 한 경우 highmem의 dirty 페이지 부분을 제외시킨다.

 

highmem_dirtyable_memory()

mm/page-writeback.c

static unsigned long highmem_dirtyable_memory(unsigned long total)
{
#ifdef CONFIG_HIGHMEM
        int node;
        unsigned long x = 0;

        for_each_node_state(node, N_HIGH_MEMORY) {
                struct zone *z = &NODE_DATA(node)->node_zones[ZONE_HIGHMEM];

                x += zone_dirtyable_memory(z);
        }
        /*
         * Unreclaimable memory (kernel memory or anonymous memory
         * without swap) can bring down the dirtyable pages below
         * the zone's dirty balance reserve and the above calculation
         * will underflow.  However we still want to add in nodes
         * which are below threshold (negative values) to get a more
         * accurate calculation but make sure that the total never
         * underflows.
         */
        if ((long)x < 0)
                x = 0;

        /*
         * Make sure that the number of highmem pages is never larger
         * than the number of the total dirtyable memory. This can only
         * occur in very strange VM situations but we want to make sure
         * that this does not occur.
         */
        return min(x, total);
#else
        return 0;
#endif
}

high memory에 대한 dirty 페이지 가능한 수를 알아온다.

 

ZLC(Zone List Cache) 관련

zlc_setup()

mm/page_alloc.c

#ifdef CONFIG_NUMA
/*
 * zlc_setup - Setup for "zonelist cache".  Uses cached zone data to
 * skip over zones that are not allowed by the cpuset, or that have
 * been recently (in last second) found to be nearly full.  See further
 * comments in mmzone.h.  Reduces cache footprint of zonelist scans
 * that have to skip over a lot of full or unallowed zones.
 *
 * If the zonelist cache is present in the passed zonelist, then
 * returns a pointer to the allowed node mask (either the current
 * tasks mems_allowed, or node_states[N_MEMORY].)
 *
 * If the zonelist cache is not available for this zonelist, does
 * nothing and returns NULL.
 *
 * If the fullzones BITMAP in the zonelist cache is stale (more than
 * a second since last zap'd) then we zap it out (clear its bits.)
 *
 * We hold off even calling zlc_setup, until after we've checked the
 * first zone in the zonelist, on the theory that most allocations will
 * be satisfied from that first zone, so best to examine that zone as
 * quickly as we can.
 */
static nodemask_t *zlc_setup(struct zonelist *zonelist, int alloc_flags)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        nodemask_t *allowednodes;       /* zonelist_cache approximation */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return NULL;

        if (time_after(jiffies, zlc->last_full_zap + HZ)) {
                bitmap_zero(zlc->fullzones, MAX_ZONES_PER_ZONELIST);
                zlc->last_full_zap = jiffies;
        }

        allowednodes = !in_interrupt() && (alloc_flags & ALLOC_CPUSET) ?
                                        &cpuset_current_mems_allowed :
                                        &node_states[N_MEMORY];
        return allowednodes;
}
#endif

NUMA 시스템에서 zlc 기능이 설정된 경우 페이지를 할당 시 이 루틴이 호출되면 현재 태스크가 메모리 할당 가능한 노드들을 반환한다. 또한 1초 간격으로 zlc->fullzones를 clear하여 각 zone의 메모리 full 상태를 해제한다.

  • zlc 기능은 속도 향상에 크게 관여하지 못하게되어 커널 v4.4에서 삭제된다.

 

  • if (time_after(jiffies, zlc->last_full_zap + HZ)) { bitmap_zero(zlc->fullzones, MAX_ZONES_PER_ZONELIST); zlc->last_full_zap = jiffies; }
    • 1초가 경과된 경우 fullzones를 clear하여 각 zone의 메모리 full 상태를 해제한다.
  • allowednodes = !in_interrupt() && (alloc_flags & ALLOC_CPUSET) ? &cpuset_current_mems_allowed : &node_states[N_MEMORY];
    • 인터럽트 핸들러에 의해 호출된 경우가 아니면서 ALLOC_CPUSET 플래그가 설정된 경우 태스크에 한정된(current->mems_allowed) 노드 비트맵을 반환하고 그렇지 않은 경우 메모리가 있는 노드 비트맵을 반환한다.

 

zlc_zone_worth_trying()

mm/page_alloc.c

/*
 * Given 'z' scanning a zonelist, run a couple of quick checks to see
 * if it is worth looking at further for free memory:
 *  1) Check that the zone isn't thought to be full (doesn't have its
 *     bit set in the zonelist_cache fullzones BITMAP).
 *  2) Check that the zones node (obtained from the zonelist_cache
 *     z_to_n[] mapping) is allowed in the passed in allowednodes mask.
 * Return true (non-zero) if zone is worth looking at further, or
 * else return false (zero) if it is not.
 *
 * This check -ignores- the distinction between various watermarks,
 * such as GFP_HIGH, GFP_ATOMIC, PF_MEMALLOC, ...  If a zone is
 * found to be full for any variation of these watermarks, it will
 * be considered full for up to one second by all requests, unless
 * we are so low on memory on all allowed nodes that we are forced
 * into the second scan of the zonelist.
 *
 * In the second scan we ignore this zonelist cache and exactly
 * apply the watermarks to all zones, even it is slower to do so.
 * We are low on memory in the second scan, and should leave no stone
 * unturned looking for a free page.
 */
static int zlc_zone_worth_trying(struct zonelist *zonelist, struct zoneref *z,
                                                nodemask_t *allowednodes)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        int i;                          /* index of *z in zonelist zones */
        int n;                          /* node that zone *z is on */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return 1;

        i = z - zonelist->_zonerefs;
        n = zlc->z_to_n[i];

        /* This zone is worth trying if it is allowed but not full */
        return node_isset(n, *allowednodes) && !test_bit(i, zlc->fullzones);
}

허락된 노드들 중 zlc(Zone List Cache)를 통해 해당 zone의 메모리가 대략 full 상태인 경우 true를 반환한다.

 

zlc_mark_zone_full()

mm/page_alloc.c

/*
 * Given 'z' scanning a zonelist, set the corresponding bit in
 * zlc->fullzones, so that subsequent attempts to allocate a page
 * from that zone don't waste time re-examining it.
 */
static void zlc_mark_zone_full(struct zonelist *zonelist, struct zoneref *z)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        int i;                          /* index of *z in zonelist zones */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return;

        i = z - zonelist->_zonerefs;

        set_bit(i, zlc->fullzones);
}

zlc의 fullzones에서 지정된 zone의 비트를 설정하여 지정된 zone에서 할당을 할 수 없도록 막는다.

 

기타

zone_allows_reclaim()

mm/page_alloc.c

static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone)
{
        return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <
                                RECLAIM_DISTANCE;
}

local_zone과 요청 zone이 RECLAIM_DISTANCE(30) 이내의 거리에 있는 경우 true를 반환한다.

  • 페이지 회수는 가까운 리모트 zone에서만 가능하게 한다.

prep_new_page()

page_alloc.c

static int prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
                                                                int alloc_flags)
{
        int i;

        for (i = 0; i < (1 << order); i++) {
                struct page *p = page + i;
                if (unlikely(check_new_page(p)))
                        return 1;
        }

        set_page_private(page, 0);
        set_page_refcounted(page);

        arch_alloc_page(page, order);
        kernel_map_pages(page, 1 << order, 1);
        kasan_alloc_pages(page, order);

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

        if (order && (gfp_flags & __GFP_COMP))
                prep_compound_page(page, order);

        set_page_owner(page, order, gfp_flags);

        /*
         * page->pfmemalloc is set when ALLOC_NO_WATERMARKS was necessary to
         * allocate the page. The expectation is that the caller is taking
         * steps that will free more memory. The caller should avoid the page
         * being used for !PFMEMALLOC purposes.
         */
        page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);

        return 0;
}

할당 받은 페이지에 문제가 있는 경우 true를 반환하고 그렇지 않은 경우 할당 받은 페이지를 초기화하고 false를 반환한다.

  • for (i = 0; i < (1 << order); i++) { struct page *p = page + i; if (unlikely(check_new_page(p))) return 1; }
    • 할당 받은 모든 페이지들을 검사하여 문제가 있는 경우 true를 반환한다.
  • set_page_private(page, 0);
    • 페이지의 private에 0을 대입한다.
  • set_page_refcounted(page);
    • 페이지의 _refcount에 1을 대입한다.
  • arch_alloc_page(page, order);
    • 페이지 할당에 대한 아키텍처 고유 함수가 준비된 경우 수행한다.
      • 32bit arm에서는 빈 함수이다.
  • kernel_map_pages(page, 1 << order, 1);
    • CONFIG_DEBUG_PAGEALLOC 커널 옵션을 사용하는 디버그에서 사용된다.
  • kasan_alloc_pages(page, order);
    • CONFIG_KASAN 커널 옵션을 사용하는 디버그에서 사용된다.
  • if (gfp_flags & __GFP_ZERO) prep_zero_page(page, order, gfp_flags);
    • __GFP_ZERO 플래그를 사용한 경우 페이지를 0으로 초기화한다.
      • 이 함수 내부에서는 highmem 페이지에 대한 초기화를 위해 kmap() 과 kummap() 함수를 이용한다. 따라서 인터럽트 context에서 사용되면 안된다.
  • if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order);
    • __GFP_COMP 플래그가 사용된 경우 compound 페이지를 설정한다.
  •  set_page_owner(page, order, gfp_flags);
    • page_ext->order, page_ext->gfp_mask를 설정하고 page_ext->flags에 PAGE_EXT_OWNER 비트를 설정한다.
  • page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
    • page->pfmemalloc에 ALLOC_NO_WATERMARKS 할당 플래그를 사용했는지 여부를 대입한다.

 

prep_zero_page()

mm/page_alloc.c

static inline void prep_zero_page(struct page *page, unsigned int order,
                                                        gfp_t gfp_flags)
{
        int i;

        /*
         * clear_highpage() will use KM_USER0, so it's a bug to use __GFP_ZERO
         * and __GFP_HIGHMEM from hard or soft interrupt context.
         */
        VM_BUG_ON((gfp_flags & __GFP_HIGHMEM) && in_interrupt());
        for (i = 0; i < (1 << order); i++)
                clear_highpage(page + i);
}

order 페이지의 모든 페이지에 대해 0으로 초기화한다.

  • 내부에서 highmem 페이지에 대한 초기화를 위해 kmap() 과 kummap() 함수를 이용한다. 따라서 이 함수는 인터럽트 context에서 사용되면 안된다.

 

prep_compound_page()

/mm/page_alloc.c

void prep_compound_page(struct page *page, unsigned long order)
{
        int i;
        int nr_pages = 1 << order;

        set_compound_page_dtor(page, free_compound_page);
        set_compound_order(page, order);
        __SetPageHead(page);
        for (i = 1; i < nr_pages; i++) {
                struct page *p = page + i;
                set_page_count(p, 0); 
                p->first_page = page;
                /* Make sure p->first_page is always valid for PageTail() */
                smp_wmb();
                __SetPageTail(p);
        }
}

compound 페이지를 설정한다.

  • 파괴자에 free_compound_page() 함수를 대입한다.
  • 두 번째 페이지에 order를 설정한다.
  • 헤더 페이지의 플래그에 PG_Head 비트를 설정한다.
  • 나머지 페이지의 _count에 0을 대입하고, first_page는 헤드 페이지를 가리키게 한 후 플래그에 PG_Tail 비트를 설정한다.

 

reset_alloc_batches()

mm/page_alloc.c

static void reset_alloc_batches(struct zone *preferred_zone)
{
        struct zone *zone = preferred_zone->zone_pgdat->node_zones;

        do {
                mod_zone_page_state(zone, NR_ALLOC_BATCH,
                        high_wmark_pages(zone) - low_wmark_pages(zone) -
                        atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]));
                clear_bit(ZONE_FAIR_DEPLETED, &zone->flags);
        } while (zone++ != preferred_zone);
}

권장 zone의 노드에 있는 zone에 대해서 처음부터 권장 zone이 나올때까지 루프를 돌며 NR_ALLOC_BATCH stat에 최고 워터마크에서 최저 워터마크를 빼고 현재의 NR_ALLOC_BATCH 값을 뺀 값으로 초기화하고 zone 플래그의 ZONE_FAIR_DEPLETED 비트를 클리어한다.

 

memalloc_noio_flags()

include/linux/sched.h

static inline gfp_t memalloc_noio_flags(gfp_t flags)
{
        if (unlikely(current->flags & PF_MEMALLOC_NOIO))
                flags &= ~(__GFP_IO | __GFP_FS);
        return flags;
}

현재 태스크의 플래그에 PF_MEMALLOC_NOIO가 사용되는 경우 인수 flags에서 __GFP_IO 및 __GFP_FS를 삭제한다.

 

구조체

alloc_context 구조체

mm/internal.h

/*
 * Structure for holding the mostly immutable allocation parameters passed
 * between functions involved in allocations, including the alloc_pages*
 * family of functions.
 *      
 * nodemask, migratetype and high_zoneidx are initialized only once in
 * __alloc_pages_nodemask() and then never change.
 *      
 * zonelist, preferred_zone and classzone_idx are set first in
 * __alloc_pages_nodemask() for the fast path, and might be later changed
 * in __alloc_pages_slowpath(). All other functions pass the whole strucure
 * by a const pointer.
 */
struct alloc_context {
        struct zonelist *zonelist;
        nodemask_t *nodemask;           
        struct zone *preferred_zone;
        int classzone_idx;
        int migratetype;
        enum zone_type high_zoneidx;
};
  • zonelist
    • 페이지 할당 시 사용하는 zonelist
  • nodemask
    • __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.
  • preferred_zone
    • zonelist의 fastpath를 위해 사용된다. __alloc_pages_slowpath()에서 변경될 수 있다.
  • classzone_idx
    • preferred_zone의 zoneref에 해당하는 인덱스 번호
  • migratetype
    • migrate type은 __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.
  • high_zoneidx
    • high zone 타입의 인덱스로 __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.

 

참고

 

 

 

pgtable_init()

 

page->ptl용 slub 캐시

DEBUG_SPINLOCK과 DEBUG_LOCK_ALLOC이 활성화된 경우 x86_64 아키텍처에서 spinlock_t는 72 바이트가 사용되고 slab(slub) 캐시를 통해 할당되는 경우 근접한 사이즈인 kmalloc-96이 사용되고 24바이트의 사이즈가 낭비된다. 수 만개 이상의 페이지에 사용되는 page->ptl에 대해 너무 많은 메모리가 낭비된다 판단하여 spinlock_t 사이즈에 딱 맞는 캐시를 별도로 만들어 사용하게 되었다.

 

pgtable_init()

include/linux/mm.h

static inline void pgtable_init(void)
{
        ptlock_cache_init();
        pgtable_cache_init();
}
  • ptlock_cache_init()
    • page->ptl에 사용하는 spinlock_t 타입용 slab(slub) 캐시를 생성한다.
  • pgtable_cache_init()
    • x86, arm, arm64에서는 빈 함수이다.

 

ptlock_cache_init()

mm/memory.c

#if USE_SPLIT_PTE_PTLOCKS && ALLOC_SPLIT_PTLOCKS
static struct kmem_cache *page_ptl_cachep;

void __init ptlock_cache_init(void)
{       
        page_ptl_cachep = kmem_cache_create("page->ptl", sizeof(spinlock_t), 0,
                        SLAB_PANIC, NULL);
}
#endif

페이지 테이블의 spinlock에 사용할 ptlock 캐시를 생성한다.

 

include/linux/mm_types.h

#define USE_SPLIT_PTE_PTLOCKS   (NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS)

 

include/linux/mm_types.h

#define ALLOC_SPLIT_PTLOCKS     (SPINLOCK_SIZE > BITS_PER_LONG/8)

 

ptlock_alloc()

mm/memory.c

bool ptlock_alloc(struct page *page)
{       
        spinlock_t *ptl;

        ptl = kmem_cache_alloc(page_ptl_cachep, GFP_KERNEL);
        if (!ptl)
                return false;
        page->ptl = ptl;
        return true;
}

spinlock에 사용할 slub object를 할당받아 page->ptl에 대입한다.

 

ptlock_free()

mm/memory.c

void ptlock_free(struct page *page)
{
        kmem_cache_free(page_ptl_cachep, page->ptl);
}

page->ptl에서 사용한 spinlock이 사용한 slub object를 해제한다.

 

CONFIG_SPLIT_PTLOCK_CPUS 커널 옵션

# Heavily threaded applications may benefit from splitting the mm-wide
# page_table_lock, so that faults on different parts of the user address
# space can be handled with less contention: split it at this NR_CPUS.
# Default to 4 for wider testing, though 8 might be more appropriate.
# ARM's adjust_pte (unused if VIPT) depends on mm-wide page_table_lock.
# PA-RISC 7xxx's spinlock_t would enlarge struct page from 32 to 44 bytes.
# DEBUG_SPINLOCK and DEBUG_LOCK_ALLOC spinlock_t also enlarge struct page.
#
config SPLIT_PTLOCK_CPUS
        int
        default "999999" if !MMU
        default "999999" if ARM && !CPU_CACHE_VIPT
        default "999999" if PARISC && !PA20
        default "4"

 

percpu_init_late()

 

percpu_init_late()

mm/percpu.c

/*
 * First and reserved chunks are initialized with temporary allocation
 * map in initdata so that they can be used before slab is online.
 * This function is called after slab is brought up and replaces those
 * with properly allocated maps.
 */
void __init percpu_init_late(void)
{
        struct pcpu_chunk *target_chunks[] =
                { pcpu_first_chunk, pcpu_reserved_chunk, NULL };
        struct pcpu_chunk *chunk;
        unsigned long flags;
        int i;

        for (i = 0; (chunk = target_chunks[i]); i++) {
                int *map;
                const size_t size = PERCPU_DYNAMIC_EARLY_SLOTS * sizeof(map[0]);

                BUILD_BUG_ON(size > PAGE_SIZE);

                map = pcpu_mem_zalloc(size);
                BUG_ON(!map);

                spin_lock_irqsave(&pcpu_lock, flags);
                memcpy(map, chunk->map, size);
                chunk->map = map;
                spin_unlock_irqrestore(&pcpu_lock, flags);
        }
}

slab(slub) 메모리 관리자가 활성화된 이후 pcpu_first_chunk와 pcpu_reserved_chunk에서 사용한 할당 map을 새로 할당 받은 메모리에 복사한다.

 

pcpu_mem_zalloc()

mm/percpu.c

/**
 * pcpu_mem_zalloc - allocate memory
 * @size: bytes to allocate
 *
 * Allocate @size bytes.  If @size is smaller than PAGE_SIZE,
 * kzalloc() is used; otherwise, vzalloc() is used.  The returned
 * memory is always zeroed.
 *
 * CONTEXT:
 * Does GFP_KERNEL allocation.
 *
 * RETURNS:
 * Pointer to the allocated area on success, NULL on failure.
 */
static void *pcpu_mem_zalloc(size_t size)
{
        if (WARN_ON_ONCE(!slab_is_available()))
                return NULL;

        if (size <= PAGE_SIZE)
                return kzalloc(size, GFP_KERNEL);
        else
                return vzalloc(size);
}

요청 size로 메모리 할당 후 0으로 초기화한다.

  • 요청 size가 PAGE_SIZE보다 작은 경우 kzalloc() 함수를 사용하고
    • 물리적으로 연속된 메모리가 확보되며 빠르지만 파편화된 메모리로 인해 실패할 확률이 vzalloc보다 크다. 따라서 작은 사이즈의 메모리의 할당에 더 어울린다.
    • DMA 버퍼에 사용하는 메모리는 물리적으로 연속된 메모리의 할당이 요구된다.
  • 요청 size가 PAGE_SIZE보다 큰 경우 vzalloc() 함수를 사용한다.
    • 연속된 물리 주소는 필요하지 않고 연속된 가상 주소만 있으면 되는 상황에서 사용된다. kzalloc()보다 파편화되지 않는다. 내부적으로는 여러 번의 kzalloc()함수가 호출되어 관리되어 사용되므로 kzalloc()보다 느리다.

 

참고