파일의 일정 범위에서 folio를 모으는 함수라면 모두 같은 결과를 돌려줄 것처럼 보입니다. 하지만 cache에 이미 있는 entry만 찾는 작업과, 없는 내용을 storage에서 읽어 오는 작업은 비용과 실패 조건이 다릅니다. 그중에서도 잠긴 folio를 기다릴지 건너뛸지, value entry를 포함할지, large folio가 범위 밖으로 걸쳐도 되는지가 달라집니다.
이 차이는 API 선택의 부수적인 옵션이 아닙니다. truncate가 끝까지 제거해야 할 대상과 cache dropping이 지금 당장 제거할 수 있는 대상은 같지 않습니다. 아래 비교와 구체적인 반환 계약은 upstream Linux v6.18을 기준으로 설명합니다.
조회와 채우기를 먼저 구분합니다
기존 entry만 찾는 lookup은 missing 구간을 돌려주지 않거나, 단일 lookup에서 없다는 결과를 반환할 수 있습니다. find_get_entries()와 filemap_get_folios()가 그 예입니다. 함수가 folio 참조를 얻었다고 내용이 uptodate이거나 folio lock까지 얻었다는 뜻은 아닙니다.
filemap_get_pages()는 read를 진행할 batch가 필요하므로 좀 더 적극적입니다. 먼저 cache를 찾고, miss이면 readahead를 시도하며, 그래도 준비하지 못하면 단일 folio를 만들어 읽는 fallback까지 진행할 수 있습니다. 여기에 no-I/O와 NOWAIT 조건이 더해집니다.
없는 folio를 할당해 cache에 넣는 일만 수행하는 중간 단계도 있습니다. FGP_CREAT를 준 단일 lookup이 새 not-uptodate folio를 돌려주는 경우입니다. allocation과 storage read를 같은 사건으로 읽으면 이렇게 다른 준비 상태를 놓치게 됩니다.
| API | exceptional value | lock과 범위 |
|---|---|---|
find_get_entries() |
포함할 수 있습니다 | folio 참조를 얻고 index 배열을 돌려줍니다 |
find_lock_entries() |
포함할 수 있습니다 | trylock, writeback·경계 조건에 따라 건너뜁니다 |
filemap_get_folios() |
제외합니다 | 범위에 걸친 large folio를 포함하고 다음 index를 갱신합니다 |
filemap_get_read_batch() |
만나면 연속 batch를 끝냅니다 | read가 처리할 연속 구간을 모읍니다 |
빠진 index 때문에 별도 위치 배열이 필요합니다
find_get_entries()는 mapping의 XArray에서 시작과 끝 사이의 entry를 ascending index 순서로 모읍니다. 입력 end는 inclusive입니다. 중간에 hole이 있거나 large folio가 여러 index를 대표하면 반환된 entry 번호와 file index가 같아지지 않습니다. 따라서 batch와 함께 각 entry의 index를 담는 배열이 필요합니다.
이 함수는 shadow나 shmem swap 같은 exceptional value도 돌려줄 수 있습니다. xa_is_value()를 확인하지 않고 모든 entry를 folio처럼 해석해서는 안 됩니다. 실제 folio에는 참조를 얻지만 정수 value에 folio_put()을 수행할 수는 없습니다.
내부 find_get_entry()는 XA_PRESENT 검색과 mark 기반 검색의 차이를 다룹니다. 정상 pointer를 찾으면 nonzero 참조를 얻고 XArray entry를 다시 대조합니다. RCU가 tree 탐색을 도와도 참조가 0으로 동결된 folio를 새 소유자로 만드는 것은 허용되지 않기 때문입니다. entry가 바뀌었다면 참조를 정리하고 cursor를 reset한 뒤 재시도합니다.
여기서 present는 page table의 present PTE를 뜻하지 않습니다. XArray 안에 entry가 존재하는지에 관한 검색 문맥입니다. cached folio의 존재와 process의 mapping을 같은 present라는 단어로 섞으면 결과를 잘못 해석하게 됩니다. batch lookup 구현
v6.18의 find_get_entries()와 find_lock_entries()는 pgoff_t *start를 받습니다. 오래된 값 인자 signature와 달리 다음 순회 위치를 caller에게 돌려줍니다. 전자는 마지막 entry의 order에 맞춰 다음 위치를 정렬하며, value도 multi-index 범위를 대표할 수 있어 무조건 index를 1만 더하지 않습니다. RCU는 writer의 수정 자체를 막는 lock이 아닙니다. 실제 folio를 사용하는 근거는 nonzero ref 확보와 reload 검증까지 완료한 것입니다. 현재 helper 이름은 folio_try_get()이며 오래된 folio_try_get_rcu() 표기와 구별합니다.
아래부터의 code는 같은 v6.18 mm/filemap.c의 함수 전체 정의입니다. 각 파일에 GPL-2.0-only와 attribution을 보존했습니다. kernel type과 helper에 의존하므로 독립 실행 예제는 아니며, 책이 생략한 옛 코드를 복원한 것도 아닙니다.
// 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static inline struct folio *find_get_entry(struct xa_state *xas, pgoff_t max,
xa_mark_t mark)
{
struct folio *folio;
retry:
if (mark == XA_PRESENT)
folio = xas_find(xas, max);
else
folio = xas_find_marked(xas, max, mark);
if (xas_retry(xas, folio))
goto retry;
/*
* A shadow entry of a recently evicted page, a swap
* entry from shmem/tmpfs or a DAX entry. Return it
* without attempting to raise page count.
*/
if (!folio || xa_is_value(folio))
return folio;
if (!folio_try_get(folio))
goto reset;
if (unlikely(folio != xas_reload(xas))) {
folio_put(folio);
goto reset;
}
return folio;
reset:
xas_reset(xas);
goto retry;
}
// 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
unsigned find_get_entries(struct address_space *mapping, pgoff_t *start,
pgoff_t end, struct folio_batch *fbatch, pgoff_t *indices)
{
XA_STATE(xas, &mapping->i_pages, *start);
struct folio *folio;
rcu_read_lock();
while ((folio = find_get_entry(&xas, end, XA_PRESENT)) != NULL) {
indices[fbatch->nr] = xas.xa_index;
if (!folio_batch_add(fbatch, folio))
break;
}
if (folio_batch_count(fbatch)) {
unsigned long nr;
int idx = folio_batch_count(fbatch) - 1;
folio = fbatch->folios[idx];
if (!xa_is_value(folio))
nr = folio_nr_pages(folio);
else
nr = 1 << xa_get_order(&mapping->i_pages, indices[idx]);
*start = round_down(indices[idx] + nr, nr);
}
rcu_read_unlock();
return folio_batch_count(fbatch);
}
trylock batch는 빠짐없는 목록이 아닙니다
find_lock_entries()는 찾은 folio에 lock을 시도하지만 이미 잠겨 있으면 기다리지 않습니다. cache dropping처럼 지금 가능한 대상을 정리할 때 긴 I/O나 lock holder에 매달리지 않으려는 선택입니다. writeback 중인 folio나 입력 범위 안에 완전히 들어오지 않는 large folio도 제외할 수 있습니다.
lock을 얻었다고 바로 batch에 넣는 것도 아닙니다. lookup 뒤 truncate가 개입했을 수 있으므로 mapping과 범위를 다시 확인합니다. 맞지 않으면 unlock과 put을 하고 다음 대상으로 진행합니다. debug build의 index 검사는 이 계약을 보강하지만 실제 경쟁 처리를 대신하지는 않습니다.
따라서 반환 개수가 적거나 0이라는 사실만으로 전체 파일에 대상이 없다고 단정할 수 없습니다. missing뿐 아니라 contention과 writeback, 범위 조건 때문에 빠졌을 수 있습니다. 이 API를 사용해 반드시 모든 대상을 처리해야 하는 작업을 한 번에 끝내려 하면 누락이 생길 수 있습니다.
filemap_get_folios()는 다른 절충을 합니다. exceptional value는 제외하고 실제 folio를 모으며, caller가 넘긴 index pointer에 다음 검색 위치를 남깁니다. 시작이나 끝 경계를 걸치는 large folio를 허용하므로 반환 개수만으로 전체 byte 범위를 계산할 수 없습니다. 마지막 index 근처에서 다음 위치를 갱신할 때에도 종료와 overflow 조건을 다뤄야 합니다.
current find_lock_entries()는 order가 있는 value의 시작·끝도 검사합니다. 범위 앞에서 시작한 value는 건너뛰고, 끝을 넘는 value에서는 중단합니다. 이 value에는 folio lock이나 refcount를 적용하지 않습니다. 실제 folio를 제외할 때에는 trylock 전 실패라면 put만, lock 뒤 실패라면 unlock과 put을 수행합니다. 반환 batch에 넣은 실제 folio의 unlock·put은 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
unsigned find_lock_entries(struct address_space *mapping, pgoff_t *start,
pgoff_t end, struct folio_batch *fbatch, pgoff_t *indices)
{
XA_STATE(xas, &mapping->i_pages, *start);
struct folio *folio;
rcu_read_lock();
while ((folio = find_get_entry(&xas, end, XA_PRESENT))) {
unsigned long base;
unsigned long nr;
if (!xa_is_value(folio)) {
nr = folio_nr_pages(folio);
base = folio->index;
/* Omit large folio which begins before the start */
if (base < *start)
goto put;
/* Omit large folio which extends beyond the end */
if (base + nr - 1 > end)
goto put;
if (!folio_trylock(folio))
goto put;
if (folio->mapping != mapping ||
folio_test_writeback(folio))
goto unlock;
VM_BUG_ON_FOLIO(!folio_contains(folio, xas.xa_index),
folio);
} else {
nr = 1 << xas_get_order(&xas);
base = xas.xa_index & ~(nr - 1);
/* Omit order>0 value which begins before the start */
if (base < *start)
continue;
/* Omit order>0 value which extends beyond the end */
if (base + nr - 1 > end)
break;
}
/* Update start now so that last update is correct on return */
*start = base + nr;
indices[fbatch->nr] = xas.xa_index;
if (!folio_batch_add(fbatch, folio))
break;
continue;
unlock:
folio_unlock(folio);
put:
folio_put(folio);
}
rcu_read_unlock();
return folio_batch_count(fbatch);
}
v6.18의 filemap_get_folios()는 filemap_get_folios_tag(..., XA_PRESENT, ...)에 위임합니다. worker는 value를 건너뛰고 batch가 차면 마지막 folio 뒤로 *start를 옮깁니다. 범위 끝까지 찾았다면 end + 1을 쓰되 end == (pgoff_t)-1에서는 같은 최댓값으로 포화시켜 0으로 wrap하지 않게 합니다. 순회 중 추가·제거되는 folio를 모두 포함하는 snapshot을 보장하지는 않습니다.
// 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
unsigned filemap_get_folios(struct address_space *mapping, pgoff_t *start,
pgoff_t end, struct folio_batch *fbatch)
{
return filemap_get_folios_tag(mapping, start, end, XA_PRESENT, fbatch);
}
filemap_get_folios_tag_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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
unsigned filemap_get_folios_tag(struct address_space *mapping, pgoff_t *start,
pgoff_t end, xa_mark_t tag, struct folio_batch *fbatch)
{
XA_STATE(xas, &mapping->i_pages, *start);
struct folio *folio;
rcu_read_lock();
while ((folio = find_get_entry(&xas, end, tag)) != NULL) {
/*
* Shadow entries should never be tagged, but this iteration
* is lockless so there is a window for page reclaim to evict
* a page we saw tagged. Skip over it.
*/
if (xa_is_value(folio))
continue;
if (!folio_batch_add(fbatch, folio)) {
unsigned long nr = folio_nr_pages(folio);
*start = folio->index + nr;
goto out;
}
}
/*
* We come here when there is no page beyond @end. We take care to not
* overflow the index @start as it confuses some of the callers. This
* breaks the iteration when there is a page at index -1 but that is
* already broke anyway.
*/
if (end == (pgoff_t)-1)
*start = (pgoff_t)-1;
else
*start = end + 1;
out:
rcu_read_unlock();
return folio_batch_count(fbatch);
}
read batch는 마지막 folio에서 멈추는 이유가 있습니다
일반 read는 hole을 건너뛰어 뒤의 cached 내용을 먼저 반환할 수 없습니다. 앞부분을 먼저 해결해야 file byte 순서대로 user buffer를 채울 수 있습니다. filemap_get_read_batch()는 연속된 구간을 모으다가 hole이나 exceptional value를 만나면 멈춥니다.
multi-index 표현의 sibling을 예상하지 못한 위치에서 만나는 경우도 있습니다. invalidation과 다른 reader의 large-folio 삽입이 경쟁해 cursor가 이전과 다른 범위 구조를 볼 수 있기 때문입니다. 별도의 folio인 것처럼 tail index를 계속 batch에 넣지 않고 caller 쪽에서 다시 판단할 기회를 줍니다.
실제 folio에는 참조를 얻은 뒤 xas_reload()로 같은 entry인지 확인합니다. 참조 확보 실패나 entry 변경은 재시도 조건입니다. 성공하면 batch에 추가하고 large folio가 대표하는 나머지 index를 건너뛰어 다음 entry로 전진합니다.
추가한 마지막 folio가 not-uptodate이거나 readahead marker를 갖고 있다면 batch를 끝낼 수 있습니다. 이 folio는 버린 대상이 아니라 caller가 다음 행동을 판단할 경계입니다. 앞의 유효한 folio들과, read 완료를 기다리거나 다음 readahead를 촉발할 마지막 folio를 나누어 처리하는 구조입니다.
filemap_get_read_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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static void filemap_get_read_batch(struct address_space *mapping,
pgoff_t index, pgoff_t max, struct folio_batch *fbatch)
{
XA_STATE(xas, &mapping->i_pages, index);
struct folio *folio;
rcu_read_lock();
for (folio = xas_load(&xas); folio; folio = xas_next(&xas)) {
if (xas_retry(&xas, folio))
continue;
if (xas.xa_index > max || xa_is_value(folio))
break;
if (xa_is_sibling(folio))
break;
if (!folio_try_get(folio))
goto retry;
if (unlikely(folio != xas_reload(&xas)))
goto put_folio;
if (!folio_batch_add(fbatch, folio))
break;
if (!folio_test_uptodate(folio))
break;
if (folio_test_readahead(folio))
break;
xas_advance(&xas, folio_next_index(folio) - 1);
continue;
put_folio:
folio_put(folio);
retry:
xas_reset(&xas);
}
rcu_read_unlock();
}
miss 뒤에도 같은 batch를 다시 찾습니다
filemap_get_pages()는 ki_pos와 iterator의 남은 byte 수로 demand index 범위를 정합니다. 현재 구현의 last_index는 exclusive end이고 batch lookup은 그보다 하나 작은 index까지 진행합니다. page-aligned 요청이면 언제나 demand folio를 하나 더 읽는 규칙으로 이해해서는 안 됩니다. speculative readahead가 요청 바깥을 준비하는 것은 별도입니다.
실제 lookup이나 I/O 전에 fatal signal을 확인하면 오래 걸리는 작업을 시작하기 전에 중단할 수 있습니다. batch가 비어 있고 새 I/O가 허용되면 synchronous readahead를 시도한 뒤 다시 cache를 조회합니다. readahead는 cache를 변화시키는 작업이므로 성공 여부를 반환된 folio batch와 다시 연결해야 합니다.
readahead가 memory 압력이나 allocation 조건 때문에 대상을 준비하지 못할 수도 있습니다. GFP_NOFS 같은 제약이 있으면 reclaim이 filesystem에 재진입해 해결하는 경로도 제한됩니다. 그런데 이 단계의 실패는 demand read가 곧바로 포기해야 한다는 뜻은 아닙니다. 마지막으로 필요한 folio 하나를 직접 할당하고 읽는 경로가 남습니다.
다른 reader가 같은 index를 먼저 채워 filemap_add_folio()가 -EEXIST를 반환할 수도 있습니다. 이는 파일 전체가 잘못되었다는 뜻이 아니라, 다시 lookup하면 먼저 들어간 folio를 얻을 수 있는 경쟁입니다. truncation 관련 재시도 결과도 오래된 pointer를 계속 사용하는 대신 처음의 조회로 돌아가 처리합니다.
현재 demand 범위는 mapping의 minimum folio byte 크기에 맞춰 올림합니다. last_index 자체를 inclusive upper bound로 넘기던 오래된 예시와 달리 실제 batch 검색에는 last_index - 1을 전달합니다. page-aligned 요청이므로 무조건 다음 page도 demand batch에 든다는 설명을 current code에 적용하면 안 됩니다.
filemap_create_folio()도 항상 order 0을 할당하지 않습니다. mapping_min_folio_order()를 사용해 allocation order와 삽입 index 정렬을 맞춥니다. IOCB_NOWAIT | IOCB_WAITQ이면 allocation 전에 -EAGAIN이며, IOCB_DONTCACHE는 dropbehind 상태를 설정합니다. 성공 시 batch에 caller의 참조를 넘기고, 실패 시 invalidate lock과 자신의 참조를 정리합니다. -EEXIST를 AOP_TRUNCATED_PAGE로 바꾸는 것은 재검색을 위한 통일된 계약이며 truncate가 실제 발생했다는 증명은 아닙니다.
// 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static int filemap_create_folio(struct kiocb *iocb, struct folio_batch *fbatch)
{
struct address_space *mapping = iocb->ki_filp->f_mapping;
struct folio *folio;
int error;
unsigned int min_order = mapping_min_folio_order(mapping);
pgoff_t index;
if (iocb->ki_flags & (IOCB_NOWAIT | IOCB_WAITQ))
return -EAGAIN;
folio = filemap_alloc_folio(mapping_gfp_mask(mapping), min_order);
if (!folio)
return -ENOMEM;
if (iocb->ki_flags & IOCB_DONTCACHE)
__folio_set_dropbehind(folio);
/*
* Protect against truncate / hole punch. Grabbing invalidate_lock
* here assures we cannot instantiate and bring uptodate new
* pagecache folios after evicting page cache during truncate
* and before actually freeing blocks. Note that we could
* release invalidate_lock after inserting the folio into
* the page cache as the locked folio would then be enough to
* synchronize with hole punching. But there are code paths
* such as filemap_update_page() filling in partially uptodate
* pages or ->readahead() that need to hold invalidate_lock
* while mapping blocks for IO so let's hold the lock here as
* well to keep locking rules simple.
*/
filemap_invalidate_lock_shared(mapping);
index = (iocb->ki_pos >> (PAGE_SHIFT + min_order)) << min_order;
error = filemap_add_folio(mapping, folio, index,
mapping_gfp_constraint(mapping, GFP_KERNEL));
if (error == -EEXIST)
error = AOP_TRUNCATED_PAGE;
if (error)
goto error;
error = filemap_read_folio(iocb->ki_filp, mapping->a_ops->read_folio,
folio);
if (error)
goto error;
filemap_invalidate_unlock_shared(mapping);
folio_batch_add(fbatch, folio);
return 0;
error:
filemap_invalidate_unlock_shared(mapping);
folio_put(folio);
return error;
}
v6.18은 batch에 앞선 유효 folio들이 있으면 마지막 not-uptodate folio를 기다리기 전에 batch를 줄여 앞부분부터 전달할 수도 있습니다. 오류 정리에서 음수 error일 때만 마지막 folio를 put하는 이유도 중요합니다. 양수 제어 결과인 AOP_TRUNCATED_PAGE는 하위 helper가 이미 caller 참조를 소비한 경로가 있으므로 다시 put하면 안 됩니다. 마지막 entry를 뺀 batch가 남으면 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static int filemap_get_pages(struct kiocb *iocb, size_t count,
struct folio_batch *fbatch, bool need_uptodate)
{
struct file *filp = iocb->ki_filp;
struct address_space *mapping = filp->f_mapping;
pgoff_t index = iocb->ki_pos >> PAGE_SHIFT;
pgoff_t last_index;
struct folio *folio;
unsigned int flags;
int err = 0;
/* "last_index" is the index of the folio beyond the end of the read */
last_index = round_up(iocb->ki_pos + count,
mapping_min_folio_nrbytes(mapping)) >> PAGE_SHIFT;
retry:
if (fatal_signal_pending(current))
return -EINTR;
filemap_get_read_batch(mapping, index, last_index - 1, fbatch);
if (!folio_batch_count(fbatch)) {
DEFINE_READAHEAD(ractl, filp, &filp->f_ra, mapping, index);
if (iocb->ki_flags & IOCB_NOIO)
return -EAGAIN;
if (iocb->ki_flags & IOCB_NOWAIT)
flags = memalloc_noio_save();
if (iocb->ki_flags & IOCB_DONTCACHE)
ractl.dropbehind = 1;
page_cache_sync_ra(&ractl, last_index - index);
if (iocb->ki_flags & IOCB_NOWAIT)
memalloc_noio_restore(flags);
filemap_get_read_batch(mapping, index, last_index - 1, fbatch);
}
if (!folio_batch_count(fbatch)) {
err = filemap_create_folio(iocb, fbatch);
if (err == AOP_TRUNCATED_PAGE)
goto retry;
return err;
}
folio = fbatch->folios[folio_batch_count(fbatch) - 1];
if (folio_test_readahead(folio)) {
err = filemap_readahead(iocb, filp, mapping, folio, last_index);
if (err)
goto err;
}
if (!folio_test_uptodate(folio)) {
if (folio_batch_count(fbatch) > 1) {
err = -EAGAIN;
goto err;
}
err = filemap_update_page(iocb, mapping, count, folio,
need_uptodate);
if (err)
goto err;
}
trace_mm_filemap_get_pages(mapping, index, last_index - 1);
return 0;
err:
if (err < 0)
folio_put(folio);
if (likely(--fbatch->nr))
return 0;
if (err == AOP_TRUNCATED_PAGE)
goto retry;
return err;
}
NOWAIT와 NOIO는 같은 금지가 아닙니다
IOCB_NOWAIT는 이 요청에서 필요한 대기를 수행할 수 없다는 문맥입니다. IOCB_NOIO는 새 I/O를 시작하지 말라는 조건입니다. 이미 진행 중인 작업을 기다리는 것, 새 speculative I/O를 시작하는 것과 새 allocation을 수행하는 것은 서로 다른 비용이므로 flag를 동의어로 취급하면 안 됩니다.
특히 NOWAIT 자체가 readahead까지 금지하는 것은 아닙니다. v6.18의 cache miss 경로는 NOWAIT이면 allocation의 no-I/O scope를 적용한 상태로 readahead를 시도할 수 있습니다. NOIO라면 빈 batch에서 새 I/O를 시작하지 않고 -EAGAIN으로 돌아갑니다. readahead marker를 처리할 때도 이 금지 조건을 지켜야 하므로, 이미 준비한 앞부분만 반환하는 partial read와 아무 byte도 읽지 못한 결과를 구별해야 합니다. 현재 flag 처리 전체
예를 들어 folio lock을 바로 얻을 수 없고 blocking이 허용되지 않으면 -EAGAIN이 가능해집니다. 새 I/O 없이는 내용을 준비할 수 없는 경우에도 no-I/O 조건에 맞게 빠져나와야 합니다. 이런 결과를 system 전체의 OOM이나 영구 I/O 오류로 바꾸어 해석해서는 안 됩니다.
앞의 folio들을 이미 확보한 상태에서 마지막 folio의 준비만 실패했다면, 준비된 앞부분으로 read를 진행할 수도 있습니다. 마지막 entry의 참조를 놓고 batch를 줄인 뒤 이미 확보한 byte를 전달하는 것입니다. 아무 대상도 없을 때의 재시도·오류 처리와 batch의 일부를 사용할 수 있을 때의 처리는 다릅니다.
folio 전체가 아니어도 필요한 byte는 유효할 수 있습니다
filemap_update_page()는 invalidate lock과 folio lock을 조합해 마지막 folio의 내용을 준비합니다. 일반 blocking 경로에서 lock을 얻지 못하면 invalidate lock을 놓고 folio_put_wait_locked()로 참조를 내려놓은 채 기다릴 수 있습니다. 이 방식으로 돌아온 뒤에는 옛 pointer로 계속 처리하지 않고 lookup부터 다시 해야 합니다.
lock을 얻은 뒤에도 mapping이 끊겼다면 truncation을 고려한 재시도로 돌아갑니다. 아직 같은 folio라면 filemap_range_uptodate()로 실제 요청 범위의 유효성을 검사합니다. 모든 caller가 이 완화를 사용할 수 있는 것은 아닙니다.
전체 folio를 요구하지 않고 filesystem에 is_partially_uptodate callback이 있으며, block 크기와 folio 크기 조건이 맞을 때 일부 byte 범위가 이미 유효한지 물을 수 있습니다. 예를 들어 한 folio 안에 여러 block이 있고 요청한 block만 준비되어 있다면 그 부분 read는 진행할 수 있습니다. 반면 folio 전체를 요구하거나 요청 자체가 folio 전체를 덮으면 이 partial shortcut을 그대로 적용하지 않습니다.
필요한 내용이 아직 없다면 locked folio에 read callback을 넘기고 완료를 확인합니다. 조회, 소속 재검증, 내용 준비와 앞부분 반환을 분리한 덕분에 batch는 하나의 거대한 성공·실패 단위가 되지 않습니다. API 이름보다 무엇을 건너뛰고 어떤 상태를 반환하는지 읽는 편이 caller의 책임을 훨씬 정확하게 보여 줍니다.
filemap_range_uptodate()의 current 입력은 iterator 자체 대신 count와 need_uptodate입니다. folio의 시작과 요청 position의 차이로 상대 offset·길이를 조정한 뒤 filesystem callback에 필요한 범위만 묻습니다. need_uptodate는 pipe처럼 partial 상태를 받을 수 없는 caller를 위한 조건입니다.
filemap_range_uptodate_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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static bool filemap_range_uptodate(struct address_space *mapping,
loff_t pos, size_t count, struct folio *folio,
bool need_uptodate)
{
if (folio_test_uptodate(folio))
return true;
/* pipes can't handle partially uptodate pages */
if (need_uptodate)
return false;
if (!mapping->a_ops->is_partially_uptodate)
return false;
if (mapping->host->i_blkbits >= folio_shift(folio))
return false;
if (folio_pos(folio) > pos) {
count -= folio_pos(folio) - pos;
pos = 0;
} else {
pos -= folio_pos(folio);
}
if (pos == 0 && count >= folio_size(folio))
return false;
return mapping->a_ops->is_partially_uptodate(folio, pos, count);
}
filemap_update_page()는 NOWAIT이면 invalidate lock도 trylock합니다. folio trylock 실패에서 NOWAIT/NOIO는 -EAGAIN, WAITQ는 async lock 등록 경로, 일반 blocking caller는 invalidate lock을 놓고 참조를 소비하며 기다린 뒤 AOP_TRUNCATED_PAGE를 반환합니다. lock을 얻어도 새 I/O가 필요한데 NOIO/NOWAIT/WAITQ가 있다면 read callback으로 내려가지 않습니다. 따라서 lock 획득 가능성, partial 유효성, 새 I/O 허용을 순서대로 읽어야 합니다.
// 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
* File-cache code: Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static int filemap_update_page(struct kiocb *iocb,
struct address_space *mapping, size_t count,
struct folio *folio, bool need_uptodate)
{
int error;
if (iocb->ki_flags & IOCB_NOWAIT) {
if (!filemap_invalidate_trylock_shared(mapping))
return -EAGAIN;
} else {
filemap_invalidate_lock_shared(mapping);
}
if (!folio_trylock(folio)) {
error = -EAGAIN;
if (iocb->ki_flags & (IOCB_NOWAIT | IOCB_NOIO))
goto unlock_mapping;
if (!(iocb->ki_flags & IOCB_WAITQ)) {
filemap_invalidate_unlock_shared(mapping);
/*
* This is where we usually end up waiting for a
* previously submitted readahead to finish.
*/
folio_put_wait_locked(folio, TASK_KILLABLE);
return AOP_TRUNCATED_PAGE;
}
error = __folio_lock_async(folio, iocb->ki_waitq);
if (error)
goto unlock_mapping;
}
error = AOP_TRUNCATED_PAGE;
if (!folio->mapping)
goto unlock;
error = 0;
if (filemap_range_uptodate(mapping, iocb->ki_pos, count, folio,
need_uptodate))
goto unlock;
error = -EAGAIN;
if (iocb->ki_flags & (IOCB_NOIO | IOCB_NOWAIT | IOCB_WAITQ))
goto unlock;
error = filemap_read_folio(iocb->ki_filp, mapping->a_ops->read_folio,
folio);
goto unlock_mapping;
unlock:
folio_unlock(folio);
unlock_mapping:
filemap_invalidate_unlock_shared(mapping);
if (error == AOP_TRUNCATED_PAGE)
folio_put(folio);
return error;
}
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.5 도입부와 §9.5.1~9.5.2.
- Linux v6.18: batch lookup, filemap_get_pages, filemap_update_page, folio와 FGP 정의.