ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - System Power Management
    Linux/kernel 2023. 8. 3. 02:32

    글의 참고

    - https://www.kernel.org/doc/html/next/driver-api/pm/devices.html#driverapi-pm-devices

    - https://lwn.net/Articles/505683/

    - http://www.wowotech.net/pm_subsystem/suspend_and_resume.html

    - Mastering Linux Device Driver Development: Write custom device drivers to support computer peripherals in Linux operating systems by John Madieu

    - https://patchwork.kernel.org/project/linux-arm-kernel/patch/20170718001925.25286-1-f.fainelli@gmail.com/

    - https://lwn.net/Articles/643892/

    - https://lists.archive.carbon60.com/linux/kernel/3541269

    - https://elinux.org/Pm_Sub_System

    - https://elixir.bootlin.com/linux/v4.17/source/Documentation/power/swsusp.txt

    - http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

    - http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html

    - http://www.wowotech.net/pm_subsystem/suspend-irq.html

    - https://www.cnblogs.com/arnoldlu/p/6253665.html

    - https://www.cnblogs.com/Linux-tech/p/13873881.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.


    글의 내용

    - Swsusp [ 참고1] 

    " `Software Suspend`의 약자로 `시스템 절전 모드`를 일컫는 말이다. 리눅스가 최초에 등장했을 때는, x86 전용 OS로 등장했다. 그래서 `swsusp`는 ACPI의 영향을 많이받아 만들어진 개념이다. ACPI에서 사용하는 S-STATE, C-STATE 등을 사용해서 파워 매니지먼트 상태를 표현한다. 예를 들어, `S-STATE`는 시스템 전체 파워 상태를 나타내고, `C-STATE`는 각 CPU의 파워 상태를 나타낸다. 


    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

     

    - Four Sleep States

    : 시스템 파워 매니지먼트는 시스템 전체를 타겟으로 한다. 시스템 전체를 low-power-mode 로 집어넣기 때문에 그 만큼 가장 적은 전류를 소모하게 된다. 그러나, 그 만큼 사용자 응답성이 늦어지는 것 또한 문제다. 즉, resume latency가 증가한다. 아키텍처마다 다르지만, 리눅스에서 시스템 파워 매니지먼트 기본적으로 4가지 상태가 존재한다.

    suspend to idle(freeze)" 유일하게 소프트웨어만으로 진입이 가능한 절전 모드다. 즉, 플랫폼에 독립적인 모드다. 이 모드를 제외하고는 나머지 3개의 모드는 플랫폼마다 지원할 수 도 있고 안할 수 도 있다. 이 모드에서는 CPU를 최대 `deep idle` 상태로 진입시킨다. 이 모드는 제일 먼저 모든 유저 스페이스 frozen 시키고, 모든 I/O 디바이스를 low-power-mode로 진입시킨다. 앞에 2가지 조건이 만족되면 이제 프로세서는 idle 상태를 오랫동안 유지할 수 있는 조건이 완성된다. 즉, 이 모드는 아래와 같이 표현할 수 있다.
    suspend to idle = frozen processes + suspended all devices + idle processors
    `suspend-to-idle`로 진입하기 위해서는 `echo freeze > /sys/power/state` 명령어로 진입할 수 있다. 이 모드는 순수 소프트웨어로 동작하기 때문에 `CONFIG_SUSPEND`만 설정되어 있다면, 항시 지원되는 모드다. 이 모드의 가장 큰 장점은 하드웨어 resme latency가 없다.

    power-on standby(standby)" 이 모드는 `suspend-to-idle`에서 한 가지가 더 추가된 모드다.
    power-on standby = suspend-to-idle + power-off non-boot CPU`s
    이 모드를 실행하기 위해서는 `echo standby > /sys/power/state`를 사용하면 된다.

    suspend to ram(mem)" 이 모드는 `power-on standby`에서 몇 가지 더 추가된 모드다.
    suspend-to-ram = power-on standby + power-off CPU`s + memory self-refresh
    RAM은 데이터를 유지하기 위해 지속적으로 `self-refresh`한다. 이 모드에서는 RAM만 동작하는 모드라고 보면된다. 그리고, 이 모드로 진입하기 전에 system과 devices는 RAM에 이전 상태를 저장한다. 이 모드를 진입하기 위해서는 `echo mem > /sys/power/state`를 입력하면 된다.

    그런데, 이 모드의 실제 동작을 결정하는 주체는 따로 있다. 즉, `echo mem > /sys/power/state` 명령어를 입력하면, `/sys/power/mem_sleep` 파일에 작성된 값에 따라 `suspend-to-ram`의 동작이 달라진다. `mem_sleep` 파일에 쓰기가 가능한 모드는 아래와 같다.
    s2idle : `suspend-to-idle`과 동일한 동작을 한다. 즉, 이 모드는 아키텍처에 의존적이지 않다. 그래서 무조건 사용이 가능한 모드다.
    shallow :  `power-on standby` 모드와 동일한 동작을 한다. 이 모드는 아키텍처에 의존적인 모드다. 즉, 아키텍처적으로 standby mode가 지원이 되야 이 모드를 소프트웨어적으로 구현이 가능하다.
    deep : 이 모드가 실제 `suspend-to-ram`과 동일한 모드다. 이 모드 역시 아키텍처에 의존적이다.
    `mem_sleep`에 지원 가능한 모드를 확인하려면, 그냥 읽으면 된다. 현재 사용중인 모드인 경우 `[]`로 닫혀있다. `mem_sleep`의 기본값은 `deep` 혹은 `s2idle`이다.
    $ cat /sys/power/mem_sleep
    s2idle [deep]

    suspend to disk(hibernation)" 이 모드는 그냥 시스템 전체를 power-off 한다. RAM도 power-off 한다. 그런데, power-off 하기 전에 종료 직전 메모리의 상태(스냅샷)를 이미지로 만들어서 비휘발성 메모리에 저장한다. 이 이미지를 `hibernation image`라고 한다. 흔히 `스냅샷`이라고도 부른다. 이 모드는 가장 긴 resume latency가 걸린다. 그래도, full boot-on sequence 보다는 시간이 적게 소요된다.

    이 모드를 진입하기 위해서는 `echo disk > /sys/power/state` 명령어를 입력하면 된다. `echo disk > /sys/power/state` 명령어가 실행되고, 현재 메모리의 상태를 이미지로 만들고 디스크에 쓴 뒤에, 어떻게 동작할지를 정할 수 가 있다. 즉, 메모리를 디스크에 쓴 후 어떻게 동작할지를 정하는 별도의 파일이 존재한다. 그 파일이 `/sys/power/disk` 파일이다. 이 파일에서 지원해주는 모드는 다음과 같다.
    platform : 플랫에 의존적인 모드다. 주로, BIOS에 의해서 구현된다.
    shutdown : 시스템을 Power-off 한다.
    reboot : 시스템을 Reboot 한다(시스템 진단용으로 자주 사용).
    suspend : `suspend-to-ram`과 동일하게 동작한다. 즉, `echo disk > /sys/power/state` 명령어를 사용하면 `suspend-to-ram` 상태가 되는 것이다. 그 이후에 시스템이 정상적으로 wakeup 되면, 기존에 디스크에 저장해놓은 스냅샷을 삭제한다. 그렇지 않을 경우, 스냅샷을 이미지를 램에 복구해서 시스템이 원복한다. 
    test_resume :
    `suspend-to-disk`에 지원 가능한 모드를 확인하려면, `/sys/power/disk` 파일을 `cat`으로 읽으면 된다. 현재 사용중인 모드인 경우 `[]`로 닫혀있다.
    $ cat /sys/power/disk 
    [platform] shutdown reboot suspend test_resume
    hibernation 기능을 사용하려면, `CONFIG_HIBERNATION` 설정이 되어있어야 한다. 이 외에도, hibernation은 이미지를 디스크의 어디에 저장할지도 알아야 한다. 리눅스에서 이 영역을 `스왑 파티션`이라고 한다.

    : `suspend-to-idle`은 아키텍처 독립적인 SW 모드이기 때문에, `suspend_noirq` 단계까지만 진행하고 그 뒤에 아키텍처 종속적인 코드들은 실행되지 않는다. 아래에서 더 자세히 살펴본다.


    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

     

    : 위에 상태들은 ACPI에 의해서 정의되어 있다. 순서대로 각각의 상태를 S0, S1, S3, S4 라고 부른다. 위 4가지 상태에서 가장 많이 사용하는 상태는 `suspend-to-ram` 모드다. 이 모드는 `wakealarm`을 지원하는 RTC를 통해서 반복 테스트를 해볼 수 있다.

     

    - Suspend Calling Sequence [ 참고1 ]

    : 리눅스 커널에서 `system suspend` 순서는 `child-before-parent` 다. 즉, 자식이 먼저 `suspend`되고, 부모가 `suspend` 된다. 위에서 자식과 부모는 드라이버가 아닌 디바이스를 기준으로 한다. 즉, suspend 과정은 드라이버가 등록된 순서를 기준이 아닌, 디바이스가 등록된 순서를 기준으로 한다. 이 말은 디바이스가 커널에 빨리 등록될 수 록 `suspend`가 늦게 호출되고, 늦게 등록될 수 록 `suspend`가 빨리 호출된다는 뜻이다. 왜냐면, 빨리 등록된 디바이스일 수 록 상위 계층 디바이스일 확률이 높기 때문이다. 예를 들어, PCI 버스 디바이스는 반드시 일반 PCI 디바이스나 PCI 브릿지보다 먼저 커널에 등록되어야 한다. `resume`에서는 `suspend` 반대 순서로 먼저 등록된 디바이스(상위 계층)이 먼저 재개되고, 늦게 등록된 디바이스(하위 계층)가 늦게 재개된다. 아래의 함수들은 `system suspend` 과정에서 시스템 전역적으로 한 번만 호출되는 함수(접두사 `system`)와 디바이스마다 호출되는 함수(접두사 `per-device`)로 구분한 것이다.

    " system wide begin()
    " per-device suspend()
    " system wide prepare()
    " per-device suspend_late()
    " system wide disable (almost) all interrupt handlers
    " per-device suspend_noirq()
    " system wide prepare_late()
    " disable nonboot CPU`s
    " syscore_suspend()
    " system wide enter()

     

    " 위의 `system-wide suspend` 과정을 보면, 간단하게 정리하면 시퀀스는 아래와 같다. `resume` 은 위의 순서를 반대로해서 진행한다.

    1. 프로세스를 동결(freeze)
    2. 모든 디바이스를 suspend
    3. 아키텍처 종속적인 suspend

     

    " 디바이스 트리가 등장하면서 디바이스 등록 순서가 DTS에 명시한 순서대로 커널에 등록이 되었다. 즉, DTS 파일에서 일찍 나타날 수 록 커널에 일찍 등록이 되었다(`__platform_populate`). 이렇게 되면 디바이스간에 계층 관계가 무너져서 suspend / resume 과정이 더욱 복잡해지게 된다. 이 문제에 대한 해결책으로 등장한 것이 드라이버의 `probe` 함수에서 `-EPROBE_DEFER`을 반환하는 것이다. `probe` 함수에서 `-EPROBE_DEFER` 값을 반환하면, 자신의 의존하고 있는 장치 드라이버가 아직 준비가 되지 않았음을 알리면서 동시에 자신의 `probe` 또한 연기해야 한다는 것을 커널 드라이버 프레임워크에 알린다.

     

    " 리눅스 커널 드라이버 프레임워크는 특정 드라이버에게 `-EPROBE_DEFER`를 받으면, 나중에 해당 드라이버의 프로브를 다시 호출해야 함으로 리스트 뒤쪽에 추가한다. 그리고, workqueue가 이 리스트에 있는 프로브를 순차적으로 탐색해서 실행한다. 리누스 커널 드라이버 프레임워크는 드라이버간에 종속 관계를 모르기 때문에, 의존 드라이버가 실행되고나서 해당 드라이버를 실행시키는 메커니즘이 없다. 그냥 의존성이 걸리면, 리스트 뒤쪽에 추가해서 나중에 다시 호출할 뿐이다. 근데, 이 말은 `-EPROBE_DEFER`가 `suspend / resume` 에서 디바이스들 간의 순서를 바꿀 수 도 있다는 소리다.

     

    " `deferred_probe_work_func` 함수는 워크큐를 통해서 `deferred probe`를 실행시키는 함수다. 중요한 건 이 함수에서 사용되는 `device_pm_move_to_tail` 함수다. 그리고 위에 주석이 중요하다. `dpm_list`에 디바이스를 추가하는 순서가 드라이버의 probe가 호출되는 순서와 동일하다. PM은 이 probe를 기준으로 장치들이 `dpm_list`에 추가된 순서가 suspend sequence 시에 가장 적합하다고 가정한다. 그런데, 여기서 `-EPROBE_DEFER` 값을 반환하면 당연히 suspend 시에 문제가 생긴다. 그래서 `-EPROBE_DEFER`를 반환한 드라이버의 디바이스도 `dpm_list` 뒤쪽으로 옮긴다. `device_pm_move_to_tail` 함수가 그 역할은 한다.

    // drivers/base/dd.c - v6.5
    /*
     * deferred_probe_work_func() - Retry probing devices in the active list.
     */
    static void deferred_probe_work_func(struct work_struct *work)
    {
    	struct device *dev;
    	struct device_private *private;
    	/*
    	 * This block processes every device in the deferred 'active' list.
    	 * Each device is removed from the active list and passed to
    	 * bus_probe_device() to re-attempt the probe.  The loop continues
    	 * until every device in the active list is removed and retried.
    	 *
    	 * Note: Once the device is removed from the list and the mutex is
    	 * released, it is possible for the device get freed by another thread
    	 * and cause a illegal pointer dereference.  This code uses
    	 * get/put_device() to ensure the device structure cannot disappear
    	 * from under our feet.
    	 */
    	mutex_lock(&deferred_probe_mutex);
    	while (!list_empty(&deferred_probe_active_list)) {
    		private = list_first_entry(&deferred_probe_active_list,
    					typeof(*dev->p), deferred_probe);
    		dev = private->device;
    		list_del_init(&private->deferred_probe);
    
    		get_device(dev);
    
    		__device_set_deferred_probe_reason(dev, NULL);
    
    		/*
    		 * Drop the mutex while probing each device; the probe path may
    		 * manipulate the deferred list
    		 */
    		mutex_unlock(&deferred_probe_mutex);
    
    		/*
    		 * Force the device to the end of the dpm_list since
    		 * the PM code assumes that the order we add things to
    		 * the list is a good order for suspend but deferred
    		 * probe makes that very unsafe.
    		 */
    		device_pm_move_to_tail(dev);
    
    		dev_dbg(dev, "Retrying from deferred list\n");
    		bus_probe_device(dev);
    		mutex_lock(&deferred_probe_mutex);
    
    		put_device(dev);
    	}
    	mutex_unlock(&deferred_probe_mutex);
    }
    static DECLARE_WORK(deferred_probe_work, deferred_probe_work_func);

     

    " 참고로, `deferred probe` 에서는 `pending probe list`와 `active probe list` 리스트가 존재한다. `driver_deferred_probe_add` 함수를 통해서 일단 `deferred probe`들을 `pending probe list`에 저장한다. 그리고, 특정 시점에 `driver_deferred_probe_trigger` 함수가 호출되면, `pending probe list` 있는 내용을 `active probe list`에 옮긴다. 내용을 옮기고 나서, `deferred_probe_work_func` 함수를 워크큐에 큐잉한다.

     

    " `device_pm_move_to_tail` 함수의 주석을 보면 기능을 바로 알 수 있다. `deferred probe`를 요청한 디바이스 하나만 뒤쪽으로 이동하는게 아니라, `children`과 `consumers` 들을 함께 뒤로 이동시킨다.

    // drivers/base/core.c - v6.5
    /**
     * device_pm_move_to_tail - Move set of devices to the end of device lists
     * @dev: Device to move
     *
     * This is a device_reorder_to_tail() wrapper taking the requisite locks.
     *
     * It moves the @dev along with all of its children and all of its consumers
     * to the ends of the device_kset and dpm_list, recursively.
     */
    void device_pm_move_to_tail(struct device *dev)
    {
    	int idx;
    
    	idx = device_links_read_lock();
    	device_pm_lock();
    	device_reorder_to_tail(dev, NULL);
    	device_pm_unlock();
    	device_links_read_unlock(idx);
    }

     

    - Remote Wakeup

    " 우리는 `원격`이라는 단어를 떠올리면 조건반사적으로 `무선`이라는 단어가 제일 먼저 떠오르는 것 같다. 리눅스 커널에서 사용되는 `Remote Wakeup` 이란, 외부 인터럽트를 의미한다(`무선 네트워크를 통한 웨이크-업`도 포함). 여기서 말하는 외부 인터럽트는 보드 내부의 외부 디바이스가 발생시킨 인터럽트를 의미하는게 아니다. 보드를 내부로 보고, 보드 밖의 환경을 외부로 본 것이다.

    When a device has been suspended, it generally doesn’t resume until the computer tells it to. Likewise, if the entire computer has been suspended, it generally doesn’t resume until the user tells it to, say by pressing a power button or opening the cover.

    However some devices have the capability of resuming by themselves, or asking the kernel to resume them, or even telling the entire computer to resume. This capability goes by several names such as “Wake On LAN”; we will refer to it generically as “remote wakeup”. When a device is enabled for remote wakeup and it is suspended, it may resume itself (or send a request to be resumed) in response to some `external event`. Examples include a suspended keyboard resuming when a key is pressed, or a suspended USB hub resuming when a device is plugged in.

    - 참고 : https://www.kernel.org/doc/html/v4.13/driver-api/usb/power-management.html

     

    " 예를 들어, PC가 절전 모드에 들어가면 누가 깨워줄까? PC 내부에도 자체적으로 `resume`이 가능한 디바이스들이 있을 것이다. 그런데, 이건 `Remote Wakeup`이라고 하지 않는다. 키보드의 키를 누르거나, 마우스를 움직이거나, 오래동안 방치한 핸드폰에 USB를 꽂는다거나 하는 동작들이 모두 `Remote Wakeup`에 속한다. 이 동작들의 특징을 보면, 보드 내부에서 아닌, 보드 외부에서 발생한 동작이라는 것을 알 수 있다.

     

    - Suspend test

    : 리눅스 커널에는 시스템 suspend를 테스트할 수 있는 방법을 제공한다. 바로 `RTC`를 이용하는 것이다. 시스템에 존재하는 RTC의 `wakealarm`을 이용해서 테스트해 볼 수 있다. 먼저, RTC에 몇초 뒤에 `wakealarm`을 발생시키겠다는 설정을 한 뒤, 시스템을 suspend 상태로 진입시키면 된다.

     

     

    - System-wide suspend

    : `system-wide suspend` 과정을 분석하기전에 가볍게 `suspend-to-ram` 함수 호출 다이어그램을 보고가자. `suspend-to-ram`은 사용자가 `echo mem > /sys/power/state`를 입력하면, 제일 먼저 `enter_state` 함수가 호출된다. 그리고, 마지막으로 `suspend_ops->enter` 함수가 `system-wide suspend` 과정에서 호출되는 마지막 함수가 된다(최종적으로 `suspend_ops->enter` 내부에서는 아키텍처 종속적인 코드들이 동작하게 된다).


    http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html

     

    : `system suspend` 과정은 단순히 PM 코어만 동작하는 과정이 아니다. `PM 코어`, `Device PM`, `Platform dependent PM` 등 많은 서브 시스템들이 참여하는 과정이다.

    1. PM Core
    kernel/power/main.c" 유저 레벨 인터페이스 제공 (`/sys/power/state`)
    kernel/power/suspend.c" suspend 주요 기능 포함
    kernel/power/suspend_test.c" suspend 테스트 기능 로직 
    kernel/power/console.c" suspend 과정에서 콘솔 관련 로직 처리
    kernel/power/process.c" suspend 과정에서 프로세스 관련 로직 처리

    2. Device PM
    drivers/base/power/*" 이 글을 참고하자.

    3. Platform dependent PM
    include/linux/suspend.h" 플랫폼에 종속적인 PM 관련 기능들을 정의
    arm64를 기준으로 /drivers/firmware/psci/psci.c 파일에 플랫폼 종속적인 PM 정의

     

    : 리눅스 커널의 system suspend & resume 과정은 아래와 같이 진행된다.

     


    http://www.wowotech.net/pm_subsystem/suspend_and_resume.html

    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

     

    " suspend의 시작은 유저가 sysfs를 통해서 시작된다. 즉, 아래의 명령어 중 하나만 실행해도 `suspend` 과정은 시작된다.

    1. echo "freeze" > /sys/power/state
    2. echo "standby" > /sys/power/state
    3. echo "mem" > /sys/power/state

     

    " `/sys/power/state`는 `state_store` 함수에 매핑되어 있다. 먼저, `power_attr` 매크로는 인자로 받은 값뒤에 `_store`와 `_show` 함수를 찾아서 sysfs에 export한다. 여기서는 `state_store` 함수만 가져왔지만, 실제로 `/kernel/power/main.c`파일에는 `state_show` 함수도 정의되어 있다.

    //kernel/power/main.c - v6.5
    static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
    			   const char *buf, size_t n)
    {
    	suspend_state_t state;
    	int error;
    
    	error = pm_autosleep_lock();
    	if (error)
    		return error;
    
    	if (pm_autosleep_state() > PM_SUSPEND_ON) {
    		error = -EBUSY;
    		goto out;
    	}
    
    	state = decode_state(buf, n);
    	if (state < PM_SUSPEND_MAX) {
    		if (state == PM_SUSPEND_MEM)
    			state = mem_sleep_current;
    
    		error = pm_suspend(state);
    	} else if (state == PM_SUSPEND_MAX) {
    		error = hibernate();
    	} else {
    		error = -EINVAL;
    	}
    
     out:
    	pm_autosleep_unlock();
    	return error ? error : n;
    }
    
    power_attr(state);

     

    " 제일 먼저 하는게 `autosleep`이 실행되지 못하게 막는다. 왜냐면, autosleep을 lock하지 않으면, suspend 과정이 2번이 진행될 수 있기 때문이다.

     

    " `system suspend` 전 과정에서 `suspend_state_t`는 계속해서 나오기 때문에 익혀놓는 것이 좋다. 대개 조건으로 `state > PM_SUSPEND_ON`이 많이나오는데, 이 조건은 대개 SLEEP의 유효성을 판단하다. 왜냐면, SLEEP에 들어가는 상태들이 모두 `0`보다 크기 때문이다. 일반적으로는 MAX 값들은 ENUM에서 올바르지 못한 매개변수로 판단하는 기준이 되는데, `system suspend`에서 MAX 값은 hibernation을 의미한다. 즉, `hibernation` == `PM_SUSPEND_MAX` 임을 주의하자.

    //include/linux/suspend.h - v6.5
    typedef int __bitwise suspend_state_t;
    
    #define PM_SUSPEND_ON		((__force suspend_state_t) 0)
    #define PM_SUSPEND_TO_IDLE	((__force suspend_state_t) 1)
    #define PM_SUSPEND_STANDBY	((__force suspend_state_t) 2)
    #define PM_SUSPEND_MEM		((__force suspend_state_t) 3)
    #define PM_SUSPEND_MIN		PM_SUSPEND_TO_IDLE
    #define PM_SUSPEND_MAX		((__force suspend_state_t) 4)

     

    " `pm_suspend` 함수는 전달받은 인자의 유효성만 판단한다. 그리고, 실제 suspend의 시작은 `enter_state` 함수에서 시작된다.

    //kernel/power/suspend.c - v6.5
    ...
    static const char * const mem_sleep_labels[] = {
    	[PM_SUSPEND_TO_IDLE] = "s2idle",
    	[PM_SUSPEND_STANDBY] = "shallow",
    	[PM_SUSPEND_MEM] = "deep",
    };
    ...
    
    /**
     * pm_suspend - Externally visible function for suspending the system.
     * @state: System sleep state to enter.
     *
     * Check if the value of @state represents one of the supported states,
     * execute enter_state() and update system suspend statistics.
     */
    int pm_suspend(suspend_state_t state)
    {
    	int error;
    
    	if (state <= PM_SUSPEND_ON || state >= PM_SUSPEND_MAX)
    		return -EINVAL;
    
    	pr_info("suspend entry (%s)\n", mem_sleep_labels[state]);
    	error = enter_state(state);
    	if (error) {
    		suspend_stats.fail++;
    		dpm_save_failed_errno(error);
    	} else {
    		suspend_stats.success++;
    	}
    	pr_info("suspend exit\n");
    	return error;
    }
    EXPORT_SYMBOL(pm_suspend);

     

    " `suspend_stats` 전역 변수는 sysfs에 export 되는 변수다. 이 변수를 만든 이유는 유저 스페이스에서 `sleep cycle`이 어떻게 되는지 등을 디버깅하기 위해서 제공된다. 근데, `suspend_stats.success++` 코드의 위치는 왜 저기 있을까? 저 위치는 suspended 후에 resume이 마무리 되는 지점이다. 즉, 유저가 시스템 suspend의 성공 여부를 알게 되는 시점은 resume이 정상적으로 완료된 후에나 알 수 있게 된다.

    Userspace can't easily discover how much of a sleep cycle was spent in a hardware sleep state without using kernel tracing and vendor specific sysfs or debugfs files.

    To make this information more discoverable, introduce 3 new sysfs files:
    1) The time spent in a hw sleep state for last cycle.
    2) The time spent in a hw sleep state since the kernel booted
    3) The maximum time that the hardware can report for a sleep cycle.

    All of these files will be present only if the system supports s2idle.

    - 참고 : https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b52124a78ab34eb0754e32edc0c9996937779176

     

    " `enter_state` 함수는 시스템이 suspend 상태로 진입하기 위해서 원활한 환경을 제공하는 역할을 한다. 이 함수에서는 2가지 큰 역할을 맡고 있다.

    1. 메모리의 내용을 파일 시스템에 sync
    2. Freezing of task

     

    " 먼저 `sync` 관련 내용을 보자. `sync_on_suspend_enabled` 전역 변수를 통해서 사용자가에게 suspend에 진입하기 전에 메모리의 내용을 파일 시스템에 저장할 것인지에 대해 결정하도록 한다. 일반적으로는 `/sys/power/sync_on_suspend` 파일에 1 혹은 0 값을 씀으로써 컨트롤이 가능하다.

    //kernel/power/suspend.c - v6.5
    /**
     * enter_state - Do common work needed to enter system sleep state.
     * @state: System sleep state to enter.
     *
     * Make sure that no one else is trying to put the system into a sleep state.
     * Fail if that's not the case.  Otherwise, prepare for system suspend, make the
     * system enter the given sleep state and clean up after wakeup.
     */
    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;
    	}
    	if (!mutex_trylock(&system_transition_mutex))
    		return -EBUSY;
    
    	if (state == PM_SUSPEND_TO_IDLE)
    		s2idle_begin();
    
    	if (sync_on_suspend_enabled) {
    		trace_suspend_resume(TPS("sync_filesystems"), 0, true);
    		ksys_sync_helper();
    		trace_suspend_resume(TPS("sync_filesystems"), 0, false);
    	}
    
    	pm_pr_dbg("Preparing system for sleep (%s)\n", mem_sleep_labels[state]);
    	pm_suspend_clear_flags();
    	error = suspend_prepare(state);
    	if (error)
    		goto Unlock;
    
    	if (suspend_test(TEST_FREEZER))
    		goto Finish;
    
    	trace_suspend_resume(TPS("suspend_enter"), state, false);
    	pm_pr_dbg("Suspending system (%s)\n", mem_sleep_labels[state]);
    	pm_restrict_gfp_mask();
    	error = suspend_devices_and_enter(state);
    	pm_restore_gfp_mask();
    
     Finish:
    	events_check_enabled = false;
    	pm_pr_dbg("Finishing wakeup.\n");
    	suspend_finish();
     Unlock:
    	mutex_unlock(&system_transition_mutex);
    	return error;
    }

     

    " 파일 시스템 관련해서는 여러 가지 논쟁이 존재한다. 예전에는 무조건 `sys_sync` 함수를 통해서 suspend 진입하기전에 동기화를 했지만, 현재는 퍼포먼스 이슈로 인해 무조건적인 suspend는 추천하지 않고 있다. 그치만, 이것과 관련해서 또 무조건 해야한다는 의견도 있다. 왜냐면, suspended 상태에서 배터리 전원이 나가는 상황이 오면 이전 상태에 대해 복구가 불가능하기 때문이다[링크].

     

    " 그리고, `Freezing of task` 과정을 진행한다. 이 과정을 진행하는 이유는 하이버네이션 이미지를 만들 때, 프로세스가 살아있으면 파일 시스템이 깨지는 이유도 있거니와 디바이스를 suspend 상태로 진입시키기 위해서는 디바이스들에 대한 `reference count`를 가지고 있는 태스크들이 먼저 suspend 상태에 들어가 있어야 하기 때문이다. 자세한 내용은 이 글을 참고하자.

     

    " 그리고 `susend_prepare` 함수로 넘어가기전에, `valid_state` 함수에 대해 알아보자. 이 함수는 사용자가 입력한 절전 모드를 현재 시스템에서 지원하는지를 판별한다. 이 글 맨앞에서도 설명했지만, `suspend-to-idle(freeze)`을 제외하고는 모든 시스템 절전 모드는 아키텍처 종속적인 지원이 필요하다.

    //kernel/power/suspend.c - v6.5
    ...
    static const struct platform_suspend_ops *suspend_ops;
    ...
    
    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;
    }

     

    " 시스템이 완벽하게 suspend 상태로 진입하기 위해서는 반드시 아키텍처 종속적인 코드의 지원이 필요하다. 그래서 PM 코어는 `platform_suspend_ops` 구조체를 제공한다. 각 제조사에서 이 구조체의 콜백 함수들을 구현하면, PM 코어가 적절하게 suspend 각 단계에서 콜백 함수들을 호출한다. `platform_suspend_ops` 구조체에는 현재 아키텍처가 어떤 절전 모드를 지원하는지 PM 코어에게 알려주는 `valid` 함수가 포함되어 있다. PM 코어는 이 함수를 통해서 사용자가 요청한 절전 모드를 지원하는지를 판단하게 된다. 참고로, 순수 소프트웨어 절전 모드인 `freeze(s2idle)`는 이 과정을 거치지 않는다.

     

    " `suspend_preapre` 함수는 본격적인 시스템 suspend가 들어가기전에 진행되는 프로세스다. 하는 일은 크게 3가지가 있다.

    1. 콘솔 suspend 진입 준비
    2. Notify of suspend preparation 
    3. Freezing of tasks

     

    " `console suspend` 은 다음에 기회가 있을 때 다루도록 하고, 지금은 `pm notify` 에 대해 간략하게만 알아보자. pm notify 는 `위치`에 굉장히 의존적이다. 예를 들어, 아래의 pm_notifier_call_chain_robust 함수는 반드시 suspend_freeze_processes 함수 앞에서 호출되어야 한다. 이 말은, `pm notifier` 에 등록된 디바이스들은 반드시 process 들이 살아있는 상태에서 호출되어야 함을 보장해야 한다는 것이다. 여기서 사용되는 notifier 는 `PM-transition` 에서 사용되는 notifier 다. PM 관련 notifier 는 아래와 같다.

    - PM-transition notifications(System PM) : register_pm_notifier / unregister_pm_notifier
    - Reboot : register_reboot_notifier / register_reboot_notifier
    - Restart : register_restart_handler / unregister_restart_handler
    //kernel/power/suspend.c - v6.5
    /**
     * suspend_prepare - Prepare for entering system sleep state.
     * @state: Target system sleep state.
     *
     * Common code run for every system sleep state that can be entered (except for
     * hibernation).  Run suspend notifiers, allocate the "suspend" console and
     * freeze processes.
     */
    static int suspend_prepare(suspend_state_t state)
    {
    	int error;
    
    	if (!sleep_state_supported(state))
    		return -EPERM;
    
    	pm_prepare_console();
    
    	error = pm_notifier_call_chain_robust(PM_SUSPEND_PREPARE, PM_POST_SUSPEND);
    	if (error)
    		goto Restore;
    
    	trace_suspend_resume(TPS("freeze_processes"), 0, true);
    	error = suspend_freeze_processes();
    	trace_suspend_resume(TPS("freeze_processes"), 0, false);
    	if (!error)
    		return 0;
    
    	suspend_stats.failed_freeze++;
    	dpm_save_failed_step(SUSPEND_FREEZE);
    	pm_notifier_call_chain(PM_POST_SUSPEND);
     Restore:
    	pm_restore_console();
    	return error;
    }

     

     

    " `sleep_state_supported` 함수는 말 그대로 사용자가 요청한 절전 모드를 현재 플랫폼에서 지원하는 모드인지를 검사한다. `suspend-to-idle`은 무조건 지원이 가능한 모드이므로, 무조건 true를 반환한다.

    //kernel/power/suspend.c - v6.5
    static bool sleep_state_supported(suspend_state_t state)
    {
    	return state == PM_SUSPEND_TO_IDLE ||
    	       (valid_state(state) && !cxl_mem_active());
    }

     

     

    " `suspend_freeze_processes` 함수는 시스템에 존재하는 모든 프로세스를 freeze 시킨다. `freeze_processes` 함수는 유저 스페이스 태스크들만 freeze 시킨다. 커널 스레드를 freeze 시키기 위해서는 먼저 유저 스페이스 태스크가 freeze 되야 하기 때문에, 커널 스레드를 freeze 시키기전에 반드시 유저 스페이스 태스크가 먼저 freeze 된다. 그 후에, `freeze_kernel_threads` 함수를 통해 커널 스레드를 포함해서 모든 태스크들을 freeze 시킨다. 만약, 프로세스를 freeze 시키는 것을 실패하면, `thaw_processes` 함수를 호출해서 동결되었던 모든 프로세스를 해동(`thaw`) 시킨다. 자세한 내용은 이 글을 참고하자.

    //kernel/power/power.h - v6.5
    static inline int suspend_freeze_processes(void)
    {
    	int error;
    
    	error = freeze_processes();
    	/*
    	 * freeze_processes() automatically thaws every task if freezing
    	 * fails. So we need not do anything extra upon error.
    	 */
    	if (error)
    		return error;
    
    	error = freeze_kernel_threads();
    	/*
    	 * freeze_kernel_threads() thaws only kernel threads upon freezing
    	 * failure. So we have to thaw the userspace tasks ourselves.
    	 */
    	if (error)
    		thaw_processes();
    
    	return error;
    }

     

     

    " `suspend_devices_and_enter` 함수는 모든 디바이스들을 suspend 상태로 보내고 시스템 상태를 절전 모드로 진입시킨다. suspend 과정을 분석할 때, 각 단계가 현재 어떤 상태인지를 아는 것이 중요하다. `suspend_devices_and_enter` 함수가 호출되는 상태는 메모리와 파일 시스템이 sync 되어 있고, 모든 태스크들이 frozen 되어있는 상태다. 앞에 작업들은 사실 전류에 아무런 영향을 미치지 못한다. 단지, 진짜 suspend 상태로 진입시키기 위해서 필요한 단계였을 뿐이다. 이제 무슨일이 남았을까? 이제는 진짜 물리적인 하드웨어를 `low-power-mode`로 보낼 때 가 됬다. `suspend`는 사실 여기서부터가 진짜다.

    1. 주변 디바이스를 low-power-mode로 진입
    2. CPU를 low-powr-mode로 진입

     

     

    " 크게는 위의 2가지 작업이 메인이 된다. 그런데, 위의 작업은 물리적인 하드웨어 측면에서 본 작업이다. 실제 코드 레벨에서는 아래와 같이 나눠볼 수 있다.

    1. 아키텍처 독립적인 코드를 통한 low-power-mode 진입
    2. 아키텍처 종속적인 코드를 통한 low-power-mode 진입  
    ///kernel/power/suspend.c - v6.5
    /**
     * suspend_devices_and_enter - Suspend devices and enter system sleep state.
     * @state: System sleep state to enter.
     */
    int suspend_devices_and_enter(suspend_state_t state)
    {
    	int error;
    	bool wakeup = false;
    
    	if (!sleep_state_supported(state))
    		return -ENOSYS;
    
    	pm_suspend_target_state = state;
    
    	if (state == PM_SUSPEND_TO_IDLE)
    		pm_set_suspend_no_platform();
    
    	error = platform_suspend_begin(state);
    	if (error)
    		goto Close;
    
    	suspend_console();
    	suspend_test_start();
    	error = dpm_suspend_start(PMSG_SUSPEND);
    	if (error) {
    		pr_err("Some devices failed to suspend, or early wake event detected\n");
    		goto Recover_platform;
    	}
    	suspend_test_finish("suspend devices");
    	if (suspend_test(TEST_DEVICES))
    		goto Recover_platform;
    
    	do {
    		error = suspend_enter(state, &wakeup);
    	} while (!error && !wakeup && platform_suspend_again(state));
    
     Resume_devices:
    	suspend_test_start();
    	dpm_resume_end(PMSG_RESUME);
    	suspend_test_finish("resume devices");
    	trace_suspend_resume(TPS("resume_console"), state, true);
    	resume_console();
    	trace_suspend_resume(TPS("resume_console"), state, false);
    
     Close:
    	platform_resume_end(state);
    	pm_suspend_target_state = PM_SUSPEND_ON;
    	return error;
    
     Recover_platform:
    	platform_recover(state);
    	goto Resume_devices;
    }

     

     

    " `platform_suspend_begin` 함수는 아키텍처 종속적인 부분으로 벤더사에서 제공하는 `suspend_begin` 함수를 실행하는 함수다. `suspend-to-idle`은 아키텍처에 의존적이지 않지만, 나머지 3개의 절전 모드는 모두 아키텍처 의존적인 코드이기 때문에 반드시 `suspend_ops`가 구현되어 있는지 확인해야 한다. 그런데, `begin` 함수는 `optional` 함수이기 때문에 없어도 성공의 의미로 `0`을 반환한다.

    //kernel/power/suspend.c - v6.5
    static int platform_suspend_begin(suspend_state_t state)
    {
    	if (state == PM_SUSPEND_TO_IDLE && s2idle_ops && s2idle_ops->begin)
    		return s2idle_ops->begin();
    	else if (suspend_ops && suspend_ops->begin)
    		return suspend_ops->begin(state);
    	else
    		return 0;
    }

     

     

    " `dpm_suspend_start` 함수는 간단하게 설명하면, 모든 `dev->driver->pm->prepare`과 `dev->driver->pm->suspend` 콜백을 호출한다. 즉, 이 함수를 호출하면 모든 디바이스들은 `low-power-mode`로 빠지는 것이다. `dpm_suspend_start` 함수 관한 내용은 이 글을 참고하자.

     

    " `suspend_test_finish` 함수는 `suspend_test_start` 함수와 짝을 이뤄서 `suspend` 가 얼마나 걸렸는지를 알려준다. 아래의 코드를 보면 알겠지만, 시간만 계산하기 때문에 코드 자체는 심플하다.

    // kernel/power/suspend_test.c - v6.5
    /*
     * We test the system suspend code by setting an RTC wakealarm a short
     * time in the future, then suspending.  Suspending the devices won't
     * normally take long ... some systems only need a few milliseconds.
     *
     * The time it takes is system-specific though, so when we test this
     * during system bootup we allow a LOT of time.
     */
    #define TEST_SUSPEND_SECONDS	10
    
    static unsigned long suspend_test_start_time;
    ...
    ...
    
    void suspend_test_start(void)
    {
    	/* FIXME Use better timebase than "jiffies", ideally a clocksource.
    	 * What we want is a hardware counter that will work correctly even
    	 * during the irqs-are-off stages of the suspend/resume cycle...
    	 */
    	suspend_test_start_time = jiffies;
    }
    
    void suspend_test_finish(const char *label)
    {
    	long nj = jiffies - suspend_test_start_time;
    	unsigned msec;
    
    	msec = jiffies_to_msecs(abs(nj));
    	pr_info("PM: %s took %d.%03d seconds\n", label,
    			msec / 1000, msec % 1000);
    
    	/* Warning on suspend means the RTC alarm period needs to be
    	 * larger -- the system was sooo slooowwww to suspend that the
    	 * alarm (should have) fired before the system went to sleep!
    	 *
    	 * Warning on either suspend or resume also means the system
    	 * has some performance issues.  The stack dump of a WARN_ON
    	 * is more likely to get the right attention than a printk...
    	 */
    	WARN(msec > (TEST_SUSPEND_SECONDS * 1000),
    	     "Component: %s, time: %u\n", label, msec);
    }

     

     

    " `WARN` 함수는 `suspend` 과정이 10초 보다 오래걸리면, 이건 문제가 있다고 경고 문구를 출력한다. 그런데, 이 `suspend_test_finish` 함수가 호출되는 위치는 저기가 맞을까? 즉, 모든 디바이스들을 `low-power-mode`로 보내고 나서 호출하는데, 사실 아직 suspend는 끝나지 않았다.

     

    " 사실, 이 기능이 왜 필요한가 싶을 수 도 있다. 그래서 이 기능이 왜 필요한지를 이 기능을 만든 `Rafael J. Wysocki`의 글을 읽어보자.

    Rafael J. Wysocki 2011-02-20 00:12:17 UTC

    The warning only means that resume takes more time than it probably should. If you want it to go away, please unset CONFIG_PM_TEST_SUSPEND in your kernel .config.

    However, it shows that the resume of devices on your system took about 1 minute, which is really too much (should be about 5 sec.).

    Probably one of the device drivers takes much time to resume and you can figure out which one by booting with initcall_debug in the kernel command line and checking dmesg output after a suspend/resume cycle.

     

     

    " `suspend_enter` 함수는 suspend 마지막 단계에 실행되는 함수로 이제 진짜 시스템을 `suspend`로 보내는 함수다. 앞에 `platform_*` 함수들은 아키텍처 종속적인 함수들이다. 아키텍처 종속적인이라는 것은 결국 CPU와 관련된 동작들을 수행한다. `dpm_*` 함수들은 아키텍처 독립적인 함수들이면서 주로 디바이스 관련 함수들을 의미한다. `suspend_enter` 함수가 호출되는 상황은 어떤 상태일까?

    //kernel/power/suspend.c - v6.5
    /**
     * suspend_enter - Make the system enter the given sleep state.
     * @state: System sleep state to enter.
     * @wakeup: Returns information that the sleep state should not be re-entered.
     *
     * This function should be called after devices have been suspended.
     */
    static int suspend_enter(suspend_state_t state, bool *wakeup)
    {
    	int error;
    
    	error = platform_suspend_prepare(state);
    	if (error)
    		goto Platform_finish;
    
    	error = dpm_suspend_late(PMSG_SUSPEND);
    	if (error) {
    		pr_err("late suspend of devices failed\n");
    		goto Platform_finish;
    	}
    	error = platform_suspend_prepare_late(state);
    	if (error)
    		goto Devices_early_resume;
    
    	error = dpm_suspend_noirq(PMSG_SUSPEND);
    	if (error) {
    		pr_err("noirq suspend of devices failed\n");
    		goto Platform_early_resume;
    	}
    	error = platform_suspend_prepare_noirq(state);
    	if (error)
    		goto Platform_wake;
    
    	if (suspend_test(TEST_PLATFORM))
    		goto Platform_wake;
    
    	if (state == PM_SUSPEND_TO_IDLE) {
    		s2idle_loop();
    		goto Platform_wake;
    	}
    
    	error = pm_sleep_disable_secondary_cpus();
    	if (error || suspend_test(TEST_CPUS))
    		goto Enable_cpus;
    
    	arch_suspend_disable_irqs();
    	BUG_ON(!irqs_disabled());
    
    	system_state = SYSTEM_SUSPEND;
    
    	error = syscore_suspend();
    	if (!error) {
    		*wakeup = pm_wakeup_pending();
    		if (!(suspend_test(TEST_CORE) || *wakeup)) {
    			trace_suspend_resume(TPS("machine_suspend"),
    				state, true);
    			error = suspend_ops->enter(state);
    			trace_suspend_resume(TPS("machine_suspend"),
    				state, false);
    		} else if (*wakeup) {
    			error = -EBUSY;
    		}
    		syscore_resume();
    	}
    
    	system_state = SYSTEM_RUNNING;
    
    	arch_suspend_enable_irqs();
    	BUG_ON(irqs_disabled());
    
     Enable_cpus:
    	pm_sleep_enable_secondary_cpus();
    
     Platform_wake:
    	platform_resume_noirq(state);
    	dpm_resume_noirq(PMSG_RESUME);
    
     Platform_early_resume:
    	platform_resume_early(state);
    
     Devices_early_resume:
    	dpm_resume_early(PMSG_RESUME);
    
     Platform_finish:
    	platform_resume_finish(state);
    	return error;
    }

     

    " `pm_sleep_disable_secondary_cpus` 함수는 기존의 `nonboots cpus`를 disable 하는 기능을 대체한다. 최종적으로 `freeze_secondary_cpus` 함수에서 현재 시스템에 존재하는 모든 online CPU`s 코어들를 순회하면서 `primary CPU`만 제외하고 모두 down 시키고 있다. x86을 기준으로 `primary CPU`란 흔히 BSP 라고 일컬어지는 프로세서를 의미한다. 이 프로세서는 부팅 및 종료를 관장하는 프로세서다.

     

    : 참고로, 예전에는 `disable_nonboot_cpus` 함수가 사용되었다. 현재는 `freeze_secondary_cpus` 함수로 대체되었다[ 참고1 참고2 ].

    // kernel/power/power.h - v6.5
    static inline int pm_sleep_disable_secondary_cpus(void)
    {
    	cpuidle_pause();
    	return suspend_disable_secondary_cpus();
    }
    // include/linux/cpu.h - v6.5
    static inline int suspend_disable_secondary_cpus(void)
    {
    	int cpu = 0;
    
    	if (IS_ENABLED(CONFIG_PM_SLEEP_SMP_NONZERO_CPU))
    		cpu = -1;
    
    	return freeze_secondary_cpus(cpu);
    }
    // kernel/cpu.c - v6.5
    #ifdef CONFIG_PM_SLEEP_SMP
    static cpumask_var_t frozen_cpus;
    
    int freeze_secondary_cpus(int primary)
    {
    	int cpu, error = 0;
    
    	cpu_maps_update_begin();
    	if (primary == -1) {
    		primary = cpumask_first(cpu_online_mask);
    		if (!housekeeping_cpu(primary, HK_TYPE_TIMER))
    			primary = housekeeping_any_cpu(HK_TYPE_TIMER);
    	} else {
    		if (!cpu_online(primary))
    			primary = cpumask_first(cpu_online_mask);
    	}
    
    	/*
    	 * We take down all of the non-boot CPUs in one shot to avoid races
    	 * with the userspace trying to use the CPU hotplug at the same time
    	 */
    	cpumask_clear(frozen_cpus);
    
    	pr_info("Disabling non-boot CPUs ...\n");
    	for_each_online_cpu(cpu) {
    		if (cpu == primary)
    			continue;
    
    		if (pm_wakeup_pending()) {
    			pr_info("Wakeup pending. Abort CPU freeze\n");
    			error = -EBUSY;
    			break;
    		}
    
    		trace_suspend_resume(TPS("CPU_OFF"), cpu, true);
    		error = _cpu_down(cpu, 1, CPUHP_OFFLINE);
    		trace_suspend_resume(TPS("CPU_OFF"), cpu, false);
    		if (!error)
    			cpumask_set_cpu(cpu, frozen_cpus);
    		else {
    			pr_err("Error taking CPU%d down: %d\n", cpu, error);
    			break;
    		}
    	}
    
    	if (!error)
    		BUG_ON(num_online_cpus() > 1);
    	else
    		pr_err("Non-boot CPUs are not disabled\n");
    
    	/*
    	 * Make sure the CPUs won't be enabled by someone else. We need to do
    	 * this even in case of failure as all freeze_secondary_cpus() users are
    	 * supposed to do thaw_secondary_cpus() on the failure path.
    	 */
    	cpu_hotplug_disabled++;
    
    	cpu_maps_update_done();
    	return error;
    }

     

     

    " `arch_suspend_disable_irqs` 함수는 인터럽트를 disable 시키는 함수다. 그런데, 아키텍처 의존적인 함수라서 각 벤더사에서 오버라이드해서 정의할 수 있다. 기본적으로는 `local_irq_disable` 함수를 사용한다. 즉, 로컬 프로세서의 IRQ 플래그를 CLEAR 한다. `__weak`는 확장자는 동일한 이름의 오버라이드 함수가 존재하면, 그 함수로 오버라이드 된다는 소리다. 만약, 동일한 이름의 함수가 없을 경우, `__weak` 확장자가 작성된 함수를 사용한다.

    // include/linux/suspend.h - v6.5
    /**
     * arch_suspend_disable_irqs - disable IRQs for suspend
     *
     * Disables IRQs (in the default case). This is a weak symbol in the common
     * code and thus allows architectures to override it if more needs to be
     * done. Not called for suspend to disk.
     */
    extern void arch_suspend_disable_irqs(void);
    ...
    
    // kernel/power/suspend.c - v6.5
    /* default implementation */
    void __weak arch_suspend_disable_irqs(void)
    {
    	local_irq_disable();
    }

     

     

    " `syscore_suspend` 함수는 syscore 서브시스템 관련한 suspend를 관장한다. 중요한 건 이 함수는 하나의 on-line CPU 에서만 실행되는 함수라는 것이다. 그리고 그 CPU는 인터럽트가 비활성화된 상태여야 한다.

    //drivers/base/syscore.c - v6.5
    /**
     * syscore_suspend - Execute all the registered system core suspend callbacks.
     *
     * This function is executed with one CPU on-line and disabled interrupts.
     */
    int syscore_suspend(void)
    {
    	struct syscore_ops *ops;
    	int ret = 0;
    
    	trace_suspend_resume(TPS("syscore_suspend"), 0, true);
    	pm_pr_dbg("Checking wakeup interrupts\n");
    
    	/* Return error code if there are any wakeup interrupts pending. */
    	if (pm_wakeup_pending())
    		return -EBUSY;
    
    	WARN_ONCE(!irqs_disabled(),
    		"Interrupts enabled before system core suspend.\n");
    
    	list_for_each_entry_reverse(ops, &syscore_ops_list, node)
    		if (ops->suspend) {
    			pm_pr_dbg("Calling %pS\n", ops->suspend);
    			ret = ops->suspend();
    			if (ret)
    				goto err_out;
    			WARN_ONCE(!irqs_disabled(),
    				"Interrupts enabled after %pS\n", ops->suspend);
    		}
    
    	trace_suspend_resume(TPS("syscore_suspend"), 0, false);
    	return 0;
    
     err_out:
    	pr_err("PM: System core suspend callback %pS failed.\n", ops->suspend);
    
    	list_for_each_entry_continue(ops, &syscore_ops_list, node)
    		if (ops->resume)
    			ops->resume();
    
    	return ret;
    }
    EXPORT_SYMBOL_GPL(syscore_suspend);

     

     

    ###############################################################################################

    : 유저 레벨에서 `/sys/power/state` 파일에 `freeze`, `standby`, `mem` 중에 하나를 전송하면, 상태가 되면, 다음의 4단계를 거친다.

     

    1. Prepare 단계

    : `prepare` 콜백 함수 단계에서는 새로운 디바이스가 PM core에 등록되는 것을 막는다. 예를 들어, 특정 디바이스의 하위(자식) 디바이스들이 마음대로 registered되면, PM core는 결코 해당 디바이스의 자식 디바이스들까지 suspended 상태가 됬는지 알 수가 없다. [By contrast, from the PM core’s perspective, devices may be unregistered at any time.] 다른 suspend 단계들과는 다르게, prepare 단계는 `top-down`오로 device hierarchy를 탐색(순회)한다.

     

    : prepare callback이 return문 까지 마치면, 디바이스들에게 새로운 자식 디바이스들을 등록할 수 없다.

     

    : For devices supporting runtime power management, the return value of the prepare callback can be used to indicate to the PM core that it may safely leave the device in runtime suspend (if runtime-suspended already), provided that all of the device’s descendants are also left in runtime suspend. Namely, if the prepare callback returns a positive number and that happens for all of the descendants of the device too, and all of them (including the device itself) are runtime-suspended, the PM core will skip the suspendsuspend_late and suspend_noirq phases as well as all of the corresponding phases of the subsequent device resume for all of these devices. 이런 경우는, `prepare` callback이 호출된 후 `complete` callback이 호출된다. and is entirely responsible for putting the device into a consistent state as appropriate.


    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

     

    : 주의할 부분은 해당 디바이스에서 Runtime PM이 disabled 상태에서도 direct-complete procedure가 적용될 수 있다는 점이다. only the runtime-PM status matters. 따라서 해당 디바이스가 system-sleep callbacks들을 가지고 있는데 runtime PM을 지원하지 않으면, 절대 `prepare` callback에서 양수를 반환해서는 안된다.

     

     

     

    2. Suspend

    : `suspend` 콜백 함수는 디바이스가 더 이상 I/O를 수행하지 못하도록 하는 단계다(`Quiesce device`). 이 단계에서는, 현시점의 디바이스 레지스터 상태를 저장하고, 해당 디바이스에 맞는 `low-power` 상태로 전환시킨다. 만약, wakeup-capable 디바이스라면, wakeup event로 활성화 시켜주고 상태를 전환한다.

     

    : 근데, 이 단계도 Runtime PM이 얽히면 조금 복잡해진다. 예를 들어, 디바이스가 이미 `Runtime suspend` 라면 어떻게 할까? `Runtime PM`은 항시 `top-down` 구조로 호출이 된다. 서브시스템에서 콜백이 호출되면, 해당 디바이스에서는 콜백이 호출되지 않는다. 

     

    Systme PM은 Runtime PM과 다르게, 상위 서브시스템의 `suspend` 콜백도 호출하고, 해당 드라이버의 suspend` 콜백도 호출하는 구조다. 대개 편의상 디바이스들의 suspend 단계는 2개로 나눌 수 있다.

    0" Quiesce device
    1" Save device state

    : 여기서 후자의 작업은 `suspend_late` 콜백에서 진행한다. `late suspend` 단계에서는 모든 디바이스들의 `runtime PM API` 를 비활성화 시킨다. 즉, `suspend_late` 콜백 함수는 해당 디바이스의 runtime PM을 비활성화 시킨 후 호출된다.

    Generally speaking, a parent (or a supplier) device can only be fully suspended if all of its child (or consumer) devices already have been fully suspended.

    Accordingly, it is better to suspend all of the bus controllers in the "late suspend" phase and resume them in the "early resume" one. Then, the dependent devices can be suspended either in the "suspend" or in the "late suspend" phase and the PM core should get the dependencies right (and analogously for resume).

    Thanks,
    Rafael

    - 참고 : https://linux.kernel.narkive.com/Ln95nXev/understanding-suspend-vs-suspend-late-for-i2c-devices

    : 버스와 같은 서브-시스템 디바이스들은 자식 및 하위 디바이스들이 모두 절전 모드가 되고나서야 자신도 절전 모드로 들어갈 수 가 있다. 그래서, 하위 디바이스들은 `suspend` 단계에서 절전 모드로 보내고, 버스와 같은 디바이스들은 `late suspend` 단계에서 절전 모드로 진입시키는 것이 좋다. 그리고, resume 단계에서는 `early resume`에서 버스를 깨우고, `resume`에서 하위 디바이스들을 깨우는 것이 좋다.

     

    : 뒤에서 다루겠지만, `suspend noirq / resume noirq` 에서는 주로 버스와 같은 인프라 디바이스들이 suspend / resume 된다. 왜냐면, suspend 시에는 하위 다바이스들이 모두 suspended 된후에 버스가 suspended 되어야 하고, resume 시에는 버스가 먼저 resume 된다(하위 디바이스들에 접근하기 위해서는 버스가 먼저 resume 되어야 한다). `syscore`에 넣으면 안될까? `syscore`에는 좀 더 핵심적인 리소스들(?)이 포함된다. 즉, platform 이나 dpm 까지는 디바이스라고 볼 수 있지만, `syscore` 부터는 리소스 개념이다. 예를 들어, `CLOCK, INTERRUPT, GPIO` 등이 있다. 근데 왜 뜬금없이 `GPIO`가 `syscore`일까? `i2c` 라인이 GPIO를 통해서 사용되기 때문이다.

     

    3. Suspend_noirq [ 참고1 참고2 ] 

    : `suspend_noirq` 단계는 IRQ handlers들이 비활성화 된 후에 호출되는 단계다. 즉, `suspend_noirq` 콜백이 실행중인 상태에서는 interrupt handler는 호출되지 못한다는 말이다. 여기서 인터럽트 핸들러의 비활성화는 `high-level("action")` 핸들러를 막는것을 의미한다. 즉, 인터럽트가 발생하면 인터럽트 컨트롤러는 인터럽트를 인지하긴 한다. 그러나, 드라이버 개발자가 작성한 `high-level("action")` 핸들러를 호출하지 않을 뿐이다. 이 상황에서 발생한 인터럽트들은 기록하고 있다가, 나중에 resume 과정에서 호출될 것이다. `suspend_noirq` 콜백에서는 이전에 저장하지 못한 디바이스 정보 및 상태를 저장한다. 그리고 마침내 이 단계에서 디바이스를 low-power 상태로 보내버린다.

     

    : 대부분의 서브 시스템들과 디바이스 드라이버들은 `suspend_noirq` 콜백을 구현할 필요는 없다. 그런데, 버스 드라이버(버스에 장착된 디바이스들끼리 인터럽트를 공유하는 것을 허용한 버스, 예를 들면, 레거시 PCI 버스는 인터럽트를 여러 디바이스들이 공유하기 때문에 `suspend_noirq` 을 구현해야 한다. 예를 들어, PCI 버스 2개의 디바이스(A, B)가 `INTA#`를 공유하고, `suspend_noirq` 를 구현하지 않았다고 치자. 유저 레벨에서 `/sys/power/state`에 `mem`을 호출해서 system-wide suspend가 진행이 되기 시작했다. 디바이스 A가 먼저 suspend 상태가 되었다. 이제 디바이스 A는 인터럽트를 발생시킬 수 없다. 그런데, 아직 suspend 상태가 아닌 디바이스 B에서 인터럽트가 발생했다. 여기서 문제가 발생한다. 레거시 PCI 버스가 인터럽트를 공유하다보니, 디바이스 A는 여전히 인터럽트를 받지 못하지만, PCI 디바이스 A의 인터럽트 핸들러는 살아있기 때문에 이걸 받아서 처리를 하게 된다. 이게 가능할까? 가능하다! 왜냐면, 인터럽트 핸들러는 CPU 에서 동작하는 친구이기 때문이다. 즉, 디바이스 B가 발생시킨 인터럽트가 공유라인이다 보니 CPU가 이걸 받고, 디바이스 A, B의 인터럽트 핸들러 2개 모두를 호출한 것 이다. 

    Of course, a PCI device in a low-power state cannot generate interrupts. Yet, if that device uses shared interrupts, then its driver’s interrupt handler may be invoked as a result of an interrupt generated by one of the other devices sharing an interrupt vector with it. Moreover, this actually is quite probable, because the suspend and resume callbacks provided by device drivers are executed sequentially [S2RAM], so it is guaranteed that one of the devices sharing the interrupt vector will be suspended earlier and resumed later than the other ones. Therefore, if one of these devices is handled by a driver with an interrupt handler that is not designed to work correctly even if the device is not in the right state, things are likely to go wrong. Namely, if the device that has not been suspended yet or that has already been resumed generates an interrupt, the other devices’ interrupt handlers will be invoked, and if they fail, the system is going to crash.

    - 참고 : https://www.kernel.org/doc/ols/2009/ols2009-pages-319-330.pdf

    : 한 가지 케이스가 더 있다. 인터럽트를 공유하는 디바이스가 `resume` 단계에서 앞쪽에 있으면(이 때, 인터럽트를 공유하고 있는 디바이스들은 `suspend`  상태라고 가정하고), 다른 디바이들의 인터럽트 핸들러가 호출되고 이건 실패하게 된다.

     

    : 위의 단계들이 끝난 시점에서는, 드라이버는 모든 I/O transactions (DMA, IRQs)를 멈추고, 디바이스가 다시 resume 되었을 때 이전 상태를 복구 및 원복하기 위한 중요 데이터들을 저장하고, low-power 상태로 보내버린다. 많은 벤더사에서 이 상태가 되면, 시스템 전반적으로 clock sources들을 gate off 한다. 그런데, 간혹 아예 전원 공급을 차단하거나, volatge scaling을 통해 전력 소모 자체를 줄이는 경우도 있다.

     

    : `device_may_makeup` 함수를 통해서 특정 디바이스가 시스템을 웨이크-업 시킬 수 있는 놈인지를 판단할 수 있다. 그래서 `suspend` 및 `resume` 콜백함수에서 이 함수를 이용해서 현재 디바이스가 시스템을 웨이크-업 시킬 수 있는 놈인지를 판단하고, 특정 GPIO를 wakeup 시그널로 연결하는 경우가 많다[참고1]. `enable_irq_wake` 함수를 통해 인자로 전달된 GPIO가 시스템을wakeup 시킬 수 있는 GPIO라는 것을 인터럽트 컨트롤러에게 알린다.

    /drivers/hid/i2c-hid/i2c-hid.c [v4.3]
    #ifdef CONFIG_PM_SLEEP
    static int i2c_hid_suspend(struct device *dev)
    {
    	struct i2c_client *client = to_i2c_client(dev);
    	struct i2c_hid *ihid = i2c_get_clientdata(client);
    	struct hid_device *hid = ihid->hid;
    	int ret = 0;
    	int wake_status;
    
    	if (hid->driver && hid->driver->suspend)
    		ret = hid->driver->suspend(hid, PMSG_SUSPEND);
    
    	disable_irq(ihid->irq);
    	if (device_may_wakeup(&client->dev)) {
    		wake_status = enable_irq_wake(ihid->irq);
    		if (!wake_status)
    			ihid->irq_wake_enabled = true;
    		else
    			hid_warn(hid, "Failed to enable irq wake: %d\n",
    				wake_status);
    	}
    
    	/* Save some power */
    	i2c_hid_set_power(client, I2C_HID_PWR_SLEEP);
    
    	return ret;
    }
    
    static int i2c_hid_resume(struct device *dev)
    {
    	int ret;
    	struct i2c_client *client = to_i2c_client(dev);
    	struct i2c_hid *ihid = i2c_get_clientdata(client);
    	struct hid_device *hid = ihid->hid;
    	int wake_status;
    
    	enable_irq(ihid->irq);
    	ret = i2c_hid_hwreset(client);
    	if (ret)
    		return ret;
    
    	if (device_may_wakeup(&client->dev) && ihid->irq_wake_enabled) {
    		wake_status = disable_irq_wake(ihid->irq);
    		if (!wake_status)
    			ihid->irq_wake_enabled = false;
    		else
    			hid_warn(hid, "Failed to disable irq wake: %d\n",
    				wake_status);
    	}
    
    	if (hid->driver && hid->driver->reset_resume) {
    		ret = hid->driver->reset_resume(hid);
    		return ret;
    	}
    
    	return 0;
    }
    #endif

    : `prepare`, `suspend`, `suspend_late`, `suspend_noirq` callbacks중에 하나라도 error를 리턴하면, 시스템은 low-power 상태로 진입하지 못한다. 그러면서 동시에 PM core는 모든 suspended 상태에 있는 디바이스들을 resume 해버린다.

     

    - Interrupt and susend-to-idle

    : `suspend-to-ram`과 `suspend-to-idle`은 suspend_noirq 까지는 동작이 동일하다. 그 이후에, 아키텍처 종속적인 부분이 존재하기 때문에 그때부터 달라진다. `suspend-to-idle` 과정에서는 non-boot CPU`s 들을 비활성화하지 않는다. `suspend_enter` 함수를 보면 `s2idle_loop` 함수가 있다. 이 함수는 모든 프로세서를 idle 상태로 보내버린다. `suspend-to-idle` 루틴에서 `s2idle_loop` 함수가 호출되면, 그 뒤에 루틴은 실행되지 않는다.


    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

    http://events17.linuxfoundation.org/sites/events/files/slides/linux_suspend.pdf

    : `suspend-to-idle` 상태에서는 `IRQF_NO_SUSPEND` 플래그가 설정된 인터럽트를 통해서 시스템을 wakeup 시킨다. 그러나, `IRQF_NO_SUSPEND` 플래그 자체는 시스템을 wakeup 하는 기능이 없다. `suspend-to-idle`은 인터럽트가 masked 된 상태가 아니기 때문에(`local_irq_disable` 함수가 호출되지 않음) `IRQF_NO_SUSPEND` 플래그가 설정된 인터럽트 핸들러가 동작할 수 있다. 그래서 만약에, IRQF_NO_SUSPEND 플래그가 설정된 인터럽트 핸들러에 `pm_stay_awake` 계열의 함수를 사용하는 코드가 있다면, 시스템을 wakeup 시킬 수 있다.

     

     

    - IRQF_NO_SUSPEND [ 참고1 ]

    : `IRQF_NO_SUSPEND`는 쉽게 suspending / resuming 과정에서도 인터럽트 핸들러를 활성화하겠다는 뜻이다. 자세한 내용은 이 글 및 `참고1` 링크를 따라가보자.

     

     

    - Hardware Wakable Pin [ 참고1 ] 

    : SoC 벤더사에 데이터시트를 받으면, GPIO 중에 `Wakeup Function`이 되는 핀이 존재한다. 그런데, 시스템 suspend / resume은 소프트웨어적인 절차다. 그런데, 하드웨어적인 요소인 GPIO가 suspended 시스템을 wakeup 시킨다? wakeup function이 설정된 GPIO가 트리거가 되면 ARM 같은 경우는 GIC에서 이 GPIO를 wakeup function으로 인식해서 시스템을 wakeup 시킨다.

     

    : 그리고 현재(kernel 6.5)는 없어졌지만, `PM_SUSPEND_FREEZE` 패치관련해서 재미있는 글이 있다. 아래 글은 `echo freeze > /sys/power/state` 인걸로 봐서, s2idle 상태로 진입시키려고 하는 것같다. s2idle 은 인터럽트가 활성화된 상태로 진입한다. 그래서 일반 인터럽트가 들어와도 프로세서는 잠깐 깨고나서 해당 인터럽트가 wake event 인지 확인하고 wakeup event 가 아니면 다시 idle 상태로 진입한다. 만약, wakeup event 관련 인터럽트가 발생할 경우, 인터럽트 핸들러가 해당 wakeup event에 대응하는 wakeup source를 activate 한다고 되어있다.

    ....
    The following describes how PM_SUSPEND_FREEZE state works.
    1. echo freeze > /sys/power/state
    2. the processes are frozen.
    3. all the devices are suspended.
    4. all the processors are blocked by a wait queue
    5. all the processors idles and enters (Deep) c-state.
    6. an interrupt fires.
    7. a processor is woken up and handles the irq.
    8. if it is a general event,
    a) the irq handler runs and quites.
    b) goto step 4.
    9. if it is a real wake event, say, power button pressing, keyboard touch, mouse moving,
    a) the irq handler runs and activate the wakeup source
    b) wakeup_source_activate() notifies the wait queue.
    c) system starts resuming from PM_SUSPEND_FREEZE
    10. all the devices are resumed.
    11. all the processes are unfrozen.
    12. system is back to working.
    ...

    - 참고 : https://lkml.indiana.edu/hypermail/linux/kernel/1302.0/02952.html
Designed by Tistory.