Любой администратор рано или поздно сталкивается с ситуацией, когда полезный скрипт или самописная утилита должны работать постоянно, переживать перезагрузки сервера и стартовать до входа пользователя в систему. Штатный ответ Windows на эту задачу - службы, а главный консольный инструмент для их регистрации - команда sc create. Она выглядит обманчиво просто, но за одной строкой с binPath= скрывается целая архитектура: диспетчер управления службами, специальный протокол общения между процессом и системой, изоляция сессий и отдельная модель учётных записей. Эта статья разбирает всю цепочку на практике - от технического устройства службы до типичной ловушки с пробелами, от обёрток для скриптов до автоперезапуска и разграничения прав. Материал написан для тех, кто автоматизирует инфраструктуру и хочет делать это осознанно, а не копируя команды из форумов.
Что такое служба Windows с технической стороны
Служба - это обычный процесс, но с одним принципиальным отличием: его жизненным циклом управляет диспетчер управления службами, Service Control Manager или просто SCM. Именно SCM запускает процесс, присылает ему команды остановки и паузы, следит за состоянием и решает, что делать при аварийном завершении. Для этого исполняемый файл службы обязан при старте вызвать функцию StartServiceCtrlDispatcher и зарегистрировать точку входа ServiceMain. Внутри ServiceMain процесс регистрирует обработчик управляющих сообщений и начинает принимать коды вроде SERVICE_CONTROL_STOP или SERVICE_CONTROL_SHUTDOWN. Пока служба жива, она обязана периодически сообщать SCM своё состояние - starting, running, stopping. Если процесс не отвечает на команду остановки в отведённый таймаут, SCM считает его зависшим.
Много системных служб вообще не живут отдельными процессами. Они собраны в DLL и загружаются внутрь процессов-хостов svchost.exe, сгруппированных по соображениям изоляции и экономии памяти. Команда tasklist /svc показывает, какая служба сидит в каком экземпляре svchost. Для собственных служб путь через svchost тоже существует, но чаще регистрируют самостоятельный исполняемый файл с типом own.
Отдельная тема - учётная запись, под которой работает служба. Встроенные варианты исторически три. LocalSystem - самая могущественная локальная учётка, фактически эквивалент полного контроля над машиной; в сети процесс под ней представляется компьютерным аккаунтом домена. NetworkService имеет минимальные локальные привилегии, но сохраняет сетевую идентичность компьютера. LocalService - минимальные права и анонимный сетевой доступ. Начиная с Windows 7 добавились виртуальные аккаунты вида NT SERVICE\ИмяСлужбы: система автоматически создаёт отдельную изолированную личность для каждой службы без пароля и без заведения пользователя. Это заметно упрощает раздачу прав на файлы и реестр - доступ выдаётся конкретной службе, а не общей учётке, которой пользуются десятки процессов. Дополнительно существуют управляемые учётные записи служб gMSA для доменных сценариев, где паролем управляет сам домен.
Синтаксис sc create и классическая ловушка с пробелами
Команда sc - это клиент к тому же API, которым пользуется оснастка services.msc, поэтому всё, что делает графическая консоль, воспроизводимо из командной строки и скриптов. Регистрация новой службы выглядит так:
sc create MyMonitor binPath= "C:\tools\monitor.exe --mode daemon" DisplayName= "Мониторинг складских задач" start= auto
Здесь сразу кроется самая известная ловушка утилиты, сломавшая не один вечер: пробел обязателен после знака равенства и запрещён перед ним. Запись binPath= "C:\app\svc.exe" корректна, а binPath="C:\app\svc.exe" вернёт невразумительную ошибку синтаксиса. Это наследие старого парсера аргументов, и меняться оно уже не будет - проще один раз выучить правило и ставить пробел всегда.
Основные параметры команды: binPath задаёт командную строку запуска вместе с аргументами, причём путь с пробелами берётся в кавычки, а во вложенных кавычках их экранируют обратным слэшем; DisplayName задаёт красивое имя для консоли; obj определяет учётную запись, а password - её пароль, если это не встроенная личность; type управляет моделью процесса и почти всегда остаётся own для одиночных программ.
Параметр start= определяет политику запуска. Значение boot и system относятся к драйверам, для обычных программ актуальны три варианта. auto - служба стартует автоматически при загрузке системы, при этом форма start= delayed-auto откладывает запуск примерно на две минуты после старта остальных автоматических служб, что разгружает диск и процессор в самый горячий момент загрузки. demand - запуск только по требованию, вручную или другой службой. disabled - служба присутствует, но запуск запрещён, пока администратор не изменит конфигурацию.
Параметр depend= задаёт зависимости через слэш: sc create Worker binPath= ... depend= Tcpip/Winmgmt означает, что SCM запустит службу только после указанных. Зависимости работают и в обратную сторону: остановка базовой службы предложит остановить зависящие от неё. Для программ, которым нужна сеть, зависимость от Tcpip или LanmanWorkstation избавляет от гонок на старте, когда сеть ещё не поднялась, а служба уже пытается открыть сокет.
Почему обычный скрипт не становится службой напрямую
Главное разочарование новичка: если прописать в binPath путь к cmd-файлу, PowerShell-скрипту или произвольному exe, служба создастся, но при запуске упадёт с ошибкой 1053 - процесс не ответил диспетчеру управления вовремя. Причина уже описана выше: программа не вызвала StartServiceCtrlDispatcher, не знает про ServiceMain и не умеет обрабатывать SERVICE_CONTROL_STOP. SCM честно запускает процесс, ждёт рукопожатия положенное время, не получает его и убивает процесс как неисправный. Скрипт же при этом мог успеть что-то сделать, что добавляет путаницы - в журнале ошибка, а результат работы частично есть.
Исторически проблему решали утилитой srvany.exe из Resource Kit. Она сама настоящая служба, умеющая говорить с SCM, а нужную программу запускает как дочерний процесс, параметры которого читает из ветки реестра Parameters своей службы. Подход рабочий и до сих пор встречается в наследованных системах, но у него есть издержки: остановка службы не всегда корректно завершает дочернее дерево процессов, а сам srvany давно вне поддержки. Современные сторонние обёртки вроде NSSM или WinSW делают то же самое аккуратнее - умеют перезапускать упавший процесс, вести логи stdout в файл, корректно гасить дочерние окружения.
Есть и принципиально другая вилка решений. Первый путь - вовсе отказаться от службы: Планировщик задач с триггером "при запуске системы" и настройкой запуска от имени SYSTEM без входа пользователя покрывает большинство сценариев фоновых скриптов, при этом даёт журнал срабатываний, повторные попытки и гибкие условия. Для задачи, которая не обязана быть видна в services.msc, это часто самый разумный выбор. Второй путь - написать настоящую службу: на .NET это пара десятков строк кода на базе ServiceBase, и программа получает полноценный диалог с SCM, честную обработку остановки и нормальные статусы. Третий путь - обёртка, когда ни исходников, ни времени на переписывание нет.
Управление службой после создания
Зарегистрировать службу - только половина дела. Повседневный цикл обслуживания строится на нескольких подкомандах sc, и все они пригождаются в скриптах развёртывания.
- sc start MyMonitor и sc stop MyMonitor - запуск и остановка; stop посылает SERVICE_CONTROL_STOP и ждёт, поэтому корректно написанная служба успевает закрыть файлы и сетевые соединения.
- sc query MyMonitor - текущее состояние и коды выхода; sc queryex добавляет PID процесса, что удобно для сопоставления с диспетчером задач и для принудительного снятия зависшего процесса.
- sc qc MyMonitor - просмотр текущей конфигурации, а sc config MyMonitor binPath= ... start= demand obj= "NT SERVICE\MyMonitor" - её изменение; правило пробела после равенства действует и здесь.
- sc description MyMonitor "Следит за очередью обмена" - человекочитаемое описание в консоли.
- sc delete MyMonitor - удаление службы; пометка на удаление снимается после закрытия открытых на неё дескрипторов, поэтому открытый services.msc иногда задерживает удаление.
Отдельного абзаца заслуживает sc failure - настройка поведения при аварийном завершении. Строка sc failure MyMonitor reset= 86400 actions= restart/60000/restart/60000/restart/300000 означает: счётчик сбоев обнуляется раз в сутки, при первом и втором падении служба перезапускается через минуту, при третьем - через пять минут. Задержка задаётся в миллисекундах, а нулевая задержка restart/0 допустима, хотя краткая пауза обычно полезна - она утихомиривает шторм перезапусков при устойчивой ошибке. Вместо restart допустимы run с указанием команды через поле reboot, а также reboot системы, но перезагрузку сервера из-за одной службы применяют редко. Те же настройки в консоли прячутся на вкладке "Восстановление".
Права, безопасность и изоляция служб
Каждая служба - точка входа с особыми привилегиями, поэтому выбор учётной записи не формальность. Служба под LocalSystem получает права, с которыми не сравнится ни один администраторский интерактивный вход: это токен SYSTEM с привилегиями вроде SeDebugPrivilege и SeTakeOwnershipPrivilege. Компрометация такой службы означает полный контроль над машиной. Правильная практика - минимальные достаточные права: сначала попробовать виртуальный аккаунт NT SERVICE\ИмяСлужбы и выдать ему явные права на нужные каталоги через icacls, затем, если нужен сетевой доступ к ресурсам домена, перейти на gMSA, и только в крайнем случае подниматься до LocalSystem.
Кто вообще вправе управлять службой? Это определяет дескриптор безопасности самой службы, записанный в формате SDDL. Запрос sc sdshow MyMonitor возвращает строку вида D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)... - набор пар "кто - что может". Буквы вроде RP, WP, DT скрывают за собой запуск, остановку, чтение статуса. Командой sc sdset можно раздать управление конкретной группе, например дать группе операторов право перезапускать службу, не делая их администраторами сервера: в SDDL добавляется элемент (A;;RPWPDTLO;;;S-1-5-21-...) с SID нужной группы, а сам SID получают через PowerShell. Работа с сырым SDDL требует аккуратности - одна ошибка в строке, и доступ теряется даже у администраторов, поэтому исходную строку sdshow всегда сохраняют перед изменением.
Ещё один важный рубеж - изоляция сессии 0, введённая начиная с Windows Vista. Ранее служба могла показать диалоговое окно прямо на консоли пользователя, и это порождало целый класс проблем безопасности, когда интерактивный пользователь общался с окном, работающим от SYSTEM. Теперь все службы живут в отдельной нулевой сессии, куда пользователь не попадает. Практические следствия: создавать окна из службы бессмысленно - их никто не увидит; сопоставленные сетевые диски с буквами существуют только в контексте интерактивной сессии, служба должна обращаться к ресурсам по UNC-путям; взаимодействовать с пользователем служба может только через отдельный агент в пользовательской сессии и канал связи вроде именованного канала. Привычка отлаживать службу выводом MsgBox обречена - весь диагностический вывод идёт в файл журнала и журнал событий.
Отладка и диагностика служб в эксплуатации
Когда служба стартует и сразу падает, действия складываются в знакомую последовательность. Первый источник - Event Viewer, журнал System с источником Service Control Manager: события 7000, 7009, 7011 и 7031 рассказывают о таймаутах запуска и зависаниях, а событие 7036 фиксирует смену состояния. Коды ошибок в этих событиях стоит читать буквально: 1053 - процесс не пожал руку SCM, то есть перед нами не-служба или бесконечная инициализация до вызова StartServiceCtrlDispatcher; ошибки 5 и 1069 - проблемы доступа и неверный пароль учётной записи; 1075 - не запустилась зависимость.
Второй источник - собственные логи программы. Для скриптов под обёрткой это просто перенаправление stdout и stderr в файл, и обёртки вроде NSSM умеют делать это с ротацией. Если логов нет вообще, службу запускают вручную из консоли тем же бинарём - обычная программа при этом выводит ошибку прямо в терминал, что мгновенно отделяет проблемы "программа не работает" от проблем "программа работает, но не как служба".
Полезны и вспомогательные приёмы. Параметр службы в реестре можно посмотреть напрямую в HKLM\SYSTEM\CurrentControlSet\Services\ИмяСлужбы - там лежат ImagePath, Start, Type, DependOnService и дескриптор Security. Зависшую в состоянии stopping службу добивают, найдя PID через sc queryex и завершив процесс через taskkill /pid, после чего разбираются, что блокировало остановку. Для тонких случаев остаётся трассировка: ProcMon покажет, к какому файлу или ветке реестра служба не получила доступ, и у большинства загадочных отказов причина именно в правах виртуального аккаунта.
В итоге рецепт укладывается в короткую формулу. Настоящая служба пишется как служба или оборачивается качественной обёрткой; задача, которой достаточно запуска при старте системы, уходит в Планировщик; sc create с обязательными пробелами после равенства регистрирует сервис, sc failure делает его живучим, виртуальный аккаунт и продуманный SDDL ограничивают ущерб, а Event Viewer и собственные логи отвечают на вопрос "почему не работает" быстрее любого гадания. Кто освоил эту связку, тот больше не держит на сервере открытый сеанс ради одного скрипта - и это и есть зрелая автоматизация.