• СТАТЬЯ Службы Windows NT: назначение и разработка Зачем и как создавать службы (сервисы) Windows NT/2000
  • Программирование на Visual C++

    Выпуск №38 от 24 марта 2001 г.

    Приветствую!

    Сегодня мы с вами углубимся в особенности систем Windows NT/2000 – а именно, научимся создавать под них особые программы, называемые службами или сервисами.

    СТАТЬЯ

    Службы Windows NT: назначение и разработка

    Зачем и как создавать службы (сервисы) Windows NT/2000

    Автор: Михаил Плакунов

    Источник: СофтТерра

    Службы Windows NT, общие понятия

    Служба Windows NT (Windows NT service) – специальный процесс, обладающий унифицированным интерфейсом для взаимодействия с операционной системой Windows NT. Службы делятся на два типа – службы Win32, взаимодействующие с операционной системой посредством диспетчера управления службами (Service Control Manager – SCM), и драйвера, работающие по протоколу драйвера устройства Windows NT. Далее в этой статье мы будем обсуждать только службы Win32.

    Применение служб

    Одним из важнейших свойств службы является неинтерактивность. Типичное службы – это незаметная для обычного пользователя работа в фоновом режиме. В силу этого службы наиболее подходят для реализации следующих типов приложений:

    • Сервера в архитектуре клиент-сервер (например, MS SQL, MS Exchange Server)

    • Сетевые службы Windows NT (Server, Workstation);

    • Серверные (в смысле функциональности) компоненты распределенных приложений (например, всевозможные программы мониторинга).

    Основные свойства служб

    От обычного приложения Win32 службу отличают 3 основных свойства. Рассмотрим каждое из них.

    Во-первых, это возможность корректного останова (приостанова) работы службы. Пользователь или другое приложение, использующие стандартные механизмы, имеют возможность изменить состояние службы – перевести ее из состояния выполнения в состояние паузы или даже остановить ее работу. При этом служба перед изменением своего состояния получает специальное уведомление, благодаря которому может совершить необходимые для перехода в новое состояние действия, например, освободить занятые ресурсы.

    Во-вторых, возможность запуска службы до регистрации пользователя и, как следствие, возможность работы вообще без зарегистрированного пользователя. Любая служба может быть запущена автоматически при старте операционной системы и начать работу еще до того как пользователь произведет вход в систему.

    И, наконец, возможность работы в произвольном контексте безопасности. Контекст безопасности Windows NT определяет совокупность прав доступа процесса к различным объектам системы и данным. В отличие от обычного приложения Win32, которое всегда запускается в контексте безопасности пользователя, зарегистрированного в данный момент в системе, для службы контекст безопасности ее выполнения можно определить заранее. Это означает, что для службы можно определить набор ее прав доступа к объектам системы заранее и тем самым ограничить сферу ее деятельности. Применительно к службам существует специальный вид контекста безопасности, используемый по умолчанию и называющийся Local System. Служба, запущенная в этом контексте, обладает правами только на ресурсы локального компьютера. Никакие сетевые операции не могут быть осуществлены с правами Local System, поскольку этот контекст имеет смысл только на локальном компьютере и не опознается другими компьютерами сети.

    Взаимодействие службы с другими приложениями

    Любое приложение, имеющее соответствующие права, может взаимодействовать со службой. Взаимодействие, в первую очередь, подразумевает изменение состояния службы, то есть перевод ее в одно из трех состояний – работающее (Запуск), приостанов (Пауза), останов и осуществляется при помощи подачи запросов SCM. Запросы бывают трех типов – сообщения от служб (фиксация их состояний), запросы, связанные с изменением конфигурации службы или получением информации о ней и запросы приложений на изменение состояния службы.

    Для управления службой необходимо в первую очередь получают ее дескриптор с помощью функции Win32 API OpenService. Функция StartService запускает службу. При необходимости изменение состояния службы производится вызовом функции ControlService.

    База данных службы

    Информация о каждой службе хранится в реестре – в ключе HKLM\SYSTEM\CurrentControlSet\Services\ServiceName. Там содержатся следующие сведения:

    • Тип службы. Указывает на то, реализована ли в данном приложении только одна служба (эксклюзивная) или же их в приложении несколько. Эксклюзивная служба может работать в любом контексте безопасности. Несколько служб внутри одного приложения могут работать только в контексте LocalSystem.

    • Тип запуска. Автоматический – служба запускается при старте системы. По требованию – служба запускается пользователем вручную. Деактивированный – служба не может быть запущена.

    • Имя исполняемого модуля (EXE-файл).

    • Порядок запуска по отношению к другим службам. В некоторых случаях для корректной работы службы требуется, чтобы была запущена одна или несколько других служб. В этом случае в реестре содержится информация о службах, запускаемых перед данной.

    • Контекст безопасности выполнения службы (сетевое имя и пароль). По умолчанию контекст безопасности соответствует LocalSystem.

    Приложения, которым требуется получить информацию о какой-либо службе или изменить тот или иной параметр службы, по сути должны изменить информацию в базе данных службы в реестре. Это можно сделать посредством соответствующих функций Win32 API:

    • OpenSCManager, CreateService, OpenService, CloseServiceHandle – для создания (открытия) службы;

    • QueryServiceConfig, QueryServiceObjectSecurity, EnumDependentServices, EnumServicesStatus – для получения информации о службе;

    • ChangeServiceConfig, SetServiceObjectSecurity, LockServiceDatabase, UnlockServiceDatabase, QueryServiceLockStatus – для изменения конфигурационной информации службы.

    Внутреннее устройство службы.

    Для того, чтобы «быть службой», приложение должно быть устроено соответствующим образом, а именно – включать в себя определенный набор функций (в терминах C++) с определенной функциональностью. Рассмотрим кратко каждую из них.

    Функция main

    Как известно функция main – точка входа любого консольного Win32 приложения. При запуске службы первым делом начинает выполняться код этой функции. Втечение 30 секунд с момента старта функция main должна обязательно вызвать StartServiceCtrlDispatcher для установления соединения между приложением и SCM. Все коммуникации между любой службой данного приложения и SCM осуществляются внутри функции StartServiceCtrlDispatcher, которая завершает работу только после остановки всех служб в приложении.

    Функция ServiceMain

    Помимо общепроцессной точки входа существует еще отдельная точка входа для каждой из служб, реализованных в приложении. Имена функций, являющихся точками входа служб (для простоты назовем их всех одинаково – ServiceMain), передаются SCM в одном из параметров при вызове StartServiceCtrlDispatcher. При запуске каждой службы для выполнения ServiceMain создается отдельный поток.

    Получив управление, ServiceMain первым делом должна зарегистрировать обработчик запросов к службе, функцию Handler, свою для каждой из служб в приложении. После этого в ServiceMain обычно следуют какие-либо действия для инициализации службы – выделение памяти, чтение данных и т.п. Эти действия должны обязательно сопровождаться уведомлениями SCM о том, что служба все еще находится в процессе старта и никаких сбоев не произошло. Уведомления посылаются при помощи вызовов функции SetServiceStatus. Все вызовы, кроме самого последнего должны быть с параметром SERVICE_START_PENDING, а самый последний – с параметром SERVICE_RUNNING. Периодичность вызовов определяется разработчиком службы, исходя их следующего условия: продолжительность временного интервала между двумя соседними вызовами SetServiceStatus не должна превышать значения параметра dwWaitHint, переданного SCM при первом из двух вызовов. В противном случае SCM, не получив во-время очередного уведомления, принудительно остановит службу. Такой способ позволяет избежать ситуации «зависания» службы на старте в результате возникновения тех или иных сбоев (вспомним, что службы обычно неинтерактивны и могут запускаться в отсутствие пользователя). Обычная практика заключается в том, что после завершения очередного шага инициализации происходит уведомление SCM.

    Функция Handler

    Как уже упоминалось выше, Handler – это прототип callback-функции, обработчика запросов к службе, своей для каждой службы в приложении. Handler вызывается, когда службе приходит запрос (запуск, приостанов, возобновление, останов, сообщение текущего состояния) и выполняет необходимые в соответствии с запросом действия, после чего сообщает новое состояние SCM.

    Один запрос следует отметить особо – запрос, поступающий при завершении работы системы (Shutdown). Этот запрос сигнализирует о необходимости выполнить деинициализацию и завершиться. Microsoft утверждает, что для завершения работы каждой службе выделяется 20 секунд, после чего она останавливается принудительно. Однако тесты показали, что это условие выполняется не всегда и служба принудительно останавливается до истечения этого промежутка времени.

    Система безопасности служб

    Любое действие над службами требует наличия соответствующих прав у приложения. Все приложения обладают правами на соединение с SCM, перечисление служб и проверку заблокированности БД службы. Регистрировать в сиситеме новую службу или блокировать БД службы могут только приложения, обладающие административными правами.

    Каждая служба имеет дескриптор безопасности, описывающий какие пользователи имеют права на ту или иную операцию. По умолчанию:

    • Все пользователи имеют права SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_ENUMERATE_DEPENDENTS, SERVICE_INTERROGATE и SERVICE_USER_DEFINED_CONTROL;

    • Пользователи, входящие в группу Power Users и учетная запись LocalSystem дополнительно имеют права SERVICE_START, SERVICE_PAUSE_CONTINUE и SERVICE_STOP;

    • Пользователи, входящие в группы Administrators и System Operators имеют право SERVICE_ALL_ACCESS.

    Службы и интерактивность

    По умолчанию интерактивные службы могут выполняться только в контексте безопасности LocalSystem. Это связано с особенностями вывода на экран монитора в Windows NT, где существует, например, такой объект как "Desktop", для работы с которым нужно иметь соответствующие права доступа, которых может не оказаться у произвольной учетной записи, отличной от LocalSystem. Несмотря на то, что в подавляющем большинстве случаев это ограничение несущественно однако иногда существует необходимость создать службу, которая выводила бы информацию на экран монитора и при этом выполнялась бы в контексте безопасности отличном от LocalSystem, например, серверная компонента приложения для запуска приложений на удаленном компьютере.

    Следующий фрагмент кода иллюстрирует такую возможность.

    // Функция, аналог MessageBox Win32 API

    int ServerMessageBox(RPC_BINDING_HANDLE h, LPSTR lpszText, LPSTR lpszTitle, UINT fuStyle) {

     DWORD dwThreadId;

     HWINSTA hwinstaSave;

     HDESK hdeskSave;

     HWINSTA hwinstaUser;

     HDESK hdeskUser;

     int result;

     // Запоминаем текущие объекты "Window station" и "Desktop".

     GetDesktopWindow();

     hwinstaSave = GetProcessWindowStation();

     dwThreadId = GetCurrentThreadId();

     hdeskSave = GetThreadDesktop(dwThreadId);

     // Меняем контекст безопасности на тот,

     // который есть у вызавшего клиента RPC

     // и получаем доступ к пользовательским

     // объектам "Window station" и "Desktop".

     RpcImpersonateClient(h);

     hwinstaUser = OpenWindowStation("WinSta0", FALSE, MAXIMUM_ALLOWED);

     if (hwinstaUser == NULL) {

      RpcRevertToSelf();

      return 0;

     }

     SetProcessWindowStation(hwinstaUser);

     hdeskUser = OpenDesktop("Default", 0, FALSE, MAXIMUM_ALLOWED);

     RpcRevertToSelf();

     if (hdeskUser == NULL) {

      SetProcessWindowStation(hwinstaSave);

      CloseWindowStation(hwinstaUser);

      return 0;

     }

     SetThreadDesktop(hdeskUser);

     // Выводим обычное текстовое окно.

     result = MessageBox(NULL, lpszText, lpszTitle, fuStyle);

     // Восстанавливаем сохраненные объекты

     // "Window station" и "Desktop".

     SetThreadDesktop(hdeskSave);

     SetProcessWindowStation(hwinstaSave);

     CloseDesktop(hdeskUser);

     CloseWindowStation(hwinstaUser);

     return result;

    }

    В этом фрагменте в ответ на запрос, посланный клиентской частью приложения последством RPC, служба выводит текстовое сообщение на экран монитора.

    Пример службы (ключевые фрагменты)

    Рассмотрим на примере ключевые фрагменты приложения на языке C++, реализующего службу Windows NT. Для наглядности несущественные части кода опущены.

    Функция main

    Вот как выглядит код функции main:

    void main() {

     SERVICE_TABLE_ENTRY steTable[] = {

      {SERVICENAME, ServiceMain}, {NULL, NULL}

     };

     // Устанавливаем соединение с SCM. Внутри этой функции

     // происходит прием и диспетчеризация запросов.

     StartServiceCtrlDispatcher(steTable);

    }

    Функция ServiceMain

    Особенностью кода, содержащегося в ServiceMain, является то, что часто невозможно заранее предсказать время выполнения той или иной операции, особенно, если учесть, что ее выполнение происходит в операционной системе с вытесняющей многозадачностью. Если операция продлится дольше указанного в параметре вызова SetServiceStatus интервала времени, служба не сможет во-время отправить следующее уведомление, в результате чего SCM остановит ее работу. Примерами потенциально операций могут служить вызовы функций работы с сетью при больших таймаутах или единовременное чтение большого количества информации с медленного носителя. Кроме того, такой подход совершенно не применим при отладке службы, поскольку выполнение программы в отладчике сопровождается большими паузами, необходимыми разработчику.

    Для преодоления этой проблемы все операции по взаимодействию с SCM следует выполнять в отдельном потоке, не зависящем от действий, происходящих на этапе инициализации.

    Алгоритм корректного запуска службы, использующий вспомогательный поток:

    void WINAPI ServiceMain(DWORD dwArgc, LPSTR *psArgv) {

     // Сразу регистрируем обработчик запросов.

     hSS = RegisterServiceCtrlHandler(SERVICENAME, ServiceHandler);

     sStatus.dwCheckPoint = 0;

     sStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;

     sStatus.dwServiceSpecificExitCode = 0;

     sStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

     sStatus.dwWaitHint = 0;

     sStatus.dwWin32ExitCode = NOERROR;

     // Для инициализации службы вызывается функция InitService();

     // Для того, чтобы в процессе инициализации система не

     // выгрузила службу, запускается поток, который раз в

     // секунду сообщает, что служба в процессе инициализации.

     // Для синхронизации потока создаётся событие.

     // После этого запускается рабочий поток, для

     // синхронизации которого также

     // создаётся событие.

     hSendStartPending = CreateEvent(NULL, TRUE, FALSE, NULL);

     HANDLE hSendStartThread;

     DWORD dwThreadId;

     hSendStartThread = CreateThread(NULL, 0, SendStartPending, NULL, 0, &dwThreadId);

     //Здесь производится вся инициализация службы.

     InitService();

     SetEvent(hSendStartPending);

     if (WaitForSingleObject(hSendStartThread, 2000) != WAIT_OBJECT_0) {

      TerminateThread(hSendStartThread, 0);

     }

     CloseHandle(hSendStartPending);

     CloseHandle(hSendStartThread);

     hWork = CreateEvent(NULL, TRUE, FALSE, NULL);

     hServiceThread = CreateThread(NULL, 0, ServiceFunc, 0, 0, &dwThreadId);

     sStatus.dwCurrentState = SERVICE_RUNNING;

     SetServiceStatus(hSS, &sStatus);

    }


    // Функция потока, каждую секунду посылающая уведомления SCM

    // о том, что процесс инициализации идёт. Работа функции

    // завершается, когда устанавливается

    // событие hSendStartPending.

    DWORD WINAPI SendStartPending(LPVOID) {

     sStatus.dwCheckPoint = 0;

     sStatus.dwCurrentState = SERVICE_START_PENDING;

     sStatus.dwWaitHint = 2000;

     // "Засыпаем" на 1 секунду. Если через 1 секунду

     // событие hSendStartPending не перешло

     // в сигнальное состояние (инициализация службы не

     // закончилась), посылаем очередное уведомление,

     // установив максимальный интервал времени

     // в 2 секунды, для того, чтобы был запас времени до

     // следующего уведомления.

     while (true) {

      SetServiceStatus(hSS, &sStatus);

      sStatus.dwCheckPoint++;

      if (WaitForSingleObject(hSendStartPending, 1000) != WAIT_TIMEOUT) break;

     }

     sStatus.dwCheckPoint = 0;

     return 0;

    }


    // Функция, инициализирующая службу. Чтение данных,

    // распределение памяти и т.п.

    void InitService() {

     ...

    }


    // Функция, содержащая «полезный» код службы.

    DWORD WINAPI ServiceFunc(LPVOID) {

     while (true) {

      if (!bPause) {

       // Здесь содержится код, который как правило

       // выполняет какие-либо циклические операции...

      }

      if (WaitForSingleObject(hWork, 1000) != WAIT_TIMEOUT) break;

      sStatus.dwCheckPoint = 0;

      return 0;

     }

    }

    Функция Handler

    А вот код функции Handler и вспомогательных потоков:

    // Обработчик запросов от SCM

    void WINAPI ServiceHandler(DWORD dwCode) {

     switch (dwCode) {

     case SERVICE_CONTROL_STOP:

     case SERVICE_CONTROL_SHUTDOWN:

      ReportStatusToSCMgr(SERVICE_STOP_PENDING, NO_ERROR, 0, 1000);

      hSendStopPending = CreateEvent(NULL, TRUE, FALSE, NULL);

      hSendStopThread = CreateThread(NULL, 0, SendStopPending, NULL, 0, & dwThreadId);

      SetEvent(hWork);

      if (WaitForSingleObject(hServiceThread, 1000) != WAIT_OBJECT_0) {

       TerminateThread(hServiceThread, 0);

      }

      SetEvent(hSendStopPending);

      CloseHandle(hServiceThread);

      CloseHandle(hWork);

      if(WaitForSingleObject(hSendStopThread, 2000) != WAIT_OBJECT_0) {

       TerminateThread(hSendStopThread, 0);

      }

      CloseHandle(hSendStopPending);

      sStatus.dwCurrentState = SERVICE_STOPPED;

      SetServiceStatus(hSS, &sStatus);

      break;

     case SERVICE_CONTROL_PAUSE:

      bPause = true;

      sStatus.dwCurrentState = SERVICE_PAUSED;

      SetServiceStatus(hSS, &sStatus);

      break;

     case SERVICE_CONTROL_CONTINUE:

      bPause = true;

      sStatus.dwCurrentState = SERVICE_RUNNING;

      SetServiceStatus(hSS, &sStatus);

      break;

     case SERVICE_CONTROL_INTERROGATE:

      SetServiceStatus(hSS, &sStatus);

      break;

     default:

      SetServiceStatus(hSS, &sStatus);

      break;

     }

    }


    // Функция потока, аналогичная SendStartPending

    // для останова службы.

    DWORD WINAPI SendStopPending(LPVOID) {

     sStatus.dwCheckPoint = 0;

     sStatus.dwCurrentState = SERVICE_STOP_PENDING;

     sStatus.dwWaitHint = 2000;

     while (true) {

      SetServiceStatus(hSS, &sStatus);

      sStatus.dwCheckPoint++;

      if (WaitForSingleObject(hSendStopPending, 1000) != WAIT_TIMEOUT) break;

     }

     sStatus.dwCheckPoint = 0;

     return 0;

    }

    Для запросов "Stop" и "Shutdown" используется алгоритм корректного останова службы, аналогичный тому, который используется при старте службы, с той лишь разницей, что вместо параметра SERVICE_START_PENDING в SetserviceStatus передается параметр SERVICE_STOP_PENDING, а вместо SERVICE_RUNNING — SERVICE_STOPPED.

    В идеале для запросов "Pause" и "Continue" тоже следует использовать этот подход. Любознательный читатель без труда сможет реализовать его, опираясь на данные примеры.

    Заключение

    В заключение хотелось бы отметить, что с переходом на Windows 2000 разработка служб не претерпела изменений. Службы по-прежнему остаются важной частью программного обеспечения на платформе Windows, что предоставляет разработчикам широкое поле деятельности.

    ВОПРОС-ОТВЕТ

    Q. Хотелось бы побольше узнать о предварительном просмотре. В русской программе он смотрится инородным телом на своем иностранном языке. Можно ли его как-то настраивать под себя?

    В этой же связи: не могу решить проблему.

    В программе 3 меню и, соответственно, 3 панели инструментов, которые создал в Create. Переключая меню, вызываю ShowControlBar – прячу ненужные панели и показываю необходимую. Но после вызова PRINT PREVIEW, в окне появляются сразу все 3 панели инструментов.

    Попутно: что означает AFX_IDS_PREVIEW_CLOSE в String Table?

    (Serg Petukhov )

    A. Отвечу по порядку. 

    1. Все языко-зависимые компоненты для печати и предварительного просмотра (панель инструментов, диалог и строки) в соответствии с идеологией MFC оформлены как ресурсы. Эти ресурсы лежат в файле MFC42.DLL, но программа будет искать их там только если они отсутствуют в головной программе. Если же программа статически линкуется с MFC, ресурсы для печати/предварительного просмотра берутся из файла afxprint.rc. Чтобы в этом всём убедиться, достаточно открыть rc-файл, сгенерённым визардом, и найти там строчки: 

    #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)

    #include "afxprint.rc" // printing/print preview resources

    #endif
     

    Теперь понятно, как поправить ситуацию.

    – Копируем ресурсы из файла afxprint.rc (без окантовочных директив, то есть от строчки "// Printing Resources") в файл ресурсов нашей программы. При этом нужно проследить, чтобы новые ресурсы попали между директивами #ifdef APPSTUDIO_INVOKED и соответствующего #endif (иначе новые ресурсы нельзя будет изменить в редакторе).

    – Убираем из файла ресурсов строчку #include "afxprint.rc" (вручную или через View→Resource includes). На самом деле, это необходимо сделать только при статической линковке с MFC, так как при динамической линковке эта строчка не используется (как я уже говорил, в этом случае ресурсы берутся из MFC42.DLL).

    – Затем запускаем редактор ресурсов Visual Studio и русифицируем новые ресурсы. Не забудьте предварительно установить для каждого ресурса в свойствах Language:Russian, иначе вместо русского языка получите иероглифы!

    – Пересобираем проект и убеждаемся, что теперь предварительный просмотр говорит по-русски. 

    2. После выхода из Print Preview запускается функция CView::OnEndPrintPreview (файл viewcore.cpp). Из неё вызывается ещё одна функция – CFrameWnd::OnSetPreviewMode (файл winfrm.cpp). Просмотрев код этой функции, нетрудно убедиться, что она делает видимыми все стандартные панели с идентификаторами от AFX_IDW_CONTROLBAR_FIRST до AFX_IDW_CONTROLBAR_FIRST+31 включительно. Таким образом, чтобы MFC не вмешивалась в вашу работу с панелями инструментов, нужно назначить им идентификаторы за пределами этого диапазона (например, AFX_IDW_CONTROLBAR_LAST-N, где N = 0, 1, 2, …):

    m_wndToolBar.CreateEx(..., AFX_IDW_CONTROLBAR_LAST);
      

    3. Что касается строки AFX_IDS_PREVIEW_CLOSE, она просто содержит подсказку для команды Close предварительного просмотра. Если вам интересно, где она появляется, запустите режим предварительного просмотра, а затем наведите курсор на пункт Close из системного меню программы (которое раскрывается по щелчку на иконке в левом верхнем углу главного окна). При этом текст подсказки о закрытии предварительного просмотра появится в строке состояния. Можете заменить его на любой другой (на русском языке).

    (Александр Шаргин (rudankort@mail.ru) )
    В ПОИСКАХ ИСТИНЫ 

    Q. Есть приложение на базе диалога. По некоторым причинам необходимо уже внутрь этого диалога вставить закладки (страницы свойств, как хотите). Все это нормально делается и проблем тут не возникает. Но вот при использовании клавиши Tab для прогулки по диалогу фокус с последнего контрола, не принадлежащего Property Page, перемещается не на закладку страницы, а на ее первый определенный в Tab Layout контрол, и только после пробегания по всем элементам Property Page попадает на закладку. Как это вылечить?

    (George Orlov )

    Это все на сегодня. Счастливо! 

    (Алекс Jenter jenter@mail.ru) (Красноярск, 2001.)







    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх