Автор: Джеффри Рихтер


Разработка сервисов Windows NT


      От переводчика
      Есть у Вас желание...?
      Несколько слов о безопасности
      Три сервисных компонента
      Начало работы сервиса
      Разработка Win32 сервиса
      Окунемся поглубже
      Заключение

От переводчика

      Данный материал впервые был опубликован в Microsoft Systems Journal в 1997 году. Естественно, многое с того времени изменилось, появились новые операционные системы, новые процессоры и другое компьютерное оборудование. Но неизменной осталась концепция операционной системы Windows NT и появившихся вместе с ней сервисов NT. Поэтому я решил довести эту статью замечательного писателя Джеффри Рихтера, посвященную разработке сервисов NT до заинтересованного читателя, каковым, на мой взгляд, будет, прежде всего, разработчик программного обеспечения. Некоторые места статьи мне пришлось отредактировать согласно сегодняшним реалиям. Это относится в основном к рисункам, использованным в статье. В любом случае, все что описано в данной статье справедливо для ОС Windows XP. Заранее прошу прощения за возможные неточности перевода, в особенности касательно специфичных терминов. Приятного Вам чтения.

Есть у Вас желание...?

      Есть у Вас желание создать приложение, выполняющее работу для различных клиентов - как локальных, так и удаленных? Чтобы это приложение имело привилегии для выполения работы в контексте клиентского уровня доступа? И, скажем, чтобы это приложение могло быть запущено независимо от того, вошел ли пользователь в систему? Тогда все что Вам нужно - это Windows NT сервис.

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

      Первое и самое главное - Windows NT сервис является Win32 приложением. Если у Вас есть желание написать сервис, Вы имеете опыт разработки DLL, Вам знакомы понятия "структурная обработка ошибок" (SEH), "система ввода/вывода" (Device I/O),"мэпированные файлы" (memory-mapped files), "виртуальная память" (virtual memory), "локальная память потока" (thread-local storage - TLS), "синхронизация потоков" (thread synchronization), "Unicode", "Win32 API", Вы свободно можете приступать к созданию собственных сервисов NT. Все эти возможности можно и нужно использовать при создании сервисов. Кроме того, понимание всего вышеперечисленного поможет Вам легко конвертировать уже имеющееся у Вас Win32 приложение в сервис NT. (Windows NT Resource Kit содержит утилиту SRVANY.EXE, позволяющую запустить Win32 приложение как сервис NT. Но запущенное таким образом приложение никогда не сможет использовать все возможности, предоставляемые операционной системой настоящим сервисам).

      Вторая вещь, которую Вам необходимо знать перед тем, как приступать к разработке сервисов - это то, что сервис NT не должен иметь пользовательского интерфейса. Большинство сервисов NT выполняют свою работу на серверах NT, которые закрыты на ключ где-нибудь в специальных помещениях. Если Ваш сервис во время работы будет время от времени показывать, например, диалоговые окна, требующие реакции пользователя, то такой сервис просто прекратит свою работу до тех пор, пока пользователь не нажмет кнопку диалогового окна. Что совершенно неприемлемо. Раз сервис не имеет пользовательского интерфейса, то нет никакой разницы, будете ли Вы его создавать как GUI-приложение (с функцией WinMain) или как консольное CUI-приложение (с функцией main).

      Вы можете спросить: если сервис не имеет пользовательского интерфейса, то как его конфигурировать? Как можно запустить его на выполнение и затем остановить? Как сервис может сообщить об ошибке или сообщить свои статистические данные? Ответ на все эти вопросы один - администрирование сервисов производится удаленно (на расстоянии). в Windows NT имеется набор административных утилит, позволяющих производить конфигурацию сервисов с других компьютеров сети, т.е. нет необходимости непосредственно находится возле компьютера, на котором работает сервис. Скорее всего Вы знакомы с многими из этих утилит: Апплет контрольной панели сервисов (Services Control Panel applet - SCP-апплет), Редактор реестра (Registry Editor - regedit.exe), Просмотрщик событий (Event Viewer - eventvwr.exe), Монитор производительности (Performance Monitor - perfmon.exe).

      При установке операционной системы, устанавливаются также и сервисы, необходимые для работы системы. В таблице ниже перечисленны основные сервисы, устанавливаемые вместе с Windows NT Workstation, и имена исполняемых файлов, содержащих код этих сервисов.

Наименование сервиса Имя исполняемого файла
Alerter Services.exe
ClipBook Server ClipSrv.exe
Computer Browser Services.exe
DHCP Client Services.exe
Directory Replicator Lmrepl.exe
Event Log Services.exe
Messenger Services.exe
Net Logon Lsass.exe
Network DDE Netdde.exe
Network DDE DSDM Netdde.exe
NT LM Security Support Provider Services.exe
Plug and Play Services.exe
Remote Access Autodial Manager RASMAN.exe
Remote Access Connection Manager RASMAN.exe
Remote Access Server Rassrv.exe
RPC Locator Locator.exe
RPC Service RpcSs.exe
Schedule Atsvc.exe
Server Services.exe
Spooler Spoolss.exe
TCP/IP NetBIOS Helper Services.exe
Telephony Service TAPISrv.exe
UPS Ups.exe
Workstation Services.exe

Несколько слов о безопасности

      В зависимости от сервиса, который Вы разрабатываете, Вам, возможно, потребуются знания об архитектуре безопасности Windows NT. В Windows NT безопасность подогнана под пользователя. Другими словами, любой запущенный в системе процесс, поток, файл, ключ реестра, мьютекс, семафор, событие и т.д. является собственностью пользователя. При вызове процесса, он запускается либо от имени пользователя компьютера (пользователя домена сети), либо от имени специального пользователя, имеющего учетную запись системы (System Account).

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

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

      Например, в системе имеется приложение WinLogon.exe, которое запускается системой в процессе начальной загрузки. Данное приложение запускается от имени системы и предназначено для отображения на экране диалогового окна ввода имени пользователя и пароля. После получения имени и пароля, WinLogon.exe отправляет эту информацию в специальную базу системы для проверки. Если пользователь в базе найден, то WinLogon.exe запускает приложение Explorer.exe в контексте учетной записи входящего в систему пользователя. Пользователь, входящий в систему таким образом (посредством ввода имени и пароля), называется интерактивным пользователем, поскольку он пользуется для этого клавиатурой и мышью.

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

Три сервисных компонента

      Для обеспечения работы сервисов предназначены три вида компонентов. Первый из них - "Контролёр сервисов" (Service Control Manager - SCM). Он имеется в каждой операционной системе Windows семейства NT и его код расположен в файле Services.exe. В момент начальной загрузки системы, он автоматически запускается и прекращает работу в момент выгрузки системы. Данный процесс работает с привилегиями системы и предоставляет унифицированный и безопасный способ общения с сервисами системы. SCM предназначен для взаимодействия с сервисами. Это своего рода "командир", который приказывает сервисам начать, прервать, продолжить или прекратить работу и т.д.

      Второй компонент - сам "сервис". Сервис, являясь по сути Win32 приложением, содержит дополнительный код, позволяющий ему принимать информацию и команды от SCM. Код сервиса кроме этого содержит вызовы функций, позволяющих отсылать информацию о статусе сервиса обратно SCM.

      Третий и последний компонент - "Программа контроля сервиса" (Service Control Program - SCP (далее SCP-приложение прим.пер.)). Это Win32 приложение, которое предоставляет пользователю графический интерфейс для работы с сервисами. С помощью ее пользователь может запустить, приостановить, продолжить или прекратить работу любого сервиса в системе. SCP содержит набор специальных функций, позволяющих обращаться к SCM. SCP-апплет, окно которого Вы видите ниже, является SCP-приложением, которое поставляется с Windows NT (в данном случае с Windows XP - прим.пер.).

scp.jpg

рис.1 Апплет контрольной панели сервисов

      В правой части окна апплета, в списке сервисов, в поле Name содержится наименование сервиса, в поле Description - его описание (для чего предназначен), в поле Status - режим, в котором сервис находится в данный момент (запущен (started), приостановлен (paused) или остановлен (пусто)), в поле Startup Type - режим запуска сервиса (автоматический (automatic), ручной (manual) или отключен (desabled)), в поле Logon As - учетная запись, от имени которой запущен сервис. Используя апплет, Вы можете запустить остановленный сервис, остановить запущенный сервис, приостановить запущенный сервис или возобновить работу приостановленного сервиса. Если Вы запускаете сервис вручную, то Вам, возможно, понадобится передать сервису какие-нибудь параметры. Для этого в меню выберите Action-->Properties... (или Properties в контекстном меню правой кнопки мыши или дважды кликните на названии сервиса в окне). Появится диалоговое окно конфигурации сервиса. В поле ввода Start Parameters Вы можете ввести дополнительные параметры необходимые для запуска сервиса. (Скорее всего Вам это не понадобится, т.к. большинство сервисов не используют параметры).

      Вам врядли понадобится писать SCP-приложение самим, поскольку фирма Microsoft побеспокоилась об этом. Поскольку SCM является RPC-сервером, то SCP-приложение может общаться с ним удаленно. Администратор, используя SCP-приложение на компьютере A, может посредством SCM компьютера B управлять работой сервисов компьютера B.

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

NET START     servicename
NET PAUSE     servicename
NET CONTINUE  servicename
NET STOP      servicename

      Кроме этого с помощью net.exe Вы можете вывести список всех сервисов, работающих в системе, набрав в командной строке:

NET START

      без указания имени сервиса

      В дополнение, со всеми версиями Windows NT Resource Kit поставляется консольное SCP-приложение (sc.exe). При использовании без параметров, оно покажет Вам справку по пользованию, как это показано ниже:

DESCRIPTION:
	SC is a command line program used for communicating with the 
	NT Service Controller and services.
USAGE:
	sc <server> [command] [service name] <option1> <option2>...

	The option <server> has the form "\\ServerName"
	Further help on commands can be obtained by typing: "sc [command]"
	Commands:
	  query-----------Queries the status for a service, or 
	                  enumerates the status for types of services.
	  queryex---------Queries the extended status for a service, or 
	                  enumerates the status for types of services.
	  start-----------Starts a service.
	  pause-----------Sends a PAUSE control request to a service.
	  interrogate-----Sends an INTERROGATE control request to a service.
	  continue--------Sends a CONTINUE control request to a service.
	  stop------------Sends a STOP request to a service.
	  config----------Changes the configuration of a service (persistant).
	  description-----Changes the description of a service.
	  failure---------Changes the actions taken by a service upon failure.
	  qc--------------Queries the configuration information for a service.
	  qdescription----Queries the description for a service.
	  qfailure--------Queries the actions taken by a service upon failure.
	  delete----------Deletes a service (from the registry).
	  create----------Creates a service. (adds it to the registry).
	  control---------Sends a control to a service.
	  sdshow----------Displays a service's security descriptor.
	  sdset-----------Sets a service's security descriptor.
	  GetDisplayName--Gets the DisplayName for a service.
	  GetKeyName------Gets the ServiceKeyName for a service.
	  EnumDepend------Enumerates Service Dependencies.

	The following commands don't require a service name:
	sc <server> <command> <option> 
	  boot------------(ok | bad) Indicates whether the last boot should
	                  be saved as the last-known-good boot configuration
	  Lock------------Locks the Service Database
	  QueryLock-------Queries the LockStatus for the SCManager Database
EXAMPLE:
	sc start MyService

Начало работы сервиса

      Перед запросом к SCM запустить сервис, информация о сервисе должна присутствовать в базе данных SCM. Эта база данных расположена в реестре в ключе: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. Приложения не должны напрямую с помощью функций, предназначенных для работы с реестром, модифицировать данные в этом ключе. Вместо этого SCP-приложения должны использовать специальный набор функций, обращающихся к SCM с "приказом" на изменение данных в базе. Во время установки на компьютер приложения, в состав которого входит Win32 сервис, программа установки должна быть SCP-приложением, которая посредством таких функций и SCM добавляет информацию об устанавливаемом сервисе в базу данных SCM.

      Как только данные о сервисе внесены в базу данных SCM, SCP-приложение (например, SCP-апплет) получает возможность извлекать информацию об установленном сервисе. Кроме отображения общей информации, показанной на рис.1, апплет позволяет производить конфигурацию сервиса. Дважды кликнув левой кнопкой мыши на названии сервиса, Вы вызовите диалоговое окно Properties сервиса (рис.2). В этом окне сверху отображается название сервиса, а в поле Startup Type Вы можете задать режим запуска сервиса - автоматический (automatic), ручной (manual) или отключен (disabled).

scp_prop.jpg

рис.2 Диалоговое окно конфигурации сервиса

      Кроме того, с помощью кнопок Start, Stop, Pause и Resume Вы можете соответственно запустить, остановить, приостановить и продолжить выполнение сервиса. А используя вторую вкладку окна задать настройки учетной записи, в контексте безопасности которой будет работать сервис.

      Главной отличительной особенностью сервисов NT, в отличие от обычных Win32 приложений, является то, что операционная система может сама запустить сервис на выполнение. В случае, если режим запуска сервиса отмечен в базе данных SCM как автоматический (automatic), система попытается запустить его сама в момент начальной загрузки. Важно понять, что автоматический запуск сервисов производится системой до момента входа пользователя в систему. Факт, что некоторые сервера сетей, устанавливаются и конфигурируются таким образом, что из работающих приложений на них присутствуют только сервисы. Никто и никогда даже не входит в систему таких серверов интерактивно. Они могут даже не иметь подсоединенных клавиатуры, мыши и монитора. Но, благодаря работающим на них сервисам, пользователи сети имеют возможность использовать файлы и принтеры таких серверов. Во время загрузки операционной системы сервера, сервисы запускаются автоматически и, впоследствии, могут обслуживать запросы клиентов сети не требуя интерактивного входа каждого из клиентов на сервер.

      Сервис также может находиться в режиме ручного (manual) запуска. Это означает, что при загрузке система не пытается запустить такой сервис на выполнение. Поэтому, если во время работы у пользователя появляется необходимость в данном сервисе, он должен запустить его вручную (например, с помощью SCP-апплета). Но бывает и так, что сервис, находящийся в режиме ручного запуска, стартует при загрузке системы. Это может случиться, если другому сервису с автоматическим запуском для нормальной работы необходим такой сервис. Это называется зависимостью сервисов. Зависимости также можно просмотреть, используя четвертую вкладку Dependencies диалогового окна конфигурации сервиса. (Позже я расскажу более подробно о зависимостях).

scp_dep.jpg

рис.3 Отображение зависимостей сервиса

      И в конце концов, сервис может быть установлен как отключенный (disabled). Это означает, что он не может быть запущен вовсе. Например, Вы можете отключить сервис DHCP Client, если Вы вручную ("жестко") присваиваете компьютеру IP-адрес, вместо того, чтобы получать его динамически от DHCP-сервера.

      В дополнение к режиму запуска, Вы можете указать под чьей учетной записью будет запускаться сервис (рис.3). Большинство сервисов выполняются в контексте системной учетной записи - System Account - (по-умолчанию). Это означает, что сервис может делать все что угодно, но только в пределах данного компьютера. Если сервис запускается от имени системы, то у Вас появляется возможность задействовать опцию интерактивного взаимодействия с рабочим столом (Allow Service to Interact with Desktop). Для большинства сервисов эта опция не задействована.

scp_acnt.jpg

рис.3 Настройка учетной записи

      Помните, я сказал, что сервис не должен иметь пользовательского интерфейса? Так вот, есть сервисы, обладающие пользовательским интерфейсом. В моей операционной системе есть два таких сервиса - Remote Access Connection Manager и Spooler. При использовании этих сервисов просто необходимо, чтобы пользователь находился перед экраном компьютера. Сервис Spooler предназначен для отсылки на принтер заданий печати. Когда принтер заканчивает печатать задание, либо если в принтере отсутствует бумага, сервис посредством диалогового окна сообщает об этом пользователю. Для того, чтобы пользователь смог увидеть сообщения, посылаемые сервисом, в его настройках должна быть установлена опция Allow service to interact with desktop. (Как было сказано выше, эта опция становится доступной только для сервисов, запускаемых от имени системы. прим.перев.)

      Если сервис запущен от имени системы, то он имеет ограниченный доступ к сетевым ресурсам - таким как, например, папки и каналы (pipes) общего пользования. В таком случае сервис все же может получить доступ к таким ресурсам, используя NULL-сессию. Для этого необходимо открыть доступ клиентам с NULL-сессией к таким папкам (каналам), путем модификации значений NullSessionPipes и NullSessionShares в ключе реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters. Если же Вы желаете открыть доступ таким клиентам ко всем папкам (каналам) общего доступа, то присвойте 0 значению RestrictNullSessionAccess в этом же ключе реестра. Небольшое дополнение: сервис, запущенный от имени системы, не имеет доступа к ветке реестра HKEY_CURRENT_USER, но зато он может спокойно открывать ключ HKEY_LOCAL_MACHINE\Security.

      В случае, если Вам не хватает возможностей локальной машины и необходим доступ к сетевым ресурсам, без использования NULL-сессии, на вкладке "Log On" диалогового окна конфигурации сервиса (см. рис.3), установите опцию This account. А затем в поле справа введите имя пользователя (учетной записи), а в поля Password и Confirm password - пароль пользователя. Если введенные имя и пароль валидны для домена сети, то запущенный от лица данного пользователя сервис получит доступ ко всем ресурсам сети, к которым имеет доступ данный пользователь.

      Некоторые сервисы, такие как DHCP Client, Messenger или Alerter, чрезвычайно просты в реализации. И было бы слишком расточительно для каждого из них создавать отдельный сервис (читай исполняемый файл), обладающий собственным виртуальным пространством в 4 Гб. Для устранения такой расточительности, фирма Microsoft дала возможность разрабатывать сервисы, которые могли бы содержать в себе несколько сервисов. Например, сервис Services.exe содержит в себе около 10 других сервисов, включая три, упомянутые выше. Но эта оптимизация имеет один минус - SCM позволяет таким сервисам работать только от имени системы. Иными словами, в настройках сервиса Вы не получите возможность указать другие имя пользователя и пароль.

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

Разработка Win32 сервиса

      В этом разделе я расскажу как нужно создавать сервисы таким образом, чтобы использовать все возможности, которые им предоставляет система Windows NT. Имеются три основных функции, составляющие костяк любого сервиса. Первая функция является входной точкой процесса - WinMain (main). Основной поток процесса запускает эту функцию на выполнение. Она же, в свою очередь, ответственна за инициализацию всего процесса. Имеется в виду, за все, что содержит в себе сервис. Вспомните, что один файл сервиса может быть всего лишь оболочкой для "кучи" сервисов, которые необходимо подготовить к работе в момент инициализации этой самой оболочки. Основной поток, кроме этого вызывает API-функцию, которая сообщает SCM сколько именно сервисов содержится внутри исполняемого файла оболочки и отправляет ему адреса функций обратного вызова (Callback-функций) ServiceMain каждого сервиса. Когда все сервисы исполняемого файла оболочки прекращают работу (по причине остановки), основной поток проводит очистку всех ресурсов, использовавшихся процессом, до того, как сам процесс будет выгружен.

      Вторая важная функция сервиса - ServiceMain - имеет следующий прототип:

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

      Эта функция вызывается самой операционной системой и содержит в себе код, который обязан привести в исполнение сам сервис. Вы можете назвать эту функцию как Вам угодно, лишь бы она соответствовала прототипу. Вы не должны сами вызывать эту функцию, поскольку функция WinMain (main) отправляет SCM адреса всех функций ServiceMain, имеющихся в сервисе. Если в исполняемом файле оболочки содержится четыре сервиса, значит Вы должны определить четыре функции ServiceMain, адреса которых будут переданы SCM.

      Для запуска функции ServiceMain предназначен отдельный поток. Когда основной поток вызывает API-функцию StartServiceCtrlDispatcher, SCM порождает отдельный поток для каждого сервиса в процессе. Каждый из этих потоков исполняется в рамках функции ServiceMain сервиса. Вот почему сервисы всегда многопоточные. Исполняемый файл, содержащий в себе только лишь один сервис, уже имеет два потока - один основной и второй для сервиса.

      Третья важная функция сервиса - Handler - должна иметь следующий прототип:

VOID WINAPI Handler( DWORD fdwControl );

      Также как и функция ServiceMain, эта функция является функцией обратного вызова (callback-функцией) и Вы должны для каждого сервиса определить отдельную функцию Handler. Таким образом, если в исполняемом файле содержится два сервиса, то в нем должно быть пять функций - одна функция WinMain (main), две функции ServiceMain и две функции Handler.

      SCM вызывает функцию Handler для изменения статуса сервиса. Например, если пользователь, используя SCP-апплет, останавливает сервис, функция Handler сервиса получает уведомление SERVICE_CONTROL_STOP. Функция Handler ответственна за действия, необходимые для безопасной остановки сервиса. Основной поток процесса запускает на выпонение все функции Handler, имеющиеся в исполняемом файле. Ваша задача - реализовать код функции Handler таким образом, чтобы она отработала как можно быстрее. Чтобы другие функции Handler, имеющиеся в исполняемом файле, получили возможность на выполнение в течение заданного отрезка времени.

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

Окунемся поглубже

      Ну вот, с основными понятиями мы разобрались. Теперь займемся деталями. Внутри функции WinMain (main) мы инициализируем структуру SERVICE_TABLE_ENTRY, которая выглядит так:

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

      Первый параметр структуры - имя сервиса, а второй - адрес callback-функции ServiceMain. Поскольку шаблон процесса содержит только один сервис, то должно быть два элемента структуры SERVICE_TABLE_ENTRY в массиве - один для сервиса и второй нулевой (NULL) для указания конца массива.

      Затем, адрес этого массива мы передаем функции StartServiceCtrlDispatcher:

BOOL StartServiceCtrlDispatcher( LPSERVICE_TABLE_ENTRY lpServiceStartTable );

      Эта API-функция, как и исполняемый процесс, посылает SCM уведомления от сервисов, находящихся в процессе. StartServiceCtrlDispatcher создает новый поток для каждого ненулевого элемента массива SERVICE_TABLE_ENTRY, переданного ей в качестве параметра. Созданный поток, начинает свою работу с функции ServiceMain, которая определена в члене lpServiceProc структуры.

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

      После отработки функция StartServiceCtrlDispatcher не сразу возвращает управление в функцию WinMain (main). Вместо этого она создает цикл (наподобии цикла сообщений стандартных исполняемых файлов см. код ниже).

Цикл StartServiceCtrlDispatcher

// нулевые значения массива не читаем int nNumRunningServices = NumElementsInServiceStartTable - 1; while (nNumRunningServices > 0) { WaitForAServiceControlCodeOrAServiceThreadToTerminate(); if (AServiceControlCode) { RemoveServiceControlCodeFromQueue() CallServiceHandler(fdwControlCode); } else { nNumRunningServices--; } } return(TRUE); // StartServiceCtrlDispatcher возвращает управление в WinMain/main

      Находясь в этом цикле функция StartServiceCtrlDispatcher как бы "усыпляет" сама себя в ожидании одного из двух событий.

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

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

      Обычно функция ServiceMain игнорирует оба передаваемых ей параметра, поскольку сервисы чаще всего не используют параметров вовсе. Лучшим способом настройки сервиса является получение параметров настройки из реестра. Сервис должен использовать функции доступа к реестру для поиска своих параметров в ключе HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters, где ServiceName - название сервиса. Конечно, Вы можете написать клиентское приложение, имеющее пользовательский интерфейс, с помощью которого пользователь сможет изменять настройки Вашего сервиса. В таком случае клиентское приложение должно сохранять параметры настройки сервиса в реестре, откуда сервис впоследствии сможет их получить. Работающий сервис может использовать API-функцию RegNotifyChangeKeyValue для получения уведомлений о внесении изменений в реестр. Это позволит сервису изменять свои параметры, как говорится, "на лету".

      Первым делом, функция ServiceMain должна сообщить SCM адрес callback-функции Handler сервиса, посредством вызова функции RegisterServiceCtrlHandler:

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler( LPCTSTR lpServiceName, LPHANDLER_FUNCTION lpHandlerProc );

      в которой первый параметр указывает за каким сервисом закреплена Handler-функция, а второй - адрес Handler-функции. Параметр lpServiceName должен быть таким же, как имя сервиса в массиве структур SERVICE_TABLE_ENTRY, который Вы передали функции StartServiceCtrlDispatcher. Более подробно о функции Handler мы поговорим позже, а сейчас сконцентрируемся на том, что делает функция ServiceMain после этого.

      Функция RegisterServiceCtrlHandler возвращает значение SERVICE_STATUS_HANDLE, которое является просто 32-битной величиной и используется SCM для уникальной идентификации сервиса. Если у сервиса появляется необходимость сообщить SCM свой текущий статус, Вы должны передать эту величину в качестве идентификатора сервиса (service handle) желаемой API-функции. Имейте в виду, что в отличие от других идентификаторов объектов, присутствующих в системе Windows, Вы не должны явным образом закрывать идентификатор сервиса, возвращаемый функцией RegisterServiceCtrlHandler.

      SCM требует, чтобы поток функции ServiceMain осуществил вызов функции RegisterServiceCtrlHandler в течение 1 секунды. Иначе, SCM решит, что в реализации сервиса присутствует ошибка. В этом случае SCM не завершает работу сервиса (как можно было бы ожидать прим. пер.); сервис вроде бы продолжает работать. Но, если Вы попытаетесь с помощью, например, SCP-апплета запустить сервис, то увидите сообщение об ошибке, которое известит Вас о том, что сервис запустить невозможно. Если Вы закроете SCP-апплет и, затем, снова его откроете, то увидите, что информация о статусе сервиса обновилась и отображает корректные данные. (В Windows XP SCP-апплет уже содержит кнопку Refresh, позволяющую избежать закрытия и повторного открытия SCP-апплета. Прим.пер.).

      По возвращении функцией RegisterServiceCtrlHandler управления функции ServiceMain, поток последней должен незамедлительно сообщить SCM о том, что инициализация сервиса продолжается. Для этого используется функция SetServiceStatus, имеющая следующий прототип:

BOOL SetServiceStatus( SERVICE_STATUS_HANDLE hService, LPSERVICE_STATUS lpServiceStatus );

      Эта функция требует в качестве первого параметра идентификатор сервиса, полученный Вами ранее при вызове функции RegisterServiceCtrlHandler, а в качестве второго параметра - адрес подготовленной структуры SERVICE_STATUS, имеющей следующее описание:

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

      Эта структура содержит семь членов, отражающих текущий статус сервиса. Очень внимательно заполняйте эту структуру перед передачей ее адреса в SetServiceStatus.

      Член структуры dwServiceType показывает тип Вашего сервиса и может принимать значения, перечисленные в таблице ниже.

Флаг Значение Описание
SERVICE_WIN32_OWN_PROCESS 0x00000010 Сервис запускается в своем собственном процессе
SERVICE_WIN32_SHARE_PROCESS 0x00000020 Сервис делит процесс с другими сервисами
SERVICE_WIN32 SERVICE_WIN32_OWN_PROCESS |
SERVICE_WIN32_SHARE_PROCESS
Является комбинацией указанных выше типов сервисов
SERVICE_KERNEL_DRIVER 0x00000001 Сервис является драйвером устройства
SERVICE_FILE_SYSTEM_DRIVER 0x00000002 Сервис является драйвером файловой системы
SERVICE_ADAPTER 0x00000004 Сервис является драйвером адаптера
SERVICE_RECOGNIZER_DRIVER 0x00000008 Сервис является драйвером распознающего устройства
SERVICE_DRIVER SERVICE_KERNEL_DRIVER |
SERVICE_FILE_SYSTEM_DRIVER |
SERVICE_RECOGNIZER_DRIVER
Комбинация драйверных сервисов
SERVICE_INTERACTIVE_PROCESS 0x00000100 Сервис может взаимодействовать с рабочим столом пользователя
SERVICE_TYPE_ALL SERVICE_WIN32 |
SERVICE_ADAPTER |
SERVICE_DRIVER |
SERVICE_INTERACTIVE_PROCESS
Является комбинацией всех вышеперечисленных типов сервисов

      Установливайте флаг SERVICE_WIN32_OWN_PROCESS в случае, если Ваш исполняемый файл содержит только один сервис, если же сервисов более одного, то устанавливайте флаг SERVICE_WIN32_SHARE_PROCESS. В дополнение к этим двум флагам, с помощью оператора OR, Вы можете установить флаг SERVICE_INTERACTIVE_PROCESS. Это позволит Вашему сервису отправлять диалоговые сообщения пользователю (я настоятельно рекомендую использовать флаг SERVICE_INTERACTIVE_PROCESS как можно реже). После установки флага (набора флагов), Вы не должны его менять до окончания работы сервиса.

      Член структуры dwCurrentState - наиболее важный член структуры SERVICE_STATUS. Он "подсказывает" SCM текущий статус сервиса. Для указания того, что сервис находится в процессе инициализации, Вам необходимо установить значение этого члена в SERVICE_START_PENDING. Остальные возможные значения Вы можете увидеть в таблице ниже:

Флаг Значение Описание
SERVICE_STOPPED 0x00000001 Сервис не запущен
SERVICE_START_PENDING 0x00000002 Сервис находится в процессе запуска
SERVICE_STOP_PENDING 0x00000003 Сервис находится в процессе остановки
SERVICE_RUNNING 0x00000004 Сервис запущен и работает
SERVICE_CONTINUE_PENDING 0x00000005 Сервис находится в процессе повторного запуска
SERVICE_PAUSE_PENDING 0x00000006 Сервис находится в процессе установки паузы
SERVICE_PAUSED 0x00000007 Сервис приостановлен

      Член структуры dwControlsAccepted показывает какого рода уведомления Ваш сервис готов получать. Если Вы собираетесь в дальнейшем использовать опцию SCP-приложения "приостановить/продолжить", (pause/continue), то используйте флаг SERVICE_ACCEPT_PAUSE_CONTINUE. Многие сервисы не поддерживают возможность приостановки сервиса, поэтому Вы сами должны решить нужна ли Вашему сервису такая возможность. При желании дать возможность SCP-приложению останавливать (stop) сервис, установите флаг SERVICE_ACCEPT_STOP. Если же Ваш сервис должен принимать уведомления о прекращении работы операционной системы, установите флаг SERVICE_ACCEPT_SHUTDOWN. Вы также можете использовать оператор OR для комбинирования флагов. Все они перечислены в таблице ниже:

Флаг управляющего кода Значение флага Описание
SERVICE_ACCEPT_STOP 0x00000001 Сервис может быть остановлен. Флаг позволяет сервису получать уведомление SERVICE_CONTROL_STOP
SERVICE_ACCEPT_PAUSE_CONTINUE 0x00000002 Сервис может быть приостановлен и запущен повторно. Флаг позволяет сервису получать уведомления SERVICE_CONTROL_PAUSE и SERVICE_CONTROL_CONTINUE
SERVICE_ACCEPT_SHUTDOWN 0x00000004 Сервис получает уведомление о завершении работы ОС. Флаг позволяет сервису получать уведомление SERVICE_CONTROL_SHUTDOWN. Имейте в виду: такое уведомление посылается только самой операционной системой
SERVICE_ACCEPT_PARAMCHANGE 0x00000008 Windows 2000/XP: Сервис может считывать свои параметры настроек из реестра без необходимости остановки и перезапуска. Флаг позволяет сервису получать уведомление SERVICE_CONTROL_PARAMCHANGE
SERVICE_ACCEPT_NETBINDCHANGE 0x00000010 Windows 2000/XP: Сервис является сетевым компонентом, который может реагировать на изменения привязки к сетевым ресурсам без необходимости остановки и перезапуска. Флаг позволяет сервису получать уведомления SERVICE_CONTROL_NETBINDADD, SERVICE_CONTROL_NETBINDREMOVE, SERVICE_CONTROL_NETBINDENABLE и SERVICE_CONTROL_NETBINDDISABLE
SERVICE_ACCEPT_HARDWAREPROFILECHANGE 0x00000020 Windows 2000/XP: Сервис получает уведомления об изменении профиля оборудования компьютера. Это позволяет системе отправлять сервису уведомление SERVICE_CONTROL_HARDWAREPROFILECHANGE. Сервис может получить это уведомление только, если был запущен функцией RegisterServiceCtrlHandlerEx. Функция ControlService не может послать такое уведомление.
SERVICE_ACCEPT_POWEREVENT 0x00000040 Windows 2000/XP: Сервис получает уведомления о смене режима питания компьютера. Это позволяет системе посылать сервису уведомление SERVICE_CONTROL_POWEREVENT. Сервис может получить это уведомление только, если был запущен функцией RegisterServiceCtrlHandlerEx. Функция ControlService не может послать такое уведомление.
SERVICE_ACCEPT_SESSIONCHANGE 0x00000080 Windows XP: Сервис получает уведомления об изменении статуса сессии. Это позволяет системе посылать сервису уведомление SERVICE_CONTROL_SESSIONCHANGE.

      Члены структуры dwWin32ExitCode и dwServiceSpecificExitCode позволяют сервису рапортовать об ошибках. Если возникает ошибка, код которой описан в файле WinError.h, код ошибки присваивается члену dwWin32ExitCode. В случае возникновения специфичной (не описанной в WinError.h) ошибки, члену dwWin32ExitCode присваивается ERROR_SERVICE_SPECIFIC_ERROR, а члену структуры dwServiceSpecificExitCode присваивается значение специфичной ошибки. Если сервис работает нормально и без сбоев, члену структуры dwWin32ExitCode необходимо присвоить значение NO_ERROR.

      О процессе запуска сервиса можно узнать благодаря двум последним членам структуры - dwCheckPoint и dwWaitHint. Когда Вы устанавливаете dwCurrentState в SERVICE_START_PENDING, то должны установить значение члена dwCheckPoint в 0, а значению dwWaitHint присвоить количество миллисекунд, необходимых сервису для полной загрузки и старта. После полной инициализации сервиса, Вы должны реинициализировать структуру SERVICE_STATUS, установив dwCurrentState в SERVICE_RUNNING, а dwCheckPoint и dwWaitHint обнулить.

      Наличие члена dwCheckPoint позволяет сервису рапортовать о процессе инициализации. Каждый раз, при вызове SetServiceStatus, Вы можете увеличивать на единицу значение dwCheckPoint. В результате значение dwCheckPoint покажет Вам на какой стадии загрузки находится сервис в данный момент времени. Вы и только Вы должны решать, как часто сервис должен отчитываться о своем состоянии. Для этого предназначен член структуры dwWaitHint. Установите его в значение, указывающее количество миллисекунд, которое, по Вашему мнению, необходимо для завершения очередного шага инициализации.

      По завершении инициализации, сервис вызывает функцию SetServiceStatus с параметром SERVICE_RUNNING - с этого момента сервис уже работает. Обычно работа сервисов заключена в цикле. Внутри этого цикла поток сервиса "спит" в ожидании запроса из сети, либо уведомления об остановке, паузе, завершении работы операционной системы и т.п. При получении запроса или уведомления, поток сервиса "просыпается", выполняет необходимые операции, затем снова входит в цикл и "засыпает" в ожидании нового запроса/уведомления.

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

      Вот теперь у нас осталась последняя функция, которую мы должны рассмотреть - функция Handler:

VOID WINAPI Handler( DWORD fdwControl );

      SCM получает и сохраняет адрес этой callback-функции, когда ServiceMain вызывает RegisterServiceCtrlHandler. SCP-приложение вызывает API-функцию, которая предписывает SCM как контролировать сервис. В настоящий момент фирма Microsoft определила пять стандартных флагов уведомлений сервиса (На момент перевода статьи мне было известно о десяти. Прим.пер.):

Флаг кода уведомления Значение флага Описание
SERVICE_CONTROL_STOP 0x00000001 Предписывает сервису остановиться.
SERVICE_CONTROL_PAUSE 0x00000002 Предписывает сервису сделать паузу.
SERVICE_CONTROL_CONTINUE 0x00000003 Предписывает сервису продолжить работу с места остановки (паузы).
SERVICE_CONTROL_INTERROGATE 0x00000004 Предписывает сервису "доложить" о своем текущем статусе SCM. Идентификатор hService должен иметь доступ SERVICE_INTERROGATE.
SERVICE_CONTROL_SHUTDOWN 0x00000005 Предписывает сервису "освежить" информацию о своем текущем статусе в SCM. Это уведомление должы поддержить все создаваемые сервисы.
SERVICE_CONTROL_PARAMCHANGE 0x00000006 Windows 2000/XP: Уведомляет сервис об изменении одного из параметров настройки. Сервис должен зановос читать параметры из реестра.
SERVICE_CONTROL_NETBINDADD 0x00000007 Windows 2000/XP: Уведомляет сетевой сервис о появлении нового компонента для привязки. Сервис должен "привязаться" к появившемуся компоненту.
SERVICE_CONTROL_NETBINDREMOVE 0x00000008 Windows 2000/XP: Уведомляет сетевой сервис об удалении связанного компонента сети. Сервис должен проверить все свои связи и разорвать связь с несуществующим компонентом.
SERVICE_CONTROL_NETBINDENABLE 0x00000009 Windows 2000/XP: Уведомляет сетевой сервис о том, что заблокированный до этого момента компонент сети, стал доступен. Сервис должен проверить свои связи и совершить привязку к появившемуся компоненту сети.
SERVICE_CONTROL_NETBINDDISABLE 0x0000000A Windows 2000/XP: Уведомляет сетевой сервис о том, что один из компонентов сети, к которому он привязан, заблокирован. Сервис должен проверить все свои связи и удалить, ставшую ненужной связь с компонентом.

      В дополнение к указанным выше кодам уведомлений, Вы можете определить свои собственные, которые должны иметь значение от 128 до 255. Функция Handler полностью отвечает за обработку посланных сервису уведомлений. Как долго она будет обрабатывать то или иное уведомление, зависит от самого уведомления.

      Когда функция Handler получает уведомление SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE или SERVICE_CONTROL_CONTINUE, она вызывает функцию SetServiceStatus для изменения статуса сервиса и получения информации о том, как много времени сервису понадобится для выполнения операции. На то время, пока сервис будет обрабатывать уведомление, вы должны установить значение члена dwCurrentState структуры SERVICE_ STATUS в SERVICE_STOP_PENDING, SERVICE_PAUSE_PENDING или SERVICE_START_PENDING соответственно.

      Пока же происходит остановка сервиса, Вы должны также определиться сколько это займет времени. Это необходимо по причине того, что сервис может быть занят какой-нибудь длительной операцией - ожидать ответа от сетевого компонента, базы данных или находиться в режиме копирования данных на диск и т.п. Отследить время, которое ему понадобится на завершение операции, можно воспользовавшись членами структуры SERVICE_ STATUS - dwCheckPoint и dwWaitHint (о них я рассказывал ранее).

      По завершении выполнения остановки, паузы или запуска сервиса, нужно снова вызвать SetServiceStatus, но на этот раз установить значение члена dwCurrentState структуры SERVICE_ STATUS в SERVICE_STOPPED, SERVICE_PAUSED или SERVICE_RUNNING соответственно. После этого обязательно обнулите dwCheckPoint и dwWaitHint.

      Когда Handler получает уведомление SERVICE_INTERROGATE, она должна просто подтвердить текущий статус сервиса, путем установки dwCurrentState в текущий статус сервиса, перед вызовом SetServiceStatus. Перед вызовом последней обнулите dwCheckPoint и dwWaitHint.

      При завершении работы операционной системы функция Handler получает уведомление SERVICE_SHUTDOWN. Это уведомление не нуждается в подтверждении. Сервис в этом случае должен как можно быстрее выполнить определенный Вами минимум операций для сохранения данных и очистки ресурсов. По умолчанию система выделяет сервисам всего 20 секунд для завершения работы. Если сервис не уложился за выделенное время, система вызывает TerminateProcess и насильственным образом "убивает" процесс сервиса. Кстати, вы можете изменить временной интервал, выделяемый системой в ключе реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control.

      При получении уведомления, определенного пользователем (в интервале от 128 до 255), функция Handler должна вызвать, пользовательскую процедуру обработки данного уведомления. В этом случае нельзя вызывать SetServiceStatus, если только пользовательская процедура не влияет на вышеперечисленные состояния сервиса.

      Основной поток сервиса, получая уведомления, запускает функцию Handler. Но при этом поток функции ServiceMain должен соответствующим образом обработать это уведомление. Например, написанный Вами сервис может заниматься обработкой клиентских запросов, поступающих по именованному каналу. Поток сервиса "засыпает" в ожидании клиентского запроса. В это время поток функции Handler получает уведомление SERVICE_CONTROL_STOP. Как в этом случае правильно завершить работу сервиса? Я видел много разработчиков, которые в этом случае просто вызывали TerminateThread прямо из функции Handler. Так вот, каждый из Вас должен знать - функция TerminateThread совершенно не подходит для данной операции, т.к. она не дает никакого шанса потоку сделать очистку ресурсов. Стэк потока не уничтожается, поток не может освободить объекты ядра, которые он использовал до вызова функции, задействованные DLL не получают никакого уведомления о завершении работы потока и т.д.

      Правильным способом остановить сервис в этом случае будет следующий. Нужно каким-то образом "разбудить" поток, убедиться в том, что он готов в настоящий момент к остановке, последовательно очистить ресурсы и затем завершить работу потока и, соответственно, сервиса. Все это означает, что Вы должны иметь своего рода связь между функцией Handler и функцией ServiceMain. Лучшим средством связи в этом случае могут быть порты завершения ввода/вывода (I/O completion ports). Но Вы можете использовать любой из механизмов, включая очередь асинхронных вызовов (asynchronous procedure call - APC), сокеты (sockets) или оконные сообщения (windows messages).

      Кроме этого, вызывает сомнение принцип, согласно которому, статус сервиса должен меняться только путем вызова SetServiceStatus. До сих пор не утихают споры по поводу того, где размещать вызовы SetServiceStatus. Многие из разработанных сервисов, которые мне доводилось видеть, делают сначала из функции Handler сервиса вызов функции SetServiceStatus для установки флага SERVICE_STOP_PENDING, передавая затем уведомление основному потоку сервиса. После чего, перед самым завершением работы потока, снова вызывают SetServiceStatus и устанавливают флаг SERVICE_STOPPED.

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

      Вот пример. Допустим, Ваш сервис получает уведомление SERVICE_CONTROL_PAUSE. В этом случае функция Handler устанавливает статус в SERVICE_PAUSE_PENDING и передает уведомление функции ServiceMain для обработки. Поток ServiceMain начинает обрабатывать уведомление, как вдруг поток функции Handler прерывает работу потока ServiceMain, т.к. получает уведомление SERVICE_CONTROL_STOP. Естественно Handler устанавливает статус SERVICE_STOP_PENDING и ставит уведомление в очередь для обработки функцией ServiceMain. Когда поток ServiceMain снова получает процессорное время, он завершает обработку уведомления SERVICE_CONTROL_PAUSE и возвращает код SERVICE_PAUSED. Затем, он видит, что в очереди для обработки находится SERVICE_CONTROL_STOP, останавливает сервис и рапортует кодом SERVICE_STOPPED. В результате всех этих операций, SCM получает статусы сервиса в следующем порядке:

SERVICE_PAUSE_PENDING
SERVICE_STOP_PENDING
SERVICE_PAUSED
SERVICE_STOPPED

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

      Когда я впервые начал работать с сервисами, то думал, что SCM сам предотвращает такого рода накладки. Но мои эксперименты показали, что SCM абсолютно на это "наплевать". Факт, SCM не делает ничего для упорядочивания уведомлений. Вот что я имею в виду: если сервис поставлен на паузу, попробуйте послать ему уведомление SERVICE_CONTROL_PAUSE. Конечно, с помощью SCP-апплета Вы этого сделать не сможете, т.к. он отслеживая состояния сервиса, делает кнопку "пауза" неактивной. Но если Вы используете утилиту наподобие SC.exe, то ничто не помешает Вам осуществить такую операцию. Я ожидал, что SCM пошлет утилите какой-нибудь код ошибки, но вместо этого он просто-напросто вызывает сервисную функцию Handler, передавая ей уведомление SERVICE_CONTROL_PAUSE.

      Я встречал много сервисов, написанных без оглядки на возможность поступления одного и того же уведомления подряд. Например, я видел сервис, который во время перехода в режим паузы закрывал идентификатор именованного канала. Затем сервис создавал новый объект ядра, которому передавал в качестве идентификатора, идентификатор закрытого канала. После этого, сервис, получая новое уведомление на переход в режим паузы, вызывал CloseHandle, передавая ей все тот же старый идентификатор именованного канала. Поскольку значения идентификаторов совпадали, свежесозданный объект ядра уничтожался, а сервис, входя в режим паузы, "сбоил" странным и довольно мистическим образом. Не говорю уже о том, сколько удовольствия доставила мне отладка этого сервиса.

      Для устранения такой проблемы, когда Вы получаете уведомление на остановку (stop), приостановку (pause) или продолжение (continue) работы сервиса, проверьте сначала не находится ли уже сервис в вызываемом состоянии. Если так, то не вызывайте SetServiceStatus и не выполняйте код изменения статуса - просто сделайте в этом месте выход из обработки.

      Вот еще одна ошибка, которую часто делают при разработке сервисов. Когда функция Handler получает уведомление SERVICE_CONTROL_PAUSE, она вызывает SetServiceStatus для установки SERVICE_PAUSE_PENDING. Затем Handler вызывает API-функцию SuspendThread для приостановки потока сервиса, а после снова вызывает SetServiceStatus для установки SERVICE_PAUSED. Это позволяет избежать накладок при поступлении однотипных уведомлений, поскольку все выполняется в одном потоке. Но ставит ли на паузу приостанавливаемый поток сервиса сам сервис? Да, но что тогда означает "приостановить сервис"? По правде говоря, ответ зависит от сервиса.

      Если я разрабатываю сервис, обрабатывающий клиентские запросы, приходящие из сети, для меня пауза - это прекращение получения каких-либо запросов. А что тогда делать с запросом, который обрабатывается в данный момент? Может прекратить обработку, чтобы не "завис" клиент? Если в этот момент моя функция Handler вызовет API-функцию SuspendThread, то сервисный поток окажется посредине непонятно чего; может быть он в процессе вызова malloc, пытаясь зарезервировать некоторое количество памяти. Если в это время другой поток, выполняющийся в этом же процессе также вызовет malloc, то и он также "замерзнет". А это как раз то, чего я совершенно не желаю.

      А вот еще вопрос. Считаете ли Вы, что должны иметь возможность остановить (stop) сервис, который находится в режиме паузы? Я считаю - да. И фирма Microsoft так считает, потому что SCM-апплет позволяет мне нажать "Stop" для сервиса, который стоит на паузе. Но в таком случае, как я смогу остановить сервис, поток которого "заморожен"? Только, пожалуйста, не отвечайте "Вызовом TerminateThread"!

      Исходя из своего опыта, скажу, лучший способ справиться со всеми этими вопросами - это постоянно иметь под рукой свободный идентификатор потока. И это должен быть поток процесса, а не поток функции Handler. Когда Handler получает код уведомления, то должна использовать какой-нибудь механизм коммуникации для постановки кода уведомления в очередь обработки потоком сервиса, и затем просто возвратить управление. Функция Handler никогда не должна сама вызывать SetServiceStatus. В этом случае сервис постоянно будет под контролем. Не будет проблем с однотипными уведомлениями, сервис сам будет принимать решение о том, что для него означает переход в режим паузы, он позволит остановить себя, даже если стоит на паузе, только сервис будет решать какой механизм коммуникации для него является наилучшим, а код функции Handler должен всего лишь соответствовать этому механизму.

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

      Очевидно, Вы не сможете об этом узнать, пока пользователь сервиса сам Вам не укажет на такое поведение сервиса. Разработчик сервиса - это не разработчик SCP-приложения. Так что же Вы можете сделать для устранения такой проблемы? Самое простое решение: заставить сервис работать эффективно и быстро, и держать поток в постоянной готовности к получению уведомлений.

      Кстати, вызов функции SetServiceStatus из функции Handler совершенно не решает этой проблемы. Скажем, внутри Handler Вы устанавливаете состояние сервиса в SERVICE_START_PENDING и задаете временной интервал dwWaitHint 5000 миллисекунд до момента вызова SetServiceStatus. Нет никакой гарантии, что поток сервиса в течении этого времени "проснется" и обработает уведомление. А если сервис не обработает уведомление в течение указанного времени, SCP-приложение решит, что сервис не отвечает.

Заключение

      Я верю, что теперь Вы в общих чертах понимаете что такое сервисы и как они взаимодействуют с операционной системой, с пользователями и управляющими программами. Эти знания понадобятся Вам в следующей моей статье, где я на реальном примере покажу как разработать сервис, клиента и управляющую сервисом программу. Более детальную информацию по сервисам и безопасности Вы сможете найти в Platform SDK и онлайновой библиотеке MSDN.

Перевод: Роман Панышев (irrona)


© 2005 ironahot@idknet.com - при использовании статей просьба делать ссылку на автора