Folio (페이지 캐시(Page Cache) 현대화)
리눅스 커널의 Folio 추상화를 심층 분석합니다. struct page의 한계와 Matthew Wilcox가 제안한 folio의 동기, struct folio 내부 구조와 compound page 관계, 페이지 캐시(filemap)의 folio 전환, LRU/회수/writeback/매핑(Mapping) API 변화, large folio와 THP 통합, 파일시스템별 지원 현황, 마이그레이션/스왑(Swap)/memcg 연동, 성능 벤치마크까지 커널 소스(include/linux/pagemap.h, mm/filemap.c, mm/folio-compat.c) 기반으로 분석합니다.
핵심 요약
- 타입 안전성 -- struct page는 head/tail 구분 없이 사용되어 런타임 버그의 원인이었으나, folio는 반드시 head page만 가리키도록 컴파일 타임에 보장합니다.
- compound page 통합 -- order-0(4KB)부터 order-9(2MB) 이상까지 모든 크기의 메모리 블록을 folio 하나로 추상화합니다.
- 페이지 캐시 현대화 -- filemap의 XArray가 folio 단위로 관리되며, readahead/writeback이 folio 크기에 맞게 최적화됩니다.
- LRU/회수 개선 -- folio_batch(pagevec 대체)로 LRU 관리, shrink_folio_list()로 회수 경로 통합.
- large folio -- 4KB 이상의 folio로 readahead, TLB 효율, I/O 병합을 개선합니다. THP(Transparent Huge Pages)도 folio로 통합됩니다.
- 점진적 전환 -- v5.16(2022)부터 도입, v6.x에서 주요 서브시스템 전환 완료, 일부 드라이버는 여전히 struct page 사용.
단계별 이해
- struct page의 문제 이해
64바이트에 과도한 union 중첩, head/tail 혼동 버그, 타입 안전성 부재를 파악합니다. - compound page 복습
Buddy Allocator가 반환하는 order > 0 페이지(Page)가 head + tail로 구성되는 구조를 이해합니다. - struct folio 구조 분석
folio가 head page를 감싸는 래퍼이며, page_folio()/folio_page() 변환 함수를 학습합니다. - 페이지 캐시 folio 전환
filemap_get_folio(), readahead_folio() 등 새 API로 I/O 경로가 어떻게 바뀌었는지 추적합니다. - LRU/회수/writeback 변화
folio_add_lru(), shrink_folio_list(), folio_start_writeback() 등 핵심 경로를 따라갑니다. - large folio와 성능
readahead에서 large folio 할당, TLB miss 감소, I/O 병합 효과를 벤치마크로 확인합니다.
include/linux/page-flags.h (struct page 플래그),
include/linux/mm_types.h (struct page, struct folio 정의),
include/linux/pagemap.h (페이지 캐시 folio API),
mm/filemap.c (filemap folio 구현),
mm/folio-compat.c (하위 호환 래퍼),
mm/swap.c (folio LRU 관리).
Matthew Wilcox (Oracle)의 "Memory Folios" 패치(Patch) 시리즈(v5.16, 2022년 1월)가 최초 머지입니다.
Folio 개요: 왜 필요한가
Linux 커널은 수십 년간 struct page를 물리 메모리(Physical Memory)의 기본 단위로 사용했습니다. 그러나 이 구조체(Struct)는 심각한 설계 문제를 안고 있었습니다:
struct page의 근본적 문제
| 문제 | 설명 | 실제 영향 |
|---|---|---|
| 타입 혼동 | head page와 tail page가 같은 타입 | tail page에 head 전용 연산 → 데이터 손상 |
| 과도한 union | 64바이트에 20개 이상 필드 중첩 | 어떤 필드가 유효한지 문맥 의존적 |
| 크기 모호성 | struct page가 4KB인지 2MB인지 불명확 | 모든 함수에서 compound_head() 검사 필요 |
| 참조 카운트(Reference Count) 분산 | head의 refcount만 유효, tail은 무시해야 | get_page(tail) 시 head refcount 증가 우회 필요 |
| API 일관성 | 같은 함수가 단일 페이지와 compound 모두 처리 | 동작 예측 어려움, 버그 유발 |
Folio 도입 타임라인
| 커널 버전 | 연도 | 주요 변화 |
|---|---|---|
| v5.16 | 2022.01 | struct folio 최초 도입, 기본 변환 매크로(Macro) |
| v5.17 | 2022.03 | filemap folio API (filemap_get_folio 등) |
| v5.18 | 2022.05 | readahead folio 전환, folio writeback |
| v5.19 | 2022.07 | LRU folio 전환 (folio_add_lru) |
| v6.0 | 2022.10 | shrink_folio_list, folio reclaim 경로 |
| v6.1 | 2022.12 | large folio readahead, 파일시스템(Filesystem) 지원 확대 |
| v6.4 | 2023.06 | anonymous large folio (mTHP) |
| v6.6 | 2023.10 | 대부분의 mm/ 코드 folio 전환 완료 |
| v6.8+ | 2024+ | struct page 축소 작업, 드라이버 전환 계속 |
Folio 생명주기 전체 흐름
struct page vs struct folio
struct page는 물리 메모리의 4KB 페이지 프레임 하나를 나타냅니다. 그러나 compound page에서는 head page와 tail page가 모두 struct page이면서 서로 다른 의미를 가집니다. struct folio는 이 혼란을 해결합니다:
/* include/linux/mm_types.h -- folio 정의 (간략화) */
struct folio {
/* 첫 번째 멤버: struct page와 메모리 레이아웃 호환 */
union {
struct {
unsigned long flags; /* page flags (PG_locked 등) */
union {
struct list_head lru;
struct {
void *__filler;
unsigned int mlock_count;
};
};
struct address_space *mapping;
pgoff_t index; /* 페이지 캐시 내 오프셋 */
union {
void *private;
swp_entry_t swap;
};
atomic_t _mapcount;
atomic_t _refcount;
};
struct page page; /* struct page와 캐스팅 호환 */
};
union {
struct {
unsigned long _flags_1;
unsigned long _head_1;
};
struct page _page_1; /* 두 번째 tail page 영역 */
};
unsigned long _folio_dtor;
unsigned long _folio_order;
};
설명
struct folio의 첫 번째 멤버는struct page와 메모리 레이아웃이 동일합니다. 이것이 page_folio() 캐스팅이 안전한 이유입니다.
folio는 head page의 포인터를 다른 타입으로 감싸는 것이므로 추가 메모리 비용이 없습니다.
_folio_order는 이 folio의 order(2^order 페이지 수)를 저장합니다.
Compound Page와 Folio 관계
Buddy Allocator에서 order > 0으로 할당하면 compound page를 반환합니다. compound page는 head page 1개와 (2^order - 1)개의 tail page로 구성됩니다. folio는 이 compound page 전체를 하나의 단위로 추상화합니다.
/* page_folio(): struct page → struct folio 변환 */
static inline struct folio *page_folio(struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (unlikely(head & 1))
return (struct folio *)(head - 1); /* tail → head 추적 */
return (struct folio *)page; /* 이미 head page */
}
/* folio_page(): struct folio의 n번째 page 접근 */
static inline struct page *folio_page(struct folio *folio, size_t n)
{
VM_BUG_ON_FOLIO(n >= folio_nr_pages(folio), folio);
return &folio->page + n;
}
/* folio 크기/order 조회 */
static inline unsigned int folio_order(struct folio *folio)
{
if (!folio_test_large(folio))
return 0;
return folio->_folio_order;
}
static inline size_t folio_size(struct folio *folio)
{
return PAGE_SIZE << folio_order(folio);
}
static inline long folio_nr_pages(struct folio *folio)
{
return 1L << folio_order(folio);
}
설명
page_folio()는 tail page의 compound_head 필드 최하위 비트가 1로 설정되어 있으면 head를 추적합니다.
이미 head page이면 바로 folio로 캐스팅합니다. 이 함수는 기존 코드에서 folio로 전환할 때 가장 많이 사용됩니다.
folio_test_large()는 PageHead 플래그를 확인하여 order > 0 여부를 빠르게 판단합니다.
struct folio 내부 구조 상세
struct folio는 struct page와 메모리 레이아웃이 호환되지만, 타입 시스템을 통해 의미를 명확히 합니다. compound page에서 head page의 struct page 영역과 첫 번째 tail page 영역까지를 folio가 커버합니다.
필드 매핑 상세
| folio 필드 | struct page 대응 | 용도 | 크기 |
|---|---|---|---|
| folio->flags | page->flags | PG_locked, PG_dirty, PG_uptodate 등 | unsigned long |
| folio->lru | page->lru | LRU 리스트 연결 (active/inactive) | struct list_head |
| folio->mapping | page->mapping | address_space 포인터 (파일 매핑 정보) | pointer |
| folio->index | page->index | 매핑 내 오프셋 (페이지 단위) | pgoff_t |
| folio->private | page->private | 파일시스템별 메타데이터 | void * |
| folio->_mapcount | page->_mapcount | 매핑 카운트 (rmap) | atomic_t |
| folio->_refcount | page->_refcount | 참조 카운트 | atomic_t |
| folio->_folio_dtor | page[1] 영역 | folio 소멸자 식별자 | unsigned long |
| folio->_folio_order | page[1] 영역 | compound order | unsigned long |
struct page 배열(mem_map 또는 vmemmap)의 head page 엔트리를 다른 타입으로 해석할 뿐입니다.
따라서 folio 도입으로 인한 메모리 오버헤드(Overhead)는 0바이트입니다.
struct folio 커널 소스 분석
include/linux/mm_types.h에 정의된 struct folio의 실제 구조를 필드별로 분석합니다. folio는 head page의 struct page와 첫 번째 tail page의 영역까지 2개 페이지 디스크립터(Descriptor) 크기를 커버합니다.
/* include/linux/mm_types.h -- struct folio 정의 (v6.8 기준, 주석 보강) */
struct folio {
/* ===== 첫 번째 cacheline: head page의 struct page와 동일 ===== */
union {
struct {
unsigned long flags;
/* PG_locked, PG_dirty, PG_uptodate, PG_lru,
* PG_active, PG_head (compound 표시) 등
* 상위 비트: ZONE, NODE, SECTION 인코딩 */
union {
struct list_head lru;
/* LRU 리스트 연결 (active/inactive file/anon)
* folio_add_lru()에서 설정 */
struct {
void *__filler;
unsigned int mlock_count;
/* mlock()된 VMA 수 추적 */
};
};
struct address_space *mapping;
/* 파일 매핑: inode->i_mapping 포인터
* 익명 매핑: anon_vma 포인터 (최하위 비트 1)
* swap 캐시: swapper_spaces[]
* NULL: 미매핑(buddy/slab 등) */
pgoff_t index;
/* 파일 매핑: 파일 내 페이지 오프셋
* 익명: 가상 주소 기반 인덱스
* swap: swap 엔트리 인덱스 */
union {
void *private;
/* 파일시스템별 메타데이터
* ext4: buffer_head 포인터
* XFS: iomap 상태
* 네트워크 FS: 추가 상태 */
swp_entry_t swap;
/* swap 캐시에 있을 때 swap 엔트리 */
};
atomic_t _mapcount;
/* PTE 매핑 수 (order-0 기준)
* -1: 매핑 없음
* 0: 단일 매핑
* N: N+1개 PTE가 이 folio 매핑 */
atomic_t _refcount;
/* 참조 카운트
* 0: 해제 가능 (buddy로 반환)
* folio_get()/folio_put()으로 조작
* 페이지 캐시 보유 시 최소 1 */
};
struct page page;
/* struct page와 union → 캐스팅 호환
* (struct folio *)page == page_folio(page) */
};
/* ===== 두 번째 cacheline: 첫 번째 tail page 영역 ===== */
union {
struct {
unsigned long _flags_1;
/* 두 번째 페이지의 flags (일반적으로 미사용) */
unsigned long _head_1;
/* compound_head 포인터 | 1
* tail page가 head를 추적하는 데 사용 */
};
struct page _page_1;
};
unsigned long _folio_dtor;
/* folio 소멸자 ID (FOLIO_DTOR_*)
* __folio_put()에서 소멸자 선택에 사용 */
unsigned long _folio_order;
/* compound order (0 = 4KB, 2 = 16KB, 9 = 2MB)
* folio_order()가 이 값을 반환 */
};
코드 설명
struct folio는 물리적으로 struct page[2]와 동일한 크기입니다. 첫 번째 struct page 크기(64바이트) 영역은 head page의 모든 필드를 그대로 포함하고, 두 번째 영역은 첫 번째 tail page의 compound_head와 _folio_dtor/_folio_order를 포함합니다.
- flags:
include/linux/page-flags.h에 정의된 PG_* 플래그와 상위 비트의 ZONE/NODE 인코딩을 포함합니다. folio 전용 API(folio_test_locked()등)는 이 필드를 조작합니다. - mapping: 최하위 비트로 매핑 유형을 구분합니다.
PAGE_MAPPING_ANON(비트 0) 설정 시anon_vma, 해제 시address_space입니다. - _mapcount: order-0 folio에서는 단순하지만, large folio에서는
folio_entire_mapcount()와folio_mapcount()가 별도로 전체/부분 매핑을 추적합니다. - _refcount: 페이지 캐시에 있으면 +1, 사용자 공간 매핑마다 +1, 커널이 직접 참조하면 +1이 추가됩니다.
- _folio_order: order-0 folio에서는 이 영역이 존재하지 않습니다(tail page가 없으므로).
folio_test_large()가 false면 order는 항상 0입니다.
struct folio와 struct page 메모리 레이아웃
struct folio가 struct page 배열 위에 어떻게 겹쳐지는지를 바이트 단위로 보여줍니다.
Folio 플래그 API
/* 기존 struct page 플래그 API → folio 플래그 API 대응 */
/* Locked */
PageLocked(page) → folio_test_locked(folio)
SetPageLocked(page) → /* folio_lock() 사용 */
ClearPageLocked(page) → folio_unlock(folio)
/* Dirty */
PageDirty(page) → folio_test_dirty(folio)
SetPageDirty(page) → folio_mark_dirty(folio)
ClearPageDirty(page) → folio_clear_dirty_for_io(folio)
/* Uptodate */
PageUptodate(page) → folio_test_uptodate(folio)
SetPageUptodate(page) → folio_mark_uptodate(folio)
/* Writeback */
PageWriteback(page) → folio_test_writeback(folio)
SetPageWriteback(page) → folio_start_writeback(folio)
ClearPageWriteback(page)→ folio_end_writeback(folio)
/* Referenced (LRU 회수 관련) */
PageReferenced(page) → folio_test_referenced(folio)
SetPageReferenced(page) → folio_set_referenced(folio)
설명
folio 플래그 API는folio_test_*, folio_set_*, folio_clear_* 패턴을 따릅니다.
내부적으로 &folio->flags를 조작하므로 struct page의 flags와 동일한 비트를 사용합니다.
핵심 차이는 tail page에서는 호출할 수 없다는 것입니다 -- 컴파일러가 타입 불일치를 검출합니다.
Folio 핵심 API
folio의 생명주기를 관리하는 핵심 API를 정리합니다. 기존 struct page API와의 대응 관계도 함께 표시합니다.
할당/해제/참조
| folio API | 기존 page API | 설명 |
|---|---|---|
folio_alloc(gfp, order) | alloc_pages(gfp, order) | order 크기의 folio 할당 |
filemap_alloc_folio(gfp, order) | page_cache_alloc() | 페이지 캐시용 folio 할당 |
folio_get(folio) | get_page(page) | 참조 카운트 증가 |
folio_put(folio) | put_page(page) | 참조 카운트 감소, 0이면 해제 |
folio_ref_count(folio) | page_count(page) | 참조 카운트 조회 |
folio_lock(folio) | lock_page(page) | PG_locked 획득 (sleep 가능) |
folio_trylock(folio) | trylock_page(page) | 논블로킹 잠금(Lock) 시도 |
folio_unlock(folio) | unlock_page(page) | PG_locked 해제 |
folio_wait_locked(folio) | wait_on_page_locked(page) | 잠금 해제 대기 |
/* folio 할당과 해제 예시 */
struct folio *folio;
/* order-0 folio (4KB) 할당 */
folio = folio_alloc(GFP_KERNEL, 0);
if (!folio)
return -ENOMEM;
/* order-2 folio (16KB) 할당 */
folio = folio_alloc(GFP_KERNEL | __GFP_COMP, 2);
/* 참조 카운트 조작 */
folio_get(folio); /* refcount++ */
pr_info("refcount: %d\n", folio_ref_count(folio));
folio_put(folio); /* refcount-- */
/* 잠금 */
folio_lock(folio); /* PG_locked 설정 (블로킹) */
/* ... 배타적 작업 ... */
folio_unlock(folio); /* PG_locked 해제 */
/* 최종 해제 */
folio_put(folio); /* refcount → 0이면 buddy로 반환 */
설명
folio_alloc()은 내부적으로 alloc_pages()를 호출한 후 결과를 struct folio *로 캐스팅합니다.
folio_put()에서 참조 카운트가 0이 되면 __folio_put()을 통해 buddy allocator로 모든 페이지를 한 번에 반환합니다.
large folio의 경우 tail page에 대한 개별 put은 불필요합니다 -- folio 단위로 일괄 관리됩니다.
folio_alloc() 할당 경로 분석
folio_alloc()은 folio를 할당하는 가장 기본적인 함수입니다. 내부적으로 Buddy Allocator의 alloc_pages()를 호출하고 결과를 struct folio *로 변환합니다.
/* include/linux/pagemap.h */
static inline struct folio *folio_alloc(gfp_t gfp, unsigned int order)
{
struct page *page = alloc_pages(gfp | __GFP_COMP, order);
/* __GFP_COMP: order > 0일 때 compound page 구성 강제 */
return page_folio(page);
/* page가 NULL이면 page_folio()도 NULL 반환 */
}
/* 페이지 캐시 전용 할당 */
static inline struct folio *filemap_alloc_folio(gfp_t gfp, unsigned int order)
{
return folio_alloc(gfp, order);
/* 의미적 구분: 페이지 캐시에 넣을 folio 할당
* 향후 NUMA 정책, memcg 차징 등 추가 가능 */
}
/* folio 해제 경로 */
void __folio_put(struct folio *folio)
{
/* large folio면 compound page 전체를 buddy로 반환 */
if (folio_test_large(folio)) {
/* 소멸자 호출: _folio_dtor에 따라 분기
* FOLIO_DTOR_ANON_THP → THP 통계 업데이트
* FOLIO_DTOR_COMPOUND → 기본 해제 */
folio_undo_large_rmappable(folio);
}
__page_cache_release(&folio->page);
free_unref_folios(folio);
/* 2^order 페이지를 한 번에 buddy로 반환 */
}
코드 설명
folio_alloc()의 핵심은 __GFP_COMP 플래그입니다. 이 플래그가 있으면 alloc_pages()는 compound page를 구성합니다:
- order-0:
__GFP_COMP가 있어도 단일 페이지를 반환합니다. compound page 구성 불필요. - order > 0: head page에
PG_head플래그를 설정하고, tail page들의compound_head를 head 포인터 | 1로 초기화합니다. - page_folio(): head page 포인터를
struct folio *로 캐스팅합니다. 메모리 레이아웃이 동일하므로 안전합니다.
해제 경로에서 __folio_put()은 _folio_dtor 필드를 확인하여 적절한 소멸자를 호출합니다. THP folio는 통계 업데이트가 필요하고, 일반 compound folio는 바로 buddy로 반환됩니다.
folio 참조 카운트/잠금 관리 패턴
folio의 생명주기를 제어하는 참조 카운트와 잠금 API의 커널 소스 구현을 분석합니다.
/* include/linux/pagemap.h -- 참조 카운트 조작 */
/* folio_get(): 참조 카운트 증가 (folio가 유효할 때만 호출) */
static inline void folio_get(struct folio *folio)
{
VM_BUG_ON_FOLIO(!folio_ref_count(folio), folio);
/* refcount가 0이면 이미 해제된 folio → 버그 */
folio_ref_inc(folio);
/* atomic_inc(&folio->_refcount) */
}
/* folio_put(): 참조 카운트 감소, 0이면 해제 */
static inline void folio_put(struct folio *folio)
{
if (folio_put_testzero(folio))
/* atomic_dec_and_test(&folio->_refcount) */
__folio_put(folio);
/* refcount → 0: buddy allocator로 반환 */
}
/* folio_trylock(): 논블로킹 잠금 시도 */
static inline bool folio_trylock(struct folio *folio)
{
return likely(!test_and_set_bit_lock(PG_locked, folio_flags(folio, 0)));
/* PG_locked 비트를 원자적으로 설정
* 이미 설정 → false 반환 (잠금 실패)
* 미설정 → true 반환 (잠금 성공) */
}
/* folio_lock(): 블로킹 잠금 (sleep 가능) */
void folio_lock(struct folio *folio)
{
might_sleep();
if (!folio_trylock(folio))
folio_wait_bit(folio, PG_locked);
/* wait_queue에서 PG_locked 해제될 때까지 대기
* I/O 완료, 다른 경로의 folio_unlock()이 깨움 */
}
/* folio_try_get(): speculative 참조 (RCU 보호 하에서) */
static inline bool folio_try_get(struct folio *folio)
{
return folio_ref_try_add(folio, 1);
/* refcount가 0이면 false (이미 해제 중)
* 페이지 캐시 lookup에서 RCU + try_get 패턴 사용 */
}
코드 설명
folio 참조 카운트 관리의 핵심 패턴을 정리합니다:
- folio_get()/folio_put() 쌍: 가장 일반적인 참조 관리 패턴.
folio_get()은 refcount가 이미 0이 아님을 전제합니다(DEBUG_VM에서 검증). - folio_try_get(): RCU read-side 구간에서 folio가 동시에 해제될 수 있을 때 사용합니다.
filemap_get_folio()의 내부에서 XArray 탐색 후 이 함수로 참조를 획득합니다. - folio_trylock()/folio_lock(): PG_locked 비트를 사용한 배타적 잠금입니다.
folio_lock()은might_sleep()을 호출하므로 인터럽트(Interrupt) 컨텍스트에서 사용할 수 없습니다. - large folio 동작: 참조 카운트와 잠금은 모두 head page(= folio)의 필드를 사용합니다. tail page에 대한 별도 조작은 불필요합니다.
페이지 캐시와 Folio
페이지 캐시는 파일 데이터를 메모리에 캐싱하는 커널의 핵심 서브시스템입니다. folio 전환으로 가장 큰 변화를 겪은 영역이기도 합니다. 페이지 캐시는 struct address_space의 XArray(i_pages)로 folio를 관리합니다.
페이지 캐시 folio API
| folio API | 기존 page API | 설명 |
|---|---|---|
filemap_get_folio(mapping, index) | find_get_page() | 캐시(Cache)에서 folio 검색 |
filemap_lock_folio(mapping, index) | find_lock_page() | 검색 + 잠금 |
filemap_add_folio(mapping, folio, index, gfp) | add_to_page_cache_lru() | 캐시에 folio 추가 |
filemap_remove_folio(folio) | delete_from_page_cache() | 캐시에서 folio 제거 |
read_cache_folio(mapping, index, filler, data) | read_cache_page() | 캐시 읽기 (없으면 I/O) |
folio_mark_accessed(folio) | mark_page_accessed() | 접근 표시 (LRU 승격) |
filemap_get_folio() 구현 분석
페이지 캐시에서 folio를 검색하는 filemap_get_folio()의 내부 구현을 추적합니다. 이 함수는 기존 find_get_page()를 완전히 대체합니다.
/* mm/filemap.c -- __filemap_get_folio() 핵심 구현 (간략화) */
struct folio *__filemap_get_folio(
struct address_space *mapping,
pgoff_t index,
fgf_t fgp_flags,
gfp_t gfp)
{
struct folio *folio;
repeat:
/* 1단계: XArray에서 folio 검색 (RCU read-side) */
folio = mapping_get_entry(mapping, index);
/* xa_load() + folio_try_get() 조합
* shadow entry, DAX entry 등 특수 엔트리 처리 */
if (folio && !xa_is_value(folio)) {
/* 2단계: 캐시 히트 -- folio를 찾았음 */
if (fgp_flags & FGP_LOCK) {
/* filemap_lock_folio()에서 사용 */
if (fgp_flags & FGP_NOWAIT) {
if (!folio_trylock(folio)) {
folio_put(folio);
return ERR_PTR(-EAGAIN);
}
} else {
folio_lock(folio);
}
/* 잠금 획득 중 folio가 이동했는지 확인 */
if (unlikely(folio->mapping != mapping)) {
folio_unlock(folio);
folio_put(folio);
goto repeat;
}
}
if (fgp_flags & FGP_ACCESSED)
folio_mark_accessed(folio);
/* LRU에서 접근 기록 → 회수 우선순위 조정 */
return folio;
}
/* 3단계: 캐시 미스 -- folio 할당 및 삽입 */
if (!(fgp_flags & FGP_CREAT))
return ERR_PTR(-ENOENT);
/* large folio 할당 시도 (order 결정) */
if (fgp_flags & FGP_WRITE)
order = mapping_max_folio_order(mapping);
/* 파일시스템이 지원하는 최대 folio order */
folio = filemap_alloc_folio(gfp, order);
if (!folio)
return ERR_PTR(-ENOMEM);
/* XArray에 folio 삽입 */
err = filemap_add_folio(mapping, folio, index, gfp);
if (err == -EEXIST) {
/* 다른 CPU가 먼저 삽입 → retry */
folio_put(folio);
goto repeat;
}
return folio;
}
코드 설명
__filemap_get_folio()는 3단계로 동작합니다:
- 1단계 (XArray 검색):
mapping_get_entry()는 RCU read-side 보호 하에서xa_load()를 호출하고, 반환된 folio에folio_try_get()으로 참조를 획득합니다. RCU 보호 덕분에 lock-free 경로입니다. - 2단계 (캐시 히트): folio를 찾았으면
fgp_flags에 따라 잠금 획득(FGP_LOCK), 접근 표시(FGP_ACCESSED)를 수행합니다.folio->mapping != mapping검사는 잠금 대기 중 folio가 truncate/migrate되었을 경우를 처리합니다. - 3단계 (캐시 미스):
FGP_CREAT플래그가 있으면filemap_alloc_folio()로 새 folio를 할당하고 XArray에 삽입합니다.EEXIST경쟁 조건을 처리하기 위해goto repeat로 재시도합니다.
filemap_get_folio()는 이 함수를 fgp_flags = FGP_ENTRY로 호출하는 얇은 래퍼입니다. filemap_lock_folio()는 FGP_LOCK | FGP_ACCESSED를 추가합니다.
/* 파일시스템의 readpage → read_folio 전환 예시 */
/* 기존 (struct page 기반) */
static int myfs_readpage(struct file *file, struct page *page)
{
void *buf = kmap_local_page(page);
/* ... I/O로 buf에 데이터 읽기 ... */
kunmap_local(buf);
SetPageUptodate(page);
unlock_page(page);
return 0;
}
/* 새로운 (struct folio 기반) */
static int myfs_read_folio(struct file *file, struct folio *folio)
{
void *buf = kmap_local_folio(folio, 0);
/* ... I/O로 folio_size(folio) 바이트 읽기 ... */
kunmap_local(buf);
folio_mark_uptodate(folio);
folio_unlock(folio);
return 0;
}
/* address_space_operations 변화 */
const struct address_space_operations myfs_aops = {
/* .readpage = myfs_readpage, // 기존 */
.read_folio = myfs_read_folio, /* 새로운 */
.writepages = myfs_writepages,
.dirty_folio = filemap_dirty_folio, /* set_page_dirty → dirty_folio */
};
설명
address_space_operations에서 .readpage는 .read_folio로, .set_page_dirty는 .dirty_folio로 교체되었습니다.
kmap_local_folio()의 두 번째 인자는 folio 내 오프셋(바이트)으로, large folio에서 특정 페이지 영역만 매핑할 수 있습니다.
기존 .readpage를 구현한 파일시스템은 mm/folio-compat.c의 래퍼를 통해 자동으로 folio로 변환됩니다.
filemap_add_folio() 구현 분석
페이지 캐시에 folio를 삽입하는 filemap_add_folio()의 내부 동작을 분석합니다. XArray 삽입, memcg 차징, LRU 추가가 원자적으로 수행됩니다.
/* mm/filemap.c -- filemap_add_folio() 핵심 경로 (간략화) */
int filemap_add_folio(
struct address_space *mapping,
struct folio *folio,
pgoff_t index,
gfp_t gfp)
{
int error;
/* 1단계: folio 초기 설정 */
__folio_set_locked(folio);
/* PG_locked 설정 (호출자가 I/O 완료 후 unlock) */
folio->mapping = mapping;
folio->index = index;
/* 캐시 내 위치 기록 */
/* 2단계: memcg 차징 */
error = mem_cgroup_charge(folio, NULL, gfp);
if (error)
goto error;
/* 메모리 cgroup 계정에 folio 크기만큼 차징
* large folio: folio_nr_pages() * PAGE_SIZE 차징 */
/* 3단계: XArray에 삽입 */
folio_ref_add(folio, folio_nr_pages(folio));
/* 참조 카운트 증가 (캐시 보유분) */
xa_lock_irq(&mapping->i_pages);
error = __filemap_add_folio(mapping, folio, index, gfp);
/* xa_store()로 XArray에 folio 포인터 저장
* large folio: index ~ index + folio_nr_pages() - 1
* 모든 슬롯에 같은 folio 포인터 저장 */
if (unlikely(error)) {
xa_unlock_irq(&mapping->i_pages);
goto error;
}
mapping->nrpages += folio_nr_pages(folio);
xa_unlock_irq(&mapping->i_pages);
/* 4단계: LRU 리스트에 추가 */
folio_add_lru(folio);
/* inactive file LRU에 삽입
* folio_batch를 통해 배치 처리 */
return 0;
error:
folio->mapping = NULL;
folio_ref_sub(folio, folio_nr_pages(folio));
return error;
}
코드 설명
filemap_add_folio()는 4단계로 folio를 페이지 캐시에 통합합니다:
- 1단계 (초기 설정):
PG_locked를 설정하여 I/O 진행 중 다른 경로가 미완성 folio를 읽지 못하게 합니다.mapping과index를 설정하여 folio가 어떤 파일의 어떤 오프셋인지 기록합니다. - 2단계 (memcg 차징):
mem_cgroup_charge()가 현재 태스크의 cgroup에 folio 크기만큼 메모리 사용량을 차징합니다. large folio는 한 번의 호출로 전체 크기를 차징합니다. - 3단계 (XArray 삽입):
xa_lock_irq로 XArray를 보호하며, large folio의 경우 여러 인덱스 슬롯에 동일한 folio 포인터를 저장합니다.nrpages를 folio 크기만큼 증가시킵니다. - 4단계 (LRU 추가):
folio_add_lru()가 folio를 inactive file LRU에 삽입합니다.folio_batch(pagevec 후속)를 통해 per-CPU 캐시에 배치한 후 일괄 처리하여 LRU lock 경합을 줄입니다.
에러 발생 시 mapping = NULL로 되돌리고 참조 카운트를 복원하여 folio가 해제될 수 있게 합니다.
LRU와 Folio
커널의 LRU(Least Recently Used) 리스트는 메모리 회수(reclaim)의 핵심입니다. folio 전환으로 LRU 관리 단위가 페이지에서 folio로 바뀌었습니다.
LRU API 변화
| folio API | 기존 page API | 설명 |
|---|---|---|
folio_add_lru(folio) | lru_cache_add(page) | folio를 LRU 리스트에 추가 |
folio_activate(folio) | activate_page(page) | inactive → active 리스트 승격 |
folio_deactivate(folio) | deactivate_page(page) | active → inactive 리스트 강등 |
folio_mark_lazyfree(folio) | mark_page_lazyfree(page) | lazyfree 표시 (MADV_FREE) |
folio_batch | pagevec | LRU 배치 처리 구조체 |
/* folio_batch: pagevec을 대체하는 배치 처리 구조체 */
struct folio_batch {
unsigned char nr; /* 현재 저장된 folio 수 */
unsigned char i; /* 반복자 인덱스 */
bool percpu_pvec_drained;
struct folio *folios[PAGEVEC_SIZE]; /* 보통 31개 */
};
/* 사용 예시 */
struct folio_batch fbatch;
folio_batch_init(&fbatch);
/* folio를 배치에 추가 */
if (!folio_batch_add(&fbatch, folio)) {
/* 배치가 가득 차면 한 번에 LRU에 추가 */
folio_batch_move_lru(&fbatch);
}
/* 잔여 folio 처리 */
if (folio_batch_count(&fbatch))
folio_batch_move_lru(&fbatch);
설명
folio_batch는 기존 pagevec을 대체합니다. LRU 조작은 lruvec 잠금을 필요로 하므로, 개별 folio마다 잠금을 잡으면 성능이 저하됩니다.
folio_batch로 최대 31개까지 모아서 한 번의 잠금 구간에서 일괄 처리합니다.
large folio는 folio_nr_pages()만큼 LRU 크기에 반영되므로 2MB folio 하나가 512개 페이지 카운트를 차지합니다.
folio->flags의 상위 비트에 세대 번호를 인코딩하며, folio_update_gen()으로 세대를 갱신합니다.
MGLRU에서 large folio는 hot/cold 판단 시 전체 folio의 접근 비트를 종합하여 판단합니다.
Folio LRU 관리 아키텍처
/* mm/swap.c -- folio_add_lru() 구현 (간략화) */
void folio_add_lru(struct folio *folio)
{
struct folio_batch *fbatch;
VM_BUG_ON_FOLIO(folio_test_lru(folio), folio);
/* per-CPU folio_batch에 추가 */
fbatch = this_cpu_ptr(&lru_pvecs.lru_add);
folio_get(folio);
if (!folio_batch_add(fbatch, folio))
/* batch가 가득 차면 drain */
folio_batch_move_lru(fbatch);
}
/* folio_batch drain: 한 번의 lock으로 여러 folio 처리 */
static void folio_batch_move_lru(struct folio_batch *fbatch)
{
spin_lock_irq(&lruvec->lru_lock);
/* 잠금 1회로 최대 31개 folio를 LRU에 삽입 */
for (int i = 0; i < folio_batch_count(fbatch); i++) {
struct folio *folio = fbatch->folios[i];
enum lru_list lru;
lru = folio_lru_list(folio);
/* file/anon x active/inactive 결정 */
lruvec_add_folio(lruvec, folio);
/* list_add(&folio->lru, &lruvec->lists[lru])
* folio_nr_pages() 만큼 lruvec 크기 갱신 */
folio_set_lru(folio);
}
spin_unlock_irq(&lruvec->lru_lock);
folio_batch_reinit(fbatch);
}
/* MGLRU에서 folio 세대 관리 */
static void folio_update_gen(struct folio *folio, int new_gen)
{
unsigned long old_flags, new_flags;
do {
old_flags = READ_ONCE(folio->flags);
new_flags = (old_flags & ~LRU_GEN_MASK) |
((new_gen + 1UL) << LRU_GEN_PGOFF);
} while (cmpxchg(&folio->flags, old_flags, new_flags) != old_flags);
/* folio->flags 상위 비트에 세대 번호 원자적 갱신 */
}
설명
folio_add_lru()는 folio를 직접 LRU에 넣지 않고 per-CPU folio_batch에 먼저 모읍니다.
배치가 가득 차면(31개) folio_batch_move_lru()가 호출되어 lruvec->lru_lock을 한 번만 잡고 모든 folio를 일괄 삽입합니다.
이 설계의 핵심은 잠금 경합(Lock Contention) 최소화입니다.
folio_lru_list()는 folio의 플래그(file vs anon, active vs inactive, unevictable)를 검사하여 5개 LRU 리스트 중 어디에 배치할지 결정합니다.
large folio는 folio_nr_pages()만큼 lruvec 크기에 반영되므로 2MB folio 하나가 512개 페이지 카운트를 차지합니다.
MGLRU의
folio_update_gen()은 cmpxchg를 사용하여 folio->flags의 상위 비트에 세대 번호를 원자적으로 갱신합니다.
세대 번호가 0이면 "세대 미할당" 상태이므로 실제 세대는 new_gen + 1로 저장합니다.
회수(Reclaim)와 Folio
메모리 압박 시 커널은 LRU 리스트에서 folio를 회수합니다. 기존 shrink_page_list()는 shrink_folio_list()로 전환되었습니다.
/* mm/vmscan.c -- shrink_folio_list() 핵심 경로 (간략화) */
static unsigned int shrink_folio_list(
struct list_head *folio_list,
struct pglist_data *pgdat,
struct scan_control *sc,
struct reclaim_stat *stat)
{
struct folio *folio;
list_for_each_entry_safe(folio, next, folio_list, lru) {
/* 1. 잠금 시도 */
if (!folio_trylock(folio))
goto keep;
/* 2. 참조 상태 확인 */
enum folio_references references = folio_check_references(folio, sc);
switch (references) {
case FOLIOREF_ACTIVATE:
goto activate_locked;
case FOLIOREF_KEEP:
goto keep_locked;
case FOLIOREF_RECLAIM:
case FOLIOREF_RECLAIM_CLEAN:
break; /* 회수 진행 */
}
/* 3. 더티 folio → writeback 시작 */
if (folio_test_dirty(folio)) {
folio_start_writeback(folio);
/* ... I/O 스케줄 ... */
}
/* 4. 매핑 해제 (rmap) */
if (folio_mapped(folio))
try_to_unmap(folio, TTU_BATCH_FLUSH);
/* 5. 페이지 캐시에서 제거 (file-backed) */
if (folio_test_private(folio))
filemap_release_folio(folio, sc->gfp_mask);
filemap_remove_folio(folio);
/* 6. buddy로 반환 */
folio_put(folio);
nr_reclaimed += folio_nr_pages(folio);
}
return nr_reclaimed;
}
설명
shrink_folio_list()는 기존 shrink_page_list()를 folio 기반으로 전면 재작성한 것입니다.
핵심 차이: large folio는 한 번의 회수로 folio_nr_pages()만큼의 페이지를 한꺼번에 회수합니다.
folio_check_references()는 rmap을 통해 PTE accessed 비트를 확인하고, 최근 참조 여부에 따라 회수/활성화/유지를 결정합니다.
file-backed folio는 filemap_remove_folio()로 페이지 캐시에서 제거 후 해제됩니다.
Writeback과 Folio
더티 folio를 디스크에 기록하는 writeback 경로도 folio 단위로 전환되었습니다. 핵심 API와 동작 방식을 정리합니다.
Writeback API 변화
| folio API | 기존 page API | 설명 |
|---|---|---|
folio_mark_dirty(folio) | set_page_dirty(page) | folio를 더티로 표시 |
folio_start_writeback(folio) | set_page_writeback(page) | writeback 시작 표시 |
folio_end_writeback(folio) | end_page_writeback(page) | writeback 완료 |
folio_wait_writeback(folio) | wait_on_page_writeback(page) | writeback 완료 대기 |
folio_clear_dirty_for_io(folio) | clear_page_dirty_for_io(page) | I/O 전 더티 플래그 해제 |
filemap_dirty_folio(mapping, folio) | __set_page_dirty_buffers(page) | 표준 dirty_folio 구현 |
/* writeback 흐름: write_cache_pages → writepage 대체 */
/* 기존: address_space_operations::writepage (페이지 단위) */
static int myfs_writepage(struct page *page,
struct writeback_control *wbc)
{
/* 4KB 단위 I/O */
}
/* 새로운: writepages에서 folio 반복자 사용 */
static int myfs_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
struct folio *folio;
struct writeback_iter iter;
writeback_iter_init(&iter, mapping, wbc);
while ((folio = writeback_iter_next(&iter)) != NULL) {
/* folio_size(folio) 만큼 I/O 제출 */
/* large folio면 16KB~2MB를 한 번에 기록 */
folio_start_writeback(folio);
folio_unlock(folio);
/* bio 제출... */
}
return 0;
}
설명
folio 기반 writeback의 핵심 이점은 large folio를 한 번의 I/O로 기록할 수 있다는 것입니다. 기존에는 2MB THP를 writeback할 때 512번의writepage() 호출이 필요했지만, folio 기반에서는 하나의 folio에 대해 한 번의 bio를 제출합니다.
writeback_iter_next()는 v6.8+에서 도입된 반복자로, 더티 folio를 순서대로 순회합니다.
Folio Writeback 흐름
매핑과 Folio
프로세스(Process)의 가상 주소(Virtual Address)와 물리 folio 사이의 매핑(rmap, PTE)도 folio 단위로 전환되었습니다.
매핑 관련 API
| folio API | 기존 page API | 설명 |
|---|---|---|
folio_mapped(folio) | page_mapped(page) | PTE 매핑 여부 확인 |
folio_mapcount(folio) | page_mapcount(page) | 매핑 카운트 조회 |
folio_add_file_rmap_range(folio, page, nr, vma) | page_add_file_rmap(page) | file rmap 추가 (범위) |
folio_add_anon_rmap_range(folio, page, nr, vma) | page_add_anon_rmap(page) | anon rmap 추가 (범위) |
folio_remove_rmap_range(folio, page, nr, vma) | page_remove_rmap(page) | rmap 제거 (범위) |
/* folio 매핑 확인과 변환 */
/* folio의 매핑 정보 조회 */
struct address_space *mapping = folio_mapping(folio);
if (mapping) {
/* file-backed folio */
pgoff_t index = folio->index;
pr_info("file mapping at index %lu, %lu pages\n",
index, folio_nr_pages(folio));
} else if (folio_test_anon(folio)) {
/* anonymous folio */
struct anon_vma *av = folio_get_anon_vma(folio);
/* rmap 역추적으로 매핑된 VMA 탐색 */
}
/* kmap_local_folio: folio 내 특정 오프셋 매핑 */
void *addr = kmap_local_folio(folio, offset);
/* offset은 folio 시작부터의 바이트 오프셋 */
/* large folio에서 특정 4KB 영역만 매핑 가능 */
memcpy(buf, addr, PAGE_SIZE);
kunmap_local(addr);
설명
folio_mapping()은 folio->mapping에서 하위 비트 마스크를 제거하고 address_space 포인터를 반환합니다.
익명(anonymous) folio의 경우 mapping 필드에 anon_vma 포인터가 OR 0x1로 저장되어 있습니다.
kmap_local_folio()의 offset 매개변수는 highmem 아키텍처에서 large folio의 일부만 매핑할 때 중요합니다.
/* include/linux/mm.h -- large folio 매핑 카운트 */
/* folio_mapcount(): 전체 PTE 매핑 수 (order-0 호환) */
static inline int folio_mapcount(struct folio *folio)
{
if (!folio_test_large(folio))
return atomic_read(&folio->_mapcount) + 1;
return folio_total_mapcount(folio);
}
/* folio_entire_mapcount(): PMD 단위 전체 매핑 수 */
static inline int folio_entire_mapcount(struct folio *folio)
{
VM_BUG_ON_FOLIO(!folio_test_large(folio), folio);
return atomic_read(&folio->_entire_mapcount) + 1;
}
/* folio_total_mapcount(): 부분 + 전체 매핑 합계 */
int folio_total_mapcount(struct folio *folio)
{
int total = folio_entire_mapcount(folio);
int nr_pages = folio_nr_pages(folio);
for (int i = 0; i < nr_pages; i++)
total += atomic_read(&folio_page(folio, i)->_mapcount) + 1;
/* 각 sub-page의 PTE 매핑 수를 합산 */
return total;
}
/* rmap_walk: folio의 모든 매핑을 순회 */
void rmap_walk(struct folio *folio, struct rmap_walk_control *rwc)
{
if (folio_test_anon(folio))
rmap_walk_anon(folio, rwc);
else
rmap_walk_file(folio, rwc);
}
/* anon rmap walk: anon_vma → vma_interval_tree 순회 */
static void rmap_walk_anon(struct folio *folio,
struct rmap_walk_control *rwc)
{
struct anon_vma *av = folio_get_anon_vma(folio);
struct anon_vma_chain *avc;
anon_vma_interval_tree_foreach(avc, &av->rb_root,
folio->index, folio->index + folio_nr_pages(folio) - 1) {
struct vm_area_struct *vma = avc->vma;
/* vma에서 folio를 가리키는 PTE 찾아서 처리 */
rwc->rmap_one(folio, vma, ...);
}
}
설명
folio_mapcount()는 모든 매핑(PTE + PMD)의 합계를 반환합니다. small folio(order-0)에서는 단순히 _mapcount + 1입니다.
folio_entire_mapcount()는 PMD 단위 전체 매핑만 카운트하며, THP가 PMD로 매핑된 횟수를 나타냅니다.
rmap_walk()은 folio에 연결된 anon_vma(또는 file의 address_space)를 통해 모든 매핑 VMA를 순회하며, 회수(reclaim), 마이그레이션, KSM 등에서 사용됩니다.
Large Folio
Large folio는 order > 0인 folio, 즉 4KB보다 큰 compound page를 감싼 folio입니다. readahead, I/O 병합, TLB 효율에서 큰 이점을 제공합니다.
/* readahead에서 large folio 할당 (mm/readahead.c) */
static void page_cache_ra_order(
struct readahead_control *ractl,
struct file_ra_state *ra,
unsigned int new_order)
{
struct folio *folio;
unsigned int order = new_order;
while (ractl->_index < ractl->_index + ra->size) {
/* 자연 정렬: folio 시작 인덱스가 order에 맞게 정렬 */
order = min(order, ffs(ractl->_index) - 1);
/* large folio 할당 시도 */
folio = filemap_alloc_folio(
ractl->mapping->gfp_mask, order);
if (!folio) {
/* order 축소 후 재시도 */
order = order > 0 ? order - 1 : 0;
continue;
}
/* 페이지 캐시에 추가 */
filemap_add_folio(ractl->mapping,
folio, ractl->_index,
ractl->mapping->gfp_mask);
ractl->_index += folio_nr_pages(folio);
}
}
/* readahead 콜백에서 folio 순회 */
static void myfs_readahead(struct readahead_control *ractl)
{
struct folio *folio;
while ((folio = readahead_folio(ractl)) != NULL) {
size_t len = folio_size(folio);
/* len 바이트만큼 I/O 제출 */
/* order-2 folio면 16KB, order-4면 64KB */
}
}
설명
page_cache_ra_order()는 readahead 윈도우를 large folio 크기에 맞춰 할당합니다.
folio 인덱스가 자연 정렬(naturally aligned)되어야 하므로, 인덱스의 최하위 설정 비트로 최대 order를 결정합니다.
large folio 할당이 실패하면 order를 줄여 재시도하므로, 메모리 단편화(Fragmentation) 시에도 graceful degradation됩니다.
파일시스템의 .readahead 콜백(Callback)에서 readahead_folio()로 folio를 받으면, 각 folio의 크기에 맞는 I/O를 제출합니다.
Large Folio 크기 선택
| Order | 크기 | 주요 용도 | TLB 엔트리 절감 |
|---|---|---|---|
| 0 | 4KB | 기본 페이지 | - |
| 1 | 8KB | 소형 readahead | 2x |
| 2 | 16KB | 일반 readahead | 4x |
| 4 | 64KB | ARM64 기본 large folio | 16x |
| 9 | 2MB (PMD) | THP / 대형 파일 | 512x |
| 13 | 32MB (PUD, 이론) | 미래 확장 | 8192x |
Large Folio 크기 조회/관리 커널 소스
large folio를 다루는 핵심 인라인 함수들의 내부 구현을 분석합니다. 이 함수들은 include/linux/mm.h와 include/linux/page-flags.h에 정의됩니다.
/* include/linux/page-flags.h */
/* folio_test_large(): compound page(order > 0) 여부 확인 */
static inline bool folio_test_large(struct folio *folio)
{
return folio_test_head(folio);
/* PG_head 플래그 검사
* = test_bit(PG_head, &folio->flags)
* order-0 folio에는 PG_head가 설정되지 않음 */
}
/* include/linux/mm.h */
/* folio_order(): folio의 compound order 반환 */
static inline unsigned int folio_order(struct folio *folio)
{
if (!folio_test_large(folio))
return 0;
return folio->_folio_order;
/* _folio_order는 두 번째 struct page 영역에 저장
* compound page 생성 시 prep_compound_head()에서 설정 */
}
/* folio_nr_pages(): folio가 커버하는 페이지 수 */
static inline long folio_nr_pages(struct folio *folio)
{
if (!folio_test_large(folio))
return 1;
return 1L << folio->_folio_order;
/* order-2: 4페이지, order-4: 16페이지, order-9: 512페이지 */
}
/* folio_size(): folio의 바이트 크기 */
static inline size_t folio_size(struct folio *folio)
{
return PAGE_SIZE << folio_order(folio);
/* order-0: 4096, order-2: 16384, order-9: 2097152 */
}
/* folio_shift(): folio 크기의 비트 시프트 값 */
static inline unsigned int folio_shift(struct folio *folio)
{
return PAGE_SHIFT + folio_order(folio);
/* order-0: 12, order-2: 14, order-9: 21 */
}
/* folio_pfn(): folio의 head page PFN */
static inline unsigned long folio_pfn(struct folio *folio)
{
return page_to_pfn(&folio->page);
/* head page의 물리 프레임 번호 */
}
/* folio_estimated_sharers(): 공유 정도 추정 */
static inline int folio_estimated_sharers(struct folio *folio)
{
return folio_mapcount(folio) - 1;
/* 매핑 수 - 1 = 공유 프로세스 수 추정
* split 결정 시 참고 (공유 많으면 split 비용 높음) */
}
코드 설명
large folio 관련 인라인 함수들의 핵심 특징:
- folio_test_large():
PG_head플래그 하나만 검사하므로 비용이 거의 없습니다. 이 함수를 게이트키퍼로 사용하여 order-0 경로를 빠르게 처리합니다. - _folio_order 접근:
folio_test_large()가 false인 folio에서_folio_order를 읽으면 쓰레기 값(tail page 영역이 없으므로)이 반환됩니다. 반드시 guard 검사 후 접근해야 합니다. - folio_nr_pages(): order-0이면 1을 직접 반환하여 비트 시프트를 회피합니다. hot path에서 분기 예측기가 이 경로를 최적화합니다.
- mTHP(Multi-size THP): v6.4+에서 익명 folio에 대해 order-2(16KB), order-4(64KB) 등 PMD 크기가 아닌 large folio를 지원합니다.
/sys/kernel/mm/transparent_hugepage/hugepages-*kB/에서 크기별 활성화를 제어합니다.
THP와 Folio 통합
Transparent Huge Pages(THP)는 folio 도입 이전부터 compound page를 사용했습니다. folio는 THP를 일반적인 "large folio"의 특수한 경우로 통합합니다.
통합 과정
| 구분 | 기존 THP | folio 통합 후 |
|---|---|---|
| 데이터 구조 | compound page (struct page[]) | struct folio (order-9) |
| 파일 THP | 별도 코드 경로 (khugepaged) | readahead large folio + khugepaged 통합 |
| 익명 THP | order-9 고정 | mTHP: order-2, 4, 9 등 유연한 크기 |
| LRU 관리 | THP 전용 split/compound 처리 | 일반 folio LRU로 통합 |
| 회수 | split_huge_page() 후 개별 회수 | large folio 단위 회수 or split |
| swapout | split 후 개별 swap | large folio swap (v6.8+) |
/* mTHP (multi-size THP): v6.4+ 익명 large folio */
/* 페이지 폴트 경로에서 large folio 할당 시도 */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct folio *folio;
int order;
/* 최적 order 결정 (VMA 크기, 정렬, sysfs 설정 고려) */
order = thp_vma_suitable_order(vmf->vma, vmf->address);
/* large anon folio 할당 시도 */
folio = vma_alloc_folio(GFP_HIGHUSER_MOVABLE, order,
vmf->vma, vmf->address);
if (!folio && order > 0) {
/* fallback: order 축소 */
order = 0;
folio = vma_alloc_folio(GFP_HIGHUSER_MOVABLE, 0,
vmf->vma, vmf->address);
}
folio_zero_user(folio, vmf->address);
/* ... PTE 설정, rmap 추가 ... */
}
/* sysfs로 mTHP order 제어 */
/* /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled */
/* always, madvise, never 설정 가능 */
설명
mTHP(multi-size THP)는 기존 order-9(2MB) 고정이던 익명 THP를 order-2(16KB), order-4(64KB) 등 다양한 크기로 확장합니다.thp_vma_suitable_order()는 VMA의 크기, 정렬, sysfs 설정을 종합하여 최적 order를 결정합니다.
할당 실패 시 graceful fallback으로 더 작은 order를 시도하므로, 단편화 상황에서도 가능한 큰 folio를 사용합니다.
파일시스템별 Folio 지원 현황
파일시스템의 folio 지원은 .read_folio, .dirty_folio, .release_folio 등 address_space_operations 콜백의 folio 버전 구현 여부로 판단합니다.
| 파일시스템 | read_folio | dirty_folio | large folio | 비고 |
|---|---|---|---|---|
| ext4 | v5.19+ | v5.19+ | v6.3+ (readahead) | 가장 적극적인 전환 |
| XFS | v5.18+ | v5.18+ | v6.0+ (iomap large folio) | iomap 프레임워크 통합 |
| btrfs | v5.19+ | v6.0+ | v6.6+ (부분) | CoW 특성으로 복잡 |
| f2fs | v6.1+ | v6.1+ | v6.4+ | 모바일/SSD 최적화 |
| tmpfs/shmem | v5.17+ | v5.17+ | v6.0+ (네이티브) | 최초 large folio 지원 |
| NFS | v6.3+ | v6.3+ | v6.5+ | 네트워크 I/O 병합 이점 |
| FUSE | v6.6+ | v6.6+ | 제한적 | 유저스페이스 의존 |
| FAT/exFAT | v6.2+ | v6.2+ | 미지원 | 레거시 호환 |
iomap 프레임워크를 사용하는 파일시스템은
iomap_readahead() → iomap_readpage_iter()에서 자동으로 large folio를 지원합니다.
iomap이 folio 크기에 맞는 extent 범위를 계산하여 최적의 I/O를 생성합니다.
/* 파일시스템이 large folio 지원을 선언하는 방법 */
/* 방법 1: mapping_set_large_folios() */
void myfs_inode_init(struct inode *inode)
{
/* i_mapping에 large folio 지원 플래그 설정 */
mapping_set_large_folios(inode->i_mapping);
}
/* 방법 2: 특정 order 범위 지정 (v6.8+) */
void myfs_inode_init_v2(struct inode *inode)
{
/* order-0 ~ order-4 (최대 64KB) 허용 */
mapping_set_folio_min_order(inode->i_mapping, 0);
mapping_set_folio_order_range(inode->i_mapping, 0, 4);
}
/* readahead 시 folio 크기 확인 */
static void myfs_readahead(struct readahead_control *ractl)
{
struct folio *folio;
while ((folio = readahead_folio(ractl))) {
size_t size = folio_size(folio);
pr_debug("readahead folio: order=%u size=%zu\n",
folio_order(folio), size);
/* size가 4KB, 16KB, 64KB 등일 수 있음 */
}
}
설명
mapping_set_large_folios()는 address_space에 AS_LARGE_FOLIO_SUPPORT 플래그를 설정합니다.
이 플래그가 없으면 readahead가 order-0 folio만 할당합니다.
v6.8+에서는 mapping_set_folio_order_range()로 최소/최대 order를 세밀하게 제어할 수 있습니다.
파일시스템이 large folio를 지원하려면 writeback, truncate, hole-punch 등에서도 올바르게 동작해야 합니다.
Folio 마이그레이션
NUMA 밸런싱, compaction, memory hotplug 등에서 folio를 다른 물리 위치로 이동하는 마이그레이션 경로도 folio 기반으로 전환되었습니다.
/* mm/migrate.c -- folio 마이그레이션 핵심 */
/* migrate_folio: 기본 마이그레이션 구현 */
int migrate_folio(struct address_space *mapping,
struct folio *dst, struct folio *src,
enum migrate_mode mode)
{
int rc;
/* 1. src folio를 XArray에서 교체 */
rc = folio_migrate_mapping(mapping, dst, src, 0);
if (rc != MIGRATEPAGE_SUCCESS)
return rc;
/* 2. 데이터 복사 (folio_size 바이트) */
if (mode != MIGRATE_SYNC_NO_COPY)
folio_migrate_copy(dst, src);
else
folio_migrate_flags(dst, src);
return MIGRATEPAGE_SUCCESS;
}
/* folio_migrate_mapping: 페이지 캐시 XArray 엔트리 교체 */
int folio_migrate_mapping(struct address_space *mapping,
struct folio *newfolio,
struct folio *folio,
int extra_count)
{
/* XArray lock 획득 */
xa_lock_irq(&mapping->i_pages);
/* XArray에서 old folio → new folio 교체 */
xa_store(&mapping->i_pages, folio->index,
newfolio, GFP_KERNEL);
/* 참조/매핑 카운트 이전 */
newfolio->index = folio->index;
newfolio->mapping = folio->mapping;
xa_unlock_irq(&mapping->i_pages);
return MIGRATEPAGE_SUCCESS;
}
설명
migrate_folio()는 address_space_operations::migrate_folio의 기본 구현입니다.
large folio의 경우 folio_migrate_copy()가 folio_size() 바이트 전체를 한 번에 복사합니다.
XArray 엔트리 교체도 large folio면 여러 슬롯을 한 번에 갱신해야 하므로, xa_store_range()가 사용됩니다.
compaction에서는 migrate_folios()로 여러 folio를 batch 마이그레이션하며, TLB flush를 최적화합니다.
Swap과 Folio
스왑 서브시스템도 folio 단위로 전환되고 있습니다. v6.8+에서는 large folio를 분할하지 않고 통째로 스왑할 수 있습니다.
스왑 관련 변화
| 기능 | 기존 | folio 전환 후 | 버전 |
|---|---|---|---|
| swapout | split_huge_page() 후 개별 swap | folio 단위 통째 swap | v6.8+ |
| swapin | 페이지 단위 읽기 | folio 단위 readahead | v6.5+ |
| swap cache | struct page 기반 | struct folio 기반 | v6.0+ |
| swap slot | 연속 슬롯 불필요 | large folio용 연속 슬롯 할당 | v6.8+ |
| zswap | 페이지 단위 압축 | folio 단위 압축 | v6.6+ |
/* large folio swap-out 경로 (간략화) */
static int swap_writepage_folio(struct folio *folio,
struct writeback_control *wbc)
{
swp_entry_t entry;
int nr_pages = folio_nr_pages(folio);
/* 연속 swap 슬롯 할당 (nr_pages개) */
entry = folio_alloc_swap(folio);
if (!entry.val)
return -ENOMEM;
/* folio 전체를 한 번의 I/O로 기록 */
swap_write_folio(folio, wbc);
return 0;
}
설명
folio_alloc_swap()은 large folio에 대해 연속된 스왑 슬롯을 할당합니다.
기존에는 order-9 THP를 스왑아웃하려면 먼저 512개 페이지로 분할해야 했지만, folio 기반에서는 통째로 처리합니다.
이는 스왑 I/O 효율과 swapin 시 readahead 효과를 크게 개선합니다.
Large Folio Swapin Readahead
스왑에서 folio를 다시 읽어올 때도 large folio 단위로 readahead가 가능합니다:
/* swapin readahead: 연속 swap 슬롯을 large folio로 읽기 */
struct folio *swap_cluster_readahead(swp_entry_t entry,
gfp_t gfp_mask,
struct vm_fault *vmf)
{
unsigned int order = swap_entry_order(entry);
struct folio *folio;
/* 연속 슬롯이면 large folio로 한 번에 swapin */
folio = folio_alloc(gfp_mask, order);
if (folio) {
/* folio_size() 바이트를 한 번의 I/O로 읽기 */
swap_read_folio(folio, entry);
folio_add_lru(folio);
}
return folio;
}
설명
swap_entry_order()는 스왑 슬롯이 연속 할당되었는지 확인하여 원래 folio의 order를 복원합니다.
이를 통해 swapout 시 large folio였던 것을 swapin 시에도 large folio로 복원하여, THP split/재조립 비용을 피합니다.
zswap의 경우 zswap_load()에서도 folio 단위 압축 해제를 지원합니다.
메모리 계정(Accounting)과 Folio
memcg(memory cgroup)의 charge/uncharge도 folio 단위로 전환되었습니다. large folio는 한 번의 charge로 전체 페이지를 계정 처리합니다.
/* memcg folio charge/uncharge */
/* folio를 memcg에 charge */
int mem_cgroup_charge(struct folio *folio,
struct mm_struct *mm,
gfp_t gfp)
{
unsigned int nr_pages = folio_nr_pages(folio);
struct mem_cgroup *memcg;
memcg = get_mem_cgroup_from_mm(mm);
/* nr_pages 만큼 한 번에 charge */
if (try_charge(memcg, gfp, nr_pages))
return -ENOMEM;
/* folio에 memcg 연결 */
folio->memcg_data = (unsigned long)memcg;
return 0;
}
/* uncharge: folio 해제 시 자동 호출 */
void mem_cgroup_uncharge(struct folio *folio)
{
unsigned int nr_pages = folio_nr_pages(folio);
struct mem_cgroup *memcg = folio_memcg(folio);
if (memcg) {
uncharge(memcg, nr_pages);
folio->memcg_data = 0;
}
}
설명
기존에는 compound page의 각 sub-page에 대한 charge/uncharge가 혼란스러웠습니다. folio 기반에서는folio_nr_pages()로 전체 크기를 한 번에 계산하여 charge합니다.
folio_memcg()는 folio->memcg_data에서 memcg 포인터를 추출합니다.
large folio split 시에는 새 folio들에 charge를 재분배해야 하므로 mem_cgroup_split_huge_fixup()이 호출됩니다.
folio_memcg() 내부 구현
folio_memcg()는 folio에 연결된 memcg 포인터를 반환하며, charge 이전(Transfer) 시에도 사용됩니다:
/* include/linux/memcontrol.h -- folio memcg 조회 */
/* folio_memcg(): folio에 연결된 memcg 반환 */
static inline struct mem_cgroup *folio_memcg(struct folio *folio)
{
unsigned long memcg_data = folio->memcg_data;
if (memcg_data & MEMCG_DATA_OBJEXTS)
return NULL; /* objcg 모드 */
return (struct mem_cgroup *)(memcg_data & ~MEMCG_DATA_FLAGS_MASK);
}
/* folio_memcg_check(): RCU 보호 하 조회 (NULL 가능) */
static inline struct mem_cgroup *folio_memcg_check(struct folio *folio)
{
if (folio_memcg_kmem(folio))
return obj_cgroup_memcg(folio_objcg(folio));
return folio_memcg(folio);
}
/* split 시 charge 재분배 */
void mem_cgroup_split_huge_fixup(struct folio *old_folio,
unsigned int nr_new)
{
struct mem_cgroup *memcg = folio_memcg(old_folio);
unsigned int old_nr = folio_nr_pages(old_folio);
unsigned int new_nr = old_nr / nr_new;
/* 새 folio들에 charge 균등 분배 */
for (int i = 1; i < nr_new; i++) {
struct folio *new_folio = ...;
new_folio->memcg_data = (unsigned long)memcg;
/* 참조 카운트 조정: memcg의 counter는 변하지 않음
* (전체 charge 양은 동일하므로) */
}
}
설명
folio_memcg()는 folio->memcg_data에서 플래그 비트를 마스킹하여 순수 memcg 포인터를 반환합니다.
MEMCG_DATA_OBJEXTS 플래그가 설정된 경우 slab 오브젝트 cgroup 모드이므로 NULL을 반환합니다.
folio_memcg_check()는 RCU 보호 하에서 안전하게 조회하며, kmem folio의 경우 obj_cgroup_memcg()를 통해 간접 변환합니다.
mem_cgroup_split_huge_fixup()은 large folio split 시 각 새 folio에 원래 memcg를 연결하되, 전체 charge 양은 변경하지 않습니다.
드라이버 영향
struct page를 직접 사용하는 드라이버는 folio 전환의 영향을 받습니다. 주요 변환 패턴과 주의사항을 정리합니다.
드라이버 전환 가이드
| 기존 코드 | folio 전환 코드 | 주의사항 |
|---|---|---|
struct page *page | struct folio *folio | 항상 head page를 가리키는지 확인 |
kmap(page) | kmap_local_folio(folio, 0) | offset 인자 추가 |
page_address(page) | folio_address(folio) | highmem에서는 사용 불가 |
get_page(page) | folio_get(folio) | tail page에서 호출하면 안 됨 |
put_page(page) | folio_put(folio) | compound 전체 해제 |
page_to_phys(page) | folio_to_phys(folio) | DMA 주소 변환(Address Translation) 시 |
set_page_dirty(page) | folio_mark_dirty(folio) | writeback 경로 확인 |
mm/folio-compat.c에 호환 래퍼가 있습니다.
예를 들어 unlock_page(page)는 내부적으로 folio_unlock(page_folio(page))를 호출합니다.
이 래퍼는 성능 오버헤드(추가 compound_head() 호출)가 있으므로 가능한 직접 folio API를 사용하세요.
/* 드라이버 전환 예시: DRM GEM 오브젝트 */
/* 기존: struct page 배열 기반 */
struct page **pages;
pages = kvmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
for (i = 0; i < npages; i++) {
pages[i] = shmem_read_mapping_page(mapping, i);
if (IS_ERR(pages[i]))
goto err;
}
/* 새로운: folio 기반 (shmem large folio 활용) */
struct folio *folio;
pgoff_t index = 0;
while (index < npages) {
folio = shmem_read_folio(mapping, index);
if (IS_ERR(folio))
goto err;
/* large folio면 여러 페이지를 한 번에 커버 */
for (j = 0; j < folio_nr_pages(folio); j++)
pages[index + j] = folio_page(folio, j);
index += folio_nr_pages(folio);
folio_put(folio);
}
설명
DRM/GEM 드라이버는 GPU 버퍼(Buffer)를 shmem 페이지로 backing합니다. folio 전환 시shmem_read_folio()를 사용하면
large folio를 통해 I/O 횟수가 줄어들고, 연속 물리 메모리 확률이 높아져 IOMMU 매핑도 효율적입니다.
기존 struct page 배열 인터페이스가 필요한 경우 folio_page()로 변환합니다.
전환 현황과 로드맵
struct page 축소 목표
folio 전환의 궁극적 목표 중 하나는 struct page를 현재 64바이트에서 8바이트까지 축소하는 것입니다:
| 단계 | struct page 크기 | 설명 |
|---|---|---|
| 현재 | 64바이트 | 모든 메타데이터 포함 |
| 중간 목표 | ~32바이트 | folio로 이전 가능한 필드 제거 |
| 최종 목표 | 8바이트 | compound_head 포인터만 유지 |
struct page 배열(vmemmap)이 약 1GB를 차지합니다.
8바이트로 축소하면 이 오버헤드가 약 125MB로 줄어들어, ~875MB의 메모리를 절약할 수 있습니다.
256GB 서버에서는 ~3.5GB가 절약됩니다.
Folio와 Direct I/O
Direct I/O는 페이지 캐시(Page Cache)를 우회하여 사용자 버퍼와 디스크 간 직접 데이터를 전송합니다. 그러나 folio와의 상호작용은 여전히 중요합니다.
페이지 캐시 무효화
Direct I/O는 쓰기(Write) 전에 해당 범위의 페이지 캐시 folio를 반드시 무효화(Invalidate)해야 합니다. 이는 캐시된 folio와 디스크 데이터의 불일치를 방지합니다:
/* iomap-based Direct I/O와 folio 상호작용 */
/* Direct I/O 진입: iomap_dio_rw() */
ssize_t iomap_dio_rw(struct kiocb *iocb,
struct iov_iter *iter,
const struct iomap_ops *ops,
const struct iomap_dio_ops *dops,
unsigned int dio_flags)
{
struct address_space *mapping = iocb->ki_filp->f_mapping;
/* 1단계: 해당 범위의 페이지 캐시 folio 무효화 */
filemap_invalidate_pages(mapping, pos, pos + count - 1);
/* 2단계: dirty folio가 남아있으면 writeback 후 재시도 */
if (filemap_write_and_wait_range(mapping, pos, end)) {
/* folio writeback 완료 대기 */
}
/* 3단계: Direct I/O 수행 (페이지 캐시 우회) */
ret = iomap_dio_bio_iter(dio, iter);
return ret;
}
설명
iomap_dio_rw()는 Direct I/O 진입점입니다. 먼저 filemap_invalidate_pages()로 해당 파일 범위의 모든 캐시된 folio를 무효화합니다.
dirty folio가 있으면 filemap_write_and_wait_range()로 writeback을 완료한 후 무효화합니다.
이 과정에서 large folio는 전체가 무효화 대상이 되므로, 부분적으로 겹치는 경우에도 전체 folio가 처리됩니다.
Buffered I/O Fallback
Direct I/O가 불가능한 상황에서는 buffered I/O로 전환(Fallback)되며, 이때 folio 할당이 발생합니다:
/* Direct I/O → Buffered I/O fallback 시나리오 */
/* 1. 파일시스템이 DIO를 지원하지 않는 경우 */
if (!mapping->a_ops->direct_IO && !iomap_dio_supported(iocb)) {
/* buffered write로 전환: folio 할당 발생 */
return generic_file_write_iter(iocb, from);
/* → filemap_grab_folio() → folio를 페이지 캐시에 삽입 */
}
/* 2. 정렬되지 않은 I/O의 경우 */
if (pos & (i_blocksize(inode) - 1)) {
/* 블록 경계에 정렬되지 않으면 buffered I/O로 전환 */
/* folio_alloc() → filemap_add_folio() */
}
/* 3. O_DSYNC DIO 후 metadata 업데이트 */
if (iocb->ki_flags & IOCB_DSYNC) {
/* 데이터는 DIO로, 메타데이터는 folio 기반 저널 */
generic_write_sync(iocb, ret);
}
설명
Direct I/O가 불가능한 경우(파일시스템 미지원, 비정렬 I/O 등) buffered I/O로 전환됩니다. 이때filemap_grab_folio()를 통해 folio가 할당되고 페이지 캐시에 삽입됩니다.
O_DSYNC 모드의 DIO에서는 데이터는 직접 기록하되, 메타데이터 업데이트는 folio 기반 저널을 사용합니다.
Buffered I/O vs Direct I/O 비교
| 항목 | Buffered I/O (folio 기반) | Direct I/O |
|---|---|---|
| 페이지 캐시 | folio를 캐시에 삽입/조회 | 캐시 우회 (사전 무효화) |
| 메모리 할당 | filemap_alloc_folio() | 사용자 버퍼 직접 사용 |
| Large Folio 활용 | readahead로 large folio 할당 | 해당 없음 |
| 데이터 복사 | 사용자 ↔ folio ↔ 디스크 (2회) | 사용자 ↔ 디스크 (1회) |
| 정렬 요구사항 | 없음 (folio 내부 오프셋 처리) | 블록 크기 정렬 필수 |
| 동시성 | folio lock으로 동기화 | invalidate lock 필요 |
| Readahead | folio 기반 readahead 활용 | 애플리케이션이 직접 관리 |
| writeback | folio_mark_dirty() → 지연 기록 | 즉시 디스크 기록 |
| 적합한 워크로드 | 반복 접근, 작은 I/O | 대용량 순차, DB 엔진 |
filemap_invalidate_lock()을 사용하여
buffered I/O와의 동시 접근을 안전하게 처리합니다. 이 락은 folio 할당/해제와 DIO가 동시에 발생하는 것을 방지합니다.
Folio와 Memory Failure (hwpoison)
하드웨어 메모리 오류(Hardware Memory Error)가 발생하면 커널은 해당 메모리를 격리하거나 복구해야 합니다. folio 기반 메모리 관리에서 이 과정은 기존 page 기반보다 더 복잡한 고려사항이 있습니다.
ECC 오류와 folio
ECC(Error Correcting Code) 메모리는 두 가지 유형의 오류를 감지합니다:
| 오류 유형 | 커널 대응 | folio 처리 |
|---|---|---|
| CE (Correctable Error) | 로그 기록, 선제적 마이그레이션 | soft_offline_page() → folio 마이그레이션 |
| UCE (Uncorrectable Error) | MCE 인터럽트, 페이지 격리 | memory_failure() → folio 격리/프로세스 종료 |
memory_failure()와 folio 처리
memory_failure() 함수는 하드웨어 오류가 발생한 물리 페이지를 처리합니다. folio 기반에서는 단일 page가 아닌 전체 folio를 고려해야 합니다:
/* mm/memory-failure.c -- folio 기반 memory failure 처리 */
int memory_failure(unsigned long pfn, int flags)
{
struct folio *folio = pfn_folio(pfn);
/* HWPoison 플래그 설정 */
folio_set_hwpoison(folio);
if (folio_test_large(folio)) {
/* Large folio: split 시도 후 해당 subpage만 poison */
if (folio_try_split(folio) == 0) {
/* split 성공: 오류 발생한 page만 격리 */
folio = pfn_folio(pfn); /* 새로운 order-0 folio */
} else {
/* split 실패: 전체 large folio poison */
}
}
if (folio_mapped(folio)) {
/* 매핑된 folio: 프로세스에 SIGBUS 전송 */
collect_procs_folio(folio, &tokill);
unmap_poisoned_folio(folio, pfn);
}
return identify_page_state(pfn, folio, flags);
}
설명
memory_failure()는 MCE(Machine Check Exception)에 의해 호출됩니다.
pfn_folio()로 해당 PFN이 속한 folio를 찾고, folio_set_hwpoison()으로 플래그를 설정합니다.
large folio의 경우 먼저 split을 시도하여 오류 page만 격리하려 합니다. split이 실패하면 전체 folio가 poison 처리됩니다.
매핑된 folio는 해당 프로세스에 SIGBUS를 전송하고, PTE를 해제하여 접근을 차단합니다.
복구 전략: Soft Offline vs Hard Offline
커널은 오류 심각도에 따라 두 가지 복구 전략을 사용합니다:
/* Soft offline: folio 마이그레이션으로 복구 (CE 대응) */
int soft_offline_page(unsigned long pfn, int flags)
{
struct folio *folio = pfn_folio(pfn);
/* 새 folio에 데이터 복사(마이그레이션) */
ret = migrate_folio(mapping, new_folio, folio, MIGRATE_SYNC);
if (ret == 0) {
/* 성공: 원래 folio를 오프라인 처리 */
folio_set_hwpoison(folio);
/* buddy에 반환하지 않고 영구 격리 */
}
return ret;
}
/* Hard offline: folio 격리 (UCE 대응) */
/* memory_failure() 내부에서 수행 */
static int me_pagecache_clean(struct folio *folio)
{
/* clean folio: 캐시에서 제거만 하면 복구 가능 */
filemap_remove_folio(folio);
return MF_RECOVERED;
}
static int me_pagecache_dirty(struct folio *folio)
{
/* dirty folio: 데이터 손실 불가피, 프로세스 종료 */
folio_clear_dirty(folio);
return MF_FAILED; /* → SIGBUS로 프로세스 kill */
}
설명
Soft offline은 교정 가능 오류(CE)에 대한 선제적 대응입니다.migrate_folio()로 folio를 새 물리 위치로 이동하고, 원래 위치를 영구 격리합니다.
Hard offline은 교정 불가능 오류(UCE)에 대한 대응입니다. clean folio는 캐시에서 제거만 하면 디스크에서 다시 읽을 수 있으므로 복구됩니다.
dirty folio는 데이터 손실이 불가피하므로 해당 프로세스에 SIGBUS를 전송하여 종료합니다.
folio_nr_pages() 만큼의 메모리 손실을 의미합니다. order-9 folio(2MB)의 경우
하나의 4KB page 오류로 2MB 전체가 격리될 수 있습니다.
커널 빌드 옵션
folio 관련 커널 설정 옵션을 정리합니다. folio 자체는 항상 활성화되어 있으며, 관련 기능들의 설정입니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_TRANSPARENT_HUGEPAGE | y (대부분) | THP/mTHP large folio 지원 |
CONFIG_READ_ONLY_THP_FOR_FS | y | 파일 THP (읽기 전용(Read-Only) large folio) |
CONFIG_LRU_GEN | y (v6.1+) | MGLRU: folio 기반 다세대 LRU |
CONFIG_MEMCG | y | memcg folio accounting |
CONFIG_MIGRATION | y | folio 마이그레이션 지원 |
CONFIG_COMPACTION | y | folio 기반 compaction |
CONFIG_SWAP | y | folio swap 지원 |
CONFIG_ZSWAP | y (선택) | folio 기반 압축 스왑 |
CONFIG_DEBUG_VM | n | folio 디버깅(Debugging) 검증 (VM_BUG_ON_FOLIO) |
# mTHP(multi-size THP) 설정 확인 및 제어
ls /sys/kernel/mm/transparent_hugepage/hugepages-*/
# 16KB anonymous large folio 활성화
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-16kB/enabled
# 64KB anonymous large folio 활성화
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
# large folio 통계 확인
grep -i folio /proc/vmstat
# thp_file_alloc: 파일 large folio 할당 수
# thp_file_fallback: large folio 할당 실패 → fallback 수
# meminfo에서 large folio 사용량
grep -E 'AnonHugePages|ShmemHugePages|FileHugePages' /proc/meminfo
설명
mTHP 설정은/sys/kernel/mm/transparent_hugepage/hugepages-NkB/ 디렉토리에서 각 크기별로 제어합니다.
/proc/vmstat의 thp_file_* 카운터로 파일 large folio의 할당 성공/실패를 모니터링할 수 있습니다.
CONFIG_DEBUG_VM을 활성화하면 VM_BUG_ON_FOLIO()가 folio 상태 불일치를 런타임에 검출합니다.
성능 영향
folio 전환과 large folio의 성능 영향을 벤치마크 데이터로 분석합니다.
벤치마크 상세
| 벤치마크 | order-0 (기준) | order-4 large folio | 개선율 |
|---|---|---|---|
| fio 순차 읽기 (ext4, NVMe) | 2.1 GB/s | 3.0 GB/s | +43% |
| fio 순차 쓰기 (XFS, NVMe) | 1.8 GB/s | 2.4 GB/s | +33% |
| kernel build (-j$(nproc)) | 38.2s | 36.1s | +5.5% |
| PostgreSQL pgbench (읽기 위주) | 45K tps | 52K tps | +15% |
| Redis GET (tmpfs) | 890K ops/s | 920K ops/s | +3.4% |
| page fault 지연(Latency) | 1.2 us | 0.9 us | -25% |
| LRU lock 경합 (perf) | 8.2% | 1.4% | -83% |
- 순차 I/O 위주 워크로드 (DB 테이블 스캔, 로그 처리) -- readahead large folio가 가장 효과적
- 대규모 파일 서빙 (CDN, 미디어 스트리밍) -- TLB 미스 감소 + I/O 병합
- 많은 프로세스가 동일 파일 공유 -- LRU lock 경합 감소
- ARM64 서버 -- contiguous PTE로 large folio TLB 이점 극대화
- 내부 단편화 -- 64KB folio에 4KB만 사용하면 60KB 낭비
- 회수 단위 증가 -- 일부만 hot해도 folio 전체를 유지해야
- 할당 실패 가능성 -- 메모리 단편화 시 high-order 할당 실패
- 파일 끝(EOF) 처리 -- 파일 크기가 folio 크기의 배수가 아닌 경우 나머지 영역 zero-fill
Folio Split
large folio를 작은 folio로 분할하는 split 연산은 회수, truncate, 부분 unmap 등에서 필요합니다.
/* folio split: large folio → 여러 작은 folio */
int split_folio(struct folio *folio)
{
/* folio를 order-0 folio들로 분할 */
return split_huge_page(&folio->page);
}
int split_folio_to_order(struct folio *folio, int new_order)
{
/* 특정 order로 분할 (예: order-9 → order-4) */
return split_huge_page_to_list_to_order(
&folio->page, NULL, new_order);
}
/* split이 필요한 상황 */
/* 1. truncate: 파일 크기 축소 시 folio의 일부만 유효 */
/* 2. 부분 munmap: VMA가 folio 일부만 커버 */
/* 3. 회수 실패: large folio 회수 불가 시 분할 후 재시도 */
/* 4. mprotect: 보호 속성이 folio 내에서 다를 때 */
설명
split_folio()는 large folio를 order-0 folio들로 완전 분할합니다.
v6.8+에서는 split_folio_to_order()로 중간 크기로 분할할 수 있어, 불필요한 완전 분할을 피합니다.
split은 비용이 높은 연산(rmap 갱신, XArray 재구성, memcg 재charge)이므로 가능한 피해야 합니다.
/* mm/huge_memory.c -- folio split 내부 구현 (간략화) */
int split_huge_page_to_list_to_order(struct page *page,
struct list_head *list, unsigned int new_order)
{
struct folio *folio = page_folio(page);
int order = folio_order(folio);
int nr_new = 1 << (order - new_order);
/* 1. folio 잠금 및 참조 확인 */
folio_lock(folio);
if (folio_ref_count(folio) !=
folio_mapcount(folio) + folio_has_private(folio) + 1) {
/* 예상 외 참조 → split 불가 */
return -EAGAIN;
}
/* 2. XArray 엔트리 분할 */
if (folio_mapping(folio)) {
xa_lock_irq(&mapping->i_pages);
/* 기존: index ~ index+nr_pages-1에 같은 folio
* 분할 후: 각 새 folio가 자기 범위를 차지 */
for (i = 0; i < nr_new; i++) {
struct folio *new_folio =
(struct folio *)folio_page(folio, i << new_order);
for (j = 0; j < (1 << new_order); j++)
xa_store(&mapping->i_pages,
folio->index + (i << new_order) + j,
new_folio, GFP_KERNEL);
}
xa_unlock_irq(&mapping->i_pages);
}
/* 3. 각 새 folio 초기화 */
for (i = 1; i < nr_new; i++) {
struct folio *tail_folio =
(struct folio *)folio_page(folio, i << new_order);
/* 새 folio의 flags, mapping, index 설정 */
tail_folio->mapping = folio->mapping;
tail_folio->index = folio->index + (i << new_order);
/* compound page 구조 재구성 */
prep_compound_head((struct page *)tail_folio, new_order);
}
/* 4. memcg charge 분배 */
mem_cgroup_split_huge_fixup(folio, nr_new);
/* 5. 원래 folio의 order 갱신 */
folio->_folio_order = new_order;
return 0;
}
설명
split_huge_page_to_list_to_order()는 large folio split의 핵심 구현입니다.
참조 카운트가 예상과 다르면(다른 경로에서 참조 중) split을 거부하고 -EAGAIN을 반환합니다.
XArray 분할에서는 기존에 하나의 multi-index 엔트리가 가리키던 범위를 여러 개의 새 folio 엔트리로 교체합니다.
v6.8+의 split_folio_to_order()는 중간 order로 분할하여 불필요한 완전 분할을 피하고, compound page 재구성 비용을 줄입니다.
Folio 디버깅
folio 관련 문제를 진단하는 도구와 기법을 정리합니다.
# folio/large folio 통계 확인
grep -E 'thp_|folio' /proc/vmstat
# 파일별 large folio 사용 확인 (v6.6+)
cat /proc/PID/smaps | grep -A5 'FilePmdMapped\|FileHugeMapped'
# mTHP 크기별 할당 통계
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/stats/anon_alloc
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/stats/anon_alloc_fallback
# ftrace: folio 할당/해제 추적
echo 1 > /sys/kernel/tracing/events/filemap/mm_filemap_add_to_page_cache/enable
echo 1 > /sys/kernel/tracing/events/filemap/mm_filemap_delete_from_page_cache/enable
cat /sys/kernel/tracing/trace_pipe
# perf: LRU lock 경합 측정 (folio 전환 효과 확인)
perf lock record -a -- sleep 10
perf lock report | grep lru
# crash/drgn: folio 상태 덤프
# (drgn 스크립트 예시)
# folio = Object(prog, 'struct folio', address=0xffff...)
# print(f"order={folio._folio_order}, flags=0x{folio.flags.value_():x}")
설명
/proc/vmstat의 thp_file_alloc, thp_file_fallback, thp_split_folio 등의 카운터로 large folio 동작을 모니터링합니다.
mTHP 크기별 통계는 /sys/kernel/mm/transparent_hugepage/hugepages-NkB/stats/에서 확인합니다.
ftrace의 filemap 이벤트는 페이지 캐시 folio의 추가/제거를 실시간(Real-time) 추적합니다.
VM_BUG_ON_FOLIO() 매크로가 다음을 검증합니다:
- folio가 tail page가 아닌지 (head page 보장)
- folio_nr_pages()가 order와 일치하는지
- folio의 refcount가 음수가 아닌지
- folio->mapping이 유효한 address_space를 가리키는지
Truncate와 Folio
파일 크기를 줄이는 truncate 연산은 large folio에서 특별한 처리가 필요합니다. folio의 일부만 잘려야 하는 경우 split이 발생합니다.
/* mm/truncate.c -- folio truncate 경로 (간략화) */
void truncate_inode_pages_range(struct address_space *mapping,
loff_t lstart, loff_t lend)
{
struct folio *folio;
struct folio_batch fbatch;
pgoff_t start = lstart >> PAGE_SHIFT;
/* 1. 완전히 잘리는 folio: 페이지 캐시에서 제거 */
folio_batch_init(&fbatch);
while (filemap_get_folios(mapping, &start, lend, &fbatch)) {
for (int i = 0; i < folio_batch_count(&fbatch); i++) {
folio = fbatch.folios[i];
folio_lock(folio);
if (folio_test_large(folio) &&
folio->index < start) {
/* 2. 부분 truncate: folio split 필요 */
if (split_folio(folio) == 0) {
/* split 성공: 분할된 개별 folio 재처리 */
folio_unlock(folio);
continue;
}
}
/* 3. folio 전체 제거 */
truncate_cleanup_folio(folio);
filemap_remove_folio(folio);
folio_unlock(folio);
}
folio_batch_release(&fbatch);
}
/* 4. 부분 zero-fill: 마지막 folio의 잘린 부분을 0으로 채움 */
if (lstart & ~PAGE_MASK) {
folio = filemap_lock_folio(mapping, lstart >> PAGE_SHIFT);
if (!IS_ERR(folio)) {
size_t offset = offset_in_folio(folio, lstart);
folio_zero_range(folio, offset,
folio_size(folio) - offset);
folio_unlock(folio);
folio_put(folio);
}
}
}
설명
truncate에서 large folio가 부분적으로 잘리는 경우(folio의 시작 인덱스가 truncate 시작점보다 앞인 경우) split이 필요합니다.folio_zero_range()는 truncate 지점 이후를 zero-fill하여 보안(정보 유출 방지)을 보장합니다.
offset_in_folio(folio, pos)는 파일 오프셋을 folio 내 바이트 오프셋으로 변환합니다.
large folio truncate는 비용이 높으므로, 빈번한 truncate가 예상되는 파일에는 large folio가 적합하지 않을 수 있습니다.
Folio API 치트시트
folio 관련 주요 API를 용도별로 정리한 빠른 참조표입니다.
생성/해제/참조
| API | 인자 | 반환 | 설명 |
|---|---|---|---|
folio_alloc(gfp, order) | GFP 플래그, order | struct folio * | Buddy에서 folio 할당 |
filemap_alloc_folio(gfp, order) | GFP 플래그, order | struct folio * | 페이지 캐시용 할당 (NUMA 고려) |
folio_get(folio) | folio | void | refcount 증가 |
folio_put(folio) | folio | void | refcount 감소 (0이면 해제) |
folio_try_get(folio) | folio | bool | refcount가 0이 아닌 경우에만 증가 |
조회/변환
| API | 인자 | 반환 | 설명 |
|---|---|---|---|
page_folio(page) | struct page * | struct folio * | page → folio (tail도 가능) |
folio_page(folio, n) | folio, 인덱스 | struct page * | folio의 n번째 page |
folio_order(folio) | folio | unsigned int | compound order |
folio_nr_pages(folio) | folio | long | 페이지 수 (2^order) |
folio_size(folio) | folio | size_t | 바이트 크기 |
folio_test_large(folio) | folio | bool | order > 0 여부 |
folio_mapping(folio) | folio | address_space * | 매핑 정보 |
folio_test_anon(folio) | folio | bool | 익명 folio 여부 |
folio_file_page(folio, index) | folio, pgoff_t | struct page * | 파일 오프셋에 해당하는 page |
folio_contains(folio, index) | folio, pgoff_t | bool | folio가 해당 인덱스를 포함하는지 |
페이지 캐시
| API | 설명 |
|---|---|
filemap_get_folio(mapping, index) | 캐시에서 folio 검색 (refcount 증가) |
filemap_lock_folio(mapping, index) | 캐시에서 검색 + PG_locked 획득 |
filemap_add_folio(mapping, folio, index, gfp) | 캐시에 folio 추가 |
filemap_remove_folio(folio) | 캐시에서 folio 제거 |
read_cache_folio(mapping, index, filler, data) | 캐시 읽기 (미스 시 I/O) |
mapping_get_entry(mapping, index) | XArray 엔트리 직접 조회 |
매핑/kmap
| API | 설명 |
|---|---|
kmap_local_folio(folio, offset) | folio 내 offset 위치를 커널 가상 주소에 매핑 |
folio_address(folio) | folio의 커널 가상 주소 (lowmem만) |
folio_zero_user(folio, addr) | folio 전체를 0으로 채움 |
folio_zero_range(folio, offset, len) | folio 일부를 0으로 채움 |
folio_copy(dst, src) | folio 전체 내용 복사 |
offset_in_folio(folio, pos) | 파일 위치를 folio 내 오프셋으로 변환 |
내부 단편화와 대응
large folio의 가장 큰 우려는 내부 단편화(internal fragmentation)입니다. 파일 끝(EOF)이나 sparse 접근 패턴에서 folio의 일부만 사용될 수 있습니다.
실전 활용: folio 상태 확인
운영 환경에서 folio 동작을 확인하고 최적화하는 실전 가이드입니다.
# 1. 현재 시스템의 large folio 사용 현황
echo "=== File Huge Pages ==="
grep FileHugePages /proc/meminfo
echo "=== Anonymous Huge Pages ==="
grep AnonHugePages /proc/meminfo
echo "=== mTHP 크기별 통계 ==="
for d in /sys/kernel/mm/transparent_hugepage/hugepages-*; do
size=$(basename $d)
alloc=$(cat $d/stats/anon_alloc 2>/dev/null || echo 0)
fallback=$(cat $d/stats/anon_alloc_fallback 2>/dev/null || echo 0)
echo "$size: alloc=$alloc fallback=$fallback"
done
# 2. 특정 프로세스의 large folio 매핑 확인
PID=1234
grep -E 'THPeligible|AnonHugePages|FilePmdMapped' /proc/$PID/smaps_rollup
# 3. readahead large folio 효과 측정
# 테스트 전 캐시 드롭
echo 3 > /proc/sys/vm/drop_caches
# 순차 읽기 벤치마크
fio --name=seqread --rw=read --bs=1M --size=4G \
--numjobs=1 --filename=/mnt/test/bigfile \
--direct=0 # direct=0으로 페이지 캐시 사용
# vmstat으로 large folio 할당 확인
grep thp_file /proc/vmstat
# 4. perf로 folio 관련 함수 프로파일링
perf top -g --call-graph dwarf -e cycles -p $PID 2>/dev/null | \
grep -E 'folio_|filemap_'
설명
/proc/meminfo의 FileHugePages는 파일 캐시에서 large folio가 차지하는 메모리를 보여줍니다.
mTHP 통계 디렉토리에서 각 크기별 할당 성공/실패를 모니터링하면 최적 order를 결정할 수 있습니다.
fio --direct=0으로 페이지 캐시를 사용하는 순차 읽기를 벤치마크하면 large folio readahead 효과를 직접 측정할 수 있습니다.
참고자료
커널 문서
- Folio — 커널 공식 Folio 문서입니다
- Folio Queue API — Folio Queue 인터페이스를 설명합니다
LWN 기사
- Memory folios — Matthew Wilcox의 Folio 도입 제안과 설계 배경입니다 (2021)
- Folios for 5.16 — 커널 5.16에 머지된 초기 Folio 패치를 다룹니다 (2021)
- Large folios for anonymous memory — 익명 메모리에 대형 Folio를 적용하는 작업입니다 (2022)
- Folio-ifying the page cache — 페이지 캐시의 Folio 전환 진행 상황입니다 (2023)
- Multi-size THP — 다중 크기 THP와 Folio의 관계를 설명합니다 (2023)
커널 소스 코드
- include/linux/mm_types.h — struct folio 정의를 포함합니다
- mm/filemap.c — 파일 매핑 관련 Folio 코드입니다
- mm/large_folio.c — 대형 Folio 처리 구현입니다