같은 파일을 두 process가 읽으면 data를 두 번 RAM에 복사해야 할까요? 일반적인 buffered read와 file mmap은 page cache의 같은 data를 재사용할 수 있습니다. 다만 두 process의 virtual address, PTE와 VMA는 각자 존재합니다. '같은 data를 쓴다'는 말과 '같은 주소 공간을 쓴다'는 말은 다릅니다.
이 연결을 읽는 중심 객체가 address_space입니다. 이름이 비슷하지만 process 전체를 설명하는 mm_struct와 다릅니다. upstream v6.18을 기준으로 file, inode, cache folio와 VMA가 어디서 만나고 어디서 분리되는지 살펴보겠습니다.
열린 file과 cache의 정체성
struct file은 열린 file description을 나타냅니다. descriptor 숫자는 task의 descriptor table에서 이 객체로 이어지는 손잡이입니다. 한 process가 같은 파일을 여러 번 열 수 있고 dup()이나 fork로 여러 descriptor가 같은 file 객체를 공유할 수도 있습니다. process마다 파일 하나당 file 객체가 반드시 하나라는 모델은 맞지 않습니다.
struct inode는 filesystem object의 metadata를 나타내며 특정 pathname 그 자체는 아닙니다. file 객체는 path와 inode 관계, 현재 I/O 상태 등을 담고 f_mapping으로 cache의 address_space를 가리킵니다. ordinary file에서는 inode의 embedded i_data가 중심이지만 i_mapping, f_mapping의 관계를 모든 special file에서도 무조건 같은 것으로 가정해서는 안 됩니다.
file-backed VMA는 vm_file reference를 유지하므로 user가 원래 descriptor를 닫아도 mapping이 즉시 무효가 되지 않습니다. descriptor 수명과 mapping이 가진 file reference의 수명이 다르기 때문입니다. VMA split도 file-backed에서 일어날 수 있습니다. 예를 들어 중간 범위만 mprotect()하면 같은 file을 가리키는 여러 VMA가 생길 수 있습니다.
address_space.host는 관련 inode를 가리킵니다. cache folio의 mapping과 index는 그 folio가 어느 cache object의 어느 page 위치인지 나타냅니다. 모든 folio의 mapping field를 raw address_space *로 캐스팅해도 된다는 뜻은 아닙니다. anonymous folio는 tagged reverse-mapping 표현 등 별도 규칙을 가지므로 helper를 사용해 의미를 구별해야 합니다.
다음은 official v6.18 address_space의 전체 정의입니다. kernel 내부 GPL 구조체이며 원래 SPDX·출처와 code/GPL-2.0.txt를 함께 보존했습니다. historical private_*와 current i_private_*의 이름 차이도 보입니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/fs.h
* Complete struct; source lines 506-526.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
struct address_space {
struct inode *host;
struct xarray i_pages;
struct rw_semaphore invalidate_lock;
gfp_t gfp_mask;
atomic_t i_mmap_writable;
#ifdef CONFIG_READ_ONLY_THP_FOR_FS
/* number of thp, only for non-shmem files */
atomic_t nr_thps;
#endif
struct rb_root_cached i_mmap;
unsigned long nrpages;
pgoff_t writeback_index;
const struct address_space_operations *a_ops;
unsigned long flags;
errseq_t wb_err;
spinlock_t i_private_lock;
struct list_head i_private_list;
struct rw_semaphore i_mmap_rwsem;
void * i_private_data;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;
i_pages는 file 길이가 아닙니다
address_space.i_pages는 XArray로 cache 위치와 folio를 연결합니다. file offset은 base-page 단위 index로 들어가고, large folio는 여러 index에 걸친 범위를 다룰 수 있습니다. 배열의 원소 하나를 항상 독립적인 한 base-page allocation으로 생각하면 현재 folio 지원을 놓칩니다. XArray에는 exceptional entry도 있을 수 있어 pointer 종류의 확인도 필요합니다.
nrpages는 cache에 있는 page 수입니다. file의 byte 길이와 다르고 unique process의 RSS와도 다릅니다. 1 GiB file을 열기만 했다면 cache가 1 GiB 준비될 필요가 없습니다. 반대로 read한 data가 cache에 남아 있어도 그 file을 mmap한 process는 하나도 없을 수 있습니다. file 크기를 정하는 inode의 size와 cache 보유량을 나누어 보아야 합니다.
같은 구조체의 nr_thps는 CONFIG_READ_ONLY_THP_FOR_FS 분기에만 있으며 non-shmem file cache의 THP 수를 따로 셉니다. base-page 단위인 nrpages와 같은 단위의 중복 합계가 아닙니다. 큰 folio를 하나의 객체로 다룬다는 사실과 그 folio가 차지하는 base page 수를 나누어 읽어야 하며, 이 조건부 field를 모든 file이나 shmem의 공통 통계로 일반화해서는 안 됩니다.
gfp_mask와 mapping_gfp_mask()는 해당 cache의 allocation 조건에 관계됩니다. cache insertion과 removal은 XArray locking 등으로 조율하고, folio 자체의 lock은 data가 읽혀서 uptodate인지, 다른 I/O와 어떻게 경쟁하는지를 다룹니다. 하나의 lock이 mapping의 모든 상태를 보호하지는 않습니다.
invalidate_lock은 cache를 채우는 쪽과 truncate 등 invalidate 작업 사이의 중요한 경계입니다. file fault가 data를 읽는 동안 backing range가 바뀌면 오래된 내용을 다시 cache에 넣거나 끝을 지난 page를 mapping할 위험이 있습니다. lock과 file size 재검사가 함께 필요한 이유입니다. cache hit라는 사실만으로 그 data가 uptodate이며 현재 file range에 유효하다고 보장되지 않습니다.
i_mmap은 file offset 기준의 역방향 index입니다
i_mmap은 그 cache object를 mapping하는 VMA들을 모은 interval tree입니다. 서로 다른 process가 같은 file offset을 mapping할 수 있으므로 이 tree의 range들은 겹칠 수 있습니다. 한 mm 안의 non-overlapping virtual range를 관리하는 Maple Tree와 다른 종류의 질문에 답합니다.

그림은 ordinary file folio를 공유하는 mapping과 private CoW의 관계를 보여 줍니다. truncate, writeback 및 page fault의 lock 순서는 생략했습니다.
file offset에서 그것을 mapping한 VMA들을 찾으려면 VMA의 vm_pgoff와 range 길이가 필요합니다. 그 후보에서 실제 virtual address를 계산하고 PTE를 조사해야 합니다. interval tree에 후보가 있다는 사실만으로 모든 해당 PTE가 present라고 판단할 수 없습니다. demand paging 때문에 아직 data를 한 번도 mapping하지 않은 부분도 있기 때문입니다.
이 tree를 읽는 쪽은 i_mmap_rwsem의 read 보호를, 연결과 범위를 바꾸는 쪽은 write 보호를 사용합니다. 반면 process의 VMA index는 mmap/VMA lock 계약을 따릅니다. 서로 다른 index를 동시에 바꿀 때는 그 호출 경로의 lock ordering을 지켜야 합니다. 같은 VMA가 두 index에 걸쳐 있다는 사실을 하나의 tree lock으로 단순화해서는 안 됩니다.
shared write 가능성을 세는 이유
i_mmap_writable은 writable shared mapping과 write denial을 조율합니다. private CoW mapping은 user가 값을 써도 원본 file folio를 임의로 변경하는 관계가 아니므로 이 counter와 같은 방식으로 세지 않습니다. 또한 이 값은 현재 모든 PTE의 RW bit를 합산한 통계가 아닙니다. VMA 수준에서 파일을 writable하게 공유할 수 있는 관계를 추적합니다.
mapping_map_writable()은 counter가 negative이면 증가하지 못하고 실패합니다. mapping_deny_writable()은 counter가 positive이면 denial을 얻지 못하고 -EBUSY를 반환합니다. denial을 획득하면 음수가 될 수 있고 mapping_allow_writable()이 그 관계를 되돌립니다. 따라서 이 atomic 값을 일반적인 unsigned mapping 개수로 읽어서는 안 됩니다.
이 배제 관계는 memfd sealing 같은 기능에 중요합니다. 이미 쓰기 가능한 shared mapping이 있는데 단순히 새로운 write를 금지한다고 기존 writer가 사라지는 것은 아니기 때문입니다. admission과 기존 mapping 관계를 원자적으로 조율해야 합니다. MAP_PRIVATE writer가 없다는 뜻과 file의 writable shared mapping이 없다는 뜻도 구분해야 합니다.
다음 네 helper는 v6.18 fs.h의 전체 정의입니다. 0은 양쪽 admission의 경계입니다. writable mapping을 받으면 positive로, denial을 받으면 negative로 진행하므로 동시에 반대 관계를 새로 만들 수 없습니다. mapping_deny_writable()이 실패하는 조건은 positive이며 0 이하일 때는 denial을 추가할 수 있다는 점에 주의해야 합니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/fs.h
* Complete function; source lines 608-612.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline int mapping_map_writable(struct address_space *mapping)
{
return atomic_inc_unless_negative(&mapping->i_mmap_writable) ?
0 : -EPERM;
}
v6_18_mapping_unmap_writable.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/fs.h
* Complete function; source lines 614-617.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mapping_unmap_writable(struct address_space *mapping)
{
atomic_dec(&mapping->i_mmap_writable);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/fs.h
* Complete function; source lines 619-623.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline int mapping_deny_writable(struct address_space *mapping)
{
return atomic_dec_unless_positive(&mapping->i_mmap_writable) ?
0 : -EBUSY;
}
v6_18_mapping_allow_writable.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/fs.h
* Complete function; source lines 625-628.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mapping_allow_writable(struct address_space *mapping)
{
atomic_inc(&mapping->i_mmap_writable);
}
private VMA는 anonymous copy를 품을 수 있습니다
private file mapping을 처음 읽으면 page cache folio를 read-only로 mapping할 수 있습니다. 이후 write는 private anonymous copy를 만들거나 그에 맞는 CoW 처리를 합니다. 그 page의 RSS 분류와 reverse mapping은 이제 ordinary file cache page와 달라집니다.
그렇다고 VMA의 vm_file이 NULL이 되거나 VMA 전체가 file의 i_mmap에서 사라져야 하는 것은 아닙니다. 같은 VMA의 다른 offset에는 여전히 file fault가 필요하고, 범위의 원래 file 관계도 유지됩니다. VMA의 backing 계약과 개별 현재 PTE가 가리키는 folio의 성격을 분리해야 하는 대표적인 경우입니다.
shared mapping에서는 writable fault에 filesystem의 page_mkwrite 등이 관여하여 write 준비와 dirtying을 조율할 수 있습니다. VMA의 fault callback과 filesystem의 address_space_operations도 같은 객체의 callback이 아닙니다. fault는 mapping을 통해 적절한 file cache folio를 구하고, filesystem의 a_ops는 data 읽기와 writeback 등 backing 작업을 제공합니다.
cache는 I/O를 줄이지만 영구 보관하지 않습니다
generic file fault인 filemap_fault()는 index가 file size에 맞는지 확인하고 cache를 조회하며 필요한 readahead와 read를 수행합니다. valid VMA 안의 주소라도 file backing의 끝을 지난 경우에는 VM_FAULT_SIGBUS가 될 수 있습니다. metadata상의 range 존재와 backing의 유효 범위가 다른 사례입니다.
readahead는 요청보다 많은 data를 미리 cache에 준비하여 연속 접근과 storage의 I/O granularity를 활용합니다. 그 data 전부를 곧바로 process의 PTE에 mapping한다는 뜻은 아닙니다. read()와 mmap은 cache를 공유할 수 있지만 page-table mapping을 만드는 동작은 다릅니다. direct I/O와 DAX는 이 ordinary buffered-cache 설명의 예외 경로입니다.
읽기가 끝난 cache는 다음 이용을 위해 남을 수 있습니다. memory pressure에서는 reclaim이 clean folio를 버리거나 dirty data의 writeback과 연계해 공간을 회수합니다. 구체적인 우선순위는 classic LRU 또는 MGLRU 등의 활성 정책에 따라 달라지므로 모든 kernel에서 하나의 단순 LRU 순서만 있다고 가정하지 않습니다.
writeback_index는 writeback 순회의 page 위치와 관계되고, a_ops는 filesystem이 제공하는 operation들을 연결합니다. flags는 mapping 전체 상태를 담으며 wb_err는 단순 errno 하나가 아니라 writeback 오류를 추적하는 errseq_t입니다. file 쪽에도 오류를 관찰한 위치에 관계되는 상태가 있습니다. filesystem 전용 bookkeeping에는 현재 이름인 i_private_lock, i_private_list, i_private_data가 쓰입니다.
address_space가 filesystem의 superblock과 관계된 inode 초기화에서 설정된다고 해서 superblock 하나당 cache 객체가 하나인 것은 아닙니다. ordinary file의 cache 정체성, open file의 I/O 상태, process의 VMA와 folio residency를 나누면 하나의 file을 여러 방식으로 읽고 mapping하는 상황을 일관되게 설명할 수 있습니다.
ordinary inode에서는 address_space가 보통 inode에 embedded된 i_data입니다. v6.18 alloc_inode()와 inode_init_always_gfp()는 filesystem의 super_operations.alloc_inode 또는 generic allocator를 이용하고, 초기화에서 i_mapping을 준비합니다. filesystem이 별도로 cache 객체를 매번 open마다 새로 만든다는 모델이 아닙니다. ext4의 __ext4_iget()는 inode 성질에 맞는 operation을 정하고 ext4_set_aops()로 backing 작업을 연결합니다. inode 생성과 filesystem별 operation 선택을 구분하면 superblock·inode·open file의 역할이 명확해집니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 4의 page cache 소개.
- Linux v6.18 fs.h: address_space, file, inode와 writable mapping helper
- Linux v6.18 filemap.c: cache lookup과 filemap_fault
- Linux v6.18 mm_types.h: folio와 VMA의 연결 field
- Linux v6.18 process_addrs: rmap과 VMA 보호 관계