← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 04/21

Address-space switch: TTBR, CR3/PCID와 SATP/ASID

task의 mm_struct가 바뀔 때 page-table root와 ASID/PCID를 재사용하고 generation wrap을 처리하는 방식을 비교합니다.

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

01 · QUESTION

무엇을 확인할 것인가

새 mm의 page-table root를 쓰면서도 매 context switch마다 전체 TLB를 비우지 않는 근거는 무엇인가?

각 mm에 hardware address-space tag와 generation을 배정한다. tag가 현재 generation에서 유효하면 root를 tag와 함께 설치해 다른 mm translation과 공존시키고, wrap이면 전체 invalidate 뒤 generation을 올린다.

mm cpumask publication, page-table root write와 speculation barrier 순서가 TLB shootdown의 대상 CPU 선택과 맞아야 한다.

지연 시간 관점fast path는 valid tag와 같은 root 재사용이고 slow path는 allocation, wrap flush, IBPB/L1D mitigation과 lazy TLB 처리다.

02 · CONTRACT

공통 계약과 architecture 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64TTBR0_EL1에 ASID와 PGD PA를 조합ASID wrap flush가 누락되면 새 mm가 이전 mm의 동일 ASID translation을 사용한다.asid generation, TTBR0_EL1, CONTEXTIDR_EL1, active_asids와 reserved_ttbr0를 본다.
x86-64CR3에 PGD PA, PCID와 no-flush bit를 조합no-flush CR3를 stale PCID에 쓰면 다른 mm translation이 살아남는다.CR3, PCID, tlb_gen, loaded_mm, user/kernel PCID와 IBPB decision을 기록한다.
RISC-VSATP.MODE, ASID와 root PPN을 조합ASID version wrap 또는 icache stale bit 누락은 data translation이나 새 code 관찰을 깨뜨린다.SATP MODE/ASID/PPN, context id version, cpumask와 icache_stale_mask를 확인한다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

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

arm64

mechanism
TTBR0_EL1에 ASID와 PGD PA를 조합
state
check_and_switch_context()는 mm context id generation을 검사하고 필요하면 새 ASID를 배정한다. CnP, PAN, EPD0와 reserved TTBR0 사용 여부가 실제 register write를 바꾼다.
checkpoint
asid generation, TTBR0_EL1, CONTEXTIDR_EL1, active_asids와 reserved_ttbr0를 본다.

x86-64

mechanism
CR3에 PGD PA, PCID와 no-flush bit를 조합
state
switch_mm_irqs_off()는 per-CPU loaded_mm, tlb_gen과 ASID slot을 비교한다. 같은 mm라도 generation이 뒤처지면 INVPCID 또는 CR3 reload가 필요하다.
checkpoint
CR3, PCID, tlb_gen, loaded_mm, user/kernel PCID와 IBPB decision을 기록한다.

RISC-V

mechanism
SATP.MODE, ASID와 root PPN을 조합
state
switch_mm()는 cpu mask, icache stale mask와 ASID version을 확인한 뒤 SATP를 쓴다. ASID extension이 없으면 주소 공간 전환마다 더 넓은 sfence.vma가 필요하다.
checkpoint
SATP MODE/ASID/PPN, context id version, cpumask와 icache_stale_mask를 확인한다.
그림 2. 공통 kernel과 architecture hook의 소유권공통 정책이 hardware state를 직접 소유하지 않는 경계를 표시합니다.
Linux common contract각 mm에 hardware address-space tag와 generation을 배정한다. tag가 현재 generation에서 유효하면 root를 tag와 함께 설치해 다른 mm translation과 공존시키고, wrap이면 전체 invalidate 뒤 generation을 올린다.
arm64TTBR0_EL1에 ASID와 PGD PA를 조합ASID allocation lock과 local flush 뒤 TTBR write가 이어지며 contextidr와 speculation state도 task 경계와 맞춘다.
x86-64CR3에 PGD PA, PCID와 no-flush bit를 조합mm_cpumask와 loaded_mm publication은 concurrent flush_tlb_mm_range()가 switch 중 CPU를 놓치지 않게 ordering을 이룬다.
RISC-VSATP.MODE, ASID와 root PPN을 조합SATP write와 local_flush_tlb_all()/ASID flush, instruction-cache synchronization이 membarrier 계약과 연결된다.
lifetime boundaryASID/PCID 숫자는 영구 identity가 아니다. generation과 한 쌍일 때만 mm를 식별하며 wrap 뒤 old TLB entry와 재사용 tag가 충돌하지 않게 해야 한다.
그림 3. publication과 관찰 순서state를 준비한 뒤 architecture ordering을 거쳐 관찰 가능한 checkpoint가 됩니다.
arm64state 준비ASID allocation lock과 local flush 뒤 TTBR write가 이어지며 contextidr와 speculation state도 task 경계와 맞춘다.관찰: asid generation, TTBR0_EL1, CONTEXTIDR_EL1, active_asids와 reserved_ttbr0를 본다.
x86-64state 준비mm_cpumask와 loaded_mm publication은 concurrent flush_tlb_mm_range()가 switch 중 CPU를 놓치지 않게 ordering을 이룬다.관찰: CR3, PCID, tlb_gen, loaded_mm, user/kernel PCID와 IBPB decision을 기록한다.
RISC-Vstate 준비SATP write와 local_flush_tlb_all()/ASID flush, instruction-cache synchronization이 membarrier 계약과 연결된다.관찰: SATP MODE/ASID/PPN, context id version, cpumask와 icache_stale_mask를 확인한다.

04 · SOURCE

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

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

arm64 · Linux 6.18.37

TTBR0_EL1에 ASID와 PGD PA를 조합

check_and_switch_context()는 mm context id generation을 검사하고 필요하면 새 ASID를 배정한다. CnP, PAN, EPD0와 reserved TTBR0 사용 여부가 실제 register write를 바꾼다.

원본 코드: arch/arm64/mm/context.c:207-279

207	asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1);
208 
209set_asid:
210	__set_bit(asid, asid_map);
211	cur_idx = asid;
212	return asid2ctxid(asid, generation);
213}
214 
215void check_and_switch_context(struct mm_struct *mm)
216{
217	unsigned long flags;
218	unsigned int cpu;
219	u64 asid, old_active_asid;
220 
221	if (system_supports_cnp())
222		cpu_set_reserved_ttbr0();
223 
224	asid = atomic64_read(&mm->context.id);
225 
226	/*
227	 * The memory ordering here is subtle.
228	 * If our active_asids is non-zero and the ASID matches the current
229	 * generation, then we update the active_asids entry with a relaxed
230	 * cmpxchg. Racing with a concurrent rollover means that either:
231	 *
232	 * - We get a zero back from the cmpxchg and end up waiting on the
233	 *   lock. Taking the lock synchronises with the rollover and so
234	 *   we are forced to see the updated generation.
235	 *
236	 * - We get a valid ASID back from the cmpxchg, which means the
237	 *   relaxed xchg in flush_context will treat us as reserved
238	 *   because atomic RmWs are totally ordered for a given location.
239	 */
240	old_active_asid = atomic64_read(this_cpu_ptr(&active_asids));
241	if (old_active_asid && asid_gen_match(asid) &&
242	    atomic64_cmpxchg_relaxed(this_cpu_ptr(&active_asids),
243				     old_active_asid, asid))
244		goto switch_mm_fastpath;
245 
246	raw_spin_lock_irqsave(&cpu_asid_lock, flags);
247	/* Check that our ASID belongs to the current generation. */
248	asid = atomic64_read(&mm->context.id);
249	if (!asid_gen_match(asid)) {
250		asid = new_context(mm);
251		atomic64_set(&mm->context.id, asid);
252	}
253 
254	cpu = smp_processor_id();
255	if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending))
256		local_flush_tlb_all();
257 
258	atomic64_set(this_cpu_ptr(&active_asids), asid);
259	raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
260 
261switch_mm_fastpath:
262 
263	arm64_apply_bp_hardening();
264 
265	/*
266	 * Defer TTBR0_EL1 setting for user threads to uaccess_enable() when
267	 * emulating PAN.
268	 */
269	if (!system_uses_ttbr0_pan())
270		cpu_switch_mm(mm->pgd, mm);
271}
272 
273unsigned long arm64_mm_context_get(struct mm_struct *mm)
274{
275	unsigned long flags;
276	u64 asid;
277 
278	if (!pinned_asid_map)
279		return 0;

주요 구문 해설

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

L207 asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1);

예약용 ASID 0을 건너뛰고 비트맵의 1번 비트부터 빈 ASID를 찾습니다. 이 버전은 비트 번호가 ASID 번호이며, KPTI를 쓰면 홀수 비트를 미리 사용 중으로 표시하여 짝수 번호만 배정합니다. 세대 교체 뒤에도 빈 번호가 남도록 할당 한계를 정합니다.

L209set_asid:

빈 ASID를 찾은 경로들이 합류하는 위치입니다. 이어 선택한 비트를 사용 중으로 표시하고 세대 정보를 합쳐 호출자에게 돌려줍니다.

L210 __set_bit(asid, asid_map);

고른 ASID의 비트를 사용 중으로 표시합니다. 뒤의 할당이 같은 번호를 다시 고르지 않도록 비트맵에 반영합니다.

L211 cur_idx = asid;

이번에 고른 비트맵 인덱스를 다음 빈 ASID 탐색의 시작점으로 남깁니다. 매번 처음부터 사용 중인 번호를 다시 훑는 비용을 줄입니다.

L212 return asid2ctxid(asid, generation);

선택한 ASID 번호와 현재 세대 비트를 OR하여 문맥 식별값을 반환합니다. 이 버전은 비트맵 인덱스 자체가 ASID 번호이며 여기에 커널의 세대 추적 정보를 합칩니다.

L215void check_and_switch_context(struct mm_struct *mm)

mm의 ASID가 현 세대에서 유효한지 보장하고 이 CPU의 사용자 주소 공간을 전환하는 함수입니다. 이미 유효한 문맥이면 빠른 경로, 번호 재할당이 필요하면 잠금을 잡는 경로를 사용합니다.

L217 unsigned long flags;

ASID 할당 잠금을 잡기 전에 로컬 IRQ 허용 상태를 보관할 변수입니다. 잠금을 풀 때 진입 전 상태를 정확히 되돌리는 데 사용합니다.

L218 unsigned int cpu;

현재 CPU의 논리 번호를 담습니다. CPU별 지연 TLB 무효화 비트와 active_asids 항목을 선택하는 인덱스입니다.

L219 u64 asid, old_active_asid;

전환할 mm의 세대 포함 ASID와 이 CPU가 앞서 사용한 ASID를 각각 담습니다. 두 값으로 빠른 전환과 세대 교체의 경쟁을 판단합니다.

L221 if (system_supports_cnp())

CPU들 사이에서 공통 페이지 테이블을 공유하는 CnP 기능을 쓰는지 검사합니다. 사용하면 ASID 변경 전에 TTBR0를 빈 예약 테이블로 바꿔 이전 매핑의 사용을 차단합니다.

L222 cpu_set_reserved_ttbr0();

CnP를 사용하는 CPU에서는 TTBR0를 예약된 빈 테이블로 바꿉니다. ASID를 교체하는 동안 이전 사용자 매핑과 새 ASID 조합으로 변환을 참조하지 않게 합니다.

L224 asid = atomic64_read(&mm->context.id);

이 주소 공간에 배정된 ASID와 세대 정보가 담긴 context.id를 원자적으로 읽습니다. 현재 CPU에서 빠른 전환 경로를 사용할 수 있는지 판단할 입력입니다.

L240 old_active_asid = atomic64_read(this_cpu_ptr(&active_asids));

현재 CPU가 활성화한 ASID를 읽습니다. 값이 0이면 세대 교체 과정에서 빠른 경로가 차단된 상태이므로 잠금을 잡는 경로로 가야 합니다.

L241 if (old_active_asid && asid_gen_match(asid) &&

이 CPU의 active ASID가 0이 아니고 mm의 ASID 세대도 최신인 경우에만 잠금 없는 전환을 시도합니다. 둘 중 하나가 맞지 않으면 아래의 잠금 경로에서 다시 확인합니다.

L242 atomic64_cmpxchg_relaxed(this_cpu_ptr(&active_asids),

CPU별 active_asids가 앞서 읽은 값 그대로일 때 새 asid로 원자 교환을 시도합니다. relaxed 연산이어도 같은 메모리 위치의 원자 갱신 순서는 보장되어 세대 교체와 경쟁할 수 있습니다.

L243 old_active_asid, asid))

비교값 old_active_asid와 새 저장값 asid를 넘기고 원래 값을 조건식에서 검사합니다. 세대 교체가 먼저 값을 0으로 만들었다면 비교 교환 결과가 0이 되어 잠금 경로로 갑니다.

L244 goto switch_mm_fastpath;

ASID가 유효하고 CPU별 활성 값도 갱신됐으므로 잠금·재할당을 건너뛰어 공통 전환 마무리로 이동합니다.

L246 raw_spin_lock_irqsave(&cpu_asid_lock, flags);

인터럽트 상태를 flags에 저장하고 로컬 IRQ를 막은 뒤 ASID 할당 잠금을 잡습니다. 여러 CPU의 ASID 할당·세대 교체와 이 CPU의 IRQ 처리가 상태를 동시에 바꾸지 않도록 합니다.

L248 asid = atomic64_read(&mm->context.id);

잠금을 얻은 뒤 mm의 ASID를 다시 읽습니다. 잠금을 기다리는 동안 다른 CPU가 세대나 배정값을 바꿨을 수 있기 때문입니다.

L249 if (!asid_gen_match(asid)) {

잠금을 잡은 뒤에도 mm의 세대가 오래된 상태이면 새 문맥 번호를 배정합니다. 이미 다른 CPU가 갱신했다면 기존 번호를 그대로 사용합니다.

L250 asid = new_context(mm);

현재 generation에서 쓸 새 ASID를 할당하는 slow path다.

L251 atomic64_set(&mm->context.id, asid);

새로 할당한 세대 포함 ASID를 mm->context.id에 기록합니다. 다음 전환에서도 이 주소 공간의 최신 식별자를 찾도록 합니다.

L254 cpu = smp_processor_id();

현재 논리 CPU 번호를 얻습니다. 이 CPU에 지연된 TLB 무효화가 있는지 비트맵에서 검사하는 데 사용합니다.

L255 if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending))

이번 CPU의 지연 TLB 무효화 비트를 읽으면서 지웁니다. 비트가 켜져 있었을 때만 전체 로컬 TLB를 비워 재사용 ASID의 이전 변환을 제거합니다.

L256 local_flush_tlb_all();

ASID 세대 교체로 이 CPU에 남겨 둔 무효화 요청을 처리하여 로컬 TLB를 모두 비웁니다. 재사용 ASID가 이전 주소 공간의 변환과 충돌하지 않게 합니다.

L258 atomic64_set(this_cpu_ptr(&active_asids), asid);

현재 CPU의 active_asids에 선택한 ASID를 기록합니다. 이후 세대 교체와 다른 전환 경로가 이 CPU의 사용 상태를 알 수 있게 합니다.

L259 raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);

ASID 잠금을 풀고 flags에 저장한 로컬 인터럽트 상태를 되돌립니다. 이어지는 CPU 전환 작업은 공유 ASID 할당 구간 밖에서 진행합니다.

L261switch_mm_fastpath:

빠른 경로와 잠금 경로가 합류합니다. 이 지점에는 사용할 ASID가 준비되어 있으며 이어 분기 예측 완화와 실제 주소 변환 문맥 전환을 수행합니다.

L263 arm64_apply_bp_hardening();

현재 CPU에 필요한 분기 예측 보안 완화 처리를 적용합니다. 다른 주소 공간으로 전환할 때 이전 실행의 예측 상태를 악용하는 위험을 줄이는 아키텍처별 콜백 경로입니다.

L269 if (!system_uses_ttbr0_pan())

TTBR0 교체로 PAN을 흉내 내는 구성인지 검사합니다. 그 구성이 아니면 지금 mm의 페이지 테이블을 적재하고, 사용하는 구성이면 사용자 메모리 접근을 여는 uaccess_enable까지 적재를 미룹니다.

L270 cpu_switch_mm(mm->pgd, mm);

PGD physical address와 mm context를 실제 TTBR state로 설치한다.

L273unsigned long arm64_mm_context_get(struct mm_struct *mm)

다른 주소 변환 사용자가 참조할 수 있도록 mm의 ASID를 고정하고 반환하는 함수입니다. 고정 중인 ASID는 세대가 바뀌어도 다른 주소 공간에 재사용하지 않습니다.

L275 unsigned long flags;

고정 ASID를 관리하는 잠금을 잡기 전 IRQ 상태를 보관합니다. 고정 비트맵·참조 수 갱신 후 원래 허용 상태로 복원하는 데 사용합니다.

L276 u64 asid;

고정할 mm의 ASID와 세대 정보를 담는 64비트 변수입니다. 할당할 수 없으면 뒤의 경로에서 0을 결과로 사용합니다.

L278 if (!pinned_asid_map)

고정 ASID용 비트맵이 준비되지 않았으면 ASID 고정 기능을 사용할 수 없습니다. 이 경우 공유 할당 상태에 접근하지 않고 0을 반환합니다.

L279 return 0;

고정한 ASID를 제공할 수 없음을 0으로 알립니다. 예약용 ASID 0을 실제 사용자 문맥 식별자로 배정한 것이 아닙니다.

x86-64 · Linux 6.18.37

CR3에 PGD PA, PCID와 no-flush bit를 조합

switch_mm_irqs_off()는 per-CPU loaded_mm, tlb_gen과 ASID slot을 비교한다. 같은 mm라도 generation이 뒤처지면 INVPCID 또는 CR3 reload가 필요하다.

원본 코드: arch/x86/mm/tlb.c:774-864

774#endif
775 
776/*
777 * This optimizes when not actually switching mm's.  Some architectures use the
778 * 'unused' argument for this optimization, but x86 must use
779 * 'cpu_tlbstate.loaded_mm' instead because it does not always keep
780 * 'current->active_mm' up to date.
781 */
782void switch_mm_irqs_off(struct mm_struct *unused, struct mm_struct *next,
783			struct task_struct *tsk)
784{
785	struct mm_struct *prev = this_cpu_read(cpu_tlbstate.loaded_mm);
786	u16 prev_asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);
787	bool was_lazy = this_cpu_read(cpu_tlbstate_shared.is_lazy);
788	unsigned cpu = smp_processor_id();
789	unsigned long new_lam;
790	struct new_asid ns;
791	u64 next_tlb_gen;
792 
793 
794	/* We don't want flush_tlb_func() to run concurrently with us. */
795	if (IS_ENABLED(CONFIG_PROVE_LOCKING))
796		WARN_ON_ONCE(!irqs_disabled());
797 
798	/*
799	 * Verify that CR3 is what we think it is.  This will catch
800	 * hypothetical buggy code that directly switches to swapper_pg_dir
801	 * without going through leave_mm() / switch_mm_irqs_off() or that
802	 * does something like write_cr3(read_cr3_pa()).
803	 *
804	 * Only do this check if CONFIG_DEBUG_VM=y because __read_cr3()
805	 * isn't free.
806	 */
807#ifdef CONFIG_DEBUG_VM
808	if (WARN_ON_ONCE(__read_cr3() != build_cr3(prev->pgd, prev_asid,
809						   tlbstate_lam_cr3_mask()))) {
810		/*
811		 * If we were to BUG here, we'd be very likely to kill
812		 * the system so hard that we don't see the call trace.
813		 * Try to recover instead by ignoring the error and doing
814		 * a global flush to minimize the chance of corruption.
815		 *
816		 * (This is far from being a fully correct recovery.
817		 *  Architecturally, the CPU could prefetch something
818		 *  back into an incorrect ASID slot and leave it there
819		 *  to cause trouble down the road.  It's better than
820		 *  nothing, though.)
821		 */
822		__flush_tlb_all();
823	}
824#endif
825	if (was_lazy)
826		this_cpu_write(cpu_tlbstate_shared.is_lazy, false);
827 
828	/*
829	 * The membarrier system call requires a full memory barrier and
830	 * core serialization before returning to user-space, after
831	 * storing to rq->curr, when changing mm.  This is because
832	 * membarrier() sends IPIs to all CPUs that are in the target mm
833	 * to make them issue memory barriers.  However, if another CPU
834	 * switches to/from the target mm concurrently with
835	 * membarrier(), it can cause that CPU not to receive an IPI
836	 * when it really should issue a memory barrier.  Writing to CR3
837	 * provides that full memory barrier and core serializing
838	 * instruction.
839	 */
840	if (prev == next) {
841		/* Not actually switching mm's */
842		VM_WARN_ON(is_dyn_asid(prev_asid) &&
843			   this_cpu_read(cpu_tlbstate.ctxs[prev_asid].ctx_id) !=
844			   next->context.ctx_id);
845 
846		/*
847		 * If this races with another thread that enables lam, 'new_lam'
848		 * might not match tlbstate_lam_cr3_mask().
849		 */
850 
851		/*
852		 * Even in lazy TLB mode, the CPU should stay set in the
853		 * mm_cpumask. The TLB shootdown code can figure out from
854		 * cpu_tlbstate_shared.is_lazy whether or not to send an IPI.
855		 */
856		if (IS_ENABLED(CONFIG_DEBUG_VM) &&
857		    WARN_ON_ONCE(prev != &init_mm && !is_notrack_mm(prev) &&
858				 !cpumask_test_cpu(cpu, mm_cpumask(next))))
859			cpumask_set_cpu(cpu, mm_cpumask(next));
860 
861		/* Check if the current mm is transitioning to a global ASID */
862		if (mm_needs_global_asid(next, prev_asid)) {
863			next_tlb_gen = atomic64_read(&next->context.tlb_gen);
864			ns = choose_new_asid(next, next_tlb_gen);

주요 구문 해설

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

L774#endif

751행에서 시작한 선택 구간을 마칩니다. 성능 측정 perf를 빌드하면 주소 공간별 RDPMC 허용 정책에 따라 CR4.PCE를 바꿉니다. 사용자가 성능 카운터를 직접 읽기 전에 다른 문맥의 카운터 흔적도 정리하며, 기능이 없으면 이 helper는 빈 함수입니다.

L782void switch_mm_irqs_off(struct mm_struct *unused, struct mm_struct *next,

로컬 IRQ를 막은 상태에서 현재 CPU의 주소 공간을 next로 바꾸는 함수입니다. 현재 문맥은 unused 인자 대신 CPU별 loaded_mm에서 읽습니다.

L783 struct task_struct *tsk)

전환할 태스크 tsk를 추가 인자로 받는 함수 정의의 마지막 부분입니다. 주소 공간뿐 아니라 태스크에 맞는 추측 실행 완화 등의 전환 상태도 이 함수에서 처리합니다.

L785 struct mm_struct *prev = this_cpu_read(cpu_tlbstate.loaded_mm);

현재 CPU의 TLB 상태에 기록된 주소 공간 loaded_mm을 읽습니다. 함수의 unused 인자 대신 실제 하드웨어에 적재했다고 추적 중인 mm를 전환의 출발점으로 사용합니다.

L786 u16 prev_asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);

현재 CPU가 앞선 주소 공간에 사용한 ASID 슬롯을 읽습니다. x86의 이 값은 PCID와 관련된 커널 관리 식별자로, ARM64의 ASID 레지스터를 읽는 동작은 아닙니다.

L787 bool was_lazy = this_cpu_read(cpu_tlbstate_shared.is_lazy);

현재 CPU가 lazy TLB 상태였는지 읽습니다. 커널 스레드 등으로 인해 사용자 주소 공간을 적극적으로 사용하지 않던 상태에서 나오는지 구분합니다.

L788 unsigned cpu = smp_processor_id();

현재 논리 CPU 번호를 구해 주소 공간의 CPU 사용 비트맵과 이후 TLB 전환 처리에 사용합니다.

L789 unsigned long new_lam;

next 주소 공간에 적용할 Linear Address Masking 설정을 담습니다. 사용자 주소 상위 비트를 메타데이터로 사용하는 LAM 제어값을 CR3 갱신과 맞추기 위한 변수입니다.

L790 struct new_asid ns;

새 ASID 선택 결과를 담는 구조체입니다. 사용할 번호와 TLB 무효화 필요 여부를 함께 받아 CR3 재적재 방식을 정합니다.

L791 u64 next_tlb_gen;

다음 주소 공간의 최신 TLB 세대 번호를 보관합니다. CPU에 남은 변환이 페이지 테이블 변경을 따라잡았는지 비교하는 기준입니다.

L795 if (IS_ENABLED(CONFIG_PROVE_LOCKING))

CONFIG_PROVE_LOCKING을 사용하는 빌드에서만 진입 조건의 IRQ 검사를 수행합니다. 이 디버그 기능이 없으면 검사 코드가 생략됩니다.

L796 WARN_ON_ONCE(!irqs_disabled());

이 함수에 IRQ가 켜진 채 들어왔으면 한 번 경고합니다. 전환 도중 원격 TLB 처리 인터럽트가 같은 CPU의 문맥 상태를 동시에 읽지 않아야 합니다.

L807#ifdef CONFIG_DEBUG_VM

DEBUG_VM은 가상 메모리 내부 상태의 일관성을 확인하는 디버깅 설정입니다. 여기서는 실제 CR3와 이전 mm·ASID로 계산한 값이 맞는지 검사하고, 틀리면 경고와 전체 TLB 정리로 복구를 시도합니다. 꺼지면 이 추가 진단은 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L808 if (WARN_ON_ONCE(__read_cr3() != build_cr3(prev->pgd, prev_asid,

현재 CR3가 loaded_mm·ASID로 계산한 예상값과 다른지 검사합니다. 불일치하면 추적 상태를 거치지 않은 페이지 테이블 변경을 의심하고 TLB 전체 무효화를 시도합니다.

L809 tlbstate_lam_cr3_mask()))) {

예상 CR3를 만들 때 현재 LAM 마스크 비트도 포함합니다. 페이지 테이블 주소만 비교하지 않고 실제 CR3 제어 상태까지 맞춰 오검사를 피합니다.

L822 __flush_tlb_all();

디버그 검사에서 CR3와 추적 상태가 맞지 않는 경로를 발견했을 때 로컬 TLB를 모두 무효화합니다. 정상적인 주소 공간 전환마다 무조건 수행하는 줄은 아닙니다.

L824#endif

807행에서 시작한 선택 구간을 마칩니다. DEBUG_VM은 가상 메모리 내부 상태의 일관성을 확인하는 디버깅 설정입니다. 여기서는 실제 CR3와 이전 mm·ASID로 계산한 값이 맞는지 검사하고, 틀리면 경고와 전체 TLB 정리로 복구를 시도합니다. 꺼지면 이 추가 진단은 없습니다.

L825 if (was_lazy)

이전 상태가 lazy TLB였을 때만 is_lazy를 해제합니다. 사용자 주소 공간을 다시 적극적으로 사용하는 CPU로 표시하여 이후 TLB 전파 판단을 맞춥니다.

L826 this_cpu_write(cpu_tlbstate_shared.is_lazy, false);

lazy 상태였다면 현재 CPU의 is_lazy를 false로 바꿉니다. 다시 주소 공간을 사용하는 CPU로 추적해야 이후 TLB 갱신을 놓치지 않습니다.

L840 if (prev == next) {

현재와 다음 mm가 같으면 다른 페이지 테이블로 바꿀 필요가 없는 경로를 택합니다. 그래도 ASID 세대·LAM·lazy 상태 변화는 별도로 처리해야 합니다.

L842 VM_WARN_ON(is_dyn_asid(prev_asid) &&

이전 ASID가 CPU별 동적 슬롯일 때 해당 슬롯의 소유 주소 공간이 next와 일치하는지 검사합니다. 전역 ASID와 동적 슬롯의 관리 방식을 구분하는 디버그 조건입니다.

L843 this_cpu_read(cpu_tlbstate.ctxs[prev_asid].ctx_id) !=

동적 ASID 슬롯에 기록된 ctx_id를 읽어 다음 줄의 next 식별자와 비교합니다. 같은 숫자 ASID 슬롯이 다른 mm의 소유로 남아 있는 오류를 찾습니다.

L844 next->context.ctx_id);

next의 고유 context 식별자와 슬롯 소유자를 비교하는 경고 조건을 마칩니다. 단순 페이지 테이블 주소가 아닌 mm 식별값으로 재사용을 구분합니다.

L856 if (IS_ENABLED(CONFIG_DEBUG_VM) &&

DEBUG_VM 빌드에서 mm의 CPU 사용 비트맵이 올바른지 검사하는 분기를 시작합니다. 일반 빌드에서는 이 진단 경로를 생략합니다.

L857 WARN_ON_ONCE(prev != &init_mm && !is_notrack_mm(prev) &&

init_mm과 추적을 생략하는 특수 mm를 제외하고 현재 CPU가 사용 비트맵에 있는지 검사합니다. 누락되면 아래에서 경고하고 CPU 비트를 다시 넣습니다.

L858 !cpumask_test_cpu(cpu, mm_cpumask(next))))

앞줄 WARN_ON_ONCE 조건의 마지막 부분입니다. 같은 주소 공간을 계속 쓰는 경로인데도 현재 CPU가 mm_cpumask(next)에 없으면 추적 상태가 잘못되었다고 판단합니다.

L859 cpumask_set_cpu(cpu, mm_cpumask(next));

디버그 검사에서 빠져 있던 현재 CPU를 next의 CPU 사용 비트맵에 다시 넣습니다. 이후 해당 주소 공간의 TLB 갱신 대상에서 누락되지 않도록 합니다.

L862 if (mm_needs_global_asid(next, prev_asid)) {

현재 주소 공간이 CPU별 ASID에서 전역 ASID로 전환 중인지 검사합니다. 필요하면 최신 TLB 세대에 맞는 새 ASID를 선택하고 CR3 재적재 경로로 갑니다.

L863 next_tlb_gen = atomic64_read(&next->context.tlb_gen);

global ASID로 전환해야 하는 주소 공간의 최신 TLB 세대를 읽습니다. 다음 ASID 선택이 이미 발생한 페이지 테이블 변경까지 반영하도록 합니다.

L864 ns = choose_new_asid(next, next_tlb_gen);

per-CPU ASID slot과 flush 필요 여부를 선택한다.

RISC-V · Linux 6.18.37

SATP.MODE, ASID와 root PPN을 조합

switch_mm()는 cpu mask, icache stale mask와 ASID version을 확인한 뒤 SATP를 쓴다. ASID extension이 없으면 주소 공간 전환마다 더 넓은 sfence.vma가 필요하다.

원본 코드: arch/riscv/mm/context.c:310-339

310		 * If cache will be flushed in switch_to, no need to flush here.
311		 */
312		if (!(task && switch_to_should_flush_icache(task)))
313			local_flush_icache_all();
314	}
315#endif
316}
317 
318void switch_mm(struct mm_struct *prev, struct mm_struct *next,
319	struct task_struct *task)
320{
321	unsigned int cpu;
322 
323	if (unlikely(prev == next))
324		return;
325 
326	membarrier_arch_switch_mm(prev, next, task);
327 
328	/*
329	 * Mark the current MM context as inactive, and the next as
330	 * active.  This is at least used by the icache flushing
331	 * routines in order to determine who should be flushed.
332	 */
333	cpu = smp_processor_id();
334 
335	set_mm(prev, next, cpu);
336 
337	flush_icache_deferred(next, cpu, task);
338}
339 

주요 구문 해설

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

L312 if (!(task && switch_to_should_flush_icache(task)))

곧 수행할 switch_to에서 이미 명령 캐시를 비우는 태스크이면 여기서는 같은 무효화를 생략합니다. 그렇지 않으면 지금 로컬 명령 캐시를 비웁니다.

L313 local_flush_icache_all();

이 CPU에 미뤄 둔 명령 캐시 무효화를 수행합니다. 다음 switch_to가 같은 작업을 해 줄 경우는 조건문에서 제외하여 중복 처리를 피합니다.

L315#endif

301행에서 시작한 선택 구간을 마칩니다. SMP는 여러 hart가 함께 동작하는 구성입니다. 다른 hart가 수정한 명령이 아직 이 hart의 명령 캐시에 반영되지 않았다면, 주소 공간 전환 때 메모리 순서를 맞추고 지연해 둔 명령 캐시 정리를 수행합니다. 단일 hart 빌드에는 이 CPU 간 조정이 필요 없습니다.

L318void switch_mm(struct mm_struct *prev, struct mm_struct *next,

CPU가 사용할 사용자 주소 공간을 prev에서 next로 전환하는 함수입니다. CPU 사용 비트맵과 주소 변환 상태, 지연된 명령 캐시 작업을 함께 처리합니다.

L319 struct task_struct *task)

새 주소 공간으로 실행할 task를 넘기는 함수 정의의 마지막 줄입니다. 이 태스크의 switch_to가 명령 캐시를 비울지 판단하여 중복 무효화를 줄입니다.

L321 unsigned int cpu;

현재 hart에 대응하는 Linux 논리 CPU 번호를 담습니다. mm별 CPU 사용 비트맵을 갱신할 위치를 선택합니다.

L323 if (unlikely(prev == next))

prev와 next가 같은 주소 공간이면 실제 mm 전환이 필요 없는지 확인하는 조건입니다. unlikely는 이 상황이 드물다는 컴파일러 분기 배치 힌트입니다.

L324 return;

같은 mm를 계속 쓰는 경우 주소 공간 전환 함수를 즉시 끝냅니다. 아래의 CPU 비트맵 변경과 페이지 테이블 전환을 수행하지 않습니다.

L326 membarrier_arch_switch_mm(prev, next, task);

membarrier가 요구하는 주소 공간 전환 시의 메모리 순서 보장을 처리합니다. prev·next와 전환할 태스크를 넘겨 사용자 공간으로 돌아가기 전 필요한 동기화를 맞춥니다.

L333 cpu = smp_processor_id();

현재 논리 CPU 번호를 구합니다. 이전 mm의 사용 CPU 표시를 지우고 새 mm의 표시를 설정하는 데 사용합니다.

L335 set_mm(prev, next, cpu);

이 CPU가 사용하는 주소 공간을 prev에서 next로 바꿉니다. CPU 사용 비트맵과 RISC-V 주소 변환 문맥을 갱신하여 이후 TLB·명령 캐시 전파 대상을 맞춥니다.

L337 flush_icache_deferred(next, cpu, task);

새 주소 공간에 대해 이 CPU에 남은 명령 캐시 무효화 요청을 처리합니다. 다른 CPU에서 코드를 바꾼 뒤 지연한 작업을 사용자 실행 전에 반영합니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

16-bit tag 재사용과 generation wrap

hardware tag가 16bit라 65536개이고 현재 generation=7, next mm context=(generation 6, ASID 42)라고 가정한다.

  1. stale 판단mm generation 6은 현재 7과 다르므로 ASID 42를 그대로 사용할 수 없다.
  2. allocategeneration 7의 free tag 103을 배정하고 context를 (7,103)으로 갱신한다.
  3. root valuePGD PA/root PPN과 tag 103을 TTBR/CR3/SATP 형식에 맞게 조합한다.
  4. wrapfree tag가 없고 generation 8로 넘어가면 old generation translation을 모든 관련 CPU에서 invalidate한 뒤 재사용한다.

결론ASID/PCID 숫자만 로그에 남기면 재사용 충돌을 판별할 수 없다. generation과 root PA를 함께 기록한다.

06 · DEEP DIVE

경계별 상세 분석

01

공통 kernel core와 architecture hook의 경계

각 mm에 hardware address-space tag와 generation을 배정한다. tag가 현재 generation에서 유효하면 root를 tag와 함께 설치해 다른 mm translation과 공존시키고, wrap이면 전체 invalidate 뒤 generation을 올린다.

mm cpumask publication, page-table root write와 speculation barrier 순서가 TLB shootdown의 대상 CPU 선택과 맞아야 한다.

02

arm64: TTBR0_EL1에 ASID와 PGD PA를 조합

check_and_switch_context()는 mm context id generation을 검사하고 필요하면 새 ASID를 배정한다. CnP, PAN, EPD0와 reserved TTBR0 사용 여부가 실제 register write를 바꾼다.

ASID allocation lock과 local flush 뒤 TTBR write가 이어지며 contextidr와 speculation state도 task 경계와 맞춘다. 디버깅할 때는 asid generation, TTBR0_EL1, CONTEXTIDR_EL1, active_asids와 reserved_ttbr0를 본다.

03

x86-64: CR3에 PGD PA, PCID와 no-flush bit를 조합

switch_mm_irqs_off()는 per-CPU loaded_mm, tlb_gen과 ASID slot을 비교한다. 같은 mm라도 generation이 뒤처지면 INVPCID 또는 CR3 reload가 필요하다.

mm_cpumask와 loaded_mm publication은 concurrent flush_tlb_mm_range()가 switch 중 CPU를 놓치지 않게 ordering을 이룬다. 디버깅할 때는 CR3, PCID, tlb_gen, loaded_mm, user/kernel PCID와 IBPB decision을 기록한다.

04

RISC-V: SATP.MODE, ASID와 root PPN을 조합

switch_mm()는 cpu mask, icache stale mask와 ASID version을 확인한 뒤 SATP를 쓴다. ASID extension이 없으면 주소 공간 전환마다 더 넓은 sfence.vma가 필요하다.

SATP write와 local_flush_tlb_all()/ASID flush, instruction-cache synchronization이 membarrier 계약과 연결된다. 디버깅할 때는 SATP MODE/ASID/PPN, context id version, cpumask와 icache_stale_mask를 확인한다.

05

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

ASID/PCID 숫자는 영구 identity가 아니다. generation과 한 쌍일 때만 mm를 식별하며 wrap 뒤 old TLB entry와 재사용 tag가 충돌하지 않게 해야 한다.

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

06

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

fast path는 valid tag와 같은 root 재사용이고 slow path는 allocation, wrap flush, IBPB/L1D mitigation과 lazy TLB 처리다.

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

07 · FAILURE

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

분류관찰되는 결과첫 확인값
arm64ASID wrap flush가 누락되면 새 mm가 이전 mm의 동일 ASID translation을 사용한다.asid generation, TTBR0_EL1, CONTEXTIDR_EL1, active_asids와 reserved_ttbr0를 본다.
x86-64no-flush CR3를 stale PCID에 쓰면 다른 mm translation이 살아남는다.CR3, PCID, tlb_gen, loaded_mm, user/kernel PCID와 IBPB decision을 기록한다.
RISC-VASID version wrap 또는 icache stale bit 누락은 data translation이나 새 code 관찰을 깨뜨린다.SATP MODE/ASID/PPN, context id version, cpumask와 icache_stale_mask를 확인한다.

08 · LAB

재현과 계측 절차

  1. 두 process가 같은 ASID/PCID 숫자를 서로 다른 generation에서 받는 순간을 trace한다.
  2. address-space switch와 동시 TLB shootdown stress로 mm_cpumask race를 재현한다.
  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 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.