ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Platform-dependent power management
    Linux/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/542967530
    Arm 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)가 스택에 저장되어있기 때문이다. 
Designed by Tistory.