application이 연속 주소를 읽더라도 그 내용이 swap area에서 연속 slot에 저장되어 있으리라는 보장은 없습니다. VMA readahead는 backing의 순서보다 virtual-address locality를 사용합니다. page table에 남아 있는 swap entry를 읽어 다음에 필요할 법한 내용을 준비하는 방식입니다.
Window 상태는 VMA에 남깁니다
VMA의 swap_readahead_info에는 이전 주소·window 크기·hit 수를 압축해 저장합니다. speculative folio의 readahead 표식을 lookup이 소비하면 hit 수를 갱신하고, 다음 miss에서는 이를 window heuristic에 전달합니다. cluster 방식과 같은 기본 helper를 사용하더라도 offset 대신 virtual page 단위의 거리를 입력합니다.
여기서 virtual PFN이라고 표현한 값은 virtual address를 PAGE_SIZE로 나눈 index입니다. 실제 physical PFN을 얻은 것이 아닙니다. 둘을 혼동하면 virtual 이웃을 같은 physical block이나 같은 swap slot이라고 잘못 해석하게 됩니다.
최대 window는 page-cluster와 별도 ceiling을 함께 제한합니다. window가 한 page라면 speculative scan은 생략하고 fault 대상만 읽습니다. 0 hit, 이전 window, 인접 접근 판단의 완충은 cluster와 비슷하지만 상태의 소속과 주소축이 다릅니다.
Forward, backward, random에 따라 위치를 바꿉니다
이전 fault 다음 page로 진행하면 현재 주소부터 앞으로 window를 둡니다. 한 page씩 뒤로 진행하면 현재 주소를 포함한 뒤쪽 범위가 됩니다. 인접하지 않은 접근이면 현재 주소 주변으로 분배합니다. backward 예에서 end는 현재 page 다음의 exclusive boundary라는 점이 중요합니다.

제안한 범위는 VMA의 시작·끝과 현재 PTE table이 다루는 범위 안으로 clamp합니다. start는 왼쪽 제안·VMA 시작·table 시작 중 가장 큰 값이고, end는 오른쪽 제안·VMA 끝·table 끝 중 가장 작은 exclusive 값입니다. 이것은 array처럼 PTE를 훑으면서 다른 table로 넘어가지 않도록 하는 경계입니다.
Page table 관찰은 곧 lock ownership이 아닙니다
readahead loop는 PTE를 관찰하는 동안 concurrent fault·unmap이 값을 바꿀 수 있습니다. PTE 값을 지역 변수로 읽고 empty/present 상태나 non_swap_entry()를 구분해 ordinary swap만 대상으로 삼습니다. speculative 관찰로 얻은 entry를 그대로 신뢰하여 present mapping을 설치하는 것이 아닙니다.
각 entry는 cache read 준비 단계에서 area·slot lifetime을 다시 검증합니다. 새 cache folio라면 read를 시작하고 fault 대상이 아닌 경우 readahead 표식을 남깁니다. 임시 reference와 PTE mapping 관찰에 필요한 자원을 정리하고, 마지막에는 실제 fault 대상 folio를 별도로 확보합니다. 이후 fault handler가 PTE identity·lock·uptodate를 검증합니다.
이런 구조 때문에 speculative readahead가 조금 빗나가도 fault correctness를 포기하지 않습니다. readahead는 advisory 최적화이고 mapping 설치는 fault의 엄격한 protocol입니다. 또한 algorithm과 helper 이름·window 계산은 version에 따라 달라지므로 오래된 swap_ra_info() 구조체 설명을 v6.12의 helper 분리와 동일시하면 안 됩니다.
Swapoff는 같은 slot의 lifetime에 참여합니다
v6.12의 swapoff는 새 slot 배정을 막고 try_to_unuse()로 남은 entry를 해소합니다. 이 작업이 실패하면 area를 allocation 목록에 다시 넣고 slot cache를 재활성화하여 teardown을 중단합니다. swapoff를 요청했다는 사실만으로 area가 이미 완전히 사라졌다고 보아서는 안 됩니다.
entry 해소가 성공해도 metadata를 곧바로 해제하지 않습니다. percpu_ref_kill(&p->users)로 새 device-user reference를 제한하고, synchronize_rcu()와 user completion 대기로 기존 RCU reader와 get_swap_device() 사용 구간이 끝나기를 기다립니다. read 경로의 device 보유는 이 metadata 수명을 보호하지만 개별 slot 참조를 대신하거나 cache·PTE identity를 영구 고정하지는 않습니다. cache identity와 PTE 재검사가 여전히 필요한 이유입니다.
slot cache를 disable한 상태에서는 cache 삽입과 SWAP_HAS_CACHE accounting 사이의 경쟁 때문에 ordinary unused-slot skip을 그대로 적용할 수 없는 경로가 있습니다. direct swap-in도 임시 cache pin으로 slot 재사용을 막아야 합니다. 같은 숫자의 type·offset을 관찰했다고 같은 data의 lifetime이 계속된다고 단정하면 안 됩니다.
swapoff 중에도 allocation·read·CoW 등에 실패할 수 있습니다. 이 글의 locality 설명을 swapoff 성공이나 pressure 해소를 보장하는 근거로 사용해서는 안 됩니다. 올바른 이해의 중심은 readahead window가 아니라 어떤 reference와 lock이 어느 기간의 identity를 보장하는지입니다.
주소·window·hit를 한 값에 저장합니다
다음은 official v6.0의 state encoding 전체입니다. page-aligned 주소의 low bits를 window와 hits에 사용합니다. hits는 mask로 제한하고, 저장값이 0이면 초기 hits 4인 값을 사용합니다. 주소가 4라는 뜻이 아니라 아직 관찰 이력이 없는 상태의 초기 feedback입니다.
// 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#L44
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
#define SWAP_RA_WIN_SHIFT (PAGE_SHIFT / 2)
#define SWAP_RA_HITS_MASK ((1UL << SWAP_RA_WIN_SHIFT) - 1)
#define SWAP_RA_HITS_MAX SWAP_RA_HITS_MASK
#define SWAP_RA_WIN_MASK (~PAGE_MASK & ~SWAP_RA_HITS_MASK)
#define SWAP_RA_HITS(v) ((v) & SWAP_RA_HITS_MASK)
#define SWAP_RA_WIN(v) (((v) & SWAP_RA_WIN_MASK) >> SWAP_RA_WIN_SHIFT)
#define SWAP_RA_ADDR(v) ((v) & PAGE_MASK)
#define SWAP_RA_VAL(addr, win, hits) \
(((addr) & PAGE_MASK) | \
(((win) << SWAP_RA_WIN_SHIFT) & SWAP_RA_WIN_MASK) | \
((hits) & SWAP_RA_HITS_MASK))
/* Initial readahead hits is 4 to start up with a small window */
#define GET_SWAP_RA_VAL(vma) \
(atomic_long_read(&(vma)->swap_readahead_info) ? : 4)
static atomic_t swapin_readahead_hits = ATOMIC_INIT(4);
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_SWAP_H
#define _LINUX_SWAP_H
/*
* Official Linux v6.0: include/linux/swap.h
* https://github.com/torvalds/linux/blob/v6.0/include/linux/swap.h#L337
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
struct vma_swap_readahead {
unsigned short win;
unsigned short offset;
unsigned short nr_pte;
#ifdef CONFIG_64BIT
pte_t *ptes;
#else
pte_t ptes[SWAP_RA_PTE_CACHE_SIZE];
#endif
};
vma_swap_readahead의 offset은 swap storage offset이 아니라 준비한 PTE 범위 안에서 demand PTE가 있는 index입니다. nr_pte는 실제 clamp 후 관찰할 PTE 개수이고 win은 heuristic이 고른 크기입니다. 64-bit에서는 PTE pointer를 유지하고 32-bit에서는 고정 배열에 복사하는 조건부 정의까지 구분해야 합니다. 이 고전 struct를 최신 helper의 동일한 자료구조라고 간주하지 않습니다.
범위의 양 끝을 숫자로 확인합니다
v6.0의 전체 범위 계산과 clamp helper는 아래와 같습니다. official code의 notice와 GPL-2.0을 유지했습니다.
// 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#L709
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static void swap_ra_info(struct vm_fault *vmf,
struct vma_swap_readahead *ra_info)
{
struct vm_area_struct *vma = vmf->vma;
unsigned long ra_val;
unsigned long faddr, pfn, fpfn;
unsigned long start, end;
pte_t *pte, *orig_pte;
unsigned int max_win, hits, prev_win, win, left;
#ifndef CONFIG_64BIT
pte_t *tpte;
#endif
max_win = 1 << min_t(unsigned int, READ_ONCE(page_cluster),
SWAP_RA_ORDER_CEILING);
if (max_win == 1) {
ra_info->win = 1;
return;
}
faddr = vmf->address;
orig_pte = pte = pte_offset_map(vmf->pmd, faddr);
fpfn = PFN_DOWN(faddr);
ra_val = GET_SWAP_RA_VAL(vma);
pfn = PFN_DOWN(SWAP_RA_ADDR(ra_val));
prev_win = SWAP_RA_WIN(ra_val);
hits = SWAP_RA_HITS(ra_val);
ra_info->win = win = __swapin_nr_pages(pfn, fpfn, hits,
max_win, prev_win);
atomic_long_set(&vma->swap_readahead_info,
SWAP_RA_VAL(faddr, win, 0));
if (win == 1) {
pte_unmap(orig_pte);
return;
}
/* Copy the PTEs because the page table may be unmapped */
if (fpfn == pfn + 1)
swap_ra_clamp_pfn(vma, faddr, fpfn, fpfn + win, &start, &end);
else if (pfn == fpfn + 1)
swap_ra_clamp_pfn(vma, faddr, fpfn - win + 1, fpfn + 1,
&start, &end);
else {
left = (win - 1) / 2;
swap_ra_clamp_pfn(vma, faddr, fpfn - left, fpfn + win - left,
&start, &end);
}
ra_info->nr_pte = end - start;
ra_info->offset = fpfn - start;
pte -= ra_info->offset;
#ifdef CONFIG_64BIT
ra_info->ptes = pte;
#else
tpte = ra_info->ptes;
for (pfn = start; pfn != end; pfn++)
*tpte++ = *pte++;
#endif
pte_unmap(orig_pte);
}
// 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#L696
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static inline void swap_ra_clamp_pfn(struct vm_area_struct *vma,
unsigned long faddr,
unsigned long lpfn,
unsigned long rpfn,
unsigned long *start,
unsigned long *end)
{
*start = max3(lpfn, PFN_DOWN(vma->vm_start),
PFN_DOWN(faddr & PMD_MASK));
*end = min3(rpfn, PFN_DOWN(vma->vm_end),
PFN_DOWN((faddr & PMD_MASK) + PMD_SIZE));
}
virtual-page index를 f, window를 w라고 하면 forward는 [f, f+w), backward는 [f-w+1, f+1)입니다. random은 left=(w-1)/2를 써서 [f-left, f+w-left)를 제안합니다. 예를 들어 w=8이면 현재 page 앞에 3개, 현재 page, 뒤에 4개입니다. 짝수 window를 양쪽에 똑같은 개수로 둔다고 설명하면 하나가 어긋납니다.
여기서 start는 제안한 왼쪽, VMA 시작, fault 주소를 포함한 PMD-aligned 경계 중 max이고 end는 제안한 오른쪽, VMA 끝, 다음 PMD 경계 중 min입니다. PMD_MASK는 low address bits를 지워 정렬된 table 범위의 시작을 얻습니다. 항상 demand 주소 이후의 PTE만 읽는 것이 아니며, backward와 random에서는 같은 PTE table 안의 앞쪽도 포함할 수 있습니다.
clamp 뒤 nr_pte=end-start, offset=f-start를 구하고 demand PTE pointer에서 offset만큼 뒤로 물러나 첫 PTE를 정합니다. end는 포함하지 않으므로 loop count와 경계가 일치합니다. VMA나 table 경계 때문에 실제 관찰 범위가 heuristic window보다 짧아질 수 있습니다.
실제 loop와 v6.12의 helper 분리
// 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#L785
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static struct page *swap_vma_readahead(swp_entry_t fentry, gfp_t gfp_mask,
struct vm_fault *vmf)
{
struct blk_plug plug;
struct swap_iocb *splug = NULL;
struct vm_area_struct *vma = vmf->vma;
struct page *page;
pte_t *pte, pentry;
swp_entry_t entry;
unsigned int i;
bool page_allocated;
struct vma_swap_readahead ra_info = {
.win = 1,
};
swap_ra_info(vmf, &ra_info);
if (ra_info.win == 1)
goto skip;
blk_start_plug(&plug);
for (i = 0, pte = ra_info.ptes; i < ra_info.nr_pte;
i++, pte++) {
pentry = *pte;
if (!is_swap_pte(pentry))
continue;
entry = pte_to_swp_entry(pentry);
if (unlikely(non_swap_entry(entry)))
continue;
page = __read_swap_cache_async(entry, gfp_mask, vma,
vmf->address, &page_allocated);
if (!page)
continue;
if (page_allocated) {
swap_readpage(page, false, &splug);
if (i != ra_info.offset) {
SetPageReadahead(page);
count_vm_event(SWAP_RA);
}
}
put_page(page);
}
blk_finish_plug(&plug);
swap_read_unplug(splug);
lru_add_drain();
skip:
/* The page was likely read above, so no need for plugging here */
return read_swap_cache_async(fentry, gfp_mask, vma, vmf->address,
ra_info.win == 1, NULL);
}
// 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#L745
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static int swap_vma_ra_win(struct vm_fault *vmf, unsigned long *start,
unsigned long *end)
{
struct vm_area_struct *vma = vmf->vma;
unsigned long ra_val;
unsigned long faddr, prev_faddr, left, right;
unsigned int max_win, hits, prev_win, win;
max_win = 1 << min(READ_ONCE(page_cluster), SWAP_RA_ORDER_CEILING);
if (max_win == 1)
return 1;
faddr = vmf->address;
ra_val = GET_SWAP_RA_VAL(vma);
prev_faddr = SWAP_RA_ADDR(ra_val);
prev_win = SWAP_RA_WIN(ra_val);
hits = SWAP_RA_HITS(ra_val);
win = __swapin_nr_pages(PFN_DOWN(prev_faddr), PFN_DOWN(faddr), hits,
max_win, prev_win);
atomic_long_set(&vma->swap_readahead_info, SWAP_RA_VAL(faddr, win, 0));
if (win == 1)
return 1;
if (faddr == prev_faddr + PAGE_SIZE)
left = faddr;
else if (prev_faddr == faddr + PAGE_SIZE)
left = faddr - (win << PAGE_SHIFT) + PAGE_SIZE;
else
left = faddr - (((win - 1) / 2) << PAGE_SHIFT);
right = left + (win << PAGE_SHIFT);
if ((long)left < 0)
left = 0;
*start = max3(left, vma->vm_start, faddr & PMD_MASK);
*end = min3(right, vma->vm_end, (faddr & PMD_MASK) + PMD_SIZE);
return win;
}
// 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#L799
* Exact definition/declaration; not a reconstruction of a book listing.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static struct folio *swap_vma_readahead(swp_entry_t targ_entry, gfp_t gfp_mask,
struct mempolicy *mpol, pgoff_t targ_ilx, struct vm_fault *vmf)
{
struct blk_plug plug;
struct swap_iocb *splug = NULL;
struct folio *folio;
pte_t *pte = NULL, pentry;
int win;
unsigned long start, end, addr;
swp_entry_t entry;
pgoff_t ilx;
bool page_allocated;
win = swap_vma_ra_win(vmf, &start, &end);
if (win == 1)
goto skip;
ilx = targ_ilx - PFN_DOWN(vmf->address - start);
blk_start_plug(&plug);
for (addr = start; addr < end; ilx++, addr += PAGE_SIZE) {
if (!pte++) {
pte = pte_offset_map(vmf->pmd, addr);
if (!pte)
break;
}
pentry = ptep_get_lockless(pte);
if (!is_swap_pte(pentry))
continue;
entry = pte_to_swp_entry(pentry);
if (unlikely(non_swap_entry(entry)))
continue;
pte_unmap(pte);
pte = NULL;
folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
&page_allocated, false);
if (!folio)
continue;
if (page_allocated) {
swap_read_folio(folio, &splug);
if (addr != vmf->address) {
folio_set_readahead(folio);
count_vm_event(SWAP_RA);
}
}
folio_put(folio);
}
if (pte)
pte_unmap(pte);
blk_finish_plug(&plug);
swap_read_unplug(splug);
lru_add_drain();
skip:
/* The folio was likely read above, so no need for plugging here */
folio = __read_swap_cache_async(targ_entry, gfp_mask, mpol, targ_ilx,
&page_allocated, false);
if (unlikely(page_allocated))
swap_read_folio(folio, NULL);
return folio;
}
v6.12는 byte address로 범위를 반환하는 swap_vma_ra_win()과 실제 PTE 관찰 loop를 분리합니다. window=1이면 scan을 생략합니다. 그렇지 않으면 PTE mapping을 준비하고, 읽은 PTE가 ordinary swap인지 확인하며 새 folio만 read합니다. 동시 page-table 변경을 관찰할 수 있으므로 최종 mapping 설치의 PTL 재검사는 여전히 fault handler의 책임입니다.
loop는 speculative folio의 reference를 매번 반환하고 block·filesystem plug를 정리합니다. PTE mapping을 해제하고 LRU add를 drain한 뒤 demand entry를 별도로 준비하며 새 object일 때 read를 시작합니다. heuristic·speculative I/O·fault mapping이라는 세 책임이 나뉘어 있으므로 readahead의 성공을 곧바로 fault 해결로 셀 수 없습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 12, Swap VMA Readahead.
- Linux v6.12 mm/swap_state.c:
swap_vma_readahead(),swap_vma_ra_win(), read 준비. - Linux v6.12 mm/memory.c: fault의 최종 identity 검사.
- Linux v6.12 mm/swapfile.c: swapoff·slot lifetime.