파일의 cache에서 folio를 제거했는데도 그 memory를 누군가 계속 참조할 수 있습니다. 이상한 상태가 아닙니다. XArray에서 찾을 수 있는지, 아직 같은 mapping에 속하는지, 객체를 사용하는 참조가 남았는지와 physical page를 allocator로 돌려줄 수 있는지는 서로 다른 질문입니다.
upstream Linux v6.18의 제거 경로를 따라가면 이 차이가 일곱 함수에 나뉘어 드러납니다. 여기서는 이미 제거해도 되는 folio를 선택한 뒤의 memory 쪽 작업을 설명합니다. 어떤 file byte를 없앨지 결정하고 PTE를 철회하는 상위 작업은 truncation의 범위와 unmap에서 별도로 다룹니다.
locked 입력이 필요한 이유입니다
filemap_remove_folio()는 caller가 folio를 잠그고 실제 cache 소속도 확인한 상태를 요구합니다. 이 함수가 임의의 살아 있는 folio를 받아 원래 mapping을 추측해 주는 것은 아닙니다. folio lock이 제거 중의 소속 변경을 직렬화하는 동안, inode의 i_lock과 XArray의 IRQ lock을 순서대로 얻습니다.
여기서 inode lock은 truncate·write 전체를 직렬화하는 i_rwsem과 같은 lock이 아닙니다. 서로 다른 보호 범위를 inode라는 이름 하나로 합치면 sleep 가능성이나 lock ordering을 잘못 해석하게 됩니다. tree의 수정이 끝나면 XArray lock을 놓고, shrink 가능한 mapping이면 inode LRU 관련 처리를 한 뒤 i_lock을 놓습니다. cache 참조를 내려놓는 단계는 그 다음입니다.
아래는 v6.18 filemap_remove_folio() 전체입니다. 모든 code 파일은 공식 source의 whole definition과 GPL-2.0-only attribution을 보존한 kernel 발췌이며 standalone program은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
void filemap_remove_folio(struct folio *folio)
{
struct address_space *mapping = folio->mapping;
BUG_ON(!folio_test_locked(folio));
spin_lock(&mapping->host->i_lock);
xa_lock_irq(&mapping->i_pages);
__filemap_remove_folio(folio, NULL);
xa_unlock_irq(&mapping->i_pages);
if (mapping_shrinkable(mapping))
inode_add_lru(mapping->host);
spin_unlock(&mapping->host->i_lock);
filemap_free_folio(mapping, folio);
}
이 함수가 cache 참조를 정리해도 caller가 보유한 참조까지 대신 소비하지는 않습니다. caller가 자신의 참조를 유지하고 있다면 folio가 즉시 free될 수 없다는 계약입니다. 반대로 그 참조도 이미 놓았다면 예전 pointer를 계속 사용해도 된다는 뜻이 아닙니다.
accounting을 끝낸 뒤 tree에서 제거합니다
내부 __filemap_remove_folio()는 필요한 lock이 이미 확보된 문맥에서 사용합니다. trace hook 다음에는 memory accounting을 변경하고, 실제 XArray 변경은 page_cache_delete()에 맡깁니다. 이 내부 함수만 단독 호출하면서 외부 wrapper의 lock 획득을 기대하면 안 됩니다.
__filemap_remove_folio_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
void __filemap_remove_folio(struct folio *folio, void *shadow)
{
struct address_space *mapping = folio->mapping;
trace_mm_filemap_delete_from_page_cache(folio);
filemap_unaccount_folio(mapping, folio);
page_cache_delete(mapping, folio, shadow);
}
page_cache_delete()는 folio가 차지하던 multi-index 범위를 order에 맞춰 설정합니다. shadow가 NULL이면 entry를 지우고, eviction 정보를 남기는 다른 caller라면 value를 저장할 수 있습니다. 이어 mark를 초기화하고 folio->mapping을 NULL로 바꿉니다. 뒤늦게 folio lock을 얻은 reader는 이 소속 변화를 보고 원래 file 내용을 계속 사용하지 않아야 합니다.
index는 일부러 남깁니다. truncation의 batch caller가 제거 뒤에도 다음 위치와 경계를 계산해야 하기 때문입니다. mapping이 NULL인데 index가 남아 있다는 사실을 초기화 누락으로 고치면 순회의 계약을 깨뜨릴 수 있습니다. mapping->nrpages 감소량도 객체 하나가 아니라 folio_nr_pages()가 돌려주는 base-page 수입니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
static void page_cache_delete(struct address_space *mapping,
struct folio *folio, void *shadow)
{
XA_STATE(xas, &mapping->i_pages, folio->index);
long nr = 1;
mapping_set_update(&xas, mapping);
xas_set_order(&xas, folio->index, folio_order(folio));
nr = folio_nr_pages(folio);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
xas_store(&xas, shadow);
xas_init_marks(&xas);
folio->mapping = NULL;
/* Leave folio->index set: truncation lookup relies upon it */
mapping->nrpages -= nr;
}
XArray에서 지우는 것은 process PTE를 자동으로 없애는 명령이 아닙니다. 정상적인 제거 문맥은 mapped 상태를 앞에서 처리해야 합니다. tree, PTE와 참조의 정리 순서를 뒤집어서는 안 됩니다.
unaccount는 정상 경로와 손상 진단을 구별합니다
filemap_unaccount_folio()는 folio가 아직 mapped인지 검사합니다. debug build의 assertion과 비-debug build에서 문제가 드러났을 때의 진단·taint 처리를 정상 caller가 기대할 제어 흐름으로 사용하면 안 됩니다. exiting mapping의 제한된 복구 분기도 이미 모든 VMA가 철회되었다는 특수 문맥을 전제로 합니다.
일반 accounting에서는 hugetlb 예외를 먼저 나누고, base-page 수만큼 file-page 통계를 내립니다. swapbacked면 shmem 관련 통계, PMD-mappable folio면 THP 관련 통계도 맞춥니다. kernel-file mapping의 별도 node 통계 역시 같은 객체 수가 아니라 page 수 기준입니다.
ordinary writeback-capable mapping에서 dirty folio를 이 시점에 만나는 것은 경고할 상황입니다. 정상 writeback으로 storage에 반영하거나 truncation이 dirty 책임을 취소했어야 하기 때문입니다. 이 함수의 마지막 accounting 보정은 writeback 성공을 뜻하지 않으며, 모든 경우에 dirty bit를 여기서 직접 내리는 것도 아닙니다. in-memory filesystem이나 teardown 중 pin 사용 같은 예외와 ordinary file의 데이터 손실 위험을 분리해야 합니다.
filemap_unaccount_folio_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
static void filemap_unaccount_folio(struct address_space *mapping,
struct folio *folio)
{
long nr;
VM_BUG_ON_FOLIO(folio_mapped(folio), folio);
if (!IS_ENABLED(CONFIG_DEBUG_VM) && unlikely(folio_mapped(folio))) {
pr_alert("BUG: Bad page cache in process %s pfn:%05lx\n",
current->comm, folio_pfn(folio));
dump_page(&folio->page, "still mapped when deleted");
dump_stack();
add_taint(TAINT_BAD_PAGE, LOCKDEP_NOW_UNRELIABLE);
if (mapping_exiting(mapping) && !folio_test_large(folio)) {
int mapcount = folio_mapcount(folio);
if (folio_ref_count(folio) >= mapcount + 2) {
/*
* All vmas have already been torn down, so it's
* a good bet that actually the page is unmapped
* and we'd rather not leak it: if we're wrong,
* another bad page check should catch it later.
*/
atomic_set(&folio->_mapcount, -1);
folio_ref_sub(folio, mapcount);
}
}
}
/* hugetlb folios do not participate in page cache accounting. */
if (folio_test_hugetlb(folio))
return;
nr = folio_nr_pages(folio);
__lruvec_stat_mod_folio(folio, NR_FILE_PAGES, -nr);
if (folio_test_swapbacked(folio)) {
__lruvec_stat_mod_folio(folio, NR_SHMEM, -nr);
if (folio_test_pmd_mappable(folio))
__lruvec_stat_mod_folio(folio, NR_SHMEM_THPS, -nr);
} else if (folio_test_pmd_mappable(folio)) {
__lruvec_stat_mod_folio(folio, NR_FILE_THPS, -nr);
filemap_nr_thps_dec(mapping);
}
if (test_bit(AS_KERNEL_FILE, &folio->mapping->flags))
mod_node_page_state(folio_pgdat(folio),
NR_KERNEL_FILE_PAGES, -nr);
/*
* At this point folio must be either written or cleaned by
* truncate. Dirty folio here signals a bug and loss of
* unwritten data - on ordinary filesystems.
*
* But it's harmless on in-memory filesystems like tmpfs; and can
* occur when a driver which did get_user_pages() sets page dirty
* before putting it, while the inode is being finally evicted.
*
* Below fixes dirty accounting after removing the folio entirely
* but leaves the dirty flag set: it has no effect for truncated
* folio and anyway will be cleared before returning folio to
* buddy allocator.
*/
if (WARN_ON_ONCE(folio_test_dirty(folio) &&
mapping_can_writeback(mapping)))
folio_account_cleaned(folio, inode_to_wb(mapping->host));
}
이 분기들을 보고 경고를 피하려고 사용자 공간에서 cache 상태를 강제로 바꾸는 실험을 할 필요는 없습니다. 중요한 것은 정상 호출의 precondition과 손상 발견 시의 방어 경로를 구별하는 일입니다.
callback과 cache 참조의 해제 순서입니다
filemap_free_folio()는 filesystem의 optional free_folio callback을 호출한 뒤 cache가 갖고 있던 base-page 수의 참조를 내려놓습니다. callback 이름에 free가 있다고 모든 physical memory 해제가 그 callback 하나에 달려 있는 것은 아닙니다. private filesystem 상태 정리와 folio refcount가 0이 되는 사건은 다릅니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
void filemap_free_folio(struct address_space *mapping, struct folio *folio)
{
void (*free_folio)(struct folio *);
free_folio = mapping->a_ops->free_folio;
if (free_folio)
free_folio(folio);
folio_put_refs(folio, folio_nr_pages(folio));
}
order-2 folio를 예로 들면 cache가 대표하는 범위는 네 base page입니다. caller가 별도 참조를 갖고 있다면 cache 쪽 네 참조를 내린 뒤에도 folio 객체는 남을 수 있습니다. 그렇다고 LRU에 있다는 이유로 무조건 참조 하나를 더 세거나, 참조의 수만으로 PTE 수를 역산해서도 안 됩니다. 각 연결의 소유권 계약을 따로 따라야 합니다.
batch는 선택한 folio만 지웁니다
delete_from_page_cache_batch()는 빈 batch면 곧바로 돌아갑니다. 대상이 있으면 inode·XArray lock 아래에서 각각의 accounting을 바꾼 뒤 tree 변경을 묶어 수행합니다. lock을 모두 놓은 뒤에 각 folio의 callback과 cache 참조 정리를 진행합니다. batch가 가진 lookup 참조까지 여기서 정리하는 것은 아닙니다.
delete_from_page_cache_batch_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
void delete_from_page_cache_batch(struct address_space *mapping,
struct folio_batch *fbatch)
{
int i;
if (!folio_batch_count(fbatch))
return;
spin_lock(&mapping->host->i_lock);
xa_lock_irq(&mapping->i_pages);
for (i = 0; i < folio_batch_count(fbatch); i++) {
struct folio *folio = fbatch->folios[i];
trace_mm_filemap_delete_from_page_cache(folio);
filemap_unaccount_folio(mapping, folio);
}
page_cache_delete_batch(mapping, fbatch);
xa_unlock_irq(&mapping->i_pages);
if (mapping_shrinkable(mapping))
inode_add_lru(mapping->host);
spin_unlock(&mapping->host->i_lock);
for (i = 0; i < folio_batch_count(fbatch); i++)
filemap_free_folio(mapping, fbatch->folios[i]);
}
내부 worker는 batch가 file index 순서로 정렬되어 있다는 계약을 사용하며, 그 사이의 hole을 허용합니다. 검색 중 새 value나 batch에 속하지 않는 folio를 만나도 함께 삭제하지 않습니다. batch의 folio들은 locked이므로 기대한 대상보다 뒤의 folio만 발견되는 상황은 정상적인 경쟁으로 받아들일 수 없어 debug 검사도 있습니다.
각 대상을 지울 때 mapping은 NULL로 끊되 index는 유지하고, 실제 base-page 감소량을 합쳐 마지막에 nrpages에서 뺍니다. 따라서 batch entry 수와 감소한 page 수를 같은 숫자로 계산하면 large folio에서 틀립니다.
page_cache_delete_batch_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition 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 user-space program.
* The book's exact source tag is not asserted.
*/
static void page_cache_delete_batch(struct address_space *mapping,
struct folio_batch *fbatch)
{
XA_STATE(xas, &mapping->i_pages, fbatch->folios[0]->index);
long total_pages = 0;
int i = 0;
struct folio *folio;
mapping_set_update(&xas, mapping);
xas_for_each(&xas, folio, ULONG_MAX) {
if (i >= folio_batch_count(fbatch))
break;
/* A swap/dax/shadow entry got inserted? Skip it. */
if (xa_is_value(folio))
continue;
/*
* A page got inserted in our range? Skip it. We have our
* pages locked so they are protected from being removed.
* If we see a page whose index is higher than ours, it
* means our page has been removed, which shouldn't be
* possible because we're holding the PageLock.
*/
if (folio != fbatch->folios[i]) {
VM_BUG_ON_FOLIO(folio->index >
fbatch->folios[i]->index, folio);
continue;
}
WARN_ON_ONCE(!folio_test_locked(folio));
folio->mapping = NULL;
/* Leave folio->index set: truncation lookup relies on it */
i++;
xas_store(&xas, NULL);
total_pages += folio_nr_pages(folio);
}
mapping->nrpages -= total_pages;
}
이 경로는 void 함수들로 이어지며 저장장치 I/O의 성공 여부를 errno로 반환하는 인터페이스가 아닙니다. 대상 선정, PTE 철회와 dirty 책임의 처리까지 끝난 문맥에서 cache membership과 소유권을 정리합니다. cache에서 사라졌다는 사실만으로 참조를 가진 모든 사용자가 동시에 사라지거나 allocator의 free count가 같은 순간에 늘어나는 것은 아닙니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.9.1.
- Linux v6.18: single/batch 제거, accounting과 참조 정리의 전체 구현.
- Linux v6.18: 상위 truncation과 private-state invalidation.