Linux 6.x~7.0에서의 folio 관련 주요 변경사항 분석
folio는 Linux 5.16에서 도입된 새로운 메모리 관리 구조체로, 기존의 struct page를 대체하는 역할을 합니다. 기존 struct page는 4KB 페이지 단위로 설계되었으나, modern 시스템에서는 대용량 페이지(THP, HugeTLB) 사용이 증가하면서 단일 페이지 관리 구조로는 비효율이 발생했습니다.
folio는 연속된 여러 페이지(compound page)를 하나의 논리 단위로 관리하여, page cache, LRU, writeback 등에서 일관된 인터페이스를 제공합니다. Linux 7.0에서 folio 관련 다양한 개선이 이루어졌으며, 이 문서에서는 주요 변경사항을 분석합니다.
소스 파일:
mm/filemap.c — 페이지 캐시 관리, folio 잠금 순서 문서화include/linux/mm_types.h — struct folio 정의, FOLIO_MATCH 검증include/linux/folio_queue.h — folio 큐 구조체 (2024 David Howells)mm/folio-compat.c — 기존 page API → folio 호환성 래퍼include/linux/mm_inline.h — folio LRU 인라인 함수# folio 관련 커널 설정 확인
zcat /proc/config.gz | grep -i folio
# folio 관련 커널 모듈/심볼 확인
cat /proc/kallsyms | grep -i folio | head -20
# folio 관련 커널 로그
dmesg | grep -i folio
# page folio 전환 상태 확인
cat /proc/meminfo | grep -i "page tables\|pagecache"
# address_space 구조체 크기 확인 (pahole 사용)
sudo apt install dwarves # pahole 설치
pahole -C address_space vmlinux 2>/dev/null | head -30
# folio_batch/pagevec 사용 현황
cat /proc/buddyinfo
cat /proc/pagetypeinfo
# folio 관련 kmalloc 캐시 확인
cat /proc/slabinfo | grep -i "folio\|page"
# MGLRU folio 추적 현황
cat /sys/kernel/mm/lru_gen/enabled
cat /sys/kernel/mm/lru_gen/min_ttl_ms
# 파일 시스템 folio 사용 현황
cat /proc/fs/ext4/*/options 2>/dev/null | grep -i "folio\|large"
# memcg folio 통계
cat /proc/meminfo | grep -i "active\|inactive\|unevictable"
# folio lock 경합 확인
cat /proc/lock_stat 2>/dev/null | grep -i "i_pages\|mapping" | head -10
folio는 4개의 struct page를 union으로 묶은 구조체입니다. 단일 페이지부터 compound page까지 모두 표현할 수 있습니다.
// include/linux/mm_types.h:401-506
struct folio {
/* 첫 번째 page 유닛 — struct page와 오프셋 일치 검증됨 */
union {
struct {
memdesc_flags_t flags; // 페이지 플래그
union {
struct list_head lru; // LRU 목록 연결
struct {
void *__filler;
unsigned int mlock_count; // mlock 잠금 카운트
};
struct dev_pagemap *pgmap; // 디바이스 메모리 매핑
};
struct address_space *mapping; // 페이지 캐시 매핑
union {
pgoff_t index; // 매핑 내 오프셋
unsigned long share; // fsdax 공유 카운트
};
union {
void *private; // 파일 시스템별 데이터
swp_entry_t swap; // 스왑 엔트리
};
atomic_t _mapcount; // 페이지 테이블 매핑 카운트
atomic_t _refcount; // 참조 카운트
/* CONFIG_MEMCG, WANT_PAGE_VIRTUAL, LAST_CPUPID 등 조건부 필드 */
};
struct page page; // struct page와의 호환성을 위한 union
};
/* 두 번째 page 유닛 — large folio 메타데이터 */
union {
struct {
unsigned long _flags_1; // 두 번째 page flags
unsigned long _head_1; // compound head
atomic_t _large_mapcount; // large folio 매핑 카운트
atomic_t _nr_pages_mapped; // 매핑된 페이지 수
#ifdef CONFIG_64BIT
atomic_t _entire_mapcount; // 전체 매핑 카운트
atomic_t _pincount; // pin 카운트
#endif
mm_id_mapcount_t _mm_id_mapcount[2]; // mm ID 매핑 카운트 (Linux 7.0)
union {
mm_id_t _mm_id[2]; // 메모리 맵 ID (Linux 7.0)
unsigned long _mm_ids;
};
};
struct page __page_1;
};
/* 세 번째 page 유닛 */
union {
struct {
unsigned long _flags_2;
unsigned long _head_2;
struct list_head _deferred_list; // 지연된 folio 목록
#ifndef CONFIG_64BIT
atomic_t _entire_mapcount;
atomic_t _pincount;
#endif
};
struct page __page_2;
};
/* 네 번째 page 유닛 — HugeTLB 전용 */
union {
struct {
unsigned long _flags_3;
unsigned long _head_3;
void *_hugetlb_subpool; // HugeTLB 서브풀
void *_hugetlb_cgroup; // HugeTLB cgroup
void *_hugetlb_cgroup_rsvd; // HugeTLB cgroup 예약
void *_hugetlb_hwpoison; // HugeTLB 하드웨어 포이즈닝
};
struct page __page_3;
};
};
compile 타임에 struct page와 struct folio의 필드 오프셋이 일치하는지 검증합니다.
// include/linux/mm_types.h:508-547
// 첫 번째 page와의 오프셋 일치 검증
#define FOLIO_MATCH(pg, fl) \
static_assert(offsetof(struct page, pg) == offsetof(struct folio, fl))
FOLIO_MATCH(flags, flags); // page.flags == folio.flags
FOLIO_MATCH(lru, lru); // page.lru == folio.lru
FOLIO_MATCH(mapping, mapping); // page.mapping == folio.mapping
FOLIO_MATCH(compound_head, lru); // page.compound_head == folio.lru
FOLIO_MATCH(__folio_index, index); // page.__folio_index == folio.index
FOLIO_MATCH(private, private); // page.private == folio.private
FOLIO_MATCH(_mapcount, _mapcount); // page._mapcount == folio._mapcount
FOLIO_MATCH(_refcount, _refcount); // page._refcount == folio._refcount
#ifdef CONFIG_MEMCG
FOLIO_MATCH(memcg_data, memcg_data);
#endif
#if defined(WANT_PAGE_VIRTUAL)
FOLIO_MATCH(virtual, virtual);
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
FOLIO_MATCH(_last_cpupid, _last_cpupid);
#endif
#undef FOLIO_MATCH
// 두 번째 page와의 오프셋 일치 검증 (첫 번째 page 크기만큼 오프셋)
#define FOLIO_MATCH(pg, fl) \
static_assert(offsetof(struct folio, fl) == \
offsetof(struct page, pg) + sizeof(struct page))
FOLIO_MATCH(flags, _flags_1);
FOLIO_MATCH(compound_head, _head_1);
FOLIO_MATCH(_mapcount, _mapcount_1);
FOLIO_MATCH(_refcount, _refcount_1);
#undef FOLIO_MATCH
// 세 번째/네 번째 page와의 오프셋 일치 검증
#define FOLIO_MATCH(pg, fl) \
static_assert(offsetof(struct folio, fl) == \
offsetof(struct page, pg) + 2 * sizeof(struct page))
FOLIO_MATCH(flags, _flags_2);
FOLIO_MATCH(compound_head, _head_2);
#undef FOLIO_MATCH
#define FOLIO_MATCH(pg, fl) \
static_assert(offsetof(struct folio, fl) == \
offsetof(struct page, pg) + 3 * sizeof(struct page))
FOLIO_MATCH(flags, _flags_3);
FOLIO_MATCH(compound_head, _head_3);
#undef FOLIO_MATCH
2024년 David Howells가 추가한 folio 큐 구조체입니다. 파일 I/O 시 folio를 세그먼트 단위로 관리합니다.
// include/linux/folio_queue.h:30-42
struct folio_queue {
struct folio_batch vec; // folio 배치 (최대 PAGEVEC_SIZE=31개)
u8 orders[PAGEVEC_SIZE]; // 각 folio의 order (페이지 크기)
struct folio_queue *next; // 다음 세그먼트
struct folio_queue *prev; // 이전 세그먼트
unsigned long marks; // 첫 번째 비트 마스크 (foliotracker용)
unsigned long marks2; // 두 번째 비트 마스크
unsigned int rreq_id; // 요청 ID (추적용)
unsigned int debug_id; // 디버그 ID
};
folio_queue 핵심 API:
// 초기화 (folio_queue.h:53-62)
static inline void folioq_init(struct folio_queue *folioq, unsigned int rreq_id)
{
folio_batch_init(&folioq->vec);
folioq->next = NULL;
folioq->prev = NULL;
folioq->marks = 0;
folioq->marks2 = 0;
folioq->rreq_id = rreq_id;
folioq->debug_id = 0;
}
// folio 추가 (folio_queue.h:192-199)
static inline unsigned int folioq_append(struct folio_queue *folioq, struct folio *folio)
{
unsigned int slot = folioq->vec.nr++;
folioq->vec.folios[slot] = folio;
folioq->orders[slot] = folio_order(folio);
return slot;
}
// folio 접근 (folio_queue.h:234-237)
static inline struct folio *folioq_folio(const struct folio_queue *folioq, unsigned int slot)
{
return folioq->vec.folios[slot];
}
// folio 제거 (folio_queue.h:275-280)
static inline void folioq_clear(struct folio_queue *folioq, unsigned int slot)
{
folioq->vec.folios[slot] = NULL;
folioq_unmark(folioq, slot);
folioq_unmark2(folioq, slot);
}
기존 struct page 기반 API를 folio 기반 API로 변환하는 래퍼 함수들입니다.
// mm/folio-compat.c:14-87
void unlock_page(struct page *page) {
return folio_unlock(page_folio(page));
}
void end_page_writeback(struct page *page) {
return folio_end_writeback(page_folio(page));
}
void mark_page_accessed(struct page *page) {
folio_mark_accessed(page_folio(page));
}
bool set_page_dirty(struct page *page) {
return folio_mark_dirty(page_folio(page));
}
bool clear_page_dirty_for_io(struct page *page) {
return folio_clear_dirty_for_io(page_folio(page));
}
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
pgoff_t index, gfp_t gfp) {
return filemap_add_folio(mapping, page_folio(page), index, gfp);
}
// include/linux/mm_inline.h:28-31
// folio가 파일 LRU에 있어야 하는지 판단
static inline int folio_is_file_lru(const struct folio *folio)
{
return !folio_test_swapbacked(folio);
}
// include/linux/mm_inline.h:87-101
// folio가 어떤 LRU 목록에 있어야 하는지 결정
static __always_inline enum lru_list folio_lru_list(const struct folio *folio)
{
enum lru_list lru;
VM_BUG_ON_FOLIO(folio_test_active(folio) && folio_test_unevictable(folio), folio);
if (folio_test_unevictable(folio))
return LRU_UNEVICTABLE;
lru = folio_is_file_lru(folio) ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;
if (folio_test_active(folio))
lru += LRU_ACTIVE;
return lru;
}
struct folio를 중심으로 page cache, LRU, 호환성 래퍼, folio 큐가 어떻게 연결되는지 한눈에 볼 수 있습니다.
Linux 7.0에서 filemap.c에 잠금 순서(lock ordering) 문서화가 추가되었습니다.
// mm/filemap.c:80-127
/*
* 잠금 순서:
*
* ->i_mmap_rwsem (truncate_pagecache)
* ->private_lock (__free_pte->block_dirty_folio)
* ->swap_lock (exclusive_swap_page, 기타)
* ->i_pages lock
*
* ->i_rwsem
* ->invalidate_lock (fs가 truncate 경로에서 획득)
* ->i_mmap_rwsem (truncate->unmap_mapping_range)
*
* ->mmap_lock
* ->i_mmap_rwsem
* ->page_table_lock or pte_lock (여러 경로, 주로 memory.c)
* ->i_pages lock (아키텍처 의존 flush_dcache_mmap_lock)
*
* ->mmap_lock
* ->invalidate_lock (filemap_fault)
* ->lock_page (filemap_fault, access_process_vm)
*
* ->i_rwsem (generic_perform_write)
* ->mmap_lock (fault_in_readable->do_page_fault)
*/
// mm/filemap.c:129-148
static void page_cache_delete(struct address_space *mapping,
struct folio *folio, void *shadow)
{
XA_STATE(xas, &mapping->i_pages, folio->index);
long nr = 1;
mapping_set_update(&xas, mapping);
xas_set_order(&xas, folio->index, folio_order(folio));
nr = folio_nr_pages(folio);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
xas_store(&xas, shadow);
xas_init_marks(&xas);
folio->mapping = NULL;
/* folio->index는 유지한다: truncation 조회가 이 값을 사용한다 */
mapping->nrpages -= nr;
}
// mm/filemap.c:222-229
void __filemap_remove_folio(struct folio *folio, void *shadow)
{
struct address_space *mapping = folio->mapping;
trace_mm_filemap_delete_from_page_cache(folio);
filemap_unaccount_folio(mapping, folio);
page_cache_delete(mapping, folio, shadow);
}
// mm/filemap.c:250-264
void filemap_remove_folio(struct folio *folio)
{
struct address_space *mapping = folio->mapping;
BUG_ON(!folio_test_locked(folio));
spin_lock(&mapping->host->i_lock);
xa_lock_irq(&mapping->i_pages);
__filemap_remove_folio(folio, NULL);
xa_unlock_irq(&mapping->i_pages);
if (mapping_shrinkable(mapping))
inode_lru_list_add(mapping->host);
spin_unlock(&mapping->host->i_lock);
filemap_free_folio(mapping, folio);
}
// mm/filemap.c:279-318
static void page_cache_delete_batch(struct address_space *mapping,
struct folio_batch *fbatch)
{
XA_STATE(xas, &mapping->i_pages, fbatch->folios[0]->index);
long total_pages = 0;
int i = 0;
struct folio *folio;
mapping_set_update(&xas, mapping);
xas_for_each(&xas, folio, ULONG_MAX) {
if (i >= folio_batch_count(fbatch))
break;
/* swap/dax/shadow 항목이 들어왔으면 건너뛴다. */
if (xa_is_value(folio))
continue;
/*
* 범위 안에 다른 page가 들어왔으면 건너뛴다.
* 현재 page는 모두 lock되어 있으므로 제거되지 않아야 한다.
* index가 우리 것보다 큰 page를 본다면 우리 page가
* 이미 제거된 셈인데, 그런 일은 일어나면 안 된다.
*/
if (folio != fbatch->folios[i]) {
VM_BUG_ON_FOLIO(folio->index >
fbatch->folios[i]->index, folio);
continue;
}
WARN_ON_ONCE(!folio_test_locked(folio));
folio->mapping = NULL;
/* folio->index는 유지한다: truncation 조회가 이 값을 사용한다 */
i++;
xas_store(&xas, NULL);
total_pages += folio_nr_pages(folio);
}
mapping->nrpages -= total_pages;
}
page_cache_delete_batch()는 정렬된 folio_batch를 따라가며 page cache에서 여러 folio를 한 번에 제거합니다. 중간에 값 엔트리나 예상치 못한 다른 folio가 끼어들면 해당 항목은 건너뜁니다.
기존 API 호출 (unlock_page 등)
↓
folio-compat.c 래퍼 함수
↓
page_folio(page) → struct folio* 획득
↓
folio 기반 함수 호출 (folio_unlock 등)
↓
실제 작업 수행
파일 I/O 시작
↓
folioq_init()으로 세그먼트 초기화
↓
folioq_append()로 folio 추가
↓
marks 비트 설정으로 상태 추적
↓
I/O 완료 후 folioq_clear()로 정리
folio 생성/할당
↓
filemap_add_folio()로 페이지 캐시에 추가
↓
페이지 캐시에서 사용 (filemap_get_folio 등)
↓
더 이상 필요 없음:
├→ __filemap_remove_folio() → page_cache_delete()
└→ filemap_remove_folio() → inode_lru_list_add()
| 항목 | struct page | struct folio |
|---|---|---|
| **크기** | 56-96 bytes (아키텍처별) | 256-384 bytes (4페이지) |
| **관리 단위** | 단일 4KB 페이지 | 복수 페이지 (order 0+) |
| **type 검증** | 런타임 체크 | FOLIO_MATCH 정적 검증 |
| **large folio** | 지원 안 함 | _large_mapcount 등 내장 |
| **HugeTLB** | 별도 처리 | _hugetlb_* 필드 포함 |
| **mm ID 추적** | 제한적 | _mm_id, _mm_id_mapcount |
| 시나리오 | 설명 |
|---|---|
| **파일 읽기** | readahead 시 folio를 세그먼트로 묶어서 처리 |
| **파일 쓰기** | writeback 시 dirty folio를 배치로 관리 |
| **net I/O** | 네트워크 스택에서 패킷 버퍼 관리 |
| ** splice/sendfile** | 데이터 전송 시 zero-copy 최적화 |
| 변경 항목 | Linux 6.x | Linux 7.0 |
|---|---|---|
| **_mm_id** | 없음 | 메모리 맵 ID 추적 |
| **_mm_id_mapcount** | 없음 | mm별 매핑 카운트 |
| **_deferred_list** | 제한적 | 지연된 folio 목록 완전 지원 |
| **_hugetlb_* 필드** | 일부 | 4종 모두 지원 (subpool, cgroup, rsvd, hwpoison) |
| **folio_queue** | 없음 | David Howells 추가 (2024) |
| **FOLIO_MATCH** | 기본 검증 | 4페이지 전체 정적 검증 |
| **lock ordering** | 문서 부족 | filemap.c에 상세 문서화 |
// 기존 코드 — struct page 사용
struct page *page;
page = alloc_pages(gfp, order);
unlock_page(page);
set_page_dirty(page);
// folio 코드 — struct folio 사용
struct folio *folio;
folio = folio_alloc(gfp, order);
folio_unlock(folio);
folio_mark_dirty(folio);
// 기존 코드 유지 가능 (folio-compat.c 래퍼 사용)
struct page *page = ...;
unlock_page(page); // 내부적으로 folio_unlock(page_folio(page)) 호출
mark_page_accessed(page); // 내부적으로 folio_mark_accessed(page_folio(page)) 호출
// 점진적 전환
struct folio *folio = page_folio(page);
folio_unlock(folio); // 직접 folio API 사용
// page → folio 변환
struct folio *folio = page_folio(page);
struct folio *folio = folio_end_page(page); // tail page의 경우
// folio → page 변환
struct page *page = folio_page(folio, 0); // head page
struct page *page = folio_file_page(folio, index); // 파일 캐시
// folio order/size
unsigned int order = folio_order(folio);
unsigned int nr_pages = folio_nr_pages(folio);
size_t size = folio_size(folio);