
Сервисы Windows (120
..pdf
|
|
Таблица 4 |
|
|
Флаги управляющих кодов |
||
|
|
|
|
Флаг управляющего кода |
Значение |
Описание |
|
флага |
|||
|
|
||
|
|
|
|
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 |
|
|
|
|
|
|
|
|
Окончание табл. 4 |
|
|
|
|
|
|
|
Флаг управляющего кода |
Значение |
|
|
Описание |
|
флага |
|
|
|
||
|
|
|
|
|
|
|
|
|
|||
SERVICE_ACCEPT_ |
0x00000020 |
Windows 2000/XP: сервис получает уведомления об изменении |
|||
HARDWAREPROFIL ECHANGE |
|
профиля оборудования компьютера. Это позволяет ОС отправ- |
|||
|
|
лять сервису уведомление |
|
||
|
|
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 |
|
||
|
|
|
|
|
|
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 – с этого момента сервис уже работает. Обычно работа сервисов состоит в выполнении цикла. Внутри этого цикла поток сервиса «спит» в ожидании запроса из сети либо уведомления об остановке, паузе, завершении работы ОС и т. п. При получении запроса или уведомления поток сервиса «просыпается», выполняет необходимые операции, затем снова входит в цикл и «засыпает» в ожидании нового запроса/уведомления.
В случае если сервис получает уведомление об остановке или о завершении работы ОС, его поток должен выйти из цикла и совершить очистку занимаемых сервисом ресурсов. На этом работа сервисного потока прекращается. По завершении работы потока функ-
33
ции ServiceMain «просыпается» поток, находящийся внутри функции StartServiceCtrlDispatcher. Работа потока заключается в том, чтобы уменьшить счетчик работающих сервисов.
Если для выполнения полезной работы сервису не нужен постоянно существующий поток, после успешной инициализации можно осуществить выход из функции ServiceMain.
Точка входа по обработке команд (Handler)
SCM получает и сохраняет адрес этой Сallback-функции, когда функция ServiceMain вызывает функцию RegisterServiceCtrl Handler. SCP-приложение вызывает API-функцию, которая предписывает SCM, каким образом контролировать сервис. Фирма Microsoft определила следующие значения флагов уведомления сервиса (табл. 5).
|
|
Таблица 5 |
Описание флагов и значений кодов уведомления сервиса |
||
|
|
|
Флаг и значение кода уведомления |
|
Описание |
|
|
|
SERVICE_CONTROL_CONTINUE |
Уведомляет приостановлен- |
|
0x00000003 |
ную службу о том, что следует |
|
|
возобновить работу |
|
|
|
|
SERVICE_CONTROL_INTERROGATE |
Уведомляет службу о том, что |
|
0x00000004 |
она должна сообщить сведе- |
|
|
ния о своем текущем состоя- |
|
|
нии SCM. Обработчик должен |
|
|
просто |
вернуть значение |
|
NO_ERROR |
|
|
|
|
SERVICE_CONTROL_NETBINDADD |
Уведомляет сетевой сервис о |
|
0x00000007 |
том, что имеется новый компо- |
|
|
нент для компоновки. Служба со- |
|
|
единится с новым компонентом |
|
|
|
|
SERVICE_CONTROL_NETBINDDISABLE |
Уведомляет сетевой сервис о |
|
0x0000000A |
том, что один из компонентов |
|
|
сети был заблокирован. Служ- |
|
|
ба должна проверить свои свя- |
|
|
зи и удалить ненужную связь |
|
|
|
|
SERVICE_CONTROL_NETBINDENABLE |
Уведомляет сетевой сервис о |
|
0x00000009 |
òîì, ÷òî |
заблокированный до |
|
этого момента компонент сети |
|
|
стал доступен. Сервис должен |
|
|
проверить свои связи и совер- |
|
|
шить привязку к появившемуся |
|
|
компоненту сети |
|
|
|
|
34
Окончание табл. 5
Флаг и значение кода уведомления |
|
Описание |
|
|
|
|
|
||||
SERVICE_CONTROL_NETBINDREMOVE |
Уведомляет сетевой сервис об |
||||
0x00000008 |
удалении |
связанного |
компо- |
||
|
нента сети. Сервис должен про- |
||||
|
верить все свои связи и разо- |
||||
|
рвать связь с несуществующим |
||||
|
компонентом |
|
|
||
SERVICE_CONTROL_PARAMCHANGE |
Уведомляет сервис об измене- |
||||
0x00000006 |
нии одного из параметров на- |
||||
|
стройки. Сервис должен заново |
||||
|
считать параметры из реестра |
||||
|
|
||||
SERVICE_CONTROL_PAUSE |
Предписывает сервису сделать |
||||
0x00000002 |
паузу |
|
|
|
|
|
|
||||
SERVICE_CONTROL_SHUTDOWN |
Сообщает сервису о том, что |
||||
0x00000005 |
ОС выключается и сервис мо- |
||||
|
æåò |
«очистить задачи». Если |
|||
|
служба принимает этот кон- |
||||
|
трольный код, она должна оста- |
||||
|
новиться после «очистки вы- |
||||
|
полняемых |
задач» |
è |
вернуть |
|
|
значение NO_ERROR. После |
||||
|
того как SCM посылает данный |
||||
|
контрольный код, он не будет |
||||
|
отправлять этому сервису дру- |
||||
|
гие управляющие коды |
|
|||
SERVICE_CONTROL_STOP |
Уведомляет службу о том, что |
||||
0x00000001 |
она должна остановиться. Ес- |
||||
|
ли служба принимает этот кон- |
||||
|
трольный код, она должна ос- |
||||
|
тановиться после получения и |
||||
|
вернуть значение NO_ERROR. |
||||
|
Затем SCM посылает ей кон- |
||||
|
трольный код, он не посылает |
||||
|
другие управляющие |
êîäû. |
|||
|
Windows XP/2000: åñëè ñëóæ- |
||||
|
áà |
возвращает |
значение |
||
|
NO_ERROR и продолжает ра- |
||||
|
ботать, она продолжает полу- |
||||
|
чать управляющие коды. Такое |
||||
|
поведение |
изменилось, начи- |
|||
|
íàÿ ñ ÎÑ Windows Server 2003 |
||||
|
è Windows XP ñ SP2 |
|
|
||
|
|
|
|
|
|
В дополнение к указанным выше кодам уведомлений программист может определить свои собственные, которые должны иметь значение в диапазоне от 128 до 255. Функция Handler полностью
35
отвечает за обработку посланных сервису уведомлений. Как долго она будет обрабатывать то или иное уведомление, зависит от характера уведомления.
Когда функция Handler получает уведомление SERVICE_ CONTROL_STOP, SERVICE_CONTROL_PAUSE или SERVICE_ CONTROL_CONTINUE, она вызывает функцию SetServiceStatus для изменения статуса сервиса и получения информации о том, как много времени сервису понадобится для выполнения операции. На то время, пока сервис будет обрабатывать уведомление, необходимо установить значение элемента dwCurrentState структуры SERVICE_ STATUS в SERVICE_STOP_PENDING, SERVICE_ PAUSE_PENDING или SERVICE_START_PENDING соответственно.
При остановке сервиса необходимо также определить, сколько это займет времени, так как сервис может быть занят какой-либо длительной операцией: ожидать ответа от сетевого компонента, базы данных или находиться в режиме копирования данных на диск и т. п. Отследить время, которое ему понадобится на завершение операции, можно, воспользовавшись элементами dwCheckPoint и dwWaitHint структуры SERVICE_ STATUS –.
По завершении выполнения остановки, паузы или запуска сервиса нужно снова вызвать функцию 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.
36
При получении уведомления, определенного пользователем (в диапазоне значений от 128 до 255), функция Handler должна вызвать пользовательскую процедуру обработки данного уведомления. В этом случае нельзя вызывать функцию SetServiceStatus, если только пользовательская процедура не влияет на перечисленные выше состояния сервиса. Основной поток сервиса, получив уведомления, запускает функцию Handler. Но при этом поток функции ServiceMain должен соответствующим образом обработать это уведомление. Например, написанный пользователем сервис может заниматься обработкой клиентских запросов, поступающих по именованному каналу. Поток сервиса «засыпает» в ожидании клиентского запроса. В это время поток функции Handler получает уведомление SERVICE_CONTROL_STOP. Для правильного завершения работы сервиса не следует просто вызывать функцию TerminateThread непосредственно из функции Handler, так как функция TerminateThread не дает никакой возможности потоку выполнить очистку ресурсов. Стэк потока не уничтожается, поток не может освободить объекты ядра, которые он использовал до вызова функции, и задействованные DLL (Dynamic Link Library – динамически подключаемая библиотека) не получают никакого уведомления о завершении работы потока и т. д.
Верным способом остановить сервис в этом случае является следующий. Нужно каким-то образом «разбудить» поток, убедиться в том, что он готов в настоящий момент к остановке, последовательно очистить ресурсы и затем завершить работу потока и, соответственно, сервиса. Все это означает, что необходимо иметь своего рода связь между функциями Handler и ServiceMain. Лучшим средством связи в этом случае могут быть порты завершения ввода/вывода (I/O Completion Ports). Но программист может использовать любой из механизмов, включая очередь асинхронных вызовов: asynchronous procedure call (APC), сокеты (Sockets) или оконные сообщения (Windows Messages).
Кроме того, вызывает сомнение принцип, согласно которому статус сервиса должен меняться только путем вызова функции SetServiceStatus. До сих пор не утихают споры по поводу того, где размещать вызовы функции SetServiceStatus. Практикой подтверждается такая последовательность действий: сначала из функции Handler сервиса вызывается функция SetServiceStatus для установки флага SERVICE_STOP_PENDING и передается уведомление
37
основному потоку сервиса; затем, перед самым завершением работы потока, снова вызывается функция 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 SERVIC
E_STOP_PENDING
SERVICE_PAUSED
SERVICE_STOPPED
Сервисы, работающие таким образом, тем не менее работают, по причине малой вероятности остановки сервиса, пока он находится в режиме паузы – но никаких гарантий нет. Можно предположить, что SCM сам предотвращает такого рода накладки. Но эксперименты Дж. Рихтера показали, что это не так [2]. Фактом является то, что SCM не делает ничего для упорядочивания уведомлений. Имеется в виду, что если сервис переведен в режим паузы, то послать ему уведомление SERVICE_CONTROL_PAUSE
38
с помощью SCP-апплета нельзя, так как он, отслеживая состояния сервиса, делает кнопку «пауза» неактивной. Но если попробовать использовать утилиту наподобие SC.exe, то ничто не помешает осуществить такую операцию. Можно было бы ожидать, что SCM пошлет утилите какой-либо код ошибки, но вместо этого он просто вызывает сервисную функцию Handler, передавая ей уведомление SERVICE_CONTROL_PAUSE.
Для устранения проблемы получения сервисом уведомления на остановку (Stop), приостановку (Pause) или продолжение (Continue) работы необходимо проверить, не находится ли уже сервис в вызываемом состоянии. Если так, то не следует вызывать функцию SetServiceStatus и выполнять код изменения статуса – нужно просто в этом месте выйти из обработки.
Рассмотрим еще одну ошибку, которую часто совершают при разработке сервисов. Когда функция Handler получает уведомление SERVICE_CONTROL_PAUSE, она вызывает функцию SetServiceStatus для установки SERVICE_PAUSE_PENDING. Затем функция Handler вызывает API-функцию SuspendThread для приостановки потока сервиса, а затем снова вызывает функцию SetServiceStatus для установки SERVICE_PAUSED. Это позволяет избежать накладок при поступлении однотипных уведомлений, поскольку все действия выполнятюся в одном потоке. Сервис сам контролирует собственную паузу, при этом, как он блокируется, зависит от самого сервиса.
Если сервис обрабатывает клиентские запросы, приходящие из сети, то пауза – это прекращение получения каких-либо запросов. А что тогда делать с запросом, который обрабатывается в данный момент? Можно ли прекратить обработку, чтобы не «завис» клиент? Если в этот момент функция Handler вызовет API-функцию SuspendThread, то сервисный поток может оказаться в процессе вызова Malloc, пытаясь зарезервировать некоторое количество памяти. Если в это время другой поток, выполняющийся в этом же процессе, также вызовет Malloc, то и он «заснет».
Можно ли остановить поток, находящийся в режиме паузы? Фирма Microsoft написала SCM-апплет так, что можно нажать кнопку «Стоп» (Stop) для сервиса, который находится в состоянии паузы. Лучший способ справиться с этими проблемами – постоянно иметь под рукой свободный идентификатор потока. Это должен быть поток процесса, а не поток функции Handler. Когда функция
39
Handler получает код уведомления, должен использоваться какойлибо механизм коммуникации для постановки кода уведомления в очередь на обработку потоком сервиса, затем управление возвращается. Функция Handler никогда не должна сама вызывать функцию SetServiceStatus. В этом случае сервис постоянно будет находиться под контролем. Тогда не возникнет проблем с однотипными уведомлениями, ведь сервис сам будет принимать решение о том, что для него означает переход в режим паузы. Сервис позволит остановить себя, даже если стоит на паузе, и только сервис будет решать, какой механизм коммуникации для него является наилучшим, а код функции Handler должен лишь соответствовать этому механизму.
Единственный недостаток такого подхода состоит в том, что сервис должен быстро обработать полученное уведомление. Если поток сервиса занят, например обработкой клиентского запроса, код уведомления будет ждать в очереди и функция SetServiceStatus не будет вызвана в течение продолжительного времени. Если функция SetServiceStatus не будет вызвана за отведенное время, SCP-приложе- ние решит, что сервис не отвечает, и пошлет пользователю сообщение об ошибке. Однако в сервисе ошибок нет и он продолжает работать. Сервис обработает уведомление, когда до него дойдет очередь. Но все равно в реальной жизни SCP-приложение отсылает пользователю сообщение об ошибке. Такие ошибки обнаружить невозможно. Они выявляются в процессе эксплуатации сервиса, так как разработ- чик сервиса не является разработчиком SCP-приложения. Самое простое решение этой проблемы – написание кода сервиса, обеспечи- вающего эффективную и быструю его работу. Кроме того, сервис должен иметь поток, находящийся в постоянной готовности для получения сообщений.
Отметим, что вызов функции SetServiceStatus из функции Handler не дает решения проблемы. Если внутри функции Handler будет устанавлено состояние сервиса в значении SERVICE_ START_PENDING и будет задан временной интервал dwWaitHint 5000 мс до момента вызова функции SetServiceStatus, то нет никакой гарантии, что поток сервиса в течение этого времени «проснется» и обработает уведомление. А если сервис не обработает уведомление в течение указанного времени, SCP-приложение решит, что сервис не отвечает.
40