Сергей Холодилов
Наша служба и опасна и трудна
И на первый взгляд как будто не видна
Ю. Энтин
В статье описаны некоторые детали, относящиеся к программированию служб Windows NT/2000/XP. Она не претендует на полноту или уникальность. Кое-что не охвачено, многое (хотя и не всё) из охваченного вы сможете найти в MSDN или другой литературе. Если вы написали свою первую службу и хотите двигаться дальше, эта статья вам поможет.
Для понимания написанного ниже вы должны быть знакомы со службами. Глубоких знаний не потребуется, достаточно представлять себе архитектуру службы (с точки зрения программиста) и помнить примерное предназначение нескольких API-функций.
Большая часть содержащихся в статье утверждений описывает реакцию Windows на какие-то действия со стороны службы. Полноценная проверка таких утверждений не представляется возможной. Тем более что некоторые из них не документированы.
Я поступил так:
В Windows 2000 Server SP1 я постарался проверить всё. В других версиях только кое-что. Возможно, некоторые полученные факты я истолковал неверно. Но пока что ошибок я не нашёл.
Если утверждение есть в MSDN и/или другом источнике, я проверял его два-три раза, если всё сходилось, считал его верным.
Если утверждение противоречит тому, что написано в MSDN и/или других источниках, продолжительность тестирования зависела от его важности (с моей точки зрения). В этом случае в статье указаны и мои результаты, и информация из MSDN или других источников. Если я считаю утверждение важным, кроме этого указано, какие моменты могли быть упущены во время тестирования. Эта версия статьи не содержит важных спорных утверждений.
Если утверждение не встретилось мне ни в одном источнике, я поступал аналогично предыдущему пункту.
В этой части статьи разобраны вопросы, имеющие непосредственное отношение к любой службе. Разделение на «непосредственные» и «косвенные» условно и субъективно. Принцип, которого я придерживался, таков: если проблема/возможность свойственна службам из-за особенностей их архитектуры, она описана в этой части. Иначе – в следующей.
Работа с любой программой начинается с установки и заканчивается удалением. Службы – не исключение. Отличие состоит в том, что при установке службу необходимо зарегистрировать. Можно, конечно, возложить эту задачу на инсталлятор, но, по-моему, правильней и проще писать службы, умеющие устанавливаться/удаляться в полуавтоматическом режиме.
ПРИМЕЧАНИЕНа всякий случай: некоторые умные люди, которые знают, как правильно писать инсталляторы, считают, что в этом вопросе я заблуждаюсь. |
Например, так:
int _tmain(int argc, TCHAR* argv[]){ // Если в командной строке что-то есть - // предположительно, запускает пользователь. if (argc == 2) { // lstricmp - сравнение без учёта регистра. if (lstrcmpi(argv[1], TEXT("/install"))==0) { CmdLine::Install(); } else if (lstrcmpi(argv[1], TEXT("/uninstall"))==0) { CmdLine::Uninstall(); } else { CmdLine::DisplayHelp();} return 0; }... | ||
ПРИМЕЧАНИЕTEXT() и _tmain – для поддержки Unicode (а можно сказать «для поддержки ANSI»). Подробнее в разделе «Unicode».CmdLine – пространство имён. Я их нежно люблю и часто использую.Вообще-то, то, что в командной строке «что-то есть» ничего не доказывает, см. «Мелочи». |
Функции, выполняющие собственно установку/удаление, выглядят примерно так:
void CmdLine::Install(){открываем SCM (OpenSCManager)создаём службу (CreateService) закрываем всё, что открыли}void CmdLine::Uninstall(){открываем SCM (OpenSCManager)открываемслужбу (OpenService)удаляемслужбу (DeleteService)закрываем всё, что открыли} |
На некоторых этапах выполнения служба должна выполнить определённые действия за определённый срок. В MSDN с разной степенью конкретности перечислены пять требований. В книге Джеффри Рихтера и Джейсона Кларка «Программирование серверных приложений в Windows 2000» приведено шестое. Ниже перечислены сами требования и мои комментарии к ним.
Служба должна вызвать StartServiceCtrlDispatcher не позже, чем через 30 секунд после начала работы, иначе выполнение службы завершится. Практика подтвердила. Кроме того, в раздел Event Log’а System будет добавлена запись об ошибке (источник – «Service Control Manager»). Если служба запускается вручную из программы Services, пользователь получит сообщение (MessageBox).
Функция ServiceMain должна вызвать RegisterServiceCtrlHandler[Ex] немедленно. Что будет в противном случае – не указано. Несоблюдение этого правила – один из случаев «нарушений во время инициализации» (термин мой), описанных ниже в этом же разделе.
Функция ServiceMain должна вызвать SetServiceStatus первый раз «почти сразу» после RegisterServiceCtrlHandler[Ex], после чего служба должна продолжать вызывать её до тех пор, пока инициализация не закончится. Неправильное использование SetServiceStatus – второй случай «нарушений во время инициализации».
При обработке сообщения служба должна вернуть управление из функции Handler[Ex] в течение 30 секунд, иначе SCM сгенерирует ошибку. Практика подтверждает, запись в Event Log добавляется. Но никаких репрессивных действий по отношению к службе я не дождался.
При получении сообщения SERVICE_CONTROL_SHUTDOWN служба должна закончить работу за время, не превышающее число миллисекунд, указанное в параметре WaitToKillServiceTimeout ключа HKLM\System\CurrentControlSet\Control, иначе будет завершена принудительно. Практика подтвердила.
После завершения работы в качестве службы (то есть после посылки службой уведомления об этом) процессу даётся 20 секунд на очистку/сохранение/ещё что-то, после этого процесс завершается. Подробнее в разделе «Корректное завершение».
Если ваша служба быстро инициализируется и мгновенно обрабатывает сообщения, в результате чего автоматически удовлетворяет всем пунктам, вам повезло. Если нет, можно использовать несколько потоков. Например, так:
Дополнительный поток выполняет необходимую инициализацию, а основной поток вызывает StartServiceCtrlDispatcher.
Я не смог придумать, зачем делать что-либо до вызова RegisterServiceCtrlHandler[Ex], но если надо, можно сделать так же, как в (1).
Один из потоков посылает уведомления о продвижении процесса, второй выполняет инициализацию. Функция первого потока может быть такой:
DWORD WINAPI SendPending(LPVOID dwState){ sStatus.dwCheckPoint = 0; sStatus.dwCurrentState = (DWORD) dwState; sStatus.dwWaitHint = 2000; for (;;) { if (WaitForSingleObject(eSendPending, 1000)!=WAIT_TIMEOUT) break; sStatus.dwCheckPoint++; SetServiceStatus(ssHandle, &sStatus); } sStatus.dwCheckPoint = 0; sStatus.dwWaitHint = 0; return 0;} |
Уведомления посылаются с помощью функции SetServiceStatus. sStatus – глобальная переменная типа SERVICE_STATUS, описывающая состояние службы, в dwState передаётся состояние, о котором необходимо сообщать, eSendPending – событие, установка которого означает окончание работы этого потока.
Забавно, что при таком подходе для служб, запускаемых вручную, видимый результат не меняется (см. ниже о нарушениях во время инициализации).
Идея в том, что, если обработка сообщения может затянуться, функция Handler[Ex] инициирует её и завершается, не дожидаясь окончания. Если рабочий поток службы в цикле ожидает каких-то событий, обработку может выполнить он, Handler[Ex] должна только проинформировать его о приходе сообщения, если рабочий поток постоянно занят, можно породить ещё один поток. При подобной реализации необходимо учесть, что следующее сообщение может прийти в течение обработки предыдущего, то есть до того, как служба пошлёт уведомление об окончании обработки. С помощью Services этого не сделать, но пользователь может использовать утилиту Net.exe (синтаксис запуска: net команда имя_службы) или написать свою.
Ограничения, накладываемые требованиями (5) и (6) обойти не удаётся. Но, в отличие от (5), в (6) момент посылки уведомления о завершении регулируете вы. Поэтому можно выполнять всю необходимую очистку/сохранение/ещё что-то заранее.
Теперь о «нарушениях в процессе инициализации». Варианты нарушений:
Задержка перед вызовом RegisterServiceCtrlHandler[Ex].
Задержка перед первым вызовом SetServiceStatus.
Слишком большие паузы между вызовами SetServiceStatus.
Не меняется поле dwCheckPoint структуры, передаваемой SetServiceStatus.
Во всех перечисленных случаях реакция системы будет одинаковой. А именно:
A) Служба запускается автоматически.
Минуты через две (если за это время «нарушение» не прекратится и служба не начнёт работать нормально) в Event Log-е появится запись «The ... service hung on starting.»
Еслихотьоднаслужба «повисла», пользовательполучитсообщение «At least one service or driver failed during system startup. Use Event Viewer to examine the event log for details.» Такое ощущение, что это сообщение появляется в тот момент, когда запускается первая «зависшая» служба (сам понимаю, что звучит нелогично, но что делать...).
B) Служба запускается вручную из Services.
Минуты три система подождёт.
Появится сообщение об ошибке.
В программе Services в столбце Status служба будет помечена словом «Starting».
В любом случае служба, в конце концов, запустится.
Эта информация не очень важна (и, кстати, не документирована), так как даже таких нарушений лучше не допускать. Но представлять, что будет, если по каким-то причинам, ваша служба слегка притормозит, полезно.
Этот вопрос возник у меня, когда я писал свою первую службу. Если чётче сформулировать, то звучит он так: который из потоков можно использовать в качестве рабочего? На первый взгляд задействовано три потока: один исполняет main/WinMain, второй – ServiceMain, третий – Handler[Ex] (не совсем так, см. «Мелочи»). Очевидно, что первый и третий потоки не подходят. Про второй поток ничего не известно и, вполне возможно, функция ServiceMain должна возвращать управление. Я поступил просто: создал в ServiceMain дополнительный поток, который выполнял работу. Окончание функции выглядело так:
... // Создаёт рабочий поток и возвращает управление Begin();} |
Это работает. Никаких дополнительных проблем при таком подходе не обнаружено.
После внимательного прочтения MSDN выяснилось, что вообще-то для работы предназначен поток, выполняющий ServiceMain. Болеетого, вописаниинаписано: «A ServiceMain function does not return until its services are ready to terminate.» Возвращать управление из ServiceMain сразу рекомендуется только службам, не нуждающимся в потоке для выполнения работы (например, вся работа может заключаться в реакции на сообщения). Я советую следовать рекомендациям Microsoft.