Автор: Джеффри Рихтер
Данный материал впервые был опубликован в 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 - прим.пер.).
![]() |
рис.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).
![]() |
рис.2 Диалоговое окно конфигурации сервиса |
Кроме того, с помощью кнопок Start, Stop, Pause и Resume Вы можете соответственно запустить, остановить, приостановить и продолжить выполнение сервиса. А используя вторую вкладку окна задать настройки учетной записи, в контексте безопасности которой будет работать сервис.
Главной отличительной особенностью сервисов NT, в отличие от обычных Win32 приложений, является то, что операционная система может сама запустить сервис на выполнение. В случае, если режим запуска сервиса отмечен в базе данных SCM как автоматический (automatic), система попытается запустить его сама в момент начальной загрузки. Важно понять, что автоматический запуск сервисов производится системой до момента входа пользователя в систему. Факт, что некоторые сервера сетей, устанавливаются и конфигурируются таким образом, что из работающих приложений на них присутствуют только сервисы. Никто и никогда даже не входит в систему таких серверов интерактивно. Они могут даже не иметь подсоединенных клавиатуры, мыши и монитора. Но, благодаря работающим на них сервисам, пользователи сети имеют возможность использовать файлы и принтеры таких серверов. Во время загрузки операционной системы сервера, сервисы запускаются автоматически и, впоследствии, могут обслуживать запросы клиентов сети не требуя интерактивного входа каждого из клиентов на сервер.
Сервис также может находиться в режиме ручного (manual) запуска. Это означает, что при загрузке система не пытается запустить такой сервис на выполнение. Поэтому, если во время работы у пользователя появляется необходимость в данном сервисе, он должен запустить его вручную (например, с помощью SCP-апплета). Но бывает и так, что сервис, находящийся в режиме ручного запуска, стартует при загрузке системы. Это может случиться, если другому сервису с автоматическим запуском для нормальной работы необходим такой сервис. Это называется зависимостью сервисов. Зависимости также можно просмотреть, используя четвертую вкладку Dependencies диалогового окна конфигурации сервиса. (Позже я расскажу более подробно о зависимостях).
![]() |
рис.3 Отображение зависимостей сервиса |
И в конце концов, сервис может быть установлен как отключенный (disabled). Это означает, что он не может быть запущен вовсе. Например, Вы можете отключить сервис DHCP Client, если Вы вручную ("жестко") присваиваете компьютеру IP-адрес, вместо того, чтобы получать его динамически от DHCP-сервера.
В дополнение к режиму запуска, Вы можете указать под чьей учетной записью будет запускаться сервис (рис.3). Большинство сервисов выполняются в контексте системной учетной записи - System Account - (по-умолчанию). Это означает, что сервис может делать все что угодно, но только в пределах данного компьютера. Если сервис запускается от имени системы, то у Вас появляется возможность задействовать опцию интерактивного взаимодействия с рабочим столом (Allow Service to Interact with Desktop). Для большинства сервисов эта опция не задействована.
![]() |
рис.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-апплет и позволяет Вам изменять параметры запуска сервиса, делать это приходится крайне редко. По той причине, что обычно программы установки, содержащие в своем пакете сервисы, устанавливают сервисы с уже готовой конфигурацией. Лично я пользуюсь возможностью изменения параметров запуска сервисов только для отладки.
В этом разделе я расскажу как нужно создавать сервисы таким образом, чтобы использовать все возможности, которые им предоставляет система 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)