Zoned Allocator -6- (Watermark)

<kernel v5.0>

Zone 워터마크

zone에서 페이지 할당을 시도할 때 남은 free 페이지가 zone 별 워터마크 기준과 비교하여 메모리 할당 여부를 판단한다. 이러한 워터마크는 다음과 같이 3 가지 워터 마크 기준 값을 지정하여 사용하며 각각의 의미는 다음과 같다.

  • WATERMARK_MIN
    • 메모리 부족의 최저 기준 점과 동일하다.
    • vm.min_free_kbytes 값에 해당하는 페이지 수를 가진다.
    • 산출된 free 페이지가 이 값 미만이 되는 경우 페이지 회수가 불가능하다. 이러한 경우 동기되어 직접(direct) 페이지 회수 작업을 허용하는 경우 페이지 회수 후 할당이 계속 진행된다.
    • 특수한 상황(atomic, oom, pfmemalloc을 제외한)에서는 이 기준 이하의 상황에서도 페이지 할당이 가능해진다.
  • WATERMARK_LOW
    • 디폴트로 min 값의 125% 값을 가진 페이지 수이다. kswapd가 자동으로 wakeup되는 시점이다.
      • 사용자가 watermark_scale_factor를 변경하여 이 기준을 더 높일 수 있다.
  • WATERMARK_MAX
    • 디폴트로 min 값의 150% 값을 가진 페이지 수이고 kswapd가 자동으로 sleep되는 시점이다.
    • 사용자가 watermark_scale_factor를 변경하여 이 기준을 더 높일 수 있다.

 

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

 

다음과 같이 nr_free_pages 및 min, low, high 워터마크 값을 확인할 수 있다.

$ cat /proc/zoneinfo
Node 0, zone    DMA32
  per-node stats
(...생략...)
  pages free     633680
        min      5632             <-----
        low      7040             <-----
        high     8448             <-----
        spanned  786432
        present  786432
        managed  765771           
        protection: (0, 0, 0)
      nr_free_pages 633680        <-----
(...생략...) 

 

managed_pages

버디 시스템이 zone 별로 관리하는 페이지 수로 dma 및 normal 존에서의 초기 값은 다음과 같다.

  • managed_pages = present_pages – 버디 시스템이 동작하기 직전 reserve한 페이지들 수
  • managed_pages = spanned_pages – absent_pages – (memmap_pages + dma_reserve + …)
    • present_pages = spanned_pages – absent_pages

 

다음과 같이 존별 managed_pages 값을 확인할 수 있다.

$ cat /proc/zoneinfo
Node 0, zone    DMA32
  per-node stats
(...생략...)
  pages free     633680
        min      5632             
        low      7040             
        high     8448             
        spanned  786432
        present  786432
        managed  765771           <-----
        protection: (0, 0, 0)
      nr_free_pages 633680        
(...생략...) 

 

빠른 워터마크 비교

zone_watermark_fast()

mm/page_alloc.c

static inline bool zone_watermark_fast(struct zone *z, unsigned int order,
                unsigned long mark, int classzone_idx, unsigned int alloc_flags)
{
        long free_pages = zone_page_state(z, NR_FREE_PAGES);
        long cma_pages = 0;

#ifdef CONFIG_CMA
        /* If allocation can't use CMA areas don't use free CMA pages */
        if (!(alloc_flags & ALLOC_CMA))
                cma_pages = zone_page_state(z, NR_FREE_CMA_PAGES);
#endif

        /*
         * Fast check for order-0 only. If this fails then the reserves
         * need to be calculated. There is a corner case where the check
         * passes but only the high-order atomic reserve are free. If
         * the caller is !atomic then it'll uselessly search the free
         * list. That corner case is then slower but it is harmless.
         */
        if (!order && (free_pages - cma_pages) > mark + z->lowmem_reserve[classzone_idx])
                return true;

        return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags,
                                        free_pages);
}

요청한 @z 존에서 @mark 기준보다 높은 free 페이지에서 @order 페이지를 확보할 수 있는지 여부를 빠르게 판단하여 반환한다. order 0 할당 요청 전용으로 판단 로직이 추가되었다. (true=워터마크 기준 이상으로 ok, false=워터마크 기준 이하로 not-ok)

  • 코드 라인 4에서 해당 존의 free 페이지를 알아온다.
  • 코드 라인 9~10에서 movable 페이지 요청이 아닌 경우 cma 영역의 페이지들을 사용할 수 없다. 일단 cma 영역을 사용하는 페이지 수를 알아온다.
  • 코드 라인 19~20에서 order 0 할당 요청인 경우 빠른 판단을 할 수 있다. cma 페이지를 제외한 free 페이지가 lowmem 리저브 영역을 더한 워터 마크 기준을 초과하는 경우 true를 반환한다.
  • 코드 라인 22~23에서 order 1 이상의 할당 요청에서 워터 마크 기준을 초과하는지 여부를 반환한다.

 

다음 그림은 order 0 페이지 할당 요청 시 워터마크 기준에 의해 빠른 할당 가능 여부를 판단하는 모습을 보여준다.

 

기본 워터마크 비교

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 페이지가 워터마크 값의 기준 보다 충분한지 확인한다.

 

__zone_watermark_ok()

mm/page_alloc.c

/*
 * Return true if free base pages are above 'mark'. For high-order checks it
 * will return true of the order-0 watermark is reached and there is at least
 * one free page of a suitable size. Checking now avoids taking the zone lock
 * to check in the allocation paths if no pages are free.
 */
bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
                         int classzone_idx, unsigned int alloc_flags,
                         long free_pages)
{
        long min = mark;
        int o;
        const bool alloc_harder = (alloc_flags & (ALLOC_HARDER|ALLOC_OOM));

        /* free_pages may go negative - that's OK */
        free_pages -= (1 << order) - 1;

        if (alloc_flags & ALLOC_HIGH)
                min -= min / 2;

        /*
         * If the caller does not have rights to ALLOC_HARDER then subtract
         * the high-atomic reserves. This will over-estimate the size of the
         * atomic reserve but it avoids a search.
         */
        if (likely(!alloc_harder)) {
                free_pages -= z->nr_reserved_highatomic;
        } else {
                /*
                 * OOM victims can try even harder than normal ALLOC_HARDER
                 * users on the grounds that it's definitely going to be in
                 * the exit path shortly and free memory. Any allocation it
                 * makes during the free path will be small and short-lived.
                 */
                if (alloc_flags & ALLOC_OOM)
                        min -= min / 2;
                else
                        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_pages -= zone_page_state(z, NR_FREE_CMA_PAGES);
#endif

        /*
         * Check watermarks for an order-0 allocation request. If these
         * are not met, then a high-order request also cannot go ahead
         * even if a suitable page happened to be free.
         */
        if (free_pages <= min + z->lowmem_reserve[classzone_idx])
                return false;

        /* If this is an order-0 request then the watermark is fine */
        if (!order)
                return true;

        /* For a high-order request, check at least one suitable page is free */
        for (o = order; o < MAX_ORDER; o++) {
                struct free_area *area = &z->free_area[o];
                int mt;

                if (!area->nr_free)
                        continue;

                for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) {
                        if (!list_empty(&area->free_list[mt]))
                                return true;
                }

#ifdef CONFIG_CMA
                if ((alloc_flags & ALLOC_CMA) &&
                    !list_empty(&area->free_list[MIGRATE_CMA])) {
                        return true;
                }
#endif
                if (alloc_harder &&
                        !list_empty(&area->free_list[MIGRATE_HIGHATOMIC]))
                        return true;
        }
        return false;
}

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

  • 코드 라인 5에서 워터마크 값을 min 값에 일단 대입한다.
  • 코드 라인 7에서 최대한 워터마크 기준을 낮춰서라도 할당을 해야 하는지 판단여부를 alloc_harder에 대입한다.
    • gfp_atomic 요청 및 oom 상황에서의 할당은 시스템이 최대한 할당을 성공시켜야 한다.
  • 코드 라인 10에서 free 페이지를 할당 요청한 페이지 수-1 만큼 감소시킨다.
  • 코드 라인 12~13에서 atomic 요청 상황인 경우 ALLOC_HIGH 할당 플래그가 설정되는데 최대한 할당할 수 있게 하기 위해 워터마크 기준을 절반으로 줄인다.
  • 코드 라인 20~21에서 alloc_harder 상황이 아니면 reserved_highatomic 페이지들은 사용을 하지 않게 하기 위해 빼둔다.
  • 코드 라인 22~33에서 alloc_harder 상황인 경우 워터마크를 더 낮추는데 atomic 요청인 경우 25%, oom 상황인 경우 50%를 더 낮춘다.
  • 코드 라인 38~39에서 movable 페이지 할당 요청이 아닌 경우에는 cma 영역을 사용할 수 없다. 따라서 free 페이지 수에서 cma 영역의 페이지들을 먼저 제외시킨다.
  • 코드 라인 47~48에서 free 페이지가 lowmem 리저브 영역을 포함한 워터마크 기준 이하로 메모리가 부족한 상황이면 false를 반환한다.
  • 코드 라인 51~52에서 order 0 할당 요청인 경우 true를 반환한다.
  • 코드 라인 55~76에서 order 1 이상의 요청에 대해서 실제 free 리스트를 검색하여 할당가능한지 여부를 체크하여 반환한다. [요청 order, MAX_ORDER) 범위의 free_area를 순회하며 다음 3 가지 기준에 포함된 경우 할당 가능하므로 true를 반환한다.
    • unmovable, movable, reclaimable 타입의 리스트에 free 페이지가 있는 경우
    •  cma 영역을 사용해도 되는 movable 페이지 요청인 경우 cma 타입의 리스트에 free 페이지가 있는 경우
    • alloc_harder 상황인 경우 highatomic 타입의 리스트에 free 페이지가 있는 경우

 

다음 그림은 zone 워터마크 설정에 따른 free 페이지 할당 가능 여부를 체크하는 모습을 보여준다.

  • 좌측 그림과 같이 남은 free 페이지 수에 여유가 있는지 확인 후
  • 우측 그림에서와 같이 free_area[]를 검색하여 최종적으로 할당 가능한지 여부를 확인한다.
  • lowmem_reserve[]에 사용된 존 인덱스는 처음 요청한 preferred 존 인덱스이다.

 

정확한 워터마크 비교

zone_watermark_ok_safe()

page_alloc.c

bool zone_watermark_ok_safe(struct zone *z, unsigned int order,
                        unsigned long mark, int classzone_idx)
{
        long free_pages = zone_page_state(z, NR_FREE_PAGES);

        if (z->percpu_drift_mark && free_pages < z->percpu_drift_mark)
                free_pages = zone_page_state_snapshot(z, NR_FREE_PAGES);

        return __zone_watermark_ok(z, order, mark, classzone_idx, 0,
                                                                free_pages);
}

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

  • 코드 라인 4에서 먼저 대략적인 free 페이지 수를 알아온다.
  • 코드 라인 6~7에서 알아온 free 페이지 수가 percpu_drift_mark 이하인 경우 free 페이지를 정확히 산출한다.
    • percpu_drift_mark 값은 refresh_zone_stat_thresholds() 함수에서 설정된다.
  • 코드 라인 9~10에서 free 페이지 수를 워터마크와 비교하여 할당 가능 여부를 반환한다.

 

존 카운터(stat)

존 카운터(stat)들은 성능을 위해 존 카운터 및 per-cpu 카운터 2개를 각각 별도로 운영하고 있다. 존의 대략적인 값만을 요구할 때에는 성능을 위해 존 카운터 값을 그대로 반환하고, 정확한 산출이 필요한 경우에만 존 카운터와 per-cpu 카운터를 더해 반환한다.

 

percpu_drift_mark

커널 메모리 관리 시 남은 free 페이지 수를 읽어 워터마크와 비교하는 것으로 메모리 부족을 판단하는 루틴들이 많이 사용된다. 그런데 정확한 free 페이지 값을 읽어내려면 존 카운터와 per-cpu 카운터를 모두 읽어 더해야하는데, 이렇게 매번 계산을 하는 경우 성능을 떨어뜨리므로, 존 카운터 값만 읽어 high 워터마크보다 일정 기준 더 큰 크기로 설정된 percpu_drift_mark 값과 비교하여 이 값 이하일 때에만 보다 정확한 연산을 하도록 유도하는 방법을 사용하여 성능을 유지시킨다.

 

stat_threshold

카운터의 증감이 발생하면 성능을 위해 per-cpu 카운터를 먼저 증감시킨다. 존 카운터의 정확도를 위해 per-cpu 카운터의 값이 일정 기준의 stat_threshold를 초과할 경우에만 이 값을 존 카운터로 옮겨 더하는 방식을 사용한다.

 

다음 그림은 해당 cpu의 zone 카운터에 변동이 있을 때 stat_threshold를 초과하면 존 카운터에 옮겨지는 과정을 보여준다.

 

다음과 같이 stat_threshold 값을 확인할 수 있다.

$ cat /proc/zoneinfo
Node 0, zone    DMA32
  per-node stats
(...생략...)
  pagesets
    cpu: 0
              count: 143
              high:  378
              batch: 63
  vm stats threshold: 24         <-----
    cpu: 1
              count: 285
              high:  378
              batch: 63
  vm stats threshold: 24         <-----
(...생략...) 

 

zone_page_state()

include/linux/vmstat.h

static inline unsigned long zone_page_state(struct zone *zone,
                                        enum zone_stat_item item)
{
        long x = atomic_long_read(&zone->vm_stat[item]);
#ifdef CONFIG_SMP
        if (x < 0)
                x = 0;
#endif
        return x;
}

존의 @item에 해당하는 대략적인 vm stat 값을 반환한다.

  • 존의 vm stat 값만을 반환한다.

 

zone_page_state_snapshot()

include/linux/vmstat.h

/*
 * More accurate version that also considers the currently pending
 * deltas. For that we need to loop over all cpus to find the current
 * deltas. There is no synchronization so the result cannot be
 * exactly accurate either.
 */
static inline unsigned long zone_page_state_snapshot(struct zone *zone,
                                        enum zone_stat_item item)
{
        long x = atomic_long_read(&zone->vm_stat[item]);

#ifdef CONFIG_SMP
        int cpu;
        for_each_online_cpu(cpu)
                x += per_cpu_ptr(zone->pageset, cpu)->vm_stat_diff[item];

        if (x < 0)
                x = 0;
#endif
        return x;
}

존의 @item에 해당하는 vm stat 값을 정확히 산출한다.

  • 정확한 값을 반환하기 위해 다음 수식을 사용한다.
    • 존의 vm stat 값 + 모든 per-cpu vm stat 값

 

다음 그림은 zone_page_state_snapshop() 함수와 zone_page_state() 함수의 차이를 보여준다.

 

zone_watermark_ok() 함수와 zone_watermark_ok_safe() 함수의 다른 점
  • zone_watermark_ok() 함수
    • 워터마크와 비교할 free 페이지 수를 대략적으로 산출된 free 페이지 값을 사용한다.
  • zone_watermark_ok_safe() 함수
    • 워터마크와 비교할 free 페이지 수에 정확히 산출된 free 페이지 값을 사용한다.

 

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

 


워터마크 및 관련 설정 초기화

워터마크 및 관련 설정들의 초기화는 다음과 같이 진행된다.

 

워터마크 min 값 설정

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 prefee
rred\n",
                                new_min_free_kbytes, user_min_free_kbytes);
        }
        setup_per_zone_wmarks();
        refresh_zone_stat_thresholds();
        setup_per_zone_lowmem_reserve();

#ifdef CONFIG_NUMA
        setup_min_unmapped_ratio();
        setup_min_slab_ratio();
#endif

        return 0;
}
core_initcall(init_per_zone_wmark_min)

lowmem의 가용 페이지 크기를 사용하여 워터마크 기준으로 사용하는 min_free_kbytes 값을 산출한다. 그 외에 각 zone의 워터마크, lowmem_reserve, totalreserve, min umpapped, min slap 페이지 등을 산출한다.

  • 코드 라인 6~18에서 lowmem의 가용 페이지 수를 사용하여 워터마크 기준 값으로 사용되는 min_free_kbytes 값을 산출한다.
    • lowmem 가용 페이지 산출식
      • managed_pages – high 워터마크 페이지
    • min_free_kbytes 산출식
      • sqrt((lowmem의 가용 페이지 수 >> 10) * 16)
        • 예) sqrt(512M * 0x10) -> 2M
      • 산출된 값은 128 ~ 65536 범위 이내로 제한 (128K ~ 64M 범위)
    • 커널이 초기화될 때 이외에도 hotplug 메모리를 사용하여 메모리가 hot add/del 될 때마다 이 초기화 루틴이 수행된다.
    • 커널이 처음 초기화될 때에는 각 zone의 워터마크 초기값은 모두 0이다.
  • 코드 라인 19에서 각 존의 워터 마크 값 및 totalreserve_pages 값을 산출한다.
  • 코드 라인 20에서 각 존의 stat 스레졸드와 percpu_drift_mark 를 산출한다.
  • 코드 라인 21에서 zone->lowmem_reserve 및 totalreserve 페이지를 산출한다.
  • 코드 라인 24에서 최소 ummapped 페이지를 산출한다.
  • 코드 라인 25에서 최소 slab 페이지를 산출한다.

 

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

  • lowmem(dma, normal)의 가용 페이지(managed – high 워터마크)를 16으로 곱하고 제곱근하여 산출한다.
    • 최초 부트업 시 high 워터마크 값은 0 이다.
  • ARM64의 경우 dma32 존을 사용하고, highmem 존은 사용하지 않는다.

 

lowmem 가용 페이지 수 산출

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에서 normal 존 이하의 존들을 대상으로 가용 페이지 수를 산출하여 반환한다.

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

 

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:
 *
 *     nr_free_zone_pages = 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(zone);
                unsigned long high = high_wmark_pages(zone);
                if (size > high)
                        sum += size - high;
        }

        return sum;
}

현재 노드의 zonelist에서 offset 이하의 zone들을 대상으로 가용한 페이지를 모두 더한 수를 반환한다.

  • 가용 페이지 수
    • managed_pages – high 워터마크 페이지

 

워터 마크 비율 설정

워터마크 간의 간격은 디폴트로 워터마크 min 값의 25%를 사용하는데 kswapd 효율을 높이기 위해 사용자가 워터 마크 비율을 변경할 수 있다.

 

watermark_scale_factor

/proc/sys/vm/watermark_scale_factor를 통해 설정할 수 있으며 초깃값은 10이다. 초깃값이 10일 때 메모리의 0.1% 값이라는 의미이고, 최댓값으로 1000이 주어질 때 메모리의 10% 값이 된다. 이러한 경우 min과 low 및 low와 high와의 최대 간격은 메모리(managed_pages)의 10%까지 가능해진다.

  • 커널 v4.6-rc1에서 대규모 메모리를 갖춘 시스템에서 kswapd 효율을 높이기 위해 워터마크 low와 high 값의 임계점을 기존보다 더 높일 수 있도록 추가되었다.

 


min, low, high 워터마크 산출

메모리의 양으로 결정된 min_free_kbytes 값을 사용하여 존 별 min, low, 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별 min, low, high 워터마크를 산출한다.

 

__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(zone);
        }

        for_each_zone(zone) {
                u64 tmp;

                spin_lock_irqsave(&zone->lock, flags);
                tmp = (u64)pages_min * zone_managed_pages(zone);
                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 control asynch page reclaim, and so should
                         * not be capped for highmem.
                         */
                        unsigned long min_pages;

                        min_pages = zone_managed_pages(zone) / 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;
                }

                /*
                 * Set the kswapd watermarks distance according to the
                 * scale factor in proportion to available memory, but
                 * ensure a minimum size on small systems.
                 */
                tmp = max_t(u64, tmp >> 2,
                            mult_frac(zone_managed_pages(zone),
                                      watermark_scale_factor, 10000));

                zone->_watermark[WMARK_LOW]  = min_wmark_pages(zone) + tmp;
                zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;
                zone->watermark_boost = 0;

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

        /* update totalreserve_pages */
        calculate_totalreserve_pages();
}

존별 low, min, high 워터마크를 산출하고 totalreserve 페이지를 갱신한다.

  • 코드 라인 3에서 전역 min_free_kbytes 값을 페이지 수 단위로 바꾸어 pages_min에 대입한다.
  • 코드 라인 9~12에서 lowmem 영역의 managed_pages를 합산하여 lowmem_pages를 구한다.
  • 코드 라인 14~19에서 각 존을 돌며 tmp에 pages_min(min_free_kbytes를 페이지로 환산)을 현재 존의 비율만큼의 페이지 수로 설정한다.
  • 코드 라인 20~41에서 highmem 존인 경우에는 실제 managed_pages를 1024로 나눈 값을 32~128까지의 범위로 제한한 값을 min 워터마크에 저장하고, highmem 존이 아닌 경우에는 min 워터마크에 위에서 산출한 tmp 값을 설정한다.
  • 코드 라인 48~53에서 각 워터마크 간의 간격은 다음 두 산출 값 중 큰 값으로 결정한다.
    • 최초 산출된 min 값의 25%
    • managed_pages * 워터마크 비율(0.1 ~ 10%)
  • 코드 라인 54에서 워터마크 부스트 값을 0으로 초기화한다.
  • 코드 라인 60에서 high 워터마크 값이 갱신되었으므로 totalreserve 페이지 수를 산출하여갱신한다.
    • high 워터마크와 lowmem reserve 페이지가 재산출될 때 마다 totalreserve 값도 갱신된다.

 

다음 그림은 min_free_kbytes 값과 lowmem 영역의 가용 페이지(managed_pages – high 워터마크)를 각 zone의 비율로 min, low, high 워터마크를 산출하는 모습을 보여준다.

__setup_per_zone_wmarks-1

 

다음 그림은 워터마크 간격 값으로 우측의 두 산출된 값 중 가장 큰 값이 적용되는 모습을 보여준다.

 


존 stat 스레졸드 산출

high 워터마크를 사용하여 존 및 노드에 대한 stat 스레졸드들을 산출한다. 산출되는 항목들은 다음과 같다.

  • 노드 stat 스레졸드
  • 존 stat 스레졸드
  • percpu_drift_mark

 

refresh_zone_stat_thresholds()

mm/vmstat.c

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

        /* Zero current pgdat thresholds */
        for_each_online_pgdat(pgdat) {
                for_each_online_cpu(cpu) {
                        per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold = 0;
                }
        }

        for_each_populated_zone(zone) {
                struct pglist_data *pgdat = zone->zone_pgdat;
                unsigned long max_drift, tolerate_drift;

                threshold = calculate_normal_threshold(zone);

                for_each_online_cpu(cpu) {
                        int pgdat_threshold;

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

                        /* Base nodestat threshold on the largest populated zone. */
                        pgdat_threshold = per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_thresholdd
;
                        per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold
                                = max(threshold, pgdat_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;
        }
}

모든 노드 및 존에 대해 노드 stat 스레졸드, 존 stat 스레졸드 및 percpu_drift_mark 를 산출한다.

  • 코드 라인 9~13에서 전체 노드를 순회하며 노드 stat 스레졸드를 0으로 초기화한다.
  • 코드 라인 15~25에서 populate존을 순회하며 존 stat 스레졸드 값을 산출한다.
  • 코드 라인 28~31에서 노드 stat 스레졸드를 가장 큰 존 stat 스레졸드 값으로 갱신한다.
  • 코드 라인 38~42에서 존 멤버인 percpu_drift_mark를 산출한다.
    • max_drift = online cpu 수 * 산출한 스레졸드 값
    • max_drift 값이 워터마크들 간의 간격보다 큰 경우 high 워터마크 + max_drift 값을 사용한다.

 

a) 메모리 비압박 시 사용할 스레졸드 산출

노멀 스레졸드 값은 다음과 같은 케이스들에서 호출되어 산출되어 설정된다.

  • 부트 업 및 메모리의 hotplug 및 hotremove로 인해 메모리가 변경되면서 호출되는 refresh_zone_stat_thresholds() 함수
  • kswapd_try_to_sleep() 함수에서 kswapd가 sleep하기 직전

 

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(zone) >> (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로 제한한다.

  • 산출식
    • threshold = 2 * (log2(온라인 cpu 수) + 1) * (1 + log2(z->managed_pages / 128M) + 1)
      • = 2 * fls(온라인 cpu 수) + fls(z->managed_pages / 128M)
    • threshold 값은 최대 125로 제한
    • 예) 6 cpu이고, zone이 managed_pages=3.5G
      • threshold=2 x 3 x 5=30
    • 예) 64 cpu이고, zone이 managed_pages=3.5G
      • threshold=2 x 7 x 5=70

 

b) 메모리 압박 시 사용할 스레졸드 산출

pressure 스레졸드 값은 메모리가 부족하여 동작되는 kswapd가 깨어난 직후에 사용되는 스레졸드 값으로 관련 함수는 다음과 같다.

  • kswapd_try_to_sleep() 함수

 

calculate_pressure_threshold()

mm/vmstat.c

int calculate_pressure_threshold(struct zone *zone)
{
        int threshold;
        int watermark_distance;

        /*
         * As vmstats are not up to date, there is drift between the estimated
         * and real values. For high thresholds and a high number of CPUs, it
         * is possible for the min watermark to be breached while the estimated
         * value looks fine. The pressure threshold is a reduced value such
         * that even the maximum amount of drift will not accidentally breach
         * the min watermark
         */
        watermark_distance = low_wmark_pages(zone) - min_wmark_pages(zone);
        threshold = max(1, (int)(watermark_distance / num_online_cpus()));

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

        return threshold;
}

요청 zone의 managed_pages 크기에 비례하여 메모리 압박 상황에서 사용할 스레졸드 값을 구하는데 최대 125로 제한한다.

  • cpu가 많은 시스템에서 스레졸드가 크면 각 cpu에서 유지되고 있는 per-cpu 카운터들이 존 카운터에 반영되지 못한 값들이 매우 많을 가능성이 있다. 따라서 메모리 부족 상황에서는 스레졸드 값을 줄일 수 있도록 다음과 같은 산출식을 사용한다.
  • 산출식
    • threshold = 워터마크 간 간격 / 온라인 cpu 수
    • threshold 값은 최대 125로 제한
    • 예) 6 cpu이고, min=1975, low=2468, high=2962,
      • threshold=493 / 6 = 82
    • 예) 64 cpu이고, min=1975, low=2468, high=2962
      • threshold=493 / 64 = 7

 


lowmem reserve 페이지 산출

메모리 할당 요청 시 원하는 존을 지정하는 방법은 다음과 같은 gfp 마스크를 사용한다. 다음 플래그를 사용하지 않는 경우 디폴트로 NORMAL 존을 선택한다.

  • __GFP_DMA
  • __GFP_DMA32
  • __GFP_HIGHMEM
  • __GFP_MOVABLE

 

커널 개발자들은 메모리 할당 시 다음과 같은 gfp 마스크를 사용하여 존을 선택한다.

  • GFP_DMA
    • __GFP_DMA gfp 플래그를 사용하고, DMA 존을 선택한다.
  • GFP_DMA32
    • __GFP_DMA32 gfp 플래그를 사용하고, DMA32 존을 선택한다.
  • GFP_KERNEL
    • 존 지정과 관련한 gfp 플래그를 사용하지 않으면 NORMAL 존을 선택한다.
  • GFP_HIGHUSER
    • __GFP_HIGHMEM gfp 플래그를 사용하고, HIGHMEM 존을 선택한다.
  • GFP_HIGHUSER_MOVABLE
    • __GFP_HIGHMEM 및 __GFP_HIGHUSER gfp 플래그를 사용하고, MOVABLE 존을 선택한다.

 

요청한 존에서 할당을 하지 못하면 zonelist를 통해 다음 존으로 fallback 되는데 그 순서는 다음과 같다.

  • MOVABLE -> HIGHMEM -> NORMAL -> DMA32 -> DMA

 

lowmem_reserve

여러 존들 사이에서 lowmem 영역에 해당하는 normal 및 dma/dma32 존에서 사용되는 메모리 영역은 커널에 미리 매핑이 되어 있어 빠른 커널 메모리 할당이 가능한 존이다. 따라서 유저 application에서 사용하는 highmem 및 movable 존에 대한 할당 요청 시 lowmem 영역을 할당하는 것을 선호하지 않는다. 부득이하게 상위 존의 메모리가 부족하여 fallback 되어 lowmem 영역의 존에서 할당해야 할 경우 해당 존의 할당을 제한할 페이지 수가 지정된다. 이 값들은 최초 요청(request) 존과 최종 할당(target) 존에 대한 매트릭스로 지정된다.

 

다음 그림의 예와 같이 각 존의 lowmem_reserve 값을 참고해본다.

  • 1G 메모리를 가진 ARM32 시스템에 3 개의 존이 각각 dma(16M), normal(784M), highmem(200M)으로 운용되고 있다고 가정한다.
  • 괄호{} 안의 값들은 시스템에서 운영되는 존 순서이고 그 값들은 할당 허용 페이지 수가 지정된다.
  • 아래 붉은 선을 따라가보면 highmem 요청을 받았으나 fallback 되어 normal 존으로 향했을 때 워터마크 기준보다 1600 페이지를 더 높여 할당을 제한하는 방법으로 lowmem 영역을 보호한다.

 

다음 그림과 같이 페이지 할당 시 최초 요구한 존에서 할당 실패할 때 fallback 되어 lowmem 영역으로 향할 때 워터마크 기준보다 더 추가된 페이지 할당 제한을 보여준다.

 

lowmem_reserve_ratio

각 존의 managed_pages를 사용하여 존별 lowmem_reserve 값을 산출하기 위한 비율이다.

 

다음과 같이 lowmem_reserve_ratio 값을 확인할 수 있다.

  • 시스템에서 운영되는 낮은 zone 부터 높은 zone 순서대로 비율이 설정된다.
  • 아래 예제 값은 dma, normal, highmem 존 순서이다.
$ cat /proc/sys/vm/lowmem_reserve_ratio
256     32      0

 

다음 그림과 같이 할당 요청 존에 대해 fallback 되었을 때 허용되는 페이지 수의 산출은 다음과 같다.

  • 분자의 값으로 요청 존에서 할당 실패한 존까지의 managed_pages 수를 더함을 알 수 있다.
  • 분모의 값으로 fallback되어 최종 할당 존의 lowmem_reserve_ratio가 지정됨을 알 수 있다.

 

다음 그림과 같이 각 존의 managed_pages 크기와 lowmem_reserve_ratio를 이용해 각 존들의 lowmem_reserve 값이 산출되는 모습을 보여준다.

 

다음은 NUMA  및 4개의 존(dma, dma32, normal, movable)으로 구성된 x86 시스템에서 노드-존별 lowmem_reserve 값을 확인해본다.

  • 아래 protection 항목을 보면된다. 처음 이 기능이 개발될 때 사용한 변수명이 protection 이었고, 나중에 lowmem_reserve로 바뀌었다.
$ cat /proc/zoneinfo
Node 0, zone      DMA
  pages free     3915
        min      5
        low      6
        high     7
        scanned  0
        spanned  4095
        present  3992
        managed  3971
    nr_free_pages 3915
...
        protection: (0, 1675, 31880, 31880)
...
Node 0, zone    DMA32
  pages free     59594
        min      588
        low      735
        high     882
        scanned  0
        spanned  1044480
        present  491295
        managed  429342
    nr_free_pages 59594
...
        protection: (0, 0, 30204, 30204)
...
Node 0, zone   Normal
  pages free     902456
        min      10607
        low      13258
        high     15910
        scanned  0
        spanned  7864320
        present  7864320
        managed  7732469
    nr_free_pages 902456
...
        protection: (0, 0, 0, 0)
...
Node 1, zone   Normal
  pages free     1133093
        min      11326
        low      14157
        high     16989
        scanned  0
        spanned  8388608
        present  8388608
        managed  8256697
    nr_free_pages 1133093
...
        protection: (0, 0, 0, 0)
...

 

다음과 같이 한 개의 존만 운영되는 시스템에서의 lowmem_reserve 값은 0이 되는 것을 확인 할 수 있다.

  • dma32, normal 존을 운영하는 ARM64 시스템의 경우 dma32 존만으로 모든 메모리를 사용하는 경우 하나의 존으로만 운영한다. 즉 fallback이 없다.
$ cat /proc/zoneinfo
Node 0, zone    DMA32
  per-node stats
(...생략...)
  pages free     633680
        min      5632             
        low      7040             
        high     8448             
        spanned  786432
        present  786432
        managed  765771           
        protection: (0, 0, 0)     <-----
      nr_free_pages 633680        
(...생략...)
Node 0, zone   Normal
  pages free     0
        min      0
        low      0
        high     0
        spanned  0
        present  0
        managed  0
        protection: (0, 0, 0)     <-----
(...생략...) 

 

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

                        zone->lowmem_reserve[j] = 0;

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

                                idx--;
                                lower_zone = pgdat->node_zones + idx;

                                if (sysctl_lowmem_reserve_ratio[idx] < 1) {
                                        sysctl_lowmem_reserve_ratio[idx] = 0;
                                        lower_zone->lowmem_reserve[j] = 0;
                                } else {
                                        lower_zone->lowmem_reserve[j] =
                                                managed_pages / sysctl_lowmem_reserve_ratio[idx];
                                }
                                managed_pages += zone_managed_pages(lower_zone);
                        }
                }
        }

        /* update totalreserve_pages */
        calculate_totalreserve_pages();
}

존의 managed_pages 값과 lowmem_reserve_ratio 비율을 사용하여 존별 lowmem_reserve[]  페이지 값들을 산출한다.

  • 코드 라인 6~11에서 전체 온라인 노드 수 만큼 순회하고, 해당 노드내 가용 존 수만큼 내부에서 순회하며 lowmem_reserve[] 값을 0으로 초기화한다.
  • 코드 라인 13~30애서 가장 처음 존을 제외하고 루프 카운터로 지정된 존부터 밑으로 루프를 돌며 첫 번째 존(idx=0)을 제외한 managed_pages를 더해 lowmem_reserve에 lowmem_reserve_ratio 비율만큼 지정한다.
  • 코드 라인 33에서 lowmem_reserve 값이 변화되었으므로 totalreserve 페이지 수도 갱신한다.

 

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

 


totalreserve 페이지 산출

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) {

                pgdat->totalreserve_pages = 0;

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

                        /* 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 > managed_pages)
                                max = managed_pages;

                        pgdat->totalreserve_pages += max;

                        reserve_pages += max;
                }
        }
        totalreserve_pages = reserve_pages;
}

노드별 totalreserve_pages 값과 전역 totalreserve_pages 값을 산출한다.

  • 코드 라인 7~9에서 모든 노드를 순회하며 totalreserve_pages를 산출하기 위해 먼저 0으로 초기화한다.
  • 코드 라인 11~14에서 모든 존을 순회하며 해당 존의 managed 페이지를 알아온다.
  • 코드 라인 17~28에서 순회 중인 존부터 마지막 존에 설정된 lowmem_reserve 값의 최대치를 알아온 후 high 워터마크를 더해 totalreserve_pages에 추가한다. 추가할 값이 순회중인 존의 managed_pages를 초과하지 않게 제한한다.
  • 코드 라인 30~33에서 각 노드와 존에서 추가한 값들을 전역 totalreserve_pages에 대입한다.

 

다음 그림은 totalreserve_pages 값을 산출하기 위해 아래 값들을 각 노드와 존을 모두 더하여 산출하는 과정을 보여준다.

  • 각 존 이상의 lowmem_reserve 최대치
  • 각 존의 high 워터마크

 


inactive_ratio 산출

다음 과정들은 커널 v4.7-rc1에서 부트업 시 inactive_ratio를 산출하는 것을 제거하였다. inactive 리스트의 비율은 이제 inactive_list_is_low() 함수내에서 전체 메모리에 맞춰 자동으로 비교되므로 사용하지 않게 되었다.

 

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

 

참고

 

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

<kernel v5.0>

Slowpath

NUMA 메모리 정책에 따른 zonelist에서 fastpath 할당이 실패한 경우 slowpath 단계를 진행한다. 만일 nofail 옵션이 사용된 경우 할당이 성공할 때까지 반복한다. slowpath 단계를 진행하는 동안 요청 옵션에 따라 free 페이지 부족 시 다음과 같은 회수 동작들을 수행한다.

  • direct-compaction
    • 페이지 할당 시 요청한 order의 페이지가 부족하여 할당 할 수 없는 상황일 때 곧바로(direct) compaction 동작을  수행하여 페이지들을 확보한 후 할당한다.
  • direct-reclaim
    • 페이지 할당 시 요청한 order의 페이지가 부족하여 할당 할 수 없는 상황일 때 곧바로(direct) reclaim 동작을 수행하여 페이지들을 확보한 후 할당한다.
    • reclaim 동작을 수행하면
  • OOM killing
    • 페이지 할당 시 요청한 order의 페이지가 부족하여 최종적으로 OOM killing을 통해 특정 태스크를 종료시키므로 확보한 페이지들로 할당한다.
  •  kswapd
    • 백그라운드에서 페이지 회수(reclaim) 매커니즘을 동작시켜 Dirty 된 파일 캐시들을 기록하고, Clean된 파일 캐시를 비우고, swap 시스템에 페이지들을 옮기는 등으로 free 페이지들을 확보한다.
  • kcompactd
    • 백그라운드에서 compaction 동작을 수행하여 파편화된 movable 페이지를 이동시켜 free 페이지들을 병합하는 것으로 더 큰 order free 페이지들을 확보한다.

 

OOM(Out of Memory) Killing

메모리가 부족한 상황에서 compaction이나 reclaim을 통해 페이지 회수가 모두 실패하여 더 이상 진행할 수 없는 경우 다음 특정 태스크 중 하나를 죽여야 하는 상황이다.

  • 현재 태스크가 종료 중인 경우 0 순위
  • OOM 상태에서 먼저 처리해야 할 지정된 태스크 1 순위
  • 태스크 들 중 일정 연산 기준 이상의 한 태스크 2 순위

 

다음은 OOM killing을 강제로 수행한 결과를 보여준다.

$ echo f > /proc/sysrq-trigger
$ dmesg
[460767.036092] sysrq: SysRq : Manual OOM execution
[460767.037248] kworker/0:0 invoked oom-killer: gfp_mask=0x24000c0, order=-1, oom_score_adj=0
[460767.038016] kworker/0:0 cpuset=/ mems_allowed=0
[460767.038468] CPU: 0 PID: 8063 Comm: kworker/0:0 Tainted: G        W       4.4.103-g94108fb3583f-dirty #4
[460767.039307] Hardware name: ROCK960 - 96boards based on Rockchip RK3399 (DT)
[460767.039948] Workqueue: events moom_callback
[460767.040348] Call trace:
[460767.040603] [<ffffff800808806c>] dump_backtrace+0x0/0x21c
[460767.041104] [<ffffff80080882ac>] show_stack+0x24/0x30
[460767.041583] [<ffffff80083b56f4>] dump_stack+0x94/0xbc
[460767.042064] [<ffffff80081bdd4c>] dump_header.isra.5+0x50/0x15c
[460767.042603] [<ffffff800817f240>] oom_kill_process+0x94/0x3dc
[460767.043128] [<ffffff800817f7fc>] out_of_memory+0x1e4/0x2ac
[460767.043639] [<ffffff800845d9ac>] moom_callback+0x48/0x70
[460767.044128] [<ffffff80080cd264>] process_one_work+0x220/0x378
[460767.044663] [<ffffff80080ce124>] worker_thread+0x2e0/0x3a0
[460767.045176] [<ffffff80080d3004>] kthread+0xe0/0xe8
[460767.045627] [<ffffff80080826c0>] ret_from_fork+0x10/0x50
[460767.046235] Mem-Info:
[460767.046484] active_anon:33752 inactive_anon:6597 isolated_anon:0
                 active_file:535284 inactive_file:190256 isolated_file:0
                 unevictable:0 dirty:31 writeback:0 unstable:0
                 slab_reclaimable:48749 slab_unreclaimable:5217
                 mapped:25870 shmem:6685 pagetables:894 bounce:0
                 free:145659 free_pcp:686 free_cma:0
[460767.049564] DMA free:582636kB min:7900kB low:9872kB high:11848kB active_anon:135008kB inactive_anon:26388kB active_file:2141136kB inactive_file:761024kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:4061184kB managed:3903784kB mlocked:0kB dirty:124kB writeback:0kB mapped:103480kB shmem:26740kB slab_reclaimable:194996kB slab_unreclaimable:20868kB kernel_stack:4352kB pagetables:3576kB unstable:0kB bounce:0kB free_pcp:2744kB local_pcp:620kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
[460767.053616] lowmem_reserve[]: 0 0 0
[460767.054043] DMA: 997*4kB (UME) 613*8kB (UME) 559*16kB (UME) 482*32kB (UM) 356*64kB (UM) 160*128kB (UME) 77*256kB (UME) 40*512kB (UM) 29*1024kB (ME) 21*2048kB (M) 96*4096kB (M) = 582636kB
[460767.055848] 732229 total pagecache pages
[460767.056242] 0 pages in swap cache
[460767.056559] Swap cache stats: add 0, delete 0, find 0/0
[460767.057065] Free swap  = 1048572kB
[460767.057390] Total swap = 1048572kB
[460767.057714] 1015296 pages RAM
[460767.058027] 0 pages HighMem/MovableOnly
[460767.058388] 39350 pages reserved
[460767.058689] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[460767.059547] [  195]     0   195     7917     1596       9       3        0             0 systemd-journal
[460767.060436] [  229]     0   229     3281      822       9       4        0         -1000 systemd-udevd
[460767.061301] [  257]   102   257     1952      979       8       4        0             0 systemd-network
[460767.062193] [  382]   101   382    20508      936      10       5        0             0 systemd-timesyn
[460767.063081] [  416]     0   416    61035     1826      18       3        0             0 upowerd
[460767.063883] [  419]   106   419     1676      980       7       3        0          -900 dbus-daemon
[460767.064739] [  434]     0   434     2481     1120       7       4        0             0 wpa_supplicant
[460767.065614] [  437]     0   437     1900     1091       9       4        0             0 systemd-logind
[460767.066533] [  441]     0   441    79590     2400      20       4        0             0 udisksd
[460767.067365] [  450]     0   450      446      231       5       4        0             0 acpid
[460767.068170] [  454]     0   454    54453      725      11       3        0             0 rsyslogd
[460767.068973] [  459]     0   459    88404     4283      28       4        0             0 NetworkManager
[460767.069849] [  478]     0   478    58975     2351      18       4        0             0 polkitd
[460767.070677] [  568]   103   568     2102     1141       8       4        0             0 systemd-resolve
[460767.071565] [  585]     0   585    58528     1963      17       4        0             0 lightdm
[460767.072396] [  590]     0   590      623      363       6       4        0             0 agetty
[460767.073213] [  592]     0   592     1841      780       8       4        0             0 login
[460767.073991] [  603]     0   603   274336    17176      90       5        0             0 Xorg
[460767.074794] [  630]   110   630      441       92       5       4        0             0 uml_switch
[460767.075645] [  658]     0   658     2357     1385       8       3        0             0 systemd
[460767.076472] [  668]     0   668     2994      454      11       4        0             0 (sd-pam)
[460767.077298] [  673]     0   673     1675     1097       7       3        0             0 bash
[460767.078102] [  772]     0   772    40489     2052      14       4        0             0 lightdm
[460767.078904] [  780]  1000   780     2476     1328       9       4        0             0 systemd
[460767.079731] [  785]  1000   785     2994      454      11       4        0             0 (sd-pam)
[460767.080559] [  788]  1000   788    61601     3212      23       3        0             0 lxsession
[460767.081398] [  808]  1000   808     1801      382       7       4        0             0 dbus-launch
[460767.082257] [  809]  1000   809     1596      681       7       4        0             0 dbus-daemon
[460767.083134] [  830]  1000   830      984       79       6       4        0             0 ssh-agent
[460767.083948] [  838]  1000   838    58269     1523      14       3        0             0 gvfsd
[460767.084752] [  848]  1000   848    13921     3757      19       3        0             0 openbox
[460767.085579] [  853]  1000   853   196606     6848      44       4        0             0 lxpanel
[460767.086479] [  854]  1000   854    98755     7408      36       3        0             0 pcmanfm
[460767.087311] [  859]  1000   859      984       79       7       4        0             0 ssh-agent
[460767.088153] [  863]  1000   863    92314    13900      46       5        0             0 blueman-applet
[460767.089028] [  868]  1000   868   112028    14267      67       5        0             0 nm-applet
[460767.089842] [  869]  1000   869    43274     2728      20       3        0             0 xfce4-power-man
[460767.090729] [  876]  1000   876     2359     1082       9       4        0             0 xfconfd
[460767.091555] [  885]  1000   885   123932     2468      19       3        0             0 pulseaudio
[460767.092403] [  898]  1000   898    39537     1649      13       3        0             0 menu-cached
[460767.093268] [  905]     0   905     1810      963       7       4        0             0 bluetoothd
[460767.094139] [  915]  1000   915    67422     2887      21       5        0             0 gvfs-udisks2-vo
[460767.095036] [  923]  1000   923    77517     2078      19       5        0             0 gvfsd-trash
[460767.095889] [  933]  1000   933     9809     1559      12       3        0             0 obexd
[460767.096706] [ 5483]     0  5483     3049     1593      11       3        0             0 sshd
[460767.097537] [ 5498]     0  5498     2968     1498      10       3        0             0 sshd
[460767.098351] [ 5511]     0  5511      573      403       5       3        0             0 sftp-server
[460767.099273] [ 5518]     0  5518     1691     1156       7       3        0             0 bash
[460767.100048] [ 5735]     0  5735     3048     1579      11       4        0             0 sshd
[460767.100810] [ 5743]     0  5743     2968     1511      10       4        0             0 sshd
[460767.101580] [ 5766]     0  5766      573      427       6       3        0             0 sftp-server
[460767.102396] [ 5773]     0  5773     1698     1173       7       3        0             0 bash
[460767.103166] [ 5994]     0  5994     3048     1565       9       4        0             0 sshd
[460767.103928] [ 5999]     0  5999     2968     1529      10       4        0             0 sshd
[460767.104697] [ 6021]     0  6021      573      409       5       4        0             0 sftp-server
[460767.105515] [ 6028]     0  6028     1699     1172       7       3        0             0 bash
[460767.106289] [ 7742]     0  7742     2968     1514      10       3        0             0 sshd
[460767.107059] [ 7758]     0  7758     1697     1221       7       3        0             0 bash
[460767.107821] [ 7849]     0  7849     2968     1506      10       4        0             0 sshd
[460767.108591] [ 7863]     0  7863     1699     1201       7       3        0             0 bash
[460767.109360] Out of memory: Kill process 603 (Xorg) score 13 or sacrifice child
[460767.110302] Killed process 603 (Xorg) total-vm:1097344kB, anon-rss:15892kB, file-rss:52812kB

 

__alloc_pages_slowpath()

다음 그림과 같이 slow-path 페이지 할당 과정을 보여준다.

 

mm/page_alloc.c -1/5-

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                                                struct alloc_context *ac)
{
        bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
        const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
        struct page *page = NULL;
        unsigned int alloc_flags;
        unsigned long did_some_progress;
        enum compact_priority compact_priority;
        enum compact_result compact_result;
        int compaction_retries;
        int no_progress_loops;
        unsigned int cpuset_mems_cookie;
        int reserve_flags;

        /*
         * We also sanity check to catch abuse of atomic reserves being used by
         * callers that are not in atomic context.
         */
        if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
                gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:
        compaction_retries = 0;
        no_progress_loops = 0;
        compact_priority = DEF_COMPACT_PRIORITY;
        cpuset_mems_cookie = read_mems_allowed_begin();

        /*
         * The fast path uses conservative alloc_flags to succeed only until
         * kswapd needs to be woken up, and to avoid the cost of setting up
         * alloc_flags precisely. So we do that now.
         */
        alloc_flags = gfp_to_alloc_flags(gfp_mask);

        /*
         * We need to recalculate the starting point for the zonelist iterator
         * because we might have used different nodemask in the fast path, or
         * there was a cpuset modification and we are retrying - otherwise we
         * could end up iterating over non-eligible zones endlessly.
         */
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                                        ac->high_zoneidx, ac->nodemask);
        if (!ac->preferred_zoneref->zone)
                goto nopage;

        if (alloc_flags & ALLOC_KSWAPD)
                wake_all_kswapds(order, gfp_mask, ac);

        /*
         * The adjusted alloc_flags might result in immediate success, so try
         * that first
         */
        page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
        if (page)
                goto got_pg;
  • 코드 라인 5에서 페이지 할당 중에 free 페이지가 기준 이하인 경우 직접 적인 페이지 회수(direct-reclaim)가 필요한데 이를 허용하는지 여부를 알아온다. direct-reclaim을 허용하는 요청들은 다음과 같다.
    • 커널 메모리 할당에 사용하는 GFP_KERNEL, GFP_KERNEL_ACCOUNT, GFP_NOIO, GFP_NOFS
    • 유저 메모리 할당에 사용하는 GFP_USER
  • 코드 라인 6에서 order 3 이상이면 높은 order로 판단하여 costly order라고 한다.
  • 코드 라인 21~23에서 불합리하게 gfp 마스크에 atomic과 direct-reclaim 요청을 동시에 한 경우 atomic 요청을 무시하도록 gfp 마스크에서 제거한다.
  • 코드 라인 25~29에서 디폴트 compact 우선 순위로 준비한다.
  • 코드 라인 36에서 gfp 마스크로 할당 플래그를 구한다.
  • 코드 라인 44~47에서 노드 마스크와 zonelist에서 리셋하여 다시 첫 zone을 선택한다. 만일 대상 노드 마스크에 가용한 zone이 없는 경우 nopage: 레이블로 이동한다.
  • 코드 라인 49~50에서 kswapd reclaim이 허용된 경우 zonelist에 관련된 가용 zone에 대한 노드들에서 free 메모리가 기준 이상 적거나 너무 많은 파편화 상태인 경우 해당 노드의 kswapd를 깨운다.
  • 코드 라인 56~58에서 조정된 할당 플래그를 사용하여 첫 번째 slow-path 할당 시도를 수행한다.

 

mm/page_alloc.c -2/5-

.       /*
         * For costly allocations, try direct compaction first, as it's likely
         * that we have enough base pages and don't need to reclaim. For non-
         * movable high-order allocations, do that as well, as compaction will
         * try prevent permanent fragmentation by migrating from blocks of the
         * same migratetype.
         * Don't try this for allocations that are allowed to ignore
         * watermarks, as the ALLOC_NO_WATERMARKS attempt didn't yet happen.
         */
        if (can_direct_reclaim &&
                        (costly_order ||
                           (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
                        && !gfp_pfmemalloc_allowed(gfp_mask)) {
                page = __alloc_pages_direct_compact(gfp_mask, order,
                                                alloc_flags, ac,
                                                INIT_COMPACT_PRIORITY,
                                                &compact_result);
                if (page)
                        goto got_pg;

                /*
                 * Checks for costly allocations with __GFP_NORETRY, which
                 * includes THP page fault allocations
                 */
                if (costly_order && (gfp_mask & __GFP_NORETRY)) {
                        /*
                         * 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 (compact_result == COMPACT_DEFERRED)
                                goto nopage;

                        /*
                         * Looks like reclaim/compaction is worth trying, but
                         * sync compaction could be very expensive, so keep
                         * using async compaction.
                         */
                        compact_priority = INIT_COMPACT_PRIORITY;
                }
        }
  • 코드 라인 10~19에서 다음 3 가지 조건을 동시에 만족시키는 경우 첫 번째 direct-compaction을 수행하고 페이지를 할당한다.
    • direct-reclaim이 허용된 상태의 할당 요청
    • costly high order 할당 요청이거나 movable이 아닌 1 order 이상의 할당 요청
    • 페이지 회수를 위해 일시적으로 할당 요청을 할 때 사용되는 pfmemalloc을 사용하지 않아야 하는 일반 할당 요청
  • 코드 라인 25~43에서 첫 번째 direct-compaction 을 통해서도 페이지 할당이 실패한 상황이다. costly order에 대해 noretry 옵션이 사용된 경우 다시 한 번 시도하기 위해 async compaction을 사용한다. 단 compact 결과가 유예상태인 경우 nopage 레이블로 이동한다.

 

mm/page_alloc.c -3/5-

retry:
        /* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
        if (alloc_flags & ALLOC_KSWAPD)
                wake_all_kswapds(order, gfp_mask, ac);

        reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
        if (reserve_flags)
                alloc_flags = reserve_flags;

        /*
         * Reset the nodemask and zonelist iterators if memory policies can be
         * ignored. These allocations are high priority and system rather than
         * user oriented.
         */
        if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
                ac->nodemask = NULL;
                ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                                        ac->high_zoneidx, ac->nodemask);
        }

        /* Attempt with potentially adjusted zonelist and alloc_flags */
        page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
        if (page)
                goto got_pg;

        /* Caller is not willing to reclaim, we can't balance anything */
        if (!can_direct_reclaim)
                goto nopage;

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

        /* 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;

        /* Try direct compaction and then allocating */
        page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                                        compact_priority, &compact_result);
        if (page)
                goto got_pg;

        /* Do not loop if specifically requested */
        if (gfp_mask & __GFP_NORETRY)
                goto nopage;

        /*
         * Do not retry costly high order allocations unless they are
         * __GFP_RETRY_MAYFAIL
         */
        if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
                goto nopage;

        if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                                 did_some_progress > 0, &no_progress_loops))
                goto retry;
  • 코드 라인 1~4에서 페이지 할당을 다시 시도하기 위한 retry: 레이블이다. kswapd 깨우기를 할 수 있는 상태로 할당 요청된 경우 메모리 상태를 보고 kswapd를 꺠운다.
  • 코드 라인 6~8에서 페이지 회수를 위해 일시적으로 할당 요청을 할 때 사용되는 pfmemalloc을 사용해야 하는 상황인 경우 워터 마크 기준을 사용하지 않도록 할당 플래그를 ALLOC_NO_WATERMARKS로 설정한다.
  • 코드 라인 15~24에서 다음 두 가지 조건 중 하나인 경우 메모리 policies 대로 순회 중인 노드와 존 순서를 무시하고 처음 노드와 존으로 바꾼 후 페이지 할당을 시도한다.
    • cpuset을 사용하는 페이지 할당 요청이 아닌 커널 할당 요청
    • pfmemalloc으로 인해 워터 마크 기준을 사용하지 않아야 하는 할당 요청
  • 코드 라인 27~28에서 atomic 할당 요청과 같이 direct-reclaim이 허용되지 않은 할당 요청인 경우 더 이상 페이지 회수를 하지 못하므로 nopage: 레이블로 이동한다.
  • 코드 라인 31~32에서 페이지 회수를 위해 일시적으로 할당 요청을 할 때 사용되는 pfmemalloc을 사용해야 하는 상황인 경우 재귀 동작이 수행되지 않도록 페이지 회수를 진행하지 않고 nopage: 레이블로 이동한다.
  • 코드 라인 35~38에서 direct-recalim을 시도한다.
  • 코드 라인 41~44에서 두 번째 direct-compaction을 시도한다.
  • 코드 라인 47~48에서 noretry 요청이 있는 경우 nopage: 레이블로 이동한다.
  • 코드 라인 54~55에서 __GFP_RETRY_MAYFAIL 플래그를 사용하지 않는 할당 요청인 경우 costly order에 대해서는 재시도를 하지 않고 nopage: 레이블로 이동한다.
  • 코드 라인 57~59에서 reclaim을 재시도할 필요가 있는 경우 retry: 레이블로 이동하여 할당을 재시도한다.

 

mm/page_alloc.c -4/5-

.       /*
         * It doesn't make any sense to retry for the compaction if the order-0
         * reclaim is not able to make any progress because the current
         * implementation of the compaction depends on the sufficient amount
         * of free memory (see __compaction_suitable)
         */
        if (did_some_progress > 0 &&
                        should_compact_retry(ac, order, alloc_flags,
                                compact_result, &compact_priority,
                                &compaction_retries))
                goto retry;


        /* Deal with possible cpuset update races before we start OOM killing */
        if (check_retry_cpuset(cpuset_mems_cookie, ac))
                goto retry_cpuset;

        /* Reclaim has failed us, start killing things */
        page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
        if (page)
                goto got_pg;

        /* Avoid allocations with no watermarks from looping endlessly */
        if (tsk_is_oom_victim(current) &&
            (alloc_flags == ALLOC_OOM ||
             (gfp_mask & __GFP_NOMEMALLOC)))
                goto nopage;

        /* Retry as long as the OOM killer is making progress */
        if (did_some_progress) {
                no_progress_loops = 0;
                goto retry;
        }
  • 코드 라인 7~11에서 compaction을 재시도할 필요가 있는 경우 retry: 레이블로 이동하여 재시도한다.
  • 코드 라인 15~16에서 cpuset에 변경이 있어 race 상황이 감지되면 retry_cpuset: 레이블로 이동하여 재시도한다.
  • 코드 라인 19~21에서 OOM 킬링을 통해 확보한 페이지로 할당을 다시 시도해본다.
  • 코드 라인 24~27에서 현재 태스크가 OOM으로 인해 특정 태스크가 killing되고 있는 상태이면 nopage: 레이블로 이동한다.
  • 코드 라인 30~33에서 OOM 킬링을 통해 페이지가 회수될 가능성이 있는 경우 retry: 레이블로 이동하여 재시도한다.

 

mm/page_alloc.c -5/5-

nopage:
        /* Deal with possible cpuset update races before we fail */
        if (check_retry_cpuset(cpuset_mems_cookie, ac))
                goto retry_cpuset;

        /*
         * Make sure that __GFP_NOFAIL request doesn't leak out and make sure
         * we always retry
         */
        if (gfp_mask & __GFP_NOFAIL) {
                /*
                 * All existing users of the __GFP_NOFAIL are blockable, so warn
                 * of any new users that actually require GFP_NOWAIT
                 */
                if (WARN_ON_ONCE(!can_direct_reclaim))
                        goto fail;

                /*
                 * PF_MEMALLOC request from this context is rather bizarre
                 * because we cannot reclaim anything and only can loop waiting
                 * for somebody to do a work for us
                 */
                WARN_ON_ONCE(current->flags & PF_MEMALLOC);

                /*
                 * non failing costly orders are a hard requirement which we
                 * are not prepared for much so let's warn about these users
                 * so that we can identify them and convert them to something
                 * else.
                 */
                WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);

                /*
                 * Help non-failing allocations by giving them access to memory
                 * reserves but do not use ALLOC_NO_WATERMARKS because this
                 * could deplete whole memory reserves which would just make
                 * the situation worse
                 */
                page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
                if (page)
                        goto got_pg;

                cond_resched();
                goto retry;
        }
fail:
        warn_alloc(gfp_mask, ac->nodemask,
                        "page allocation failure: order:%u", order);
got_pg:
        return page;
}
  • 코드 라인 1~4에서 페이지를 할당하지 못하고 포기 직전인 nopage: 레이블이다. 만일 cpuset에 변경이 있어 race 상황이 감지되면 retry_cpuset: 레이블로 이동하여 재시도한다.
  • 코드 라인 10~45에서 nofail 옵션을 사용한 경우 페이지가 할당될 때 까지 재시도한다. 단 direct-reclaim 이 허용되지 않는 상황이면 fail 처리한다.

 

GFP 마스크 -> 할당 플래그 변환

gfp_to_alloc_flags()

mm/page_alloc.c

static inline unsigned int
gfp_to_alloc_flags(gfp_t gfp_mask)
{
        unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;

        /* __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 (__GFP_ATOMIC) and ALLOC_HIGH (__GFP_HIGH).
         */
        alloc_flags |= (__force int) (gfp_mask & __GFP_HIGH);

        if (gfp_mask & __GFP_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 (gfp_mask & __GFP_KSWAPD_RECLAIM)
                alloc_flags |= ALLOC_KSWAPD;

#ifdef CONFIG_CMA
        if (gfpflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
                alloc_flags |= ALLOC_CMA;
#endif
        return alloc_flags;
}

@gfp_mask 값으로 할당 플래그를 구성하여 반환한다. 반환되는 할당 플래그와 조건 들은 다음과 같다.

  • ALLOC_WMARK_MIN(0)
    • 디폴트
  • ALLOC_NO_WATERMARKS
    • pfmemalloc 요청인 경우
  • ALLOC_CPUSET
    • atomic 요청이 아닌 경우
  • ALLOC_HIGH
    • high 요청이 있는 경우
  • ALLOC_HARDER
    • rt 태스크 요청인 경우
    • atomic 요청이 있으면서 nomemalloc이 없는 경우
  • ALLOC_CMA
    • movable 페이지 타입인 할당 요청인 경우
  • ALLOC_KSWAPD
    • swapd_reclaim 요청이 있는 경우

 

  • 코드 라인 4에서 할당 플래그에 min 워터마크 할당과 cpuset 사용을 하도록 한다.
  • 코드 라인 15에서 할당 플래그에 high 요청 여부를 추가한다.
  • 코드 라인 17~28에서 atomic 요청인 경우 할당 플래그에서 cpuset을 제거한다. 또한 nomemalloc 요청이 아닌 한 harder 플래그를 추가한다.
  • 코드 라인 29~30에서 rt 태스크에서 요청한 경우에도 harder 플래그를 추가한다.
  • 코드 라인 32~33에서 swapd_reclaim 요청이 있는 경우 kswpd 플래그를 추가한다.
  • 코드 라인 36~37에서 removable 요청이 있는 경우 cma 플래그를 추가한다.

 

gfp 플래그 -> migrate 타입 변환

gfpflags_to_migratetype()

include/linux/gfp.h

static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{
        VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
        BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE);
        BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE);

        if (unlikely(page_group_by_mobility_disabled))
                return MIGRATE_UNMOVABLE;

        /* Group based on mobility */
        return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}

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

  • MIGRATE_UNMOVABLE
  • MIGRATE_RECLAIMABLE
  • MIGRATE_MOVABLE

 

모든 kswapd 깨우기

wake_all_kswapds()

mm/page_alloc.c

static void wake_all_kswapds(unsigned int order, gfp_t gfp_mask,
                             const struct alloc_context *ac)
{
        struct zoneref *z;
        struct zone *zone;
        pg_data_t *last_pgdat = NULL;
        enum zone_type high_zoneidx = ac->high_zoneidx;

        for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, high_zoneidx,
                                        ac->nodemask) {
                if (last_pgdat != zone->zone_pgdat)
                        wakeup_kswapd(zone, gfp_mask, order, high_zoneidx);
                last_pgdat = zone->zone_pgdat;
        }
}

high_zoneidx 이하의 zonelist에서 high_zoneidx 이하의 zone이면서 노드 마스크에 설정된 노드들인 존을 순회하며 해당 노드의 kswapd를 모두 깨운다.

 

reclaim 재시도 필요 체크

should_reclaim_retry()

mm/page_alloc.c

/*
 * Checks whether it makes sense to retry the reclaim to make a forward progress
 * for the given allocation request.
 *
 * We give up when we either have tried MAX_RECLAIM_RETRIES in a row
 * without success, or when we couldn't even meet the watermark if we
 * reclaimed all remaining pages on the LRU lists.
 *
 * Returns true if a retry is viable or false to enter the oom path.
 */
static inline bool
should_reclaim_retry(gfp_t gfp_mask, unsigned order,
                     struct alloc_context *ac, int alloc_flags,
                     bool did_some_progress, int *no_progress_loops)
{
        struct zone *zone;
        struct zoneref *z;
        bool ret = false;

        /*
         * Costly allocations might have made a progress but this doesn't mean
         * their order will become available due to high fragmentation so
         * always increment the no progress counter for them
         */
        if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
                *no_progress_loops = 0;
        else
                (*no_progress_loops)++;

        /*
         * Make sure we converge to OOM if we cannot make any progress
         * several times in the row.
         */
        if (*no_progress_loops > MAX_RECLAIM_RETRIES) {
                /* Before OOM, exhaust highatomic_reserve */
                return unreserve_highatomic_pageblock(ac, true);
        }

        /*
         * Keep reclaiming pages while there is a chance this will lead
         * somewhere.  If none of the target zones can satisfy our allocation
         * request even if all reclaimable pages are considered then we are
         * screwed and have to go OOM.
         */
        for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
                                        ac->nodemask) {
                unsigned long available;
                unsigned long reclaimable;
                unsigned long min_wmark = min_wmark_pages(zone);
                bool wmark;

                available = reclaimable = zone_reclaimable_pages(zone);
                available += zone_page_state_snapshot(zone, NR_FREE_PAGES);

                /*
                 * Would the allocation succeed if we reclaimed all
                 * reclaimable pages?
                 */
                wmark = __zone_watermark_ok(zone, order, min_wmark,
                                ac_classzone_idx(ac), alloc_flags, available);
                trace_reclaim_retry_zone(z, order, reclaimable,
                                available, min_wmark, *no_progress_loops, wmark);
                if (wmark) {
                        /*
                         * If we didn't make any progress and have a lot of
                         * dirty + writeback pages then we should wait for
                         * an IO to complete to slow down the reclaim and
                         * prevent from pre mature OOM
                         */
                        if (!did_some_progress) {
                                unsigned long write_pending;

                                write_pending = zone_page_state_snapshot(zone,
                                                        NR_ZONE_WRITE_PENDING);

                                if (2 * write_pending > reclaimable) {
                                        congestion_wait(BLK_RW_ASYNC, HZ/10);
                                        return true;
                                }
                        }

                        ret = true;
                        goto out;
                }
        }

out:
        /*
         * Memory allocation/reclaim might be called from a WQ context and the
         * current implementation of the WQ concurrency control doesn't
         * recognize that a particular WQ is congested if the worker thread is
         * looping without ever sleeping. Therefore we have to do a short sleep
         * here rather than calling cond_resched().
         */
        if (current->flags & PF_WQ_WORKER)
                schedule_timeout_uninterruptible(1);
        else
                cond_resched();
        return ret;
}

회수 가능한 페이지와 남은 free 페이지를 확인하여 reclaim 시도를 계속할 지 여부를 체크한다.( true=계속, false=스탑) costly high order 초과 요청 시 MAX_RECLAIM_RETRIES(16)번 이내에서 반복 가능하며 마지막 시도 시에는 atomic 요청으로 high order 처리를 위해 남겨둔(reserve) highatomic 타입의 free 페이지를 모두 요청한 페이지 타입으로 변경하여 활용하도록 한다.

  • 코드 라인 15~27에서 이 함수 호출 전에 수행한 direct-reclaim에서 페이지 회수가 있었고 high order를 초과하는 요청인 경우 direct-reclaim을 재시도한 회수를 저장하기 위해 출력 인자 no_progress_loops를 1 증가 시킨다. 이 값이 최대 reclaim 시도 수를 초과하면 OOM 직전의 상황이므로 atomic 요청으로 high order 처리를 위해 남겨둔(reserve) highatomic 타입의 free 페이지를 모두 회수하여 요청한 페이지 타입으로 변환한다. 기존 direct-reclaim 처리 시 회수한 페이지가 하나도 없거나, costly high order 이하의 요청인 경우에는 retry 카운터를 0으로 지정한다.
  • 코드 라인 35~43에서 zonelist에서 노드 마스크를 대상으로하는 high_zoneidx 이하의 존을 순회하며 해당 존의 최대 회수 가능한 페이지를 더한 free 페이지 수를 산출한다.
  • 코드 라인 49~74에서 availble 페이지 수가 min 워터마크 기준을 상회하는 경우 reclaim을 다시 시도할 수 있도록 true를 반환하기 위해 out: 레이블로 이동한다. 만일 기존 시도 했던 reclaim 페이지 수가 0이고 회수 가능한 페이지의 50% 이상이 write 지연 상태이면 곧바로 재시도해도 페이지 회수 가능성이 낮기 때문에 0.1초간 대기한 후 true를 곧바로 반환한다.
  • 코드 라인 77~89에서 out: 레이블에서는 함수를 빠져나가기 전에 슬립 여부를 결정한다. 워커 스레드에서 페이지 할당을 요청한 경우 congestion 상태에서 슬립없이 루프를 도는 일이 발생하므로 최소 1틱을 쉬며 다른 태스크에게 실행을 양보한다. 그 외의 경우는 premption 요청이 있으면 슬립하고 그렇지 않으면 슬립하지 않고 곧바로 함수를 빠져나간다.

 

compaction 재시도 필요 체크

should_compact_retry()

mm/page_alloc.c

static inline bool
should_compact_retry(struct alloc_context *ac, int order, int alloc_flags,
                     enum compact_result compact_result,
                     enum compact_priority *compact_priority,
                     int *compaction_retries)
{
        int max_retries = MAX_COMPACT_RETRIES;
        int min_priority;
        bool ret = false;
        int retries = *compaction_retries;
        enum compact_priority priority = *compact_priority;

        if (!order)
                return false;

        if (compaction_made_progress(compact_result))
                (*compaction_retries)++;

        /*
         * compaction considers all the zone as desperately out of memory
         * so it doesn't really make much sense to retry except when the
         * failure could be caused by insufficient priority
         */
        if (compaction_failed(compact_result))
                goto check_priority;

        /*
         * make sure the compaction wasn't deferred or didn't bail out early
         * due to locks contention before we declare that we should give up.
         * But do not retry if the given zonelist is not suitable for
         * compaction.
         */
        if (compaction_withdrawn(compact_result)) {
                ret = compaction_zonelist_suitable(ac, order, alloc_flags);
                goto out;
        }

        /*
         * !costly requests are much more important than __GFP_RETRY_MAYFAIL
         * costly ones because they are de facto nofail and invoke OOM
         * killer to move on while costly can fail and users are ready
         * to cope with that. 1/4 retries is rather arbitrary but we
         * would need much more detailed feedback from compaction to
         * make a better decision.
         */
        if (order > PAGE_ALLOC_COSTLY_ORDER)
                max_retries /= 4;
        if (*compaction_retries <= max_retries) {
                ret = true;
                goto out;
        }

        /*
         * Make sure there are attempts at the highest priority if we exhausted
         * all retries or failed at the lower priorities.
         */
check_priority:
        min_priority = (order > PAGE_ALLOC_COSTLY_ORDER) ?
                        MIN_COMPACT_COSTLY_PRIORITY : MIN_COMPACT_PRIORITY;

        if (*compact_priority > min_priority) {
                (*compact_priority)--;
                *compaction_retries = 0;
                ret = true;
        }
out:
        trace_compact_retry(order, priority, compact_result, retries, max_retries, ret);
        return ret;
}

compaction을 재시도할 필요가 있는지 여부를 반환한다. (true=재시도 필요, false=재시도 불필요)

  • 코드 라인 13~14에서 0 오더 할당 요청에 대해서는 compaction 시도가 필요 없으므로 false를 반환한다.
  • 코드 라인 16~17에서 지난 compaction 과정에서 migrate한 페이지가 있는 경우 compaction_retries 카운터를 증가시킨다.
  • 코드 라인 24~25에서 지난 compaction 과정이 완전히 완료된 경우 check_priority: 레이블로 이동한다.
  • 코드 라인 33~36에서 지난 compaction 과정이 몇 가지 이유로 완료되지 않은 경우이다. 다시 compaction을 시도해도 적절할지 판단 여부를 반환한다.
  • 코드 라인 46~51에서 최대 compaction 재시도 수 만큼 반복하기 위해 true를 반환한다. 단 costly high order를 초과하는 할당 요청인 경우 16번의 재시도를 1/4로 줄여 4번까지만 재시도하게 한다.
  • 코드 라인 57~65에서 check_priority: 레이블이다. compact 우선 순위가 높은 경우에 대해 재시도 기회를 더 부여하고자 한다. 따라서 compact 우선순위가 최소 priority를 초과한 경우 compact 우선 순위를 1 저하시킨 후 재시도 수를 0으로 리셋하고 true를 반환한다.

 

OOM 킬링을 통한 페이지 할당

__alloc_pages_may_oom()

mm/page_alloc.c

static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
        const struct alloc_context *ac, unsigned long *did_some_progress)
{
        struct oom_control oc = {
                .zonelist = ac->zonelist,
                .nodemask = ac->nodemask,
                .memcg = NULL,
                .gfp_mask = gfp_mask,
                .order = order,
        };
        struct page *page;

        *did_some_progress = 0;

        /*
         * Acquire the oom lock.  If that fails, somebody else is
         * making progress for us.
         */
        if (!mutex_trylock(&oom_lock)) {
                *did_some_progress = 1;
                schedule_timeout_uninterruptible(1);
                return NULL;
        }

        /*
         * Go through the zonelist yet one more time, keep very high watermark
         * here, this is only to catch a parallel oom killing, we must fail if
         * we're still under heavy pressure. But make sure that this reclaim
         * attempt shall not depend on __GFP_DIRECT_RECLAIM && !__GFP_NORETRY
         * allocation which will never fail due to oom_lock already held.
         */
        page = get_page_from_freelist((gfp_mask | __GFP_HARDWALL) &
                                      ~__GFP_DIRECT_RECLAIM, order,
                                      ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);
        if (page)
                goto out;

        /* Coredumps can quickly deplete all memory reserves */
        if (current->flags & PF_DUMPCORE)
                goto out;
        /* The OOM killer will not help higher order allocs */
        if (order > PAGE_ALLOC_COSTLY_ORDER)
                goto out;
        /*
         * We have already exhausted all our reclaim opportunities without any
         * success so it is time to admit defeat. We will skip the OOM killer
         * because it is very likely that the caller has a more reasonable
         * fallback than shooting a random task.
         */
        if (gfp_mask & __GFP_RETRY_MAYFAIL)
                goto out;
        /* The OOM killer does not needlessly kill tasks for lowmem */
        if (ac->high_zoneidx < ZONE_NORMAL)
                goto out;
        if (pm_suspended_storage())
                goto out;
        /*
         * XXX: GFP_NOFS allocations should rather fail than rely on
         * other request to make a forward progress.
         * We are in an unfortunate situation where out_of_memory cannot
         * do much for this context but let's try it to at least get
         * access to memory reserved if the current task is killed (see
         * out_of_memory). Once filesystems are ready to handle allocation
         * failures more gracefully we should just bail out here.
         */

        /* The OOM killer may not free memory on a specific node */
        if (gfp_mask & __GFP_THISNODE)
                goto out;

        /* Exhausted what can be done so it's blame time */
        if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) {
                *did_some_progress = 1;

                /*
                 * Help non-failing allocations by giving them access to memory
                 * reserves
                 */
                if (gfp_mask & __GFP_NOFAIL)
                        page = __alloc_pages_cpuset_fallback(gfp_mask, order,
                                        ALLOC_NO_WATERMARKS, ac);
        }
out:
        mutex_unlock(&oom_lock);
        return page;
}

OOM 킬링을 통해 페이지 확보를 시도한다. 이를 통해 확보가 가능할 수 있는 경우 did_some_progress에 1이 출력된다.

  • 코드 라인 20~24에서 oom lock 획득이 실패하는 경우 1 틱 동안 스케줄하여 다른 태스크에게 실행을 양보한다.
  • 코드 라인 33~37에서 hardwall 및 cpuset 추가하고, direct-reclaim은 제외한 상태로 high 워터마크를 기준으로 페이지 할당을 다시 한 번 시도한다.
    • OOM 킬링이 발생하면서 메모리 압박이 풀릴 가능성이 있기 때문이다.
  • 코드 라인 40~41에서 이미 현재 태스크가 코어 덤프 중인 경우 out: 레이블로 이동한다.
  • 코드 라인 43~44에서 costly high order를 초과한 요청인 경우 OOM 킬러로 극복하지 못하므로 out 레이블로 이동한다.
  • 코드 라인 51~52에서 __GFP_RETRY_MAYFAIL 요청인 경우 이미 많은 reclaim 기회를 다 소진하였고, 합리적인 fallback을 가질 가능성이 있기 때문에 OOM 킬링을 skip 하기위해 out:레이블로 이동한다.
    • migrate_pages() -> new_page()를 통해 할당 요청 시 __GFP_RETRY_MAYFILE이 사용된다.
  • 코드 라인 54~55에서 DMA32 이하의 존에서 할당을 요청한 경우 OOM 킬링을 skip 하기 위해 out:레이블로 이동한다.
  • 코드 라인 56~57에서 io 및 fs를 사용하지 못하는 경우 OOM 킬링을 skip 하기 위해 out: 레이블로 이동한다.
  • 코드 라인 69~70에서 로컬 노드에서만 할당하라는 요청인 경우 OOM 킬링을 skip 하기 위해 out: 레이블로 이동한다.
  • 코드 라인 73~83에서 OOM 킬링을 수행하여 페이지 회수가 되었거나 nofail 옵션을 사용한 경우 출력 인수 did_some_progress에 1을 대입한다. 만일 nofail 옵션이 사용된 경우 cpuset fallback을 통해 할당을 시도해본다.
  • 코드 라인 84~86에서 out: 레이블에서는 oom 락을 풀고 페이지를 반환한다.

 

OOM 킬링 후 cpuset fallback

__alloc_pages_cpuset_fallback()

mm/page_alloc.c

static inline struct page *
__alloc_pages_cpuset_fallback(gfp_t gfp_mask, unsigned int order,
                              unsigned int alloc_flags,
                              const struct alloc_context *ac)
{
        struct page *page;

        page = get_page_from_freelist(gfp_mask, order,
                        alloc_flags|ALLOC_CPUSET, ac);
        /*
         * fallback to ignore cpuset restriction if our nodes
         * are depleted
         */
        if (!page)
                page = get_page_from_freelist(gfp_mask, order,
                                alloc_flags, ac);

        return page;
}

alloc 플래그에 cpuset을 적용한 후 페이지 할당을 먼저 해본 후 할당이 실패하면 cpuset을 제외하고 다시 할당을 시도한다.

 

참고

 

 

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

<kernel v5.0>

버디 시스템의 구조

버디 시스템이라 불리는 버디 페이지 할당자는 페이지 단위의 메모리 할당과 해제를 수행한다. 버디 시스템은 연속으로 할당 가능한 페이지를 2의 승수 단위로 관리한다. 페이지들은 2^0 = 1페이지부터 2^(MAX_ORDER – 1)까지 각 order 슬롯으로 나누어 관리한다.

 

페이지 할당 order

페이지 할당자에서 사용하는 버디 시스템에서는 오더(order)라는 용어로 페이지 할당을 요청한다. 이것은 2의 제곱승 단위로만 요청을 할 수 있음을 의미한다. 예를 들어, 오더 3에 해당하는 페이지를 요청하는 경우 2^3 = 8페이지를 요청하는 것이다.

 

다음 그림은 버디 시스템이 할당 요청 받을 수 있는 order 페이지들을 보여준다.

  • 0x1234_5000 ~ 0x1236_2000 사이에 연속된 9개의 fee 페이지가 존재하지만, order 단위로 align되어 관리하는 버디 시스템에서 할당 가능한 페이지들은 다음과 같다.
    • 0 order 페이지 1개
    • 1 order 페이지 2개
    • 2 order 페이지 1개

 

MAX_ORDER

버디 시스템에서 한 번에 최대 할당 가능한  페이지 수는 2^(MAX_ORDER-1) 페이지이다.

  • 예) PAGE_SIZE=4K, MAX_ORDER=11인 경우 한 번에 최대 할당 가능한 페이지는 1024 페이지이고 바이트로는 4M이다.
    • 2^0, 2^1, 2^2, … 2^10 페이지
    • 4K, 8K, 16K, …, 4M 페이지

 

다음 그림은 페이지를 관리하는 order 슬롯이 0 부터 최대 MAX_ORDER-1 까지 free 페이지들을 관리하고 있는 모습을 보여준다.

  • 각 free 메모리에 대한 page 구조체들 중 head에 해당하는 page 구조체가 대표 페이지이며 리스트에 연결된다.

 

ARM32 및 ARM64 커널의 디폴트 설정으로 MAX_ORDER는 11로 정의되어 있다. 또한 CONFIG_FORCE_MAX_ZONEORDER 커널 옵션을 사용하여 크기를 바꿀 수 있다. 각 오더 슬롯 또한 단편화되지 않도록 관리하기 위해 다음과 같은 구조로 이루어져 있다.

  • 같은 mobility 속성을 가진 페이지들끼리 가능하면 뭉쳐 있도록 각 오더 슬롯은 마이그레이션 타입별로 나누어 관리한다. 이렇게 나누어 관리함으로써 페이지 회수 및 메모리 컴팩션(compaction) 과정에서 효율을 높일 수 있다. 특별히 MIGRATE_MOVABLE 타입으로만 구성된 ZONE_MOVABLE 영역을 만들 수도 있다.
  • 각 페이지를 담는 free_list에서 free 페이지들은 짝(버디)을 이루어 2개의 짝(버디)이 모이면 더 큰 오더로 합병되어 올라가고 필요시 분할하여 하나 더 적은 오더로 나뉠 수 있다. 이제 더 이상 짝(버디)을 관리할 때 map이라는 이름의 bitmap을 사용하지 않고 free_list라는 이름의 리스트와 페이지 정보만을 사용하여 관리한다.
  • free_list는 선두 방향으로 hot 속성을 갖고 후미 방향으로 cold 속성을 갖는다. hot, cold 속성은 각각 리스트의 head와 tail의 위치로 대응하여 관리된다. 앞부분에 놓인 페이지들은 다시 할당되어 사용될 가능성이 높은 페이지다. 뒷부분에 놓인 페이지들은 오더가 통합되어 점점 상위 오더로 올라갈 가능성이 높은 페이지다. 이를 통해 free 페이지의 단편화 방지에 도움을 주고 캐시의 지속성을 높여 성능을 올리는 효과도 있다.

 

버디 시스템의 관리 기법이 계속 발전하면서 복잡도는 증가하고 있지만, 최대한 버디 시스템의 효율(비단편화)을 높이는 쪽으로 발전하고 있다. 그림 4-41은 버디 메모리 할당자의 코어 부분을 보여준다.

 


페이지 블록과 mobility(migrate) 속성

메모리를 페이지 블록 단위로 나누어 페이지 블록마다 4비트를 사용한 비트맵으로 mobility 속성을 표현한다. 첫 3비트를 사용하여 각 메모리 블록을 마이그레이션 타입으로 구분하여 mobility 속성을 관리한다. 페이지 블록은 2pageblock_order만큼 페이지를 관리하고 그 페이지들이 가장 많이 사용하는 mobility 속성을 메모리 블록에서 대표 mobility 속성으로 기록하여 관리한다. 나머지 1비트를 사용하여 컴팩션 기능에 의해 스킵할 수 있도록 한다.

 

migrate 타입

페이지 타입이라고도 불린다. 각 order 페이지는 각각 mobility 속성을 표현하기 위해 migrate 타입을 갖고 있으며, 가능하면 같은 속성을 가진 페이지들끼리 뭉쳐 있도록 하여 연속된 메모리의 단편화를 억제한다. 최대한 커다랗고 연속된 free 메모리를 유지하고자 하는 목적으로 버디 시스템에 설계되었다. 버디 시스템의 free_list는 다음과 같은 migrate 타입별로 관리된다. 단 migrate 타입별로 1 개 이상의 페이지 블럭을 확보하지 못하는 메모리가 극히 적은 시스템(수M ~ 수십M)에서는 모든 페이지를 unmovable로 구성한다.

참고로 버디의 pcp 캐시는 아래 타입 중 가장 많이 사용되는 아래 3가지 타입만을 사용하여 관리한다.

  •  MIGRATE_UNMOVABLE
    • 이동과 메모리 회수가 불가능한 타입이다.
    • 용도
      • 커널에서 할당한 페이지, 슬랩, I/O 버퍼, 커널 스택, 페이지 테이블 등에 사용되는 타입이다.
  • MIGRATE_MOVABLE
    • 연속된 큰 메모리가 필요한 경우, 현재 사용되는 페이지를 이동시켜 최대한 단편화를 막기 위해 사용되는 타입이다.
    • 용도
      • 유저(file, anon) 메모리 할당 시 사용된다.
  • MIGRATE_RECLAIMABLE
    • 이동은 불가능하지만 메모리 부족 시 메모리 회수가 가능한 경우에 사용되는 타입이며, 자주 사용하는 타입이 아니다.
    • 용도
      • __GFP_RECLAIMABLE 플래그를 특별히 지정하여 생성한 슬랩 캐시인 경우 이 타입으로 이용한다
  •  MIGRATE_HIGHATOMIC
    • high order 페이지 할당에 대해 atomic 할당 요청 시 실패될 확률을 줄이기 위해 커널은 이 유형의 페이지 타입을 1 블럭씩 미리 준비하고 있다. 이 타입의 메모리는 최대 메모리의 1% 범위까지 확장될 수 있다.
    • 용도
      • 주로 RT 스케줄러를 사용하는 커널 스레드나 인터럽트 핸들러 등에서 메모리 할당 시 GFP_ATOMIC을 사용하여 메모리 할당을 요청하면, 슬립을 일으킬 수 있는 페이지 회수(reclaim) 없이 처리해야 한다. 그런데 high order 페이지 할당 요청을 하는 경우 페이지 회수 없이 처리하다 보면 메모리가 충분한 경우에도 high order 페이지가 하나도 없어 OOM(Out Of Memory)이 발생할 수 있다. 이러한 경우를 위해 미리 reserve된 MIGRATE_HIGHATOMIC 타입의 페이지를 할당하여 위기를 넘길 수 있다.
    • 커널 4.4-rc1에서 MIGRATE_RESERVE 타입이 삭제되었고 대신 high-order atomic allocation을 지원하기 위해 MIGRATE_HIGHATOMIC이 추가되었다.
  • MIGRATE_CMA
    • CMA 메모리 할당자가 별도로 관리하는 페이지 타입이다. CMA 영역을 버디 시스템에 구성하는 경우 이 영역은 movable 페이지도 할당될 수 있다. CMA 요청 시 이 영역이 부족하면 movable 페이지를 다른 영역으로 이동시킨다. CMA 페이지로 할당되면 각 페이지들의 할당 및 관리는 별도로 CMA 메모리 할당자에서 수행한다.
    • 용도
      • 커널이 DMA 용도 등으로 사용하기 위한 메모리 할당 시 사용된다.
  • MIGRATE_ISOLATE
    • 커널이 특정 범위의 사용 중인 movable 페이지들을 다른 곳으로 migration 하기 위해 잠시 이 타입으로 변경하여 관리한다. 그리고 이 타입으로 있는 페이지들에 대해 버디 시스템은 절대 사용하지 않는다.
    • 용도
      • CMA 영역의 메모리가 부족한 상황에서 free 페이지를 확보하기 위해 CMA 영역에 있는 movable 페이지들을 CMA 영역의 밖으로 이동시켜 확보할 때 사용된다.
      • 메모리 hot-remove를 위해 해당 영역의 사용 중인 movable 페이들을 다른 곳으로 옮길 때 사용된다.

 

다음 그림은 각 order 슬롯마다 6개의 migrate type 별로 free 페이지를 관리하는 리스트의 모습을 보여준다.

 

page_blockorder

매크로로 정의된 pageblock_order는 페이지 블록을 구성하는 페이지의 개수를 승수 단위로 표현한다.

 

ARM64 커널에서는 huge 페이지를 지원하며 huge 페이지 단위에 맞춰 사용한다. huge 페이지를 사용하지 않는 경우는 버디 시스템이 사용하는 최대 페이지 크기에 맞춰사용한다.

  • 예) 4K 페이지 및 huge 페이지 사용 시 pageblock_order=9
  • 예) 4K 페이지 및 huge 페이지 미사용 시 pageblock_order=10

 

include/linux/pageblock-flags.h

#define pageblock_order         HUGETLB_PAGE_ORDER

 

ARM32 커널은 버디 시스템이 사용하는 최대 페이지 크기에 맞춰사용한다.

  • 예) 4K 페이지 사용 시 pageblock_order=10

 

include/linux/pageblock-flags.h

#define pageblock_order (MAX_ORDER-1)

 

다음 ARM64 시스템은 pageblock_order가 9로 설정되어 있음을 알 수 있고, 각 노드, zone 및 order, migrate 타입별 버디에서 관리되고 있는 free 페이지 수를 보여준다. 또한 각 노드, zone 및 migrate 타입별 페이지 블럭 수를 보여준다.

$ cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone    DMA32, type    Unmovable      0      0    168    322    108      6      2      0      0      1      0
Node    0, zone    DMA32, type      Movable      0      0     31      9      3      4      0      0      0      0    412
Node    0, zone    DMA32, type  Reclaimable      0      0      1      0      0      1      1      1      1      0      0
Node    0, zone    DMA32, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone    DMA32, type          CMA      0      0      0      0      0      0      1      1      1      1      7
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0

Number of blocks type     Unmovable      Movable  Reclaimable   HighAtomic          CMA      Isolate
Node 0, zone    DMA32          232         1250           38            0           16            0

Number of mixed blocks    Unmovable      Movable  Reclaimable   HighAtomic          CMA      Isolate
Node 0, zone    DMA32            0            1            1            0            0            0

 

다음 ARM32 시스템은 pageblock_order가 10으로 설정되어 있음을 알 수 있다.

$ cat /proc/pagetypeinfo
Page block order: 10
Pages per block:  1024

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone   Normal, type    Unmovable     10     13      9      6      2      2      0      1      0      1      0
Node    0, zone   Normal, type  Reclaimable      1      2      2      3      0      0      1      0      1      0      0
Node    0, zone   Normal, type      Movable      6      3      1      1    447    109      4      0      0      1    147
Node    0, zone   Normal, type      Reserve      0      0      0      0      0      0      0      0      0      0      2
Node    0, zone   Normal, type          CMA      1      1      1      2      1      2      1      0      1      1      0
Node    0, zone   Normal, type      Isolate      0      0      0      0      0      0      0      0      0      0      0

Number of blocks type     Unmovable  Reclaimable      Movable      Reserve          CMA      Isolate
Node 0, zone   Normal            4            5          223            2            2            0
  • rpi2의 경우 MAX_ORDER=11로 설정되어 있고 pageblock_order 역시 MAX_ORDER-1로 설정되어 있는 것을 알 수 있다.
    • 커널 버전에 따라 MAX_ORDER값이 다르다. 기존에는 9, 10 등이 사용되었었다.

 

다음 그림은 각 pageblock에 포함된 order 페이지들의 migrate 타입이 하나로 유지된 것과, 그렇지 않고 여러 개가 mix된 사례를 보여준다.

  • 커널은 가능하면 같은 블럭내에서 한 가지 migrate 타입을 유지하려 하지만 메모리 부족 등의 상황에서 migrate 타입이 섞일 수도 있다.
  • migrate 타입이 섞이지 않게 관리하면 파편화되지 않을 가능성이 높아진다.

 

pageblock 별 대표 migrate type

order 페이지들이 migrate 타입으로 관리되는 것과 동일하게, pageblock들도 각각의 migrate 타입을 관리한다. 만일 pageblock 내에서 여러 개의 migrate 타입을 사용하는 order 페이지들이 섞여 사용 중인 경우 그 들중 가장 많이 사용되는 migrate 타입을 해당 블럭의 대표 migrate 타입으로 지정한다. 이러한 대표 migrate 타입 3 비트와 compaction에서 사용하는 1 비트의 skip 비트를 추가하여 usemap에 저장한다.

 

usemap 각 비트의 용도
  • 대표 migrate 타입(3 bits)
    • 파편화를 회피하기 위해 페이지 할당 시 요청한 migrate 타입과 동일한 대표 migrate 타입이 있는 pageblock에서 할당하려고 노력한다.
  • skip 비트(1bits)
    • 메모리 부족 시compaction을 수행할 때 compaction을 skip 하기 위한 비트이다.

 

다음 그림은 usemap에 표현된 각 pageblock 별 대표 migrate 타입을 보여준다.

 

PF_MEMALLOC

메모리 회수와 관련한 특수한 커널 스레드 또는 페이지 회수 시 잠시 페이지 할당을 해야 하는 스레드에서 사용하는 플래그 비트이다. 워터마크 기준 이하로 메모리가 부족해진 경우 페이지 회수 시스템이 동작한다. 이 때 페이지를 회수하는 과정에서 네트웍을 이용한 Swap 등을 포함한 몇 개의 특수한 페이지 회수 요청에서 오히려 페이지를 잠시 할당해야 하는 경우가 있다. 예를 들어 페이지 부족 시 네트워크를 사용하여 swap을 해야 할 때 네트웍 처리 시 필요한 skb(소켓버퍼)등이 할당되어야 한다. 이렇게  메모리 할당 요청을 할 때 다시 메모리 부족에 대한 페이지 회수 시스템이 동작하는 등을 반복하게 되는 문제가 있다. 따라서 이러한 재귀적인 문제가 발생하지 않도록 특수한 목적으로 페이지 할당을 해야 하는 경우에 “메모리 부족을 해결하기 위한 임시 메모리 할당 요청”임을 식별하게 하는 PF_MEMALLOC 플래그를 사용한다. 이 플래그를 사용하면 다음과 같은 동작을 수행하게 한다.

  • 워터마크 기준 이하의 메모리도 할당한다.
  • 페이지 회수가 반복되지 않도록 모든 종류의 페이지 회수를 다시 요청하지 않는다.

 

PF_MEMALLOC 플래그와 같이 메모리가 부족한 상황에서 임시 메모리를 할당하기 위해 호출될 때 사용하기 위해 아래 두 개의 플래그가 추가되었다.

PF_MEMALLOC_NOIO
  • 워터마크 기준 이하의 메모리도 할당한다.
  • IO 요청을 동반한 페이지 회수가 반복되지 않게 한다. 즉 IO 요청이 아닌 메모리 내에서만 동작하는 페이지 회수는 동작시킬 수 있다.

 

PF_MEMALLOC_NOFS
  • 워터마크 기준 이하의 메모리도 할당한다.
  • 파일 시스템을 이용하는 페이지 회수가 반복되지 않게 한다. 즉 파일 시스템이 아닌 메모리나 다른 종류의 IO를 사용하는 페이지 회수는 동작시킬 수 있다.
  • 참고: mm: introduce memalloc_nofs_{save,restore} API

 


페이지 할당자 구조

 

다음 그림은 페이지 할당자를 구성하는 주요 항목들을 보여준다.

  • 노드별
    • NUMA 메모리 정책에 따른 노드 및 zonelist
    • 페이지 회수 매커니즘
  • 존별
    • 버디 코어(심장)
    • 버디 캐시(pcp)

 

페이지 할당 Sequence

페이지 할당자는 크게 다음과 같은 루틴들을 통해 할당된다.

  • 가장 먼저 NUMA 메모리 정책을 통해 대상이 되는 노드 또는 노드들을 정한다.
  • Memory Control Group 통제 내에서 할당된다.
  • 버디 시스템을 통해 할당을 수행한다.
    • 1 페이지(0-order) 할당 요청인 경우 버디 캐시 시스템인 pcp를 사용하여 할당한다.
    • 메모리 부족 시 인터럽트 여부 또는 요청한 플래그 옵션에 따라 메모리 회수를 동반할 수 있다.

 

GFP 마스크(gfp_mask)

페이지 할당 요청 시 사용되는 플래그들이다.

 

할당 플래그(alloc_flags)들

페이지 할당 함수에서 gfp_mask와 별도로 사용되며, 함수 내부(internal) 용도로 사용되는 할당 플래그들이다.

  • ALLOC_WMARK_MIN
    • min 워터마크 이상에서만 페이지를 할당하도록 제한한다.
  • ALLOC_WMARK_LOW
    • low 워터마크 이상에서만 페이지를 할당하도록 제한한다.
  • ALLOC_WMARK_HIGH
    • high 워터마크 이상에서만 페이지를 할당하도록 제한한다.
  • ALLOC_HARDER
    • 보통 RT 스케줄러를 사용하는 커널 스레드 또는 인터럽트 핸들러에서 GFP_ATOMIC gfp 플래그 옵션에 포함된 __GFP_ATOMIC gfp 플래그 옵션에 의해 이 할당 플래그가 설정되는데, high order 페이지 할당 시 실패하면 MIGRATE_HIGHATOMIC 타입의 free 페이지를 할당할 수 있게 최대한 노력하여 할당 시도를 한다.
  • ALLOC_HIGH
    • 보통 RT 스케줄러를 사용하는 커널 스레드 또는 인터럽트 핸들러에서 GFP_ATOMIC gfp 플래그 옵션에 포함된 __GFP_HIGH gfp 플래그 옵션에 의해 이 할당 플래그가 설정되면 최대한 높은 우선 순위에서 처리되도록 요청한다.
  • ALLOC_CPUSET
    • 유저 태스크가 요청하는 메모리를 cgroup의 cpuset 서브시스템을 사용하여 제한한다.
  • ALLOC_CMA
    • movable 페이지인 경우 CMA 영역도 사용할 수  있게 허가한다.
  • ALLOC_NOFRAGMENT
    • 페이지 블럭내에서 migrate 타입이 섞이지 않도록 요청한다.
  • ALLOC_KSWAPD
    • 메모리 부족 시 kswapd를 깨울 수 있다.

 

ALLOC_FAIR

ALLOC_FAIR 플래그를 사용한 fair zone 정책은 NUMA 시스템의 메모리 정책을 사용하면서 불필요하게 되어 커널 v4.8-rc1에서 제거되었다.

 

NUMA 메모리 정책(Policy)

 


물리 페이지 할당(alloc)

 

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

 

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

버디 시스템을 통해 연속된 2^order 페이지들을 할당받는다. NUMA 시스템을 사용하는 경우 NUMA 메모리 정책을 반영하기 위해 alloc_pages_current( ) 함수를 통해 노드가 선택되고, 그 후 그 함수 내부에서 alloc_pages_node( ) 함수를 호출하여 페이지를 할당받는다.

 

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.
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
        struct mempolicy *pol = &default_policy;
        struct page *page;

        if (!in_interrupt() && !(gfp & __GFP_THISNODE))
                pol = get_task_policy(current);

        /*
         * 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_node(gfp, pol, numa_node_id()),
                                policy_nodemask(gfp, pol));

        return page;
}
EXPORT_SYMBOL(alloc_pages_current);

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

  • 코드 라인 3~7에서 인터럽트 처리중이거나 현재 노드에서만 할당하라는 요청인 경우에는 디폴트 메모리 정책을 선택한다. 그 외의 경우에는 현재 태스크에 주어진 메모리 정책을 선택한다. 만일 태스크에도 메모리 정책이 설정되지 않은 경우 노드를 지정한 경우 해당 노드에 우선 처리되는 메모리 정책을 사용한다. 노드가 지정되지 않은 경우에는 디폴트 메모리 정책을 선택한다.디폴트 메모리 정책은 로컬 노드를 사용하여 할당한다.
    • __GFP_THISNODE 플래그를 사용하여 로컬 노드로 제한한 경우 -> 디폴트 메모리 정책 사용
    • 인터럽트 중 -> 디폴트(로컬 노드 preferred) 메모리 정책 사용
    • 태스크에 지정된 정책
      • 태스크에 지정된 정책이 있으면 -> 태스크에 지정된 메모리 정책
      • 지정된 노드가 있으면 -> 노드에 지정된 우선 메모리 정책 사용
      • 지정된 노드가 없으면 -> 디폴트(로컬 노드 preferred) 메모리 정책
  • 코드 라인 13~14에서 인터리브 메모리 정책을 사용하는 경우 페이지를 노드별로 돌아가며 할당하게 한다.
  • 코드 라인 15~16에서 그 외의 정책을 사용하는 경우 요청한 노드 제한내에서 order 페이지를 할당 받는다.

 

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

지정된 노드에서 연속된 2^order 페이지들을 할당 받는다. 만일 알 수 없는 노드가 지정된 경우 현재 노드에서 할당 받는다.

 

__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에서 2^order 페이지 만큼 연속된 물리메모리를 할당 받는다.

 


버디 할당자의 심장

지정 노드들에서 페이지 할당하기

__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, int preferred_nid,
                                                        nodemask_t *nodemask)
{
        struct page *page;
        unsigned int alloc_flags = ALLOC_WMARK_LOW;
        gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
        struct alloc_context ac = { };

        /*
         * There are several places where we assume that the order value is sane
         * so bail out early if the request is out of bound.
         */
        if (unlikely(order >= MAX_ORDER)) {
                WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
                return NULL;
        }

        gfp_mask &= gfp_allowed_mask;
        alloc_mask = gfp_mask;
        if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc__
flags))
                return NULL;

        finalise_ac(gfp_mask, &ac);

        /*
         * Forbid the first pass from falling back to types that fragment
         * memory until all local zones are considered.
         */
        alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);

        /* First allocation attempt */
        page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
        if (likely(page))
                goto out;

        /*
         * Apply scoped allocation constraints. This is mainly about GFP_NOFS
         * resp. GFP_NOIO which has to be inherited for all allocation requests
         * from a particular context which has been marked by
         * memalloc_no{fs,io}_{save,restore}.
         */
        alloc_mask = current_gfp_context(gfp_mask);
        ac.spread_dirty_pages = false;

        /*
         * Restore the original nodemask if it was potentially replaced with
         * &cpuset_current_mems_allowed to optimize the fast-path attempt.
         */
        if (unlikely(ac.nodemask != nodemask))
                ac.nodemask = nodemask;

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

out:
        if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
            unlikely(memcg_kmem_charge(page, gfp_mask, order) != 0)) {
                __free_pages(page, order);
                page = NULL;
        }

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

        return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);

지정된 노드마스크, zonelist 및 flags 설정을 참고하여 노드와 zone을 선택한 후 2^order 페이지만큼 연속된 물리 메모리 할당을 한다.

  • 코드 라인 6에서 할당 플래그의 초기 값으로 low 워터마크를 사용하는 것으로 지정한다.
  • 코드 라인 14~17에서 @order 값이 MAX_ORDER 이상을 사용할 수 없다. 이러한 경우 null을 반환한다.
    • 버디 시스템에서 연속된 메모리를 한 번에 요청할 수 있는 최대 페이지 수는 2^(MAX_ORDER-1) 이다.
  • 코드 라인 19에서 커널 부트업 프로세스가 처리되는 동안은 페이지 할당을 위해 IO 처리를 위한 드라이버나 파일 시스템이 준비되지 않으며, 따라서 메모리 회수 시스템도 구동되지 않고 있는 상태다. 따라서 이러한 요청들이 발생할 때 이 기능을 사용하지 못하게 막기 위해 gfp 플래그에서 _ _GFP_RECLAIM, _ _GFP_IO 및 _ _GFP_FS 비트를 제거한다.
    • 부팅 중에는 전역 변수 gfp_allowed_mask에 GFP_BOOT_MASK를 대입한다.
      • GFP_BOOT_MASK에는 __GFP_RECLAIM | __GFP_IO | __GFP_FS를 제거한 비트들이 담겨있다.
  • 코드 라인 20~23에서 페이지 할당을 시도하기 전에 필요한 할당 context 및 필요한 할당 플래그를 추가하여 준비한다.
  • 코드 라인 25에서 마지막으로 alloc 컨텍스트의 추가 멤버를 준비한다.
  • 코드 라인 31에서 zone 및 gfp 마스크 요청에 따라 nofragment 등의 alloc 플래그를 추가한다.
  • 코드 라인34~36에서 처음 Fast-path 페이지 할당을 시도한다.
  • 코드 라인 44~54에서 Fast-path 할당이 실패한 경우 dirty zone 밸런싱을 하지 않도록 설정하고, slow-path 할당을 시도한다.
  • 코드 라인 56~61에서 out: 레이블이다. 할당된 페이지를 반환하는데, 메모리 컨트롤 그룹의 리밋을 벗어나는 경우 할당을 포기한다.

 

할당 context 준비

prepare_alloc_pages()

mm/page_alloc.c

static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
                int preferred_nid, nodemask_t *nodemask,
                struct alloc_context *ac, gfp_t *alloc_mask,
                unsigned int *alloc_flags)
{
        ac->high_zoneidx = gfp_zone(gfp_mask);
        ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
        ac->nodemask = nodemask;
        ac->migratetype = gfpflags_to_migratetype(gfp_mask);

        if (cpusets_enabled()) {
                *alloc_mask |= __GFP_HARDWALL;
                if (!ac->nodemask)
                        ac->nodemask = &cpuset_current_mems_allowed;
                else
                        *alloc_flags |= ALLOC_CPUSET;
        }

        fs_reclaim_acquire(gfp_mask);
        fs_reclaim_release(gfp_mask);

        might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);

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

        if (IS_ENABLED(CONFIG_CMA) && ac->migratetype == MIGRATE_MOVABLE)
                *alloc_flags |= ALLOC_CMA;

        return true;
}

페이지 할당을 시도하기 전에 각 하위 함수들에 전달할 값들을 모아 ac_context 구조체에 준비한다. 그리고 입출력 인자 @alloc_flags에 필요 시 플래그를 추가하여 준비한다. 디버깅 목적으로 강제로 할당을 실패하게 한 경우에만 false를 반환한다.

  • 코드 라인 6에서 @gfp_mask에 해당하는 존 인덱스를 알아온다.
  • 코드 라인 7에서 @nid 노드에서 @flags 값에 따라 두 zonelist 중 하나를 선택하여 반환한다.
  • 코드 라인 8~9에서 노드 마스크와 할당받을 마이그레이션 타입을 gfp 플래그에서 구한다.
  • 코드 라인 11~17에서 컨트롤 그룹의 cpuset 을 사용하는 경우 alloc_mask에 hardwall 플래그를 추가하여 현재 cpuset 디렉토리 설정을 사용하여 할당하게 한다. 또한 할당 함수에서 노드 마스크 지정여부에 따라 다음과 같이 나뉜다.
    • hardwall + 할당 함수에서 요청한 노드마스크에 지정된 노드들
    • hardwall + 할당 함수에서 요청한 노드마스크 없으면 태스크가 지정한 노드들
  • 코드 라인 22에서 direct 회수를 허용한 경우 preempt point를 수행한다.
    • 일반적인 유저 및 커널 메모리 할당 요청 시 direct 회수는 허용된다.
  • 코드 라인 24~25에서 디버깅 목적으로 실패 상황을 만들 수 있다.
  • 코드 라인 27~28에서  movable 페이지인 경우 cma 영역의 사용을 허락한다.

 

finalise_ac()

mm/page_alloc.c

/* Determine whether to spread dirty pages and what the first usable zone */
static inline void finalise_ac(gfp_t gfp_mask, struct alloc_context *ac)
{
        /* Dirty zone balancing only done in the fast path */
        ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);

        /*
         * The preferred zone is used for statistics but crucially it is
         * also used as the starting point for the zonelist iterator. It
         * may get reset for allocations that ignore memory policies.
         */
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                                        ac->high_zoneidx, ac->nodemask);
}

alloc 컨텍스트의 추가 멤버를 준비하여 마무리한다.

  • 코드 라인 5에서 gfp 플래그에 _ _GFP_WRITE 요청이 있다면 fastpath 페이지 할당에서만 더티 존(dirty zone) 밸런싱을 사용한다
  • 코드 라인 12~13에서 현재 zonelist의 사용 가능한 가장 첫 번째 존을 preferred_zoneref에 저장한다. 이 값은 나중에 통계에서 사용한다. 또한 첫 존이 없는 경우에는 페이지 할당을 할 수 없으므로 out 레이블로 이동하여 함수를 빠져나간다. ac.nodemask가 지정되지 않아 NULL인 경우에는 현재 태스크에 cpuset으로 지정된 노드마스크를 사용한다.

 

alloc_flags_nofragment()

mm/page_alloc.c

/*
 * The restriction on ZONE_DMA32 as being a suitable zone to use to avoid
 * fragmentation is subtle. If the preferred zone was HIGHMEM then
 * premature use of a lower zone may cause lowmem pressure problems that
 * are worse than fragmentation. If the next zone is ZONE_DMA then it is
 * probably too small. It only makes sense to spread allocations to avoid
 * fragmentation between the Normal and DMA32 zones.
 */
static inline unsigned int
alloc_flags_nofragment(struct zone *zone, gfp_t gfp_mask)
{
        unsigned int alloc_flags = 0;

        if (gfp_mask & __GFP_KSWAPD_RECLAIM)
                alloc_flags |= ALLOC_KSWAPD;

#ifdef CONFIG_ZONE_DMA32
        if (zone_idx(zone) != ZONE_NORMAL)
                goto out;

        /*
         * If ZONE_DMA32 exists, assume it is the one after ZONE_NORMAL and
         * the pointer is within zone->zone_pgdat->node_zones[]. Also assume
         * on UMA that if Normal is populated then so is DMA32.
         */
        BUILD_BUG_ON(ZONE_NORMAL - ZONE_DMA32 != 1);
        if (nr_online_nodes > 1 && !populated_zone(--zone))
                goto out;

out:
#endif /* CONFIG_ZONE_DMA32 */
        return alloc_flags;
}

zone 및 gfp 마스크 요청에 따라 nofragment 등의 alloc 플래그를 추가한다.

  • 코드 라인 6~7에서 gfp 플래그로 __GFP_KSWAPD_RECLAIM이 요청된 경우 alloc 플래그에서 ALLOC_KSWAPD를 추가하여 메모리 부족 시 kswapd를 깨울 수 있게 한다.
  • 코드 라인 9~23에서 dma32 존과 normal 존을 모두 운용하는 경우에 normal 존에 할당 요청을 한 경우ALLOC_NOFRAGMENT플래그를추가한다. (단 5.0 코드는 버그)

 


Fastpath 페이지 할당

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

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

 

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

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

 

get_page_from_freelist()

mm/page_alloc.c -1/2-

/*
 * 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 zoneref *z;
        struct zone *zone;
        struct pglist_data *last_pgdat_dirty_limit = NULL;
        bool no_fallback;

retry:
        /*
         * Scan zonelist, looking for a zone with enough free.
         * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
         */
        no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
        z = ac->preferred_zoneref;
        for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
                                                                ac->nodemask) {
                struct page *page;
                unsigned long mark;

                if (cpusets_enabled() &&
                        (alloc_flags & ALLOC_CPUSET) &&
                        !__cpuset_zone_allowed(zone, gfp_mask))
                                continue;
                /*
                 * When allocating a page cache page for writing, we
                 * want to get it from a node that is within its dirty
                 * limit, such that no single node holds more than its
                 * proportional share of globally allowed dirty pages.
                 * The dirty limits take into account the node's
                 * lowmem reserves and high watermark so that kswapd
                 * should be able to balance it without having to
                 * write pages from its LRU list.
                 *
                 * XXX: For now, allow allocations to potentially
                 * exceed the per-node dirty limit in the slowpath
                 * (spread_dirty_pages unset) before going into reclaim,
                 * which is important when on a NUMA setup the allowed
                 * nodes are together not big enough to reach the
                 * global limit.  The proper fix for these situations
                 * will require awareness of nodes in the
                 * dirty-throttling and the flusher threads.
                 */
                if (ac->spread_dirty_pages) {
                        if (last_pgdat_dirty_limit == zone->zone_pgdat)
                                continue;

                        if (!node_dirty_ok(zone->zone_pgdat)) {
                                last_pgdat_dirty_limit = zone->zone_pgdat;
                                continue;
                        }
                }

                if (no_fallback && nr_online_nodes > 1 &&
                    zone != ac->preferred_zoneref->zone) {
                        int local_nid;

                        /*
                         * If moving to a remote node, retry but allow
                         * fragmenting fallbacks. Locality is more important
                         * than fragmentation avoidance.
                         */
                        local_nid = zone_to_nid(ac->preferred_zoneref->zone);
                        if (zone_to_nid(zone) != local_nid) {
                                alloc_flags &= ~ALLOC_NOFRAGMENT;
                                goto retry;
                        }
                }

 

할당 context 정보를 토대로 요청한 2^@order 페이지들을 할당한다. 할당을 성공하면 해당 페이지를 반환하고, 실패하면 null을 반환한다.

  • 코드 라인 15에서 처음 할당 시도 시 페이지 블럭 내에서 fragment 되지 않도록 할당 플래그에 ALLOC_NOFRAGMENT 플래그가 있는 경우 no_fallback 여부를 지정한다.
    • 페이지 블럭내에서 여러 mobility 타입이 혼재될 수 있다.
    • 파편화를 방지하기 위해서 메모리에 여유가 있으면 각 페이지 블럭에서 한 가지 mobility 타입으로 유도하는 것이 좋다.
  • 코드 라인 16~18에서 요청한 노드와 zonelist에서 [선호 존, high_zoneidx]의 존에 대해 순서대로 zone을 순회한다.
  • 코드 라인 22~25에서 현재 태스크가 control group의 cpuset이 지원하는 존을 지원하지 않는 경우 제외하기 위해 skip 한다.
  • 코드 라인 45~53에서 존 별로 dirty limit이 제한되어 있다. 모든 존이 dirty limit을 초과한 경우가 아니라면 dirty limit을 초과한 존은 skip 하기 위함이다. slowpath 할당 시에는 spread_dirty_pages 값은 false로 호출되어 dirty limit 제한을 받지 않는다.
  • 코드 라인 55~69에서 페이지 할당 시 리모트 노드에서 nofragment 요청 보다 fragment 되더라도 로컬 노드에서 할당하는 것이 더 중요한 상황이다. 만일 2개 이상의 노드를 가진 시스템에서 nofragment 요청을 가졌지만 다른 노드에서 할당을 해야 하는 상황이라면 nofragment 요청을 제거하고 로컬 노드에서 할당할 수 있도록 retry 레이블로 이동한다.

 

mm/page_alloc.c -2/2-

                mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
                if (!zone_watermark_fast(zone, order, mark,
                                       ac_classzone_idx(ac), alloc_flags)) {
                        int ret;

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
                        /*
                         * Watermark failed for this zone, but see if we can
                         * grow this zone if it contains deferred pages.
                         */
                        if (static_branch_unlikely(&deferred_pages)) {
                                if (_deferred_grow_zone(zone, order))
                                        goto try_this_zone;
                        }
#endif
                        /* 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 (node_reclaim_mode == 0 ||
                            !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
                                continue;

                        ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
                        switch (ret) {
                        case NODE_RECLAIM_NOSCAN:
                                /* did not scan */
                                continue;
                        case NODE_RECLAIM_FULL:
                                /* scanned but unreclaimable */
                                continue;
                        default:
                                /* did we reclaim enough */
                                if (zone_watermark_ok(zone, order, mark,
                                                ac_classzone_idx(ac), alloc_flags))
                                        goto try_this_zone;

                                continue;
                        }
                }

try_this_zone:
                page = rmqueue(ac->preferred_zoneref->zone, zone, order,
                                gfp_mask, alloc_flags, ac->migratetype);
                if (page) {
                        prep_new_page(page, order, gfp_mask, alloc_flags);

                        /*
                         * If this is a high-order atomic allocation then check
                         * if the pageblock should be reserved for the future
                         */
                        if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
                                reserve_highatomic_pageblock(page, zone, order);

                        return page;
                } else {
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
                        /* Try again if zone has deferred pages */
                        if (static_branch_unlikely(&deferred_pages)) {
                                if (_deferred_grow_zone(zone, order))
                                        goto try_this_zone;
                        }
#endif
                }
        }

        /*
         * It's possible on a UMA machine to get through all zones that are
         * fragmented. If avoiding fragmentation, reset and try again.
         */
        if (no_fallback) {
                alloc_flags &= ~ALLOC_NOFRAGMENT;
                goto retry;
        }

        return NULL;
}
  • 코드 라인 1~3에서 빠른 산출을 위해 대략적으로 추산한 남은 free 페이지 수와 할당 플래그로 요청 받은 3가지 high, low, min 워터마크 값 중 하나와 비교하여 기준 이하의 메모리 부족 상태인 경우이다.
  • 코드 라인 11~14에서 x86 시스템 등의 대용량 메모리 시스템에서 부트업 중에 일부 메모리의 초기화를 유예시킨다. 현재 그러한 상황이라 현재 존의 page들이 초기화되지 않은 상태라면 진짜 메모리 부족이 아닌 경우이므로 이 존에서 할당을 시도하게 한다.
  • 코드 라인 18~19에서 워터 마크 기준이 설정되지 않은 경우에도 이 존에서 할당을 시도하게 한다.
  • 코드 라인 21~23에서 “/sys/vm/zone_reclaim_mode” 설정이 0(디폴트) 이거나 로컬 또는 근거리의 리모트 노드가 아니면 이 존에서 할당을 skip 한다.
  • 코드 라인 25~32에서 노드에 대한 페이지 회수를 위해 scan을 하지 않은 경우이거나, 이미 full scan 하여 더 이상 효과가 없는 상태인 경우 이 존을 skip 한다.
  • 코드 라인 33~40에서 노드에 대해 페이지 회수를 한 결과가 일부 있거나 성공적이면 다시 한 번 정확히 추산하여 남은 free 페이지 수와 할당 플래그로 요청 받은 3가지 high, low, min 워터마크 값 중 하나와 비교하여 기준을 넘어 메모리 부족 상태가 아니면 이 존에서 할당을 시도한다. 그렇지 않고 메모리가 여전히 부족한 상태이면 이 존을 skip 한다.
  • 코드 라인 43~45에서 try_this_zone: 레이블에서는 실제 버디 시스템을 통해 order 페이지를 할당해본다.
  • 코드 라인 46~56에서 메모리가 정상적으로 할당된 경우 새 페이지 구조체에 대한 준비를 수행한 후 해당 페이지를 반환한다.
  • 코드 라인 57~65에서 초기화 유예된 상태인 경우 다시 한번 이 존에서 페이지 할당을 시도한다.
  • 코드 라인 72~75에서 첫 번째 할당 시도가 실패한 경우 블럭 내에서 migrate 타입이 달라도 할당을 할 수 있도록, nofragment 속성을 제거후 다시 시도한다.
  • 코드 라인 77에서 두 번째 할당 시도도 실패한 경우 null을 반환한다.

다음 그림은 get_page_from_freelist() 함수를 통해 처리되는 과정을 보여준다.

 

zone_reclaim_mode
  • NUMA 시스템을 지원하는 커널에서 “proc/sys/fs/zone_reclaim_mode” 파일 값으로 설정한다
    • RECLAIM_OFF(0)
      • 워터마크 기준 이하인 경우 현재 zone의 회수 처리 없이 다음 zone으로 skip
    • RECLAIM_ZONE(1)
      • inactive LRU list를 대상으로 회수 처리한다.
    • RECLAIM_WRITE(2)
      • 수정된 파일 페이지에 대해 writeout 과정을 통해 회수 처리한다.
    • RECLAIM_UNMAP(3)
      • 매핑된 파일 페이지에 대해 unmap 과정을 통해 회수 처리한다.

 

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 void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
                                                        unsigned int alloc_flags)
{
        int i;

        post_alloc_hook(page, order, gfp_flags);

        if (!free_pages_prezeroed() && (gfp_flags & __GFP_ZERO))
                for (i = 0; i < (1 << order); i++)
                        clear_highpage(page + i);

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

        /*
         * page is set pfmemalloc 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.
         */
        if (alloc_flags & ALLOC_NO_WATERMARKS)
                set_page_pfmemalloc(page);
        else
                clear_page_pfmemalloc(page);
}

할당 받은 2^order 페이지 사이즈 메모리에 해당하는 모든 페이지 디스크립터를 초기화한다.

  • 코드 라인 6에서 할당받은 2^order 페이지 사이즈 메모리에 해당하는 첫 번째 페이지 디스크립터를 초기화한다.
  • 코드 라인 8~10에서 zero 초기화 요청을 받은 경우 할당 받은 메모리를 0으로 모두 초기화한다.
    • 32bit 시스템의 highmem 영역에 속한 메모리들은 임시 매핑하여 0으로 초기화한 후 다시 매핑해제한다.
  • 코드 라인 12~13에서 compound  페이지인 경우 페이지 디스크립터들을 compound 페이지로 초기화한다.
    • 모든 tail 페이지들은 head 페이지를 가리킨다.
  • 코드 라인 21~24에서 no watermark 기준으로 할당 요청한 경우 해당 페이지 디스크립터의 index 멤버에 -1을 대입하여 pfmemalloc 상태에서 할당 받았다는 표식을 한다.

 

post_alloc_hook()

mm/page_alloc.c

inline void post_alloc_hook(struct page *page, unsigned int order,
                                gfp_t gfp_flags)
{
        set_page_private(page, 0);
        set_page_refcounted(page);

        arch_alloc_page(page, order);
        kernel_map_pages(page, 1 << order, 1);
        kernel_poison_pages(page, 1 << order, 1);
        kasan_alloc_pages(page, order);
        set_page_owner(page, order, gfp_flags);
}

할당받은 2^order 페이지 사이즈 메모리에 해당하는 첫 번째 페이지 디스크립터를 초기화한다.

  • 코드 라인 4에서 페이지 디스크립터의 private 멤버를 0으로 초기화한다.
  • 코드 라인 5에서 참조 카운터를 1로 초기화한다.
  • 코드 라인 7에서 아키텍처에 대응하는 페이지 할당 후크를 호출한다.
    • ARM, ARM64는 해당 호출 함수가 없다.
  • 코드 라인 8에서 디버그용 페이지 할당을 호출한다.
  • 코드 라인 9에서 poison을 사용한 디버깅을 수행한다. 할당된 메모리에 미리 표식된 poison이 이상 없는지 체크한다.
  • 코드 라인 10에서 KASAN 디버깅을 위해 호출한다.
  • 코드 라인 11에서 디버그용 페이지 오너 트래킹을 위해 호출한다.

 


CPUSET 관련

cpuset_zone_allowed()

include/linux/cpuset.h

static inline int cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask)
{
        if (cpusets_enabled())
                return __cpuset_zone_allowed(z, gfp_mask);
        return true;
}

요청 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 __cpuset_node_allowed(zone_to_nid(z), 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 @node is set in
 * current's mems_allowed, yes.  If it's not a __GFP_HARDWALL request and this
 * node is set in the nearest hardwalled cpuset ancestor to current's cpuset,
 * yes.  If current has access to memory reserves as an oom victim, yes.
 * Otherwise, no.
 *
 * 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.
 * 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
 *      tsk_is_oom_victim   - any node ok
 *      GFP_KERNEL   - any node in enclosing hardwalled cpuset ok
 *      GFP_USER     - only nodes in current tasks mems allowed ok.
 */
bool __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())
                return true;
        if (node_isset(node, current->mems_allowed))
                return true;
        /*
         * Allow tasks that have access to memory reserves because they have
         * been OOM killed to get memory anywhere.
         */
        if (unlikely(tsk_is_oom_victim(current)))
                return true;
        if (gfp_mask & __GFP_HARDWALL)  /* If hardwall request, stop here */
                return false;

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

        /* 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를 반환한다.

  • 코드 라인 7~8에서 인터럽트 핸들러에서 호출된 경우 true를 반환한다.
  • 코드 라인 9~10에서 현재 태스크가 허락하는 노드인 경우 true를 반환한다.
  • 코드 라인 15~16에서 낮은 확률로 현재 태스크가 메모리 부족으로 인해 종료되고 있는 중이면 true를 반환한다.
  • 코드 라인 17~18에서 hardwall 요청인 경우 false를 반환한다.
  • 코드 라인 20~21에서 현재 태스크가 종료 중인 경우 true를 반환한다.
  • 코드 라인 24~32에서 hardwall 요청이 없고, 현재 태스크가 허락하지 않는 노드인 경우이다. 이러한 경우 현재 태스크의 cpuset에서 가장 가까운 hardwall 부모 cpuset을 알아와서 cpuset에 허락된 메모리 노드의 여부를 반환한다.

 

__GFP_HARDWALL 플래그를 사용하는 케이스는 다음 3가지이다.

  • fastpath 페이지 할당 요청
  • 사용자 태스크에서 페이지 할당 요청
  • 슬랩(slab) 페이지 할당 요청

 

커널이 메모리를 할당 요청할 때엔 슬랩(slab) 페이지를 위한 할당 등의 특수한 경우를 제외하고 __GFP_HARDWALL 플래그를 사용하지 않는다.

  • __GFP_HARDWALL 플래그를 사용하지 않으면 cgroup을 사용한다.
  • cgroup의 cpuset 서브시스템에서 현재 태스크가 포함된 그룹을 기점으로 부모 방향으로 hardwall 또는 exclusive 설정이 된 가장 가까운 상위 그룹을 찾아 사용한다.
  • cgroup의 cpuset 서브시스템에 있는 cpuset.mem_exclusive와 cpuset.mem_hardwall의 값을 각각 1로 변경하는 것으로 해당 그룹의 hardwall 및 exclusive가 설정된다.
  • cgroup에 있는 모든 서브시스템의 형상은 트리 구조로 관리가 되며 특별히 값을 설정하지 않아도 부모의 값을 자식이 상속하는 구조로 구성된다. 이 때 hardwall 기능을 사용하면 자신의 그룹과 부모 그룹을 막는 벽이 생기는 것이다

 

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을 계속 찾는다.

 

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)
{
        if (!static_branch_unlikely(&cpusets_pre_enable_key))
                return 0;
        return read_seqcount_begin(&current->mems_allowed_seq);
}

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

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

 


Dirty 노드 밸런싱

dirty(파일에 기록하였지만 파일 캐시 메모리에 상주된 상태로 지연(lazy) 기록되는 상태) limit을 지정하여 사용하는데, 이러한 파일 기록을 요청한 노드를 대상으로 dirty limit을 초과하는 경우 다른 노드에 할당하여 dirty 파일들이 분산되도록 한다.

  • 만일 모든 노드에서 dirty limit을 초과하여 할당을 실패하는 경우 그냥 dirty limit을 풀고 다시 시도하여 할당한다.

 

다음 그림은 dirty 페이지 할당 요청들로부터 20%의 dirty 제한을 초과하지 않게 운영되는 모습을 보여준다.

 

node_dirty_ok()

mm/page-writeback.c

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

        nr_pages += node_page_state(pgdat, NR_FILE_DIRTY);
        nr_pages += node_page_state(pgdat, NR_UNSTABLE_NFS);
        nr_pages += node_page_state(pgdat, NR_WRITEBACK);

        return nr_pages <= limit;
}

요청한 노드가 dirty 제한 이하인지 여부를 반환한다. 1=dirty 제한 범위 이하, 0=dirty 제한 범위 초과

  • 노드의 NR_FILE_DRITY + NR_UNSTABLE_NFS + NR_WRITEBACK 페이지들이 노드의 dirty(write buffer) 한계 이하인 경우 true를 반환한다.
  • zone 카운터들 대한 수치 확인은 

 

다음과 같이 “cat /proc/zoneinfo” 명령을 통해 노드별 카운터 정보를 확인할 수 있다.

  • nodeinfo 파일이 아니라 zoneinfo 파일인 이유: 기존에는 노드별 정보가 아니라 존별 정보를 출력 하였었다.
$ cat /proc/zoneinfo
Node 0, zone    DMA32
  per-node stats
      nr_inactive_anon 2162
      nr_active_anon 4186
      nr_inactive_file 8415
      nr_active_file 5303
      nr_unevictable 0
      nr_slab_reclaimable 3490
      nr_slab_unreclaimable 6347
      nr_isolated_anon 0
      nr_isolated_file 0
      workingset_nodes 0
      workingset_refault 0
      workingset_activate 0
      workingset_restore 0
      workingset_nodereclaim 0
      nr_anon_pages 4141
      nr_mapped    6894
      nr_file_pages 15924
      nr_dirty     91                       <-----
      nr_writeback 0                        <-----
      nr_writeback_temp 0
      nr_shmem     2207
      nr_shmem_hugepages 0
      nr_shmem_pmdmapped 0
      nr_anon_transparent_hugepages 0
      nr_unstable  0                        <-----
      nr_vmscan_write 0
      nr_vmscan_immediate_reclaim 0
      nr_dirtied   330
      nr_written   239
      nr_kernel_misc_reclaimable 0
  pages free     634180
        min      5632
        low      7040
        high     8448
        spanned  786432
        present  786432
        managed  765785
        protection: (0, 0, 0)
      nr_free_pages 634180
      nr_zone_inactive_anon 2162
      nr_zone_active_anon 4186
      nr_zone_inactive_file 8415
      nr_zone_active_file 5303
      nr_zone_unevictable 0
      nr_zone_write_pending 91
      nr_mlock     0
      nr_page_table_pages 233
      nr_kernel_stack 1472
      nr_bounce    0
      nr_free_cma  8128
      numa_hit     97890
      numa_miss    0
      numa_foreign 0
      numa_interleave 6202
      numa_local   97890
      numa_other   0
  pagesets
    cpu: 0
              count: 320
              high:  378
              batch: 63
  vm stats threshold: 24
    cpu: 1
              count: 275
              high:  378
              batch: 63
  vm stats threshold: 24
  node_unreclaimable:  0
  start_pfn:           262144
Node 0, zone   Normal
  pages free     0
        min      0
        low      0
        high     0
        spanned  0
        present  0
        managed  0
        protection: (0, 0, 0)
Node 0, zone  Movable
  pages free     0
        min      0
        low      0
        high     0
        spanned  0
        present  0
        managed  0
        protection: (0, 0, 0)

 

node_dirty_limit()

mm/page-writeback.c

/**
 * node_dirty_limit - maximum number of dirty pages allowed in a node
 * @pgdat: the node
 *
 * Returns the maximum number of dirty pages allowed in a node, based
 * on the node's dirtyable memory.
 */
static unsigned long node_dirty_limit(struct pglist_data *pgdat)
{
        unsigned long node_memory = node_dirtyable_memory(pgdat);
        struct task_struct *tsk = current;
        unsigned long dirty;

        if (vm_dirty_bytes)
                dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE) *
                        node_memory / global_dirtyable_memory();
        else
                dirty = vm_dirty_ratio * node_memory / 100;

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

        return dirty;
}

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

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

 

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

 

node_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.
 */

/**
 * node_dirtyable_memory - number of dirtyable pages in a node
 * @pgdat: the node
 *
 * Returns the node's number of pages potentially available for dirty
 * page cache.  This is the base value for the per-node dirty limits.
 */
static unsigned long node_dirtyable_memory(struct pglist_data *pgdat)
{
        unsigned long nr_pages = 0;
        int z;

        for (z = 0; z < MAX_NR_ZONES; z++) {
                struct zone *zone = pgdat->node_zones + z;

                if (!populated_zone(zone))
                        continue;

                nr_pages += zone_page_state(zone, NR_FREE_PAGES);
        }

        /*
         * Pages reserved for the kernel should not be considered
         * dirtyable, to prevent a situation where reclaim has to
         * clean pages in order to balance the zones.
         */
        nr_pages -= min(nr_pages, pgdat->totalreserve_pages);

        nr_pages += node_page_state(pgdat, NR_INACTIVE_FILE);
        nr_pages += node_page_state(pgdat, NR_ACTIVE_FILE);

        return nr_pages;
}

해당 노드의 dirty 가능한 페이지 수를 반환한다. (노드의 free 페이지 + 사용된 파일 캐시 페이지 – totalreserve_pages)

  • 코드 라인 6~13에서 노드의 모든 populate 존의 free 페이지를 산출한다.
  • 코드 라인 20에서 산출된 페이지에서 totalreserve_pages는 제외한다.
  • 코드 라인 22~23에서 노드의 모든(inactive+active) 파일 캐시 페이지를 추가한다.

 

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_zone_page_state(NR_FREE_PAGES);
        /*
         * Pages reserved for the kernel should not be considered
         * dirtyable, to prevent a situation where reclaim has to
         * clean pages in order to balance the zones.
         */
        x -= min(x, totalreserve_pages);

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

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

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

시스템에서 dirty 가능한 페이지 수를 반환한다. (시스템의 free 페이지 + file 캐시 페이지 – totalreserve_pages)

  • free 페이지 + file 캐시로 사용 중인 페이지 – dirty_balance_reserve 값을 반환한다.
  • 만일 highmem을 dirty 페이지로 사용되지 못하게 한 경우 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;
        int i;

        for_each_node_state(node, N_HIGH_MEMORY) {
                for (i = ZONE_NORMAL + 1; i < MAX_NR_ZONES; i++) {
                        struct zone *z;
                        unsigned long nr_pages;

                        if (!is_highmem_idx(i))
                                continue;

                        z = &NODE_DATA(node)->node_zones[i];
                        if (!populated_zone(z))
                                continue;

                        nr_pages = zone_page_state(z, NR_FREE_PAGES);
                        /* watch for underflows */
                        nr_pages -= min(nr_pages, high_wmark_pages(z));
                        nr_pages += zone_page_state(z, NR_ZONE_INACTIVE_FILE);
                        nr_pages += zone_page_state(z, NR_ZONE_ACTIVE_FILE);
                        x += nr_pages;
                }
        }

        /*
         * 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 페이지 가능한 수를 알아온다. (64비트 시스템은 사용하지 않는다.)

 


구조체

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 migratetype;
        enum zone_type high_zoneidx;
        bool spread_dirty_pages;
};

alloc_pages* 패밀리 함수들에서 여러 가지 파라미터를 전달하기 위한 목적으로 사용되는 구조체이다.

  •  zonelist
    • 페이지 할당 시 사용하는 zonelist
  • nodemask
    • zonelist의 노드들 중 지정한 노드들에서만 할당 가능하도록 제한한다.
    • 지정하지 않으면 모든 노드가 대상이 된다.
  • preferred_zone
    • fastpath에서 가장 우선 할당할 존이 지정된다.
    • slowpath에서는 zonelist의 가용한 첫 존이 지정된다.
  • migratetype
    • 할당할 migrate(페이지) 타입 유형
  • high_zoneidx
    • zonelist의 존들 중 지정한 high zone 이하에서만 할당 가능하도록 제한한다.
  • spread_dirty_pages
    • 더티 존(dirty zone) 밸런싱 여부로 다음과 같이 사용된다.
      • fastpath 할당 요청 시 1로 설정
      • slowpath 할당 요청 시 0으로 설정

 

참고

 

 

pgtable_init()

<kernel v5.0>

페이지 테이블용 슬랩 캐시 준비

pgtable_init()

include/linux/mm.h

static inline void pgtable_init(void)
{
        ptlock_cache_init();
        pgtable_cache_init();
}

페이지 테이블용 슬랩 캐시들을 준비한다.

  • page->ptl에 사용되는 슬랩 캐시
  • pgd 테이블용 슬랩 캐시
    • x86, arm에서는 빈 함수이다.

 

page->ptl용 슬랩 캐시 준비

DEBUG_SPINLOCK과 DEBUG_LOCK_ALLOC이 활성화된 경우 수 만개 이상의 페이지에 사용되는 page->ptl용 spinlock_t 사이즈가 커진다. 이를 kmalloc 메모리 할당자를 통해 할당하게 되면 2의 배수 크기 중 하나를 사용하게 되므로 사용하지 못하고 낭비되는 메모리 양이 매우 커진다. 이에 따라 디버그 상황에서는 메모리 낭비를 최소화하기 위해 page->ptl용 전용 슬랩 캐시를 만들어 사용하게 하였다.

 

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)

 

페이지 테이블 lock/unlock

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 커널 옵션

mm/Kconfig

# 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"

 


pgd 테이블용 슬랩 캐시 준비

pgtable_cache_init() – ARM64

arch/arm64/include/asm/pgtable.h

#define pgtable_cache_init      pgd_cache_init

 

pgd_cache_init() – ARM64

arch/arm64/mm/pgd.c

void __init pgd_cache_init(void)
{
        if (PGD_SIZE == PAGE_SIZE)
                return;

#ifdef CONFIG_ARM64_PA_BITS_52
        /*
         * With 52-bit physical addresses, the architecture requires the
         * top-level table to be aligned to at least 64 bytes.
         */
        BUILD_BUG_ON(PGD_SIZE < 64);
#endif

        /*
         * Naturally aligned pgds required by the architecture.
         */
        pgd_cache = kmem_cache_create("pgd_cache", PGD_SIZE, PGD_SIZE,
                                      SLAB_PANIC, NULL);
}

pgd 테이블용 슬랩 캐시를 준비한다.

 

pgd 테이블 할당/해제

pgd_alloc()

arch/arm64/mm/pgd.c

pgd_t *pgd_alloc(struct mm_struct *mm)
{
        if (PGD_SIZE == PAGE_SIZE)
                return (pgd_t *)__get_free_page(PGALLOC_GFP);
        else
                return kmem_cache_alloc(pgd_cache, PGALLOC_GFP);
}

pgd 테이블 슬랩 캐시를 사용하여 pgd 테이블을 할당한다.

 

pgd_free()

arch/arm64/mm/pgd.c

void pgd_free(struct mm_struct *mm, pgd_t *pgd)
{
        if (PGD_SIZE == PAGE_SIZE)
                free_page((unsigned long)pgd);
        else
                kmem_cache_free(pgd_cache, pgd);
}

pgd 테이블 슬랩 캐시에 pgd 테이블을 할당 해제한다.

 

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()보다 느리다.

 

참고