kernel 안에서 파일의 한 page가 필요하다면 어떤 helper를 골라야 할까요? cache에 있는지만 알고 싶을 수도 있고, 없으면 buffer만 만들어 두고 싶을 수도 있으며, 반환 직후 내용을 읽을 수 있어야 할 수도 있습니다. 이름에 get이나 read가 있다는 이유만으로 이 완료 조건을 같게 기대하면 안 됩니다.
여기서는 upstream Linux v6.18의 단일 folio lookup, cache 삽입과 read completion을 연결합니다. 이전 구현에 등장하는 FGP_ENTRY, mapping_get_entry()나 NULL 실패 관습은 현재 함수의 계약과 구분해 읽습니다.
filemap_get_folio는 내용을 채우는 함수가 아닙니다
filemap_get_folio()는 기존 folio를 찾고 참조를 얻는 lookup입니다. 반환된 folio가 locked이거나 uptodate일 필요는 없습니다. 현재의 __filemap_get_folio()는 missing에 ERR_PTR(-ENOENT)를 반환하므로 NULL만 확인하는 caller는 잘못된 pointer를 사용할 수 있습니다.
shadow나 shmem swap value 같은 exceptional entry까지 필요하면 filemap_get_entry()의 계약을 사용해야 합니다. value entry는 folio가 아니므로 refcount를 올리거나 folio_put()을 해서는 안 됩니다. 현재 단일-folio helper는 그런 value를 실제 folio가 없는 것으로 취급합니다.
FGP_CREAT를 더하면 missing 구간에 folio를 할당하고 cache에 넣을 수 있습니다. 하지만 새 folio의 contents는 storage에서 아직 읽어 오지 않은 상태입니다. buffer가 생겼다는 사실과 user에게 보여 줄 내용이 유효하다는 사실을 분리해야 합니다.
아래는 v6.18 header의 wrapper 전체입니다. 이 글의 code 파일은 pinned source의 함수 전체를 담고 license와 출처를 보존한 kernel 발췌입니다. 독립 user program이나 책의 옛 code를 복원한 것은 아닙니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright 1995 Linus Torvalds */
/* Complete definitions from Linux v6.18 include/linux/pagemap.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Kernel header excerpt; not a standalone user-space program.
*/
static inline struct folio *filemap_get_folio(struct address_space *mapping,
pgoff_t index)
{
return __filemap_get_folio(mapping, index, 0, 0);
}
FGP flag는 반환 상태와 allocation 문맥을 바꿉니다
FGP_LOCK은 folio lock을 얻은 상태로 반환하도록 요구합니다. 기다린 다음에는 folio->mapping을 다시 확인해야 합니다. 다른 mapping으로 바뀌었거나 truncate로 제거되었다면 unlock과 put을 하고 lookup부터 다시 시작합니다. lock 전에 얻은 참조는 memory 수명을 유지할 뿐 파일 소속을 고정하지는 않습니다.
FGP_NOWAIT가 함께 있으면 lock을 기다리지 않고 trylock합니다. 실패할 때에는 얻었던 참조를 놓고 현재 API의 ERR_PTR(-EAGAIN)을 반환합니다. create allocation에도 nonblocking GFP 조건을 적용하며, nonblocking allocation 때문에 발생한 -ENOMEM은 blocking 재시도를 요청할 수 있도록 -EAGAIN으로 바뀔 수 있습니다.
FGP_NOFS는 allocation 중 filesystem에 재진입하는 reclaim을 제한합니다. filesystem 내부에서 자기 lock을 잡고 folio를 구할 때 같은 filesystem의 경로로 다시 들어가면 deadlock이 생길 수 있기 때문입니다. 이것은 새 storage data를 반드시 읽어도 된다는 별도의 허가가 아닙니다.
FGP_WRITE는 쓰기 목적의 allocation 및 idle 상태 처리에 참여하고, FGP_ACCESSED는 reclaim에 최근 사용을 전달합니다. 기존 folio와 새 folio의 초기 상태를 같은 지점에서 처리해야 한다는 요구는 남지만, allocation과 lookup 경로에서 사용하는 세부 helper는 다를 수 있습니다.
FGP_STABLE은 folio_wait_stable()을 사용합니다. 모든 mapping에서 무조건 writeback을 기다리는 것이 아니라 stable writes가 필요한 mapping의 조건을 따릅니다. FGP_FOR_MMAP은 새로 cache에 넣은 folio를 fault 경로가 원하는 unlocked 상태로 돌려주는 데 참여합니다. 다른 thread가 잡고 있는 기존 folio lock을 임의로 풀어 주는 flag는 아닙니다.
large folio allocation은 정렬과 mapping의 범위를 따릅니다
현재 helper는 mapping의 최소·최대 folio order와 요청 order를 함께 봅니다. index를 mapping 조건에 맞추고, 정렬되지 않은 위치라면 더 작은 order를 선택할 수 있습니다. 더 큰 allocation에 실패했을 때 최소 order까지 fallback하는 경우도 있습니다.
큰 order의 speculative 시도에는 과도한 reclaim이나 경고를 피하는 GFP 보정을 사용할 수 있습니다. 따라서 먼저 요청한 order가 언제나 실제 반환된 folio order라고 가정할 수 없습니다. caller는 반환된 folio의 크기와 포함 index를 확인하는 계약을 따라야 합니다.
NUMA 배치와 cpuset 같은 제약도 physical allocation에 영향을 줍니다. filemap_alloc_folio() 아래에 실제 page allocator가 있다는 사실이 단순히 여유 RAM 총량만 보면 성공 여부를 알 수 있다는 뜻은 아닙니다. allocation 실패, XArray node 준비 실패와 다른 reader에 대한 삽입 경쟁도 구별해야 합니다. 단일 lookup과 allocation 전체 구현
현재 fgp_flags의 type은 fgf_t입니다. FGP_CREAT에 LOCK과 FOR_MMAP 중 어느 것도 없으면 경고하고 LOCK을 보완하는 분기가 있습니다. FGP_ACCESSED는 기존 folio에 accessed 표시를 하고 새 folio에는 referenced 초기화를 사용합니다. FGP_WRITE의 idle clear는 기존 folio에서 ACCESSED 분기와 else if 관계이며, write allocation hint는 mapping이 writeback을 지원할 때만 추가됩니다. FGP_DONTCACHE의 dropbehind 상태도 별도로 추적합니다.
아래 전체 함수에서 NOWAIT의 -EAGAIN, missing의 -ENOENT, allocation·삽입 오류를 구별할 수 있습니다. FGP_LOCK이나 CREAT 경로의 잠금은 allocation용 GFP가 atomic하다는 이유만으로 자동 nonblocking이 되지 않으므로, caller의 대기 허용 계약까지 맞춰야 합니다.
// 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.
*/
struct folio *__filemap_get_folio(struct address_space *mapping, pgoff_t index,
fgf_t fgp_flags, gfp_t gfp)
{
struct folio *folio;
repeat:
folio = filemap_get_entry(mapping, index);
if (xa_is_value(folio))
folio = NULL;
if (!folio)
goto no_page;
if (fgp_flags & FGP_LOCK) {
if (fgp_flags & FGP_NOWAIT) {
if (!folio_trylock(folio)) {
folio_put(folio);
return ERR_PTR(-EAGAIN);
}
} else {
folio_lock(folio);
}
/* Has the page been truncated? */
if (unlikely(folio->mapping != mapping)) {
folio_unlock(folio);
folio_put(folio);
goto repeat;
}
VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);
}
if (fgp_flags & FGP_ACCESSED)
folio_mark_accessed(folio);
else if (fgp_flags & FGP_WRITE) {
/* Clear idle flag for buffer write */
if (folio_test_idle(folio))
folio_clear_idle(folio);
}
if (fgp_flags & FGP_STABLE)
folio_wait_stable(folio);
no_page:
if (!folio && (fgp_flags & FGP_CREAT)) {
unsigned int min_order = mapping_min_folio_order(mapping);
unsigned int order = max(min_order, FGF_GET_ORDER(fgp_flags));
int err;
index = mapping_align_index(mapping, index);
if ((fgp_flags & FGP_WRITE) && mapping_can_writeback(mapping))
gfp |= __GFP_WRITE;
if (fgp_flags & FGP_NOFS)
gfp &= ~__GFP_FS;
if (fgp_flags & FGP_NOWAIT) {
gfp &= ~GFP_KERNEL;
gfp |= GFP_NOWAIT;
}
if (WARN_ON_ONCE(!(fgp_flags & (FGP_LOCK | FGP_FOR_MMAP))))
fgp_flags |= FGP_LOCK;
if (order > mapping_max_folio_order(mapping))
order = mapping_max_folio_order(mapping);
/* If we're not aligned, allocate a smaller folio */
if (index & ((1UL << order) - 1))
order = __ffs(index);
do {
gfp_t alloc_gfp = gfp;
err = -ENOMEM;
if (order > min_order)
alloc_gfp |= __GFP_NORETRY | __GFP_NOWARN;
folio = filemap_alloc_folio(alloc_gfp, order);
if (!folio)
continue;
/* Init accessed so avoid atomic mark_page_accessed later */
if (fgp_flags & FGP_ACCESSED)
__folio_set_referenced(folio);
if (fgp_flags & FGP_DONTCACHE)
__folio_set_dropbehind(folio);
err = filemap_add_folio(mapping, folio, index, gfp);
if (!err)
break;
folio_put(folio);
folio = NULL;
} while (order-- > min_order);
if (err == -EEXIST)
goto repeat;
if (err) {
/*
* When NOWAIT I/O fails to allocate folios this could
* be due to a nonblocking memory allocation and not
* because the system actually is out of memory.
* Return -EAGAIN so that there caller retries in a
* blocking fashion instead of propagating -ENOMEM
* to the application.
*/
if ((fgp_flags & FGP_NOWAIT) && err == -ENOMEM)
err = -EAGAIN;
return ERR_PTR(err);
}
/*
* filemap_add_folio locks the page, and for mmap
* we expect an unlocked page.
*/
if (folio && (fgp_flags & FGP_FOR_MMAP))
folio_unlock(folio);
}
if (!folio)
return ERR_PTR(-ENOENT);
/* not an uncached lookup, clear uncached if set */
if (folio_test_dropbehind(folio) && !(fgp_flags & FGP_DONTCACHE))
folio_clear_dropbehind(folio);
return folio;
}
lockless lookup은 ref 확보 뒤 entry를 다시 읽습니다
XArray를 RCU로 순회한다고 lookup과 제거가 서로 배타적으로 실행되는 것은 아닙니다. reader가 옛 entry를 읽은 직후 remover가 cache에서 folio를 떼어 낼 수 있습니다. refcount가 이미 0으로 동결되었다면 reader는 참조를 새로 얻을 수 없어야 합니다.
filemap_get_entry()는 internal retry를 처리하고, 실제 folio에는 nonzero 참조를 얻은 뒤 xas_reload()로 여전히 같은 entry인지 확인합니다. entry가 바뀌었다면 잠깐 얻었던 참조를 놓고 재시도합니다. 먼저 ref를 얻고 나중에 entry를 확인하는 두 단계가 함께 있어야 합니다.
ref를 얻고 entry도 일치했다고 이후 truncation이 금지되지는 않습니다. folio lock을 얻은 뒤 mapping을 다시 확인하는 것은 별도의 다음 단계입니다. tree에 소속되어 있었다는 사실, memory가 살아 있다는 사실과 내용이 여전히 같은 파일 offset을 뜻한다는 사실을 차례대로 확인합니다.
모든 kernel 연결이 무조건 ref 하나를 새로 추가한다고 단순화해서도 안 됩니다. cache의 참조, caller의 참조, PTE가 유지하는 mapping과 filesystem/BIO의 별도 참조는 각 계약에 따릅니다. LRU list에 연결되어 있다는 사실만으로 독립적인 ref 하나를 임의로 더해 계산하면 정리 경로를 잘못 이해하게 됩니다.
제거 쪽의 ref freeze·XArray 제거·allocator 반환 순서와 lookup 쪽의 load·nonzero get·reload는 서로 맞물립니다. speculative lookup이 이미 재사용된 page에 잠깐 참조를 얻을 가능성까지 있어 마지막 참조를 folio_put()으로 놓을 수 있는 계약이 중요합니다. 단순히 RCU grace period 동안 모든 physical page 재사용이 금지된다고 읽어서는 안 됩니다. current filemap_get_entry()는 과거 mapping_get_entry()에 대응하는 함수이며 exceptional value에는 get을 수행하지 않습니다.
// 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.
*/
void *filemap_get_entry(struct address_space *mapping, pgoff_t index)
{
XA_STATE(xas, &mapping->i_pages, index);
struct folio *folio;
rcu_read_lock();
repeat:
xas_reset(&xas);
folio = xas_load(&xas);
if (xas_retry(&xas, folio))
goto repeat;
/*
* A shadow entry of a recently evicted page, or a swap entry from
* shmem/tmpfs. Return it without attempting to raise page count.
*/
if (!folio || xa_is_value(folio))
goto out;
if (!folio_try_get(folio))
goto repeat;
if (unlikely(folio != xas_reload(&xas))) {
folio_put(folio);
goto repeat;
}
out:
rcu_read_unlock();
return folio;
}
이름과 반환 type이 달랐던 구현을 비교할 때에는 v6.0의 mapping_get_entry() 전체를 볼 수 있습니다. void *로 exceptional value까지 돌려주며 folio_try_get_rcu()와 reload를 조합합니다. 아래는 확인 가능한 공식 tag를 선택한 비교 code입니다. 책의 모든 Listing이 정확히 이 release에서 왔다고 주장하는 것은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.0 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* This tag is not asserted to be the book's exact source revision.
*/
static void *mapping_get_entry(struct address_space *mapping, pgoff_t index)
{
XA_STATE(xas, &mapping->i_pages, index);
struct folio *folio;
rcu_read_lock();
repeat:
xas_reset(&xas);
folio = xas_load(&xas);
if (xas_retry(&xas, folio))
goto repeat;
/*
* A shadow entry of a recently evicted page, or a swap entry from
* shmem/tmpfs. Return it without attempting to raise page count.
*/
if (!folio || xa_is_value(folio))
goto out;
if (!folio_try_get_rcu(folio))
goto repeat;
if (unlikely(folio != xas_reload(&xas))) {
folio_put(folio);
goto repeat;
}
out:
rcu_read_unlock();
return folio;
}
cache 삽입은 XArray 범위와 accounting을 함께 바꿉니다
filemap_add_folio()는 folio를 locked 상태로 만들고 내부 __filemap_add_folio()에 실제 삽입을 맡깁니다. 현재 wrapper에는 memcg charge와 실패 시 uncharge, 성공 후 LRU 등록 및 workingset refault 관련 처리도 있습니다. 정상 반환에서 folio lock을 유지한다는 점이 다음 read의 사전조건과 연결됩니다.
내부 삽입은 folio->mapping과 index를 설정하고 cache가 보유할 참조를 base-page 수에 맞춰 추가합니다. order-2 folio는 객체 하나지만 네 base page를 대표합니다. mapping->nrpages 역시 folio 객체 수가 아니라 base page 수를 셉니다. 삽입 성공이 process PTE 설치를 의미하지도 않습니다.
XA_STATE의 목표 order를 정했다고 이미 있는 entry가 같은 order인 것은 아닙니다. 큰 shadow entry의 일부를 새 folio가 대체해야 하면 적절히 나누고, 대상 범위의 conflict를 검사해야 합니다. 실제 folio와 겹친다면 -EEXIST이며, 기존 pointer를 덮어쓰고 새 allocation을 성공으로 처리할 수 없습니다.
shadow value를 새 resident folio로 대체하는 것은 허용할 수 있지만 그 범위와 split에 필요한 node memory가 준비되어야 합니다. XArray lock 안에서 잠드는 allocation을 하지 않으려고 xas_nomem() 경로가 lock 밖에서 memory를 준비한 뒤 반복할 수 있습니다. 최종 실패라면 미리 추가한 참조와 mapping 상태를 되돌려야 합니다.
-EEXIST의 일반적인 처리 방식은 새 folio의 allocation 참조를 놓고 이미 들어간 대상을 lookup하는 것입니다. cache 삽입이 실패했는데도 caller가 pointer를 계속 사용하거나, 반대로 cache가 보유한 참조까지 자기 소유라고 생각해 추가로 put하면 오류가 생깁니다.
current 내부 삽입은 folio가 이미 locked이고 swapbacked가 아니며 mapping의 minimum order·index alignment를 만족하는지 debug 검사합니다. XArray에는 mapping update callback도 연결합니다. shadow split은 과거 xas_split_alloc() 예시와 달리 lock 안의 conflict order를 기준으로 xas_try_split()을 반복하고, 실패한 node 준비는 lock 밖 xas_nomem() 재시도로 연결합니다. 오류 정리에서는 mapping을 NULL로 돌리지만 index는 truncation이 필요로 하므로 남깁니다.
// 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.
*/
noinline int __filemap_add_folio(struct address_space *mapping,
struct folio *folio, pgoff_t index, gfp_t gfp, void **shadowp)
{
XA_STATE_ORDER(xas, &mapping->i_pages, index, folio_order(folio));
bool huge;
long nr;
unsigned int forder = folio_order(folio);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
VM_BUG_ON_FOLIO(folio_test_swapbacked(folio), folio);
VM_BUG_ON_FOLIO(folio_order(folio) < mapping_min_folio_order(mapping),
folio);
mapping_set_update(&xas, mapping);
VM_BUG_ON_FOLIO(index & (folio_nr_pages(folio) - 1), folio);
huge = folio_test_hugetlb(folio);
nr = folio_nr_pages(folio);
gfp &= GFP_RECLAIM_MASK;
folio_ref_add(folio, nr);
folio->mapping = mapping;
folio->index = xas.xa_index;
for (;;) {
int order = -1;
void *entry, *old = NULL;
xas_lock_irq(&xas);
xas_for_each_conflict(&xas, entry) {
old = entry;
if (!xa_is_value(entry)) {
xas_set_err(&xas, -EEXIST);
goto unlock;
}
/*
* If a larger entry exists,
* it will be the first and only entry iterated.
*/
if (order == -1)
order = xas_get_order(&xas);
}
if (old) {
if (order > 0 && order > forder) {
unsigned int split_order = max(forder,
xas_try_split_min_order(order));
/* How to handle large swap entries? */
BUG_ON(shmem_mapping(mapping));
while (order > forder) {
xas_set_order(&xas, index, split_order);
xas_try_split(&xas, old, order);
if (xas_error(&xas))
goto unlock;
order = split_order;
split_order =
max(xas_try_split_min_order(
split_order),
forder);
}
xas_reset(&xas);
}
if (shadowp)
*shadowp = old;
}
xas_store(&xas, folio);
if (xas_error(&xas))
goto unlock;
mapping->nrpages += nr;
/* hugetlb pages do not participate in page cache accounting */
if (!huge) {
__lruvec_stat_mod_folio(folio, NR_FILE_PAGES, nr);
if (folio_test_pmd_mappable(folio))
__lruvec_stat_mod_folio(folio,
NR_FILE_THPS, nr);
}
unlock:
xas_unlock_irq(&xas);
if (!xas_nomem(&xas, gfp))
break;
}
if (xas_error(&xas))
goto error;
trace_mm_filemap_add_to_page_cache(folio);
return 0;
error:
folio->mapping = NULL;
/* Leave folio->index set: truncation relies upon it */
folio_put_refs(folio, nr);
return xas_error(&xas);
}
wrapper의 memcg charge 자체가 먼저 실패할 수도 있습니다. charge 성공 뒤 내부 삽입이 실패하면 uncharge와 locked flag 정리가 이어집니다. 정상 경로는 locked 상태를 유지하며 LRU에 넣고, shadow refault 처리는 __GFP_WRITE 여부를 고려합니다. 성공 시 lock을 푼다는 설명은 실제 반환 계약과 맞지 않습니다.
// 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.
*/
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;
}
read_cache_folio는 필요한 내용을 얻는 쪽입니다
filesystem과 driver가 내용까지 유효한 단일 folio를 필요로 할 때에는 read_cache_folio() 계열을 사용할 수 있습니다. 내부 do_read_cache_folio()는 optional filler가 없으면 mapping의 read_folio callback을 사용합니다. mapping의 invalidate lock을 caller가 이미 잡아야 하는 wrapper도 있으므로 사용하는 진입점의 계약을 확인해야 합니다.
기존 folio가 uptodate이면 곧바로 accessed 처리를 하고 반환할 수 있습니다. missing이면 mapping의 최소 order로 할당하고 정렬한 뒤 cache에 넣습니다. 삽입 경쟁에서 지면 새 allocation의 참조를 놓고 다시 찾습니다. XArray node allocation 같은 다른 실패는 해당 오류로 돌아갑니다.
기존 folio가 not-uptodate이면 trylock을 합니다. lock을 기다려야 한다면 참조를 놓고 기다린 뒤 다시 lookup하는 경로가 있습니다. lock을 얻은 뒤에는 mapping이 사라졌는지와 그 짧은 사이 다른 reader가 이미 uptodate로 만들었는지를 다시 확인합니다. 이미 준비되었다면 같은 read를 중복 제출할 필요가 없습니다.
여전히 내용을 읽어야 하는 locked folio라면 filler로 넘깁니다. truncation을 뜻하는 재시도 결과는 처음부터 다시 찾고, 복구할 수 없는 오류는 참조를 정리해 caller에게 전달합니다. 성공한 folio는 내용이 유효하고 caller가 사용할 참조가 있는 상태입니다. 단순 get과 이 read 계열이 반환하는 준비 수준의 차이입니다.
read_mapping_folio()는 read_cache_folio()에 기본 filler를 선택하도록 위임하고, page 기반 read_mapping_page()는 read_cache_page() 계열로 이어집니다. filesystem·driver·block 쪽 caller가 이 wrapper들을 재사용합니다. 아래 wrapper와 worker의 반환 type 및 invalidate-lock 사전조건을 함께 읽어야 합니다. read_cache_folio()는 caller가 mapping의 invalidate lock을 보유할 것을 요구합니다. worker가 기존 locked folio를 기다리는 경로는 TASK_UNINTERRUPTIBLE이며, 실제 read wrapper의 killable completion 대기와 같은 대기가 아닙니다.
page 기반 caller에는 GFP를 지정하는 read_cache_page_gfp()도 있습니다. read_cache_page()와 함께 do_read_cache_page()를 거쳐 같은 folio worker를 사용하며, wrapper는 folio가 포함한 요청 index의 subpage를 골라 struct page *로 반환합니다. 오류 pointer를 정상 page로 바꾸는 경로는 아닙니다. filesystem·driver뿐 아니라 block/SCSI, swap 활성화나 uprobe처럼 특정 file page의 내용을 필요로 하는 kernel 작업도 이런 계열을 재사용할 수 있습니다. 이 호출자 범주가 각자의 storage read 구현을 별도로 만든다는 뜻은 아닙니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright 1995 Linus Torvalds */
/* Complete definitions from Linux v6.18 include/linux/pagemap.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Kernel header excerpt; not a standalone user-space program.
*/
static inline struct folio *read_mapping_folio(struct address_space *mapping,
pgoff_t index, struct file *file)
{
return read_cache_folio(mapping, index, NULL, file);
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright 1995 Linus Torvalds */
/* Complete definitions from Linux v6.18 include/linux/pagemap.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Kernel header excerpt; not a standalone user-space program.
*/
static inline struct page *read_mapping_page(struct address_space *mapping,
pgoff_t index, struct file *file)
{
return read_cache_page(mapping, index, NULL, file);
}
// 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.
*/
struct folio *read_cache_folio(struct address_space *mapping, pgoff_t index,
filler_t filler, struct file *file)
{
return do_read_cache_folio(mapping, index, filler, file,
mapping_gfp_mask(mapping));
}
// 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.
*/
static struct folio *do_read_cache_folio(struct address_space *mapping,
pgoff_t index, filler_t filler, struct file *file, gfp_t gfp)
{
struct folio *folio;
int err;
if (!filler)
filler = mapping->a_ops->read_folio;
repeat:
folio = filemap_get_folio(mapping, index);
if (IS_ERR(folio)) {
folio = filemap_alloc_folio(gfp,
mapping_min_folio_order(mapping));
if (!folio)
return ERR_PTR(-ENOMEM);
index = mapping_align_index(mapping, index);
err = filemap_add_folio(mapping, folio, index, gfp);
if (unlikely(err)) {
folio_put(folio);
if (err == -EEXIST)
goto repeat;
/* Presumably ENOMEM for xarray node */
return ERR_PTR(err);
}
goto filler;
}
if (folio_test_uptodate(folio))
goto out;
if (!folio_trylock(folio)) {
folio_put_wait_locked(folio, TASK_UNINTERRUPTIBLE);
goto repeat;
}
/* Folio was truncated from mapping */
if (!folio->mapping) {
folio_unlock(folio);
folio_put(folio);
goto repeat;
}
/* Someone else locked and filled the page in a very small window */
if (folio_test_uptodate(folio)) {
folio_unlock(folio);
goto out;
}
filler:
err = filemap_read_folio(file, filler, folio);
if (err) {
folio_put(folio);
if (err == AOP_TRUNCATED_PAGE)
goto repeat;
return ERR_PTR(err);
}
out:
folio_mark_accessed(folio);
return folio;
}
callback의 0 반환은 I/O 완료 통지가 아닙니다
filemap_read_folio()의 입력 folio는 이미 locked 상태여야 합니다. filesystem callback은 필요한 block mapping을 찾아 read를 시작하고, I/O가 끝날 때 uptodate 상태와 unlock을 처리합니다. callback 자체가 0을 반환했다는 사실만으로 이 후속 작업이 끝났다고 판단할 수 없습니다.
generic wrapper는 killable unlock 대기를 수행한 뒤 uptodate를 확인합니다. signal로 대기가 중단되면 그 오류를 반환할 수 있고, unlock 뒤에도 내용이 유효하지 않다면 -EIO입니다. 열린 file이 있는 경로에서는 I/O 오류에 대한 반응으로 readahead 크기를 줄일 수도 있습니다.
현재 이 wrapper는 과거의 folio error bit 처리와 completion helper 이름을 그대로 따르지 않습니다. 성공 시 uptodate를 설정하고 waiter를 깨우는 folio_end_read()와 filesystem별 completion이 있습니다. 모든 filesystem이 같은 helper를 반드시 호출하는 것은 아니므로, callback의 제출 계약과 최종 folio 상태를 기준으로 읽어야 합니다.
filler_t는 int를 반환하며 (struct file *, struct folio *)를 받는 함수 type입니다. 0은 제출 단계의 성공이지 storage completion의 보편적인 완료 통지가 아닙니다. current wrapper에는 workingset read의 PSI memstall 구간도 있으며, 과거의 folio_clear_error()와 PG_error 경로를 이 code에 그대로 덧붙여 읽지 않습니다. unlock 대기 뒤에도 uptodate가 아니면 file이 있을 때 ra_pages를 4분의 1로 줄이고 -EIO를 반환합니다.
callback type의 전체 typedef는 다음과 같습니다.
typedef int filler_t(struct file *, struct folio *);
// 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.
*/
static int filemap_read_folio(struct file *file, filler_t filler,
struct folio *folio)
{
bool workingset = folio_test_workingset(folio);
unsigned long pflags;
int error;
/* Start the actual read. The read will unlock the page. */
if (unlikely(workingset))
psi_memstall_enter(&pflags);
error = filler(file, folio);
if (unlikely(workingset))
psi_memstall_leave(&pflags);
if (error)
return error;
error = folio_wait_locked_killable(folio);
if (error)
return error;
if (folio_test_uptodate(folio))
return 0;
if (file)
shrink_readahead_size_eio(&file->f_ra);
return -EIO;
}
shrink_readahead_size_eio_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.
*/
static void shrink_readahead_size_eio(struct file_ra_state *ra)
{
ra->ra_pages /= 4;
}
현재 read completion helper는 아래처럼 locked bit를 내리면서 성공한 경우 uptodate bit를 올리고 waiter를 깨웁니다. 모든 filesystem이 이 helper를 사용한다는 뜻은 아니며 NMI에서 호출할 수 있는 interface도 아닙니다. 과거 page_endio()의 write 분기는 mapping error를 기록하고 writeback을 끝냈지만, read completion을 설명할 때 writeback 종료와 folio unlock을 하나로 합치면 안 됩니다.
과거 page_endio()의 read 분기는 성공하면 uptodate를 올리고, 실패하면 uptodate를 내리면서 PG_error를 올린 뒤 어느 경우든 page를 unlock했습니다. write 분기의 오류는 page error와 관련 mapping의 오류 상태에 남고, 정상·오류 모두 writeback 종료로 이어졌습니다. 이것은 과거 helper의 분기 설명이며 현재 read helper 전체와 동일한 구현이라는 뜻은 아닙니다.
다음은 이 두 분기를 모두 담은 v6.0 page_endio()의 전체 정의입니다. void 함수이므로 I/O error를 return value로 돌려주는 것이 아니라 flag와 mapping의 오류 상태에 기록합니다. read caller는 locked page를 넘기고 completion이 unlock하도록 하며, write caller는 writeback 종료 계약을 따릅니다. 이 함수를 v6.18의 새 filesystem code에 그대로 호출하라는 뜻은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.0 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* This tag is not asserted to be the book's exact source revision.
*/
void page_endio(struct page *page, bool is_write, int err)
{
if (!is_write) {
if (!err) {
SetPageUptodate(page);
} else {
ClearPageUptodate(page);
SetPageError(page);
}
unlock_page(page);
} else {
if (err) {
struct address_space *mapping;
SetPageError(page);
mapping = page_mapping(page);
if (mapping)
mapping_set_error(mapping, err);
}
end_page_writeback(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.
*/
void folio_end_read(struct folio *folio, bool success)
{
unsigned long mask = 1 << PG_locked;
/* Must be in bottom byte for x86 to work */
BUILD_BUG_ON(PG_uptodate > 7);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
VM_BUG_ON_FOLIO(success && folio_test_uptodate(folio), folio);
if (likely(success))
mask |= 1 << PG_uptodate;
if (folio_xor_flags_has_waiters(folio, mask))
folio_wake_bit(folio, PG_locked);
}
다음은 구현코드가 아닌 역할별 호출관계입니다. 서로 다른 caller가 같은 fill·completion 계약을 재사용한다는 점을 표시하며 모든 분기가 한 번에 실행되는 것은 아닙니다.
lookup: filemap_get_folio() -> __filemap_get_folio()
entry: filemap_get_entry() -> xas_load() -> xas_reload()
insert: filemap_add_folio() -> __filemap_add_folio()
fill: filemap_read_folio() -> filler(file, folio)
read: read_cache_folio() -> do_read_cache_folio()
page: read_mapping_page() -> read_cache_page() -> do_read_cache_page()
gfp: read_cache_page_gfp() -> do_read_cache_page()
worker: do_read_cache_page() -> do_read_cache_folio()
folio 하나를 얻는 API도 object 존재, 참조, lock, 내용의 유효성을 어디까지 책임지는지에 따라 달라집니다. caller가 나머지 조건을 정확히 이어받아야 lookup 성공을 storage read 성공으로 바꾸어 오해하지 않습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.5.3~9.6.
- Linux v6.18: single lookup, insertion, read와 completion, pagemap.h, fs.h.
- Linux v6.0: mapping_get_entry와 page_endio의 전체 비교 정의.