PTE를 지운 뒤에도 folio를 바로 free하면 안 되는 이유

CPU 0이 PTE를 지웠어도 CPU 1의 TLB에는 옛 PFN이 남을 수 있습니다. 그 PFN을 다른 용도로 재사용하면 CPU 1이 새 소유자의 data를 읽거나 바꿀 수 있습니다. MMU-gather는 이 수명 경계를 지키면서 많은 unmap을 묶어 처리합니다.

제거, invalidation, release의 순서

핵심 순서는 page-table 연결 제거, 관련 translation invalidation, 마지막으로 해당 reference release입니다. PTE를 남긴 채 flush하면 hardware가 같은 translation을 다시 cache할 수 있고, flush 전에 free하면 stale translation이 재사용된 memory에 접근할 수 있습니다.

VMA를 주소 index에서 분리하는 작업도 이 세 단계와 같지 않습니다. munmap은 먼저 제거 대상을 안정적으로 정리하고, unmap_vmas() 계열에서 entry를 지우며, table page 정리와 gather 완료를 연결합니다. 새로운 주소 lookup에서 보이지 않는 것만으로 다른 CPU가 기존 mapping을 사용할 가능성까지 사라지지는 않습니다.

range와 release batch는 다른 집합입니다

mmu_gather는 대상 mm, flush할 start·exclusive end, cleared-level 상태와 fullmm 같은 조건, release할 folio·table batch를 기록합니다. flush range는 듬성듬성 지운 주소를 포함하는 큰 interval일 수 있지만 release batch에는 실제 reference를 내릴 대상만 들어갑니다.

예를 들어 서로 떨어진 세 PTE를 지우면 flush interval에는 그 사이의 빈 주소도 들어갈 수 있습니다. 반면 release batch에는 그 빈 주소에 대응하는 가상의 folio를 추가하지 않습니다. 주소 범위와 object 목록을 같은 집합으로 그리면 이 차이가 사라집니다.

작은 local batch로 시작하고 필요하면 추가 batch를 allocation합니다. tlb_gather_mmu_fullmm()은 exit·execve처럼 사용자가 없는 mm의 전체 address space를 파괴할 때 쓰는 초기화입니다. 단순히 넓은 range를 지운다는 이유로 이 초기화를 선택해서는 안 됩니다. 다만 fullmm flag 자체가 언제나 이 상황만 뜻하지는 않습니다. v6.0의 tlb_finish_mmu()는 병렬 PTE batching을 감지했을 때도 더 넓은 invalidation을 위해 이를 설정합니다. v6.0 초기화와 nested gather 처리

pending flush count는 동시 작업을 나타내며 초기화·종료가 짝을 이루어야 합니다. 아직 끝나지 않은 flush는 PTE의 현재 값만으로는 보이지 않는 translation 사용 가능성도 알려 줍니다. v6.0 x86의 pte_accessible()은 _PAGE_PRESENT뿐 아니라 _PAGE_PROTNONE이 설정되고 mm->tlb_flush_pending이 0이 아닌 경우도 accessible로 판단합니다. NUMA protection 변경 뒤 flush가 미뤄진 동안에는 옛 TLB translation이 남을 수 있기 때문입니다. 이는 PROTNONE PTE에 새 user 접근을 허용한다는 뜻이 아니라, ptep_clear_flush() 같은 제거 경로가 필요한 invalidation을 빠뜨리지 않게 하는 판단입니다. v6.0 pte_accessible()

VMA와 table 경계를 따라 작업합니다

unmap_vmas()는 VMA별로 요청 범위를 clamp하고 page-table walk로 내려갑니다. tlb_start_vma()와 tlb_end_vma()는 architecture가 필요한 cache 처리와 range 정책의 경계입니다. x86에서 일부 cache hook이 no-op이라고 다른 architecture에서도 불필요하다고 볼 수는 없습니다.

walk는 각 level의 다음 boundary를 사용하여 큰 empty range를 건너뜁니다. corrupted entry에는 bad-entry 처리가 있으며 huge leaf와 device mapping은 별도 분기입니다. MMU notifier는 CPU 밖의 translation 사용자에게 invalidation을 알리는 protocol이므로 CPU TLB flush만으로 모두 대체되지 않습니다.

현재 batch 표현을 상수로 외우지 않습니다

이전 구현의 page-pointer array나 local bundle 8 같은 수는 해당 struct·page-size 조합의 결과입니다. v6.0의 capacity 식은 (PAGE_SIZE - sizeof(struct mmu_gather_batch)) / sizeof(void *)입니다. 4 KiB page, 8-byte pointer와 16-byte header인 LP64 layout을 가정하면 510입니다. 책의 509를 x86-64의 고정 상수로 옮기지 않겠습니다. 이는 명시한 layout의 산술 계산이며 실제 kernel ABI를 실행 측정한 결과가 아닙니다. v6.18에는 delayed rmap 정보를 함께 담는 encoded entry가 사용됩니다. 이를 단순 struct page * array로 바꾸어 설명하면 rmap release 시점까지 잘못 이해할 수 있습니다.

batch 공간이 부족하면 무한히 모으지 않고 flush·release하여 진행합니다. allocation 실패가 “이미 모은 reference를 버린다”는 의미는 아닙니다. MMU-gather는 최종 finish 한 번만 flush하는 최적화가 아니라, 필요하면 중간 flush를 수행하면서도 수명 순서를 유지하는 protocol입니다.

Gather의 시작과 notifier 범위를 전체 source로 읽습니다

TLB는 Translation Lookaside Buffer입니다. CPU data cache와는 목적이 다르며 kernel direct map도 모든 상황에서 translation maintenance가 면제되는 것은 아닙니다. 아래 historical 경로의 unmap_region()은 caller가 이미 잘라 분리한 VMA chain을 받습니다. 임의 VMA를 넣고 vm_next 전체를 정리하는 일반 utility가 아닙니다.

unmap_region.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code	<alan@lxorguk.ukuu.org.uk>
 */

/* Upstream Linux v6.0: mm/mmap.c:2612-2626
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static void unmap_region(struct mm_struct *mm,
		struct vm_area_struct *vma, struct vm_area_struct *prev,
		unsigned long start, unsigned long end)
{
	struct vm_area_struct *next = vma_next(mm, prev);
	struct mmu_gather tlb;

	lru_add_drain();
	tlb_gather_mmu(&tlb, mm);
	update_hiwater_rss(mm);
	unmap_vmas(&tlb, vma, start, end);
	free_pgtables(&tlb, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,
				 next ? next->vm_start : USER_PGTABLES_CEILING);
	tlb_finish_mmu(&tlb);
}

mmu_gather.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:267-319
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
struct mmu_gather {
	struct mm_struct	*mm;

#ifdef CONFIG_MMU_GATHER_TABLE_FREE
	struct mmu_table_batch	*batch;
#endif

	unsigned long		start;
	unsigned long		end;
	/*
	 * we are in the middle of an operation to clear
	 * a full mm and can make some optimizations
	 */
	unsigned int		fullmm : 1;

	/*
	 * we have performed an operation which
	 * requires a complete flush of the tlb
	 */
	unsigned int		need_flush_all : 1;

	/*
	 * we have removed page directories
	 */
	unsigned int		freed_tables : 1;

	/*
	 * at which levels have we cleared entries?
	 */
	unsigned int		cleared_ptes : 1;
	unsigned int		cleared_pmds : 1;
	unsigned int		cleared_puds : 1;
	unsigned int		cleared_p4ds : 1;

	/*
	 * tracks VM_EXEC | VM_HUGETLB in tlb_start_vma
	 */
	unsigned int		vma_exec : 1;
	unsigned int		vma_huge : 1;
	unsigned int		vma_pfn  : 1;

	unsigned int		batch_count;

#ifndef CONFIG_MMU_GATHER_NO_GATHER
	struct mmu_gather_batch *active;
	struct mmu_gather_batch	local;
	struct page		*__pages[MMU_GATHER_BUNDLE];

#ifdef CONFIG_MMU_GATHER_PAGE_SIZE
	unsigned int page_size;
#endif
#endif
};

mmu_gather_batch.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:241-246
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
struct mmu_gather_batch {
	struct mmu_gather_batch	*next;
	unsigned int		nr;
	unsigned int		max;
	struct page		*pages[];
};

mmu_gather_batch.pages[]는 flexible array member이고 local bundle의 backing storage는 gather 안에 별도로 마련되어 있습니다. CONFIG_MMU_GATHER_NO_GATHER가 정의되면 이름 그대로 일반 batch 수집을 사용하지 않는 경로입니다. #ifndef 조건을 반대로 읽고 이 option이 batching을 활성화한다고 해석해서는 안 됩니다.

tlb_gather_mmu.c



/* Upstream Linux v6.0: mm/mmu_gather.c:297-300
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm)
{
	__tlb_gather_mmu(tlb, mm, false);
}

tlb_gather_mmu_fullmm.c



/* Upstream Linux v6.0: mm/mmu_gather.c:313-316
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void tlb_gather_mmu_fullmm(struct mmu_gather *tlb, struct mm_struct *mm)
{
	__tlb_gather_mmu(tlb, mm, true);
}

__tlb_gather_mmu.c



/* Upstream Linux v6.0: mm/mmu_gather.c:265-287
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static void __tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,
			     bool fullmm)
{
	tlb->mm = mm;
	tlb->fullmm = fullmm;

#ifndef CONFIG_MMU_GATHER_NO_GATHER
	tlb->need_flush_all = 0;
	tlb->local.next = NULL;
	tlb->local.nr   = 0;
	tlb->local.max  = ARRAY_SIZE(tlb->__pages);
	tlb->active     = &tlb->local;
	tlb->batch_count = 0;
#endif

	tlb_table_init(tlb);
#ifdef CONFIG_MMU_GATHER_PAGE_SIZE
	tlb->page_size = 0;
#endif

	__tlb_reset_range(tlb);
	inc_tlb_flush_pending(tlb->mm);
}

__tlb_reset_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:331-349
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline void __tlb_reset_range(struct mmu_gather *tlb)
{
	if (tlb->fullmm) {
		tlb->start = tlb->end = ~0;
	} else {
		tlb->start = TASK_SIZE;
		tlb->end = 0;
	}
	tlb->freed_tables = 0;
	tlb->cleared_ptes = 0;
	tlb->cleared_pmds = 0;
	tlb->cleared_puds = 0;
	tlb->cleared_p4ds = 0;
	/*
	 * Do not reset mmu_gather::vma_* fields here, we do not
	 * call into tlb_start_vma() again to set them if there is an
	 * intermediate flush.
	 */
}

일반 초기화와 fullmm 초기화는 같은 내부 helper에 다른 teardown 의미를 전달합니다. inc_tlb_flush_pending()으로 시작한 lifetime은 finish의 decrement와 짝을 이룹니다. 중간 flush에서 range를 reset하더라도 현재 VMA의 exec·PFN 관련 state를 아무렇게나 지울 수는 없습니다.

unmap_vmas()는 MMU notifier의 start/end 사이에서 VMA들을 처리합니다. CPU PTE를 지운 사실이 device나 secondary MMU의 cached translation까지 자동으로 종료시킨다는 뜻은 아닙니다. even_cows와 marker policy도 notifier 범위와 함께 실제 전체 함수에서 확인합니다.

unmap_vmas.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:1716-1733
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
void unmap_vmas(struct mmu_gather *tlb,
		struct vm_area_struct *vma, unsigned long start_addr,
		unsigned long end_addr)
{
	struct mmu_notifier_range range;
	struct zap_details details = {
		.zap_flags = ZAP_FLAG_DROP_MARKER,
		/* Careful - we need to zap private pages too! */
		.even_cows = true,
	};

	mmu_notifier_range_init(&range, MMU_NOTIFY_UNMAP, 0, vma, vma->vm_mm,
				start_addr, end_addr);
	mmu_notifier_invalidate_range_start(&range);
	for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next)
		unmap_single_vma(tlb, vma, start_addr, end_addr, &details);
	mmu_notifier_invalidate_range_end(&range);
}

Source: Linux v6.0 mm/mmu_gather.c, tlb.h, memory.c, mmap.c. 전체 explicit definition을 보존했지만 book exact tag는 입증하지 않았습니다. v6.18은 별도 비교 tag이며 latest라는 의미가 아닙니다. 원래 copyright notice와 COPYING, GPL-2.0를 함께 보존했습니다. 실행 검증은 하지 않았습니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 7 §7.1–§7.1.4. Linux v6.18 include/asm-generic/tlb.h, mm/mmu_gather.c를 참고하실 수 있습니다.