Программирование служб: подробности (45832)

Посмотреть архив целиком

Программирование служб: подробности

Сергей Холодилов

Наша служба и опасна и трудна

И на первый взгляд как будто не видна

Ю. Энтин

В статье описаны некоторые детали, относящиеся к программированию служб 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.»


Случайные файлы

Файл
103045.rtf
8313.rtf
96373.rtf
116307.rtf
27627.rtf




Чтобы не видеть здесь видео-рекламу достаточно стать зарегистрированным пользователем.
Чтобы не видеть никакую рекламу на сайте, нужно стать VIP-пользователем.
Это можно сделать совершенно бесплатно. Читайте подробности тут.