File System · Linux v6.6 · source + diagram note

page cache

읽기 miss의 folio 준비·삽입·데이터 읽기와 쓰기·writeback 경로를 분리해 본다.

전체 흐름

열린 파일의 f_mapping은 inode의 address_space를 가리킨다. 그 안의 i_pages xarray에서 파일 offset에 대응하는 folio를 찾는다. 경로명 조회와 파일 데이터 읽기·쓰기는 구별해야 한다.

핵심 확인 사항

읽기는 cache hit인지, miss 뒤 readahead가 채웠는지, 직접 folio를 생성했는지 구분한다. 새 folio의 할당·mapping 삽입·데이터 읽기는 서로 다른 단계다.

오류 처리 확인

filemap_create_folio()는 할당 실패, 삽입 충돌, 읽기 실패를 각각 처리한다. 삽입 성공만으로 데이터가 Uptodate가 된 것은 아니다.

그림 1. 열린 파일에서 page cache mapping까지
pathname (open)이름 해석의 한 진입점
→
dentry / mountname cache and namespace
→
inode / filemetadata and open instance
→
address_space.i_pagesoffset(index) → cached folio

경로명 조회는 열린 파일을 얻는 한 경로일 뿐이다. filemap_read()는 이미 열린 file의 f_mapping에서 시작한다 (filemap.c:2617–2624).

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

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

설명: mm/filemap.c에서 cache 조회, miss 처리, mapping 삽입, 일반 buffered write와 writeback 요청을 구분한다. filesystem의 read_folio, write_begin, write_end 구현과 실제 I/O 완료는 해당 filesystem 코드까지 확인해야 한다.

대표 코드

filemap_read

원본 코드: mm/filemap.c:2617-2723 · v6.6 · 로컬 원문
filemap_read 함수 전체 (107줄)
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 >= inode->i_sb->s_maxbytes))
		return 0;
	if (unlikely(!iov_iter_count(iter)))
		return 0;

	iov_iter_truncate(iter, inode->i_sb->s_maxbytes);
	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++)
			folio_put(fbatch.folios[i]);
		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_get_pages()가 돌려준 folio의 데이터를 복사하기 전에 파일 크기를 다시 확인한다. 이미 일부를 복사했다면 최종 반환은 오류보다 복사한 바이트 수를 우선한다. cache miss 처리와 folio의 Uptodate 확인은 이 함수가 호출한 하위 경로까지 따라가야 보인다.

filemap_add_folio

원본 코드: mm/filemap.c:932-957 · v6.6 · 로컬 원문
int filemap_add_folio(struct address_space *mapping, struct folio *folio,
				pgoff_t index, gfp_t gfp)
{
	void *shadow = NULL;
	int ret;

	__folio_set_locked(folio);
	ret = __filemap_add_folio(mapping, folio, index, gfp, &shadow);
	if (unlikely(ret))
		__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;
}

호출자가 이미 준비한 folio를 잠금 상태로 만들고 __filemap_add_folio()로 mapping의 xarray에 삽입한다. 내부 과금·삽입 실패 시 관련 상태를 정리하고, 성공하면 조건부 refault 처리와 LRU 추가를 한다. 이 함수는 folio를 할당하거나 파일 데이터를 채우지 않는다. 삽입 성공도 데이터 읽기 완료를 뜻하지 않는다.

함수별 분석

파일 데이터는 address_space.i_pages의 folio로 cache한다. 아래 읽기 경로는 hit, readahead, 직접 생성의 조건부 분기이고, buffered write와 writeback은 별도 경로다.

filemap_read()는 filemap_get_pages()로 읽을 folio를 얻는다. miss가 계속되면 filemap_create_folio()가 할당 → filemap_add_folio() 삽입 → filemap_read_folio() 데이터 읽기를 맡는다.

그림 2. 읽기 miss의 조건부 처리와 별도 쓰기·writeback 경로 (원본 도식)
읽기에서 xarray hit라도 읽을 범위가 유효한지 확인한 뒤 복사한다. miss면 readahead 후 다시 찾고, 그래도 없을 때 기존에 할당한 folio를 mapping에 삽입한 다음 filesystem read_folio로 데이터를 읽는다. 쓰기는 write_begin·write_end의 별도 경로에서 dirty 상태로 갈 수 있고 writeback은 이후 별도 요청이다.

읽기 miss마다 새 folio를 만드는 것은 아니다. readahead가 채우거나 NOWAIT 조건에서 -EAGAIN으로 끝날 수 있다. 출처: filemap.c:2535–2595, 2478–2521, 3932–4011, 371–423. 그림 크게 열기.

1. filemap_read

filemap_read()는 file->f_mapping에서 filemap_get_pages()를 호출한다. 읽을 범위가 유효한 folio를 얻으면 파일 크기를 다시 확인하고 사용자 버퍼에 복사한다. hit이면 삽입 함수가 필요 없고, miss에서는 readahead 뒤에도 비어 있을 때만 직접 생성 경로로 간다.

filemap_get_pages()는 IOCB_NOIO, IOCB_NOWAIT, IOCB_WAITQ에 따라 I/O 또는 대기를 피할 수 있다. 이미 있는 folio가 Uptodate가 아니어도 filesystem의 is_partially_uptodate가 요청 범위를 유효하다고 판단할 수 있다. 그렇지 않으면 갱신/읽기 경로로 처리한다.

읽기 결과는 cache의 folio 내용 복사다. pathname cache나 buffer cache와 같은 객체로 취급하지 않는다.

2. filemap_add_folio

filemap_add_folio()는 할당된 folio를 mapping->i_pages에 넣는다. filemap_create_folio()에서 할당은 이 호출 전의 filemap_alloc_folio(), 내용 읽기는 삽입 후의 filemap_read_folio()가 담당한다.

삽입 충돌(-EEXIST)은 이 직접 생성 경로에서 재시도로 바뀔 수 있다. 성공한 삽입을 Uptodate, dirty, writeback 또는 디스크 반영으로 해석하면 안 된다.

쓰기의 write_begin/write_end와 dirty 상태, 이후 writeback 요청은 읽기 miss의 필수 후속 단계가 아니다.

함수입력상태 변경검증 질문
filemap_read열린 file, iocb, iterfilemap_get_pages()로 folio 확보 후 복사hit, readahead, 직접 생성 중 어떤 경로인가?
filemap_add_folio이미 존재하는 folio, mapping, indexxarray 삽입, 성공 시 조건부 refault·LRU삽입 직후에도 파일 데이터 읽기가 필요한가?
generic_perform_writefile, 쓰기 위치, 사용자 iterfilesystem의 write_begin/write_end 호출dirty 처리와 디스크 I/O 완료는 어디서 확인하는가?

구조체와 필드

address_space.i_pages는 파일 offset의 page index와 folio를 연결한다. Uptodate는 folio 전체의 유효성, Dirty는 변경 사항, Writeback은 진행 중인 쓰기 I/O를 나타낸다. 일반 읽기는 filesystem이 허용한 부분 유효 범위도 사용할 수 있다. 이 플래그들은 하나의 필수 진행 순서가 아니다.

그림 3. mapping과 folio의 관계
file.f_mapping열린 파일의 mapping
→
address_space.i_pagesindex → folio xarray
→
foliocached file data
→
상태 플래그Uptodate / Dirty / Writeback

화살표는 객체 참조와 속성 관계다. 오른쪽 세 플래그가 순서대로 모두 켜진다는 뜻이 아니다. readahead 상태는 열린 파일의 f_ra와 읽기 흐름에서 별도로 추적한다.

객체 관계와 각 상태가 뜻하는 완료 지점을 나눠 읽는다.

필드읽는 법
file.f_mapping열린 파일에서 address_space로 가는 참조
address_space.i_pagespage index에서 cached folio를 찾는 xarray
folio Uptodatefolio 전체의 데이터 유효성. 일반 읽기는 is_partially_uptodate가 승인한 요청 범위도 허용할 수 있음
folio Dirty / Writeback변경된 데이터 / 진행 중인 쓰기 I/O. 읽기 경로의 필수 후속 상태가 아님
readahead_controlfolio의 필드가 아니라 읽기 요청 때 구성하는 문맥. 예를 들어 DEFINE_READAHEAD가 열린 파일의 f_ra를 참조한다 (filemap.c:2527).

실행 단계와 상태 변화

읽기는 cache hit와 miss로 갈라지고, miss도 readahead 성공·직접 생성·재시도·-EAGAIN으로 나뉜다. 쓰기와 writeback은 읽기 완료 뒤 자동으로 이어지는 단계가 아니다.

그림 4. 읽기와 쓰기의 서로 다른 상태 경로
읽기 · folio가 준비됨

xarray hit 또는 readahead 뒤 재조회에서 folio를 얻는다. 요청 범위가 유효한지 확인하고, 필요하면 대기하거나 데이터를 읽은 뒤 사용자 버퍼로 복사한다.

읽기 · 계속 miss

대기가 허용된 경우 직접 생성한다. filemap_alloc_folio() → filemap_add_folio() → filemap_read_folio() 순서다.

쓰기 · 별도 경로

write_begin/write_end를 거쳐 dirty 상태가 생길 수 있다. writeback은 뒤에 별도 요청되며 완료 확인도 별개다.

첫째와 둘째 상자는 읽기의 대안 분기다. 셋째는 별도 buffered write 경로다. filemap_fdatawrite_wbc()는 mapping이 writeback을 지원하고 dirty tag가 있을 때 do_writepages()를 요청하며, 그 반환만으로 영속 저장 완료를 단정할 수 없다 (filemap.c:380–423).

상태의미진입 조건깨지는 지점
lookup읽기에서 기존 cached folio 조회filemap_get_read_batch()miss를 hit로 오인하거나 요청 범위의 유효성을 확인하지 않음
직접 생성readahead 후에도 비어 있으면 할당·삽입·데이터 읽기filemap_create_folio()의 조건부 호출삽입 성공을 내용 읽기 완료로 오인
Dirty쓰기 경로에서 변경된 folio 상태filesystem write_end 등 구현에 따름읽기 miss의 필수 단계 또는 디스크 완료로 오인
Writebackdirty 데이터의 별도 쓰기 I/O 진행writeback 요청과 mapping 상태에 따름시작·완료·영속성 보장을 혼동
검토 기준: 삽입 ≠ Uptodate, Dirty ≠ Writeback 완료, 읽기 miss ≠ 쓰기 경로

불변 조건과 실패 사례

삽입과 데이터 준비

filemap_add_folio()는 mapping의 xarray에 folio를 넣는다. 이 순간의 folio가 아직 Uptodate가 아닐 수 있으므로 읽기 경로는 별도 확인·대기를 수행한다.

동시성

삽입은 잠긴 folio와 xarray의 동시성을 다룬다. 다른 CPU가 먼저 넣은 folio와 충돌하면 -EEXIST가 가능하고, 직접 생성 경로는 이를 재시도 조건으로 바꾼다.

오류와 재시도

filemap_create_folio()는 할당 실패와 삽입/읽기 실패를 구분한다. 삽입 충돌은 AOP_TRUNCATED_PAGE로 변환해 상위 조회를 다시 시도할 수 있다.

쓰기와 완료

buffered write는 filesystem callback을 거치고, dirty folio는 나중에 writeback 대상이 될 수 있다. writeback 시작과 I/O 완료, 영속성 확인은 서로 다른 관찰 지점이다.

주의

page cache의 folio를 pathname/dentry cache나 filesystem의 buffer 관리 객체와 같은 것으로 취급하지 않는다.

주의

Dirty는 변경 사항의 존재를 뜻한다. writeback 요청이나 디스크 영속 저장 완료의 증거가 아니다.

주의

mmap fault와 filemap_read()는 같은 파일 mapping의 folio를 활용할 수 있지만 호출 경로와 확인할 조건이 다르다.

계측과 검증

삽입, 데이터 준비, dirty, writeback은 서로 다른 사건이다. 계측값 하나로 그 다음 단계까지 완료됐다고 단정하지 않는다.

tracefs 이벤트는 빌드 설정과 filesystem에 따라 다르므로 실제 available_events에서 확인한다.

도구보는 것
filemap:mm_filemap_add_to_page_cachexarray 삽입 사건. 데이터 읽기 완료의 증거는 아님
writeback 이벤트writeback 진행 관찰. dirty 발생·영속성 확인과 분리
/proc/meminfoCached·Dirty·Writeback 등의 총량 추세. 특정 file/folio의 완료 여부는 아님
기본 추적 경로:
# 예시: tracefs가 활성화된 보드에서 제공 이벤트를 먼저 확인한다.
grep -E 'filemap|writeback' /sys/kernel/tracing/available_events
grep -E '^(Cached|Dirty|Writeback):' /proc/meminfo

추가 확인 사항

  • filemap_get_pages()에서 hit, readahead 후 재조회, 직접 생성은 어떤 조건으로 나뉘는가?
  • filemap_create_folio()의 할당·삽입·읽기 중 어느 단계에서 실패했는지 어떻게 구별하는가?
  • filemap_add_folio() 성공 직후 Uptodate와 사용자 버퍼 복사를 보장할 수 있는가?
  • 쓰기의 dirty 처리와 이후 writeback 시작·완료는 각 filesystem과 trace에서 어디서 확인하는가?