fault가 한 page에서 발생했다고 I/O도 늘 한 page만 읽는 것은 아닙니다. 가까운 내용을 미리 읽어 다음 fault 비용을 줄일 수 있습니다. cluster readahead에서 '가까움'은 virtual address가 아니라 swap area의 연속 offset입니다.
Slot locality와 application locality는 다릅니다
인접한 swap slot이 같은 process의 인접 주소라는 보장은 없습니다. allocator와 reclaim의 순서 때문에 서로 다른 process의 page가 이웃할 수 있습니다. cluster 방식은 backing의 연속 접근에 유리할 수 있지만 application이 다음에 쓸 주소를 정확히 예측하는 모델은 아닙니다.
swap readahead는 VMA 방식과 cluster 방식 중 조건에 맞는 경로를 선택합니다. enable_vma_readahead를 끄면 VMA 방식 대신 cluster 방식을 사용할 수 있으며, 모든 swap readahead가 꺼지는 것은 아닙니다. rotational swap 존재 여부도 방식 선택에 관여합니다.
Window는 hit feedback으로 커집니다
미리 읽은 folio에 PG_readahead를 남기면 후속 lookup이 이를 소비하고 hit 수를 누적합니다. cluster mode는 atomic hit counter를 window 계산 시 exchange하여 읽고 초기화합니다. 이전 window와 hit가 없었던 offset을 함께 기억합니다.
__swapin_nr_pages()의 heuristic은 hits + 2에서 시작합니다. hit가 없으면 이전 offset과 바로 이웃한 접근인지 보아 sequential 가능성을 판단하고, 그렇지 않으면 한 page 수준으로 줄일 수 있습니다. hit가 있으면 최소 4부터 power-of-two로 올림합니다. 최대 크기를 제한한 뒤 이전 window의 절반보다 급격히 줄이지 않도록 완화합니다.
이 값은 실험 법칙에서 온 heuristic이지 workload의 미래를 증명하는 식이 아닙니다. 간단한 상태에는 swap type까지 정밀하게 구별하지 않는 부분도 있어 서로 다른 area의 offset 관계를 우연히 sequential로 볼 여지가 있습니다. 그 오차보다 낮은 비용과 단순성을 택한 설계입니다.
page-cluster는 page 수가 아니라 log2 설정입니다
vm.page-cluster 값 3은 최대 여덟 base page를 뜻하고, 0이면 최대 한 page이므로 추가 readahead를 억제합니다. v6.12의 swap_setup()은 RAM이 16 MiB 미만이면 2, 그 밖에는 3을 초기값으로 둡니다. 따라서 정확히 16 MiB인 경우까지 작은 쪽 조건에 포함하지 않습니다. 이 값을 storage byte 수나 swap slot allocation batch와 혼동하면 안 됩니다.
한 번의 window가 정해지면 window - 1 mask로 fault offset을 포함하는 aligned range를 만듭니다. swap header인 offset 0과 area 끝을 넘어가지 않도록 경계를 제한합니다. 각 slot마다 cache lookup·새 folio 준비를 수행하고 새로 준비된 경우에만 read를 제출합니다.
미리 읽은 folio와 fault 대상은 구분합니다
readahead 표식은 fault 대상이 아닌 speculative page에 남깁니다. 'loop의 첫 page'가 무조건 fault 대상인 것은 아닙니다. aligned window 중간에서 fault가 났을 수 있기 때문입니다. offset identity로 구분해야 합니다.
각 iteration의 임시 reference를 반환하고 필요한 LRU add batch를 drain한 뒤, 마지막에는 fault 대상 entry를 반드시 확보합니다. window read를 일부 시작했다는 사실로 실제 fault 대상까지 읽었다고 가정할 수는 없습니다. 이미 cache에 들어간 folio나 allocation 실패, 사용되지 않는 slot 때문에 loop의 결과가 달라질 수 있습니다.
cluster readahead가 만든 cache는 다른 reclaim 대상과 마찬가지로 pressure의 영향을 받습니다. 미리 읽은 양이 많다고 항상 성능이 좋아지는 것은 아니며, cache pollution과 불필요한 I/O도 비용입니다. hit feedback은 이 비용과 이득 사이를 다음 miss에서 조정하는 장치입니다.
Heuristic의 연산 순서와 상한을 구별합니다
아래 official v6.0 전체 함수에서는 hit가 0이면 인접 offset에 2 page, 비인접 offset에 1 page를 제안합니다. hit가 있으면 hits + 2를 최소 4부터 power-of-two로 올립니다. 그다음 새 maximum을 적용하고, 마지막에 이전 window 절반이라는 하한을 적용합니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.0: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L528
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static unsigned int __swapin_nr_pages(unsigned long prev_offset,
unsigned long offset,
int hits,
int max_pages,
int prev_win)
{
unsigned int pages, last_ra;
/*
* This heuristic has been found to work well on both sequential and
* random loads, swapping to hard disk or to SSD: please don't ask
* what the "+ 2" means, it just happens to work well, that's all.
*/
pages = hits + 2;
if (pages == 2) {
/*
* We can have no readahead hits to judge by: but must not get
* stuck here forever, so check for an adjacent offset instead
* (and don't even bother to check whether swap type is same).
*/
if (offset != prev_offset + 1 && offset != prev_offset - 1)
pages = 1;
} else {
unsigned int roundup = 4;
while (roundup < pages)
roundup <<= 1;
pages = roundup;
}
if (pages > max_pages)
pages = max_pages;
/* Don't shrink readahead too fast */
last_ra = prev_win / 2;
if (pages < last_ra)
pages = last_ra;
return pages;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.0: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L568
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static unsigned long swapin_nr_pages(unsigned long offset)
{
static unsigned long prev_offset;
unsigned int hits, pages, max_pages;
static atomic_t last_readahead_pages;
max_pages = 1 << READ_ONCE(page_cluster);
if (max_pages <= 1)
return 1;
hits = atomic_xchg(&swapin_readahead_hits, 0);
pages = __swapin_nr_pages(READ_ONCE(prev_offset), offset, hits,
max_pages,
atomic_read(&last_readahead_pages));
if (!hits)
WRITE_ONCE(prev_offset, offset);
atomic_set(&last_readahead_pages, pages);
return pages;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* linux/mm/swap.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
*/
/*
* This file contains the default values for the operation of the
* Linux VM subsystem. Fine-tuning documentation can be found in
* Documentation/admin-guide/sysctl/vm.rst.
* Started 18.12.91
* Swap aging added 23.2.95, Stephen Tweedie.
* Buffermem limits added 12.3.98, Rik van Riel.
*/
/*
* Official Linux v6.0: mm/swap.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap.c#L1071
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
void __init swap_setup(void)
{
unsigned long megs = totalram_pages() >> (20 - PAGE_SHIFT);
/* Use a smaller cluster for small-memory machines */
if (megs < 16)
page_cluster = 2;
else
page_cluster = 3;
/*
* Right now other parts of the system means that we
* _really_ don't want to cluster much more
*/
}
순서 때문에 실행 중 page-cluster를 낮추면 이전 window의 완충 하한이 새 maximum보다 잠시 클 수 있습니다. 예를 들어 이전 window가 8이고 새 maximum이 2라면 helper 결과는 4가 될 수 있습니다. 따라서 page-cluster가 1 이상일 때 그 순간의 실제 결과가 언제나 새 maximum 이하라고 단정하지 않습니다. page-cluster=0은 상위 swapin_nr_pages()가 1을 바로 반환하므로 이 완충 계산과 atomic hit reset도 거치지 않습니다. 이 예시는 식의 계산이며 설정을 변경한 실험 결과가 아닙니다.
VMA 방식도 아래 v6.12의 같은 순서인 helper를 사용합니다. ceiling은 새 제안의 최대 크기를 정하지만, 저장된 이전 window와의 완충까지 생각해야 합니다. GPL-2.0 code와 upstream notice를 함께 제공합니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.12: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.12/mm/swap_state.c#L577
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static unsigned int __swapin_nr_pages(unsigned long prev_offset,
unsigned long offset,
int hits,
int max_pages,
int prev_win)
{
unsigned int pages, last_ra;
/*
* This heuristic has been found to work well on both sequential and
* random loads, swapping to hard disk or to SSD: please don't ask
* what the "+ 2" means, it just happens to work well, that's all.
*/
pages = hits + 2;
if (pages == 2) {
/*
* We can have no readahead hits to judge by: but must not get
* stuck here forever, so check for an adjacent offset instead
* (and don't even bother to check whether swap type is same).
*/
if (offset != prev_offset + 1 && offset != prev_offset - 1)
pages = 1;
} else {
unsigned int roundup = 4;
while (roundup < pages)
roundup <<= 1;
pages = roundup;
}
if (pages > max_pages)
pages = max_pages;
/* Don't shrink readahead too fast */
last_ra = prev_win / 2;
if (pages < last_ra)
pages = last_ra;
return pages;
}
방식 선택과 실제 read loop를 연결합니다
v6.0의 selector는 VMA readahead 활성화와 rotational swap 부재를 함께 봅니다. false면 cluster 경로이지 readahead 전체 중지가 아닙니다. window 계산에 쓸 global hit counter의 초기값도 4입니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.0: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L314
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static inline bool swap_use_vma_readahead(void)
{
return READ_ONCE(enable_vma_readahead) && !atomic_read(&nr_rotate_swap);
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.0: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L847
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
struct vm_fault *vmf)
{
return swap_use_vma_readahead() ?
swap_vma_readahead(entry, gfp_mask, vmf) :
swap_cluster_readahead(entry, gfp_mask, vmf);
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.0: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L607
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
struct page *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
struct vm_fault *vmf)
{
struct page *page;
unsigned long entry_offset = swp_offset(entry);
unsigned long offset = entry_offset;
unsigned long start_offset, end_offset;
unsigned long mask;
struct swap_info_struct *si = swp_swap_info(entry);
struct blk_plug plug;
struct swap_iocb *splug = NULL;
bool do_poll = true, page_allocated;
struct vm_area_struct *vma = vmf->vma;
unsigned long addr = vmf->address;
mask = swapin_nr_pages(offset) - 1;
if (!mask)
goto skip;
do_poll = false;
/* Read a page_cluster sized and aligned cluster around offset. */
start_offset = offset & ~mask;
end_offset = offset | mask;
if (!start_offset) /* First page is swap header. */
start_offset++;
if (end_offset >= si->max)
end_offset = si->max - 1;
blk_start_plug(&plug);
for (offset = start_offset; offset <= end_offset ; offset++) {
/* Ok, do the async read-ahead now */
page = __read_swap_cache_async(
swp_entry(swp_type(entry), offset),
gfp_mask, vma, addr, &page_allocated);
if (!page)
continue;
if (page_allocated) {
swap_readpage(page, false, &splug);
if (offset != entry_offset) {
SetPageReadahead(page);
count_vm_event(SWAP_RA);
}
}
put_page(page);
}
blk_finish_plug(&plug);
swap_read_unplug(splug);
lru_add_drain(); /* Push any new pages onto the LRU now */
skip:
/* The page was likely read above, so no need for plugging here */
return read_swap_cache_async(entry, gfp_mask, vma, addr, do_poll, NULL);
}
v6_12_swap_cluster_readahead.c
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/swap_state.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
*
* Rewritten to use page cache, (C) 1998 Stephen Tweedie
*/
/*
* Official Linux v6.12: mm/swap_state.c
* https://github.com/torvalds/linux/blob/v6.12/mm/swap_state.c#L656
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
struct mempolicy *mpol, pgoff_t ilx)
{
struct folio *folio;
unsigned long entry_offset = swp_offset(entry);
unsigned long offset = entry_offset;
unsigned long start_offset, end_offset;
unsigned long mask;
struct swap_info_struct *si = swp_swap_info(entry);
struct blk_plug plug;
struct swap_iocb *splug = NULL;
bool page_allocated;
mask = swapin_nr_pages(offset) - 1;
if (!mask)
goto skip;
/* Read a page_cluster sized and aligned cluster around offset. */
start_offset = offset & ~mask;
end_offset = offset | mask;
if (!start_offset) /* First page is swap header. */
start_offset++;
if (end_offset >= si->max)
end_offset = si->max - 1;
blk_start_plug(&plug);
for (offset = start_offset; offset <= end_offset ; offset++) {
/* Ok, do the async read-ahead now */
folio = __read_swap_cache_async(
swp_entry(swp_type(entry), offset),
gfp_mask, mpol, ilx, &page_allocated, false);
if (!folio)
continue;
if (page_allocated) {
swap_read_folio(folio, &splug);
if (offset != entry_offset) {
folio_set_readahead(folio);
count_vm_event(SWAP_RA);
}
}
folio_put(folio);
}
blk_finish_plug(&plug);
swap_read_unplug(splug);
lru_add_drain(); /* Push any new pages onto the LRU now */
skip:
/* The page was likely read above, so no need for plugging here */
folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
&page_allocated, false);
if (unlikely(page_allocated))
swap_read_folio(folio, NULL);
return folio;
}
두 cluster 함수는 같은 swap type의 aligned offset 구간을 훑습니다. 새 cache object를 준비한 경우에만 read를 제출하고 demand offset 이외에 readahead 표식을 둡니다. block plug 종료와 filesystem read plug flush는 서로 다른 batching 정리입니다. 두 정리와 LRU drain 뒤에 demand entry를 다시 얻습니다. speculative 대상 일부가 allocation·slot 상태 때문에 빠져도 demand read의 성공 여부는 별도로 확인해야 합니다.
v6.0은 window가 1이면 speculative loop를 생략하며 demand read의 do_poll=true 요청을 유지합니다. v6.12는 그 bool 인자가 사라지고 swap_read_folio() 내부의 backend별 경로를 따릅니다. window가 더 크면 loop에서 read를 시작하고 마지막 demand lookup으로 object를 확보하지만, 두 version 모두 반환된 folio가 곧바로 unlocked·uptodate라는 계약은 아닙니다. 최종 사용 가능 여부는 fault의 lock·uptodate 검사로 확인합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 12, Swap Cluster Readahead.
- Linux v6.12 mm/swap_state.c:
swap_cluster_readahead(),swapin_nr_pages(),__swapin_nr_pages(). - Linux v6.12 VM sysctl 문서: page-cluster.