LoginSignup
11
10

More than 5 years have passed since last update.

Linux Power Managementのカーネル実装(freeze)を読む(その1)

Posted at

今回はfreezeを調べてみます

前回はLinuxのPower Managementの導入部分を調べてみました。
今回は、

Documentation/power/states.txt
state:      Suspend-To-Idle
ACPI state: S0
Label:      "freeze"

について調べてみようと思います。(7月のもくもく会のネタを放置していた・・・)

そもそもfreezeってなんだ。

Documentation/power/freezing-of-tasks.txt に書かれています。
正式な名前(?)はfreezing of tasksらしいです。

以下freezing-of-tasks.txtの前半2/3の要約を書きます。
後半1/3も面白いけど、freezing of tasksの実装含めた概要を知るためにはとりあえず前半2/3で十分だと思います。

  • freezing of tasksとは、システムのPower stateがhibernationもしくはsystem-wide suspendの場合に実施される、ユーザプロセスやいくつかのカーネルスレッドをfreeze状態にする仕組みです。

  • freeze状態を制御するために、プロセス構造体内のフラグにセットできる3つのフラグビットがあります。include/linux/sched.hに以下3つの定義があります。

定義名 意味
PF_NOFREEZE システムのPower modeがsuspendまたはhibernationの際に該当プロセスをfreeze状態にしない
PF_FROZEN このプロセスは既にfreeze状態
PF_FREEZER_SKIP 該当プロセスをfreeze状態にしない。PF_NOFREEZEとの違いは今のところ不明...
  • system_freezing_cntという変数もあり、これでfreeze移行の可否を決定します。この値はfreeze_processes()という関数経由で「freezeしたい」意思表示をして変えます。

  • freeze移行の意思表示をした場合、try_to_freeze_tasks()という関数が呼び出されます。これによって、全ユーザ空間プロセスに擬似シグナル(fake signal)が送出されます。また、このときカーネルスレッドを全て起こします(wake up)。

  • この擬似シグナルを受けたfreeze可能なユーザ空間プロセスは、try_to_freeze()呼び出しにより応答(react)しなければいけません。この応答によって、該当プロセスはfreeze状態に遷移します。

  • ユーザスレッド自信はtry_to_freeze()呼び出しについて特に意識する必要はありません。しかし、カーネルスレッドの場合、必要であれば各スレッドごとに明示的にfreeze状態遷移のコードを書く必要があります。

  • freeze状態を設けた目的は、「hibernationやるときにファイルシステムにアクセスされたくない」「hibernation結構RAM使うので、邪魔なのどいてくれ」「カーネルスレッドやデバイスドライバが、suspend途中もしくはsuspend後のドライバに干渉してほしくない」といったところです。(1番目と3番目は結構シビアです。)

何となく実装のイメージがつかめたのではないでしょうか。

まずは、main.cのfreeze系の処理をみる

要約に書かれているtry_to_freeze_tasks()関数を見ます。
少し長いので、まず前半を見たいと思います。

kernel/power/process.c
/* 
 * Timeout for stopping processes
 */
unsigned int __read_mostly freeze_timeout_msecs = 20 * MSEC_PER_SEC;

static int try_to_freeze_tasks(bool user_only)
{
    struct task_struct *g, *p;
    unsigned long end_time;
    unsigned int todo;
    bool wq_busy = false;
    struct timeval start, end;
    u64 elapsed_msecs64;
    unsigned int elapsed_msecs;
    bool wakeup = false;
    int sleep_usecs = USEC_PER_MSEC;

    do_gettimeofday(&start);

    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;

            if (!freezer_should_skip(p))
                todo++;
        }
        read_unlock(&tasklist_lock);
/* 略 */

スレッドごとのforeachで呼び出されているfreeze_task()が重要な役割を担っていそうです。freeze_task()を見ましょう。

kernel/freezer.c
bool freeze_task(struct task_struct *p) 
{
    unsigned long flags;

    /*  
     * This check can race with freezer_do_not_count, but worst case that
     * will result in an extra wakeup being sent to the task.  It does not 
     * race with freezer_count(), the barriers in freezer_count() and 
     * freezer_should_skip() ensure that either freezer_count() sees
     * freezing == true in try_to_freeze() and freezes, or
     * freezer_should_skip() sees !PF_FREEZE_SKIP and freezes the task
     * normally.
     */  
    if (freezer_should_skip(p))
        return false;

    spin_lock_irqsave(&freezer_lock, flags);
    if (!freezing(p) || frozen(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_INTERRUPTIBLE);

    spin_unlock_irqrestore(&freezer_lock, flags);
    return true;
}

途中で呼び出されているfreezing()とfrozen()については以下の実装です。
frozen()の中で、タスク構造体のフラグにPF_FROZENが立っているか調べていることを押さえてください。つまり、タスク構造体のフラグにPF_FROZENが立っている場合、そのタスクはfreezing状態であると言えます。

include/linux/freezer.h
/*
 * Check if a process has been frozen
 */
static inline bool frozen(struct task_struct *p) 
{
    return p->flags & PF_FROZEN;
}

extern bool freezing_slow_path(struct task_struct *p);

/*
 * Check if there is a request to freeze a process
 */
static inline bool freezing(struct task_struct *p) 
{
    if (likely(!atomic_read(&system_freezing_cnt)))
        return false;
    return freezing_slow_path(p);
}

続いて、freezingの対象がカーネルスレッドの場合とユーザスレッドの場合とで処理を分けている箇所があります。先ほどのDocumentation要約のとおりです。
ユーザスレッドの場合では、fake_signal_wake_up()を呼び出します。実装は以下のとおりです。

kernel/freezer.c
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);
    }   
}

また、signal_wake_up()の実装は以下のとおりです。

include/linux/sched.h
static inline void signal_wake_up(struct task_struct *t, bool resume)
{      
    signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
}

更に続いてsignal_wake_up_state()を見ましょう。

kernel/signal.c
/*
 * 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)
{
    set_tsk_thread_flag(t, TIF_SIGPENDING);

実は、ここが後に重要になります。シグナルを送出してはいませんが、タスク構造体に「ペンディングされているシグナルがある」という印をつけておきます。
ちなみに、TIF_SIGPENDINGの「TIF」はThread InFormation」の略らしいです。この値はCPUアーキテクチャ依存(arch)の下に定義があるが、名前の通り「Signal Pending」のようです。
signal_wake_up_state()の続きを読みます。

kernel/signal.c
    /*
     * 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);
}

wake_up_state()の実装は以下のとおりです。単にtry_to_wake_up()を呼んでいます。

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

ここまでのところで、stateに渡される値はTASK_INTERRUPTIBLEです。
以下の実装を読みますが、単にスレッドを起こす(スレッドを実行可能状態にする)ような実装に見えます。ここまでのところで誰もfreezingにしていないように見えます。

kernel/sched/core.c
/**
 * try_to_wake_up - wake up a thread
 * @p: the thread to be awakened
 * @state: the mask of task states that can be woken
 * @wake_flags: wake modifier flags (WF_*)
 *
 * Put it on the run-queue if it's not already there. The "current"
 * thread is always on the run-queue (except when the actual
 * re-schedule is in progress), and as such you're allowed to do
 * the simpler "current->state = TASK_RUNNING" to mark yourself
 * runnable without the overhead of this.
 *
 * Return: %true if @p was woken up, %false if it was already running.
 * or @state didn't match @p's state.
 */
static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
/* 略 */
    cpu = task_cpu(p);

    if (p->on_rq && ttwu_remote(p, wake_flags))
        goto stat;

/* 略 */

    ttwu_queue(p, cpu);
stat:
    ttwu_stat(p, cpu, wake_flags);
out:
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

    return success;
}

一体誰がスレッドをfreezingにしているのでしょうか。
それは次回。

11
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10