Swap cache miss에서 같은 slot을 두 번 읽지 않는 방법

여러 thread가 같은 swap entry에서 fault를 일으키면 모두 cache miss를 볼 수 있습니다. 각자 folio를 만들고 같은 slot을 읽기 시작하면 resident object가 중복되고 slot 재사용과도 충돌합니다. cache lookup과 read 준비 사이에는 별도의 ownership protocol이 필요합니다.

Lookup은 resident object의 reference를 반환합니다

v6.12 swap cache lookup은 area lifetime을 보호하면서 swapper address space의 XArray에서 folio를 찾고 reference를 얻습니다. returned folio를 lock하거나 uptodate로 만드는 일은 뒤의 caller 책임입니다. v6.18에서는 table lookup으로 구조가 달라져도 '찾음과 내용 준비는 별개'라는 구분은 유지됩니다.

lookup이 readahead로 미리 읽은 folio를 만나면 그 표식을 clear하고 hit 통계를 갱신할 수 있습니다. 이것은 그 자리에서 다음 readahead I/O를 연쇄 시작한다는 뜻이 아닙니다. 다음 cache-miss read에서 window를 계산할 때 쓸 feedback입니다.

Cache를 다시 보는 데 이유가 있습니다

read_swap_cache_async() 계열은 실제 read를 호출하기 전에 __read_swap_cache_async()로 folio를 찾거나 새 cache entry를 준비합니다. 이미 caller가 miss를 보았더라도 다시 확인합니다. allocation이나 scheduling 사이 다른 thread가 먼저 등록했을 수 있기 때문입니다.

사용되지 않은 slot은 불필요한 readahead 대상에서 건너뛸 수 있습니다. 다만 swapoff 중 slot cache가 disable된 경우에는 cache 삽입과 SWAP_HAS_CACHE 표시 사이의 경쟁을 고려해야 하므로 단순 swapcount=0 조건으로 항상 중단하지 않습니다. area가 더 이상 사용 가능하지 않다면 lookup 자체를 포기해야 합니다.

새 folio allocation을 먼저 한 뒤 swapcache_prepare()로 slot의 cache ownership을 확보합니다. reservation을 먼저 잡고 memory allocation을 기다리면 같은 entry를 기다리는 다른 fault를 오래 묶을 수 있기 때문입니다. allocation과 reservation 사이에도 slot이 사라질 수 있으므로 실패 처리는 필수입니다.

EEXIST는 slot이 없어졌다는 뜻이 아닙니다

-EEXIST는 다른 경로가 이미 cache 보유 상태를 확보했다는 신호입니다. 그 object가 아직 실제 cache에 보이지 않을 수 있으므로 잠시 기다렸다가 다시 lookup할 수 있습니다. slot 해제나 다른 오류를 뜻하는 모든 실패와 같은 것으로 처리해서는 안 됩니다.

재귀적인 동일-entry read가 자기 reservation을 기다리며 영원히 도는 문제도 있습니다. caller가 skip_if_exists 같은 제어를 사용하는 이유입니다. 'race면 무조건 재시도'만으로 설명하면 이 종료 조건을 놓칩니다.

등록과 I/O는 서로 다른 성공 단계입니다

reservation이 성공하면 folio를 locked·swap-backed 상태로 준비하고 cache에 삽입하며 charge·통계를 맞춥니다. 실패하면 reservation, allocation, folio reference를 정리합니다. 새로 등록한 경우에만 caller가 backing read를 시작해야 합니다. 이미 있던 folio를 얻은 caller가 같은 read를 다시 제출하지 않습니다.

read backend는 SWP_FS_OPS이면 filesystem 경로를, 그 밖에는 block path 등의 구현을 사용합니다. SWP_SYNCHRONOUS_IO는 sequential-access 선호라는 뜻이 아닙니다. synchronous block read와 asynchronous submission의 선택에 관련된 backend 성격입니다. completion은 성공 시 uptodate를 설정하고 folio를 unlock하며 오류 시 fault handler가 실패를 확인할 수 있도록 상태를 남깁니다.

함수 이름에 async가 들어 있어도 반환된 object의 data가 이미 사용 가능하다는 의미는 아닙니다. caller는 lock·retry protocol로 completion과 동기화해야 합니다. cache lifetime과 content readiness를 분리하는 것이 중복 read와 잘못된 mapping을 동시에 피하는 방법입니다.

Lookup의 반환 계약도 version에 속합니다

v6.0 lookup_swap_cache()은 device-user reference를 내부에서 확보·반환하며 page reference를 돌려줍니다. v6.12 swap_cache_get_folio()는 XArray lookup과 readahead feedback을 수행하지만 이 함수 자체에서 get_swap_device()를 호출하지 않습니다. 이 경로의 caller가 area 수명을 보호한다는 조건과 함께 읽어야 합니다. large folio는 이 tag에서 readahead 표식 처리를 건너뜁니다.

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

struct page *lookup_swap_cache(swp_entry_t entry, struct vm_area_struct *vma,
			       unsigned long addr)
{
	struct page *page;
	struct swap_info_struct *si;

	si = get_swap_device(entry);
	if (!si)
		return NULL;
	page = find_get_page(swap_address_space(entry), swp_offset(entry));
	put_swap_device(si);

	if (page) {
		bool vma_ra = swap_use_vma_readahead();
		bool readahead;

		/*
		 * At the moment, we don't support PG_readahead for anon THP
		 * so let's bail out rather than confusing the readahead stat.
		 */
		if (unlikely(PageTransCompound(page)))
			return page;

		readahead = TestClearPageReadahead(page);
		if (vma && vma_ra) {
			unsigned long ra_val;
			int win, hits;

			ra_val = GET_SWAP_RA_VAL(vma);
			win = SWAP_RA_WIN(ra_val);
			hits = SWAP_RA_HITS(ra_val);
			if (readahead)
				hits = min_t(int, hits + 1, SWAP_RA_HITS_MAX);
			atomic_long_set(&vma->swap_readahead_info,
					SWAP_RA_VAL(addr, win, hits));
		}

		if (readahead) {
			count_vm_event(SWAP_RA_HIT);
			if (!vma || !vma_ra)
				atomic_inc(&swapin_readahead_hits);
		}
	}

	return page;
}

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

struct folio *swap_cache_get_folio(swp_entry_t entry,
		struct vm_area_struct *vma, unsigned long addr)
{
	struct folio *folio;

	folio = filemap_get_folio(swap_address_space(entry), swap_cache_index(entry));
	if (!IS_ERR(folio)) {
		bool vma_ra = swap_use_vma_readahead();
		bool readahead;

		/*
		 * At the moment, we don't support PG_readahead for anon THP
		 * so let's bail out rather than confusing the readahead stat.
		 */
		if (unlikely(folio_test_large(folio)))
			return folio;

		readahead = folio_test_clear_readahead(folio);
		if (vma && vma_ra) {
			unsigned long ra_val;
			int win, hits;

			ra_val = GET_SWAP_RA_VAL(vma);
			win = SWAP_RA_WIN(ra_val);
			hits = SWAP_RA_HITS(ra_val);
			if (readahead)
				hits = min_t(int, hits + 1, SWAP_RA_HITS_MAX);
			atomic_long_set(&vma->swap_readahead_info,
					SWAP_RA_VAL(addr, win, hits));
		}

		if (readahead) {
			count_vm_event(SWAP_RA_HIT);
			if (!vma || !vma_ra)
				atomic_inc(&swapin_readahead_hits);
		}
	} else {
		folio = NULL;
	}

	return folio;
}

VMA 방식이 선택되면 lookup은 저장된 window·hits를 읽고, readahead 표식이 있었을 때 hits를 상한까지 증가시키며 현재 주소를 기록합니다. VMA가 없거나 cluster 방식이면 실제 readahead hit에서 global atomic counter를 증가시킵니다. 모든 cache hit가 readahead hit는 아닙니다.

v6.18은 다음처럼 table entry를 읽고 folio reference 획득을 재시도합니다. shadow·빈 값은 resident folio가 아니므로 NULL입니다. 내부 table read의 보호만으로 caller의 folio lock까지 생기지는 않습니다. 이 code 역시 area·slot 수명에 관한 caller 계약과 함께 사용해야 합니다.

v6_18_swap_cache_get_folio.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.18: mm/swap_state.c
 * https://github.com/torvalds/linux/blob/v6.18/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.
 */

struct folio *swap_cache_get_folio(swp_entry_t entry)
{
	unsigned long swp_tb;
	struct folio *folio;

	for (;;) {
		swp_tb = swap_table_get(__swap_entry_to_cluster(entry),
					swp_cluster_offset(entry));
		if (!swp_tb_is_folio(swp_tb))
			return NULL;
		folio = swp_tb_to_folio(swp_tb);
		if (likely(folio_try_get(folio)))
			return folio;
	}

	return NULL;
}

Reservation 뒤에는 charge와 등록 실패도 남습니다

아래는 v6.0의 준비 helper와 read wrapper 전체입니다. cache를 재조회할 때 feedback을 다시 기록하는 lookup을 호출하지 않는 이유도 code에서 확인할 수 있습니다.

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

struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
			struct vm_area_struct *vma, unsigned long addr,
			bool *new_page_allocated)
{
	struct swap_info_struct *si;
	struct page *page;
	void *shadow = NULL;

	*new_page_allocated = false;

	for (;;) {
		int err;
		/*
		 * First check the swap cache.  Since this is normally
		 * called after lookup_swap_cache() failed, re-calling
		 * that would confuse statistics.
		 */
		si = get_swap_device(entry);
		if (!si)
			return NULL;
		page = find_get_page(swap_address_space(entry),
				     swp_offset(entry));
		put_swap_device(si);
		if (page)
			return page;

		/*
		 * Just skip read ahead for unused swap slot.
		 * During swap_off when swap_slot_cache is disabled,
		 * we have to handle the race between putting
		 * swap entry in swap cache and marking swap slot
		 * as SWAP_HAS_CACHE.  That's done in later part of code or
		 * else swap_off will be aborted if we return NULL.
		 */
		if (!__swp_swapcount(entry) && swap_slot_cache_enabled)
			return NULL;

		/*
		 * Get a new page to read into from swap.  Allocate it now,
		 * before marking swap_map SWAP_HAS_CACHE, when -EEXIST will
		 * cause any racers to loop around until we add it to cache.
		 */
		page = alloc_page_vma(gfp_mask, vma, addr);
		if (!page)
			return NULL;

		/*
		 * Swap entry may have been freed since our caller observed it.
		 */
		err = swapcache_prepare(entry);
		if (!err)
			break;

		put_page(page);
		if (err != -EEXIST)
			return NULL;

		/*
		 * We might race against __delete_from_swap_cache(), and
		 * stumble across a swap_map entry whose SWAP_HAS_CACHE
		 * has not yet been cleared.  Or race against another
		 * __read_swap_cache_async(), which has set SWAP_HAS_CACHE
		 * in swap_map, but not yet added its page to swap cache.
		 */
		schedule_timeout_uninterruptible(1);
	}

	/*
	 * The swap entry is ours to swap in. Prepare the new page.
	 */

	__SetPageLocked(page);
	__SetPageSwapBacked(page);

	if (mem_cgroup_swapin_charge_page(page, NULL, gfp_mask, entry))
		goto fail_unlock;

	/* May fail (-ENOMEM) if XArray node allocation failed. */
	if (add_to_swap_cache(page, entry, gfp_mask & GFP_RECLAIM_MASK, &shadow))
		goto fail_unlock;

	mem_cgroup_swapin_uncharge_swap(entry);

	if (shadow)
		workingset_refault(page_folio(page), shadow);

	/* Caller will initiate read into locked page */
	lru_cache_add(page);
	*new_page_allocated = true;
	return page;

fail_unlock:
	put_swap_page(page, entry);
	unlock_page(page);
	put_page(page);
	return NULL;
}

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

struct page *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
				   struct vm_area_struct *vma,
				   unsigned long addr, bool do_poll,
				   struct swap_iocb **plug)
{
	bool page_was_allocated;
	struct page *retpage = __read_swap_cache_async(entry, gfp_mask,
			vma, addr, &page_was_allocated);

	if (page_was_allocated)
		swap_readpage(retpage, do_poll, plug);

	return retpage;
}

swapcache_prepare() 성공 뒤 memcg charge나 XArray 등록이 실패하면 cache reservation을 반환하고 page lock·reference를 정리합니다. 성공한 새 object만 new_page_allocated=true로 전달합니다. 기존 cache object를 발견한 경우에는 이 flag가 false라서 wrapper가 중복 read를 시작하지 않습니다. 작업 중 등록된 folio는 아직 locked이고, 내용을 채우는 것은 wrapper 또는 readahead caller의 책임입니다.

v6.12는 new_folio를 retry 사이에 유지하고 마지막에 필요 없어진 allocation을 반환합니다. device-user reference도 함수 전체 동안 보유합니다. skip_if_exists는 pending cache ownership을 발견한 재귀 호출이 자신이 유발한 reclaim·zswap 경로를 기다리며 교착하는 것을 피하는 종료 조건입니다. 단순히 기다리는 시간을 줄이는 선택지만은 아닙니다.

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

struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
		bool skip_if_exists)
{
	struct swap_info_struct *si;
	struct folio *folio;
	struct folio *new_folio = NULL;
	struct folio *result = NULL;
	void *shadow = NULL;

	*new_page_allocated = false;
	si = get_swap_device(entry);
	if (!si)
		return NULL;

	for (;;) {
		int err;
		/*
		 * First check the swap cache.  Since this is normally
		 * called after swap_cache_get_folio() failed, re-calling
		 * that would confuse statistics.
		 */
		folio = filemap_get_folio(swap_address_space(entry),
					  swap_cache_index(entry));
		if (!IS_ERR(folio))
			goto got_folio;

		/*
		 * Just skip read ahead for unused swap slot.
		 * During swap_off when swap_slot_cache is disabled,
		 * we have to handle the race between putting
		 * swap entry in swap cache and marking swap slot
		 * as SWAP_HAS_CACHE.  That's done in later part of code or
		 * else swap_off will be aborted if we return NULL.
		 */
		if (!swap_swapcount(si, entry) && swap_slot_cache_enabled)
			goto put_and_return;

		/*
		 * Get a new folio to read into from swap.  Allocate it now if
		 * new_folio not exist, before marking swap_map SWAP_HAS_CACHE,
		 * when -EEXIST will cause any racers to loop around until we
		 * add it to cache.
		 */
		if (!new_folio) {
			new_folio = folio_alloc_mpol(gfp_mask, 0, mpol, ilx, numa_node_id());
			if (!new_folio)
				goto put_and_return;
		}

		/*
		 * Swap entry may have been freed since our caller observed it.
		 */
		err = swapcache_prepare(entry, 1);
		if (!err)
			break;
		else if (err != -EEXIST)
			goto put_and_return;

		/*
		 * Protect against a recursive call to __read_swap_cache_async()
		 * on the same entry waiting forever here because SWAP_HAS_CACHE
		 * is set but the folio is not the swap cache yet. This can
		 * happen today if mem_cgroup_swapin_charge_folio() below
		 * triggers reclaim through zswap, which may call
		 * __read_swap_cache_async() in the writeback path.
		 */
		if (skip_if_exists)
			goto put_and_return;

		/*
		 * We might race against __delete_from_swap_cache(), and
		 * stumble across a swap_map entry whose SWAP_HAS_CACHE
		 * has not yet been cleared.  Or race against another
		 * __read_swap_cache_async(), which has set SWAP_HAS_CACHE
		 * in swap_map, but not yet added its folio to swap cache.
		 */
		schedule_timeout_uninterruptible(1);
	}

	/*
	 * The swap entry is ours to swap in. Prepare the new folio.
	 */
	__folio_set_locked(new_folio);
	__folio_set_swapbacked(new_folio);

	if (mem_cgroup_swapin_charge_folio(new_folio, NULL, gfp_mask, entry))
		goto fail_unlock;

	/* May fail (-ENOMEM) if XArray node allocation failed. */
	if (add_to_swap_cache(new_folio, entry, gfp_mask & GFP_RECLAIM_MASK, &shadow))
		goto fail_unlock;

	mem_cgroup_swapin_uncharge_swap(entry, 1);

	if (shadow)
		workingset_refault(new_folio, shadow);

	/* Caller will initiate read into locked new_folio */
	folio_add_lru(new_folio);
	*new_page_allocated = true;
	folio = new_folio;
got_folio:
	result = folio;
	goto put_and_return;

fail_unlock:
	put_swap_folio(new_folio, entry);
	folio_unlock(new_folio);
put_and_return:
	put_swap_device(si);
	if (!(*new_page_allocated) && new_folio)
		folio_put(new_folio);
	return result;
}

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

struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
		struct vm_area_struct *vma, unsigned long addr,
		struct swap_iocb **plug)
{
	bool page_allocated;
	struct mempolicy *mpol;
	pgoff_t ilx;
	struct folio *folio;

	mpol = get_vma_policy(vma, addr, 0, &ilx);
	folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
					&page_allocated, false);
	mpol_cond_put(mpol);

	if (page_allocated)
		swap_read_folio(folio, plug);
	return folio;
}

Read 제출자와 completion은 reference를 나눠 가집니다

아래 v6.0 read 함수와 bio completion을 함께 보겠습니다. official tag code의 notice와 GPL-2.0을 유지했습니다. frontswap load 성공은 uptodate·unlock 후 끝나며, filesystem operation과 synchronous bdev read 성공도 일반 bio fallback과 다른 경로입니다.

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

int swap_readpage(struct page *page, bool synchronous,
		  struct swap_iocb **plug)
{
	struct bio *bio;
	int ret = 0;
	struct swap_info_struct *sis = page_swap_info(page);
	bool workingset = PageWorkingset(page);
	unsigned long pflags;

	VM_BUG_ON_PAGE(!PageSwapCache(page) && !synchronous, page);
	VM_BUG_ON_PAGE(!PageLocked(page), page);
	VM_BUG_ON_PAGE(PageUptodate(page), page);

	/*
	 * Count submission time as memory stall. When the device is congested,
	 * or the submitting cgroup IO-throttled, submission can be a
	 * significant part of overall IO time.
	 */
	if (workingset)
		psi_memstall_enter(&pflags);
	delayacct_swapin_start();

	if (frontswap_load(page) == 0) {
		SetPageUptodate(page);
		unlock_page(page);
		goto out;
	}

	if (data_race(sis->flags & SWP_FS_OPS)) {
		swap_readpage_fs(page, plug);
		goto out;
	}

	if (sis->flags & SWP_SYNCHRONOUS_IO) {
		ret = bdev_read_page(sis->bdev, swap_page_sector(page), page);
		if (!ret) {
			count_vm_event(PSWPIN);
			goto out;
		}
	}

	ret = 0;
	bio = bio_alloc(sis->bdev, 1, REQ_OP_READ, GFP_KERNEL);
	bio->bi_iter.bi_sector = swap_page_sector(page);
	bio->bi_end_io = end_swap_bio_read;
	bio_add_page(bio, page, thp_size(page), 0);
	/*
	 * Keep this task valid during swap readpage because the oom killer may
	 * attempt to access it in the page fault retry time check.
	 */
	if (synchronous) {
		get_task_struct(current);
		bio->bi_private = current;
	}
	count_vm_event(PSWPIN);
	bio_get(bio);
	submit_bio(bio);
	while (synchronous) {
		set_current_state(TASK_UNINTERRUPTIBLE);
		if (!READ_ONCE(bio->bi_private))
			break;

		blk_io_schedule();
	}
	__set_current_state(TASK_RUNNING);
	bio_put(bio);

out:
	if (workingset)
		psi_memstall_leave(&pflags);
	delayacct_swapin_end();
	return ret;
}

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

static void end_swap_bio_read(struct bio *bio)
{
	struct page *page = bio_first_page_all(bio);
	struct task_struct *waiter = bio->bi_private;

	if (bio->bi_status) {
		SetPageError(page);
		ClearPageUptodate(page);
		pr_alert_ratelimited("Read-error on swap-device (%u:%u:%llu)\n",
				     MAJOR(bio_dev(bio)), MINOR(bio_dev(bio)),
				     (unsigned long long)bio->bi_iter.bi_sector);
		goto out;
	}

	SetPageUptodate(page);
out:
	unlock_page(page);
	WRITE_ONCE(bio->bi_private, NULL);
	bio_put(bio);
	if (waiter) {
		blk_wake_io_task(waiter);
		put_task_struct(waiter);
	}
}

bio fallback은 callback·sector·page 범위를 설정하고 제출합니다. synchronous 인자가 참이면 현재 task reference를 추가하고 bi_private에 waiter를 둡니다. 제출자는 bio reference를 별도로 얻어 completion이 먼저 실행되어도 자신이 기다리는 bio가 사라지지 않게 합니다. uninterruptible 상태를 먼저 설정한 뒤 READ_ONCE(bi_private)를 검사하고 필요하면 I/O 대기를 하며, 종료할 때 TASK_RUNNING과 자신의 bio put을 맞춥니다.

completion은 성공일 때 uptodate를 설정하고 오류일 때 error와 non-uptodate를 남깁니다. 둘 다 page를 unlock한 뒤 private waiter 표식을 clear하고 자신의 bio reference를 반환합니다. 저장해 둔 waiter가 있으면 깨우고 task reference도 반환합니다. folio 내용의 준비와 task·bio object 수명은 서로 다른 책임이므로 callback에서 page unlock만 했다고 정리가 끝나는 것은 아닙니다.

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 12, Looking Up a Folio / Reading Swapped Out Folios.
  • Linux v6.12 mm/swap_state.c: __read_swap_cache_async()와 readahead hit accounting.
  • Linux v6.12 mm/page_io.c: swap_read_folio()와 read completion.