Watermark를 통과한 page allocation도 다시 실패할 수 있습니다

watermark 검사가 성공했다면 memory가 예약된 것일까요. 그렇지 않습니다. 이 검사는 zone에 요청을 받아들일 만한 여유가 있는지 판단합니다. 실제 free list에서 block을 제거하는 일은 뒤쪽의 lock 안에서 일어납니다. 그 사이 다른 CPU가 같은 공급원을 사용할 수 있습니다.

Linux v6.1의 watermark와 rmqueue()를 연결해 보면 총량 검사, block 존재 검사, 실제 획득과 state 검증이라는 서로 다른 단계가 드러납니다.

Free 총량에서 바로 쓸 수 없는 양을 조정합니다

__zone_watermark_unusable_free()는 요청에 바로 쓸 수 없는 부분을 계산합니다. 기본 항은 2^order - 1입니다. order 자체에서 1을 빼는 계산이 아닙니다. order-3 요청이라면 이 항은 7 base page가 됩니다. free 총량만으로 원하는 크기의 block이 있다는 결론을 내리지 않도록 하는 보수적인 조정입니다.

일반 권한으로 접근할 수 없는 highatomic reserve와, CMA를 사용할 수 없는 요청의 CMA free 부분도 조건에 따라 제외합니다. 이렇게 얻은 adjusted free를 선택한 mark와 lowmem_reserve[highest_zoneidx]에 비교합니다. adjusted free가 그 합 이하이면 거절합니다.

이 계산은 page를 즉시 list에서 떼어 예약하는 연산이 아닙니다. 2^order - 1 page가 특정 주소에서 봉인되는 것도 아닙니다. 총량 판단과 실제 적합한 block 탐색을 나누어 둔 것입니다.

Order-0 fast check는 일반 검사로 이어질 수 있습니다

zone_watermark_fast()는 흔한 order-0 요청에서 복잡한 검사를 줄입니다. 현재 free와 unusable 부분을 고려해 mark와 protection보다 충분히 크면 빠르게 통과할 수 있습니다. 이 간단한 조건으로 판정하지 못했거나 higher-order라면 일반 __zone_watermark_ok()를 사용합니다.

일반 검사는 internal allocation flag에 따라 mark를 완화할 수 있습니다. ALLOC_HIGH는 mark의 절반을 덜어 내고, HARDER는 남은 기준을 추가로 4분의 1 낮춥니다. OOM 대상에 대한 완화는 더 크며 OOM과 HARDER가 동시에 있으면 OOM 쪽 분기를 사용합니다. 둘을 독립적으로 계속 곱하는 것은 아닙니다.

정수 반올림과 다른 항을 잠시 제외하면 남는 mark는 기본 1, HARDER 3/4, HIGH 1/2, OOM 1/2입니다. HIGH와 HARDER는 3/8, HIGH와 OOM은 1/4입니다. OOM에 HARDER가 더해졌다고 3/8로 바뀌는 것은 아닙니다. 이것은 mark 항의 비율이며 protection까지 같은 비율로 깎는 표가 아닙니다.

각 비율은 CONFIG_MMU에서 OOM이 별도 bit인 경우의 mark 항만 나타냅니다. 정수 나눗셈의 반올림은 잠시 제외했습니다. protection까지 같은 비율로 깎는 표가 아닙니다.

적용하는 flag 조합 남는 mark 두 번째 완화 분기
없음 1 없음
HARDER 3/4 HARDER
OOM 1/2 OOM
OOM + HARDER 1/2 OOM만 적용합니다.
HIGH 1/2 없음
HIGH + HARDER 3/8 HARDER
HIGH + OOM 1/4 OOM
HIGH + OOM + HARDER 1/4 OOM만 적용합니다.

실제로는 각 나눗셈의 소수 부분을 버립니다. 예를 들어 mark가 9이면 HIGH 뒤 5, 이어 HARDER 뒤 4가 됩니다. 실수 비율인 9 * 3/8을 한 번 계산해 버리는 것과 다릅니다. NOMMU의 OOM alias와 watermark 우회 조건은 내부 flag 정의에서 따로 확인할 수 있습니다.

watermark boost가 있는 zone에서 order-0 atomic 요청이 MIN을 검사하는 경우에는 boost를 제외한 MIN으로 다시 검사하는 예외도 있습니다. 이 제한된 예외를 “atomic 요청은 watermark를 전부 무시한다”로 확대하면 안 됩니다.

Higher-order에는 실제 크기의 free block도 필요합니다

총량 조건을 통과한 뒤 higher-order 요청은 요청 order 이상의 free area에서 사용할 수 있는 block이 있는지 확인합니다. 작은 page가 충분해도 contiguous block이 없으면 실패할 수 있기 때문입니다. 일반 migrate list뿐 아니라 허용된 CMA·highatomic 여부도 조건에 맞게 해석합니다.

이 존재 확인 역시 block 획득은 아닙니다. free list 상태는 변하므로 rmqueue()가 lock을 잡고 실제 block을 찾을 때는 결과가 달라질 수 있습니다. 실패하면 다른 zone이나 이후 경로로 넘어갈 수 있어야 합니다. watermark의 참을 allocation 성공으로 로그에 기록하면 동시성에 따른 정상적인 실패를 설명하지 못합니다.

rmqueue는 PCP와 buddy 중 가능한 경로를 고릅니다

지원 order와 CMA policy 등 조건이 맞으면 rmqueue_pcplist()를 먼저 시도합니다. CONFIG_CMA가 켜진 환경의 MOVABLE PCP list에는 CMA page가 섞일 수 있으므로, MOVABLE 요청이면서 ALLOC_CMA 권한이 없으면 이 PCP 경로를 건너뜁니다. PCP 경로를 사용할 수 없거나 결과를 얻지 못하면 rmqueue_buddy()가 공급원이 됩니다. PCP가 있다고 모든 요청이 반드시 그 list를 거치는 것은 아닙니다.

PCP 접근은 현재 CPU를 안정화하는 제어와 trylock, IRQ 상태 관리를 함께 사용합니다. lock 취득 실패도 빠른 경로의 정상적인 우회 조건입니다. __rmqueue_pcplist()는 order와 migrate type으로 list index를 고르고, 비었으면 rmqueue_bulk()로 refill합니다. refill 양은 요청 order에 맞게 조정하며 고정된 개수의 block이 언제나 채워지는 것은 아닙니다.

PCP에서 block을 꺼내면 base-page 단위 count를 줄입니다. order-k block 하나의 count 변화는 2^k입니다. allocation 쪽에서 free_factor를 낮추는 것도 연속 free에 따른 적극적인 drain 경향을 완화하려는 부분입니다. 이 값과 실제 block 개수를 같은 단위로 다루면 안 됩니다.

PCP에서 성공해도 rmqueue()의 공통 반환 지점으로 갑니다. 여기서 ZONE_BOOSTED_WATERMARK가 관찰되면 그 bit를 지우고 wakeup_kswapd()를 호출합니다. 앞쪽 fallback이 ALLOC_KSWAPD 조건으로 boost 상태를 표시하는 단계와, 이 공통 지점에서 표시를 처리하는 단계는 구별해야 합니다. buddy 경로를 사용했을 때만 wakeup을 검사하거나, 현재 요청의 GFP를 여기서 다시 검사하는 구조는 아닙니다. wakeup 요청이 곧 reclaim 완료를 의미하지도 않습니다.

Buddy 획득과 state 검증은 lock의 안팎을 구별합니다

rmqueue_buddy()는 zone lock 아래에서 free list를 수정합니다. order가 0보다 크고 ALLOC_HARDER가 있으면 HIGHATOMIC list의 smallest를 먼저 시도하고, 결과가 없으면 일반 __rmqueue()를 사용합니다. order-0을 같은 특별 reserve 요청으로 취급하지 않습니다. list에서 block을 제거하는 동작과 새 owner에게 안전한 상태인지 확인하는 동작은 다릅니다.

실제 block 제거와 free-page accounting은 zone lock 안에서 처리하지만 check_new_pages()는 lock을 푼 뒤 수행합니다. 이 검사는 예상하지 못한 mapping, mapcount, refcount, flag 등을 확인합니다. 잘못된 page state는 이전 소유자의 잘못된 반환이나 memory corruption을 의심하게 하는 신호입니다. 검사에 걸린 block을 warning만 남기고 정상 결과로 반환하지 않고, 다시 lock을 잡아 다른 후보를 획득하는 경로가 있습니다. hardware poison처럼 별도 처리가 필요한 경우도 있습니다. 검사를 통과한 뒤 allocation event와 NUMA 관련 통계를 갱신하고 결과를 반환합니다.

PCP 쪽의 check_pcp_refill()과 check_new_pcp()는 DEBUG_VM·debug-pagealloc configuration에 따라 검사 시점이 달라집니다. 모든 build에서 refill과 pop 양쪽이 같은 비용으로 검사한다고 일반화할 수 없습니다. 어느 시점의 검사를 생략하는지와 정상 상태 계약 자체를 없애는 것은 다릅니다.

Bulk refill도 일부만 성공할 수 있습니다

rmqueue_bulk()는 zone lock 한 번으로 같은 order block 여러 개를 가져와 list에 추가하려는 경로입니다. 요청한 count는 block 개수이고 PCP accounting은 base page 수입니다. 중간에 공급원이 비거나 잘못된 page를 거르면 실제 반환 개수는 요청보다 작을 수 있습니다. 호출자는 실제 얻은 개수를 사용해야 합니다.

같은 type의 block이 없을 때 __rmqueue()는 허용된 fallback으로 free-list 구성을 바꾼 뒤 다시 같은 type 탐색을 수행할 수 있습니다. __rmqueue_fallback()의 true는 page pointer를 반환했다는 뜻이 아니라 재분류 뒤 탐색을 재시도할 수 있다는 뜻입니다. 어느 한 함수가 page 하나를 꺼낸다는 설명만으로 끝내면 이런 retry와 policy 분기를 놓칩니다.

watermark는 입장 조건이고 lock 안의 list 제거가 획득이며, state 검사와 preparation이 끝나야 caller에게 넘길 수 있습니다. 이 경계를 나누어 보면 free 수가 넉넉한데도 실패하는 사례를 총량 부족으로만 설명하지 않게 됩니다.

총량 판정에서 order별 block 검사까지의 전문입니다

아래 세 함수는 Linux v6.1의 mm/page_alloc.c에 있는 실제 구현 전문입니다. SPDX는 GPL-2.0-only이며 들여쓰기만 정리했습니다. kernel 내부 helper를 읽기 위한 code로, supporting type·macro를 따로 정의한 user-space 실습 프로그램은 아닙니다.

static inline long __zone_watermark_unusable_free(struct zone *z,
                unsigned int order, unsigned int alloc_flags)
{
    const bool alloc_harder = (alloc_flags & (ALLOC_HARDER|ALLOC_OOM));
    long unusable_free = (1 << order) - 1;

    /*
     * 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))
        unusable_free += z->nr_reserved_highatomic;

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

    return unusable_free;
}
bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
             int highest_zoneidx, 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 -= __zone_watermark_unusable_free(z, order, alloc_flags);

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

    if (unlikely(alloc_harder)) {
        /*
         * 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;
    }

    /*
     * 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[highest_zoneidx])
        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 (!free_area_empty(area, mt))
                return true;
        }

#ifdef CONFIG_CMA
        if ((alloc_flags & ALLOC_CMA) &&
            !free_area_empty(area, MIGRATE_CMA)) {
            return true;
        }
#endif
        if (alloc_harder && !free_area_empty(area, MIGRATE_HIGHATOMIC))
            return true;
    }
    return false;
}
static inline bool zone_watermark_fast(struct zone *z, unsigned int order,
                unsigned long mark, int highest_zoneidx,
                unsigned int alloc_flags, gfp_t gfp_mask)
{
    long free_pages;

    free_pages = zone_page_state(z, NR_FREE_PAGES);

    /*
     * Fast check for order-0 only. If this fails then the reserves
     * need to be calculated.
     */
    if (!order) {
        long usable_free;
        long reserved;

        usable_free = free_pages;
        reserved = __zone_watermark_unusable_free(z, 0, alloc_flags);

        /* reserved may over estimate high-atomic reserves. */
        usable_free -= min(usable_free, reserved);
        if (usable_free > mark + z->lowmem_reserve[highest_zoneidx])
            return true;
    }

    if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
                    free_pages))
        return true;
    /*
     * Ignore watermark boosting for GFP_ATOMIC order-0 allocations
     * when checking the min watermark. The min watermark is the
     * point where boosting is ignored so that kswapd is woken up
     * when below the low watermark.
     */
    if (unlikely(!order && (gfp_mask & __GFP_ATOMIC) && z->watermark_boost
        && ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) {
        mark = z->_watermark[WMARK_MIN];
        return __zone_watermark_ok(z, order, mark, highest_zoneidx,
                    alloc_flags, free_pages);
    }

    return false;
}

order-0 fast check는 reserved 추정치가 free보다 큰 경우에도 min(usable_free, reserved)만 뺍니다. 이 빠른 통과 조건이 성립하지 않으면 일반 함수가 조정된 mark와 high-order 후보를 다시 검사합니다. 일반 함수는 먼저 총량에서 거절한 뒤에만 block 존재를 확인하므로, 큰 block 하나를 발견했다고 lower-zone protection을 건너뛰지 않습니다.

PCP 선택과 buddy lock 경계의 실제 구현입니다

다음은 같은 파일의 rmqueue(), rmqueue_buddy(), __rmqueue() 전문입니다. SPDX는 GPL-2.0-only입니다. 첫 함수의 memory-sanitizer attribute와 inline 지정도 생략하지 않았습니다.

__no_sanitize_memory
static inline
struct page *rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, unsigned int alloc_flags,
            int migratetype)
{
    struct page *page;

    /*
     * We most definitely don't want callers attempting to
     * allocate greater than order-1 page units with __GFP_NOFAIL.
     */
    WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));

    if (likely(pcp_allowed_order(order))) {
        /*
         * MIGRATE_MOVABLE pcplist could have the pages on CMA area and
         * we need to skip it when CMA area isn't allowed.
         */
        if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
                migratetype != MIGRATE_MOVABLE) {
            page = rmqueue_pcplist(preferred_zone, zone, order,
                    migratetype, alloc_flags);
            if (likely(page))
                goto out;
        }
    }

    page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags,
                            migratetype);

out:
    /* Separate test+clear to avoid unnecessary atomics */
    if (unlikely(test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags))) {
        clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
        wakeup_kswapd(zone, 0, 0, zone_idx(zone));
    }

    VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
    return page;
}
static __always_inline
struct page *rmqueue_buddy(struct zone *preferred_zone, struct zone *zone,
               unsigned int order, unsigned int alloc_flags,
               int migratetype)
{
    struct page *page;
    unsigned long flags;

    do {
        page = NULL;
        spin_lock_irqsave(&zone->lock, flags);
        /*
         * order-0 request can reach here when the pcplist is skipped
         * due to non-CMA allocation context. HIGHATOMIC area is
         * reserved for high-order atomic allocation, so order-0
         * request should skip it.
         */
        if (order > 0 && alloc_flags & ALLOC_HARDER)
            page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
        if (!page) {
            page = __rmqueue(zone, order, migratetype, alloc_flags);
            if (!page) {
                spin_unlock_irqrestore(&zone->lock, flags);
                return NULL;
            }
        }
        __mod_zone_freepage_state(zone, -(1 << order),
                      get_pcppage_migratetype(page));
        spin_unlock_irqrestore(&zone->lock, flags);
    } while (check_new_pages(page, order));

    __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
    zone_statistics(preferred_zone, zone, 1);

    return page;
}
/*
 * Do the hard work of removing an element from the buddy allocator.
 * Call me with the zone->lock already held.
 */
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
                        unsigned int alloc_flags)
{
    struct page *page;

    if (IS_ENABLED(CONFIG_CMA)) {
        /*
         * Balance movable allocations between regular and CMA areas by
         * allocating from CMA when over half of the zone's free memory
         * is in the CMA area.
         */
        if (alloc_flags & ALLOC_CMA &&
            zone_page_state(zone, NR_FREE_CMA_PAGES) >
            zone_page_state(zone, NR_FREE_PAGES) / 2) {
            page = __rmqueue_cma_fallback(zone, order);
            if (page)
                return page;
        }
    }
retry:
    page = __rmqueue_smallest(zone, order, migratetype);
    if (unlikely(!page)) {
        if (alloc_flags & ALLOC_CMA)
            page = __rmqueue_cma_fallback(zone, order);

        if (!page && __rmqueue_fallback(zone, order, migratetype,
                                alloc_flags))
            goto retry;
    }
    return page;
}

rmqueue_buddy()의 do/while은 check_new_pages()가 잘못된 상태를 발견하면 다음 block을 찾습니다. free-page accounting은 lock을 풀기 전에 줄지만 검사는 lock 밖에 있습니다. 실패한 block을 되돌려 주지 않고 다음 후보로 진행하는 이 경계가 긴 검사를 zone lock 안으로 옮기지 않는 이유와 연결됩니다.

__rmqueue()에는 두 CMA 기회가 있습니다. CMA free가 zone free의 절반보다 많고 허용 flag가 있으면 일반 탐색보다 먼저 시도하며, 그 밖에는 같은 type 탐색 실패 뒤 CMA를 검토합니다. 그마저 없으면 일반 type fallback이 list를 바꿨을 때만 retry로 돌아갑니다. fallback의 bool 반환을 page pointer처럼 취급하면 이 실제 공급 순서를 잃습니다.

PCP lock, refill, pop을 함수 단위로 읽습니다

다음 세 함수는 Linux v6.1의 mm/page_alloc.c에서 확인한 전문입니다. SPDX는 GPL-2.0-only이며 들여쓰기만 정리했습니다. 이 tag의 rmqueue_pcplist()는 별도의 gfp_flags 인수를 받지 않습니다. 서로 다른 source의 호출식과 signature를 섞어 옮기지 않아야 합니다.

static struct page *rmqueue_pcplist(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            int migratetype, unsigned int alloc_flags)
{
    struct per_cpu_pages *pcp;
    struct list_head *list;
    struct page *page;
    unsigned long flags;
    unsigned long __maybe_unused UP_flags;

    /*
     * spin_trylock may fail due to a parallel drain. In the future, the
     * trylock will also protect against IRQ reentrancy.
     */
    pcp_trylock_prepare(UP_flags);
    pcp = pcp_spin_trylock_irqsave(zone->per_cpu_pageset, flags);
    if (!pcp) {
        pcp_trylock_finish(UP_flags);
        return NULL;
    }

    /*
     * On allocation, reduce the number of pages that are batch freed.
     * See nr_pcp_free() where free_factor is increased for subsequent
     * frees.
     */
    pcp->free_factor >>= 1;
    list = &pcp->lists[order_to_pindex(migratetype, order)];
    page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list);
    pcp_spin_unlock_irqrestore(pcp, flags);
    pcp_trylock_finish(UP_flags);
    if (page) {
        __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
        zone_statistics(preferred_zone, zone, 1);
    }
    return page;
}
static inline
struct page *__rmqueue_pcplist(struct zone *zone, unsigned int order,
            int migratetype,
            unsigned int alloc_flags,
            struct per_cpu_pages *pcp,
            struct list_head *list)
{
    struct page *page;

    do {
        if (list_empty(list)) {
            int batch = READ_ONCE(pcp->batch);
            int alloced;

            /*
             * Scale batch relative to order if batch implies
             * free pages can be stored on the PCP. Batch can
             * be 1 for small zones or for boot pagesets which
             * should never store free pages as the pages may
             * belong to arbitrary zones.
             */
            if (batch > 1)
                batch = max(batch >> order, 2);
            alloced = rmqueue_bulk(zone, order,
                    batch, list,
                    migratetype, alloc_flags);

            pcp->count += alloced << order;
            if (unlikely(list_empty(list)))
                return NULL;
        }

        page = list_first_entry(list, struct page, pcp_list);
        list_del(&page->pcp_list);
        pcp->count -= 1 << order;
    } while (check_new_pcp(page, order));

    return page;
}

이미 PCP에 쓸 수 있는 block이 있다면 zone lock을 잡지 않고 반환할 수 있지만, 비어 있어 refill하면 아래의 rmqueue_bulk()가 zone lock을 잡습니다. 따라서 PCP 경로 전체가 언제나 zone lock을 회피하는 것은 아닙니다. 바깥 함수의 trylock 실패는 준비 상태를 복구한 뒤 NULL을 반환하고, 성공한 경우에는 PCP lock을 푼 뒤 allocation 통계를 갱신합니다. 이 tag의 PGALLOC event 수는 1 << order인 base-page 단위입니다.

refill의 batch > 1 조건도 중요합니다. 그때만 max(batch >> order, 2)로 요청 block 수를 조정합니다. batch가 1인 boot pageset 등의 경우까지 최소 2 block을 강제하는 공식이 아닙니다. 실제 받은 block 수가 alloced이고 PCP count에는 alloced << order를 더합니다. pop 하나의 감소량 역시 1 << order입니다.

다음은 같은 파일의 rmqueue_bulk() 전문입니다. 이 함수는 __alloc_pages_bulk()와 이름이 비슷하지만 caller의 PCP list를 refill하는 별도 경로입니다.

static int rmqueue_bulk(struct zone *zone, unsigned int order,
            unsigned long count, struct list_head *list,
            int migratetype, unsigned int alloc_flags)
{
    int i, allocated = 0;

    /* Caller must hold IRQ-safe pcp->lock so IRQs are disabled. */
    spin_lock(&zone->lock);
    for (i = 0; i < count; ++i) {
        struct page *page = __rmqueue(zone, order, migratetype,
                                alloc_flags);
        if (unlikely(page == NULL))
            break;

        if (unlikely(check_pcp_refill(page, order)))
            continue;

        /*
         * Split buddy pages returned by expand() are received here in
         * physical page order. The page is added to the tail of
         * caller's list. From the callers perspective, the linked list
         * is ordered by page number under some conditions. This is
         * useful for IO devices that can forward direction from the
         * head, thus also in the physical page order. This is useful
         * for IO devices that can merge IO requests if the physical
         * pages are ordered properly.
         */
        list_add_tail(&page->pcp_list, list);
        allocated++;
        if (is_migrate_cma(get_pcppage_migratetype(page)))
            __mod_zone_page_state(zone, NR_FREE_CMA_PAGES,
                          -(1 << order));
    }

    /*
     * i pages were removed from the buddy list even if some leak due
     * to check_pcp_refill failing so adjust NR_FREE_PAGES based
     * on i. Do not confuse with 'allocated' which is the number of
     * pages added to the pcp list.
     */
    __mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
    spin_unlock(&zone->lock);
    return allocated;
}

caller가 IRQ-safe PCP lock을 가진 상태에서 zone lock을 한 번 잡습니다. i는 buddy에서 제거한 block 수, allocated는 검사를 통과하여 PCP list에 실제 넣은 block 수입니다. 잘못된 page를 거르면 두 수가 달라질 수 있으므로 buddy의 NR_FREE_PAGES 감소와 PCP 증가에 같은 변수를 쓰지 않습니다. CMA accounting도 보존되어 있습니다. 반환 순서가 조건에 따라 physical page 순서에 유리할 수 있어도 서로 다른 block 모두의 물리 연속성을 보장하지는 않습니다.

Debug 설정은 검사 시점도 바꿉니다

다음은 같은 tag의 두 PCP 검사 helper가 정의되는 전체 조건부 영역입니다. SPDX는 GPL-2.0-only입니다.

#ifdef CONFIG_DEBUG_VM
/*
 * With DEBUG_VM enabled, order-0 pages are checked for expected state when
 * being allocated from pcp lists. With debug_pagealloc also enabled, they are
 * also checked when pcp lists are refilled from the free lists.
 */
static inline bool check_pcp_refill(struct page *page, unsigned int order)
{
    if (debug_pagealloc_enabled_static())
        return check_new_pages(page, order);
    else
        return false;
}

static inline bool check_new_pcp(struct page *page, unsigned int order)
{
    return check_new_pages(page, order);
}
#else
/*
 * With DEBUG_VM disabled, free order-0 pages are checked for expected state
 * when pcp lists are being refilled from the free lists. With debug_pagealloc
 * enabled, they are also checked when being allocated from the pcp lists.
 */
static inline bool check_pcp_refill(struct page *page, unsigned int order)
{
    return check_new_pages(page, order);
}
static inline bool check_new_pcp(struct page *page, unsigned int order)
{
    if (debug_pagealloc_enabled_static())
        return check_new_pages(page, order);
    else
        return false;
}
#endif /* CONFIG_DEBUG_VM */

DEBUG_VM이 있으면 pop을 항상 검사하고 runtime debug-pagealloc이 켜진 경우 refill도 검사합니다. DEBUG_VM이 없으면 반대로 refill을 항상 검사하고 runtime debug-pagealloc이 켜진 경우 pop도 검사합니다. 그러므로 CONFIG_DEBUG_PAGEALLOC 하나만 보고 검사 유무를 단정할 수 없습니다. 검사 실패 뒤의 loop는 잘못된 block을 caller에게 정상 결과로 넘기지 않기 위한 경로입니다.

참고 자료