readahead policy가 다음 32 page를 읽겠다고 정해도 그만큼의 BIO가 바로 생기지는 않습니다. 이미 cache에 있는 구간을 피하고, 필요한 folio를 먼저 준비하고, filesystem이 실제 storage mapping과 I/O를 구성해야 합니다. 이 과정에서는 pointer를 반환하는 helper가 새 참조를 주는지부터 정확히 알아야 합니다.
이 글의 함수와 소유권 규칙은 upstream Linux v6.18을 기준으로 합니다. 특히 readahead_folio()를 일반적인 get 함수처럼 읽으면 참조를 한 번 더 내려놓는 실수를 할 수 있습니다.
EOF를 적용하는 층과 unchecked 층이 있습니다
일반적인 do_page_cache_ra()는 현재 i_size를 읽습니다. 길이가 0이거나 시작 index가 마지막 file page를 벗어나면 더 진행할 필요가 없습니다. 요청 page 수는 마지막 byte를 포함한 page까지만 준비하도록 제한합니다.
그 아래 page_cache_ra_unbounded()는 이름처럼 그 자체가 일반적인 EOF 제한을 모두 책임지는 인터페이스가 아닙니다. filesystem이 특별한 문맥에서 직접 부를 수 있지만, 평범한 caller가 단지 더 빨리 읽기 위해 선택하는 대체 함수는 아닙니다. 상위 wrapper가 적용한 범위 검사를 잃어버릴 수 있기 때문입니다.
large folio를 사용하는 경로는 mapping의 최소·최대 order, 정렬과 allocation 결과에 맞춰 준비합니다. 이전 설명에서 order-0 fallback을 중심으로 읽었다고 현재 모든 readahead folio가 order-0이라고 가정할 수는 없습니다. policy window의 page 수와 실제 folio 객체 수가 여기서 달라집니다.
아래는 v6.18 do_page_cache_ra()의 전체 정의입니다. EOF를 포함한 마지막 page index는 (i_size - 1) >> PAGE_SHIFT이며, 0-byte file 검사를 먼저 수행합니다. 이 글의 include들은 source와 license를 보존한 kernel 함수 전체이며 standalone user program은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static void do_page_cache_ra(struct readahead_control *ractl,
unsigned long nr_to_read, unsigned long lookahead_size)
{
struct inode *inode = ractl->mapping->host;
unsigned long index = readahead_index(ractl);
loff_t isize = i_size_read(inode);
pgoff_t end_index; /* The last page we want to read */
if (isize == 0)
return;
end_index = (isize - 1) >> PAGE_SHIFT;
if (index > end_index)
return;
/* Don't read past the page containing the last byte of the file */
if (nr_to_read > end_index - index)
nr_to_read = end_index - index + 1;
page_cache_ra_unbounded(ractl, nr_to_read, lookahead_size);
}
큰 folio는 범위 끝과 정렬을 함께 맞춥니다
현재 page_cache_ra_order()는 preferred order를 별도 인자로 받지 않고 ra->order에서 가져옵니다. mapping의 maximum, window 크기의 ilog2, minimum order로 조정한 뒤 index alignment와 파일 끝을 다시 맞춥니다. order > min_order인 동안 끝을 넘는 order를 낮추므로 mapping의 minimum granularity와 byte 단위 EOF는 구별해야 합니다. 큰 folio 지원이 없으면 order를 0으로 돌리고 일반 경로로 fallback합니다.
할당한 앞부분은 오류가 생겨도 read_pages()에 먼저 넘깁니다. fallback에서는 이미 진행한 index - start를 window에서 빼고 남은 양만 준비합니다. filesystem callback이 window 크기를 바꿨을 수도 있어 ra->size가 진행량보다 큰지도 다시 검사합니다. 모든 실패가 전체 window rollback이나 OOM이라는 뜻은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
void page_cache_ra_order(struct readahead_control *ractl,
struct file_ra_state *ra)
{
struct address_space *mapping = ractl->mapping;
pgoff_t start = readahead_index(ractl);
pgoff_t index = start;
unsigned int min_order = mapping_min_folio_order(mapping);
pgoff_t limit = (i_size_read(mapping->host) - 1) >> PAGE_SHIFT;
pgoff_t mark = index + ra->size - ra->async_size;
unsigned int nofs;
int err = 0;
gfp_t gfp = readahead_gfp_mask(mapping);
unsigned int new_order = ra->order;
trace_page_cache_ra_order(mapping->host, start, ra);
if (!mapping_large_folio_support(mapping)) {
ra->order = 0;
goto fallback;
}
limit = min(limit, index + ra->size - 1);
new_order = min(mapping_max_folio_order(mapping), new_order);
new_order = min_t(unsigned int, new_order, ilog2(ra->size));
new_order = max(new_order, min_order);
ra->order = new_order;
/* See comment in page_cache_ra_unbounded() */
nofs = memalloc_nofs_save();
filemap_invalidate_lock_shared(mapping);
/*
* If the new_order is greater than min_order and index is
* already aligned to new_order, then this will be noop as index
* aligned to new_order should also be aligned to min_order.
*/
ractl->_index = mapping_align_index(mapping, index);
index = readahead_index(ractl);
while (index <= limit) {
unsigned int order = new_order;
/* Align with smaller pages if needed */
if (index & ((1UL << order) - 1))
order = __ffs(index);
/* Don't allocate pages past EOF */
while (order > min_order && index + (1UL << order) - 1 > limit)
order--;
err = ra_alloc_folio(ractl, index, mark, order, gfp);
if (err)
break;
index += 1UL << order;
}
read_pages(ractl);
filemap_invalidate_unlock_shared(mapping);
memalloc_nofs_restore(nofs);
/*
* If there were already pages in the page cache, then we may have
* left some gaps. Let the regular readahead code take care of this
* situation below.
*/
if (!err)
return;
fallback:
/*
* ->readahead() may have updated readahead window size so we have to
* check there's still something to read.
*/
if (ra->size > index - start)
do_page_cache_ra(ractl, ra->size - (index - start),
ra->async_size);
}
ra_alloc_folio()는 marker를 실제 folio order에 맞춰 내림 정렬하고, 삽입 성공 뒤에만 control의 page count와 workingset 상태를 늘립니다. allocation 실패는 -ENOMEM, cache 삽입 실패는 해당 error를 돌려주며 새 allocation 참조를 놓습니다. ractl_alloc_folio()는 dropbehind hint를 새 folio로 옮기는 단계입니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static struct folio *ractl_alloc_folio(struct readahead_control *ractl,
gfp_t gfp_mask, unsigned int order)
{
struct folio *folio;
folio = filemap_alloc_folio(gfp_mask, order);
if (folio && ractl->dropbehind)
__folio_set_dropbehind(folio);
return folio;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static inline int ra_alloc_folio(struct readahead_control *ractl, pgoff_t index,
pgoff_t mark, unsigned int order, gfp_t gfp)
{
int err;
struct folio *folio = ractl_alloc_folio(ractl, gfp, order);
if (!folio)
return -ENOMEM;
mark = round_down(mark, 1UL << order);
if (index == mark)
folio_set_readahead(folio);
err = filemap_add_folio(ractl->mapping, folio, index, gfp);
if (err) {
folio_put(folio);
return err;
}
ractl->_nr_pages += 1UL << order;
ractl->_workingset |= folio_test_workingset(folio);
return 0;
}
먼저 준비한 locked folio가 reclaim의 대상을 막을 수 있습니다
readahead는 folio들을 cache에 넣고 아직 I/O를 제출하지 않은 상태를 잠시 만들 수 있습니다. 다음 folio를 할당하다 reclaim에 들어가면, reclaim이 같은 filesystem의 writeback이나 lock을 기다리며 다시 들어오는 경쟁이 생길 수 있습니다.
memalloc_nofs_save()로 NOFS allocation scope를 설정하는 것은 이 재진입 문제를 제한하려는 장치입니다. mapping에 맞는 GFP mask도 사용하고, shared invalidate lock으로 cache를 준비하는 동안 invalidation과 storage mapping 변경의 필요한 직렬화를 유지합니다. 마친 뒤에는 lock과 NOFS scope를 모두 원래 상태로 복구해야 합니다.
이는 모든 memory 압력을 없애는 해결책이 아닙니다. allocation이 실패할 수 있고 다른 reader가 먼저 같은 index에 folio를 넣을 수도 있습니다. speculative 작업은 여기서 적당히 중단하고 demand 경로가 나중에 필요한 내용을 직접 읽게 할 수 있습니다. 준비와 제출 경로
이미 있는 구간을 만나면 앞의 연속 batch를 제출합니다
page_cache_ra_unbounded()는 XArray에서 현재 구간이 이미 resident인지 살핍니다. 기존 folio가 있으면 준비 중이던 연속 batch의 I/O를 먼저 시작하고 그 구간을 지나갈 수 있습니다. 이미 있는 folio를 지금의 새 allocation과 같은 방식으로 다시 소유하려고 하지 않습니다.
missing 구간에는 folio를 할당하고 filemap_add_folio()로 cache에 넣습니다. 성공한 folio는 locked 상태라 filesystem이 내용 준비를 맡을 수 있습니다. 실패한 allocation의 참조는 정리해야 하며, -ENOMEM 같은 자원 실패와 다른 reader가 먼저 넣은 경쟁은 다음 행동이 같지 않을 수 있습니다.
새 folio가 marker 위치에 해당하면 PG_readahead를 설정합니다. 현재 구현은 mapping의 최소 folio 크기에 맞춰 marker와 시작 index를 보정합니다. 이미 있는 cached folio에 안정적인 참조도 없이 marker를 붙이려고 별도의 비싼 탐색을 덧붙이지는 않습니다.
마지막에는 남은 batch도 read_pages()로 넘깁니다. speculative read의 I/O 오류는 그 자리에서 demand read의 오류처럼 모두 보고하지 않을 수 있습니다. 나중에 demand caller가 not-uptodate 상태를 확인하고 다시 읽으며 오류를 처리할 수 있기 때문입니다.
current unchecked 경로는 mapping의 minimum folio 크기로 시작을 정렬하고 그 차이만큼 요청량을 보정합니다. marker는 index + nr_to_read - lookahead_size를 minimum page 수에 맞춰 올림한 위치로 정합니다. lookahead_size > nr_to_read이면 marker 계산을 하지 않습니다. allocation이 실패하거나 삽입이 -ENOMEM이면 중단하지만, 다른 삽입 충돌이면 준비된 batch를 제출하고 그 구간을 지나갑니다. 이미 존재하는 folio에 I/O를 새로 제출한다는 뜻이 아니라, 그 앞에서 자신이 준비해 둔 folio들을 제출하는 것입니다.
page_cache_ra_unbounded_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
void page_cache_ra_unbounded(struct readahead_control *ractl,
unsigned long nr_to_read, unsigned long lookahead_size)
{
struct address_space *mapping = ractl->mapping;
unsigned long index = readahead_index(ractl);
gfp_t gfp_mask = readahead_gfp_mask(mapping);
unsigned long mark = ULONG_MAX, i = 0;
unsigned int min_nrpages = mapping_min_folio_nrpages(mapping);
/*
* Partway through the readahead operation, we will have added
* locked pages to the page cache, but will not yet have submitted
* them for I/O. Adding another page may need to allocate memory,
* which can trigger memory reclaim. Telling the VM we're in
* the middle of a filesystem operation will cause it to not
* touch file-backed pages, preventing a deadlock. Most (all?)
* filesystems already specify __GFP_NOFS in their mapping's
* gfp_mask, but let's be explicit here.
*/
unsigned int nofs = memalloc_nofs_save();
trace_page_cache_ra_unbounded(mapping->host, index, nr_to_read,
lookahead_size);
filemap_invalidate_lock_shared(mapping);
index = mapping_align_index(mapping, index);
/*
* As iterator `i` is aligned to min_nrpages, round_up the
* difference between nr_to_read and lookahead_size to mark the
* index that only has lookahead or "async_region" to set the
* readahead flag.
*/
if (lookahead_size <= nr_to_read) {
unsigned long ra_folio_index;
ra_folio_index = round_up(readahead_index(ractl) +
nr_to_read - lookahead_size,
min_nrpages);
mark = ra_folio_index - index;
}
nr_to_read += readahead_index(ractl) - index;
ractl->_index = index;
/*
* Preallocate as many pages as we will need.
*/
while (i < nr_to_read) {
struct folio *folio = xa_load(&mapping->i_pages, index + i);
int ret;
if (folio && !xa_is_value(folio)) {
/*
* Page already present? Kick off the current batch
* of contiguous pages before continuing with the
* next batch. This page may be the one we would
* have intended to mark as Readahead, but we don't
* have a stable reference to this page, and it's
* not worth getting one just for that.
*/
read_pages(ractl);
ractl->_index += min_nrpages;
i = ractl->_index + ractl->_nr_pages - index;
continue;
}
folio = ractl_alloc_folio(ractl, gfp_mask,
mapping_min_folio_order(mapping));
if (!folio)
break;
ret = filemap_add_folio(mapping, folio, index + i, gfp_mask);
if (ret < 0) {
folio_put(folio);
if (ret == -ENOMEM)
break;
read_pages(ractl);
ractl->_index += min_nrpages;
i = ractl->_index + ractl->_nr_pages - index;
continue;
}
if (i == mark)
folio_set_readahead(folio);
ractl->_workingset |= folio_test_workingset(folio);
ractl->_nr_pages += min_nrpages;
i += min_nrpages;
}
/*
* Now start the IO. We ignore I/O errors - if the folio is not
* uptodate then the caller will launch read_folio again, and
* will then handle the error.
*/
read_pages(ractl);
filemap_invalidate_unlock_shared(mapping);
memalloc_nofs_restore(nofs);
}
read_pages는 callback을 고르고 남은 folio도 정리합니다
read_pages()는 readahead count가 0이면 바로 끝납니다. 대상이 있으면 filesystem의 a_ops->readahead를 사용하거나, 그 callback이 없을 때 각 folio에 read_folio를 호출하는 fallback을 사용합니다. storage extent나 network 요청으로 바꾸는 일은 filesystem이 맡습니다.
filesystem의 readahead callback은 전달된 범위를 끝까지 소비하지 못할 수 있습니다. generic code는 남은 folio들을 순회하며 cache에서 제거하고 unlock과 put으로 정리합니다. 아직 내용 준비를 시작하지 않은 locked folio를 cache에 영구히 남겨 놓아 다음 reader를 막아서는 안 됩니다.
이 cleanup은 단순히 pointer를 버리는 작업이 아닙니다. cache 참조를 제거한 뒤에도 unlock을 마칠 때까지 객체가 살아 있어야 합니다. 현재 코드는 임시 folio_get()을 얻은 뒤 filemap_remove_folio(), unlock, put을 순서대로 수행합니다. 왜 임시 참조가 필요한지는 바로 다음 helper의 반환 계약과 연결됩니다.
과거 예시에는 남은 folio만큼 ra->size·async_size를 줄이고 tail에 해당할 때만 cache에서 제거하는 분기가 있습니다. v6.18의 아래 함수는 남은 folio를 모두 제거하며 그 field를 여기서 같은 방식으로 줄이지 않습니다. block plug의 시작·종료, PSI memstall의 진입·복구와 최종 count 검사도 포함한 전체 정의로 확인할 수 있습니다. block plug는 제출 batching이지 folio 내용의 completion lock이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 mm/readahead.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/readahead.c
* Copyright (C) 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
*/
static void read_pages(struct readahead_control *rac)
{
const struct address_space_operations *aops = rac->mapping->a_ops;
struct folio *folio;
struct blk_plug plug;
if (!readahead_count(rac))
return;
if (unlikely(rac->_workingset))
psi_memstall_enter(&rac->_pflags);
blk_start_plug(&plug);
if (aops->readahead) {
aops->readahead(rac);
/* Clean up the remaining folios. */
while ((folio = readahead_folio(rac)) != NULL) {
folio_get(folio);
filemap_remove_folio(folio);
folio_unlock(folio);
folio_put(folio);
}
} else {
while ((folio = readahead_folio(rac)) != NULL)
aops->read_folio(rac->file, folio);
}
blk_finish_plug(&plug);
if (unlikely(rac->_workingset))
psi_memstall_leave(&rac->_pflags);
rac->_workingset = false;
BUG_ON(readahead_count(rac));
}
readahead_folio는 추가 소유 참조를 주지 않습니다
다음은 v6.18 pagemap.h의 함수 전체입니다. 아래 helper가 다음 locked folio를 찾으면 allocation 참조를 내부에서 내려놓고 pointer를 반환합니다. caller가 별도 참조를 하나 받았다고 생각해 무조건 한 번 더 put하면 안 됩니다.
// SPDX-License-Identifier: GPL-2.0
/* Copyright 1995 Linus Torvalds */
/*
* Complete definition from Linux v6.18 include/linux/pagemap.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Linux kernel contributors
* Kernel excerpt; not a standalone user-space program.
*/
static inline struct folio *readahead_folio(struct readahead_control *ractl)
{
struct folio *folio = __readahead_folio(ractl);
if (folio)
folio_put(folio);
return folio;
}
cache가 유지하는 참조와 locked 상태에 기대는 수명, filesystem이나 BIO가 별도로 보유하는 참조는 구별해야 합니다. filesystem은 해당 folio의 I/O가 끝나면 unlock해야 합니다. 비동기 작업에 필요한 다른 객체의 수명은 그 callback의 계약에 맞춰 별도로 유지합니다.
내부 iterator는 직전 folio가 대표한 base page 수만큼 index를 전진시킵니다. 다음은 같은 header에서 확인한 함수 전체입니다. BUG_ON은 잔여 count보다 큰 batch count가 생기는 잘못된 상태를 검사하고, VM_BUG_ON_FOLIO는 반환하려는 folio가 locked인지 검증합니다.
// SPDX-License-Identifier: GPL-2.0
/* Copyright 1995 Linus Torvalds */
/*
* Complete definition from Linux v6.18 include/linux/pagemap.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Linux kernel contributors
* Kernel excerpt; not a standalone user-space program.
*/
static inline struct folio *__readahead_folio(struct readahead_control *ractl)
{
struct folio *folio;
BUG_ON(ractl->_batch_count > ractl->_nr_pages);
ractl->_nr_pages -= ractl->_batch_count;
ractl->_index += ractl->_batch_count;
if (!ractl->_nr_pages) {
ractl->_batch_count = 0;
return NULL;
}
folio = xa_load(&ractl->mapping->i_pages, ractl->_index);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
ractl->_batch_count = folio_nr_pages(folio);
return folio;
}
_batch_count가 이전 folio의 크기인 이유는 다음 호출에서 그만큼을 지나가야 하기 때문입니다. order-2 folio를 하나 받았다면 다음 호출은 네 base page 뒤를 찾습니다. 남은 count가 0이면 _batch_count를 정리하고 NULL로 종료합니다. xa_load() 자체가 새 caller 참조를 만드는 함수라는 뜻은 아닙니다.
제출 완료와 내용 준비 완료를 나눕니다
readahead callback은 void일 수 있고 여러 I/O를 queue한 뒤 돌아올 수 있습니다. callback이 반환했다고 모든 folio가 uptodate이라는 뜻은 아닙니다. 실제 completion은 성공 상태를 표시하고 folio를 unlock해 기다리는 demand caller를 깨웁니다.
따라서 demand read나 fault는 readahead가 앞에서 실행되었다는 이유만으로 내용 검사를 생략하지 않습니다. lock 대기와 uptodate 확인이 남습니다. speculative 단계는 미래의 비용을 줄일 기회를 제공하고, 최종 caller가 현재 필요한 내용을 사용할 수 있는지 확정합니다.
readahead의 결과를 세려면 window의 page 수, 실제 allocation 수, callback이 소비한 folio 수와 I/O 완료를 따로 보아야 합니다. 소유권도 마찬가지입니다. pointer가 전달되는 모양보다 어느 참조가 이미 내려갔고 cache에서 제거될 때 무엇이 남는지를 따라가야 비동기 I/O의 정리 경로를 정확히 이해할 수 있습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.7.4~9.7.5.
- Linux v6.18: readahead 준비와 read_pages, readahead_folio와 iterator, folio 삽입과 제거.