관련 소스: mm/filemap.c, mm/folio-compat.c, mm/page-writeback.c
관련 문서: 메모리 관리 개요 · Buddy Allocator · 페이지 회수 · VMA / mmap · tmpfs / shmem
Folio는 Linux 5.16에서 도입된 메모리 관리의 핵심 단위로, 기존 struct page의 단점을 보완합니다. 기존 페이지 캐시는 개별 페이지 단위로 관리되어 large folio(THP 등) 지원에 비효율적이었으나, folio는 compound page 전체를 하나의 단위로 관리하여 여러 페이지를 한 번에 처리할 수 있습니다. 페이지 캐시(Page Cache)는 파일 시스템의 블록 데이터를 메모리에 캐싱하는 데 사용되며, address_space 구조체를 통해 관리됩니다.
folio의 핵심 장점은 compound page의 head page만 추적하면 되어 lock 관리, dirty/writeback 추적, reference counting이 단순해진다는 점입니다. 또한 기존 page API와의 호환성을 위해 folio-compat.c에 레거시 래퍼 함수가 제공됩니다. Linux 7.0에서는 _mm_id 기반 rmap 최적화, _deferred_list를 통한 대기 분할(split under pressure), HugeTLB 전용 필드 등이 추가되어 구조적으로 더 정교해졌습니다.
소스 파일 경로:
mm/filemap.c — 페이지 캐시 핵심 연산 (조회, 추가, 읽기, 쓰기, 폴트)
mm/folio-compat.c — 기존 page API → folio 래퍼 (unlock_page, mark_page_accessed 등)
mm/page-writeback.c — 더티 페이지 writeback 및 dirty throttling
include/linux/mm_types.h — struct folio 정의 (Linux 7.0: _mm_id, _deferred_list 포함)
include/linux/fs.h — struct address_space, address_space_operations 정의
include/linux/pagemap.h — folio_lock, folio_trylock 등 잠금 API
# 1. 페이지 캐시에 캐싱된 페이지 수 확인
cat /proc/meminfo | grep -i "Cached"
# 2. 현재 시스템의 Active/Inactive(file) 페이지 수 확인
cat /proc/meminfo | grep -E "Active\(file\)|Inactive\(file\)"
# 3. writeback 중인 페이지 수 확인
cat /proc/meminfo | grep -i "Dirty\|Writeback"
# 4. 페이지 캐시 관련 slab 캐시 확인 (radix_tree_node → xarray)
slabtop -o | grep -i "xarray\|inode_cache\|dentry"
# 5. vm.dirty_ratio / vm.dirty_background_ratio 파라미터 확인
sysctl vm.dirty_ratio vm.dirty_background_ratio vm.dirty_writeback_interval vm.dirty_expire_interval
# 6. 특정 inode의 페이지 캐시 크기 확인 (debugfs)
cat /proc/<pid>/smaps | grep -A 5 "file" | grep -i "size"
# 7. 파일 캐시 히트율 확인 (perf 또는 sar)
sar -B 1 5 # pgscank/s (page reclaim), pgscand/s (direct reclaim) 관찰
# 8. 파일 시스템별 페이지 캐시 통계 확인
cat /proc/meminfo | grep -E "Cached|Buffers|SwapCached"
# 9. folio lock 대기 공정성 확인
sysctl vm.page_lock_unfairness # 기본값: 5
# 10. writeback 스레드 동작 확인
cat /proc/vmstat | grep -E "nr_dirty|nr_writeback|nr_congested"
# 11. mmap된 파일 페이지 폴트 통계
cat /proc/vmstat | grep -E "pgmajfault|pgfault"
# 12. 주요 프로세스의 페이지 캐시 사용량 확인
cat /proc/<pid>/smaps_rollup | grep -E "RSS|PSS"
folio는 하나 이상의 연속된 물리 페이지를 하나의 논리적 단위로 관리합니다. struct page와의 호환성을 위해 union으로 겹쳐져 있으며, Linux 7.0에서는 _mm_id 기반 rmap 최적화, _deferred_list 등 새로운 필드가 추가되었습니다.
// include/linux/mm_types.h:401-506 (Linux 7.0)
struct folio {
union {
struct {
memdesc_flags_t flags; // Atomic 플래그 (locked, uptodate, dirty 등)
union {
struct list_head lru; // LRU 리스트 (active/inactive)
struct { void *__filler; unsigned long mlock_count; }; // Unevictable
struct dev_pagemap *pgmap; // ZONE_DEVICE용
};
struct address_space *mapping; // 소유 address_space 포인터
union {
pgoff_t index; // 파일 내 오프셋 (페이지 인덱스)
unsigned long share; // fsdax용 share count
};
union {
void *private; // 파일 시스템 전용 데이터 (buffer_head 등)
swp_entry_t swap; // swap 캐시에서의 swap entry
};
atomic_t _mapcount; // RMAP에서의 매핑 카운트 (-1 = 매핑 안됨)
atomic_t _refcount; // 참조 카운트
unsigned long memcg_data; // memcgroup 데이터
};
struct page page; // 기존 struct page와 호환성
};
// 2번째 struct page 영역 (large folio용)
union {
struct {
atomic_t _large_mapcount; // large folio 전체 매핑 카운트
atomic_t _nr_pages_mapped; // 실제로 매핑된 페이지 수
atomic_t _entire_mapcount; // 전체 매핑 카운트 (64bit)
atomic_t _pincount; // DMA 핀 카운트
mm_id_mapcount_t _mm_id_mapcount[2]; // rmap용 MM ID별 매핑 카운트
union {
mm_id_t _mm_id[2]; // rmap용 MM ID
unsigned long _mm_ids; // 비트 플래그 (잠금 포함)
};
unsigned int _nr_pages; // folio 내 총 페이지 수
};
struct page __page_1;
};
// 3번째 struct page 영역 (deferred split용)
union {
struct {
struct list_head _deferred_list; // 메모리 압박 시 분할 대기 folio 리스트
};
struct page __page_2;
};
// 4번째 struct page 영역 (HugeTLB 전용)
union {
struct {
void *_hugetlb_subpool; // HugeTLB 서브풀
void *_hugetlb_cgroup; // HugeTLB memcg
void *_hugetlb_cgroup_rsvd; // HugeTLB memcg 예약
void *_hugetlb_hwpoison; // HugeTLB 하드웨어 오류
};
struct page __page_3;
};
};
Linux 7.0 FOLIO_MATCH 정적 검증 (mm_types.h:508-547):
// struct folio와 struct page의 필드 오프셋이 일치하는지 컴파일 타임 검증
FOLIO_MATCH(flags, flags); // folio->flags == page->flags
FOLIO_MATCH(lru, lru); // folio->lru == page->lru (compound_head)
FOLIO_MATCH(mapping, mapping); // folio->mapping == page->mapping
FOLIO_MATCH(__folio_index, index); // folio->index == page->__folio_index
FOLIO_MATCH(_mapcount, _mapcount); // folio->_mapcount == page->_mapcount
FOLIO_MATCH(_refcount, _refcount); // folio->_refcount == page->_refcount
핵심 필드 설명:
| 필드 | 역할 |
|---|---|
| `flags` | 페이지 상태 플래그 (locked, uptodate, dirty, writeback, lru 등). `include/linux/page-flags.h`에서 `PG_locked`, `PG_dirty`, `PG_uptodate` 등 정의 |
| `mapping` | 이 folio가 속한 `address_space` 포인터. `NULL`이면 unlinked. LSB가 1이면 anonymous page (`FOLIO_MAPPING_ANON`) |
| `index` | 파일 내 페이지 인덱스 (page offset). `address_space->i_pages` xarray에서의 키 |
| `private` | 파일 시스템 전용 데이터. buffer_head 포인터, swap entry 등으로 사용 |
| `_mapcount` | 이 페이지가 몇 개의 PTE에 매핑되어 있는지 추적. `-1`이면 매핑 없음 |
| `_refcount` | 이 folio에 대한 참조 카운트. 0이 되면 free 가능 |
| `_large_mapcount` | large folio에서 전체 매핑 카운트 (2번째 page 영역) |
| `_nr_pages_mapped` | large folio에서 실제로 매핑된 페이지 수 |
| `_entire_mapcount` | large folio 전체 매핑 카운트 (64bit 시스템) |
| `_mm_id` | Linux 7.0 rmap 최적화: MM ID 기반 매핑 추적. 기존 anon_vma 체인 순회 대비 O(1) 접근 가능 |
| `_deferred_list` | 메모리 압박 시 large folio를 split하기 위해 대기하는 리스트 |
| `_nr_pages` | folio가 포함하는 물리 페이지 수 (large folio) |
| `_hugetlb_*` | HugeTLB 전용 필드: 서브풀, memcg, 하드웨어 오류 추적 |
파일 캐시의 루트 구조체입니다. 모든 캐싱된 페이지는 이 구조체의 i_pages xarray에 저장됩니다.
// include/linux/fs.h:470-490
struct address_space {
struct inode *host; // 소유 inode (파일 또는 block device)
struct xarray i_pages; // 페이지 캐시 (XArray 기반)
struct rw_semaphore invalidate_lock; // invalidate 시 coherency 보장
gfp_t gfp_mask; // 할당 시 사용할 GFP 플래그
atomic_t i_mmap_writable; // VM_SHARED writable 매핑 수
#ifdef CONFIG_READ_ONLY_THP_FOR_FS
atomic_t nr_thps; // non-shmem THP 수
#endif
struct rb_root_cached i_mmap; // mmap된 VMA 트리 (private/shared)
unsigned long nrpages; // 총 캐싱된 페이지 수
pgoff_t writeback_index; // writeback 시작 오프셋
const struct address_space_operations *a_ops; // 파일 시스템 연산 테이블
unsigned long flags; // AS_EIO, AS_ENOSPC, AS_DIRTY 등
errseq_t wb_err; // 최근 writeback 에러
spinlock_t i_private_lock; // i_private_list 보호
struct list_head i_private_list; // 파일 시스템 전용 리스트
struct rw_semaphore i_mmap_rwsem; // i_mmap 보호
void *i_private_data; // 파일 시스템 전용 데이터
} __attribute__((aligned(sizeof(long))));
파일 시스템이 구현해야 할 페이지 캐시 연산 테이블입니다.
// include/linux/fs.h:403-444
struct address_space_operations {
int (*read_folio)(struct file *, struct folio *); // 단일 folio 읽기
int (*writepages)(struct address_space *, struct writeback_control *); // 더티 페이지 writeback
bool (*dirty_folio)(struct address_space *, struct folio *); // folio를 dirty로 표시
void (*readahead)(struct readahead_control *); // readahead 연산
int (*write_begin)(const struct kiocb *, struct address_space *,
loff_t pos, unsigned len,
struct folio **foliop, void **fsdata); // 쓰기 시작
int (*write_end)(const struct kiocb *, struct address_space *,
loff_t pos, unsigned len, unsigned copied,
struct folio *folio, void *fsdata); // 쓰기 완료
bool (*release_folio)(struct folio *, gfp_t); // folio 해제 검사
void (*free_folio)(struct folio *folio); // folio 완전 해제
// ... swap, direct_IO, migrate 등
};
// mm/filemap.c:1940-2057
struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
pgoff_t index, fgf_t fgp_flags, gfp_t gfp, struct mempolicy *policy)
역할: 지정된 mapping과 index에 해당하는 folio를 페이지 캐시에서 검색합니다. 없으면 FGP_CREAT 플래그에 따라 새 folio를 할당하여 추가합니다.
분기 로직:
1. filemap_get_entry(mapping, index)로 XArray에서 folio/shadow entry 검색
2. 찾은 folio가 있고 FGP_LOCK이면:
- FGP_NOWAIT: folio_trylock() 실패 시 -EAGAIN 반환
- 일반: folio_lock() (블로킹)
- truncate 확인: folio->mapping != mapping이면 unlock 후 재시도
3. FGP_ACCESSED이면 folio_mark_accessed() 호출
4. 찾지 못하고 FGP_CREAT이면:
- order 결정: mapping_min_folio_order(mapping) ~ FGF_GET_ORDER(fgp_flags)
- filemap_alloc_folio()로 할당
- filemap_add_folio()로 XArray에 추가
- 실패 시 order를 줄여서 재시도 (최소 min_order까지)
// mm/filemap.c:848-946
noinline int __filemap_add_folio(struct address_space *mapping,
struct folio *folio, pgoff_t index, gfp_t gfp, void **shadowp)
역할: 할당된 folio를 mapping->i_pages XArray에 삽입합니다. 기존 shadow entry와 충돌 시 분기합니다.
분기 로직:
1. XA_STATE_ORDER로 XArray 상태 초기화
2. xas_lock_irq() 후 충돌 항목 반복 (xas_for_each_conflict)
3. 충돌 항목이 일반 folio(!xa_is_value)이면 -EEXIST 반환 (이미 존재)
4. shadow entry이면 기존 entry의 order 확인
5. 기존 entry가 더 크면(order > forder): xas_try_split()로 large entry를 smaller pieces로 분할
6. xas_store(&xas, folio)로 최종 삽입
7. 통계 업데이트: mapping->nrpages += nr, NR_FILE_PAGES, NR_FILE_THPS
// mm/filemap.c:949-990
int filemap_add_folio(struct address_space *mapping, struct folio *folio,
pgoff_t index, gfp_t gfp)
역할: memcgroup 체크 후 __filemap_add_folio()를 호출합니다. 추가 성공 시 LRU에 등록합니다.
흐름:
1. mem_cgroup_charge(folio, ...) — memcgroup 과금
2. __filemap_set_locked(folio) — folio 잠금
3. __filemap_add_folio() — 실제 XArray 삽입
4. 실패 시 memcgroup uncharge
5. 성공 시 workingset_refault()로 recently-evicted folio 활성화
6. folio_add_lru(folio)로 LRU 리스트에 추가
// mm/filemap.c:2768-2880
ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter, ssize_t already_read)
역할: 페이지 캐시에서 파일 데이터를 읽어 사용자 공간으로 복사합니다.
흐름:
1. 유효성 검사 (위치, 크기)
2. do-while 루프에서 반복:
- filemap_get_pages() — 읽을 folio 배치 가져오기 (readahead 포함)
- folio_mark_accessed() — 접근 표시 (LRU 활성화)
- copy_folio_to_iter() — 사용자 공간으로 데이터 복사
- folio_put() — 참조 해제
3. file_accessed(filp) — atime 업데이트
// mm/filemap.c:3512-3670
vm_fault_t filemap_fault(struct vm_fault *vmf)
역할: mmap된 파일 영역에 대한 페이지 폴트를 처리합니다. 페이지 캐시에서 folio를 찾고, 없으면 I/O로 읽어옵니다.
흐름:
1. 페이지 캐시에 folio 존재 여부 확인 (filemap_get_folio)
2. 존재하면 async readahead 시도 (do_async_mmap_readahead)
3. 존재하지 않으면:
- PGMAJFAULT 카운트 증가 (major fault)
- sync readahead 트리거 (do_sync_mmap_readahead)
- __filemap_get_folio(FGP_CREAT|FGP_FOR_MMAP)로 새 folio 생성
4. lock_folio_maybe_drop_mmap() — folio 잠금
5. folio_test_uptodate() 확인:
- Uptodate이면 vmf->page 설정 후 반환
- 아니면 filemap_read_folio()로 직접 읽기
6. VM_FAULT_MAJOR 또는 VM_FAULT_LOCKED 플래그 반환
// mm/filemap.c:675-698
int filemap_write_and_wait_range(struct address_space *mapping,
loff_t lstart, loff_t lend)
역할: 지정된 범위의 더티 페이지를 디스크에 기록하고 완료를 대기합니다.
흐름:
1. mapping_needs_writeback(mapping) 확인
2. 더티 페이지가 있으면 filemap_fdatawrite_range()로 writeback 시작
3. -EIO가 아니면 __filemap_fdatawait_range()로 완료 대기
4. filemap_check_errors()로 에러 확인
// mm/filemap.c:250-264
void filemap_remove_folio(struct folio *folio)
역할: 잠금이 걸린 folio를 페이지 캐시에서 제거하고 해제합니다.
흐름:
1. BUG_ON(!folio_test_locked(folio)) — 잠금 확인
2. spin_lock(&mapping->host->i_lock) — inode 잠금
3. xa_lock_irq(&mapping->i_pages) — XArray 잠금
4. __filemap_remove_folio(folio, NULL) — XArray에서 제거
5. mapping_shrinkable(mapping) → inode_lru_list_add() — inode LRU 추가
6. filemap_free_folio(mapping, folio) — a_ops->free_folio + folio_put_refs
// mm/filemap.c:81-127 — 커널이 지키는 잠금 순서
// i_mmap_rwsem > page_table_lock > i_pages lock (일반 경로)
// mmap_lock > invalidate_lock > lock_page (폴트 경로)
// i_rwsem > mmap_lock (쓰기 경로)
| 경로 | 잠금 순서 |
|---|---|
| truncate | `i_mmap_rwsem` → `private_lock` → `swap_lock` → `i_pages lock` |
| mmap fault | `mmap_lock` → `invalidate_lock` → `lock_page(folio)` |
| 일반 쓰기 | `i_rwsem` → `mmap_lock` → `page_table_lock` → `i_pages lock` |
| 페이지 폴트 | `mmap_lock` → `page_table_lock` → `i_pages lock` |
// mm/filemap.c:1076 — sysctl_page_lock_unfairness 기본값
static int sysctl_page_lock_unfairness = 5;
folio lock 대기 시 unfairness 카운터가 0 이하가 되면 WQ_FLAG_CUSTOM 플래그가 설정되어 공정한 잠금 양도(fair lock handoff)가 활성화됩니다. 이를 통해 thundering herd 문제를 완화합니다.
// mm/filemap.c:1267-1273
repeat:
wait->flags = 0;
if (behavior == EXCLUSIVE) {
wait->flags = WQ_FLAG_EXCLUSIVE;
if (--unfairness < 0)
wait->flags |= WQ_FLAG_CUSTOM; // 공정한 양도 활성화
}
EXCLUSIVE 대기: 하나의 대기자만 깨움 (기본)WQ_FLAG_CUSTOM: 잠금 소유자가 직접 양도 (공정성 보장)sysctl vm.page_lock_unfairness로 런타임 조정 가능generic_file_read_iter()
└─ filemap_read()
├─ filemap_get_pages()
│ ├─ filemap_get_read_batch() ← XArray에서 folio 배치 조회
│ ├─ page_cache_sync_ra() ← 없으면 sync readahead
│ ├─ filemap_create_folio() ← 그래도 없으면 새 folio 생성
│ └─ filemap_readahead() ← readahead 트리거
├─ folio_mark_accessed() ← LRU 활성화
└─ copy_folio_to_iter() ← 사용자 공간으로 복사
do_read_fault() / do_fault()
└─ filemap_fault()
├─ filemap_get_folio() ← 페이지 캐시 조회
│ ├─ [hit] do_async_mmap_readahead()
│ └─ [miss] do_sync_mmap_readahead()
│ └─ __filemap_get_folio(FGP_CREAT) ← 새 folio 생성
├─ lock_folio_maybe_drop_mmap() ← folio 잠금
├─ folio_test_uptodate() 검사
│ ├─ [uptodate] → vmf->page 설정
│ └─ [not uptodate] filemap_read_folio() ← 직접 I/O
└─ VM_FAULT_MAJOR | VM_FAULT_LOCKED 반환
__filemap_get_folio_mpol()
└─ filemap_add_folio()
├─ mem_cgroup_charge() ← memcgroup 과금
├─ __filemap_set_locked() ← folio 잠금
├─ __filemap_add_folio() ← XArray 삽입
│ ├─ xas_for_each_conflict() ← 충돌 검사
│ │ └─ xas_try_split() ← large entry 분할
│ └─ xas_store(folio) ← 최종 삽입
├─ workingset_refault() ← recently-evicted 활성화
└─ folio_add_lru() ← LRU에 추가
balance_dirty_pages_ratelimited()
└─ balance_dirty_pages()
├─ balance_domain_limits() ← global/memcg 도메인 제한 계산
├─ wb_start_background_writeback() ← bg_thresh 초과 시 writeback 시작
├─ balance_wb_limits() ← pos_ratio 계산
└─ throttle 로직 ← task_ratelimit에 따라 쓰기 throttling
| 항목 | struct page | struct folio |
|---|---|---|
| 크기 | 64 bytes (기본) | 64 bytes × N (compound) |
| 관리 단위 | 개별 페이지 | 하나 이상의 연속 페이지 |
| page cache 키 | page index | folio index |
| lock 범위 | 개별 페이지 | folio 전체 (head page lock) |
| dirty 추적 | 개별 page flag | folio flag (전체 dirty 추적) |
| compatibility | 모든 커널 | 5.16+ (기존 API 래퍼 제공) |
| large folio | 제한적 (compound_page) | 기본 지원 |
| rmap 추적 | anon_vma 체인 순회 | _mm_id 기반 O(1) 매핑 카운트 (Linux 7.0) |
| memcg 과금 | 개별 page | folio 단위 (large folio도 1회 과금) |
FOLIO_MAPPING_ANON 플래그 (Linux 7.0):
// folio->mapping의 LSB가 1이면 anonymous page
// folio->mapping에서 실제 address_space는 ~1로 마스킹하여 얻음
// anon_vma는 address_space 대신 사용
| 시나리오 | 경로 | 특징 |
|---|---|---|
| 일반 read | `filemap_read()` → `filemap_get_pages()` | 페이지 캐시 히트 시 I/O 없음 |
| mmap fault (hit) | `filemap_fault()` → `folio_test_uptodate()` | async readahead 가능 |
| mmap fault (miss) | `filemap_fault()` → `do_sync_mmap_readahead()` | major fault, sync I/O |
| direct I/O | `generic_file_read_iter()` → `mapping->a_ops->direct_IO()` | 페이지 캐시 우회 |
| 조건 | 동작 | 파라미터 |
|---|---|---|
| `nr_dirty > bg_thresh` | 백그라운드 writeback 시작 | `vm.dirty_background_ratio` (기본 10%) |
| `nr_dirty > dirty_thresh` | 쓰기 프로세스 throttling | `vm.dirty_ratio` (기본 20%) |
| `dirty_expire_interval` 초과 | 만료된 더티 페이지 즉시 writeback | `vm.dirty_expire_interval` (기본 30s) |
| `dirty_writeback_interval` | kupdate 스타일 주기적 writeback | `vm.dirty_writeback_interval` (기본 5s) |
| `ratelimit_pages` 초과 | per-CPU throttling 체크 | `vm.dirty_writeback_interval` (기본 32 pages) |
// mm/page-writeback.c:68,75,92,103,110
static long ratelimit_pages = 32; // per-CPU throttling 임계값
static int dirty_background_ratio = 10; // 백그라운드 writeback 시작 비율
static int vm_dirty_ratio = 20; // 쓰기 throttling 시작 비율
unsigned int dirty_writeback_interval = 5 * 100; // centiseconds (5초)
unsigned int dirty_expire_interval = 30 * 100; // centiseconds (30초)
| 플래그 | 의미 | 관련 함수 |
|---|---|---|
| `PG_locked` | folio lock 획득됨 | `folio_lock()`, `folio_trylock()` |
| `PG_uptodate` | 데이터가 디스크와 일치 | `folio_end_read()`, `folio_mark_uptodate()` |
| `PG_dirty` | 수정된 데이터 존재 | `folio_mark_dirty()`, `folio_clear_dirty_for_io()` |
| `PG_writeback` | writeback 진행 중 | `folio_start_writeback()`, `folio_end_writeback()` |
| `PG_lru` | LRU 리스트에 있음 | `folio_add_lru()`, `folio_del_lru()` |
| `PG_active` | 활성 LRU에 있음 | `folio_set_active()`, `folio_clear_active()` |
minzkn.com 메모리 관리 개요 페이지에서 struct page, folio 관련 내용을 확인한 결과:
minzkn 핵심 내용:
struct page의 기본 필드 설명 (flags, lru, mapping, index, private, _refcount, _mapcount)우리 문서에서 보강된 내용:
struct page 설명을 folio 관점에서 확장하여 Linux 7.0의 _mm_id, _deferred_list, _hugetlb_* 필드까지 포함folio_mark_accessed(), folio_clear_dirty_for_io() 같은 folio API와 연결