Управление системными службами Windows NT

Часть 1

Автор: Александр Федотов
Опубликовано: 24.03.2002
Введение
О демонстрационных программах
Соединение с менеджером системных служб
Функция OpenSCManager
Соединение c другим компьютером
Чтение и изменение конфигурации служб
Параметры конфигурации системной службы
Функции чтения и изменения конфигурации
Продолжение следует
Ссылки

Демонстрационные программы
Исходный код программ

Введение

Системная служба Windows NT (по-английски - system service; в дальнейшем - просто служба) - это фоновый процесс, который может запускаться и работать без участия интерактивного пользователя. В этом смысле системные службы похожи на демоны Unix и резидентные программы MS-DOS. Как правило, системные службы стартуют при загрузке системы и продолжают работать, пока система не будет остановлена, хотя возможны запуск и остановка служб уже в процессе работы.

Архитектура системных служб Windows NT подразумевает три вида компонентов (см. рис. 1). Ee ядром является менеджер системных служб (Service Control Manager или SCM [произносится как "скам"]). SCM запускается при загрузке системы и работает, пока компьютер не будет выключен. Менеджер системных служб взаимодействует с одной стороны с управляющими программами, а с другой - с системными службами. Основными задачами менеджера являются:


Рис. 1. Архитектура системных служб Windows NT

Windows NT определяет два вида системных служб. Один из них - это так называемые службы режима ядра (kernel-mode services), которые есть ни что иное, как драйверы устройств. Другой вид - службы Win32 - это обычные Win32-процессы, использующие специальный набор функций для взаимодействия с менеджером системных служб.

Управляющие программы - это обычные Win32-приложения, которые манипулируют службами с использованием программного интерфейса, предоставляемого менеджером системных служб. Типичный пример управляющей программы - это MMC snap-in "Службы" в Windows 2000.

В этой статье мы постараемся не затрагивать вопросы программирования системных служб (хотя полностью их избежать не удастся). Вместо этого, мы сконцентрируемся на вопросах создания управляющих программ, которые почему-то обходятся стороной многими авторами. Мы будем рассматривать только службы Win32, хотя многие конфигурационные параметры и системные функции одинаково применимы и к службам ядра. Прочитав эту статью, вы досконально изучите окружение, в котором выполняются системные службы. Это поможет вам как при создании своих служб, так и при взаимодействии с другими службами.

О демонстрационных программах

Эту статью сопровождают две демонстрационные программы. Одна из них, svcadmin.exe, является расширенным вариантом MMC snap-in "Службы". Главное ее отличие от системного аналога в том, что она открывает доступ ко всем без исключения параметрам конфигурации системных служб. Надеюсь, пользовательский интерфейс программы достаточно интуитивен и не нуждается в комментариях.


Рис. 2. Программа "Администратор системных служб"

Другая программа, voidsvc.exe, реализует "пустую" системную службу, которая ничего не делает, кроме того, что запускается и останавливается. Вы можете использовать ее в качестве подопытной службы, конфигурацию которой можно изменять, не опасаясь последствий. При запуске и остановке она заносит запись в системный журнал событий - это позволит использовать ее для экспериментов с порядком запуска служб.

voidsvc можно зарегистрировать несколько раз с разными именами. Для установки службы используется командная строка следующего вида:

voidsvc /install:<имя службы>

Удаление службы выполняется аналогично:

voidsvc /uninstall:<имя службы>

Соединение с менеджером системных служб

Функция OpenSCManager

Первое действие, которое должна сделать любая управляющая программа, - это установить соединение с менеджером системных служб, для чего служит функция OpenSCManager:

SC_HANDLE OpenSCManager(
    IN PCTSTR pszMachineName,   // имя компьютера
    IN PCTSTR pszDatabaseName,  // имя базы данных
    IN DWORD dwDesiredAccess    // предполагаемый доступ
    );

При успешном завершении эта функция возвращает handle менеджера системных служб. Этот handle используется в дальнейшем в качестве параметра для других функций. Ниже приведено подробное описание параметров этой функции.

pszMachineName

Этот параметр задает имя компьютера, с менеджером системных служб которого нужно соединиться. Для соединения с SCM на локальном компьютере достаточно указать значение NULL или пустую строку. Если же требуется соединиться с другим компьютером, то перед его именем должно быть указано два обратных слэша, например, "\\MYCOMPUTER".

pszDatabaseName

Задает имя базы данных системных служб. В настоящее время для этого параметра определено только одно значение - SERVICES_ACTIVE_DATABASE, которое означает текущую активную базу данных. NULL также является допустимым значением для этого параметра, его использование эквивалентно использованию константы SERVICES_ACTIVE_DATABASE.

Интересно отметить, что файл winsvc.h, поставляемый в составе Platform SDK, содержит определение еще одной константы, SERVICES_FAILED_DATABASE, которая могла бы использоваться в качестве имени базы данных, и которая нигде не упоминается в официальной документации:

#define SERVICES_ACTIVE_DATABASEW      L"ServicesActive"
#define SERVICES_FAILED_DATABASEW      L"ServicesFailed"

Однако попытка вызвать OpenSCManager с таким параметром приводит к ошибке с кодом ERROR_DATABASE_DOES_NOT_EXIST на всех доступных мне версиях операционной системы, начиная Windows NT 4.0 и заканчивая Windows XP.

dwDesiredAccess

Этот параметр задает виды доступа, которые вызывающая программа предполагает использовать при работе с SCM. Как и для многих других объектов, подсистема безопасности Windows NT проверяет указанные флаги доступа на соответствие дескриптору безопасности, ассоциированному с менеджером системных служб, и разрешает или запрещает доступ к менеджеру.

Права доступа, определенные для менеджера системных служб, приведены в таблице 1. Стандартные права доступа не имеют большого смысла для менеджера системных служб, потому что Win32 API не предоставляет функций, которые могли бы реализовать эти права. Другими словами, ни удалить менеджер системных служб, ни манипулировать его дескриптором безопасности нельзя.

Право доступа Описание
SC_MANAGER_CONNECT Подключение к менеджеру системных служб
SC_MANAGER_ENUMERATE_SERVICE Перечисление системных служб с помошью EnumServicesStatus
SC_MANAGER_QUERY_LOCK_STATUS Определение состояния блокировки с помощью QueryServiceLockStatus
SC_MANAGER_CREATE_SERVICE Создание новой системной службы с помошью CreateService
SC_MANAGER_LOCK Захват блокировки базы данных системных служб посредством LockServiceDatabase
SC_MANAGER_MODIFY_BOOT_CONFIG Сохранение статуса загрузки с помощью функции NotifyBootConfigStatus
SC_MANAGER_ALL_ACCESS Эта константа объединяет все перечисленные выше флаги доступа, а также четыре стандартных права доступа: READ_CONTROL, WRITE_DAC, WRITE_OWNER и DELETE.
Таблица 1. Права доступа к менеджеру системных служб

Содержимое дескриптора безопасности SCM документировано в статье Q179249 базы знаний MSDN:

Everyone (Все)* SC_MANAGER_CONNECT
SC_MANAGER_ENUMERATE_SERVICE
SC_MANAGER_QUERY_LOCK_STATUS
System (Система) SC_MANAGER_CONNECT
SC_MANAGER_ENUMERATE_SERVICE
SC_MANAGER_QUERY_LOCK_STATUS
SC_MANAGER_MODIFY_BOOT_CONFIG
Administrators (Администраторы) SC_MANAGER_ALL_ACCESS
*В Windows XP вместо группы Everyone используется группа Authenticated Users (Прошедшие проверку)

На практике это означает, что только администраторы могут устанавливать новые службы в системе, в то время как все пользователи могут получить список установленных служб.

Когда handle SCM, возвращаемый OpenSCManager, больше не нужен, его следует закрыть с помощью функции CloseServiceHandle:

BOOL CloseServiceHandle(
    IN SC_HANDLE Handle
    );

Соединение c другим компьютером

Параметры OpenSCManager весьма очевидны, и, на первый взгляд, не должно быть никаких проблем соединиться с другим компьютером. Все что нужно сделать, это передать функции правильно сформированное имя компьютера (как вы помните, функция требует два обратных слэша в начале имени).

Но давайте задумаемся на минуту, а какое имя пользователя будет использовано при подключении? Естественно ожидать (и это действительно может оказаться так - см. ниже), что будет использовано имя того пользователя, в контексте которого выполняется поток, вызывающий OpenSCManager. В этом как раз и кроется проблема, потому что текущий пользователь нашего компьютера может не иметь необходимых прав на удаленной машине, или, еще хуже, удаленный компьютер может даже не иметь учетной записи с таким именем.

Таким образом, нам нужен способ явным образом задать имя пользователя и пароль, с которыми будет производиться подключение к удаленному компьютеру, но OpenSCManager не предоставляет такой возможности. Решение проблемы нам может подсказать то, что соединение с менеджером системных служб на удаленном компьютере реализовано с использованием протокола SMB. Это тот же самый протокол, который еще со времен Lan Manager используется для доступа к дискам и принтерам других компьютеров, только в нашем случае подключение производится к ресурсу со специальным именем IPC$.

Протокол SMB известен своей особенностью: как только установлено соединение с каким-либо ресурсом на компьютере, все последующие соединения с этим компьютером используют те же самые имя пользователя и пароль, что и при первом соединении. Это означает, что если вы, скажем, подключили сетевой диск на удаленном компьютере, указав какое-то имя пользователя и пароль, а потом вызываете OpenSCManager, то будет использовано то же самое имя пользователя и тот же пароль, что и при подключении диска. Если же вы не имели никаких соединений с этим компьютером до момента вызова OpenSCManager, то для подключения будут использованы то имя и тот пароль, с которыми вы вошли в систему. Несколько запутанно, не правда ли?

Теперь понятно, что если мы заранее создадим соединение с удаленным компьютером, указав нужные нам имя пользователя и пароль, то OpenSCManager будет использовать именно их. Установить новое соединение можно с помощью функции WNetAddConnection2, но что делать, если это не первое соединение с удаленным компьютером? Если мы попытаемся задать имя пользователя и пароль, отличные от тех, что были использованы при первом соединении, WNetAddConnection2 завершится с ошибкой ERROR_SESSION_CREDENTIAL_CONFLICT. В этом случае не остается ничего, как предложить пользователю закрыть все соединения с этим компьютером (впрочем, вы можете самостоятельно перечислить и закрыть все соединения, но не забудьте предупредить пользователя об этом - он может расстроиться, если вы прервете его в процессе копирования файлов на сетевой диск).

Листинг 1 содержит пример функции, которая устанавливает соединение со SCM на удаленном компьютере, используя указанные имя пользователя и пароль для входа на удаленную систему.

Листинг 1. Соединение с менеджером системных служб
//-----------------------------------------------------------------------
// SCMConnect
//
//  Устанавливает соединение с SCM на локальном или удаленном компьюте- 
//  ре.  При соединении с удаленным компьютером позволяет задать альтер- 
//  нативные имя пользователя и пароль.
//
//  Параметры:
//    pszMachine  - имя удаленного компьютера  или NULL,  для соединения 
//                  с SCM на локальной машине; если указано имя компью- 
//                  тера,  то оно должно начинаться с двух обратных слэ- 
//                  шей
//    pszUserName - имя пользователя,  используется только если произво- 
//                  дится соединение с удаленным компьютером; этот пара- 
//                  метр может  быть NULL,  в таком случае  используется 
//                   имя пользователя по умолчанию
//    pszPassword - пароль;  этот параметр может быть NULL,  только если 
//                  pszUserName тоже NULL
//    dwAccess    - запрашиваемый доступ к менеджеру системных служб
//
//  Возвращает:
//    при уcпешном завершении возвращает handle менеджера системных 
//    служб; в случае ошибки возвращает NULL и код ошибки может быть по- 
//    лучен с помощью GetLastError.
//
SC_HANDLE SCMConnect(
    IN PCTSTR pszMachine,
    IN PCTSTR pszUserName,
    IN PCTSTR pszPassword,
    IN DWORD dwAccess
    )
{
    if (pszMachine != NULL && pszUserName != NULL)
    {
        _ASSERTE(pszPassword != NULL);

        NETRESOURCE NetRes;
        DWORD dwStatus;
        TCHAR szRemoteName[RMLEN + 1];

        _sntprintf(szRemoteName, RMLEN + 1, _T("%s\\IPC$"),
                   pszMachine);

        NetRes.dwType = RESOURCETYPE_ANY;
        NetRes.lpLocalName = NULL;
        NetRes.lpRemoteName = szRemoteName;
        NetRes.lpProvider = NULL;

        // попробуем установить соединение с ресурсом IPC$ на
        // удаленной машине
        dwStatus = WNetAddConnection2(&NetRes, pszPassword, 
                                      pszUserName, 0);
        if (dwStatus != ERROR_SUCCESS)
            return SetLastError(dwStatus), FALSE;
    }

    // устанавливаем соединение с SCM
    return OpenSCManager(pszMachine, NULL, dwAccess);
}

По правилам хорошего тона, если вы установили соединение с удаленной машиной в процессе работы программы, то при завершении программы его следует закрыть. Для этого можно воспользоваться функцией WNetCancelConnection2, вызывать ее надо после того, как закрыт handle менеджера системных служб. Написание функции SCMDisconnect для отсоединения от SCM предлагается вам в качестве упражнения.

Остается только заметить, что некоторые другие сервисы Windows, такие, как удаленный доступ к реестру посредством RegConnectRegistry и удаленный доступ к счетчикам производительности, реализованы сходным образом, поэтому описанный метод аутентификации применим и к ним в полном объеме.

Теперь, когда мы знаем, как соединиться с менеджером системных служб, давайте вернемся к нашей основной теме и посмотрим, что же можно сделать после того, как соединение установлено.

Чтение и изменение конфигурации служб

Мы начнем наше погружение в глубины управления системными службами с рассмотрения конфигурационных параметров служб, потому что все остальные задачи так или иначе связаны с параметрами конфигурации. Например, если мы хотим установить новую службу, необходимо указать конфигурацию для создаваемой службы. Если мы запускаем или останавливаем службу, параметры конфигурации также оказывают влияние на этот процесс.

Параметры конфигурации системной службы

Конфигурация каждой системной службы описывается фиксированным набором параметров, которые перечислены ниже:

Менеджер системных служб хранит информацию о конфигурации служб в ключе реестра

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services

Каждой системной службе соответствует подключ, имя которого совпадает с именем службы. Подключи служб, в свою очередь, содержат набор именованных значений, которые и задают параметры службы. Список всех значений приведен в таблице 2.

Имя Тип Назначение
DisplayName REG_SZ Название службы
Description REG_SZ Описание службы
ImagePath REG_EXPAND_SZ Путь к исполняемому файлу службы
Type REG_DWORD Тип службы
Start REG_DWORD Способ запуска службы
ErrorControl REG_DWORD Реакция на ошибки при загрузке системы
DependOnService REG_MULTI_SZ Службы, от которых зависит данная служба
DependOnGroup REG_MULTI_SZ Группы загрузки, от которых зависит данная служба
Group REG_SZ Группа загрузки, которой принадлежит служба
ObjectName REG_SZ Учетная запись службы
FailureActions REG_BINARY Действия при сбое
Таблица 2. Параметры конфигурации службы в реестре

Дескриптор безопасности хранится в отдельном подключе с именем Security.

SCM загружает сразу всю конфигурационную базу данных в память при старте системы, поэтому любые изменения в реестре не вступят в силу до перезагрузки системы, а для изменения конфигурации служб следует пользоваться специальными функциями, которые мы рассмотрим в следующем разделе. Сейчас же мы подробно изучим каждый из конфигурационных параметров.

Название и внутреннее имя службы

Каждая системная служба имеет два имени: внутреннее имя службы и собственно название. Название используется в пользовательском интерфейсе программ управления службами, таких как MMC snap-in "Службы". Внутреннее имя службы используется в программных интерфейсах, например, оно является аргументом функции OpenService, позволяющей получить handle системной службы.

На внутреннее имя службы накладываются определенные ограничения: оно не должно содержать символов '/' и '\' и его длина не должна превышать 256 символов. Название ограничено только по длине, которая также не должна быть более 256 символов.

И название, и внутреннее имя службы должны быть уникальными. Вы не сможете создать новую службу, если одно из этих имен уже используется другой службой. Хотя менеджер системных служб сохраняет регистр букв в именах служб, все сравнения производятся без учета регистра. Это означает, что, например, имена "MyService" и "myservice" считаются эквивалентными.

Менеджер системных служб предоставляет две функции, позволяющие по внутреннему имени службы получить ее название, и наоборот:

BOOL GetServiceDisplayName(
    IN SC_HANDLE hSCM,        // handle SCM
    IN PCTSTR pszServiceName, // внутреннее имя службы
    IN PTSTR pszDisplayName,  // принимает название службы
    IN PDWORD pcchBuffer      // указывает размер выходного буфера
    );
    
BOOL GetServiceKeyName(
    IN SC_HANDLE hSCM,        // handle SCM
    IN PCTSTR pszDisplayName, // название службы
    IN PTSTR pszServiceName,  // принимает внутреннее имя службы
    IN PDWORD pcchBuffer      // указывает размер выходного буфера
    );

GetServiceDisplayName по внутреннему имени службы выдает ее название, а GetServiceKeyName, наоборот, по названию выдает внутреннее имя службы.

Описание службы

Начиная с Windows 2000, с каждой системной службой можно ассоциировать краткое описание, которое поясняет назначение службы для пользователей. Например, вот так выглядит описание для службы RunAs:

Позволяет запускать процессы от имени другого пользователя.

Максимальная длина описания ограничена 1024 символами. Для установки и чтения описания службы используются функции ChangeServiceConfig2 и QueryServiceConfig2, доступные в Windows 2000 и более поздних версиях.

Тип службы

Этот атрибут задает тип системной службы. Он может принимать одно из четырех значений:

Кроме того, для служб Win32, выполняющихся в контексте системной учетной записи, может быть установлен следующий флаг:

Таким образом, существует два вида служб Win32: выполняющиеся в отдельном процессе и разделяющие процесс с другими службами. Зачем сделано такое различие, почему бы не запускать каждую службу в отдельном процессе? Ответ: для экономии ресурсов. В системе существует довольно много относительно небольших служб, и с каждой версией системы их количество растет. Выполнять каждую из них в отдельном процессе было бы слишком расточительно.

Однако совместное использование процесса несколькими службами имеет и свои недостатки. Прежде всего, это снижающаяся надежность, ведь сбой в одной из служб может привести к отказу всех остальных служб, выполняющихся в этом процессе. Кроме того, это накладывает определенные ограничения со стороны безопасности. В NT 4 и более ранних версиях Windows NT, только службы, выполняющиеся в контексте системной учетной записи (LocalSystem), могли совместно использовать один процесс. В последующих версиях системы это ограничение снято, но все службы в процессе должны быть зарегистрированы под одной учетной записью.

В Windows NT 4 было несколько служб, совместно использовавших процесс services.exe. В Windows 2000 и Windows XP количество служб, совместно использующих процессы, значительно возросло. Чтобы облегчить себе создание такого рода служб, фирма Microsoft создала универсальную оболочку для служб - svchost.exe. Если вы запустите демонстрационную программу Администратор Служб на Windows 2000 или Windows XP, то увидите, что добрая половина всех служб выполняется в процессе svchost.exe:


Рис. 3. Многие службы Windows 2000 и Windows XP выполняются в процессе svchost.exe

Каждой группе служб, которые должны выполняться в одном процессе svchost.exe, назначается уникальное имя, которое указывается в качестве параметра для svchost.exe при регистрации службы. Например, все службы, представленные на рисунке 3, принадлежат группе с именем "netsvcs". svchost.exe хранит информацию о группах в ключе реестра

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\svchost

Будучи запущенным менеджером системных служб, он по параметру командной строки определяет имя группы и считывает из реестра список служб, которые будут представлены этим процессом. Каждой службе соответствует отдельная DLL, которая экспортирует функцию ServiceMain. К сожалению, Microsoft не предоставляет документации, необходимой для использования svchost.exe, поэтому этот механизм пока недоступен независимым разработчикам.

Интерактивные службы

В Windows NT каждый выполняющийся процесс имеет ассоциированную оконную станцию и рабочий стол (window station и desktop). Эти объекты предназначены для разграничения доступа к объектам подсистем User и GDI, таким как окна и перехватчики сообщений (hooks).

Все объекты исполнительной системы Windows NT, такие как файлы и мьютексы, имеют свои собственные дескрипторы безопасности. Когда приложение открывает объект, например, с помощью функции CreateFile, дескриптор безопасности используется, чтобы определить, являются ли допустимыми запрошенные приложением права доступа. Когда создавалась оконная подсистема Windows NT, ее разработчики не стали следовать этой модели, потому что, учитывая интенсивность работы с объектами оконной подсистемы, проверки доступа составили бы значительную часть накладных расходов. Кроме того, это сильно усложнило бы программный интерфейс, который должен был быть совместимым с 16-битной Windows. Поэтому было принято решение создать объекты-контейнеры, такие как оконная станция и рабочий стол, которые содержат в себе объекты User и GDI, и которые защищены дескриптором безопасности. Как только процесс получает доступ к оконной станции или рабочему столу, он автоматически получает доступ ко всем содержащимся в них объектам.

Объект оконной станции содержит в себе буфер обмена (clipboard), набор глобальных атомов и набор объектов рабочих столов. Одна из оконных станций, а именно оконная станция с именем WinSta0, является интерактивной. Эта станция видна интерактивному пользователю, она может получать события ввода, такие, как нажатия клавиш на клавиатуре. Все остальные оконные станции являются неинтерактивными, они никогда не видны интерактивному пользователю и не могут получать события ввода.

Объекты рабочего стола содержат в себе поверхность экрана, набор окон, меню и перехватчики сообщений. Оконная станция может иметь несколько рабочих столов, при этом на интерактивной рабочей станции один из рабочих столов может быть назначен активным - это тот рабочий стол, который сейчас видит пользователь. Как правило, оконная станция WinSta0 содержит три рабочих стола:

То, с какой рабочей станцией связан процесс, влияет на его возможность взаимодействия с другими процессами. Например, оконные сообщения можно посылать только в пределах одного рабочего стола. Если попытаться послать сообщение окну с другого рабочего стола, то сообщение не дойдет, так, как будто этого окна не существует. Перехватчики сообщений получают только сообщения на текущем рабочем столе, они не получают сообщений, адресованных окнам на других рабочих столах.

Я думаю, уже достаточно сказано, чтобы вы задали себе вопрос и с нетерпением ожидали от меня ответа: так на какой же оконной станции и рабочем столе выполняются системные службы? Что ж, ответ зависит от учетной записи, под которой зарегистрирована служба, и от наличия флага SERVICE_INTERACTIVE_PROCESS:

Service-0x0-3e7$\default Учетной записью службы является LocalSystem и флаг SERVICE_INTERACTIVE_PROCESS не установлен
Winsta0\default Учетной записью службы является LocalSystem и флаг SERVICE_INTERACTIVE_PROCESS установлен
Service-0xX-Y$\default Учетная запись службы отлична от LocalSystem; X и Y здесь обозначают части идентификатора логон-сессии

Таким образом, если установлен флаг SERVICE_INTERACTIVE_PROCESS (что, напомню, можно сделать только для службы, выполняющейся в контексте системной учетной записи), то служба будет выполняться на той же оконной станции и том же рабочем столе, что и приложения интерактивного пользователя. Это позволит службе создавать окна, которые будут видимы пользователю, устанавливать перехватчики сообщений, которые будут получать сообщения из пользовательских приложений и т.д. Такая служба называется интерактивной.

Если же этот флаг не установлен, то служба будет неинтерактивной и не будет иметь доступа к рабочему столу пользователя (впрочем, при наличии достаточных привилегий изначально неинтерактивная служба может получить доступ к рабочему столу пользователя, но это тема для отдельной статьи [5]). Создаваемые службой окна будут невидимы, и она не сможет получать события ввода. Поскольку системные службы изначально предназначены для выполнения фоновых задач, не требующих вмешательства пользователя, большинство служб являются неинтерактивными.

Исполняемый файл

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

Например, если путь к службе указан как

C:\Long Path\mysvc.exe -parameter

и существует файл C:\Long.exe, то будет выполнен именно он, а не mysvc.exe. Теперь представьте себе, что обычные пользователи имеют право на запись в корень диска C:, а служба зарегистрирована для работы в контексте системной учетной записи. В результате любой пользователь может получить административные привилегии, просто создав файл C:\Long.exe с подходящими командами. Этого эффекта не будет, если имя исполняемого файла включено в кавычки:

"C:\Long Path\mysvc.exe" -parameter

При задании командной строки для служб, которые должны совместно использовать один процесс, следует иметь в виду, что службы будут исполняться в одном процессе, только если их командные строки совпадают без учета регистра символов. Например, если командные строки заданы следующим образом,

"C:\Long Path\mysvc.exe" -Parameter
"C:\long path\mysvc.exe" -parameter

службы будут запущены в одном процессе, а если добавить хотя бы один лишний пробел,

"C:\Long Path\mysvc.exe" -Parameter
"C:\long path\mysvc.exe"  -parameter

то для каждой службы будет создан свой собственный процесс.

Способ запуска

Способ запуска определяет, каким образом запускается системная служба. Для служб Win32 этот параметр может принимать одно из трех значений:

Последнее значение может быть полезным для борьбы с программами, запускающими службы, запуск которых нежелателен.

Для служб, запускающихся автоматически, порядок запуска определяется принадлежностью службы к группе загрузки (load order group) и зависимостями от других служб. Мы рассмотрим порядок запуска более подробно, когда будем говорить о группе загрузки и зависимостях службы.

Реакция на ошибки при загрузке системы

Этот параметр конфигурации задает реакцию менеджера системных служб в случае, если служба, отмеченная для автоматического запуска при старте системы, не может быть запущена. Параметр может принимать одно из четырех значений:

*Службы Win32 всегда запускаются менеджером системных служб, однако драйвера устройств могут также загружаться системным загрузчиком или процедурой инициализации системы, поэтому здесь используется более общий термин "программа загрузки".

Флаги SERVICE_ERROR_SEVERE и SERVICE_ERROR_CRITICAL используются главным образом драйверами устройств. В стандартной установке Windows NT ни одна из служб Win32 не имеет такого типа реакции на ошибки при загрузке системы.

Менеджер системных служб хранит копию конфигурационной базы данных для последней успешной загрузки системы. Если служба или драйвер, отмеченные флагом SERVICE_ERROR_SEVERE или SERVICE_ERROR_CRITICAL, по каким-либо причинам не загружаются, система пытается загрузиться заново, используя сохраненную базу данных. Это дает шанс системному администратору загрузить систему и исправить ошибку в конфигурации.

Каким образом система определяет, что загрузка была успешной? Загрузка считается успешной, как только была вызвана функция NotifyBootConfigStatus. По умолчанию эту функцию вызывает Winlogon, после того как совершен первый успешный интерактивный вход в систему.

Такое определение успешной загрузки хорошо подходит для интерактивной рабочей станции, но не очень годится для сервера, который работает без участия человека. Например, система, специально выделенная для работы Web-сервера, вряд ли может считаться загруженной успешно, если служба Web-cервера не запустилась. В то же время, наличие успешного интерактивного входа в систему в этом случае не так важно.

Операционная система предоставляет возможность изменения логики успешной загрузки. Прежде всего, можно запретить Winlogon сообщать об успешной загрузке, для чего в ключе реестра

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon

следует установить значение ReportBootOK в ноль. Теперь ответственность за вызов NotifyBootConfigStatus и, тем самым, признание загрузки успешной лежит на вас. Это можно сделать двумя способами.

Во-первых, можно зарегистрировать службу bootvrfy.exe, которая входит в поставку любой версии Windows NT. Эта служба ничего не делает, кроме как вызывает NotifyBootConfigStatus при старте. Поэтому, если зарегистрировать ее для автоматического запуска и настроить ее зависимости таким образом, что она будет запускаться только после того, как запущены все важные службы, то загрузка будет считаться успешной после запуска этих служб.

Если же требуется более сложная логика, то существует возможность написать свою собственную программу проверки и зарегистрировать ее в ключе реестра

HKLM\SYSTEM\CurrentControlSet\Control\BootVerificationProgram

В нем следует создать строковое значение ImagePath (если оно еще не создано), содержащее путь к программе. Менеджер системных служб будет запускать эту программу при каждой загрузке. Программа в свою очередь должна определить, является ли загрузка успешной или нет, используя только ей одной известную логику, и вызвать NotifyBootConfigStatus.

Группа загрузки

Многие службы полагаются в своей работе на другие службы. Например, любая служба, являющаяся также и COM-сервером, может работать, только если запущена служба удаленного вызова процедур (RPC). При автоматическом запуске служб в процессе загрузки системы необходимо обеспечить такую последовательность запуска, чтобы при запуске каждой службы все необходимые ей службы уже работали. Менеджер системных служб решает эту задачу двумя способами: объединением служб в группы загрузки и заданием зависимостей между службами.

Группы загрузки (load order groups) позволяют объединить системные службы в группы и запускать группы по очереди. Службы из каждой группы запускаются только тогда, когда запущены все службы из предыдущей группы, отмеченные для автоматического старта. Службы, которые не принадлежат ни одной из групп загрузки, запускаются в последнюю очередь. Порядок запуска служб Win32 в пределах одной группы не определен (для служб ядра порядок загрузки в пределах одной группы устанавливается с помощью дополнительного параметра - тега; для служб Win32 он не используется).

Менеджер системных служб хранит список групп загрузки в ключе реестра

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ServiceGroupOrder

Значение List в этом ключе, имеющее тип REG_MULTI_SZ, перечисляет группы загрузки в том порядке, в котором они должны загружаться. Сразу после установки системы список содержит около 30 стандартных групп, которые используются главным образом драйверами устройств. Запустив Администратор системных служб (демонстрационное приложение для этой статьи), вы найдете, что относительно немного служб Win32 принадлежат к какой-либо группе загрузки.

Разработчик службы может определить свою собственную группу загрузки, для чего нужно вставить в подходящей позиции новый элемент в упомянутый выше список групп. Если в конфигурации службы указать группу загрузки, которая не перечислена в списке, то служба будет запущена после того, как запустятся все группы, но до запуска служб, группа загрузки которых не указана.

Зависимости

Более тонкую настройку порядка запуска можно осуществить, установив зависимости между службами. Каждая служба имеет так называемый список зависимостей - список служб, от которых зависит данная служба. Служба может быть запущена только тогда, когда запущены все службы, от которых она зависит.

При старте системы менеджер системных служб строит граф зависимостей между службами и путем обхода этого графа определяет порядок, в котором он будет запускать службы. Очевидно, это можно сделать только в том случае, если граф не будет содержать циклов, то есть круговых зависимостей. SCM обнаруживает круговые зависимости еще на этапе конфигурации службы, если вы попытаетесь установить службе такие зависимости, которые приводят к появлению цикла в графе, то ChangeServiceConfig вернет код ошибки ERROR_CIRCULAR_DEPENDENCY. На рис. 4 показаны зависимости службы Fax в Windows XP.


Рис. 4. Зависимости службы Fax

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

Если в конфигурации службы указана и группа загрузки, и зависимости, то при возникновении конфликтной ситуации группа загрузки имеет приоритет. Например, возможно задать такую конфигурацию, где две службы, Служба A и Служба B, принадлежат группам загрузки Группа A и Группа B соответственно, причем Группа А запускается перед Группой B. Если теперь задать зависимость Службы A от Службы B, то возникнет противоречие: с одной стороны, Служба B должна быть запущена после Службы A, так как она принадлежит более поздней группе загрузки, а с другой - Служба B должна быть запущена перед Службой A, так как последняя зависит от Службы B. В итоге Служба A не будет запущена вовсе, а в журнал событий будет записано сообщение об ошибке.

Завершая обсуждение групп загрузки и зависимостей, заметим, что группы загрузки имеют значение только при автоматическом запуске служб при старте системы и на дальнейшую работу не оказывают влияния. В отличие от них, зависимости службы анализируются как при автоматическом старте, так и при программном запуске служб с помощью StartService. Так, при запуске службы SCM пытается сначала запустить все службы, от которых зависит запускаемая служба. Мы коснемся этого вопроса еще раз, когда будем обсуждать запуск и остановку служб.

Учетная запись службы

Любой процесс в Windows NT выполняется от лица какого-либо пользователя. Всякий раз, когда один из потоков процесса осуществляет доступ к ресурсу, например, открывая файл или ключ реестра, подсистема безопасности проверяет, имеет ли этот пользователь право доступа к указанному ресурсу. Каждый пользователь идентифицируется своей учетной записью (logon account) в системе, и когда говорят о пользователях в контексте безопасности, на самом деле имеют в виду их учетные записи.

Службы здесь не являются исключением. Создавая процесс службы, SCM осуществляет процедуру входа в систему для учетной записи, указанной в конфигурации службы, и процесс службы выполняется от имени этой учетной записи (к сожалению, очень тяжело подобрать подходящий русскоязычный термин для английского logon, "вход в систему" не вполне точно отражает суть процесса. Для русского человека это словосочетание ассоциируется с действиями конкретного пользователя, в то время как в данном контексте это сугубо программный процесс аутентификации, который может происходить без участия и ведома пользователя). Как уже отмечалось, если несколько служб совместно используют процесс, то всем им должна быть назначена одна и та же учетная запись. В Windows NT 4 и Windows 2000 по большому счету вы имеете выбор из трех вариантов:

Windows XP добавляет еще два пункта в меню:

Мы не будем подробно рассматривать все последствия, которые влечет выбор той или иной учетной записи, поскольку этот вопрос заслуживает отдельной статьи.

Задавая учетную запись для службы, вы должны указать имя пользователя и пароль. Специальные учетные записи, LocalSystem, LocalService и NetworkService, не имеют пароля. Для обычных учетных записей требуется пароль, который менеджер системных служб сохраняет и использует в процедуре входа в систему, которая необходима для создания процесса от имени этой учетной записи. Если пароль задан неверно, это обнаружится только в момент запуска службы, так как именно в этот момент выполняется процедура входа в систему и проверяется пароль.

Кроме того, учетная запись, назначаемая службе, должна иметь привилегию входа в систему в качестве службы (SE_SERVICE_LOGON_NAME). LocalSystem, LocalService и NetworkService уже имеют эту привилегию, а вот обычные пользовательские учетные записи, как правило, нет. И опять, если несоблюдение этого условия будет обнаружено только при запуске службы. Конечно, будучи администратором, вы можете добавить все необходимые привилегии, но функции управления конфигурацией служб не делают это автоматически. Поэтому вам придется запрограммировать добавление привилегии самостоятельно. Вы можете взять за основу статью Q132958 базы знаний MSDN или функцию GrantPrivilege из исходного кода svcadmin.exe.

Действия при сбое

Начиная с Windows 2000, на менеджер системных служб, помимо его основных задач, возложена также задача мониторинга работающих служб. Если процесс службы перестает существовать, не известив менеджер об остановке службы, то такая ситуация считается сбоем службы. SCM ведет счетчик сбоев службы, начиная с загрузки системы, и позволяет ассоциировать определенное действие с каждым сбоем.

Набор действий при сбое определяется структурой SERVICE_FAILURE_ACTIONS:

typedef struct _SC_ACTION {
    SC_ACTION_TYPE  Type;
    DWORD           Delay;
} SC_ACTION, *LPSC_ACTION;

typedef struct _SERVICE_FAILURE_ACTIONS {
    DWORD           dwResetPeriod;
    PTSTR           lpRebootMsg;
    PTSTR           lpCommand;
    DWORD           cActions;
    SC_ACTION *     lpsaActions;
} SERVICE_FAILURE_ACTIONS, *LPSERVICE_FAILURE_ACTIONS;

Система предлагает выбор из четырех действий, которые можно выполнить при сбое службы. Эти действия задаются элементами перечисления SC_ACTION_TYPE:

SC_ACTION_NONE Не выполнять никакого действия
SC_ACTION_REBOOT Перезагрузить систему
SC_ACTION_RESTART Перезапустить службу
SC_ACTION_RUN_COMMAND Запустить указанную программу

Поле Delay структуры SC_ACTION указывает задержку в миллисекундах, которую менеджер системных служб должен сделать, прежде чем выполнять действие. Это полезно главным образом при перезагрузке системы.

Вы можете задать любое количество действий. Когда список действий будет исчерпан, при каждом новом сбое службы SCM будет выполнять последнее действие в списке. Кроме того, можно заставить менеджера сбросить счетчик сбоев, если служба работает нормально в течение определенного интервала времени. Это достигается с помощью поля dwResetPeriod структуры SERVICE_FAILURE_ACTIONS, которое задает длительность интервала в секундах. Если указано значение INFINITE, счетчик сбоев не будет сбрасываться.

Список действий при сбое можно задать для всех служб, кроме тех, что исполняются в одном процессе с менеджером системных служб, то есть services.exe. Причину такого ограничения нетрудно понять: если процесс services.exe завершится в результате сбоя, то менеджер системных служб завершит свою работу вместе с ним и выполнять указанные действия будет некому (на самом деле, если такое произойдет и процесс services.exe прекратит существование, система в скором времени остановится на синем экране смерти).

Давайте более подробно рассмотрим каждое из возможных действий. Если вы хотите поддержать теорию экспериментом, вы может воспользоваться тестовой службой, которая прилагается к этой статье. Чтобы имитировать сбой службы, достаточно принудительно завершить процесс службы, что можно сделать, например, с помощью утилит из статьи Как принудительно завершить процесс?

Перезагрузка системы

Если указано это действие, SCM инициирует перезагрузку системы в случае сбоя службы. Перед тем как инициировать перезагрузку, он рассылает всем компьютерам в сети сообщение, указанное в поле lpRebootMsg структуры SERVICE_FAILURE_ACTIONS. Этот механизм призван привлечь внимание администраторов к возникшей проблеме (если, конечно, такое возможно).

Это может показаться неочевидным, но если вы добавляете это действие в конфигурацию службы на локальном компьютере, вы должны иметь привилегию SE_SHUTDOWN_NAME, которая должна быть включена перед вызовом ChangeServiceConfig2. Вездесущая подсистема безопасности Windows NT не позволит вам перезагрузить компьютер даже таким опосредованным методом, если у вас нет достаточных привилегий.

Перезапуск службы

Это действие означает автоматический перезапуск службы при сбое. Например, служба удаленного доступа к реестру в Windows 2000 по умолчанию сконфигурирована именно таким образом. И снова подсистема безопасности дает о себе знать: если вы хотите добавить это действие в конфигурацию службы, вы должны открыть handle службы с правом доступа SERVICE_START, помимо SERVICE_CHANGE_CONFIG, которое обычно необходимо для изменения конфигурации.

Выполнение программы

Это действие позволяет вам задать выполнение произвольной программы в случае сбоя службы. Командная строка, используемая для запуска программы, задается полем lpCommand структуры SERVICE_FAILURE_ACTIONS. Программа будет выполняться в том же контексте безопасности (под той же учетной записью), что и неожиданно завершившаяся служба.

Как видите, можно задать только одну командную строку на все действия. Это означает, что если в вашем списке действий действие запуска программы встречается несколько раз, например, для первого и второго сбоя, то и для первого, и для второго сбоя будет запущена одна и та же программа. Чтобы позволить программе отличить один сбой от другого, SCM позволяет задать в командной строке метапараметр "%1%". При запуске программы эта последовательность будет заменена номером сбоя.

Например, если вы зададите командную строку как:

"С:\Program Files\My Application\recovery.exe" %1%

то при первом и втором сбоях программа будет вызвана как

"С:\Program Files\My Application\recovery.exe" 1
"С:\Program Files\My Application\recovery.exe" 2

соответственно. Таким образом программа сможет определить номер сбоя и предпринять соответствующие действия.

Дескриптор безопасности

Наконец, последний параметр конфигурации службы - это дескриптор безопасности, который разграничивает доступ к объекту службы. Предусмотренные для службы права доступа перечислены в таблице 3.

Право доступа Описание
SERVICE_QUERY_CONFIG Чтение конфигурации службы с помощью функций QueryServiceConfig и QueryServiceConfig2
SERVICE_CHANGE_CONFIG Изменение конфигурации службы с помощью ChangeServiceConfig и ChangeServiceConfig2
SERVICE_QUERY_STATUS Запрос текущего состояния службы посредством QueryServiceStatus
SERVICE_ENUMERATE_DEPENDENTS Перечисление зависимых служб с помощью функции EnumDependentServices
SERVICE_START Запуск службы с помощью StartService
SERVICE_STOP Остановка службы посредством функции ControlService с кодом управления SERVICE_CONTROL_STOP
SERVICE_PAUSE_CONTINUE Приостановка и возбновление службы с помощью ControlService с кодами управления SERVICE_CONTROL_PAUSE и SERVICE_CONTROL_CONTINUE соответственно
SERVICE_INTERROGATE Требование к службе немедленно доложить о своем статусе с помощью ControlService с кодом SERVICE_CONTROL_INTERROGATE
SERVICE_USER_DEFINED_CONTROL Посылка пользовательских сигналов управления посредством ControlService
DELETE Удаление службы с помощью DeleteService
READ_CONTROL Чтение дескриптора безопасности с помощью QueryServiceObjectSecurity
WRITE_DAC Установка нового списка доступа в дескриптор безопасности с помощью SetServiceObjectSecurity
WRITE_OWNER Смена владельца в дескрипторе безопасности посредством SetServiceObjectSecurity
SERVICE_ALL_ACCESS Эта константа является комбинацией всех перечисленных выше прав доступа.
Таблица 3. Права доступа к системной службе

В статье Q180116 базы знаний MSDN описано, что по умолчанию содержит дескриптор безопасности службы. В отличие от дескриптора безопасности SCM, дескриптор безопасности службы может быть изменен с помощью функции SetServiceObjectSecurity. На практике, однако, к этому приходится прибегать крайне редко, потому что стандартный дескриптор безопасности, назначаемый системой, вполне хорошо подходит в большинстве случаев:

Everyone (Все)* SERVICE_ENUMERATE_DEPENDENTS
SERVICE_INTERROGATE
SERVICE_QUERY_CONFIG
SERVICE_QUERY_STATUS
SERVICE_USER_DEFINED_CONTROL
READ_CONTROL
System (Система)
Power Users (Опытные пользователи)
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_INTERROGATE
SERVICE_PAUSE_CONTINUE
SERVICE_QUERY_CONFIG
SERVICE_QUERY_STATUS
SERVICE_START
SERVICE_STOP
SERVICE_USER_DEFINED_CONTROL
READ_CONTROL
Administrators (Администраторы)
Server Operators (Операторы сервера)**
SERVICE_ALL_ACCESS
*В Windows 2000 и Windows XP вместо группы Everyone используется группа Authenticated Users (Прошедшие проверку), которая включает всех аутентифицированных пользователей (группа Everyone включает еще и анонимных пользователей).
**Эта группа присутствует только в Windows NT 4.

Функции чтения и изменения конфигурации

Достаточно теории, приступим к практике. В этом разделе мы рассмотрим функции, которые предоставляет SCM для управления конфигурацией служб. Но прежде чем двинуться дальше, мы рассмотрим функцию OpenService, которую необходимо вызвать, чтобы получить handle системной службы. Этот handle идентифицирует службу при вызове всех функций управления службами, будь то функции чтения и изменения конфигурации или функции для запуска и остановки служб.

SC_HANDLE OpenService(
    IN SC_HANDLE hSCM,          // handle SCM
    IN PCTSTR pszServiceName,   // имя системной службы
    IN DWORD dwDesiredAccess    // предполагаемый доступ
    );

При успешном завершении функция возвращает handle системной службы. В случае ошибки возвращаемое значение равно NULL, и код ошибки, как обычно, может быть получен с помощью функции GetLastError. Давайте более подробно изучим параметры этой функции.

hSCM

Этот параметр указывает SCM, которому принадлежит открываемая служба. Как нетрудно догадаться, в качестве этого параметра должно использоваться возвращаемое значение OpenSCManager. Никаких особенных прав доступа к менеджеру системных служб для успешного выполнения этой функции не нужно, достаточно иметь право доступа SC_MANAGER_CONNECT (которое в любом случае необходимо для соединения с SCM).

pszServiceName

Этот параметр задает имя службы, handle которой нужно получить. Как вы помните, каждая служба имеет два имени: название и внутреннее имя. При вызове OpenService, очевидно, надо указать второе имя.

dwDesiredAccess

Последний параметр задает набор прав доступа, которые требуется получить. Полный список прав доступа к службе был приведен в разделе, посвященном дескриптору безопасности службы.

Задавая этот параметр, всегда нужно указывать именно те флаги доступа, которые действительно необходимы. Это позволит пользоваться вашей программой наиболее широкому кругу пользователей. Например, если вы хотите отображать текущее состояние некоторой службы иконкой на панели задач, но при вызове OpenService указали SERVICE_ALL_ACCESS, то программой, скорее всего, смогут пользоваться только администраторы, поскольку только администраторам по умолчанию разрешен полный доступ к службам. Если бы вы указали только право доступа SERVICE_QUERY_STATUS, которое на самом деле необходимо в данной ситуации, то и обычные пользователи смогли бы воспользоваться этой программой.

Handle службы, полученный с помощью OpenService, как и handle SCM, следует закрывать с помощью CloseServiceHandle, когда он больше не нужен.

QueryServiceConfig

Функция QueryServiceConfig возвращает информацию о конфигурации службы.

BOOL QueryServiceConfig(
    IN SC_HANDLE hService,               // handle службы
    OUT LPQUERY_SERVICE_CONFIG pConfig,	 // приемный буфер
    IN DWORD cbBufSize,                  // размер приемного буфера
    OUT PDWORD pcbBytesNeeded            // требуемый размер буфера
    );

При успешном завершении функция возвращает TRUE. В случае ошибки функция возвращает FALSE, и код ошибки может быть получен с помощью GetLastError.

hService

Задает handle службы, конфигурационную информацию которой требуется получить. Для успешного выполнения функции, handle должен быть открыт с правом доступа SERVICE_QUERY_CONFIG.

pConfig

Задает указатель на буфер, в который функция заносит структуру QUERY_SERVICE_CONFIG. Эта структура содержит практически всю конфигурационную информацию, кроме описания службы и действий при сбое, которые доступны через QueryServiceConfig2:

typedef struct _QUERY_SERVICE_CONFIG { 
    DWORD  dwServiceType;      // тип службы
    DWORD  dwStartType;        // способ запуска службы
    DWORD  dwErrorControl;     // реакция на ошибки при загрузке системы
    PTSTR  lpBinaryPathName;   // исполняемый файл службы
    PTSTR  lpLoadOrderGroup;   // группа загрузки
    DWORD  dwTagId;            // тег (не используется для служб Win32)
    PTSTR  lpDependencies;     // зависимые службы
    PTSTR  lpServiceStartName; // учетная запись службы
    PTSTR  lpDisplayName;      // название службы
} QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG; 

Поля этой структуры говорят сами за себя. Поле lpDependencies содержит указатель на строку, содержащую имена служб и групп загрузки, от которых зависит данная служба,разделенные нулем. Конец списка обозначается двумя идущими подряд нулевыми символами. Чтобы можно было отличить имена служб от имен групп загрузки, перед именами последних ставится символ, задаваемый макросом SC_GROUP_IDENTIFIER, который определен в файле winsvc.h как '+'. Например, если некоторая служба зависит от службы RPCSS и группы NetworkDDE, поле lpDependencies будет указывать на такую строку:

RPCSS\0+NetworkDDE\0\0

cbBufSize

Задает размер выходного буфера, в который функция записывает конфигурационную информацию. Поскольку функция заносит строки в тот же буфер, что и саму структуру QUERY_SERVICE_CONFIG, необходимый размер буфера всегда больше размера структуры и предсказать его заранее сложно. Чтобы определить требуемый размер буфера, используется следующий параметр функции.

pcbBytesNeeded

Этот параметр является указателем на переменную, в которую функция заносит необходимый размер буфера. Обычно QueryServiceConfig вызывается два раза: первый раз с нулевым размером буфера, чтобы получить необходимый размер, а затем, предварительно выделив буфер необходимого размера, второй раз, чтобы получить конфигурационную информацию. Пример, представленный в конце этого раздела, демонстрирует этот подход.

ChangeServiceConfig

Функция ChangeServiceConfig служит для изменения конфигурации службы.

BOOL ChangeServiceConfig(
    IN SC_HANDLE hService,          // handle службы
    IN DWORD     dwServiceType,     // тип службы
    IN DWORD     dwStartType,       // способ запуска службы
    IN DWORD     dwErrorControl,    // реакция на ошибки при загрузке
    IN PCTSTR    pBinaryPathName,   // исполняемый файл
    IN PCTSTR    pLoadOrderGroup,   // группа загрузки
    IN PDWORD    pdwTagId,          // тег (NULL для служб Win32)
    IN PCTSTR    pDependencies,     // зависимости
    IN PCTSTR    pServiceStartName, // учетная запись службы
    IN PCTSTR    pPassword,         // пароль учетной записи
    IN PCTSTR    pDisplayName       // название службы
    );

Функция возвращает TRUE при успешном завершении и FALSE - в случае ошибки. В последнем случае код ошибки можно получить с помощью GetLastError.

Параметры этой функции достаточно очевидны, и мы не будем подробно описывать каждый из них. Отметим, что эта функция позволяет выборочно задавать параметры конфигурации. Если вы не хотите изменять какой-либо параметр, следует передать значение SERVICE_NO_CHANGE для параметров типа DWORD, или NULL - для параметров типа PCTSTR. Например, вот такой вызов изменяет только название службы:

ChangeServiceConfig(hService, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
                    SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL,
                    _T("My Service"));

Если задается список зависимостей, он должен быть в том же формате, в котором его возвращает QueryServiceConfig, то есть отдельные имена служб должны быть разделены нулевым символом, а конец списка обозначается двумя идущими подряд нулевыми символами.

QueryServiceConfig2

Функция QueryServiceConfig2 позволяет получить дополнительные параметры конфигурации службы, такие как описание и список действий при сбое службы. Эта функция присутствует только в Windows 2000 и более поздних версиях.

BOOL QueryServiceConfig2(
    IN SC_HANDLE hService,     // handle службы
    IN DWORD dwInfoLevel,      // тип информации
    OUT PBYTE pBuffer,         // приемный буфер
    IN DWORD cbBufSize,        // размер приемного буфера
    OUT PDWORD pcbBytesNeeded  // требуемый размер буфера
    );

Как и другие функции, возвращающие булево значение, она возвращает TRUE при успешном завершении и FALSE - в случае ошибки. Код ошибки может быть получен с помощью GetLastError.

hService

Задает handle службы, конфигурационную информацию которой требуется получить. Для успешного выполнения функции, handle должен быть открыт с правом доступа SERVICE_QUERY_CONFIG.

dwInfoLevel

Указывает, какую информацию требуется получить. В настоящее время определены только два значения этого параметра. Не исключено, что в будущих версиях Windows этот набор будет расширен.

SERVICE_CONFIG_DESCRIPTION Требуется получить описание службы
SERVICE_CONFIG_FAILURE_ACTIONS Требуется получить список действий при сбое

pBuffer

Указывает адрес буфера, в который функция поместит запрашиваемую информацию. Если запрашивается описание службы, то формат буфера описывается структурой SERVICE_DESCRIPTION, которая содержит всего лишь одно поле, указывающее на описание службы:

typedef struct _SERVICE_DESCRIPTION {
  PTSTR    lpDescription;   // описание службы
} SERVICE_DESCRIPTION, * LPSERVICE_DESCRIPTION;

Если запрашивается набор действий при сбое, содержимое буфера будет соотвествовать структуре SERVICE_FAILURE_ACTIONS, с которой мы уже знакомы (см. выше).

cbBufSize

Задает размер выходного буфера. Как и для QueryServiceConfig, записываемые в буфер данные имеют переменный размер, и, чтобы правильно определить требуемый размер, необходимо воспользоваться последним параметром функции.

pcbBytesNeeded

Этот параметр является указателем на переменную, в которую функция заносит требуемый размер буфера. В конце этого раздела приведен пример, демонстрирующий использование этого параметра.

ChangeServiceConfig2

Функция ChangeServiceConfig2 позволяет установить описание и список действий при сбое службы.

BOOL ChangeServiceConfig2(
    IN SC_HANDLE hService,  // handle службы
    IN DWORD dwInfoLevel,   // тип информации
    IN PVOID pInfo          // устанавливаемая информация
);

hService

Задает handle службы, конфигурационную информацию которой требуется изменить. Для успешного выполнения функции, handle должен быть открыт с правом доступа SERVICE_CHANGE_CONFIG. Однако если устанавливается список действий при сбое, включающий перезапуск службы, handle должен быть открыт с правом доступа SERVICE_START.

Кроме того, если для службы на локальном компьютере, устанавливается список действий при сбое, содержащий перезагрузку системы, то в токене вызывающего должна быть включена привилегия SE_SHUTDOWN_NAME.

dwInfoLevel

Указывает тип устанавливаемой информации. Это те же два значения, которые используются вместе с QuerySystemConfig2:

SERVICE_CONFIG_DESCRIPTION Требуется установить описание службы
SERVICE_CONFIG_FAILURE_ACTIONS Требуется установить список действий при сбое

pInfo

Этот параметр указывает на структуру, содержащую устанавливаемую информацию. В зависимости от типа информации это может быть структура SERVICE_DESCRIPTION либо структура SERVICE_FAILURE_ACTIONS. Функция ChangeServiceConfig2 трактует строковые поля в этих структурах особым образом: если поле равно NULL, то соответствующий параметр остается без изменений. Если же оно указывает на пустую строку, параметр конфигурации удаляется.

Ниже приведен пример установки списка действий при сбое службы. Этот фрагмент указывает менеджеру системных служб при первом и втором сбое службы немедленно перезапустить ее, а при возникновении третьего сбоя, перезагрузить компьютер, выдержав паузу в 60 секунд. Помимо этого, фрагмент удаляет командную строку, если она была ранее задана, поскольку поле lpCommand указывает на пустую строку, но оставляет без изменения сообщение, рассылаемое по сети перед перезагрузкой, так как соответствующий параметр установлен в NULL:

SC_ACTION Action[3];
Action[0].Type = SC_ACTION_RESTART;
Action[0].Delay = 0;
Action[1].Type = SC_ACTION_RESTART;
Action[1].Delay = 0;
Action[2].Type = SC_ACTION_REBOOT
Action[2].Delay = 60000;

SERVICE_FAILURE_ACTIONS FailureActions;
FailureActions.dwResetPeriod = INFINITE;
FailureActions.lpRebootMsg = NULL;
FailureActions.lpCommand = _T("");
FailureActions.cActions = 3;
FailureActions.lpsaActions = Action;

ChangeServiceConfig2(hService, SERVICE_CONFIG_FAILURE_ACTIONS,
		     &FailureActions);

Пример: получение конфигурационной информации службы

Как и было обещано, в завершение части статьи, посвященной конфигурации системных служб, мы рассмотрим небольшой пример (см. листинг 2). Это консольное приложение, отображающее конфигурацию службы, имя которой указано в качестве параметра командной строки.

Чтобы не загромождать пример лишними деталями, я решил ограничить части, специфичные для Windows 2000 и более поздних версий, директивами препроцессора, вместо того, чтобы определять версию операционной системы в процессе выполнения и находить соответствующие функции в библиотеке ADVAPI32.DLL.

Листинг 2. Получение конфигурационной информации службы
int
_tmain(
    int argc,
    _TCHAR * argv[]
    )
{
    if (argc < 2)
        return PrintUsage();

    SC_HANDLE hSCM = NULL;
    SC_HANDLE hService = NULL;

    LPQUERY_SERVICE_CONFIG pConfig = NULL;

#if _WIN32_WINNT >= 0x500
    LPSERVICE_DESCRIPTION pDesc = NULL;
    LPSERVICE_FAILURE_ACTIONS pActions = NULL;
#endif

    DWORD cbNeeded;
    BOOL bOk = FALSE;

    for (;;)
    {
        // открываем handle менеджера системных служб
        hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
        if (hSCM == NULL)
        {
            PrintError(_T("OpenSCManager failed"), GetLastError());
            break;
        }

        // открываем handle службы, запрашивая право доступа
        // SERVICE_QUERY_CONFIG
        hService = OpenService(hSCM, argv[1], SERVICE_QUERY_CONFIG);
        if (hService == NULL)
        {
            PrintError(_T("OpenService failed"), GetLastError());
            break;
        }

        // определяем, сколько места необходимо, чтобы получить
        // конфигурацию службы
        if (!QueryServiceConfig(hService, NULL, 0, &cbNeeded))
        {
            if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            {
                PrintError(_T("QueryServiceConfig failed"), 
                           GetLastError());
                break;
            }
        }

        // выделяем память для буфера конфигурации
        pConfig = (LPQUERY_SERVICE_CONFIG)_alloca(cbNeeded);
        _ASSERTE(pConfig != NULL);

        // получаем конфигурацию службы
        if (!QueryServiceConfig(hService, pConfig, cbNeeded, &cbNeeded))
        {
            PrintError(_T("QueryServiceConfig failed"), GetLastError());
            break;
        }

#if _WIN32_WINNT >= 0x500
        // определяем, сколько места необходимо, чтобы получить
        // описание службы
        if (!QueryServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION,
                                 NULL, 0, &cbNeeded))
        {
            if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            {
                PrintError(_T("QueryServiceConfig2 failed"),
                           GetLastError());
                break;
            }
        }

        // выделяем память для описания службы
        pDesc = (LPSERVICE_DESCRIPTION)_alloca(cbNeeded);
        _ASSERTE(pDesc != NULL);

        // получаем описание службы
        if (!QueryServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION,
                                 (PBYTE)pDesc, cbNeeded, &cbNeeded))
        {
            PrintError(_T("QueryServiceConfig2 failed"), GetLastError());
            break;
        }

        // определяем, сколько места необходимо, чтобы получить
        // действия при сбое службы
        if (!QueryServiceConfig2(hService, SERVICE_CONFIG_FAILURE_ACTIONS,
                                 NULL, 0, &cbNeeded))
        {
            if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            {
                PrintError(_T("QueryServiceConfig2 failed"), 
                           GetLastError());
                break;
            }
        }

        // выделяем память для действий при сбое службы
        pActions = (LPSERVICE_FAILURE_ACTIONS)_alloca(cbNeeded);
        _ASSERTE(pActions != NULL);

        // получаем действия при сбое службы
        if (!QueryServiceConfig2(hService, SERVICE_CONFIG_FAILURE_ACTIONS,
                                 (PBYTE)pActions, cbNeeded, &cbNeeded))
        {
            PrintError(_T("QueryServiceConfig2 failed"), GetLastError());
            break;
        }
#endif

        bOk = TRUE;
        break;
    }

    if (hService != NULL)
        _VERIFY(CloseServiceHandle(hService));
    if (hSCM != NULL)
        _VERIFY(CloseServiceHandle(hSCM));

    if (!bOk)
        return 1;

    // подготавливаем строки
    PCTSTR pszServiceType = NULL;
    PCTSTR pszInteractive = NULL;
    PCTSTR pszStartType = NULL;
    PCTSTR pszErrorControl = NULL;

    switch (pConfig->dwServiceType & ~SERVICE_INTERACTIVE_PROCESS)
    {
        case SERVICE_KERNEL_DRIVER:
            pszServiceType = _T("Kernel Driver");
            break;
        case SERVICE_FILE_SYSTEM_DRIVER:
            pszServiceType = _T("File System Driver");
            break;
        case SERVICE_WIN32_OWN_PROCESS:
            pszServiceType = _T("Win32 Own Process");
            break;
        case SERVICE_WIN32_SHARE_PROCESS:
            pszServiceType = _T("Win32 Share Process");
            break;
        default:
            pszServiceType = _T("Unknown");
            break;
    }

    if (pConfig->dwServiceType & SERVICE_INTERACTIVE_PROCESS)
        pszInteractive = _T("(Interactive)");
    else
        pszInteractive = _T("");

    switch (pConfig->dwStartType)
    {
        case SERVICE_BOOT_START:
            pszStartType = _T("Boot");
            break;
        case SERVICE_SYSTEM_START:
            pszStartType = _T("System");
            break;
        case SERVICE_AUTO_START:
            pszStartType = _T("Auto");
            break;
        case SERVICE_DEMAND_START:
            pszStartType = _T("On Demand");
            break;
        case SERVICE_DISABLED:
            pszStartType = _T("Disabled");
            break;
        default:
            pszStartType = _T("Unknown");
            break;
    }

    switch (pConfig->dwErrorControl)
    {
        case SERVICE_ERROR_IGNORE:
            pszErrorControl = _T("Ignore");
            break;
        case SERVICE_ERROR_NORMAL:
            pszErrorControl = _T("Normal");
            break;
        case SERVICE_ERROR_SEVERE:
            pszErrorControl = _T("Severe");
            break;
        case SERVICE_ERROR_CRITICAL:
            pszErrorControl = _T("Critical");
            break;
        default:
            pszErrorControl = _T("Unknown");
            break;
    }

    // выводим основную конфигурационную информацию
    _tprintf(_T("Name:             %s\n"), argv[1]);
    _tprintf(_T("Display Name:     %s\n"), pConfig->lpDisplayName);
    _tprintf(_T("Service Type:     %s %s\n"), 
             pszServiceType, pszInteractive);
    _tprintf(_T("Start Type:       %s\n"), pszStartType);
    _tprintf(_T("Error Control:    %s\n"), pszErrorControl);
    _tprintf(_T("Binary File:      %s\n"), pConfig->lpBinaryPathName);
    _tprintf(_T("Logon Account:    %s\n"), pConfig->lpServiceStartName);
    _tprintf(_T("Load Order Group: %s\n"), pConfig->lpLoadOrderGroup);
    
    // выводим список зависимостей
    _tprintf(_T("Dependencies:\n"));

    PCTSTR psz = pConfig->lpDependencies;
    while (*psz != 0)
    {
        if (*psz == SC_GROUP_IDENTIFIER)
            _tprintf(_T("\tGroup:   %s\n"), psz + 1);
        else
            _tprintf(_T("\tService: %s\n"), psz);

        psz += lstrlen(psz) + 1;
    }

#if _WIN32_WINNT >= 0x500

    // выводим описание службы
    _tprintf(_T("\nDescription:\n%s\n\n"), pDesc->lpDescription);

    // выводим действия при сбое службы
    _tprintf(_T("Failure Actions:\n"));
    _tprintf(_T("\tReset Period:   %d sec\n"), pActions->dwResetPeriod);
    _tprintf(_T("\tCommand Line:   %s\n"), pActions->lpCommand);
    _tprintf(_T("\tReboot Message: %s\n"), pActions->lpRebootMsg);

    SC_ACTION * p = pActions->lpsaActions;
    for (ULONG i = 0; i < pActions->cActions; i++)
    {
        PCTSTR pszAction;
        switch (p->Type)
        {
            case SC_ACTION_NONE:        pszAction = _T("None");
                                        break;
            case SC_ACTION_REBOOT:        pszAction = _T("Reboot");
                                        break;
            case SC_ACTION_RESTART:        pszAction = _T("Restart");
                                        break;
            case SC_ACTION_RUN_COMMAND: pszAction = _T("Run Command");
                                        break;
            default:                    pszAction = _T("Unknown");
                                        break;
        }

        _tprintf(_T("\t%d Delay: %6 msec  Action: %s\n"), i + 1,
                 p->Delay, pszAction);
    }

#endif

    return 0;
}

Вы также можете найти полный исходный текст этой программы в подкаталоге svcconf в архиве с исходным кодом. Готовая версия программы, svcconf.exe, которую вы можете найти в архиве с демонстрационными программами, скомпилирована для Windows 2000 и более поздних версий. Если вы хотите запустить эту программу на Windows NT 4, вам придется скомпилировать ее заново, указав макрос препроцессора _WIN32_WINNT как 0x400.

Продолжение следует

Во второй части статьи мы научимся запускать и останавливать службы, а также рассмотрим вопросы установки системных служб.

Ссылки

  1. Jeffrey Richter, Jason D. Clark, Programming Server-Side Applications for Microsoft Windows 2000. Microsoft Press, 2000. (Имеется русский перевод: Джеффри Рихтер, Джейсон Кларк, Программирование серверных приложений для Windows 2000. Издательства "Русская Редакция", "Питер", 2001.
  2. Kevin Miller, Professional NT Services. Wrox Press, 1998.
  3. Keith Brown, Programming Windows Security. Addisson-Wesley, 2000.
  4. David A. Solomon, Mark E. Russinovich, Inside Microsoft Windows 2000, Third Edition. Microsoft Press, 2000. (Имеется русский перевод: Дэвид Соломон, Марк Руссинович, Внутреннее устройство Microsoft Windows 2000. Издательства "Русская Редакция", "Питер", 2001).
  5. Keith Brown, Handle Logons in Windows NT and Windows 2000 with Your Own Logon Session Broker, Microsoft Systems Journal, February 2000.

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.