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 | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | TLBI ASIDE1/VAAE1 계열과 DSB/ISB sequence | TLBI만 실행하고 completion barrier를 빼면 page free 뒤 remote walker가 old PA를 사용한다. | TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다. |
| x86-64 | INVLPG, INVPCID 또는 CR3 reload와 TLB generation | CPU가 mm를 막 load하는 순간 대상 mask에서 빠지면 stale PTE를 무기한 사용한다. | start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다. |
| RISC-V | local/remote SFENCE.VMA와 SBI RFENCE 또는 IPI | SBI/firmware가 잘못된 hart mask나 ASID를 받으면 일부 hart에만 stale translation이 남는다. | start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다. |
03 · DIAGRAMS
세 그림으로 먼저 읽기
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을 확인한다.
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개 구문에 설명을 붙였습니다.
static inline void flush_tlb_all(void)EL1 주소 변환 체계 전체의 TLB를 공유 범위의 CPU들에 무효화하고 완료까지 동기화하는 함수입니다. 특정 태스크의 ASID 하나로 범위를 제한하지 않습니다.
dsb(ishst);이전 페이지 테이블 쓰기가 inner-shareable 범위에 완료된 뒤 TLB 무효화를 시작하도록 DSB ISHST를 실행합니다. 새 테이블 값보다 무효화가 먼저 관찰되는 순서를 막습니다.
__tlbi(vmalle1is);TLBI VMALLE1IS로 현재 EL1 변환 체계의 모든 stage-1 TLB 항목을 inner-shareable 범위의 CPU들에 무효화 요청합니다. 특정 mm나 한 주소만 고르는 명령이 아니며, 완료 보장은 다음 동기화에서 수행합니다.
__tlbi_sync_s1ish_kernel();앞서 내보낸 전체 TLB 무효화가 완료되도록 DSB ISH로 기다립니다. 필요한 CPU에서는 반복 TLBI 보완도 적용하며, 그 다음 ISB가 이후 명령 실행에 바뀐 변환 상태를 반영합니다.
isb();완료된 TLB 유지보수 이후의 명령 실행 문맥을 ISB로 동기화합니다. 현재 CPU가 변경된 주소 변환 상태를 반영해 계속 실행하도록 합니다.
static inline void flush_tlb_mm(struct mm_struct *mm)한 mm에 속하는 ASID의 TLB 항목을 전체 주소 범위에서 무효화합니다. 사용자용 ASID 분리와 보조 TLB 알림도 함께 처리합니다.
unsigned long asid;mm의 ASID를 TLBI 명령이 받는 비트 배치로 만든 값을 보관합니다. 원시 ASID 숫자만 들어 있는 변수로 보면 안 됩니다.
dsb(ishst);이 mm의 페이지 테이블 갱신이 완료된 뒤 ASID 무효화가 수행되도록 저장 장벽을 둡니다.
asid = __TLBI_VADDR(0, ASID(mm));mm의 ASID를 TLBI 피연산자 상위 비트에 배치합니다. ASID 전체를 무효화하는 ASIDE1IS는 특정 VA가 필요하지 않으므로 주소 부분에 0을 사용합니다.
__tlbi(aside1is, asid);inner-shareable domain에서 해당 ASID의 stage-1 translation을 무효화한다.
__tlbi_user(aside1is, asid);사용자 모드에서 커널 매핑을 분리하는 구성이면 USER_ASID_FLAG가 붙은 사용자용 ASID에도 같은 무효화를 요청합니다. 한 mm에 대응하는 커널·사용자 ASID 쌍 중 한쪽만 오래된 상태로 남지 않게 합니다.
__tlbi_sync_s1ish(mm);mm에 대한 stage-1 무효화 완료를 DSB ISH로 보장하고 필요한 CPU 오류 우회와 SME 동기화를 처리합니다. 여러 CPU로 요청을 보낸 것과 그 요청이 끝난 것을 구분하는 단계입니다.
mmu_notifier_arch_invalidate_secondary_tlbs(mm, 0, -1UL);mm 전체 주소 범위를 사용하는 보조 TLB 구독자에게도 무효화를 알립니다. 0과 -1UL은 전체 범위를 뜻하며, CPU TLB 외의 주소 변환 캐시가 이전 매핑을 계속 쓰지 않도록 연결합니다.
static inline void __flush_tlb_page_nosync(struct mm_struct *mm,mm의 한 사용자 페이지에 무효화 요청을 내는 내부 함수입니다. 완료 대기를 생략하므로 호출자가 나중에 동기화할 수 있습니다.
unsigned long uaddr)무효화할 페이지를 포함하는 사용자 가상주소 uaddr를 받습니다. 실제 TLBI 피연산자는 아래에서 ASID와 결합해 만듭니다.
unsigned long addr;가상주소와 ASID가 합쳐진 TLBI 피연산자를 담습니다. 일반 메모리를 역참조할 포인터가 아닙니다.
dsb(ishst);해당 페이지의 테이블 쓰기가 무효화 요청에 앞서 반영되도록 DSB ISHST로 순서를 맞춥니다.
addr = __TLBI_VADDR(uaddr, ASID(mm));사용자 가상주소와 mm의 ASID를 TLBI 명령 형식에 맞게 합칩니다. 주소를 12비트 오른쪽으로 옮긴 필드와 ASID 상위 필드를 만들며, 가상주소를 물리주소로 바꾸는 계산은 아닙니다.
__tlbi(vale1is, addr);TLBI VALE1IS로 지정한 ASID·가상주소의 최종 단계 변환을 공유 범위에서 무효화합니다. 이 nosync 함수는 요청을 발행하되 완료 대기는 호출자 쪽에 맡깁니다.
__tlbi_user(vale1is, addr);커널 매핑 분리가 켜져 있으면 같은 주소의 사용자용 ASID 항목에도 최종 단계 무효화를 요청합니다. 별도 사용자 ASID가 없는 구성에서는 이 매크로가 추가 명령을 내지 않습니다.
mmu_notifier_arch_invalidate_secondary_tlbs(mm, uaddr & PAGE_MASK,mm를 따라가는 보조 TLB에도 한 페이지의 무효화를 알립니다. PAGE_MASK로 주소를 페이지 시작 경계에 맞춰 시작점을 넘깁니다.
(uaddr & PAGE_MASK) + PAGE_SIZE);페이지 시작에 PAGE_SIZE를 더해 배타적 끝 주소를 지정합니다. 보조 TLB가 받는 범위는 해당 페이지 전체인 [시작, 시작 + PAGE_SIZE)입니다.
static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,VMA를 받는 공개 형태의 한 페이지 무효화 요청 함수입니다. VMA에서 mm를 꺼내 내부 함수에 전달하며 완료는 기다리지 않습니다.
unsigned long uaddr)해당 VMA 안에서 무효화할 사용자 가상주소를 받습니다. VMA 전체를 비우는 요청이 아니라 이 페이지를 지정합니다.
return __flush_tlb_page_nosync(vma->vm_mm, uaddr);VMA가 속한 mm와 사용자 주소를 내부 nosync 함수에 넘기고 끝냅니다. void 함수의 위임 표현이며 성공·오류 코드를 반환하는 문장은 아닙니다.
static inline void flush_tlb_page(struct vm_area_struct *vma,한 사용자 페이지를 무효화하고 완료까지 기다리는 함수입니다. 요청만 하는 nosync 형태 뒤에 공통 동기화를 덧붙입니다.
unsigned long uaddr)무효화할 가상주소를 받습니다. 어떤 주소 공간인지는 함께 전달된 VMA의 vm_mm에서 얻습니다.
flush_tlb_page_nosync(vma, uaddr);vma의 주소 공간에서 uaddr 페이지의 TLB 무효화를 요청하고 보조 TLB에도 해당 페이지 범위를 알립니다. 이름의 nosync는 최종 완료 대기를 다음 줄에서 한 번 수행한다는 뜻입니다.
__tlbi_sync_s1ish(vma->vm_mm);바로 앞에서 요청한 한 페이지의 무효화가 완료될 때까지 동기화합니다. vma->vm_mm을 넘겨 해당 주소 공간에 필요한 SME 관련 동기화도 함께 처리합니다.
static inline bool arch_tlbbatch_should_defer(struct mm_struct *mm)공통 TLB 배치 코드에 완료 대기를 미뤄도 되는지 알려 주는 정책 함수입니다. arm64는 각 페이지의 TLBI를 먼저 내고 대기를 묶을 수 있습니다.
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개 구문에 설명을 붙였습니다.
#ifdef CONFIG_DEBUG_VMDEBUG_VM을 켜면 CPU별 TLB flush 정보의 중첩 사용을 추적합니다. 이 지점에서 사용 카운터를 내리고 컴파일러 배리어를 넣어 앞의 재진입 검사를 마무리하며, 꺼지면 이 진단용 작업을 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
barrier();컴파일러가 앞선 flush 정보 사용을 아래의 재진입 검사 카운터 감소 뒤로 옮기지 못하게 합니다. CPU 명령 수준의 메모리 장벽을 발행하는 동작은 아닙니다.
this_cpu_dec(flush_tlb_info_idx);현재 CPU의 flush_tlb_info 사용 깊이를 하나 내립니다. 디버그용 재진입 검사를 끝내는 처리이며 할당된 메모리를 해제하는 함수는 아닙니다.
#endif1441행에서 시작한 선택 구간을 마칩니다. DEBUG_VM을 켜면 CPU별 TLB flush 정보의 중첩 사용을 추적합니다. 이 지점에서 사용 카운터를 내리고 컴파일러 배리어를 넣어 앞의 재진입 검사를 마무리하며, 꺼지면 이 진단용 작업을 생략합니다.
void flush_tlb_mm_range(struct mm_struct *mm, unsigned long start,mm의 지정 가상주소 구간에 대한 TLB 무효화를 조직합니다. CPU별 세대를 갱신하고 하드웨어 broadcast·원격 CPU·로컬 처리 중 필요한 경로를 고릅니다.
unsigned long end, unsigned int stride_shift,end는 무효화 구간의 배타적 끝이며 stride_shift는 페이지 간격을 2의 지수로 나타냅니다. 큰 페이지와 기본 페이지의 무효화 단위를 구분합니다.
bool freed_tables)페이지 테이블 자체를 해제했는지 받습니다. 단순 매핑 변경과 달리 오래된 테이블 참조도 남지 않아야 하므로 이 정보를 CPU별 처리에 전달합니다.
struct flush_tlb_info *info;mm·주소 범위·단위·세대를 묶은 이번 무효화 요청을 가리킵니다. 로컬 처리기와 원격 처리기가 같은 요청 내용을 읽습니다.
int cpu = get_cpu();선점을 막고 현재 CPU 번호를 얻습니다. 함수가 CPU별 flush 정보와 loaded_mm 상태를 처리하는 동안 다른 CPU로 이동하지 않도록 합니다.
u64 new_tlb_gen;이번 페이지 테이블 변경 뒤의 새 TLB 세대를 보관합니다. CPU는 자신의 마지막 처리 세대와 비교하여 누락된 무효화가 있는지 알 수 있습니다.
new_tlb_gen = inc_mm_tlb_gen(mm);PTE 변경 세대를 올려 switch path와 remote flush가 stale generation을 탐지하게 한다.
info = get_flush_tlb_info(mm, start, end, stride_shift, freed_tables,현재 CPU용 요청 자료에 mm, 범위, 페이지 단위와 테이블 해제 여부를 채웁니다. 범위가 너무 크면 내부에서 전체 무효화로 바꿀 수도 있습니다.
new_tlb_gen);방금 증가시킨 mm의 TLB 세대도 요청에 함께 넣습니다. 처리기가 현재 CPU의 변환 캐시가 어느 변경까지 반영했는지 갱신할 기준입니다.
* flush_tlb_multi() is not optimized for the common case in which only선택한 CPU mask에 shootdown callback을 보내고 completion을 기다리는 경계다.
if (mm_global_asid(mm)) {이 mm에 모든 CPU에서 공유하는 global ASID가 배정돼 있는지 검사합니다. 있으면 같은 식별자로 하드웨어 broadcast 무효화를 수행할 수 있습니다.
broadcast_tlb_flush(info);모든 CPU에서 같은 값으로 쓰는 global ASID를 가진 mm이므로 INVLPGB로 범위를 broadcast 무효화합니다. 필요한 ASID 전환도 마무리하고 TLBSYNC로 완료를 기다려, CPU마다 IPI 처리기를 호출하는 부담을 줄입니다.
} else if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {현재 CPU 외에도 mm 사용 마스크에 다른 CPU가 있는지 검사합니다. 있다면 로컬 처리만으로 부족하므로 해당 CPU들을 포함해 무효화합니다.
info->trim_cpumask = should_trim_cpumask(mm);mm의 CPU 사용 마스크를 정리할 주기가 되었는지 확인해 요청에 기록합니다. 이 검사는 약 1초 간격으로 허용하며, 원격 처리에서 더 이상 해당 mm를 쓰지 않는 CPU를 마스크에서 빼 불필요한 요청을 줄입니다.
flush_tlb_multi(mm_cpumask(mm), info);선택한 CPU mask에 shootdown callback을 보내고 completion을 기다리는 경계다.
consider_global_asid(mm);INVLPGB를 쓸 수 있는 환경에서 이 mm에 global ASID를 줄지 간헐적으로 검사합니다. 네 개 이상의 CPU에서 동시에 활발히 쓰이는 주소 공간이면 하드웨어 broadcast 무효화의 이득을 얻도록 전환을 검토합니다.
} else if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {다른 대상 CPU가 없고 이 CPU가 실제로 mm를 로드한 경우인지 확인합니다. 맞으면 원격 요청을 준비할 필요 없이 로컬 처리기를 직접 실행합니다.
lockdep_assert_irqs_enabled();현재 경로가 로컬 IRQ를 허용한 상태에서 들어왔는지 잠금 검증기로 확인합니다. 바로 뒤에서 IRQ를 끄고 로컬 TLB 처리를 직접 호출하기 위한 전제를 검사합니다.
local_irq_disable();현재 CPU의 인터럽트를 막습니다. 로컬 loaded_mm과 TLB 세대를 읽고 갱신하는 동안 인터럽트 경로와 섞이지 않게 합니다.
flush_tlb_func(info);info의 주소 공간·범위·세대를 사용해 이 CPU의 TLB 갱신 필요 여부를 판단하고 필요한 무효화를 수행합니다. 다른 CPU로 IPI를 보내는 경로가 아니라 로컬 처리입니다.
local_irq_enable();로컬 TLB 처리를 끝낸 뒤 인터럽트를 다시 허용합니다. 진입 전 IRQ가 허용돼 있어야 한다는 위 검사와 짝을 이룹니다.
put_flush_tlb_info();CPU별 flush 요청 정보의 사용을 마칩니다. 디버그 구성에서는 사용 깊이를 낮춰 다음 요청의 재진입 검사가 가능하게 합니다.
put_cpu();get_cpu에서 막았던 선점을 다시 허용합니다. CPU별 상태에 의존하는 작업이 끝났으므로 태스크가 다른 CPU로 이동할 수 있습니다.
mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, end);CPU TLB 외에 이 mm를 따라가는 보조 주소 변환 사용자에게 [start, end) 무효화를 알립니다. 가상화 등 별도 변환 캐시가 오래된 매핑을 유지하지 않도록 합니다.
static void do_flush_tlb_all(void *info)각 CPU에서 전체 TLB 무효화를 수행할 콜백입니다. 통계를 남기고 현재 CPU의 TLB를 비우며, info 인자는 이 구현에서 사용하지 않습니다.
count_vm_tlb_event(NR_TLB_REMOTE_FLUSH_RECEIVED);원격 전체 TLB 무효화 요청을 이 CPU가 받았다는 통계를 증가시킵니다. 실제 TLB 작업은 다음 줄에서 수행합니다.
__flush_tlb_all();이 CPU의 전체 TLB를 무효화합니다. 모든 CPU를 대상으로 실행되는 do_flush_tlb_all 콜백의 실제 하드웨어 작업입니다.
void flush_tlb_all(void)모든 CPU의 TLB를 무효화하는 진입점입니다. INVLPGB 지원 시 하드웨어 broadcast를, 아니면 각 CPU에서 실행하는 콜백을 사용합니다.
count_vm_tlb_event(NR_TLB_REMOTE_FLUSH);전체 TLB 무효화 요청을 보냈다는 VM 통계를 갱신합니다. 요청 횟수 기록과 실제 무효화는 별개의 단계입니다.
if (cpu_feature_enabled(X86_FEATURE_INVLPGB))CPU가 INVLPGB 하드웨어 broadcast 무효화를 지원하는지 검사합니다. 지원하지 않을 때는 IPI 기반 방식으로 같은 전체 무효화를 수행합니다.
invlpgb_flush_all();INVLPGB 기능이 있는 CPU에서는 하드웨어의 broadcast 무효화로 전체 TLB 갱신을 수행합니다. 소프트웨어 IPI 콜백을 CPU마다 보내는 비용을 줄이는 경로입니다.
elseINVLPGB가 없는 CPU의 경로입니다. on_each_cpu로 각 CPU가 자신의 TLB를 직접 비우도록 요청합니다.
on_each_cpu(do_flush_tlb_all, NULL, 1);하드웨어 broadcast 방식이 없으면 각 CPU에서 do_flush_tlb_all을 실행합니다. 마지막 인자 1은 모든 대상 CPU의 처리가 끝날 때까지 기다리도록 합니다.
static void invlpgb_kernel_range_flush(struct flush_tlb_info *info)커널 주소 범위를 INVLPGB가 표현할 수 있는 크기로 나누어 무효화합니다. 모든 조각을 요청한 다음 한 번 완료를 기다립니다.
unsigned long addr, nr;addr는 이번에 처리할 가상주소이고 nr는 해당 요청에서 비울 기본 페이지 수입니다. 바이트 주소와 페이지 개수를 분리해 관리합니다.
for (addr = info->start; addr < info->end; addr += nr << PAGE_SHIFT) {start부터 end 직전까지 반복하며 방금 처리한 nr개 페이지의 바이트 수만큼 전진합니다. 한 명령의 범위 제한보다 큰 구간도 여러 번에 나눕니다.
nr = (info->end - addr) >> PAGE_SHIFT;남은 바이트 길이를 PAGE_SHIFT만큼 나누어 기본 페이지 수로 구합니다. 다음 줄이 하드웨어의 한 번 처리 한도에 맞춰 이 수를 제한합니다.
nr = clamp_val(nr, 1, invlpgb_count_max);한 번에 무효화할 페이지 수를 최소 1, 최대 invlpgb_count_max 사이로 제한합니다. 명령 한 번이 표현할 수 있는 범위보다 큰 요청은 루프에서 나눠 처리합니다.
invlpgb_flush_addr_nosync(addr, nr);addr부터 nr개 페이지에 대해 INVLPGB 무효화를 요청하되 매번 완료를 기다리지 않습니다. 여러 구간을 요청한 뒤 루프 밖에서 한 번 동기화합니다.
__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개 구문에 설명을 붙였습니다.
mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, start + size);CPU 쪽 무효화를 마친 뒤 mm에 연결된 보조 TLB 구독자에게 [start, start + size) 범위를 알립니다. 가상화나 장치 쪽 변환 캐시도 같은 주소 변경을 따라가게 하며, mm가 없는 커널 공통 요청에는 호출하지 않습니다.
void flush_tlb_mm(struct mm_struct *mm)한 mm의 전체 사용자 주소 공간을 무효화하는 외부 함수입니다. 그 mm를 사용한 CPU 마스크를 공통 처리기에 전달합니다.
__flush_tlb_range(mm, mm_cpumask(mm), 0, FLUSH_TLB_MAX_SIZE, PAGE_SIZE);이 mm를 사용한 CPU 마스크와 전체 주소 공간을 뜻하는 범위로 공통 무효화 함수를 호출합니다. 필요한 대상에 따라 로컬 SFENCE, SBI 원격 fence 또는 IPI 방식을 선택하게 합니다.
void flush_tlb_mm_range(struct mm_struct *mm,한 mm의 일부 주소 구간을 호출자가 지정한 페이지 간격으로 무효화하는 함수입니다.
unsigned long start, unsigned long end,시작 주소와 배타적 끝 주소를 받습니다. 내부 호출에서는 이 두 주소의 차이를 바이트 길이로 바꿉니다.
unsigned int page_size)개별 주소 무효화의 간격을 바이트 단위 page_size로 받습니다. 기본 페이지 또는 큰 페이지에 맞는 단위로 진행하도록 합니다.
__flush_tlb_range(mm, mm_cpumask(mm), start, end - start, page_size);끝 주소를 end - start 바이트 길이로 바꾸어 무효화할 구간을 전달합니다. page_size는 개별 주소 무효화의 간격이며, mm_cpumask로 해당 주소 공간과 관련된 CPU들에 작업을 한정합니다.
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)VMA 안의 주소 하나에 해당하는 기본 페이지를 무효화합니다. 주소 공간과 대상 CPU는 VMA의 mm에서 가져옵니다.
__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),VMA의 mm와 그 주소 공간을 사용한 CPU 마스크를 공통 TLB 처리에 전달합니다.
addr, PAGE_SIZE, PAGE_SIZE);addr부터 PAGE_SIZE만큼을 기본 페이지 간격으로 무효화하도록 지정합니다. 첫 PAGE_SIZE는 범위 길이, 두 번째는 주소를 전진할 간격입니다.
void flush_tlb_range(struct vm_area_struct *vma, unsigned long start,VMA의 종류에 맞는 간격을 골라 지정 범위를 무효화합니다. HugeTLB 및 Svnapot 배치에서는 기본 페이지와 다른 고려가 필요합니다.
unsigned long end)무효화할 구간의 배타적 끝 주소입니다. start와의 차이가 공통 처리기에 넘길 범위 길이가 됩니다.
unsigned long stride_size;개별 TLB 무효화 사이의 바이트 간격을 보관합니다. 큰 매핑을 불필요하게 잘게 처리하지 않으면서 필요한 PTE를 빠뜨리지 않도록 고릅니다.
if (!is_vm_hugetlb_page(vma)) {명시적 HugeTLB VMA가 아닌지 검사합니다. 일반 VMA는 기본 페이지 간격으로 안전하게 처리합니다.
stride_size = PAGE_SIZE;일반 VMA의 무효화 간격을 기본 PAGE_SIZE로 정합니다. 큰 매핑 여부를 섣불리 가정해 중간 페이지를 건너뛰지 않습니다.
} else {HugeTLB VMA의 경로입니다. 먼저 그 영역에 설정된 거대 페이지 크기를 얻고 필요하면 Svnapot에 맞춰 더 촘촘히 조정합니다.
stride_size = huge_page_size(hstate_vma(vma));HugeTLB 영역에 연결된 hstate에서 실제 거대 페이지 크기를 구해 무효화 간격으로 사용합니다. 뒤에서 Svnapot처럼 더 작은 간격이 필요한 경우를 보정하므로, 이 값이 항상 최종 간격인 것은 아닙니다.
if (has_svnapot()) {Svnapot 연속 매핑 확장을 사용할 수 있는지 검사합니다. NAPOT 영역의 모든 관련 PTE를 무효화해야 하므로 거대 페이지 크기를 그대로 간격으로 쓰지 않을 수 있습니다.
if (stride_size >= PGDIR_SIZE)거대 페이지 크기가 최상위 PGDIR 항목의 담당 범위 이상인지 확인합니다. 크기에 맞는 페이지 테이블 단계 단위로 간격을 낮춥니다.
stride_size = PGDIR_SIZE;무효화 간격을 PGDIR_SIZE로 제한합니다. 거대 페이지 전체 크기만큼 건너뛰어 필요한 테이블 항목을 놓치지 않도록 합니다.
else if (stride_size >= P4D_SIZE)PGDIR보다 작지만 P4D 한 항목의 범위 이상인지 검사합니다. 이 경우 P4D 단위가 다음 후보입니다.
stride_size = P4D_SIZE;무효화 간격을 P4D_SIZE로 정합니다. 이 상수의 실제 크기는 사용 중인 페이지 테이블 구성에 따릅니다.
else if (stride_size >= PUD_SIZE)더 작은 거대 페이지가 PUD 범위 이상인지 검사합니다. 해당하면 PUD 항목별 간격을 사용합니다.
stride_size = PUD_SIZE;무효화 간격을 PUD_SIZE로 정해 필요한 PUD 범위마다 주소 무효화를 수행하도록 합니다.
else if (stride_size >= PMD_SIZE)거대 페이지가 PMD 항목의 범위 이상인지 확인합니다. 맞으면 PMD 단위로 구간을 나눕니다.
stride_size = PMD_SIZE;무효화 간격을 PMD_SIZE로 정합니다. 더 큰 거대 페이지 크기만큼 건너뛰지 않도록 줄이는 선택입니다.
else거대 페이지 크기가 위의 테이블 단계 범위보다 작은 경우입니다. NAPOT를 구성하는 기본 PTE들을 모두 다룰 간격이 필요합니다.
stride_size = PAGE_SIZE;무효화 간격을 기본 PAGE_SIZE로 낮춥니다. NAPOT 연속 매핑의 각 PTE를 빠뜨리지 않도록 기본 페이지마다 처리합니다.
__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),VMA의 주소 공간과 관련 CPU 집합을 공통 무효화 함수에 전달합니다. 아래에서 정한 범위와 간격으로 로컬·원격 fence 방식을 선택합니다.
start, end - start, stride_size);[start, end) 구간의 길이와 VMA·Svnapot에 맞춰 선택한 간격을 넘깁니다. 길이는 전체 범위이고 stride_size는 개별 무효화의 간격입니다.
void flush_tlb_kernel_range(unsigned long start, unsigned long end)여러 태스크가 공유하는 커널 가상주소 구간의 TLB를 무효화합니다. 한 사용자 mm에 대상을 제한할 수 없는 요청입니다.
__flush_tlb_range(NULL, cpu_online_mask,mm를 NULL로 두고 모든 online CPU를 대상으로 지정합니다. 커널 공통 매핑을 바꿨으므로 특정 프로세스의 CPU 마스크를 사용하지 않습니다.
start, end - start, PAGE_SIZE);커널 구간의 길이를 end - start로, 간격을 PAGE_SIZE로 지정합니다. 기본 페이지마다 필요한 무효화를 모든 대상 CPU에 적용합니다.
#ifdef CONFIG_TRANSPARENT_HUGEPAGETransparent Huge Page는 여러 기본 페이지를 큰 페이지 매핑으로 묶어 TLB 부담을 줄이는 기능입니다. 지원 빌드에서는 PMD 크기 단위로 관련 번역을 무효화하는 경로를 추가하고, 꺼지면 이 전용 flush 함수가 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
void flush_pmd_tlb_range(struct vm_area_struct *vma, unsigned long start,THP 구성에서 PMD 단위 매핑의 TLB 구간을 무효화하는 함수입니다. VMA를 통해 대상 mm를 선택합니다.
unsigned long end)PMD 무효화 구간의 배타적 끝 주소를 받습니다. 아래에서 시작 주소와의 차이로 범위 길이를 계산합니다.
__flush_tlb_range(vma->vm_mm, mm_cpumask(vma->vm_mm),VMA의 mm와 관련 CPU 집합에 무효화를 요청합니다. 다음 줄의 PMD_SIZE가 개별 주소 무효화 간격이 되어 THP의 PMD 매핑 단위에 맞춥니다.
05 · WORKED EXAMPLE
숫자로 검산하기
4KiB range flush와 full flush 선택
256KiB 범위, 4KiB page, mm 사용 CPU 6개, per-page invalidate 80ns, IPI 왕복 2us라고 가정한다.
- pages256KiB / 4KiB = 64개 translation이다.
- local costpage별이면 64x80ns=5.12us로 full-context flush 비용과 비교한다.
- remote cost6개 CPU에 병렬 IPI를 보내도 완료 시간은 가장 늦은 CPU의 2us+flush 시간이다.
- free boundary모든 ack 뒤에만 unmapped physical page와 table page를 allocator에 반환한다.
결론threshold는 page 수만이 아니라 CPU mask 크기, PCID/ASID 유지 이점과 remote interrupt latency에 따라 달라진다.
06 · DEEP DIVE
경계별 상세 분석
공통 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를 결정한다.
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를 확인한다.
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를 본다.
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을 확인한다.
객체 수명과 소유권을 먼저 고정한다
invalidated physical page와 page-table page는 remote TLB entry가 사라지기 전까지 재사용하면 안 된다. mmu_gather가 이 deferred free를 관리한다.
주소나 register 값이 맞는지만 확인하면 stale state를 놓친다. producer, publication, consumer와 폐기 지점을 같은 표에 기록한다.
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
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | TLBI만 실행하고 completion barrier를 빼면 page free 뒤 remote walker가 old PA를 사용한다. | TLBI operand의 ASID/VA, range scale, DSB domain과 mm cpumask를 확인한다. |
| x86-64 | CPU가 mm를 막 load하는 순간 대상 mask에서 빠지면 stale PTE를 무기한 사용한다. | start/end, stride_shift, freed_tables, new_tlb_gen, INVPCID capability와 target cpumask를 본다. |
| RISC-V | SBI/firmware가 잘못된 hart mask나 ASID를 받으면 일부 hart에만 stale translation이 남는다. | start/end, page size, ASID, hart mask, SBI return과 remote completion을 확인한다. |
08 · LAB
재현과 계측 절차
tlb:tlb_flush와 IPI trace를 함께 수집해 initiator와 가장 늦은 remote CPU를 찾는다.- CPU 하나에서 interrupt를 길게 막아 shootdown upper bound가 어떻게 늘어나는지 측정한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 좌표
- arm64arch/arm64/include/asm/tlbflush.h:364-420
- x86-64arch/x86/mm/tlb.c:1440-1520
- RISC-Varch/riscv/mm/tlbflush.c:149-213
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.