01 · QUESTION
무엇을 확인할 것인가
suspend 함수가 C 호출처럼 return하는 것처럼 보여도 왜 page table, stack, interrupt controller와 timer를 다시 초기화해야 하는가?
cpuidle은 idle state별 exit latency와 target residency를 보고 선택하고 system suspend는 device quiesce, secondary CPU offline, firmware entry와 resume를 수행한다. 깊은 state는 CPU context 또는 coherency를 잃을 수 있다.
kernel가 보존하는 memory context, architecture assembly save area, firmware contract와 always-on hardware를 나눈다. noirq 단계 이후에는 일반 interrupt, allocator와 sleep 가능한 service를 기대할 수 없다.
02 · CONTRACT
공통 규약과 아키텍처별 구현
| architecture | 핵심 mechanism | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | WFI/WFE, PSCI CPU_SUSPEND와 cpu_suspend save area | PSCI state encoding, context ID, resume PA 또는 cache flush가 틀리면 firmware가 즉시 실패하거나 resume 직후 stale memory를 실행한다. | PSCI return, MPIDR/power state, save area PA, SCTLR/TTBR, GIC redistributor sleep와 architected timer context를 본다. |
| x86-64 | HLT/MWAIT, ACPI C-state와 S3/S0ix resume | TSC stop, LAPIC timer 정지, 잘못된 wakeup physical address 또는 BSP/AP resume 순서가 time jump와 suspend hang을 만든다. | ACPI state latency, MWAIT hint, PM timer/TSC continuity, wakeup header PA, CR0/CR3/CR4/EFER와 APIC state를 확인한다. |
| RISC-V | WFI와 SBI system suspend/hart suspend | SBI extension/version 불일치, hart state transition race 또는 resume address mode 오류가 invalid-param과 영구 정지를 만든다. | hartid, SBI suspend type/return, resume address/opaque, SATP/SSTATUS, interrupt enable와 timer comparator를 기록한다. |
03 · DIAGRAMS
코드에서 확인되는 세 경계
arm64 · cpu_suspend
- save
- IRQ 문맥을 저장하고
__cpu_suspend_enter()로 CPU 문맥 저장 분기에 들어갑니다(L125–142). - first entry
- 호출자가 넘긴
fn(arg)를 호출합니다(L142–144). 이 함수 자체가 PSCI를 직접 호출하지 않습니다. - return
- finisher가 C 반환을 하면 실패 분기, 정상 resume은 다른 분기로 들어와
__cpu_suspend_exit()를 호출합니다(L146–159).
x86-64 · FFH MWAIT
- offline
acpi_processor_ffh_play_dead()는 CPU별 hint로mwait_play_dead()를 호출합니다(L209–217).- idle
acpi_processor_ffh_cstate_enter()는 해당 CPU의 C-state hint로mwait_idle_with_hints()를 호출합니다(L219–228).- limit
- 이 발췌에는 S3 wakeup code가 없습니다. 별도 sleep.c:54–149와 구분합니다.
RISC-V · cpu_suspend
- save
- CSR 문맥 저장과
__cpu_suspend_enter()가 finisher 호출보다 앞섭니다(L77–90). - handoff
- finisher에 복귀 진입 물리주소와 context 포인터를 전달합니다(L90–91).
- return
- finisher의 C 반환 0은
-EOPNOTSUPP로 바꾸고, 이후 CSR을 복원합니다(L93–108).
__cpu_suspend_enter(&state) 참 → fn(arg) 호출(L142–144)-EOPNOTSUPP로 변환(L146–156)cpu_resume()에서 복귀한 경우 else → __cpu_suspend_exit() → IRQ 문맥 복원(L157–164)__cpu_suspend_enter(&context) 참 → finish(arg, resume PA, context)(L87–91)-EOPNOTSUPP로 변환. 정상 재개와 공유하는 후처리에서 CSR 복원(L93–108)04 · SOURCE
Linux 6.18.37 원본 코드와 주요 구문 해설
소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.
arm64 · Linux 6.18.37
WFI/WFE, PSCI CPU_SUSPEND와 cpu_suspend save area
shallow idle은 WFI로 돌아오지만 power-down state는 cpu_suspend()가 callee-saved context와 resume PA를 보존하고 PSCI firmware에 power state를 요청한다. resume assembly가 MMU/cache와 CPU feature state를 복구한다.
원본 코드: arch/arm64/kernel/suspend.c:89-169
89
90/*
91 * cpu_suspend
92 *
93 * arg: argument to pass to the finisher function
94 * fn: finisher function pointer
95 *
96 */
97int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
98{
99 int ret = 0;
100 unsigned long flags;
101 struct sleep_stack_data state;
102 struct arm_cpuidle_irq_context context;
103
104 /*
105 * Some portions of CPU state (e.g. PSTATE.{PAN,DIT}) are initialized
106 * before alternatives are patched, but are only restored by
107 * __cpu_suspend_exit() after alternatives are patched. To avoid
108 * accidentally losing these bits we must not attempt to suspend until
109 * after alternatives have been patched.
110 */
111 WARN_ON(!system_capabilities_finalized());
112
113 /* Report any MTE async fault before going to suspend */
114 mte_suspend_enter();
115
116 /*
117 * From this point debug exceptions are disabled to prevent
118 * updates to mdscr register (saved and restored along with
119 * general purpose registers) from kernel debuggers.
120 *
121 * Strictly speaking the trace_hardirqs_off() here is superfluous,
122 * hardirqs should be firmly off by now. This really ought to use
123 * something like raw_local_daif_save().
124 */
125 flags = local_daif_save();
126
127 /*
128 * Function graph tracer state gets inconsistent when the kernel
129 * calls functions that never return (aka suspend finishers) hence
130 * disable graph tracing during their execution.
131 */
132 pause_graph_tracing();
133
134 /*
135 * Switch to using DAIF.IF instead of PMR in order to reliably
136 * resume if we're using pseudo-NMIs.
137 */
138 arm_cpuidle_save_irq_context(&context);
139
140 ct_cpuidle_enter();
141
142 if (__cpu_suspend_enter(&state)) {
143 /* Call the suspend finisher */
144 ret = fn(arg);
145
146 /*
147 * Never gets here, unless the suspend finisher fails.
148 * Successful cpu_suspend() should return from cpu_resume(),
149 * returning through this code path is considered an error
150 * If the return value is set to 0 force ret = -EOPNOTSUPP
151 * to make sure a proper error condition is propagated
152 */
153 if (!ret)
154 ret = -EOPNOTSUPP;
155
156 ct_cpuidle_exit();
157 } else {
158 ct_cpuidle_exit();
159 __cpu_suspend_exit();
160 }
161
162 arm_cpuidle_restore_irq_context(&context);
163
164 unpause_graph_tracing();
165
166 /*
167 * Restore pstate flags. OS lock and mdscr have been already
168 * restored, so from this point onwards, debugging is fully
169 * reenabled if it was enabled when core started shutdown.주요 구문 해설
원본 81줄은 그대로 표시하며, 검토한 21개 구문에 설명을 붙였습니다.
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))CPU 실행 상태를 저장한 뒤 플랫폼의 절전 마무리 함수 fn(arg)을 부르는 진입점입니다. 정상 재개는 저장한 문맥으로 돌아오며, 절전 진입에 실패하면 오류를 반환합니다.
int ret = 0;CPU suspend의 반환 상태를 성공값 0으로 초기화합니다. 정상 resume 경로는 이 값을 유지하고, suspend finisher가 그대로 돌아온 실패 경로는 뒤에서 오류값으로 바꿉니다.
unsigned long flags;절전 진입 전에 쓰던 DAIF 예외 마스크를 보관합니다. 모든 상태 복구가 끝난 뒤 이 값으로 디버그·IRQ 등의 허용 상태를 되돌립니다.
struct sleep_stack_data state;레지스터와 복귀에 필요한 스택 정보를 담을 절전 문맥입니다. __cpu_suspend_enter가 채우며 재개 경로가 이 자료로 원래 실행을 이어갑니다.
struct arm_cpuidle_irq_context context;GIC 우선순위 마스킹을 사용하는 구성에서 DAIF·PMR의 기존 상태를 보관할 공간입니다. 진입용 마스킹으로 바꿨다가 재개 후 되돌리는 데 사용합니다.
WARN_ON(!system_capabilities_finalized());CPU 기능 판별과 alternatives 적용이 아직 끝나지 않았다면 경고합니다. PAN·DIT처럼 재개 코드가 조건부로 복원하는 상태를 잃지 않으려면 그 작업이 끝난 뒤 절전해야 합니다.
mte_suspend_enter();절전 전에 아직 보고되지 않은 비동기 MTE 태그 오류를 확인합니다. 메모리 태그 검사 오류가 CPU 상태 저장·복구 과정에 묻혀 뒤늦게 처리되는 일을 막기 위한 절차입니다.
flags = local_daif_save();현재 DAIF 예외 마스크를 flags에 보관하고 예외를 막습니다. 특히 디버그 예외가 저장 중인 MDSCR_EL1 상태를 바꾸지 못하게 하며, 함수 끝에서 기존 마스크로 복구합니다.
pause_graph_tracing();현재 태스크의 함수 진입·복귀 추적을 잠시 멈춥니다. 절전에 성공하면 fn 호출 지점으로 곧바로 돌아오는 대신 cpu_resume 경로를 거치므로 일반 함수 복귀처럼 추적할 수 없기 때문입니다.
arm_cpuidle_save_irq_context(&context);GIC 우선순위 마스킹을 사용하는 CPU라면 DAIF와 PMR 상태를 context에 보관하고 절전 진입에 맞는 마스크로 바꿉니다. 복귀 직후에도 DAIF로 인터럽트를 제어할 수 있게 준비합니다.
ct_cpuidle_enter();이 CPU가 idle 구간에 들어간다고 문맥 추적과 RCU에 알립니다. IRQ 추적 상태도 그 순서에 맞춰 조정하여, CPU가 쉬는 동안 RCU가 일반 커널 실행을 기다리지 않게 합니다.
if (__cpu_suspend_enter(&state)) {문맥을 처음 저장한 경로에서는 참을 반환하여 fn을 호출합니다. 전원이 돌아와 저장 문맥을 복원한 경로에서는 거짓이 되어 else의 재개 후처리로 이어집니다.
ret = fn(arg);PSCI 또는 platform backend를 호출해 실제 low-power state로 진입한다.
if (!ret)suspend finisher가 돌아왔는데 반환값까지 0인지 검사합니다. 정상 suspend/resume은 cpu_resume을 통해 복귀해야 하므로 finisher의 평범한 C 반환을 성공으로 취급하지 않습니다.
ret = -EOPNOTSUPP;finisher가 정상 반환한 듯이 0을 줬어도 suspend 진입 실패로 보고 -EOPNOTSUPP로 바꿉니다. 실패 경로가 최종 호출자에게 성공으로 전달되는 것을 막습니다.
ct_cpuidle_exit();절전용 fn이 반환한 실패 경로에서 idle 진입 처리를 되돌립니다. 실제로 잠들지 못했더라도 앞에서 바꾼 RCU·IRQ 추적 상태는 정상 커널 실행 상태로 돌려놓아야 합니다.
} else {절전 finisher에서 반환한 실패 경로가 아니라 cpu_resume을 통해 복원된 경로입니다. idle 추적 상태와 추가 CPU 상태를 복구합니다.
ct_cpuidle_exit();cpu_resume을 통해 돌아온 성공 경로에서 idle 문맥을 종료합니다. 이어지는 __cpu_suspend_exit가 일반 커널 문맥에서 CPU 상태의 나머지를 복원할 수 있도록 합니다.
__cpu_suspend_exit();전원 복귀 뒤 CPU-local register와 kernel execution 조건을 재구성한다.
arm_cpuidle_restore_irq_context(&context);진입 전에 보관한 인터럽트 우선순위와 마스크 상태를 context에서 복구합니다. GIC 우선순위 마스킹을 쓰는 구성에서 절전 진입용으로 바꾼 상태를 되돌리는 부분입니다.
unpause_graph_tracing();절전 시도 중 멈췄던 함수 진입·복귀 추적을 다시 허용합니다. 실패로 돌아온 경우와 정상 재개한 경우가 모두 이 정리 절차를 거칩니다.
x86-64 · Linux 6.18.37
HLT/MWAIT, ACPI C-state와 S3/S0ix resume
cpuidle은 ACPI _CST 또는 native state에서 I/O port, MWAIT/UMWAIT를 선택한다. system suspend는 wakeup trampoline을 low memory에 준비하고 firmware가 real mode/32-bit resume vector로 CPU를 되돌리면 long mode, CR3와 per-CPU state를 복원한다.
원본 코드: arch/x86/kernel/acpi/cstate.c:211-251
211 unsigned int cpu = smp_processor_id();
212 struct cstate_entry *percpu_entry;
213
214 percpu_entry = per_cpu_ptr(cpu_cstate_entry, cpu);
215 mwait_play_dead(percpu_entry->states[cx->index].eax);
216}
217EXPORT_SYMBOL_GPL(acpi_processor_ffh_play_dead);
218
219void __cpuidle acpi_processor_ffh_cstate_enter(struct acpi_processor_cx *cx)
220{
221 unsigned int cpu = smp_processor_id();
222 struct cstate_entry *percpu_entry;
223
224 percpu_entry = per_cpu_ptr(cpu_cstate_entry, cpu);
225 mwait_idle_with_hints(percpu_entry->states[cx->index].eax,
226 percpu_entry->states[cx->index].ecx);
227}
228EXPORT_SYMBOL_GPL(acpi_processor_ffh_cstate_enter);
229
230static int __init ffh_cstate_init(void)
231{
232 struct cpuinfo_x86 *c = &boot_cpu_data;
233
234 if (c->x86_vendor != X86_VENDOR_INTEL &&
235 c->x86_vendor != X86_VENDOR_AMD &&
236 c->x86_vendor != X86_VENDOR_HYGON)
237 return -1;
238
239 cpu_cstate_entry = alloc_percpu(struct cstate_entry);
240 return 0;
241}
242
243static void __exit ffh_cstate_exit(void)
244{
245 free_percpu(cpu_cstate_entry);
246 cpu_cstate_entry = NULL;
247}
248
249arch_initcall(ffh_cstate_init);
250__exitcall(ffh_cstate_exit);
251 주요 구문 해설
원본 41줄은 그대로 표시하며, 검토한 25개 구문에 설명을 붙였습니다.
unsigned int cpu = smp_processor_id();현재 실행 중인 CPU 번호를 얻습니다. CPU를 오프라인 상태로 보내는 이 경로에서, 그 CPU에 맞춰 저장해 둔 C-state 진입 힌트를 찾기 위한 인덱스입니다.
struct cstate_entry *percpu_entry;오프라인 상태로 보내려는 현재 CPU의 C-state 힌트 자료를 가리킬 포인터입니다. 다음 줄에서 CPU별 할당 중 해당 CPU의 항목을 선택합니다.
percpu_entry = per_cpu_ptr(cpu_cstate_entry, cpu);CPU별로 따로 할당된 C-state 자료에서 현재 CPU의 항목을 선택합니다. 같은 C-state 번호라도 CPU에 따라 저장된 MWAIT 힌트를 사용하도록 합니다.
mwait_play_dead(percpu_entry->states[cx->index].eax);선택한 C-state의 EAX 힌트를 넘겨 CPU를 MWAIT 기반의 정지 대기 경로로 보냅니다. 일반 idle 함수와 달리 이 play_dead 경로는 호출자에게 정상 반환하는 용도로 사용하지 않습니다.
EXPORT_SYMBOL_GPL(acpi_processor_ffh_play_dead);acpi_processor_ffh_play_dead 심볼을 GPL 호환 모듈이 참조할 수 있도록 공개합니다. 함수 실행이 아니라 링크에 필요한 심볼 등록이며, 실제 절전 진입은 이 함수를 호출할 때 일어납니다.
void __cpuidle acpi_processor_ffh_cstate_enter(struct acpi_processor_cx *cx)ACPI가 선택한 cx 상태로 현재 CPU를 MWAIT 대기에 넣는 함수입니다. __cpuidle은 idle 코드의 위치를 구분하는 표식이며, 깨어나면 호출자에게 돌아옵니다.
unsigned int cpu = smp_processor_id();일반 C-state 진입을 수행하는 현재 CPU 번호를 구합니다. 아래에서 그 CPU 전용 EAX·ECX MWAIT 힌트를 선택할 때 사용합니다.
struct cstate_entry *percpu_entry;일반 idle 진입에서 사용할 현재 CPU 전용 C-state 자료의 포인터입니다. cx->index에 해당하는 EAX·ECX 힌트를 이 자료에서 읽습니다.
percpu_entry = per_cpu_ptr(cpu_cstate_entry, cpu);현재 CPU의 C-state 힌트 배열을 가리킵니다. 이어지는 mwait_idle_with_hints가 cx->index에 해당하는 EAX·ECX 값을 받아 요청한 대기 상태로 진입합니다.
mwait_idle_with_hints(percpu_entry->states[cx->index].eax,ACPI FFH가 제공한 MWAIT hint로 CPU-local low-power state에 진입한다.
percpu_entry->states[cx->index].ecx);MWAIT의 ECX 입력 힌트를 선택한 C-state 자료에서 넘깁니다. 앞줄의 EAX가 대기 상태를 지정하고 ECX는 함께 적용할 확장 동작을 지정합니다.
EXPORT_SYMBOL_GPL(acpi_processor_ffh_cstate_enter);일반 FFH C-state 진입 함수의 심볼을 GPL 호환 모듈에 공개합니다. 이 줄 자체에서 CPU를 쉬게 하는 것은 아니며, 공개된 함수를 통해 MWAIT 진입 경로를 사용할 수 있게 합니다.
static int __init ffh_cstate_init(void)부팅 때 FFH 방식의 C-state 지원 자료를 준비합니다. __init 코드는 초기화 후 버릴 수 있으며, 지원 제조사인지 검사한 뒤 CPU별 저장 공간을 할당합니다.
struct cpuinfo_x86 *c = &boot_cpu_data;부트 CPU의 제조사 등 기능 정보에 접근할 포인터입니다. 시스템에서 이 FFH 초기화 경로를 적용할지 판단하는 기준으로 사용합니다.
if (c->x86_vendor != X86_VENDOR_INTEL &&Intel이 아닌지 검사하는 첫 조건입니다. 아래 AMD·Hygon 검사까지 모두 참이어야 지원 대상 밖으로 판정합니다.
c->x86_vendor != X86_VENDOR_AMD &&Intel이 아니라도 AMD이면 전체 AND 조건이 거짓이 되어 초기화를 계속합니다. AMD도 이 MWAIT 힌트 저장 경로의 지원 대상입니다.
c->x86_vendor != X86_VENDOR_HYGON)Hygon까지 아니라면 세 지원 제조사에 모두 해당하지 않습니다. 이 경우 바로 반환해 FFH용 CPU별 자료를 만들지 않습니다.
return -1;지원 제조사 밖인 경우 초기화 실패 값 -1을 반환합니다. 여기서는 CPU별 C-state 저장 공간을 할당하지 않습니다.
cpu_cstate_entry = alloc_percpu(struct cstate_entry);가능한 CPU마다 별도의 cstate_entry 저장 공간을 할당합니다. C-state 탐색에서 얻은 각 CPU의 MWAIT 힌트를 서로 덮어쓰지 않고 보관하려는 것입니다. 이 초기화 함수는 할당 결과를 전역 포인터에 넣고 0을 반환합니다.
return 0;지원 제조사 경로의 초기화를 0으로 끝냅니다. 이 원문에는 바로 앞 alloc_percpu의 NULL 결과를 별도로 검사하는 분기가 없습니다.
static void __exit ffh_cstate_exit(void)FFH C-state 자료를 정리하는 종료 함수입니다. CPU별 할당을 반환한 뒤 전역 포인터도 비워 해제된 공간을 가리키지 않게 합니다.
free_percpu(cpu_cstate_entry);alloc_percpu로 확보했던 CPU별 C-state 저장 공간을 반환합니다. 특정 CPU 한 개의 항목만 지우는 것이 아니라 해당 per-CPU 할당 전체를 해제합니다.
cpu_cstate_entry = NULL;해제한 CPU별 자료의 전역 포인터를 NULL로 바꿉니다. 유효한 C-state 저장 공간이 남아 있지 않다는 상태를 표시합니다.
arch_initcall(ffh_cstate_init);ffh_cstate_init을 부팅 중 architecture initcall 단계에 실행할 초기화 함수로 등록합니다. 여기서 즉시 호출하는 문장이 아니며, CPU별 C-state 자료 할당을 부팅 순서에 연결합니다.
__exitcall(ffh_cstate_exit);ffh_cstate_exit을 종료용 함수 포인터가 놓이는 exitcall 섹션에 등록합니다. 정리 함수는 CPU별 할당을 해제하지만, 이 선언을 매 부팅 종료나 매 idle 복귀 때 실행하는 호출로 읽으면 안 됩니다.
RISC-V · Linux 6.18.37
WFI와 SBI system suspend/hart suspend
shallow idle은 WFI를 사용하고 deeper state는 SBI HSM(개별 hart) 또는 SUSP(시스템 suspend) 확장이 hart state와 resume address를 firmware에 넘긴다. cpu_suspend()는 non-retentive state에서 architecture context를 저장하고 resume path를 호출한다.
원본 코드: arch/riscv/kernel/suspend.c:57-125
57#endif
58 csr_write(CSR_STIMECMP, context->stimecmp);
59 }
60
61 csr_write(CSR_SATP, context->satp);
62#endif
63}
64
65int cpu_suspend(unsigned long arg,
66 int (*finish)(unsigned long arg,
67 unsigned long entry,
68 unsigned long context))
69{
70 int rc = 0;
71 struct suspend_context context = { 0 };
72
73 /* Finisher should be non-NULL */
74 if (!finish)
75 return -EINVAL;
76
77 /* Save additional CSRs*/
78 suspend_save_csrs(&context);
79
80 /*
81 * Function graph tracer state gets incosistent when the kernel
82 * calls functions that never return (aka finishers) hence disable
83 * graph tracing during their execution.
84 */
85 pause_graph_tracing();
86
87 /* Save context on stack */
88 if (__cpu_suspend_enter(&context)) {
89 /* Call the finisher */
90 rc = finish(arg, __pa_symbol(__cpu_resume_enter),
91 (ulong)&context);
92
93 /*
94 * Should never reach here, unless the suspend finisher
95 * fails. Successful cpu_suspend() should return from
96 * __cpu_resume_entry()
97 */
98 if (!rc)
99 rc = -EOPNOTSUPP;
100 }
101
102 /* Enable function graph tracer */
103 unpause_graph_tracing();
104
105 /* Restore additional CSRs */
106 suspend_restore_csrs(&context);
107
108 return rc;
109}
110
111#ifdef CONFIG_RISCV_SBI
112static int sbi_system_suspend(unsigned long sleep_type,
113 unsigned long resume_addr,
114 unsigned long opaque)
115{
116 struct sbiret ret;
117
118 ret = sbi_ecall(SBI_EXT_SUSP, SBI_EXT_SUSP_SYSTEM_SUSPEND,
119 sleep_type, resume_addr, opaque, 0, 0, 0);
120 if (ret.error)
121 return sbi_err_map_linux_errno(ret.error);
122
123 return ret.value;
124}
125 주요 구문 해설
원본 69줄은 그대로 표시하며, 검토한 32개 구문에 설명을 붙였습니다.
#endif54행에서 시작한 선택 구간을 마칩니다. RV32는 64비트 타이머 비교값을 두 CSR로 나누어 복원해야 합니다. 낮은 부분을 일단 최댓값으로 두고 상위 부분을 복원한 뒤 실제 낮은 부분을 써서 중간의 잘못된 비교값으로 인터럽트가 생기는 것을 피합니다. RV64에는 이 분할 복원이 필요 없습니다.
csr_write(CSR_STIMECMP, context->stimecmp);Sstc 확장의 supervisor 타이머 비교값을 저장된 stimecmp로 복구합니다. RV64에서는 전체 값을 쓰고, RV32에서는 위에서 상위 절반을 복원한 뒤 이 줄로 하위 절반을 확정하여 재개 후 타이머 기준을 되살립니다.
csr_write(CSR_SATP, context->satp);저장한 SATP 값을 써서 절전 전 주소 변환 모드·ASID·페이지 테이블 루트를 복구합니다. MMU가 있는 구성에서만 필요한 supervisor 주소 공간 상태입니다.
#endif52행에서 시작한 선택 구간을 마칩니다. MMU 지원 구성의 복귀에서는 Sstc 타이머 비교값을 지원 여부에 따라 되살리고 SATP의 페이지 표 문맥도 복원합니다. MMU가 없는 구성에는 이 SATP 복원 구간을 넣지 않습니다.
int cpu_suspend(unsigned long arg,RISC-V CPU 문맥을 보존하고 플랫폼 절전 함수로 넘기는 공통 진입점입니다. arg는 플랫폼에 전달할 절전 종류 등의 값입니다.
int (*finish)(unsigned long arg,finish는 실제 절전을 요청할 콜백입니다. 첫 인자로 받은 arg를 그대로 전달하여 공통 저장 코드와 플랫폼의 절전 방식을 분리합니다.
unsigned long entry,콜백의 entry 인자는 깨어났을 때 실행할 커널 재개 코드의 물리주소입니다. 아직 일반 커널 가상주소 실행 환경이 복구되지 않은 진입에 사용합니다.
unsigned long context))콜백의 context 인자는 스택에 보관한 문맥의 주소를 전달하는 값입니다. 재개 코드가 어떤 문맥을 복원해야 하는지 식별하도록 넘깁니다.
int rc = 0;정상 재개 때 반환할 기본 결과를 0으로 둡니다. 절전 마무리 함수가 실패로 반환한 경우에만 그 오류로 바뀝니다.
struct suspend_context context = { 0 };레지스터·CSR 복원 자료를 담을 구조체를 0으로 초기화합니다. 지원되는 CPU 기능의 상태를 이후 저장 함수가 채웁니다.
if (!finish)실제 절전을 수행할 콜백이 없는지 검사합니다. 없으면 문맥을 저장하거나 추적 상태를 바꾸기 전에 요청을 거절합니다.
return -EINVAL;필수 finish 인자가 없으므로 잘못된 인자 오류 -EINVAL을 반환합니다.
suspend_save_csrs(&context);절전으로 잃을 수 있는 supervisor CSR 상태를 context에 저장합니다. 일반 정수 레지스터를 저장하는 __cpu_suspend_enter와 별도로 인터럽트·주소 변환·지원되는 타이머 상태를 복구할 자료를 마련합니다.
pause_graph_tracing();함수 진입·복귀 추적을 일시 중지합니다. 절전 finisher가 성공하면 원래 호출의 복귀 경로 대신 재개 진입점을 거치므로, 추적기가 서로 맞지 않는 함수 호출·복귀로 해석하지 않게 합니다.
if (__cpu_suspend_enter(&context)) {정수 레지스터 등의 문맥을 저장한 최초 경로면 절전 콜백을 실행합니다. 재개 코드가 문맥을 복원해 돌아온 경우는 이 본문을 건너뜁니다.
rc = finish(arg, __pa_symbol(__cpu_resume_enter),플랫폼에 arg와 __cpu_resume_enter의 물리주소를 전달해 절전을 요청합니다. 정상적으로 잠들었다면 이 호출 아래가 아니라 재개 진입점을 통해 돌아옵니다.
(ulong)&context);스택의 context 주소를 콜백의 불투명 문맥 값으로 전달합니다. 재개 경로가 저장해 둔 레지스터·CSR 자료를 찾는 데 사용합니다.
if (!rc)절전 콜백이 호출 지점으로 돌아왔는데 결과까지 0인 상황을 검사합니다. 정상 성공은 별도 재개 경로여야 하므로 이 0을 성공으로 받아들이지 않습니다.
rc = -EOPNOTSUPP;실제로 절전에 들어가지 못한 0 반환을 -EOPNOTSUPP로 바꿉니다. 호출자가 정상 재개와 실패 복귀를 구분하도록 합니다.
unpause_graph_tracing();절전 실패 또는 재개 경로가 합쳐진 뒤 함수 추적을 다시 허용합니다. 이어서 suspend_restore_csrs가 앞에서 보관한 CSR 상태를 되돌립니다.
suspend_restore_csrs(&context);복귀 뒤 supervisor CSR과 Linux execution context를 재설치한다.
return rc;추적과 CSR 복구를 마친 뒤 결과를 돌려줍니다. 재개 경로는 0이고, 실패 경로는 finisher의 오류 또는 보정한 -EOPNOTSUPP입니다.
#ifdef CONFIG_RISCV_SBISBI는 S-mode Linux가 상위 펌웨어에 서비스를 요청하는 인터페이스입니다. 지원 빌드에서는 SYSTEM_SUSPEND 호출에 수면 종류·재개 주소·opaque 인수를 전달하고 펌웨어 오류를 Linux errno로 변환하는 경로를 포함합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
static int sbi_system_suspend(unsigned long sleep_type,SBI SUSP 확장으로 시스템 절전을 요청하는 플랫폼 콜백입니다. sleep_type은 펌웨어에 요청할 절전 종류를 지정합니다.
unsigned long resume_addr,펌웨어가 절전에서 깬 뒤 제어를 넘길 물리주소를 받습니다. 앞의 공통 cpu_suspend가 커널 재개 진입점을 계산해 줍니다.
unsigned long opaque)커널이 지정한 문맥 식별값을 opaque로 받습니다. 펌웨어는 내용을 해석하기보다 재개 경로에 되돌려 줄 값으로 취급합니다.
struct sbiret ret;SBI 호출이 돌려줄 오류 코드와 결과값을 함께 받을 sbiret 구조체를 선언합니다. ret.error와 ret.value는 각각 별도 long 필드이며 CPU 복귀 명령을 뜻하지 않습니다.
ret = sbi_ecall(SBI_EXT_SUSP, SBI_EXT_SUSP_SYSTEM_SUSPEND,SBI의 system suspend 기능을 펌웨어에 요청합니다. 다음 줄의 sleep_type·resume_addr·opaque는 절전 유형, 재개 진입 주소, 재개 시 전달할 값이며 응답의 오류와 결과는 ret에 받습니다.
sleep_type, resume_addr, opaque, 0, 0, 0);SBI 호출 인자에 절전 종류·재개 주소·문맥 값을 넣고 사용하지 않는 나머지 인자는 0으로 채웁니다. 반환된 SBI 오류는 이어지는 코드가 Linux 오류값으로 바꿉니다.
if (ret.error)SBI 응답의 error 필드가 0이 아닌지 검사합니다. 펌웨어 오류 번호를 그대로 Linux 호출자에게 전달하지 않고 다음 줄에서 Linux errno로 바꿉니다.
return sbi_err_map_linux_errno(ret.error);SBI 오류를 -EPERM·-EINVAL·-EFAULT 등 대응하는 Linux 음수 오류로 바꾸어 반환합니다. SBI의 error와 value 두 필드를 구분하여 실패를 전달하는 경로입니다.
return ret.value;SBI가 오류 없이 돌아왔을 때 응답의 value 필드를 반환합니다. 다만 이 finisher가 그대로 0을 반환하면 상위 cpu_suspend는 성공적인 resume 경로가 아니라고 보고 -EOPNOTSUPP로 바꿉니다.
05 · WORKED EXAMPLE
숫자로 검산하기
idle state 선택의 deadline 계산
다음 timer가 800us 뒤이고 C1 exit 2us/target 5us, C2 exit 90us/target 200us, C3 exit 700us/target 1500us라고 가정한다.
- C1800us가 target 5us보다 길고 exit 2us를 충분히 감당하므로 가능하다.
- C2800us가 target 200us보다 길고 90us exit 뒤에도 약 710us 여유가 있어 가능하다.
- C3target residency 1500us를 채우지 못하고 700us exit는 scheduler latency budget도 거의 소모하므로 제외한다.
- choicegovernor는 energy와 최근 실제 residency를 고려해 C1/C2 중 C2를 선택할 수 있다.
결론깊은 state 이름이 아니라 exit latency, minimum residency와 다음 deadline을 숫자로 비교해야 한다.
06 · DEEP DIVE
주요 동작 상세 분석
공통 커널 코드와 아키텍처별 함수의 역할 구분
cpuidle은 idle state별 exit latency와 target residency를 보고 선택하고 system suspend는 device quiesce, secondary CPU offline, firmware entry와 resume를 수행한다. 깊은 state는 CPU context 또는 coherency를 잃을 수 있다.
kernel가 보존하는 memory context, architecture assembly save area, firmware contract와 always-on hardware를 나눈다. noirq 단계 이후에는 일반 interrupt, allocator와 sleep 가능한 service를 기대할 수 없다.
arm64: WFI/WFE, PSCI CPU_SUSPEND와 cpu_suspend save area
shallow idle은 WFI로 돌아오지만 power-down state는 cpu_suspend()가 callee-saved context와 resume PA를 보존하고 PSCI firmware에 power state를 요청한다. resume assembly가 MMU/cache와 CPU feature state를 복구한다.
cache clean과 coherency exit, firmware call, resume page-table/MMU enable 순서가 memory 가시성을 보장한다. 디버깅할 때는 PSCI return, MPIDR/power state, save area PA, SCTLR/TTBR, GIC redistributor sleep와 architected timer context를 본다.
x86-64: HLT/MWAIT, ACPI C-state와 S3/S0ix resume
cpuidle은 ACPI _CST 또는 native state에서 I/O port, MWAIT/UMWAIT를 선택한다. system suspend는 wakeup trampoline을 low memory에 준비하고 firmware가 real mode/32-bit resume vector로 CPU를 되돌리면 long mode, CR3와 per-CPU state를 복원한다.
MONITOR/MWAIT 조건과 broadcast timer, wake vector publication, cache flush 및 platform ACPI ordering을 맞춘다. 디버깅할 때는 ACPI state latency, MWAIT hint, PM timer/TSC continuity, wakeup header PA, CR0/CR3/CR4/EFER와 APIC state를 확인한다.
RISC-V: WFI와 SBI system suspend/hart suspend
shallow idle은 WFI를 사용하고 deeper state는 SBI HSM(개별 hart) 또는 SUSP(시스템 suspend) 확장이 hart state와 resume address를 firmware에 넘긴다. cpu_suspend()는 non-retentive state에서 architecture context를 저장하고 resume path를 호출한다.
cache coherency가 platform-defined인 경우 memory save area publication과 firmware call 전 fence가 필요하다. timer pending과 IPI wake source도 firmware contract에 포함된다. 디버깅할 때는 hartid, SBI suspend type/return, resume address/opaque, SATP/SSTATUS, interrupt enable와 timer comparator를 기록한다.
객체를 사용하고 해제하는 주체와 시점을 확인한다
resume entry physical address, save area와 page table은 suspend 동안 DRAM retention 또는 reserved memory에 유지되어야 한다. firmware가 그 memory를 덮거나 전원을 끄면 복귀할 수 없다.
주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.
최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다
exit latency는 power domain, PLL, cache/coherency, firmware, secondary CPU와 device resume의 합이다. governor는 평균이 아니라 deadline까지 남은 시간과 state별 worst-case latency를 비교한다.
평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.
arm64 system suspend는 PSCI SYSTEM_SUSPEND 호출 하나로 끝나지 않는다
Linux v6.18.37의 CONFIG_SUSPEND 경로는 PSCI SYSTEM_SUSPEND feature 조회 결과가 PSCI_RET_NOT_SUPPORTED가 아닐 때 suspend ops를 등록하며, valid 검사는 PM_SUSPEND_MEM으로 제한한다. 공통 suspend_enter()는 device late/noirq 정지, 보조 CPU 정지, IRQ 비활성화와 syscore 정지를 거친다. 앞 단계 오류·test 모드·대기 중인 wakeup은 실제 platform 진입을 건너뛸 수 있고, suspend-to-idle은 별도 경로다. arm64 진입 콜백은 cpu_suspend(0, psci_system_suspend)를 거쳐 cpu_resume의 물리 주소를 선택된 SMC/HVC conduit에 전달한다. 따라서 suspend 요청을 보냈다는 사실만으로 펌웨어 호출이나 실제 저전력 진입이 완료됐다고 볼 수 없다. Linux: 기능 검사와 suspend ops Linux: 공통 suspend 진입과 오류 경로 Linux: SYSTEM_SUSPEND 호출
TF-A v2.15.0의 psci_system_suspend()는 마지막 ON CPU인지 확인하고 복귀 entrypoint를 검증한다. 플랫폼에 system suspend 전원 상태를 질의한 뒤 목표 suspend level이 PLAT_MAX_PWR_LVL에 도달하는지도 검사한다. 마지막 CPU 또는 최고 power level 조건을 만족하지 못하면 PSCI_E_DENIED를 반환하며 entrypoint 오류도 반환한다. power-down 시작 이후에도 interrupt 등으로 진입이 취소되어 함수가 돌아올 수 있다. Linux PSCI 변환은 DENIED를 -EPERM, 잘못된 주소·인자를 -EINVAL로 옮긴다. 이 절차는 개별 CPU idle인 CPU_SUSPEND와 구분해야 하며, 특정 보드의 절전 성공·소비 전력·복귀 지연을 측정했다는 뜻이 아니다. TF-A: 검증·power-down·취소 반환 Linux: PSCI 오류 변환
07 · FAILURE
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | PSCI state encoding, context ID, resume PA 또는 cache flush가 틀리면 firmware가 즉시 실패하거나 resume 직후 stale memory를 실행한다. | PSCI return, MPIDR/power state, save area PA, SCTLR/TTBR, GIC redistributor sleep와 architected timer context를 본다. |
| x86-64 | TSC stop, LAPIC timer 정지, 잘못된 wakeup physical address 또는 BSP/AP resume 순서가 time jump와 suspend hang을 만든다. | ACPI state latency, MWAIT hint, PM timer/TSC continuity, wakeup header PA, CR0/CR3/CR4/EFER와 APIC state를 확인한다. |
| RISC-V | SBI extension/version 불일치, hart state transition race 또는 resume address mode 오류가 invalid-param과 영구 정지를 만든다. | hartid, SBI suspend type/return, resume address/opaque, SATP/SSTATUS, interrupt enable와 timer comparator를 기록한다. |
08 · LAB
재현과 계측 절차
- cpuidle trace와 hardware counter로 requested state, actual residency, exit latency를 architecture별로 비교한다.
- suspend resume의 마지막 console 지점과 firmware trace를 맞춰 Linux와 firmware 중 어느 쪽에서 멈췄는지 분리한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 위치
- arm64arch/arm64/kernel/suspend.c:89-169
- x86-64arch/x86/kernel/acpi/cstate.c:211-251
- RISC-Varch/riscv/kernel/suspend.c:57-125
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.
wakeup_count는 절전 전후의 이벤트 경쟁을 검사합니다
/sys/power/wakeup_count는 절전 횟수나 절전 효율 지표가 아닙니다. Linux v6.18의 공통 전원관리 코드는 사용자 공간이 값을 읽어 보관하고, 절전 준비를 마친 뒤 그 값을 다시 쓰는 절차를 설명합니다. 새 값을 다시 읽어 덮어쓰는 것이 아니라 준비 전에 보관한 값과 이벤트 상태를 대조해야 합니다.
그 쓰기가 실패하면 state에 절전을 요청하지 않습니다. 새 이벤트나 진행 중인 이벤트뿐 아니라 잘못된 입력·autosleep 상태 같은 오류도 구분합니다. 성공한 뒤에도 새 wakeup 이벤트가 검출되면 전환이 중단될 수 있으므로, 쓰기 성공을 실제 저전력 상태 진입이나 정상 복귀의 증거로 삼지 않습니다. 읽기 자체의 중단과 실제 요청의 반환값도 확인합니다.