← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 05/21

TLB shootdown: TLBI, INVLPG/INVPCID와 SFENCE.VMA

PTE 변경을 다른 CPU가 언제부터 관찰하는지 range flush, ASID tag, IPI와 completion 경계로 분석합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 203줄
기준 tag
Linux v6.18.37
분석 축
state · ordering · lifetime · latency

01 · QUESTION

무엇을 확인할 것인가

PTE를 memory에서 바꾼 뒤 remote CPU가 old translation을 더 이상 사용하지 않는 시점은 언제인가?

initiator는 PTE write를 publish하고 mm를 실행할 수 있는 CPU mask를 얻어 local invalidate와 remote request를 보낸다. page-table page free는 모든 대상 CPU의 invalidate completion 뒤에만 가능하다.

range와 full-mm 선택, freed_tables 여부, huge mapping stride와 lazy CPU 제외가 shootdown 비용과 correctness를 결정한다.

지연 시간 관점Tshoot = Tpublish + max(Tipi_delivery + Tremote_flush) + Tack. CPU 수보다 실제 mm cpumask 크기와 interrupt-off 구간이 tail latency를 지배한다.

02 · CONTRACT

공통 계약과 architecture 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequenceTLBI만 실행하고 completion barrier를 빼면 page free 뒤 remote walker가 old PA를 사용한다.TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다.
x86-64INVLPG, INVPCID 또는 CR3 reload와 TLB generationCPU가 mm를 막 load하는 순간 대상 mask에서 빠지면 stale PTE를 무기한 사용한다.start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다.
RISC-Vlocal/remote SFENCE.VMA와 SBI RFENCE 또는 IPISBI/firmware가 잘못된 hart mask나 ASID를 받으면 일부 hart에만 stale translation이 남는다.start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 목적, 서로 다른 mechanism각 ISA에서 실제로 추적할 state와 checkpoint를 한 줄에 맞췄습니다.

arm64

mechanism
TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequence
state
range TLBI 지원 여부와 granule에 따라 한 명령 또는 반복 invalidate를 선택한다. inner-shareable TLBI와 DSB가 다른 PE의 completion을 보장한다.
checkpoint
TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다.

x86-64

mechanism
INVLPG, INVPCID 또는 CR3 reload와 TLB generation
state
range page 수, ceiling과 freed_tables에 따라 per-page INVLPG와 full context flush를 고른다. remote CPU는 flush info의 new_tlb_gen을 적용한다.
checkpoint
start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다.

RISC-V

mechanism
local/remote SFENCE.VMA와 SBI RFENCE 또는 IPI
state
ASID와 range 크기에 따라 local fence, IPI 기반 remote fence 또는 SBI remote sfence를 선택한다. hardware range invalidate가 제한적이면 page별 또는 전체 fence로 확대한다.
checkpoint
start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다.
그림 2. 공통 kernel과 architecture hook의 소유권공통 정책이 hardware state를 직접 소유하지 않는 경계를 표시합니다.
Linux common contractinitiator는 PTE write를 publish하고 mm를 실행할 수 있는 CPU mask를 얻어 local invalidate와 remote request를 보낸다. page-table page free는 모든 대상 CPU의 invalidate completion 뒤에만 가능하다.
arm64TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequencePTE store 뒤 DSB, TLBI, completion DSB와 필요한 ISB 순서를 지킨다.
x86-64INVLPG, INVPCID 또는 CR3 reload와 TLB generationswitch_mm의 loaded_mm/tlb_gen publication과 shootdown side의 mm_cpumask read가 ordering pair를 이룬다.
RISC-Vlocal/remote SFENCE.VMA와 SBI RFENCE 또는 IPIPTE store의 visibility와 remote fence request ordering이 보장되어야 receiver가 새 PTE를 읽는다.
lifetime boundaryinvalidated physical page와 page-table page는 remote TLB entry가 사라지기 전까지 재사용하면 안 된다. mmu_gather가 이 deferred free를 관리한다.
그림 3. publication과 관찰 순서state를 준비한 뒤 architecture ordering을 거쳐 관찰 가능한 checkpoint가 됩니다.
arm64state 준비PTE store 뒤 DSB, TLBI, completion DSB와 필요한 ISB 순서를 지킨다.관찰: TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다.
x86-64state 준비switch_mm의 loaded_mm/tlb_gen publication과 shootdown side의 mm_cpumask read가 ordering pair를 이룬다.관찰: start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다.
RISC-Vstate 준비PTE store의 visibility와 remote fence request ordering이 보장되어야 receiver가 새 PTE를 읽는다.관찰: start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다.

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequence

range TLBI 지원 여부와 granule에 따라 한 명령 또는 반복 invalidate를 선택한다. inner-shareable TLBI와 DSB가 다른 PE의 completion을 보장한다.

원본 코드: arch/arm64/include/asm/tlbflush.h:364-420

364static inline void flush_tlb_all(void)
365{
366	dsb(ishst);
367	__tlbi(vmalle1is);
368	__tlbi_sync_s1ish_kernel();
369	isb();
370}
371 
372static inline void flush_tlb_mm(struct mm_struct *mm)
373{
374	unsigned long asid;
375 
376	dsb(ishst);
377	asid = __TLBI_VADDR(0, ASID(mm));
378	__tlbi(aside1is, asid);
379	__tlbi_user(aside1is, asid);
380	__tlbi_sync_s1ish(mm);
381	mmu_notifier_arch_invalidate_secondary_tlbs(mm, 0, -1UL);
382}
383 
384static inline void __flush_tlb_page_nosync(struct mm_struct *mm,
385					   unsigned long uaddr)
386{
387	unsigned long addr;
388 
389	dsb(ishst);
390	addr = __TLBI_VADDR(uaddr, ASID(mm));
391	__tlbi(vale1is, addr);
392	__tlbi_user(vale1is, addr);
393	mmu_notifier_arch_invalidate_secondary_tlbs(mm, uaddr & PAGE_MASK,
394						(uaddr & PAGE_MASK) + PAGE_SIZE);
395}
396 
397static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,
398					 unsigned long uaddr)
399{
400	return __flush_tlb_page_nosync(vma->vm_mm, uaddr);
401}
402 
403static inline void flush_tlb_page(struct vm_area_struct *vma,
404				  unsigned long uaddr)
405{
406	flush_tlb_page_nosync(vma, uaddr);
407	__tlbi_sync_s1ish(vma->vm_mm);
408}
409 
410static inline bool arch_tlbbatch_should_defer(struct mm_struct *mm)
411{
412	return true;
413}
414 
415/*
416 * To support TLB batched flush for multiple pages unmapping, we only send
417 * the TLBI for each page in arch_tlbbatch_add_pending() and wait for the
418 * completion at the end in arch_tlbbatch_flush(). Since we've already issued
419 * TLBI for each page so only a DSB is needed to synchronise its effect on the
420 * other CPUs.

주요 구문 해설

원본 57줄은 그대로 표시하며, 검토한 31개 구문에 설명을 붙였습니다.

L364static inline void flush_tlb_all(void)

EL1 주소 변환 체계 전체의 TLB를 공유 범위의 CPU들에 무효화하고 완료까지 동기화하는 함수입니다. 특정 태스크의 ASID 하나로 범위를 제한하지 않습니다.

L366 dsb(ishst);

이전 페이지 테이블 쓰기가 inner-shareable 범위에 완료된 뒤 TLB 무효화를 시작하도록 DSB ISHST를 실행합니다. 새 테이블 값보다 무효화가 먼저 관찰되는 순서를 막습니다.

L367 __tlbi(vmalle1is);

TLBI VMALLE1IS로 현재 EL1 변환 체계의 모든 stage-1 TLB 항목을 inner-shareable 범위의 CPU들에 무효화 요청합니다. 특정 mm나 한 주소만 고르는 명령이 아니며, 완료 보장은 다음 동기화에서 수행합니다.

L368 __tlbi_sync_s1ish_kernel();

앞서 내보낸 전체 TLB 무효화가 완료되도록 DSB ISH로 기다립니다. 필요한 CPU에서는 반복 TLBI 보완도 적용하며, 그 다음 ISB가 이후 명령 실행에 바뀐 변환 상태를 반영합니다.

L369 isb();

완료된 TLB 유지보수 이후의 명령 실행 문맥을 ISB로 동기화합니다. 현재 CPU가 변경된 주소 변환 상태를 반영해 계속 실행하도록 합니다.

L372static inline void flush_tlb_mm(struct mm_struct *mm)

한 mm에 속하는 ASID의 TLB 항목을 전체 주소 범위에서 무효화합니다. 사용자용 ASID 분리와 보조 TLB 알림도 함께 처리합니다.

L374 unsigned long asid;

mm의 ASID를 TLBI 명령이 받는 비트 배치로 만든 값을 보관합니다. 원시 ASID 숫자만 들어 있는 변수로 보면 안 됩니다.

L376 dsb(ishst);

이 mm의 페이지 테이블 갱신이 완료된 뒤 ASID 무효화가 수행되도록 저장 장벽을 둡니다.

L377 asid = __TLBI_VADDR(0, ASID(mm));

mm의 ASID를 TLBI 피연산자 상위 비트에 배치합니다. ASID 전체를 무효화하는 ASIDE1IS는 특정 VA가 필요하지 않으므로 주소 부분에 0을 사용합니다.

L378 __tlbi(aside1is, asid);

inner-shareable domain에서 해당 ASID의 stage-1 translation을 무효화한다.

L379 __tlbi_user(aside1is, asid);

사용자 모드에서 커널 매핑을 분리하는 구성이면 USER_ASID_FLAG가 붙은 사용자용 ASID에도 같은 무효화를 요청합니다. 한 mm에 대응하는 커널·사용자 ASID 쌍 중 한쪽만 오래된 상태로 남지 않게 합니다.

L380 __tlbi_sync_s1ish(mm);

mm에 대한 stage-1 무효화 완료를 DSB ISH로 보장하고 필요한 CPU 오류 우회와 SME 동기화를 처리합니다. 여러 CPU로 요청을 보낸 것과 그 요청이 끝난 것을 구분하는 단계입니다.

L381 mmu_notifier_arch_invalidate_secondary_tlbs(mm, 0, -1UL);

mm 전체 주소 범위를 사용하는 보조 TLB 구독자에게도 무효화를 알립니다. 0과 -1UL은 전체 범위를 뜻하며, CPU TLB 외의 주소 변환 캐시가 이전 매핑을 계속 쓰지 않도록 연결합니다.

L384static inline void __flush_tlb_page_nosync(struct mm_struct *mm,

mm의 한 사용자 페이지에 무효화 요청을 내는 내부 함수입니다. 완료 대기를 생략하므로 호출자가 나중에 동기화할 수 있습니다.

L385 unsigned long uaddr)

무효화할 페이지를 포함하는 사용자 가상주소 uaddr를 받습니다. 실제 TLBI 피연산자는 아래에서 ASID와 결합해 만듭니다.

L387 unsigned long addr;

가상주소와 ASID가 합쳐진 TLBI 피연산자를 담습니다. 일반 메모리를 역참조할 포인터가 아닙니다.

L389 dsb(ishst);

해당 페이지의 테이블 쓰기가 무효화 요청에 앞서 반영되도록 DSB ISHST로 순서를 맞춥니다.

L390 addr = __TLBI_VADDR(uaddr, ASID(mm));

사용자 가상주소와 mm의 ASID를 TLBI 명령 형식에 맞게 합칩니다. 주소를 12비트 오른쪽으로 옮긴 필드와 ASID 상위 필드를 만들며, 가상주소를 물리주소로 바꾸는 계산은 아닙니다.

L391 __tlbi(vale1is, addr);

TLBI VALE1IS로 지정한 ASID·가상주소의 최종 단계 변환을 공유 범위에서 무효화합니다. 이 nosync 함수는 요청을 발행하되 완료 대기는 호출자 쪽에 맡깁니다.

L392 __tlbi_user(vale1is, addr);

커널 매핑 분리가 켜져 있으면 같은 주소의 사용자용 ASID 항목에도 최종 단계 무효화를 요청합니다. 별도 사용자 ASID가 없는 구성에서는 이 매크로가 추가 명령을 내지 않습니다.

L393 mmu_notifier_arch_invalidate_secondary_tlbs(mm, uaddr & PAGE_MASK,

mm를 따라가는 보조 TLB에도 한 페이지의 무효화를 알립니다. PAGE_MASK로 주소를 페이지 시작 경계에 맞춰 시작점을 넘깁니다.

L394 (uaddr & PAGE_MASK) + PAGE_SIZE);

페이지 시작에 PAGE_SIZE를 더해 배타적 끝 주소를 지정합니다. 보조 TLB가 받는 범위는 해당 페이지 전체인 [시작, 시작 + PAGE_SIZE)입니다.

L397static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,

VMA를 받는 공개 형태의 한 페이지 무효화 요청 함수입니다. VMA에서 mm를 꺼내 내부 함수에 전달하며 완료는 기다리지 않습니다.

L398 unsigned long uaddr)

해당 VMA 안에서 무효화할 사용자 가상주소를 받습니다. VMA 전체를 비우는 요청이 아니라 이 페이지를 지정합니다.

L400 return __flush_tlb_page_nosync(vma->vm_mm, uaddr);

VMA가 속한 mm와 사용자 주소를 내부 nosync 함수에 넘기고 끝냅니다. void 함수의 위임 표현이며 성공·오류 코드를 반환하는 문장은 아닙니다.

L403static inline void flush_tlb_page(struct vm_area_struct *vma,

한 사용자 페이지를 무효화하고 완료까지 기다리는 함수입니다. 요청만 하는 nosync 형태 뒤에 공통 동기화를 덧붙입니다.

L404 unsigned long uaddr)

무효화할 가상주소를 받습니다. 어떤 주소 공간인지는 함께 전달된 VMA의 vm_mm에서 얻습니다.

L406 flush_tlb_page_nosync(vma, uaddr);

vma의 주소 공간에서 uaddr 페이지의 TLB 무효화를 요청하고 보조 TLB에도 해당 페이지 범위를 알립니다. 이름의 nosync는 최종 완료 대기를 다음 줄에서 한 번 수행한다는 뜻입니다.

L407 __tlbi_sync_s1ish(vma->vm_mm);

바로 앞에서 요청한 한 페이지의 무효화가 완료될 때까지 동기화합니다. vma->vm_mm을 넘겨 해당 주소 공간에 필요한 SME 관련 동기화도 함께 처리합니다.

L410static inline bool arch_tlbbatch_should_defer(struct mm_struct *mm)

공통 TLB 배치 코드에 완료 대기를 미뤄도 되는지 알려 주는 정책 함수입니다. arm64는 각 페이지의 TLBI를 먼저 내고 대기를 묶을 수 있습니다.

L412 return true;

항상 배치 지연을 허용합니다. 페이지마다 DSB로 기다리는 대신 여러 요청을 낸 뒤 배치 끝에서 한 번 완료를 기다리도록 할 수 있습니다.

x86-64 · Linux 6.18.37

INVLPG, INVPCID 또는 CR3 reload와 TLB generation

range page 수, ceiling과 freed_tables에 따라 per-page INVLPG와 full context flush를 고른다. remote CPU는 flush info의 new_tlb_gen을 적용한다.

원본 코드: arch/x86/mm/tlb.c:1440-1520

1440{
1441#ifdef CONFIG_DEBUG_VM
1442	/* Complete reentrancy prevention checks */
1443	barrier();
1444	this_cpu_dec(flush_tlb_info_idx);
1445#endif
1446}
1447 
1448void flush_tlb_mm_range(struct mm_struct *mm, unsigned long start,
1449				unsigned long end, unsigned int stride_shift,
1450				bool freed_tables)
1451{
1452	struct flush_tlb_info *info;
1453	int cpu = get_cpu();
1454	u64 new_tlb_gen;
1455 
1456	/* This is also a barrier that synchronizes with switch_mm(). */
1457	new_tlb_gen = inc_mm_tlb_gen(mm);
1458 
1459	info = get_flush_tlb_info(mm, start, end, stride_shift, freed_tables,
1460				  new_tlb_gen);
1461 
1462	/*
1463	 * flush_tlb_multi() is not optimized for the common case in which only
1464	 * a local TLB flush is needed. Optimize this use-case by calling
1465	 * flush_tlb_func_local() directly in this case.
1466	 */
1467	if (mm_global_asid(mm)) {
1468		broadcast_tlb_flush(info);
1469	} else if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {
1470		info->trim_cpumask = should_trim_cpumask(mm);
1471		flush_tlb_multi(mm_cpumask(mm), info);
1472		consider_global_asid(mm);
1473	} else if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {
1474		lockdep_assert_irqs_enabled();
1475		local_irq_disable();
1476		flush_tlb_func(info);
1477		local_irq_enable();
1478	}
1479 
1480	put_flush_tlb_info();
1481	put_cpu();
1482	mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, end);
1483}
1484 
1485static void do_flush_tlb_all(void *info)
1486{
1487	count_vm_tlb_event(NR_TLB_REMOTE_FLUSH_RECEIVED);
1488	__flush_tlb_all();
1489}
1490 
1491void flush_tlb_all(void)
1492{
1493	count_vm_tlb_event(NR_TLB_REMOTE_FLUSH);
1494 
1495	/* First try (faster) hardware-assisted TLB invalidation. */
1496	if (cpu_feature_enabled(X86_FEATURE_INVLPGB))
1497		invlpgb_flush_all();
1498	else
1499		/* Fall back to the IPI-based invalidation. */
1500		on_each_cpu(do_flush_tlb_all, NULL, 1);
1501}
1502 
1503/* Flush an arbitrarily large range of memory with INVLPGB. */
1504static void invlpgb_kernel_range_flush(struct flush_tlb_info *info)
1505{
1506	unsigned long addr, nr;
1507 
1508	for (addr = info->start; addr < info->end; addr += nr << PAGE_SHIFT) {
1509		nr = (info->end - addr) >> PAGE_SHIFT;
1510 
1511		/*
1512		 * INVLPGB has a limit on the size of ranges it can
1513		 * flush. Break up large flushes.
1514		 */
1515		nr = clamp_val(nr, 1, invlpgb_count_max);
1516 
1517		invlpgb_flush_addr_nosync(addr, nr);
1518	}
1519	__tlbsync();
1520}

주요 구문 해설

원본 81줄은 그대로 표시하며, 검토한 44개 구문에 설명을 붙였습니다.

L1441#ifdef CONFIG_DEBUG_VM

DEBUG_VM을 켜면 CPU별 TLB flush 정보의 중첩 사용을 추적합니다. 이 지점에서 사용 카운터를 내리고 컴파일러 배리어를 넣어 앞의 재진입 검사를 마무리하며, 꺼지면 이 진단용 작업을 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L1443 barrier();

컴파일러가 앞선 flush 정보 사용을 아래의 재진입 검사 카운터 감소 뒤로 옮기지 못하게 합니다. CPU 명령 수준의 메모리 장벽을 발행하는 동작은 아닙니다.

L1444 this_cpu_dec(flush_tlb_info_idx);

현재 CPU의 flush_tlb_info 사용 깊이를 하나 내립니다. 디버그용 재진입 검사를 끝내는 처리이며 할당된 메모리를 해제하는 함수는 아닙니다.

L1445#endif

1441행에서 시작한 선택 구간을 마칩니다. DEBUG_VM을 켜면 CPU별 TLB flush 정보의 중첩 사용을 추적합니다. 이 지점에서 사용 카운터를 내리고 컴파일러 배리어를 넣어 앞의 재진입 검사를 마무리하며, 꺼지면 이 진단용 작업을 생략합니다.

L1448void flush_tlb_mm_range(struct mm_struct *mm, unsigned long start,

mm의 지정 가상주소 구간에 대한 TLB 무효화를 조직합니다. CPU별 세대를 갱신하고 하드웨어 broadcast·원격 CPU·로컬 처리 중 필요한 경로를 고릅니다.

L1449 unsigned long end, unsigned int stride_shift,

end는 무효화 구간의 배타적 끝이며 stride_shift는 페이지 간격을 2의 지수로 나타냅니다. 큰 페이지와 기본 페이지의 무효화 단위를 구분합니다.

L1450 bool freed_tables)

페이지 테이블 자체를 해제했는지 받습니다. 단순 매핑 변경과 달리 오래된 테이블 참조도 남지 않아야 하므로 이 정보를 CPU별 처리에 전달합니다.

L1452 struct flush_tlb_info *info;

mm·주소 범위·단위·세대를 묶은 이번 무효화 요청을 가리킵니다. 로컬 처리기와 원격 처리기가 같은 요청 내용을 읽습니다.

L1453 int cpu = get_cpu();

선점을 막고 현재 CPU 번호를 얻습니다. 함수가 CPU별 flush 정보와 loaded_mm 상태를 처리하는 동안 다른 CPU로 이동하지 않도록 합니다.

L1454 u64 new_tlb_gen;

이번 페이지 테이블 변경 뒤의 새 TLB 세대를 보관합니다. CPU는 자신의 마지막 처리 세대와 비교하여 누락된 무효화가 있는지 알 수 있습니다.

L1457 new_tlb_gen = inc_mm_tlb_gen(mm);

PTE 변경 세대를 올려 switch path와 remote flush가 stale generation을 탐지하게 한다.

L1459 info = get_flush_tlb_info(mm, start, end, stride_shift, freed_tables,

현재 CPU용 요청 자료에 mm, 범위, 페이지 단위와 테이블 해제 여부를 채웁니다. 범위가 너무 크면 내부에서 전체 무효화로 바꿀 수도 있습니다.

L1460 new_tlb_gen);

방금 증가시킨 mm의 TLB 세대도 요청에 함께 넣습니다. 처리기가 현재 CPU의 변환 캐시가 어느 변경까지 반영했는지 갱신할 기준입니다.

L1463 * flush_tlb_multi() is not optimized for the common case in which only

선택한 CPU mask에 shootdown callback을 보내고 completion을 기다리는 경계다.

L1467 if (mm_global_asid(mm)) {

이 mm에 모든 CPU에서 공유하는 global ASID가 배정돼 있는지 검사합니다. 있으면 같은 식별자로 하드웨어 broadcast 무효화를 수행할 수 있습니다.

L1468 broadcast_tlb_flush(info);

모든 CPU에서 같은 값으로 쓰는 global ASID를 가진 mm이므로 INVLPGB로 범위를 broadcast 무효화합니다. 필요한 ASID 전환도 마무리하고 TLBSYNC로 완료를 기다려, CPU마다 IPI 처리기를 호출하는 부담을 줄입니다.

L1469 } else if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {

현재 CPU 외에도 mm 사용 마스크에 다른 CPU가 있는지 검사합니다. 있다면 로컬 처리만으로 부족하므로 해당 CPU들을 포함해 무효화합니다.

L1470 info->trim_cpumask = should_trim_cpumask(mm);

mm의 CPU 사용 마스크를 정리할 주기가 되었는지 확인해 요청에 기록합니다. 이 검사는 약 1초 간격으로 허용하며, 원격 처리에서 더 이상 해당 mm를 쓰지 않는 CPU를 마스크에서 빼 불필요한 요청을 줄입니다.

L1471 flush_tlb_multi(mm_cpumask(mm), info);

선택한 CPU mask에 shootdown callback을 보내고 completion을 기다리는 경계다.

L1472 consider_global_asid(mm);

INVLPGB를 쓸 수 있는 환경에서 이 mm에 global ASID를 줄지 간헐적으로 검사합니다. 네 개 이상의 CPU에서 동시에 활발히 쓰이는 주소 공간이면 하드웨어 broadcast 무효화의 이득을 얻도록 전환을 검토합니다.

L1473 } else if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {

다른 대상 CPU가 없고 이 CPU가 실제로 mm를 로드한 경우인지 확인합니다. 맞으면 원격 요청을 준비할 필요 없이 로컬 처리기를 직접 실행합니다.

L1474 lockdep_assert_irqs_enabled();

현재 경로가 로컬 IRQ를 허용한 상태에서 들어왔는지 잠금 검증기로 확인합니다. 바로 뒤에서 IRQ를 끄고 로컬 TLB 처리를 직접 호출하기 위한 전제를 검사합니다.

L1475 local_irq_disable();

현재 CPU의 인터럽트를 막습니다. 로컬 loaded_mm과 TLB 세대를 읽고 갱신하는 동안 인터럽트 경로와 섞이지 않게 합니다.

L1476 flush_tlb_func(info);

info의 주소 공간·범위·세대를 사용해 이 CPU의 TLB 갱신 필요 여부를 판단하고 필요한 무효화를 수행합니다. 다른 CPU로 IPI를 보내는 경로가 아니라 로컬 처리입니다.

L1477 local_irq_enable();

로컬 TLB 처리를 끝낸 뒤 인터럽트를 다시 허용합니다. 진입 전 IRQ가 허용돼 있어야 한다는 위 검사와 짝을 이룹니다.

L1480 put_flush_tlb_info();

CPU별 flush 요청 정보의 사용을 마칩니다. 디버그 구성에서는 사용 깊이를 낮춰 다음 요청의 재진입 검사가 가능하게 합니다.

L1481 put_cpu();

get_cpu에서 막았던 선점을 다시 허용합니다. CPU별 상태에 의존하는 작업이 끝났으므로 태스크가 다른 CPU로 이동할 수 있습니다.

L1482 mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, end);

CPU TLB 외에 이 mm를 따라가는 보조 주소 변환 사용자에게 [start, end) 무효화를 알립니다. 가상화 등 별도 변환 캐시가 오래된 매핑을 유지하지 않도록 합니다.

L1485static void do_flush_tlb_all(void *info)

각 CPU에서 전체 TLB 무효화를 수행할 콜백입니다. 통계를 남기고 현재 CPU의 TLB를 비우며, info 인자는 이 구현에서 사용하지 않습니다.

L1487 count_vm_tlb_event(NR_TLB_REMOTE_FLUSH_RECEIVED);

원격 전체 TLB 무효화 요청을 이 CPU가 받았다는 통계를 증가시킵니다. 실제 TLB 작업은 다음 줄에서 수행합니다.

L1488 __flush_tlb_all();

이 CPU의 전체 TLB를 무효화합니다. 모든 CPU를 대상으로 실행되는 do_flush_tlb_all 콜백의 실제 하드웨어 작업입니다.

L1491void flush_tlb_all(void)

모든 CPU의 TLB를 무효화하는 진입점입니다. INVLPGB 지원 시 하드웨어 broadcast를, 아니면 각 CPU에서 실행하는 콜백을 사용합니다.

L1493 count_vm_tlb_event(NR_TLB_REMOTE_FLUSH);

전체 TLB 무효화 요청을 보냈다는 VM 통계를 갱신합니다. 요청 횟수 기록과 실제 무효화는 별개의 단계입니다.

L1496 if (cpu_feature_enabled(X86_FEATURE_INVLPGB))

CPU가 INVLPGB 하드웨어 broadcast 무효화를 지원하는지 검사합니다. 지원하지 않을 때는 IPI 기반 방식으로 같은 전체 무효화를 수행합니다.

L1497 invlpgb_flush_all();

INVLPGB 기능이 있는 CPU에서는 하드웨어의 broadcast 무효화로 전체 TLB 갱신을 수행합니다. 소프트웨어 IPI 콜백을 CPU마다 보내는 비용을 줄이는 경로입니다.

L1498 else

INVLPGB가 없는 CPU의 경로입니다. on_each_cpu로 각 CPU가 자신의 TLB를 직접 비우도록 요청합니다.

L1500 on_each_cpu(do_flush_tlb_all, NULL, 1);

하드웨어 broadcast 방식이 없으면 각 CPU에서 do_flush_tlb_all을 실행합니다. 마지막 인자 1은 모든 대상 CPU의 처리가 끝날 때까지 기다리도록 합니다.

L1504static void invlpgb_kernel_range_flush(struct flush_tlb_info *info)

커널 주소 범위를 INVLPGB가 표현할 수 있는 크기로 나누어 무효화합니다. 모든 조각을 요청한 다음 한 번 완료를 기다립니다.

L1506 unsigned long addr, nr;

addr는 이번에 처리할 가상주소이고 nr는 해당 요청에서 비울 기본 페이지 수입니다. 바이트 주소와 페이지 개수를 분리해 관리합니다.

L1508 for (addr = info->start; addr < info->end; addr += nr << PAGE_SHIFT) {

start부터 end 직전까지 반복하며 방금 처리한 nr개 페이지의 바이트 수만큼 전진합니다. 한 명령의 범위 제한보다 큰 구간도 여러 번에 나눕니다.

L1509 nr = (info->end - addr) >> PAGE_SHIFT;

남은 바이트 길이를 PAGE_SHIFT만큼 나누어 기본 페이지 수로 구합니다. 다음 줄이 하드웨어의 한 번 처리 한도에 맞춰 이 수를 제한합니다.

L1515 nr = clamp_val(nr, 1, invlpgb_count_max);

한 번에 무효화할 페이지 수를 최소 1, 최대 invlpgb_count_max 사이로 제한합니다. 명령 한 번이 표현할 수 있는 범위보다 큰 요청은 루프에서 나눠 처리합니다.

L1517 invlpgb_flush_addr_nosync(addr, nr);

addr부터 nr개 페이지에 대해 INVLPGB 무효화를 요청하되 매번 완료를 기다리지 않습니다. 여러 구간을 요청한 뒤 루프 밖에서 한 번 동기화합니다.

L1519 __tlbsync();

앞서 발행한 비동기 INVLPGB 요청이 완료되도록 TLBSYNC를 실행합니다. 이 지점 뒤에는 범위 전체의 무효화가 끝난 것으로 진행할 수 있습니다.

RISC-V · Linux 6.18.37

local/remote SFENCE.VMA와 SBI RFENCE 또는 IPI

ASID와 range 크기에 따라 local fence, IPI 기반 remote fence 또는 SBI remote sfence를 선택한다. hardware range invalidate가 제한적이면 page별 또는 전체 fence로 확대한다.

원본 코드: arch/riscv/mm/tlbflush.c:149-213

149		mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, start + size);
150}
151 
152void flush_tlb_mm(struct mm_struct *mm)
153{
154	__flush_tlb_range(mm, mm_cpumask(mm), 0, FLUSH_TLB_MAX_SIZE, PAGE_SIZE);
155}
156 
157void flush_tlb_mm_range(struct mm_struct *mm,
158			unsigned long start, unsigned long end,
159			unsigned int page_size)
160{
161	__flush_tlb_range(mm, mm_cpumask(mm), start, end - start, page_size);
162}
163 
164void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)
165{
166	__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),
167			  addr, PAGE_SIZE, PAGE_SIZE);
168}
169 
170void flush_tlb_range(struct vm_area_struct *vma, unsigned long start,
171		     unsigned long end)
172{
173	unsigned long stride_size;
174 
175	if (!is_vm_hugetlb_page(vma)) {
176		stride_size = PAGE_SIZE;
177	} else {
178		stride_size = huge_page_size(hstate_vma(vma));
179 
180		/*
181		 * As stated in the privileged specification, every PTE in a
182		 * NAPOT region must be invalidated, so reset the stride in that
183		 * case.
184		 */
185		if (has_svnapot()) {
186			if (stride_size >= PGDIR_SIZE)
187				stride_size = PGDIR_SIZE;
188			else if (stride_size >= P4D_SIZE)
189				stride_size = P4D_SIZE;
190			else if (stride_size >= PUD_SIZE)
191				stride_size = PUD_SIZE;
192			else if (stride_size >= PMD_SIZE)
193				stride_size = PMD_SIZE;
194			else
195				stride_size = PAGE_SIZE;
196		}
197	}
198 
199	__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),
200			  start, end - start, stride_size);
201}
202 
203void flush_tlb_kernel_range(unsigned long start, unsigned long end)
204{
205	__flush_tlb_range(NULL, cpu_online_mask,
206			  start, end - start, PAGE_SIZE);
207}
208 
209#ifdef CONFIG_TRANSPARENT_HUGEPAGE
210void flush_pmd_tlb_range(struct vm_area_struct *vma, unsigned long start,
211			unsigned long end)
212{
213	__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),

주요 구문 해설

원본 65줄은 그대로 표시하며, 검토한 37개 구문에 설명을 붙였습니다.

L149 mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, start + size);

CPU 쪽 무효화를 마친 뒤 mm에 연결된 보조 TLB 구독자에게 [start, start + size) 범위를 알립니다. 가상화나 장치 쪽 변환 캐시도 같은 주소 변경을 따라가게 하며, mm가 없는 커널 공통 요청에는 호출하지 않습니다.

L152void flush_tlb_mm(struct mm_struct *mm)

한 mm의 전체 사용자 주소 공간을 무효화하는 외부 함수입니다. 그 mm를 사용한 CPU 마스크를 공통 처리기에 전달합니다.

L154 __flush_tlb_range(mm, mm_cpumask(mm), 0, FLUSH_TLB_MAX_SIZE, PAGE_SIZE);

이 mm를 사용한 CPU 마스크와 전체 주소 공간을 뜻하는 범위로 공통 무효화 함수를 호출합니다. 필요한 대상에 따라 로컬 SFENCE, SBI 원격 fence 또는 IPI 방식을 선택하게 합니다.

L157void flush_tlb_mm_range(struct mm_struct *mm,

한 mm의 일부 주소 구간을 호출자가 지정한 페이지 간격으로 무효화하는 함수입니다.

L158 unsigned long start, unsigned long end,

시작 주소와 배타적 끝 주소를 받습니다. 내부 호출에서는 이 두 주소의 차이를 바이트 길이로 바꿉니다.

L159 unsigned int page_size)

개별 주소 무효화의 간격을 바이트 단위 page_size로 받습니다. 기본 페이지 또는 큰 페이지에 맞는 단위로 진행하도록 합니다.

L161 __flush_tlb_range(mm, mm_cpumask(mm), start, end - start, page_size);

끝 주소를 end - start 바이트 길이로 바꾸어 무효화할 구간을 전달합니다. page_size는 개별 주소 무효화의 간격이며, mm_cpumask로 해당 주소 공간과 관련된 CPU들에 작업을 한정합니다.

L164void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)

VMA 안의 주소 하나에 해당하는 기본 페이지를 무효화합니다. 주소 공간과 대상 CPU는 VMA의 mm에서 가져옵니다.

L166 __flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),

VMA의 mm와 그 주소 공간을 사용한 CPU 마스크를 공통 TLB 처리에 전달합니다.

L167 addr, PAGE_SIZE, PAGE_SIZE);

addr부터 PAGE_SIZE만큼을 기본 페이지 간격으로 무효화하도록 지정합니다. 첫 PAGE_SIZE는 범위 길이, 두 번째는 주소를 전진할 간격입니다.

L170void flush_tlb_range(struct vm_area_struct *vma, unsigned long start,

VMA의 종류에 맞는 간격을 골라 지정 범위를 무효화합니다. HugeTLB 및 Svnapot 배치에서는 기본 페이지와 다른 고려가 필요합니다.

L171 unsigned long end)

무효화할 구간의 배타적 끝 주소입니다. start와의 차이가 공통 처리기에 넘길 범위 길이가 됩니다.

L173 unsigned long stride_size;

개별 TLB 무효화 사이의 바이트 간격을 보관합니다. 큰 매핑을 불필요하게 잘게 처리하지 않으면서 필요한 PTE를 빠뜨리지 않도록 고릅니다.

L175 if (!is_vm_hugetlb_page(vma)) {

명시적 HugeTLB VMA가 아닌지 검사합니다. 일반 VMA는 기본 페이지 간격으로 안전하게 처리합니다.

L176 stride_size = PAGE_SIZE;

일반 VMA의 무효화 간격을 기본 PAGE_SIZE로 정합니다. 큰 매핑 여부를 섣불리 가정해 중간 페이지를 건너뛰지 않습니다.

L177 } else {

HugeTLB VMA의 경로입니다. 먼저 그 영역에 설정된 거대 페이지 크기를 얻고 필요하면 Svnapot에 맞춰 더 촘촘히 조정합니다.

L178 stride_size = huge_page_size(hstate_vma(vma));

HugeTLB 영역에 연결된 hstate에서 실제 거대 페이지 크기를 구해 무효화 간격으로 사용합니다. 뒤에서 Svnapot처럼 더 작은 간격이 필요한 경우를 보정하므로, 이 값이 항상 최종 간격인 것은 아닙니다.

L185 if (has_svnapot()) {

Svnapot 연속 매핑 확장을 사용할 수 있는지 검사합니다. NAPOT 영역의 모든 관련 PTE를 무효화해야 하므로 거대 페이지 크기를 그대로 간격으로 쓰지 않을 수 있습니다.

L186 if (stride_size >= PGDIR_SIZE)

거대 페이지 크기가 최상위 PGDIR 항목의 담당 범위 이상인지 확인합니다. 크기에 맞는 페이지 테이블 단계 단위로 간격을 낮춥니다.

L187 stride_size = PGDIR_SIZE;

무효화 간격을 PGDIR_SIZE로 제한합니다. 거대 페이지 전체 크기만큼 건너뛰어 필요한 테이블 항목을 놓치지 않도록 합니다.

L188 else if (stride_size >= P4D_SIZE)

PGDIR보다 작지만 P4D 한 항목의 범위 이상인지 검사합니다. 이 경우 P4D 단위가 다음 후보입니다.

L189 stride_size = P4D_SIZE;

무효화 간격을 P4D_SIZE로 정합니다. 이 상수의 실제 크기는 사용 중인 페이지 테이블 구성에 따릅니다.

L190 else if (stride_size >= PUD_SIZE)

더 작은 거대 페이지가 PUD 범위 이상인지 검사합니다. 해당하면 PUD 항목별 간격을 사용합니다.

L191 stride_size = PUD_SIZE;

무효화 간격을 PUD_SIZE로 정해 필요한 PUD 범위마다 주소 무효화를 수행하도록 합니다.

L192 else if (stride_size >= PMD_SIZE)

거대 페이지가 PMD 항목의 범위 이상인지 확인합니다. 맞으면 PMD 단위로 구간을 나눕니다.

L193 stride_size = PMD_SIZE;

무효화 간격을 PMD_SIZE로 정합니다. 더 큰 거대 페이지 크기만큼 건너뛰지 않도록 줄이는 선택입니다.

L194 else

거대 페이지 크기가 위의 테이블 단계 범위보다 작은 경우입니다. NAPOT를 구성하는 기본 PTE들을 모두 다룰 간격이 필요합니다.

L195 stride_size = PAGE_SIZE;

무효화 간격을 기본 PAGE_SIZE로 낮춥니다. NAPOT 연속 매핑의 각 PTE를 빠뜨리지 않도록 기본 페이지마다 처리합니다.

L199 __flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),

VMA의 주소 공간과 관련 CPU 집합을 공통 무효화 함수에 전달합니다. 아래에서 정한 범위와 간격으로 로컬·원격 fence 방식을 선택합니다.

L200 start, end - start, stride_size);

[start, end) 구간의 길이와 VMA·Svnapot에 맞춰 선택한 간격을 넘깁니다. 길이는 전체 범위이고 stride_size는 개별 무효화의 간격입니다.

L203void flush_tlb_kernel_range(unsigned long start, unsigned long end)

여러 태스크가 공유하는 커널 가상주소 구간의 TLB를 무효화합니다. 한 사용자 mm에 대상을 제한할 수 없는 요청입니다.

L205 __flush_tlb_range(NULL, cpu_online_mask,

mm를 NULL로 두고 모든 online CPU를 대상으로 지정합니다. 커널 공통 매핑을 바꿨으므로 특정 프로세스의 CPU 마스크를 사용하지 않습니다.

L206 start, end - start, PAGE_SIZE);

커널 구간의 길이를 end - start로, 간격을 PAGE_SIZE로 지정합니다. 기본 페이지마다 필요한 무효화를 모든 대상 CPU에 적용합니다.

L209#ifdef CONFIG_TRANSPARENT_HUGEPAGE

Transparent Huge Page는 여러 기본 페이지를 큰 페이지 매핑으로 묶어 TLB 부담을 줄이는 기능입니다. 지원 빌드에서는 PMD 크기 단위로 관련 번역을 무효화하는 경로를 추가하고, 꺼지면 이 전용 flush 함수가 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L210void flush_pmd_tlb_range(struct vm_area_struct *vma, unsigned long start,

THP 구성에서 PMD 단위 매핑의 TLB 구간을 무효화하는 함수입니다. VMA를 통해 대상 mm를 선택합니다.

L211 unsigned long end)

PMD 무효화 구간의 배타적 끝 주소를 받습니다. 아래에서 시작 주소와의 차이로 범위 길이를 계산합니다.

L213 __flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),

VMA의 mm와 관련 CPU 집합에 무효화를 요청합니다. 다음 줄의 PMD_SIZE가 개별 주소 무효화 간격이 되어 THP의 PMD 매핑 단위에 맞춥니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

4KiB range flush와 full flush 선택

256KiB 범위, 4KiB page, mm 사용 CPU 6개, per-page invalidate 80ns, IPI 왕복 2us라고 가정한다.

  1. pages256KiB / 4KiB = 64개 translation이다.
  2. local costpage별이면 64x80ns=5.12us로 full-context flush 비용과 비교한다.
  3. remote cost6개 CPU에 병렬 IPI를 보내도 완료 시간은 가장 늦은 CPU의 2us+flush 시간이다.
  4. free boundary모든 ack 뒤에만 unmapped physical page와 table page를 allocator에 반환한다.

결론threshold는 page 수만이 아니라 CPU mask 크기, PCID/ASID 유지 이점과 remote interrupt latency에 따라 달라진다.

06 · DEEP DIVE

경계별 상세 분석

01

공통 kernel core와 architecture hook의 경계

initiator는 PTE write를 publish하고 mm를 실행할 수 있는 CPU mask를 얻어 local invalidate와 remote request를 보낸다. page-table page free는 모든 대상 CPU의 invalidate completion 뒤에만 가능하다.

range와 full-mm 선택, freed_tables 여부, huge mapping stride와 lazy CPU 제외가 shootdown 비용과 correctness를 결정한다.

02

arm64: TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequence

range TLBI 지원 여부와 granule에 따라 한 명령 또는 반복 invalidate를 선택한다. inner-shareable TLBI와 DSB가 다른 PE의 completion을 보장한다.

PTE store 뒤 DSB, TLBI, completion DSB와 필요한 ISB 순서를 지킨다. 디버깅할 때는 TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다.

03

x86-64: INVLPG, INVPCID 또는 CR3 reload와 TLB generation

range page 수, ceiling과 freed_tables에 따라 per-page INVLPG와 full context flush를 고른다. remote CPU는 flush info의 new_tlb_gen을 적용한다.

switch_mm의 loaded_mm/tlb_gen publication과 shootdown side의 mm_cpumask read가 ordering pair를 이룬다. 디버깅할 때는 start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다.

04

RISC-V: local/remote SFENCE.VMA와 SBI RFENCE 또는 IPI

ASID와 range 크기에 따라 local fence, IPI 기반 remote fence 또는 SBI remote sfence를 선택한다. hardware range invalidate가 제한적이면 page별 또는 전체 fence로 확대한다.

PTE store의 visibility와 remote fence request ordering이 보장되어야 receiver가 새 PTE를 읽는다. 디버깅할 때는 start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다.

05

객체 수명과 소유권을 먼저 고정한다

invalidated physical page와 page-table page는 remote TLB entry가 사라지기 전까지 재사용하면 안 된다. mmu_gather가 이 deferred free를 관리한다.

주소나 register 값이 맞는지만 확인하면 stale state를 놓친다. producer, publication, consumer와 폐기 지점을 같은 표에 기록한다.

06

latency upper bound는 hardware instruction 하나가 아니다

Tshoot = Tpublish + max(Tipi_delivery + Tremote_flush) + Tack. CPU 수보다 실제 mm cpumask 크기와 interrupt-off 구간이 tail latency를 지배한다.

평균값 외에 interrupt-off 구간, remote CPU 응답, firmware 호출과 retry 횟수를 분리해야 최악 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64TLBI만 실행하고 completion barrier를 빼면 page free 뒤 remote walker가 old PA를 사용한다.TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다.
x86-64CPU가 mm를 막 load하는 순간 대상 mask에서 빠지면 stale PTE를 무기한 사용한다.start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다.
RISC-VSBI/firmware가 잘못된 hart mask나 ASID를 받으면 일부 hart에만 stale translation이 남는다.start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다.

08 · LAB

재현과 계측 절차

  1. tlb:tlb_flush와 IPI trace를 함께 수집해 initiator와 가장 늦은 remote CPU를 찾는다.
  2. CPU 하나에서 interrupt를 길게 막아 shootdown upper bound가 어떻게 늘어나는지 측정한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinuxobjdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 좌표

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.