dirty가 아닌 page cache라면 언제든 제거할 수 있을까요? clean은 storage와 다시 맞출 변경 책임이 없다는 뜻이지만, 누군가 그 memory를 사용 중인지와 filesystem private state를 해제할 수 있는지까지 말해 주지는 않습니다. 그래서 cache dropping은 전체 cache를 비우는 강제 명령보다 지금 제거 가능한 대상을 찾는 best effort에 가깝습니다.
이 글에서는 upstream Linux v6.18의 구현으로 어떤 조건이 eviction을 막는지 설명합니다. 실제 시스템의 cache나 sysctl을 바꾸는 실험을 전제로 하지 않습니다.
truncate와 cache dropping은 지키려는 내용이 다릅니다
truncate는 파일에서 더 이상 유효하지 않은 범위를 제거합니다. dirty 내용이라도 그 file 범위의 의미가 사라진 문맥과 맞추어 처리해야 합니다. 반면 cache dropping은 여전히 유효한 내용을 RAM에서 내려놓고 필요하면 backing storage에서 다시 읽으려는 작업입니다.
따라서 dirty 내용이나 진행 중 writeback을 무시하고 버릴 수 없습니다. cache dropping은 sync를 대신하지 않으며, 먼저 dirty data를 안전하게 저장했다는 약속도 제공하지 않습니다. 같은 mapping cleanup helper가 등장해도 어느 작업의 조건인지 확인해야 합니다.
vm.drop_caches는 page cache와 reclaimable slab 처리 등을 선택할 수 있는 관리 interface입니다. bit 0은 page-cache 처리, bit 1은 slab 처리, bit 2는 이후 log 출력을 억제하는 상태에 참여합니다. 이를 지속적으로 사용해야 memory 관리가 정상화되는 것은 아닙니다. reclaim은 memory pressure에서 필요한 cache를 자체적으로 회수합니다.
global list lock 안에서 긴 invalidation을 수행하지 않습니다
drop_caches_sysctl_handler()는 입력을 검사한 뒤 page-cache 경로에서 지연된 LRU 등록을 drain하고 활성 superblock들을 순회합니다. 이 drain은 per-CPU 지연 상태를 반영하려는 작업이지 모든 pin과 I/O 제약을 해제하는 동작은 아닙니다.
superblock 목록은 동시 mount·unmount와 경쟁할 수 있습니다. iterate_supers() 계열은 잠깐의 목록 보호와 객체 참조를 이용하고, 실제 callback을 수행할 때 전역 list spinlock을 계속 보유하지 않도록 합니다. superblock이 준비된 상태인지와 unmount 관련 read-side 보호를 얻었는지도 확인해야 합니다.
아래는 v6.18 sysctl handler의 전체입니다. proc_dointvec_minmax()가 입력을 처리한 후에도 실제 동작은 write일 때만 실행합니다. page-cache bit가 켜져 있으면 lru_add_drain_all() 뒤에 superblock 순회가 이어지며 slab bit는 별도로 drop_slab()을 호출합니다. stfu는 함수 안의 static 상태이므로 bit 2를 한 번 쓴 뒤 값을 되돌렸다고 이후 log 출력도 자동으로 원상복구되는 것은 아닙니다.
drop_caches_sysctl_handler_v6_18.c
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 fs/drop_caches.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/drop_caches.c
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static int drop_caches_sysctl_handler(const struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
int ret;
ret = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (ret)
return ret;
if (write) {
static int stfu;
if (sysctl_drop_caches & 1) {
lru_add_drain_all();
iterate_supers(drop_pagecache_sb, NULL);
count_vm_event(DROP_PAGECACHE);
}
if (sysctl_drop_caches & 2) {
drop_slab();
count_vm_event(DROP_SLAB);
}
if (!stfu) {
pr_info("%s (%d): drop_caches: %d\n",
current->comm, task_pid_nr(current),
sysctl_drop_caches);
}
stfu |= sysctl_drop_caches & 4;
}
return 0;
}
현재 iterate_supers()는 공통 worker에 flags 0을 넘깁니다. worker는 sb_lock 아래에서 목록을 고르고 s_count를 올린 다음, 목록 lock을 놓은 뒤 super_lock()을 얻어 callback을 수행합니다. SB_DYING은 제외하고 SB_BORN 또는 SB_DYING이 보일 때까지 기다리며, s_umount를 얻은 뒤에도 dying 상태를 재검사합니다. 오래된 구현의 hlist_unhashed()와 s_root 조건을 현재 함수의 분기처럼 읽어서는 안 됩니다. 아래는 v6.18 fs/super.c의 세 함수 전체입니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 fs/super.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/super.c
* Copyright (C) 1991, 1992 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void iterate_supers(void (*f)(struct super_block *, void *), void *arg)
{
__iterate_supers(f, arg, 0);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 fs/super.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/super.c
* Copyright (C) 1991, 1992 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static void __iterate_supers(void (*f)(struct super_block *, void *), void *arg,
enum super_iter_flags_t flags)
{
struct super_block *sb, *p = NULL;
bool excl = flags & SUPER_ITER_EXCL;
guard(spinlock)(&sb_lock);
for (sb = first_super(flags);
!list_entry_is_head(sb, &super_blocks, s_list);
sb = next_super(sb, flags)) {
if (super_flags(sb, SB_DYING))
continue;
sb->s_count++;
spin_unlock(&sb_lock);
if (flags & SUPER_ITER_UNLOCKED) {
f(sb, arg);
} else if (super_lock(sb, excl)) {
f(sb, arg);
super_unlock(sb, excl);
}
spin_lock(&sb_lock);
if (p)
__put_super(p);
p = sb;
}
if (p)
__put_super(p);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 fs/super.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/super.c
* Copyright (C) 1991, 1992 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static __must_check bool super_lock(struct super_block *sb, bool excl)
{
lockdep_assert_not_held(&sb->s_umount);
/* wait until the superblock is ready or dying */
wait_var_event(&sb->s_flags, super_flags(sb, SB_BORN | SB_DYING));
/* Don't pointlessly acquire s_umount. */
if (super_flags(sb, SB_DYING))
return false;
__super_lock(sb, excl);
/*
* Has gone through generic_shutdown_super() in the meantime.
* @sb->s_root is NULL and @sb->s_active is 0. No one needs to
* grab a reference to this. Tell them so.
*/
if (sb->s_flags & SB_DYING) {
super_unlock(sb, excl);
return false;
}
WARN_ON_ONCE(!(sb->s_flags & SB_BORN));
return true;
}
여기서 객체를 참조한다는 사실과 mounted 상태를 안정화하는 lock은 다른 역할입니다. 참조가 memory 수명을 유지해도 filesystem의 teardown과 callback이 아무렇게나 겹쳐도 되는 것은 아닙니다. 반대로 잠들 수 있는 전체 callback 동안 전역 spinlock을 붙잡으면 다른 filesystem의 진행까지 막습니다. superblock 순회
inode는 안정된 참조를 얻은 뒤 list lock 밖에서 처리합니다
drop_pagecache_sb()는 s_inodes를 순회하면서 inode의 i_lock으로 상태를 봅니다. I_FREEING, I_WILL_FREE, I_NEW처럼 freeing이나 초기화가 진행 중인 inode는 건너뜁니다. 새 inode가 목록에 보였다고 내용을 안정적으로 사용할 수 있는 것은 아닙니다.
mapping이 비었으면 대체로 할 일이 없지만, reschedule이 필요하다면 잠깐 lock을 놓는 경로를 진행할 이유가 있습니다. 긴 inode 순회가 빈 inode만 본다는 이유로 CPU와 spinlock을 너무 오래 독점해서는 안 되기 때문입니다.
처리할 inode에는 __iget()으로 참조를 얻고 inode lock과 list lock을 놓은 뒤 mapping invalidation을 요청합니다. 이전 inode의 iput()도 적절한 지점으로 미룹니다. 마지막 참조를 놓는 작업이 다른 lock을 얻는 정리 경로로 이어질 수 있어 lock ordering을 지켜야 합니다. 이는 unlock을 단순히 반대 순서로 쓰는 스타일 문제가 아니라 deadlock을 피하는 계약입니다. inode 순회와 invalidation
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 fs/drop_caches.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/drop_caches.c
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static void drop_pagecache_sb(struct super_block *sb, void *unused)
{
struct inode *inode, *toput_inode = NULL;
spin_lock(&sb->s_inode_list_lock);
list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
spin_lock(&inode->i_lock);
/*
* We must skip inodes in unusual state. We may also skip
* inodes without pages but we deliberately won't in case
* we need to reschedule to avoid softlockups.
*/
if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
(mapping_empty(inode->i_mapping) && !need_resched())) {
spin_unlock(&inode->i_lock);
continue;
}
__iget(inode);
spin_unlock(&inode->i_lock);
spin_unlock(&sb->s_inode_list_lock);
invalidate_mapping_pages(inode->i_mapping, 0, -1);
iput(toput_inode);
toput_inode = inode;
cond_resched();
spin_lock(&sb->s_inode_list_lock);
}
spin_unlock(&sb->s_inode_list_lock);
iput(toput_inode);
}
invalidation batch는 기다릴 대상을 건너뜁니다
현재 invalidate_mapping_pages()는 mapping_try_invalidate() 계열을 사용합니다. 이전 이름의 pagevec 중심 설명과 실제 함수 이름이 달라도 핵심 선택은 같습니다. 지금 lock할 수 있는 folio를 batch로 찾고, writeback이나 사용 상태 때문에 지울 수 없는 대상은 남길 수 있습니다.
batch에는 shadow 같은 value entry가 섞일 수 있습니다. 실제 folio의 refcount 연산과 value의 제거를 구별해야 합니다. folio 삭제 후에도 index가 순회에 필요할 수 있으므로, batch의 위치와 lookup index를 그대로 혼동해서도 안 됩니다.
eviction이 실패하면 해당 folio를 deactivate하여 이후 reclaim의 기회를 높일 수 있습니다. 이것이 지금 물리 page를 반환했다는 뜻은 아닙니다. batch release 역시 caller 참조를 놓는 동작이며 마지막 참조가 사라질 때의 실제 free와 구별해야 합니다.
invalidate_mapping_pages()의 반환값은 제거한 page 수입니다. page-cache 범위의 시작과 끝은 page index이며 끝도 포함합니다. current worker에서 value entry를 만나면 먼저 개수를 세고 표시해 두며, batch 범위의 shadow를 정리한 후에는 exceptional entry를 batch에서 빼고 folio 참조를 release합니다. eviction이 실패한 folio는 unlock한 뒤 deactivate하고 nr_failed가 주어졌다면 실패 개수도 올립니다. 다음은 v6.18 mm/truncate.c의 두 함수 전체입니다.
invalidate_mapping_pages_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/truncate.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/truncate.c
* Copyright (C) 2002, Linus Torvalds
* 10Sep2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
unsigned long invalidate_mapping_pages(struct address_space *mapping,
pgoff_t start, pgoff_t end)
{
return mapping_try_invalidate(mapping, start, end, NULL);
}
mapping_try_invalidate_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/truncate.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/truncate.c
* Copyright (C) 2002, Linus Torvalds
* 10Sep2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
unsigned long mapping_try_invalidate(struct address_space *mapping,
pgoff_t start, pgoff_t end, unsigned long *nr_failed)
{
pgoff_t indices[PAGEVEC_SIZE];
struct folio_batch fbatch;
pgoff_t index = start;
unsigned long ret;
unsigned long count = 0;
int i;
folio_batch_init(&fbatch);
while (find_lock_entries(mapping, &index, end, &fbatch, indices)) {
bool xa_has_values = false;
int nr = folio_batch_count(&fbatch);
for (i = 0; i < nr; i++) {
struct folio *folio = fbatch.folios[i];
/* We rely upon deletion not changing folio->index */
if (xa_is_value(folio)) {
xa_has_values = true;
count++;
continue;
}
ret = mapping_evict_folio(mapping, folio);
folio_unlock(folio);
/*
* Invalidation is a hint that the folio is no longer
* of interest and try to speed up its reclaim.
*/
if (!ret) {
deactivate_file_folio(folio);
/* Likely in the lru cache of a remote CPU */
if (nr_failed)
(*nr_failed)++;
}
count += ret;
}
if (xa_has_values)
clear_shadow_entries(mapping, indices[0], indices[nr-1]);
folio_batch_remove_exceptionals(&fbatch);
folio_batch_release(&fbatch);
cond_resched();
}
return count;
}
pagevec라는 이름의 경로와 비교하려면 다음 v6.0 invalidate_mapping_pagevec()를 따로 읽는 편이 명확합니다. 이는 현재 함수의 복원본이 아니라 해당 tag의 전체 정의입니다. 현재 helper와 마찬가지로 shadow와 실제 folio를 구별하고, 실패한 folio를 deactivate한 뒤 batch 참조를 놓습니다.
invalidate_mapping_pagevec_v6_0.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.0 mm/truncate.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/truncate.c
* Copyright (C) 2002, Linus Torvalds
* 10Sep2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
unsigned long invalidate_mapping_pagevec(struct address_space *mapping,
pgoff_t start, pgoff_t end, unsigned long *nr_pagevec)
{
pgoff_t indices[PAGEVEC_SIZE];
struct folio_batch fbatch;
pgoff_t index = start;
unsigned long ret;
unsigned long count = 0;
int i;
folio_batch_init(&fbatch);
while (find_lock_entries(mapping, index, end, &fbatch, indices)) {
for (i = 0; i < folio_batch_count(&fbatch); i++) {
struct folio *folio = fbatch.folios[i];
/* We rely upon deletion not changing folio->index */
index = indices[i];
if (xa_is_value(folio)) {
count += invalidate_exceptional_entry(mapping,
index,
folio);
continue;
}
index += folio_nr_pages(folio) - 1;
ret = mapping_evict_folio(mapping, folio);
folio_unlock(folio);
/*
* Invalidation is a hint that the folio is no longer
* of interest and try to speed up its reclaim.
*/
if (!ret) {
deactivate_file_folio(folio);
/* It is likely on the pagevec of a remote CPU */
if (nr_pagevec)
(*nr_pagevec)++;
}
count += ret;
}
folio_batch_remove_exceptionals(&fbatch);
folio_batch_release(&fbatch);
cond_resched();
index++;
}
return count;
}
mapping_evict_folio는 clean보다 많은 조건을 확인합니다
mapping_evict_folio()는 locked folio를 전제로 합니다. dirty 또는 writeback 상태면 0을 반환합니다. 아직 storage에 반영되지 않은 내용을 best-effort cache dropping 때문에 잃어서는 안 되기 때문입니다.
다음으로 reference count를 기대 범위와 비교합니다. 현재 구현은 cache가 base-page 수에 맞춰 보유한 참조, private state와 호출자의 임시 참조를 고려한 기준을 사용합니다. PTE mapping이나 다른 kernel 사용자가 있어 refcount가 더 높다면 제거할 수 없습니다. 단지 user process가 읽기만 한다는 이유로 아무 참조도 없다고 생각해서는 안 됩니다.
filesystem private state도 filemap_release_folio()로 해제할 수 있어야 합니다. attached buffer나 다른 상태를 지금 놓을 수 없다면 folio는 남습니다. 마지막의 remove_mapping()은 reclaim과 공유하는 최종 제거 규칙을 적용합니다. 단순한 refcount의 사전 관찰만으로 동시 lookup까지 배제한 것은 아닙니다. eviction과 invalidation 구현
구체적인 사전 기준은 folio_nr_pages(folio) + folio_has_private(folio) + 1입니다. 마지막 1은 이 경로가 보유한 임시 참조이고 large folio의 cache 참조는 base-page 수만큼 계산합니다. 현재 구현은 filemap_release_folio(folio, 0)를 private bit 검사 안에만 가두지 않고 호출합니다. mapping이 이미 없으면 0으로 끝나며, dirty·writeback 검사나 참조 상한 검사에 걸려도 0입니다. 실제 제거의 결과는 마지막 remove_mapping()에서 얻습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/truncate.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/truncate.c
* Copyright (C) 2002, Linus Torvalds
* 10Sep2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
{
/* The page may have been truncated before it was locked */
if (!mapping)
return 0;
if (folio_test_dirty(folio) || folio_test_writeback(folio))
return 0;
/* The refcount will be elevated if any page in the folio is mapped */
if (folio_ref_count(folio) >
folio_nr_pages(folio) + folio_has_private(folio) + 1)
return 0;
if (!filemap_release_folio(folio, 0))
return 0;
return remove_mapping(mapping, folio);
}
이렇게 보면 cache가 일부 남는 현상은 곧바로 실패나 leak의 증거가 아닙니다. clean, unlocked, unmapped, private state와 reference 조건을 모두 만족하는지를 보아야 합니다. drop_caches 뒤의 숫자 하나보다 어떤 소유권과 작업이 그 folio를 붙잡고 있는지가 더 중요한 질문입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.9.4~9.9.5.
- Linux v6.18: drop_caches.c, super.c, truncate.c.
- VM sysctl documentation.