꿈소년의 개발 이야기

리눅스 커널과 안드로이드의 Suspend/Resume 본문

Android Development

리눅스 커널과 안드로이드의 Suspend/Resume

꿈소년 2013. 3. 30. 10:53
반응형

출처 : 

출처: http://kzjblog.appsp0t.com/2010/11/20/suspend-en.html
       (원문 번역과 더불어 내용을 추가/수정/보완/삭제 하였음을 밝힙니다.)

개요

Suspend/Resume 은 리눅스 커널이 제공하는 기능으로 모바일 환경이 점점 보급되고 빠른 부팅 시간에 대한 요구가 늘어나면서 필요성이 증대되었다. 이 글은 리눅스의 Suspend/Resume을 개괄적으로 알아보고 안드로이드가 전원을 관리하는 방법에 대해 소개한다.


Version
Linux Kernel: v2.6.35.7
Android: Gingerbread(v2.3.3)


Suspend 소개
Suspend는 크게 3부분으로 나눌 수 있다: 프로세스와 태스크들을 프리징하고(Freezing)/모든 드라이버의 suspend 콜백 함수를 호출하고/CPU와 코어 시스템 디바이스를 Suspend한다. 프로세스의 프리징은 모든 프로세스를 멈추는 것과 동일하다. 그리고 resume을 할 때 프로세스들은 중단한 일이 없었던 것처럼 다시 실행하기 시작한다. user space프로세스와 kernel space의 태스크들은 멈췄었다는 사실을 전혀 인지하지 못한다. 그러면 사용자는 어떻게 리눅스 시스템을 suspend 시킬까? 사용자는 sys 파일시스템의 파일을 읽고 쓸 수 있다: /sys/power/state 파일을 통해 커널의 전원 관리 서비스를 이용할 수 있다.
예를 들어
      echo standby > /sys/power/state
명령으로 시스템을 suspend 시키고,
      cat /sys/power/state
명령으로 커널에서 제공하는 전원 관리 상태(PM method)의 수를 알 수 있다.
이 상태는 'on', 그리고 'standby', 'mem'의 값으로 하드 코딩(pm_states[PM_SUSPEND_MAX])되어 있다.



리눅스 시스템의 Suspend
파일:

아래의 경로에서 해당되는 리눅스의 소스코드를 확인할 수 있다.
   - linux_soruce/kernel/power/main.c
   - linux_source/kernel/arch/xxx/mach-xxx/pm.c
이제 Suspend가 어떻게 일어나는지 살펴보자. user space 인터페이스인 /sys/power/state는 main.c의 state_store() 함수와 관련이 있다: const char * const pm_state[]으로 정의된 문자열("mem" 또는 "standby")을 사용할 수 있다. 일반적인 리눅스 커널에서 시스템이 Suspend 상태가 될 때 제일 먼저 suspend.c의 enter_state() 함수로 들어온다. enter_state() 함수는 먼저 시스템 상태가 유효(valid)한지 검사하고 파일 시스템을 sync(do all page writeback)한다.

/-*
 * enter_state - Do common work of entering low-power state.
 * @state:  pm_state structure for state we're entering.
 *
 * Make sure we're the only ones trying to enter a sleep state. Fail
 * if someone has beat us to it, since we don't want anything weird to
 * happen when we wake up.
 * Then, do the setup for suspend, enter the state, and cleaup (after
 * we've woken up).
 *-
int enter_state(suspend_state_t state)
{
    int error;

    suspend_test_start_level(TEST_SUSPEND_TIME_TOP);

    if (!valid_state(state)) {
        error = -ENODEV;
        goto End;
    }

    if (!mutex_trylock(&pm_mutex)) {
        error = -EBUSY;
        goto End;
    }

    printk(KERN_INFO "PM: Syncing filesystems ... ");
    sys_sync();
    printk("done.\n");

    pr_debug("PM: Preparing system for %s sleep\n", pm_states[state]);
    error = suspend_prepare();
    if (error)
        goto Unlock;

    if (suspend_test(TEST_FREEZER))
        goto Finish;

    pr_debug("PM: Entering %s sleep\n", pm_states[state]);
    error = suspend_devices_and_enter(state);

    Finish:
    pr_debug("PM: Finishing wakeup.\n");
    suspend_finish();
    Unlock:
    mutex_unlock(&pm_mutex);

    End:
    suspend_test_finish_level(TEST_SUSPEND_TIME_TOP, "total transition time of resume");
    return error;
}

준비, 프로세스 프리징
suspend_prepare() 함수는 suspend를 위한 콘솔을 할당하고(pm_prepare_console()) suspend notifier를 실행하고(pm_notifier_call_chain(PM_SUSPEND_PREPARE)) user 모드 헬퍼를 비활성화하고(usermodehelper_disable()) suspend_freeze_processes() 함수를 호출한다. suspend_freeze_processes() 함수는 모든 프로세스의 현재 상태를 저장한다. 프리징 단계에서 몇몇 태스크나 user space의 프로세스가 프리징되는 것을 거부하는 경우에는 suspend 상태로 들어가는 것을 취소하고 모든 프로세스의 프리징을 해제한다.

/-*
 * suspend_prepare - Do prep work before entering low-power state.
 *
 * This is common code that is called for each state that we're entering.
 * Run suspend notifiers, allocate a console and stop all processes.
 *-
static int suspend_prepare(void)
{
    int error;

    if (!suspend_ops || !suspend_ops->enter)
        return -EPERM;

    pm_prepare_console();

    error = pm_notifier_call_chain(PM_SUSPEND_PREPARE);
    if (error)
        goto Finish;

    error = usermodehelper_disable();
    if (error)
        goto Finish;

    error = suspend_freeze_processes();
    if (!error)
        return 0;

    suspend_thaw_processes();
    usermodehelper_enable();
    Finish:
    pm_notifier_call_chain(PM_POST_SUSPEND);
    pm_restore_console();
    return error;
}

Suspend Devices
이제 모든 프로세스(process/workqueue/kthread)가 멈췄고 이 프로세스들은 lock된 세마포어를 가지고 있을 것이다. 이 때 드라이버의 suspend 함수에서 프로세스를 기다리고 있다면 데드락이 발생한다. 그리고 커널은 나중을 위하여 어느 정도의 메모리 공간을 free한다. 마지막으로 모든 장치를 suspend하기 위하여 suspend_devices_and_enter()를 호출한다. 이 함수에서는 시스템에 suspend_ops->begin()가 있는 경우 suspend_ops->begin() 함수를 호출하고 driver/base/power/main.c의 dpm_suspend() 함수를 호출한다. dpm_suspend() 함수에서는 device_suspend() 함수를 호출하여모든 non-sysdev 장치의 suspend 콜백 함수를 호출한다. 다시 suspend_devices_and_enter() 함수로 돌아와서 suspen_enter()를 호출한다. 이 함수에서는 시스템과 관련된 준비를 하기 위해 suspend_ops->prepare()를 호출하고 레이스 컨디션을 피하기 위해 nonboot CPU를 비활성화 한다. 이 과정을 마치면 단 한개의 cpu만 실행이 되는 상태가 된다. suspend_ops는 시스템 관련 전원 관리 함수를 묶어놓은 스트럭쳐로 일반적으로 arch/xxx/mach-xxx/pm.c에서 등록된다. arch_suspend_disable_irqs() 함수를 호출하여 IRQ가 비활성화하고, sysdev_suspend() 함수를 호출하여 모든 시스템 장치를 suspend한다. 이 때 /sys/devices/system/ 위치의 모든 시스템 디바이스가 suspend된다. 마지막으로 suspend_ops->enter() 함수가 호출되는데, cpu가 파워 세이브 모드로 진입하고 시스템의 동작이 여기서 모두 멈춘다(당연히 모든 코드 수행이 여기에서 멈추게 된다).

/-*
 * suspend_devices_and_enter - suspend devices and enter the desired system
 *        sleep state.
 * @state:    state to enter
 *-
int suspend_devices_and_enter(suspend_state_t state)
{
    int error;
    gfp_t saved_mask;

    if (!suspend_ops)
        return -ENOSYS;

    if (suspend_ops->begin) {
        error = suspend_ops->begin(state);
        if (error)
            goto Close;
    }
    suspend_console();
    saved_mask = clear_gfp_allowed_mask(GFP_IOFS);
    suspend_test_start();
    error = dpm_suspend_start(PMSG_SUSPEND);
    if (error) {
        printk(KERN_ERR "PM: Some devices failed to suspend\n");
        goto Recover_platform;
    }
    suspend_test_finish("suspend devices");
    if (suspend_test(TEST_DEVICES))
        goto Recover_platform;

    suspend_enter(state);

    Resume_devices:
    suspend_test_start();
    dpm_resume_end(PMSG_RESUME);
    suspend_test_finish("resume devices");
    set_gfp_allowed_mask(saved_mask);
    resume_console();
    Close:
    if (suspend_ops->end)
        suspend_ops->end();
    return error;

    Recover_platform:
    if (suspend_ops->recover)
        suspend_ops->recover();
    goto Resume_devices;
}


Resume
만약 시스템이 인터럽트 혹은 다른 이벤트에 의해 깨어나면 코드의 수행이 다시 시작한다. 가장 먼저 resume되는 것은 /sys/devices/system/ 위치에 있는 장치들이다(sysdev_resume()). 그리고 IRQ를 활성화하고(arch_suspend_enable_irqs()) nonboot CPU를 활성화한 후(enable_nonboot_cpus()) 시스템 resume이 시작되었다는 것을 알리기 위해 suspend_ops->wake() 함수와 suspend_ops->finish() 함수를 호출한다. suspend_devices_and_enter() 함수에서 모든 장치를 깨우기 위해 dpm_resume_end() 함수를 호출하여 모든 장치의 resume() 함수를 호출하고 콘솔을 resume 하며 마지막으로 suspend_ops->end()를 호출한다. 다시 enter_state() 함수로 돌아오면 suspend_devices_and_enter() 함수를 빠져나와 장치가 실행되는 상태이지만 user space의 프로세스와 태스크들은 여전히 프리즈된 상태이다. enter_state() 함수는 나중에 suspend_finish() 함수를 호출하여 프로세스를 깨우고(suspend_thaw_processes()) user 모드 헬퍼를 활성화하며(usermodehelper_enable()) 모든 pm에 suspend 단계에서 빠져나왔다는 것을 알린 후(pm_notifier_call_chain) 콘솔을 복구(restore)한다.
이 단계가 일반적인 리눅스의 suspend와 resume가 실행되는 과정이다.



안드로이드 Suspend
안드로이드용으로 패치된 커널에서는 Suspend가 되기 전(enter_state() 함수를 호출하기 전)에 kernel/power/earlysuspend.c 파일의 request_suspend_state()함수를 호출한다(안드로이드는 Early Suspend와 Wake Lock 기능을 커널에 추가했다). 이를 좀 더 자세한 이해를 위해 먼저 안드로이드가 추가한 새로운 기능에 대해서 소개하겠다.

파일:
   - linux_source/kernel/power/main.c
   - linux_source/kernel/power/earlysuspend.c
   - linux_source/kernel/power/wakelock.c
 
기능
Early Suspend
Early Suspend는 안드로이드가 리눅스 커널에 새로 추가한 메커니즘이다. Early Suspend는 리눅스의 '진짜' Suspend와 LCD 스크린 오프의 사이에 존재하는 상태이다. LCD 스크린을 끄면 배터리 수명과 몇몇 기능적인 요구사항에 의해 LCD 백라이트나 G-Sensor, 터치스크린 등의 동작이 멈추게 된다.



Late Resume
Late Resume은 Early Suspend와 쌍을 이루는 메커니즘으로 커널과 시스템 Resume이 끝난 후 수행이 된다. 이 때 Early Suspend에서 Suspend된 장치들이 Resume하게 된다.



Wake Lock
Wake Lock은 안드로이드 전원 관리 시스템의 핵심을 이루는 기능이다. Wake Lock은 kernel space 혹은 user space의 서비스나 어플리케이션에서 lock을 잡고 있는 경우(timeout lock이거나 timeout lock이 아니거나)에 시스템은 Sleep 상태로 진입할 수 없도록 막는다. timeout lock은 설정한 시간이 되면 자동으로 lock이 풀리고 안드로이드용으로 패치된 리눅스 커널(이하 안드로이드 커널)은 리눅스 Suspend(pm_suspend())를 불러 전체 시스템을 Suspend시킨다.



안드로이드 Suspend
사용자가 /sys/power/state에 "mem"이나 "standby"를 write하면 state_store()가 호출된다. 그리고 request_suspend_state() 함수로 들어온다. 이 함수에서 상태를 검사하고 Suspend 요청인 경우 early_suspend_work->early_suspend()를 큐에 넣는다.

void request_suspend_state(suspend_state_t new_state)
{
    unsigned long irqflags;
    int old_sleep;

    spin_lock_irqsave(&state_lock, irqflags);
    old_sleep = state & SUSPEND_REQUESTED;
    if (debug_mask & DEBUG_USER_STATE) {
        struct timespec ts;
        struct rtc_time tm;
        getnstimeofday(&ts);
        rtc_time_to_tm(ts.tv_sec, &tm);
        pr_info("request_suspend_state: %s (%d->%d) at %lld "
            "(%d-%02d-%02d %02d:%02d:%02d.%09lu UTC)\n",
            new_state != PM_SUSPEND_ON ? "sleep" : "wakeup",
            requested_suspend_state, new_state,
            ktime_to_ns(ktime_get()),
            tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
            tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec);
    }
    if (!old_sleep && new_state != PM_SUSPEND_ON) {
        state |= SUSPEND_REQUESTED;
        queue_work(suspend_work_queue, &early_suspend_work);
    } else if (old_sleep && new_state == PM_SUSPEND_ON) {
        state &= ~SUSPEND_REQUESTED;
        wake_lock(&main_wake_lock);
        queue_work(suspend_work_queue, &late_resume_work);
    }
    requested_suspend_state = new_state;
    spin_unlock_irqrestore(&state_lock, irqflags);
}

Early Suspend
early_suspend()함수에서는 먼저 요청한 상태를 검사하는데, 요청한 상태가 Suspend(SUSPEND_REQUESTED)인 경우 Suspend 취소 요청을 막기 위해 상태를 Suspend로 변경한다(user 프로세스가 여전히 동작하고 있기 때문에). 그리고 early suspend에 등록된 모든 핸들러의 suspend() 함수를 호출한다. 그 다음 파일 시스템을 sync하고 main_wake_lock을 푼다(unlock). 

static void early_suspend(struct work_struct *work)
{
    struct early_suspend *pos;
    unsigned long irqflags;
    int abort = 0;

    mutex_lock(&early_suspend_lock);
    spin_lock_irqsave(&state_lock, irqflags);
    if (state == SUSPEND_REQUESTED)
        state |= SUSPENDED;
    else
        abort = 1;
    spin_unlock_irqrestore(&state_lock, irqflags);

    if (abort) {
        if (debug_mask & DEBUG_SUSPEND)
            pr_info("early_suspend: abort, state %d\n", state);
        mutex_unlock(&early_suspend_lock);
        goto abort;
    }

    if (debug_mask & DEBUG_SUSPEND)
        pr_info("early_suspend: call handlers\n");
    list_for_each_entry(pos, &early_suspend_handlers, link) {
        if (pos->suspend != NULL)
            pos->suspend(pos);
    }
    mutex_unlock(&early_suspend_lock);

    if (debug_mask & DEBUG_SUSPEND)
        pr_info("early_suspend: sync\n");

    sys_sync();
abort:
    spin_lock_irqsave(&state_lock, irqflags);
    if (state == SUSPEND_REQUESTED_AND_SUSPENDED)
    {
#ifndef CONFIG_EARLY_SUSPEND_ONLY
        wake_unlock(&main_wake_lock);
#endif /- EARLY_SUSPEND_ONLY *-
    }
    spin_unlock_irqrestore(&state_lock, irqflags);
}

 

Late Resume
모든 커널 Resume 과정을 마치면 user space의 프로세스와 서비스가 실행된다. 시스템이 깨어나는 경우는 다음과 같다.

  - 전화가 왔을 때
   전화가 오면 모뎀은 rild(RING 명령)로 명령을 모내고 rild는 Call 이벤트 처리를 위하여 WindowManager와 어플리케이션으로 이벤트 메시지를 보낸다. PowerManagerSerive는 또한 /sys/power/state에 "on"을 write하여 커널이 Late Resume을 수행하도록 한다.

  - 키 이벤트
   시스템이 파워키나 메뉴키 등의 키 이벤트에 의해 깨어난 경우 이벤트는 WindowManager와 어플리케이션으로 보내지고 해당 이벤트를 처리한다. 키 이벤트는 두 가지 경우로 나눌 수 있는데 만약 리턴 키나 홈 키와 같이 시스템을 깨울 수 있는 키가 아닌 경우 WindowManager는 Wake Lock을 풀어 다시 시스템이 Suspend로 돌아가도록 한다. 반면에 키 이벤트가 시스템을 깨울 수 있는 키인 경우 WindowManager는 PowerManagerService를 호출하여 Late Resume을 수행할 수있도록 한다.


  - Late Resume은 Early Suspend 장치 리스트의 resume 함수를 호출한다.

static void late_resume(struct work_struct *work)
{
    struct early_suspend *pos;
    unsigned long irqflags;
    int abort = 0;

    mutex_lock(&early_suspend_lock);
    spin_lock_irqsave(&state_lock, irqflags);
    if (state == SUSPENDED)
        state &= ~SUSPENDED;
    else
        abort = 1;
    spin_unlock_irqrestore(&state_lock, irqflags);

    if (abort) {
        if (debug_mask & DEBUG_SUSPEND)
            pr_info("late_resume: abort, state %d\n", state);
        goto abort;
    }
    if (debug_mask & DEBUG_SUSPEND)
        pr_info("late_resume: call handlers\n");
    list_for_each_entry_reverse(pos, &early_suspend_handlers, link)
        if (pos->resume != NULL)
            pos->resume(pos);
    if (debug_mask & DEBUG_SUSPEND)
        pr_info("late_resume: done\n");
abort:
    mutex_unlock(&early_suspend_lock);
}


기존 리눅스 Suspend와 다른 점
안드로이드에서도 pm_suspend()가 enter_state()를 호출하여 suspend 상태가 되지만 기존 리눅스 커널의 suspend 과정과 100% 동일하지는 않다:
프로세스를 Freezing할 때 안드로이드는 wake lock이 있는지 확인하고, wake lock이 있는 경우 suspend 과정이 실행되지 않는다.



추가 참고 자료

Wake Lock 관련: http://www.kandroid.org/online-pdk/guide/power_management.html#androidPowerWakeLocks
RIL 관련: http://www.kandroid.org/board/board.php?board=AndroidLib&command=body&no=41


'Android Development' 카테고리의 다른 글

Activity unregister / Leaked IntentReceiver issue  (0) 2013.05.09
NFC Guide  (0) 2013.05.08
DDMS thread dump  (0) 2013.03.30
ID3 TAG Parsing & Saving  (0) 2013.03.26
안드로이드 이미지 언팩  (0) 2013.03.22