ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Freezing of task
    Linux/kernel 2023. 9. 1. 02:35

    글의 참고

    - https://www.kernel.org/doc/html/next/power/freezing-of-tasks.html

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

    - https://www.kernel.org/doc/Documentation/power/freezing-of-tasks.txt

    - https://blogs.oracle.com/linux/post/freezing-tasks-ksplice

    - https://lore.kernel.org/lkml/20220822114649.055452969@infradead.org/

    - https://f0rm2l1n.github.io/2022-09-07-How-Signal-Works-inside-the-Kernel/

    - https://blog.csdn.net/farmwang/article/details/70174192

    - https://zhuanlan.zhihu.com/p/402374191

    - http://blog.chinaunix.net/uid-20545423-id-1930171.html

    - https://stackoverflow.com/questions/62079388/x86-how-does-linux-signal-interrupt-instruction-stream

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

    - https://blog.csdn.net/m0_74282605/article/details/128239466


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Freezing or task

    : `Freezing of task`는 `hibernation` 및 `system-wide suspend` 시에, 모든 유저 프로세스와 일부 커널 스레드를 동결(`freeze`) 시키는 것을 의미한다. 근데 왜 동결을 할까?

    1" 하이버네이션 이미지를 만들 때, 특정 시점을 기준으로 `스냅샷`을 찍는다. 그리고 스냅샷이 찍힌 시점을 기준으로 하이버네이션 이미지를 만드는데, 태스크들이 살아있으면 파일 시스템을 다시 어지럽힐 확률이 높기 때문에, 앞전에 찍어논 스냅샷이 의미가 없어진다. 그러므로, 스냅샷을 찍기전에 반드시 파일 시스템을 건드리는 태스크들은 모두 freezing 되어야 한다.

    2" 모든 디바이스들을 안전하게 suspend / resume 하기 위해서는 반드시 모든 유저 및 일부 커널 스레드는 freezing 되어야 한다. 유저 프로세스 같은 경우는 파일 시스템 API(i.e sysfs) 를 통해서 디바이스에 액세스한다. 이럴 경우, 해당 디바이스는 busy 상태가 되서 suspend 상태로 진입하지 못하게 된다. 커널 스레드 같은 경우는 직접 코드에서 디바이스가 외부로 노출한 API 를 사용하거나 파일 시스템을 통해서 디바이스에 액세스한다. 그런데, 이렇게 태스크들이 디바이스를 사용하고 있을 경우에는 디바이스가 절대로 suspend 상태가 될 수 없다. 그러므로, 반드시 디바이스들을 사용하는 태스크들을 먼저 freezing 하고 디바이스와 링크가 해제된 후에야, 디바이스 suspend 가 진행된다.

     

    : 위와 같은 이유로 `Freezing of tasks` 가 필요하게 되었다.

     

     

    - Freeze object & count

    : 리눅스 커널에서 동결 대상으로 `유저 프로세스`, `커널 프로세스`, `워크-큐`를 들 수 있다. 모든 `유저 프로세스`는 기본적으로 동결 대상이된다. `유저 프로세스`를 동결 시키기 위해 `시그널` 메커니즘이 사용된다. `커널 프로세스`와 `워크-큐`는 기본적으로 동결 대상이 될 수 없다. 그러나, 일부 커널 스레드 및 워크-큐는 생성시에 `freezable` 상태로 만들어진다. 이러한 프로세스들은 시스템 suspending 과정에서 유저 프로세스와 함께 동결 대상이 된다.

    1. 커널 스레드 : 파일 시스템, 디바이스 드라이버 등과 관련된 몇몇 커널 스레드는 `freezing`이 될 수 있다. 이 때, `freezing`이 되길 바라는 커널 스레드는 자기 스스로 `PF_FREEZABLE` 플래그를 설정해놓으면, `freezer`가 suspend 과정에서 해당 커널 스레드에 `TIF_FREEZE` 플래그를 SET 하게 되고, 이 커널 스레드는 유저 프로세스와 마찬가지로 `try_to_freeze` 함수를 호출할 수 있게 된다. [참고1]

    2. 워크-큐 :  모든 워크-큐의 `max_active`를 0으로 설정한다. `max_active`가 0이 되면, 새로운 워크-큐는 추가될 수 없다. 그리고, 현재 워크-큐에 들어있는 작업들도 모두 멈춘다. 그리고, 동결이 완료되면, `nr_active` 또한 0이 된다. [참고1 참고2]
    On freeze, all cwq->max_active's are quenched to zero. Freezing is complete when nr_active on all cwqs reach zero.

    - 참고 : https://linux.kernel.narkive.com/hisfqMP4/patch-30-40-workqueue-implement-work-busy#post3

     

    : `hibernation` 및 `system-wide suspend` 시에, `freezing of tasks` 과정을 반드시 거치게 된다. 이 때, 실제 동결이 진짜로 되었는지 확인할 수 있어야 한다. 리눅스 커널에서는 이걸 확인하기 위해 3개의 전역 변수를 제공해준다.

     

    1. freezer_active(system_freezing_cnt) : 태스크마다 개별적으로 존재하는 플래그가 아닌, 시스템에 하나만 존재하는 변수다. 시스템이 동결 작업을 시작할지를 결정하는 변수다. 그와 동시에, 시스템이 현재 동결 작업을 진행중임을 나타낸다. 이 변수는 `freeze_processes` 함수에서 설정된다. 리눅스 커널 6.5 버전에서는 `freezer_active` 변수로 대체되었다.

    2. pm_freezing : 유저 프로세스 동결 작업이 진행중임을 의미한다.

    3. pm_nosig_freezing : 일부 커널 스레드 및 워크-큐의 동결 작업이 진행중임을 의미한다.

     

    - Freezer

    : `freezer` 이라는 메커니즘은 task를 `TASK_UNINTERRUPTIBLE` 상태에서 무한 루프로 보내는 것을 의미한다. 이 무한 루프를 `refrigerator`라고 부른다. 일단, 태스크가 냉장고에 들어가면 스케줄링 되지 않는다는 것이 보장된다. 그리고, `hibernation process`를 방해하지 못한다. 그런데, `프로세스 상태`라는 것이 뭘까?

     

    : 리눅스 커널에서 모든 프로세스는 `struct task_struct` 구조체로 표현하며, 리스트 자료 구조를 통해서 관리된다. `struct task_struct` 구조체에는 태스크의 상태 필드가 존재한다. 이 중에서 중요한 상태 정보는 3가지가 있다.

    TASK_RUNNING"
    - 태스크가 아직 실행은 아니지만 즉각적으로 실행이 가능하다거나, 혹은 현재 실행 중 이거나, 실행을 기다리는 상태를 의미한다. 이론적으로는 `RUNNABLE`이라는 상태도 존재하는데, 리눅스에서는 현재 동작하고 있는 태스크(RUNNING)와 동작이 즉각적으로 가능한(RUNNABLE) 태스크를 구별하지 않는다.

    TASK_INTERRUPTIBLE"
    - 태스크가 특정 조건이 될 때 까지 잠자고 있는 상황을 의미한다. 이 상황에서 TASK_RUNNING 상태로 가기 위해서는 wakeup 조건들이 필요하다. 아래와 같은 상황들이 있다.
    1" 특정 리소스에 대한 LOCK의 소유권을 얻기 위해 기다리는 상황. 혹은, 파일 읽기 인터럽트를 기다리느라 잠자고 있는 상황
    2" 하드웨어 wakeup 인터럽트
    3" SW적인 wakeup signal

    TASK_UNINTERRUPTIBLE"
    - 이 상태는 기본적으로 `TASK_INTERRUPTIBLE`와 동일하지만, 시그널에 의해서는 wakeup 하지 못하는 상태다. 이 상태는 특정한 조건에서만 깨어날 수 있는 상황이다.

     

    : 우리는 `system-wide suspend` 과정에서 `Freezing of task`를 살펴볼 것 이다. 최초의 시작은 심플하다. `suspend_prepare` 함수에서 프로세스를 동결시키는 절차가 필요하다. 그리고 `suspend_freeze_processes` 함수에서 유저 태스크 먼저 freezing(`freeze_processes`)하고, 커널 스레드를 freezing(`freeze_kernel_threads`) 시킨다.

    /**
     * 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)
    {
    	...
    	trace_suspend_resume(TPS("freeze_processes"), 0, true);
    	error = suspend_freeze_processes();
    	trace_suspend_resume(TPS("freeze_processes"), 0, false);
            ...
    }
    
    // kernel/power/power.h - v6.5
    #ifdef CONFIG_SUSPEND_FREEZER
    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;
    }

     

    : `Freezing of tasks` 과정의 함수 호출 관계는 아래와 같다.


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

     

    - Freeze user process

    : 유저 프로세스 freezing의 시작은 `freeze_processes` 함수로 시작한다. 이 함수는 먼저 현재 `user process freezing in progress` 상태를 알리기 위해 `pm_freezing`을 `true`로 설정한다. 그와 동시에 `freezer_active` 값을 1 증가시킨다. 이 변수는 무슨 변수일까?

    //kernel/power/process.c - v6.5
    /**
     * freeze_processes - Signal user space processes to enter the refrigerator.
     * The current thread will not be frozen.  The same process that calls
     * freeze_processes must later call thaw_processes.
     *
     * On success, returns 0.  On failure, -errno and system is fully thawed.
     */
    int freeze_processes(void)
    {
    	int error;
    
    	error = __usermodehelper_disable(UMH_FREEZING);
    	if (error)
    		return error;
    
    	/* Make sure this task doesn't get frozen */
    	current->flags |= PF_SUSPEND_TASK;
    
    	if (!pm_freezing)
    		static_branch_inc(&freezer_active);
    
    	pm_wakeup_clear(0);
    	pm_freezing = true;
    	error = try_to_freeze_tasks(true);
    	if (!error)
    		__usermodehelper_set_disable_depth(UMH_DISABLED);
    
    	BUG_ON(in_atomic());
    
    	/*
    	 * Now that the whole userspace is frozen we need to disable
    	 * the OOM killer to disallow any further interference with
    	 * killable tasks. There is no guarantee oom victims will
    	 * ever reach a point they go away we have to wait with a timeout.
    	 */
    	if (!error && !oom_killer_disable(msecs_to_jiffies(freeze_timeout_msecs)))
    		error = -EBUSY;
    
    	if (error)
    		thaw_processes();
    	return error;
    }

    : `freezer_active` 전역 변수는 기존 `system_freezing_cnt` 전역 변수를 대체하는 변수다. 이 변수가 0이 아니면, 현재 시스템은  `Freezing of tasks`가 진행 중이라는 것을 의미한다. `pm_wakeup_clear`함수는 `pm_abort_suspend` 전역 변수를 CLEAR 하기 위해서 호출된다. 즉, `Freezing of tasks` 과정에서 강제로 시스템을 wakeup 하는 것을 허용하지 않겠다는 소리다.

    ...
    /* total number of freezing conditions in effect */
    -atomic_t system_freezing_cnt = ATOMIC_INIT(0);
    -EXPORT_SYMBOL(system_freezing_cnt);
    +DEFINE_STATIC_KEY_FALSE(freezer_active);
    +EXPORT_SYMBOL(freezer_active);

    @@ -29,7 +30,7 @@ static DEFINE_SPINLOCK(freezer_lock);
    * freezing_slow_path - slow path for testing whether a task needs to be frozen
    * @p: task to be tested
    *
    - * This function is called by freezing() if system_freezing_cnt isn't zero
    + * This function is called by freezing() if freezer_active isn't zero
    * and tests whether @p needs to enter and stay in frozen state. Can be
    * called under any context. The freezers are responsible for ensuring the
    * target tasks see the updated state.
    ...

    - 참고 : https://lore.kernel.org/lkml/20220822114649.055452969@infradead.org/

     

    : `freeze_processes` 함수는 `Freezing of tasks`의 준비 셋업이라고 보면 된다. 즉, 현재 `freezing` 중이라는 마크만 해주는 정도다. 시스템에 존재하는 모든 프로세스를 freezing 시키는 함수는 `try_to_freeze_tasks` 함수다. 이 함수는 기본적으로 무한 루프를 통해서 `running` & `freezable` 한 상태를 갖는 태스크들한테 `freeze_task` 함수를 호출시킨다.

     

    // kernel/power/process.c - v6.5
    /*
     * Timeout for stopping processes
     */
    unsigned int __read_mostly freeze_timeout_msecs = 20 * MSEC_PER_SEC;
    
    static int try_to_freeze_tasks(bool user_only)
    {
    	const char *what = user_only ? "user space processes" :
    					"remaining freezable tasks";
    	struct task_struct *g, *p;
    	unsigned long end_time;
    	unsigned int todo;
    	bool wq_busy = false;
    	ktime_t start, end, elapsed;
    	unsigned int elapsed_msecs;
    	bool wakeup = false;
    	int sleep_usecs = USEC_PER_MSEC;
    
    	pr_info("Freezing %s\n", what);
    
    	start = ktime_get_boottime();
    
    	end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs);
    
    	if (!user_only)
    		freeze_workqueues_begin();
    
    	while (true) {
    		todo = 0;
    		read_lock(&tasklist_lock);
    		for_each_process_thread(g, p) {
    			if (p == current || !freeze_task(p))
    				continue;
    
    			todo++;
    		}
    		read_unlock(&tasklist_lock);
    
    		if (!user_only) {
    			wq_busy = freeze_workqueues_busy();
    			todo += wq_busy;
    		}
    
    		if (!todo || time_after(jiffies, end_time))
    			break;
    
    		if (pm_wakeup_pending()) {
    			wakeup = true;
    			break;
    		}
    
    		/*
    		 * We need to retry, but first give the freezing tasks some
    		 * time to enter the refrigerator.  Start with an initial
    		 * 1 ms sleep followed by exponential backoff until 8 ms.
    		 */
    		usleep_range(sleep_usecs / 2, sleep_usecs);
    		if (sleep_usecs < 8 * USEC_PER_MSEC)
    			sleep_usecs *= 2;
    	}
    
    	end = ktime_get_boottime();
    	elapsed = ktime_sub(end, start);
    	elapsed_msecs = ktime_to_ms(elapsed);
    
    	if (todo) {
    		pr_err("Freezing %s %s after %d.%03d seconds "
    		       "(%d tasks refusing to freeze, wq_busy=%d):\n", what,
    		       wakeup ? "aborted" : "failed",
    		       elapsed_msecs / 1000, elapsed_msecs % 1000,
    		       todo - wq_busy, wq_busy);
    
    		if (wq_busy)
    			show_freezable_workqueues();
    
    		if (!wakeup || pm_debug_messages_on) {
    			read_lock(&tasklist_lock);
    			for_each_process_thread(g, p) {
    				if (p != current && freezing(p) && !frozen(p))
    					sched_show_task(p);
    			}
    			read_unlock(&tasklist_lock);
    		}
    	} else {
    		pr_info("Freezing %s completed (elapsed %d.%03d seconds)\n",
    			what, elapsed_msecs / 1000, elapsed_msecs % 1000);
    	}
    
    	return todo ? -EBUSY : 0;
    }

    : `try_to_freeze_tasks` 함수의 구체적인 절차는 다음과 같다.

    1. `start = ktime ... ` : 모든 프로세스를 동결시키는데 얼마나 시간이 걸렸는지를 계산하기 위해 사용된다.

    2. `end_time = jiffi ... ` : 프로세스 동결 타임아웃은 기본 20sec(`freeze_timeout_msecs`)다. 

    3. `freeze_workqueues_begin` : 커널 워크-큐 동결 작업시작

    4. 무한 루프를 시작한다. 여기서 중요한 부분은 매 루프를 돌 때마다 `todo`가 0으로 초기화 된다는 게  중요하다.
     4.1 `for_each_process_thread` 문에서 시스템에 존재하는 모든 태스크를 순회하며서 동결시킨다. 그러나, `todo`가 매 루프를  순회할 때 마다 `0`으로 초기화된다는 것은 `for_each_process_thread` 문에서 한 번에  모든 프로세스를 동결시키지는 못한다 는 소리다. 즉, 매 루프때마다 `todo`의 값이 변하는 것을 의미한다. 
     4.2 무한 루프를 탈출했을 때, `todo`의 값이 0이 아니면, `wakeup event` 혹은 `default 20sec timeout` 때문에 탈출한 것을 의미한다.

    5. `wq_busy = freeze_work ...` : 커널에 프로세스는 워크-큐도 포함된다. 매 루프때 마다 동결 프로세스이 증가될 것을 예상할 수 있으므로, 매 루프마다 워크-큐들이 모두 동결되었는지를 체크한다. wq_busy가 0이면, 모든 워크-큐가 동결되었음을 의미한다. 만약, wq_busy가 1이면, 아직 active한 워크-큐가 있음을 의미한다.

    6. 무한 루프 탈출 조건으로 총 3가지가 있다. 참고로, 동결이 실패해서 `return` 문으로 함수를 끝내는게 아니라, 무한 루프만 종료하고 이후에 루틴은 계속 진행한다. 왜냐면, 뒤에서 `왜 실패했는가`에 대한 디버그 로그를 출력해야 하기 때문이다.
     6.1 `if (!todo || time_after(jiffies, end_time))` : 동결시킬 프로세스가 이제 더 이상 없거나(`!todo!), `freeze timeout`이 발생한  경우. 즉, 동결과정이 20초가 넘는 경우(`time_after`). 그러나, 실제로 핵심은 `타임아웃`이다. 왜냐면, 시간만 많이 주어진다면 모든 프로세스를 동결시키는 것은 시간문제다. 그러나, 동결 타임아웃이 디폴트로 20초 제한이 있기 때문에, 아직 동결 시키지 못한 프로세스가 생기는 것이다.
     6.2  `if(pm_wakeup_pending())` : 동결과정중에 wakeup 이벤트가 발생한 경우

    7. `usleep_range ... ` : 매 루프마다 모든 프로세스를 냉장고로 보내려고 시도하는데, 이 때 쉬지 않고 루프를 계속 순환하는 것은 해당 프로세스가 CPU를 긴 시간 독점할 여지가 있다. 그러므로, 프로세스가 `냉장고`에 들어가는 과정은 생각보다 시간이 좀 필요하기 때문에, 그 시간 동안은 다른 프로세스들에게 CPU를 양보하기 위해 잠에 들게한다. 

    8. `end = ktime_ ...  ; elapsed = ktime_ ...` : 모든 동결가능한 프로세스를 동결시키는데 걸린 시간(`ktime_sub(end, start)`). 만약, 동결이 실패해도 이 루틴은 탄다.  

    9. `if(todo) ... ` : 만약, 동결과정이 20초가 넘었다면, 디버깅 로그들을 출력한다.
     9.1 먼저 `wakeup event` 때문에 동결 과정이 `aborted`가 된 것인지, `timeout` 때문에 동결 과정이 `failed` 가 된 것인지를 출력한다. 그리고, 걸린 시간을 출력하고 동결을 하지 못한 프로세스 개수(`todo - wq_busy`)를 출력한다. 그런데, 어떻게 저게 프로세스 개수가 될까? `wq_busy`는 0 아니면 1이다. 즉, 워크-큐는 1개의 프로세스로 보는 것이다. 즉, 여러 워크-큐 작업들은 여러 개의 스레드로 보되, 실제 워크-큐 프로세스는 1개인 것이다.  
     9.2 만약, 동결되지 못한 워크-큐가 남아있다면, `freezable workqueues`들의 상태를 출력한다.
     9.3 if (!wakeup || pm_ ... ` : 만약, timeout으로 인해 루프가 종료되었고, `pm_debug_message_on`가 참이면 모든 프로세스를 순회하면서 각 태스크의 상태를 출력한다. 위에서도 말했지만, `timeout`으로 루프를 탈출했다는 것은 `todo`가 `0`이 아님을 의미하는 것이고, todo가 `0`이 아니라는것은 동결시키지 못한 프로세스가 존재한다는 것을 의미한다.

    : 이렇게 `try_to_freeze_task` 함수가 마무리된다. 그 다음은 시스템에 존재하는 모든 태스크들이 루프를 돌면서 수행하는 함수인 `freeze_task` 함수를 분석한다.

     

    : `freeze_task` 함수는 인자로 전달된 태스크가 유저인지 커널인지에 따라 어떻게 동결을 시킬지를 결정한다. 아래에서 다시 자세하게 다룬다.

    // kernel/freezer.c - v6.5
    /**
     * freeze_task - send a freeze request to given task
     * @p: task to send the request to
     *
     * If @p is freezing, the freeze request is sent either by sending a fake
     * signal (if it's not a kernel thread) or waking it up (if it's a kernel
     * thread).
     *
     * RETURNS:
     * %false, if @p is not freezing or already frozen; %true, otherwise
     */
    bool freeze_task(struct task_struct *p)
    {
    	unsigned long flags;
    
    	spin_lock_irqsave(&freezer_lock, flags);
    	if (!freezing(p) || frozen(p) || __freeze_task(p)) {
    		spin_unlock_irqrestore(&freezer_lock, flags);
    		return false;
    	}
    
    	if (!(p->flags & PF_KTHREAD))
    		fake_signal_wake_up(p);
    	else
    		wake_up_state(p, TASK_NORMAL);
    
    	spin_unlock_irqrestore(&freezer_lock, flags);
    	return true;
    }

     

    : `freezing` 함수는 인자로 전달된 태스크가 동결이 가능한 태스크인지를 판별한다. 그래서 제일 먼저, 시스템이 현재 `Freezing of task`를 진행 중 상태인지를 파악한다. 해당 내용은 `freezer_active`가 `0`이 아닌지를 검사해서 판별이 가능하다. 그리고 실제 전달된 태스크가 동결이 가능한 태스크인지를 판별하는 기능은 `freezing_slow_path` 함수가 한다. 이 함수는 뒤에서 다시 다룬다.

    // include/linux/freezer.h - v6.5
    ...
    ...
    
    /*
     * Check if there is a request to freeze a process
     */
    static inline bool freezing(struct task_struct *p)
    {
    	if (static_branch_unlikely(&freezer_active))
    		return freezing_slow_path(p);
    
    	return false;
    }
    
    ...
    ...
    
    // kernel/freezer.c - v6.5
    bool frozen(struct task_struct *p)
    {
    	return READ_ONCE(p->__state) & TASK_FROZEN;
    }

    : `frozen` 함수는 인자로 전달된 태스크가 이미 동결 상태인지를 판별한다. 현재는 `PF_FROZEN`은 `TASK_FROZEN`으로 대체되었다.

    ...
    By replacing PF_FROZEN with TASK_FROZEN, a special block state, it is ensured frozen tasks stay frozen until thawed and don't randomly wake up early, as is currently possible.

    As such, it does away with PF_FROZEN and PF_FREEZER_SKIP, freeing up two PF_flags (yay!).
    ...

    - 참고 : https://lore.kernel.org/lkml/20220822114649.055452969@infradead.org/

    : `freeze_task` 함수에서 프로세스의 동결 유효성 검사가 끝나면 이제 진짜 동결을 시켜야 한다. 일반적으로, `task_struct.flags`에 `PF_KTHREAD`가 SET 되어있으면, 해당 스레드는 커널 스레드다. 어떤 태스크냐에 따라 함수 호출이 아래와 같이 달라진다.

    유저 태스크 - fake_signal_wake_up
    커널 태스크 - wake_up_state

    : 유저 태스크냐 커널 태스크냐에 따라 분기가 달라 보이지만, 결국에는 2개 모두 `wake_up_state` 함수를 호출한다. 그래서, 태스크를 wakeup 하고 특정 작업을 수행하도록 한다. 그 작업이 태스크들을 `refrigerator`로 보내는 작업이다. 즉, 태스크들을 wakeup 시키는 이유는 freeze 시키기 위해서라는 소리다.

     

    : 유저 태스크를 wakeup 시키기 위해서 `signal_wake_up` 함수를 호출한다. 크게 2가지 역할을 한다.

    1" 유저 태스크에 TIF_SIGPENDING 설정
    2" 유저 태스크를 wakeup 시킨다.

    : 리눅스 커널에서 `TASK_INTERRUPTIBLE` 설정된 `sleeping` 혹은 `blocked` 프로세스가 시그널을 받으면 wakeup 하게 되어있다. 이 동작이 바로 `signal_wake_up_state` 함수에서 수행된다.

    // kernel/freezer.c - v6.5
    static void fake_signal_wake_up(struct task_struct *p)
    {
    	unsigned long flags;
    
    	if (lock_task_sighand(p, &flags)) {
    		signal_wake_up(p, 0);
    		unlock_task_sighand(p, &flags);
    	}
    }
    ...
    
    // include/linux/sched/signal.h - v6.5
    static inline void signal_wake_up(struct task_struct *t, bool fatal)
    {
    	unsigned int state = 0;
    	if (fatal && !(t->jobctl & JOBCTL_PTRACE_FROZEN)) {
    		t->jobctl &= ~(JOBCTL_STOPPED | JOBCTL_TRACED);
    		state = TASK_WAKEKILL | __TASK_TRACED;
    	}
    	signal_wake_up_state(t, state);
    }
    ...
    
    // kernel/signal.c - v6.5
    /*
     * Tell a process that it has a new active signal..
     *
     * NOTE! we rely on the previous spin_lock to
     * lock interrupts for us! We can only be called with
     * "siglock" held, and the local interrupt must
     * have been disabled when that got acquired!
     *
     * No need to set need_resched since signal event passing
     * goes through ->blocked
     */
    void signal_wake_up_state(struct task_struct *t, unsigned int state)
    {
    	lockdep_assert_held(&t->sighand->siglock);
    
    	set_tsk_thread_flag(t, TIF_SIGPENDING);
    
    	/*
    	 * TASK_WAKEKILL also means wake it up in the stopped/traced/killable
    	 * case. We don't check t->state here because there is a race with it
    	 * executing another processor and just now entering stopped state.
    	 * By using wake_up_state, we ensure the process will wake up and
    	 * handle its death signal.
    	 */
    	if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
    		kick_process(t);
    }

    : 먼저 `fake_signal_wake_up` 함수는 실제 아무런 시그널을 받은게 없는 상황인데, 유저 프로세스들을 깨우기 위해 마치 실제 `시그널`이 발생한 것처럼 만든다. 그래서, 일단 유저 프로세스를 깨워야 하니 실제 `시그널`이 발생하지는 않았지만, 처리할 시그널이 있다고 `뻥`을 친다. 이 뻥이 `TIF_SIGPENDING` 시그널을 설정하는 것이다.

     

    : `wak_up_state` 함수는 내부적으로 `try_to_wake_up` 함수를 호출한다. `try_to_wake_up` 함수는 굉장히 복잡한 함수다. 

    간단하게 요약하면, 인자로 전달된 프로세스를 `TASK_RUNNING` 상태로 변경하고 로컬 CPU의 런큐에 삽입한다. 그리고 대기큐에 있는 프로세스 혹은 시그널 처리를 기다리는 프로세스를 실행시킨다. `try_to_wake_up` 함수의 세번 째 인자는 `try_to_wake_up` 함수덕에 활성화된 프로세스가 현재 로컬 CPU의 프로세스를 선점하는 것을 방지하는 목적으로 사용된다. 

    // kernel/sched/core.c - v6.5
    int wake_up_state(struct task_struct *p, unsigned int state)
    {
    	return try_to_wake_up(p, state, 0);
    }

    : 또 이 함수는 SMP 환경이라면, 깨울 프로세스가 최근에 실행된 CPU의 런큐에서 다른 CPU의 런큐로 이동해야 하는지 여부를 판별한다.

     

    : 그 다음 할 일은 freezing 시킬 프로세스가 현재 `TASK_RUNNING` 상태냐 아니냐에 따라 행동이 달라진다. 만약, `wake_up_state` 함수가 0을 반환하면, 동결 시킬 프로세스가 다른 프로세서에서 열심히 실행 중 이라는 뜻이다. 즉, 이 프로세스는 깨울 필요없이 즉각적으로 시그널을 처리하도록 해야 한다. 그러기 위해서 `kick_process` 함수를 실행해서 해당 스레드를 커널 모드로 진입시켜 시그널을 처리하도록 해야 한다. 어떻게 커널 모드로 진입시킬까? x86에서는 CPU끼리 인터럽트를 주고 받을 수 있는 메커니즘(`Inter-Processor-Interrupt`)이 존재한다. 프로세서는 IPI를 받으면, 자신이 등록한 IPI 핸들러로 제어권이 넘어간다. 그리고 이 IPI 핸들러에서 해당 인터럽트가 시그널 관련 인터럽트라는 것을 확인되면, 유저 모드로 시그널 처리를 즉각적으로 넘긴다.

     

    : 그런데, 이 유저 모드로 넘어가는 과정에서 해당 프로세스를 `냉장고`로 진입시키게 된다. 그 과정을 한 번 보도록 하자. 해당 내용은 x86을 기준으로 설명한다. 리눅스 커널의 시그널 처리방법은 기본적으로 `커널 -> 유저`로 처리된다. 이 때, x86 사용되는 함수가 `syscall_exit_to_user_mode` 함수다. 이 함수는 커널이 유저 모드로 전환할 때 사용되는 함수다. 호출 스택은 아래와 같다.

    do_syscall_64() -> x86_64의 시스템 콜 엔트리 포인트 함수
       arch/x86/entry/common.c:syscall_exit_to_user_mode()
          kernel/entry/common.c:__syscall_exit_to_user_mode_work()
             :exit_to_user_mode_prepare()
                :exit_to_user_mode_loop()
                    arch/x86/kernel/signal.c:arch_do_signal_or_restart()
                        kernel/signal.c:get_signal()

    - 참고 : https://hackmd.io/@ypl/r1bBRTgP5

    : `exit_to_user_mode_loop` 함수에서는 여러 가지 기능을 하지만, 우리가 볼 부분은 현재 스레드에 펜딩 시그널이 있는지를 검사하는 코드 부분이다.

    // kernel/entry/common.c - v6.5
    static unsigned long exit_to_user_mode_loop(struct pt_regs *regs,
    					    unsigned long ti_work)
    {
    	/*
    	 * Before returning to user space ensure that all pending work
    	 * items have been completed.
    	 */
    	while (ti_work & EXIT_TO_USER_MODE_WORK) {
            ....
    		if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
    			arch_do_signal_or_restart(regs);
            ....
    	}
    
    	/* Return the latest work state for arch_exit_to_user_mode() */
    	return ti_work;
    }

    : `arch_do_signal_or_restart` 함수에서는 `get_signal` 함수를 통해 해당 스레드에 발견된 시그널 핸들러가 존재하는지 검사한다. 만약, 없다면 디폴트 시그널 핸들러를 사용한다. 그리고, `handle_signal` 함수는 유저 애플리케이션에서 시그널 핸들러를 등록했을 경우, 해당 시그널 핸들러 함수를 호출한다.

    // arch/x86/kernel/signal.c - v6.5
    /*
     * Note that 'init' is a special process: it doesn't get signals it doesn't
     * want to handle. Thus you cannot kill init even with a SIGKILL even by
     * mistake.
     */
    void arch_do_signal_or_restart(struct pt_regs *regs)
    {
    	struct ksignal ksig;
    
    	if (get_signal(&ksig)) {
    		/* Whee! Actually deliver the signal.  */
    		handle_signal(&ksig, regs);
    		return;
    	}

    : 최종적으로 `get_signal` 함수에서 `try_to_freeze` 함수가 호출되는 것을 볼 수 있다. 이 함수에서 내부적으로 `__refrigerator` 함수가 호출되는 것을 확인이 가능하다.

    // kernel/signal.c - v6.5
    bool get_signal(struct ksignal *ksig)
    {
    	struct sighand_struct *sighand = current->sighand;
    	struct signal_struct *signal = current->signal;
    	int signr;
    
    	clear_notify_signal();
    	if (unlikely(task_work_pending(current)))
    		task_work_run();
    
    	if (!task_sigpending(current))
    		return false;
    
    	if (unlikely(uprobe_deny_signal()))
    		return false;
    
    	/*
    	 * Do this once, we can't return to user-mode if freezing() == T.
    	 * do_signal_stop() and ptrace_stop() do freezable_schedule() and
    	 * thus do not need another check after return.
    	 */
    	try_to_freeze();
            ...
    }

    : `try_to_freeze` 함수는 현재 스레드가 동결 상태로 들어갈 수 있는 태스크인지를 판단한다. 

    // include/linux/freezer.h - v6.5
    static inline bool try_to_freeze(void)
    {
    	might_sleep();
    	if (likely(!freezing(current)))
    		return false;
    	if (!(current->flags & PF_NOFREEZE))
    		debug_check_no_locks_held();
    	return __refrigerator(false);
    }

    : `__refrigerator` 함수는 무한 루프안에 스레드를 계속 가둬둠으로써 `동결` 시킨다. 제일 먼저, 태스크의 현재 상태를 저장한다. 그리고 무한 루프안으로 들어가게 되는데, 제일 먼저 태스크의 상태를 `TASK_FROZEN`으로 변경한다(이전 커널 코드에서는 `TASK_UNINTERRUPTIBLE` 였다. 동일한 의미로 보면 될 듯 싶다. 그리고 `PF_FROZEN`이 `TASK_FROZEN`으로 대체되었다). `냉장고`를 탈출하기 위해서는 `TASK_FROZEN` 플래그가 CLEAR 되거나 `check_kthr_stop && kthread_should_stop()`이 false 여야 한다. 그 외에 방법은 존재하지 않는다.

    //kernel/freezer.c - v6.5
    /* Refrigerator is place where frozen processes are stored :-). */
    bool __refrigerator(bool check_kthr_stop)
    {
    	unsigned int state = get_current_state();
    	bool was_frozen = false;
    
    	pr_debug("%s entered refrigerator\n", current->comm);
    
    	WARN_ON_ONCE(state && !(state & TASK_NORMAL));
    
    	for (;;) {
    		bool freeze;
    
    		set_current_state(TASK_FROZEN);
    
    		spin_lock_irq(&freezer_lock);
    		freeze = freezing(current) && !(check_kthr_stop && kthread_should_stop());
    		spin_unlock_irq(&freezer_lock);
    
    		if (!freeze)
    			break;
    
    		was_frozen = true;
    		schedule();
    	}
    	__set_current_state(TASK_RUNNING);
    
    	pr_debug("%s left refrigerator\n", current->comm);
    
    	return was_frozen;
    }
    EXPORT_SYMBOL(__refrigerator);

    : `schedule` 함수는 현재 실행되고 프로세스를 제어권을 다른 프로세스에게 넘긴다. 어차피, 계속 무한 루프를 돌 것이기 때문에, 굳이 이 동결이 완료된 프로세스를 실행할 필요가 없기 때문이다. 

     

     

    - Kernel thread

    : 기본적으로 커널 스레드는 `PF_NOFREEZE` 플래그가 SET 되어 있기 때문에, freeze 되지 않는다. `freezing_slow_path` 함수는 인자로 전달된 태스크가 freeze가 될 수 있는지 여부를 판별한다. 기본적으로 `freezing_slow_path` 함수에서 스레드의 플래그를 확인해서 `PF_NOFREEZE` 혹은 `PF_SUSPEND_TASK`는 freeze 하지 않는다. 즉, 커널 스레드와 suspend 과정을 주체하는 스레드는 freeze 되지 못하게 한다.

    // /kernel/freezer.c - v6.5
    /**
     * freezing_slow_path - slow path for testing whether a task needs to be frozen
     * @p: task to be tested
     *
     * This function is called by freezing() if freezer_active isn't zero
     * and tests whether @p needs to enter and stay in frozen state.  Can be
     * called under any context.  The freezers are responsible for ensuring the
     * target tasks see the updated state.
     */
    bool freezing_slow_path(struct task_struct *p)
    {
    	if (p->flags & (PF_NOFREEZE | PF_SUSPEND_TASK))
    		return false;
    
    	if (test_tsk_thread_flag(p, TIF_MEMDIE))
    		return false;
    
    	if (pm_nosig_freezing || cgroup_freezing(p))
    		return true;
    
    	if (pm_freezing && !(p->flags & PF_KTHREAD))
    		return true;
    
    	return false;
    }
    EXPORT_SYMBOL(freezing_slow_path);

    : 근데, 커널 `Freezing of task` 문서에서는 일부 커널 스레드는 freeze가 된다고 나와있다. `freezing_slow_path` 함수는 `suspend freeze processes` 과정에서 반드시 호출되는 함수다. 그렇기 때문에, 일부 커널 스레드는 이 함수에서 반드시 true를 받아야 한다. 그 조건문은 `pm_nosig_freezing || cgroup_freezing(p)` 가 된다. 여기서 `pm_nosig_freezing`가 일부 커널 스레드를 freeze 시키기는 조건문이 된다.

     

    : `freeze_kernel_threads` 함수를 보면 `pm_nosig_freezing` 변수를 true로 설정하는 것을 볼 수 있다. `pm_nosig_freezing` 변수 이름에서도 알 수 있다시피, freezing을 하는데 signal이 필요없는 것을 의미한다. 즉, 유저 프로세스는 signal을 통해서 freezing 한다(`pm_freezing`). `시그널`이 없으면, 유저 프로세스를 동결시킬 방법이 없다. 그러나, 커널 스레드는 커널에 있기 때문에, `시그널`이 필요없다. 결국 이 말은 `pm_nosig_freezing` 전역 변수가 kernel thread를 freezing 하기 위한 만들어진 변수라는 것을 의미한다.

    // kernel/power/process.c - v6.5
    /**
     * freeze_kernel_threads - Make freezable kernel threads go to the refrigerator.
     *
     * On success, returns 0.  On failure, -errno and only the kernel threads are
     * thawed, so as to give a chance to the caller to do additional cleanups
     * (if any) before thawing the userspace tasks. So, it is the responsibility
     * of the caller to thaw the userspace tasks, when the time is right.
     */
    int freeze_kernel_threads(void)
    {
    	int error;
    
    	pm_nosig_freezing = true;
    	error = try_to_freeze_tasks(false);
    
    	BUG_ON(in_atomic());
    
    	if (error)
    		thaw_kernel_threads();
    	return error;
    }

    : 그런데, 아직 문제가 남아있다. 커널 스레드는 여전히 PF_NOFREEZE 플래그 비트가 SET 되어 있다. 여기서 중요한 전제가 선행된다. 동결을 하는 이유가 `hibernation` 및 `suspend-to-ram` 시에 디바이스를 정상적으로 suspend 상태로 보내기 위해서는 태스크들이 먼저 suspend 가 되어야 한다고 했다. 그런데, 커널 스레드는 생성시에 PF_NOFREEZE 비트가 SET 되어 동결되지 않는다. 이 말은 기본적으로 커널 스레드는 `hibernation` 및 `suspend-to-ram` 과정에서 시스템에 큰 영향을 주지 않는다는 것을 기본으로 한다. 만약, 위협이 되는 커널 스레드가 있으면 직접적으로 커널 스레드의 상태를 동결될 수 있도록 바꿔야 한다. 이 때, 사용되는 함수가 `set_freezable` 함수다.

    // kernel/freezer.c - v6.5
    /**
     * set_freezable - make %current freezable
     *
     * Mark %current freezable and enter refrigerator if necessary.
     */
    bool set_freezable(void)
    {
    	might_sleep();
    
    	/*
    	 * Modify flags while holding freezer_lock.  This ensures the
    	 * freezer notices that we aren't frozen yet or the freezing
    	 * condition is visible to try_to_freeze() below.
    	 */
    	spin_lock_irq(&freezer_lock);
    	current->flags &= ~PF_NOFREEZE;
    	spin_unlock_irq(&freezer_lock);
    
    	return try_to_freeze();
    }
    EXPORT_SYMBOL(set_freezable);

    : 상당히 심플한 구성이다. 현재 스레드의 `PF_NOFREEZE` 플래그 비트를 CLEAR한다. 그리고, 즉각적으로 `try_to_freeze` 함수르 호출한다. 커널 스레드는 시그널이 필요가 없기 때문에, 그냥 곧 바로 냉장고로 보내버리면 된다.

     

     

    - Exit from refrigerator

    : 냉장고에 들어간 태스크들은 스케줄링 대상도 아니고, 시그널도 받지 못한다(`TASK_UNINTERRUPTIBLE`). 어떻게 해야 할까? 위의 `__refrigerator` 함수안에 이미 탈출 조건이 명시되어 있다. 그리고, 우리는 이미 알고 있다. `suspend` 과정에서 동결되었던 프로세스가 `resume` 과정에서 해동되는 것을 말이다. 즉, `resume`을 분석하면 동결 프로세스를 냉장고에서 탈출시키는 방법을 찾을 수 있을 것이다.

    // kernel/power/hibernate.c - v6.5
    static int software_resume(void)
    {
        ....
        thaw_processes();
        ....
    }
    
    
    // kernel/power/process.c - v6.5
    void thaw_processes(void)
    {
            ....
    	if (pm_freezing)
    		static_branch_dec(&freezer_active);
    	pm_freezing = false;
    	pm_nosig_freezing = false;
    
    	read_lock(&tasklist_lock);
    	for_each_process_thread(g, p) {
    		/* No other threads should have PF_SUSPEND_TASK set */
    		WARN_ON((p != curr) && (p->flags & PF_SUSPEND_TASK));
    		__thaw_task(p);
    	}
    	read_unlock(&tasklist_lock);
            ....
    }
    
    ....
    ....
    
    // kernel/freezer.c - v6.5
    void __thaw_task(struct task_struct *p)
    {
        ....
    	if (lock_task_sighand(p, &flags2)) {
    		/* TASK_FROZEN -> TASK_{STOPPED,TRACED} */
    		bool ret = task_call_func(p, __set_task_special, NULL);
            ....
    	}
    
    	wake_up_state(p, TASK_FROZEN);
        ....
    }

    : 호출 순서는 아래와 같다. 최종적으로, `wake_up_state` 함수를 호출함으로써 동결된 태스크를 `TASK_RUNNING` 상태로 변경함으로써, `냉장고`에서 빠져나 올 수 있게 만들어준다.

    software_resume -> thaw_processes -> __thaw_task
Designed by Tistory.