EternalWindows
サービス / SCM

今回は、サービスをプログラムの視点から考えてみたいと思います。 サービスの開発は、環境設定の仕方やWindowsAPIを呼び出すことなど、 普通のWindowsアプリケーションの開発と同じの面が多々あります。 たとえば、サービスのエントリポイントは、いつものようにWinMainで構いません。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	StartServiceCtrlDispatcher(...); // 簡単のため、引数は省略

	return 0;
}

上記コードが呼び出しているStartServiceCtrlDispatcherという関数は、 内部でSCM(スカムと呼ぶ)とのコマンド通信を行っています。 SCMは、Service Control Managerの略で、全てのサービスを管理します。 たとえば、自動起動タイプのサービスはシステムの起動時と共に実行しなければなりませんが、 そのサービスを起動してくれるのがSCMです。 また、SCMはSCPからの再開や一時停止などの制御コードを受け取り、それをサービスに送信します。 これにより、サービスは制御コードに応じた処理を行えることになります。 これらの背景から分かるように、サービスはSCMの援護なしでは動作できないため、 最初にStartServiceCtrlDispatcher関数でSCMと接触する必要があるのです。 この点がプログラミング上において、一般のWindowsアプリケーションとの最も大きな違いになります。 ちなみに、SCMの正体はservices.exeであり、自動起動のサービスを実行しなくてはならないこと等から、 システムの起動時に必ず実行されます。

StartServiceCtrlDispatcherは、内部で名前付きパイプを作成し、 SCMからの制御コードを受け取るためにループに入ります。 SCMは最初にサービスの開始の制御コードを送信し、 それを受け取ったStartServiceCtrlDispatcherは、サービススレッドを作成します。 StartServiceCtrlDispatcherがループから抜け出して制御を返すのは、 このサービススレッドが制御を返してからとなります。 ここで、サービススレッドの目的を理解するために、 StartServiceCtrlDispatcherの引数を確認します。

BOOL StartServiceCtrlDispatcher(
  const LPSERVICE_TABLE_ENTRY lpServiceTable
);

LPSERVICE_TABLE_ENTRYは、SERVICE_TABLE_ENTRY構造体へのポインタです。 SERVICE_TABLE_ENTRY構造体は、以下のように定義されています。

typedef struct _SERVICE_TABLE_ENTRY { 
  LPTSTR lpServiceName;
  LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;

lpServiceProcが非常に重要な引数です。 この引数には、サービススレッドのエントリポイントなる関数のアドレスを指定します。 この関数は、次のようなプロトタイプを持たなくてはなりません。

VOID WINAPI ServiceMain(
  DWORD dwArgc,
  LPTSTR* lpszArgv
);

エントリポイントはアプリケーションで用意する関数であるため、 このServiceMainという関数名は自由に変更することができます。 しかし、サービススレッドはこのServiceMainで一体何を行うべきなのでしょうか。 答えは、そのサービス特有の処理です。 たとえば、webサーバーをサービスとして実装しているような場合は、 クライアントからのHTTP要求を受信するためのコードを書くことになるでしょう。 つまり、本当の意味でのサービスとはServiceMainを実行するスレッド、 即ちサービススレッドということになります。

これまでの話で理解しがたいのは、サービス特有の処理をメインスレッドで行わず、 わざわざスレッドを作成してそこで処理を行うという点だと思われます。 このように考えてしまうのは、メインスレッドで呼び出しているStartServiceCtrlDispatcherが、 スレッドを作成するだけの関数に見えてしまうからですが、これはあくまで1つの処理に過ぎません。 サービスは、SCMからの制御コードなしでは動作できないため、SCMとの接触は絶対に必要です。 これが、StartServiceCtrlDispatcherを呼び出す本当の理由です。 そして、この関数は内部でループに入り、SCMの制御コードを受信するまで待機しますから、 メインスレッドの進行をブッロクしているわけです。 これでは、サービス特有のコードをメインスレッドで実行できませんから、 StartServiceCtrlDispatcherはスレッドを作成し、 開発者はそのスレッドにサービス特有のコードを書くことになるわけです。 サービスを開発するにあたって、この点を理解しておくことは決定的に重要です。

次のコードは、StartServiceCtrlDispatcher呼び出しす例を示しています。 一応、コンパイルすることはできますが、サービスとしては不完全であるため、 実行しても何も起きることはありません。

#include <windows.h>

VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SERVICE_TABLE_ENTRY serviceTable[] = {
		{TEXT("ServiceName"), ServiceMain}, {NULL, NULL}
	};
	
	StartServiceCtrlDispatcher(serviceTable);

	return 0;
}

// この関数は、StartServiceCtrlDispatcherが作成したスレッドが呼び出す。
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	// サービス特有の処理を行う
}

SERVICE_TABLE_ENTRY構造体の最初のメンバは、サービス名を指定します。 サービス名とは、プログラムの中でサービスを識別する名前のことで、自由に決めることができます。 2番目のメンバは、既に述べたようにサービススレッドのエントリポイントを指定します。。 この関数名も自由に決めて構いません。 serviceTableは配列として宣言され、2番目の要素はどちらのメンバもNULLで構成されていますがm これは、1つのプロセスが複数のサービスを持つことが可能であるためです。 サービスを複数提供するということは、その数だけ要素数が増えることになるため、 提供するサービスの数というものを明示しなくてはなりません。 n番目の要素のメンバをどちらもNULLにすることにより、 StartServiceCtrlDispatcherは提供しているサービスの数が、 n個であるということを理解できるようになります。

ServiceMainの2つの引数はまずもって使用することはないので、説明は省略します。 今の段階では、ServiceMainはサービススレッドによって呼び出されることと、 その内部でサービス特有の処理を行うことになるという2点を理解しておけば十分です。 また、ServiceMainが制御を返すと、StartServiceCtrlDispatcherも制御を返す点も覚えておいてください。 ちなみに、プロセスのメインスレッドはStartServiceCtrlDispatcherの呼び出しを30秒以内に行わなければならず、 さらにStartServiceCtrlDispatcherから復帰後、 30秒以内にWinMainから制御を返さなくてはならない制約があります。


戻る