Архивы: по дате | по разделам | по авторам

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

АрхивПрограммазм (архив)
автор : Михаил Плакунов   24.02.2001

Зачем и как создавать службы (сервисы) 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, например, серверная компонента приложения для запуска приложений на удаленном компьютере.

Фрагмент кода из Примера 1. иллюстрирует такую возможность.

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

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

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

Функция main

В Примере 2. показан код функции main.

Функция ServiceMain

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

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

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

Функция Handler

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

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

Заключение

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

Пример 1

// Функция, аналог 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; 
}

Пример 2

void main()
{
  SERVICE_TABLE_ENTRY steTable[] = 
  {
    {SERVICENAME, ServiceMain},
    {NULL, NULL}
  };

  // Устанавливаем соединение с SCM. Внутри этой функции
  // происходит прием и диспетчеризация запросов.
  StartServiceCtrlDispatcher(steTable);
}

Пример 3

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;
  }

  return 0;
}

Пример 4

// Обработчик запросов от 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;
}

© ООО "Компьютерра-Онлайн", 1997-2024
При цитировании и использовании любых материалов ссылка на "Компьютерру" обязательна.