Bulk page allocation의 반환값은 새 page 수와 같지 않습니다

page pointer 배열에 빈 자리가 많다면 한 번에 여러 page를 얻는 API가 편리합니다. 하지만 반환값이 요청 수와 같지 않을 수 있고, 반환값과 이번에 새로 allocation한 수가 다를 수도 있습니다. 이미 채워진 slot을 어떻게 세는지와 어느 시점에 일반 한-page 경로로 돌아가는지가 핵심입니다.

Linux v6.1의 array wrapper에서 PCP fast path와 단일-page fallback까지 살펴보겠습니다. 여기서 bulk는 order-0 page 여러 개를 의미하며 하나의 큰 연속 physical block을 의미하지 않습니다. 각 code block은 표시한 함수의 전문이고 supporting kernel type·macro가 필요한 내부 구현입니다.

Array wrapper는 정책 선택의 차이를 숨기지 않습니다

다음 두 함수는 include/linux/gfp.h의 전문입니다. SPDX는 GPL-2.0이며 들여쓰기와 긴 인수 행의 줄바꿈만 정리했습니다.

static inline unsigned long
alloc_pages_bulk_array(gfp_t gfp, unsigned long nr_pages, struct page **page_array)
{
    return __alloc_pages_bulk(gfp, numa_mem_id(), NULL, nr_pages, NULL, page_array);
}
static inline unsigned long
alloc_pages_bulk_array_node(gfp_t gfp, int nid, unsigned long nr_pages,
    struct page **page_array)
{
    if (nid == NUMA_NO_NODE)
        nid = numa_mem_id();

    return __alloc_pages_bulk(gfp, nid, NULL, nr_pages, NULL, page_array);
}

둘 다 array 출력을 선택하여 중앙 bulk 함수에 전달합니다. node 인수가 없는 쪽과 NUMA_NO_NODE를 받은 쪽은 numa_mem_id()를 기본 선호점으로 사용합니다. 명시 node가 있다고 모든 다른 node 사용까지 금지하는 것은 아닙니다. THISNODE의 zonelist 선택이나 cpuset·nodemask 제한은 별도입니다.

task의 NUMA memory policy까지 선택하려면 다른 wrapper를 읽어야 합니다. 다음은 mm/mempolicy.c의 alloc_pages_bulk_array_mempolicy() 전문이며 SPDX는 GPL-2.0-only입니다.

unsigned long alloc_pages_bulk_array_mempolicy(gfp_t gfp,
        unsigned long nr_pages, struct page **page_array)
{
    struct mempolicy *pol = &default_policy;

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

    if (pol->mode == MPOL_INTERLEAVE)
        return alloc_pages_bulk_array_interleave(gfp, pol,
                             nr_pages, page_array);

    if (pol->mode == MPOL_PREFERRED_MANY)
        return alloc_pages_bulk_array_preferred_many(gfp,
                numa_node_id(), pol, nr_pages, page_array);

    return __alloc_pages_bulk(gfp, policy_node(gfp, pol, numa_node_id()),
                  policy_nodemask(gfp, pol), nr_pages, NULL,
                  page_array);
}

interrupt 밖이고 THISNODE가 아니면 current task policy를 선택합니다. INTERLEAVE와 PREFERRED_MANY는 별도의 처리를 거치고, 일반 policy는 선호 node와 허용 nodemask로 바뀝니다. 이름에 bulk가 들어간 세 wrapper를 policy 효과까지 같은 함수로 취급하면 안 됩니다.

Interleave의 계획량과 실제 성공량을 구별합니다

다음 두 helper도 mm/mempolicy.c의 전문이며 SPDX는 GPL-2.0-only입니다.

static unsigned long alloc_pages_bulk_array_interleave(gfp_t gfp,
        struct mempolicy *pol, unsigned long nr_pages,
        struct page **page_array)
{
    int nodes;
    unsigned long nr_pages_per_node;
    int delta;
    int i;
    unsigned long nr_allocated;
    unsigned long total_allocated = 0;

    nodes = nodes_weight(pol->nodes);
    nr_pages_per_node = nr_pages / nodes;
    delta = nr_pages - nodes * nr_pages_per_node;

    for (i = 0; i < nodes; i++) {
        if (delta) {
            nr_allocated = __alloc_pages_bulk(gfp,
                    interleave_nodes(pol), NULL,
                    nr_pages_per_node + 1, NULL,
                    page_array);
            delta--;
        } else {
            nr_allocated = __alloc_pages_bulk(gfp,
                    interleave_nodes(pol), NULL,
                    nr_pages_per_node, NULL, page_array);
        }

        page_array += nr_allocated;
        total_allocated += nr_allocated;
    }

    return total_allocated;
}
static unsigned long alloc_pages_bulk_array_preferred_many(gfp_t gfp, int nid,
        struct mempolicy *pol, unsigned long nr_pages,
        struct page **page_array)
{
    gfp_t preferred_gfp;
    unsigned long nr_allocated = 0;

    preferred_gfp = gfp | __GFP_NOWARN;
    preferred_gfp &= ~(__GFP_DIRECT_RECLAIM | __GFP_NOFAIL);

    nr_allocated  = __alloc_pages_bulk(preferred_gfp, nid, &pol->nodes,
                       nr_pages, NULL, page_array);

    if (nr_allocated < nr_pages)
        nr_allocated += __alloc_pages_bulk(gfp, numa_node_id(), NULL,
                nr_pages - nr_allocated, NULL,
                page_array + nr_allocated);
    return nr_allocated;
}

INTERLEAVE는 policy의 node 수로 요청량을 나누고 나머지를 앞쪽 선택에 한 page씩 더합니다. 각 시도의 실제 반환량만큼 array pointer와 합계를 전진시키므로 계획한 균등 분배와 실제 성공 분포가 반드시 같지는 않습니다. 이 helper는 이미 유효하게 구성된 policy를 전제로 하며 임의의 빈 node 집합을 받아 검증하는 함수가 아닙니다.

PREFERRED_MANY는 먼저 preferred node 집합에 NOWARN을 더하고 DIRECT_RECLAIM·NOFAIL을 뺀 GFP로 시도합니다. 부족하면 원래 GFP와 다른 기본 선호점으로 나머지를 요청합니다. 첫 시도가 direct reclaim을 피한다고 KSWAPD 요구까지 무조건 없어진 것은 아닙니다. 두 번째 nodemask가 NULL이어도 cpuset 같은 다른 제약이 자동 해제되지는 않습니다.

nr_populated와 nr_account는 다른 숫자입니다

array 버전에서 nr_pages는 이번에 새로 얻을 수의 단순한 상수가 아니라 처리할 array 구간의 상한으로 사용됩니다. 먼저 연속한 기존 non-NULL slot을 건너뛰며, fast loop에서도 기존 slot을 발견하면 allocation 없이 진행합니다.

다음은 한 번의 fast loop를 설명하는 개념적 array 상태입니다. kernel 구현이나 실행 측정값이 아닙니다.

slot index:       0       1       2
before:        [old]   [NULL]   [old]
fast success:  [old]   [new]    [old]
nr_populated:    3
nr_account:      1

이 경우 반환 진행값은 3이지만 새로 받은 것은 1 page입니다. 반대로 한-page fallback은 첫 빈 slot을 채운 직후 반환할 수 있으므로 뒤쪽의 기존 slot을 끝까지 재탐색하여 최대로 채워진 prefix 길이를 계산하는 함수도 아닙니다. 반환값과 실제 array 내용을 caller 계약에 맞게 사용해야 하며, 기존 page를 이번 allocation의 새 결과로 중복 소유하거나 중복 해제해서는 안 됩니다.

list 출력에서는 기존 list를 순회해 길이를 재지 않습니다. nr_populated는 이 호출에서 추가한 수를 세는 방식으로 시작합니다. array와 list가 같은 내부 함수에 들어간다고 모든 pre-populated 출력의 counting 계약까지 같다고 가정하지 않습니다.

PCP를 잡기 전에 일반 한-page 경로로 갈 수 있습니다

이 tag의 bulk core는 memcg accounting을 요청했거나 실제 남은 수가 하나이면 일반 allocator로 fallback합니다. PAGE_OWNER가 활성화된 경우에도 stack 저장을 위한 재귀 allocation과 PCP lock 관계 때문에 단일 경로를 선택할 수 있습니다. 반면 준비 단계의 failure injection 실패는 out으로 나갑니다. 모든 실패 label이 같은 일을 하지는 않습니다.

허용 local zone을 찾을 때는 필요한 여유량에 nr_pages를 더한 watermark를 검사합니다. 다른 node로 이동해야 하거나 적합한 zone이 없으면 단일 경로를 시도합니다. PCP trylock도 실패할 수 있습니다. lock을 얻은 뒤에는 order-0 list에서 반복해서 꺼내지만 공급이 중간에 끊기면 이미 얻은 일부만 반환합니다.

다음은 mm/page_alloc.c의 __alloc_pages_bulk() 전문입니다. SPDX는 GPL-2.0-only이며 공백과 긴 인수 행의 줄바꿈만 정리했습니다. 함수 뒤의 source에는 EXPORT_SYMBOL_GPL(__alloc_pages_bulk)가 있습니다. header의 inline wrapper와 이 별도 export 표시는 다른 종류의 정보입니다.

unsigned long __alloc_pages_bulk(gfp_t gfp, int preferred_nid,
            nodemask_t *nodemask, int nr_pages,
            struct list_head *page_list,
            struct page **page_array)
{
    struct page *page;
    unsigned long flags;
    unsigned long __maybe_unused UP_flags;
    struct zone *zone;
    struct zoneref *z;
    struct per_cpu_pages *pcp;
    struct list_head *pcp_list;
    struct alloc_context ac;
    gfp_t alloc_gfp;
    unsigned int alloc_flags = ALLOC_WMARK_LOW;
    int nr_populated = 0, nr_account = 0;

    /*
     * Skip populated array elements to determine if any pages need
     * to be allocated before disabling IRQs.
     */
    while (page_array && nr_populated < nr_pages && page_array[nr_populated])
        nr_populated++;

    /* No pages requested? */
    if (unlikely(nr_pages <= 0))
        goto out;

    /* Already populated array? */
    if (unlikely(page_array && nr_pages - nr_populated == 0))
        goto out;

    /* Bulk allocator does not support memcg accounting. */
    if (memcg_kmem_enabled() && (gfp & __GFP_ACCOUNT))
        goto failed;

    /* Use the single page allocator for one page. */
    if (nr_pages - nr_populated == 1)
        goto failed;

#ifdef CONFIG_PAGE_OWNER
    /*
     * PAGE_OWNER may recurse into the allocator to allocate space to
     * save the stack with pagesets.lock held. Releasing/reacquiring
     * removes much of the performance benefit of bulk allocation so
     * force the caller to allocate one page at a time as it'll have
     * similar performance to added complexity to the bulk allocator.
     */
    if (static_branch_unlikely(&page_owner_inited))
        goto failed;
#endif

    /* May set ALLOC_NOFRAGMENT, fragmentation will return 1 page. */
    gfp &= gfp_allowed_mask;
    alloc_gfp = gfp;
    if (!prepare_alloc_pages(gfp, 0, preferred_nid, nodemask, &ac,
            &alloc_gfp, &alloc_flags))
        goto out;
    gfp = alloc_gfp;

    /* Find an allowed local zone that meets the low watermark. */
    for_each_zone_zonelist_nodemask(zone, z, ac.zonelist,
            ac.highest_zoneidx, ac.nodemask) {
        unsigned long mark;

        if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) &&
            !__cpuset_zone_allowed(zone, gfp)) {
            continue;
        }

        if (nr_online_nodes > 1 && zone != ac.preferred_zoneref->zone &&
            zone_to_nid(zone) != zone_to_nid(ac.preferred_zoneref->zone)) {
            goto failed;
        }

        mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK) + nr_pages;
        if (zone_watermark_fast(zone, 0,  mark,
                zonelist_zone_idx(ac.preferred_zoneref),
                alloc_flags, gfp)) {
            break;
        }
    }

    /*
     * If there are no allowed local zones that meets the watermarks then
     * try to allocate a single page and reclaim if necessary.
     */
    if (unlikely(!zone))
        goto failed;

    /* Is a parallel drain in progress? */
    pcp_trylock_prepare(UP_flags);
    pcp = pcp_spin_trylock_irqsave(zone->per_cpu_pageset, flags);
    if (!pcp)
        goto failed_irq;

    /* Attempt the batch allocation */
    pcp_list = &pcp->lists[order_to_pindex(ac.migratetype, 0)];
    while (nr_populated < nr_pages) {

        /* Skip existing pages */
        if (page_array && page_array[nr_populated]) {
            nr_populated++;
            continue;
        }

        page = __rmqueue_pcplist(zone, 0, ac.migratetype, alloc_flags,
                                pcp, pcp_list);
        if (unlikely(!page)) {
            /* Try and allocate at least one page */
            if (!nr_account) {
                pcp_spin_unlock_irqrestore(pcp, flags);
                goto failed_irq;
            }
            break;
        }
        nr_account++;

        prep_new_page(page, 0, gfp, 0);
        if (page_list)
            list_add(&page->lru, page_list);
        else
            page_array[nr_populated] = page;
        nr_populated++;
    }

    pcp_spin_unlock_irqrestore(pcp, flags);
    pcp_trylock_finish(UP_flags);

    __count_zid_vm_events(PGALLOC, zone_idx(zone), nr_account);
    zone_statistics(ac.preferred_zoneref->zone, zone, nr_account);

out:
    return nr_populated;

failed_irq:
    pcp_trylock_finish(UP_flags);

failed:
    page = __alloc_pages(gfp, 0, preferred_nid, nodemask);
    if (page) {
        if (page_list)
            list_add(&page->lru, page_list);
        else
            page_array[nr_populated] = page;
        nr_populated++;
    }

    goto out;
}

Lock 해제와 반환량은 label마다 확인합니다

PCP lock을 잡은 뒤 아직 하나도 얻지 못했다면 먼저 lock을 풀고 failed_irq로 가서 준비 상태를 끝낸 다음 단일 allocator를 호출합니다. 일부를 얻은 뒤 list가 비면 loop를 끝내고 lock을 풀어 그 일부를 반환합니다. nr_account는 이 fast path에서 실제 새로 받은 page의 통계에 쓰며, 기존 array slot 수를 allocation event에 더하지 않습니다.

failed는 __alloc_pages()를 한 번 호출합니다. 요청의 나머지를 모두 채울 때까지 이 label이 loop하는 것은 아닙니다. 그 결과가 있으면 출력에 하나를 추가하고 반환 진행값을 늘립니다. caller는 부분 결과와 기존 slot을 처리할 수 있어야 합니다.

또한 v6.1의 이 bulk 진입 code는 allowed mask 뒤에 prepare_alloc_pages()를 호출하며, 단일 allocator 진입부에 있는 current_gfp_context() 호출을 이 위치에 명시적으로 두고 있지 않습니다. 단일-page fallback은 별도의 __alloc_pages()를 거칩니다. 두 경로가 닮았다고 진입 전처리와 모든 task-scope 처리까지 같은 순서라고 추정해서는 안 됩니다. 운영 중인 다른 kernel tag나 WSL build를 해석할 때에도 그 구현을 따로 확인해야 합니다.

core의 nr_pages 인수는 이 tag에서 int이며 wrapper의 unsigned long과도 폭이 다릅니다. caller는 유효한 array 범위와 지원되는 요청 크기를 지켜야 합니다. bulk API는 큰 수를 넘기면 알아서 큰 연속 범위를 만들어 주는 인터페이스가 아닙니다. 장점은 조건이 맞을 때 PCP lock 아래에서 여러 order-0 page를 효율적으로 공급받는 데 있습니다.

참고 자료