zap이 PTE의 dirty와 young bit를 버리지 않는 이유

mapping을 제거할 때 PTE를 0으로 만들면 끝일 것 같지만 그 entry에는 file contents의 변경과 접근 흔적이 남아 있습니다. 이를 folio 상태에 반영하지 않고 버리면 writeback이 data 변경을 놓치거나 reclaim 판단이 실제 사용 이력과 달라질 수 있습니다.

모든 present page를 같은 이유로 지우지 않습니다

zap_pte_range() 계열은 PTE lock 아래에서 entry를 확인합니다. empty, present, swap-like·migration·marker는 처리 방법이 다릅니다. 예를 들어 범위 unmap과 file invalidate는 anonymous CoW까지 지울지의 정책이 다를 수 있어 zap_details와 even_cows 같은 조건을 봅니다. “non-present면 언제나 아무것도 할 일이 없다”는 설명은 swap entry 정리를 놓칩니다.

normal page 판정은 ordinary refcount 대상을 찾는 과정입니다. zero·raw PFN special entry는 translation을 지우더라도 일반 folio의 rmap·put 계약을 그대로 적용하지 않습니다. 이 구별을 한 뒤 실제 지울 대상인지 결정합니다.

PTE를 clear하면서 이전 상태를 얻습니다

entry를 원자적으로 읽어 제거하는 helper는 old dirty·young 정보를 돌려줍니다. 그 뒤 gather의 cleared-PTE 상태와 flush range를 넓힙니다. file-backed page라면 dirty bit를 folio dirty 상태에 전달하고, 적절한 경우 young을 accessed 이력으로 전달합니다. sequential access advice가 이 해석에 영향을 줄 수도 있습니다.

특히 dirty file entry는 PTE lock 아래 flush가 필요한 조건을 가질 수 있습니다. writeback이 이미 비어 있는 PTE만 보고 stale writable translation까지 사라졌다고 오판하면 안 되기 때문입니다. 이 flush는 모든 reference release를 의미하지 않으며, lock을 놓은 뒤 release batch를 처리하는 단계와 구분합니다.

mapcount는 수명 전체가 아닙니다

RSS는 해당 mapping 종류에 맞춰 줄이고 rmap 제거를 기록합니다. order-0의 마지막 mapcount 전환은 anonymous/file mapped 통계와 mlock 관련 상태 정리에 영향을 줍니다. 그러나 folio mapping pointer를 무조건 즉시 지우거나 refcount가 0이 되었다고 가정하는 것은 아닙니다.

v6.18에는 일부 rmap 감소를 TLB flush 뒤까지 지연하는 경로가 있습니다. stale translation이 사용 가능한데 mapcount를 먼저 낮춰 다른 code가 reuse 가능하다고 판단하지 않게 하는 조건입니다. PTE clear와 모든 rmap 정리가 항상 동시에 끝난다고 도식화해서는 안 됩니다.

긴 unmap은 중간에 양보합니다

많은 entry를 처리하면 scheduler에 양보할 필요가 있습니다. batch가 차거나 reschedule이 필요하면 적절히 flush하고 lock을 놓은 뒤 재개합니다. loop를 중단하는 것이 지금까지 지운 entry를 원상 복구한다는 뜻은 아닙니다. 이미 진행한 구간과 아직 남은 구간의 상태가 정확해야 합니다.

tlb_flush_mmu_tlbonly()는 invalidation을, tlb_flush_mmu()는 모인 reference release까지 포함합니다. VMA 종료에서 architecture별 최적화를 거친 뒤 gather finish는 남은 작업을 마칩니다. 동시에 진행한 flush가 있으면 더 넓은 invalidation이 필요할 수도 있습니다. 이처럼 zap의 작은 loop에도 data 보존, 경쟁 방지와 scheduling의 세 요구가 함께 들어 있습니다.

VMA에서 PTE까지 내려가는 전체 경로

아래는 historical Linux v6.0 정의입니다. unmap_single_vma()는 주소를 VMA 범위로 제한하고 uprobes·PFN tracking·HugeTLB를 구분합니다. 상위 level의 empty entry를 건너뛰는 loop는 다음 경계까지 진행하며, huge leaf와 devmap도 별도 처리합니다. 책에서 줄임표로 생략한 분기를 공식 source로 보충했습니다.

unmap_single_vma.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1652-1696
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static void unmap_single_vma(struct mmu_gather *tlb,
		struct vm_area_struct *vma, unsigned long start_addr,
		unsigned long end_addr,
		struct zap_details *details)
{
	unsigned long start = max(vma->vm_start, start_addr);
	unsigned long end;

	if (start >= vma->vm_end)
		return;
	end = min(vma->vm_end, end_addr);
	if (end <= vma->vm_start)
		return;

	if (vma->vm_file)
		uprobe_munmap(vma, start, end);

	if (unlikely(vma->vm_flags & VM_PFNMAP))
		untrack_pfn(vma, 0, 0);

	if (start != end) {
		if (unlikely(is_vm_hugetlb_page(vma))) {
			/*
			 * It is undesirable to test vma->vm_file as it
			 * should be non-null for valid hugetlb area.
			 * However, vm_file will be NULL in the error
			 * cleanup path of mmap_region. When
			 * hugetlbfs ->mmap method fails,
			 * mmap_region() nullifies vma->vm_file
			 * before calling this function to clean up.
			 * Since no pte has actually been setup, it is
			 * safe to do nothing in this case.
			 */
			if (vma->vm_file) {
				zap_flags_t zap_flags = details ?
				    details->zap_flags : 0;
				i_mmap_lock_write(vma->vm_file->f_mapping);
				__unmap_hugepage_range_final(tlb, vma, start, end,
							     NULL, zap_flags);
				i_mmap_unlock_write(vma->vm_file->f_mapping);
			}
		} else
			unmap_page_range(tlb, vma, start, end, details);
	}
}

unmap_page_range.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1631-1649
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void unmap_page_range(struct mmu_gather *tlb,
			     struct vm_area_struct *vma,
			     unsigned long addr, unsigned long end,
			     struct zap_details *details)
{
	pgd_t *pgd;
	unsigned long next;

	BUG_ON(addr >= end);
	tlb_start_vma(tlb, vma);
	pgd = pgd_offset(vma->vm_mm, addr);
	do {
		next = pgd_addr_end(addr, end);
		if (pgd_none_or_clear_bad(pgd))
			continue;
		next = zap_p4d_range(tlb, vma, pgd, addr, next, details);
	} while (pgd++, addr = next, addr != end);
	tlb_end_vma(tlb, vma);
}

tlb_start_vma.c

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:489-498
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void tlb_start_vma(struct mmu_gather *tlb, struct vm_area_struct *vma)
{
	if (tlb->fullmm)
		return;

	tlb_update_vma_flags(tlb, vma);
#ifndef CONFIG_MMU_GATHER_NO_FLUSH_CACHE
	flush_cache_range(vma, vma->vm_start, vma->vm_end);
#endif
}

tlb_update_vma_flags.c

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:397-414
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void
tlb_update_vma_flags(struct mmu_gather *tlb, struct vm_area_struct *vma)
{
	/*
	 * flush_tlb_range() implementations that look at VM_HUGETLB (tile,
	 * mips-4k) flush only large pages.
	 *
	 * flush_tlb_range() implementations that flush I-TLB also flush D-TLB
	 * (tile, xtensa, arm), so it's ok to just add VM_EXEC to an existing
	 * range.
	 *
	 * We rely on tlb_end_vma() to issue a flush, such that when we reset
	 * these values the batch is empty.
	 */
	tlb->vma_huge = is_vm_hugetlb_page(vma);
	tlb->vma_exec = !!(vma->vm_flags & VM_EXEC);
	tlb->vma_pfn  = !!(vma->vm_flags & (VM_PFNMAP|VM_MIXEDMAP));
}

flush_cache_range()라는 이름의 architecture hook은 CPU cache maintenance에 관한 것입니다. 이것을 뒤의 translation invalidation과 같은 호출이라고 읽지 않겠습니다. 또 mmap_lock read-side에서 수행되는 작업은 다른 read-side fault와 동시에 진행될 수 있습니다. PMD를 한 번 읽었으니 끝까지 같은 PTE table일 것이라는 추론은 zap_pmd_range()의 재검사 조건과 충돌합니다.

zap_p4d_range.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1612-1629
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline unsigned long zap_p4d_range(struct mmu_gather *tlb,
				struct vm_area_struct *vma, pgd_t *pgd,
				unsigned long addr, unsigned long end,
				struct zap_details *details)
{
	p4d_t *p4d;
	unsigned long next;

	p4d = p4d_offset(pgd, addr);
	do {
		next = p4d_addr_end(addr, end);
		if (p4d_none_or_clear_bad(p4d))
			continue;
		next = zap_pud_range(tlb, vma, p4d, addr, next, details);
	} while (p4d++, addr = next, addr != end);

	return addr;
}

zap_pud_range.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1583-1610
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline unsigned long zap_pud_range(struct mmu_gather *tlb,
				struct vm_area_struct *vma, p4d_t *p4d,
				unsigned long addr, unsigned long end,
				struct zap_details *details)
{
	pud_t *pud;
	unsigned long next;

	pud = pud_offset(p4d, addr);
	do {
		next = pud_addr_end(addr, end);
		if (pud_trans_huge(*pud) || pud_devmap(*pud)) {
			if (next - addr != HPAGE_PUD_SIZE) {
				mmap_assert_locked(tlb->mm);
				split_huge_pud(vma, pud, addr);
			} else if (zap_huge_pud(tlb, vma, pud, addr))
				goto next;
			/* fall through */
		}
		if (pud_none_or_clear_bad(pud))
			continue;
		next = zap_pmd_range(tlb, vma, pud, addr, next, details);
next:
		cond_resched();
	} while (pud++, addr = next, addr != end);

	return addr;
}

zap_pmd_range.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1537-1581
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline unsigned long zap_pmd_range(struct mmu_gather *tlb,
				struct vm_area_struct *vma, pud_t *pud,
				unsigned long addr, unsigned long end,
				struct zap_details *details)
{
	pmd_t *pmd;
	unsigned long next;

	pmd = pmd_offset(pud, addr);
	do {
		next = pmd_addr_end(addr, end);
		if (is_swap_pmd(*pmd) || pmd_trans_huge(*pmd) || pmd_devmap(*pmd)) {
			if (next - addr != HPAGE_PMD_SIZE)
				__split_huge_pmd(vma, pmd, addr, false, NULL);
			else if (zap_huge_pmd(tlb, vma, pmd, addr))
				goto next;
			/* fall through */
		} else if (details && details->single_folio &&
			   folio_test_pmd_mappable(details->single_folio) &&
			   next - addr == HPAGE_PMD_SIZE && pmd_none(*pmd)) {
			spinlock_t *ptl = pmd_lock(tlb->mm, pmd);
			/*
			 * Take and drop THP pmd lock so that we cannot return
			 * prematurely, while zap_huge_pmd() has cleared *pmd,
			 * but not yet decremented compound_mapcount().
			 */
			spin_unlock(ptl);
		}

		/*
		 * Here there can be other concurrent MADV_DONTNEED or
		 * trans huge page faults running, and if the pmd is
		 * none or trans huge it can change under us. This is
		 * because MADV_DONTNEED holds the mmap_lock in read
		 * mode.
		 */
		if (pmd_none_or_trans_huge_or_clear_bad(pmd))
			goto next;
		next = zap_pte_range(tlb, vma, pmd, addr, next, details);
next:
		cond_resched();
	} while (pmd++, addr = next, addr != end);

	return addr;
}

present 경로만 잘라 보면 놓치는 것

zap_pte_range() 전체를 한 파일로 보존하여 preface·main loop·finalization 사이의 state를 이어 읽을 수 있게 했습니다. pte_none()과 non-present는 동의어가 아닙니다. non-present entry에도 swap reference, migration, device-private, marker 등이 남을 수 있습니다. should_zap_page()는 지울 policy를 결정하고, 실제 entry 제거와 lifetime 정리는 다른 단계입니다.

zap_pte_range.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1402-1535
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static unsigned long zap_pte_range(struct mmu_gather *tlb,
				struct vm_area_struct *vma, pmd_t *pmd,
				unsigned long addr, unsigned long end,
				struct zap_details *details)
{
	struct mm_struct *mm = tlb->mm;
	int force_flush = 0;
	int rss[NR_MM_COUNTERS];
	spinlock_t *ptl;
	pte_t *start_pte;
	pte_t *pte;
	swp_entry_t entry;

	tlb_change_page_size(tlb, PAGE_SIZE);
again:
	init_rss_vec(rss);
	start_pte = pte_offset_map_lock(mm, pmd, addr, &ptl);
	pte = start_pte;
	flush_tlb_batched_pending(mm);
	arch_enter_lazy_mmu_mode();
	do {
		pte_t ptent = *pte;
		struct page *page;

		if (pte_none(ptent))
			continue;

		if (need_resched())
			break;

		if (pte_present(ptent)) {
			page = vm_normal_page(vma, addr, ptent);
			if (unlikely(!should_zap_page(details, page)))
				continue;
			ptent = ptep_get_and_clear_full(mm, addr, pte,
							tlb->fullmm);
			tlb_remove_tlb_entry(tlb, pte, addr);
			zap_install_uffd_wp_if_needed(vma, addr, pte, details,
						      ptent);
			if (unlikely(!page))
				continue;

			if (!PageAnon(page)) {
				if (pte_dirty(ptent)) {
					force_flush = 1;
					set_page_dirty(page);
				}
				if (pte_young(ptent) &&
				    likely(!(vma->vm_flags & VM_SEQ_READ)))
					mark_page_accessed(page);
			}
			rss[mm_counter(page)]--;
			page_remove_rmap(page, vma, false);
			if (unlikely(page_mapcount(page) < 0))
				print_bad_pte(vma, addr, ptent, page);
			if (unlikely(__tlb_remove_page(tlb, page))) {
				force_flush = 1;
				addr += PAGE_SIZE;
				break;
			}
			continue;
		}

		entry = pte_to_swp_entry(ptent);
		if (is_device_private_entry(entry) ||
		    is_device_exclusive_entry(entry)) {
			page = pfn_swap_entry_to_page(entry);
			if (unlikely(!should_zap_page(details, page)))
				continue;
			/*
			 * Both device private/exclusive mappings should only
			 * work with anonymous page so far, so we don't need to
			 * consider uffd-wp bit when zap. For more information,
			 * see zap_install_uffd_wp_if_needed().
			 */
			WARN_ON_ONCE(!vma_is_anonymous(vma));
			rss[mm_counter(page)]--;
			if (is_device_private_entry(entry))
				page_remove_rmap(page, vma, false);
			put_page(page);
		} else if (!non_swap_entry(entry)) {
			/* Genuine swap entry, hence a private anon page */
			if (!should_zap_cows(details))
				continue;
			rss[MM_SWAPENTS]--;
			if (unlikely(!free_swap_and_cache(entry)))
				print_bad_pte(vma, addr, ptent, NULL);
		} else if (is_migration_entry(entry)) {
			page = pfn_swap_entry_to_page(entry);
			if (!should_zap_page(details, page))
				continue;
			rss[mm_counter(page)]--;
		} else if (pte_marker_entry_uffd_wp(entry)) {
			/* Only drop the uffd-wp marker if explicitly requested */
			if (!zap_drop_file_uffd_wp(details))
				continue;
		} else if (is_hwpoison_entry(entry) ||
			   is_swapin_error_entry(entry)) {
			if (!should_zap_cows(details))
				continue;
		} else {
			/* We should have covered all the swap entry types */
			WARN_ON_ONCE(1);
		}
		pte_clear_not_present_full(mm, addr, pte, tlb->fullmm);
		zap_install_uffd_wp_if_needed(vma, addr, pte, details, ptent);
	} while (pte++, addr += PAGE_SIZE, addr != end);

	add_mm_rss_vec(mm, rss);
	arch_leave_lazy_mmu_mode();

	/* Do the actual TLB flush before dropping ptl */
	if (force_flush)
		tlb_flush_mmu_tlbonly(tlb);
	pte_unmap_unlock(start_pte, ptl);

	/*
	 * If we forced a TLB flush (either due to running out of
	 * batch buffers or because we needed to flush dirty TLB
	 * entries before releasing the ptl), free the batched
	 * memory too. Restart if we didn't do everything.
	 */
	if (force_flush) {
		force_flush = 0;
		tlb_flush_mmu(tlb);
	}

	if (addr != end) {
		cond_resched();
		goto again;
	}

	return addr;
}

should_zap_page.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/memory.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */

/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

/*
 * Real VM (paging to/from disk) started 18.12.91. Much more work and
 * thought has to go into this. Oh, well..
 * 19.12.91  -  works, somewhat. Sometimes I get faults, don't know why.
 *		Found it. Everything seems to work now.
 * 20.12.91  -  Ok, making the swap-device changeable like the root.
 */

/*
 * 05.04.94  -  Multi-page memory management added for v1.1.
 *              Idea by Alex Bligh (alex@cconcepts.co.uk)
 *
 * 16.07.99  -  Support of BIGMEM added by Gerhard Wichert, Siemens AG
 *		(Gerhard.Wichert@pdb.siemens.de)
 *
 * Aug/Sep 2004 Changed to four level page tables (Andi Kleen)
 */

/* Upstream Linux v6.0: mm/memory.c:1365-1377
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline bool should_zap_page(struct zap_details *details, struct page *page)
{
	/* If we can make a decision without *page.. */
	if (should_zap_cows(details))
		return true;

	/* E.g. the caller passes NULL for the case of a zero page */
	if (!page)
		return true;

	/* Otherwise we should only zap non-anon pages */
	return !PageAnon(page);
}

이전에 reclaim이 미뤄 둔 invalidation과 충돌하지 않도록 flush_tlb_batched_pending()은 PTL 아래에서 호출됩니다. flush 동안 새 pending generation이 생겼다면 compare-exchange가 이전 snapshot으로 덮어쓰지 못하게 합니다. 단순히 flag를 0으로 대입하는 방식과 다른 이유입니다.

flush_tlb_batched_pending.c

/*
 * mm/rmap.c - physical to virtual reverse mappings
 *
 * Copyright 2001, Rik van Riel <riel@conectiva.com.br>
 * Released under the General Public License (GPL).
 *
 * Simple, low overhead reverse mapping scheme.
 * Please try to keep this thing as modular as possible.
 *
 * Provides methods for unmapping each kind of mapped page:
 * the anon methods track anonymous pages, and
 * the file methods track pages belonging to an inode.
 *
 * Original design by Rik van Riel <riel@conectiva.com.br> 2001
 * File methods by Dave McCracken <dmccr@us.ibm.com> 2003, 2004
 * Anonymous methods by Andrea Arcangeli <andrea@suse.de> 2004
 * Contributions by Hugh Dickins 2003, 2004
 */

/*
 * Lock ordering in mm:
 *
 * inode->i_rwsem	(while writing or truncating, not reading or faulting)
 *   mm->mmap_lock
 *     mapping->invalidate_lock (in filemap_fault)
 *       page->flags PG_locked (lock_page)   * (see hugetlbfs below)
 *         hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share)
 *           mapping->i_mmap_rwsem
 *             hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             anon_vma->rwsem
 *               mm->page_table_lock or pte_lock
 *                 swap_lock (in swap_duplicate, swap_info_get)
 *                   mmlist_lock (in mmput, drain_mmlist and others)
 *                   mapping->private_lock (in block_dirty_folio)
 *                     folio_lock_memcg move_lock (in block_dirty_folio)
 *                       i_pages lock (widely used)
 *                         lruvec->lru_lock (in folio_lruvec_lock_irq)
 *                   inode->i_lock (in set_page_dirty's __mark_inode_dirty)
 *                   bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty)
 *                     sb_lock (within inode_lock in fs/fs-writeback.c)
 *                     i_pages lock (widely used, in set_page_dirty,
 *                               in arch-dependent flush_dcache_mmap_lock,
 *                               within bdi.wb->list_lock in __sync_single_inode)
 *
 * anon_vma->rwsem,mapping->i_mmap_rwsem   (memory_failure, collect_procs_anon)
 *   ->tasklist_lock
 *     pte map lock
 *
 * * hugetlbfs PageHuge() pages take locks in this order:
 *         mapping->i_mmap_rwsem
 *           hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             page->flags PG_locked (lock_page)
 */

/* Upstream Linux v6.0: mm/rmap.c:721-736
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void flush_tlb_batched_pending(struct mm_struct *mm)
{
	int batch = atomic_read(&mm->tlb_flush_batched);
	int pending = batch & TLB_FLUSH_BATCH_PENDING_MASK;
	int flushed = batch >> TLB_FLUSH_BATCH_FLUSHED_SHIFT;

	if (pending != flushed) {
		flush_tlb_mm(mm);
		/*
		 * If the new TLB flushing is pending during flushing, leave
		 * mm->tlb_flush_batched as is, to avoid losing flushing.
		 */
		atomic_cmpxchg(&mm->tlb_flush_batched, batch,
			       pending | (pending << TLB_FLUSH_BATCH_FLUSHED_SHIFT));
	}
}

tlb_remove_tlb_entry.h

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:563-567
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
#define tlb_remove_tlb_entry(tlb, ptep, address)		\
	do {							\
		tlb_flush_pte_range(tlb, address, PAGE_SIZE);	\
		__tlb_remove_tlb_entry(tlb, ptep, address);	\
	} while (0)

tlb_flush_pte_range.c

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:524-529
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void tlb_flush_pte_range(struct mmu_gather *tlb,
				     unsigned long address, unsigned long size)
{
	__tlb_adjust_range(tlb, address, size);
	tlb->cleared_ptes = 1;
}

__tlb_adjust_range.c

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:323-329
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void __tlb_adjust_range(struct mmu_gather *tlb,
				      unsigned long address,
				      unsigned int range_size)
{
	tlb->start = min(tlb->start, address);
	tlb->end = max(tlb->end, address + range_size);
}

마지막 세 helper는 range와 cleared-level state를 축적합니다. 이름에 flush가 들어 있어도 여기서 모두 즉시 hardware instruction을 실행하는 것은 아닙니다. Dirty file mapping에서는 old writable translation을 writeback보다 먼저 끝내기 위해 PTL을 놓기 전 tlbonly가 필요할 수 있습니다. Reference를 내리는 batch release는 lock을 놓은 뒤로 분리됩니다.

가득 찬 batch는 실패한 enqueue가 아닙니다

__tlb_remove_page_size()는 page를 먼저 현재 batch에 넣습니다. 그 뒤 다음 batch를 얻지 못하면 true로 flush 필요를 알립니다. 따라서 caller가 같은 page를 다시 넣거나 “실패했으니 직접 free”하면 double release가 될 수 있습니다. tlb_next_batch()의 allocation 실패는 이미 수집한 page 목록을 버리지 않습니다.

__tlb_remove_page_size.c



/* Upstream Linux v6.0: mm/mmu_gather.c:79-103
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
bool __tlb_remove_page_size(struct mmu_gather *tlb, struct page *page, int page_size)
{
	struct mmu_gather_batch *batch;

	VM_BUG_ON(!tlb->end);

#ifdef CONFIG_MMU_GATHER_PAGE_SIZE
	VM_WARN_ON(tlb->page_size != page_size);
#endif

	batch = tlb->active;
	/*
	 * Add the page and check if we are full. If so
	 * force a flush.
	 */
	batch->pages[batch->nr++] = page;
	if (batch->nr == batch->max) {
		if (!tlb_next_batch(tlb))
			return true;
		batch = tlb->active;
	}
	VM_BUG_ON_PAGE(batch->nr > batch->max, page);

	return false;
}

tlb_next_batch.c



/* Upstream Linux v6.0: mm/mmu_gather.c:17-43
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static bool tlb_next_batch(struct mmu_gather *tlb)
{
	struct mmu_gather_batch *batch;

	batch = tlb->active;
	if (batch->next) {
		tlb->active = batch->next;
		return true;
	}

	if (tlb->batch_count == MAX_GATHER_BATCH_COUNT)
		return false;

	batch = (void *)__get_free_pages(GFP_NOWAIT | __GFP_NOWARN, 0);
	if (!batch)
		return false;

	tlb->batch_count++;
	batch->next = NULL;
	batch->nr   = 0;
	batch->max  = MAX_GATHER_BATCH;

	tlb->active->next = batch;
	tlb->active = batch;

	return true;
}

tlb_end_vma.c

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* include/asm-generic/tlb.h
 *
 *	Generic TLB shootdown code
 *
 * Copyright 2001 Red Hat, Inc.
 * Based on code from mm/memory.c Copyright Linus Torvalds and others.
 *
 * Copyright 2011 Red Hat, Inc., Peter Zijlstra
 */

/* Upstream Linux v6.0: include/asm-generic/tlb.h:500-518
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void tlb_end_vma(struct mmu_gather *tlb, struct vm_area_struct *vma)
{
	if (tlb->fullmm)
		return;

	/*
	 * VM_PFNMAP is more fragile because the core mm will not track the
	 * page mapcount -- there might not be page-frames for these PFNs after
	 * all. Force flush TLBs for such ranges to avoid munmap() vs
	 * unmap_mapping_range() races.
	 */
	if (tlb->vma_pfn || !IS_ENABLED(CONFIG_MMU_GATHER_MERGE_VMAS)) {
		/*
		 * Do a TLB flush and reset the range at VMA boundaries; this avoids
		 * the ranges growing with the unused space between consecutive VMAs.
		 */
		tlb_flush_mmu_tlbonly(tlb);
	}
}

tlb_finish_mmu.c



/* Upstream Linux v6.0: mm/mmu_gather.c:325-359
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void tlb_finish_mmu(struct mmu_gather *tlb)
{
	/*
	 * If there are parallel threads are doing PTE changes on same range
	 * under non-exclusive lock (e.g., mmap_lock read-side) but defer TLB
	 * flush by batching, one thread may end up seeing inconsistent PTEs
	 * and result in having stale TLB entries.  So flush TLB forcefully
	 * if we detect parallel PTE batching threads.
	 *
	 * However, some syscalls, e.g. munmap(), may free page tables, this
	 * needs force flush everything in the given range. Otherwise this
	 * may result in having stale TLB entries for some architectures,
	 * e.g. aarch64, that could specify flush what level TLB.
	 */
	if (mm_tlb_flush_nested(tlb->mm)) {
		/*
		 * The aarch64 yields better performance with fullmm by
		 * avoiding multiple CPUs spamming TLBI messages at the
		 * same time.
		 *
		 * On x86 non-fullmm doesn't yield significant difference
		 * against fullmm.
		 */
		tlb->fullmm = 1;
		__tlb_reset_range(tlb);
		tlb->freed_tables = 1;
	}

	tlb_flush_mmu(tlb);

#ifndef CONFIG_MMU_GATHER_NO_GATHER
	tlb_batch_list_free(tlb);
#endif
	dec_tlb_flush_pending(tlb->mm);
}

tlb_end_vma()는 PFN-only mapping과 merge policy에 맞춰 flush 경계를 정합니다. tlb_finish_mmu()는 nested gather를 감지하면 더 보수적인 fullmm 처리를 선택하고, 마지막 batch와 pending count를 정리합니다. finish를 생략해도 언젠가 CPU가 알아서 정리할 것이라는 계약은 없습니다.

Source: v6.0 mm/memory.c, mm/rmap.c, mm/mmu_gather.c, tlb.h. Source file notice와 COPYING, GPL-2.0를 보존했습니다. v6.0은 historical layout reference이고 book exact tag는 미확정입니다. v6.18 비교 내용을 v6.0 code의 동작으로 섞지 않았으며 compile·runtime 검증은 하지 않았습니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 7 §7.1.4. 현재 zap과 delayed rmap은 Linux v6.18 mm/memory.c, mm/mmu_gather.c에 있습니다.