Linux v6.18.37 · SBI v3.0의 HSM 규약 · RV64 S-mode, 비-XIP 예

RISC-V에서 보조 CPU가 시작할 때 받는 값

SBI 호출의 인자와 새 hart의 레지스터를 구분하고, Linux가 task와 stack을 준비하는 코드를 읽습니다.

호출한 CPU와 시작되는 CPU는 서로 다릅니다

hart는 독립적으로 명령을 실행하는 하드웨어 실행 단위입니다. Linux의 논리 CPU 번호와 hart ID가 반드시 같지는 않습니다. CPU 1을 온라인으로 만들더라도 SBI에 넘길 값은 매핑을 통해 구한 실제 hart ID입니다. OpenSBI는 SBI의 대표적인 구현이며, SBI는 운영체제와 실행 환경 사이의 호출 규약입니다.

시점a0a1a2
실행 중인 CPU가 HSM hart_start를 요청할 때대상 hart ID시작할 코드의 물리 주소새 hart에 넘길 opaque 값
새 hart가 요청한 주소에서 실행을 시작할 때새 hart 자신의 ID요청에 실었던 opaque 값HSM이 의미를 보장하지 않는 값
일반적인 Linux 첫 부팅 진입부팅 hart IDDTB의 물리 주소이 표에서는 사용하지 않습니다.

같은 이름의 a1이라도 어느 CPU의 어느 시점인지를 적어야 합니다. Linux의 보조 CPU 시작 경로에서 opaque는 DTB 주소가 아니라 sbi_hart_boot_data의 물리 주소입니다. SBI 호출 규약과 HSM Linux가 준비하는 boot_data RISC-V의 첫 부팅 조건

Linux가 코드 주소와 부팅 자료를 준비합니다

v6.18.37의 sbi_cpu_start()secondary_start_sbi의 물리 주소, 대상 hart ID, 대상 CPU의 idle task와 stack을 준비합니다. 아래는 구조를 풀어 쓴 의사 코드이며 실제 함수 전체를 복사한 것은 아닙니다.

target = logical_cpu_to_hart_id(cpu);
entry_pa = physical_address(secondary_start_sbi);
boot_data[cpu].task_ptr = idle_task;
boot_data[cpu].stack_ptr = initial_stack_pointer(idle_task);
publish_boot_data();
hart_start(target, entry_pa, physical_address(&boot_data[cpu]));
하는 일과 값의 의미
1논리 CPU 번호를 펌웨어가 식별할 hart ID로 바꿉니다. 배열 번호를 그대로 하드웨어 ID라고 가정하지 않습니다.
2새 hart가 처음 실행할 어셈블리 위치를 물리 주소로 나타냅니다. 새 hart는 satp=0 상태로 시작합니다.
3대상 CPU의 idle task를 가리키는 커널 포인터를 구조체에 저장합니다. task_struct 전체를 SBI 인자로 복사하는 것이 아닙니다.
4그 CPU가 사용할 초기 stack pointer 값을 저장합니다. stack 메모리 내용 자체와 주소 값은 다릅니다.
5새 CPU를 시작시키기 전에 준비한 메모리가 올바른 순서로 관찰되게 합니다. 실제 소스는 task 준비 전후의 smp_mb()를 포함합니다.
6세 값을 HSM 요청에 넣습니다. 요청 성공만으로 Linux의 CPU online 단계가 모두 끝난 것은 아닙니다.

실제 코드의 __pa_symbol(), __pa(), task_pt_regs()는 각 주소의 종류와 stack 배치를 반영합니다. 위의 영문 의사 함수들은 커널 API 이름이 아닙니다. sbi_cpu_start 구현

ecall 전후의 레지스터를 봅니다

CPU A의 요청에서 CPU B의 진입까지

1. CPU A가 인자를 준비합니다

레지스터내용
a0시작할 CPU B의 hart ID
a1secondary_start_sbi의 물리 주소
a2B를 위해 준비한 boot_data의 물리 주소
a6HSM의 hart_start 함수 번호 0
a7HSM 확장 번호 0x48534D

a0~a2의 기존 내용은 이 요청의 인자로 덮어씁니다. 아직 B의 레지스터를 CPU A가 직접 쓰는 동작은 아닙니다.

2. ecall로 실행 환경에 요청합니다

S-mode의 ecall이 SBI를 처리하는 실행 환경으로 제어를 넘깁니다. 일반적인 OpenSBI 구성에서는 M-mode 펌웨어가 처리합니다. 가상 머신에서는 hypervisor가 SBI 서비스를 제공할 수도 있습니다.

3. CPU A는 결과를 받습니다

a0는 SBI 오류 코드, a1은 반환값에 사용됩니다. hart_start가 성공을 반환해도 B의 Linux 초기화는 진행 중일 수 있습니다. 이 시점의 a1을 다시 시작 주소라고 읽으면 안 됩니다.

4. CPU B가 진입합니다

상태HSM이 정한 시작 값
PC요청한 start_addr
a0CPU B의 hart ID
a1opaque, 여기서는 boot_data의 물리 주소
satp0: 주소 변환 비활성
sstatus.SIE0: supervisor interrupt 비활성

규약이 보장하지 않은 다른 레지스터를 0으로 가정하지 않습니다. B는 자신의 stack과 trap 처리부터 준비해야 합니다.

단계 이동은 요청과 시작 과정입니다. CPU A와 B가 항상 이 그림의 한 단계씩 번갈아 실행한다는 뜻은 아닙니다. 두 CPU의 진행은 겹칠 수 있습니다.

boot_data 안의 값을 실제 레지스터로 읽습니다

아래 네 줄은 비-XIP RV64·CONFIG_MMU=y 구성에서 task 포인터를 읽는 부분을 이해하기 위한 축약 예입니다. 실제 소스의 REG_L은 레지스터 폭에 맞는 load로 전개됩니다. XIP_FIXUP_OFFSET 등의 설정별 처리는 원문에서 함께 확인해야 합니다.

li    a2, SBI_HART_BOOT_TASK_PTR_OFFSET
add   a2, a2, a1
REG_L tp, (a2)
/* stack_ptr도 같은 방식으로 읽어 sp에 넣습니다. */
명령실행 전실행 후
li a2, OFFSETa2는 여기서 사용하지 않을 값입니다.a2 = boot_data 시작부터 task_ptr 필드까지의 바이트 거리입니다. 구조체의 task 포인터 자체가 아닙니다.
add a2, a2, a1a1 = boot_data 물리 주소, a2 = 필드 offset입니다.a2 = task_ptr 필드가 저장된 메모리의 주소입니다. 아직 메모리를 읽지는 않았습니다.
REG_L tp, (a2)a2가 task_ptr 필드의 위치를 가리킵니다.그 위치에 저장된 포인터 값을 읽어 tp에 넣습니다. a1·a2와 해당 메모리의 내용은 이 load 때문에 바뀌지 않습니다.

task 포인터 값은 커널의 가상 주소일 수 있습니다. MMU를 켜기 전에도 그 숫자를 레지스터로 읽어 옮기는 일은 가능합니다. 그 주소로 task 내용에 접근하는 시점에는 올바른 주소 변환 상태가 필요합니다. 이후 head.S는 MMU·trap 경로를 준비하고 smp_callin()으로 넘어갑니다. secondary_start_sbi와 MMU 전환

hart_start·IPI·WFI를 구분합니다

동작용도같다고 보면 생기는 오류
HSM hart_start정지한 hart를 지정한 진입 주소에서 시작합니다.일반적인 reschedule IPI만 보내면 offline hart가 새 Linux stack으로 시작한다고 생각하게 됩니다.
IPI다른 hart에 인터럽트로 작업이나 상태 변화를 알립니다.수신 CPU의 interrupt·전원 상태를 고려하지 않고 모든 상태에서 실행을 강제한다고 생각하게 됩니다.
WFI인터럽트 등을 기다릴 수 있게 하는 명령입니다.WFI를 실행하면 반드시 전원이 차단되거나 CPU가 Linux offline 상태가 된다고 생각하게 됩니다.

시작 실패를 조사할 때는 요청의 오류 코드, hart ID, 시작 주소의 실행 가능 여부, boot_data 공개 순서, B의 첫 명령 도달 여부, Linux online 완료 여부를 나누어 봅니다. HSM 상태와 Linux의 online mask도 서로 다른 계층의 상태입니다.