LoginSignup
8
5

More than 5 years have passed since last update.

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

Posted at

お久しぶりです

以前Linux Power Management実装の概要紹介をした際、以下のことをかきました。

以下suspend-to-swapの仕組みのひとつである「swsusp」も気になります。特にswapとどのように統合され、違い(制約)は何かというところも興味深いです。
Documentation/power/states.txt
For suspend-to-disk, a mechanism called 'swsusp' (Swap Suspend) is used to write memory contents to free swap space. swsusp has some restrictive requirements, but should work in most cases. 

swswapの概要

swswapの概要はDocumentationの下にあるswsusp.txtに書かれています。

Documentation/power/swsusp.txt
Some warnings, first.

 * BIG FAT WARNING *********************************************************
 *
 * If you touch anything on disk between suspend and resume...
 *              ...kiss your data goodbye.
 *
 * If you do resume from initrd after your filesystems are mounted...
 *              ...bye bye root partition.
 *          [this is actually same case as above]
 *

このような不吉な書き出しで始まります。
特に、省エネ機能を開発するエンジニアにとって、suspendとresumeの間の期間は魔の期間です。バグるとストールするなど致命的かつ追いかけにくい障害となります。
結構長い引用になりますが、続きを見ます。
swswap.txtの引用が続きますが、しばらくの辛抱です。

まず最初の質問は「swswapを実施中にはデバイス群に何が起きるのか?システムがサスペンドする間にresumeがかかるようにも思えます。」というものです。

要するに、「swswapをする場合、Diskへの書き込みが発生するはずで、デバイスが動けるようにしなければいけないはず。しかし、suspendするということはデバイスを止めてしまうということだよね。それっておかしくね?」という意味だと思ってください。
回答にもあるとおり、一度resumeしてDiskを動けるようにしたあと、再度suspendするとのことです。
長いですが、以下に引用します。

Documentation/power/swsusp.txt

Q: What happens to devices during swsusp? They seem to be resumed
during system suspend?

A: That's correct. We need to resume them if we want to write image to
disk. Whole sequence goes like

      Suspend part
      ~~~~~~~~~~~~
      running system, user asks for suspend-to-disk

      user processes are stopped

      suspend(PMSG_FREEZE): devices are frozen so that they don't interfere
                  with state snapshot

      state snapshot: copy of whole used memory is taken with interrupts disabled

      resume(): devices are woken up so that we can write image to swap

      write image to swap

      suspend(PMSG_SUSPEND): suspend devices so that we can power off

      turn the power off

      Resume part
      ~~~~~~~~~~~
      (is actually pretty similar)

      running system, user asks for suspend-to-disk

      user processes are stopped (in common case there are none, but with resume-from-initrd, no one knows)

      read image from disk

      suspend(PMSG_FREEZE): devices are frozen so that they don't interfere
                  with image restoration

      image restoration: rewrite memory with image

      resume(): devices are woken up so that system can continue

      thaw all user processes

次の質問。swapfileに対してsuspendできるのかという質問には「条件付きでできる」というのが回答になります。

Documentation/power/swsusp.txt
/* 略 */
Q: Can I suspend to a swap file?

A: Generally, yes, you can.  However, it requires you to use the "resume=" and
"resume_offset=" kernel command line parameters, so the resume from a swap file
cannot be initiated from an initrd or initramfs image.  See
swsusp-and-swap-files.txt for details.

これらの回答を見る限り、swswapはswapパーティションへスナップショットを退避することのようです。
これを頭に入れつつ読んでみましょう。

swswapの処理を読んでみる。

状態遷移を行う関数は前回見たとおり、state_store()でしたね。

kernel/power/main.c
static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
               const char *buf, size_t n)
{
// 略
    state = decode_state(buf, n);
    if (state < PM_SUSPEND_MAX)
        error = pm_suspend(state);
    else if (state == PM_SUSPEND_MAX)
        error = hibernate();
    else
        error = -EINVAL;

include/linux/suspend.h
#define PM_SUSPEND_MAX               ((__force suspend_state_t) 4)

上記より、swswapの場合、hibernate()が呼ばれます。

hibernate()について

hibernate()から呼び出している関数を見ていきます。

hibernation_available()

呼び出している関数の名前からおそらくhibernation機能が利用可能かどうか調べるようです。

kernel/power/hibernate.c
/**
 * hibernate - Carry out system hibernation, including saving the image.
 */
int hibernate(void)
{
    int error;

    if (!hibernation_available()) {
        pr_debug("PM: Hibernation not available.\n");
        return -EPERM;
    }    

hibernation_available()は以下のとおりです。

include/linux/suspend.h
#ifdef CONFIG_HIBERNATION
// 略
extern bool hibernation_available(void);
// 略
#else
// 略
static inline bool hibernation_available(void) { return false; }
// 略
#endif

コンフィグでhibernationを有効にしない場合は常にfalseが返ります。
hibernationを有効にしている場合はどうでしょうか。

kernel/power/hibernate.c
bool hibernation_available(void)
{
    return (nohibernate == 0);
}

もう少し複雑な条件付けがあるかと思いましたが、意外です。たった一つのグローバル変数nohibernateのみで判断しています。
nohibernate_setup()で、nohibernateは1にされています。
nohibernate_setup()は __init属性付きなのでカーネル立ち上がり時のみに呼ばれることは明白です。

kernel/power/hibernate.c
// 略
static int __init nohibernate_setup(char *str)
{
    noresume = 1; 
    nohibernate = 1; 
    return 1;
}
// 略
__setup("nohibernate", nohibernate_setup);
// 略

nohibernate_setup()をカーネル内から関数呼び出しの形で呼び出している様子はありません。
注)nohibernate_setup()はkaslr_nohibernate_setup()から呼ばれているが、kaslr_nohibernate_setup()自体を直接呼んでいる箇所はない。これを指して「関数呼び出しの形で呼び出している様子はない」と書いています。

ここで、__setup()というマクロらしきものを見つけました。これは何でしょうか。

include/linux/init.h
/*
 * Only for really core code.  See moduleparam.h for the normal way.
 *
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.setup.
 */
#define __setup_param(str, unique_id, fn, early)            \
    static const char __setup_str_##unique_id[] __initconst \
        __aligned(1) = str; \
    static struct obs_kernel_param __setup_##unique_id  \
        __used __section(.init.setup)           \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_##unique_id, fn, early }

#define __setup(str, fn)                    \
    __setup_param(str, fn, fn, 0)

このマクロはわかるでしょうか。
今回のケースであれば、以下のように展開されます。

展開
    static const char __setup_str_nohibernate_setup[] __initconst
        __aligned(1) = "nohibernate";
    static struct obs_kernel_param __setup_nohibernate_setup  \
        __used __section(.init.setup)           \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_nohibernate_setup, nohibernate_setup, 0 }

やっていることは変数宣言なのですが、この変数は.init.setupというELFセクションに配置されます。
.init.setupセクションについては以下のコードを見てください。.init.setupセクションの先頭は__setup_startというシンボル名で参照できます。(なお、リンカスクリプトは各自で調べてみてください)

include/asm-generic/vmlinux.lds.h
#define INIT_SETUP(initsetup_align)                 \
        . = ALIGN(initsetup_align);             \
        VMLINUX_SYMBOL(__setup_start) = .;          \
        *(.init.setup)                      \
        VMLINUX_SYMBOL(__setup_end) = .;

詳細は各自ソースを読んでみて欲しいのですが、以下do_early_param()という関数があります。
処理中、setup_funcという関数ポインタ経由で先の__setup()マクロで指定した関数を呼び出すことになります。

init/main.c
/* Check for early params. */
static int __init do_early_param(char *param, char *val, const char *unused)
{
    const struct obs_kernel_param *p;

    for (p = __setup_start; p < __setup_end; p++) {
        if ((p->early && parameq(param, p->str)) ||
            (strcmp(param, "console") == 0 &&
             strcmp(p->str, "earlycon") == 0) 
        ) {
            if (p->setup_func(val) != 0)
                pr_warn("Malformed early option '%s'\n", param);
        }
    }
    /* We accept everything at this stage. */
    return 0;
}

先に進みます

hibernate()に戻ります。以下は単なる排他処理です。すでにsnapshotを作成中であれば、何もせず処理を終了します。

kernel/power/hibernate.c
    /* The snapshot device should not be opened while we're running */
    if (!atomic_add_unless(&snapshot_device_available, -1, 0)) {
        error = -EBUSY;
        goto Unlock;
    }    

更に先に進むと、以下の処理があります。ここで、pm_prepare_console()は飛ばします。

kernel/power/hibernate.c
    pm_prepare_console();
    error = pm_notifier_call_chain(PM_HIBERNATION_PREPARE);
    if (error)
        goto Exit;

pm_notifier_call_chain()は以下のとおりです。

kernel/power/main.c
int pm_notifier_call_chain(unsigned long val)

int pm_notifier_call_chain(unsigned long val)
{
    int ret = blocking_notifier_call_chain(&pm_chain_head, val, NULL);

    return notifier_to_errno(ret);
}

詳しくは見ませんが、kernel/notifier.cにnotifierの処理群があります。
上の処理は、イベント通知の仕組みと考えてください。つまり、ここはhibernateのイベントを通知する、と考えれば良いです。
シャットダウン時に実施される特殊なイベント通知もあったりするので、興味があればぜひ目を通してください。

hibernate()に戻ります。
syncした上、ユーザプロセスが実行状態に遷移しないようにします。freeze_process()は前回書いたとおりです。

kernel/power/hibernate.c
    printk(KERN_INFO "PM: Syncing filesystems ... ");
    sys_sync();
    printk("done.\n");

    error = freeze_processes();
    if (error)
        goto Exit;

sys_sync()およびfreeze_processes()の呼び出しは「suspendになったストレージに書き込みを防ぐ」という意図で実施していると思われます。

sync()をしないと、ダーティなファイルキャッシュをsync()するための書き込みが行われる可能性もあります。また、ユーザプロセスが実行可能になると、ストレージ上のファイルにアクセスする可能性もあります。

次回は

今回はhibernate()のほんの入り口でした。
次回はさらにhibernate()を読みます。スナップショットを作るところまで・・・いければいいなあ。

8
5
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
8
5