-
[리눅스 커널] PM - Platform-dependent power managementLinux/kernel 2023. 9. 18. 20:03
글의 참고
- https://elinux.org/images/0/09/Elce11_pieralisi.pdf
- https://zhuanlan.zhihu.com/p/542967530
- https://www.cnblogs.com/arnoldlu/p/6344847.html
- https://www.thegoodpenguin.co.uk/blog/an-overview-of-psci/
- https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/psci.txt
- https://zhuanlan.zhihu.com/p/491107819
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Platform dependent PM
: 시스템이 완전한 SUSPEND 상태로 들어가기 위해서는 반드시 아키텍처 종속적인 힘이 필요하다. 왜 아키텍처 종속적인 힘이 필요할까? 운영 체제의 중요한 역할 중 하나는 하드웨어의 추상화다. 즉, 아키텍처 독립적인 코드를 제공해준다. 그런데, 아키텍처 독립적인 코드들은 인터페이스가 통일화 되어있기 때문에, 각 아키텍처들이 갖고 있는 완전한 기능을 끌어내기가 쉽지 않다.
예를 들어, `x86`과 `arm64`는 물리적으로 다른 구조를 가지고 있고, 각각 파워 매니지먼트 관련해서 `ACPI`와 `PSCI` 스펙을 따른다. `ACPI`와 `PSCI`는 스펙 자체가 다르기 때문에, CPU 및 시스템이 SLEEP 으로 들어가는 절차 및 흐름이 완전히 다를 수 있다. 물론, 일부분 공통점이 있을 수 있지만, 독립적인 부분들이 또한 존재한다. 즉, 완전한 기능을 끌어내려면 `아키텍처 독립적인` 부분들 또한 반드시 필요하게 된다. `System PM`에서 이런 역할은 맞는 구조체가 존재한다.
// include/linux/suspend.h - v6.5 .... /** * struct platform_suspend_ops - Callbacks for managing platform dependent * system sleep states. * * @valid: Callback to determine if given system sleep state is supported by * the platform. * Valid (ie. supported) states are advertised in /sys/power/state. Note * that it still may be impossible to enter given system sleep state if the * conditions aren't right. * There is the %suspend_valid_only_mem function available that can be * assigned to this if the platform only supports mem sleep. .... * @enter: Enter the system sleep state indicated by @begin() or represented by * the argument if @begin() is not implemented. * This callback is mandatory. It returns 0 on success or a negative * error code otherwise, in which case the system cannot enter the desired * sleep state. .... */ struct platform_suspend_ops { int (*valid)(suspend_state_t state); int (*begin)(suspend_state_t state); int (*prepare)(void); int (*prepare_late)(void); int (*enter)(suspend_state_t state); void (*wake)(void); void (*finish)(void); bool (*suspend_again)(void); void (*end)(void); void (*recover)(void); }; struct platform_s2idle_ops { int (*begin)(void); int (*prepare)(void); int (*prepare_late)(void); void (*check)(void); bool (*wake)(void); void (*restore_early)(void); void (*restore)(void); void (*end)(void); }; ....
: `struct platform_suspend_ops` 에서 반드시 구현해야 하는 함수는 `enter` 콜백 함수다. 이 함수는 최종적으로 시스템을 SUSPEND 상태로 진입시키는 함수다. 그런데, 거의 대부분의 벤더사들은 `enter` 콜백 함수와 함께 `valid` 함수도 함께 제공한다. 왜냐면, 이 함수를 구현하지 않으면 애초에 시스템 SUSPEND가 시작되는 `enter_state` 함수에서부터 failed이 발생한다.
: `enter_state` 함수 도입부에 `valid_state` 라는 함수가 보인다. 이 함수는 현재 CPU가 인자로 전달받은 SUSPEND 상태를 지원하는지를 판별한다. 그런데, 상태는 둘 째 치고, `valid_state` 함수에서 볼 수 있다시피, `.valid`와 `.enter` 콜백 함수는 무조건 제공해야 한다. 이 2개의 콜백 함수를 제공하지 않을 경우, `System suspend`는 종료된다.
/kernel/power/suspend.c .... static bool valid_state(suspend_state_t state) { /* * The PM_SUSPEND_STANDBY and PM_SUSPEND_MEM states require low-level * support and need to be valid to the low-level implementation. * * No ->valid() or ->enter() callback implies that none are valid. */ return suspend_ops && suspend_ops->valid && suspend_ops->valid(state) && suspend_ops->enter; } .... static int enter_state(suspend_state_t state) { int error; trace_suspend_resume(TPS("suspend_enter"), state, true); if (state == PM_SUSPEND_TO_IDLE) { #ifdef CONFIG_PM_DEBUG if (pm_test_level != TEST_NONE && pm_test_level <= TEST_CPUS) { pr_warn("Unsupported test mode for suspend to idle, please choose none/freezer/devices/platform.\n"); return -EAGAIN; } #endif } else if (!valid_state(state)) { return -EINVAL; } ..... } ....
: 칩 벤더사는 아키텍처 독립적인 기능들으 구현하기 위해 위에 2가지 구조체에서 제공하는 콜백 함수들을 구현해야 한다. 주의할 점은 위의 구조체 함수들은 직접 호출하는 용도로 사용하면 안된다. 왜냐면, `System PM`이 각 단계에서 상황에 맞게 함수를 호출하기 때문이다. 아래 플로우 차트는 `System PM` 의 전체적인 흐름을 보여준다. 아래에서 `platform PM ops`는 CPU 관련 코드라고 보면 된다. 그리고, `device PM ops`는 CPU를 제외한 시스템에 존재하는 모든 주변 장치들에게 각각 적용되는 코드라고 보면 된다.
https://blog.csdn.net/taochao90/article/details/79799434: 각 칩 벤더사는 위의 구조체들을 구현하고 나서, 아래의 함수들을 통해 해당 구조체를 시스템에게 제공해야 한다. `suspend_set_ops` 함수는 전달받은 인자를 `kernel/power/suspend.c::suspend_ops` 변수에 저장해서 사용한다.
// include/linux/suspend.h - v6.5 .... /** * suspend_set_ops - set platform dependent suspend operations * @ops: The new suspend operations to set. */ extern void suspend_set_ops(const struct platform_suspend_ops *ops); .... extern void s2idle_set_ops(const struct platform_s2idle_ops *ops); .... // kernel/power/suspend.c - v6.5 .... static const struct platform_suspend_ops *suspend_ops; static const struct platform_s2idle_ops *s2idle_ops; ....
: 그런데, 만약에 칩 제조사에서 `struct platform_suspend_ops` \ `struct platform_s2idle_ops` 를 제공하지 않으면 어떻게 될까? 커널은 한 번더 래핑을 한다. 예를 들어, `platform_suspend_prepare` 함수안에 `suspend_ops->prepare` 함수가 존재하지 않을 경우, 아무 기능도 실행하지 않고 `0`을 반환한다.
// kernel/power/suspend.c - v6.5 .... static int platform_suspend_prepare(suspend_state_t state) { return state != PM_SUSPEND_TO_IDLE && suspend_ops->prepare ? suspend_ops->prepare() : 0; } static int platform_suspend_prepare_late(suspend_state_t state) { return state == PM_SUSPEND_TO_IDLE && s2idle_ops && s2idle_ops->prepare ? s2idle_ops->prepare() : 0; } .... static void platform_recover(suspend_state_t state) { if (state != PM_SUSPEND_TO_IDLE && suspend_ops->recover) suspend_ops->recover(); } static bool platform_suspend_again(suspend_state_t state) { return state != PM_SUSPEND_TO_IDLE && suspend_ops->suspend_again ? suspend_ops->suspend_again() : false; } ....
: `arm64`에서는 이 내용이 어떻게 구현되어 있을까?
- Arm64 PSCI
: x86에 `ACPI`가 있듯이, arm에는 `PSCI`가 있다. `PSCI`는 `Power State Coordination Interface`의 약자로 arm의 `power management standard interface`라고 생각하면 된다. `PSCI`는 주로 CPU Core에 관해서만 설명하고 있다. `DVFS` 및 `Device power management` 관련 내용을 궁금하다면, `SCMI`를 참고하자. 아래는 리눅스 커널에서 `suspend_ops->enter` 함수 호출후에 PSCI 스펙에 따른 arm64 함수 호출 과정을 나타낸다.
http://www.wowotech.net/pm_subsystem/491.html: `arm64` 같은 경우는 `suspend_ops->enter` 에 등록되는 함수가 `psci_system_suspend_enter` 다. 그리고, `arm64` 같은 경우는, `suspend_ops->valid`에 `suspend_valid_only_mem` 함수를 등록해서 오직 `suspend-to-ram` 모드만 지원하도록 강제한다.
// kernel/power/suspend.c - v6.5 .... /** * suspend_valid_only_mem - Generic memory-only valid callback. * @state: Target system sleep state. * * Platform drivers that implement mem suspend only and only need to check for * that in their .valid() callback can use this instead of rolling their own * .valid() callback. */ int suspend_valid_only_mem(suspend_state_t state) { return state == PM_SUSPEND_MEM; } EXPORT_SYMBOL_GPL(suspend_valid_only_mem); .... // drivers/firmware/psci/psci.c - v6.5 .... static int psci_system_suspend_enter(suspend_state_t state) { return cpu_suspend(0, psci_system_suspend); } .... static const struct platform_suspend_ops psci_suspend_ops = { .valid = suspend_valid_only_mem, .enter = psci_system_suspend_enter, }; .... static void __init psci_init_system_suspend(void) { int ret; if (!IS_ENABLED(CONFIG_SUSPEND)) return; ret = psci_features(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND)); if (ret != PSCI_RET_NOT_SUPPORTED) suspend_set_ops(&psci_suspend_ops); } ....
: `arm64`에서 일반적으로 `PSCI 펌웨어` 제공하는 기능을 사용하기 위해서는 `SMC` 명령어를 통해서 `PSCI`에게 요청을 전달해야 한다. `PSCI 펌웨어`에게 요청을 할 때는 `Secure Monitor Call Calling Convertion(SMCCC)`에 맞춰서 데이터를 전송해야 한다.
// include/uapi/linux/psci.h - v6.5 /* * PSCI v0.1 interface * * The PSCI v0.1 function numbers are implementation defined. * * Only PSCI return values such as: SUCCESS, NOT_SUPPORTED, * INVALID_PARAMS, and DENIED defined below are applicable * to PSCI v0.1. */ /* PSCI v0.2 interface */ #define PSCI_0_2_FN_BASE 0x84000000 #define PSCI_0_2_FN(n) (PSCI_0_2_FN_BASE + (n)) #define PSCI_0_2_64BIT 0x40000000 #define PSCI_0_2_FN64_BASE \ (PSCI_0_2_FN_BASE + PSCI_0_2_64BIT) #define PSCI_0_2_FN64(n) (PSCI_0_2_FN64_BASE + (n)) .... #define PSCI_1_0_FN_SYSTEM_SUSPEND PSCI_0_2_FN(14) .... #define PSCI_1_0_FN64_SYSTEM_SUSPEND PSCI_0_2_FN64(14) .... // drivers/firmware/psci/psci.c - v6.5 .... /* * While a 64-bit OS can make calls with SMC32 calling conventions, for some * calls it is necessary to use SMC64 to pass or return 64-bit values. * For such calls PSCI_FN_NATIVE(version, name) will choose the appropriate * (native-width) function ID. */ #ifdef CONFIG_64BIT #define PSCI_FN_NATIVE(version, name) PSCI_##version##_FN64_##name #else #define PSCI_FN_NATIVE(version, name) PSCI_##version##_FN_##name #endif .... static int psci_system_suspend(unsigned long unused) { phys_addr_t pa_cpu_resume = __pa_symbol(cpu_resume); return invoke_psci_fn(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND), pa_cpu_resume, 0, 0); } ....
: 아래 내용은 PSCI 펌웨어에서 제공하는 기능 중 하나인 `SYSTEM_SUSPEND`에 대한 내용이다. `SYSTEM_SUSPEND` 기능은 `suspend-to-ram`과 동일한 기능이라고 보면 된다. AArch32(32비트) 인지 AArch64(64비트)냐에 따라서, PSCI 펌웨어에 제공해야 하는 Function ID가 달라진다. `SYSTEM_SUSPEND` 기능을 요청할 때, 인자로 `entry_point_address`를 전송해야 한다. 이 주소는 CPU Core가 `resume` 되었을 때, 실행을 이어나갈 주소를 의미한다.
Arm Power State Coordination Interface: 리눅스 커널이 PSCI 펌웨어가 파워 매니지먼트 관련 기능을 요청하기 위해서는 `SMC` 명령어를 사용해야 한다. 이 때, `SMC` 호출을 편리하게 하기 위해 각 기능에 ID(번호)를 붙였다. 위에 `SYSTEM_SUSPEND` 같은 경우는 `0xE` 라고 볼 수 있다. 제공하는 총 개수는 32비트, 64비트 구분없이 `31(0x1F)` 개를 지원한다.
ARM-DEN-0028_SMC_Calling_Convention_v1_2_Non_Conf_REL.pdf: `invoke_psci_fn` 함수는 `suspend / resume` 함수에서만 사용하는 함수가 아니다. PSCI Firmware와 통신 및 컨트롤하기 위해서는 반드시 `invoke_psci_fn` 함수를 통해서 해야한다. 그래서 아래 그림을 보면, 파워 매니지먼트 관련 모든 동작들이 최종적으로는 `invoke_psci_fn` 함수에 수렴되는 것을 확인할 수 있다.
https://www.cnblogs.com/arnoldlu/p/6344847.html: `arm64`에서는 AP를 진정한 SUSPEND 상태로 진입시키 위해서 `PSCI 펌웨어`의 도움이 필요하다. 그런데, `PSCI 펌웨어`는 시스템의 파워 매니지먼트를 책임질만큼 권한이 강력한 프로그램이다. 그렇기 때문에, 일반 영역이 아닌 보안 영역에 존재한다. `arm64`에서는 `ARM Trusted Firmware`라고 해서 `BL31` 라는 펌웨어가 존재한다. 이 펌웨어안에 `PSCI 펌웨어`가 포함되어 있다. 그래서, 리눅스 커널이 `PSCI 펌웨어`를 제어하기 위해서는 SMC(`Secure Monitor Call`)을 통해서 파워 매니지먼트 요청을 해야 한다.
https://fredericb.info/2016/10/amlogic-s905-soc-bypassing-not-so.html
https://zhuanlan.zhihu.com/p/542967530Arm Trusted firmware BL31
: The Arm Trusted firmware BL31 consists of the PSCI, Secure monitor framework, and Secure partition manager.
Power state coordination interface - The application processor Trusted RAM firmware defines a Secure Monitor Call (SMC) interface to support rich OS power management in accordance with the PSCI system software on Arm systems.
....
- 참고 : https://developer.arm.com/documentation/102337/0000/Software-stack/Application-processor-firmware/Arm-Trusted-firmware-BL31: `get_set_conduit_method` 함수를 통해서 디바이스 트리에 작성된 PSCI의 정보를 파싱해온다. 그리고, `set_conduit` 함수를 통해서 `hvc`와 `smc` 중에 어떤 방식으로 PSCI 펌웨어와 통신할 것인지를 선택하게 된다.
// drivers/firmware/psci/psci.c .... typedef unsigned long (psci_fn)(unsigned long, unsigned long, unsigned long, unsigned long); static psci_fn *invoke_psci_fn; .... static __always_inline unsigned long __invoke_psci_fn_hvc(unsigned long function_id, unsigned long arg0, unsigned long arg1, unsigned long arg2) { struct arm_smccc_res res; arm_smccc_hvc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res); return res.a0; } static __always_inline unsigned long __invoke_psci_fn_smc(unsigned long function_id, unsigned long arg0, unsigned long arg1, unsigned long arg2) { struct arm_smccc_res res; arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res); return res.a0; } .... static void set_conduit(enum arm_smccc_conduit conduit) { switch (conduit) { case SMCCC_CONDUIT_HVC: invoke_psci_fn = __invoke_psci_fn_hvc; break; case SMCCC_CONDUIT_SMC: invoke_psci_fn = __invoke_psci_fn_smc; break; default: WARN(1, "Unexpected PSCI conduit %d\n", conduit); } psci_conduit = conduit; } static int get_set_conduit_method(const struct device_node *np) { const char *method; pr_info("probing for conduit method from DT.\n"); if (of_property_read_string(np, "method", &method)) { pr_warn("missing \"method\" property\n"); return -ENXIO; } if (!strcmp("hvc", method)) { set_conduit(SMCCC_CONDUIT_HVC); } else if (!strcmp("smc", method)) { set_conduit(SMCCC_CONDUIT_SMC); } else { pr_warn("invalid \"method\" property: %s\n", method); return -EINVAL; } return 0; } ....
: PSCI 버전에 따라 작성법은 다르지만, `method` 속성은 어떤 버전이든 존재하는 공통 속성이다. 이 속성은 PSCI firmware를 호출할 때, 어떤 방식으로 호출할 지를 결정한다. 위에서 `of_property_read_string(np, "method", &method)" 함수를 통해서 `method` 값에 `smc` 혹은 `hvc` 값이 들어오게 된다.
// https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/psci.txt - method : The method of calling the PSCI firmware. Permitted values are "smc" or "hvc" .... psci { compatible = "arm,psci-0.2", "arm,psci"; method = "smc"; .... };
: 최종적으로, `invoke_psci_fn` 함수에는 `method`가 `hvc`면, `__invoke_psci_fn_hvc` 함수가, `smc`면, `__invoke_psci_smc` 함수가 들어오게 된다.
: `cpu_suspend` 함수는 현재 사용되던 CPU 레지스터들을 모두 스택에 저장하고, 시스템을 SUSPEND로 보내는 함수다.
1. 현재 CPU 레지스터 저장 : __cpu_suspend_enter
2. 시스템 SUSPEND 진입 : fn(psci_system_suspend): 이 함수에서 또 중요한 부분은 `__cpu_suspend_enter` 함수의 플로우다. 이 함수는 마치 `fork`와 같이 한 번 호출되지만, 리턴값이 2번 반환된다. 그 원리에 대해서는 뒤에서 다시 다루도록 한다.
// arch/arm64/kernel/suspend.c - v6.5 /* * cpu_suspend * * arg: argument to pass to the finisher function * fn: finisher function pointer * */ int cpu_suspend(unsigned long arg, int (*fn)(unsigned long)) { int ret = 0; unsigned long flags; struct sleep_stack_data state; struct arm_cpuidle_irq_context context; /* Report any MTE async fault before going to suspend */ mte_suspend_enter(); /* * From this point debug exceptions are disabled to prevent * updates to mdscr register (saved and restored along with * general purpose registers) from kernel debuggers. * * Strictly speaking the trace_hardirqs_off() here is superfluous, * hardirqs should be firmly off by now. This really ought to use * something like raw_local_daif_save(). */ flags = local_daif_save(); /* * Function graph tracer state gets inconsistent when the kernel * calls functions that never return (aka suspend finishers) hence * disable graph tracing during their execution. */ pause_graph_tracing(); /* * Switch to using DAIF.IF instead of PMR in order to reliably * resume if we're using pseudo-NMIs. */ arm_cpuidle_save_irq_context(&context); ct_cpuidle_enter(); if (__cpu_suspend_enter(&state)) { /* Call the suspend finisher */ ret = fn(arg); /* * Never gets here, unless the suspend finisher fails. * Successful cpu_suspend() should return from cpu_resume(), * returning through this code path is considered an error * If the return value is set to 0 force ret = -EOPNOTSUPP * to make sure a proper error condition is propagated */ if (!ret) ret = -EOPNOTSUPP; ct_cpuidle_exit(); } else { ct_cpuidle_exit(); __cpu_suspend_exit(); } arm_cpuidle_restore_irq_context(&context); unpause_graph_tracing(); /* * Restore pstate flags. OS lock and mdscr have been already * restored, so from this point onwards, debugging is fully * reenabled if it was enabled when core started shutdown. */ local_daif_restore(flags); return ret; }
: 이 함수에서 또 중요한 부분은 `struct sleep_stack_data` 구조체다. 이 구조체 이름 그대로 시스템이 `SLEEP(SUSPEND)`에 들어갈 때, CPU 레지스터 정보를 저장하는 역할을 한다. 그래서, `state` 변수를 지역 변수로 선언해서 스택 주소를 할당받게 했다. 그리고, 해당 변수의 주소를, 즉, 스택 주소를 `__cpu_suspend_enter` 함수에게 전달해서 현재 CPU 레지스터 정보들을 스택에 저장할 수 있도록 한다. 참고로, `ctx`는 `context`의 약자이다.
: 왜 `struct cpu_suspend_ctx` 구조체에 `__aligned(16)`를 사용했을까? 아래 주석에서도 볼 수 있다시피, `AArch64`는 하드웨어적으로 스택 영역을 16바이트 정렬을 강제하고 있기 때문이다[참고1]. 즉, 이 말은 `stack pointer`가 스택 영역에 접근할 때, 16바이트를 기준으로만 접근한다는 소리다. 그래서 조금있다 어셈블리 코드를 보겠지만, 스택을 이용할 때, `push` 혹은 `pop` 명령어를 아예 사용하지 않는다. `stp x0, x1, [sp, #-16]!` 와 같은 방식으로 스택에 데이터를 저장한다.
// arch/arm64/include/asm/suspend.h - v6.5 .... #define NR_CTX_REGS 13 #define NR_CALLEE_SAVED_REGS 12 /* * struct cpu_suspend_ctx must be 16-byte aligned since it is allocated on * the stack, which must be 16-byte aligned on v8 */ struct cpu_suspend_ctx { /* * This struct must be kept in sync with * cpu_do_{suspend/resume} in mm/proc.S */ u64 ctx_regs[NR_CTX_REGS]; u64 sp; } __aligned(16); /* * Memory to save the cpu state is allocated on the stack by * __cpu_suspend_enter()'s caller, and populated by __cpu_suspend_enter(). * This data must survive until cpu_resume() is called. * * This struct desribes the size and the layout of the saved cpu state. * The layout of the callee_saved_regs is defined by the implementation * of __cpu_suspend_enter(), and cpu_resume(). This struct must be passed * in by the caller as __cpu_suspend_enter()'s stack-frame is gone once it * returns, and the data would be subsequently corrupted by the call to the * finisher. */ struct sleep_stack_data { struct cpu_suspend_ctx system_regs; unsigned long callee_saved_regs[NR_CALLEE_SAVED_REGS]; }; ....
: `__cpu_suspend_enter` 함수는 `cpu_suspend` 함수로부터 전달받은 스택 영역(`struct sleep_stack_data `)에 현재 CPU 레지스터들을 모두 저장한다.
// arch/arm64/kernel/asm-offsets.c - v6.5 .... #ifdef CONFIG_CPU_PM DEFINE(CPU_CTX_SP, offsetof(struct cpu_suspend_ctx, sp)); DEFINE(MPIDR_HASH_MASK, offsetof(struct mpidr_hash, mask)); DEFINE(MPIDR_HASH_SHIFTS, offsetof(struct mpidr_hash, shift_aff)); DEFINE(SLEEP_STACK_DATA_SYSTEM_REGS, offsetof(struct sleep_stack_data, system_regs)); DEFINE(SLEEP_STACK_DATA_CALLEE_REGS, offsetof(struct sleep_stack_data, callee_saved_regs)); #endif .... // arch/arm64/kernel/sleep.S - v6.5 /* * Save CPU state in the provided sleep_stack_data area, and publish its * location for cpu_resume()'s use in sleep_save_stash. * * cpu_resume() will restore this saved state, and return. Because the * link-register is saved and restored, it will appear to return from this * function. So that the caller can tell the suspend/resume paths apart, * __cpu_suspend_enter() will always return a non-zero value, whereas the * path through cpu_resume() will return 0. * * x0 = struct sleep_stack_data area */ SYM_FUNC_START(__cpu_suspend_enter) stp x29, lr, [x0, #SLEEP_STACK_DATA_CALLEE_REGS] stp x19, x20, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+16] stp x21, x22, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+32] stp x23, x24, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+48] stp x25, x26, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+64] stp x27, x28, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+80] /* save the sp in cpu_suspend_ctx */ mov x2, sp str x2, [x0, #SLEEP_STACK_DATA_SYSTEM_REGS + CPU_CTX_SP] /* find the mpidr_hash */ ldr_l x1, sleep_save_stash mrs x7, mpidr_el1 adr_l x9, mpidr_hash ldr x10, [x9, #MPIDR_HASH_MASK] /* * Following code relies on the struct mpidr_hash * members size. */ ldp w3, w4, [x9, #MPIDR_HASH_SHIFTS] ldp w5, w6, [x9, #(MPIDR_HASH_SHIFTS + 8)] compute_mpidr_hash x8, x3, x4, x5, x6, x7, x10 add x1, x1, x8, lsl #3 str x0, [x1] add x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS stp x29, lr, [sp, #-16]! bl cpu_do_suspend ldp x29, lr, [sp], #16 mov x0, #1 ret SYM_FUNC_END(__cpu_suspend_enter)
: callee-svaed registers 를 저장하면서 lr(x30)도 같이 스택에 저장하고 있다. lr 에는 __cpu_suspend_enter 함수로 복귀하는 주소가 들어있다. 이걸 왜 저장할까? 뒤에서 다시 다룬다.
: `add x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS`를 통해서 x0이 `struct sleep_stack_data.system_regs` 주소를 바라보도록 하고 있다. 이걸 통해 `__cpu_suspend_enter` 함수는 callee-saved register(`x19 ~ x28`) 저장하고, `cpu_do_suspend` 함수는 system register를 저장한다는 것을 알 수 있다.
: `cpu_do_suspend` 함수는 위에서 언급했던 것처럼, 시스템 레지스터들을 스택에 저장한다. cpu_do_suspend 함수에서 `get_this_cpu_...` 를 기준으로 앞쪽 코드는 시스템 레지스터를 CPU 범용 레지스터에 저장한다. 그리고, 뒤쪽 코드가 이 저장한 내용을 다시 스택에 저장하는 코드다. 근데, 왜 `시스템 레지스터 -> CPU 범용 레지스터 -> 스택` 과정을 거칠까? 시스템 레지스터 내용을 한 번에 스택에 저장할 수는 없을까? 없다. 시스템 레지스터의 값을 스택에 저장하려면, 반드시 범용 레지스터에 저장한 뒤에 스택으로 옮겨야 한다.
// arch/arm64/mm/proc.S - v6.5 #ifdef CONFIG_CPU_PM /** * cpu_do_suspend - save CPU registers context * * x0: virtual address of context pointer * * This must be kept in sync with struct cpu_suspend_ctx in <asm/suspend.h>. */ SYM_FUNC_START(cpu_do_suspend) mrs x2, tpidr_el0 mrs x3, tpidrro_el0 mrs x4, contextidr_el1 mrs x5, osdlr_el1 mrs x6, cpacr_el1 mrs x7, tcr_el1 mrs x8, vbar_el1 mrs x9, mdscr_el1 mrs x10, oslsr_el1 mrs x11, sctlr_el1 get_this_cpu_offset x12 mrs x13, sp_el0 stp x2, x3, [x0] stp x4, x5, [x0, #16] stp x6, x7, [x0, #32] stp x8, x9, [x0, #48] stp x10, x11, [x0, #64] stp x12, x13, [x0, #80] /* * Save x18 as it may be used as a platform register, e.g. by shadow * call stack. */ str x18, [x0, #96] ret SYM_FUNC_END(cpu_do_suspend)
: `cpu_do_suspend` 함수까지 호출되고 나면, `__cpu_suspend_enter` 함수는 마지막에 `mov x0, #1`을 통해서 리턴값을 `1`로 설정한다. 이 부분에 주목할 필요가 있다. `__cpu_suspend_enter` 함수가 `1을 반환할 경우에, `ret = fn(arg)` 코드가 실행되는데, 여기서 `fn`은 `psci_system_suspend` 함수가 된다. `psci_system_suspend` 함수는 `hvc` 및 `smc` 명령어를 통해서 bl31 펌웨어에게 시스템 SUSPEND 진입을 요청한다.
: 위에 과정까지 마치고 나면, ARM CPU는 `WFI` 상태에 있게 된다. 그리고, wake-up 인터럽트를 받는 시점에 `cpu_resume` 함수가 실행된다.
// arch/arm64/kernel/sleep.S - v6.5 SYM_CODE_START(cpu_resume) mov x0, xzr bl init_kernel_el mov x19, x0 // preserve boot mode #if VA_BITS > 48 ldr_l x0, vabits_actual #endif bl __cpu_setup /* enable the MMU early - so we can access sleep_save_stash by va */ adrp x1, swapper_pg_dir adrp x2, idmap_pg_dir bl __enable_mmu ldr x8, =_cpu_resume br x8 SYM_CODE_END(cpu_resume) .ltorg .popsection SYM_FUNC_START(_cpu_resume) mov x0, x19 bl finalise_el2 mrs x1, mpidr_el1 adr_l x8, mpidr_hash // x8 = struct mpidr_hash virt address /* retrieve mpidr_hash members to compute the hash */ ldr x2, [x8, #MPIDR_HASH_MASK] ldp w3, w4, [x8, #MPIDR_HASH_SHIFTS] ldp w5, w6, [x8, #(MPIDR_HASH_SHIFTS + 8)] compute_mpidr_hash x7, x3, x4, x5, x6, x1, x2 /* x7 contains hash index, let's use it to grab context pointer */ ldr_l x0, sleep_save_stash ldr x0, [x0, x7, lsl #3] add x29, x0, #SLEEP_STACK_DATA_CALLEE_REGS // --- 2 add x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS // --- 2 /* load sp from context */ ldr x2, [x0, #CPU_CTX_SP] mov sp, x2 /* 1 */ /* * cpu_do_resume expects x0 to contain context address pointer */ bl cpu_do_resume #if defined(CONFIG_KASAN) && defined(CONFIG_KASAN_STACK) mov x0, sp bl kasan_unpoison_task_stack_below #endif ldp x19, x20, [x29, #16] ldp x21, x22, [x29, #32] ldp x23, x24, [x29, #48] ldp x25, x26, [x29, #64] ldp x27, x28, [x29, #80] ldp x29, lr, [x29] /* 1 */ mov x0, #0 // --- 3 ret SYM_FUNC_END(_cpu_resume)
1. `_cpu_resume`의 뒤쪽 부분에 `cpu_do_resume` 함수를 통해 스택에 저장된 시스템 레지스터에 내용을 복구하고, callee-saved registers 들을 복구하는 코드가 나온다. 그런데, 레지스터 복구 작업은 왜 뒤쪽에서 진행할까? `cpu_resume` 함수를 보면, 리눅스 커널이 최초의 시작되었을 때 사용했던 몇 가지 함수들을 호출한다. 그런데, 이 때, CPU 레지스터는 지저분해 질 수 밖에 없다. 즉, `cpu_resume` 함수가 호출된 이후에는 CPU 레지스터가 `__cpu_suspend_enter` 함수를 호출하기 이전 상태와 비슷해야 하므로, 가장 뒤쪽에서 복구 작업을 진행하는 것이다. resume 과정을 굳이 분리하자면, 아래와 같이 3개의 영역으로 구분할 수 있다.
2. `add x29, x0, #SLEEP_STACK_DATA_CALLEE_REGS` / `add x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS` 코드를 통해서 `x29`에는 `_cpu_suspend_enter` 함수에서 `callee-saved registers`들이 저장된 스택 주소가 들어간다. 그리고, `x0`에는 `cpu_do_suspend` 함수에서 `system registers`들이 저장된 스택 주소가 들어간다.
3. `_cpu_resume` 함수가 리턴값으로 `0`을 반환하는것에 주목할 필요가 있다(`mov x0, #0`).
이 글 앞부분에서 `
psci_system_suspend` 함수에서 CPU가 resume 시에 점프해야 하는 물리 주소를 SCP 에게 전달하는 코드가 있었다. 이제, CPU가 resume 을 할 때, 위에 어셈블리코드로 작성된 `cpu_resume` 심볼로 점프한다. 근데, cpu_resume 코드를 보면, 마지막에 ret 명령어가 있는데, 이건 어디로 복귀할까?
static int psci_system_suspend(unsigned long unused) { phys_addr_t pa_cpu_resume = __pa_symbol(cpu_resume); return invoke_psci_fn(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND), pa_cpu_resume, 0, 0); }
: __cpu_suspend_enter 함수에 제일 처음 코드가 `stp x29, lr, [x0, #SLEEP_STACK_DATA_CALLEE_REGS]` 다. lr(x30) 에는 __cpu_suspend_enter 의 주소가 들어가게 된다. 즉, cpu_resume 에서 ret 명령어를 실행하면, 리턴값을 0으로 세팅하고 __cpu_suspend_enter 함수로 복귀한다. 그리고, 0을 반환하면서 __cpu_suspend_exit 함수를 실행하게 된다(1을 반환하면, SYSTEM_SUSPEND 과정을 다시 밟게된다).: 시스템이 SUSPEND 되기 전에는 __cpu_suspend_enter 함수에서 1을 리턴하고 psci_system_suspend 함수가 호출된다. 이 함수는, 시스템을 SUSPEND 상태로 집어넣고, CPU가 resume 될 때, 어디로 점프해야 할 지를 지정한다. 정리하면, 다음과 같다.
1. 빨간선이 먼저 수행되는 플로우다. 즉, __cpu_suspend_enter 함수는 먼저 0을 반환해서 psci_system_suspend 함수를 호출한다. 그리고, CPU가 resume 될 때, 점프할 cpu_resume 주소를 지정한다.
2. 파란선은 CPU resume 시에 수행되는 플로우다. 이 시점에 중요한 건, 스택에 저장되어 있는 정보다. 왜냐면, cpu_resume 마지막에 ret 명령어가 있는데, 이 때 복귀주소(__cpu_suspend_enter)가 스택에 저장되어있기 때문이다.'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] LDM - kobj_type & sysfs & kernfs (0) 2023.09.23 [리눅스 커널] LDM - kobject, kset, ktype (0) 2023.09.22 [리눅스 커널] PM - restart & shutdown & halt (0) 2023.09.18 [리눅스 커널] Timer - Broadcast timer (0) 2023.09.17 [리눅스 커널] Cpuidle - idle process (0) 2023.09.12