-
[리눅스 커널] PM - restart & shutdown & haltLinux/kernel 2023. 9. 18. 19:06
글의 참고
- Power Control System Architecture 2.1
- http://www.wowotech.net/pm_subsystem/reboot.html
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Overview
" 리눅스 커널에서 restart 와 shutdown 은 reboot 시스템 콜을 실행할 수 있다. reboot 는 기본적으로 `shutdown + power-on` 의 조합으로 이루어진 절차다. 그러므로, reboot 가 가능하다면 shutdown 도 당연히 가능해야 한다. 결국, shutdown 프로세스는 reboot 프로세스의 하위 집합 프로세스라고 볼 수 있다. halt 는 아키텍처 독립적인 소프트웨어 `reboot process` 라고 보면 된다.
http://www.wowotech.net/pm_subsystem/reboot.html" arm에서는 어떻게 power-off 를 진행할까? 예를 들어, SoC 레벨에서 CPU 나 device 는 어떤 순서로, 어떻게 power-off 를 진행해야 할까? arm 의 PSCI 스펙을 보면, SYSTEM_OFF 라는 기능이 있다.
Power State Coordination Interface" SYSTEM_OFF 전에 반드시 선행되어야 하는 작업들이 있다.
1. SYSTEM_OFF 를 요청할 마지막 CPU 를 제외한 모든 online CPUs 들을 `known state` 로 만들어야 한다(명확하지는 않지만, 여기서 `known state` 라는 것은 `CPU_OFF` 으로 봐도 무방할 듯 하다).
2. 저장할 필요가 있다고 판단되는 데이터들은 모두 `non-volatile storage`에 저장해야 한다." 모든 코어를 offline 상태로 만들기 위해서 online 상태에 있는 각 코어들을 대상으로 CPU_OFF 기능을 사용하게 하면 된다. 대신, 한 개의 CPU 코어는 남겨 놔야한다. 왜냐면, 최종적으로 SYSTEM_OFF 기능을 수행해야 할 CPU 코어가 필요하기 때문이다.
- Entry point starting to reboot?
" 유저 스페이스에서 쉘을 통해 `reboot` 명령어를 입력하면, reboot 시스템 콜이 호출된다. 커널에서는 아래의 함수가 호출된다.
1. `reboot` 명령어를 실행한 유저 프로세스는 `CAP_SYS_BOOT` capability 를 가지고 있어야 한다.
2. magic 넘버를 사용해서 `reboot` 명령어가 조작되지 않았는지를 검사한다.
3. 만약, `POWER_OFF` 를 요청했는데, 아키텍처 차원에서 지원되지 않으면, `HALT`로 대신한다.
4. cmd에 따라 다양한 reboot 프로세스가 존재한다.
- 일반적으로 `RESTART`는 `cold reset`을 의미하고, `RESTART2`는 `warm reset`을 의미한다.
- `C_A_D`는 `Ctrl + Alt + Del` 키를 눌러 `restart` 프로세스를 진행하는 것을 의미한다.
- `SW_SUSPEND`는 하이버네이션 프로세스를 의미한다.// kernel/reboot.c - v6.5 SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg) { struct pid_namespace *pid_ns = task_active_pid_ns(current); char buffer[256]; int ret = 0; if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT)) // --- 1 return -EPERM; if (magic1 != LINUX_REBOOT_MAGIC1 || // --- 2 (magic2 != LINUX_REBOOT_MAGIC2 && magic2 != LINUX_REBOOT_MAGIC2A && magic2 != LINUX_REBOOT_MAGIC2B && magic2 != LINUX_REBOOT_MAGIC2C)) return -EINVAL; ret = reboot_pid_ns(pid_ns, cmd); if (ret) return ret; if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !kernel_can_power_off()) // --- 3 cmd = LINUX_REBOOT_CMD_HALT; mutex_lock(&system_transition_mutex); switch (cmd) { // --- 4 case LINUX_REBOOT_CMD_RESTART: kernel_restart(NULL); break; case LINUX_REBOOT_CMD_CAD_ON: C_A_D = 1; break; case LINUX_REBOOT_CMD_CAD_OFF: C_A_D = 0; break; case LINUX_REBOOT_CMD_HALT: kernel_halt(); do_exit(0); case LINUX_REBOOT_CMD_POWER_OFF: kernel_power_off(); do_exit(0); break; case LINUX_REBOOT_CMD_RESTART2: ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1); if (ret < 0) { ret = -EFAULT; break; } buffer[sizeof(buffer) - 1] = '\0'; kernel_restart(buffer); break; #ifdef CONFIG_KEXEC_CORE case LINUX_REBOOT_CMD_KEXEC: ret = kernel_kexec(); break; #endif #ifdef CONFIG_HIBERNATION case LINUX_REBOOT_CMD_SW_SUSPEND: ret = hibernate(); break; #endif default: ret = -EINVAL; break; } mutex_unlock(&system_transition_mutex); return ret; }
" 리눅스 커널의 reboot은 `power-off`, `restart`, `halt`로 구성되어 있다. 이 3가지 메커니즘은 나름의 공통점을 가지고 있다.
1. `kernel_xxx_prepare` 함수를 호출해서 각 단계를 본격적으로 진입하기 전에 준비하는 과정을 거친다. 이 때 수행하는 작업들은 다음과 같다.
- `block_notifier_call_chain` 함수를 호출한다. 이 때, 전환되는 시스템 상태(`SYS_RESTART, SYS_HALT, SYS_POWER_OFF`)가 전달된다. `power-off, halt`는 시스템 상태 파라미터만 전달되지만, `restart`는 별도의 파라미터를 전달받는다(`restart`를 하는 방법은 여러 가지가 있기 때문이다).
- 시스템 상태를 `SYS_RESTART, SYS_HALT, SYS_POWER_OFF` 중에 하나로 설정한다.
- `usermodehelper_disable` 함수를 호출한다(`uevent` 관련)
- `device_shutdown` 함수를 호출해서 모든 주변 장치들을 shutdown 시킨다.
2. `restart & power-off`는 `do_kernel_xxxx_prepare` 함수를 호출한다. `halt`는 `do_kernel_xxx_prepare` 함수가 없다. 이걸 통해 알 수 있는 부분은, 실제 시스템이 종료가 되는 프로세스는 이 함수를 호출한다는 공통점이 있다. 이 함수는 호출한 드라이버들은 `SYS_OFF_MODE_POWER_OFF_PREPARE` 열거형을 사용해서 콜백 함수를 등록한다. 공통점은 주로 PMIC 드라이버가 이 기능을 구현하고 있고, 내용은 `PMIC에게 시스템이 곧 종료된다는 것`을 알리는 용도로 사용되고 있다.
3. `migrate_to_reboot_cpu` 함수를 호출해서, BSP 만이 현재 코드(`reboot 절차`)를 실행하도록 설정한다. 즉, 다른 CPU가 `reboot 절차` 코드를 수행할 수 없도록 한다.
4. `syscore shutdown` 함수를 호출해서 시스템 코어 디바이스들을 종료한다. 예를 들어, 인터럽트 혹은 GPIO 등이 있다(일반적으로, `device_shutdown` 함수는 주변 장치용이다).
5. `machine-xxx` 함수를 호출해서 아키텍처 종속적인 `reboot 절차`를 수행한다.- How to power-off a system?
" 리눅스 커널의 `power-off` 시퀀스의 시작은 `kernel_power_off` 함수로 시작한다. 이 함수는 크게 3가지 동작을 수행한다.
1. Shutdown all devices in system
2. Shutdown secondary cpu`s
3. Shutdown system" 위의 동작들에 초점을 맞춰 함수들을 분석해보자.
//kernel/reboot.c - v6.5 void kernel_power_off(void) { kernel_shutdown_prepare(SYSTEM_POWER_OFF); do_kernel_power_off_prepare(); migrate_to_reboot_cpu(); syscore_shutdown(); pr_emerg("Power down\n"); kmsg_dump(KMSG_DUMP_SHUTDOWN); machine_power_off(); }
" 제일 먼저 `power-off` 되는 컴포넌트는 `디바이스` 다. 디바이스들은 `kernel_shutdown_prepare -> device_shutdown` 함수에서 모두 shutdown 된다. 그전에 진행하는 몇 가지 작업을 알아보자. `reboot_notifier_list`에게 시스템이 `SHUTDOWN` 된다는 것을 먼저 알린다. 이 때, 주로 `WATCHDOG` 타이머를 비활성화한다(`bootlin`에 `SYS_POWER_OFF` 열거형을 검색해보자. 주로 `watchdog` 드라이버에서 사용되는 것을 확인할 수 있다). 그리고, 시스템이 현재 `SHUTDOWN` 진행중이라는 것을 알리기 위해 시스템 상태를 `SHUTDOWN`으로 변경한다. 그리고 모든 디바이스들을 `SHUTDOWN` 하기 위해 `device_shutdown` 함수를 호출한다.
// kernel/reboot.c - v6.5 .... static void kernel_shutdown_prepare(enum system_states state) { blocking_notifier_call_chain(&reboot_notifier_list, (state == SYSTEM_HALT) ? SYS_HALT : SYS_POWER_OFF, NULL); system_state = state; usermodehelper_disable(); device_shutdown(); } ....
" device_shutdown() 함수는 리눅스 커널 LDD 기반 모든 디바이스들의 `SHUTDOWN` 콜백 함수를 호출한다. 이 함수에서 중요한 부분은 디바이스를 탐색할 때, dpm_list 가 아닌 devices_ket 을 탐색한다는 것이다. 그리고, `shutdown` 이기 때문에 device_kset 에 디바이스들을 계속 저장 해놓을 필요가 없다. 즉, `shutdown` 된 디바이스는 devices_kset 리스트에서 제거해도 된다. 그래서, 루프 탐색 조건이 `while(!list_empty(...))` 이다.
1. dpm_list : 시스템 suspend / resume 시에는 이 변수를 사용한다.
2. devices_kset : 시스템 shutdown 시에는 이 변수를 사용한다. `/sys/devices/` 폴더를 나타낸다. 이 디바이스는 시스템에 존재하는 모든 디바이스들을 포함한다(`device_kset->list`).// drivers/base/core.c - v6.5 void device_shutdown(void) { struct device *dev, *parent; wait_for_device_probe(); device_block_probing(); cpufreq_suspend(); spin_lock(&devices_kset->list_lock); while (!list_empty(&devices_kset->list)) { // --- 3 dev = list_entry(devices_kset->list.prev, struct device, kobj.entry); parent = get_device(dev->parent); get_device(dev); list_del_init(&dev->kobj.entry); spin_unlock(&devices_kset->list_lock); if (parent) device_lock(parent); device_lock(dev); pm_runtime_get_noresume(dev); pm_runtime_barrier(dev); if (dev->class && dev->class->shutdown_pre) { if (initcall_debug) dev_info(dev, "shutdown_pre\n"); dev->class->shutdown_pre(dev); } if (dev->bus && dev->bus->shutdown) { if (initcall_debug) dev_info(dev, "shutdown\n"); dev->bus->shutdown(dev); } else if (dev->driver && dev->driver->shutdown) { if (initcall_debug) dev_info(dev, "shutdown\n"); dev->driver->shutdown(dev); } device_unlock(dev); if (parent) device_unlock(parent); put_device(dev); put_device(parent); spin_lock(&devices_kset->list_lock); } spin_unlock(&devices_kset->list_lock); }
" USB 마우스 및 키보드를 `shutdown` 시키기 위해서는 먼저 `USB 버스 컨트롤러`가 active 되어 있어야 한다. 그러므로, `get_device(dev->parent)` 를 통해 부모 디바이스를 먼저 active 시킨다.
" devices_kset 을 탐색할 때, list 가 empty 가 될 때 까지 탐색한다. 즉, shutdown 을 진행한 디바이스는 device_kset 에서 제거되야 함을 의미한다.
" 만약, `Runtime PM` 과 `System PM` 을 모두 구현한 디바이스가 있다고 하자. 이 때, `device_shutdown` 과정에서 디바이스가 이미 `runtime-suspended` 라고 하자. 그런데, 파워 세이빙 측면에서 `shutdown` > `suspend` 더 강하다. 그러므로, 디바이스를 wake-up 시킨 뒤, `shutdown` 프로세스를 수행하도록 한다.
" 왜 `dev->class`를 왜 별도의 `if`로 뺏을까? `if(class) ... else if(bus) ... else if(driver) ...` 형태로 놓을 순 없었을까? `class, type, pm_domain`은 LDD 에 포함되지 않는 객체들이다. 별도의 분류 체계로 봐야한다(그리고, 앞에 3개는 실제 디바이스라고 볼 수 도 없다). 그런데, 버스와 드라이버(디바이스)는 LDD에 포함되기 하나의 `if ... else if ... ` 로 묶는 것이다.
" 위에서 보면, `dev->bus->shutdown` 콜백 함수가 존재하면, 드라이버의 `shutdown` 콜백 함수가 호출되지 않는 것처럼 보인다. 그런데, 사실상 `dev->bus->shutdown` 콜백 함수 내부적으로 드라이버의 `shutdown` 콜백 함수를 호출해주고 있다(이 내용은 `platform device`를 기준으로 한다).
// drivers/base/platform.c - v6.5 .... struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups, .match = platform_match, .uevent = platform_uevent, .probe = platform_probe, .remove = platform_remove, .shutdown = platform_shutdown, .dma_configure = platform_dma_configure, .dma_cleanup = platform_dma_cleanup, .pm = &platform_dev_pm_ops, }; EXPORT_SYMBOL_GPL(platform_bus_type); .... static void platform_shutdown(struct device *_dev) { struct platform_device *dev = to_platform_device(_dev); struct platform_driver *drv; if (!_dev->driver) return; drv = to_platform_driver(_dev->driver); if (drv->shutdown) drv->shutdown(dev); } ....
" `migrate_to_reboot_cpu` 함수는 현재 `restart | power-of | halt` 작업을 처리하는 프로세스를 BSP 에서 실행하도록 한다. `PF_NO_SETAFFINITY` 플래그가 SET 되면, 유저 스페이스에서 해당 스레드의 `AFFINITY` 를 변경할 수 없게 만든다. 즉, 커널만이 스레드의 `AFFINITY` 를 변경시킬 수 있다. `set_cpus_allowed_ptr` 함수는 임의의 스레드를 특정 CPU`s 에서만 동작하도록 한다. 아래 코드에서는 `reboot_cpu` 가 `0` 이기 때문에, 현재 코드를 실행중 인 스레드를 `0`번 CPU 에서만 실행되도록 한다[참고1].
// kernel/reboot.c - v6.5 void migrate_to_reboot_cpu(void) { /* The boot cpu is always logical cpu 0 */ int cpu = reboot_cpu; cpu_hotplug_disable(); /* Make certain the cpu I'm about to reboot on is online */ if (!cpu_online(cpu)) cpu = cpumask_first(cpu_online_mask); /* Prevent races with other tasks migrating this task */ current->flags |= PF_NO_SETAFFINITY; /* Make certain I only run on the appropriate processor */ set_cpus_allowed_ptr(current, cpumask_of(cpu)); }
" `do_kernel_power_off_prepare` 함수는 시스템이 `power-off` 되기전에 수행해야 할 작업이 필. 이 함수는 `kernel_shutdown_prepare` 함수와 차이가 뭘까? `do_kernel_power_off_prepare` 함수가 호출되는 시점은 아래와 같은 상황이 전제된다.
1. 대부분의 `watchdog` 드라이버들이 `kernel_shutdown_prepare` 함수에서 중지된다.
2. 시스템 상태가 `SYS_POWER_OFF`다.
3. 주변 장치가 모두 `power-off` 상태다. system core 장치(SoC 내부 장치)들과 PMIC 등이 살아있다.
4. 유저 모드 헬퍼 함수를 사용할 수 없다." 즉, `do_kernel_power_off_prepare` 함수가 하는 일은 위의 상황들이 전제된 상황에서 이루어져야 하는 일들에 대해 작성해야 한다(`do_kernel_power_off_prepare` 함수는 주로 PMIC 드라이버가 구현하고 있다).
// kernel/reboot.c - v6.5 .... static void do_kernel_power_off_prepare(void) { blocking_notifier_call_chain(&power_off_prep_handler_list, 0, NULL); } ....
" `local_irq_disable` 함수는 로컬 인터럽트를 비활성화한다. 즉, `local_irq_disable` 함수를 실행한 CPU 만 모든 인터럽트가 비활성화된다. 왜냐면, `start-up & power-off` 와 같은 부트 시퀀스 작업들은 반드시 인터럽트가 비활성화 되어있어야 한다. 그리고, `smp_send_stop` 함수를 통해 현재 실행중 인 CPU 를 제외하고 나머지 CPUs 들은 `power-off` 한다. 그리고, 최종적으로 `do_kernel_power_off` 함수를 호출해서 시스템을 power-off 한다.
// arch/arm64/kernel/process.c - v6.5 /* * Power-off simply requires that the secondary CPUs stop performing any * activity (executing tasks, handling interrupts). smp_send_stop() * achieves this. When the system power is turned off, it will take all CPUs * with it. */ void machine_power_off(void) { local_irq_disable(); smp_send_stop(); do_kernel_power_off(); }
" `smp_send_stop` 함수는 위에서 얘기했지만, BSP 를 제외한 `Secondary CPU`s`들을 모두 `offline` 상태로 만든다.
// arch/arm64/kernel/smp.c - v6.5 static inline unsigned int num_other_online_cpus(void) { unsigned int this_cpu_online = cpu_online(smp_processor_id()); return num_online_cpus() - this_cpu_online; } void smp_send_stop(void) { unsigned long timeout; if (num_other_online_cpus()) { // --- 1 cpumask_t mask; cpumask_copy(&mask, cpu_online_mask); cpumask_clear_cpu(smp_processor_id(), &mask); if (system_state <= SYSTEM_RUNNING) pr_crit("SMP: stopping secondary CPUs\n"); smp_cross_call(&mask, IPI_CPU_STOP); // --- 2 } /* Wait up to one second for other CPUs to stop */ timeout = USEC_PER_SEC; while (num_other_online_cpus() && timeout--) // --- 3 udelay(1); if (num_other_online_cpus()) pr_warn("SMP: failed to stop secondary CPUs %*pbl\n", cpumask_pr_args(cpu_online_mask)); sdei_mask_local_cpu(); // --- 4 }
1. `num_other_online_cpus` 현재 코드를 실행중 인 프로세서를 제외한 나머지 online CPU`s 의 개수를 반환한다. `cpumask_copy` 함수를 통해 현재 online CPU`s 들을 알아내고, `cpumas_clear_cpu` 함수를 통해 현재 CPU를 제외시킨다. 만약, 현재 CPU 제외하고도 아직 online CPU`s가 있다면, IPI를 통해서 CPU를 종료시킨다(`smp_cross_call`).
2. `smp_cross_call` 함수는 모든 online CPU`s 들에게 IPI(`IPI_CPU_STOP`)을 전송해서 종료하게 만든다.
3. CPU`s 들을 종료하는데 시간이 걸릴 수 있기 때문에, `busy-wait` 방식으로 대기한다.
4. `sdei_mask_local_cpu` 함수는 현재 CPU에 `SDEI event`를 비활성화 한다. `SDEI`는 `Software Delegated Exception Interface`의 약자로 `secure firmware`가 OS에게 제공해주는 `system event`라고 보면 된다. `system event`는 우선 순위가 가장 높은 이벤트로 OS는 해당 이벤트를 받으면 즉각적으로 처리해야 한다. `sdei_mask_local_cpu` 함수는 이런 `system event`를 비활성화한다.
Software Delegated Exception Interface
" arm은 `power-off` 과정에서 `CPU_SUSPEND 혹은 CPU_OFF` 기능을 사용하기 전에, `SDEI_PE_MASK` 기능을 먼저 호출해야 한다고 명시하고 있다." `do_kernel_power_off` 함수는 `power-off` 마지막 단계에 호출되는 함수다. 이미 메모리에서 디스크로 저장해야 할 정보들은 모두 저장되었고, 모든 주변 장치, 시스템 코어 디바이스, Secondary CPU`s 들도 종료된 상태다. 이제 CPU 하나와 시스템만 Off 하면 된다. 어떻게 `power-off` 해야 할까? 이 시점에서 아키텍처 종속적인 명령어가 필요한 시점이다. arm64는 여기서 PSCI 기능을 통해서 시스템을 Off 한다. 전원은 언제 차단할까? 이 상태는 전원을 차단해도 되는 시점이다. 그리고, 실제로 전원을 차단해야 한다. 퀄컴 SoC 기준으로는 `외부 전원 -> PMIC -> CPU` 형태로 전원이 공급된다. 여기서 `PMIC -> CPU`의 전원을 Off 하는 것이다(`외부 전원 -> PMIC`는 소프트웨어적으로 컨트롤할 수 있는 영역이 아니다).
//include/linux/pm.h - v6.5 /* * Callbacks for platform drivers to implement. */ extern void (*pm_power_off)(void); // kernel/reboot.c - v6.5 static int legacy_pm_power_off(struct sys_off_data *data) { if (pm_power_off) pm_power_off(); return NOTIFY_DONE; } void do_kernel_power_off(void) { struct sys_off_handler *sys_off = NULL; if (pm_power_off) sys_off = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF, SYS_OFF_PRIO_DEFAULT, legacy_pm_power_off, NULL); atomic_notifier_call_chain(&power_off_handler_list, 0, NULL); unregister_sys_off_handler(sys_off); }
" 그럼 이제 방금 말한 아키텍처 종속적으로, 즉, arm64가 어떻게 시스템을 `power-off` 하는지 알아보자.
- sys-off handler & pm_power_off
" `sys-off handler 및 pm_power_off`는 각 벤더사 및 칩 제조사에서 제공해야 하는 아키텍처 종속적인 함수다. 이 함수들의 역할은 최종적으로 BSP와 시스템을 power-off 하는 것이다. 각 벤더사들이 자신들의 아키텍처에 맞게 이 내용을 작성을 해놓았다. 그런데, 현재 `pm_power_off` 함수 포인터를 이용해서 `power-off`를 진행하는 방식은 레거시 방식이 되었다[참고1 참고2]. 현재는 `sys-off handler`를 통해서 하는 방식으로 바뀌었다. 그러나, 2023년 9월을 기준으로 1년 반정도 밖에 되지 않는 기술이어서, 아직은 많은 드라이버 제조사에서 여전히 `pm_power_off` 포인터를 통해 시스템을 `power-off` 한다.
" `pm_power_off` 함수 포인터는 글로벌 변수다. 각 벤더사는 아키텍처 의존적인 `power-off` 기능을 `pm_power_off` 함수 포인터에 매핑해서 커널에게 제공해야 한다. 그렇면, 커널은 `power-off` 및 `restart` 시퀀스에 맞춰서 적합한 시점에 `pm_power_off` 함수를 호출하는 식으로 파워 시퀀스가 진행된다. 그런데, `pm_power_off`는 전역 변수이고 이 값을 설정하기 위한 `get / set` 함수를 제공하지 않아서, 이 변수에 접근하는 것에 대한 권한 문제가 있을 수 있다. 이것이 `pm_power_off` 변수의 첫 번째 문제다.
" 아래 소스 코드는 퀄컴의 `restart & reboot & power-off` 관련된 소스가 모아져있는 파일이다. 여기서, `pm_power_off` 글로벌 변수에 직접적으로 `do_msm_poweroff` 함수를 저장하는 것을 확인할 수 있다. 대부분의 드라이버들은 아래와 같이 `probe` 함수가 호출되는 시점에 `pm_power_off`에 자신들의 `power-off` 함수로 교체한다(퀄컴은 굉장히 단순하게, `PS_HOLD` 핀을 LOW로 내리는 방법으로 시스템 `power-off`를 구현했다).
// drivers/power/reset/msm-poweroff.c - v6.5 static void __iomem *msm_ps_hold; static int deassert_pshold(struct notifier_block *nb, unsigned long action, void *data) { writel(0, msm_ps_hold); mdelay(10000); return NOTIFY_DONE; } static struct notifier_block restart_nb = { .notifier_call = deassert_pshold, .priority = 128, }; static void do_msm_poweroff(void) { deassert_pshold(&restart_nb, 0, NULL); } static int msm_restart_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct resource *mem; mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); msm_ps_hold = devm_ioremap_resource(dev, mem); if (IS_ERR(msm_ps_hold)) return PTR_ERR(msm_ps_hold); register_restart_handler(&restart_nb); pm_power_off = do_msm_poweroff; return 0; }
" 두 번째 문제로, `restart` 와는 다르게 `power-off`는 `call-chanin`이 없다(`notifier callback`을 등록하는 함수가 없다). `power-off` 관련된 함수가 딸랑, `pm_power_off` 하나밖에 없다. 그래서 `sys-off handler`가 등장했다. `sys-off handler` 메커니즘은 다수의 `power-off` 핸들러를 호출하는 구조를 가지고 있다. `pm_power_off`도 `sys-off handler`에 콜백 인자로 던지면, 특정 시점에 알아서 호출되기 때문에 , 대부분의 벤더사들이 여전히 `pm_power_off`를 제거하지 않는 듯 하다.
" 본론으로 돌아와서, 칩 제조사 및 벤더사의 코드를 봐야하는데, 우리는 arm64의 `PSCI 0.2` 를 살펴볼 것 이다. `PSCI 0.2`는 `pm_power_off`로 `psci_sys_poweroff` 함수를 사용한다.
// drivers/firmware/psci/psci.c - v6.5 static void __init psci_0_2_set_functions(void) { .... register_restart_handler(&psci_sys_reset_nb); pm_power_off = psci_sys_poweroff; } static int __init psci_probe(void) { .... psci_0_2_set_functions(); .... return 0; } static int __init psci_0_2_init(const struct device_node *np) { .... return psci_probe(); } static const struct of_device_id psci_of_match[] __initconst = { .... { .compatible = "arm,psci-0.2", .data = psci_0_2_init}, .... };
" BSP(CPU[0])가 `do_kernel_power_off` 함수를 호출하면, `power_off_handler_list` 에 `pm_power_off` 가 추가된다. 이 때, `pm_power_off` 함수가 `psci_sys_poweroff` 가 된다. 그리고, `atomic_notifier_call_chain` 함수를 호출하면 `psci_sys_poweroff` 함수가 호출되면서, `SYSTEM_OFF` 기능을 호출하게 된다. 이 때, 시스템과 함께 BSP 도 `power-off` 가 된다(이 글 맨 처음에 `SYSTEM_OFF` 관련 PSCI 스펙을 보면, 마지막 남은 CPU는 `CPU_OFF`가 아닌, `SYSTEM_OFF` 를 호출해야 한다고 명시되어 있다).
//include/linux/reboot.h - v6.5 static void psci_sys_poweroff(void) { invoke_psci_fn(PSCI_0_2_FN_SYSTEM_OFF, 0, 0, 0); }
" arm64의 시스템 `power-off`는 이렇게 마무리된다. 이제, `restart`를 알아보자.
- How to re-start a system ?
" 커널에서 `re-start`의 엔트리 포인트는 `kernel_restart` 함수다. 아래 함수를 보면 알겠지만, `kernel_power_off` 함수와 중복되는 작업이 있다.
// kernel/reboot.c - v6.5 void kernel_restart(char *cmd) { kernel_restart_prepare(cmd); do_kernel_restart_prepare(); migrate_to_reboot_cpu(); syscore_shutdown(); if (!cmd) pr_emerg("Restarting system\n"); else pr_emerg("Restarting system with command '%s'\n", cmd); kmsg_dump(KMSG_DUMP_SHUTDOWN); machine_restart(cmd); }
" `kernel_restart_prepare` 함수는 `kernel_shutdown_prepare` 함수와 크게 다르지 않다(`power-off` 쪽 내용을 참고하자).
// kernel/reboot.c - v6.5 void kernel_restart_prepare(char *cmd) { blocking_notifier_call_chain(&reboot_notifier_list, SYS_RESTART, cmd); system_state = SYSTEM_RESTART; usermodehelper_disable(); device_shutdown(); }
" 왜 `kernel_restart_prepare` / `do_kernel_restart_prepare` 처럼 `prepare` 함수가 2개나 존재할까? `do_kernel_resstart_prepare:restart_prep_handler_list` 와 `kernel_restart_prepare:reboot_notifier_list` 의 차이가 뭘까?
1. `kernel_restart_prepare`의 reboot_notifier_list : 시스템 restart의 최초 시작 지점이다. 모든 디바이스가 살아있어서 뭔가 액션을 취하기는 어려운 위치다. 그렇기 때문에, 시스템이 `restart`를 시작한다는 것을 다른 드라이버들에게 알리는 용도로 사용된다. 그런데, `SYS_POWER_OFF`에서는 watchdog 드라이버들이 자주 사용하는걸 보았는데, `SYS_RESTART`는 명확하게 어떤 드라이버들이 사용하고 있는지가 눈에 들어오지 않는다.
2. `do_kernel_restart_prepare`의 restart_prep_handler_list : 시스템 상태가 `SYSTEM_RESTART`이고, 모든 디바이스가 shutdown 된, 상태에서 호출되는 리스트. `do_kernel_restart_prepare` 함수의 usage case를 알기 위해서 `bootlin`에 `SYS_OFF_MODE_RESTART_PREPARE`을 검색해보면 커널 6.5를 기준으로 `/drivers/acpi/sleep.c`에서만 사용하고 있다.// kernel/reboot.c - v6.5 static void do_kernel_restart_prepare(void) { blocking_notifier_call_chain(&restart_prep_handler_list, 0, NULL); }
" `migrate_to_reboot_cpu` / `syscore_shutdown` 함수 또한 위에 `power-off` 쪽을 참고하자.
" `machine_restart` 함수는 칩 제조사에서 제공하는 기능을 이용해서 시스템을 restart 시키는 함수다. 먼저, `restart sequence` 중에 방해받지 않도록, 인터럽트를 비활성화한다. 그리고, `smp_send_stop` 함수를 호출해서 `Secondary CPU`s` 들을 `offline`상태로 만든다.
// arch/arm64/kernel/process.c - v6.5 void machine_restart(char *cmd) { /* Disable interrupts first */ local_irq_disable(); smp_send_stop(); /* * UpdateCapsule() depends on the system being reset via * ResetSystem(). */ if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_reboot(reboot_mode, NULL); /* Now call the architecture specific reboot code. */ do_kernel_restart(cmd); printk("Reboot failed -- System halted\n"); while (1); }
" `do_kernel_restart` 함수는 주석에도 나와있지만, `restart sequence`의 가장 마지막에 호출되는 함수다. 만약에, `restart handler`가 등록되어 있다면, 시스템은 즉각적으로 `restart` 한다고 명시되어 있다. arm64의 `restart handler`는 PSCI 0.2를 기준으로 `psci_0_2_set_functions` 함수에서 등록된다.
/** * .... * Expected to be called from machine_restart as last step of the restart * sequence. * * Restarts the system immediately if a restart handler function has been * registered. Otherwise does nothing. */ void do_kernel_restart(char *cmd) { atomic_notifier_call_chain(&restart_handler_list, reboot_mode, cmd); }
" `restart handler`로 등록되는 함수는 `psci_sys_reset` 함수다.
//include/linux/reboot.h - v6.5 enum reboot_mode { REBOOT_UNDEFINED = -1, REBOOT_COLD = 0, REBOOT_WARM, REBOOT_HARD, REBOOT_SOFT, REBOOT_GPIO, }; extern enum reboot_mode reboot_mode; .... // drivers/firmware/psci/psci.c - v6.5 static int psci_sys_reset(struct notifier_block *nb, unsigned long action, void *data) { if ((reboot_mode == REBOOT_WARM || reboot_mode == REBOOT_SOFT) && psci_system_reset2_supported) { /* * reset_type[31] = 0 (architectural) * reset_type[30:0] = 0 (SYSTEM_WARM_RESET) * cookie = 0 (ignored by the implementation) */ invoke_psci_fn(PSCI_FN_NATIVE(1_1, SYSTEM_RESET2), 0, 0, 0); } else { invoke_psci_fn(PSCI_0_2_FN_SYSTEM_RESET, 0, 0, 0); } return NOTIFY_DONE; } static struct notifier_block psci_sys_reset_nb = { .notifier_call = psci_sys_reset, .priority = 129, };
" 만약, `reboot_mode`가 `REBOOT_WARM` 혹은 `REBOOT_SOFT` 이면서, PSCI 1.1 버전을 사용중이면, `SYSTEM_RESET2` 기능을 호출한다. `SYSTEM_RESET2` 기능을 호출할 때, `reset type` 이라는 인자를 전달하는데, 이 값의 31번째 비트에 따라 2가지 리셋 모드를 지원한다.
1. reset type의 31번째 비트 == 1 : vendor-specific resets i.e) qualcomm, intel, broadcom ...
2. reset type의 31번째 비트 == 0 : architectural resets i.e) x86, arm, risc-v ..." 만약, `reset type의 31번째 비트 == 1` 이면, vendor 사에서 리셋 기능을 제공한다. 만약, `0`이면 `reset type[30:0] 비트`를 통해서 어떤 리셋을 할지를 결정한다. 만약, reset type[30:0] 비트가 `0`이면 `SYSTEM_WARM_RESET`을 진행한다. 그런데, reset type[31] 비트말고는 나머지 비트([30:0])는 모두 `0`이여야 한다. 즉, `reset type`이 architectural reset 일 경우, WARM RESET 만 할 수 있다.
" `reset type`이 architectural reset 일 경우, [0x7FFF_FFFF:0x1] 까지 어떤 리셋을 사용할지를 reserved for future로 남겨놓는다(0x0은 `warm reset`으로 사용). vendor-specific reset 일 경우, [0xFFFF_FFFF:0x8000_0000] 까지 어떤 리셋을 사용할지를 reserved for future로 남겨놓는다.
Power State Coordination Interface
Power State Coordination Interface" `SYSTEM_WARM_RESET`은 `system main memory` 가 reset 되지 않기 때문에, `fast reboot` 목적으로 사용한다.
" 만약, `SYSTEM_RESET2`를 사용할 수 없다면, PSCI 0.2 `SYSTEM_RESET` 명령어를 사용한다. 이 기능은 `system cold reset`을 수행하는 명령어다. 즉, `hardware power-cycle sequence`를 다시 시작하는 것을 의미한다.
Power State Coordination Interface
Power State Coordination Interface- How to halt a system ?
" 리눅스 커널의 `halt` 는 앞에서 언급했던, power-off 와 restart 프로세스와 거의 동일하다. 그런데, 함수 호출 프로세스에서 명확한 차이가 하나있다. 바로 do_kernel_xxx_prepare() 함수를 호출하지 않는다는 것이다. 한 번 생각해보자. power-off & restart 의 공통점과 halt 와의 차이점은 뭘까? 바로 power-off 시퀀스의 여부다. power-off & restart 프로세스들은 모두 power-off 를 진행한다. 그러나, halt 는 power-off 를 하지 않는다. 그러므로, SoC 가 power-off 되기 전에 사전 작업을 수행하는 do_kernel_xxx_prepare() 함수를 호출할 필요가 없는 것이다.
// kernel/reboot.c - v6.5 void kernel_halt(void) { kernel_shutdown_prepare(SYSTEM_HALT); migrate_to_reboot_cpu(); syscore_shutdown(); pr_emerg("System halted\n"); kmsg_dump(KMSG_DUMP_SHUTDOWN); machine_halt(); }
" `kernel_shutdown_prepare(SYSTEM_HALT)` 함수 호출시에 이벤트를 notification 받는 드라이버는 누가 있을까? bootlin 에 SYSTEM_HALT 를 검색하면, 다수의 watchdog 드라이버들이 SYSTEM_HALT 시에 STOP 하는 것을 확인할 수 있다.
" arm64 의 machine_halt() 함수는 심플하다. BSP 의 모든 로컬 인터럽트를 비활성화하고, Secondary CPUs 들을 모두 offline 상태로 만든다(smp_send_stop()). 그리고, BSP 는 무한 루프에 진입한다. 바로 이 부분이 halt process 가 software reboot process 라고 하는 이유다. 즉, 각 CPU 아키텍처에 의존적인 명령어를 사용하지 않고, 모든 아키텍처에 독립적인 C 언어 레벨에서 halt process 를 구현한 것이다(`while(1)`). 그렇다면, shell 창에서 halt 명령어를 입력하고 wake-up 하는 방법은 뭘까? 없다. 왜냐면, secondary CPUs 들이 모두 power-off 되고, boot CPU 만 남은 시점에서 local interrupts 를 모두 disabled 하고, 무한 루프로 진입하기 때문에, 깨울 수 있는 방법이 없다[참고1].
// arch/arm64/kernel/process.c - v6.5 void machine_halt(void) { local_irq_disable(); smp_send_stop(); while (1); }
" halt 는 restart & power-off 와는 달리 소프트웨어적인 reboot process 이기 때문에, 아키텍처 독립적인 기능이다. PSCI 스펙에도 SYSTEM_OFF & SYSTEM_RESET 은 필수이지만, HALT 는 명시되어 있지 않다.
Power State Coordination Interface- Summary
" 위에 내용을 봐서 알겠지만, 리눅스 커널의 reboot process 는 커널이 아웃라인을 잡고 컨트롤한다. 칩 제조사 및 벤더사에 소속된 드라이버 개발자가 아니라면, 2 가지만 기억하면 된다.
1. CPU 아키텍처에 따른 리눅스 커널의 reboot 프로세스(restart, shotdown, halt) 가 어떻게 진행되는 알고 있어야 한다.
2. 필요하다면, 자신이 맡은 디바이스가 reboot 프로세스(restart, shotdown, halt) 에서 올바르게 동작할 수 있도록 커널에 shutdown, restart, halt 기능을 제공해야 한다." 만약, 제조사 및 벤더사에 소속되어 있다면, 아키텍처 종속적인 스펙들에 디테일하게 알고 있어야 한다(arm - PSCI, x86 - ACPI).
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] LDM - kobject, kset, ktype (0) 2023.09.22 [리눅스 커널] PM - Platform-dependent power management (0) 2023.09.18 [리눅스 커널] Timer - Broadcast timer (0) 2023.09.17 [리눅스 커널] Cpuidle - idle process (0) 2023.09.12 [리눅스 커널] PM QoS - CPU latency QoS framework (0) 2023.09.11