이 코드는 어떤 문제를 푸나요?
페이지 캐시는 파일의 내용을 메모리에 보관해 다시 읽을 때 저장장치 접근을 줄입니다. 하지만 folio를 할당한 것만으로 어느 파일의 어느 위치인지 정해지지는 않습니다. filemap_add_folio는 메모리 사용량을 계산하고 파일의 인덱스에 folio를 연결하며 회수 관리에 등록합니다. 실제 내용을 읽어 채우는 단계와 캐시에 자리를 잡는 단계는 구분해야 합니다.
읽을 범위: v6.18.37 · mm/filemap.c · filemap_add_folio 962–1002행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
address_space
한 파일의 캐시된 데이터를 인덱스로 관리하는 객체입니다. 이름에 address가 있어도 여기서는 프로세스의 전체 가상 주소 공간을 뜻하지 않습니다.
folio
한 개 이상의 물리 페이지를 함께 관리하는 메모리 단위입니다. 항상 정확히 한 페이지라고 가정하면 큰 folio를 잘못 이해하게 됩니다.
인덱스
파일의 어느 위치에 대응하는지 나타내는 페이지 단위 값입니다. 사용자 가상주소나 디스크의 LBA와 같지 않습니다.
shadow와 workingset
최근 캐시에서 쫓겨난 자리의 흔적을 남겨 다시 들어온 데이터가 자주 쓰이는지 판단합니다. 실제 파일 데이터의 백업 사본은 아닙니다.
처음 읽을 때
예를 들어 기본 페이지가 4KiB인 환경에서 인덱스 2는 파일의 8KiB 위치를 가리킵니다. 이는 설명용 환경이며 페이지 크기와 folio 크기는 구성에 따라 다릅니다. 캐시에 등록한 뒤 데이터 유효성은 별도 상태로 확인합니다.
더 깊이 살펴볼 때
memcg 비용 계산, folio 잠금, 인덱스 삽입, LRU 등록의 순서를 보십시오. 삽입 실패 때 비용과 잠금 상태를 되돌리는 이유와 __GFP_WRITE일 때 refault 처리가 달라지는 이유도 확인하십시오.
그림으로 보는 변화

1. 메모리 비용을 반영합니다
folio를 어떤 메모리 cgroup에 계산할지 정하고 사용량을 청구합니다.
화살표는 데이터 이동이 아니라 메모리 사용량 귀속입니다.
2. 파일 위치에 연결합니다
folio를 잠근 상태로 mapping의 해당 인덱스에 삽입합니다. 실패하면 앞 단계를 정리합니다.
화살표는 파일 인덱스에서 folio로 연결되는 참조입니다.
3. 회수 관리에 등록합니다
최근 재접근 여부를 반영하고 LRU 관리에 넣습니다.
화살표는 캐시 객체가 회수 정책의 관리 대상이 되는 순서입니다.
filemap_add_folio를 한 줄씩 읽기
줄 번호는 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;
}int filemap_add_folio(struct address_space *mapping, struct folio *folio,파일의 mapping과 등록할 folio를 받습니다. 이 함수는 데이터를 읽는 것보다 캐시 등록 절차에 초점을 둡니다.
pgoff_t index, gfp_t gfp)파일 내 페이지 인덱스와 메모리 할당·회수 정책을 나타내는 gfp 플래그를 추가로 받습니다.
void *shadow = NULL;해당 위치의 과거 회수 이력이 없다는 초기값으로 shadow를 준비합니다.
int ret;단계별 실패 코드를 담을 변수입니다.
struct mem_cgroup *tmp;임시로 바꿀 활성 메모리 cgroup을 복구하기 위한 포인터입니다.
bool kernel_file = test_bit(AS_KERNEL_FILE, &mapping->flags);커널 파일로 표시된 mapping인지 비트 플래그를 확인합니다.
if (kernel_file)커널 파일에 해당하면 일반 태스크의 메모리 귀속과 다른 처리를 적용합니다.
tmp = set_active_memcg(root_mem_cgroup);root 메모리 cgroup을 잠시 활성 대상으로 설정하고 이전 값을 저장합니다.
ret = mem_cgroup_charge(folio, NULL, gfp);folio의 메모리 사용량을 해당 cgroup에 청구합니다. 한도를 초과하면 실패할 수 있습니다.
if (kernel_file)앞서 활성 cgroup을 바꾼 경로인지 확인합니다.
set_active_memcg(tmp);임시 설정을 원래대로 돌려 이후의 무관한 할당에 영향을 주지 않습니다.
if (ret)메모리 비용 청구가 실패했는지 검사합니다.
return ret;등록을 시작하지 않고 오류를 반환합니다.
__folio_set_locked(folio);새 folio를 잠긴 상태로 표시하여 등록과 데이터 준비 동안의 접근 규칙을 세웁니다.
ret = __filemap_add_folio(mapping, folio, index, gfp, &shadow);mapping의 index 위치에 실제로 folio를 삽입하고 이전 shadow 정보도 받습니다.
if (unlikely(ret)) {인덱스 충돌이나 할당 문제 등으로 삽입에 실패한 경로입니다.
mem_cgroup_uncharge(folio);등록하지 못한 folio에 대해 앞서 청구한 cgroup 사용량을 취소합니다.
__folio_clear_locked(folio);설정해 둔 folio 잠금 비트도 해제하여 호출자가 실패 상태를 정리할 수 있게 합니다.
} else {캐시 삽입에 성공했을 때의 회수 정책 등록 경로입니다.
WARN_ON_ONCE(folio_test_active(folio));아직 활성 상태가 아니어야 하는 새 캐시 folio에 잘못된 active 표시가 있는지 경고합니다.
if (!(gfp & __GFP_WRITE) && shadow)덮어쓰기를 위한 할당이 아니면서 과거 회수 흔적이 있는 경우인지 검사합니다.
workingset_refault(folio, shadow);최근에 쫓겨났다가 다시 필요해진 데이터를 working set 판단에 반영합니다.
folio_add_lru(folio);folio를 LRU 관리에 추가하여 이후 메모리 회수 정책이 추적할 수 있게 합니다.
if (kernel_file)커널 파일의 캐시였다면 전용 계측도 갱신합니다.
mod_node_page_state(folio_pgdat(folio),folio가 속한 메모리 노드의 페이지 계수를 갱신합니다.
NR_KERNEL_FILE_PAGES,NR_KERNEL_FILE_PAGES라는 커널 파일 페이지 통계를 선택합니다.
folio_nr_pages(folio));folio가 포함하는 실제 기본 페이지 수만큼 더합니다. folio 하나를 무조건 1페이지로 세지 않습니다.
return ret;등록 성공 0 또는 삽입 오류를 반환합니다.
함께 생각해 볼 질문
캐시에 넣었으면 파일 데이터도 읽힌 건가요?
이 함수만으로 내용 읽기 완료를 보장하지 않습니다. 데이터 유효성과 I/O 완료는 후속 경로에서 확인합니다.
folio는 항상 4KiB인가요?
아닙니다. 기본 페이지 크기와 folio 차수에 따라 여러 크기가 가능합니다.
shadow는 파일 내용을 복사해 둔 버퍼인가요?
아닙니다. 과거 회수 이력을 이용해 working set을 판단하기 위한 정보입니다.
출처와 읽은 범위
Linux stable v6.18.37 · mm/filemap.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고
