Идёт загрузка страницы...

htp://aptem.net.ru





Службы Windows NT. Назначение и разработка

Михаил Плакунов, СофтТерра

Службы Windows NT, общие понятия


Служба Windows NT (Windows NT service) — специальный процесс, обладающий
унифицированным интерфейсом для взаимодействия с операционной системой
Windows NT. Службы делятся на два типа — службы Win32, взаимодействующие с
операционной системой посредством диспетчера управления службами (Service
Control Manager — SCM), и драйвера, работающие по протоколу драйвера
устройства Windows NT. Далее в этой статье мы будем обсуждать только
службы Win32.
Применение служб


Одним из важнейших свойств службы является неинтерактивность. Типичное
«поведение» службы — это незаметная для обычного пользователя работа в
фоновом режиме. В силу этого службы наиболее подходят для реализации
следующих типов приложений:
Сервера в архитектуре клиент-сервер (например, MS SQL, MS Exchange
Server)
Сетевые службы Windows NT (Server, Workstation);
Серверные (в смысле функциональности) компоненты распределенных
приложений (например, всевозможные программы мониторинга).
Основные свойства служб


От обычного приложения Win32 службу отличают 3 основных свойства.
Рассмотрим каждое из них.
Во-первых, это возможность корректного останова (приостанова) работы
службы. Пользователь или другое приложение, использующие стандартные
механизмы, имеют возможность изменить состояние службы — перевести ее из
состояния выполнения в состояние паузы или даже остановить ее работу. При
этом служба перед изменением своего состояния получает специальное
уведомление, благодаря которому может совершить необходимые для перехода в
новое состояние действия, например, освободить занятые ресурсы.
Во-вторых, возможность запуска службы до регистрации пользователя и, как
следствие, возможность работы вообще без зарегистрированного пользователя.
Любая служба может быть запущена автоматически при старте операционной
системы и начать работу еще до того как пользователь произведет вход в
систему.
И, наконец, возможность работы в произвольном контексте безопасности.
Контекст безопасности Windows NT определяет совокупность прав доступа
процесса к различным объектам системы и данным. В отличие от обычного
приложения Win32, которое всегда запускается в контексте безопасности
пользователя, зарегистрированного в данный момент в системе, для службы
контекст безопасности ее выполнения можно определить заранее. Это
означает, что для службы можно определить набор ее прав доступа к объектам
системы заранее и тем самым ограничить сферу ее деятельности.
Применительно к службам существует специальный вид контекста безопасности,
используемый по умолчанию и называющийся Local System. Служба, запущенная
в этом контексте, обладает правами только на ресурсы локального
компьютера. Никакие сетевые операции не могут быть осуществлены с правами
Local System, поскольку этот контекст имеет смысл только на локальном
компьютере и не опознается другими компьютерами сети.
Взаимодействие службы с другими приложениями


Любое приложение, имеющее соответствующие права, может взаимодействовать
со службой. Взаимодействие, в первую очередь, подразумевает изменение
состояния службы, то есть перевод ее в одно из трех состояний — работающее
(Запуск), приостанов (Пауза), останов и осуществляется при помощи подачи
запросов SCM. Запросы бывают трех типов — сообщения от служб (фиксация их
состояний), запросы, связанные с изменением конфигурации службы или
получением информации о ней и запросы приложений на изменение состояния
службы.
Для управления службой необходимо в первую очередь получают ее дескриптор
с помощью функции Win32 API OpenService. Функция StartService запускает
службу. При необходимости изменение состояния службы производится вызовом
функции ControlService.
База данных службы


Информация о каждой службе хранится в реестре — в ключе
HKLM\SYSTEM\CurrentControlSet\Services\ServiceName. Там содержатся
следующие сведения:
Тип службы. Указывает на то, реализована ли в данном приложении только
одна служба (эксклюзивная) или же их в приложении несколько.
Эксклюзивная служба может работать в любом контексте безопасности.
Несколько служб внутри одного приложения могут работать только в
контексте LocalSystem.
Тип запуска. Автоматический — служба запускается при старте системы. По
требованию — служба запускается пользователем вручную. Деактивированный
— служба не может быть запущена.
Имя исполняемого модуля (EXE-файл).
Порядок запуска по отношению к другим службам. В некоторых случаях для
корректной работы службы требуется, чтобы была запущена одна или
несколько других служб. В этом случае в реестре содержится информация о
службах, запускаемых перед данной.
Контекст безопасности выполнения службы (сетевое имя и пароль). По
умолчанию контекст безопасности соответствует LocalSystem.
Приложения, которым требуется получить информацию о какой-либо службе или
изменить тот или иной параметр службы, по сути должны изменить информацию
в базе данных службы в реестре. Это можно сделать посредством
соответствующих функций Win32 API:
OpenSCManager, CreateService, OpenService, CloseServiceHandle — для
создания (открытия) службы;
QueryServiceConfig, QueryServiceObjectSecurity, EnumDependentServices,
EnumServicesStatus — для получения информации о службе;
ChangeServiceConfig, SetServiceObjectSecurity, LockServiceDatabase,
UnlockServiceDatabase, QueryServiceLockStatus — для изменения
конфигурационной информации службы.
Внутреннее устройство службы.


Для того, чтобы «быть службой», приложение должно быть устроено
соответствующим образом, а именно — включать в себя определенный набор
функций (в терминах C++) с определенной функциональностью. Рассмотрим
кратко каждую из них.
Функция main


Как известно функция main — точка входа любого консольного Win32
приложения. При запуске службы первым делом начинает выполняться код этой
функции. Втечение 30 секунд с момента старта функция main должна
обязательно вызвать StartServiceCtrlDispatcher для установления соединения
между приложением и SCM. Все коммуникации между любой службой данного
приложения и SCM осуществляются внутри функции StartServiceCtrlDispatcher,
которая завершает работу только после остановки всех служб в приложении.
Функция ServiceMain


Помимо общепроцессной точки входа существует еще отдельная точка входа для
каждой из служб, реализованных в приложении. Имена функций, являющихся
точками входа служб (для простоты назовем их всех одинаково —
ServiceMain), передаются SCM в одном из параметров при вызове
StartServiceCtrlDispatcher. При запуске каждой службы для выполнения
ServiceMain создается отдельный поток.
Получив управление, ServiceMain первым делом должна зарегистрировать
обработчик запросов к службе, функцию Handler, свою для каждой из служб в
приложении. После этого в ServiceMain обычно следуют какие-либо действия
для инициализации службы — выделение памяти, чтение данных и т.п. Эти
действия должны обязательно сопровождаться уведомлениями SCM о том, что
служба все еще находится в процессе старта и никаких сбоев не произошло.
Уведомления посылаются при помощи вызовов функции SetServiceStatus. Все
вызовы, кроме самого последнего должны быть с параметром
SERVICE_START_PENDING, а самый последний — с параметром SERVICE_RUNNING.
Периодичность вызовов определяется разработчиком службы, исходя их
следующего условия: продолжительность временного интервала между двумя
соседними вызовами SetServiceStatus не должна превышать значения параметра
dwWaitHint, переданного SCM при первом из двух вызовов. В противном случае
SCM, не получив во-время очередного уведомления, принудительно остановит
службу. Такой способ позволяет избежать ситуации «зависания» службы на
старте в результате возникновения тех или иных сбоев (вспомним, что службы
обычно неинтерактивны и могут запускаться в отсутствие пользователя).
Обычная практика заключается в том, что после завершения очередного шага
инициализации происходит уведомление SCM.
Функция Handler


Как уже упоминалось выше, Handler — это прототип callback-функции,
обработчика запросов к службе, своей для каждой службы в приложении.
Handler вызывается, когда службе приходит запрос (запуск, приостанов,
возобновление, останов, сообщение текущего состояния) и выполняет
необходимые в соответствии с запросом действия, после чего сообщает новое
состояние SCM.
Один запрос следует отметить особо — запрос, поступающий при завершении
работы системы (Shutdown). Этот запрос сигнализирует о необходимости
выполнить деинициализацию и завершиться. Microsoft утверждает, что для
завершения работы каждой службе выделяется 20 секунд, после чего она
останавливается принудительно. Однако тесты показали, что это условие
выполняется не всегда и служба принудительно останавливается до истечения
этого промежутка времени.
Система безопасности служб


Любое действие над службами требует наличия соответствующих прав у
приложения. Все приложения обладают правами на соединение с SCM,
перечисление служб и проверку заблокированности БД службы. Регистрировать
в сиситеме новую службу или блокировать БД службы могут только приложения,
обладающие административными правами.
Каждая служба имеет дескриптор безопасности, описывающий какие
пользователи имеют права на ту или иную операцию. По умолчанию:
Все пользователи имеют права SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS,
SERVICE_ENUMERATE_DEPENDENTS, SERVICE_INTERROGATE и
SERVICE_USER_DEFINED_CONTROL;
Пользователи, входящие в группу Power Users и учетная запись LocalSystem
дополнительно имеют права SERVICE_START, SERVICE_PAUSE_CONTINUE и
SERVICE_STOP;
Пользователи, входящие в группы Administrators и System Operators имеют
право SERVICE_ALL_ACCESS.
Службы и интерактивность


По умолчанию интерактивные службы могут выполняться только в контексте
безопасности LocalSystem. Это связано с особенностями вывода на экран
монитора в Windows NT, где существует, например, такой объект как
“Desktop”, для работы с которым нужно иметь соответствующие права доступа,
которых может не оказаться у произвольной учетной записи, отличной от
LocalSystem. Несмотря на то, что в подавляющем большинстве случаев это
ограничение несущественно однако иногда существует необходимость создать
службу, которая выводила бы информацию на экран монитора и при этом
выполнялась бы в контексте безопасности отличном от LocalSystem, например,
серверная компонента приложения для запуска приложений на удаленном
компьютере.
Фрагмент кода из Примера 1. иллюстрирует такую возможность.
В этом фрагменте в ответ на запрос, посланный клиентской частью приложения
последством RPC, служба выводит текстовое сообщение на экран монитора.
Пример службы (ключевые фрагменты)


Рассмотрим на примере ключевые фрагменты приложения на языке С++,
реализующего службу Windows NT. Для наглядности несущественные части кода
опущены.
Функция main


В Примере 2. показан код функции main.
Функция ServiceMain


Особенностью кода, содержащегося в ServiceMain, является то, что часто
невозможно заранее предсказать время выполнения той или иной операции,
особенно, если учесть, что ее выполнение происходит в операционной системе
с вытесняющей многозадачностью. Если операция продлится дольше указанного
в параметре вызова SetServiceStatus интервала времени, служба не сможет
во-время отправить следующее уведомление, в результате чего SCM остановит
ее работу. Примерами потенциально «опасных» операций могут служить вызовы
функций работы с сетью при больших таймаутах или единовременное чтение
большого количества информации с медленного носителя. Кроме того, такой
подход совершенно не применим при отладке службы, поскольку выполнение
программы в отладчике сопровождается большими паузами, необходимыми
разработчику.
Для преодоления этой проблемы все операции по взаимодействию с SCM следует
выполнять в отдельном потоке, не зависящем от действий, происходящих на
этапе инициализации.
В Примере 3. показан алгоритм корректного запуска службы, использующий
вспомогательный поток.
Функция Handler


В Примере 4. показан код функции Handler и вспомогательных потоков. Для
запросов “Stop” и “Shutdown” используется алгоритм корректного останова
службы, аналогичный тому, который используется при старте службы, с той
лишь разницей, что вместо параметра SERVICE_START_PENDING в
SetserviceStatus передается параметр SERVICE_STOP_PENDING, а вместо
SERVICE_RUNNING — SERVICE_STOPPED.
В идеале для запросов “Pause” и “Continue” тоже следует использовать этот
подход. Любознательный читатель без труда сможет реализовать его, опираясь
на данные примеры.
Заключение


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


Пример 1


// Функция, аналог MessageBox Win32 API
int ServerMessageBox(RPC_BINDING_HANDLE h, LPSTR lpszText,
LPSTR lpszTitle, UINT fuStyle)
{
DWORD dwThreadId;
HWINSTA hwinstaSave;
HDESK hdeskSave;
HWINSTA hwinstaUser;
HDESK hdeskUser;
int result;

// Запоминаем текущие объекты “Window station” и “Desktop”.
GetDesktopWindow();
hwinstaSave = GetProcessWindowStation();
dwThreadId = GetCurrentThreadId();
hdeskSave = GetThreadDesktop(dwThreadId);

// Меняем контекст безопасности на тот,
// который есть у вызавшего клиента RPC
// и получаем доступ к пользовательским
// объектам “Window station” и “Desktop”.
RpcImpersonateClient(h);
hwinstaUser = OpenWindowStation(“WinSta0”,
FALSE, MAXIMUM_ALLOWED);
if (hwinstaUser == NULL)
{
RpcRevertToSelf();
return 0;
}
SetProcessWindowStation(hwinstaUser);
hdeskUser = OpenDesktop(“Default”, 0, FALSE, MAXIMUM_ALLOWED);
RpcRevertToSelf();
if (hdeskUser == NULL)
{
SetProcessWindowStation(hwinstaSave);
CloseWindowStation(hwinstaUser);
return 0;
}
SetThreadDesktop(hdeskUser);

// Выводим обычное текстовое окно.
result = MessageBox(NULL, lpszText, lpszTitle, fuStyle);

// Восстанавливаем сохраненные объекты
// “Window station” и “Desktop”.
SetThreadDesktop(hdeskSave);
SetProcessWindowStation(hwinstaSave);
CloseDesktop(hdeskUser);
CloseWindowStation(hwinstaUser);

return result;
}

Пример 2


void main()
{
SERVICE_TABLE_ENTRY steTable[] =
{
{SERVICENAME, ServiceMain},
{NULL, NULL}
};

// Устанавливаем соединение с SCM. Внутри этой функции
// происходит прием и диспетчеризация запросов.
StartServiceCtrlDispatcher(steTable);
}

Пример 3


void WINAPI ServiceMain(DWORD dwArgc, LPSTR *psArgv)
{
// Сразу регистрируем обработчик запросов.
hSS = RegisterServiceCtrlHandler(SERVICENAME, ServiceHandler);

sStatus.dwCheckPoint = 0;
sStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_PAUSE_CONTINUE;
sStatus.dwServiceSpecificExitCode = 0;
sStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
sStatus.dwWaitHint = 0;
sStatus.dwWin32ExitCode = NOERROR;

// Для инициализации службы вызывается функция InitService();
// Для того, чтобы в процессе инициализации система не
// выгрузила службу, запускается поток, который раз в
// секунду сообщает, что служба в процессе инициализации.
// Для синхронизации потока создаётся событие.
// После этого запускается рабочий поток, для
// синхронизации которого также
// создаётся событие.

hSendStartPending = CreateEvent(NULL, TRUE, FALSE, NULL);

HANDLE hSendStartThread;
DWORD dwThreadId;

hSendStartThread = CreateThread(NULL, 0, SendStartPending,
NULL, 0, &dwThreadId);

//Здесь производится вся инициализация службы.
InitService();

SetEvent(hSendStartPending);

if(
WaitForSingleObject(hSendStartThread, 2000)
!= WAIT_OBJECT_0)
{
TerminateThread(hSendStartThread, 0);
}

CloseHandle(hSendStartPending);
CloseHandle(hSendStartThread);

hWork = CreateEvent(NULL, TRUE, FALSE, NULL);

hServiceThread = CreateThread(NULL, 0, ServiceFunc,
0, 0, &dwThreadId);

sStatus.dwCurrentState = SERVICE_RUNNING;

SetServiceStatus(hSS, &sStatus);
}

// Функция потока, каждую секунду посылающая уведомления SCM
// о том, что процесс инициализации идёт. Работа функции
// завершается, когда устанавливается
// событие hSendStartPending.

DWORD WINAPI SendStartPending(LPVOID)
{
sStatus.dwCheckPoint = 0;
sStatus.dwCurrentState = SERVICE_START_PENDING;
sStatus.dwWaitHint = 2000;

// “Засыпаем” на 1 секунду. Если через 1 секунду
// событие hSendStartPending не перешло
// в сигнальное состояние (инициализация службы не
// закончилась), посылаем очередное уведомление,
// установив максимальный интервал времени
// в 2 секунды, для того, чтобы был запас времени до
// следующего уведомления.
while (true)
{
SetServiceStatus(hSS, &sStatus);
sStatus.dwCheckPoint++;
if(WaitForSingleObject(hSendStartPending,
1000)!=WAIT_TIMEOUT)
break;
}

sStatus.dwCheckPoint = 0;
return 0;
}

// Функция, инициализирующая службу. Чтение данных,
// распределение памяти и т.п.
void InitService()
{
...
}

// Функция, содержащая «полезный» код службы.
DWORD WINAPI ServiceFunc(LPVOID)
{
while (true)
{
if (!bPause)
{
// Здесь содержится код, который как правило
// выполняет какие-либо циклические операции...
}

if (WaitForSingleObject(hWork, 1000)!=WAIT_TIMEOUT)
break;
}

return 0;
}

Пример 4


// Обработчик запросов от SCM
void WINAPI ServiceHandler(DWORD dwCode)
{
switch (dwCode)
{
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
ReportStatusToSCMgr(SERVICE_STOP_PENDING,
NO_ERROR, 0, 1000);
hSendStopPending = CreateEvent(NULL, TRUE, FALSE, NULL);
hSendStopThread = CreateThread(NULL, 0,
SendStopPending, NULL, 0, & dwThreadId);
SetEvent(hWork);
if (WaitForSingleObject(hServiceThread,
1000) != WAIT_OBJECT_0)
{
TerminateThread(hServiceThread, 0);
}
SetEvent(hSendStopPending);
CloseHandle(hServiceThread);
CloseHandle(hWork);
if(WaitForSingleObject(hSendStopThread,
2000) != WAIT_OBJECT_0)
{
TerminateThread(hSendStopThread, 0);
}
CloseHandle(hSendStopPending);

sStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hSS, &sStatus);
break;
case SERVICE_CONTROL_PAUSE:
bPause = true;
sStatus.dwCurrentState = SERVICE_PAUSED;
SetServiceStatus(hSS, &sStatus);
break;
case SERVICE_CONTROL_CONTINUE:
bPause = true;
sStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hSS, &sStatus);
break;
case SERVICE_CONTROL_INTERROGATE:
SetServiceStatus(hSS, &sStatus);
break;
default:
SetServiceStatus(hSS, &sStatus);
break;
}
}


// Функция потока, аналогичная SendStartPending
// для останова службы.
DWORD WINAPI SendStopPending(LPVOID)
{
sStatus.dwCheckPoint = 0;
sStatus.dwCurrentState = SERVICE_STOP_PENDING;
sStatus.dwWaitHint = 2000;

while (true)
{
SetServiceStatus(hSS, &sStatus);
sStatus.dwCheckPoint++;
if(WaitForSingleObject(hSendStopPending,
1000)!=WAIT_TIMEOUT)
break;
}

sStatus.dwCheckPoint = 0;
return 0;
}

СОДЕРЖАНИЕ