File System · Linux 6.18.37 LTS · source + diagram note

page cache

최신 LTS 기준: kernel.org longterm 6.18.37 기준으로 파일 위치와 링크를 맞춘 원본 코드 분석 노트입니다. address_space, folio, readahead, writeback.

전체 흐름

VFS/filesystem 코드는 path name을 dentry/inode/file/page cache operation으로 번역하는 계층이다. 이름 해석의 consistency와 데이터 I/O의 consistency를 분리해서 봐야 한다.

핵심 확인 사항

함수의 입력과 처리 결과를 먼저 확인한다. 구조체를 생성하고 연결하는 시점, 다른 코드에서 사용할 수 있게 하는 시점, 더 이상 사용하지 않아 회수하는 시점을 구분한다.

오류 처리 확인

정상 처리와 오류 처리를 함께 확인한다. 오류 복구, 장치의 실행 중 연결·분리(hotplug), 자원 해제 과정에서도 상태가 올바르게 정리되는지 점검한다.

그림 1. page cache 이 kernel 안에서 놓이는 위치
pathname / fduser visible handle
→
dentry / mountname cache and namespace
→
inode / filemetadata and open instance
→
address_space / fspage cache, journal, writeback

왼쪽부터 처리 단계를 나열했다. 각 코드 조각에서 바뀌는 상태를 해당 단계와 대조한다.

소스 코드 위치

먼저 확인할 Linux 6.18.37 LTS 소스 파일

원본 코드:
Linux 6.18.37 LTS: mm/filemap.c
Linux 6.18.37 LTS: mm/readahead.c
Linux 6.18.37 LTS: mm/page-writeback.c
Linux 6.18.37 LTS: include/linux/pagemap.h

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

filemap_read

원본 코드: mm/filemap.c:2717-2829 · v6.18.37 · 로컬 원문
filemap_read 함수 전체 (113줄)
ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
		ssize_t already_read)
{
	struct file *filp = iocb->ki_filp;
	struct file_ra_state *ra = &filp->f_ra;
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	struct folio_batch fbatch;
	int i, error = 0;
	bool writably_mapped;
	loff_t isize, end_offset;
	loff_t last_pos = ra->prev_pos;

	if (unlikely(iocb->ki_pos < 0))
		return -EINVAL;
	if (unlikely(iocb->ki_pos >= inode->i_sb->s_maxbytes))
		return 0;
	if (unlikely(!iov_iter_count(iter)))
		return 0;

	iov_iter_truncate(iter, inode->i_sb->s_maxbytes - iocb->ki_pos);
	folio_batch_init(&fbatch);

	do {
		cond_resched();

		/*
		 * If we've already successfully copied some data, then we
		 * can no longer safely return -EIOCBQUEUED. Hence mark
		 * an async read NOWAIT at that point.
		 */
		if ((iocb->ki_flags & IOCB_WAITQ) && already_read)
			iocb->ki_flags |= IOCB_NOWAIT;

		if (unlikely(iocb->ki_pos >= i_size_read(inode)))
			break;

		error = filemap_get_pages(iocb, iter->count, &fbatch, false);
		if (error < 0)
			break;

		/*
		 * i_size must be checked after we know the pages are Uptodate.
		 *
		 * Checking i_size after the check allows us to calculate
		 * the correct value for "nr", which means the zero-filled
		 * part of the page is not copied back to userspace (unless
		 * another truncate extends the file - this is desired though).
		 */
		isize = i_size_read(inode);
		if (unlikely(iocb->ki_pos >= isize))
			goto put_folios;
		end_offset = min_t(loff_t, isize, iocb->ki_pos + iter->count);

		/*
		 * Once we start copying data, we don't want to be touching any
		 * cachelines that might be contended:
		 */
		writably_mapped = mapping_writably_mapped(mapping);

		/*
		 * When a read accesses the same folio several times, only
		 * mark it as accessed the first time.
		 */
		if (!pos_same_folio(iocb->ki_pos, last_pos - 1,
				    fbatch.folios[0]))
			folio_mark_accessed(fbatch.folios[0]);

		for (i = 0; i < folio_batch_count(&fbatch); i++) {
			struct folio *folio = fbatch.folios[i];
			size_t fsize = folio_size(folio);
			size_t offset = iocb->ki_pos & (fsize - 1);
			size_t bytes = min_t(loff_t, end_offset - iocb->ki_pos,
					     fsize - offset);
			size_t copied;

			if (end_offset < folio_pos(folio))
				break;
			if (i > 0)
				folio_mark_accessed(folio);
			/*
			 * If users can be writing to this folio using arbitrary
			 * virtual addresses, take care of potential aliasing
			 * before reading the folio on the kernel side.
			 */
			if (writably_mapped)
				flush_dcache_folio(folio);

			copied = copy_folio_to_iter(folio, offset, bytes, iter);

			already_read += copied;
			iocb->ki_pos += copied;
			last_pos = iocb->ki_pos;

			if (copied < bytes) {
				error = -EFAULT;
				break;
			}
		}
put_folios:
		for (i = 0; i < folio_batch_count(&fbatch); i++) {
			struct folio *folio = fbatch.folios[i];

			filemap_end_dropbehind_read(folio);
			folio_put(folio);
		}
		folio_batch_init(&fbatch);
	} while (iov_iter_count(iter) && iocb->ki_pos < isize && !error);

	file_accessed(filp);
	ra->prev_pos = last_pos;
	return already_read ? already_read : error;
}

확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.

filemap_add_folio

원본 코드: mm/filemap.c:962-1002 · v6.18.37 · 로컬 원문
int filemap_add_folio(struct address_space *mapping, struct folio *folio,
				pgoff_t index, gfp_t gfp)
{
	void *shadow = NULL;
	int ret;
	struct mem_cgroup *tmp;
	bool kernel_file = test_bit(AS_KERNEL_FILE, &mapping->flags);

	if (kernel_file)
		tmp = set_active_memcg(root_mem_cgroup);
	ret = mem_cgroup_charge(folio, NULL, gfp);
	if (kernel_file)
		set_active_memcg(tmp);
	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);
		if (kernel_file)
			mod_node_page_state(folio_pgdat(folio),
					    NR_KERNEL_FILE_PAGES,
					    folio_nr_pages(folio));
	}
	return ret;
}

메모리 cgroup 과금이 성공한 뒤 folio를 잠금 상태로 만들고 __filemap_add_folio()로 mapping에 연결한다. 연결 실패 시 과금과 잠금 표시를 되돌린다. 성공 경로는 shadow entry를 이용한 refault 처리와 LRU 추가를 수행한다. 삽입 성공이 파일 데이터 읽기 완료를 뜻하지는 않는다.

함수별 분석

파일 읽기는 먼저 XArray에서 folio를 찾고, 없을 때에만 readahead 또는 새 folio 생성 경로를 고려한다. 쓰기와 writeback은 별도의 상태 변화다.

filemap_read()와 filemap_add_folio()는 항상 이어지는 두 함수가 아니다. 삽입 함수는 읽기 miss 후에도 조건부로만 호출된다.

그림 2. 읽기 경로의 함수 경계
filemap_read읽기 범위와 반환값 관리
→
filemap_get_pagesXArray 조회·조건부 readahead
→
folio → iter확인된 데이터 범위를 복사

XArray 조회가 비어 있고 readahead 뒤에도 folio가 없을 때 filemap_create_folio()가 할당 → filemap_add_folio()로 삽입 → filemap_read_folio()로 데이터 읽기를 시도한다. 적중 경로에는 이 생성 단계가 없다. 근거: mm/filemap.c:2549–2599, 2616–2674, 2754–2805.

1. filemap_read

filemap_get_pages()를 통해 batch를 얻고, 파일 크기를 다시 확인한 뒤 읽을 수 있는 folio 범위를 iterator에 복사한다. 반환값은 실제 복사한 바이트 수 또는 오류다. 조회가 적중하면 새 folio를 삽입하지 않는다.

확인할 분기: 빈 batch에서 IOCB_NOIO는 -EAGAIN을 반환하고, I/O가 허용되면 동기 readahead 후 재조회한다.

2. filemap_add_folio

이미 마련된 folio와 mapping·index를 받아 과금, 잠금 표시, XArray 삽입, LRU 등록을 처리한다. 성공 시 해당 index에 folio가 존재하지만, 파일 데이터가 준비되었거나 dirty라는 뜻은 아니다.

확인할 분기: 같은 index의 기존 folio와 충돌하면 -EEXIST가 가능하며, 호출자는 재시도 여부를 결정한다.

함수입력확인 가능한 결과소스에서 볼 분기
filemap_readkiocb, iov_iter준비된 범위의 복사 바이트 수 또는 오류조회·readahead·생성 조건
filemap_add_foliomapping, 이미 할당한 folio, index, gfp성공 시 XArray 연결·LRU 등록; 데이터 읽기 완료는 별개삽입 충돌·과금 되돌림

구조체와 필드

객체 관계와 실행 조건을 나누어 읽는다. address_space의 XArray가 index를 folio에 연결하지만, readahead는 조회 결과에 따라 시작되는 별도 요청이다.

그림 3. 캐시 객체의 연결 관계
address_space파일별 캐시 mapping
→
i_pages (XArray)index → folio
→
folio데이터 범위·상태 flags

readahead_control은 이 객체 사슬의 다음 단계가 아니다. 읽기 batch가 비었을 때의 동기 readahead나 readahead 표식이 있는 folio에서의 비동기 readahead에 쓰이는 요청 문맥이다. 근거: mm/filemap.c:2634–2660.

객체 연결과 조건부 요청 문맥을 구분해 읽는다.

객체·상태읽는 법
address_space파일별 캐시 mapping을 관리한다.
i_pages (XArray)index에서 folio를 찾는다. 삽입만으로 내용 유효성이 보장되지는 않는다.
folio flagsUptodate·Dirty·Writeback은 서로 다른 조건을 나타낸다.
readahead_control조건부 readahead 호출에 전달하는 요청 문맥이다.

읽기와 쓰기의 상태 변화

조회 실패는 읽기 경로의 분기이고, Dirty·Writeback은 파일 데이터 변경과 I/O 진행을 나타내는 별도 상태다. 읽기 miss가 반드시 dirty로 이어지지는 않는다.

그림 4. 두 경로를 한 줄로 연결하지 않는다
읽기XArray 조회 → 유효 범위 확인 → 복사
데이터 변경Dirty 표시 → 별도 writeback 경로

두 상자는 서로 다른 경로다. Writeback은 진행 중인 I/O를 나타내며, 이 표시만으로 디스크 영속화를 단정하지 않는다. 근거: mm/filemap.c:2754–2805, 3897–3917, mm/page-writeback.c:2985–3067.

아래 분기 그림과 상태 표에서 조회, 데이터 유효성, Dirty, Writeback을 각각 확인한다.

읽기 분기와 독립 상태

개념 설명의 과금·삽입·LRU 순서와 달리, 여기서는 읽기 요청이 언제 캐시 적중, IOCB_NOIO 거절, readahead, 새 folio 생성으로 갈라지는지 본다.

보충 그림. 캐시 조회 뒤의 세 분기
filemap_read의 XArray 조회가 적중, IOCB_NOIO의 EAGAIN, I/O 허용 후 readahead와 조건부 folio 생성으로 갈라지는 소스 기반 분기 그림

소스 기준의 설명용 분기이며 실측 trace가 아니다. filemap_add_folio()는 folio 삽입만 맡고 데이터 읽기는 뒤의 filemap_read_folio()가 맡는다. mm/filemap.c:2549–2674.

그림 크게 열기 · 작은 화면에서는 그림을 가로로 스크롤할 수 있다.

같은 folio를 볼 때 서로 따로 확인할 네 가지 상태
판정 축소스에서 바뀌는 지점이 상태만으로 알 수 없는 것
XArray에 연결__filemap_add_folio()가 mapping->i_pages에 저장하고 nrpages를 갱신한다. filemap.c:880–959파일 데이터가 이미 읽혔는지는 알 수 없다.
읽을 범위 유효Uptodate 또는 허용된 부분 유효 범위를 검사하고, 필요하면 folio를 갱신한다. filemap.c:2466–2545읽기 가능성이 Dirty 표시를 뜻하지 않는다.
Dirty예를 들어 mmap 쓰기 fault 경로는 folio_mark_dirty()를 호출한다. filemap.c:3897–3917, page-writeback.c:2795–2830디스크 반영 완료나 읽기 miss 직후 상태를 뜻하지 않는다.
Writeback시작 시 PG_writeback을 설정하고, mapping이 writeback tag를 쓰면 XArray tag도 표시한다. 종료 시 표시와 계수를 정리한다. page-writeback.c:2985–3067표시만으로 I/O 완료나 영속화를 뜻하지 않는다.

표의 축은 단일 정상 진행 순서가 아니다. folio의 연결 여부, 읽을 범위의 유효성, 수정 여부, writeback 진행 여부를 각각 확인한다.

불변 조건과 실패 사례

lifetime

page cache의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.

ordering

다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.

오류 복구

중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.

bring-up symptom

임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.

주의

page cache와 buffer cache를 혼동함

주의

dirty page가 곧 disk 반영이라고 봄

주의

mmap fault와 read path가 같은 mapping을 공유하는 점을 놓침

계측과 검증

로그를 무작정 추가하지 말고 확인할 가설을 먼저 세운다. 상태표에서 처리가 멈춘 단계를 찾은 뒤, 상태를 바꾸는 함수와 그 결과를 사용하는 함수를 함께 살펴본다.

로그가 출력되었다고 처리가 끝난 것으로 판단하지 않는다. 원본 코드에서 로그 앞뒤의 필드 변경을 확인한다.

도구보는 것
filemap:*page cache 실행이 어느 단계에서 멈추는지 확인
writeback:*page cache 실행이 어느 단계에서 멈추는지 확인
vmscan:*page cache 실행이 어느 단계에서 멈추는지 확인
cat /proc/meminfopage cache 실행이 어느 단계에서 멈추는지 확인
기본 추적 경로:
# 예시: tracefs가 켜진 보드에서 토픽별 event를 좁혀 본다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo ':mod:*' > /sys/kernel/tracing/set_ftrace_filter
cat /sys/kernel/tracing/trace_pipe

추가 확인 사항

  • page cache 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
  • page cache 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
  • 실험으로 확인한다면 'page cache와 buffer cache를 혼동함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
  • 실험으로 확인한다면 'dirty page가 곧 disk 반영이라고 봄' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?