# Buddy의 큰 block과 PCP의 빠른 재사용은 어떻게 공존합니까

page가 해제되었다고 해서 즉시 큰 연속 block으로 합쳐지는 것은 아닙니다. CPU가 곧 다시 사용할 수 있도록 PCP에 보관할 수 있기 때문입니다. 작은 allocation의 lock 경합을 줄이는 선택과 큰 block을 만들기 위한 coalescing 사이에는 이런 시간차가 있습니다.

buddy와 PCP는 서로 다른 allocator가 독립된 RAM을 소유하는 구조라기보다, 같은 zone의 free memory를 서로 다른 granularity와 동기화 비용으로 다루는 계층입니다. Linux `v6.1`의 자료 구조를 기준으로 두 계층을 연결해 보겠습니다.

## Buddy는 정렬된 power-of-two block을 관리합니다

order-k block은 `2^k`개의 연속한 base page입니다. 같은 order의 짝은 시작 PFN에서 bit k를 뒤집어 얻습니다. 예를 들어 order-2의 PFN 8 block은 8부터 11까지이고 buddy는 PFN 12부터 15까지입니다. PFN 4부터 7까지도 인접하지만 이 block의 buddy는 아닙니다. 정렬된 parent 안에서 절반을 나누는 관계가 중요합니다.

후보 PFN은 XOR로 계산하지만 그 결과가 실제 합병 가능한 buddy라는 보장은 없습니다. 후보가 free buddy 또는 허용된 debug guard인지, 같은 order인지, 같은 zone인지 확인해야 합니다. 주소 계산식을 임의 PFN의 descriptor 유효성 검사로 사용해서도 안 됩니다.

합병이 되면 두 시작 PFN의 AND가 parent 시작점을 만들고 order가 하나 커집니다. allocation에서는 반대로 큰 block을 절반씩 나누어 필요한 order를 남깁니다. order 수에 비례하는 이 탐색 구조를 설명할 때의 복잡도와, reclaim·compaction·lock 대기까지 포함한 allocation 전체 latency는 별개의 이야기입니다.

![buddy의 split과 merge를 정렬된 PFN 범위로 설명합니다. 한 zone의 단순한 예이며 migrate type, debug guard, reserve와 동시 allocation은 생략했습니다. 인접한 모든 범위가 buddy인 것은 아닙니다.](assets/buddy_split_merge.png)

## Free area의 숫자는 page 수가 아니라 block 수입니다

zone의 `free_area[order]`에는 migrate type별 `free_list`와 `nr_free`가 있습니다. order-3의 `nr_free`가 2이면 base page 두 개가 아니라 8-page block 두 개입니다. 전체 page 수로 해석하려면 order에 따른 가중치가 필요합니다.

list에는 block의 대표 descriptor가 연결됩니다. 그 block 안의 모든 descriptor를 같은 list에 넣는 것은 아닙니다. head의 buddy 표시와 private order를 통해 block 크기를 해석하며, split·merge 때 list와 count를 함께 갱신합니다. 이 공통 자료를 보호하는 것이 `zone->lock`입니다.

`v6.1`의 list 조작은 buddy 쪽에서 `page->buddy_list`, PCP 쪽에서 `page->pcp_list`라는 member 이름을 사용합니다. 이들은 descriptor의 union 공간을 재사용하므로 같은 page가 두 계층의 list에 동시에 연결되어 있다고 읽으면 안 됩니다. `add_to_free_list()`·`add_to_free_list_tail()`은 list에 추가하고 `nr_free`를 늘리며, `move_to_free_list()`는 type별 list 사이에서 소속을 옮깁니다. `del_page_from_free_list()`는 제거와 buddy 상태·order 정리, block count 감소를 수행합니다.

`/proc/buddyinfo`에서 높은 order가 비었다고 모든 page가 사용 중인 것은 아닙니다. 낮은 order로 흩어져 있거나 PCP에 있어 아직 해당 buddy list에 나타나지 않을 수 있습니다. 반대로 높은 order block이 있어도 caller의 zone·migrate type·watermark 제약 때문에 사용할 수 없을 수 있습니다.

## PCP는 이미 free인 page를 CPU 가까이에 둡니다

`struct per_cpu_pages`에는 자체 lock, `count`, `high`, `batch`, `free_factor`, 여러 list가 있습니다. `count`는 base-page 단위입니다. list에서 order-3 block 하나를 꺼내면 count는 1이 아니라 8만큼 변합니다. `high`는 cache 상한 판단에, `batch`는 refill·drain 단위에 쓰입니다.

PCP가 지원하는 order는 제한됩니다. `v6.1`의 낮은 order 범위는 order 0부터 `PAGE_ALLOC_COSTLY_ORDER`인 3까지이며 configuration에 따른 THP 관련 order도 list index에 포함될 수 있습니다. 모든 higher-order allocation을 PCP가 캐시한다고 일반화하면 안 됩니다. `NR_PCP_LISTS`는 지원 migrate type과 order의 조합을 담으며 `order_to_pindex()`와 `pcp_allowed_order()`의 조건을 함께 보아야 합니다.

PCP에 들어간 page는 이전 owner에게서 반환된 free memory이지만 아직 buddy의 합병 대상은 아닙니다. 같은 CPU의 재사용에는 유리하고 큰 연속 block 구성에는 지연이 생길 수 있습니다. 이 지연을 제한하려고 high 초과, reclaim, 주기적인 drain 같은 정책이 필요합니다.

![PCP-free와 buddy-free는 coalescing 가능 시점이 다릅니다. 한 zone의 개념도이며 PCP 지원 order, batch 조절과 모든 drain trigger는 생략했습니다.](assets/more_pcp_coalescing.png)

## Free라는 소유 상태와 NR_FREE_PAGES counter를 구별합니다

`v6.1`에서 PCP page는 owner에게서 반환된 free memory이지만 `NR_FREE_PAGES`에 항상 포함되는 것은 아닙니다. [`rmqueue_bulk()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L3124-L3167)는 buddy에서 꺼낸 양을 이 counter에서 빼고, [`__rmqueue_pcplist()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L3734-L3772)는 PCP로 실제 받은 양을 `pcp->count`에 더합니다. PCP에서 caller에게 하나를 넘길 때는 PCP count를 줄이지, 같은 양을 `NR_FREE_PAGES`에서 다시 빼지 않습니다.

유효한 일반 page n개가 이동하고 동시 변경·debug guard·isolation·compaction capture가 없다고 한정하면, 장부의 이동은 다음과 같습니다. n은 block 개수가 아닌 base page 수입니다.

| 이동 | NR_FREE_PAGES 변화 | 해당 PCP count 변화 |
| --- | --- | --- |
| buddy → PCP refill | -n | +n |
| PCP → caller | 0 | -n |
| caller → PCP | 0 | +n |
| PCP → buddy drain | +n | -n |
| buddy → caller 직접 획득 | -n | 0 |
| caller → buddy 직접 반환 | +n | 0 |

[`free_unref_page_commit()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L3442-L3469)는 list에 넣고 PCP count를 늘리며, high 조건 때문에 곧 drain할 수도 있습니다. [`free_pcppages_bulk()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L1546-L1620)가 PCP에서 제거하고 `__free_one_page()`로 돌려줄 때 일반 buddy free accounting이 증가합니다. 이때 사용하는 [`__mod_zone_freepage_state()`](https://github.com/torvalds/linux/blob/v6.1/include/linux/vmstat.h#L484-L490)는 `NR_FREE_PAGES`를 갱신하고 CMA이면 CMA counter도 별도로 갱신합니다. 검증에 실패하여 PCP에 넣지 못한 page가 있으면 refill의 'buddy에서 제거한 수'와 'PCP에 추가한 수'가 다를 수 있어 `rmqueue_bulk()`는 두 수를 따로 셉니다.

진단 출력의 [`show_free_areas()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L6197-L6251)도 `free`와 `free_pcp`를 따로 표시합니다. 여기서 `free_pcp`는 해당 zone의 online CPU별 PCP count를 합한 값이고 `local_pcp`는 현재 CPU의 count입니다. 따라서 `free_pcp + local_pcp`는 같은 local 부분을 두 번 더합니다. `NR_FREE_PAGES`에 PCP가 이미 항상 들어 있다고 주장하는 것도 반대 방향의 오류입니다. 마지막으로 vmstat의 per-CPU 갱신 지연과 순차적으로 읽은 snapshot 차이가 있으므로 진단 숫자를 한 순간의 원자적 보존식으로 대입하지는 않습니다.

## Per-CPU라고 lock이 사라지는 것은 아닙니다

현재 CPU의 PCP pointer를 구한 다음 task가 다른 CPU로 이동하면 잘못된 cache를 조작할 수 있습니다. allocation 경로는 CPU 고정과 PCP lock을 연결하고 IRQ 상태도 관리합니다. PREEMPT_RT인지 등에 따라 preemption·migration 제어 방식이 달라질 수 있어 `this_cpu_ptr()` 한 줄만으로 동기화가 끝나지 않습니다.

trylock 실패 시 buddy로 우회할 수 있다는 점도 중요합니다. PCP는 반드시 통과해야 하는 소유권 관문이 아니라 빠른 공급 경로입니다. remote drain이나 관리 경로도 존재하므로 다른 CPU가 절대 관여하지 않는 자료라고 전제할 수 없습니다.

allocation이 PCP에서 성공하면 `free_factor`를 낮추고, 연속 free에서는 drain 양을 키우는 방향으로 조정합니다. `free_factor`는 단순한 page 개수가 아니라 batch shift에 쓰는 지수 성격의 값입니다. 증가를 “매번 factor 자체를 두 배로 만든다”라고 설명하면 실제 계산과 어긋납니다.

## Boot pageset에서 zone별 pageset으로 전환합니다

boot 초기에는 완전한 per-CPU allocation 준비 전에도 page allocator가 동작해야 합니다. `boot_pageset`은 `high = 0`, `batch = 1`과 같은 최소 설정으로 사용되고, 이후 `setup_zone_pageset()`이 zone별 per-CPU storage와 list를 준비합니다. list head 초기화와 threshold 설정이 실제 cache 사용에 앞서야 합니다.

`zone_batchsize()`의 기본 계산은 managed 규모의 일부와 약 1 MiB에 해당하는 page 수 중 작은 값을 출발점으로 삼고, 추가 축소와 최소값·power-of-two 관련 조정을 거칩니다. `v6.1`에서는 managed를 1,024로 나눈 값과 `SZ_1M / PAGE_SIZE`를 비교한 뒤 4로 나누는 단계가 있습니다. 따라서 batch가 언제나 정확히 1 MiB라는 설명은 맞지 않습니다.

high는 zone의 watermark나 CPU 수, `percpu_pagelist_high_fraction` 설정 등의 영향을 받습니다. 갱신에는 `READ_ONCE()`·`WRITE_ONCE()`가 쓰이지만 high와 batch를 한 번에 바꾸는 atomic multi-field transaction은 아닙니다. 잠깐 다른 세대의 값을 읽더라도 안전하도록 설계된 경로라는 점과 완전한 snapshot 보장은 구별해야 합니다.

vmstat의 주기적인 갱신에서는 한동안 사용하지 않은 remote zone의 pageset을 지연 drain하는 처리도 연결됩니다. local CPU가 가까운 공급원을 빠르게 재사용하는 목적과, 사용하지 않는 remote cache를 계속 붙잡지 않는 목적을 함께 만족시키려는 정책입니다. `/proc/zoneinfo`는 zone별 watermark·pageset·통계를 읽는 데 도움이 되지만 모든 CPU의 상태를 동시에 고정한 snapshot은 아닙니다.

`v6.1`의 `refresh_cpu_vm_stats()`는 zone 통계의 per-CPU 차이를 반영했을 때 NUMA PCP의 `expire`를 3으로 다시 설정합니다. pageset 처리가 허용되고 count·expire가 남아 있는 경우에만 만료를 진행하며, 현재 CPU에 local인 zone은 expire를 지우고 이 remote-drain 경로에서 제외합니다. remote zone의 expire가 감소해 0이 되면 `drain_zone_pages()`가 count와 batch 중 작은 양을 반환합니다. 이를 모든 local PCP를 주기마다 전부 비우는 규칙이나 정확한 wall-clock 만료 보장으로 읽어서는 안 됩니다.

PCP는 빠른 재사용을 위해 buddy의 합병을 잠시 미룹니다. 이 구조를 알면 free count, buddy order 분포, allocation latency가 왜 같은 숫자로 설명되지 않는지 이해할 수 있습니다.

## Buddy 후보를 검증하는 세 helper를 함께 읽습니다

다음은 [Linux `v6.1`의 `mm/internal.h`](https://github.com/torvalds/linux/blob/v6.1/mm/internal.h#L271-L342)에 있는 세 함수의 전문입니다. SPDX는 `GPL-2.0-or-later`이며 들여쓰기만 정리했습니다. 호출자는 hole이 아닌 유효 descriptor 범위를 확보해야 하고, buddy 상태·order의 변경은 zone lock과 함께 해석해야 합니다.

```c
/*
 * Locate the struct page for both the matching buddy in our
 * pair (buddy1) and the combined O(n+1) page they form (page).
 *
 * 1) Any buddy B1 will have an order O twin B2 which satisfies
 * the following equation:
 *     B2 = B1 ^ (1 << O)
 * For example, if the starting buddy (buddy2) is #8 its order
 * 1 buddy is #10:
 *     B2 = 8 ^ (1 << 1) = 8 ^ 2 = 10
 *
 * 2) Any buddy B will have an order O+1 parent P which
 * satisfies the following equation:
 *     P = B & ~(1 << O)
 *
 * Assumption: *_mem_map is contiguous at least up to MAX_ORDER
 */
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
    return page_pfn ^ (1 << order);
}
```

```c
static inline struct page *find_buddy_page_pfn(struct page *page,
            unsigned long pfn, unsigned int order, unsigned long *buddy_pfn)
{
    unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order);
    struct page *buddy;

    buddy = page + (__buddy_pfn - pfn);
    if (buddy_pfn)
        *buddy_pfn = __buddy_pfn;

    if (page_is_buddy(page, buddy, order))
        return buddy;
    return NULL;
}
```

```c
static inline bool page_is_buddy(struct page *page, struct page *buddy,
                 unsigned int order)
{
    if (!page_is_guard(buddy) && !PageBuddy(buddy))
        return false;

    if (buddy_order(buddy) != order)
        return false;

    /*
     * zone check is done late to avoid uselessly calculating
     * zone/node ids for pages that could never merge.
     */
    if (page_zone_id(page) != page_zone_id(buddy))
        return false;

    VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);

    return true;
}
```

`find_buddy_page_pfn()`은 PFN 차이로 descriptor 위치를 얻습니다. buddy PFN이 더 작을 때의 unsigned 차이를 포함하는 kernel의 mem_map 배치 관례이며, 임의 user-space pointer에 적용할 안전한 산술 예제는 아닙니다. 이 함수 자체가 `pfn_valid()`나 memory hotplug 수명 보호를 대신하지도 않습니다.

`page_is_buddy()`의 첫 조건에는 debug guard 예외가 있습니다. 이어 order와 zone ID를 비교하고, 정상 buddy의 refcount가 0인지 assertion으로 확인합니다. refcount가 0이라는 조건 하나로 모든 descriptor를 free block으로 판정하는 순서가 아닙니다.

## Free-list와 PCP 선언의 전체 범위입니다

다음 두 선언은 [`include/linux/mmzone.h`](https://github.com/torvalds/linux/blob/v6.1/include/linux/mmzone.h)의 전문이며 SPDX는 `GPL-2.0`입니다. `nr_free`는 block 수, PCP의 `count`는 base page 수라는 단위 차이를 코드 옆에 두고 읽어야 합니다.

```c
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long        nr_free;
};
```

```c
struct per_cpu_pages {
    spinlock_t lock;    /* Protects lists field */
    int count;        /* number of pages in the list */
    int high;        /* high watermark, emptying needed */
    int batch;        /* chunk size for buddy add/remove */
    short free_factor;    /* batch scaling factor during free */
#ifdef CONFIG_NUMA
    short expire;        /* When 0, remote pagesets are drained */
#endif

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    struct list_head lists[NR_PCP_LISTS];
} ____cacheline_aligned_in_smp;
```

같은 tag의 PCP list 수 정의는 다음과 같습니다. macro의 전체 정의를 보인 것이며, 모든 supporting header를 포함하는 독립 실행 프로그램은 아닙니다.

```c
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
#define NR_PCP_THP 1
#else
#define NR_PCP_THP 0
#endif
#define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1))
#define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP)

```

낮은 order용 list 수는 migrate type 수에 `PAGE_ALLOC_COSTLY_ORDER + 1`을 곱합니다. THP 구성에서는 별도의 list **하나**를 그 합에 더합니다. THP list까지 세 migrate type으로 다시 곱하는 공식이 아닙니다. THP가 없으면 추가분은 0입니다. `expire`도 NUMA 구성에만 있으며, remote zone의 사용하지 않는 cache를 뒤늦게 drain하는 정책에 쓰입니다.

## Batch와 high의 실제 계산은 서로 다릅니다

다음은 [`mm/page_alloc.c`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L7065-L7158)의 두 함수 전문입니다. SPDX는 `GPL-2.0-only`입니다. NOMMU에서는 batching과 지연 free가 연속 범위를 깨뜨릴 수 있어 두 함수가 0을 반환하는 분기도 보존했습니다.

```c
static int zone_batchsize(struct zone *zone)
{
#ifdef CONFIG_MMU
    int batch;

    /*
     * The number of pages to batch allocate is either ~0.1%
     * of the zone or 1MB, whichever is smaller. The batch
     * size is striking a balance between allocation latency
     * and zone lock contention.
     */
    batch = min(zone_managed_pages(zone) >> 10, SZ_1M / PAGE_SIZE);
    batch /= 4;        /* We effectively *= 4 below */
    if (batch < 1)
        batch = 1;

    /*
     * Clamp the batch to a 2^n - 1 value. Having a power
     * of 2 value was found to be more likely to have
     * suboptimal cache aliasing properties in some cases.
     *
     * For example if 2 tasks are alternately allocating
     * batches of pages, one task can end up with a lot
     * of pages of one half of the possible page colors
     * and the other with pages of the other colors.
     */
    batch = rounddown_pow_of_two(batch + batch/2) - 1;

    return batch;

#else
    /* The deferral and batching of frees should be suppressed under NOMMU
     * conditions.
     *
     * The problem is that NOMMU needs to be able to allocate large chunks
     * of contiguous memory as there's no hardware page translation to
     * assemble apparent contiguous memory from discontiguous pages.
     *
     * Queueing large contiguous runs of pages for batching, however,
     * causes the pages to actually be freed in smaller chunks.  As there
     * can be a significant delay between the individual batches being
     * recycled, this leads to the once large chunks of space being
     * fragmented and becoming unavailable for high-order allocations.
     */
    return 0;
#endif
}
```

```c
static int zone_highsize(struct zone *zone, int batch, int cpu_online)
{
#ifdef CONFIG_MMU
    int high;
    int nr_split_cpus;
    unsigned long total_pages;

    if (!percpu_pagelist_high_fraction) {
        /*
         * By default, the high value of the pcp is based on the zone
         * low watermark so that if they are full then background
         * reclaim will not be started prematurely.
         */
        total_pages = low_wmark_pages(zone);
    } else {
        /*
         * If percpu_pagelist_high_fraction is configured, the high
         * value is based on a fraction of the managed pages in the
         * zone.
         */
        total_pages = zone_managed_pages(zone) / percpu_pagelist_high_fraction;
    }

    /*
     * Split the high value across all online CPUs local to the zone. Note
     * that early in boot that CPUs may not be online yet and that during
     * CPU hotplug that the cpumask is not yet updated when a CPU is being
     * onlined. For memory nodes that have no CPUs, split pcp->high across
     * all online CPUs to mitigate the risk that reclaim is triggered
     * prematurely due to pages stored on pcp lists.
     */
    nr_split_cpus = cpumask_weight(cpumask_of_node(zone_to_nid(zone))) + cpu_online;
    if (!nr_split_cpus)
        nr_split_cpus = num_online_cpus();
    high = total_pages / nr_split_cpus;

    /*
     * Ensure high is at least batch*4. The multiple is based on the
     * historical relationship between high and batch.
     */
    high = max(high, batch << 2);

    return high;
#else
    return 0;
#endif
}
```

`zone_set_pageset_high_and_batch()`는 실제 batch를 최소 1로 보정하여 사용합니다. high의 기본 기반은 LOW watermark이고, fraction을 명시한 경우에는 managed 수의 해당 비율입니다. node에 가까운 online CPU들로 나누되 memory-only node라면 전체 online CPU 수를 사용합니다. CPU hotplug 중 추가 CPU를 반영하는 인수도 있으며 최종 high는 적어도 `batch * 4`입니다.

## 연속 free에서는 배출량의 지수를 조정합니다

다음 [`nr_pcp_free()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L3395-L3422) 전문은 factor와 실제 page 수의 관계를 보여 줍니다.

```c
static int nr_pcp_free(struct per_cpu_pages *pcp, int high, int batch,
               bool free_high)
{
    int min_nr_free, max_nr_free;

    /* Free everything if batch freeing high-order pages. */
    if (unlikely(free_high))
        return pcp->count;

    /* Check for PCP disabled or boot pageset */
    if (unlikely(high < batch))
        return 1;

    /* Leave at least pcp->batch pages on the list */
    min_nr_free = batch;
    max_nr_free = high - batch;

    /*
     * Double the number of pages freed each time there is subsequent
     * freeing of pages without any allocation.
     */
    batch <<= pcp->free_factor;
    if (batch < max_nr_free)
        pcp->free_factor++;
    batch = clamp(batch, min_nr_free, max_nr_free);

    return batch;
}
```

`free_factor`가 1 늘면 다음 계산의 shift가 커져 배출량이 두 배 방향으로 갑니다. factor 자체를 곱하기 2 하는 코드는 아닙니다. 결과는 최소 batch와 `high - batch` 사이로 제한됩니다. 반대로 `rmqueue_pcplist()`의 allocation은 `free_factor >>= 1`로 이 지수를 낮춥니다. high-order를 집중적으로 반환하는 `free_high` 조건에서는 PCP count 전체를 drain 대상으로 삼을 수 있습니다.

같은 파일의 [`nr_pcp_high()`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L3424-L3440)는 이때의 실제 high를 정합니다.

```c
static int nr_pcp_high(struct per_cpu_pages *pcp, struct zone *zone,
               bool free_high)
{
    int high = READ_ONCE(pcp->high);

    if (unlikely(!high || free_high))
        return 0;

    if (!test_bit(ZONE_RECLAIM_ACTIVE, &zone->flags))
        return high;

    /*
     * If reclaim is active, limit the number of pages that can be
     * stored on pcp lists
     */
    return min(READ_ONCE(pcp->batch) << 2, high);
}
```

설정 high가 0이거나 `free_high`이면 0을 사용하고, reclaim이 active일 때는 `batch * 4`와 high 중 작은 값까지 줄입니다. 따라서 설정의 high, 이번 free에서 적용하는 high, 실제 배출량은 서로 다른 값입니다.

## PCP의 cached type은 index slot을 다시 사용합니다

다음은 [Linux `v6.1`의 `mm/page_alloc.c`](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L301-L309)에 있는 두 helper 전문입니다. SPDX는 `GPL-2.0-only`이며 들여쓰기를 정리했습니다.

```c
static inline int get_pcppage_migratetype(struct page *page)
{
    return page->index;
}
```

```c
static inline void set_pcppage_migratetype(struct page *page, int migratetype)
{
    page->index = migratetype;
}
```

같은 `page->index`라도 file-backed 역할의 mapping index와 PCP-free 역할의 cached migrate type은 다른 시점의 해석입니다. 둘이 동시에 독립된 field로 존재하지 않습니다. PCP list 선택에 쓰는 일반 type으로 정규화한 값과 descriptor에 기록한 pageblock type도 구별해야 합니다. 나중에 isolation이 생길 수 있는 drain 경로는 zone lock 아래에서 현재 pageblock type을 다시 확인합니다.

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access, 2025, Physical Memory 장.
- Linux `v6.1`: [free_area와 per_cpu_pages](https://github.com/torvalds/linux/blob/v6.1/include/linux/mmzone.h), [buddy helper·PCP setup·zone_batchsize](https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c), [vmstat 갱신과 remote pageset drain](https://github.com/torvalds/linux/blob/v6.1/mm/vmstat.c).
- Linux man-pages: [proc_buddyinfo(5)](https://man7.org/linux/man-pages/man5/proc_buddyinfo.5.html).
