Приветствую!
Сегодня мы с вами углубимся в особенности систем Windows NT/2000 - а именно, научимся
создавать под них особые программы, называемые службами или сервисами.
/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /
Службы Windows NT: назначение и разработка
Автор: Михаил Плакунов
Зачем и как создавать службы (сервисы) Windows
NT/2000.
Источник: СофтТерра
Служба Windows NT (Windows NT service) - специальный процесс, обладающий унифицированным интерфейсом для взаимодействия с операционной системой Windows NT. Службы делятся на два типа - службы Win32, взаимодействующие с операционной системой посредством диспетчера управления службами (Service Control Manager - SCM), и драйвера, работающие по протоколу драйвера устройства Windows NT. Далее в этой статье мы будем обсуждать только службы Win32.
Одним из важнейших свойств службы является неинтерактивность. Типичное «поведение» службы - это незаметная для обычного пользователя работа в фоновом режиме. В силу этого службы наиболее подходят для реализации следующих типов приложений:
От обычного приложения Win32 службу отличают 3 основных свойства. Рассмотрим каждое из них.
Во-первых, это возможность корректного останова (приостанова) работы службы. Пользователь или другое приложение, использующие стандартные механизмы, имеют возможность изменить состояние службы - перевести ее из состояния выполнения в состояние паузы или даже остановить ее работу. При этом служба перед изменением своего состояния получает специальное уведомление, благодаря которому может совершить необходимые для перехода в новое состояние действия, например, освободить занятые ресурсы.
Во-вторых, возможность запуска службы до регистрации пользователя и, как следствие, возможность работы вообще без зарегистрированного пользователя. Любая служба может быть запущена автоматически при старте операционной системы и начать работу еще до того как пользователь произведет вход в систему.
И, наконец, возможность работы в произвольном контексте безопасности. Контекст безопасности Windows NT определяет совокупность прав доступа процесса к различным объектам системы и данным. В отличие от обычного приложения Win32, которое всегда запускается в контексте безопасности пользователя, зарегистрированного в данный момент в системе, для службы контекст безопасности ее выполнения можно определить заранее. Это означает, что для службы можно определить набор ее прав доступа к объектам системы заранее и тем самым ограничить сферу ее деятельности. Применительно к службам существует специальный вид контекста безопасности, используемый по умолчанию и называющийся Local System. Служба, запущенная в этом контексте, обладает правами только на ресурсы локального компьютера. Никакие сетевые операции не могут быть осуществлены с правами Local System, поскольку этот контекст имеет смысл только на локальном компьютере и не опознается другими компьютерами сети.
Любое приложение, имеющее соответствующие права, может взаимодействовать со службой. Взаимодействие, в первую очередь, подразумевает изменение состояния службы, то есть перевод ее в одно из трех состояний - работающее (Запуск), приостанов (Пауза), останов и осуществляется при помощи подачи запросов SCM. Запросы бывают трех типов - сообщения от служб (фиксация их состояний), запросы, связанные с изменением конфигурации службы или получением информации о ней и запросы приложений на изменение состояния службы.
Для управления службой необходимо в первую очередь получают ее дескриптор с помощью функции Win32 API OpenService. Функция StartService запускает службу. При необходимости изменение состояния службы производится вызовом функции ControlService.
Информация о каждой службе хранится в реестре - в ключе HKLM \ SYSTEM \ CurrentControlSet \ Services \ ServiceName. Там содержатся следующие сведения:
Приложения, которым требуется получить информацию о какой-либо службе или изменить тот или иной параметр службы, по сути должны изменить информацию в базе данных службы в реестре. Это можно сделать посредством соответствующих функций Win32 API:
Для того, чтобы «быть службой», приложение должно быть устроено соответствующим образом, а именно - включать в себя определенный набор функций (в терминах C++) с определенной функциональностью. Рассмотрим кратко каждую из них.
Как известно функция main - точка входа любого консольного Win32 приложения. При запуске службы первым делом начинает выполняться код этой функции. Втечение 30 секунд с момента старта функция main должна обязательно вызвать StartServiceCtrlDispatcher для установления соединения между приложением и SCM. Все коммуникации между любой службой данного приложения и SCM осуществляются внутри функции StartServiceCtrlDispatcher, которая завершает работу только после остановки всех служб в приложении.
Помимо общепроцессной точки входа существует еще отдельная точка входа для каждой из служб, реализованных в приложении. Имена функций, являющихся точками входа служб (для простоты назовем их всех одинаково - ServiceMain), передаются SCM в одном из параметров при вызове StartServiceCtrlDispatcher. При запуске каждой службы для выполнения ServiceMain создается отдельный поток.
Получив управление, ServiceMain первым делом должна зарегистрировать обработчик запросов к службе, функцию Handler, свою для каждой из служб в приложении. После этого в ServiceMain обычно следуют какие-либо действия для инициализации службы - выделение памяти, чтение данных и т.п. Эти действия должны обязательно сопровождаться уведомлениями SCM о том, что служба все еще находится в процессе старта и никаких сбоев не произошло. Уведомления посылаются при помощи вызовов функции SetServiceStatus. Все вызовы, кроме самого последнего должны быть с параметром SERVICE_START_PENDING, а самый последний - с параметром SERVICE_RUNNING. Периодичность вызовов определяется разработчиком службы, исходя их следующего условия: продолжительность временного интервала между двумя соседними вызовами SetServiceStatus не должна превышать значения параметра dwWaitHint, переданного SCM при первом из двух вызовов. В противном случае SCM, не получив во-время очередного уведомления, принудительно остановит службу. Такой способ позволяет избежать ситуации «зависания» службы на старте в результате возникновения тех или иных сбоев (вспомним, что службы обычно неинтерактивны и могут запускаться в отсутствие пользователя). Обычная практика заключается в том, что после завершения очередного шага инициализации происходит уведомление SCM.
Как уже упоминалось выше, Handler - это прототип callback-функции, обработчика запросов к службе, своей для каждой службы в приложении. Handler вызывается, когда службе приходит запрос (запуск, приостанов, возобновление, останов, сообщение текущего состояния) и выполняет необходимые в соответствии с запросом действия, после чего сообщает новое состояние SCM.
Один запрос следует отметить особо - запрос, поступающий при завершении работы системы (Shutdown). Этот запрос сигнализирует о необходимости выполнить деинициализацию и завершиться. Microsoft утверждает, что для завершения работы каждой службе выделяется 20 секунд, после чего она останавливается принудительно. Однако тесты показали, что это условие выполняется не всегда и служба принудительно останавливается до истечения этого промежутка времени.
Любое действие над службами требует наличия соответствующих прав у приложения. Все приложения обладают правами на соединение с SCM, перечисление служб и проверку заблокированности БД службы. Регистрировать в сиситеме новую службу или блокировать БД службы могут только приложения, обладающие административными правами.
Каждая служба имеет дескриптор безопасности, описывающий какие пользователи имеют права на ту или иную операцию. По умолчанию:
По умолчанию интерактивные службы могут выполняться только в контексте безопасности LocalSystem. Это связано с особенностями вывода на экран монитора в Windows NT, где существует, например, такой объект как “Desktop”, для работы с которым нужно иметь соответствующие права доступа, которых может не оказаться у произвольной учетной записи, отличной от LocalSystem. Несмотря на то, что в подавляющем большинстве случаев это ограничение несущественно однако иногда существует необходимость создать службу, которая выводила бы информацию на экран монитора и при этом выполнялась бы в контексте безопасности отличном от LocalSystem, например, серверная компонента приложения для запуска приложений на удаленном компьютере.
Следующий фрагмент кода иллюстрирует такую возможность.
// Функция, аналог 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;
}
В этом фрагменте в ответ на запрос, посланный клиентской частью приложения последством RPC, служба выводит текстовое сообщение на экран монитора.
Рассмотрим на примере ключевые фрагменты приложения на языке С++, реализующего службу Windows NT. Для наглядности несущественные части кода опущены.
Вот как выглядит код функции main:
void main()
{
SERVICE_TABLE_ENTRY steTable[] =
{
{SERVICENAME, ServiceMain},
{NULL, NULL}
};
// Устанавливаем соединение с SCM. Внутри этой функции
// происходит прием и диспетчеризация запросов.
StartServiceCtrlDispatcher(steTable);
}
Особенностью кода, содержащегося в ServiceMain, является то, что часто невозможно заранее предсказать время выполнения той или иной операции, особенно, если учесть, что ее выполнение происходит в операционной системе с вытесняющей многозадачностью. Если операция продлится дольше указанного в параметре вызова SetServiceStatus интервала времени, служба не сможет во-время отправить следующее уведомление, в результате чего SCM остановит ее работу. Примерами потенциально «опасных» операций могут служить вызовы функций работы с сетью при больших таймаутах или единовременное чтение большого количества информации с медленного носителя. Кроме того, такой подход совершенно не применим при отладке службы, поскольку выполнение программы в отладчике сопровождается большими паузами, необходимыми разработчику.
Для преодоления этой проблемы все операции по взаимодействию с SCM следует выполнять в отдельном потоке, не зависящем от действий, происходящих на этапе инициализации.
Алгоритм корректного запуска службы, использующий вспомогательный поток:
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;
}
А вот код функции Handler и вспомогательных потоков:
// Обработчик запросов от 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;
}
Для запросов “Stop” и “Shutdown” используется алгоритм корректного останова службы, аналогичный тому, который используется при старте службы, с той лишь разницей, что вместо параметра SERVICE_START_PENDING в SetserviceStatus передается параметр SERVICE_STOP_PENDING, а вместо SERVICE_RUNNING - SERVICE_STOPPED.
В идеале для запросов “Pause” и “Continue” тоже следует использовать этот подход. Любознательный читатель без труда сможет реализовать его, опираясь на данные примеры.
В заключение хотелось бы отметить, что с переходом на Windows 2000 разработка служб не претерпела изменений. Службы по-прежнему остаются важной частью программного обеспечения на платформе Windows, что предоставляет разработчикам широкое поле деятельности.
/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /
Q|
Хотелось бы побольше узнать о предварительном просмотре. В русской программе он смотрится
инородным телом на своем иностранном языке. Можно ли его как-то настраивать под себя?
В этой же связи: не могу решить проблему.
В программе 3 меню и, соответственно, 3 панели инструментов, которые создал в Create. Переключая меню, вызываю
ShowControlBar - прячу ненужные панели и показываю необходимую. Но после вызова PRINT PREVIEW, в окне
появляются сразу все 3 панели инструментов.
Попутно: что означает AFX_IDS_PREVIEW_CLOSE в String Table?
- Serg Petukhov
1. Все языко-зависимые компоненты для печати и предварительного просмотра
(панель инструментов, диалог и строки) в соответствии с идеологией MFC
оформлены как ресурсы. Эти ресурсы лежат в файле MFC42.DLL, но программа
будет искать их там только если они отсутствуют в головной программе. Если
же программа статически линкуется с MFC, ресурсы для печати/предварительного
просмотра берутся из файла afxprint.rc. Чтобы в этом всём убедиться,
достаточно открыть rc-файл, сгенерённым визардом, и найти там строчки:
Теперь понятно, как поправить ситуацию. 2. После выхода из Print Preview запускается функция
CView::OnEndPrintPreview (файл viewcore.cpp). Из неё вызывается ещё одна
функция - CFrameWnd::OnSetPreviewMode (файл winfrm.cpp). Просмотрев код этой
функции, нетрудно убедиться, что она делает видимыми все стандартные панели
с идентификаторами от AFX_IDW_CONTROLBAR_FIRST до
AFX_IDW_CONTROLBAR_FIRST+31 включительно. Таким образом, чтобы MFC не
вмешивалась в вашу работу с панелями инструментов, нужно назначить им
идентификаторы за пределами этого диапазона (например,
AFX_IDW_CONTROLBAR_LAST-N, где N=0,1,2,...): 3. Что касается строки AFX_IDS_PREVIEW_CLOSE, она просто содержит подсказку
для команды Close предварительного просмотра. Если вам интересно, где она
появляется, запустите режим предварительного просмотра, а затем наведите
курсор на пункт Close из системного меню программы (которое раскрывается по
щелчку на иконке в левом верхнем углу главного окна). При этом текст
подсказки о закрытии предварительного просмотра появится в строке состояния.
Можете заменить его на любой другой (на русском языке).
/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /
Q|
Есть приложение на базе диалога. По некоторым причинам необходимо уже внутрь этого диалога вставить закладки (страницы
свойств, как хотите).
Все это нормально делается и проблем тут не возникает. Но вот при использовании клавиши Tab для прогулки по диалогу фокус с
последнего контрола, не принадлежащего Property Page, перемещается не на закладку страницы, а на ее первый определенный в Tab
Layout контрол, и только после пробегания по всем элементам Property Page попадает на закладку.
Как это вылечить?
- George Orlov
|A
Отвечу по порядку.
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)
...
#include "afxprint.rc" // printing/print preview resources
#endif
- Копируем ресурсы из файла afxprint.rc (без окантовочных директив, то есть
от строчки "// Printing Resources") в файл ресурсов нашей программы. При
этом нужно проследить, чтобы новые ресурсы попали между директивами #ifdef
APPSTUDIO_INVOKED и соответствующего #endif (иначе новые ресурсы нельзя
будет изменить в редакторе).
- Убираем из файла ресурсов строчку #include "afxprint.rc" (вручную или
через
- Затем запускаем редактор ресурсов Visual Studio и русифицируем новые
ресурсы. Не забудьте предварительно установить для каждого ресурса в
свойствах Language:Russian, иначе вместо русского языка получите иероглифы!
- Пересобираем проект и убеждаемся, что теперь предварительный просмотр
говорит по-русски.
m_wndToolBar.CreateEx(..., AFX_IDW_CONTROLBAR_LAST);
Ответить на вопрос
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
Это все на сегодня. Счастливо!
Алекс Jenter
jenter@mail.ru
Красноярск, 2001.
|
http://subscribe.ru/
E-mail: ask@subscribe.ru |
| В избранное | ||