Page Flag로 알아보는 Cache 생성

freevmstat으로 메모리 상태를 보면 실제 사용 중인 메모리보다 buff/cache 항목이 훨씬 큰 경우가 많다. 이 대부분이 Page Cache인데, page_type이나 page->flags를 직접 들여다보지 않고서는 특정 페이지가 왜 캐시에 남아 있는지, 언제 회수(reclaim) 대상이 되는지, dirty 상태인지 아닌지를 코드 레벨에서 설명하기 어렵다. 커널은 이런 페이지의 상태를 비트 플래그(page->flags)로 관리하고, 어떤 함수가 어떤 시점에 어떤 플래그를 설정·해제하는지를 알아야 캐시 동작을 제대로 추적할 수 있다. 이 글에서는 Page Cache가 생성되는 주요 경로와 그때마다 설정되는 Page Flag를 정리하고, 커널 소스 코드 레벨에서 실제로 어떻게 동작하는지 살펴본다.

Page Cache는 디스크 블록 장치에 있는 데이터가 메모리 페이지로 로드되는 순간 생성된다. 대표적인 경로는 다음과 같다.

  • 파일 read/write
  • mmap으로 접근
  • ELF 실행 시 로드
  • 파일시스템 메타 데이터를 읽을 때

Page Cache 생성 및 Page Flag 설정

Page Cache가 생성되는 경우와 이에 따라 각 page->flags 값은 페이지의 역할과 상태(state)에 따라 다르다.

아래는 include/linux/page-flags.h 를 기준으로 Page Cache관련 주요 플래그가 어떤 상황에서 설정되는지를 정리한 표이다.

구분상황대표 함수 (커널 내부)설정되는 주요 Page Flag설명
1파일 읽기(read, mmap 시 최초 로드)folio_mark_accessed(), mark_page_accessed()PG_locked, PG_uptodate, PG_referenced– 디스크에서 읽히는 동안 PG_locked – I/O 완료 후 PG_uptodate로 설정 (정상 데이터) – 최근 접근 페이지로 PG_referenced 설정
2파일 쓰기(write, pwrite) 시 Dirty Page 생성pagecache_get_page(), set_page_dirty()PG_dirty, PG_uptodate, PG_referenced– 사용자 데이터가 캐시에 쓰이면 PG_dirty 설정 – 쓰기 완료 후에도 메모리상 최신 데이터 (PG_uptodate 유지)
3mmap 파일 접근 중 page fault 발생filemap_fault(), do_page_cache_ra()PG_locked, PG_uptodate, PG_referenced– fault 시 읽기 중 PG_locked – 로드 완료 후 PG_uptodatePG_referenced
4프로그램 실행 (ELF, shared object load)do_read_cache_page(), add_to_page_cache_lru()PG_uptodate, PG_referenced, PG_file– 실행 파일 코드 페이지도 일반 파일 페이지처럼 관리 – PG_file 설정되어 파일 기반임을 명시
5readahead(선행 읽기)do_page_cache_ra(), read_pages()PG_readahead, PG_locked, PG_uptodate– 선행 로딩된 페이지는 PG_readahead로 표시 – I/O 완료 시 PG_uptodate
6디렉터리, inode 메타데이터 캐싱read_mapping_page() (fs 레벨)PG_uptodate, PG_locked, PG_referenced– 일반 파일과 동일하게 처리 – 메타데이터 블록도 page cache 기반
7writeback(디스크로 플러시 중)write_cache_pages(), pageout()PG_writeback, PG_locked– 디스크로 쓰는 중에는 PG_writeback 활성화 – 완료 후 PG_writeback clear, PG_dirty clear
8tmpfs / shmem 페이지 (비파일형)shmem_alloc_page(), add_to_page_cache_lru()PG_swapbacked, PG_uptodate, PG_locked– swap 지원 shmem은 PG_swapbacked 설정 – 파일기반 아님 (!PG_file)
9page fault 중 신규 생성 (no page 존재 시)__page_cache_alloc(), alloc_page()PG_locked– 새 페이지가 할당되면 기본적으로 lock 상태 – 이후 I/O 후 uptodate로 전환
10swap-in (파일 백드 페이지 복구 시)read_swap_cache_async()PG_locked, PG_uptodate, PG_swapbacked– swap된 페이지를 다시 메모리로 읽어올 때 설정
11NFS/FUSE 등 네트워크 FS 접근 시nfs_readpage(), fuse_readpages()PG_locked, PG_uptodate, PG_writeback– 네트워크 I/O 완료 시 PG_uptodate – 쓰기 작업 시 PG_writeback 추가

Page Flag 요약

주요 Page Flag를 요약하면 다음과 같다.

Flag의미사용 시점
PG_locked페이지가 I/O 중임 (page lock held)read/write/mmap fault 시
PG_uptodate페이지 내용이 최신 (디스크와 일치)read 완료, mmap fault 완료 후
PG_dirty페이지 내용이 디스크와 불일치write() 이후
PG_writeback페이지가 디스크로 쓰이는 중dirty page flush 시
PG_referenced최근 접근된 페이지LRU 활성화 시, mark_page_accessed()
PG_readaheadreadahead로 미리 로드된 페이지순차 접근 예측 시
PG_file파일 기반 페이지 (file-backed)filemap_cache 사용 시
PG_swapbackedswap 가능한 shmem/tmpfs 페이지tmpfs, swap page
PG_mlockedmlock()으로 잠긴 페이지pageout 금지
PG_reserved시스템 예약 페이지buddy allocator 제외

Page Flag 설정 매크로

/*
 * Macros to create function definitions for page flags
 */
#define FOLIO_TEST_FLAG(name, page)                                     \
static __always_inline bool folio_test_##name(const struct folio *folio) \
{ return test_bit(PG_##name, const_folio_flags(folio, page)); }

#define FOLIO_SET_FLAG(name, page)                                      \
static __always_inline void folio_set_##name(struct folio *folio)       \
{ set_bit(PG_##name, folio_flags(folio, page)); }

#define FOLIO_CLEAR_FLAG(name, page)                                    \
static __always_inline void folio_clear_##name(struct folio *folio)     \
{ clear_bit(PG_##name, folio_flags(folio, page)); }

#define __FOLIO_SET_FLAG(name, page)                                    \
static __always_inline void __folio_set_##name(struct folio *folio)     \
{ __set_bit(PG_##name, folio_flags(folio, page)); }

#define __FOLIO_CLEAR_FLAG(name, page)                                  \
static __always_inline void __folio_clear_##name(struct folio *folio)   \
{ __clear_bit(PG_##name, folio_flags(folio, page)); }

#define FOLIO_TEST_SET_FLAG(name, page)                                 \
static __always_inline bool folio_test_set_##name(struct folio *folio)  \
{ return test_and_set_bit(PG_##name, folio_flags(folio, page)); }

#define FOLIO_TEST_CLEAR_FLAG(name, page)                               \
static __always_inline bool folio_test_clear_##name(struct folio *folio) \
{ return test_and_clear_bit(PG_##name, folio_flags(folio, page)); }

#define FOLIO_FLAG(name, page)                                          \
FOLIO_TEST_FLAG(name, page)                                             \
FOLIO_SET_FLAG(name, page)                                              \
FOLIO_CLEAR_FLAG(name, page)

#define TESTPAGEFLAG(uname, lname, policy)                              \
FOLIO_TEST_FLAG(lname, FOLIO_##policy)                                  \
static __always_inline int Page##uname(const struct page *page)         \
{ return test_bit(PG_##lname, &policy(page, 0)->flags); }

#define SETPAGEFLAG(uname, lname, policy)                               \
FOLIO_SET_FLAG(lname, FOLIO_##policy)                                   \
static __always_inline void SetPage##uname(struct page *page)           \
{ set_bit(PG_##lname, &policy(page, 1)->flags); }

#define CLEARPAGEFLAG(uname, lname, policy)                             \
FOLIO_CLEAR_FLAG(lname, FOLIO_##policy)                                 \
static __always_inline void ClearPage##uname(struct page *page)         \
{ clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define __SETPAGEFLAG(uname, lname, policy)                             \
__FOLIO_SET_FLAG(lname, FOLIO_##policy)                                 \
static __always_inline void __SetPage##uname(struct page *page)         \
{ __set_bit(PG_##lname, &policy(page, 1)->flags); }

#define __CLEARPAGEFLAG(uname, lname, policy)                           \
__FOLIO_CLEAR_FLAG(lname, FOLIO_##policy)                               \
static __always_inline void __ClearPage##uname(struct page *page)       \
{ __clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTSETFLAG(uname, lname, policy)                               \
FOLIO_TEST_SET_FLAG(lname, FOLIO_##policy)                              \
static __always_inline int TestSetPage##uname(struct page *page)        \
{ return test_and_set_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTCLEARFLAG(uname, lname, policy)                             \
FOLIO_TEST_CLEAR_FLAG(lname, FOLIO_##policy)                            \
static __always_inline int TestClearPage##uname(struct page *page)      \
{ return test_and_clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define PAGEFLAG(uname, lname, policy)                                  \
        TESTPAGEFLAG(uname, lname, policy)                              \
        SETPAGEFLAG(uname, lname, policy)                               \
        CLEARPAGEFLAG(uname, lname, policy)

#define __PAGEFLAG(uname, lname, policy)				\
	TESTPAGEFLAG(uname, lname, policy)				\
	__SETPAGEFLAG(uname, lname, policy)				\
	__CLEARPAGEFLAG(uname, lname, policy)

#define TESTSCFLAG(uname, lname, policy)				\
	TESTSETFLAG(uname, lname, policy)				\
	TESTCLEARFLAG(uname, lname, policy)
__PAGEFLAG(Locked, locked, PF_NO_TAIL)
FOLIO_FLAG(waiters, FOLIO_HEAD_PAGE)
FOLIO_FLAG(referenced, FOLIO_HEAD_PAGE)
	FOLIO_TEST_CLEAR_FLAG(referenced, FOLIO_HEAD_PAGE)
	__FOLIO_SET_FLAG(referenced, FOLIO_HEAD_PAGE)
PAGEFLAG(Dirty, dirty, PF_HEAD) TESTSCFLAG(Dirty, dirty, PF_HEAD)
	__CLEARPAGEFLAG(Dirty, dirty, PF_HEAD)
PAGEFLAG(LRU, lru, PF_HEAD) __CLEARPAGEFLAG(LRU, lru, PF_HEAD)
	TESTCLEARFLAG(LRU, lru, PF_HEAD)
FOLIO_FLAG(active, FOLIO_HEAD_PAGE)
	__FOLIO_CLEAR_FLAG(active, FOLIO_HEAD_PAGE)
	FOLIO_TEST_CLEAR_FLAG(active, FOLIO_HEAD_PAGE)
PAGEFLAG(Workingset, workingset, PF_HEAD)
	TESTCLEARFLAG(Workingset, workingset, PF_HEAD)
PAGEFLAG(Checked, checked, PF_NO_COMPOUND)	   /* Used by some filesystems */

Page Cache 설정 관련 커널 코드 흐름

파일을 read()하는 흐름을 예로 들면, 시스템 콜은 generic_file_read_iter()를 거쳐 실제 페이지 캐시 조회·읽기를 담당하는 filemap_read()로 들어간다. 이 함수는 요청한 오프셋에 해당하는 folio가 이미 페이지 캐시에 있는지부터 확인하고, 없으면 새로 읽어와 캐시에 채워 넣는다. 이 “캐시에 없어서 새로 채워 넣는” 경로에서 호출되는 함수가 filemap_add_folio()이고, 이미 캐시에 있어서 곧바로 반환하는 경로(cache hit)에서는 대신 folio_mark_accessed()가 호출되어 해당 folio가 최근에 다시 접근됐음을 LRU 상태에 반영한다.

먼저 캐시가 이미 존재해서 재접근하는 경우다. folio_mark_accessed()PG_referenced 플래그와 LRU 상의 active/inactive 상태를 갱신하는 역할을 한다. 참고로 이 함수는 linux/mm/folio-compat.c가 아니라 linux/mm/swap.c에 정의돼 있다(레거시 struct page 기반 API인 mark_page_accessed()가 이 함수를 감싸는 호환 래퍼로 folio-compat.c에 따로 존재하는 것과 헷갈리기 쉬운 부분이다).

void folio_mark_accessed(struct folio *folio)
{
	if (lru_gen_enabled()) {
		folio_inc_refs(folio);
		return;
	}

	if (!folio_test_referenced(folio)) {
		folio_set_referenced(folio);
	} else if (folio_test_unevictable(folio)) {
		/*
		 * Unevictable pages are on the "LRU_UNEVICTABLE" list. But,
		 * this list is never rotated or maintained, so marking an
		 * unevictable page accessed has no effect.
		 */
	} else if (!folio_test_active(folio)) {
		/*
		 * If the folio is on the LRU, queue it for activation via
		 * cpu_fbatches.lru_activate. Otherwise, assume the folio is in a
		 * folio_batch, mark it active and it'll be moved to the active
		 * LRU on the next drain.
		 */
		if (folio_test_lru(folio))
			folio_activate(folio);
		else
			__lru_cache_activate_folio(folio);
		folio_clear_referenced(folio);
		workingset_activation(folio);
	}
	if (folio_test_idle(folio))
		folio_clear_idle(folio);
}
EXPORT_SYMBOL(folio_mark_accessed);

주목할 부분은 lru_gen_enabled() 분기다. MGLRU(Multi-Gen LRU)가 활성화된 커널에서는 folio_inc_refs()로 세대(generation) 카운터만 올리고 곧바로 반환하며, 전통적인 PG_referenced/PG_active 2단계 상태 전이는 MGLRU가 비활성화된 경우에만 수행된다. 그 다음 folio_test_referenced()가 false면 PG_referenced만 세우고 끝나고, 이미 referenced 상태에서 다시 접근되면(즉 두 번째 접근부터) active LRU로 승격시키는 2단계 구조로 돼 있다. 한 번 접근했다고 바로 active로 올리지 않는 이유는, 한 번 스치고 지나가는 페이지(예: 순차적으로 한 번만 읽는 로그 파일)까지 active로 승격시키면 정작 반복 접근되는 페이지가 밀려나기 쉬워지기 때문이다.

이번엔 캐시에 아직 없어서 새로 채워 넣는 경우다. filemap_add_folio()는 새로 읽은 folio를 페이지 캐시(address_space의 xarray)에 등록하는 함수로, mm/filemap.c에 정의돼 있다.

int filemap_add_folio(struct address_space *mapping, struct folio *folio,
				pgoff_t index, gfp_t gfp)
{
	void *shadow = NULL;
	int ret;

	ret = mem_cgroup_charge(folio, NULL, gfp);
	if (ret)
		return ret;

	__folio_set_locked(folio);
	ret = __filemap_add_folio(mapping, folio, index, gfp, &shadow);
	if (unlikely(ret)) {
		mem_cgroup_uncharge(folio);
		__folio_clear_locked(folio);
	} else {
		/*
		 * The folio might have been evicted from cache only
		 * recently, in which case it should be activated like
		 * any other repeatedly accessed folio.
		 * The exception is folios getting rewritten; evicting other
		 * data from the working set, only to cache data that will
		 * get overwritten with something else, is a waste of memory.
		 */
		WARN_ON_ONCE(folio_test_active(folio));
		if (!(gfp & __GFP_WRITE) && shadow)
			workingset_refault(folio, shadow);
		folio_add_lru(folio);
	}
	return ret;
}
EXPORT_SYMBOL_GPL(filemap_add_folio);

순서를 보면 mem_cgroup_charge()로 이 folio를 어느 cgroup의 메모리 사용량으로 계산할지부터 먼저 정산하고, __folio_set_locked()PG_locked를 세운 뒤에야 실제 xarray 삽입(__filemap_add_folio())을 시도한다. 삽입에 실패하면 방금 잡았던 cgroup 과금과 lock을 그대로 되돌리고, 성공하면 folio_add_lru()로 LRU 리스트에 편입시킨다. 이 시점에는 아직 디스크에서 실제 데이터를 읽어오기 전이므로 PG_uptodate는 세워지지 않고, 이후 I/O가 끝난 뒤 별도 경로에서 PG_uptodate가 설정되고 PG_locked가 해제된다. 앞서 표에서 “읽기 중에는 PG_locked, 완료 후 PG_uptodate”라고 정리한 부분이 바로 이 지점에 해당한다.

주의사항

  • PG_locked는 “데이터가 변경됐다”는 뜻이 아니라 “이 페이지에 대한 I/O나 상태 변경이 진행 중이니 다른 경로가 동시에 건드리면 안 된다”는 직렬화(serialization) 락이다. dirty 여부는 별도로 PG_dirty가 담당하므로 두 플래그를 혼동하지 않아야 한다.
  • 커널 버전에 따라 struct page 기반 API(SetPageXXX, mark_page_accessed() 등)와 struct folio 기반 API(folio_set_xxx(), folio_mark_accessed() 등)가 섞여 있다. 최신 커널의 실제 구현은 folio 기반이 우선이고, page 기반 함수들은 대부분 mm/folio-compat.c의 얇은 래퍼로 남아 있는 경우가 많으므로, 소스를 추적할 때는 어느 버전의 어느 함수를 보고 있는지 먼저 확인해야 한다.
  • lru_gen_enabled() 분기에서 볼 수 있듯 MGLRU 활성화 여부에 따라 동일한 folio_mark_accessed() 호출도 내부적으로 완전히 다른 경로를 탄다. 특정 배포판/커널 설정에서 코드를 읽을 때는 MGLRU가 켜져 있는지(CONFIG_LRU_GEN, /sys/kernel/mm/lru_gen/enabled)를 함께 확인하지 않으면 실제 동작과 다른 결론을 내리기 쉽다.
  • 이 글에서 인용한 코드는 커널 v6.12 기준이다. page->flags 자체의 정의와 folio/page API 경계는 버전마다 계속 바뀌고 있으므로, 실제 운영 중인 커널 버전의 소스를 elixir.bootlin.com 등에서 직접 대조하는 습관이 필요하다.

마무리

Page Cache는 단일 이벤트로 한 번에 만들어지는 게 아니라, cache miss로 새 folio를 xarray에 삽입하는 경로(filemap_add_folio())와 cache hit으로 기존 folio의 접근 상태만 갱신하는 경로(folio_mark_accessed())가 서로 다른 Page Flag 조합을 남기며 맞물려 동작하는 결과물이다. PG_locked·PG_uptodate·PG_referenced·PG_dirty 같은 플래그들이 각각 무엇을 직렬화하고 무엇을 기록하는지 구분해두면, free 출력의 buff/cache 수치나 /proc/<pid>/smaps의 캐시 관련 통계를 볼 때도 그 이면에서 어떤 코드 경로가 지나갔는지 추론할 수 있다.

참고

답글 남기기