QUESTION
trap이 발생했을 때 kernel은 원래 thread pointer와 stack을 어떻게 찾고, scause의 interrupt bit와 exception code를 어디에서 분리하는가?
RISC-V는 stvec가 가리키는 entry로 trap을 보내고 scause, sepc, stval, sstatus에 원인을 남긴다. Linux는 sscratch와 tp를 교환해 user에서 왔는지 kernel에서 왔는지 판별하고 task의 kernel stack에 pt_regs를 만든다.
scause의 최상위 bit는 interrupt 여부이고 나머지는 cause code다. assembly는 signed compare로 최상위 bit를 검사해 interrupt면 do_irq, exception이면 excp_vect_table[cause]로 보낸다.
secondary hart는 trap 진입과 별개의 초기화 경로를 가진다. SBI HSM이 secondary_start_sbi에 hartid와 boot data를 넘기고, assembly가 task pointer와 stack을 복원한 뒤 SATP와 stvec를 준비해 smp_callin()으로 들어간다.
ENTRY CONTRACT
들어오는 상태와 내보내는 상태
| 경계 | 입력 | 이 경계가 완성하는 상태 |
|---|---|---|
| User trap | sscratch=kernel tp, tp=user TLS | kernel tp와 task stack |
| Kernel trap | sscratch=0, tp=current | 현재 kernel stack 또는 overflow stack |
| Interrupt cause | scause MSB=1 | do_irq와 irqchip |
| Exception cause | scause MSB=0 | exception vector table |
| SBI hart start | a0=hartid, a1=boot data | smp_callin과 online CPU |
ARCHITECTURE FIGURES
주소와 register를 먼저 그려 본다
pt_regs 실제 바이트 오프셋PT_EPC · trap 복귀 PCPT_RA · x1PT_SP · user trap 이전 stack pointerPT_GP · x3PT_TP · 진입 전 thread pointerPT_T0 · x5PT_STATUS · sstatus/mstatusPT_BADADDR · stval/mtvalPT_CAUSE · scause/mcause숫자는 저장 명령의 실행 순서가 아니라 struct pt_regs 시작 기준 RV64 바이트 오프셋이다. entry.S는 PT_* 오프셋에 저장한다. orig_a0는 구조체에 있지만 이 글의 일반 handle_exception 저장 구간이 값을 채운다고 단정할 수 없다. 좁은 화면에서는 그림을 좌우로 밀어 확인할 수 있다.
MSB = 1
MSB = 0
최상위 bit는 interrupt 표식이고 나머지 index는 exception 또는 interrupt 원인을 나타낸다.
boot CPU의 cpu_start 요청과 secondary hart의 completion 사이를 분리한다.
SOURCE 01
handle_exception의 user/kernel 판별과 cause dispatch
sscratch 교환으로 출처를 판별하고 task kernel stack에 register frame을 만든 뒤 interrupt와 exception을 나눈다.
원본: arch/riscv/kernel/entry.S 96-204줄
96SYM_CODE_START(handle_exception)
97 /*
98 * If coming from userspace, preserve the user thread pointer and load
99 * the kernel thread pointer. If we came from the kernel, the scratch
100 * register will contain 0, and we should continue on the current TP.
101 */
102 csrrw tp, CSR_SCRATCH, tp
103 bnez tp, .Lsave_context
104
105.Lrestore_kernel_tpsp:
106 csrr tp, CSR_SCRATCH
107
108#ifdef CONFIG_64BIT
109 /*
110 * The RISC-V kernel does not eagerly emit a sfence.vma after each
111 * new vmalloc mapping, which may result in exceptions:
112 * - if the uarch caches invalid entries, the new mapping would not be
113 * observed by the page table walker and an invalidation is needed.
114 * - if the uarch does not cache invalid entries, a reordered access
115 * could "miss" the new mapping and traps: in that case, we only need
116 * to retry the access, no sfence.vma is required.
117 */
118 new_vmalloc_check
119#endif
120
121 REG_S sp, TASK_TI_KERNEL_SP(tp)
122
123#ifdef CONFIG_VMAP_STACK
124 addi sp, sp, -(PT_SIZE_ON_STACK)
125 srli sp, sp, THREAD_SHIFT
126 andi sp, sp, 0x1
127 bnez sp, handle_kernel_stack_overflow
128 REG_L sp, TASK_TI_KERNEL_SP(tp)
129#endif
130
131.Lsave_context:
132 REG_S sp, TASK_TI_USER_SP(tp)
133 REG_L sp, TASK_TI_KERNEL_SP(tp)
134 addi sp, sp, -(PT_SIZE_ON_STACK)
135 REG_S x1, PT_RA(sp)
136 REG_S x3, PT_GP(sp)
137 REG_S x5, PT_T0(sp)
138 save_from_x6_to_x31
139
140 /*
141 * Disable user-mode memory access as it should only be set in the
142 * actual user copy routines.
143 *
144 * Disable the FPU/Vector to detect illegal usage of floating point
145 * or vector in kernel space.
146 */
147 li t0, SR_SUM | SR_FS_VS
148
149 REG_L s0, TASK_TI_USER_SP(tp)
150 csrrc s1, CSR_STATUS, t0
151 csrr s2, CSR_EPC
152 csrr s3, CSR_TVAL
153 csrr s4, CSR_CAUSE
154 csrr s5, CSR_SCRATCH
155 REG_S s0, PT_SP(sp)
156 REG_S s1, PT_STATUS(sp)
157 REG_S s2, PT_EPC(sp)
158 REG_S s3, PT_BADADDR(sp)
159 REG_S s4, PT_CAUSE(sp)
160 REG_S s5, PT_TP(sp)
161
162 /*
163 * Set the scratch register to 0, so that if a recursive exception
164 * occurs, the exception vector knows it came from the kernel
165 */
166 csrw CSR_SCRATCH, x0
167
168 /* Load the global pointer */
169 load_global_pointer
170
171 /* Load the kernel shadow call stack pointer if coming from userspace */
172 scs_load_current_if_task_changed s5
173
174#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
175 move a0, sp
176 call riscv_v_context_nesting_start
177#endif
178 move a0, sp /* pt_regs */
179
180 /*
181 * MSB of cause differentiates between
182 * interrupts and exceptions
183 */
184 bge s4, zero, 1f
185
186 /* Handle interrupts */
187 call do_irq
188 j ret_from_exception
1891:
190 /* Handle other exceptions */
191 slli t0, s4, RISCV_LGPTR
192 la t1, excp_vect_table
193 la t2, excp_vect_table_end
194 add t0, t1, t0
195 /* Check if exception code lies within bounds */
196 bgeu t0, t2, 3f
197 REG_L t1, 0(t0)
1982: jalr t1
199 j ret_from_exception
2003:
201
202 la t1, do_trap_unknown
203 j 2b
204SYM_CODE_END(handle_exception)주요 구문 해설
SYM_CODE_START(handle_exception)인터럽트와 동기 예외가 처음 진입하는 공통 코드 심벌을 정의합니다. C 핸들러 호출 전에 중단된 실행의 레지스터를 저장합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
csrrw tp, CSR_SCRATCH, tptp와 sscratch를 원자적으로 교환한다. user 진입이면 새 tp가 kernel task pointer가 되고, kernel 진입이면 0을 받아 별도 복구 경로로 간다.
bnez tp, .Lsave_contextscratch와 교환한 tp가 0이 아니면 사용자 모드에서 들어온 경우이므로, 확보한 커널 task 포인터로 문맥 저장 단계에 갑니다. 0이면 커널 안에서 발생한 예외이므로 기존 tp를 되찾습니다.
.Lrestore_kernel_tpsp:커널 실행 도중 발생한 예외가 기존 커널 tp와 스택을 복구하는 위치입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
csrr tp, CSR_SCRATCH직전 교환으로 scratch에 들어간 원래 커널 tp를 다시 읽습니다. 사용자 모드 진입 때의 task 교체와 달리 현재 task를 그대로 유지합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#ifdef CONFIG_64BIT64비트 RISC-V에서는 새 vmalloc 매핑의 TLB 반영을 늦출 수 있습니다. 해당 매핑을 아직 보지 못해 trap이 난 경우 new_vmalloc_check가 필요한 무효화 또는 접근 재시도를 처리합니다. 32비트 빌드에는 이 경로가 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
new_vmalloc_check새 vmalloc 매핑이 아직 이 hart의 주소 변환에 반영되지 않아 발생한 페이지 폴트인지 검사합니다. 해당하면 필요에 따라 변환 캐시를 정리하고 같은 명령을 재시도하여 전체 예외 처리 경로를 줄입니다.
#endif108행에서 시작한 선택 구간을 마칩니다. 64비트 RISC-V에서는 새 vmalloc 매핑의 TLB 반영을 늦출 수 있습니다. 해당 매핑을 아직 보지 못해 trap이 난 경우 new_vmalloc_check가 필요한 무효화 또는 접근 재시도를 처리합니다. 32비트 빌드에는 이 경로가 없습니다.
REG_S sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
#ifdef CONFIG_VMAP_STACKVMAP_STACK은 가상 매핑된 커널 스택과 보호 영역으로 스택 넘침을 잡는 기능입니다. 켜지면 pt_regs를 놓을 위치의 정렬 비트를 검사해 범위를 벗어났을 때 전용 overflow handler로 보내고, 꺼지면 이 추가 검사를 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
addi sp, sp, -(PT_SIZE_ON_STACK)예외 프레임을 쌓았다고 가정한 위치까지 sp를 낮춥니다. 이 값이 스택 경계를 넘어가는지 먼저 검사하려는 계산입니다.
srli sp, sp, THREAD_SHIFTsp를 스택 크기의 지수 THREAD_SHIFT만큼 밀어 스택 영역 구분에 필요한 비트를 꺼냅니다.
andi sp, sp, 0x1VMAP 스택의 정렬 규칙을 이용해 경계 초과를 나타내는 비트만 남깁니다. 다음 분기는 값이 1이면 스택 오버플로 처리로 이동합니다.
bnez sp, handle_kernel_stack_overflowVMAP stack guard를 넘은 경우 손상된 task stack 대신 per-CPU overflow stack으로 전환한다.
REG_L sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
#endif123행에서 시작한 선택 구간을 마칩니다. VMAP_STACK은 가상 매핑된 커널 스택과 보호 영역으로 스택 넘침을 잡는 기능입니다. 켜지면 pt_regs를 놓을 위치의 정렬 비트를 검사해 범위를 벗어났을 때 전용 overflow handler로 보내고, 꺼지면 이 추가 검사를 생략합니다.
.Lsave_context:사용자·커널 예외 경로가 레지스터 저장을 위해 합류하는 지점입니다. tp는 현재 커널 task를 가리키고 있어야 합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
REG_S sp, TASK_TI_USER_SP(tp)중단된 실행의 sp를 task의 임시 저장 필드에 보관합니다. 필드 이름은 USER_SP이지만 커널에서 발생한 예외에서는 당시 커널 sp가 저장됩니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_L sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
addi sp, sp, -(PT_SIZE_ON_STACK)커널 스택에서 pt_regs를 위한 공간만큼 sp를 낮춥니다. 이후 store들은 이 새 프레임에 레지스터 값을 기록합니다.
REG_S x1, PT_RA(sp)복귀 주소 레지스터 x1, 즉 ra를 pt_regs.ra 위치에 저장합니다. 예외 처리 중 함수 호출로 ra가 바뀌더라도 원래 값을 되찾을 수 있습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S x3, PT_GP(sp)중단된 실행의 전역 포인터 x3, 즉 gp를 저장합니다. 뒤에서 커널용 gp 또는 Shadow Call Stack 포인터를 준비하기 전에 원래 값을 보존합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S x5, PT_T0(sp)임시 레지스터 x5, 즉 t0를 저장합니다. 다음 상태 비트 계산이 t0를 덮어쓰므로 먼저 보관합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
save_from_x6_to_x31x6부터 x31까지의 일반 레지스터를 정해진 pt_regs 오프셋에 저장하는 매크로입니다. x2(sp)와 x4(tp)는 진입 과정에서 용도가 바뀌어 별도로 저장합니다.
li t0, SR_SUM | SR_FS_VShandler가 실수로 user memory, FPU, vector state를 사용하지 못하도록 status bit를 내린다.
REG_L s0, TASK_TI_USER_SP(tp)task에 임시 보관했던 예외 발생 시점의 sp를 s0로 읽습니다. 곧 pt_regs.sp에 넣을 원래 스택 포인터입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
csrrc s1, CSR_STATUS, t0기존 상태 레지스터 값을 s1에 받으면서 SUM·FS·VS를 끕니다. 사용자 메모리 접근과 부동소수점·벡터 사용을 제한한 상태에서 커널 핸들러를 실행하고, 복귀용 상태는 따로 보존합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
csrr s2, CSR_EPCtrap이 끝난 뒤 재개하거나 수정할 instruction address를 frame에 보존한다.
csrr s3, CSR_TVALpage fault address나 illegal instruction 보조 정보처럼 cause별 추가 값을 보존한다.
csrr s4, CSR_CAUSEMSB interrupt bit와 cause number를 보존한다.
csrr s5, CSR_SCRATCH진입 때 scratch로 옮겨 둔 원래 tp를 s5로 읽습니다. 사용자 thread pointer든 커널 task pointer든 중단된 실행의 값을 프레임에 보존합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
REG_S s0, PT_SP(sp)원래 스택 포인터 s0를 pt_regs.sp에 기록합니다. 현재 프레임을 가리키는 sp와 구분해야 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s1, PT_STATUS(sp)비트를 끄기 전 상태 레지스터 값을 프레임에 저장합니다. 예외 복귀 때 권한·인터럽트 등의 상태를 되돌리는 근거가 됩니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s2, PT_EPC(sp)예외 PC를 pt_regs.epc에 저장합니다. 핸들러는 이 주소를 보고 원인을 분석하거나 재개 위치를 조정할 수 있습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s3, PT_BADADDR(sp)트랩 부가 정보인 TVAL을 badaddr 필드에 기록합니다. 페이지 폴트에서는 문제의 접근 주소이며, 모든 예외에서 유효한 주소인 것은 아닙니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s4, PT_CAUSE(sp)인터럽트 여부와 예외 번호가 들어 있는 CAUSE 값을 프레임에 저장합니다. 이 값으로 뒤의 처리 경로를 선택합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s5, PT_TP(sp)진입 전 tp 값을 프레임에 저장합니다. 현재 커널 task를 가리키는 tp를 그대로 저장하는 것이 아니라 s5에 보존한 원래 값입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
csrw CSR_SCRATCH, x0scratch를 0으로 만들어 지금부터는 커널 실행 중임을 표시합니다. 여기서 다시 예외가 발생하면 진입 코드가 사용자 모드에서 온 것으로 오인하지 않습니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
load_global_pointerShadow Call Stack을 끈 빌드에서는 gp에 커널의 __global_pointer$를 읽어 C 핸들러의 전역 데이터 접근을 준비합니다. 켠 빌드에서는 이 매크로가 비어 있어 gp를 바꾸지 않습니다. SCS 포인터를 바꿀지는 다음 scs_load_current_if_task_changed가 진입 전후 task 포인터를 비교해 결정합니다.
scs_load_current_if_task_changed s5진입 전 tp와 현재 커널 tp가 달라졌을 때 현재 task의 Shadow Call Stack 포인터를 gp에 읽습니다. 커널 안의 중첩 예외에서는 진행 중이던 포인터를 불필요하게 되돌리지 않습니다.
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE커널의 RISC-V Vector 사용 중에도 선점을 허용하는 구성입니다. 예외 진입 때 pt_regs를 전달하여 벡터 문맥 중첩을 기록하므로 중단된 커널 벡터 상태를 올바르게 보존할 수 있습니다. 설정이 없으면 이 중첩 관리 호출은 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
move a0, sp방금 만든 pt_regs의 주소를 첫 인자 a0에 넣습니다. 벡터 문맥의 중첩 상태를 기록하는 함수에 넘길 값입니다.
call riscv_v_context_nesting_start커널 벡터 연산 중 예외가 중첩되었는지 관리하는 진입 처리를 호출합니다. 선점 가능한 벡터 사용의 저장·복원 상태를 추적하는 기능입니다.
#endif174행에서 시작한 선택 구간을 마칩니다. 커널의 RISC-V Vector 사용 중에도 선점을 허용하는 구성입니다. 예외 진입 때 pt_regs를 전달하여 벡터 문맥 중첩을 기록하므로 중단된 커널 벡터 상태를 올바르게 보존할 수 있습니다. 설정이 없으면 이 중첩 관리 호출은 없습니다.
move a0, sp /* pt_regs */C 예외·인터럽트 핸들러의 첫 인자로 pt_regs 주소를 준비합니다. 앞선 호출이 a0를 바꿀 수 있어 다시 설정합니다.
bge s4, zero, 1fsigned compare로 scause MSB를 검사한다. 음수면 interrupt, 0 이상이면 synchronous exception이다.
call do_irq비동기 인터럽트를 처리하는 C 함수 do_irq를 호출합니다. a0로 받은 프레임에는 인터럽트 발생 당시의 PC와 레지스터가 들어 있습니다.
j ret_from_exception인터럽트 처리가 끝나면 공통 복귀 코드로 이동합니다. 이 점프 자체가 레지스터나 권한 상태를 복원하는 것은 아니며, 대상 코드가 그 작업을 수행합니다.
1:CAUSE의 최상위 비트가 0인 동기 예외 처리 경로입니다. 예외 번호로 함수 포인터 표를 찾아갑니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
slli t0, s4, RISCV_LGPTR예외 번호에 포인터 크기를 곱합니다. RV64에서는 8바이트 단위의 함수 포인터 표에서 해당 항목의 바이트 오프셋을 만드는 계산입니다.
la t1, excp_vect_tableexception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.
la t2, excp_vect_table_endexception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.
add t0, t1, t0예외 처리 함수 표의 시작 주소에 오프셋을 더해 선택할 항목의 주소를 만듭니다.
bgeu t0, t2, 3f계산한 항목 주소가 표의 끝 이상이면 등록 범위 밖의 예외이므로 미지원 예외 처리 경로로 이동합니다.
REG_L t1, 0(t0)선택한 표 항목에서 실제 예외 처리 함수 주소를 t1으로 읽습니다. 표 항목의 주소와 그 안에 저장된 함수 주소는 서로 다릅니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
2: jalr t1t1이 가리키는 예외 처리 함수를 간접 호출합니다. ra에는 다음 명령의 주소가 저장되어 C 함수가 돌아오면 공통 복귀 분기를 실행합니다.
j ret_from_exception동기 예외 핸들러가 돌아온 뒤 공통 문맥 복원 코드로 이동합니다.
3:예외 번호가 함수 표 범위를 벗어났을 때 도착하는 위치입니다. 알려지지 않은 예외를 처리할 함수를 선택합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
la t1, do_trap_unknown미지원 예외를 처리하는 do_trap_unknown의 주소를 t1에 넣습니다. 사용자·커널 문맥에 맞는 오류 처리로 이어집니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
j 2b앞에 있는 2번 레이블로 돌아가 방금 선택한 do_trap_unknown을 호출합니다. 2b의 b는 뒤쪽 명령이 아니라 앞서 나온 레이블을 뜻합니다.
SYM_CODE_END(handle_exception)handle_exception 코드 심벌의 범위를 끝냅니다. CPU의 복귀 명령이 아니라 도구가 함수 범위를 알도록 하는 어셈블러 매크로입니다.
SOURCE 02
secondary_start_sbi가 task와 stack을 받아 MMU를 켜는 경로
SBI HSM이 시작한 hart는 interrupt를 막고 boot data에서 idle task pointer와 stack pointer를 꺼낸 뒤 boot CPU가 만든 page table을 사용한다.
원본: arch/riscv/kernel/head.S 128-175줄
128 .global secondary_start_sbi
129secondary_start_sbi:
130 /* Mask all interrupts */
131 csrw CSR_IE, zero
132 csrw CSR_IP, zero
133
134#ifndef CONFIG_RISCV_M_MODE
135 /* Enable time CSR */
136 li t0, 0x2
137 csrw CSR_SCOUNTEREN, t0
138#endif
139
140 /* Load the global pointer */
141 load_global_pointer
142
143 /*
144 * Disable FPU & VECTOR to detect illegal usage of
145 * floating point or vector in kernel space
146 */
147 li t0, SR_FS_VS
148 csrc CSR_STATUS, t0
149
150 /* Set trap vector to spin forever to help debug */
151 la a3, .Lsecondary_park
152 csrw CSR_TVEC, a3
153
154 /* a0 contains the hartid & a1 contains boot data */
155 li a2, SBI_HART_BOOT_TASK_PTR_OFFSET
156 XIP_FIXUP_OFFSET a2
157 add a2, a2, a1
158 REG_L tp, (a2)
159 li a3, SBI_HART_BOOT_STACK_PTR_OFFSET
160 XIP_FIXUP_OFFSET a3
161 add a3, a3, a1
162 REG_L sp, (a3)
163
164.Lsecondary_start_common:
165
166#ifdef CONFIG_MMU
167 /* Enable virtual memory and relocate to virtual address */
168 la a0, swapper_pg_dir
169 XIP_FIXUP_OFFSET a0
170 call relocate_enable_mmu
171#endif
172 call .Lsetup_trap_vector
173 scs_load_current
174 call smp_callin
175#endif /* CONFIG_SMP */주요 구문 해설
.global secondary_start_sbiSBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.
secondary_start_sbi:SBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.
csrw CSR_IE, zero보조 hart의 인터럽트 허용 비트를 0으로 만듭니다. 스택과 예외 벡터를 준비하는 동안 일반 인터럽트가 들어오지 않도록 합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
csrw CSR_IP, zero인터럽트 pending CSR에 0을 씁니다. 소프트웨어가 지울 수 있는 pending 상태를 정리하는 동작이며, 외부 장치가 유지하는 요청까지 모두 없애는 것은 아닙니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#ifndef CONFIG_RISCV_M_MODEM-mode가 아닌 S-mode 커널에서는 SCOUNTEREN의 time 비트를 세워 사용자 모드의 time CSR 읽기를 허용합니다. M-mode 구성은 이 S-mode 전용 초기화를 사용하지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
li t0, 0x2SCOUNTEREN의 TM 비트에 해당하는 값 2를 준비합니다. 사용자 모드에서 time CSR을 읽을 수 있게 할 때 사용하는 비트입니다.
csrw CSR_SCOUNTEREN, t0SCOUNTEREN 전체에 2를 써서 TM 비트만 1로 두고, cycle·instret 등 다른 카운터의 허용 비트는 0으로 만듭니다. 상위 권한의 허용 조건도 충족되어 있으면 사용자 모드가 time CSR을 읽을 수 있습니다. 타이머 인터럽트를 켜는 설정은 아닙니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#endif134행에서 시작한 선택 구간을 마칩니다. M-mode가 아닌 S-mode 커널에서는 SCOUNTEREN의 time 비트를 세워 사용자 모드의 time CSR 읽기를 허용합니다. M-mode 구성은 이 S-mode 전용 초기화를 사용하지 않습니다.
load_global_pointerShadow Call Stack을 끈 빌드에서는 gp에 __global_pointer$를 넣어 커널 전역 데이터 접근을 준비합니다. 켠 빌드에서는 gp를 복귀 주소 전용 스택 포인터로 쓰므로 여기서는 아무 명령도 만들지 않고, 뒤의 scs_load_current가 보조 CPU의 idle task에 속한 SCS 포인터를 읽습니다.
li t0, SR_FS_VS부동소수점 FS와 벡터 VS 상태 필드를 지울 마스크를 t0에 만듭니다.
csrc CSR_STATUS, t0현재 상태 레지스터의 FS·VS 비트를 지워 부동소수점과 벡터 사용을 꺼 둡니다. 커널에서 허가되지 않은 사용이 있으면 조용히 상태를 망가뜨리는 대신 예외가 나게 합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
la a3, .Lsecondary_park보조 hart 초기화 중 예외가 나면 진입할 정지 루프 주소를 a3에 준비합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
csrw CSR_TVEC, a3정지 루프를 임시 트랩 벡터로 설치합니다. 정상 벡터와 실행 환경이 완성되기 전의 예외를 이곳에서 멈춥니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
/* a0 contains the hartid & a1 contains boot data */SBI HSM contract에 따라 a0에는 hart ID, a1에는 kernel이 준비한 per-hart boot data가 들어 있다.
li a2, SBI_HART_BOOT_TASK_PTR_OFFSET부트 데이터 구조체 안의 task 포인터 필드 오프셋을 a2에 넣습니다. 구조체 주소 자체는 SBI가 전달한 a1에 있습니다.
XIP_FIXUP_OFFSET a2XIP 빌드에서는 a2에 CONFIG_PHYS_RAM_BASE - 현재 _sdata 주소를 더하는 보정을 적용합니다. 일반 빌드에서는 비어 있습니다. 다음 줄에서 SBI가 넘긴 부트 데이터 주소를 더합니다.
add a2, a2, a1a1의 부트 데이터 주소에 task 필드 오프셋을 더합니다. 다음 load는 이 위치에서 보조 CPU의 idle task 포인터를 가져옵니다.
REG_L tp, (a2)boot data에서 이 hart의 idle task pointer를 복원해 current 기반을 만든다.
li a3, SBI_HART_BOOT_STACK_PTR_OFFSET부트 데이터 안에서 초기 스택 포인터를 저장한 필드의 오프셋을 a3에 넣습니다.
XIP_FIXUP_OFFSET a3XIP 빌드에서는 a3에도 CONFIG_PHYS_RAM_BASE - 현재 _sdata 주소를 더합니다. 일반 빌드에서는 아무 명령도 만들지 않으며, 다음 줄의 부트 데이터 주소 덧셈으로 이어집니다.
add a3, a3, a1부트 데이터 기준 주소 a1을 더해 stack_ptr 필드의 주소를 만듭니다. 다음 줄에서 해당 필드 값을 sp로 읽습니다.
REG_L sp, (a3)이 hart 전용 kernel stack을 복원한다.
.Lsecondary_start_common:보조 hart가 task와 스택을 확보한 뒤 합류하는 공통 시작점입니다. 이후 MMU와 예외 벡터를 준비하고 C 초기화로 넘어갑니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
#ifdef CONFIG_MMUMMU 지원 보조 hart는 부트 hart가 준비한 swapper 페이지 표로 가상 주소 변환을 켭니다. MMU가 없으면 이 전환 없이 같은 공통 trap·task 초기화로 이어집니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la a0, swapper_pg_dirboot CPU가 완성한 kernel page-table root를 secondary hart에도 활성화한다.
XIP_FIXUP_OFFSET a0XIP 빌드에서는 swapper_pg_dir의 주소를 RAM 쪽 실제 위치로 보정합니다. 보조 hart가 사용할 페이지 테이블입니다.
call relocate_enable_mmuswapper_pg_dir를 사용해 MMU를 켜고 가상 주소에서 계속 실행하도록 전환합니다. 복귀 주소도 전환된 주소 공간에 맞춰 바뀝니다.
#endif166행에서 시작한 선택 구간을 마칩니다. MMU 지원 보조 hart는 부트 hart가 준비한 swapper 페이지 표로 가상 주소 변환을 켭니다. MMU가 없으면 이 전환 없이 같은 공통 trap·task 초기화로 이어집니다.
call .Lsetup_trap_vector커널의 정상 예외 진입점 handle_exception을 트랩 벡터에 설치하고 scratch 상태를 초기화하는 루틴을 호출합니다.
scs_load_currentShadow Call Stack을 사용하면 현재 idle task의 별도 복귀 주소 스택 포인터를 gp에 읽습니다. 일반 커널 스택 포인터 sp와 별개입니다.
call smp_callinarchitecture assembly 준비를 끝내고 C CPU hotplug/online 단계로 들어간다.
#endif /* CONFIG_SMP */127행에서 시작한 선택 구간을 마칩니다. 여러 hart를 지원하는 빌드에서는 부트 hart 이외의 CPU가 들어올 secondary_start_sbi 및 공통 보조 CPU 초기화 경로를 포함합니다. 단일 CPU 구성에서는 이 시작 경로가 필요 없습니다.
SOURCE 03
boot CPU 요청과 secondary hart의 online 게시
__cpu_up()은 architecture cpu_ops를 통해 hart를 시작하고 completion을 기다린다. secondary의 smp_callin()은 IPI, topology와 cache/TLB를 준비한 뒤 online bit를 게시한다.
원본: arch/riscv/kernel/smpboot.c 174-252줄
174static int start_secondary_cpu(int cpu, struct task_struct *tidle)
175{
176 if (cpu_ops->cpu_start)
177 return cpu_ops->cpu_start(cpu, tidle);
178
179 return -EOPNOTSUPP;
180}
181
182int __cpu_up(unsigned int cpu, struct task_struct *tidle)
183{
184 int ret = 0;
185 tidle->thread_info.cpu = cpu;
186
187 ret = start_secondary_cpu(cpu, tidle);
188 if (!ret) {
189 wait_for_completion_timeout(&cpu_running,
190 msecs_to_jiffies(1000));
191
192 if (!cpu_online(cpu)) {
193 pr_crit("CPU%u: failed to come online\n", cpu);
194 ret = -EIO;
195 }
196 } else {
197 pr_crit("CPU%u: failed to start\n", cpu);
198 }
199
200 return ret;
201}
202
203void __init smp_cpus_done(unsigned int max_cpus)
204{
205}
206
207/*
208 * C entry point for a secondary processor.
209 */
210asmlinkage __visible void smp_callin(void)
211{
212 struct mm_struct *mm = &init_mm;
213 unsigned int curr_cpuid = smp_processor_id();
214
215 if (has_vector()) {
216 /*
217 * Return as early as possible so the hart with a mismatching
218 * vlen won't boot.
219 */
220 if (riscv_v_setup_vsize())
221 return;
222 }
223
224 /* All kernel threads share the same mm context. */
225 mmgrab(mm);
226 current->active_mm = mm;
227
228 store_cpu_topology(curr_cpuid);
229 notify_cpu_starting(curr_cpuid);
230
231 riscv_ipi_enable();
232
233 numa_add_cpu(curr_cpuid);
234
235 pr_debug("CPU%u: Booted secondary hartid %lu\n", curr_cpuid,
236 cpuid_to_hartid_map(curr_cpuid));
237
238 set_cpu_online(curr_cpuid, true);
239
240 /*
241 * Remote cache and TLB flushes are ignored while the CPU is offline,
242 * so flush them both right now just in case.
243 */
244 local_flush_icache_all();
245 local_flush_tlb_all();
246 complete(&cpu_running);
247 /*
248 * Disable preemption before enabling interrupts, so we don't try to
249 * schedule a CPU that hasn't actually started yet.
250 */
251 local_irq_enable();
252 cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);주요 구문 해설
static int start_secondary_cpu(int cpu, struct task_struct *tidle)논리 CPU 번호와 idle task를 받아 플랫폼의 CPU 시작 함수를 호출하는 작은 연결 함수입니다. 실제 기동 방식은 cpu_ops에 등록된 구현이 담당합니다.
if (cpu_ops->cpu_start)SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.
return cpu_ops->cpu_start(cpu, tidle);SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.
return -EOPNOTSUPP;플랫폼이 CPU 시작 함수를 제공하지 않으면 지원하지 않는 작업이라는 오류 -EOPNOTSUPP를 반환합니다. CPU 기동에 성공한 값이 아닙니다.
int __cpu_up(unsigned int cpu, struct task_struct *tidle)CPU를 온라인으로 올리는 공통 계층이 호출하는 RISC-V 구현입니다. 기동 요청을 보내고 보조 CPU의 초기화 완료를 기다립니다.
int ret = 0;기동 결과를 담을 ret를 성공 값 0으로 초기화합니다. 이후 시작 함수의 오류나 온라인 전환 실패 값으로 바뀔 수 있습니다.
tidle->thread_info.cpu = cpu;idle task의 thread_info에 이 task가 속할 논리 CPU 번호를 기록합니다. 보조 CPU가 task를 받아 실행할 때 자신의 CPU 번호를 알 수 있습니다.
ret = start_secondary_cpu(cpu, tidle);보조 CPU의 시작을 요청하고 반환된 결과를 ret에 저장합니다. 요청 성공과 실제 온라인 전환은 별개여서 아래에서 완료 상태를 추가로 검사합니다.
if (!ret) {기동 요청이 성공한 경우에만 완료 통지를 기다립니다. ret가 음수 오류이면 대기를 하지 않고 시작 실패를 기록합니다.
wait_for_completion_timeout(&cpu_running,secondary가 C online 경로에 도달하는 데 1초 제한을 둔다. timeout과 start request 자체의 오류를 구분한다.
msecs_to_jiffies(1000));대기 한도인 1,000밀리초를 커널 타이머 단위 jiffies로 바꿔 앞줄의 완료 대기 함수에 넘깁니다. 정확히 1초 동안 반드시 잠든다는 뜻은 아닙니다.
if (!cpu_online(cpu)) {대기가 끝난 뒤에도 대상 CPU의 online 비트가 꺼져 있는지 검사합니다. 기동 요청만 수락되고 보조 CPU 초기화는 끝나지 않은 경우를 잡습니다.
pr_crit("CPU%u: failed to come online\n", cpu);어느 논리 CPU가 온라인 상태에 도달하지 못했는지 심각도 높은 로그를 남깁니다. 다음 줄에서 반환 오류도 설정합니다.
ret = -EIO;온라인 전환 실패를 나타내도록 ret에 -EIO를 넣습니다. 함수 호출자에게 실패가 전달됩니다.
} else {기동 요청 자체가 실패했을 때 실행하는 경로입니다. 온라인 완료를 기다리는 경로와 구분합니다.
pr_crit("CPU%u: failed to start\n", cpu);보조 CPU 시작 요청이 실패한 CPU 번호를 출력합니다. ret에 있는 원래 오류는 유지됩니다.
return ret;기동 및 온라인 확인 결과를 호출자에게 돌려줍니다. 0이면 이 함수가 성공으로 판정한 것이고, 음수이면 시작 또는 완료 확인에서 오류가 난 것입니다.
void __init smp_cpus_done(unsigned int max_cpus)SMP CPU 준비가 끝난 뒤 공통 부팅 코드가 호출하는 훅입니다. 이 버전의 RISC-V 구현은 본문이 비어 있어 추가 작업을 하지 않습니다.
asmlinkage __visible void smp_callin(void)보조 hart가 어셈블리 초기화를 마친 뒤 들어오는 C 진입 함수입니다. 현재 CPU의 task·토폴로지·인터럽트 환경을 준비하고 온라인 상태를 알립니다.
struct mm_struct *mm = &init_mm;커널의 초기 주소 공간 init_mm을 mm에 담습니다. idle task 같은 커널 스레드는 전용 사용자 주소 공간 대신 이 문맥을 사용합니다.
unsigned int curr_cpuid = smp_processor_id();현재 실행 중인 보조 hart의 논리 CPU 번호를 읽습니다. 하드웨어 hart ID와 구분되는 Linux 내부 인덱스입니다.
if (has_vector()) {이 시스템에서 벡터 확장을 사용할 수 있는 경우 벡터 레지스터 크기 검사를 수행합니다. 다른 CPU와 호환되지 않는 크기이면 기동을 계속할 수 없습니다.
if (riscv_v_setup_vsize())현재 hart의 벡터 상태 크기를 설정하고 기존 CPU와 호환되는지 검사합니다. 실패하면 바로 다음 줄에서 초기화를 중단합니다.
return;벡터 크기 검사를 통과하지 못했으므로 이 보조 CPU의 C 초기화를 종료합니다. 아래의 online 표시와 완료 통지에는 도달하지 않습니다.
mmgrab(mm);idle kernel thread가 init_mm을 active_mm으로 참조하도록 수명을 보존한다.
current->active_mm = mm;현재 idle task가 실제로 사용하는 주소 공간을 init_mm으로 지정합니다. 바로 앞의 mmgrab은 이 문맥의 수명을 유지하기 위한 참조를 확보합니다.
store_cpu_topology(curr_cpuid);현재 CPU가 어느 코어·클러스터에 속하는지 토폴로지 정보를 저장합니다. 스케줄러 등이 CPU 간 관계를 판단할 때 사용합니다.
notify_cpu_starting(curr_cpuid);CPU가 시작 중이라는 사실을 CPU hotplug 초기화 절차에 알립니다. 각 하위 시스템의 해당 단계 초기화가 이 경로에서 진행됩니다.
riscv_ipi_enable();이 hart가 reschedule, call-function, TLB shootdown 같은 IPI를 받을 수 있게 local interrupt source를 연다.
numa_add_cpu(curr_cpuid);현재 CPU를 해당 NUMA 노드의 CPU 목록에 추가합니다. CPU와 메모리의 거리 관계를 사용할 수 있도록 구성하는 단계입니다.
pr_debug("CPU%u: Booted secondary hartid %lu\n", curr_cpuid,보조 CPU가 부팅되었다는 디버그 로그에 논리 CPU 번호를 넣습니다. 줄이 이어지면서 실제 hart ID도 함께 출력합니다.
cpuid_to_hartid_map(curr_cpuid));논리 CPU 번호를 하드웨어 hart ID로 바꾸어 앞줄 로그의 두 번째 값으로 넘깁니다. 서로 다른 두 번호를 함께 보여 주기 위한 처리입니다.
set_cpu_online(curr_cpuid, true);scheduler와 다른 subsystem이 이 CPU를 사용 가능한 것으로 관찰하는 게시 지점이다.
local_flush_icache_all();offline 동안 무시된 remote instruction-cache flush를 online 직전에 보충한다.
local_flush_tlb_all();이 CPU의 주소 변환 캐시를 모두 무효화합니다. CPU가 offline인 동안 원격 TLB 갱신 대상에서 빠져 있었을 수 있으므로 online 직후 오래된 변환을 정리합니다.
complete(&cpu_running);boot CPU의 __cpu_up() 대기를 해제한다.
local_irq_enable();preemption과 online 준비를 맞춘 뒤 local interrupt를 허용한다.
cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);보조 CPU를 online idle 단계의 실행 루프로 넘깁니다. 이후 이 CPU는 idle 상태로 기다리다가 스케줄러가 선택한 작업을 실행합니다.
SOURCE 04
SBI HSM 호출 직전 task·stack을 physical boot data로 게시하는 구간
boot CPU는 hart를 깨우기 전에 secondary가 읽을 idle task와 stack pointer를 기록하고 memory barrier를 둔다. SBI에는 kernel virtual pointer가 아니라 entry와 boot data의 physical address를 전달한다.
원본: arch/riscv/kernel/cpu_ops_sbi.c 65-80줄
65static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle)
66{
67 unsigned long boot_addr = __pa_symbol(secondary_start_sbi);
68 unsigned long hartid = cpuid_to_hartid_map(cpuid);
69 unsigned long hsm_data;
70 struct sbi_hart_boot_data *bdata = &boot_data[cpuid];
71
72 /* Make sure tidle is updated */
73 smp_mb();
74 bdata->task_ptr = tidle;
75 bdata->stack_ptr = task_pt_regs(tidle);
76 /* Make sure boot data is updated */
77 smp_mb();
78 hsm_data = __pa(bdata);
79 return sbi_hsm_hart_start(hartid, boot_addr, hsm_data);
80}주요 구문 해설
static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle)SBI 펌웨어를 통해 지정한 논리 CPU를 시작하는 함수입니다. Linux CPU 번호를 hart ID로 바꾸고, 진입 주소와 초기 task·스택 정보를 펌웨어에 전달합니다.
unsigned long boot_addr = __pa_symbol(secondary_start_sbi);firmware가 MMU-off hart의 PC에 넣을 수 있도록 secondary assembly entry의 physical address를 계산한다.
unsigned long hartid = cpuid_to_hartid_map(cpuid);Linux logical CPU 번호와 firmware가 식별하는 sparse hart ID를 변환한다. 둘을 같은 숫자로 가정하면 다른 hart를 시작할 수 있다.
unsigned long hsm_data;SBI HSM의 opaque 인자로 넘길 부트 데이터 물리 주소를 저장할 변수를 선언합니다. 보조 hart는 이 값을 a1으로 받아 task와 스택을 찾습니다.
struct sbi_hart_boot_data *bdata = &boot_data[cpuid];시작할 CPU에 대응하는 boot_data 배열 항목을 선택합니다. CPU마다 별도 초기 task 포인터와 스택 포인터를 보관합니다.
smp_mb();idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.
bdata->task_ptr = tidle;보조 CPU가 처음 실행할 idle task 포인터를 부트 데이터에 기록합니다. 진입 어셈블리는 이 필드를 읽어 tp를 준비합니다.
bdata->stack_ptr = task_pt_regs(tidle);idle task kernel stack의 pt_regs 위치를 secondary가 초기 SP로 사용할 값으로 게시한다.
smp_mb();idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.
hsm_data = __pa(bdata);SBI가 MMU-off 상태의 a1에 넣을 boot data physical address로 바꾼다. secondary는 SATP 활성화 전 이 주소를 읽는다.
return sbi_hsm_hart_start(hartid, boot_addr, hsm_data);hart ID, physical entry, physical opaque data 세 값을 firmware HSM state machine에 넘긴다.
WORKED EXAMPLES
숫자를 넣어 끝까지 계산한다
scause를 interrupt bit와 code로 분리한다
RV64에서 supervisor timer interrupt의 scause=0x8000000000000005, load page fault의 scause=0x000000000000000d를 비교한다.
- timer interruptbit63=1이므로 interrupt이고 code는 하위 63bit의 5다.
do_irq에서 timer irqchip 경로로 간다. - load page faultbit63=0이므로 synchronous exception이고 code 13으로 exception vector table을 찾는다.
- sepctimer에서는 중단된 instruction, page fault에서는 fault를 일으킨 load instruction 주소다.
- stvalpage fault에서는 접근한 virtual address가 유효하지만 interrupt에서는 원인 해석에 쓰지 않는다.
판정로그에는 interrupt/code, sepc, stval, sstatus.SPP를 함께 남겨 user/kernel 출처와 재개 지점을 동시에 판별한다.
SBI HSM에 넘기는 logical CPU와 hart ID를 구분한다
Linux logical CPU 2가 DT의 hart ID 7에 대응하고, secondary_start_sbi PA=0x80200100, boot data PA=0x81002000이라고 가정한다.
- mapping
cpuid_to_hartid_map(2)의 결과 7을 HSM hartid에 사용한다. - start addressMMU-off firmware가 사용할 수 있도록 entry는 virtual symbol이 아닌 physical
0x80200100이다. - opaque argumenta1에는 boot data physical address
0x81002000이 들어가며 그 안의 task/SP store가 먼저 visible해야 한다. - completionHSM call 성공과 Linux CPU online은 다르다. secondary의
smp_callin()completion까지 측정한다.
판정timeout 분석에서는 logical CPU, hart ID, HSM return, secondary a0/a1, completion timestamp를 같은 행에 기록한다.
DEEP DIVE
코드에서 놓치기 쉬운 경계
sscratch는 user TLS를 저장하는 register가 아니다
user mode에서는 tp가 TLS를 가리키므로 kernel은 sscratch에 current task pointer를 준비해 둔다. trap 첫 줄에서 둘을 교환하면 한 명령으로 kernel current를 얻고 user tp를 보존할 수 있다.
kernel 안에서 trap이 중첩될 때 sscratch를 0으로 두는 것은 이미 kernel tp와 stack을 쓰고 있음을 표시한다.
pt_regs는 복귀에 필요한 최소 architecture 상태의 소유자다
general registers 외에도 status, epc, bad address, cause가 frame에 들어간다. signal, ptrace, page fault, syscall은 이 frame을 읽거나 일부 값을 바꿀 수 있다.
handler가 frame pointer를 장기 보관하면 안 된다. trap return과 함께 stack frame 수명이 끝나기 때문이다.
new_vmalloc_check는 모든 page fault를 처리하지 않는다
RISC-V 구현이 invalid PTE를 cache했거나 access가 page-table update보다 먼저 관찰된 특수한 kernel vmalloc fault를 빠르게 재시도하는 경로다. 일반 user page fault는 exception table의 page-fault handler로 간다.
sfence.vma가 필요한 CPU와 단순 retry로 충분한 CPU를 feature 조건으로 나눈다는 점이 중요하다.
interrupt bit와 cause number를 분리해서 기록한다
scause를 10진수 한 값으로만 찍으면 최상위 interrupt bit 때문에 매우 큰 숫자로 보인다. is_interrupt = scause >> (XLEN-1), code = scause & ~(1UL << (XLEN-1))로 나눠야 한다.
sepc와 stval의 의미는 cause code마다 달라지므로 세 register를 한 묶음으로 남긴다.
SBI는 interrupt controller가 아니라 firmware 호출 경계다
SBI HSM은 hart start/stop을 제공하고 SBI IPI extension은 remote hart에 IPI를 요청할 수 있다. 실제 local interrupt pending과 irqchip dispatch는 Linux의 RISC-V interrupt architecture에서 별도로 처리된다.
OpenSBI log와 Linux IPI trace를 같은 timestamp 축에 놓으면 firmware request와 kernel handler 사이를 구분할 수 있다.
CPU online 게시 전 cache와 TLB 부채를 갚는다
offline CPU에는 remote flush가 생략될 수 있으므로 smp_callin()이 local I-cache와 TLB를 모두 비운 뒤 completion과 interrupt enable로 간다. 이 순서를 바꾸면 오래된 translation이나 instruction을 실행할 수 있다.
online bit, completion, IRQ enable은 비슷해 보여도 서로 다른 관찰자를 깨우는 세 경계다.
FAILURE PATH
멈춘 위치보다 먼저 볼 값
| 조건 | 관찰되는 증상 | 첫 확인 값 |
|---|---|---|
| sscratch 초기화 오류 | user trap에서 tp가 0이거나 user TLS를 current로 오해해 stack 전환이 깨진다. | stvec 설치와 sscratch 값 |
| pt_regs offset 불일치 | sepc/scause 또는 GPR 복원이 바뀌어 sret 직후 재 fault가 난다. | asm offset과 struct pt_regs |
| cause decoding 오류 | external interrupt를 exception handler로 보내거나 반대로 처리한다. | scause MSB와 code |
| SBI boot data 오류 | secondary hart가 0 stack 또는 잘못된 task pointer를 읽고 park된다. | a0/a1과 boot data fields |
| online 이전 flush 누락 | secondary에서만 stale instruction 또는 TLB fault가 재현된다. | local flush와 completion 순서 |
LAB
직접 확인할 실험
- 01trap entry breakpoint에서 sscratch, tp, sp, scause, sepc, stval을 user page fault와 timer interrupt 각각 기록한다.
명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 02
trace-cmd record -e irq -e ipi로 local IRQ와 IPI를 분리해 수집한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 03OpenSBI HSM debug log와 Linux
CPU%u: Booted secondary hartid로그의 hart ID를 대조한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 04CPU hotplug를 반복하면서
__cpu_up,secondary_start_sbi,smp_callin, completion의 timestamp를 기록한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
PRIMARY REFERENCES