◆レジューム(サスペンド/休止からの自動復帰)・・・・電源管理2 その1

[ ホーム ] [ 上へ ] [ 電源管理2 その2 ]

電源管理についてはVC++版ではその1で、 VBA版ではその2で書きましたが、いずれも シャットダウンサスペンドなどPCを停止する話でした。
今回はその逆で起動する方法について書いてみます。起動するといっても大きく分けて2つの 意味があります。1つは電源が入っていない状態からのスイッチON→ログオンという意味、 2つめはサスペンド/休止状態からの自動復帰(レジューム)という意味があるかと思います。 前者についてはリモートコンピュータの話で、認証などセキュリティ関連のことがてんこ盛りに なりたいへんなので、より簡単な後者のレジュームについて書きます。

・VC++ 2005 MFC編

<概要>

サスペンド/休止(ハイバーネーション)状態からの自動復帰 (以後、レジュームと呼びます)自体は
SetWaitableTimerというWin32APIで簡単にできます。 CreateWaitableTimerで待機可能タイマオブジェクトを作成して、そのハンドルと レジューム時間などを引数としてSetWaitableTimer に渡します。
SetWaitableTimerを呼び出したプロセスは終了させないで、 一旦手動でサスペンド/休止させると、SetWaitableTimerで 設定した時間にレジュームが実行されます。
つまり、

LARGE_INTEGER   li;
li.QuadPart = レジューム時間;

HANDLE          h = ::CreateWaitableTimer(NULL, FALSE, "TimerName");
::SetWaitableTimer(h, &li, 0, NULL, NULL, TRUE);

実行ファイル(exeファイル)内で上記のようなコードを実行した後、 アプリケーションを終了させないで、サスペンド/休止させてレジューム時間を待つだけです。

これで一件落着ならばいいのですが実はそう簡単ではないです。レジュームが 実行される状況を考えてみると、通常、夜間などPCの前にユーザーが存在しない状態で実行され、 何らかの作業を行うようなことが想像できます。
自動的にレジュームが実行されると、CPUやHDDは活動していますが、キーボードやマウスなど ユーザーによる活動はないのでシステムは非活動状態と判断してしまいます。その結果、 レジューム後、任意の処理中に再びスリープ状態に戻ってしまうということが起きてしまいます。
従って、レジュームしたらシステム(OS)にアプリケーションが活動している(busy)であることを 通知して、システムがサスペンドなどスリープ状態になることを防ぐ必要がでてきます。 このアプリケーションが活動中であることをシステムに通知してくれるAPIが SetThreadExecutionStateです。

レジュームが完了したらSetThreadExecutionState を呼び出さなければなりませんが、レジュームの完了はどのように認識するのでしょう?
レジュームの完了を認識できるのが最初に紹介したSetWaitableTimer です。ここでこのAPIを詳しくみてみます。

< SetWaitableTimer >

SetWaitableTimerの定義は次のようです。

BOOL SetWaitableTimer(
  HANDLE              hTimer,                    // タイマオブジェクトのハンドル
  const LARGE_INTEGER *pDueTime,                 // タイマをシグナル状態にするまでの時間
  LONG                lPeriod,                   // 周期タイマの間隔
  PTIMERAPCROUTINE    pfnCompletionRoutine,      // 完了ルーチン
  LPVOID              lpArgToCompletionRoutine,  // 完了ルーチンのデータ
  BOOL                fResume                    // 再開状態のフラグ
);
引数 変数名 内容
第一 hTimer CreateWaitableTimer, OpenWaitableTimerで取得したタイマオブジェクトのハンドル
第二 pDueTime タイマがシグナル(レジューム)になるまでの時間。つまり、この時間まで "待機"してこの時間以降、第三引数に指定した間隔でタイマが稼動します。
実際に指定できる数値ですが、絶対時間(1970/1/1 9:0:0からの秒数-日本の場合)と 相対時間(現時刻からシグナルにするまでの時間)があります。前者の場合は正の値を、 後者の場合は負の値を指定します。
ただし、時間(秒数)をそのまま指定するとNGです。単位が100ナノ秒なので、例えば、 相対時間指定で、5秒後にシグナルにしたい場合は-50000000になります。
第三 lPriod 0にすると、第二引数で指定した時間に1度だけシグナルになります。正の値を 指定すると周期タイマになります。つまり、第二引数で指定した時間で最初の シグナルになった後、この引数の値の間隔でタイマオブジェクトがシグナルになります。 単位はミリ秒単位なので5秒間隔でシグナルにしたい時は5000になります。
第四 pfnCompletionRoutine タイマがシグナルになると呼び出されるコールバック関数(NULL指定も可)。 ここで指定するコールバック関数はSetWaitableTimerを呼び出したスレッド内でコールされ、 そのスレッドは"警告可能状態"でなければなりません。
スレッドを"警告可能状態"にするにはSleepExなど待機関数を使用しないと いけませんが、このコールバック関数のシグネチャを含めて詳しくは以降をご覧ください。
第五 lpArgToCompletionRoutine 第四引数に指定するコールバック関数が呼ばれたときに、その第一引数に渡す値。 (構造体)へのポインタ。第四引数がNULLの場合はこの引数もNULL。
第六 fResume 第二引数に指定した時間でシグナルになったときに、スタンバイ/休止状態から 復帰するかどうかを指定。SetWaitableTimerの中心的機能(第一引数〜第五引数)とは別に、 この引数によって自動レジュームを実行しようというのが本題です。

SetWaitableTimerの機能は上記のようですが、 どのような動作になるかをもう少し詳しく書きます。

位置 シグナル/非シグナル(同期タイマ) アクティブ/非アクティブ 備考
非シグナル アクティブ CreateWaitableTimerでタイマオブジェクトを作成した後、SetWaitableTimerを 実行するとタイマは"アクティブ"になります。
非シグナル アクティブ シグナルになるまでの時間。この間に再びSetWaitableTimerを呼び出すと、 pDueTimeの時間はリセットされ、シグナルになるにはpDueTimeの時間が必要です。
シグナル 非アクティブ pDueTimeの時間が経過すると、アクティブではなくなり、タイマは シグナルになります。
第六引数をTRUEにして、Aの期間中にサスペンド/休止 状態になった時はこの位置で自動でレジュームされます。
タイマがシグナルになったので、SetWaitableTimerを呼び出したスレッドが "警告可能状態"の場合は、タイマ完了ルーチンとして第四引数に指定した コールバック関数が呼ばれます。(第四引数がNULLの場合は呼ばれない)
"警告可能状態"にするにはSetWaitableTimerが実行されたスレッドで、 SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, SignalObjectAndWait, MsgWaitForMultipleObjectsExのいずれかの待機関数を 呼ぶことになります。
つまり、コールバック関数を呼ぶためには、SetWaitableTimerを呼び出したら、 上記の待機関数を呼び出して、スレッドを一時停止にする必要があるということです。 このことは後の実際のコーディングで意味を持ってくるので覚えておいてください。
非シグナル 非アクティブ 第三引数が0の場合はここ以降は無効になります。
2でコールバック関数が呼ばれたら同期タイマの場合は非シグナルになります。
シグナル アクティブ 前回のタイマオブジェクトのシグナルからlPriodで指定した時間が経過すると 再び、タイマはアクティブになり、第四引数のコールバック関数が呼ばれて 完了ルーチンが実行されます。

第四引数のコールバック関数の呼び出しの仕組みはAPC(Asynchronous Procedure Calls) についてもう少し詳しく書かないといけないのですが、上記のことが分かれば、 本題の自動レジュームにはあまり関係ないので割愛します。

<プログラム要件>

冒頭のほうでSetWaitableTimerを 呼び出してレジュームの設定をしたのち、スリープ状態にして自動レジュームした場合は、 システムが活動を認識できないので再度のスリープ状態にならないように、アプリケーションが 活動していることを通知するためにSetThreadExecuteState を呼ぶ必要があると書きました。そして、それを呼び出すのはレジュームが完了した時ということも 書きました。

上図レジュームが完了した時は2の位置であること がわかります。pDueTimeの時間を待機して2の位置に来ると タイマオブジェクトがシグナルになるということ、あるいは 第四引数に指定した場合コールバック関数が呼ばれるとということで レジュームの完了を認識できます。

しかし、ここで問題がでてきます。タイマオブジェクトがシグナルになることを 認識するにはWaitForSingleObject等、待機関数を使用する必要があります。また、 SetWaitableTimerの第四引数に指定するコールバック関数を利用するにはスレッドを "警告可能状態"にするために、同様にWaitForSingleObjectsExなどの待機関数を使わなければ ならないということです。
つまり、レジュームを認識するためにはSetWaitableTimerを実行した スレッドは待機関数によって一時停止状態になるということです。

SetWaitableTimerをメインスレッドで呼んで待機関数によって一時停止してしまうのは 明らかに不味いように思われます。待機関数で一時停止することによって、そのプロセス(exe)が フリーズしてしまったようになるからです。これを回避するにはマルチスレッドにして、 別スレッドでSetWaitableTimer→待機関数を実行しなければなりません。
マルチスレッドに関してはこちらも参考にしていただくとして、 同期などの処理が加わりとたんにプログラムが複雑化します。

レジュームに関してまとめると次のようになります。
・レジュームにはSetWaitableTimerを実行すればよい。
・レジューム後、アイドル状態の継続による再スリープを回避するためにSetThreadExecutionStateを 呼ぶ必要がある。
・レジュームされたことを認識するために待機関数を利用しなければならないので、スレッドの一時 停止の対応策として、マルチスレッドでレジューム設定処理をしなければならない。

上記の要件を踏まえて次に実際にコーディングしてみます。