파일을 MAP_SHARED로 매핑하고 같은 page에 계속 저장하면, 매 store마다 kernel로 들어가지는 않습니다. 처음 writable PTE를 준비할 때 dirty 추적을 설정하고 이후의 store는 hardware가 수행합니다. 그 뒤 writeback을 하려면 kernel은 다시 다음 변경을 알아챌 방법을 마련해야 합니다.
이 순환의 접점이 write-protection fault와 page_mkwrite callback입니다. 여기서는 generic file mapping의 upstream v6.18 경로를 기준으로 설명합니다. filesystem은 block allocation, journaling, extent 처리 때문에 자체 callback을 쓸 수 있습니다.
writable 권한을 주기 전에 filesystem에 묻습니다
VMA가 쓰기를 허용한다는 것과 현재 PTE가 writable이라는 것은 다릅니다. shared file-backed PTE에 대한 write fault에서는 MM이 filesystem의 page_mkwrite 처리를 거쳐 변경을 허용할 준비를 합니다. 이것을 MAP_PRIVATE의 COW와 섞어서는 안 됩니다. private copy에 쓴 내용은 같은 방식으로 원래 파일의 dirty data가 되지 않습니다.
generic_file_mmap()이 설치하는 generic_file_vm_ops에는 filemap_fault, filemap_map_pages, filemap_page_mkwrite가 연결되어 있습니다. 각각 필요한 내용을 확보하는 fault, 이미 cached된 주변 page의 매핑, shared write 준비라는 다른 역할입니다. 하나의 callback이 이 세 가지를 모두 처리하는 것은 아닙니다. v6.18 mm/filemap.c
다음은 CONFIG_MMU 경로의 현재 전체 함수와 callback 객체입니다. read_folio가 없는 mapping은 -ENOEXEC로 거절하고, 성공할 때 access timestamp 처리를 거쳐 VMA에 callback을 연결합니다. filesystem이 자체 mmap·fault callback을 쓰는 경우까지 이 객체 하나로 설명하지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* CONFIG_MMU definition.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int generic_file_mmap(struct file *file, struct vm_area_struct *vma)
{
struct address_space *mapping = file->f_mapping;
if (!mapping->a_ops->read_folio)
return -ENOEXEC;
file_accessed(file);
vma->vm_ops = &generic_file_vm_ops;
return 0;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
const struct vm_operations_struct generic_file_vm_ops = {
.fault = filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = filemap_page_mkwrite,
};
아래는 구현코드가 아닌 호출관계입니다.
shared write-protection fault
-> filesystem page_mkwrite callback
-> filemap_page_mkwrite() [generic implementation]
-> sb_start_pagefault()
-> folio_lock()
-> folio_mark_dirty()
-> folio_wait_stable()
-> sb_end_pagefault()
-> MM revalidation and writable PTE handling
freeze 보호와 folio lock은 역할이 다릅니다
sb_start_pagefault()는 filesystem freeze와 write fault의 진행을 조율합니다. freeze가 안정된 filesystem 상태를 만들려고 할 때 새 write 준비가 무제한으로 끼어들어서는 안 됩니다. 이것은 folio 내용 하나를 보호하는 lock과 범위가 다릅니다.
generic callback은 timestamp 갱신을 시도하고 folio lock을 얻습니다. 이후 folio->mapping이 예상 mapping인지 다시 검사합니다. fault가 시작한 뒤 truncate 등이 먼저 진행될 수 있으므로, 옛 pointer를 가지고 있다는 이유만으로 여전히 같은 파일 내용이라고 믿지 않습니다. mapping이 달라졌으면 unlock한 뒤 VM_FAULT_NOPAGE를 반환합니다.
정상 경로에서는 folio를 dirty로 표시하고 필요하면 stable-write 대기를 수행합니다. 성공 반환은 VM_FAULT_LOCKED이며 folio lock은 상위 fault 처리에 넘깁니다. 반면 superblock의 pagefault 보호는 callback이 끝날 때 해제합니다. 서로 다른 두 보호 수단의 해제 시점이 같지 않습니다.
다음은 v6.18 filemap_page_mkwrite() 전체입니다. timestamp 갱신의 반환값을 여기서 별도로 검사하는 분기는 없습니다. mapping mismatch는 read I/O 오류로 변환하는 것이 아니라 VM_FAULT_NOPAGE로 상위 경로에 돌려줍니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* CONFIG_MMU definition.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
vm_fault_t filemap_page_mkwrite(struct vm_fault *vmf)
{
struct address_space *mapping = vmf->vma->vm_file->f_mapping;
struct folio *folio = page_folio(vmf->page);
vm_fault_t ret = VM_FAULT_LOCKED;
sb_start_pagefault(mapping->host->i_sb);
file_update_time(vmf->vma->vm_file);
folio_lock(folio);
if (folio->mapping != mapping) {
folio_unlock(folio);
ret = VM_FAULT_NOPAGE;
goto out;
}
/*
* We mark the folio dirty already here so that when freeze is in
* progress, we are guaranteed that writeback during freezing will
* see the dirty folio and writeprotect it again.
*/
folio_mark_dirty(folio);
folio_wait_stable(folio);
out:
sb_end_pagefault(mapping->host->i_sb);
return ret;
}
freeze 보호 helper도 실제로는 다음과 같이 SB_FREEZE_PAGEFAULT 수준을 사용합니다. 이미 frozen 상태이면 시작 쪽이 기다릴 수 있고, folio lock과 달리 filesystem 전체의 freeze 진행에 참여합니다. lock ordering에서도 보통 mmap_lock 다음의 보호라는 점을 지켜야 합니다. 다음은 v6.18 fs.h의 두 전체 wrapper입니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete scope from Linux v6.18 include/linux/fs.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/fs.h
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static inline void sb_start_pagefault(struct super_block *sb)
{
__sb_start_write(sb, SB_FREEZE_PAGEFAULT);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete scope from Linux v6.18 include/linux/fs.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/fs.h
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static inline void sb_end_pagefault(struct super_block *sb)
{
__sb_end_write(sb, SB_FREEZE_PAGEFAULT);
}
dirty를 먼저 보이게 해야 freeze가 찾습니다
filemap_page_mkwrite()가 writable PTE 설치 이전에 folio를 dirty로 만드는 데에는 이유가 있습니다. freeze가 진행 중일 때 writeback이 이 folio를 발견하고 다시 write-protect할 수 있어야 합니다. writable 접근이 가능해졌는데 dirty 검색에서는 보이지 않는 구간이 생겨서는 안 됩니다.
folio_mark_dirty()는 mapping의 dirty_folio callback으로 내려갑니다. buffer 기반의 block_dirty_folio()라면 i_private_lock 아래에서 연결된 buffer들을 dirty로 만들고 folio의 최초 dirty 전환을 검사합니다. 새 dirty 전환일 때 XArray mark와 inode dirty 추적이 이어집니다. 이 lock은 buffer 목록이 바뀌는 경로와도 맞물립니다. v6.18 block_dirty_folio
folio_mark_dirty()가 reclaim과 readahead에 겹쳐 쓰이는 flag를 정리하는 부분도 보입니다. 이것은 모든 호출이 새로운 dirty 전환을 만든다는 뜻이 아닙니다. callback의 반환값은 처음 dirty로 바뀌었는지를 구별합니다.
다음 두 전체 함수로 공통 dirty dispatch와 buffer 기반 처리를 연결할 수 있습니다. mapping이 없는 경우 folio_mark_dirty()는 noop_dirty_folio()로 내려가며, mapping이 있다고 모든 filesystem에 block_dirty_folio()를 강제하지는 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
bool folio_mark_dirty(struct folio *folio)
{
struct address_space *mapping = folio_mapping(folio);
if (likely(mapping)) {
/*
* readahead/folio_deactivate could remain
* PG_readahead/PG_reclaim due to race with folio_end_writeback
* About readahead, if the folio is written, the flags would be
* reset. So no problem.
* About folio_deactivate, if the folio is redirtied,
* the flag will be reset. So no problem. but if the
* folio is used by readahead it will confuse readahead
* and make it restart the size rampup process. But it's
* a trivial problem.
*/
if (folio_test_reclaim(folio))
folio_clear_reclaim(folio);
return mapping->a_ops->dirty_folio(mapping, folio);
}
return noop_dirty_folio(mapping, folio);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
bool block_dirty_folio(struct address_space *mapping, struct folio *folio)
{
struct buffer_head *head;
bool newly_dirty;
spin_lock(&mapping->i_private_lock);
head = folio_buffers(folio);
if (head) {
struct buffer_head *bh = head;
do {
set_buffer_dirty(bh);
bh = bh->b_this_page;
} while (bh != head);
}
/*
* Lock out page's memcg migration to keep PageDirty
* synchronized with per-memcg dirty page counters.
*/
newly_dirty = !folio_test_set_dirty(folio);
spin_unlock(&mapping->i_private_lock);
if (newly_dirty)
__folio_mark_dirty(folio, mapping, 1);
if (newly_dirty)
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
return newly_dirty;
}
buffer를 folio보다 먼저 dirty로 만드는 순서도 의미가 있습니다. 반대로 folio를 먼저 dirty로 만들면 writeback이 그 bit를 지우고 아직 clean인 buffer들을 본 뒤 지나갈 수 있습니다. 그 뒤 buffer만 dirty가 되면 상위 dirty 검색과 맞지 않습니다. i_private_lock은 buffer 목록을 순회하는 동안 제거와 새 목록 부착을 직렬화합니다. 이 helper가 sleep할 수 없는 문맥에서도 쓰인다는 조건과 folio lock의 잠들 수 있는 획득을 혼동하지 않습니다.
stable write는 crash transaction이 아닙니다
writeback 중인 메모리 내용을 동시에 고치면 device가 읽는 데이터가 한 시점의 완전한 snapshot이 아닐 수 있습니다. 특정 filesystem이나 device는 checksum, parity 등의 이유로 제출된 내용이 완료까지 변하지 않기를 요구합니다. 이때 mapping의 stable-write 조건에 따라 folio_wait_stable()이 writeback 종료를 기다립니다.
이 조건이 없는 경로에서는 새 변경을 다시 dirty로 추적하여 다음 writeback에서 저장하는 방식이 가능합니다. 그렇다고 application이 동시 store와 crash에 대해 원자적인 record 저장을 보장받는 것은 아닙니다. stable-write 대기는 I/O 중 메모리 안정성의 조건이며 application transaction이나 여러 sector에 걸친 crash atomicity의 보장이 아닙니다.
folio_wait_writeback()에서 깨어났다는 사실도 I/O 성공을 대신하지 않습니다. writeback flag는 완료 시 풀리지만 error는 별도 경로로 전달됩니다. msync()나 fsync()의 성공 조건을 단순한 flag 관찰로 대체할 수 없습니다. v6.18 mm/page-writeback.c
현재 helper는 mapping_stable_writes()를 검사합니다. 예전 SB_I_STABLE_WRITES superblock flag 검사를 현재의 같은 조건식으로 제시하지 않습니다. 아래 두 함수는 v6.18 전체 정의이며 process 문맥, spinlock 미보유, folio 참조 유지가 필요합니다. folio lock 없이 기다렸다면 종료 직후 새 writeback이 시작할 수 있습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
void folio_wait_stable(struct folio *folio)
{
if (mapping_stable_writes(folio_mapping(folio)))
folio_wait_writeback(folio);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
void folio_wait_writeback(struct folio *folio)
{
while (folio_test_writeback(folio)) {
trace_folio_wait_writeback(folio, folio_mapping(folio));
folio_wait_bit(folio, PG_writeback);
}
}
동시에 바뀌는 memory에서 여러 부분을 읽어 I/O를 구성하면 서로 다른 시점의 내용이 섞일 수 있습니다. stable-write 요구는 이런 memory 변경과 진행 중 I/O 사이의 조율입니다. writeback 완료가 별도로 다시 설정된 dirty까지 지워서 영구히 clean으로 만든다는 뜻은 아니며, crash 시 application record 전체의 atomicity도 보장하지 않습니다.
다음 dirty를 잡으려면 PTE를 다시 보호합니다
한 번 writable이 된 PTE에서 발생하는 후속 store는 다시 fault를 내지 않습니다. 따라서 writeback 준비 시에는 reverse mapping을 통해 관련 PTE를 clean하고 write-protect하는 과정이 필요합니다. 이후 새 write가 fault를 일으켜 folio를 다시 dirty로 만들 수 있습니다.
이 과정에서 folio lock은 PTE 변경과 folio dirty state의 연결이 빠지지 않도록 fault 경로와 writeback 경로를 직렬화합니다. PTE의 dirty와 folio의 dirty는 같은 bit가 아니며, 둘 중 하나만 보고 다른 것을 추정하면 변경을 놓칠 수 있습니다.
마지막으로 fault를 통한 dirtying도 rate control에 참여합니다. dirty balancing은 사용자 코드가 byte를 복사하는 write() 경로에만 붙는 기능이 아닙니다. 다만 모든 store마다 비싼 계산을 하는 것은 아니며, dirty 전환과 task·CPU별 누적량을 바탕으로 호출 빈도를 조절합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Page Fault Dirty Tracking.
- Linux v6.18 mm/filemap.c:
generic_file_vm_ops,filemap_page_mkwrite(). - Linux v6.18 mm/memory.c: shared write-protection fault와 상위 처리.
- Linux v6.18 mm/page-writeback.c: dirty callback과 stable-write 대기.