파일을 앞에서부터 읽을 때 다음 read가 올 때마다 disk를 기다리면 application의 계산과 I/O를 겹치기 어렵습니다. Linux는 지금 필요한 범위보다 조금 더 읽고, application이 그 범위를 소비하는 도중 다음 window의 I/O를 시작하려 합니다. readahead는 이 겹침을 만들기 위한 예측이며 사용자의 접근 순서를 증명하는 장치는 아닙니다.
어떤 범위를 예상할지와 실제로 folio를 할당해 I/O를 제출하는 일도 나뉩니다. 이 글은 전자의 정책과 상태를 upstream Linux v6.18에 맞춰 설명합니다. 이전 구현의 ondemand_readahead() 하나에 모였던 분기를 현재의 필수 호출 stack처럼 사용하지 않습니다.
열린 file의 예상과 mapping의 데이터는 따로 남습니다
struct file_ra_state는 start, size, async_size로 직전 readahead window를 표현합니다. 단위는 base page입니다. size는 파일을 연 뒤 누적해서 읽은 page 수가 아니라 최근 시도한 window의 크기입니다. async_size는 그 window를 얼마나 남겨 둔 시점에 다음 readahead를 시작할지 나타냅니다.
ra_pages는 기본적인 window 상한이며 BDI 설정에서 초기화됩니다. 큰 demand read에서는 bdi->io_pages까지 고려할 수 있으므로 모든 read의 절대 I/O byte 상한으로 해석해서는 안 됩니다. current prev_pos는 마지막 read가 끝난 exclusive byte 위치이며 마지막으로 포함한 byte 자체가 아닙니다. mmap_miss는 mmap 접근에서 readaround가 유용한지 판단하는 heuristic 상태입니다.
이 상태는 열린 file 쪽에 있고 cached data는 mapping 쪽에 있습니다. 같은 파일을 별도로 연 두 file은 각각 다른 예상 상태로 같은 cached folio를 만날 수 있습니다. 반대로 같은 open file description을 공유한 stream들은 예상 상태를 서로 흔들 수 있습니다.
한 번의 작업에는 struct readahead_control이 사용됩니다. file은 관련 열린 file, mapping은 대상 address_space, ra는 정책 상태를 가리킵니다. 내부 _index, _nr_pages, _batch_count는 이번 전달 범위와 최근 얻은 folio의 base-page 수를 추적합니다. byte 위치와 길이가 필요하면 readahead_pos()와 readahead_length(), page 단위면 readahead_index()와 readahead_count()를 사용합니다.
DEFINE_READAHEAD()는 이 문맥을 초기화합니다. index가 하나씩 움직일 것이라고 직접 가정하면 large folio의 subpage 수를 놓칠 수 있습니다. 정책의 window 단위와 filesystem callback이 가져가는 folio 객체의 단위는 다릅니다.
file과 ra가 없는 filesystem 내부 요청도 있으므로 모든 control이 user fd에 연결된다고 가정하면 안 됩니다. initializer에 없는 count와 batch field는 0으로 시작합니다. v6.18 control에는 dropbehind, PSI 관련 _workingset·_pflags도 있고, file_ra_state에는 preferred folio order가 추가되어 있습니다. 이 private 상태를 accessor 대신 직접 수정하는 것이 일반 caller의 API는 아닙니다.
다음은 현재 control과 initializer, file의 정책 상태의 전체 선언입니다. order와 mmap_miss의 현재 type도 직접 구별할 수 있습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright 1995 Linus Torvalds */
/* Linux v6.18 include/linux/pagemap.h; complete struct and initializer.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/pagemap.h
* Kernel header excerpt; not a standalone user-space program.
*/
struct readahead_control {
struct file *file;
struct address_space *mapping;
struct file_ra_state *ra;
/* private: use the readahead_* accessors instead */
pgoff_t _index;
unsigned int _nr_pages;
unsigned int _batch_count;
bool dropbehind;
bool _workingset;
unsigned long _pflags;
};
#define DEFINE_READAHEAD(ractl, f, r, m, i) \
struct readahead_control ractl = { \
.file = f, \
.mapping = m, \
.ra = r, \
._index = i, \
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v6.18 include/linux/fs.h; complete struct definition.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/fs.h
* Kernel header excerpt; not a standalone user-space program.
*/
struct file_ra_state {
pgoff_t start;
unsigned int size;
unsigned int async_size;
unsigned int ra_pages;
unsigned short order;
unsigned short mmap_miss;
loff_t prev_pos;
};
file_ra_state_init()는 caller가 구조체를 먼저 zero-initialize했다는 전제에서 BDI의 ra_pages를 복사하고 prev_pos를 -1로 둡니다. VM_READAHEAD_PAGES는 128 KiB를 PAGE_SIZE로 나눈 값이므로 base page가 4 KiB일 때 32 page인 것이지 모든 build에서 32라는 뜻은 아닙니다. 아래는 v6.18 초기화 함수 전체입니다. 이 글의 include 파일들은 pinned source 전체 정의와 license를 보존한 kernel 발췌이며 standalone 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.
*/
void
file_ra_state_init(struct file_ra_state *ra, struct address_space *mapping)
{
ra->ra_pages = inode_to_bdi(mapping->host)->ra_pages;
ra->prev_pos = -1;
}
hint는 추측을 바꾸지만 현재 요청을 취소하지 않습니다
madvise()의 MADV_RANDOM과 MADV_SEQUENTIAL은 VMA의 접근 정책입니다. posix_fadvise()의 POSIX_FADV_RANDOM과 POSIX_FADV_SEQUENTIAL은 열린 file의 readahead 정책에 영향을 줍니다. 같은 mapping의 contents를 process별 cache로 나누는 동작은 아닙니다.
일반적인 non-executable VMA의 random 정책에서 mmap readahead를 생략하더라도 실제 fault에 필요한 folio read까지 없어지는 것은 아닙니다. read() 역시 요청한 user buffer를 채워야 합니다. file의 random hint 때문에 큰 read를 page 하나씩 잘라 읽기만 하면 오히려 비효율적이므로 demand 범위를 forced 경로로 처리할 수 있습니다. THP와 executable VMA의 current 예외는 아래 mmap 분기에서 따로 설명합니다.
MADV_RANDOM은 VM_RAND_READ, MADV_SEQUENTIAL은 VM_SEQ_READ 쪽 정책과 연결됩니다. MADV_NORMAL은 두 특별 hint를 해제하는 기본 모드입니다. file 쪽 POSIX_FADV_RANDOM은 FMODE_RANDOM을 사용하지만 sequential을 뜻하는 대칭적인 FMODE_SEQUENTIAL bit가 있는 것은 아닙니다. Linux의 POSIX_FADV_SEQUENTIAL은 BDI 기본 window의 두 배를 사용하고, POSIX_FADV_NORMAL은 기본값으로 되돌립니다. 실제 API 이름은 POSIX_FADV_SEQUENTIAL이며 POSIX_FADV_SEQ가 아닙니다. 별도로 open한 file까지 이 설정을 공유하는 것도 아닙니다. hint와 열린 file의 범위
이 설명은 일반적인 buffered-file 경로에 대한 것입니다. current generic_fadvise()는 DAX inode나 noop_backing_dev_info에서는 유효한 advice를 받아도 적용하지 않고 0을 반환할 수 있습니다. 0 반환을 정책 변경이나 I/O 발생의 증거로 해석하면 안 됩니다. VMA의 normal/random/sequential 변환과 file mode 갱신은 각각 madvise.c, fadvise.c에서 확인할 수 있습니다.
VMA의 random과 sequential hint는 반대쪽 flag를 해제하면서 새 flag를 설정합니다. file 쪽 sequential도 FMODE_RANDOM을 내리고, normal은 random과 FMODE_NOREUSE를 함께 해제합니다. 이 file mode 변경은 file->f_lock으로 보호합니다. 이미 켜 둔 random hint에 단순히 window 크기만 더하는 동작이 아닙니다.
BDI의 read_ahead_kb와 block-device ioctl이 쓰는 단위도 구분해야 합니다. BLKRAGET은 ra_pages * PAGE_SIZE / 512를 돌려주며 BLKRASET은 반대 방향으로 환산합니다. set 경로에는 CAP_SYS_ADMIN 검사도 있습니다. 예를 들어 512라는 sector count는 256 KiB이지 512 KiB가 아닙니다. BDI의 read_ahead_kb는 KiB 입력을 base-page 수로 변환하므로 page보다 작은 나머지까지 보존하는 byte 단위 설정은 아닙니다. 이 값은 정책 입력이지 장치 throughput이나 실제 BIO 크기 그 자체가 아닙니다. ioctl 구현, BDI attribute 구현
등록된 BDI의 attribute는 /sys/class/bdi/<bdi-name>/read_ahead_kb에서 찾을 수 있습니다. 모든 partition이 /sys/block 바로 아래 독립 disk처럼 놓인다고 가정해 경로를 만들면 안 됩니다. BDI와 disk의 symlink·device 이름을 먼저 구별해야 하며, ioctl을 사용하는 blockdev --getra의 sector 단위와도 다릅니다. 여기서는 경로와 환산 관계만 설명하며 설정을 바꾸지 않습니다.
readahead() syscall도 별도의 영구적인 cache를 만들지 않습니다. 지정 범위를 POSIX_FADV_WILLNEED 성격으로 요청하는 경로로 이어집니다. 해당 내용이 나중에 반드시 cache에 남아 있다는 보장까지 제공하는 것은 아닙니다.
current syscall helper는 fd·읽기 mode 오류에 -EBADF, 지원할 mapping이나 regular/block file 조건을 만족하지 않으면 -EINVAL을 반환합니다. 정상 경로가 vfs_fadvise(..., POSIX_FADV_WILLNEED)로 이어지는 전체 정의는 다음과 같습니다. 이 code를 설명하기 위해 syscall이나 설정 변경을 실행하지는 않았습니다.
// 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.
*/
ssize_t ksys_readahead(int fd, loff_t offset, size_t count)
{
struct file *file;
const struct inode *inode;
CLASS(fd, f)(fd);
if (fd_empty(f))
return -EBADF;
file = fd_file(f);
if (!(file->f_mode & FMODE_READ))
return -EBADF;
/*
* The readahead() syscall is intended to run only on files
* that can execute readahead. If readahead is not possible
* on this file, then we must return -EINVAL.
*/
if (!file->f_mapping)
return -EINVAL;
if (!file->f_mapping->a_ops)
return -EINVAL;
inode = file_inode(file);
if (!S_ISREG(inode->i_mode) && !S_ISBLK(inode->i_mode))
return -EINVAL;
if (IS_ANON_FILE(inode))
return -EINVAL;
return vfs_fadvise(fd_file(f), offset, count, POSIX_FADV_WILLNEED);
}
다음은 구현코드가 아닌 호출관계입니다. buffered read의 miss, mmap의 miss·marker hit, 명시적인 WILLNEED 요청은 서로 다른 입구입니다. 일반 policy 뒤의 큰 folio 경로와 forced 경로도 같은 입구 하나로 합쳐지지 않습니다. filesystem이 직접 wrapper를 호출하는 경우도 있으며, 마지막 read_pages()는 readahead callback이나 folio별 read_folio fallback을 선택합니다.
buffered miss: page_cache_sync_readahead() -> page_cache_sync_ra()
mmap miss: do_sync_mmap_readahead() -> sequential or readaround
marker: page_cache_async_readahead() -> page_cache_async_ra()
read marker: filemap_readahead() -> page_cache_async_ra()
WILLNEED: generic_fadvise() -> force_page_cache_readahead()
forced: force_page_cache_ra() -> do_page_cache_ra()
prepare: page_cache_ra_order() or page_cache_ra_unbounded()
submit: read_pages() -> readahead() or read_folio()
synchronous와 asynchronous는 I/O의 완료 방식이 아닙니다
cache miss에서 지금 필요한 내용을 준비하면서 시작하는 readahead를 synchronous 경로라고 부릅니다. callback은 locked folio에 비동기 I/O를 제출하고 돌아올 수 있으며, 필요한 folio의 내용은 demand caller가 따로 기다립니다. window 전체의 read가 완료되었다는 의미의 sync가 아닙니다.
asynchronous 경로는 이미 cached folio를 소비하다 readahead marker를 만났을 때 다음 작업을 촉발합니다. 이 경로는 file-backed fault뿐 아니라 buffered read에서도 사용합니다. read()의 cache hit을 그 자체로 하드웨어 page fault나 minor fault라고 부를 필요는 없습니다.
window가 start에서 시작하고 길이가 size, tail 길이가 async_size이면 marker는 대략 start + size - async_size에 놓입니다. large folio와 mapping의 최소 order에 맞춘 정렬 보정은 별도로 적용됩니다. tail은 아직 I/O를 제출하지 않은 예약 공간이 아니라 현재 window를 소비하면서 다음 window의 I/O를 겹칠 시점을 정하는 영역입니다.

marker를 이미 존재하는 cached folio마다 다시 붙이지 않는 것도 중요합니다. 공유 cache에는 다른 stream이 준비한 folio가 있을 수 있습니다. 그때마다 새 marker를 만들면 불필요한 lookup과 중복 readahead를 일으킬 수 있습니다.
mmap은 sequential과 readaround를 나눕니다
VM_SEQ_READ 문맥에서는 fault 위치에서 앞으로 읽을 가능성을 우선합니다. 일반 mmap readaround는 뒤쪽 내용도 접근할 수 있다는 가정으로 fault 앞뒤의 window를 준비합니다. 그래서 fault page가 언제나 window의 첫 page는 아닙니다.
readaround가 계속 빗나가면 불필요한 I/O만 늘어납니다. mmap_miss를 높이거나 cache hit에서 낮추는 정책으로 그 유용성을 추정하고, miss가 많으면 추측을 줄이거나 생략합니다. 이 수치를 정확한 disk 요청 counter라고 해석해서는 안 됩니다.
일반 readaround는 start = max(0, pgoff - ra_pages / 2), size = ra_pages, async_size = ra_pages / 4를 사용합니다. fault index를 window 중간 근처에 두되 파일 처음에서는 0에 clamp합니다. mmap_miss는 MMAP_LOTSAMISS * 10인 1000까지 증가를 제한하고, 보통의 readaround를 포기하는 검사는 100을 넘었는지 봅니다. 이 두 threshold와 1/4 tail을 같은 값으로 혼동하면 안 됩니다.
긴 I/O를 시작하기 전에 fault lock을 놓는 경우에는 file 참조를 확보합니다. 반환되는 fpin은 그 file의 수명을 위한 것이지 VMA를 고정하는 pointer가 아닙니다. 준비한 내용이 있어도 원래 mapping을 다시 검사하기 위해 fault를 retry해야 하는 이유는 여전히 남습니다.
asynchronous mmap 경로는 이미 retry한 fault에서 중복 작업을 피하고, random 정책이나 비활성 상태를 고려한 뒤 marker를 검사합니다. cache hit 자체가 예상에 도움이 되므로 marker가 없는 hit과, 실제 다음 I/O를 시작할 marker hit을 구별합니다. mmap readahead
v6.18 synchronous 함수는 THP 요청을 일반 random/disabled 검사보다 먼저 처리할 수 있습니다. 지원되는 VM_HUGEPAGE 경로는 PMD 크기로 정렬하고, random이면 한 PMD, 그렇지 않으면 두 PMD 분량을 준비하는 조건을 둡니다. executable VMA도 별도입니다. 일반 VM_RAND_READ 거절식은 VM_EXEC를 함께 고려하며, executable 경로는 architecture가 정한 exec_folio_order()를 사용하고 VMA 경계 안으로 범위를 제한한 뒤 async tail을 0으로 둡니다. random이면 항상 아무 준비도 하지 않는다는 한 문장으로 current 전체 함수를 설명할 수 없는 이유입니다.
do_sync_mmap_readahead_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 struct file *do_sync_mmap_readahead(struct vm_fault *vmf)
{
struct file *file = vmf->vma->vm_file;
struct file_ra_state *ra = &file->f_ra;
struct address_space *mapping = file->f_mapping;
DEFINE_READAHEAD(ractl, file, ra, mapping, vmf->pgoff);
struct file *fpin = NULL;
vm_flags_t vm_flags = vmf->vma->vm_flags;
unsigned short mmap_miss;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
/* Use the readahead code, even if readahead is disabled */
if ((vm_flags & VM_HUGEPAGE) && HPAGE_PMD_ORDER <= MAX_PAGECACHE_ORDER) {
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
ractl._index &= ~((unsigned long)HPAGE_PMD_NR - 1);
ra->size = HPAGE_PMD_NR;
/*
* Fetch two PMD folios, so we get the chance to actually
* readahead, unless we've been told not to.
*/
if (!(vm_flags & VM_RAND_READ))
ra->size *= 2;
ra->async_size = HPAGE_PMD_NR;
ra->order = HPAGE_PMD_ORDER;
page_cache_ra_order(&ractl, ra);
return fpin;
}
#endif
/*
* If we don't want any read-ahead, don't bother. VM_EXEC case below is
* already intended for random access.
*/
if ((vm_flags & (VM_RAND_READ | VM_EXEC)) == VM_RAND_READ)
return fpin;
if (!ra->ra_pages)
return fpin;
if (vm_flags & VM_SEQ_READ) {
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
page_cache_sync_ra(&ractl, ra->ra_pages);
return fpin;
}
/* Avoid banging the cache line if not needed */
mmap_miss = READ_ONCE(ra->mmap_miss);
if (mmap_miss < MMAP_LOTSAMISS * 10)
WRITE_ONCE(ra->mmap_miss, ++mmap_miss);
/*
* Do we miss much more than hit in this file? If so,
* stop bothering with read-ahead. It will only hurt.
*/
if (mmap_miss > MMAP_LOTSAMISS)
return fpin;
if (vm_flags & VM_EXEC) {
/*
* Allow arch to request a preferred minimum folio order for
* executable memory. This can often be beneficial to
* performance if (e.g.) arm64 can contpte-map the folio.
* Executable memory rarely benefits from readahead, due to its
* random access nature, so set async_size to 0.
*
* Limit to the boundaries of the VMA to avoid reading in any
* pad that might exist between sections, which would be a waste
* of memory.
*/
struct vm_area_struct *vma = vmf->vma;
unsigned long start = vma->vm_pgoff;
unsigned long end = start + vma_pages(vma);
unsigned long ra_end;
ra->order = exec_folio_order();
ra->start = round_down(vmf->pgoff, 1UL << ra->order);
ra->start = max(ra->start, start);
ra_end = round_up(ra->start + ra->ra_pages, 1UL << ra->order);
ra_end = min(ra_end, end);
ra->size = ra_end - ra->start;
ra->async_size = 0;
} else {
/*
* mmap read-around
*/
ra->start = max_t(long, 0, vmf->pgoff - ra->ra_pages / 2);
ra->size = ra->ra_pages;
ra->async_size = ra->ra_pages / 4;
ra->order = 0;
}
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
ractl._index = ra->start;
page_cache_ra_order(&ractl, ra);
return fpin;
}
async 함수는 folio가 이미 locked이면 다른 fault와 경합 중일 가능성을 고려해 mmap_miss를 낮추지 않습니다. 그렇지 않은 cache hit에서는 marker가 없어도 nonzero counter를 낮출 수 있고, marker가 있을 때에만 다음 I/O를 시도하며 필요하면 fpin을 돌려줍니다.
do_async_mmap_readahead_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 struct file *do_async_mmap_readahead(struct vm_fault *vmf,
struct folio *folio)
{
struct file *file = vmf->vma->vm_file;
struct file_ra_state *ra = &file->f_ra;
DEFINE_READAHEAD(ractl, file, ra, file->f_mapping, vmf->pgoff);
struct file *fpin = NULL;
unsigned short mmap_miss;
/* If we don't want any read-ahead, don't bother */
if (vmf->vma->vm_flags & VM_RAND_READ || !ra->ra_pages)
return fpin;
/*
* If the folio is locked, we're likely racing against another fault.
* Don't touch the mmap_miss counter to avoid decreasing it multiple
* times for a single folio and break the balance with mmap_miss
* increase in do_sync_mmap_readahead().
*/
if (likely(!folio_test_locked(folio))) {
mmap_miss = READ_ONCE(ra->mmap_miss);
if (mmap_miss)
WRITE_ONCE(ra->mmap_miss, --mmap_miss);
}
if (folio_test_readahead(folio)) {
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
page_cache_async_ra(&ractl, folio, ra->ra_pages);
}
return fpin;
}
forced 경로는 예측보다 요청 범위를 우선합니다
force_page_cache_ra()는 filesystem에 readahead나 read_folio callback이 있는지부터 확인합니다. 내용을 채울 callback이 없으면 범위만 계산해도 read를 진행할 수 없습니다.
가능한 양은 BDI의 I/O 관련 page 수와 file의 readahead 상한 등을 고려해 제한합니다. 요청이 크면 2 MiB 단위 chunk로 나누어 처리하는 현재 구현도 있습니다. 이 chunk는 한 번에 너무 많은 memory를 붙잡지 않으려는 batching이지, 모든 filesystem이 정확히 2 MiB BIO를 제출해야 한다는 규칙이 아닙니다.
readahead가 꺼진 조건에서도 demand read에 필요한 최소 범위를 준비하려고 forced 경로를 사용할 수 있습니다. 그래서 readahead 설정이 0이라는 사실과 모든 buffered read가 storage에 접근할 수 없다는 주장은 다릅니다. no-I/O flag가 있는 caller의 금지 조건은 별도로 지켜야 합니다.
page_cache_sync_ra()는 ra_pages == 0 또는 blk_cgroup_congested()이면 file이 없는 요청에서는 돌아가고, file이 있으면 요청량을 1 page로 제한해 forced 경로로 보냅니다. 이것은 I/O가 반드시 성공한다는 보장이 아닙니다. forced helper의 상한은 max(bdi->io_pages, ra->ra_pages)이며 io_pages만이 항상 더 작은 hard ceiling이라는 뜻도 아닙니다. 2 MiB chunk 사이의 index 전진은 current control 소비 경로가 맡으므로 과거의 local index 대입을 그대로 추가하지 않습니다.
// 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 force_page_cache_ra(struct readahead_control *ractl,
unsigned long nr_to_read)
{
struct address_space *mapping = ractl->mapping;
struct file_ra_state *ra = ractl->ra;
struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
unsigned long max_pages;
if (unlikely(!mapping->a_ops->read_folio && !mapping->a_ops->readahead))
return;
/*
* If the request exceeds the readahead window, allow the read to
* be up to the optimal hardware IO size
*/
max_pages = max_t(unsigned long, bdi->io_pages, ra->ra_pages);
nr_to_read = min_t(unsigned long, nr_to_read, max_pages);
while (nr_to_read) {
unsigned long this_chunk = (2 * 1024 * 1024) / PAGE_SIZE;
if (this_chunk > nr_to_read)
this_chunk = nr_to_read;
do_page_cache_ra(ractl, this_chunk, 0);
nr_to_read -= this_chunk;
}
}
marker hit과 miss에서 다른 증거를 찾습니다
page_cache_async_ra()는 writeback 중인 folio를 먼저 배제합니다. PG_readahead와 PG_reclaim이 bit를 공유하는 문맥이 있기 때문입니다. 정상 marker를 처리할 때에는 이를 지워 중복 trigger를 제한합니다.
예상 위치의 marker를 만났다면 직전 window의 끝으로 start를 옮기고 다음 window 크기를 키웁니다. 예상과 다른 위치의 marker라면 interleaved stream 등으로 file의 정책 상태가 달라졌을 수 있습니다. 이때는 앞쪽 cache에 남은 연속 구간과 다음 missing 위치를 보고 stream의 흔적을 다시 추정합니다.
synchronous miss에서는 파일 시작, 큰 요청, prev_pos와 인접한 위치가 간단한 sequential 증거입니다. 그렇지 않으면 뒤쪽의 cached history를 살핍니다. 충분한 연속 page가 있고 파일 시작부터 이어진 흔적이라면 window를 더 크게 잡을 근거가 됩니다. history가 부족하면 demand 범위만 읽고 기존 stream 상태를 불필요하게 바꾸지 않는 쪽을 선택합니다.
이 판단은 접근 순서의 증명이 아닙니다. 다른 reader가 cache를 채웠거나 reclaim이 중간 page를 지웠을 수도 있습니다. 틀린 추측의 비용을 감수하되 앞선 I/O와 application의 실행을 겹칠 가능성을 얻는 정책입니다.
현재 max 계산은 큰 요청이고 bdi->io_pages가 더 클 때만 min(req_size, io_pages)로 높입니다. synchronous 경로는 index 0, oversize request, 이전 byte 위치로부터 같은 page 또는 다음 page인지를 먼저 검사합니다. 그다음 이전 hole을 찾아 얻은 contig_count가 요청량 이하이면 상태를 오염시키지 않고 demand 범위만 읽습니다. 파일 처음까지 cache가 이어졌다는 ULONG_MAX sentinel이면 history를 두 배로 가중하고, context 추정에 성공하면 tail을 1 page로 둡니다.
// 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 unsigned long ractl_max_pages(struct readahead_control *ractl,
unsigned long req_size)
{
struct backing_dev_info *bdi = inode_to_bdi(ractl->mapping->host);
unsigned long max_pages = ractl->ra->ra_pages;
/*
* If the request exceeds the readahead window, allow the read to
* be up to the optimal hardware IO size
*/
if (req_size > max_pages && bdi->io_pages > max_pages)
max_pages = min(req_size, bdi->io_pages);
return max_pages;
}
read 호출용 wrapper는 control을 만들고 이 policy에 위임합니다. 아래 두 함수는 과거 ondemand_readahead()의 synchronous 역할이 현재 어디에 있는지 보여 줍니다. 현재 초기 tail은 window가 요청량보다 크면 그 차이이고, 그렇지 않으면 window 절반입니다. 오래된 async_size = size 분기를 current 초기화로 옮겨서는 안 됩니다.
page_cache_sync_readahead_v6_18.h
// 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
void page_cache_sync_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *file, pgoff_t index,
unsigned long req_count)
{
DEFINE_READAHEAD(ractl, file, ra, mapping, index);
page_cache_sync_ra(&ractl, req_count);
}
// 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_sync_ra(struct readahead_control *ractl,
unsigned long req_count)
{
pgoff_t index = readahead_index(ractl);
bool do_forced_ra = ractl->file && (ractl->file->f_mode & FMODE_RANDOM);
struct file_ra_state *ra = ractl->ra;
unsigned long max_pages, contig_count;
pgoff_t prev_index, miss;
trace_page_cache_sync_ra(ractl->mapping->host, index, ra, req_count);
/*
* Even if readahead is disabled, issue this request as readahead
* as we'll need it to satisfy the requested range. The forced
* readahead will do the right thing and limit the read to just the
* requested range, which we'll set to 1 page for this case.
*/
if (!ra->ra_pages || blk_cgroup_congested()) {
if (!ractl->file)
return;
req_count = 1;
do_forced_ra = true;
}
/* be dumb */
if (do_forced_ra) {
force_page_cache_ra(ractl, req_count);
return;
}
max_pages = ractl_max_pages(ractl, req_count);
prev_index = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
/*
* A start of file, oversized read, or sequential cache miss:
* trivial case: (index - prev_index) == 1
* unaligned reads: (index - prev_index) == 0
*/
if (!index || req_count > max_pages || index - prev_index <= 1UL) {
ra->start = index;
ra->size = get_init_ra_size(req_count, max_pages);
ra->async_size = ra->size > req_count ? ra->size - req_count :
ra->size >> 1;
goto readit;
}
/*
* Query the page cache and look for the traces(cached history pages)
* that a sequential stream would leave behind.
*/
rcu_read_lock();
miss = page_cache_prev_miss(ractl->mapping, index - 1, max_pages);
rcu_read_unlock();
contig_count = index - miss - 1;
/*
* Standalone, small random read. Read as is, and do not pollute the
* readahead state.
*/
if (contig_count <= req_count) {
do_page_cache_ra(ractl, req_count, 0);
return;
}
/*
* File cached from the beginning:
* it is a strong indication of long-run stream (or whole-file-read)
*/
if (miss == ULONG_MAX)
contig_count *= 2;
ra->start = index;
ra->size = min(contig_count + req_count, max_pages);
ra->async_size = 1;
readit:
ra->order = 0;
ractl->_index = ra->start;
page_cache_ra_order(ractl, ra);
}
async policy는 marker를 지운 뒤 cgroup congestion이면 더 진행하지 않습니다. 예상 marker는 현재 folio page 수로 내림 정렬한 위치입니다. 기대한 marker라면 window 뒤로 옮기고 크기를 키우며, 예상과 다르면 page_cache_next_miss()로 다음 hole을 찾습니다. 이후 preferred order를 높이고 끝을 alignment에 맞춰 줄인 뒤 tail을 새 window 전체로 설정합니다. 원본의 round-up 비교나 self-marker window 합치기 branch가 current code에도 그대로 있다고 가정하면 안 됩니다.
// 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_async_ra(struct readahead_control *ractl,
struct folio *folio, unsigned long req_count)
{
unsigned long max_pages;
struct file_ra_state *ra = ractl->ra;
pgoff_t index = readahead_index(ractl);
pgoff_t expected, start, end, aligned_end, align;
/* no readahead */
if (!ra->ra_pages)
return;
/*
* Same bit is used for PG_readahead and PG_reclaim.
*/
if (folio_test_writeback(folio))
return;
trace_page_cache_async_ra(ractl->mapping->host, index, ra, req_count);
folio_clear_readahead(folio);
if (blk_cgroup_congested())
return;
max_pages = ractl_max_pages(ractl, req_count);
/*
* It's the expected callback index, assume sequential access.
* Ramp up sizes, and push forward the readahead window.
*/
expected = round_down(ra->start + ra->size - ra->async_size,
folio_nr_pages(folio));
if (index == expected) {
ra->start += ra->size;
/*
* In the case of MADV_HUGEPAGE, the actual size might exceed
* the readahead window.
*/
ra->size = max(ra->size, get_next_ra_size(ra, max_pages));
goto readit;
}
/*
* Hit a marked folio without valid readahead state.
* E.g. interleaved reads.
* Query the pagecache for async_size, which normally equals to
* readahead size. Ramp it up and use it as the new readahead size.
*/
rcu_read_lock();
start = page_cache_next_miss(ractl->mapping, index + 1, max_pages);
rcu_read_unlock();
if (!start || start - index > max_pages)
return;
ra->start = start;
ra->size = start - index; /* old async_size */
ra->size += req_count;
ra->size = get_next_ra_size(ra, max_pages);
readit:
ra->order += 2;
align = 1UL << min(ra->order, ffs(max_pages) - 1);
end = ra->start + ra->size;
aligned_end = round_down(end, align);
if (aligned_end > ra->start)
ra->size -= end - aligned_end;
ra->async_size = ra->size;
ractl->_index = ra->start;
page_cache_ra_order(ractl, ra);
}
이전 ondemand_readahead()의 self-marker 병합은 별개의 알고리즘입니다. 당시에는 index == start이고 size == async_size이면 자신이 만든 marker를 곧바로 만날 조건으로 보고 add_pages = get_next_ra_size()를 계산했습니다. 두 window의 합이 상한 이하이면 기존 size에 add_pages를 더하고 새 tail을 add_pages로 삼았습니다. 합이 상한을 넘으면 size를 상한으로 제한하고 tail을 그 절반으로 두었습니다. 앞서 초기 tail을 window 전체로 정하던 분기와 이 병합 조건이 연결되어 있었습니다. 이 과거 조건을 현재의 절반-tail 초기화나 marker 내림 정렬과 섞어 쓰면 실제보다 이른 trigger와 큰 window를 예측하게 됩니다. 위 current 함수 전체는 이 옛 함수의 누락된 code를 복원한 것이 아닙니다.
Readahead의 상태 전이를 함수 전체로 읽는 비교 글에는 공식 v6.0 tag의 ondemand_readahead()와 두 history helper 전체를 함께 실었습니다. 두 시기의 분기를 섞지 않고 expected marker, cache history와 self-marker 병합의 순서를 확인할 수 있습니다.
buffered read에서 marker를 넘기는 helper도 NOIO이면 -EAGAIN입니다. IOCB_DONTCACHE의 dropbehind를 control에 전달한 뒤 async policy로 이어지는 전체 정의는 다음과 같습니다.
// 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_readahead(struct kiocb *iocb, struct file *file,
struct address_space *mapping, struct folio *folio,
pgoff_t last_index)
{
DEFINE_READAHEAD(ractl, file, &file->f_ra, mapping, folio->index);
if (iocb->ki_flags & IOCB_NOIO)
return -EAGAIN;
if (iocb->ki_flags & IOCB_DONTCACHE)
ractl.dropbehind = 1;
page_cache_async_ra(&ractl, folio, last_index - folio->index);
return 0;
}
초기 크기와 다음 크기는 다른 식으로 자랍니다
get_init_ra_size()는 요청 page 수를 다음 power of two로 올림한 뒤 최대값과의 비율에 따라 초기 window를 정합니다. 올림한 수를 다시 제곱하는 식이 아닙니다. 다음은 v6.18의 함수 전체이며, kernel 내부 helper라 독립적인 userspace 프로그램은 아닙니다.
// 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 unsigned long get_init_ra_size(unsigned long size, unsigned long max)
{
unsigned long newsize = roundup_pow_of_two(size);
if (newsize <= max / 32)
newsize = newsize * 4;
else if (newsize <= max / 4)
newsize = newsize * 2;
else
newsize = max;
return newsize;
}
다음 크기를 고르는 함수는 이미 설정된 ra->size를 입력으로 사용합니다. 작은 단계에서는 빠르게 키우고 상한 근처에서는 증가 폭을 낮춥니다. 아래도 같은 source의 함수 전체입니다.
// 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 unsigned long get_next_ra_size(struct file_ra_state *ra,
unsigned long max)
{
unsigned long cur = ra->size;
if (cur < max / 16)
return 4 * cur;
if (cur <= max / 2)
return 2 * cur;
return max;
}
상한을 64 page, 요청을 2 page, order-0이라고 가정하면 초기 계산은 8 page이고 다음 크기는 16, 32, 64 page로 자랄 수 있습니다. 이는 식의 예시이지 실제 시스템의 측정 결과가 아닙니다. allocation 실패, EOF와 large folio 정렬이 실제 준비량을 바꿀 수 있습니다.
초기 8-page window에서 요청이 2 page라면 marker가 index 2에 놓일 수 있습니다. 그러나 현재 demand batch의 exclusive end가 2이면 그 read가 index 2를 반드시 조회하는 것은 아닙니다. 첫 synchronous readahead 직후 같은 read()에서 언제나 async trigger가 생긴다고 일반화하지 않는 이유입니다.
readahead를 읽을 때에는 예상한 window, 새로 할당한 folio, 실제 제출한 I/O와 나중에 설치된 PTE의 수를 따로 세어야 합니다. 이 네 수를 하나로 합치지 않으면 cache hit인데 fault가 남거나 window가 커졌는데 I/O 수가 같지 않은 현상도 설명할 수 있습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9, §9.7~9.7.3.
- Linux v6.18: readahead.c, pagemap.h, filemap.c.
- Linux v6.18: madvise.c, fadvise.c, backing-dev.c, block/ioctl.c.
- posix_fadvise(2).