CPU를 깨운다는 말부터 나눕니다
이 글은 Linux v6.18.37, TF-A LTS 2.14.3을 기준으로 ARM64의 보조 CPU 시작을 따라갑니다. 중심 경로는 PSCI 0.2 이상을 사용하는 비보안 Linux가 SMC로 EL3의 TF-A에 CPU_ON을 요청하는 구성입니다. CPU 번호, 물리 주소, 전원 제어 레지스터는 보드마다 달라집니다. 아래 기호와 예시 값은 실제 보드에서 측정한 값이 아닙니다.
| 상황 | 사용하는 경로 | 다시 실행하는 위치 |
|---|---|---|
| 부팅 중 보조 CPU 시작 / 꺼진 CPU를 hotplug로 재투입 | PSCI CPU_ON 또는 부팅 시 spin-table | Linux secondary_entry, 또는 holding pen을 거친 secondary_startup |
| 이미 online인 CPU가 얕은 idle 상태에서 대기 | WFI와 인터럽트 등 해당 상태의 복귀 조건 | 보통 대기 명령 뒤의 기존 실행 흐름 |
| CPU 상태를 잃는 깊은 절전에서 복귀 | 상태 저장 → PSCI CPU_SUSPEND → 펌웨어의 복귀 처리 | cpu_resume 등 저장 상태를 복구하는 경로 |
| 잠든 태스크를 실행 가능하게 변경 | 스케줄러의 wakeup과 필요시 다른 CPU로 보내는 IPI | 대상 CPU의 스케줄링 경로; 매번 CPU_ON을 호출하지 않습니다. |
PSCI는 전원 관리 요청의 규격입니다. SMCCC는 SMC/HVC 호출에서 레지스터를 사용하는 규칙입니다. TF-A는 이 규격을 구현하는 펌웨어 중 하나입니다. 서로 같은 이름의 기능이 아닙니다.
Device Tree에서 CPU 시작 방식을 고릅니다
Device Tree를 쓰는 구성에서는 CPU 노드의 enable-method = "psci"가 커널의 cpu_psci_ops를 선택하게 합니다. 별도의 PSCI 노드에 있는 method = "smc"는 실제 호출 명령을 정합니다. enable-method와 method는 서로 다른 선택입니다. ACPI 구성은 FADT의 PSCI 정보 등을 사용하므로 아래 DT 예시를 그대로 적용하지 않습니다.
/* 속성의 관계만 보이는 예시입니다. 완전한 보드 DTS가 아닙니다. */
cpu@100 {
device_type = "cpu";
reg = <0x0 0x100>; /* 부모의 #address-cells = <2>인 예시 */
enable-method = "psci";
};
psci {
compatible = "arm,psci-1.0";
method = "smc";
};| 이름 | 예시 / 실제 확인할 값 | 의미 |
|---|---|---|
| Linux 논리 CPU 번호 | cpu = 1 | 배열과 CPU mask 등에 쓰는 커널 내부 번호입니다. |
cpu_logical_map(1) | 예시 0x100 | 대상 CPU의 MPIDR affinity 값입니다. 논리 번호 1과 같다고 가정하지 않습니다. |
__pa_symbol(secondary_entry) | 기호 P_entry | 해당 빌드와 적재 위치에서 결정되는 커널 보조 CPU 진입점의 물리 주소입니다. |
이 표의 대응 관계는 번호를 주소로 변환한다는 뜻이 아닙니다. CPU 식별자와 코드 주소는 서로 다른 인자로 전달됩니다.
커널은 PSCI 버전을 조회하고 지원하는 호출을 준비합니다. method = "hvc"라면 요청은 EL2의 하이퍼바이저 쪽으로 전달될 수 있으며, 하이퍼바이저가 PSCI를 구현하거나 중계합니다. HVC 자체가 EL3의 TF-A로 직접 들어가는 명령은 아닙니다. CPU 시작 방식 선택 · PSCI 버전과 conduit 선택
실행 중인 CPU가 새 CPU의 준비를 합니다
여기서는 요청하는 CPU를 A, 시작할 CPU를 B라고 부르겠습니다. A가 B의 레지스터 파일에 직접 값을 쓰는 것은 아닙니다. A는 커널의 공유 데이터와 펌웨어 호출 인자를 준비합니다. B는 자신의 진입 코드에서 필요한 상태를 새로 구성합니다.
v6.18.37 · arch/arm64/kernel/smp.c · 120–138행
secondary_data.task = idle;
update_cpu_boot_status(CPU_MMU_OFF);
/* Now bring the CPU into our world */
ret = boot_secondary(cpu, idle);
if (ret) {
if (ret != -EPERM)
pr_err("CPU%u: failed to boot: %d\n", cpu, ret);
return ret;
}
/*
* CPU was successfully started, wait for it to come online or
* time out.
*/
wait_for_completion_timeout(&cpu_running,
msecs_to_jiffies(5000));
if (cpu_online(cpu))
return 0;| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
secondary_data.task = idle; | B가 처음 사용할 idle 태스크의 task_struct *를 공유 변수에 저장합니다. A의 현재 태스크를 복사하는 코드가 아닙니다. |
update_cpu_boot_status(CPU_MMU_OFF) | 시작 결과를 추적할 상태값을 준비합니다. 이 이름의 상수를 기록한다고 B의 MMU가 직접 꺼지는 것은 아닙니다. |
boot_secondary(cpu, idle) | 선택된 cpu_operations.cpu_boot를 호출합니다. PSCI 구성이라면 다음의 cpu_psci_cpu_boot로 연결됩니다. |
if (ret) | 시작 요청 단계에서 실패하면 바로 반환합니다. |
wait_for_completion_timeout(...5000) | 요청 성공 뒤에도 B가 준비 완료를 알릴 때까지 최대 5초 기다립니다. 5초는 이 커널 구현의 대기 시간이지 PSCI 규격의 CPU 기동 보장 시간이 아닙니다. |
cpu_online(cpu) | 대기 함수의 반환만 보지 않고 B가 실제로 online으로 표시됐는지 확인합니다. |
v6.18.37 · arch/arm64/kernel/psci.c · 38–46행
static int cpu_psci_cpu_boot(unsigned int cpu)
{
phys_addr_t pa_secondary_entry = __pa_symbol(secondary_entry);
int err = psci_ops.cpu_on(cpu_logical_map(cpu), pa_secondary_entry);
if (err && err != -EPERM)
pr_err("failed to boot CPU%d (%d)\n", cpu, err);
return err;| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
__pa_symbol(secondary_entry) | 커널 심볼의 가상 주소 표현에서 펌웨어에 넘길 물리 진입 주소를 얻습니다. 임의의 포인터에 무조건 적용할 수 있는 변환은 아닙니다. |
cpu_logical_map(cpu) | 논리 CPU 번호를 대상의 하드웨어 affinity 식별자로 바꿉니다. |
psci_ops.cpu_on(...) | CPU 식별자와 진입 주소를 전달합니다. 이 함수 포인터는 PSCI 초기화 때 연결됐습니다. |
err && err != -EPERM | 일부 실패는 이 위치에서 로그를 생략하지만 반환값 자체는 호출자에게 전달합니다. -EPERM이 성공이라는 뜻은 아닙니다. |
SMC 직전과 새 CPU 진입 때의 레지스터
v6.18.37 · drivers/firmware/psci/psci.c · 217–233행
static int __psci_cpu_on(u32 fn, unsigned long cpuid, unsigned long entry_point)
{
int err;
err = invoke_psci_fn(fn, cpuid, entry_point, 0);
return psci_to_linux_errno(err);
}
static int psci_0_1_cpu_on(unsigned long cpuid, unsigned long entry_point)
{
return __psci_cpu_on(psci_0_1_function_ids.cpu_on, cpuid, entry_point);
}
static int psci_0_2_cpu_on(unsigned long cpuid, unsigned long entry_point)
{
return __psci_cpu_on(PSCI_FN_NATIVE(0_2, CPU_ON), cpuid, entry_point);
}| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
invoke_psci_fn(fn, cpuid, entry_point, 0) | 마지막 0은 context_id입니다. 이 Linux 경로는 idle 태스크 주소를 x3로 넘기지 않습니다. |
PSCI_FN_NATIVE(0_2, CPU_ON) | ARM64에서는 64비트 CPU_ON 함수 ID인 0xC4000003을 선택합니다. 함수 이름의 0_2는 함수 ID를 정의한 규격 계열이며 실제 펌웨어가 반드시 PSCI 0.2라는 뜻은 아닙니다. |
psci_to_linux_errno(err) | 펌웨어의 PSCI 반환값을 Linux 오류 번호로 변환합니다. 서로 같은 번호 체계가 아닙니다. |
| 위치 | 값 | 그 값의 의미와 출처 |
|---|---|---|
| A: x0 | 0xC4000003 | PSCI의 64비트 CPU_ON 서비스 번호 |
| A: x1 | M_target = cpu_logical_map(cpu) | 깨울 CPU B의 affinity |
| A: x2 | P_entry = __pa_symbol(secondary_entry) | B가 나중에 비보안 영역에서 실행할 물리 코드 주소 |
| A: x3 | 0 | 이 Linux 구현이 선택한 context_id |
| A: SMC 복귀 뒤 x0 | PSCI 반환 코드 | 0은 요청 성공입니다. B의 Linux online 완료를 뜻하지 않습니다. |
| B: EL3에서 커널로 나올 때 PC | P_entry | TF-A가 보관한 entrypoint를 EL3 복귀 상태에 반영합니다. |
| B: 커널 진입 때 x0 | context_id = 0 | A의 x0에 있던 서비스 번호를 그대로 받는 것이 아닙니다. |
A의 x2 → B의 PC는 펌웨어가 진입 정보를 저장한 뒤 새 CPU의 복귀 상태를 구성한다는 의미입니다. A와 B가 같은 레지스터를 공유한다는 뜻이 아닙니다.
SMC의 #0은 CPU 번호나 PSCI 서비스 번호가 아닙니다. 서비스 선택에 쓰는 값은 x0에 있습니다. arm_smccc_smc의 반환 결과는 struct arm_smccc_res에 기록되고 PSCI 코드는 그중 a0를 읽습니다.
v6.18.37 · arch/arm64/kernel/smccc-call.S · 11–15행
.macro SMCCC instr
\instr #0
ldr x4, [sp]
stp x0, x1, [x4, #ARM_SMCCC_RES_X0_OFFS]
stp x2, x3, [x4, #ARM_SMCCC_RES_X2_OFFS]| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
.macro SMCCC instr | 어셈블러가 instr 자리에 smc 또는 hvc를 넣어 코드를 만듭니다. CPU가 이 선언을 실행하는 것은 아닙니다. |
\instr #0 | 선택한 예외 호출 명령을 실행합니다. 처리 후 돌아오면 다음 명령을 이어 실행합니다. |
ldr x4, [sp] | 호출 규약에 따라 스택으로 전달된 결과 구조체 포인터를 읽습니다. |
stp x0, x1 ... / stp x2, x3 ... | 돌아온 네 레지스터 값을 결과 구조체에 두 개씩 저장합니다. 대상 CPU B의 레지스터를 저장하는 것이 아닙니다. |
레지스터와 메모리 변화를 단계별로 봅니다
기호 P_entry는 커널 진입 물리 주소, M_target은 대상 affinity, T_idle은 idle 태스크 포인터입니다. 숫자를 만들어 넣지 않고 값의 출처를 표시했습니다.
호출 직전: CPU A
| 위치 | 내용 | 의미 |
|---|---|---|
| A의 x0 | 0xC4000003 | 64비트 PSCI CPU_ON 서비스 번호 |
| A의 x1 | M_target | cpu_logical_map(cpu)로 얻은 CPU B의 affinity |
| A의 x2 | P_entry | secondary_entry의 물리 주소 |
| A의 x3 | 0 | 이 Linux 경로의 context_id |
| 공유 메모리 | secondary_data.task = T_idle | B가 사용할 idle 태스크 포인터 |
A → TF-A: SMC 호출
A의 인자는 호출 규약으로 정해집니다. B의 레지스터에 이 값들이 모두 복사되는 것은 아닙니다.
TF-A가 CPU B의 시작 정보를 보관합니다
| 위치 | 내용 | 의미 |
|---|---|---|
| B용 ep->pc | P_entry | 나중에 비보안 영역에서 실행할 주소 |
| B용 ep->args.arg0 | 0 | A가 x3로 전달한 context_id |
| B의 affinity 상태 | ON_PENDING | 전원 켜기 요청이 진행 중 |
| 플랫폼 | pwr_domain_on(M_target) | 대상 CPU 전원 제어 요청 |
TF-A → 플랫폼: 전원 켜기 요청
이 상태 그림은 성공 경로입니다. 플랫폼 요청이 실패하면 affinity 상태를 OFF로 되돌립니다.
CPU A로 반환 / CPU B의 warm boot
| 위치 | 내용 | 의미 |
|---|---|---|
| A의 반환 x0 | PSCI_SUCCESS = 0 | 요청 성공; Linux online 완료와 다릅니다. |
| A의 다음 동작 | cpu_running 완료 대기 | B가 커널 준비를 마칠 때까지 기다립니다. |
| B의 실행 위치 | TF-A warm boot 경로 | CPU별 설정과 필요시 OP-TEE 초기화를 수행합니다. |
두 CPU가 별도로 진행합니다
이 단계의 좌우 순서는 전체적인 관계를 나타냅니다. A의 반환과 B의 warm boot 중 실제로 어느 쪽이 먼저 진행되는지는 고정하지 않습니다.
CPU B가 Linux에 들어옵니다
| 위치 | 내용 | 의미 |
|---|---|---|
| B의 PC | P_entry = secondary_entry | 펌웨어가 준비한 비보안 진입 주소 |
| B의 진입 x0 | context_id = 0 | 서비스 번호가 아닙니다. |
| mov x0, xzr 뒤 | x0 = 0 | x0를 덮어씁니다. |
| init_kernel_el 반환 뒤 | x0 = CPU boot mode | 진입 EL을 나타내는 값으로 역할이 바뀝니다. |
TF-A → CPU B의 커널: EL3에서 비보안 실행으로 전환
PC로 이동한 값의 출처는 A가 넘긴 P_entry입니다. 이것을 “이전 x2”라고만 쓰면 어느 CPU와 시점인지 알 수 없습니다.
CPU B의 스택과 current를 준비합니다
| 위치 | 내용 | 의미 |
|---|---|---|
| B의 x2 | T_idle | MMU 전환 뒤 secondary_data.task에서 읽었습니다. |
| B의 SP_EL0 | T_idle | current를 찾는 태스크 포인터 |
| B의 실제 SP | stack_base + THREAD_SIZE - PT_REGS_SIZE | idle 태스크에 할당된 커널 스택 |
| CPU별 접근 기준 | __per_cpu_offset[task_cpu] | 이 CPU의 per-CPU 데이터 위치 |
공유 메모리 → CPU B: 태스크와 스택 정보 읽기
여기서 x2는 태스크 포인터입니다. SMC 직전 A의 x2에 있던 P_entry와 다릅니다.
CPU B가 online으로 표시됩니다
| 위치 | 내용 | 의미 |
|---|---|---|
| B의 시작 상태 | CPU_BOOT_SUCCESS | 커널 초기 시작 성공 |
| Linux CPU 집합 | cpu_online(B) = true | 커널이 B를 online으로 표시 |
| 완료 통지 | complete(&cpu_running) | A의 대기가 끝나도록 알림 |
| A의 확인 | cpu_online(B) 검사 | CPU_ON 반환만으로 성공을 결론 내리지 않음 |
CPU B → CPU A: 커널 완료 통지
이 완료 통지는 PSCI의 반환값과 다른 커널 내부 동기화입니다.
재생은 설명용 단계 표시이며 실행 시간이나 모든 레지스터를 시뮬레이션하지 않습니다. 화살표 옆 문구가 호출·전원 요청·데이터 읽기·완료 통지를 구분합니다.
TF-A가 요청을 받아 전원 제어를 시작합니다
lts-v2.14.3 · lib/psci/psci_main.c · 25–50행
int psci_cpu_on(u_register_t target_cpu,
uintptr_t entrypoint,
u_register_t context_id)
{
int rc;
entry_point_info_t *ep = NULL;
unsigned int target_idx = (unsigned int)plat_core_pos_by_mpidr(target_cpu);
/* Validate the target CPU */
if (!is_valid_mpidr(target_cpu)) {
return PSCI_E_INVALID_PARAMS;
}
ep = get_cpu_data_by_index(target_idx, warmboot_ep_info);
/* Validate the lower EL entry point and put it in the entry_point_info */
rc = psci_validate_entry_point(ep, entrypoint, context_id);
if (rc != PSCI_E_SUCCESS) {
return rc;
}
/*
* To turn this cpu on, specify which power
* levels need to be turned on
*/
return psci_cpu_on_start(target_cpu, ep);| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
target_cpu, entrypoint, context_id | SMC handler에서 전달받은 대상 CPU, 커널 진입 주소, 시작 인자입니다. |
is_valid_mpidr | 플랫폼이 지원하는 CPU인지 검사합니다. 잘못된 식별자면 PSCI_E_INVALID_PARAMS를 반환합니다. |
warmboot_ep_info | 해당 CPU가 나중에 사용할 진입 정보 저장 공간을 찾습니다. |
psci_validate_entry_point | 플랫폼의 비보안 진입 주소 검사와 실행 상태 구성을 수행합니다. 유효한 주소인지는 커널의 함수 이름이 아니라 전달된 주소와 플랫폼 정책으로 검사합니다. |
psci_cpu_on_start | 대상 CPU의 상태 검사와 전원 켜기 요청으로 넘어갑니다. |
AArch64용 psci_get_ns_ep_info는 ep->pc = entrypoint, ep->args.arg0 = context_id를 기록합니다. 이때의 진입 정보 구조체와 나중에 B에서 만드는 CPU context를 구별해야 합니다. 이 버전은 B의 warm boot 경로에서 cm_init_my_context(ep)를 호출합니다. 진입 정보 저장과 warm boot 처리
- 대상 CPU의 잠금 획득
psci_spin_lock_cpu(target_idx)로 같은 CPU에 대한 시작 처리를 직렬화합니다.같은 대상을 동시에 시작하지 않도록 보호
- 현재 상태 검사
OFF가 아니면 ALREADY_ON 또는 ON_PENDING 오류가 날 수 있습니다.
시작할 수 있으면 상태 갱신
- ON_PENDING 기록과 cache 정리
새 CPU가 cache가 꺼진 상태에서도 필요한 상태값을 관찰할 수 있게 합니다.
플랫폼 전원 제어 함수 호출
- pwr_domain_on(target_cpu)
보드의 전원 제어 구현에 요청합니다. 실패하면 affinity 상태를 OFF로 되돌립니다.
요청한 CPU A로 성공 또는 실패 반환
- CPU A는 커널로 복귀
성공했어도 B의 커널 초기화가 끝났는지는 따로 기다립니다.
아래 화살표는 CPU A에서 진행하는 처리 순서입니다. B가 실행을 시작한 뒤에는 두 CPU가 별도로 진행하므로 이 그림만으로 둘의 세부 실행 시각을 비교할 수 없습니다.
TF-A의 affinity 상태 OFF / ON_PENDING / ON와 전원 도메인의 local state RUN / RETENTION / OFF는 구분합니다. “SUSPEND”를 affinity 상태의 네 번째 값처럼 섞어 쓰면 안 됩니다. 잠금·상태 변경·실패 복구 원문
lts-v2.14.3 · plat/arm/board/fvp/fvp_pm.c · 150–165행
static int fvp_pwr_domain_on(u_register_t mpidr)
{
int rc = PSCI_E_SUCCESS;
unsigned int psysr;
/*
* Ensure that we do not cancel an inflight power off request for the
* target cpu. That would leave it in a zombie wfi. Wait for it to power
* off and then program the power controller to turn that CPU on.
*/
do {
psysr = fvp_pwrc_read_psysr(mpidr);
} while ((psysr & PSYSR_AFF_L0) != 0U);
fvp_pwrc_write_pponr(mpidr);
return rc;| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
fvp_pwrc_read_psysr(mpidr) | 공개된 Arm FVP 구현의 전원 상태를 읽습니다. 진행 중인 power-off를 취소해 버리지 않도록 기다립니다. |
fvp_pwrc_write_pponr(mpidr) | FVP 전원 컨트롤러에 해당 CPU의 전원 켜기를 요청합니다. |
return rc | 전원 요청 결과를 반환합니다. 여기서 Linux의 online 상태를 확인하지는 않습니다. |
이 코드는 FVP의 예시입니다. 모든 SoC가 같은 레지스터를 쓰거나 “reset → clock → power”처럼 동일한 순서로 동작하지는 않습니다. 다른 보드에서는 그 보드의 plat_psci_ops_t.pwr_domain_on 구현과 공개 전원 제어 문서를 확인해야 합니다.
CPU A의 반환과 CPU B의 실행은 서로 다른 흐름입니다
| 시점 | CPU A: 이미 실행 중 | CPU B: 새로 시작 |
|---|---|---|
| 요청 준비 | idle 태스크 주소와 진입 주소를 준비합니다. | 아직 Linux 코드를 실행하지 않습니다. |
| 전원 요청 | SMC로 TF-A에 진입해 pwr_domain_on을 요청합니다. | 플랫폼이 정한 reset/warm boot 경로에서 실행을 시작합니다. |
| 요청 이후 | SMC에서 돌아와 cpu_running 완료를 기다립니다. | BL31의 warm boot 처리를 거쳐 커널 secondary_entry로 들어갑니다. |
| 커널 준비 완료 | 완료 통지를 받고 cpu_online(B)를 확인합니다. | set_cpu_online, complete를 실행합니다. |
같은 행은 두 흐름의 관계를 묶은 것입니다. 행 안의 좌우 동작이 같은 순간에 일어난다는 뜻은 아닙니다. 요청 반환과 B의 실행 시작 중 어느 것이 먼저 관찰되는지는 실행 환경에 따라 달라질 수 있습니다.
B는 보통 A가 호출한 C 함수에서 “반환”하며 등장하지 않습니다. TF-A의 warm boot 경로가 B의 EL3 실행 환경을 준비한 뒤 비보안 진입 주소로 나갑니다. U-Boot의 전체 부팅 절차나 Linux의 start_kernel()을 B마다 처음부터 반복하는 것도 아닙니다.
lts-v2.14.3 · lib/psci/psci_common.c · 1058–1085행
if (psci_get_aff_info_state() == AFF_STATE_ON_PENDING) {
psci_cpu_on_finish(cpu_idx, &state_info);
} else {
unsigned int max_off_lvl = psci_find_max_off_lvl(&state_info);
assert(max_off_lvl != PSCI_INVALID_PWR_LVL);
psci_cpu_suspend_to_powerdown_finish(cpu_idx, max_off_lvl, &state_info, false);
}
/*
* Caches and (importantly) coherency are on so we can rely on seeing
* whatever the primary gave us without explicit cache maintenance
*/
entry_point_info_t *ep = get_cpu_data(warmboot_ep_info);
cm_init_my_context(ep);
/*
* Generic management: Now we just need to retrieve the
* information that we had stashed away during the cpu_on
* call to set this cpu on its way.
*/
cm_prepare_el3_exit_ns();
/*
* Set the requested and target state of this CPU and all the higher
* power domains which are ancestors of this CPU to run.
*/
psci_set_pwr_domains_to_run(cpu_idx, end_pwrlvl);| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
AFF_STATE_ON_PENDING 검사 | CPU_ON으로 새로 켜진 경우와 suspend에서 복귀한 경우를 구분합니다. |
psci_cpu_on_finish | 플랫폼의 마무리, coherency·아키텍처 상태 준비, 필요한 Secure Payload 콜백을 수행합니다. |
get_cpu_data(warmboot_ep_info) | A가 준비한 B의 비보안 진입 정보를 B가 읽습니다. |
cm_init_my_context(ep) | B에서 사용할 CPU context를 진입 정보로 초기화합니다. |
cm_prepare_el3_exit_ns() | 비보안 영역으로 나갈 레지스터와 실행 상태를 준비합니다. 실제 최종 전환은 EL3 exit 어셈블리 경로에서 이루어집니다. |
psci_set_pwr_domains_to_run | 펌웨어의 전원 도메인 상태를 갱신합니다. Linux의 cpu_online_mask를 쓰는 함수가 아닙니다. |
새 CPU가 head.S에서 C 코드로 들어갑니다
v6.18.37 · arch/arm64/kernel/head.S · 355–378행
SYM_FUNC_START(secondary_entry)
mov x0, xzr
bl init_kernel_el // w0=cpu_boot_mode
b secondary_startup
SYM_FUNC_END(secondary_entry)
SYM_FUNC_START_LOCAL(secondary_startup)
/*
* Common entry point for secondary CPUs.
*/
mov x20, x0 // preserve boot mode
#ifdef CONFIG_ARM64_VA_BITS_52
alternative_if ARM64_HAS_VA52
bl __cpu_secondary_check52bitva
alternative_else_nop_endif
#endif
bl __cpu_setup // initialise processor
adrp x1, swapper_pg_dir
adrp x2, idmap_pg_dir
bl __enable_mmu
ldr x8, =__secondary_switched
br x8| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
SYM_FUNC_START(secondary_entry) | 커널의 함수 심볼과 관련 메타데이터를 만드는 매크로입니다. 이 줄 자체가 CPU 명령은 아닙니다. |
mov x0, xzr | B의 x0를 0으로 덮어씁니다. xzr은 읽으면 0이 나오는 zero register입니다. 메모리를 지우거나 A의 x0를 바꾸지 않습니다. |
bl init_kernel_el | 현재 진입 EL에 맞는 초기 상태를 준비합니다. bl은 돌아올 주소를 x30에 기록합니다. 반환값은 부팅 출발 EL을 나타냅니다. |
b secondary_startup | 공통 보조 CPU 초기화로 이동합니다. b는 이 분기를 위한 새 복귀 주소를 만들지 않습니다. |
mov x20, x0 | 초기화 함수가 반환한 부팅 모드를 보존합니다. 이 시점의 x0는 PSCI 서비스 번호가 아닙니다. |
CONFIG_ARM64_VA_BITS_52와 alternative_if | 52비트 가상 주소 구성 및 실제 CPU 기능에 따른 검사입니다. 전처리 조건과 부팅 중 대체 명령 패치를 각각 구별해야 합니다. |
bl __cpu_setup | 주소 변환과 cache 제어에 필요한 CPU 설정을 준비합니다. |
adrp x1, swapper_pg_dir / adrp x2, idmap_pg_dir | 이 초기 경로에서 커널 매핑과 identity mapping의 페이지 테이블 주소를 준비합니다. 값은 빌드·적재 위치에 따라 결정됩니다. |
bl __enable_mmu | 지원 페이지 크기를 확인하고 TTBR 및 SCTLR 설정으로 MMU를 켭니다. |
ldr x8, =__secondary_switched / br x8 | 가상 주소로 실행할 다음 위치를 읽고 그 주소로 분기합니다. =심볼 표기는 어셈블러의 주소 적재 의사 명령입니다. |
v6.18.37 · arch/arm64/kernel/head.S · 394–404행
adr_l x0, secondary_data
ldr x2, [x0, #CPU_BOOT_TASK]
cbz x2, __secondary_too_slow
init_cpu_task x2, x1, x3
#ifdef CONFIG_ARM64_PTR_AUTH
ptrauth_keys_init_cpu x2, x3, x4, x5
#endif
bl secondary_start_kernel| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
adr_l x0, secondary_data | 공유 구조체의 주소를 x0에 만듭니다. adr_l은 커널의 어셈블리 매크로입니다. |
ldr x2, [x0, #CPU_BOOT_TASK] | 구조체의 task 필드에서 A가 저장한 B의 idle 태스크 포인터를 읽습니다. CPU_BOOT_TASK는 빌드에서 C 구조체 배치로 산출한 오프셋입니다. |
cbz x2, __secondary_too_slow | 포인터가 0이면 준비가 해제된 상태로 보고 진행하지 않습니다. 예를 들어 A가 시간 초과 뒤 task를 NULL로 바꾼 경우입니다. |
init_cpu_task x2, x1, x3 | x2의 태스크를 current로 찾도록 SP_EL0에 기록하고, 그 태스크의 커널 스택 및 per-CPU offset을 설정합니다. x1과 x3은 임시 레지스터입니다. |
CONFIG_ARM64_PTR_AUTH | 포인터 인증을 사용하는 빌드에서는 CPU의 인증 키 초기화 코드가 포함됩니다. CPU 기동 자체를 선택하는 조건은 아닙니다. |
bl secondary_start_kernel | 스택 등 C 실행에 필요한 준비를 마치고 B의 커널 초기화 함수로 들어갑니다. |
| 위치 | 이 시점의 내용 | 다음에 사용하는 곳 |
|---|---|---|
| B의 x2 | T_idle = secondary_data.task | idle 태스크의 task_struct를 가리킵니다. PSCI 호출 당시 A의 x2와는 다른 값입니다. |
[T_idle + TSK_STACK] | idle 태스크에 할당한 커널 스택의 시작 주소 | init_cpu_task가 스택 상단을 계산합니다. |
| 실제 SP | stack_base + THREAD_SIZE - PT_REGS_SIZE | C 함수의 스택으로 사용합니다. 호출 시 프레임을 추가하면 일반적으로 낮은 주소 쪽을 사용합니다. |
| SP_EL0 | T_idle | 이 커널 경로에서 current 태스크를 찾는 값입니다. 실제 실행 중인 커널 SP와 같은 값이 아닙니다. |
| per-CPU offset | __per_cpu_offset[task_cpu] | B의 CPU별 데이터에 접근할 때 사용합니다. |
기호는 출처가 있는 실행 시 값입니다. 주소 숫자를 모른다고 “이전 x2”로만 표시하지 않습니다. 실제 값은 해당 커널의 태스크·스택 할당과 적재 위치를 관찰해야 알 수 있습니다.
언제 Linux가 CPU를 사용할 수 있다고 판단하나요?
v6.18.37 · arch/arm64/kernel/smp.c · 235–257행
cpuinfo_store_cpu();
store_cpu_topology(cpu);
/*
* Enable GIC and timers.
*/
notify_cpu_starting(cpu);
ipi_setup(cpu);
numa_add_cpu(cpu);
/*
* OK, now it's safe to let the boot CPU continue. Wait for
* the CPU migration code to notice that the CPU is online
* before we continue.
*/
pr_info("CPU%u: Booted secondary processor 0x%010lx [0x%08x]\n",
cpu, (unsigned long)mpidr,
read_cpuid_id());
update_cpu_boot_status(CPU_BOOT_SUCCESS);
set_cpu_online(cpu, true);
complete(&cpu_running);| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
cpuinfo_store_cpu / store_cpu_topology | B의 CPU 정보와 topology를 기록합니다. |
notify_cpu_starting(cpu) | CPU 시작 단계에 등록된 콜백을 실행합니다. 인터럽트와 타이머 등 CPU별 준비가 이 경로에 연결됩니다. |
ipi_setup(cpu) | CPU 간 인터럽트 처리에 필요한 준비를 합니다. |
numa_add_cpu(cpu) | NUMA의 CPU 정보에 반영합니다. |
update_cpu_boot_status(CPU_BOOT_SUCCESS) | 초기 CPU 부팅 상태 기록을 성공으로 바꿉니다. |
set_cpu_online(cpu, true) | Linux의 online CPU 집합에 B를 포함합니다. |
complete(&cpu_running) | A가 기다리던 완료를 통지합니다. PSCI의 SUCCESS 반환과는 별도의 커널 동기화입니다. |
그 뒤 B는 인터럽트 마스크를 정상 커널 실행에 맞게 복구하고 cpu_startup_entry(CPUHP_AP_ONLINE_IDLE)로 들어갑니다. 이후 CPU hotplug 단계와 스케줄러 준비가 이어집니다. “펌웨어가 CPU를 켰다”, “커널 진입점에 도달했다”, “Linux가 online으로 표시했다”는 서로 다른 관측 지점입니다.
OP-TEE도 새 CPU의 시작에 참여할 수 있습니다
TF-A를 전통적인 opteed dispatcher와 OP-TEE로 구성했다면 PSCI의 CPU 시작 마무리에서 Secure Payload의 svc_on_finish 콜백이 호출될 수 있습니다. OP-TEE가 CPU별 보안 상태를 준비한 뒤 TF-A로 돌아오고, 이어서 비보안 Linux 진입이 준비됩니다. Linux가 CPU마다 일반 TA 세션을 열어 이 초기화를 수행하는 것은 아닙니다.
- TF-A psci_cpu_on_finish
등록된
svc_on_finish콜백을 확인합니다.콜백 호출
- opteed_cpu_on_finish_handler
B의 Secure context를 만들고 OP-TEE의
cpu_on_entry를 선택합니다.Secure World로 실행 전환
- OP-TEE CPU별 초기화
새 CPU의 보안 실행 상태를 준비합니다.
처리 결과를 TF-A에 반환
- TF-A의 PSCI warm boot 처리 계속
비보안 진입 context를 준비한 뒤 Linux로 나갑니다.
화살표마다 표시한 호출·전환·반환은 모두 CPU B에서 일어납니다. FF-A/SPMC 구성은 dispatcher와 진입 경로가 다르므로 이 opteed 도식을 그대로 적용하지 않습니다.
lts-v2.14.3 · services/spd/opteed/opteed_pm.c · 102–131행
******************************************************************************/
void opteed_cpu_on_finish_handler(u_register_t unused)
{
int32_t rc = 0;
uint32_t linear_id = plat_my_core_pos();
optee_context_t *optee_ctx = &opteed_sp_context[linear_id];
entry_point_info_t optee_on_entrypoint;
assert(optee_vector_table);
assert(get_optee_pstate(optee_ctx->state) == OPTEE_PSTATE_OFF ||
get_optee_pstate(optee_ctx->state) == OPTEE_PSTATE_UNKNOWN);
opteed_init_optee_ep_state(&optee_on_entrypoint, opteed_rw,
(uint64_t)&optee_vector_table->cpu_on_entry,
0, 0, 0, 0, optee_ctx);
/* Initialise this cpu's secure context */
cm_init_my_context(&optee_on_entrypoint);
/* Enter OPTEE */
rc = opteed_synchronous_sp_entry(optee_ctx);
/*
* Read the response from OPTEE. A non-zero return means that
* something went wrong while communicating with OPTEE.
*/
if (rc != 0)
panic();
/* Update its context to reflect the state OPTEE is in */| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
plat_my_core_pos() | 현재 실행 중인 B에 해당하는 펌웨어의 CPU별 인덱스를 구합니다. Linux 논리 CPU 번호와 같다고 가정하지 않습니다. |
optee_vector_table->cpu_on_entry | OP-TEE가 제공한 CPU 시작 처리 위치를 선택합니다. |
cm_init_my_context(&optee_on_entrypoint) | 이때는 OP-TEE를 실행하기 위한 Secure context를 준비합니다. 뒤의 Linux용 Non-secure context와 구분합니다. |
opteed_synchronous_sp_entry(optee_ctx) | OP-TEE에 들어갔다가 처리가 끝나면 돌아옵니다. |
if (rc != 0) panic() | 보안 초기화가 실패하면 정상적으로 Linux 진입을 계속하지 못합니다. |
spin-table에는 대기 장소가 두 군데 있습니다
enable-method = "spin-table"은 PSCI CPU_ON과 다른 보조 CPU 해제 방식입니다. 이것만으로 “보안 펌웨어가 없다”거나 “Linux가 모든 CPU 전원을 직접 관리한다”고 결론 내릴 수 없습니다. 펌웨어가 CPU를 실행 가능한 대기 상태에 놓아 두었다는 준비가 필요합니다.
- 1. 펌웨어가 cpu-release-addr를 기다림
DT에 적힌 주소는 실행할 코드 자체가 아니라, 시작 주소를 기록할 64비트 메모리 칸의 물리 주소입니다.
커널이 진입 주소 기록 + cache 정리 + SEV
- 2. Linux secondary_holding_pen에서 대기
각 CPU가 자신의 MPIDR와
secondary_holding_pen_release값을 비교합니다.대상 MPIDR 기록 + cache 정리 + SEV
- 3. 일치한 CPU만 secondary_startup으로 이동
다른 CPU가 이벤트를 받아도 MPIDR가 다르면 다시 기다립니다.
첫 번째 화살표는 펌웨어 대기 루프를 해제하는 과정이고, 두 번째는 커널 내부에서 대상 CPU를 고르는 과정입니다. SEV는 다시 검사하도록 알리는 이벤트이며 주소나 MPIDR 값을 운반하지 않습니다.
v6.18.37 · arch/arm64/kernel/smp_spin_table.c · 92–104행
writeq_relaxed(pa_holding_pen, release_addr);
dcache_clean_inval_poc((__force unsigned long)release_addr,
(__force unsigned long)release_addr +
sizeof(*release_addr));
/*
* Send an event to wake up the secondary CPU.
*/
sev();
iounmap(release_addr);
return 0;| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
writeq_relaxed(pa_holding_pen, release_addr) | 첫 대기 장소의 64비트 칸에 secondary_holding_pen의 물리 주소를 기록합니다. 이 부팅 규약은 little-endian 표현을 요구합니다. |
dcache_clean_inval_poc | 기록한 값을 다른 CPU가 볼 수 있도록 해당 구간의 cache를 정리합니다. volatile이나 SEV만으로 이 작업을 대체하지 않습니다. |
sev() | 대기 중인 CPU가 조건을 다시 확인하도록 이벤트를 보냅니다. 꺼진 CPU의 전원을 켜는 범용 명령이 아닙니다. |
v6.18.37 · arch/arm64/kernel/head.S · 340–348행
mrs x2, mpidr_el1
mov_q x1, MPIDR_HWID_BITMASK
and x2, x2, x1
adr_l x3, secondary_holding_pen_release
pen: ldr x4, [x3]
cmp x4, x2
b.eq secondary_startup
wfe
b pen| 코드에서 볼 부분 | 이 줄이 하는 일과 다음 단계에 미치는 영향 |
|---|---|
mrs x2, mpidr_el1와 mask | 현재 CPU 자신의 affinity 값을 만듭니다. |
adr_l x3, secondary_holding_pen_release | 커널 내부의 두 번째 해제 조건이 저장된 주소를 구합니다. |
ldr x4, [x3] | 지금 허용된 CPU의 affinity 값을 메모리에서 다시 읽습니다. |
cmp x4, x2 / b.eq | 자기 CPU와 일치해야 공통 초기화로 진행합니다. |
wfe / b pen | 이벤트를 기다린 뒤 조건을 다시 읽습니다. WFE는 이벤트가 이미 기록돼 있으면 즉시 진행할 수도 있으므로 조건 검사 루프가 필요합니다. |
WFI 복귀와 CPU_SUSPEND 복귀는 CPU_ON과 다릅니다
online 상태에서 얕은 idle에 들어간 CPU는 인터럽트 등 wakeup 조건이 만족되면 대기에서 나옵니다. GIC의 pending·enable·routing, CPU의 마스크와 플랫폼의 wakeup 설정은 함께 확인해야 합니다. “아무 인터럽트나 오면 무조건 정상 실행한다”는 뜻은 아닙니다. WFI에서 나오는 조건과 실제 인터럽트 핸들러 진입 조건도 구분합니다.
| 비교 | CPU_ON | 상태를 잃는 CPU_SUSPEND |
|---|---|---|
| 요청하는 CPU | 대상 CPU와 다른 실행 중인 CPU | 절전에 들어갈 CPU 자신 |
| 커널 진입 주소 | secondary_entry의 물리 주소 | cpu_resume의 물리 주소 |
| 커널이 준비하는 데이터 | 대상 idle 태스크 및 시작 상태 | 잠들기 전 레지스터·스택 등 복구할 실행 상태 |
| 정상 복귀 뒤 목적 | 보조 CPU 초기화와 online 진입 | 저장한 실행 흐름의 연속성 회복 |
두 경로 모두 펌웨어 warm boot 일부를 지날 수 있어도 Linux에서 돌아갈 위치와 준비 데이터는 다릅니다.
psci_cpu_suspend_enter는 CPU 상태를 잃는지에 따라 경로를 나눕니다. 상태를 잃는 경우 cpu_suspend로 상태를 저장하고, finisher가 __pa_symbol_nodebug((unsigned long)cpu_resume)로 구한 물리 주소를 펌웨어에 전달합니다. 이 버전의 finisher는 noinstr 함수이므로 주소 변환도 해당 경로의 제약에 맞춘 형태입니다. 모든 idle 상태가 이 과정을 거치는 것은 아닙니다. PSCI suspend 분기 · 커널 상태 저장 · 복귀 어셈블리
태스크 wakeup은 또 다른 층입니다. 실행 가능해진 태스크를 어느 online CPU에서 실행할지 정하고 필요하면 reschedule IPI를 보냅니다. 잠든 태스크 하나를 깨울 때마다 전원이 꺼진 CPU에 PSCI CPU_ON을 호출하지 않습니다. 스케줄러 wakeup 코드
CPU가 올라오지 않을 때 어디까지 왔는지 구분합니다
| PSCI 원래 반환값 | 뜻 | 이 커널의 CPU_ON 경로에서 변환된 값 |
|---|---|---|
| 0 SUCCESS | 시작 요청 성공 | 0; online 여부는 별도 확인 |
| -1 NOT_SUPPORTED | 지원하지 않는 요청 | -EOPNOTSUPP |
| -2 INVALID_PARAMS / -9 INVALID_ADDRESS | 잘못된 인자 또는 진입 주소 | -EINVAL |
| -3 DENIED | 요청 거부 | -EPERM |
| -4 ALREADY_ON / -5 ON_PENDING | 이미 켜졌거나 이전 시작 요청 진행 중 | 이 변환 함수에서는 기본 분기로 -EINVAL |
| -6 INTERNAL_FAILURE | 내부 처리 실패 | 이 변환 함수에서는 기본 분기로 -EINVAL |
따라서 커널 로그의 -EINVAL 하나만 보고 “진입 주소가 틀렸다”고 단정하지 않습니다. 펌웨어 반환값을 확인할 수 있는 지점에서 원래 PSCI 오류를 구분해야 합니다. psci_to_linux_errno · PSCI 상수 정의
| 확인한 마지막 지점 | 다음에 확인할 것 | 혼동하지 말아야 할 점 |
|---|---|---|
| PSCI 호출 전 | DT/ACPI의 시작 방식, CPU affinity, P_entry | 논리 CPU 번호를 그대로 MPIDR라고 기록하지 않습니다. |
| SMC에서 오류 반환 | 펌웨어 원래 반환값과 변환된 Linux errno | 이 커널은 일부 PSCI 오류를 같은 -EINVAL로 변환합니다. |
| CPU_ON 성공, B의 warm entry는 관찰되지 않음 | 플랫폼 전원 상태, reset 경로, warm entry 설정 | SUCCESS만으로 B가 코드를 실행했다고 단정하지 않습니다. |
| TF-A warm entry에 도달 | coherency, 플랫폼 finish 콜백, 구성한 OP-TEE CPU 초기화 | 새 CPU가 펌웨어에서 막힌 것인지 커널에서 막힌 것인지 나눕니다. |
secondary_entry에 도달 | EL, 페이지 크기·주소 폭 지원, page table, MMU 전환 | PC에 가상 주소가 들어가야 하는 시점과 물리 주소 시점을 구분합니다. |
__secondary_switched에 도달 | secondary_data.task, 스택, per-CPU offset | NULL이면 __secondary_too_slow로 갈 수 있습니다. |
secondary_start_kernel에 도달 | CPU 기능 검사, 시작 콜백, online 표시와 완료 통지 | CPU_ON 반환보다 훨씬 뒤의 커널 준비 단계입니다. |
공개적으로 기록할 때에는 CPU 논리 번호와 MPIDR, 함수 위치, 반환값, 단계별 도달 여부를 구분하면 됩니다. 실제 제품의 비공개 메모리 지도나 전원 레지스터 값을 이 예시 값으로 추정해서 채우지 않습니다.
cat /sys/devices/system/cpu/possible
cat /sys/devices/system/cpu/present
cat /sys/devices/system/cpu/online
cat /sys/devices/system/cpu/offline
dmesg | grep -Ei "psci|Booted secondary|failed to (boot|come online)|CPU[0-9]"위 명령은 상태와 로그를 읽습니다. possible은 커널이 관리할 수 있는 CPU 집합, present는 시스템에 존재한다고 등록한 집합, online은 현재 online 집합입니다. CPU hotplug 실험은 지원 여부와 사용 중인 작업의 영향을 확인한 환경에서 별도로 수행합니다. 이 글의 코드 경로는 고정 버전의 소스로 확인했으며 실제 보드에서 측정한 기동 시간이나 실행 로그를 제시하는 것은 아닙니다.
기준 소스와 확인 범위
각 코드의 링크는 위 버전의 고정 커밋을 가리킵니다. 프로젝트별 버전을 함께 명시한 것은 비교 기준이며, 이 조합을 특정 보드에서 빌드·부팅해 호환성을 검증했다는 뜻은 아닙니다. 코드는 표시한 범위의 실제 원문이며, 설명 표는 그 범위의 동작을 묶어서 읽도록 작성했습니다.
- v6.18.37 · arch/arm64/kernel/psci.c
- v6.18.37 · arch/arm64/kernel/smp.c
- v6.18.37 · arch/arm64/kernel/head.S
- v6.18.37 · arch/arm64/kernel/smp_spin_table.c
- v6.18.37 · arch/arm64/kernel/cpu_ops.c
- v6.18.37 · arch/arm64/kernel/suspend.c
- v6.18.37 · arch/arm64/kernel/sleep.S
- v6.18.37 · arch/arm64/kernel/smccc-call.S
- v6.18.37 · drivers/firmware/psci/psci.c
- v6.18.37 · include/uapi/linux/psci.h
- v6.18.37 · kernel/sched/core.c
- lts-v2.14.3 · lib/psci/psci_main.c
- lts-v2.14.3 · lib/psci/psci_on.c
- lts-v2.14.3 · lib/psci/psci_common.c
- lts-v2.14.3 · plat/arm/board/fvp/fvp_pm.c
- lts-v2.14.3 · services/spd/opteed/opteed_pm.c