-
[리눅스 커널] PM - Power Management InterfaceLinux/kernel 2023. 8. 3. 02:15
글의 참고
- http://www.wowotech.net/pm_subsystem/pm_interface.html
- https://lwn.net/Articles/416690/
- https://elinux.org/Pm_Sub_System
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
글의 내용
- Overview
: 리눅스 커널의 Power management 서브 시스템 중 상당수는 Hibernate, Suspend, Runtime PM 등의 기능을 처리하는 코드로 이루어져 있다. 그런데, 위에 기능들은 `전력 소비`와 깊은 관련이 있다보니, 하드웨어 디펜던시가 강한 코드들이 상당히 많다. 예를 들어, x86의 ACPI, arm의 PSCI 등을 예로 들 수 있다. 리눅스는 OS 다 보니, 하드웨어 디펜던시가 강한 인터페이스를 추상화시켜서 드라이버 개발자들에게 제공할 의무가 있다. 그리고, API 의 편의성 또한 굉장히 중요하다. 드라이버 레벨에서 굉장히 자주 사용되는 기능들을 OS 레벨에서 추상화하는 시킴으로써, OS 레벨에서 API 를 관리해주면 안전성 및 편의성이 증가한다. 그래서, 이 글에서는 아래의 2가지 내용에 대해 알아보도록 한다.
1. 리눅스 커널 파워 매니지먼트 서브 시스템은 어떻게 하드웨어 종속적인 코드를 어떻게 추상화 하는가 ? Device PM callback 섹션
2. 리눅스 커널 파워 매니지먼트 서브 시스템은 드라이버 개발자들에게 어떻게 편의성을 제공하는가 ? Back to the purpose of this article 섹션- History
: 리눅스 커널 파워 매니지먼트 API 를 분석하기전에 한 가지 원칙에 대해 알 필요가 있다. 시스템에는 수 많은 디바이스가 존재한다. 디바이스 전원이 인가되면, 전력 소비를 통해 동작하게 된다. 그렇다면, 디바이스가 수가 많을 수 록, 전력 소비가 증가한다고 말할 수 있을 것 같다. 그렇다면, 파워 매니지먼트는 어떻게 해야 할까? 하드웨어 측면에서 보면, 전원을 인가하지 않으면 된다. 즉, 전원을 인가하지 않음으로써 전려 소비를 줄이는 것이다. 그런데, 그냥 무작정 전원을 Off 하는 것은 전혀 논리적이지 않다. 그렇다면, 언제 전원을 Off 해야 할까? 안드로이드가 이것에 대한 대답을 해주었다. 바로 `동작할 필요가 없을 때, 전원을 Off 하자` 이다. 여기서 `동작할 필요가 없을 때` 를 결정하는 것은 사실상 굉장히 어렵다. 이 질문에 대한 대답 또한 안드로이드에서 내놓았다. 해당 내용이 궁금하다면 이 글을 참고하자.
: 리눅스는 OS 다. 그리고, 전원과 같은 비용이 비싼 리소스는 사용자 영역이 아닌, 커널 영역에서 컨트롤하는 것이 좋다. 대신, 각 디바이스마다 파워 컨트롤은 자기 자신이 가장 잘 알고있다. 그래서, 파워 매니지먼트의 전체 큰 흐름은 리눅스가 컨트롤하고, 디바이스를 특정 파워 상태로 진입시키는 것은 디바이스에서 직접 컨트롤하는 구조가 만들어졌다.
: 위와 같은 구조를 만들기 위해서 리눅스 커널은 디바이스 드라이버 개발자들에게 `device PM callbacks` 이라는 것을 제공한다. 드라이버 개발자들이 자신의 디바이스 특성에 맞게 해당 인터페이스를 구현하면 리눅스 커널이 적절한 시점(wakeup_source 가 모두 de-activated 될 때)에 system-wide power transition 을 진행한다.
: 그렇다면, device PM callbacks 이라는 것이 뭘까?
- Device PM callbacks
: device PM callbacks 은 리눅스 커널에 등록된 모든 디바이스들에게 일관된 파워 매니지먼트 방법을 제공하기 위해 만들어진 콜백 함수의 집합이다. 즉, device 들이 `적절한 타이밍`에 `특정 상태`로 `일관된 방식` 을 정의하는 것을 의미한다. 예를 들어, a, b, c 디바이스가 있다고 하자. 이 때, 3개의 디바이스들 모두 pm callback을 구현을 했고, 이건 linux kernel PM core에게 등록됬다고 하자. 그렇면 시스템이 절전 모드에 들어가야 할 때, 예를 들어, 노트북 케이스를 닫거나, 스마트폰 화면 끔 버튼을 누르거나, 핸드폰에서 USB 를 뽑거나 등을 통해 리눅스 커널에 등록된 wakeup sources 들이 모두 de-actived 되면, PM core 는 디바이스 suspend 함수를 일괄적으로 모두 호출한다. 여기서 `적절한 타이밍 = 노트북 케이스 닫기, 핸드폰 전원 버튼 끔, USB 를 뽑음` , `특정 상태 = 각 디바이스마다 구현된 특정 파워 상태` , `통일된 방식 = 리눅스 커널에서 제공하는 suspend 함수` 이라고 보면 된다.
// include/linux/pm.h - v6.5 struct dev_pm_ops { // For System PM int (*prepare)(struct device *dev); void (*complete)(struct device *dev); int (*suspend)(struct device *dev); int (*resume)(struct device *dev); int (*freeze)(struct device *dev); int (*thaw)(struct device *dev); int (*poweroff)(struct device *dev); int (*restore)(struct device *dev); int (*suspend_late)(struct device *dev); int (*resume_early)(struct device *dev); int (*freeze_late)(struct device *dev); int (*thaw_early)(struct device *dev); int (*poweroff_late)(struct device *dev); int (*restore_early)(struct device *dev); int (*suspend_noirq)(struct device *dev); int (*resume_noirq)(struct device *dev); int (*freeze_noirq)(struct device *dev); int (*thaw_noirq)(struct device *dev); int (*poweroff_noirq)(struct device *dev); int (*restore_noirq)(struct device *dev); // For Runtime PM int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); };
: 이 글에서 위의 함수들을 일일히 설명하는 것은 바람직하지 못하다. 왜냐면, 위 함수들의 사용법은 각 디바이스마다 다르기 때문이다. `struct dev_pm_ops` 의 주석은 정말 어마무시하게 길다. 각 함수의 사용법이 자세하게 작성되어 있지만, 그걸 이해하는 것과 적용의 문제는 별개다. 즉, 위에 함수들의 정확하게 이해 및 사용하고 싶다면, 각 함수들의 사용 사례를 찾아보는 것이 가장 정확하고 빠른 방법이 될 것이다. 참고로, 위에 함수들 중 중요한 콜백 함수들은 이 글에서 자세히 설명하고 있기 때문이다. 여기서는 몇 가지 기준으로 분류만 해본다.
1. 접두사 noirq : 인터럽트가 off 된 상태에서도 호출 가능한 콜백 함수들을 의미한다. 이 함수들의 자세한 내용은 이 글을 참고하자.
2. 접두사 late : power transition 중에서 suspend 과정에서만 있는 단계다. 예를 들어, 일반적인 suspend 콜백 함수에는 각 디바이스들의 Power-off 코드가 포함되어 있다. 그런데, 이 디바이스가 power-off 되기전에 절대 먼저 power-off 되면 안되는 친구들이 있다. 예를 들어, adc 가 i2c 를 이용하고 있다면 adc 디바이스가 power-off 되기 전에 절대로 i2c 컨트롤러는 off 가 되면 안된다. 왜냐면, i2c 컨트롤러가 off 되면 adc 를 off 시키려면 i2c 를 사용해야 하는데, i2c 컨트롤러가 먼저 off 되면 i2c 통신이 불가능하기 때문이다.
3. 접두사 early : power transition 중에서 resume 과정에서만 있는 단계다. 예를 들어, 일반적인 resume 콜백 함수에는 각 디바이스들의 Power-on 코드가 포함되어 있다. 그런데, 이 디바이스가 power-on 되기전에 먼저 power-on 되어야 하는 친구들이 있을 수 있다. 예를 들어, adc 가 i2c 를 이용하고 있다면 adc 디바이스가 power-on 되기 전에 반드시 i2c 컨트롤러가 on 되어야 한다.: 위에 접두사를 사용하는 함수들은 공통점이 하나있다. 주로 `서브-시스템` 레벨의 디바이스에서 위에 함수들이 사용된다. 즉, 버스, 컨트롤러 등에서 사용된다는 말이다. 왜 그런지는 역시나 이 글을 참고하자.
- Power information in LDM
: 리눅스 디바이스 모델에서 PM callback 은 파워 매니지먼트 `동작` 과 관련이 있다. 그렇다면, 마치 알고리즘 & 자료구조의 관계처럼 알고리즘이 `동작` 이라면, 자료 구조에 해당하는 `상태` 는 없는걸까?
// include/linux/device.h - v6.5 struct device { .... struct dev_pm_info power; struct dev_pm_domain *pm_domain; .... };
: struct dev_pm_info 와 struct dev_pm_domain 은 디바이스의 파워 인포메이션을 나타낸다고 볼 수 있다. 이 구조체들에 대해서 알아보자.
1. struct dev_pm_info
: struct dev_pm_info 구조체는 리눅스 v2.6 버전 이전부터 존재했던 역사가 깊은 구조체다. 이 구조체는 멤버 변수의 개수에 비해 생각보다 구조를 파악하기가 쉽다. 크게 3개로 나눠 볼 수 있다. 아래 내용에서 볼 수 있다시피, 이 구조체의 기능은 명확하다. systme PM & rumtime PM 관련 정보들이 모두 이 구조체에 저장되고, 이후에 관련 정보들을 추가할 때도 이 구조체에 저장하면 된다.
1. System PM
1.1 `power_state ~ lock` 필드 : suspend 및 resume 과정에서 사용되는 플래그
1.2 `entry ~ should_wakeup` : wakeup source 에서 사용되는 필드
2. Runtime PM
- `suspend_timer ~ accounting_timestamp` : Runtime PM 에서 사용되는 필드// include/linux/pm.h - v6.5 struct dev_pm_info { pm_message_t power_state; unsigned int can_wakeup:1; unsigned int async_suspend:1; bool in_dpm_list:1; /* Owned by the PM core */ bool is_prepared:1; /* Owned by the PM core */ bool is_suspended:1; /* Ditto */ bool is_noirq_suspended:1; bool is_late_suspended:1; bool no_pm:1; bool early_init:1; /* Owned by the PM core */ bool direct_complete:1; /* Owned by the PM core */ u32 driver_flags; spinlock_t lock; #ifdef CONFIG_PM_SLEEP struct list_head entry; struct completion completion; struct wakeup_source *wakeup; bool wakeup_path:1; bool syscore:1; bool no_pm_callbacks:1; /* Owned by the PM core */ unsigned int must_resume:1; /* Owned by the PM core */ unsigned int may_skip_resume:1; /* Set by subsystems */ #else unsigned int should_wakeup:1; #endif #ifdef CONFIG_PM struct hrtimer suspend_timer; u64 timer_expires; struct work_struct work; wait_queue_head_t wait_queue; struct wake_irq *wakeirq; atomic_t usage_count; atomic_t child_count; unsigned int disable_depth:3; unsigned int idle_notification:1; unsigned int request_pending:1; unsigned int deferred_resume:1; unsigned int needs_force_resume:1; unsigned int runtime_auto:1; bool ignore_children:1; unsigned int no_callbacks:1; unsigned int irq_safe:1; unsigned int use_autosuspend:1; unsigned int timer_autosuspends:1; unsigned int memalloc_noio:1; unsigned int links_count; enum rpm_request request; enum rpm_status runtime_status; enum rpm_status last_status; int runtime_error; int autosuspend_delay; u64 last_busy; u64 active_time; u64 suspended_time; u64 accounting_timestamp; #endif struct pm_subsys_data *subsys_data; /* Owned by the subsystem. */ void (*set_latency_tolerance)(struct device *, s32); struct dev_pm_qos *qos; };
- 아래 Makefile 파일에서도 볼 수 있다시피, runtime PM(runtime.o) 이 CONFIG_PM 과 system PM(main.o , wakeup.o) 이 CONFIG_PM_SLEEP 과 관련 있음을 확인할 수 있다.
/drivers/base/power/Makefile
obj-$(CONFIG_PM) += sysfs.o generic_ops.o common.o qos.o runtime.o wakeirq.o
obj-$(CONFIG_PM_SLEEP) += main.o wakeup.o wakeup_stats.o
1. System PM with `CONFIG_PM_SLEEP`
- in_dpm_list : 해당 디바이스가 PM Core의 `dpm_list`에 포함되어 있는지 여부를 나타낸다.
- is_prepared : `system PM transition` 과정에서 `non-sysdev` 디바이스가 `->prepare` 함수를 성공적으로 호출하면, 이 필드가 SET 된다.
- is_suspended : `system PM transition` 과정에서 `non-sysdev` 디바이스가 `->suspend` 함수를 성공적으로 호출하면, 이 필드가 SET 된다.
- is_noirq_suspended : `system PM transition` 과정에서 `non-sysdev` 디바이스가 `->suspend_noirq` 함수를 성공적으로 호출하면, 이 필드가 SET 된다.
- is_late_suspended : `system PM transition` 과정에서 `non-sysdev` 디바이스가 `->suspend_late` 함수를 성공적으로 호출하면, 이 필드가 SET 된다.
- direct_complete : `system PM transition` 과정에서 디바이스가 이미 `runtime-suspended` 상태면, 이 값이 SET 된다. 그래서, 이후에 `suspend, suspend_late, suspend_noirq` 모두 그냥 패스한다. 혹은, 디바이스가 `PM callbacks`을 구현하고 있지 않다면(`dev->power.no_pm_callbacks`) 이 필드가 SET 된다[참고1].
- syscore : 해당 디바이스가 sysdev 인지를 나타낸다. 직접 설정하기 보다는 `dev_pm_syscore_device`를 통해서 값이 변경된다. 이 값이 SET 되어있으면 별도의 syscore callback 루틴으로 suspend 에 들어간다.
- no_pm_callbacks : 디바이스가 `PM callbacks`을 구현하고 있는지 여부를 나타낸다. 근데, 이 필드는 해당 디바이스의 `PM callbacks`을 검사하는게 아니라, 디바이스의 `bus, class, type, pm_domain`의 `dev_pm_ops`를 모두 검사한다. 즉, 앞에 언급된 드라이버들이 모두 `PM callbacks`을 구현하고 있지 않을 때, 이 값이 SET 된다(`device_pm_check_callbacks()`).
- Runtime PM with `CONFIG_PM`
- usage_count : Runtime PM을 사용하는 디바이스는 이 필드가 `0`이면, `runtime suspended` 상태로 진입한다.
- child_count : `active`한 자식 디바이스 개수를 나타낸다. 부모는 `active` 한 자식 디바이스가 하나라도 있을 경우, sleep 에 들어갈 수 없다.
- disable_depth : 이 값이 양수면, runtime helper 함수들은 동작하지 않는다. 즉, Runtime PM을 사용할 것이라면, 이 필드는 0 으로 만들어야 한다.
- ignore_children : 이 값이 SET 되면, `dev->power.child_count` 값은 무시된다.2. struct dev_pm_domain [참고1]
: dev_pm_domain 은 파워 매니지먼트 서브-시스템에서 가장 높은 우선 순위를 가지고 있다. 그 이유는 LDM 이 실제 SoC 구조를 정확히 반영하지는 못하기 때문이다. LDM 은 버스와 디바이스를 가지고만 계층 구조를 만들려고한다. 심지어, struct platform_device 및 bus_type platform_bus_type 은 모든 디바이스를 추상화시키 위해 구체적인 내용들을 모두 배제한다. 즉, 통일된 인터페이스를 제공하기 위해 실제 물리적인 설계 구조등을 배제한다. 그래서, 실제 SoC 구조를 그대로 반영할 수 있는 추상화된 자료 구조가 필요했고, 그것이 struct dev_pm_domain 과 genpd 의 등장 배경이 되었다.
PM: Add support for device power domains
The platform bus type is often used to handle Systems-on-a-Chip (SoC) where all devices are represented by objects of type struct platform_device. In those cases the same "platform" device driver may be used with multiple different system configurations, but the actions needed to put the devices it handles into a low-power state and back into the full-power state may depend on the design of the given SoC. The driver, however, cannot possibly include all the information necessary for the power management of its device on all the systems it is used with. Moreover, the device hierarchy in its current form also is not suitable for representing this kind of information.
The patch below attempts to address this problem by introducing objects of type struct dev_power_domain that can be used for representing power domains within a SoC. Every struct dev_power_domain object provides a sets of device power management callbacks that can be used to perform what's needed for device power management in addition to the operations carried out by the device's driver and subsystem.
Namely, if a struct dev_power_domain object is pointed to by the pwr_domain field in a struct device, the callbacks provided by its ops member will be executed in addition to the corresponding callbacks provided by the device's subsystem and driver during all power transitions.
- 참고 : https://github.com/torvalds/linux/commit/7538e3db6e015e890825fbd9f8659952896ddd5b: `strcut dev_pm_domain` 구조체는 genpd 와 직접적인 연관을 띄고 있다. 디바이스의 probe 함수가 호출되는 시점에 dev_pm_domain_attach 함수가 호출되는데, 여기서 디바이스 트리를 파싱해서 해당 디바이스의 파워 도메인을 체크한다. 만약 디바이스 트리에 디바이스의 파워 도메인이 존재하고 디바이스가 성공적으로 파워 도메인에 attach 될 경우, 2개의 콜백 함수가 자동으로 디바이스의 pm_domain 에 등록된다.
1. dev->pm_domain->detach = genpd_dev_pm_detach
2. dev->pm_domain->sync = genpd_dev_pm_sync// include/linux/pm.h - v6.5 /** * struct dev_pm_domain - power management domain representation. * * @ops: Power management operations associated with this domain. * @start: Called when a user needs to start the device via the domain. * @detach: Called when removing a device from the domain. * @activate: Called before executing probe routines for bus types and drivers. * @sync: Called after successful driver probe. * @dismiss: Called after unsuccessful driver probe and after driver removal. * * Power domains provide callbacks that are executed during system suspend, * hibernation, system resume and during runtime PM transitions instead of * subsystem-level and driver-level callbacks. */ struct dev_pm_domain { struct dev_pm_ops ops; int (*start)(struct device *dev); void (*detach)(struct device *dev, bool power_off); int (*activate)(struct device *dev); void (*sync)(struct device *dev); void (*dismiss)(struct device *dev); };
: 이 구조체는 나중에 genpd 글에서 자세히 설명한다.
- Back to the purpose of this article
: 이 글을 작성한 이유는 다음과 같다. 아래의 내용을 이해하기 위해 우리는 2 종류의 인터페이스 집합을 살펴볼 것이다.
1. 리눅스 커널 파워 매니지먼트 서브 시스템은 어떻게 하드웨어 종속적인 코드를 어떻게 추상화 하는가 ?
2. 리눅스 커널 파워 매니지먼트 서브 시스템은 드라이버 개발자들에게 어떻게 편의성을 제공하는가 ?1. 개별 PM 다비이스 인터페이스
: 리눅스 커널은 power management 프레임워크 역할을 함으로써 power transition 을 컨트롤 한다고 했다. 이 말은 드라이버 개발자들은 단순히 인터페이스를 구현하면, 커널이 특정 상황이 되면 알아서 호출한다는 뜻이다. 그런데, 드라이버 개발자가 본인이 원하는 시점에 해당 디바이스만 suspended 상태로 진입시키고 싶을 수 있다. 바로 이 때, pm_generic_* 함수를 사용하면 된다.
// include/linux/pm.h - v6.5 #ifdef CONFIG_PM_SLEEP extern int pm_generic_prepare(struct device *dev); extern int pm_generic_suspend_late(struct device *dev); extern int pm_generic_suspend_noirq(struct device *dev); extern int pm_generic_suspend(struct device *dev); extern int pm_generic_resume_early(struct device *dev); extern int pm_generic_resume_noirq(struct device *dev); extern int pm_generic_resume(struct device *dev); extern int pm_generic_freeze_noirq(struct device *dev); extern int pm_generic_freeze_late(struct device *dev); extern int pm_generic_freeze(struct device *dev); extern int pm_generic_thaw_noirq(struct device *dev); extern int pm_generic_thaw_early(struct device *dev); extern int pm_generic_thaw(struct device *dev); extern int pm_generic_restore_noirq(struct device *dev); extern int pm_generic_restore_early(struct device *dev); extern int pm_generic_restore(struct device *dev); extern int pm_generic_poweroff_noirq(struct device *dev); extern int pm_generic_poweroff_late(struct device *dev); extern int pm_generic_poweroff(struct device *dev); extern void pm_generic_complete(struct device *dev); #endif
: `pm_generic_*` 함수는 아래에서 볼 수 있다시피, 함수의 구현이 모두 동일하다. 그런데, 이렇게 개별적으로 디바이스 콜백 함수를 호출할 일이 있을까? 사실 거의 없다. 왜냐면, Runtime PM 이 등장했기 때문이다. 드라이버 개발자들에게 편리성을 제공해주기 위해 나왔지만, 현재는 버스 타입의 드라이버에서만 간혹 사용되고 있다.
// drivers/base/power/generic_ops.c - v6.5 /** * pm_generic_prepare - Generic routine preparing a device for power transition. * @dev: Device to prepare. * * Prepare a device for a system-wide power transition. */ int pm_generic_prepare(struct device *dev) { struct device_driver *drv = dev->driver; int ret = 0; if (drv && drv->pm && drv->pm->prepare) ret = drv->pm->prepare(dev); return ret; } /** * pm_generic_suspend - Generic suspend callback for subsystems. * @dev: Device to suspend. */ int pm_generic_suspend(struct device *dev) { const struct dev_pm_ops *pm = dev->driver ? dev->driver->pm : NULL; return pm && pm->suspend ? pm->suspend(dev) : 0; } /** * pm_generic_resume - Generic resume callback for subsystems. * @dev: Device to resume. */ int pm_generic_resume(struct device *dev) { const struct dev_pm_ops *pm = dev->driver ? dev->driver->pm : NULL; return pm && pm->resume ? pm->resume(dev) : 0; }
2) 전체 PM 디바이스 인터페이스
: 위에서는 개별 디바이스를 suspend 상태로 진입시키기 위한 인터페이스에 대해 알아봤다. 그런데 만약에, 전체 디바이스를 prepare 상태로 만들고 싶을 경우에는 어떻게 할까? 모든 PM 디바이스를 탐색해서 일일히 pm_generic_prepare 함수를 호출해야 할까? 이렇게 시스템 전체 디바이스를 특정한 상태로 진입시키는 인터페이스가 dpm_* 다.
// include/linux/pm.h - v6.5 extern void device_pm_lock(void); extern void dpm_resume_start(pm_message_t state); extern void dpm_resume_end(pm_message_t state); extern void dpm_resume_noirq(pm_message_t state); extern void dpm_resume_early(pm_message_t state); extern void dpm_resume(pm_message_t state); extern void dpm_complete(pm_message_t state); extern void device_pm_unlock(void); extern int dpm_suspend_end(pm_message_t state); extern int dpm_suspend_start(pm_message_t state); extern int dpm_suspend_noirq(pm_message_t state); extern int dpm_suspend_late(pm_message_t state); extern int dpm_suspend(pm_message_t state); extern int dpm_prepare(pm_message_t state);
: dpm_* 계열 인터페이스들의 인자가 `struct device` 가 아닌 것은 시스템의 존재하는 모든 디바이스를 대상으로 하기 때문이다. 그렇기 때문에, 디바이스를 인자로 전달할 필요가 없다. 대신 인자로 전달된 state 는 전환할 상태를 받는다.
// drivers/base/power/main.c - v6.5 LIST_HEAD(dpm_list); static LIST_HEAD(dpm_prepared_list); static LIST_HEAD(dpm_suspended_list); static LIST_HEAD(dpm_late_early_list); static LIST_HEAD(dpm_noirq_list); /** * dpm_prepare - Prepare all non-sysdev devices for a system PM transition. * @state: PM transition of the system being carried out. * * Execute the ->prepare() callback(s) for all devices. */ int dpm_prepare(pm_message_t state) { int error = 0; trace_suspend_resume(TPS("dpm_prepare"), state.event, true); might_sleep(); /* * Give a chance for the known devices to complete their probes, before * disable probing of devices. This sync point is important at least * at boot time + hibernation restore. */ wait_for_device_probe(); // --- 1 /* * It is unsafe if probing of devices will happen during suspend or * hibernation and system behavior will be unpredictable in this case. * So, let's prohibit device's probing here and defer their probes * instead. The normal behavior will be restored in dpm_complete(). */ device_block_probing(); // --- 1 mutex_lock(&dpm_list_mtx); while (!list_empty(&dpm_list) && !error) { // --- 2 struct device *dev = to_device(dpm_list.next); get_device(dev); // --- 2 mutex_unlock(&dpm_list_mtx); trace_device_pm_callback_start(dev, "", state.event); error = device_prepare(dev, state); // --- 3 trace_device_pm_callback_end(dev, error); mutex_lock(&dpm_list_mtx); if (!error) { dev->power.is_prepared = true; // --- 4 if (!list_empty(&dev->power.entry)) // --- 4 list_move_tail(&dev->power.entry, &dpm_prepared_list); // --- 4 } else if (error == -EAGAIN) { error = 0; } else { dev_info(dev, "not prepared for power transition: code %d\n", error); } mutex_unlock(&dpm_list_mtx); put_device(dev); // --- 2 mutex_lock(&dpm_list_mtx); } mutex_unlock(&dpm_list_mtx); trace_suspend_resume(TPS("dpm_prepare"), state.event, false); return error; }
0. prepare 단계는 기본적으로 모든 추가, 탐색, 등록 과정을 막는 단계다. 왜 막을까? 예를 들어, suspend 중에 디바이스가 추가되면 어떻게 될까? suspend 는 모든 디바이스가 suspend 될 때 까지 수행되는데, 이와중에 add 과정이 계속 발생하면, suspend 와 add 과정이 무한반복 될 수도 있다. 그렇기 때문에, 시스템이 suspend 로 진입하기 위해서 먼저 모든 추가, 탐색, 등록 과정을 들을 멈춰야 한다.
그리고, dpm_prepare 함수에서 눈 여겨 봐야할 점이 하나있다. 왜 이 함수는 동기화 기법으로 `mutex` 를 사용했을까 ? mutex 는 상당히 전통적인 동기화 기법으로 mutual exclusion 한 환경을 제공한다. 즉, reader 든 writer 는 mutex 영역안에는 1명만 들어갈 수 있다. 이걸 통해 system-wide power transition 은 반드시 하나의 CPU 가 하나의 태스크를 통해(선점없이) 쭉 끌고가야 하는 절차라는 것을 알 수 있다.
1. dpm_list 를 순회하여 device 포인터를 차례로 꺼낸다(LDM 에서 디바이스를 device_add 함수를 호출하여 커널에 등록하면, device_pm_add 함수 호출하여 해당 디바이스를 전역 연결 리스트 dpm_list 에 추가된다).
2. 왜 device_prepare 함수를 호출하기전에 get_device / put_device 함수를 호출할까? get_device / put_device 는 디바이스의 파워 보다는 디바이스를 동작시키는 소프트웨어, 즉, 드라이버의 라이프 사이클과 관련이 있다. 즉, 드라이버를 메모리에서 해제할 지 말지를 결정하는 인터페이스다. 만약, 이미 device 의 usage count 가 0 이라면, 메모리에서 드라이버가 제거되었고 디바이스는 power-off 되있을 가능성이 높다. 이 시점에 get_device 함수를 사용하는 것은 2 가지 가능성을 염두해 볼 수 있다.
1. usage count 가 0 이지만, 아직 메모리에서는 해제되지 않은 경우(즉, 제거 중 이라면), abort 해버린다.
2. 만약, usage count 가 1 이라면,
3. device_prepare 함수를 호출하여 실제 prepare 동작을 수행한다.
4. device_prepare() 성공하면, dev->power.is_prepared(struct dev_pm_info)를 TRUE로 설정하여 장치가 준비되었음을 나타낸다. 동시에 장치를 dpm_prepared_list에 추가한다(이 연결된 리스트은 이미 준비 상태에 있는 모든 디바이스를 저장함).
: device_prepare 함수는 드라이버에서 설정한 `->prepare` 콜백을 호출하는 함수다. 물론, 리눅스 커널의 파워 매니지먼트는 서브-시스템과 이루는 계층 구조 때문에 상위 계층의 `->prepare` 콜백이 존재할 경우, 드라이버의 `->prepare` 는 호출되지 않을 수 있다(그러나, 서브-시스템 내부적으로 드라이버의 PM callback 을 호출하는 구조를 가지고 있다).// drivers/base/power/main.c - v6.5 /** * device_prepare - Prepare a device for system power transition. * @dev: Device to handle. * @state: PM transition of the system being carried out. * * Execute the ->prepare() callback(s) for given device. No new children of the * device may be registered after this function has returned. */ static int device_prepare(struct device *dev, pm_message_t state) { int (*callback)(struct device *) = NULL; int ret = 0; /* * If a device's parent goes into runtime suspend at the wrong time, * it won't be possible to resume the device. To prevent this we * block runtime suspend here, during the prepare phase, and allow * it again during the complete phase. */ pm_runtime_get_noresume(dev); if (dev->power.syscore) return 0; device_lock(dev); dev->power.wakeup_path = false; if (dev->power.no_pm_callbacks) goto unlock; if (dev->pm_domain) callback = dev->pm_domain->ops.prepare; else if (dev->type && dev->type->pm) callback = dev->type->pm->prepare; else if (dev->class && dev->class->pm) callback = dev->class->pm->prepare; else if (dev->bus && dev->bus->pm) callback = dev->bus->pm->prepare; if (!callback && dev->driver && dev->driver->pm) callback = dev->driver->pm->prepare; if (callback) ret = callback(dev); unlock: device_unlock(dev); if (ret < 0) { suspend_report_result(dev, callback, ret); pm_runtime_put(dev); return ret; } /* * A positive return value from ->prepare() means "this device appears * to be runtime-suspended and its state is fine, so if it really is * runtime-suspended, you can leave it in that state provided that you * will do the same thing with all of its descendants". This only * applies to suspend transitions, however. */ spin_lock_irq(&dev->power.lock); dev->power.direct_complete = state.event == PM_EVENT_SUSPEND && (ret > 0 || dev->power.no_pm_callbacks) && !dev_pm_test_driver_flags(dev, DPM_FLAG_NO_DIRECT_COMPLETE); spin_unlock_irq(&dev->power.lock); return 0; }
1. 해당 디바이스가 syscore 디바이스 인지 확인한다. syscore 디바이스인 경우, 자신이 등록한 syscore_suspend 콜백 함수만 호출 되어야 한다. 그러므로, prepare, suspend, suspend_late, suspend_noirq 단계를 모두 건너뛴다. 이건 resume 과정에서도 마찬가지다.
2. prepare 단계에서는 pm_runtime_get_noresume() 함수를 호출하여 runtime suspend 기능을 끈다. runtime suspend로 인해 정상적으로 깨어날 수 없는 Issue를 피하십시오. 이 기능은 complete 단계 다시 켜집니다.
-> 이거 공식 문서에서도 보긴 했는데, 제대로 이해가 안간다. 제대로 찾아봐야 할 듯 하다.
3. device_may_wakeup() 함수를 호출하여 현재 장치에 wakeup source(dev->power.wakeup)가 있는지 여부와 wakeup(dev->power.can_wakeup)이 허용되는지 여부에 따라 장치가 wakeup path(dev->power.wakeup_path에 기록됨)인지 여부를 결정합니다.
4. 우선순위에 따라 prepare에 대한 callback 함수를 얻는다. 리눅스 커널의 디바이스 모델에서는 bus, driver, device 등 여러 계층이 있다. 근데 이놈들(bus, driver, device 등) 마다 dev_pm_ops가 있어서, prepare 인터페이스는 임의의 계층에 의해 구현될 수 있습니다. 여기서 우선순위는 우선순위가 높은 계층이 prepare를 등록하면 우선순위가 낮은 prepare를 사용하지 않는 것을 의미한다. 우선 순위는 dev->pm_domain->ops, dev->type->pm, dev->class->pm, dev->bus->pm, dev->driver->pm입니다(이 우선순위는 다른 device PM callbacks에도 적용된다).: `dpm_suspend` 함수는 prepare 과정을 완료한 디비이스를 대상으로 `->suspend` 콜백을 호출한다. 이 함수 또한 앞서 설명한 dpm_prepare 와 다를바가 없다. non-sysdev 의 suspend 는 거의 power-off 와 다를 바가 없다.
// drivers/base/power/main.c - v6.5 /** * dpm_suspend - Execute "suspend" callbacks for all non-sysdev devices. * @state: PM transition of the system being carried out. */ int dpm_suspend(pm_message_t state) { ktime_t starttime = ktime_get(); int error = 0; trace_suspend_resume(TPS("dpm_suspend"), state.event, true); might_sleep(); devfreq_suspend(); cpufreq_suspend(); mutex_lock(&dpm_list_mtx); pm_transition = state; async_error = 0; while (!list_empty(&dpm_prepared_list)) { // --- 1 struct device *dev = to_device(dpm_prepared_list.prev); get_device(dev); mutex_unlock(&dpm_list_mtx); error = device_suspend(dev); mutex_lock(&dpm_list_mtx); if (error) { pm_dev_err(dev, state, "", error); dpm_save_failed_dev(dev_name(dev)); } else if (!list_empty(&dev->power.entry)) { // --- 2 list_move(&dev->power.entry, &dpm_suspended_list); // --- 2 } mutex_unlock(&dpm_list_mtx); put_device(dev); mutex_lock(&dpm_list_mtx); if (error || async_error) break; } mutex_unlock(&dpm_list_mtx); async_synchronize_full(); if (!error) error = async_error; if (error) { suspend_stats.failed_suspend++; dpm_save_failed_step(SUSPEND_SUSPEND); } dpm_show_time(starttime, state, error, NULL); trace_suspend_resume(TPS("dpm_suspend"), state.event, false); return error; }
1. dpm_prepared_list 는 prepare 과정을 마친 디바이스들이 저장되어 있는 리스트다.
2. dpm_suspended_list 는 suspend 과정을 마친 디바이스들이 저장되어 있는 리스트다.: 나머지 dpm_* 계열의 함수들은 모두 비슷한 동작들을 수행한다. dpm_suspend_start() 함수는 dpm_prepare와 dpm_suspend 두 동작을 순차적으로 수행한다. dpm_suspend_end() 함수는 모든 디바이스의 `dev->driver->pm->suspend_late" 콜백과 모든 디바이스의 `dev->driver->pm->suspend_noirq()`을 차례로 실행한다. 동작은 위에서 설명한 것들과 유사하다.
: dpm_resume, dpm_complete, dpm_resume_start, dpm_resume_end는 전원 관리 프로세스의 wake-up 동작을 의미한다. dpm_suspend_xxx 시리즈의 인터페이스와 유사합니다. 주의점은 suspend 와 resume이 서로 반대과정이기 때문에, 함수 호출도 반대로 이루어진다.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] 커널 컨텍스트 (0) 2023.08.03 [리눅스 커널] Loadable Kernel Module(LKM) (0) 2023.08.03 [리눅스 커널] PM - Wakeup events framework (2) 2023.08.03 [LINUX][KERNE][ISSUE] sysfs store() 무한으로 계속 write하는 이슈 (0) 2023.08.03 [리눅스 커널] PM - Linux legacy power management (0) 2023.03.01