スリープとウェイクアップ

このページの目次

眠れない、眠るとすぐに目覚める、復元後にデバイスが無効になる——これらは異なる段階における問題です。システムスリープとデバイスのruntime PMは異なる状態マシンであり、⁠プラットフォームの能力、段階的な依存関係、およびウェイクアッププロトコルに基づいて特定する必要があります。

参照目標参照箇所
サポートされるモードとデバイスの段階最初の2節
ミラー復元とオンデマンドサスペンドhibernate、runtime PM
イベント競合と診断ウェイクアップ、失敗段階

まず、現在のプラットフォームが何に対応しているか確認する

cat /sys/power/state
cat /sys/power/mem_sleep

state は要求可能な状態の一覧を示します。例えば freeze、mem、disk などです。mem_sleep は mem に関連するオプションモードの一覧を示し、角括弧で現在の選択項目が示されます。例えば s2idle [deep] は、mem が現在 deep に選択されていることを意味しますが、すべてのマシンが deep をサポートしているわけではありません。システムスリープ状態

方法復元で依存する状態主な違い
suspend-to-idle (s2idle、freeze)RAM内の既存状態ソフトウェアによる協調凍結、デバイスサスペンド、CPUアイドル。従来のS3は不要
suspend-to-RAM (一般的なdeep)RAMに保持される状態プラットフォームはより深いシステムスリープ状態に入る。ACPIプラットフォームでは通常S3に対応
hibernation (disk)不揮発ストレージ内のメモリミラー次回起動時にミラーを読み込み、古いカーネルの実行状態を復元

プラットフォームによっては shallow などの状態もオプションで存在します。消費電力と復元時間は、デバイス、ファームウェア、ミラーの規模などによって総合的に決定されるため、1W、100ms、または10秒といった値をインターフェースの保証として記述すべきではありません。また、s2idle が「浅いスリープしかできない」ことを意味するわけではありません。適切なプラットフォームでは、これを通じてより深い全体機器の低消費電力状態に達することも可能です。

サスペンドを段階に分ける理由

システムサスペンド:静止点を確立し、実行を再開する 準備と凍結 ユーザー空間、カーネルスレッド、ウェイクアップイベントの調整 1 デバイスサスペンドの各段階 依存関係に従い、suspend、late、noirq などのコールバックを処理 2 選択されたシステムスリープ状態への移行 s2idle と deep ではプラットフォームの移行方法が異なる 3 デバイスの復元と凍結解除 ウェイクアップ後に依存関係を復元し、失敗を処理してタスクを再開 4 これは完全なソースコードの呼び出しスタックを表すものではなく、デバイスの順序、プラットフォームの手順、および失敗時のフォールバックを考慮する必要があります。

システムは、新しい作業がデバイスに次々と流入するのを阻止しつつ、完了に必要な基盤を保持する必要があります。デバイスは通常、新しいI/Oを停止し、状態を保存してから、より遅い段階へ移行します。復元時には、まず依存関係を再構築し、その後上位層が作業を再発行できるようにします。

デバイスPMコールバックには、prepare/complete、suspend/resume、late/early、noirq などの段階があります。noirq は「ドライバがついにすべての割り込みを自由に無効にできる」という同義語ではなく、PMコアがデバイスの割り込み処理を調整済みの段階を意味します。非同期サスペンドや device links などは依存関係の実行に影響を与えるため、デバイス名の順序だけで推論することはできません。

ユーザータスクの凍結は、それらがすべてのユーザー空間ミューテックスを解放することを要求するものではなく、任意のビジネストランザクションを一貫性のあるスナップショットに変えるものでもありません。カーネルロックと凍結可能なスレッドについては、「凍結された側が復元またはサスペンドに必要なリソースを保持している」という待機ループを形成しないようにする必要があります。デバイスPM

hibernate:ミラーの保存と古い実行コンテキストの復元

hibernate では、復元可能なメモリミラーを作成し、不揮発ストレージに書き出す必要があります。これは「全RAM」を単純にバイト単位でコピーすることではありません。アイドルページ、保存不要なページ、および回収結果などはミラーの規模に影響を与え、ミラーは圧縮される可能性があります。また、すべてのページキャッシュが条件なくスキップされるわけでもありません。

ミラーを書き出す際、デバイスは凍結、凍結解除、そして最終的な電源状態への移行など、異なるパスを経験する可能性があります。そのため、ドライバの freeze/thaw、poweroff/restore コールバックには独立した意味があり、これらをすべて suspend/resume の別名として扱うことはできません。

復元は通常、新しく起動したカーネルまたは initramfs によってミラーが発見・ロードされ、保存されたカーネル状態に戻ります。デバイスresume、スワップファイルのオフセット、暗号化デバイスのロック解除、および起動段階のサポートは連携して行う必要があり、単に resume= を追加すれば必ず成功するわけではありません。また、ミラーが存在する状態で別の実行環境から関連ファイルシステムを書き換え、復元後の古いキャッシュが一貫性を保つことを期待することもできません。swsusp

ミラーにはメモリ内の機密状態が含まれるため、ストレージと復元のチェーンは、システムが既に持っている暗号化と信頼できる起動の設計に従うべきです。電源断後の全体機器の待機電力があるかどうかはハードウェアに依存し、hibernate という用語だけで絶対的なゼロワットが保証されるわけではありません。

runtime PM:使用カウント、依存関係、およびドライバ状態

runtime PM は、システム実行中に特定のデバイスのアクティビティとサスペンドを調整します。使用カウントは、まだ必要とされているデバイスがサスペンドされないようにするプロトコルの一つであり、サブデバイス、依存関係、ドライバの許可状態、および未完了のリクエストに関与します。autosuspend_delay_ms はその条件の一つに過ぎません。

ドライバがデバイスにアクセスする前には、pm_runtime_resume_and_get などの適切な同期取得および復元インターフェースを使用して取得し、失敗を確認する必要があります。使用を終了したら、プロトコルに従ってビジー時間をマーキングし、使用参照を解放します。復元失敗後にレジスタへのアクセスを続けることはできません。runtime PM

power/control の auto は runtime PM を許可し、on は通常 runtime サスペンドを禁止します。これは、システムスリープが許可されるかどうかとは異なります。PCI D3hot と D3cold にも、異なる電力供給および設定アクセスの条件があり、D3hot を「デバイスが完全に電源オフだがリンクは維持されている」と簡略化して記述することはできません。

使用参照がカバーする作業の範囲

同期デバイスアクセスの順序は、次の通りです:ランタイム参照の取得とデバイスの復元に成功し、ハードウェアにアクセスし、最後にこの参照を解放します。復元失敗時には利用可能なデバイスが得られないため、I/O を続けることもできず、失敗を成功した取得と見なして盲目的に解放することもできません。pm_runtime_resume_and_get() と pm_runtime_get_sync() のエラーパスにおけるカウントのセマンティクスは異なります。後者はエラー発生時に増加した使用カウントを自動的に取り消しません。runtime PM ヘルパーのセマンティクス

パスハードウェアへのアクセス現在の呼び出しで解放すべき使用参照
resume-and-get 失敗なし0
取得成功、I/O 成功はい1
取得成功、I/O 失敗試行済み1
取得成功、非同期I/Oが送信済みだが未完了デバイスは依然として動作中参照は、所有権プロトコルに従って完了/取消パスによって解放されるべき

以下のモデルは参照のペアリングのみをチェックし、復元失敗は戻り値で表し、転送失敗は例外で表します。PMカウントが0になっても直接サスペンドが実行されるわけではなく、したがって「put が返った時点でデバイスが電源オフになっている」と誤って示唆することもありません。

class Usage:
    def __init__(self, resume_ok=True):
        self.resume_ok = resume_ok
        self.count = 0
        self.accesses = 0

    def resume_and_get(self):
        if not self.resume_ok:
            return False
        self.count += 1
        return True

    def put(self):
        assert self.count > 0
        self.count -= 1

    def io(self, fail=False):
        assert self.count > 0
        self.accesses += 1
        if fail:
            raise OSError("transfer failed")
        return "ok"

def transfer(pm, fail=False):
    if not pm.resume_and_get():
        return "resume failed"
    try:
        return pm.io(fail)
    finally:
        pm.put()

pm = Usage(False)
assert transfer(pm) == "resume failed" and (pm.count, pm.accesses) == (0, 0)
pm = Usage()
assert transfer(pm) == "ok" and (pm.count, pm.accesses) == (0, 1)
try:
    transfer(pm, fail=True)
except OSError:
    pass
else:
    raise AssertionError("転送エラーを伝播すべき")
assert (pm.count, pm.accesses) == (0, 2)
assert pm.resume_and_get() and pm.resume_and_get()
pm.put()
assert pm.count == 1  # 1つの呼び出しが終了し、別の呼び出しの参照を消費しない
pm.put()
assert pm.count == 0

Linux 7.2 の pm_runtime_put_autosuspend() は、自身で last_busy を更新してから参照を解放します。二重アンダースコア付きの __pm_runtime_put_autosuspend() は、この時間更新を担当しません。古いドライバにおける "mark_last_busy + put" の組み合わせを読む際には、ターゲットバージョンの include/linux/pm_runtime.h に照らして確認する必要があります。autosuspend は、遅延、アクティブなサブデバイス、PM QoS、ドライバの状態、および保留中のリクエストなどの条件にも制約されます。

転送にDMAを使用する場合、関数が「送信成功」を返した時点で、ハードウェアはメモリへのアクセスを続けている可能性があります。参照のライフサイクルは、完了または停止が確認された取消パスまで延長され、両方のパスで参照が一度だけ解放されることが保証されなければなりません。送信関数の finally だけで参照を解放すると、この同期モデルが非同期デバイスに誤って適用されてしまいます。

サブデバイスの使用参照を解放した後、親デバイスのサスペンドを試み、その後サブデバイスのサスペンドを進めます。使用カウントがゼロ、待機期間が経過、サブデバイスがアイドル、ドライバが許可している——これらはそれぞれ満たすべき条件です。

図を準備しています
カウントが0になれば、デバイスはすぐに眠るのか?

put が0になっても、サスペンドを防ぐ条件の一つが取り除かれるだけであり、デバイスがすでにサスペンドされていることを保証するものではありません。サブデバイスの復元には、その親依存関係がまず利用可能である必要があります。アクティブなサブデバイスは親デバイスのサスペンドを阻止することもあります。

ウェイクアップ:能力、ポリシー、および実際のイベントを分離する

条件確認すべきこと
デバイスの能力ウェイクアップイベントを生成できるか
ポリシーの許可power/wakeup などが許可しているか
実際の伝播イベントが親デバイスおよびプラットフォームを通じてCPUに到達できるか

属性を有効化しても、ハードウェアの配線、ファームウェア、および全体の依存パスの条件を代替することはできません。

スリープ準備中に新しいイベントが発生し、サスペンドの中止を要求する場合があります。ユーザー空間とカーネルは、wakeup_count などのプロトコルを使用して、「イベントがないことを確認してから、実際にスリープする前に新しいイベントが来る」という競合を処理できます。pm_wakeup_irq は限定的なIRQの手がかりを提供しますが、すべてのウェイクアップソースの完全な台帳ではありません。

wakeup_count は「確認してから眠る」間の窓をどのように閉じるか

1回の協調試行は、次のように理解できます:まず、アクティブなウェイクアップイベントがない状態でのカウント n を読み取り、ユーザー空間の準備を完了し、その後 n を書き戻します。カウントが同じままで、処理中のイベントがない場合に限り、書き戻しが成功します。書き戻しが成功すると、その後のチェックが有効になり、新しいイベントによって今回のサスペンドを中止させることができます。読み取りは、処理中のイベントの終了を待つ場合もあり、常に即座に返る単純な統計読み取りではありません。

新しいイベントの発生位置元のスリープ判断を沿用できない理由
読み取り後、書き戻し前に処理が終了カウントが n から n+1 に変化し、n への書き戻しが失敗
書き戻し時に処理中完了カウントがまだ増加していなくても、アクティブイベントにより書き戻しが失敗
書き戻し成功後、スリープチェック前チェックで新しいカウントまたはアクティブイベントが発見され、今回の変換が中止
1回のスリープ試行には前後2回のイベントチェックが必要 カウント n を読み取る アクティブイベントの終了を待つ n を書き戻す カウントとアクティブ状態を比較 スリープ中のチェック 変化があれば今回の変換を中止 イベントウィンドウ ① イベントウィンドウ ② ① は書き戻し失敗を引き起こす;② は後のスリープチェックで pending を発見させる これは協調プロトコルの模式図であり、実際のスリープエントリおよびスリープ進入後のイベントは、プラットフォームのウェイクアップメカニズムに依存します。

以下は、1回の試行中のアトミックチェックポイントをシミュレートしたものです。finished は終了したモデルのウェイクアップ活動の数を表し、active は処理中の数を表します。これらは元の割り込みカウントではありません。実際のカーネルには、ロック、カウントのパッキング、ブロッキング待機、および追加の中止ソースが含まれます。ここでの pending は状態のみを比較し、カーネルがイベントを検出した後にチェックの有効化をクリアする副作用を再現するものではありません。

class WakeupGate:
    def __init__(self):
        self.finished = 0
        self.active = 0
        self.saved = None

    def begin(self):
        self.active += 1

    def end(self):
        assert self.active > 0
        self.active -= 1
        self.finished += 1

    def read(self):
        if self.active:
            raise BlockingIOError("実際のブロッキング読み取りでは、アクティブ終了を待つ必要がある")
        return self.finished

    def save(self, observed):
        self.saved = None
        if self.active or observed != self.finished:
            return False
        self.saved = observed
        return True

    def pending(self):
        assert self.saved is not None
        return self.active > 0 or self.finished != self.saved

# 読み書きの間に完了:カウントの変化が識別可能。
gate = WakeupGate()
n = gate.read()
gate.begin(); gate.end()
assert not gate.save(n)

# 処理中:完了カウントのみを比較することはできない。
gate = WakeupGate()
n = gate.read()
gate.begin()
assert gate.finished == n and not gate.save(n)
try:
    gate.read()
except BlockingIOError:
    pass
else:
    raise AssertionError("アクティブイベント中は、このモデルの安定した読み取りを提供できない")

# 書き戻し後:アクティブ期間中および処理終了後の両方で変化を発見可能。
gate = WakeupGate()
assert gate.save(gate.read()) and not gate.pending()
gate.begin()
assert gate.pending()
gate.end()
assert gate.pending()
# 新しいイベントの処理が完了し、協調試行を再開。
assert gate.save(gate.read()) and not gate.pending()

チェックが通過したのは、このチェックポイントが条件を満たしていることを示すだけで、その後に割り込みが発生しないことを意味しません。その後のPMチェックおよびプラットフォームのウェイクアップメカニズムは、実際のスリープエントリに近づいた時点、さらにはスリープ進入後のイベントを処理する必要があります。能力、ポリシー、および伝播パスは依然として成立している必要があります。したがって、モデル内の not pending() を「無条件でウェイクアップを無効化してスリープする」と記述してはいけません。インターフェース契約は、カーネル Documentation/ABI/testing/sysfs-power の wakeup_count エントリで確認でき、実装は drivers/base/power/wakeup.c にあります。

失敗の段階に応じて特定する

実際の mem_sleep を記録してから、段階を特定します:

現象調査の起点
進入できない凍結、デバイスコールバック、および依存関係が完了したか
進入直後に目覚める実際のウェイクアップイベントと準備中の競合
目覚められない能力、ポリシー、およびプラットフォームの伝播パス
復元後に単一デバイスが無効対応する復元コールバックおよびリソース再構築

最後のログエントリで言及されたデバイスが、必ずしも根本原因であるとは限りません。

journalctl -k -b
cat /sys/power/pm_wakeup_irq

属性の存在とログの権限は環境に依存します。power:suspend_resume などの tracepoint を組み合わせて段階的な所要時間を観察できます。トレースを有効化する際には、trace_pipe を任意の現在のディレクトリ内のファイルとして扱うのではなく、マシンに実際にマウントされている tracefs のパスを使用してください。

CONFIG_PM_DEBUG をサポートするシステムでは、pm_test を使用して freezer、devices などの段階を階層型でテストし、プラットフォームが実際にスリープ状態に入る前の故障範囲を絞り込むことができます。テストは実際にタスクを凍結したりデバイスをサスペンドしたりするため、復元手段のあるテスト環境で実行する必要があります。このページでリストされている読み取りコマンド自体はスリープをトリガーしません。デバッグドキュメント

pm_trace は RTC を使用してトレースフィンガープリントを保存しますが、クロックに影響を与える副作用があるため、説明なしで最初のデバッグコマンドとして使用するのは適切ではありません。ドライバのエラーは、コールバックの戻り値、依存関係、および繰り返しテストと組み合わせて確認する必要があり、ハッシュの一致のみを根因と見なしてはいけません。