4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

前回のおさらい

前回はfreezingの実装を見ました。しかし、「シグナルを送出していないのにシグナルを送出した」旨のフラグをタスク構造体に立てて、スレッドを起こすだけのように見えました。
覚えているでしょうか。

kernel/signal.c
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
    set_tsk_thread_flag(t, TIF_SIGPENDING);

いったい誰がスレッドをfreeze状態にしているのでしょうか。

実はfreezing状態にするルートは複数あります。この文書では代表的と思われる2つについて紹介します。
そのうちの一つには、シグナル処理が大きく関係しています。なので、freezing処理とともに、Linuxのシグナル受信処理の一部もあわせてみていきます。

で、誰がスレッドをfreeze状態にしているの?

思い出してください。前回frozen()という関数を紹介しました。
そこではp->flagsにPF_FROZENが立っているかどうかを見ています。

なら、このフラグを立てている箇所を検索してあたりをつければよさそうです。
検索すると、このフラグを立てているのはたった一箇所です。

kernel/freezer.c:               current->flags |= PF_FROZEN;

早速実装を見ましょう。
一言で言うと「実行状態のスレッドにPF_FROZENフラグを立てた上で、自分以外のスレッドに実行権を譲ることを期待してスケジューラに再スケジューリングを依頼する」というところです。

kernel/freezer.c
/* Refrigerator is place where frozen processes are stored :-). */
bool __refrigerator(bool check_kthr_stop)
{
/* 略 */

    for (;;) {
        set_current_state(TASK_UNINTERRUPTIBLE);

        spin_lock_irq(&freezer_lock);
        current->flags |= PF_FROZEN;
        if (!freezing(current) ||
            (check_kthr_stop && kthread_should_stop()))
            current->flags &= ~PF_FROZEN;
        spin_unlock_irq(&freezer_lock);

        if (!(current->flags & PF_FROZEN))
            break;
        was_frozen = true;
        schedule();
    }

/* 略 */

    return was_frozen;
}
EXPORT_SYMBOL(__refrigerator);

冷蔵庫(refrigerator)とは、なんとも洒落た名前です。
__refrigerator()の呼び出し箇所は以下のとおりです。

include/linux/freezer.h:        return __refrigerator(false);
kernel/kthread.c:               frozen = __refrigerator(true);

今回はユーザスレッドを見ていきますので、kthread.cは無視してinclude/linux/freezer.hを見ます。
なお、以下ソースを見るとわかりますが、try_to_freeze_unsafe()は呼び出し時にfreezing(current)を呼び出して、戻り値がtrueのときにのみ__refrigerator()を呼び出していることに注意してください。
freezing()は「システムに対してfreeze要求があったかどうか」を見ています。

詳細は、前回抜粋したfreezing()の実装およびDocumentation要約内のsystem_freezing_cntに関する説明を参照してください。

include/linux/freezer.h
static inline bool try_to_freeze_unsafe(void)
{
    might_sleep();
    if (likely(!freezing(current)))
        return false;
    return __refrigerator(false);
}

static inline bool try_to_freeze(void)
{
    if (!(current->flags & PF_NOFREEZE))
        debug_check_no_locks_held();
    return try_to_freeze_unsafe();
}

/* 略 */

/* DO NOT ADD ANY NEW CALLERS OF THIS FUNCTION */
static inline void freezer_count_unsafe(void)
{
    current->flags &= ~PF_FREEZER_SKIP;
    smp_mb();
    try_to_freeze_unsafe();
}

実は、try_to_freeze_unsafe()の呼び出し元は上記ヘッダにある2つの関数try_to_freeze()とfreezer_count_unsafe()です。
コメントから、freezer_count_unsafe()は特殊な用途が想定されますので、一般ケースであろうtry_to_freeze()の呼び出し元を見ます。

検索をかけると、try_to_freeze()はカーネルの複数箇所で呼ばれていることがわかります。

ただ、大事なのはこれから紹介する二箇所ではないかな、と考えます。それらを以降で紹介します。

try_to_freeze()呼び出し元その1

以下のとおり、__wait_event_freezable()です。

include/linux/wait.h
#define ___wait_event(wq, condition, state, exclusive, ret, cmd)    \
    for (;;) {
        if (condition)                      \
            break;
        cmd;                            \
    }                               \
    finish_wait(&wq, &__wait);                  \
__out:  __ret;                              \
})
#define __wait_event_freezable(wq, condition)               \
    ___wait_event(wq, condition, TASK_INTERRUPTIBLE, 0, 0,      \
                schedule(); try_to_freeze())

Linuxカーネルでイベント待ち(wait - wakeup)をするための仕組みでwait_event()があることはご存知かと思います。
その亜種でwait_event_freezable()というのがあります。wait_event_freezable()は__wait_event_freezable()を呼び出しています。
この実装では、イベント待ちを行い、イベントが発生条件が成立したときにtry_to_freezeを呼ぶ実装になっています。

そして、再度思い出して欲しいのが、__refrigerator()の実装が以下の通りになっていることです。

kernel/freezer.c
    for (;;) {
/* 略 */
        current->flags |= PF_FROZEN;
/* 略 */
        if (!(current->flags & PF_FROZEN))
            break;
        was_frozen = true;
        schedule();
    }

つまり、自らをPF_FROZEN状態にして、PF_FROZEN状態が解除されるまでschedule()を呼びます。これによって、再スケジューリングが行われますので他のスレッドに実行権を渡すか、実行権を持ったままループします(そしていつか他のスレッドに実行権を譲る)。
確かに、明示的に「freezingのときには待っても良い」と主張するこのケースではスレッドが"freezing"します。

try_to_freeze()呼び出し元その2

try_to_freezeはget_signal()で呼ばれています。
ただ、いきなりget_signal()を見てもわかりにくいので、Linuxのシグナル処理の前半部分を見ながらget_signal()に到達してみます。

シグナル処理について

シグナルを受信すると、該当タスク構造体のフラグにはTIF_SIGPENDINGが立ちます。
しかし、シグナルは基本的に即時処理されません。
実行タイミングのひとつに、「割り込みや例外が発生したとき」というのがあります。実は割り込み処理や例外処理終了直前に、「実行中のタスク(プロセス)にはペンディングされたシグナルがあるか」を判定します。判定方法は、該当タスク構造体のフラグにTIF_SIGPENDINGが立っているかを調べることです。

ここまでを押さえて、「割り込みや例外が発生したとき」におけるシグナル受信処理のコードを読みましょう。

いきなりアセンブラだけど

ユーザモードで実行中に例外や割り込みが発生するとカーネルモードに遷移します。
そして、処理が終わるとその後ユーザモードに戻ります。x86の場合、この戻り処理を実施しているのがresume_userspace()です。

arch/x86/kernel/entry_32.S
ENTRY(resume_userspace)
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx

    # 追加コメント
    # 割り込みや例外終了のタイミングで実施すべきことがあれば実施する。
    # 実施はwork_pendingラベルへのジャンプで行う。
    andl $_TIF_WORK_MASK, %ecx  # is there any work to be done on
                    # int/exception return?
    jne work_pending
    jmp restore_all
END(ret_from_exception)

追加コメントのとおり、割り込みや例外終了のタイミングで実施すべきことがあればwork_pending処理にジャンプします。
この判定に使われている_TIF_WORK_MASKは以下の定義となっています。

arch/x86/include/asm/thread_info.h
#define _TIF_SIGPENDING     (1 << TIF_SIGPENDING)
/* 略 */
/* work to do on interrupt/exception return */
#define _TIF_WORK_MASK                          \
    (0x0000FFFF &                           \
     ~(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|           \
       _TIF_SINGLESTEP|_TIF_SECCOMP|_TIF_SYSCALL_EMU))

_TIF_WORK_MASKは、タスク構造体のフラグのうち、割り込みや例外の戻り時になすべきイベントに関するものが立っているかを調べるためのマスク値です。

_TIF_SYSCALL_TRACE、_TIF_SYSCALL_AUDIT、_TIF_SINGLESTEP、_TIF_SECCOMP、_TIF_SYSCALL_EMUの5つ以外が対象のイベントに該当します。
よって、_TIF_SIGPENDING(ペンディングされたシグナルがある)が立っている場合、割り込みや例外の戻り時になすべきイベントがあるとみなします。そして、work_pendingへとジャンプします。

work_pendingにジャンプしたあとは、以下のようにdo_notify_resume()を呼び出します。

arch/x86/kernel/entry_32.S
# 略
## work_pendingラベルはここ。
work_pending:
# 略 
    call do_notify_resume
    jmp resume_userspace

do_notify_resume()およびそこから呼び出されるdo_signal()はarchの下のコードで、x86の場合は、以下の実装です。

arch/x86/kernel/signal.c
static void do_signal(struct pt_regs *regs)
{
    struct ksignal ksig;

    if (get_signal(&ksig)) {
        /* Whee! Actually deliver the signal.  */
        handle_signal(&ksig, regs);
        return;
    }

                                                  /*
 * notification of userspace execution resumption
 * - triggered by the TIF_WORK_MASK flags
 */
__visible void
do_notify_resume(struct pt_regs *regs, void *unused, __u32 thread_info_flags)
{
    user_exit();

    if (thread_info_flags & _TIF_UPROBE)
        uprobe_notify_resume(regs);

    /* deal with pending signal delivery */
    if (thread_info_flags & _TIF_SIGPENDING)
        do_signal(regs);

ここを見るとわかるように、_TIF_SIGPENDINGが立っている場合、do_signal()が呼ばれ、そこからさらにget_signal()が呼ばれます。

kernel/signal.c
int get_signal(struct ksignal *ksig)
{
    struct sighand_struct *sighand = current->sighand;
    struct signal_struct *signal = current->signal;
    int signr;

    if (unlikely(current->task_works))
        task_work_run();

    if (unlikely(uprobe_deny_signal()))
        return 0;

    /*   
     * 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();

確かに呼んでいます。明示的に「freezingのときには待っても良い」と主張しない大部分のケースであっても、シグナル受信と同様の仕組みでスレッドをfreezingさせることができます。
なかなか面白い仕組みです。

次回は

少し考えさせてください。読んでみたいところがいろいろあるので・・・。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?