본문 바로가기

진리는어디에

Writing a Windows Service Program

얼마 전 리눅스의 데몬(Daemon)이라는 것에 대해 약간이나마 공부를 해야 할 일이 있었습니다. 그리고 그 과정에서 윈도우에서는 그와 비슷한 서비스라는 것이 있다는 것을 알았습니다. 이번 포스팅에서는 서비스 프로그램을 작성하는 방법에 대해 알아 보도록 하겠습니다. 원문은 코드구루(http://www.codeguru.com)의 'Writing a Service Program(Jonathan Ng)' 을 기본으로 하고 있습니다.

윈도우 서비스란?

윈도우 운영 체제에서 '서비스(services)'라는 것은 일반적으로 시스템 부팅시점 부터 시작해 시스템이 끝나는 시점 까지 그 수명을 같이 하는 프로세스를 가리키기도 합니다. 예를 들어 윈도우 베이스 프로그램과 컴포넌트가 남기는 로그를 기록해주는 '로그 이벤트'같은 프로세스 같은 것이 있습니다. 리눅스(혹은 유닉스) 시스템에서 윈도우의 서비스와 비슷한 역할을 하는 것으로는 데몬(daemon) 프로세스가 있습니다.

서비스 에플릿은 Control Panel > Administrator tools > Service에서 찾아 볼 수 있으며, 새로운 서비스 설치는 이 애플릿과 Registry(HKEY_LOCAL_MACHINE\System\CurrentControlSet\Service)에 앤트리를 생성합니다. 이렇게 해서 시스템은 부트타이밍에 어떤 서비스들이 등록 되어야 하는지 알 수 있습니다.

모든 서비스들은 반드시 아래의 표준 이벤트(standard event)에 응답하도록 디자인 되어야만 합니다. 아래의 이벤트들은 윈도우 서비스에서 버튼의 형태로 보여지게 됩니다 :

  • START : 서비스를 수동으로 시작하거나, 서비스가 중단 되었다면 서비스를 시작한다.
  • STOP : 서비스를 중단한다.
  • PAUSE : 서비스를 일시 중지한다.
  • CONTINUE : 중단되었던 서비스를 재 시작한다.

Service Control Manager(SCM)

서비스들은 Service Control Manager 시스템에 의해 관리 됩니다. Service Control Manager는 레지스트리에 서비스 목록을 유지하며, 부트타임이나 아니면 따로 매뉴얼하게 스타트가 요구 될 경우 레지스트리에 등록되어 있는 서비스 프로그램을 실행하게 됩니다. 일반적으로 서비스 프로그램은 일반 exe 확장자 형태를 가지고 있지만 Service Sonctrol Manager(SCM)에 정상적으로 접속하기 위해서는 아래의 특정한 요구사항들을 만족 시켜야 합니다 :

main 또는 WinMain 함수와 StartServiceCtrlDiespatcher

exe 는 반드시 main 또는 WinMain 함수를 가지고 있어야하고, StartServiceCtrlDiespatcher 함수를 호출 해야 합니다. StartServiceCtrlDiespatcher 함수는 EXE 파일을 SCM에 등록하고, SCM이 ServiceMain 함수에게 포인터를 넘겨 주는 역할을 합니다.

void main(int argc, char *argv[])
{
    SERVICE_TABLE_ENTRY serviceTable[] =
    {
        { ServiceName, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
        { NULL, NULL}
    };

    BOOL success;
    if(argc == 2)
       //..check arguments
    else
    {
        //register with SCM
        success = StartServiceCtrlDispatcher(serviceTable);
        if (!success)
            ErrorHandler("StartServiceCtrlDispatcher", GetLastError());
    }
}

RegisterServiceCtrlHandler

SCM은 서비스 시작 요청이 들어 오면 ServiceMain 함수를 호출 합니다. ServiceMain 함수는 그 즉시 RegisterServiceCtrlHandler 함수를 호출하여 Handler 함수를 SCM에 등록합니다.

void ServiceMain(DWORD argc, LPTSTR *argv)
{
    BOOL success;

    //레지스터 함수 호출
    serviceStatusHandle =
         RegisterServiceCtrlHandler(ServiceName, (LPHANDLER_FUNCTION)ServiceCtrlHandler);

    //SCM에게 통보 및 종료 이벤트 생성
    terminateEvent = CreateEvent (0, TRUE, FALSE, 0);
        
    //notify SCM
    
    //startup parameter 체크
    //start service
    //notify SCM service is runnning
    SendStatusToSCM(SERVICE_RUNNING, NO_ERROR, 0 , 0, 0);

    //wait for stop signal and then terminate
    WaitForSingleObject(terminateEvent, INFINITE);

    terminate(0);
}

요청 처리 핸들러

위 예제에서 RegisterServiceCtrlHandler의 인자로 ServiceCtrlHandler의 함수 포인터를 넘겨 준것을 주목해주세요. Handler 함수는 SCM으로 부터 넘어오는 표준 이벤트(standard event)요청을 처리하는 함수로써 주로 switch 구문으로 만들어 집니다.

void ServiceCtrlHandler(DWORD controlCode)
{
    DWORD currentState = 0;
    BOOL success;

    switch(controlCode)
    {
        // START = ServiceMain()

        // STOP
        case SERVICE_CONTROL_STOP:
            currentState = SERVICE_STOP_PENDING;
            //notify SCM
            SendStatusToSCM(SERVICE_STOP_PENDING,
                                        NO_ERROR,
                                        0,
                                        1,
                                        5000);
            //stop service
            StopService();
            return;

        // PAUSE
        case SERVICE_CONTROL_PAUSE:
            if (runningService && !pauseService)
            {
                 //notify SCM
                 success = SendStatusToSCM(SERVICE_PAUSE_PENDING,
                                                             NO_ERROR,
                                                             0,
                                                             1,
                                                             1000);
                 PauseService();
                 currentState = SERVICE_PAUSED;
            }
            break;

        // RESUME
        case SERVICE_CONTROL_CONTINUE:
            if (runningService && pauseService)
            {
                 //notify SCM
                 success = SendStatusToSCM(SERVICE_CONTINUE_PENDING,
                                                             NO_ERROR,
                                                             0,
                                                             1,
                                                             1000);
                 ResumeService();
                 currentState = SERVICE_RUNNING;
            }
            break;

        // UPDATE
        case SERVICE_CONTROL_INTERROGATE:
            //update status out of switch()
            break;

        case SERVICE_CONTROL_SHUTDOWN:
            //do nothing
            return;
        default:
            break;
    }
    //notify SCM current state
    SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);
}

서비스 프로그램은 main, ServiceMain, Handler 함수들 뿐만 아니라 서비스 자기 자신을 위한 스레드를 갖추었을때 비로소 서비스 프로그램으로써 모든 것을 다 갖추었다고 말 할 수 있습니다.

위와 같이 서비스 프로그램을 만들었다고 해서 모든 것이 끝난 것은 아닙니다. 서비스는 특별한 프로세스로 취급되기 때문에에 일반 exe 처럼 더블클릭이나 커맨드 창에 이름을 적어 주는 것으로 실행 되지 않습니다. 서비스 프로세스는 서비스 매니저에 의해서 실행 되어져야 하며, 서비스 매니저에 의해 관리 되기 위해서는 등록 과정이 필요합니다.

이런 등록을 위해서 윈도우에서는 'sc' 라는 명령어를 제공하고 있습니다. 프로그램 내에서 특별한 인자를 받아 스스로 등록하도록 하는 방법도 있지만 여기서는 sc 에 대해서만 다루고 나머지는 따로 자리를 마련하도록 하겠습니다.

c:\> sc create [service name] [binPath= ] ..

기본 형태는 위와 같습니다. 서비스 등록을 위해서 sc 뒤에 create라는 인자를 주고 등록할 서비스의 이름과 실행할 바이너리 경로를 적어줍니다.

이 때 주의할 점은 binPath= 와 뒤에 들어가는 패스 사이에는 반드시 스페이스 문자가 있어야 한다는 것입니다. 예를 들어 binPath= C:\bin\path\some.exe 와 같은 형식으로 말입니다. sc /? 를 커맨드 창에 입력하시면 보다 자세한 설명이 출력 됩니다.

부록. 참조

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!