Swap-out의 네 시점: 예약, PTE 교체, I/O, RAM 반환

swap write를 제출했는데 RSS가 바로 줄지 않는 것은 이상한 일이 아닙니다. backing을 예약하는 시점, present PTE를 바꾸는 시점, storage I/O를 완료하는 시점, folio를 allocator에 돌려주는 시점이 다르기 때문입니다. 이 글은 v6.12의 ordinary swap-backed anon 경로를 기준으로 설명합니다.

Slot cache와 data cache는 다른 것입니다

folio_alloc_swap()은 사용할 swap entry를 얻습니다. ordinary 작은 folio에서는 per-CPU swap slot cache를 이용할 수 있고, 필요하면 get_swap_pages()로 area에서 slot을 확보합니다. 여기서 SWAP_BATCH=64는 slot allocation을 모으는 크기이지 data를 64 page씩 쓰거나 읽으라는 뜻이 아닙니다.

v6.12의 order-0 slot-cache allocation은 cache->alloc_lock mutex로 보호합니다. refill이 sleep할 수 있으므로 이 per-CPU 자료구조의 접근을 무조건 preemption-disabled 구간이라고 가정하면 안 됩니다. allocation 경로는 반환 slot 배열을 다루지 않아 free_lock과도 구분됩니다. LRU operation batch의 local lock 규칙을 slot cache에 그대로 적용할 수 없는 이유입니다.

swap cache는 resident data folio를 찾는 구조입니다. slot을 확보했더라도 cache metadata allocation이나 등록은 실패할 수 있습니다. add_to_swap()은 이런 실패에 slot accounting을 되돌려야 하며, 성공한 folio를 dirty로 표시해 새 backing에 내용을 보존해야 함을 분명히 합니다. 이 dirty는 I/O가 벌써 시작되었다는 표시가 아닙니다.

Cache 등록에는 folio와 자료구조 양쪽 변화가 있습니다

v6.12의 add_to_swap_cache()는 적절한 swapper address space의 XArray를 준비하고 folio에 cache-held reference를 추가합니다. PG_swapcache와 entry metadata를 설정하며 각 subpage에 대응하는 index 관계를 유지합니다. XArray node 확보는 lock 안에서 무조건 성공한다고 가정할 수 없으므로 오류와 재시도·rollback이 필요합니다.

등록 실패 시 v6.12의 helper는 cache flag와 추가 reference를 되돌리고 caller가 slot을 반환합니다. 이때 folio->swap 필드를 0으로 지우지는 않으므로, 그 필드에 값이 남았다는 사실만으로 유효한 cache membership을 판정하면 안 됩니다. user mapping이 유지하는 reference와 cache가 추가한 reference를 구분해야 이 정리가 맞습니다. 이 설명을 v6.18의 per-cluster swap table에 그대로 대입할 수는 없습니다.

ordinary order-0 storage-write 성공 경로의 시간축입니다. PTE가 바뀐 뒤에도 RAM folio는 cache에 남으며, I/O 완료 후에도 별도 reclaim 검사와 최종 free가 필요합니다. 재접근이나 다른 backend는 별도 branch입니다.
ordinary order-0 storage-write 성공 경로의 시간축입니다. PTE가 바뀐 뒤에도 RAM folio는 cache에 남으며, I/O 완료 후에도 별도 reclaim 검사와 최종 free가 필요합니다. 재접근이나 다른 backend는 별도 branch입니다.

Dirty clear 뒤 backend가 writeback을 시작합니다

unmap이 완료되고 dirty인 folio는 reclaim의 pageout()을 통해 swap write 경로로 갑니다. folio_mapping()이 반환하는 swapper operation의 writepage callback은 swap_writepage()입니다. pageout()은 dirty를 I/O용으로 정리하고 reclaim 표시를 남긴 뒤 callback을 호출합니다. PG_writeback을 실제로 시작하는 책임은 하위 backend에 있습니다.

swap_writepage()를 호출했다고 항상 disk write를 제출하는 것은 아닙니다. 더 이상 slot을 유지할 필요가 없어 cache를 정리할 수 있거나, zero-filled 내용 또는 compressed backing으로 처리하는 branch도 있습니다. 'writepage 호출 횟수 = physical disk write 수'라는 등식은 성립하지 않습니다.

storage write가 필요하면 backend에 따라 filesystem operation을 사용하거나 block I/O를 구성합니다. block 경로에서는 bio를 준비하고 folio를 writeback 상태로 전환한 뒤 unlock하여 I/O를 제출합니다. filesystem-mediated batch의 SWAP_CLUSTER_MAX는 slot allocation batch나 readahead window와 다른 정책 단위입니다.

Completion은 다시 reclaim할 기회입니다

비동기 I/O 동안에는 folio lock이 풀려 있어도 PG_writeback이 내용을 보호하는 protocol의 일부입니다. 이때 RAM을 다른 data로 재사용하면 I/O가 엉뚱한 내용을 읽을 수 있으므로 free할 수 없습니다. write error가 나면 redirty와 오류 처리를 거쳐 내용을 보존해야 합니다.

completion은 writeback 상태를 끝내고 필요한 경우 inactive-tail rotation을 요청합니다. 모든 folio가 무조건 즉시 회수되는 것은 아닙니다. callback 뒤 dirty·writeback과 lock 상태를 다시 검사해야 하며, 그 사이 fault가 같은 cache folio를 재사용할 수도 있습니다. PAGE_SUCCESS도 보통 제출 성공이지 I/O 완료나 final free가 아닙니다.

Cache에서 떼는 순간에도 refcount를 확인합니다

다음 reclaim에서 clean·unmapped·non-writeback 상태인 folio는 __remove_mapping()으로 cache 연결을 끊을 수 있습니다. v6.12에서는 XArray lock과 folio lock을 사용하고, 예상 참조 수인 1 + folio_nr_pages(folio)와 정확히 일치할 때 folio_ref_freeze()로 원자적으로 0으로 바꿉니다. 1은 reclaim isolation이 가진 참조이고 나머지는 cache의 base-page별 보유분입니다. 추가 kernel reference나 pin으로 count가 더 크면 freeze가 실패하므로 folio를 유지합니다.

freeze 뒤 dirty를 재검사하여 뒤늦은 수정이 보이면 unfreeze하고 보존합니다. cache entry는 빈 값이나 working-set shadow로 바뀌고 PG_swapcache·slot cache accounting을 정리합니다. 그 뒤 caller가 folio를 bulk free해야 allocator가 RAM을 재사용할 수 있습니다. cache 삭제 자체가 final folio free는 아닙니다.

RAM은 유지하고 slot을 반환하는 경우

resident folio를 계속 쓰더라도 swap PTE가 더 이상 없고 writeback 같은 제약을 만족하면 folio_free_swap()으로 backing/cache를 정리할 수 있습니다. 이때 cache를 떼면 folio를 dirty로 표시하여 backing 없이 남은 내용의 보존 책임을 유지합니다. present mapping이 folio를 살려 두므로 RAM은 그대로일 수 있습니다.

folio_swapped()가 참이면 아직 slot을 참조하는 swap PTE가 있는 방향이므로 자유롭게 backing을 버릴 수 없습니다. writeback 중이거나 hibernation storage 보존 같은 특별 조건에서도 제거를 제한합니다. 함수 이름에 free가 있다고 모든 entry 참조와 memory를 한 번에 버린다고 해석해서는 안 됩니다.

Slot allocation 실패와 cache 등록 실패를 분리합니다

v6.0에서는 folio_alloc_swap()이 swp_entry_t를 반환하고, add_to_swap()은 bool을 반환합니다. 앞 함수의 0 entry와 뒤 함수의 false는 서로 다른 단계의 실패입니다. 아래는 두 함수와 cache 등록의 전체 정의입니다. official tag code의 notice와 GPL-2.0을 유지했습니다.

v6_0_folio_alloc_swap.c

// SPDX-License-Identifier: GPL-2.0
/*
 * Manage cache of swap slots to be used for and returned from
 * swap.
 *
 * Copyright(c) 2016 Intel Corporation.
 *
 * Author: Tim Chen <tim.c.chen@linux.intel.com>
 *
 * We allocate the swap slots from the global pool and put
 * it into local per cpu caches.  This has the advantage
 * of no needing to acquire the swap_info lock every time
 * we need a new slot.
 *
 * There is also opportunity to simply return the slot
 * to local caches without needing to acquire swap_info
 * lock.  We do not reuse the returned slots directly but
 * move them back to the global pool in a batch.  This
 * allows the slots to coalesce and reduce fragmentation.
 *
 * The swap entry allocated is marked with SWAP_HAS_CACHE
 * flag in map_count that prevents it from being allocated
 * again from the global pool.
 *
 * The swap slots cache is protected by a mutex instead of
 * a spin lock as when we search for slots with scan_swap_map,
 * we can possibly sleep.
 */

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

swp_entry_t folio_alloc_swap(struct folio *folio)
{
	swp_entry_t entry;
	struct swap_slots_cache *cache;

	entry.val = 0;

	if (folio_test_large(folio)) {
		if (IS_ENABLED(CONFIG_THP_SWAP) && arch_thp_swp_supported())
			get_swap_pages(1, &entry, folio_nr_pages(folio));
		goto out;
	}

	/*
	 * Preemption is allowed here, because we may sleep
	 * in refill_swap_slots_cache().  But it is safe, because
	 * accesses to the per-CPU data structure are protected by the
	 * mutex cache->alloc_lock.
	 *
	 * The alloc path here does not touch cache->slots_ret
	 * so cache->free_lock is not taken.
	 */
	cache = raw_cpu_ptr(&swp_slots);

	if (likely(check_cache_active() && cache->slots)) {
		mutex_lock(&cache->alloc_lock);
		if (cache->slots) {
repeat:
			if (cache->nr) {
				entry = cache->slots[cache->cur];
				cache->slots[cache->cur++].val = 0;
				cache->nr--;
			} else if (refill_swap_slots_cache(cache)) {
				goto repeat;
			}
		}
		mutex_unlock(&cache->alloc_lock);
		if (entry.val)
			goto out;
	}

	get_swap_pages(1, &entry, 1);
out:
	if (mem_cgroup_try_charge_swap(folio, entry)) {
		put_swap_page(&folio->page, entry);
		entry.val = 0;
	}
	return entry;
}

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

bool add_to_swap(struct folio *folio)
{
	swp_entry_t entry;
	int err;

	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
	VM_BUG_ON_FOLIO(!folio_test_uptodate(folio), folio);

	entry = folio_alloc_swap(folio);
	if (!entry.val)
		return false;

	/*
	 * XArray node allocations from PF_MEMALLOC contexts could
	 * completely exhaust the page allocator. __GFP_NOMEMALLOC
	 * stops emergency reserves from being allocated.
	 *
	 * TODO: this could cause a theoretical memory reclaim
	 * deadlock in the swap out path.
	 */
	/*
	 * Add it to the swap cache.
	 */
	err = add_to_swap_cache(&folio->page, entry,
			__GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN, NULL);
	if (err)
		/*
		 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
		 * clear SWAP_HAS_CACHE flag.
		 */
		goto fail;
	/*
	 * Normally the folio will be dirtied in unmap because its
	 * pte should be dirty. A special case is MADV_FREE page. The
	 * page's pte could have dirty bit cleared but the folio's
	 * SwapBacked flag is still set because clearing the dirty bit
	 * and SwapBacked flag has no lock protected. For such folio,
	 * unmap will not set dirty bit for it, so folio reclaim will
	 * not write the folio out. This can cause data corruption when
	 * the folio is swapped in later. Always setting the dirty flag
	 * for the folio solves the problem.
	 */
	folio_mark_dirty(folio);

	return true;

fail:
	put_swap_page(&folio->page, entry);
	return false;
}

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

int add_to_swap_cache(struct page *page, swp_entry_t entry,
			gfp_t gfp, void **shadowp)
{
	struct address_space *address_space = swap_address_space(entry);
	pgoff_t idx = swp_offset(entry);
	XA_STATE_ORDER(xas, &address_space->i_pages, idx, compound_order(page));
	unsigned long i, nr = thp_nr_pages(page);
	void *old;

	VM_BUG_ON_PAGE(!PageLocked(page), page);
	VM_BUG_ON_PAGE(PageSwapCache(page), page);
	VM_BUG_ON_PAGE(!PageSwapBacked(page), page);

	page_ref_add(page, nr);
	SetPageSwapCache(page);

	do {
		xas_lock_irq(&xas);
		xas_create_range(&xas);
		if (xas_error(&xas))
			goto unlock;
		for (i = 0; i < nr; i++) {
			VM_BUG_ON_PAGE(xas.xa_index != idx + i, page);
			old = xas_load(&xas);
			if (xa_is_value(old)) {
				if (shadowp)
					*shadowp = old;
			}
			set_page_private(page + i, entry.val + i);
			xas_store(&xas, page);
			xas_next(&xas);
		}
		address_space->nrpages += nr;
		__mod_node_page_state(page_pgdat(page), NR_FILE_PAGES, nr);
		__mod_lruvec_page_state(page, NR_SWAPCACHE, nr);
unlock:
		xas_unlock_irq(&xas);
	} while (xas_nomem(&xas, gfp));

	if (!xas_error(&xas))
		return 0;

	ClearPageSwapCache(page);
	page_ref_sub(page, nr);
	return xas_error(&xas);
}

large folio는 THP swap과 architecture 지원이 있을 때 연속한 base-page 수만큼 별도 배정을 시도합니다. 작은 folio는 mutex로 보호한 per-CPU slot cache에서 하나를 꺼내고, 비었으면 refill 후 다시 봅니다. cache가 비활성이거나 slot을 얻지 못했다면 get_swap_pages()로 fallback합니다. 마지막 memcg charge가 실패해도 이미 얻은 slot을 반환해야 합니다.

cache 등록은 XA_STATE_ORDER로 범위를 준비하고 base-page 수만큼 reference를 추가합니다. XArray lock 안에서 range 생성이 실패하면 unlock한 뒤 xas_nomem()으로 memory 확보·재시도를 시도합니다. 끝내 실패하면 cache flag와 추가 reference를 되돌립니다. 이 helper는 -EEXIST를 반환하는 reservation 경쟁용 helper가 아니므로 add_to_swap()은 자신의 배정 entry의 cache 보유 상태를 안전하게 정리할 수 있습니다.

성공한 folio를 항상 dirty로 만드는 데에는 MADV_FREE 경쟁도 포함됩니다. PTE dirty가 이미 clear되었지만 swap-backed flag가 아직 남은 순간에는 뒤의 unmap만 믿고 dirty 전파를 기대할 수 없습니다. 내용이 있는 folio에 새 backing을 배정한 이상 writeout 필요성을 잃지 않아야 합니다.

v6.12는 같은 boolean 예약 wrapper를 유지하되 cache 등록 인자는 folio이고 entry는 folio->swap에 둡니다. subpage의 page_private()를 채우는 v6.0 code와 구별해 보십시오.

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

bool add_to_swap(struct folio *folio)
{
	swp_entry_t entry;
	int err;

	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
	VM_BUG_ON_FOLIO(!folio_test_uptodate(folio), folio);

	entry = folio_alloc_swap(folio);
	if (!entry.val)
		return false;

	/*
	 * XArray node allocations from PF_MEMALLOC contexts could
	 * completely exhaust the page allocator. __GFP_NOMEMALLOC
	 * stops emergency reserves from being allocated.
	 *
	 * TODO: this could cause a theoretical memory reclaim
	 * deadlock in the swap out path.
	 */
	/*
	 * Add it to the swap cache.
	 */
	err = add_to_swap_cache(folio, entry,
			__GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN, NULL);
	if (err)
		/*
		 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
		 * clear SWAP_HAS_CACHE flag.
		 */
		goto fail;
	/*
	 * Normally the folio will be dirtied in unmap because its
	 * pte should be dirty. A special case is MADV_FREE page. The
	 * page's pte could have dirty bit cleared but the folio's
	 * SwapBacked flag is still set because clearing the dirty bit
	 * and SwapBacked flag has no lock protected. For such folio,
	 * unmap will not set dirty bit for it, so folio reclaim will
	 * not write the folio out. This can cause data corruption when
	 * the folio is swapped in later. Always setting the dirty flag
	 * for the folio solves the problem.
	 */
	folio_mark_dirty(folio);

	return true;

fail:
	put_swap_folio(folio, entry);
	return false;
}

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

int add_to_swap_cache(struct folio *folio, swp_entry_t entry,
			gfp_t gfp, void **shadowp)
{
	struct address_space *address_space = swap_address_space(entry);
	pgoff_t idx = swap_cache_index(entry);
	XA_STATE_ORDER(xas, &address_space->i_pages, idx, folio_order(folio));
	unsigned long i, nr = folio_nr_pages(folio);
	void *old;

	xas_set_update(&xas, workingset_update_node);

	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
	VM_BUG_ON_FOLIO(folio_test_swapcache(folio), folio);
	VM_BUG_ON_FOLIO(!folio_test_swapbacked(folio), folio);

	folio_ref_add(folio, nr);
	folio_set_swapcache(folio);
	folio->swap = entry;

	do {
		xas_lock_irq(&xas);
		xas_create_range(&xas);
		if (xas_error(&xas))
			goto unlock;
		for (i = 0; i < nr; i++) {
			VM_BUG_ON_FOLIO(xas.xa_index != idx + i, folio);
			if (shadowp) {
				old = xas_load(&xas);
				if (xa_is_value(old))
					*shadowp = old;
			}
			xas_store(&xas, folio);
			xas_next(&xas);
		}
		address_space->nrpages += nr;
		__node_stat_mod_folio(folio, NR_FILE_PAGES, nr);
		__lruvec_stat_mod_folio(folio, NR_SWAPCACHE, nr);
unlock:
		xas_unlock_irq(&xas);
	} while (xas_nomem(&xas, gfp));

	if (!xas_error(&xas))
		return 0;

	folio_clear_swapcache(folio);
	folio_ref_sub(folio, nr);
	return xas_error(&xas);
}

v6.18에서는 아래 하나의 folio_alloc_swap(folio, gfp)가 slot 배정과 swap table 연결을 수행합니다. 성공은 0, THP swap이 꺼진 large allocation은 -EAGAIN, cluster보다 큰 요청은 -EINVAL, 공간·charge 실패는 -ENOMEM입니다. local lock을 쓰는 per-CPU cluster 경로를 고전 slot-cache mutex 경로와 섞지 않습니다.

v6_18_folio_alloc_swap.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/swapfile.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *  Swap reorganised 29.12.95, Stephen Tweedie
 */

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

int folio_alloc_swap(struct folio *folio, gfp_t gfp)
{
	unsigned int order = folio_order(folio);
	unsigned int size = 1 << order;
	swp_entry_t entry = {};

	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
	VM_BUG_ON_FOLIO(!folio_test_uptodate(folio), folio);

	if (order) {
		/*
		 * Reject large allocation when THP_SWAP is disabled,
		 * the caller should split the folio and try again.
		 */
		if (!IS_ENABLED(CONFIG_THP_SWAP))
			return -EAGAIN;

		/*
		 * Allocation size should never exceed cluster size
		 * (HPAGE_PMD_SIZE).
		 */
		if (size > SWAPFILE_CLUSTER) {
			VM_WARN_ON_ONCE(1);
			return -EINVAL;
		}
	}

	local_lock(&percpu_swap_cluster.lock);
	if (!swap_alloc_fast(&entry, order))
		swap_alloc_slow(&entry, order);
	local_unlock(&percpu_swap_cluster.lock);

	/* Need to call this even if allocation failed, for MEMCG_SWAP_FAIL. */
	if (mem_cgroup_try_charge_swap(folio, entry))
		goto out_free;

	if (!entry.val)
		return -ENOMEM;

	swap_cache_add_folio(folio, entry, NULL);

	return 0;

out_free:
	put_swap_folio(folio, entry);
	return -ENOMEM;
}

Write 제출과 completion의 전체 경로

고전 v6.0 pageout()은 caller의 freeable 검사, mapping·private buffer 처리, dirty clear와 callback을 함께 보여 줍니다. nr_to_write=SWAP_CLUSTER_MAX는 writeback budget이며 실제로 32 page의 disk write가 발생했다는 결과가 아닙니다. writepage가 AOP_WRITEPAGE_ACTIVATE를 반환하면 reclaim 표시를 지우고 PAGE_ACTIVATE를 반환합니다. 음수 오류를 기록하는 것과 backend가 folio를 unlock했는지도 읽어야 합니다.

v6_0_pageout.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar (kanoj@sgi.com).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

static pageout_t pageout(struct folio *folio, struct address_space *mapping,
			 struct swap_iocb **plug)
{
	/*
	 * If the folio is dirty, only perform writeback if that write
	 * will be non-blocking.  To prevent this allocation from being
	 * stalled by pagecache activity.  But note that there may be
	 * stalls if we need to run get_block().  We could test
	 * PagePrivate for that.
	 *
	 * If this process is currently in __generic_file_write_iter() against
	 * this folio's queue, we can perform writeback even if that
	 * will block.
	 *
	 * If the folio is swapcache, write it back even if that would
	 * block, for some throttling. This happens by accident, because
	 * swap_backing_dev_info is bust: it doesn't reflect the
	 * congestion state of the swapdevs.  Easy to fix, if needed.
	 */
	if (!is_page_cache_freeable(folio))
		return PAGE_KEEP;
	if (!mapping) {
		/*
		 * Some data journaling orphaned folios can have
		 * folio->mapping == NULL while being dirty with clean buffers.
		 */
		if (folio_test_private(folio)) {
			if (try_to_free_buffers(folio)) {
				folio_clear_dirty(folio);
				pr_info("%s: orphaned folio\n", __func__);
				return PAGE_CLEAN;
			}
		}
		return PAGE_KEEP;
	}
	if (mapping->a_ops->writepage == NULL)
		return PAGE_ACTIVATE;

	if (folio_clear_dirty_for_io(folio)) {
		int res;
		struct writeback_control wbc = {
			.sync_mode = WB_SYNC_NONE,
			.nr_to_write = SWAP_CLUSTER_MAX,
			.range_start = 0,
			.range_end = LLONG_MAX,
			.for_reclaim = 1,
			.swap_plug = plug,
		};

		folio_set_reclaim(folio);
		res = mapping->a_ops->writepage(&folio->page, &wbc);
		if (res < 0)
			handle_write_error(mapping, folio, res);
		if (res == AOP_WRITEPAGE_ACTIVATE) {
			folio_clear_reclaim(folio);
			return PAGE_ACTIVATE;
		}

		if (!folio_test_writeback(folio)) {
			/* synchronous write or broken a_ops? */
			folio_clear_reclaim(folio);
		}
		trace_mm_vmscan_write_folio(folio);
		node_stat_add_folio(folio, NR_VMSCAN_WRITE);
		return PAGE_SUCCESS;
	}

	return PAGE_CLEAN;
}

v6_0_swap_writepage.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  linux/mm/page_io.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, 
 *  Asynchronous swapping added 30.12.95. Stephen Tweedie
 *  Removed race in async swapping. 14.4.1996. Bruno Haible
 *  Add swap of shared pages through the page cache. 20.2.1998. Stephen Tweedie
 *  Always use brw_page, life becomes simpler. 12 May 1998 Eric Biederman
 */

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

int swap_writepage(struct page *page, struct writeback_control *wbc)
{
	int ret = 0;

	if (try_to_free_swap(page)) {
		unlock_page(page);
		goto out;
	}
	/*
	 * Arch code may have to preserve more data than just the page
	 * contents, e.g. memory tags.
	 */
	ret = arch_prepare_to_swap(page);
	if (ret) {
		set_page_dirty(page);
		unlock_page(page);
		goto out;
	}
	if (frontswap_store(page) == 0) {
		set_page_writeback(page);
		unlock_page(page);
		end_page_writeback(page);
		goto out;
	}
	ret = __swap_writepage(page, wbc, end_swap_bio_write);
out:
	return ret;
}

v6_0__swap_writepage.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  linux/mm/page_io.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, 
 *  Asynchronous swapping added 30.12.95. Stephen Tweedie
 *  Removed race in async swapping. 14.4.1996. Bruno Haible
 *  Add swap of shared pages through the page cache. 20.2.1998. Stephen Tweedie
 *  Always use brw_page, life becomes simpler. 12 May 1998 Eric Biederman
 */

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

int __swap_writepage(struct page *page, struct writeback_control *wbc,
		     bio_end_io_t end_write_func)
{
	struct bio *bio;
	int ret;
	struct swap_info_struct *sis = page_swap_info(page);

	VM_BUG_ON_PAGE(!PageSwapCache(page), page);
	/*
	 * ->flags can be updated non-atomicially (scan_swap_map_slots),
	 * but that will never affect SWP_FS_OPS, so the data_race
	 * is safe.
	 */
	if (data_race(sis->flags & SWP_FS_OPS))
		return swap_writepage_fs(page, wbc);

	ret = bdev_write_page(sis->bdev, swap_page_sector(page), page, wbc);
	if (!ret) {
		count_swpout_vm_event(page);
		return 0;
	}

	bio = bio_alloc(sis->bdev, 1,
			REQ_OP_WRITE | REQ_SWAP | wbc_to_write_flags(wbc),
			GFP_NOIO);
	bio->bi_iter.bi_sector = swap_page_sector(page);
	bio->bi_end_io = end_write_func;
	bio_add_page(bio, page, thp_size(page), 0);

	bio_associate_blkg_from_page(bio, page);
	count_swpout_vm_event(page);
	set_page_writeback(page);
	unlock_page(page);
	submit_bio(bio);

	return 0;
}

v6_0_end_swap_bio_write.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  linux/mm/page_io.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, 
 *  Asynchronous swapping added 30.12.95. Stephen Tweedie
 *  Removed race in async swapping. 14.4.1996. Bruno Haible
 *  Add swap of shared pages through the page cache. 20.2.1998. Stephen Tweedie
 *  Always use brw_page, life becomes simpler. 12 May 1998 Eric Biederman
 */

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

void end_swap_bio_write(struct bio *bio)
{
	struct page *page = bio_first_page_all(bio);

	if (bio->bi_status) {
		SetPageError(page);
		/*
		 * We failed to write the page out to swap-space.
		 * Re-dirty the page in order to avoid it being reclaimed.
		 * Also print a dire warning that things will go BAD (tm)
		 * very quickly.
		 *
		 * Also clear PG_reclaim to avoid folio_rotate_reclaimable()
		 */
		set_page_dirty(page);
		pr_alert_ratelimited("Write-error on swap-device (%u:%u:%llu)\n",
				     MAJOR(bio_dev(bio)), MINOR(bio_dev(bio)),
				     (unsigned long long)bio->bi_iter.bi_sector);
		ClearPageReclaim(page);
	}
	end_page_writeback(page);
	bio_put(bio);
}

이 tag의 swap_writepage()은 불필요해진 cache를 제거할 수 있으면 unlock하고 write를 생략합니다. architecture metadata 준비 실패는 redirty하고 종료하며, frontswap 저장 성공은 일반 block write 대신 해당 backend에서 writeback을 마칩니다. filesystem-mediated 경로와 bdev_write_page() 성공 경로도 bio 직접 제출과 별개입니다.

bio 경로는 sector·callback·page 범위·block-cgroup 연결을 정한 뒤 writeback을 시작하고 page를 unlock하여 제출합니다. write completion 오류는 error와 dirty를 설정하고 reclaim 표시를 지워 잘못된 eviction을 막습니다. 성공·실패 모두 writeback을 끝내고 bio reference를 반환합니다. 완료 뒤 회수 순서에 가까워질 수는 있어도 callback이 folio를 allocator에 직접 반환하는 것은 아닙니다.

Freeze는 dirty 검사보다 앞에 있어야 합니다

아래 v6.0 __remove_mapping()은 기대한 count를 0으로 freeze한 뒤 dirty를 검사합니다. 반대로 dirty를 먼저 보고 나중에 count만 확인하면, 중간에 reference를 얻은 writer가 수정·dirty 설정·put을 끝내 count만 원래대로 돌아오는 경쟁을 놓칠 수 있습니다. atomic freeze의 ordering과 뒤의 dirty 관찰이 함께 필요합니다.

v6_0__remove_mapping.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar (kanoj@sgi.com).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
			    bool reclaimed, struct mem_cgroup *target_memcg)
{
	int refcount;
	void *shadow = NULL;

	BUG_ON(!folio_test_locked(folio));
	BUG_ON(mapping != folio_mapping(folio));

	if (!folio_test_swapcache(folio))
		spin_lock(&mapping->host->i_lock);
	xa_lock_irq(&mapping->i_pages);
	/*
	 * The non racy check for a busy page.
	 *
	 * Must be careful with the order of the tests. When someone has
	 * a ref to the page, it may be possible that they dirty it then
	 * drop the reference. So if PageDirty is tested before page_count
	 * here, then the following race may occur:
	 *
	 * get_user_pages(&page);
	 * [user mapping goes away]
	 * write_to(page);
	 *				!PageDirty(page)    [good]
	 * SetPageDirty(page);
	 * put_page(page);
	 *				!page_count(page)   [good, discard it]
	 *
	 * [oops, our write_to data is lost]
	 *
	 * Reversing the order of the tests ensures such a situation cannot
	 * escape unnoticed. The smp_rmb is needed to ensure the page->flags
	 * load is not satisfied before that of page->_refcount.
	 *
	 * Note that if SetPageDirty is always performed via set_page_dirty,
	 * and thus under the i_pages lock, then this ordering is not required.
	 */
	refcount = 1 + folio_nr_pages(folio);
	if (!folio_ref_freeze(folio, refcount))
		goto cannot_free;
	/* note: atomic_cmpxchg in page_ref_freeze provides the smp_rmb */
	if (unlikely(folio_test_dirty(folio))) {
		folio_ref_unfreeze(folio, refcount);
		goto cannot_free;
	}

	if (folio_test_swapcache(folio)) {
		swp_entry_t swap = folio_swap_entry(folio);
		mem_cgroup_swapout(folio, swap);
		if (reclaimed && !mapping_exiting(mapping))
			shadow = workingset_eviction(folio, target_memcg);
		__delete_from_swap_cache(folio, swap, shadow);
		xa_unlock_irq(&mapping->i_pages);
		put_swap_page(&folio->page, swap);
	} else {
		void (*free_folio)(struct folio *);

		free_folio = mapping->a_ops->free_folio;
		/*
		 * Remember a shadow entry for reclaimed file cache in
		 * order to detect refaults, thus thrashing, later on.
		 *
		 * But don't store shadows in an address space that is
		 * already exiting.  This is not just an optimization,
		 * inode reclaim needs to empty out the radix tree or
		 * the nodes are lost.  Don't plant shadows behind its
		 * back.
		 *
		 * We also don't store shadows for DAX mappings because the
		 * only page cache pages found in these are zero pages
		 * covering holes, and because we don't want to mix DAX
		 * exceptional entries and shadow exceptional entries in the
		 * same address_space.
		 */
		if (reclaimed && folio_is_file_lru(folio) &&
		    !mapping_exiting(mapping) && !dax_mapping(mapping))
			shadow = workingset_eviction(folio, target_memcg);
		__filemap_remove_folio(folio, shadow);
		xa_unlock_irq(&mapping->i_pages);
		if (mapping_shrinkable(mapping))
			inode_add_lru(mapping->host);
		spin_unlock(&mapping->host->i_lock);

		if (free_folio)
			free_folio(folio);
	}

	return 1;

cannot_free:
	xa_unlock_irq(&mapping->i_pages);
	if (!folio_test_swapcache(folio))
		spin_unlock(&mapping->host->i_lock);
	return 0;
}

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

void __delete_from_swap_cache(struct folio *folio,
			swp_entry_t entry, void *shadow)
{
	struct address_space *address_space = swap_address_space(entry);
	int i;
	long nr = folio_nr_pages(folio);
	pgoff_t idx = swp_offset(entry);
	XA_STATE(xas, &address_space->i_pages, idx);

	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
	VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);
	VM_BUG_ON_FOLIO(folio_test_writeback(folio), folio);

	for (i = 0; i < nr; i++) {
		void *entry = xas_store(&xas, shadow);
		VM_BUG_ON_PAGE(entry != folio, entry);
		set_page_private(folio_page(folio, i), 0);
		xas_next(&xas);
	}
	folio_clear_swapcache(folio);
	address_space->nrpages -= nr;
	__node_stat_mod_folio(folio, NR_FILE_PAGES, -nr);
	__lruvec_stat_mod_folio(folio, NR_SWAPCACHE, -nr);
}

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

void delete_from_swap_cache(struct folio *folio)
{
	swp_entry_t entry = folio_swap_entry(folio);
	struct address_space *address_space = swap_address_space(entry);

	xa_lock_irq(&address_space->i_pages);
	__delete_from_swap_cache(folio, entry, NULL);
	xa_unlock_irq(&address_space->i_pages);

	put_swap_page(&folio->page, entry);
	folio_ref_sub(folio, folio_nr_pages(folio));
}

low-level 삭제는 각 XArray index를 shadow 또는 빈 값으로 바꾸고 private entry·cache flag·통계를 정리합니다. 이 함수 자체가 cache-held folio reference를 무조건 put하는 것은 아닙니다. 이미 freeze한 reclaim caller와, 별도 reference를 보유한 delete_from_swap_cache() wrapper가 각자의 reference protocol을 마무리합니다. wrapper는 cache-held base-page 수만큼 count를 줄이지만 caller가 가진 reference 때문에 이 호출만으로 final free하지 않습니다.

Cache를 버릴 수 있는 조건을 끝까지 검사합니다

v6.0 try_to_free_swap()의 성공은 swap cache이며 writeback 중이 아니고 folio_swapped()가 false이며 hibernation storage 보호에 막히지 않을 때입니다. '참조가 하나 이상 있으므로 free한다'는 반대 해석은 맞지 않습니다. 여기서 남은 swap reference와 resident folio reference는 다른 수입니다.

v6_0_try_to_free_swap.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/swapfile.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *  Swap reorganised 29.12.95, Stephen Tweedie
 */

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

int try_to_free_swap(struct page *page)
{
	struct folio *folio = page_folio(page);
	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);

	if (!folio_test_swapcache(folio))
		return 0;
	if (folio_test_writeback(folio))
		return 0;
	if (folio_swapped(folio))
		return 0;

	/*
	 * Once hibernation has begun to create its image of memory,
	 * there's a danger that one of the calls to try_to_free_swap()
	 * - most probably a call from __try_to_reclaim_swap() while
	 * hibernation is allocating its own swap pages for the image,
	 * but conceivably even a call from memory reclaim - will free
	 * the swap from a page which has already been recorded in the
	 * image as a clean swapcache page, and then reuse its swap for
	 * another page of the image.  On waking from hibernation, the
	 * original page might be freed under memory pressure, then
	 * later read back in from swap, now with the wrong data.
	 *
	 * Hibernation suspends storage while it is writing the image
	 * to disk so check that here.
	 */
	if (pm_suspended_storage())
		return 0;

	delete_from_swap_cache(folio);
	folio_set_dirty(folio);
	return 1;
}

v6_12_folio_swapcache_freeable.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/swapfile.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *  Swap reorganised 29.12.95, Stephen Tweedie
 */

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

static bool folio_swapcache_freeable(struct folio *folio)
{
	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);

	if (!folio_test_swapcache(folio))
		return false;
	if (folio_test_writeback(folio))
		return false;

	/*
	 * Once hibernation has begun to create its image of memory,
	 * there's a danger that one of the calls to folio_free_swap()
	 * - most probably a call from __try_to_reclaim_swap() while
	 * hibernation is allocating its own swap pages for the image,
	 * but conceivably even a call from memory reclaim - will free
	 * the swap from a folio which has already been recorded in the
	 * image as a clean swapcache folio, and then reuse its swap for
	 * another page of the image.  On waking from hibernation, the
	 * original folio might be freed under memory pressure, then
	 * later read back in from swap, now with the wrong data.
	 *
	 * Hibernation suspends storage while it is writing the image
	 * to disk so check that here.
	 */
	if (pm_suspended_storage())
		return false;

	return true;
}

v6_12_folio_free_swap.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/swapfile.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *  Swap reorganised 29.12.95, Stephen Tweedie
 */

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

bool folio_free_swap(struct folio *folio)
{
	if (!folio_swapcache_freeable(folio))
		return false;
	if (folio_swapped(folio))
		return false;

	delete_from_swap_cache(folio);
	folio_set_dirty(folio);
	return true;
}

v6.12는 공통 freeable 검사를 별도 helper로 분리합니다. hibernation이 clean swapcache의 slot을 image에 기록해 둔 뒤 이를 다른 내용에 재사용하면 복귀 후 잘못된 내용을 읽을 수 있어 storage-suspended 상태를 제한합니다. 조건을 통과한 cache 제거 후에는 RAM 내용의 보존 책임이 계속되므로 dirty로 남깁니다.

참고 자료