Swap entry가 있는데 page는 왜 RAM에 남아 있을까요?

process의 PTE가 swap entry로 바뀌었다고 그 내용이 이미 disk에 있고 RAM에서는 사라졌다고 생각하기 쉽습니다. 실제로는 I/O를 시작하기 전에도 swap entry가 만들어지고, I/O 중에도 fault가 들어오며, swap-in 뒤에도 cache와 slot이 남을 수 있습니다. 이 사이를 연결하는 것이 swap cache입니다.

swap은 ordinary anonymous memory에 복구 가능한 backing을 제공하여 reclaim의 선택지를 넓힙니다. clean file cache만 계속 버리면 file workload가 thrashing할 수 있으므로 anonymous 내용도 다른 backing으로 옮길 수 있어야 합니다. 다만 swap storage를 RAM과 같은 속도의 확장 공간이나 kernel의 emergency reserve로 보아서는 안 됩니다.

overcommit과 demand paging 때문에 virtual range의 예약량과 실제 resident RAM은 일치하지 않습니다. 이 차이를 허용한다고 모든 mapping의 물리 backing을 언제나 보장하는 것은 아닙니다. pressure 아래서 막 버린 내용을 다시 fault-in하고 다른 내용을 다시 밀어내는 일이 반복되면 유용한 계산보다 read·write와 대기에 시간을 쓰는 thrashing이 됩니다. clean file 내용을 버린 뒤의 다음 접근도 항상 physical disk read를 유발하지는 않습니다. 다른 cache·readahead·backend 상태에 따라 fault와 I/O 결과가 달라집니다.

PTE, resident folio, backing slot은 세 가지입니다

ordinary swap entry는 swap area를 나타내는 type과 그 안의 base-page offset을 조합합니다. swp_entry_t는 architecture-independent 표현이고, raw PTE의 encoding과는 다릅니다. type은 C의 data type이 아니라 area index입니다. offset도 physical RAM의 PFN이나 최종 disk block 번호가 아닙니다.

이 entry를 key로 swap cache에서 resident folio를 찾습니다. folio가 있다면 새 storage read를 피할 수 있지만, 내용이 uptodate인지나 현재 writeback 중인지까지 cache hit 하나로 확정할 수는 없습니다. cache는 disk 내용을 읽고 쓰는 동안 같은 entry의 내용을 공유하고 경쟁을 조정하는 RAM object입니다.

swap cache에 들어가는 순간은 ordinary anon allocation 시점이 아닙니다. reclaim이 backing을 배정하거나 swap-in read가 resident object를 등록할 때입니다. 따라서 PG_swapbacked인 anon folio와 PG_swapcache인 folio를 동의어로 사용할 수 없습니다. swap-backed는 anonymous 여부의 보편적인 판별식도 아닙니다. shmem도 swap backing을 사용하며, MADV_FREE로 lazy-free가 된 anon folio는 별도 상태 전환을 거칩니다.

v6.12의 swapper address space

v6.12 설명의 기준은 swapper_spaces입니다. 고정된 type별 pointer 아래에 필요한 수의 address_space 배열을 동적으로 만듭니다. 각 object의 i_pages XArray가 resident swap cache folio를 담습니다. underlying swap file의 일반 page cache와는 독립된 abstraction입니다.

SWAP_ADDRESS_SPACE_SHIFT=14라면 한 address space는 16,384 base pages의 offset 범위를 담당합니다. 4 KiB page에서는 64 MiB입니다. entry의 type으로 area를 고르고 offset의 상위 범위로 shard를 찾은 뒤 해당 cache index로 lookup합니다. 전체 area 크기가 커져도 하나의 거대한 XArray에 모두 넣지 않는 구성입니다.

init_swap_address_space()는 page 수를 이 단위로 올림 나누어 object 수를 구하고 kvcalloc()으로 배열을 준비합니다. nr_swapper_spaces[type]는 page 수가 아니라 address-space object 수입니다. 각 object의 XArray·operation을 초기화하며 일반 writeback tag 탐색은 사용하지 않습니다. 그렇다고 swap writeback이 없는 것은 아닙니다. swap_aops의 writepage 경로가 별도로 있습니다.

Folio에서 entry를 찾는 방향도 필요합니다

cache lookup이 entry에서 folio로 가는 방향이라면 reclaim은 folio에서 자기 backing entry를 알아야 합니다. v6.12의 folio swap metadata와 folio_swap_entry()·page_swap_entry() 같은 helper가 이를 연결합니다. large folio에서는 subpage별 entry offset의 관계도 유지해야 합니다. 오래된 page_private() 설명을 최신 folio field layout에 그대로 대입해서는 안 됩니다.

folio_mapping()은 swap cache folio에 대해 swapper address space를 돌려주므로 reclaim이 file cache와 공통 interface를 활용할 수 있습니다. 이것은 user PTE가 아직 folio를 mapping하는지를 묻는 folio_mapped()와 다른 질문입니다.

Refcount와 slot 사용량을 따로 셉니다

RAM object를 살려 두는 folio reference와 swap slot의 사용 accounting은 다릅니다. PTE가 swap entry를 참조하는 수, SWAP_HAS_CACHE 상태, cache가 가진 folio reference, present PTE가 가진 folio reference를 분리해야 합니다.

세 개의 swap PTE가 같은 entry를 참조하는 order-0 가상 예시입니다. read fault 후에도 cache가 유지될 수 있으며 cache 제거와 slot 재사용, resident folio 수명은 서로 다른 조건을 가집니다. 전체 refcount를 재현한 표는 아닙니다.
세 개의 swap PTE가 같은 entry를 참조하는 order-0 가상 예시입니다. read fault 후에도 cache가 유지될 수 있으며 cache 제거와 slot 재사용, resident folio 수명은 서로 다른 조건을 가집니다. 전체 refcount를 재현한 표는 아닙니다.

마지막 swap PTE가 사라져도 cache 보유가 남으면 slot은 즉시 재사용되지 않을 수 있습니다. cache를 제거해도 present mapping이나 pin이 folio를 유지하면 RAM은 남습니다. 반대로 RAM folio를 reclaim해도 swap PTE가 남으면 backing slot을 유지해야 합니다. 'swap entry를 free했다'는 표현이 어느 accounting을 줄였는지 확인해야 하는 이유입니다.

v6.18에서는 resident lookup 구조가 달라집니다

v6.18은 고정 범위 address-space XArray 대신 per-cluster swap table로 resident lookup을 수행합니다. table entry는 빈 상태, folio, working-set shadow를 구분합니다. shadow는 page 내용이나 folio pointer가 아니라 eviction 이력입니다.

추가·삭제에서는 folio lock을 먼저 잡고 그다음 swap cluster lock을 잡습니다. 두 lock이 필요하다는 사실뿐 아니라 이 순서도 지켜야 합니다. lookup은 RCU와 atomic read로 보호하며, 찾은 folio를 사용하려면 필요한 reference와 folio lock을 별도로 확보해야 합니다. v6.12의 XArray lock 설명과 v6.18의 table lock을 섞어 하나의 code path로 만들면 안 됩니다. higher-level reclaim이 사용하는 swapper abstraction이 남아 있다는 사실도 resident 자료구조가 그대로라는 뜻은 아닙니다.

SwapCached를 사용량에 더하지 않습니다

SwapTotal - SwapFree는 swap area의 사용 측면이고 SwapCached는 RAM에 남은 swap cache 측면입니다. 겹치지 않는 용량 두 개를 더하는 계산이 아닙니다. RSS·swap entry·cache residency도 서로 다른 관찰입니다. /proc/PID/pagemap의 swapped bit는 해당 PTE 상태를 나타내며 physical disk I/O가 이미 발생했다는 보장은 아닙니다.

Cache의 key와 container를 code로 연결합니다

고전 구현을 구체적으로 읽을 때는 v6.0을 별도의 기준으로 삼겠습니다. 다음은 해당 official tag의 전체 declaration과 helper입니다. v6.12에서도 XArray 기반이라는 큰 구조는 이어지지만, folio field와 cache index helper까지 동일한 것은 아닙니다. code는 각 upstream의 notice를 유지했으며 GPL-2.0을 함께 제공합니다.

swp_entry_t는 unsigned long을 감싼 구조입니다. generic type을 위쪽에, mask한 offset을 아래쪽에 조합하며, 이를 풀 때도 같은 generic shift와 mask를 사용합니다. architecture의 raw swap PTE를 이 값에 바로 대입하는 과정은 없습니다.

v6_0_swp_entry_t.h

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_MM_TYPES_H
#define _LINUX_MM_TYPES_H

/*
 * Official Linux v6.0: include/linux/mm_types.h
 * https://github.com/torvalds/linux/blob/v6.0/include/linux/mm_types.h#L818
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

typedef struct {
	unsigned long val;
} swp_entry_t;

v6_0_swp_entry.c

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_SWAPOPS_H
#define _LINUX_SWAPOPS_H

/*
 * Official Linux v6.0: include/linux/swapops.h
 * https://github.com/torvalds/linux/blob/v6.0/include/linux/swapops.h#L41
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static inline swp_entry_t swp_entry(unsigned long type, pgoff_t offset)
{
	swp_entry_t ret;

	ret.val = (type << SWP_TYPE_SHIFT) | (offset & SWP_OFFSET_MASK);
	return ret;
}

v6_0_swp_type.c

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_SWAPOPS_H
#define _LINUX_SWAPOPS_H

/*
 * Official Linux v6.0: include/linux/swapops.h
 * https://github.com/torvalds/linux/blob/v6.0/include/linux/swapops.h#L53
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static inline unsigned swp_type(swp_entry_t entry)
{
	return (entry.val >> SWP_TYPE_SHIFT);
}

v6_0_swp_offset.c

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_SWAPOPS_H
#define _LINUX_SWAPOPS_H

/*
 * Official Linux v6.0: include/linux/swapops.h
 * https://github.com/torvalds/linux/blob/v6.0/include/linux/swapops.h#L62
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static inline pgoff_t swp_offset(swp_entry_t entry)
{
	return entry.val & SWP_OFFSET_MASK;
}

두 전역 배열의 역할도 다릅니다. swapper_spaces[type]은 그 area가 사용할 address-space 배열의 시작이고 nr_swapper_spaces[type]은 그 배열의 object 수입니다. 아래 macro의 offset shift는 어느 object를 고를지 결정합니다. 실제 cache 안의 index 규칙은 해당 version의 lookup과 함께 읽어야 합니다.

v6_0_swapper_spaces_declarations.h

// 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#L40
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

struct address_space *swapper_spaces[MAX_SWAPFILES] __read_mostly;
static unsigned int nr_swapper_spaces[MAX_SWAPFILES] __read_mostly;

v6_0_swap_address_space_macro.h

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _MM_SWAP_H
#define _MM_SWAP_H

#ifdef CONFIG_SWAP

/*
 * Official Linux v6.0: mm/swap.h
 * https://github.com/torvalds/linux/blob/v6.0/mm/swap.h#L26
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

/* One swap address space for each 64M swap space */
#define SWAP_ADDRESS_SPACE_SHIFT	14
#define SWAP_ADDRESS_SPACE_PAGES	(1 << SWAP_ADDRESS_SPACE_SHIFT)
extern struct address_space *swapper_spaces[];
#define swap_address_space(entry)			    \
	(&swapper_spaces[swp_type(entry)][swp_offset(entry) \
		>> SWAP_ADDRESS_SPACE_SHIFT])

v6_0_init_swap_address_space.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.0: mm/swap_state.c
 * https://github.com/torvalds/linux/blob/v6.0/mm/swap_state.c#L661
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
	struct address_space *spaces, *space;
	unsigned int i, nr;

	nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES);
	spaces = kvcalloc(nr, sizeof(struct address_space), GFP_KERNEL);
	if (!spaces)
		return -ENOMEM;
	for (i = 0; i < nr; i++) {
		space = spaces + i;
		xa_init_flags(&space->i_pages, XA_FLAGS_LOCK_IRQ);
		atomic_set(&space->i_mmap_writable, 0);
		space->a_ops = &swap_aops;
		/* swap cache doesn't use writeback related tags */
		mapping_set_no_writeback_tags(space);
	}
	nr_swapper_spaces[type] = nr;
	swapper_spaces[type] = spaces;

	return 0;
}

초기화는 DIV_ROUND_UP()으로 object 수를 구하고 kvcalloc() 실패 시 -ENOMEM을 반환합니다. kvcalloc()을 slab-only allocation이라고 제한하면 안 됩니다. 각 XArray는 IRQ-safe locking에 맞게 초기화하고 i_mmap_writable을 0으로 두며 swap_aops와 no-writeback-tags 상태를 설정합니다. 배열 준비에 성공한 뒤에만 type별 pointer와 개수를 공개합니다.

반대 방향의 조회에서는 folio가 swap cache인지 먼저 봅니다. v6.0 folio_mapping()은 slab이면 NULL, swap cache이면 entry에 대응하는 swapper mapping, 그 밖에는 mapping flag를 검사한 결과를 반환합니다. anonymous folio의 mapping을 ordinary file address_space *로 무조건 해석할 수 없는 이유입니다.

v6_0_folio_mapping.c

// SPDX-License-Identifier: GPL-2.0-only

/*
 * Official Linux v6.0: mm/util.c
 * https://github.com/torvalds/linux/blob/v6.0/mm/util.c#L799
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

struct address_space *folio_mapping(struct folio *folio)
{
	struct address_space *mapping;

	/* This happens if someone calls flush_dcache_page on slab page */
	if (unlikely(folio_test_slab(folio)))
		return NULL;

	if (unlikely(folio_test_swapcache(folio)))
		return swap_address_space(folio_swap_entry(folio));

	mapping = folio->mapping;
	if ((unsigned long)mapping & PAGE_MAPPING_FLAGS)
		return NULL;

	return mapping;
}

v6_0_folio_swap_entry.c

/* 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#L348
 * Exact definition/declaration; not a reconstruction of a book listing.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static inline swp_entry_t folio_swap_entry(struct folio *folio)
{
	swp_entry_t entry = { .val = page_private(&folio->page) };
	return entry;
}

마지막 helper는 v6.0에서 head page의 private 값으로 entry를 얻습니다. 같은 tag의 cache 등록은 subpage마다 연속 entry를 기록합니다. 반면 v6.12 등록은 folio->swap을 사용하므로 private field 설명을 version을 넘겨 그대로 적용하지 않습니다. RAM의 cache-held reference를 추가하는 것은 일반 object 수명 관리이며, 그 자체를 FOLL_PIN DMA pin 획득이라고 부르지 않습니다.

참고 자료