책정리/윈도우 API 정복2

51장 서비스

GONII 2020. 3. 26. 23:42

01 서비스
. 서비스
서비스(Service) 윈도우즈에서 실행되는 많은 프로그램들의 종류 하나이다. 그러나 다른 프로그램과는 다른 아주 특별한 성질들을 가지고 있으며 사용용도나 개발 방법도 다르다. 서비스를 한마디로 문장화해서 정의 내리자면 "백그라운드에서 실행되는 프로그램"이라고 있는데 배경에 숨어서 사용자를 위한 어떤 작업을 하는 프로그램이다. 이름 그대로 사용자에게 서비스를 한다. 도스식으로 표현한다면 램상주 프로그램쯤 되며 유닉스식으로 표현하면 데몬(Daemon)이라고 있다.
Win32 환경에서 서비스는 Win32서비스와 드라이버 서비스로 크게 구분된다. 드라이버 서비스는 하드웨어 인터페이스를 담당하는 일종의 디바이스 드라이버를 의미하는데 이는 일반적인 SDK 영역이 아닌 DDK 영역이다.
서비스의 가장 특징이라면 백그라운드에서 실행된다는 점이다. 사용자에게 직접 보이지는 않지만 시스템 유지, 데이터 제공, 하드웨어 관리 등의 여러 가지 일들을 하고 있다. 배경에서 작업을 하기 때문에 사용자와는 적어도 직접적으로 상호작용을 하지 않는다. 사용자와 통신할 필요가 없기 때문에 UI 가지지 않으며 그래서 서비스는 통산 콘솔 프로그램 형태를 띠고 있다.
서비스는 부팅 직후부터, 로그인을 하지 않은 상태에서도 실행되며 서버가 종료될 같이 종료된다.
2000서버에서 SQL 서버를 설치하면 서버는 데이터베이스 서버로서 동작하는데 이는 데이터베이스 기능을 제공하는 MSSQLServer라는 서비스 프로그램이 부팅 직후부터 실행되기 때문이다. 서비스는 서버가 가진 데이터베이스를 관리하며 클라이언트의 요청이 있으면 처리해서 결과를 넘겨 준다.
서비스의 다른 예는 바이러스 예방 프로그램이다. 시스템 부팅과 동시에 실행되어 항상 시스템을 감시하며 입출력 동작중에 바이러스 코드가 발견되는지를 살펴본다.
서비스는 시스템과의 많은 약속들과 의무사항을 지켜가면서 동작하므로 명세에 대해 알고 있어야 한다.
. 서비스 애플릿
서비스는 시스템 부팅시에 실행되는데 시스템은 시작과 동시에 실행해야 서비스의 목록을 다음 레지스트리 아래에 유지하고 있다.
HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services
레지스트리에 있는 정보를 서비스 데이터 베이스라고 하는데 관리자로 로긴했으면 레지스트리 편집기에서 직접 내용을 확인할 있다.
 
서비스별로 하나의 서비키를 구성하고 있는데 DB 어떤 서비스들이 등록되어 있는가는 시스템에 어떤 소프트웨어를 설치했는가에 따라 달라진다. 서브키에는 서비스의 속성들이 기록되어 있다. DB 설사 구조를 완전하게 알고 있더라도 우리가 직접 편집하거나, 첨삭, 수정하는 것은 바람직하지 않으며 DB 관리하는 관리자에게 요청해야 한다. 서비스 DB 관리하는 시스템 프로그램을 SCM(Service Control Manager)라고 하며 다음과 같은 일들을 하고 있다.

설치된 서비스의 DB 관리
부팅시 또는 사용자의 요구가 있을 서비스를 시작
설치된 서비스의 목록을 열거한다
실행중인 서비스의 현재 상태를 관리
실행중인 서비스에게 제어 신호를 보낸다.

서비스에 관한 모든 것들을 관장하는 이른바 서비스 중앙 통제 센터라고 있다. SCM 부팅할 시작되며 RPC 서버이므로 원격지에서도 제어할 있다. 원격지에서도 서비스를 시작하거나 제어 코드를 전달할 있다. SCM 백그라운드에서 실행되는 프로세스이므로 사용자에게 보이지는 않는다. SCM 존재를 직접 눈으로 확인하려면 제이판의 서비스 애플릿을 열어보면 된다.
애플릿은 SCM 모든 기능을 사용하며 서비스에 관한 거의 대부분의 제어를 있다. 서비스의 특성들이 독특하고 복잡한 관계로 새로운 서비스를 설치하거나 제거하는 기능은 포함되어 있지 않다. 윈도우즈 2000에서는 서비스 애플릿은 MMC 스냅인으로 작성되어 있는데 위쪽에 툴바가 있고 중앙에 설치된 서비스의 목록이 나열되어 있다. 목록에 어떤 서비스가 표시되는가는 시스템에 따라 다르다.
 
서비스의 팝업 메뉴에서 등록 정보(속성) 보자.
등록정보는 모두 4개의 페이지로 구성되어 있으며 서비스의 현재 상태 변경, 로그온 계정 지정, 실패시의 복구 계획, 서비스간의 종속성 등을 설정하거나 참조할 있다. 일반 탭에는 서비스의 실행 상태를 변경할 있는 4개의 버튼이 있는데 의미는 버튼에 적힌 캡션 그대로이다. 서비스 상태가 "시작됨"으로 되어 있으면 프로세스가 실행중이다.
. 가지 프로그램 유형
서비스를 하나 만들기 위해서는 가지의 프로그램을 만들어야 한다. 프로세스는 SCM 기능을 각각 다른 방식으로 사용하는데 완벽한 서비스 구성을 위해 모두 필요하다. 요소는 경우에 따라 통합이 가능하기도 하다.
서비스 프로그램
실제로 서비스 코드를 제공하는 프로그램, 그냥 서비스를 칭하며 프로그램을 말한다. 백그라운드에서 사용자의 요구 또는 시스템의 변화를 감지하여 실제 작업을 처리하는 서비스 자체라고 있다.
서비스 설정 프로그램
SCM 설치된 서비스의 목록을 레지스트리에 DB형태로 저장하며 관리한다. 만들어진 서비스를 시스템에 설치하려면 DB 접속하여 서비스를 등록하는 프로그램을 직접 만들어야 하는데 서비스 설정 프로그램(Configuration Program) 바로 서비스 설치와 제거에 대한 작업을 수행하는 프로그램이다. 또한 설정 프로그램은 서비스 유형, 시작 유형, 설명 설치된 서비스의 여러 가지 환경 설정을 조사하거나 수정할 있는 기능도 제공한다.
서비스 제어 프로그램
서비스는 백그라운드에서 사용자와 상호작용 없이 동작하지만 사용자가 서비스를 제어할 있는 방법을 제공해야 필요가 있다. 예를 들어 바이러스 체크 서비스의 경우 체크 방법을 변경한다거나 지금 당장 모든 시스템을 샅샅이 뒤져 바이러스가 있는지 조사하는 명령을 내릴 수도 있어야 한다.
그래서 사용자로부터 명령을 받아 서비스에게 명령을 전달하는 프로그램이 필요한데 프로그램을 서비스 제어 프로그램이라고 한다.
. 서비스 DB
서비스에 대한 설정 정보들은 레지스트리에 DB형태로 저장되어 있다. 레지스트리 편집기를 열어 서비스가 어떤 정보들을 담고 있는지 살펴보면서 서비스의 속성들에 대해 미리 알아보자.
서비스 명칭
서비스도 서로간에 구분을 위해 이름을 가져야 하는데 이를 서비스 이름 또는 명칭이라고 한다. 서비스 명칭은 상호간의 구분은 물론이고 레지스트리에 서비스의 서브키를 구성하는데도 사용된다. 명칭이므로 중간에 공백을 가질 없으며 다른 서비스와 중복되어서도 안된다. OpenServiice 등의 API 함수에서 대상 서비스를 지칭할 때는 명칭이 사용된다.
표시명
명칭은 서비스간의 구분을 위해서 사용되는 이름이며 표시명은 사용자에게 서비스의 기능을 간략하게 보여주기 위한 설명적인 문자열이다. DisplayName이라는 값으로 기억된다. 단순한 문자열이므로 256자까지의 길이를 가질 있으며 중간에 공백을 포함할 수도 있다.
설명
설명은 표시명보다 훨씬 자세하게 서비스에 대한 설명을 있는 문자열이며 레지스트리에는 Description이라는 값으로 기억된다. 기능은 없으며 단지 사용자에게 서비스에 대한 요약적인 정보를 보여주기만 한다. 최대 1024길이로 작성할 있다.
서비스 유형
레지스트리에 Type값으로 기억되는 정보이며 서비스의 유형을 지정한다. 서비스의 유형에는 다음과 같은 것들이 있다.

유형
설명
SERVICE_WIN32_OWN_PROCESS
실행 파일 하나에 서비스 하나인 형태
SERVICE_WIN32_SHARE_PROCESS
실행 파일 하나에 여러 개의 서비스가 있는 형태
SERVICE_KERNEL_DRIVER
드라이버 서비스
SERVICE_FILE_SYSTEM_DRIVER
파일 시스템 드라이버 서비스
SERVICE_INTERACTIVE_PROCESS
위쪽 유형과 함께 사용되며 데스크탑과 상호작용을 있는 서비스이다.

시작 유형
서비스를 언제 시작할 것인가를 지정하는 유형이며 레지스트리에 Start값으로 기억된다.

유형
설명
SERVICE_BOOT_START
시스템 시작시에 같이 시작되며 드라이버 서비스에만 적용된다.
SERVICE_SYSTEM_START
loInitSystem 함수에 의해 시작되며 드라이버 서비스에만 적용된다.
SERVICE_AUTO_START
시스템이 시작될 SCM 의해 자동으로 시작된다.
SERVICE_DEMAND_START
사용자의 요구가 있을 StartService함수로 시작된다.
SERVIICE_DISABLED
사용중지된 서비스이다. 상태의 서비스는 수동으로 시작할 없다.

위쪽 시작 유형은 드라이버 서비스에만 적용되며 Win32 서비스가 가질 있는 시작 유형은 자동(AUTO), 수동(DEMAND) 가지가 있다. 자동은 시스템 부팅시에 같이 시작되므로 일단 설치만 놓으면 부팅할 때마다 실행된다. 사용자의 의사와 상관없이 실행되는, 또한 선택의 여지없이 실행되어야만 하는 서비스들이 자동 시작 유형으로 작성된다.
수동 시작 유형은 사용자의 명시적인 요구가 있을 때만 시작되는데 항상 실행될 필요가 없는 서비스는 수동으로 작성해 놓고 필요할 때만 실행하는 것이 시스템 자원 절약에 유리하다. 디폴트 서비스 중에는 내게 필요한 옵션 서비스가 수동으로 되어 있는데 기능은 사용자가 제어판에서 사용하겠다는 명령을 내렸을 때만 시작하면 부팅할 때마다 시작할 필요는 없다.
에러 제어 수준
레지스트리의 ErrorControl값으로 기억되어 있는 에러 제어 수준(Error Control Level) 서비스 시동시에 에러가 발생한 경우의 처리를 지정한다.

설명
SERVICE_ERROR_IGNORE
에러 로그만 남기고 부팅을 계속 진행한다.
SERVICE_ERROR_NORMAL
에러 로그를 남기고 메시지 박스를 띄워 사용자에게 에러가 발생했음을 알린다. 부팅은 계속 진행한다.
SERVICE_ERROR_SERVERE
에러 발생하기 전의 서비스 구성(LKG) 읽어 재부팅한다. LKG 재부팅중에 에러가 발생하면 에러 로그를 남긴 부팅을 계속 진행한다.
SERVICE_ERROR_CRITICAL
에러가 발생하기 전의 서비스 구성(LKG) 읽어 재부팅한다. LKG 재부팅중에 에러가 발생하면 부팅은 실패한다.

서비스도 사람이 만든 프로그램이므로 당연히 에러가 발생할 있다. 또한 하드웨어를 제어하는 드라이버 서비스는 하드웨어의 고장이나 변화에 의해 에러가 발생하기도 한다. 서비스는 일반 프로그램과는 달리 부팅시에 시작되므로 에러 발생시의 대처 방법도 속성으로 미리 정의해 놓고 에러 제어 수준에 따라 부팅 계속 여부를 판단해야 한다.
LKG(Last Known Good) 수비게 말해 서비스 DB 백업본으로서 최후로 부팅에 성공한 서비스 구성표라고 있다. 시스템은 부팅할 때마다 백업본을 다음 레지스트리 위치에 작성해 놓는다.
HKEY_LOCAL_MACHINE\SYSTEM\ControlXXX\Services
XXX 백업본의 일련번호라고 있으며 다른 레지스트리(System\Select\LastKnownGood) 저장되어 있다. 서비스르 ㄹ잘못 다루게 되면 최악의 경우 시스템을 다시 설치해야 되는 경우도 있기 때문에 사용자의 실수나 잘못된 서비스로부터 시스템을 보호하기 위해 운영체제는 부팅할 때마다 서비스 구성표의 백업을 만들어 놓는다. Servere, Critical 에러 제어 수준은 에러 발생시 백업본을 읽어 부팅을 다시 시도한다.
실행 파일 경로
레지스트리에 ImagePath 기억되며 서비스 코드를 가지는 실행 파일 경로이다. ImagePath 서비스 프로그램이 설치되어 있는 완전 경로를 가지고 있으며 운영체제는 경로에서 서비스 프로세스를 실행한다.
종속성
서비스끼리는 종속성을 가지고 있어 하나의 서비스가 실행되기 위해서는 다른 서비스가 실행중이어야 하는 경우가 있다. 종속성 정보도 레지스트리에 저장되어 있다. SCM 서비스 시작시에 해당 서비스가 종속되어 있는 서비스가 아직 실행되지 않았다면 서비스를 먼저 실행한 해당 서비스를 실행시킨다.
계정
서비스의 계정 정보는 서비스가 동작할 계정을 지정하며 계정에 따라 서비스가 있는 작업의 범위가 달라진다. 계정을 지정하지 않으면 LocalSystem이라는 특수한 계정으로 실행되는데 계정은 서비스와 같은 시스템 프로세스를 위해 미리 정의되어 있는 계정이며 패스워드는 가지지 않는다.
02 서비스 프로그램
. MemStat 서비스
. 세가지 요소

#include <windows.h>
 
#define SERVICE_CONTROL_NEWFILE 128
 
void MyServiceMain(DWORD argc, LPTSTR* argv);
void MyServiceHandler(DWORD opCode);
 
SERVICE_STATUS_HANDLE g_hSrv;
DWORD g_NowState;
BOOL g_bPause;
HANDLE g_ExitEvent;
TCHAR gbuf[65536] = "메모리 통계 파일\r\n";
 
int main()
{
SERVICE_TABLE_ENTRY ste[] = { { "MemStat", (SERVICE_MAIN_FUNCTION)MyServiiceMain }, {NULL, NULL} };
 
StartServiceCtrlDispatcher(ste);
 
return 0;
}
 
void MySetStatus(DWORD dwState, DWORD dwAccept=SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE)
{
SERVICE_STATUS ss;
ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
ss.dwCurrentState = dwState;
ss.dwControlsAccepted = dwAccept;
ss.dwWin32ExitCode = 0;
ss.dwServiceSpecificExitCode = 0;
ss.dwCheckPoint = 0;
ss.dwWaitHint = 0;
 
// 현재 상태를 보관해 둔다.
g_NowState = dwState;
SetServiceStatus(g_hSrv, &ss);
}
 
void MyServiceMain(DWORD argc, LPTSTR* argv)
{
HANDLE hFile;
MEMORYSTATUS ms;
DWORD dwWritten;
TCHAR str[256];
SYSTEMTIME st;
 
// 서비스 핸들러를 등록한다.
g_hSrv = RegisterServiceCtrlHandler("MemStat", (LPHANDLER_FUNCTION)MyServiceHandler);
if (g_hSrv==0) {
return;
}
 
// 서비스가 시작중임을 알린다.
MySetStatus(SERVICE_START_PENDING);
 
// 전역 변수를 초기화한다.
g_bPause = FALSE;
 
// 이벤트를 생성한다.
g_ExitEvent = CreateEvent(NULL, TRUE, FALSE, "MemStatExit");
 
// 새로운 로그 파일을 작성한다.
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
CloseHandle(hFile);
 
// 서비스가 시작되었음을 알린다.
MySetStatus(SERVICE_RUNNING);
 
// 10초에 한번씩 메모리 통계를 작성한다.
while (true) {
if (g_bPause == FALSE) {
// 현재 시간과 메모리 양을 조사하여 문자열로 조립한다.
GetLocalTime(&st);
GlobalMemoryStatus(&ms);
wsprintf(str, "%d %d %02d %02d %02d =>"
"사용 가능 물리 메모리=%dMB(%d%%), 사용가능 가상메모리=%dMB, "
"사용가능 페이징 파일=%dMB\r\n",
st.wMonth, st.wDay, st.wHour, st.wMilliseconds, st.wSecond,
ms.dwAvailPhys / 1048576, 100 - ms.dwMemoryLoad, ms.dwAvailVirtual / 1048576,
ms.dwAvailPageFile / 1048576);
 
// 파일로 통계 출력
if (60000 < lstrlen(gbuf)) {
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
lstrcpy(gbuf, "메모리 통계파일\r\n");
}
else {
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
}
 
            lstrcat(gbuf, str);
            WriteFile(hFile, gbuf, lstrlen(gbuf), &dwWritten, NULL);
            CloseHandle(hFile);
            }
 
           if (WaitForSingleObject(g_ExitEvent, 10000) == WAIT_OBJECT_0)
        break;
}
 
MySetStatus(SERVICE_STOPPED);
}
 
// 핸들러 함수
void MyServiceHandle(DWORD dwControl)
{
HANDLE hFile;
 
// 현재 상태롸 같은 제어 코드일 경우느 처리할 필요 없다.
if (dwControl == g_NowState)
return;
 
switch (dwControl)
{
case SERVICE_CONTROL_PAUSE:
MySetStatus(SERVICE_PAUSE_PENDING, 0);
g_bPause = TRUE;
MySetStatus(SERVICE_PAUSED);
break;
case SERVICE_CONTROL_CONTINUE:
MySetStatus(SERVICE_CONTINUE_PENDING, 0);
g_bPause = FALSE;
MySetStatus(SERVICE_RUNNING);
break;
case SERVICE_CONTROL_STOP:
MySetStatus(SERVICE_STOP_PENDING, 0);
SetEvent(g_ExitEvent);
break;
case SERVICE_CONTROL_NEWFILE:
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
lstrcpy(gbuf, "메모리 통계 파일\r\n");
CloseHandle(hFile);
case SERVICE_CONTROL_INTERROGATE:
default:
MySetStatus(g_NowState);
break;
}
}

서비스 프로그램은 main함수와 서비스 메인, 핸들러 함수 가지로 구성되어 있는데 서비스 프로그램은 세가지 요소를 반드시 구성해야 한다. 요소들은 서비스가 SCM 상호작용하기 위해 반드시 필요하며 서비스 프로그램을 만들기 위한 일종의 의무사항이다.
main함수
서비스 프로그램은 사용자와 상호 작용이 필요없기 때문에 보토 ㅇ콘솔 프로세스로 작성되며 그래서 프로그램의 시작점이 main이다.  GUI 형태의 서비스를 작성한다면 Winmain 가능하기는 하지만 일반적이지 않다. main 함수가 특별히 어떤 의미를 가진다기 보다는 일반적인 c프로그램의 시작점일 뿐이며 서비스 프로세스도 당연히 main으로부터 시작된다.
main 함수가 해야 가장 중요한 일은 디스패처 스레드를 실행하는 것이다. 디스패처(Service Control Dispatcher) 서비스를 시작하고 핸들러에게 제어 코드를 전달하는 일을 하는 독립된 스레드이다.
서비스 메인
서비스 메인은 실베 서비스 작업을 하는 본체라고 있다. 서비스 메인은 main함수에서 등록되어 서비스를 실행시킬 디스패처에 의해 호출되며 서비스 운영에 관한 여러가지 일들을 한다. 핸들러를 등록하고 서비스를 기동하며 자신의 상태 변화를 SCM에게 알린다. 함수의 원형은 다음과 같이 정의되어 있다.

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

콘솔 main함수와 비슷한 원형을 가지며 쓰이지는 않지만 명령행 인수를 받아들일 있다.
핸들러
핸들러 함수는 서비스의 제어 신호를 처리하는 함수로서 서비스의 메시지 루프라고 보아도 좋다. 핸들러는 서비스 메인에서 디스패처에 등록된다. 서비스는 사용자로부터 직접 명령을 받지는 않지만 서비스 제어 프로그램이나 서비스 애플릿 등으로부터 명령을 받아 처리한다. SCM 명령을 받아 디스패처로 전달하며 디스패처는 등록된 핸들러에게 제어 신호를 보낸다. 함수의 원형은 다음과 같이 정의되어 있다.

VOID WINAPI Handler(DWORD fdwControl);

인수로 서비스가 해야 작업 내용을 담고 있는 제어 신호값을 받아들인다. 함수 이름은 물론 마음대로 정할 있다.
. 디스패처
서비스 프로그램이 실행되면 서비스 프로세스의 main 함수가 제일 먼저 실행된다. 이때 main 함수에서 해야 가장 중요한 일은 프로세스와 프로세스에 속한 서비스들이 SCM 통신하기 위한 장치를 마련하는 것이다. 실행중인 서비스들은 SCM으로부터 끊임없이 제어 신호를 받아들여야 하고 자신의 상태를 SCM에게 복해야 하기 때문이다. 디스패처(Service Control Dispatcher) SCM 서비스 프로세스와의 통신을 담당하는 스레드이다.
새로운 서비스를 시작하라는 명령을 받으면 디스패처는 서비스를 위한 스레드를 생성하고 서비스 메인 함수를 스레드에서 실행한다. 서비스를 잠시 중지하거나 재개하라는 명령을 받으면 서비스의 핸들러에게 신호를 전달하여 서비스가 SCM으로부터, 또는 사용자로부터의 제어 신호를 처리하도록 한다. 디스패처는 서비스가 실행중인 동안 무한루프를 돌며 서비스 시작과 제어 신호 전달을 수행한다.
그러기 위해서 디스패처는 프로세스가 어떠한 서비스들을 가지고 있으며 서비스들의 서비스 메인 함수의 시작번지(Entry Point) 알고 있어야 한다. 정보들은 main함수가 디스패처를 시작할 전달해야 하는데 이때 다음 함수가 사용된다.

BOOL StartServiceCtrlDispatcher(CONST LPSERVICE_TABLE_ENTRY lpServiceTable);
 
typedef struct _SERVICE_TABLE_ENTRY {
    LPSTR lpServicieName;
    LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;

lpServiceName 서비스의 명칭이며 lpServiceProc 서비스 메인 함수의 시작 번지이다. 구조체 배열이므로 복수개의 서비스에 대한 정보를 전달할 있다. 배열의 끝을 표시하기 위해 마지막 요소는 NULL, NULL값으로 되어야한다.
int main()
{
SERVICE_TABLE_ENTRY ste[] = { { "MemStat", (SERVICE_MAIN_FUNCTION)MyServiiceMain }, {NULL, NULL} };
 
StartServiceCtrlDispatcher(ste);
 
return 0;
}
SCM으로부터 서비스 시작 명령과 제어 신호를 받아 서비스를 시작하고 제어 신호를 전달할 준비를 마친것이다. 사용자가 제어판의 서비스 애플릿으로, 또는 전용 서비스 제어 프로그램으로 서비스 시작 명령을 내렸을 SCM 서비스 프로세스는 어떤 과정으로 서비스를 시작시키고 동작하는지를 연구해보자.
  1. SCM 해당 서비스를 가지고 있는 프로세스의 경로를 레지스트리에서 찾는다. 만약 프로세스가 실행중이 아니라면 프로세스를 실행한다. 프로세스가 이미 실행중이면, 프로세스에 속한 다른 서비스가 이미 실행중이라면 디스패처가 이미 생성되어 있으므로 프로세스 실행 과정과 디스패처 생성 과정은 생략된다.
  2. 서비스 프로세스의 main 함수에서 디스패처 스레드를 생성하며 이때 main 디스패처에게 자신이 가지고 있는 서비스의 목록과 서비스 메인 함수의 시작 번지를 전달한다. 디스패처는 프로세스에 속한 서비스의 정보를 가지고 SCM 통신을 시작한다.
  3. 해당 서비스를 시작하기 위해 디스패처는 서비스를 위한 별도의 스레드를 생성하고 서비스 메인 함수를 호출하여 서비스를 시작한다. 디스패처는 서비스 하나당 별도의 스레드를 생성해 준다. 서비스 메인에서는 서비스를 위한 초기화와 핸들러 등록을 한다.
  4. 디스패처에 의해 서비스가 시작되면 서비스는 실행중에 자신의 상태를 SCM에게 보고한다. 그리고 SCM으로부터의 제어 신호가 들어오면 디스패처가 이를 서비스의 핸들러에게 전달하며 핸들러가 제어 신호를 처리한다. 여기까지 진행되면 서비스가 안정적으로 실행되고 있는 것이다.
  5. 서비스가 종료되면 디스패처는 실행중인 서비스 카운터를 1 감소시키고 만약 카운터가 0이면 디스패처도 종료되고 main 함수로 리턴한다. 디스패처는 자신이 생성한 모든 서비스가 종료될 때까지 계속 무한 루프에서 대기하며 모든 서비스가 종료될 비로소 리턴한다. main 함수는 마지막 뒷정리를 하고 프로세스를 종료한다.
만약 프로세스 하나당 서비스가 무조건 하나뿐이라면 디스패처가 불필요할 것이다. 프로세스를 실행하는 것이 서비스를 실행하는 것이 되며 main 함수가 서비스의 메인이 있기 때문이다. 그러나 프로세스당 복수개의 서비스를 가질 있기 때문에 서비스 목록을 유지하고 관리하는 디스패처라는 것이 필요해졌다.
. 서비스 메인
서비스 메인은 서비스의 시작함수로서 실제 작업을 담당한다. MemStat 서비스의 경우 10초마다 번씩 메모리 상태를 조사하여 로그 파일에 기록하는 작업을 서비스 메인에서 해야 것이다. 프로세스에 여러 개의 서비스가 있다면 수만큼 서비스 메인 함수를 만들고 디스패처에게 전달하면 된다. 서비스 메인은 디스패처에 의해 새로운 스레드 위에서 호출된다. 서비스 메인은 메인 프로세스와는 완전히 분리된 스레드에서 실행되며 서비스 프로세스는 기본적으로 멀티스레드 구조를 가진다.
서비스 프로세스는 최소한 서비스 개수 + 1개의 스레드를 가지게 되는 셈이다.
서비스 메인은 다음과 같은 일들을 수행한다.
핸들러 등록
서비스 메인이 제일 먼저 해야 일은 서비스의 제어 신호를 처리하기 위한 핸들러를 등록하는 것이다. 서비스 메인은 작업 스레드(Worker Thread)이며 메시지 루프를 가지지 않기 때문에 SCM으로부터 신호를 받을 있는 장치를 별도로 마련해야 하는데 그것이 바로 핸들러이다. 작업은 다른 어떤 것보다 우선적으로 처리해야 한다. 핸들러를 등록할 때는 RegisterServiceCtrlHandler라는 함수를 사용한다. 핸들러 함수를 등록함으로써 서비스는 SCM으로부터 전달되는 제어신호를 받아 처리할 있게 된다.
핸들러 등록 함수는 핸들러를 등록한 SERVICE_STATUS_HANDLE 형의 서비스 상태 핸들을 리턴하는데 핸들은 이후 서비스가 자신의 상태를 SCM에게 보고할 사용한다. 핸들은 서비스 루틴에서 계속적으로 사용되므로 전역변수에 별도로 보관해 놓는 것이 좋으며 서비스가 종료될 SCM 닫으므로 명시적으로 핸들을 닫을 필요는 없다.
상태 보고
서비스는 실해애중에 자신의 상태 변화가 있을 때마다 SCM에게 보고해야 한다. 자신의 상태를 정확하게 보고해야 SCM 서비스가 어떤 상태에 있고 무슨 일을 하고 있으며 어떤 제어 신호를 받을 있는질르 파악할 있다. 초기화 중일 , 초기화 완료 , 실행 , 잠시 중지 등을 정확하게 보고해야 SCM 서비스의 현재 상태를 있다. 상태를 보고할 때는 다음 함수를 사용한다.
BOOL SetServiceStatus(SERVICE_STATUS_HANDLE hServiceStatus, LPSERVICE_STATUS lpServiceStatus);
첫번째 인수 hServiceStatus 보고 주체인 서비스의 상태 핸들인데 핸들은 핸들러를 등록할 리턴되는 값이다. 핸들은 수시로 사용되므로 별도의 전역변수에 보관해 두어야 한다. 번째 인수 lpServiceStatus 서비스의 현재 상태를 나타내는 구조체의 포인터이다. 구조체는 다음과 같이 선언되어 있다.

typedef struct _SERVICE_STATUS {
    DWORD dwServiceType;
    DWORD dwCurrentState;
    DWORD dwControlsAccepted;
    DWORD dwWin32ExitCode;
    DWORD dwServiceSpecificExitCode;
    DWORD dwCheckPoint;
    DWORD dwWaitHint;
} SERVICIE_STATUS, *LPSESRVICE_STATUS;

dwServiceType 서비스의 유형을 알려주는데 대개의 경우 프로세스에 하나의 서비스만 있으므로 SERVICE_WIN32_OWN_PROCESS 것이다.
dwCurrentState 서비스의 현재 상태를 알려주는 멤버인데 다음 일곱가지의 상태 하나가 된다.

상태
설명
SERVICE_STOPPED
서비스가 중지되어 있다.
SERVICE_START_PENDING
시작중이다.
SERVICE_STOP_PENDING
중지중이다.
SERVICE_RUNNING
실행 상태이다.
SERVICE_CONTINUE_PENDING
재개중이다.
SERVICE_PAUSE_PENDING
일시 중지중이다.
SERVICE_PAUSED
일시 중지되어 있다.

서비스는 중지 상태, 실행상태, 일시 중지 상태 가지 상태 하나를 가지는데 상태로 변환중인 (PENDING) 하나의 상태로 취급된다. 그런가 하면 상태 변환 과정이 무척 경우도 있기 때문인데 이때도 SCM에게 서비스가 무엇을 하고 있다는 것을 정확하게 보고해야 하기 때문이다.
dwControlsAccepted 서비스의 핸들러가 받아들일 있고 처리할 있는 제어 신호의 종류를 설정한다. 모든 서비스는 현재 서비스 상태를 질문하는 SERVICE_CONTROL_INTERROGATE 신호는 의무적으로 받아들여야 하며 나머지 신호는 선택적으로 받아들일 있다.

신호
설명
SERVICE_ACCEPT_STOP
SERVICE_CONTROL_STOP신호를 받는다. 서비스를 중지시킬 있다.
SERVICE_ACCEPT_PAUSE_CONTINUE
일시 중지하거나 재개할 있다. PAUSE CONTINUE 신호를 받는다.
SERVICE_ACCEPT_SHUTDOWN
시스템 셧다운 통지를 받아들인다.
SERVICE_ACCEPT_PARAMACHGNE
서비스를 중지하지 않고 파라미터를 다시 읽어들일 있다.
SERVICE_ACCEPT_NETBINDCHANGE
서비스를 중지하지 않고 네트워크 바인딩 변경 통지를 받을 있다.
SERVICE_ACCEPT_HARDWAREPROFILECHANGE
하드웨어 프로필 변경 통지를 받을 있다.
SERVICE_ACCEPT_POWEREVENT
컴퓨터의 전원 상태 변경을 통지받을 있다.

아래쪽 4 신호는 윈도우즈 200이후 추가된 것들이다.
어떤 서비스는 중지하거나 일시중지할 있는 반면 어떤 서비스는 시작하면 절대로 중지해서는 안되는 것들도 있고 중지하는 것이 의미가 없는 서비스도 있다. 예를 들어 이벤트 로그 서비스는 운영체제의 일부분이므로 중지할 없으며 부팅과 함께 시작되어 항상 실행중이어야 한다. 그래서 서비스 애플릿으로 이벤트 로그 서비스의 등록정보를 보면 중지, 일시 중지 버튼들이 모두 희미하게 사용금지 되어있다.
서비스가 어떤 제어 신호를 받아들일 것인가는 서비스별로 다르지만 같은 서비스라도 상황에 따라 달라질 있다. 예를 들어 중지 신호를 받아들일 있는 서비스라도 시작하는 (START_PENDING) 중지 신호를 받아들이는 것은 부자연스럽다. 그래서 받아들일 신호의 종류를 설치시에 지정하지 않고 상태 변경 지정하며 언제든지 변경할 있다.
dwWin32ExitCode, dwServiceSpecificExitCode 서비스 실행중에 에러 발생시 어떤 에러가 발생했는지 알리는 멤버이다. win32에러가 발생했을 경우 dwWin32ExitCode 에러 코드를 리턴해야 한다. 서비스 고유의 에러 코드를 리턴할 수도 있는데 이때는 dwWin32ExitCode ERROR_SERVICE_SPECIFIC_ERROR 대입하고 dwServiceSpecificExitCode 멤버에 고유 에러 코드를 리턴한다. 에러 상황이 아니면 NO_ERROR 설정하면 된다.
dwWaitHint 서비스가 시간이 소요되는 초기화, 중지 작업을 SCM 얼마나 기다려야 하는지 대기 시간을 알리기 위해 사용한다. 1/1000 단위로 시간을 지정하며 시간이 지나도 서비스가 별도의 다른 보고를 하지 않으면 SCM 서비스에서 에러가 발생한 것으로 간주한다.
dwCheckPoint 긴작업을 사용되는재 현재 어디까지 작업이 진행되었는지를 알리며 서비스는 값을 계속 증가시키면서 주기적으로 보고 해야 한다. 서비스 제어 프로그램이나 서비스 애플릿은 dwCheckPoint값으로 프로그래스 바를 갱신하는 등의 처리를 함으로써 작업이 어느정도 진행되는지를 사용자에게 보여준다.
dwWaitHint dwCheckPoint멤버는 시간이 걸리는 작업이 진행중일 SCM에게 작업 진행 상태를 보고하기 위해 사용하는데 외의 경우 PENDING 상태가 아닌 경우는 사용되지 않으며 반드시 0으로 설정되어야 한다.
초기화
서비스 메인은 서비스 시작 직후 서비스 동작을 위한 초기화를 한다.
프로그램에서는 일시 중지 상태를 위해 g_bPause 전역변수를 사용하며 서비스 중지를 위해 g_ExitEvent 이벤트를 사용하고 있다. 전역변수에 값을 대입하는 것과 이벤트를 생성하는 것도 초기화의 일부분이다.

// 서비스가 시작중임을 알린다.
MySetStatus(SERVICE_START_PENDING);
 
// 전역 변수를 초기화한다.
g_bPause = FALSE;
 
// 이벤트를 생성한다.
g_ExitEvent = CreateEvent(NULL, TRUE, FALSE, "MemStatExit");
 
// 새로운 로그 파일을 작성한다.
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
CloseHandle(hFile);
 
// 서비스가 시작되었음을 알린다.
MySetStatus(SERVICE_RUNNING);

초기화를 하기 전에 서비스의 상태를 SERVICE_START_PENDING으로 바꾸어 초기화가 진행중임을 SCM에게 보고하고 초기화가 완료된 후에 SERVICE_RUNNING 상태로 다시 바꾼다.
초기화 과정 동안 계속 SERVIICE_START_PENDING상태를 반복해서 전달하되 dwWaitHint dwCheckPoint값을 적절히 설정해서 SCM 얼마나 기다려야 하는지 어디까지 초기화가 되었는지를 보고하는 것이 좋다. SCM에게 상태를 보고하는 일은 가급적 자주, 즉시 해야 한다.
서비스 실행
서비스 메인은 서비스 초기화를 실제 서비스 코드를 실행한다. 예제에서 실제 서비스 코드는 for(;;) 루틴 안에 작성되어 있는데 10초에 번씩 현재 시간과 메모리 상태를 조사해 파일로 출력하는 일을 하고 있다.

// 서비스가 시작되었음을 알린다.
MySetStatus(SERVICE_RUNNING);
 
// 10초에 한번씩 메모리 통계를 작성한다.
while (true) {
if (g_bPause == FALSE) {
// 현재 시간과 메모리 양을 조사하여 문자열로 조립한다.
GetLocalTime(&st);
GlobalMemoryStatus(&ms);
wsprintf(str, "%d %d %02d %02d %02d =>"
"사용 가능 물리 메모리=%dMB(%d%%), 사용가능 가상메모리=%dMB, "
"사용가능 페이징 파일=%dMB\r\n",
st.wMonth, st.wDay, st.wHour, st.wMilliseconds, st.wSecond,
ms.dwAvailPhys / 1048576, 100 - ms.dwMemoryLoad, ms.dwAvailVirtual / 1048576,
ms.dwAvailPageFile / 1048576);
 
// 파일로 통계 출력
if (60000 < lstrlen(gbuf)) {
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
lstrcpy(gbuf, "메모리 통계파일\r\n");
}
else {
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
}
 
            lstrcat(gbuf, str);
            WriteFile(hFile, gbuf, lstrlen(gbuf), &dwWritten, NULL);
            CloseHandle(hFile);
            }
 
           if (WaitForSingleObject(g_ExitEvent, 10000) == WAIT_OBJECT_0)
        break;
}

서비스 실행 코드 중간에 g_bPause 전역변수를 점검하여 일시 중지를 처리하고 있으며 g_ExitEvent 신호상태를 점검하여 대기하는 코드가 있다. g_bPause FALSE 때만 로그를 작성하여 값이 TRUE 도미ㅕㄴ 로그 작성 루틴을 무시하고 루프만 계속 돈다. 로그 작성 g_ExitEvent 기다리며 10초간 대기하면서 시간을 끈다. 대기중에 이벤트가 신호상태가 되면 즉시 루프를 탈출하여 서비스를 중지시킨다. 서비스를 중지시킨 SCM에게 SERVICE_STOPPED 상태를 보고한다.
서비스 메인의 인수
서비스 메인 함수는 인수를 가지고 있는데 인수는 서비스 시작 함수인 StartServicie 함수에서 전달된다. 서비스 애플릿으로 서비스를 시작할 때도 시작 매개 변수를 지정하는 에디트가 제공된다. 프로그램의 인수와 마찬가지로 서비스의 시작 옵션을 제공하고자 서비스 메인의 인수를 활용할 있다.
그러나 서비스 메인의 인수는 거의 사용되지 않는다.
. 핸들러
핸들러 등록
핸들러는 SCM으로부터 전달되는 제어 신호를 처리하는 함수이다. 서비스느 ㄴ실행중에 SCM 끊임없이 통신을 수행하는데 SCM과의 통신 방법으로 핸들러를 제공해야 한다. 서비스 메인에서 핸들러를 등록하면 시스템은 핸들러의 번지를 보관하며 SCM으로부터의 제어 신호를 핸들러에게 전달한다. 핸들러를 등록하는 함수는 다음 가지가 있다.

SERVICIE_STATUS_HANDLE RegisterServiceCtrlHandler(LPCTSTR lpServiceName, LPHANDLER_FUNCTION lpHandlerProc);
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx(LPCTSTR lpServiceName, LPHANDLER_FUNCTION_EX lpHandlerProc, LPVOID lpContext);

함수들이 등록하는 핸들러 함수는 각각 다음과 같은 원형을 가진다. 함수 이름은 마음대로 지정하면 된다.

VOID WINAPI Handler(DWORD fdwControl);
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);

Handler함수는 정수형의 제어 신호값 하나만을 인수로 받아들이는데 비해 HandlerEx함수는 많은 인수를 가진다. HandlerEx 함수는 Handler 함수가 처리할 있는 제어 신호보다 많은 복잡한 제어 신호를 받을 있는 장점이 있지만 2000이전 버전과는 호환되지 않는다. 서비스 실행중에 전원 변경이나 네트워크 구성 변경에 대해서까지 반응하지 않는다면 굳이 HandlerEx함수를 사용할 필요는 없다.
제어 신호
핸들러 함수가 받을 있는 제어 신호에는 다음과 같은 것들이 있다. 제어 신호는 단순한 1바이트 정수값이다.

신호
설명
SERVICE_CONTROL_STOP
서비스를 중지하라
SERVICE_CONTROL_PAUSE
서비스를 일시 중지해라
SERVICE_CONTROL_CONTINUE
일시중지된 서비스를 재개하라
SERVICE_CONTROL_INTERROGATE
서비스의 현재 상태를 알려달라
SERVICE_CONTROL_SHUTDOWN
시스템이 셧다운되므로 종료처리를 하라
SERVICE_CONTROL_PARAMCHANGE
서비스 시작 파라미터가 변경되었다.
SERVICE_CONTROL_NETBINDADD
바인딩이 추가되었다.
SERVICE_CONTROL_NETBINDREMOVE
바인딩이 제거되었다.
SERVICE_CONTROL_NETBINDENABLE
바인딩이 사용가능하다.
SERVICE_CONTROL_NETBINDDISIABLE
바인딩이 사용 불가능하다.

제어 신호들은 시스템에 의해 미리 정의된 것들이며 0x128~0x255까지의 제어신호는 사용자가 정의해서 사용할 수도 있다.
PARAMCHANGE 이후는 윈도우즈2000에서 추가된 제어 신호들이며 HandlerEx 외에도 가지 제어 신호들을 처리할 있다.

// 핸들러 함수
void MyServiceHandle(DWORD dwControl)
{
HANDLE hFile;
 
// 현재 상태롸 같은 제어 코드일 경우느 처리할 필요 없다.
if (dwControl == g_NowState)
return;
 
switch (dwControl)
{
case SERVICE_CONTROL_PAUSE:
MySetStatus(SERVICE_PAUSE_PENDING, 0);
g_bPause = TRUE;
MySetStatus(SERVICE_PAUSED);
break;
case SERVICE_CONTROL_CONTINUE:
MySetStatus(SERVICE_CONTINUE_PENDING, 0);
g_bPause = FALSE;
MySetStatus(SERVICE_RUNNING);
break;
case SERVICE_CONTROL_STOP:
MySetStatus(SERVICE_STOP_PENDING, 0);
SetEvent(g_ExitEvent);
break;
case SERVICE_CONTROL_NEWFILE:
hFile = CreateFile("c:\\MemStat.txt", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
lstrcpy(gbuf, "메모리 통계 파일\r\n");
CloseHandle(hFile);
case SERVICE_CONTROL_INTERROGATE:
default:
MySetStatus(g_NowState);
break;
}
}

서비스 메인과 통신
특정 서비스를 어떻게 중지하고 재개하는지는 서비스 본체 코드를 가지고 있는 서비스 메인만이 알고 있다.
그래서 핸들러는 신호를 받는 즉시 서비스 메인이 신호를 처리할 있어야 하며 어떤 형태로든 서비스 메인에게 알려야 한다. 핸들러와 서비스 메인이 상호 통신할 있는 방법으로 여러 가지를 생각할 있다. 서비스 메인에게 어떤 신호를 보낸다거나 뮤텍스, 이벤트같은 동기화 오브젝트를 사용하는 것도 괜찮은 방법이지만 간단하고 단순한 방법은 전역변수를 사용하는 것이다. 약속된 전역변수에 일정한 의미를 부여하고 서비스 메인은 실행중에 주기적으로 전역변수의 상태를 관찰하여 자신의 동작을 결정한다. 핸들러는 신호가 왔을 전역변수를 변경함으로써 서비스 메인의 동작에 영향을 주게 된다.
전역변수는 구조화 프로그래밍 방법에서 일반적으로 금기시되는 요소이기는 하지만 이런 경우는 복잡한 동기화 오브젝트보다는 오히려 사용하기 편하고 직관적이다. 어떤 방법을 사용하든지 서비스 메인이 서비스를 중지하건, 일시중지 있도록 통신할 수만 있으면 된다.
서비스 일시 중지
핸들러는 잠시 중지하라는 SERVICE_CONTROL_PAUSE 제어 신호를 받으면 g_bPause TRUE 바꿈으로써 서비스 메인이 로그 파일 작성을 잠시 그만두게 한다. 일시 중지는 서비스를 종료하는 것과는 다르므로 g_bPause TRUE 되더라도 서비스는 비록 루프를 돌고 있더라도 계속 실행되며 언제든지 g_bPause FALSE 되면 다시 로그를 작성할 있다.
이런 작어블 때도 서비스의 상태가 변경된다는 것을 SCM에게 일일이 보고해야 한다. 서비스가 이리시 중지될 중지중이라는 것을 먼저 알리고 g_bPause TRUE 바꾼 서비스가 일시 중지 되었다는 것을 알렸다. 전역변수의 값을 바꾸는 것은 지극히 짧은 시간에 수행될 있어 PENDING상태 설정은 생략할 수도 있겠지만 이런 작업이 약간의 시간을 소모한다면 진행중 상태도 보고하는 것이 바람직하다.
일시 중지중에 재개 명령을 보내거나 재개 진행중에 중지 명령을 보내는 것은 자연스럽지 못하다. 이런 부자연스러운 제어 신호가 보내졌을 서비스가 어떻게 동작할 것인지는 서비스에 따라 다르겠지만 일반적으로 결과 예측이 거의 불가능하다. 그래서 이런 복잡한 상황을 조금이라도 피하려면 서비스는 어떤 작업을 진행할 진행중(PENDING)이라는 것을 SCM에게 철저히 알려야 하며 진행중일 받아들일 있는 신호를 제한해야 한다.

case SERVICE_CONTROL_PAUSE:
MySetStatus(SERVICE_PAUSE_PENDING, 0);
g_bPause=TRUE;
MySetStatus(SERVICE_PAUSED);
break;

서비스 중지
서비스 메인은 완전히 부닐된 스레드이므로 Sleep(10000)으로 10초간 휴식을 취해도 상관없겠지만 이렇게 되면 즉각적인 서비스 중지를 없다.
그래서 이벤트를 생성하고 이벤트가 신호상태가 아닌 10초동안만 쉬어야 한다. 10초동안 이벤트를 대기하면서 쉬되 대기 중에도 이벤트가 신호상태가 되면 즉시 대기 루틴을 빠져나와 서비스를 중지해야 한다. 서비스 메인의 초기화 루틴에서 g_ExitEvent 수동으로 생성하였다. 로그 작성 루프에서 10초간 대기하는 코드는 다음과 같다.

if (WaiitForSingleObject(g_ExitEvent, 10000) == WAIT_OBJECT_0)
break;

루프를 빠져 나와 서비스 메인을 종료하기 직전에 서비스의 현재 상태를 SERVICE_STOPPED 보고한다. 핸들러에서는 다음과 같이 중지 처리하면 된다.

case SERVICE_CONTROL_STOP:
MySetStatus(SERVICE_STOP_PENDING, 0);
SetEvent(g_ExitEvent);
break;

먼저 중지 작업이 진행중이면 이때는 어떠한 제어 신호도 받아들이지 않는다는 것을 SCM에게 보고한다. 핸들러에서 g_ExitEvent 신호상태로만 바꿔놓으면 서비스 메인은 신호를 받아 즉시 서비스를 중지할 것이다.
상태 알리기
서비스가 특정 상황에서 어떤 제어 신호를 받아들일 것인가는 SetServiceStatus 함수의 dwControlsAccepted 인수로 지정할 있다. 그러나 SERVICIE_CONTROL_INTERROGATE 제어 신호는 모든 서비스가 선택의 여지 없이 의무적으로 처리해야 한다. 신호는 SCM 서비스의 현재 상태를 서비스에게 물어볼 보내는 신호이다. 서비스는 신호를 받으면 SetServicieStatus함수의 dwCurrentState 자신의 현재 상태값을 전달해야 한다. 서비스는 스스로 자신의 상태를 항상 기억하고 있어야 하며 그래서 MySetStataus함수는 서비스의 상태가 변경될 때마다 현재 상태를 g_NowState 전역변수에 저장하고 있다가 INTERROGATE 제어 신호를 받으면 g_NowState SCM에게 보고한다.
셧다운 신호
SERVICE_CONTROL_SHUTDOWN 제어신호는 시스템이 셧다운될 서비스로 보내진다. 제어 신호는 SetServiceStatus함수의 dwControlsAccepted SERVICE_ACCEPT_SHUTDOWN 플래그가 설정되어 있을 때만 보내지는데 일반적인 경우 서비스는 항상 셧다운을 준비하고 있기 때문에 제어 신호를 받지도 않고 처리하지도 않는다.
그러나 효율상의 목적으로 파일을 열어놓거나 네트워크 연결을 유지하고 있는 서비스는 반드시 신호를 받아서 셧다운시에 사용하고 있던 모든 자원을 반납하고 저장하지 않은 정보를 저장해야 한다. 이때 서비스에게 주어지는 시간은 20 정도인데 시간에 모든 셧다운 처리를 완료해야 한다. 시간 안에 셧다운을 처리하지 못하면 시스템은 서비스의 상황에 상관없이 강제로 서비스를 죽여버린다. 만약 서비스가 셧다운에 많은 시간이 필요하다면 STOP_PENDING 상태를 보고하고 dwWaitHint 얼마간 기다려 달라는 보고를 반드시 해야 한다. 그래야 시스템이 얼마나 기다려야할지 정확하게 있다.
셧다운 제어 신호를 받으면 안정적인 셧다운을 있지만 그보다는 셧다운 처리를 하지 않아도 되게끔 서비스를 만드는 것이 바람직하다. 갑작스런 정전이나 시스템 오류가 발생하더라도 다음 부팅에 영향을 미치는 것은 결코 바람직하지 않기 때문이다.
사용자 정의 신호
03 서비스 설치
. 서비스 설치

#include "stdafx.h"
 
// 서비스 설치
void Install()
{
SC_HANDLE hScm, hSrv;
TCHAR SrvPath[MAX_PATH];
SERVICE_DESCRIPTION lpDes;
TCHAR Desc[1024];
 
// scm open
hScm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (hScm == NULL) {
// scm open fail
return;
}
 
// 등록할 서비스 파일 조사
GetCurrentDirectory(MAX_PATH, SrvPath);
lstrcat(SrvPath, "\\");
lstrcat(SrvPath, EXENAME);
if (_access(SrvPath, 0) != 0) {
CloseServiceHandle(hScm);
// 같은 디렉토리에 서비스 파일이 없음
return;
}
 
// 서비스 등록
hSrv = CreateService(hScm, SRVNAME, DISPNAME
, SERVICE_PAUSE_CONTINUE | SERVICE_CHANGE_CONFIG
, SERVICE_WIN32_OWN_PROCESS
, SERVICE_DEMAND_START
, SERVICE_ERROR_IGNORE
, SrvPath
, NULL, NULL, NULL, NULL, NULL);
if (hSrv == NULL) {
// 설치 실패
}
else {
// 설명 등록
GetDlgItemText(hDlgMain, IDC_DESC, Desc, 1024);
lpDes.lpDescription = Desc;
ChangeServiceConfig2(hSrv, SERVICE_CONFIG_DESCRIPTION, &lpDes);
CloseServiceHandle(hSrv);
}
CloseServiceHandle(hScm);
}

서비스에 관한 작업을 하려면 서비스를 총괄하는 SCM 열어야 하는데 다음 함수를 사용한다.

SC_HANDLE OpenSCManager(LPCTSTR lpMachineName, LPCTSTR lpDatabaseName, DWORD dwDesiredAccess);

함수는 지정한 컴퓨터의 SCM 연결하며 해당 컴퓨터의 서비스 DB 연결한다.
첫번째 인수로 접속하고자 하는 컴퓨터를 지정하는데 값이 NULL이거나 문자열이면 컴퓨터의 SCM 연다. 네트워크상의 다른 컴퓨터에 서비스를 설치하려면 "\\"다음에 컴퓨터 이름을 적어주면 된다.
두번째 인수는 서비스 DB이름인데 현재로서는 사용할 있는 옵션이 SERVICE_ACTIVE_DATABASE 하나밖에 없으며 NULL 주면 옵션이 선택된다.
세번째 인수는 SCM 요청할 액세스 권한을 지정한다. 요청할 액세스 권한은 다음과 같은 종류가 있으며 플래그들 외에 가지 플래그의 조합값들을 지정할 있다.

액세스, 플래그
설명
SC_MANAGER_ALL_ACCESS
모든 권한을 가진다.
SC_MANAGER_CONNECT
SCM 연결할 있는 권한
SC_MANAGER_CREATE_SERVICE
서비스를 설치할 있는 권한
SC_MANAGER_ENUMERATE_SERVICE
서비스 DB 서비스를 열거할 있는 권한
SC_MANAER_LOCK
서비스 DB 잠글 있는 권한
SC_MANAGER_QUERY_LOCK_STATUS
서비스 DB 잠금 상태를 조사할 있는 권한
GENERIC_READ

STANDARD_RIGHTS_READ,
SC_MANAGER_ENUMERATE_SERVICE,
SC_MANAGER_QUERY_LOCK_STATUS,
GENERIC_WRITE

STANDARD_RIGHTS_WRITE,
SC_MANAGER_ENUMERATE_SERVICE,
SC_MANAGER_CREATE_SERVICE
GENERIC_EXECUTE

STANDARD_RIGHTS_EXECUTE,
SC_MANAGER_CONNECT,
SC_MANAGER_LOCK

프로세스가 요청한 권한을 가지고 있지 않다면 SCM 열리지 않으며 함수는 NULL 리턴한다. 디폴트로 모든 프로세스는 CONNECT, ENUMERATE_SERVICE, QUERY_LOCK_STATUS권한은 가지고 있으나 CREATE_SERVICE 권한은 Administrators 그룹의 계정으로 로그인했을 때만 주어진다.
함수 호출이 성공하면 SC_HANDLE 형의 서비스 DB 핸들을 리턴하는데 핸들은 서비스 설치, 제거, 편집 서비스 DB 액세스하는 모든 함수들이 사용한다. 핸들은 일부러 닫지 않아도 프로세스가 종료되면 자동으로 닫히기는 하지만 사용하고 후에 CloseServiceHandle 함수로 핸들을 닫는 것이 좋다.
서비스를 설치하는 함수는 다음과 같다.

SC_HANDLE CreateService(
SC_HANDLE hSCManager,
LPCTSTR lpServiceName,
LPCTSTR lpDisplayName,
DWORD dwDesiredAccess,
DWORD dwServiceType,
DWORD dwStartType,
DWORD dwErrorControl,
LPCTSTR lpBinaryPathName,
LPCTSTR lpLoadOrderGroup,
LPDWORD lpdwTagId,
LPCTSTR lpDependencies,
LPCTSTR lpServiceStartName,
LPCTSTR lpPassword);

dwDesiredAccess 인수는 서비스를 만든 리턴되는 서비스 핸들로 어떤 작업을 것인지 액세스 권한을 지정한다. 어떤 권한을 지정하는가에 따라 호출할 있는 함수의 종류가 달라진다.

액세스
설명
SERVICE_ALL_ACCESS
모든 권한을 가진다.
SERVICE_CHANGE_CONFIG
서비스의 설정을 변경한다.
SERVICE_ENUMERATE_DEPENDENTS
서비스에 의존하는 서비스를 열거할 있다.
SERVICE_INTERROGATE
ControlService 함수로 서비스의 상태를 조사하는 제어 코드를 보낸다.
SERVICE_PAUSE_CONTINUE
서비스를 일시 중지한다.
SERVICE_QUERY_CONFIG
서비스의 설정을 조사한다.
SERVICE_QUERY_STATUS
QueryServiceStatus 함수로 서비스의 상태를 조사할 있다.
SERVICE_START
서비스를 시작한다.
SERVICE_STOP
서비스를 중지한다.
SERVICE_USER_DEFINED_CONTROL
 

플래그들의 조합인 GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE 플래그도 사용할 있다.
. 설명 편집
. 서비스 제거

void Uninstall()
{
SC_HANDLE hScm, hSrv;
SERVICE_STATUS ss;
 
// SMC open
hScm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (hScm==NULL) {
// 실패
return;
}
 
// 서비스 핸들
hSrv = OpenService(hScm, SVRNAME, SERVICE_ALL_ACCESS);
if (hSrv == NULL) {
// 설치안됨
return;
}
 
// 중지
QueryServiceStatus(hSrv, &ss);
if (ss.dwCurrentState != SERVICE_STOPPED) {
ControlService(hSrv, SERVICE_CONTROL_STOP, &ss);
Sleep(2000);
}
 
// 제거
if (DeleteService(hSrv)) {
// 제거 완료
}
else {
// 제거 실패
}
 
CloseServiceHandle(hSrv);
CloseServiceHandle(hScm);
}

 

BOOL DeleteService(SC_HANDLE hService);

함수는 실제로 서비스를 삭제하지 않고 레지스트리에 삭제표시만 한다. 서비스 핸들이 닫힐 , 그리고 서비스가 실핼중이 아닐 실제로 서비스가 삭제된다. 만약 서비스가 중지될 없는 상황이라면 서비스는 다음 부팅할 삭제된다.
. 서비스 옵션
04 서비스 제어
. MemStatControl
서비스 제어 프로그램 별도로 제작

#include <Windows.h>
#include "resource.h"
 
#define SRVNAME "MemStatService"
#define SERVICE_CONTROL_NEWFILE 128
 
BOOL CALLBACK MainDlgProc(HWND hDlg, UINT iMessage, WPARAM wParam, LPARAM lParam);
HINSTANCE g_hInst;
HWND hDlgMain, hStatic;
SC_HANDLE hScm, hSrv;
SERVICE_STATUS ss;
 
void MemStart();
void MemControl(DWORD dwControl);
void QueryService();
 
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
g_hInst = hInstance;
DialogBox(g_hInst, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, MainDlgProc);
return 0;
}
 
BOOL CALLBACK MainDlgProc(HWND hDlg, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
switch (iMessage) {
case WM_INITDIALOG:
SetWindowPos(hDlg, HWND_TOP, 100, 100, 0, 0, SWP_NOSIZE);
hDlgMain = hDlg;
hStatic = GetDlgItem(hDlg, IDC_STATIC1);
 
// SCM
hScm = OpenSCManager(NULL, NULL, GENERIC_READ);
if (hScm == NULL) {
// scm fail
EndDialog(hDlg, 0);
}
 
// 서비스
hSrv = OpenService(hScm, SRVNAME, SERVICE_ALL_ACCESS);
if (hSrv == NULL) {
// service fail
EndDialog(hDlg, 0);
}
else {
CloseServiceHandle(hSrv);
}
QueryService();
 
return TRUE;
case  WM_DESTROY:
CloseServiceHandle(hScm);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_START:
MemStart();
return TRUE;
case IDC_STOP:
MemControl(SERVICE_CONTROL_STOP);
return TRUE;
case IDC_PAUSE:
MemControl(SERVICE_CONTROL_PAUSE);
return TRUE;
case IDC_CONTINUE:
MemControl(SERVICE_CONTROL_CONTINUE);
return TRUE;
case IDC_NEWFILE:
MemControl(SERVICE_CONTROL_NEWFILE);
return TRUE;
case IDOK:
case IDCANCEL:
EndDialog(hDlgMain, 0);
return TRUE;
}
return FALSE;
}
}
 
void QueryService()
{
hSrv = OpenService(hScm, "MemStatService", SERVICE_INTERROGATE);
 
do {
ControlService(hSrv, SERVICE_CONTROL_INTERROGATE, &ss);
} while ((ss.dwCurrentState != SERVICE_STOPPED)
&& (ss.dwCurrentState != SERVICE_RUNNING)
&& (ss.dwCurrentState != SERVICE_PAUSED));
 
EnableWindow(GetDlgItem(hDlgMian, IDC_START), FALSE);
EnableWindow(GetDlgItem(hDlgMian, IDC_STOP), FALSE);
EnableWindow(GetDlgItem(hDlgMian, IDC_PAUSE), FALSE);
EnableWindow(GetDlgItem(hDlgMian, IDC_CONTINUE), FALSE);
EnableWindow(GetDlgItem(hDlgMian, IDC_NEWFILE), FALSE);
 
switch (ss.dwCurrentState)
{
case SERVICE_STOPPED:
EnableWindow(GetDlgItem(hDlgMian, IDC_START), TRUE);
// 중지
break;
case SERVICE_RUNNING:
EnableWindow(GetDlgItem(hDlgMian, IDC_STOP), TRUE);
EnableWindow(GetDlgItem(hDlgMian, IDC_PAUSE), TRUE);
EnableWindow(GetDlgItem(hDlgMian, IDC_NEWFILE), TRUE);
break;
SERVICE_PAUSED:
EnableWindow(GetDlgItem(hDlgMian, IDC_STOP), TRUE);
EnableWindow(GetDlgItem(hDlgMian, IDC_CONTINUE), TRUE);
break;
}
 
CloseServiceHandle(hSrv);
}
 
void MemStart()
{
hSrv = OpenService(hScm, "MemStatService", SERVICE_START | SERVICE_QUERY_STATUS);
 
SetCursor(LoadCursor(NULL, IDC_WAIT));
if (StartService(hSrv, 0, NULL) == TRUE) {
QueryServiceStatus(hSrv, &ss);
while (ss.dwCurrentState != SERVICE_RUNNING) {
Sleep(ss.dwWaitHint);
QueryServiceStatus(hSrv, &ss);
}
}
 
SetCursor(LoadCursor(NULL, IDC_ARROW));
 
CloseServiceHandle(hSrv);
QueryService();
}
 
void MemControl(DWORD dwControl)
{
hSrv = OpenService(hScm, "MemStatService", GENERIC_EXECUTE);
 
ControlService(hSrv, dwControl, &ss);
 
CloseServiceHandle(hSrv);
QueryService();
}

. 서비스의 상태 조사
BOOL QueryServiceStatus(SC_HANDLE hService, LPSERVICE_STATUS lpServiceStatus);
BOOL ControlService(SC_HANDLE hService, DWORD dwControl, LPSERVICE_STATUS lpServiceStatus);
. 서비스 시작
BOOL StartService(SC_HANDLE hService, DWORD dwNumServiceArgs, LPCTSTR &lpServiceArgVectors);
. 제어 신호 보내기
BOOL ConotrolService(SC_HANDLE hService, DWORD dwControl, LPSERVICE_STATUS lpServiceStatus);

반응형

'책정리 > 윈도우 API 정복2' 카테고리의 다른 글

42장 동기화  (0) 2019.05.07
41장 멀티 스레드  (0) 2019.04.16
39장 메모리  (0) 2019.04.04
40장 프로세스  (0) 2019.04.04
39장 메모리  (0) 2019.04.03