Page flag를 읽을 때 folio와 tail 규칙을 함께 봅니다

PageDirty(page)와 PageTail(page)는 이름이 비슷하지만 같은 저장 공간의 bit를 읽는 함수라고 할 수는 없습니다. 어떤 helper는 flags를 읽고, 어떤 helper는 pointer tag나 page type을 해석합니다. compound page에서는 head의 bit를 볼지 특정 tail의 bit를 볼지도 달라집니다.

folio API가 필요한 이유도 여기에 있습니다. “지금 받은 page가 tail일 수도 있는가”라는 질문을 모든 호출 지점에서 반복하기보다, 전체 allocation 단위를 나타내는 타입과 helper로 의도를 드러내려는 것입니다. Linux v6.1의 표현을 기준으로 읽어 보겠습니다.

Flag 이름과 저장 위치를 일대일로 연결하지 않습니다

struct page.flags의 낮은 영역에는 PG_locked, PG_dirty, PG_lru, PG_active, PG_writeback 등의 상태가 들어갑니다. 높은 영역 일부는 configuration에 따라 node·zone·section 등의 식별 정보에 사용됩니다. 사용할 수 있는 bit 수가 한정되어 있으므로 모든 configuration이 같은 layout을 갖는 것은 아닙니다.

enum에 이름이 있다고 같은 이름의 public helper가 생성되는 것도 아닙니다. v6.1의 PG_arch_1·PG_arch_2와 filesystem cache가 재사용하는 PG_fscache를 보고 임의의 PageArch1()·PageFscache() API를 추측해서는 안 됩니다. 해당 architecture나 filesystem이 정한 접근 경로를 확인해야 합니다.

page lock, dirty/writeback, LRU active/unevictable, swap-backed와 swap-cache, private data, mlock 등은 서로 다른 상태 축입니다. PG_locked가 있다는 사실이 LRU list 변경까지 그 lock 하나로 보호한다는 뜻은 아닙니다. 상태를 읽는 helper와 상태를 바꾸는 protocol을 함께 보아야 합니다.

이름이 Page...인 모든 판별이 PG_... bit에 대응하지도 않습니다. PageTail()은 compound_head의 tag를 이용하고 PageAnon()·PageKsm()·PageMovable() 계열은 mapping tag와 연결됩니다. PageBuddy()·PageTable() 같은 type 판별도 일반 flags와 구별해야 합니다. 같은 bit를 문맥별로 다른 이름으로 해석하는 alias도 있어 이름 개수를 독립 상태 bit 개수로 셀 수 없습니다.

page_type은 userspace mapping count로 쓰지 않는 non-slab page에서 의미가 있습니다. v6.1의 PAGE_TYPE_OPS는 초기값 -1과 구별되는 type encoding을 사용하므로 __SetPageBuddy()가 해당 bit를 지우고 __ClearPageBuddy()가 다시 세우는 역방향 표현입니다. PageOffline()·PageTable()·PageGuard()도 같은 type 계열입니다. “Set이라는 이름이니 raw bit는 항상 1”이라고 가정하면 잘못 읽습니다. PageOffline()은 section 전체의 offline 여부와도 별개입니다.

alias의 의미는 사용 문맥과 함께 읽어야 합니다. PG_readahead는 PG_reclaim과, PG_checked·PG_swapcache는 PG_owner_priv_1과 bit를 공유합니다. anonymous 쪽의 PG_anon_exclusive는 PG_mappedtodisk 위치를 재사용하지만 file의 disk-block 상태를 뜻하지 않습니다. PageAnonExclusive()는 anonymous 조건을 전제로 하며 PTE-mapped THP에서는 tail별 상태가 필요할 수도 있습니다. hugetlb에는 별도 head 조건도 있으므로 이를 모든 page에서 사용할 수 있는 보편적인 ownership bit로 읽으면 안 됩니다.

Atomic helper는 전체 상태 전이를 대신하지 않습니다

보통 PageFoo()는 test, SetPageFoo()와 ClearPageFoo()는 bit 변경, TestSetPageFoo()와 TestClearPageFoo()는 이전 상태를 돌려주면서 변경하는 형태입니다. __SetPageFoo()처럼 double underscore를 쓰는 계열은 non-atomic bit operation을 사용하는 경우가 있습니다. caller가 요구되는 배타성이나 초기화 문맥을 이미 갖추었는지 확인해야 합니다.

이 naming pattern은 모든 flag에 모든 조합이 있다는 약속이 아닙니다. 예를 들어 v6.1의 dirty 관련 helper를 보고 존재하지 않는 __SetPageDirty()를 만들어 호출할 수는 없습니다. writeback이나 uptodate처럼 별도 ordering·accounting 의미가 있는 상태도 단순한 bit 변경으로 대체하면 안 됩니다. atomic bit operation 하나가 mapping, list, count를 함께 원자적으로 바꾸는 것도 아닙니다.

configuration에 따라 등장하는 상태도 있습니다. hardware poison, idle/young tracking, Xen, KASAN, memory hotplug, free-page reporting 관련 helper를 모든 build에 있다고 가정하면 code와 dump 해석이 어긋납니다. PagePoisoned()처럼 poisoned descriptor 자체의 잘못된 사용을 잡는 검사와 실제 data page의 hardware poison 상태도 구별해야 합니다.

Policy macro는 어느 descriptor를 볼지 정합니다

compound에서는 같은 상태 이름이라도 descriptor 선택 정책이 필요합니다. v6.1의 page-flag helper 생성 macro에는 PF_ANY, PF_HEAD, PF_ONLY_HEAD, PF_NO_TAIL, PF_NO_COMPOUND, PF_SECOND 같은 policy가 사용됩니다. 이는 단순한 optimization 옵션이 아니라 API의 입력 조건입니다.

PF_HEAD 계열은 head를 선택하는 동작과 연결되고, PF_ONLY_HEAD나 PF_NO_TAIL은 tail 사용에 대한 제약을 드러냅니다. PF_NO_COMPOUND는 compound 자체를 허용하지 않는 경우이며 PF_SECOND는 first tail 쪽 저장 공간을 선택합니다. assertion이 켜진 build에서 잘못된 입력이 드러날 수 있지만, assertion이 없다고 계약이 사라지는 것은 아닙니다. PF_POISONED_CHECK 계열의 검사가 결합되는 지점도 같은 관점으로 읽어야 합니다.

어떤 flag가 어느 policy를 택했는지를 확인하지 않고 tail pointer를 전달하면, 우연히 head로 정규화되는 경우와 금지된 사용을 구별할 수 없습니다. 함수 이름만 외우기보다 page-flags.h에서 해당 helper가 만들어지는 선언까지 내려가 보는 것이 정확합니다.

test와 변경의 입력 조건이 다른 policy도 있습니다. PF_NO_TAIL은 특히 set·clear 시 tail을 금지하며 PF_NO_COMPOUND도 변경 경로에서 compound 사용을 제한합니다. PF_ONLY_HEAD와 PF_SECOND의 assertion은 head가 필요한 위치에 임의 tail을 전달하는 오류를 드러냅니다. PF_SECOND의 second page는 head 다음인 first tail을 뜻합니다.

여기서 VM_BUG_ON_PGFLAGS()의 runtime 검사를 고르는 v6.1 설정은 CONFIG_DEBUG_VM_PGFLAGS입니다. architecture의 page-table helper 의미를 시험하는 CONFIG_DEBUG_VM_PGTABLE과는 다릅니다. 이 검사가 꺼져 있어도 API의 head·tail 조건은 그대로이며, 검사를 생략하는 build가 잘못된 입력을 허용하는 것은 아닙니다.

Folio는 allocation 단위라는 의도를 타입에 담습니다

folio는 하나 이상의 base page로 이루어진 단위를 나타냅니다. large folio는 power-of-two 개수의 page로 구성되며 physical 범위와 관련 alignment를 갖습니다. v6.1의 folio 계약은 kernel virtual address도 그 단위의 크기에 맞게 정렬되도록 합니다. page cache에서의 file offset 역시 folio 크기의 배수여야 하지만, user virtual address의 mapping 위치는 별개입니다. user mapping이 항상 large-folio 크기 경계에서 시작한다고 일반화할 수는 없습니다.

이때 연속성은 physical 범위뿐 아니라 해당 kernel virtual view와 논리적인 byte 범위에도 적용됩니다. file에서 연속한 page-cache 범위라는 사실과 어느 process가 그 전부를 하나의 mapping으로 보고 있다는 사실은 같지 않습니다. folio 하나가 있다고 user VMA 하나와 일대일 대응하는 것은 아닙니다.

v6.1의 struct folio는 기존 head descriptor와 겹치는 배치를 사용합니다. 변환할 때 내용을 새 구조체로 복사하는 것이 아닙니다. page_folio(page)는 _compound_head()로 tail을 정규화하고 _Generic으로 입력 pointer의 const 여부를 보존합니다. NULL을 전달할 수는 없습니다. folio_page(folio, n)은 nth_page()를 통해 그 안의 n번째 base-page descriptor를 구하지만 index가 folio 범위 안인지 검사하지는 않습니다. caller가 범위와 reference를 보장해야 합니다.

이 overlay에서 folio의 lru와 mlock_count는 같은 저장 공간을 역할에 맞게 해석하며, mapping은 file의 address_space 또는 anonymous 쪽 관리 관계를 나타냅니다. index는 byte가 아니라 base-page 단위 위치이고, private는 filesystem별 자료나 swap-cache entry 같은 소유자별 의미를 갖습니다. 공통 flags·mapcount·refcount와 선택적인 memcg_data도 head descriptor의 같은 위치를 공유합니다. folio라는 타입을 쓴다고 page cache와 anonymous memory의 구별이 사라지는 것은 아닙니다.

다만 공통 field가 겹친다는 말이 sizeof(struct folio)와 sizeof(struct page)가 같다는 뜻은 아닙니다. v6.1의 folio에는 large-folio용 추가 flags, order, mapping·pin count 등 다음 descriptor 위치에 대응하는 field도 선언되어 있습니다. 작은 folio에 large-folio 전용 field까지 무조건 접근할 수 있는 것은 아닙니다. 공통 head 부분의 배치와 large 단위에만 존재하는 metadata를 구별해야 합니다.

변환 helper는 새 참조를 취득하지 않습니다. 특히 page_folio()의 변환 자체에는 page의 reference나 lock이 필요하지 않지만, reference 없이 찾은 결과는 folio split과 경합할 수 있습니다. 이 방식으로 찾았다면 folio의 reference를 얻은 뒤에도 원래 page가 그 folio에 여전히 속하는지 다시 확인해야 합니다. pointer의 표현을 바꾸는 일, 안정된 참조를 얻는 일, 필요한 상태를 lock으로 보호하는 일은 서로 다른 단계입니다.

다음은 Linux v6.1의 struct folio 선언 전체입니다. 원본 파일의 SPDX-License-Identifier: GPL-2.0를 표시하고 들여쓰기만 정돈했습니다. 뒤쪽 large-folio field까지 포함하므로 공통 head 부분만 있는 단순 배치와 구별할 수 있습니다. 이는 공인 tag의 code이며 다른 판본의 생략된 code를 복원한 것이 아닙니다.

/* SPDX-License-Identifier: GPL-2.0 */
struct folio {
    /* private: don't document the anon union */
    union {
        struct {
    /* public: */
            unsigned long flags;
            union {
                struct list_head lru;
    /* private: avoid cluttering the output */
                struct {
                    void *__filler;
    /* public: */
                    unsigned int mlock_count;
    /* private: */
                };
    /* public: */
            };
            struct address_space *mapping;
            pgoff_t index;
            void *private;
            atomic_t _mapcount;
            atomic_t _refcount;
#ifdef CONFIG_MEMCG
            unsigned long memcg_data;
#endif
    /* private: the union with struct page is transitional */
        };
        struct page page;
    };
    unsigned long _flags_1;
    unsigned long __head;
    unsigned char _folio_dtor;
    unsigned char _folio_order;
    atomic_t _total_mapcount;
    atomic_t _pincount;
#ifdef CONFIG_64BIT
    unsigned int _folio_nr_pages;
#endif
};

Folio flag도 모든 이름이 기계적으로 대응하지는 않습니다

folio_test_*, folio_set_*, folio_clear_* 계열은 folio 의도를 드러내지만 저장 위치가 모두 첫 descriptor라고 단정할 수 없습니다. folio_flags() 계열은 필요한 descriptor 쪽 flags를 선택하며 v6.1의 double-map이나 일부 hardware-poison 관련 상태처럼 offset 1 쪽을 사용하는 경우도 있습니다.

folio_test_large()는 large 단위를 판별하는 표현이며, folio_mapping_flags()처럼 이름이 folio_test_ pattern을 따르지 않는 helper도 있습니다. anonymous·movable mapping tag를 읽는 helper와 실제 flags bit를 읽는 helper는 여기서도 구별됩니다. folio API가 늘어난다고 기존 page helper가 어느 특정 시점에 전부 사라진다고 예측할 필요도 없습니다.

기계적인 이름 치환이 실패하는 대표적인 경우가 uptodate입니다. v6.1은 folio_mark_uptodate()·__folio_mark_uptodate()라는 이름을 사용하며, data가 준비되었다는 사실을 알리는 ordering도 관련됩니다. folio_test_hugetlb()·folio_test_transhuge() 같은 종류 판별에 임의의 folio_set_hugetlb()를 만들어 짝지을 수도 없습니다. page type의 buddy·offline·table·guard 판별은 이 버전의 folio flag 생성 계열이 대신하지 않습니다.

folio 쪽 생성 macro의 FOLIO_PF_* 표현은 주로 descriptor offset을 고르는 데 쓰이며 page helper의 policy 검사를 그대로 반복하는 구조는 아닙니다. folio_flags()에는 folio 자체가 tail이 아닌지, tail 쪽 flags를 요구할 때 compound head가 맞는지 확인하는 debug assertion이 있습니다. _mapcount와 _refcount도 raw field를 직접 조작하기보다 해당 helper와 folio_get()·folio_put()의 계약으로 다루어야 합니다.

조회에는 각각 folio_mapcount()와 folio_ref_count()를 사용합니다. folio_flags(folio, n)이 돌려주는 것은 선택한 descriptor의 flags 주소이지 그 descriptor에 대한 새로운 reference가 아닙니다. offset 1의 특수 상태를 읽는 경로와 base page를 고르는 folio_page()를 같은 범용 index 검사 인터페이스라고 보면 안 됩니다.

실제 주소 선택 함수의 전문은 다음과 같습니다. v6.1 page-flags.h의 code이며 원본 SPDX 표시를 유지했습니다. n에 대한 임의의 bounds check를 추가한 구현이 아니라 두 assertion 뒤에 해당 flags 주소를 돌려주는 원래 구현입니다.

/* SPDX-License-Identifier: GPL-2.0 */
static unsigned long *folio_flags(struct folio *folio, unsigned n)
{
    struct page *page = &folio->page;

    VM_BUG_ON_PGFLAGS(PageTail(page), page);
    VM_BUG_ON_PGFLAGS(n > 0 && !test_bit(PG_head, &page->flags), page);
    return &page[n].flags;
}

실제 code를 읽을 때는 세 가지를 차례로 확인하면 됩니다. 이 판별이 무엇을 뜻하는지, 그 상태가 어느 descriptor와 저장 공간에 있는지, 그 상태를 읽거나 바꾸는 동안 어떤 lifetime·동기화 조건이 필요한지입니다. folio는 이 질문을 없애는 추상화라기보다 잘못된 질문을 덜 하도록 돕는 인터페이스입니다.

Page와 folio 변환의 실제 macro입니다

아래는 Linux v6.1의 page-flags.h에 있는 두 macro 전문입니다. SPDX는 GPL-2.0입니다. page_folio()가 사용하는 _compound_head()와 fake-head configuration의 전체 정의는 compound 글에 있습니다.

#define page_folio(p)        (_Generic((p),                \
    const struct page *:    (const struct folio *)_compound_head(p), \
    struct page *:        (struct folio *)_compound_head(p)))
#define folio_page(folio, n)    nth_page(&(folio)->page, n)

_Generic은 const page pointer에서 const folio pointer로 가는 구분을 유지합니다. NULL을 받아도 되는 탐색 API는 아닙니다. lock이나 reference 없이 변환할 수 있다는 계약은 변환값의 수명을 무제한 보장한다는 뜻이 아닙니다. split과 경합할 수 있는 사용자는 reference를 얻은 뒤 그 folio가 여전히 해당 page를 포함하는지 다시 확인해야 합니다.

folio_page()는 n이 folio 범위 안인지 검사하지 않고 nth_page()를 사용합니다. 임의 index를 검증하는 helper가 아니며 호출자의 범위·reference 전제를 보존한 변환입니다.

참고 자료