MOVABLE pageblock이라고 지금 모든 page를 옮길 수 있는 것은 아닙니다

큰 연속 allocation이 필요한데 free page가 작은 조각으로 흩어져 있으면 총량만으로는 해결되지 않습니다. 장기간 움직일 수 없는 page 하나가 큰 범위의 합병을 막을 수도 있습니다. Linux는 비슷한 성격의 allocation을 가까이 배치하려고 migrate type을 사용합니다.

이 분류는 개별 page의 현재 상태를 증명하는 인증서가 아닙니다. “가능하면 이런 성격의 allocation을 이 범위에 모으겠다”는 allocator 정책입니다. Linux v6.1의 pageblock과 free list를 기준으로 그 경계를 살펴보겠습니다.

Migration과 reclaim은 다른 방법으로 공간을 만듭니다

migration은 내용과 논리적 소유 관계를 유지한 채 다른 physical page로 옮기는 일입니다. 이를 위해서는 mapping 변경, reference 확인, lock과 해당 subsystem의 migration protocol이 필요합니다. 저장된 주소를 다른 값으로 바꾸는 것만으로 끝나지 않습니다.

reclaim은 지금 차지한 memory를 더 이상 보관하지 않아도 되도록 만드는 일입니다. clean file cache를 버리거나, 필요한 writeback 뒤 cache를 반환하거나, slab shrinker가 사용하지 않는 object를 회수하는 경우가 해당합니다. reclaimable object가 원래 위치에서 사라질 수 있다는 사실과 살아 있는 object를 임의로 migration할 수 있다는 사실은 다릅니다.

UNMOVABLE은 일반적인 page migration으로 재배치하기 어려운 allocation을 뜻합니다. kernel object를 가리키는 pointer를 모든 소유자가 추적·수정할 수 없다면 물리 위치 변경이 곤란합니다. MOVABLE은 migration 또는 reclaim을 통해 범위를 비울 수 있는 용도와 연결됩니다. RECLAIMABLE은 주로 shrinker로 회수할 수 있는 kernel allocation을 구별합니다.

slab의 reclaimable 분류와 관련된 cache flag는 SLAB_RECLAIM_ACCOUNT입니다. memcg accounting에 관한 SLAB_ACCOUNT와 이름이 비슷하지만 목적이 다릅니다. memory를 어느 cgroup에 청구하는지와 그 allocation을 reclaimable 성격으로 배치하는지는 별도 질문입니다.

Pageblock은 grouping이고 free list는 현재의 공급원입니다

pageblock에는 migrate type이 기록되고 buddy의 각 order에는 migrate type별 free list가 있습니다. allocator는 먼저 요청과 같은 type의 free block을 찾아, 서로 다른 성격이 한 pageblock에 섞이는 일을 줄입니다. 이때 type은 모든 allocated page의 상태를 실시간으로 세어 얻는 완벽한 분류가 아닙니다.

초기에 MOVABLE로 분류된 범위가 다른 요청에 할당되면서 type이 바뀔 수 있습니다. 이미 사용 중인 page를 그 자리에서 migration해서 type을 바꾸는 것이 아니라, pageblock의 policy와 그 안의 free block이 속한 list를 조정하는 것입니다. fallback이 쌓이면 한 pageblock 안에 여러 성격이 섞일 수도 있습니다.

v6.1의 초기화는 memmap_init_zone_range()에서 memmap_init_range()로 이어지고, pageblock 경계의 descriptor에서 set_pageblock_migratetype()를 호출합니다. 일반 boot 범위에는 MOVABLE을 전달하여 오래 사는 kernel allocation이 곳곳에 먼저 흩어지는 것을 줄입니다. 다만 memmap_init_range()는 migratetype 인수를 받으므로 모든 호출·모든 device 범위가 조건 없이 같은 초기값이라는 뜻은 아닙니다. hole·deferred 초기화와 hotplug의 reserved 처리는 각각 별도 분기입니다.

pageblock은 2^pageblock_order개의 base page로 자연 정렬됩니다. 4 KiB base page와 order 9를 가정하면 2 MiB이지만 이 수치는 build마다 고정된 ABI가 아닙니다. 일반 free 경로는 pageblock에서 type을 읽어 list를 선택하고, PCP에 넣기 전에는 그 type을 descriptor에 따로 저장해 빠르게 사용합니다. PCP에 기록한 type과 pageblock 정책의 저장 위치가 같다고 가정하면 안 됩니다.

이동 가능한 page가 빠져나간 뒤 남는 free 범위를 비교합니다. U는 남아 있는 unmovable allocation이며, 함께 모아 두면 정렬된 order-2 후보가 남을 수 있습니다. 실제 pageblock 크기를 나타내는 그림은 아니며 migration 성공이나 현재 pin 부재를 보장하지 않습니다.
이동 가능한 page가 빠져나간 뒤 남는 free 범위를 비교합니다. U는 남아 있는 unmovable allocation이며, 함께 모아 두면 정렬된 order-2 후보가 남을 수 있습니다. 실제 pageblock 크기를 나타내는 그림은 아니며 migration 성공이나 현재 pin 부재를 보장하지 않습니다.

그러므로 MOVABLE pageblock 안의 page가 pin되어 있다면 해당 순간 migration이 막힐 수 있습니다. 반대로 다른 type의 pageblock 안에도 이동 가능한 page가 섞여 있을 수 있습니다. compaction이 실제로 page를 옮길 수 있는지 확인하는 단계와 allocation 때 grouping하는 단계는 서로 보완하지만 같은 검사가 아닙니다.

실제 enum에서도 조건부 항목을 확인할 수 있습니다. 다음은 Linux v6.1의 enum migratetype 전문입니다. SPDX는 GPL-2.0이며 들여쓰기만 정리했습니다.

enum migratetype {
    MIGRATE_UNMOVABLE,
    MIGRATE_MOVABLE,
    MIGRATE_RECLAIMABLE,
    MIGRATE_PCPTYPES,    /* the number of types on the pcp lists */
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    /*
     * MIGRATE_CMA migration type is designed to mimic the way
     * ZONE_MOVABLE works.  Only movable pages can be allocated
     * from MIGRATE_CMA pageblocks and page allocator never
     * implicitly change migration type of MIGRATE_CMA pageblock.
     *
     * The way to use it is to change migratetype of a range of
     * pageblocks to MIGRATE_CMA which can be done by
     * __free_pageblock_cma() function.
     */
    MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,    /* can't allocate from here */
#endif
    MIGRATE_TYPES
};

MIGRATE_PCPTYPES는 일반 PCP type 수의 경계이고 HIGHATOMIC은 그 값부터 시작합니다. CMA는 CONFIG_CMA, ISOLATE는 CONFIG_MEMORY_ISOLATION에 달려 있습니다. 따라서 type 수나 뒤쪽 enum 번호를 모든 build에 같은 상수로 고정해서 해석하지 않습니다.

HIGHATOMIC은 일반 fallback 목록의 다음 항목이 아닙니다

MIGRATE_HIGHATOMIC은 sleep·reclaim이 어려운 higher-order atomic 요청을 위해 보호하는 pageblock 분류입니다. 일반 요청이 UNMOVABLE·RECLAIMABLE·MOVABLE list에서 실패했다고 곧바로 같은 규칙으로 소모하는 여유가 아닙니다. reserve 접근 권한과 별도 경로가 필요합니다.

reserve를 무한히 늘릴 수도 없습니다. v6.1의 reserve_highatomic_pageblock()은 zone managed 규모에 대한 상한을 확인하고 zone lock 안에서 다시 검사한 뒤, 일반 mergeable type의 pageblock만 전환합니다. allocator가 부족할 때 reserve를 해제하는 경로도 별도로 존재합니다. HIGHATOMIC은 영구적으로 봉인된 물리 구역보다 운영 상태에 맞춰 관리하는 reserve 정책에 가깝습니다.

MIGRATE_CMA는 contiguous memory allocation을 위한 범위와 연결됩니다. 평소 이동 가능한 용도로 빌려 쓸 수 있어도 일반 UNMOVABLE 요청까지 무제한으로 허용하면 나중의 contiguous allocation을 방해합니다. allocation flag와 migratetype에 따른 CMA 허용 검사가 따로 필요한 이유입니다.

MIGRATE_ISOLATE는 ordinary allocator의 후보로 소비하면 안 되는 범위입니다. memory offlining 등에서 pageblock을 일반 공급원과 분리하는 데 사용합니다. 이름이 다른 여섯 개의 list를 모두 같은 fallback chain에 이어 붙이면 이런 특수 제약을 잃어버립니다.

GFP mobility bit는 주소 zone과도 구별합니다

__GFP_MOVABLE과 __GFP_RECLAIMABLE은 요청의 grouping 의도를 전달합니다. 같은 요청에 두 성격을 동시에 지정하는 것은 정상적인 조합이 아닙니다. zone을 선택하는 __GFP_DMA·__GFP_DMA32·__GFP_HIGHMEM과도 구별해야 합니다.

다만 __GFP_MOVABLE은 zone 선택 표에도 관여합니다. __GFP_HIGHMEM과의 조합은 ZONE_MOVABLE 선택과 연결될 수 있습니다. 그래서 “MOVABLE은 오직 free-list type bit일 뿐이다”라고 말해도 불완전합니다. GFP는 몇 개의 독립적인 이름을 무작정 OR하는 인터페이스가 아니라 허용된 조합을 해석하는 계약입니다.

이 해석에는 global grouping 정책도 관여합니다. 다음은 Linux v6.1의 gfp_migratetype() 전문과 그 계산에 쓰는 local macro 정의입니다. SPDX는 GPL-2.0이며 들여쓰기만 정리했습니다.

#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3

static inline int gfp_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);
    BUILD_BUG_ON((___GFP_RECLAIMABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_RECLAIMABLE);
    BUILD_BUG_ON(((___GFP_MOVABLE | ___GFP_RECLAIMABLE) >>
              GFP_MOVABLE_SHIFT) != MIGRATE_HIGHATOMIC);

    if (unlikely(page_group_by_mobility_disabled))
        return MIGRATE_UNMOVABLE;

    /* Group based on mobility */
    return (__force unsigned long)(gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}
#undef GFP_MOVABLE_MASK
#undef GFP_MOVABLE_SHIFT

page_group_by_mobility_disabled이면 반환 type은 UNMOVABLE입니다. GFP에 MOVABLE bit가 있다는 사실만 보고 언제나 MOVABLE free list를 선택한다고 단정하면 이 분기를 놓칩니다. 두 mobility bit를 동시에 지정하는 조합에는 warning이 있으며, shift 결과가 우연히 HIGHATOMIC 번호와 같아도 그것을 HIGHATOMIC reserve를 요구하는 정상 API로 사용할 수는 없습니다.

Fallback은 혼합을 줄이면서도 실패를 피하려는 절충입니다

같은 type이 부족하면 일반 세 type 사이에서 fallback을 검토합니다. 큰 free block이라면 pageblock 전체의 free 영역을 새 type 쪽으로 옮기는 편이 작은 allocation을 여러 곳에 흩뿌리는 것보다 나을 수 있습니다. 반대로 MOVABLE 요청 하나 때문에 큰 non-movable block을 쪼개는 것을 피하려고 작은 후보를 다시 찾는 분기도 있습니다.

이 선택은 pageblock의 모든 live page가 정확히 같은 type이라는 증명을 하지 않습니다. free page와 유사한 성격의 page 수를 이용한 heuristic으로 ownership을 바꿉니다. 사용 중인 data의 relocation, free-list 이동, pageblock type 변경을 구별해야 fallback trace를 실제 compaction이나 migration 완료로 오해하지 않습니다.

큰 allocation 실패를 볼 때는 free 총량과 함께 분포를 보아야 합니다. migrate type은 그 분포가 장기적으로 나빠지는 것을 줄이려는 장치이지, pin·lock·동시 allocation을 무시하고 연속 범위를 만들어 주는 보장은 아닙니다.

참고 자료