ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - restart & shutdown & halt
    Linux/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).

Designed by Tistory.