← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 01/21

Context switch: callee-saved register와 task state

scheduler의 공통 context_switch() 아래에서 arm64, x86-64, RISC-V가 어떤 register와 thread state를 저장하고 다음 task에 넘기는지 읽습니다.

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

01 · QUESTION

무엇을 확인할 것인가

CPU 하나의 실행 소유권이 prev task에서 next task로 넘어갔다고 판정하려면 어떤 state가 함께 바뀌어야 하는가?

scheduler core는 prevnext를 선택하지만 실제 SP, callee-saved register, TLS, address-space와 architecture control register의 저장 형식은 switch_to hook에 맡긴다. 반환값 last는 단순한 prev pointer가 아니라 switch를 거쳐 다시 실행된 task가 관찰하는 control-flow 계약이다.

공통 prepare_task_switch()finish_task_switch() 사이에서 architecture code가 실행된다. interrupt-disabled 여부, runqueue lock 인계와 stack lifetime을 함께 보지 않으면 함수 호출처럼 보이는 비선형 실행을 잘못 해석한다.

지연 시간 관점Tswitch = Tmm + Tcallee + Ttls + Textended + Tmitigation으로 나눈다. x86의 speculation mitigation과 debug register, arm64의 TLS/MTE, RISC-V의 envcfg/vector 조건이 config에 따라 긴 tail을 만든다.

02 · CONTRACT

공통 계약과 architecture 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64C hook 뒤 cpu_switch_to가 x19-x29, SP와 LR을 교체TLS 또는 contextidr가 prev 값으로 남아 user return 직후 다른 task의 상태를 관찰한다.prev/next, SP, x19-x29, TPIDR_EL0, CONTEXTIDR_EL1과 last를 한 checkpoint에서 비교한다.
x86-64inactive_task_frame과 per-CPU/TSS state를 교체GS base 또는 TSS.sp0가 prev task 값이면 다음 user exception이 잘못된 kernel stack이나 TLS로 진입한다.RSP, FSBASE, GSBASE, TSS.sp0, PKRU, debug register와 TIF_NEED_FPU_LOAD를 기록한다.
RISC-Vassembly가 ra, sp와 s0-s11을 thread_struct에 저장s-register 한 칸의 asm-offset 불일치가 return address나 frame pointer를 다른 field로 복원한다.TASK_THREAD_* generated offset, ra, sp, s0-s11, tp와 task kernel stack 범위를 대조한다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

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

arm64

mechanism
C hook 뒤 cpu_switch_to가 x19-x29, SP와 LR을 교체
state
__switch_to()는 FPSIMD, TLS, hw breakpoint, contextidr, entry task와 MTE state를 순서대로 갱신한 뒤 assembly switch로 내려간다. x18 platform register와 PAC/SCS 설정도 build option에 따라 경계에 포함된다.
checkpoint
prev/next, SP, x19-x29, TPIDR_EL0, CONTEXTIDR_EL1과 last를 한 checkpoint에서 비교한다.

x86-64

mechanism
inactive_task_frame과 per-CPU/TSS state를 교체
state
__switch_to()는 FPU lazy load 표시, FS/GS segment와 base, TLS array, TSS.sp0, IO bitmap, PKRU와 speculation control을 처리한다. 일반 GPR 대부분은 상위 switch assembly의 inactive frame이 담당한다.
checkpoint
RSP, FSBASE, GSBASE, TSS.sp0, PKRU, debug register와 TIF_NEED_FPU_LOAD를 기록한다.

RISC-V

mechanism
assembly가 ra, sp와 s0-s11을 thread_struct에 저장
state
__switch_to는 prev의 callee-saved integer register를 저장하고 next 값을 복원한 뒤 tp를 next task로 바꾼다. FPU/vector는 switch macro의 별도 helper가 status dirty bit를 보고 처리한다.
checkpoint
TASK_THREAD_* generated offset, ra, sp, s0-s11, tp와 task kernel stack 범위를 대조한다.
그림 2. 공통 kernel과 architecture hook의 소유권공통 정책이 hardware state를 직접 소유하지 않는 경계를 표시합니다.
Linux common contractscheduler core는 prevnext를 선택하지만 실제 SP, callee-saved register, TLS, address-space와 architecture control register의 저장 형식은 switch_to hook에 맡긴다. 반환값 last는 단순한 prev pointer가 아니라 switch를 거쳐 다시 실행된 task가 관찰하는 control-flow 계약이다.
arm64C hook 뒤 cpu_switch_to가 x19-x29, SP와 LR을 교체새 task state가 CPU에 보이기 전에 pending asynchronous MTE fault와 user register state를 정리한다.
x86-64inactive_task_frame과 per-CPU/TSS state를 교체FS/GS base write와 per-CPU current 갱신, TSS stack 게시 순서는 NMI와 user return이 중간 상태를 보지 않게 해야 한다.
RISC-Vassembly가 ra, sp와 s0-s11을 thread_struct에 저장저장과 복원 사이에 tp가 바뀌므로 per-task field 접근이 어느 task 기준인지 instruction 순서로 확인해야 한다.
lifetime boundaryprev의 live register는 prev->thread와 prev kernel stack이 소유하고, next의 저장 state는 restore 직후 CPU register가 소유한다. switch가 완료된 뒤 old stack의 local pointer를 다른 CPU가 임의로 사용해서는 안 된다.
그림 3. publication과 관찰 순서state를 준비한 뒤 architecture ordering을 거쳐 관찰 가능한 checkpoint가 됩니다.
arm64state 준비새 task state가 CPU에 보이기 전에 pending asynchronous MTE fault와 user register state를 정리한다.관찰: prev/next, SP, x19-x29, TPIDR_EL0, CONTEXTIDR_EL1과 last를 한 checkpoint에서 비교한다.
x86-64state 준비FS/GS base write와 per-CPU current 갱신, TSS stack 게시 순서는 NMI와 user return이 중간 상태를 보지 않게 해야 한다.관찰: RSP, FSBASE, GSBASE, TSS.sp0, PKRU, debug register와 TIF_NEED_FPU_LOAD를 기록한다.
RISC-Vstate 준비저장과 복원 사이에 tp가 바뀌므로 per-task field 접근이 어느 task 기준인지 instruction 순서로 확인해야 한다.관찰: TASK_THREAD_* generated offset, ra, sp, s0-s11, tp와 task kernel stack 범위를 대조한다.

04 · SOURCE

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

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

arm64 · Linux 6.18.37

C hook 뒤 cpu_switch_to가 x19-x29, SP와 LR을 교체

__switch_to()는 FPSIMD, TLS, hw breakpoint, contextidr, entry task와 MTE state를 순서대로 갱신한 뒤 assembly switch로 내려간다. x18 platform register와 PAC/SCS 설정도 build option에 따라 경계에 포함된다.

원본 코드: arch/arm64/kernel/process.c:734-784

734	/* ISB required for the kernel uaccess routines when setting TCF0. */
735	isb();
736}
737 
738/*
739 * Thread switching.
740 */
741__notrace_funcgraph __sched
742struct task_struct *__switch_to(struct task_struct *prev,
743				struct task_struct *next)
744{
745	struct task_struct *last;
746 
747	fpsimd_thread_switch(next);
748	tls_thread_switch(next);
749	hw_breakpoint_thread_switch(next);
750	contextidr_thread_switch(next);
751	entry_task_switch(next);
752	ssbs_thread_switch(next);
753	cntkctl_thread_switch(prev, next);
754	ptrauth_thread_switch_user(next);
755	permission_overlay_switch(next);
756	gcs_thread_switch(next);
757 
758	/*
759	 * Complete any pending TLB or cache maintenance on this CPU in case the
760	 * thread migrates to a different CPU. This full barrier is also
761	 * required by the membarrier system call. Additionally it makes any
762	 * in-progress pgtable writes visible to the table walker; See
763	 * emit_pte_barriers().
764	 */
765	dsb(ish);
766 
767	/*
768	 * MTE thread switching must happen after the DSB above to ensure that
769	 * any asynchronous tag check faults have been logged in the TFSR*_EL1
770	 * registers.
771	 */
772	mte_thread_switch(next);
773	/* avoid expensive SCTLR_EL1 accesses if no change */
774	if (prev->thread.sctlr_user != next->thread.sctlr_user)
775		update_sctlr_el1(next->thread.sctlr_user);
776 
777	/* the actual thread switch */
778	last = cpu_switch_to(prev, next);
779 
780	return last;
781}
782 
783struct wchan_info {
784	unsigned long	pc;

주요 구문 해설

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

L735 isb();

SCTLR_EL1 변경이 이후 명령 실행에 반영되도록 ISB로 문맥을 동기화합니다. 특히 사용자 메모리 접근 코드가 새 MTE 태그 검사 모드 TCF0를 보도록 해야 합니다.

L741__notrace_funcgraph __sched

문맥 전환 함수에 함수 그래프 추적을 금지하고 스케줄러 코드 영역 속성을 붙입니다. 스택·현재 태스크가 바뀌는 중간 상태를 일반 함수 호출 추적처럼 다루지 않게 합니다.

L742struct task_struct *__switch_to(struct task_struct *prev,

아키텍처별 태스크 문맥 전환 함수입니다. prev는 나갈 태스크이며 반환값은 나중에 이 실행이 다시 재개될 때 직전에 CPU를 내준 태스크를 나타냅니다.

L743 struct task_struct *next)

다음에 실행할 next를 두 번째 인자로 받습니다. 아래에서 next의 TLS·디버그·보안 상태를 준비한 뒤 스택과 보존 레지스터를 실제로 바꿉니다.

L745 struct task_struct *last;

cpu_switch_to가 반환할 직전 태스크 포인터를 받을 변수입니다. 스케줄링을 거쳐 이 태스크가 다시 실행될 때 정리할 이전 태스크를 상위 코드에 알립니다.

L747 fpsimd_thread_switch(next);

lazy FPSIMD ownership을 next 기준으로 바꾸는 첫 extended-state 경계다.

L748 tls_thread_switch(next);

현재 스레드의 TLS 레지스터 값을 보관하고 next의 TLS 상태를 복원합니다. 사용자 공간의 스레드별 데이터 접근이 이전 태스크의 주소를 계속 사용하지 않게 합니다.

L749 hw_breakpoint_thread_switch(next);

다음 태스크의 하드웨어 breakpoint·watchpoint 상태를 CPU에 반영합니다. 디버그 예외가 현재 실행할 태스크에 설정된 감시 조건에 맞게 발생하도록 합니다.

L750 contextidr_thread_switch(next);

설정에 따라 CONTEXTIDR_EL1에 next의 식별자를 기록합니다. 디버그·추적 장치가 관측한 실행을 어느 태스크의 실행인지 구분할 수 있게 합니다.

L751 entry_task_switch(next);

이 CPU의 __entry_task에 next를 기록합니다. 사용자 공간에서 예외로 다시 들어올 때 사용할 현재 태스크 정보를 전환 전에 준비합니다.

L752 ssbs_thread_switch(next);

next에 필요한 speculative store bypass 완화 상태를 적용합니다. CPU의 SSBS 지원과 태스크의 완화 설정에 맞춰 사용자 실행의 추측 동작을 맞춥니다.

L753 cntkctl_thread_switch(prev, next);

이전·다음 태스크의 32비트 실행 여부와 타이머 접근 정책을 비교하여 필요할 때 CNTKCTL_EL1을 갱신합니다. CPU 오류 우회나 PR_TSC_SIGSEGV 설정이 요구하면 사용자 CNTVCT 접근을 막습니다.

L754 ptrauth_thread_switch_user(next);

지원되는 키 종류에 따라 next의 사용자 APIB·APDA·APDB·APGA 키를 CPU에 설치합니다. 사용자 포인터 인증 문맥을 전환하는 처리이며, 커널에서도 쓰는 APIA 키는 이 함수에서 바꾸지 않습니다.

L755 permission_overlay_switch(next);

지원 CPU에서 현재 POR_EL0를 보관하고 next의 permission overlay 값으로 바꿉니다. 사용자 페이지의 기본 권한 위에 적용되는 태스크별 추가 접근 제한을 전환합니다.

L756 gcs_thread_switch(next);

지원 CPU에서 현재 guarded control stack 포인터를 저장하고 next의 포인터와 활성화 모드를 복원합니다. 필요하면 GCS 전용 동기화도 수행하여 CPU 이동 시 보호 스택의 메모리 효과를 정렬합니다.

L765 dsb(ish);

inner-shareable 영역의 대기 중 메모리·TLB·캐시 작업을 완료합니다. 태스크가 다른 CPU로 이동해도 유지되어야 할 순서, membarrier 요구, 페이지 테이블 쓰기와 비동기 MTE 오류 기록을 함께 맞춥니다.

L772 mte_thread_switch(next);

next에 맞는 사용자 태그 검사 제어값과 태그 생성 제외 마스크를 준비하고 TCO 상태를 조정합니다. 이어 EL1의 비동기 태그 오류를 검사하므로 앞의 DSB가 TFSR_EL1 기록을 먼저 완료시켜야 합니다.

L774 if (prev->thread.sctlr_user != next->thread.sctlr_user)

두 태스크의 사용자 SCTLR 제어값이 다를 때만 레지스터 갱신을 수행합니다. 같으면 비싼 시스템 레지스터 쓰기와 ISB를 생략합니다.

L775 update_sctlr_el1(next->thread.sctlr_user);

태스크별 SCTLR 사용자 제어값이 달라질 때만 next의 값을 적용합니다. CPU 제어 레지스터 접근 비용을 줄이면서 사용자 접근 동작은 다음 태스크에 맞춥니다.

L778 last = cpu_switch_to(prev, next);

assembly가 callee-saved register와 SP를 실제로 교체하며 이후 실행 stack의 소유자가 바뀐다.

L780 return last;

실제 전환 함수가 돌려준 last 태스크를 스케줄러에 전달합니다. 이 함수가 다시 실행을 얻었을 때 바로 전에 실행하던 태스크를 마무리할 수 있게 합니다.

L783struct wchan_info {

잠든 태스크의 대기 위치를 스택에서 찾는 wchan_info 구조체를 정의합니다. 위 문맥 전환 함수는 이미 끝났으며 이 구조체는 다음 스택 탐색 함수의 자료입니다.

L784 unsigned long pc;

스택 탐색 중 발견한 스케줄러 밖의 명령 주소를 저장할 pc 필드입니다. 실제 CPU의 PC를 이 선언으로 바꾸는 것이 아닙니다.

x86-64 · Linux 6.18.37

inactive_task_frame과 per-CPU/TSS state를 교체

__switch_to()는 FPU lazy load 표시, FS/GS segment와 base, TLS array, TSS.sp0, IO bitmap, PKRU와 speculation control을 처리한다. 일반 GPR 대부분은 상위 switch assembly의 inactive frame이 담당한다.

원본 코드: arch/x86/kernel/process_64.c:601-673

601 *
602 * This could still be optimized:
603 * - fold all the options into a flag word and test it with a single test.
604 * - could test fs/gs bitsliced
605 *
606 * Kprobes not supported here. Set the probe on schedule instead.
607 * Function graph tracer not supported too.
608 */
609__no_kmsan_checks
610__visible __notrace_funcgraph struct task_struct *
611__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
612{
613	struct thread_struct *prev = &prev_p->thread;
614	struct thread_struct *next = &next_p->thread;
615	int cpu = smp_processor_id();
616 
617	WARN_ON_ONCE(IS_ENABLED(CONFIG_DEBUG_ENTRY) &&
618		     this_cpu_read(hardirq_stack_inuse));
619 
620	switch_fpu(prev_p, cpu);
621 
622	/* We must save %fs and %gs before load_TLS() because
623	 * %fs and %gs may be cleared by load_TLS().
624	 *
625	 * (e.g. xen_load_tls())
626	 */
627	save_fsgs(prev_p);
628 
629	/*
630	 * Load TLS before restoring any segments so that segment loads
631	 * reference the correct GDT entries.
632	 */
633	load_TLS(next, cpu);
634 
635	/*
636	 * Leave lazy mode, flushing any hypercalls made here.  This
637	 * must be done after loading TLS entries in the GDT but before
638	 * loading segments that might reference them.
639	 */
640	arch_end_context_switch(next_p);
641 
642	/* Switch DS and ES.
643	 *
644	 * Reading them only returns the selectors, but writing them (if
645	 * nonzero) loads the full descriptor from the GDT or LDT.  The
646	 * LDT for next is loaded in switch_mm, and the GDT is loaded
647	 * above.
648	 *
649	 * We therefore need to write new values to the segment
650	 * registers on every context switch unless both the new and old
651	 * values are zero.
652	 *
653	 * Note that we don't need to do anything for CS and SS, as
654	 * those are saved and restored as part of pt_regs.
655	 */
656	savesegment(es, prev->es);
657	if (unlikely(next->es | prev->es))
658		loadsegment(es, next->es);
659 
660	savesegment(ds, prev->ds);
661	if (unlikely(next->ds | prev->ds))
662		loadsegment(ds, next->ds);
663 
664	x86_fsgsbase_load(prev, next);
665 
666	x86_pkru_load(prev, next);
667 
668	/*
669	 * Switch the PDA and FPU contexts.
670	 */
671	raw_cpu_write(current_task, next_p);
672	raw_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p));
673 

주요 구문 해설

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

L609__no_kmsan_checks

이 문맥 전환 함수에서 KMSAN의 미초기화 값 검사를 억제하는 속성입니다. 태스크별 실행 상태를 교체하는 특수 경로에 일반 검사 코드를 삽입하지 않게 합니다.

L610__visible __notrace_funcgraph struct task_struct *

어셈블리 등 외부 진입점에서 사용할 수 있게 함수를 보존하고 함수 그래프 추적을 금지합니다. 반환형은 직전에 실행하던 태스크를 가리키는 task_struct 포인터입니다.

L611__switch_to(struct task_struct *prev_p, struct task_struct *next_p)

prev_p에서 next_p로 x86의 아키텍처별 태스크 상태를 바꾸는 함수입니다. 보존 스택 전환과 협력하여 FPU·TLS·세그먼트·CPU별 current 정보를 맞춥니다.

L613 struct thread_struct *prev = &prev_p->thread;

이전 태스크의 thread_struct 주소를 prev에 둡니다. FS/GS·세그먼트·PKRU 등 x86 전용 저장 상태에 짧게 접근하기 위한 포인터입니다.

L614 struct thread_struct *next = &next_p->thread;

다음 태스크의 x86 전용 저장 상태를 next로 가리킵니다. 아래 복원 경로가 이 구조체에서 CPU에 올릴 값을 읽습니다.

L615 int cpu = smp_processor_id();

현재 논리 CPU 번호를 얻습니다. 다음 태스크의 TLS 디스크립터를 어느 CPU의 GDT에 설치할지 지정하는 데 사용합니다.

L617 WARN_ON_ONCE(IS_ENABLED(CONFIG_DEBUG_ENTRY) &&

진입 디버그 기능을 켠 경우 IRQ 전용 스택 안에서 문맥 전환을 시도했는지 검사합니다. 다음 줄의 hardirq_stack_inuse와 함께 잘못된 스케줄링 문맥을 경고합니다.

L618 this_cpu_read(hardirq_stack_inuse));

CONFIG_DEBUG_ENTRY가 켜졌을 때 hardirq 스택 사용 중인지 검사하는 WARN_ON_ONCE 조건을 마칩니다. IRQ 전용 스택에서 태스크 전환을 수행하는 잘못된 진입 상태를 경고하기 위한 검사입니다.

L620 switch_fpu(prev_p, cpu);

prev의 live xstate ownership을 정리하고 next user return에 필요한 lazy-load flag를 만든다.

L627 save_fsgs(prev_p);

이전 태스크의 FS·GS 선택자와 base를 저장합니다. 이어지는 TLS 디스크립터 변경이 특히 반가상화 환경에서 이 상태에 영향을 줄 수 있어 먼저 보관합니다.

L633 load_TLS(next, cpu);

next의 TLS 디스크립터를 현재 CPU의 GDT에 설치합니다. 뒤에서 ES·DS·FS·GS 선택자를 복원할 때 참조할 디스크립터가 새 태스크의 값이어야 합니다.

L640 arch_end_context_switch(next_p);

반가상화 환경의 지연된 문맥 전환 처리를 끝내고 보류된 hypercall 등을 반영합니다. 앞의 TLS 갱신이 실제로 적용된 뒤 세그먼트 레지스터를 복원하도록 순서를 맞춥니다.

L656 savesegment(es, prev->es);

현재 ES 선택자를 prev->es에 저장합니다. 이전 태스크가 다시 실행될 때 세그먼트 상태를 되돌릴 수 있게 합니다.

L657 if (unlikely(next->es | prev->es))

이전 ES와 다음 ES 중 하나라도 0이 아니면 다음 ES 선택자를 다시 로드합니다. 둘 다 0이면 상태가 동일한 기본값이므로 비용이 큰 세그먼트 복원을 생략합니다.

L658 loadsegment(es, next->es);

이전 또는 다음 ES 값이 0이 아닌 경우 next의 ES 선택자를 로드합니다. 두 태스크 모두 기본 0 값이면 불필요한 세그먼트 접근을 생략합니다.

L660 savesegment(ds, prev->ds);

현재 DS 선택자를 prev->ds에 저장합니다. 사용자 데이터 세그먼트 선택자도 태스크별 문맥의 일부로 보관합니다.

L661 if (unlikely(next->ds | prev->ds))

이전 DS와 다음 DS가 모두 0인지 검사합니다. 하나라도 다르면 같은 선택자여도 디스크립터가 달라질 수 있으므로 다음 태스크의 DS를 다시 로드합니다.

L662 loadsegment(ds, next->ds);

이전 또는 다음 DS가 0이 아닌 경우 next의 DS를 복원합니다. 이전 태스크의 선택자가 다음 태스크에 남지 않게 합니다.

L664 x86_fsgsbase_load(prev, next);

CPU의 FSGSBASE 지원과 두 태스크의 선택자·base 값에 맞춰 FS/GS 상태를 복원합니다. 사용자 TLS 주소 계산이 다음 태스크의 base를 사용하도록 합니다.

L666 x86_pkru_load(prev, next);

이전 태스크의 protection key 권한 상태를 저장하고 next의 PKRU를 로드합니다. 같은 가상 주소라도 태스크별로 달라질 수 있는 pkey 접근 권한을 전환합니다.

L671 raw_cpu_write(current_task, next_p);

현재 CPU의 current_task 포인터를 next로 바꿉니다. 이후 이 CPU에서 current를 조회하면 다음 태스크를 가리키게 됩니다.

L672 raw_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p));

CPU별 커널 스택 최상단 값을 next의 스택 상단으로 갱신합니다. 이후 진입 경로가 새 태스크의 커널 스택 위치를 찾게 합니다.

RISC-V · Linux 6.18.37

assembly가 ra, sp와 s0-s11을 thread_struct에 저장

__switch_to는 prev의 callee-saved integer register를 저장하고 next 값을 복원한 뒤 tp를 next task로 바꾼다. FPU/vector는 switch macro의 별도 helper가 status dirty bit를 보고 처리한다.

원본 코드: arch/riscv/kernel/entry.S:380-440

380 *   a0: previous task_struct (must be preserved across the switch)
381 *   a1: next task_struct
382 *
383 * The value of a0 and a1 must be preserved by this function, as that's how
384 * arguments are passed to schedule_tail.
385 */
386SYM_FUNC_START(__switch_to)
387	/* Save context into prev->thread */
388	li    a4,  TASK_THREAD_RA
389	add   a3, a0, a4
390	add   a4, a1, a4
391	REG_S ra,  TASK_THREAD_RA_RA(a3)
392	REG_S sp,  TASK_THREAD_SP_RA(a3)
393	REG_S s0,  TASK_THREAD_S0_RA(a3)
394	REG_S s1,  TASK_THREAD_S1_RA(a3)
395	REG_S s2,  TASK_THREAD_S2_RA(a3)
396	REG_S s3,  TASK_THREAD_S3_RA(a3)
397	REG_S s4,  TASK_THREAD_S4_RA(a3)
398	REG_S s5,  TASK_THREAD_S5_RA(a3)
399	REG_S s6,  TASK_THREAD_S6_RA(a3)
400	REG_S s7,  TASK_THREAD_S7_RA(a3)
401	REG_S s8,  TASK_THREAD_S8_RA(a3)
402	REG_S s9,  TASK_THREAD_S9_RA(a3)
403	REG_S s10, TASK_THREAD_S10_RA(a3)
404	REG_S s11, TASK_THREAD_S11_RA(a3)
405 
406	/* save the user space access flag */
407	csrr  s0, CSR_STATUS
408	REG_S s0, TASK_THREAD_SUM_RA(a3)
409 
410	/* Save the kernel shadow call stack pointer */
411	scs_save_current
412	/* Restore context from next->thread */
413	REG_L s0,  TASK_THREAD_SUM_RA(a4)
414	li    s1,  SR_SUM
415	and   s0,  s0, s1
416	csrs  CSR_STATUS, s0
417	REG_L ra,  TASK_THREAD_RA_RA(a4)
418	REG_L sp,  TASK_THREAD_SP_RA(a4)
419	REG_L s0,  TASK_THREAD_S0_RA(a4)
420	REG_L s1,  TASK_THREAD_S1_RA(a4)
421	REG_L s2,  TASK_THREAD_S2_RA(a4)
422	REG_L s3,  TASK_THREAD_S3_RA(a4)
423	REG_L s4,  TASK_THREAD_S4_RA(a4)
424	REG_L s5,  TASK_THREAD_S5_RA(a4)
425	REG_L s6,  TASK_THREAD_S6_RA(a4)
426	REG_L s7,  TASK_THREAD_S7_RA(a4)
427	REG_L s8,  TASK_THREAD_S8_RA(a4)
428	REG_L s9,  TASK_THREAD_S9_RA(a4)
429	REG_L s10, TASK_THREAD_S10_RA(a4)
430	REG_L s11, TASK_THREAD_S11_RA(a4)
431	/* The offset of thread_info in task_struct is zero. */
432	move tp, a1
433	/* Switch to the next shadow call stack */
434	scs_load_current
435	ret
436SYM_FUNC_END(__switch_to)
437 
438#ifndef CONFIG_MMU
439#define do_page_fault do_trap_unknown
440#endif

주요 구문 해설

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

L386SYM_FUNC_START(__switch_to)

__switch_to 함수의 심볼과 함수 범위를 시작합니다. a0에는 이전 task_struct, a1에는 다음 task_struct가 전달되며 두 인자는 전환을 거쳐 유지됩니다.

L388 li a4, TASK_THREAD_RA

task_struct 시작에서 thread.ra까지의 오프셋을 a4에 넣습니다. 이후 저장·복원 주소를 만들 기준 위치이며 실제 반환 주소 값을 읽는 명령은 아닙니다.

L389 add a3, a0, a4

prev task_struct 주소 a0에 thread.ra 오프셋을 더해 a3에 저장 영역 기준 주소를 만듭니다. 뒤의 *_RA 오프셋들은 이 위치를 기준으로 계산됩니다.

L390 add a4, a1, a4

next task_struct 주소 a1에 같은 오프셋을 더해 a4를 다음 태스크의 복원 기준 주소로 바꿉니다. a0·a1을 그대로 유지하며 두 문맥을 동시에 참조합니다.

L391 REG_S ra, TASK_THREAD_RA_RA(a3)

prev의 복귀 PC를 thread_struct의 RA slot에 저장한다.

L392 REG_S sp, TASK_THREAD_SP_RA(a3)

이전 태스크의 sp를 prev->thread의 스택 포인터 저장 슬롯에 기록합니다. a3는 prev의 thread.ra 위치를 기준으로 잡은 주소이며, 나중에 이 태스크로 돌아올 때 이 커널 스택을 재사용합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L393 REG_S s0, TASK_THREAD_S0_RA(a3)

이전 태스크의 보존 레지스터 s0를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L394 REG_S s1, TASK_THREAD_S1_RA(a3)

이전 태스크의 보존 레지스터 s1를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L395 REG_S s2, TASK_THREAD_S2_RA(a3)

이전 태스크의 보존 레지스터 s2를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L396 REG_S s3, TASK_THREAD_S3_RA(a3)

이전 태스크의 보존 레지스터 s3를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L397 REG_S s4, TASK_THREAD_S4_RA(a3)

이전 태스크의 보존 레지스터 s4를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L398 REG_S s5, TASK_THREAD_S5_RA(a3)

이전 태스크의 보존 레지스터 s5를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L399 REG_S s6, TASK_THREAD_S6_RA(a3)

이전 태스크의 보존 레지스터 s6를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L400 REG_S s7, TASK_THREAD_S7_RA(a3)

이전 태스크의 보존 레지스터 s7를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L401 REG_S s8, TASK_THREAD_S8_RA(a3)

이전 태스크의 보존 레지스터 s8를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L402 REG_S s9, TASK_THREAD_S9_RA(a3)

이전 태스크의 보존 레지스터 s9를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L403 REG_S s10, TASK_THREAD_S10_RA(a3)

이전 태스크의 보존 레지스터 s10를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L404 REG_S s11, TASK_THREAD_S11_RA(a3)

이전 태스크의 보존 레지스터 s11를 prev->thread의 해당 슬롯에 저장합니다. a3를 기준으로 한 오프셋을 쓰며, C 호출 규약상 호출 전후 유지되어야 하는 값이 스케줄링으로 사라지지 않게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L407 csrr s0, CSR_STATUS

CSR_STATUS의 현재 값을 s0에 읽습니다. 뒤에서 이전 태스크의 SUM 상태를 보관하므로 사용자 메모리 접근 허용 상태가 전환 중 사라지지 않게 합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.

L408 REG_S s0, TASK_THREAD_SUM_RA(a3)

방금 CSR_STATUS에서 읽은 값을 이전 태스크의 SUM 저장 슬롯에 보관합니다. 사용자 메모리 접근을 허용하던 상태를 태스크 전환 뒤 복원할 자료입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L411 scs_save_current

Shadow Call Stack 구성에서는 gp에 든 현재 보호 스택 포인터를 이전 태스크의 thread_info에 저장합니다. 일반 sp와 별도로 복귀 주소 보호 스택의 위치도 보존합니다.

L413 REG_L s0, TASK_THREAD_SUM_RA(a4)

next->thread의 SUM 저장값을 s0로 읽습니다. 다음 두 명령이 SR_SUM 비트만 추려 CSR_STATUS에 반영하므로 다른 상태 비트 전체를 덮어쓰는 것은 아닙니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L414 li s1, SR_SUM

S 모드의 사용자 페이지 접근 허용 비트인 SR_SUM 마스크를 s1에 넣습니다. next가 저장한 status 전체 중 복원할 비트 하나만 고르기 위한 값입니다.

L415 and s0, s0, s1

next의 저장 status에서 SUM 비트만 남깁니다. 다른 상태 비트를 현재 CPU에 덮어쓰지 않도록 마스크합니다.

L416 csrs CSR_STATUS, s0

선택한 SUM 비트를 CSR_STATUS에 설정합니다. CSRS는 1인 비트만 켜므로 next의 전체 status를 통째로 복원하거나 인터럽트 비트를 변경하는 명령이 아닙니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.

L417 REG_L ra, TASK_THREAD_RA_RA(a4)

다음 태스크가 저장한 반환 주소를 ra로 복원합니다. __switch_to가 끝나면 이 주소를 통해 next가 중단되었던 스케줄링 흐름으로 돌아갑니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L418 REG_L sp, TASK_THREAD_SP_RA(a4)

다음 태스크의 커널 스택 포인터를 sp로 복원합니다. 이후 스택 접근은 이전 태스크가 아닌 next의 커널 스택을 사용합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L419 REG_L s0, TASK_THREAD_S0_RA(a4)

next->thread에 보관된 s0를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L420 REG_L s1, TASK_THREAD_S1_RA(a4)

next->thread에 보관된 s1를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L421 REG_L s2, TASK_THREAD_S2_RA(a4)

next->thread에 보관된 s2를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L422 REG_L s3, TASK_THREAD_S3_RA(a4)

next->thread에 보관된 s3를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L423 REG_L s4, TASK_THREAD_S4_RA(a4)

next->thread에 보관된 s4를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L424 REG_L s5, TASK_THREAD_S5_RA(a4)

next->thread에 보관된 s5를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L425 REG_L s6, TASK_THREAD_S6_RA(a4)

next->thread에 보관된 s6를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L426 REG_L s7, TASK_THREAD_S7_RA(a4)

next->thread에 보관된 s7를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L427 REG_L s8, TASK_THREAD_S8_RA(a4)

next->thread에 보관된 s8를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L428 REG_L s9, TASK_THREAD_S9_RA(a4)

next->thread에 보관된 s9를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L429 REG_L s10, TASK_THREAD_S10_RA(a4)

next->thread에 보관된 s10를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L430 REG_L s11, TASK_THREAD_S11_RA(a4)

next->thread에 보관된 s11를 CPU 레지스터로 복원합니다. a4는 다음 태스크의 저장 문맥을 가리키며, next는 이전에 중단된 C 호출의 보존 레지스터 값을 그대로 이어받습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.

L432 move tp, a1

next task_struct 주소를 tp로 옮겨 현재 태스크를 바꿉니다. thread_info가 task_struct의 시작에 있어 이후 tp 기준 접근이 next의 정보를 읽습니다.

L434 scs_load_current

새 tp가 가리키는 next에서 Shadow Call Stack 포인터를 gp에 로드합니다. 다음 태스크의 함수 복귀 주소 보호 스택을 이어 쓰게 합니다.

L435 ret

앞에서 next의 문맥으로 복원한 ra 주소로 분기하여 해당 태스크의 커널 실행을 이어갑니다. ret는 jalr x0, 0(ra)의 축약형이며, 스택과 보존 레지스터는 앞줄에서 이미 복원했습니다. sret와 달리 권한 단계나 인터럽트 허용 비트를 바꾸지 않습니다.

L436SYM_FUNC_END(__switch_to)

__switch_to의 심볼 크기와 함수 끝을 어셈블러에 표시합니다. 실제 복귀는 앞의 ret 명령에서 이미 이루어졌습니다.

L438#ifndef CONFIG_MMU

MMU가 없는 커널은 페이지 표를 이용한 demand paging을 처리하지 않습니다. 따라서 do_page_fault를 알 수 없는 trap 처리기로 연결하여, 페이지 오류 벡터가 존재하지 않는 MMU handler를 참조하지 않게 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L439#define do_page_fault do_trap_unknown

MMU를 사용하지 않는 빌드에서는 예외 표의 do_page_fault를 do_trap_unknown으로 치환합니다. 가상 주소 변환의 페이지 폴트를 복구할 MMU 경로가 없으므로 공통 예외 표를 유지하면서 해당 항목을 알 수 없는 트랩 처리로 보냅니다.

L440#endif

438행에서 시작한 선택 구간을 마칩니다. MMU가 없는 커널은 페이지 표를 이용한 demand paging을 처리하지 않습니다. 따라서 do_page_fault를 알 수 없는 trap 처리기로 연결하여, 페이지 오류 벡터가 존재하지 않는 MMU handler를 참조하지 않게 합니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

두 task의 stack과 복귀 PC를 수치로 검산

prev SP=0xffff80001234fe00, next->thread.sp=0xffff80004567fdc0, next->thread.pc=0xffff800080123400이라고 가정한다.

  1. saveprev SP와 callee-saved register가 prev->thread 또는 inactive frame 범위에 기록되는지 확인한다.
  2. restoreCPU SP가 0xffff80004567fdc0으로 바뀐 뒤 모든 local access는 next stack 범위여야 한다.
  3. resumenext 복귀 PC 0xffff800080123400이 text mapping에 있고 shadow stack/PAC/ORC 조건을 만족하는지 확인한다.
  4. ownershipper-CPU current, address-space root와 TLS가 모두 next를 가리킬 때 switch 완료로 판정한다.

결론PC만 next code에 있어도 TSS/TLS/MMU가 prev 상태면 완료가 아니다. 최소 SP, current, mm, TLS 네 축을 묶어 판정한다.

06 · DEEP DIVE

경계별 상세 분석

01

공통 kernel core와 architecture hook의 경계

scheduler core는 prevnext를 선택하지만 실제 SP, callee-saved register, TLS, address-space와 architecture control register의 저장 형식은 switch_to hook에 맡긴다. 반환값 last는 단순한 prev pointer가 아니라 switch를 거쳐 다시 실행된 task가 관찰하는 control-flow 계약이다.

공통 prepare_task_switch()finish_task_switch() 사이에서 architecture code가 실행된다. interrupt-disabled 여부, runqueue lock 인계와 stack lifetime을 함께 보지 않으면 함수 호출처럼 보이는 비선형 실행을 잘못 해석한다.

02

arm64: C hook 뒤 cpu_switch_to가 x19-x29, SP와 LR을 교체

__switch_to()는 FPSIMD, TLS, hw breakpoint, contextidr, entry task와 MTE state를 순서대로 갱신한 뒤 assembly switch로 내려간다. x18 platform register와 PAC/SCS 설정도 build option에 따라 경계에 포함된다.

새 task state가 CPU에 보이기 전에 pending asynchronous MTE fault와 user register state를 정리한다. 디버깅할 때는 prev/next, SP, x19-x29, TPIDR_EL0, CONTEXTIDR_EL1과 last를 한 checkpoint에서 비교한다.

03

x86-64: inactive_task_frame과 per-CPU/TSS state를 교체

__switch_to()는 FPU lazy load 표시, FS/GS segment와 base, TLS array, TSS.sp0, IO bitmap, PKRU와 speculation control을 처리한다. 일반 GPR 대부분은 상위 switch assembly의 inactive frame이 담당한다.

FS/GS base write와 per-CPU current 갱신, TSS stack 게시 순서는 NMI와 user return이 중간 상태를 보지 않게 해야 한다. 디버깅할 때는 RSP, FSBASE, GSBASE, TSS.sp0, PKRU, debug register와 TIF_NEED_FPU_LOAD를 기록한다.

04

RISC-V: assembly가 ra, sp와 s0-s11을 thread_struct에 저장

__switch_to는 prev의 callee-saved integer register를 저장하고 next 값을 복원한 뒤 tp를 next task로 바꾼다. FPU/vector는 switch macro의 별도 helper가 status dirty bit를 보고 처리한다.

저장과 복원 사이에 tp가 바뀌므로 per-task field 접근이 어느 task 기준인지 instruction 순서로 확인해야 한다. 디버깅할 때는 TASK_THREAD_* generated offset, ra, sp, s0-s11, tp와 task kernel stack 범위를 대조한다.

05

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

prev의 live register는 prev->thread와 prev kernel stack이 소유하고, next의 저장 state는 restore 직후 CPU register가 소유한다. switch가 완료된 뒤 old stack의 local pointer를 다른 CPU가 임의로 사용해서는 안 된다.

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

06

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

Tswitch = Tmm + Tcallee + Ttls + Textended + Tmitigation으로 나눈다. x86의 speculation mitigation과 debug register, arm64의 TLS/MTE, RISC-V의 envcfg/vector 조건이 config에 따라 긴 tail을 만든다.

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

07 · FAILURE

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

분류관찰되는 결과첫 확인값
arm64TLS 또는 contextidr가 prev 값으로 남아 user return 직후 다른 task의 상태를 관찰한다.prev/next, SP, x19-x29, TPIDR_EL0, CONTEXTIDR_EL1과 last를 한 checkpoint에서 비교한다.
x86-64GS base 또는 TSS.sp0가 prev task 값이면 다음 user exception이 잘못된 kernel stack이나 TLS로 진입한다.RSP, FSBASE, GSBASE, TSS.sp0, PKRU, debug register와 TIF_NEED_FPU_LOAD를 기록한다.
RISC-Vs-register 한 칸의 asm-offset 불일치가 return address나 frame pointer를 다른 field로 복원한다.TASK_THREAD_* generated offset, ra, sp, s0-s11, tp와 task kernel stack 범위를 대조한다.

08 · LAB

재현과 계측 절차

  1. sched_switch tracepoint와 architecture switch function graph를 동시에 수집해 core와 hook 시간을 분리한다.
  2. GDB에서 prev/next의 thread_struct와 switch 직후 CPU register를 대조한다.
  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 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.