За привычным экраном входа в Windows скрывается довольно замысловатая архитектура идентичности. Каждый вошедший пользователь получает собственную сессию со своим набором процессов, оконных станций и рабочих столов, и распоряжается всем этим хозяйством менеджер сессий - один из самых первых процессов, стартующих в системе. Понимание этой механики нужно не только системным программистам: от неё зависит, почему служба не может показать окно, как работают удалённые рабочие столы и куда попадает процесс, запущенный из планировщика.

Чем является SMSS и как устроена иерархия сессий

Session Manager Subsystem, процесс smss.exe, стартует самым первым пользовательским процессом системы сразу после ядра. Его список обязанностей широк: отложенные операции переименования файлов, создание файлов подкачки, запуск подсистем окружения и, главное, порождение сессий. Для каждой новой сессии SMSS стартует пару процессов: подсистему клиента и сервера времени исполнения (csrss.exe) и процесс входа (winlogon.exe), а уже winlogon поднимает компонент входа, который проверяет учётные данные и запускает оболочку пользователя.

Сессия в терминах ядра - это контекст, объединяющий пространство имён объектов, оконные станции и набор процессов. Сессия с номером ноль исторически принадлежала первому пользователю, но с эпохи Vista она изолирована: там живут только службы и системные процессы, и никакой пользовательский интерфейс туда больше не проникает. Это решение закрыло целый класс атак, известный под бытовым именем "разбитое стекло сервисов", когда вредоносное окно отправляло службе опасные сообщения. Для наследников того кода, кто по привычке показывает диалоги из служб, изоляция стала источником бесконечных сюрпризов: окно есть, а увидеть его некому.

Дальше иерархия ветвится. Внутри сессии существуют оконные станции - контейнеры буферов обмена, наборов дескрипторов и видимости. Единственная интерактивная станция носит имя WinSta0. На станции лежат рабочие столы: Default (рабочий стол пользователя), Winlogon (тот самый безопасный экран входа), Screensaver и создаваемые программами отдельные столы. Правило простое: окно принадлежит столу, стол - станции, станция - сессии, и пересечь эту иерархию без особых разрешений нельзя.

Зачем программам создавать собственные рабочие столы

Практическая манипуляция с рабочими столами выглядит просто: функция CreateDesktopEx создаёт новый стол в текущей станции, SetThreadDesktop прикрепляет к нему поток, а SwitchDesktop делает стол видимым. Зачем вообще это нужно? Применений накопилось предостаточно, от повышения приватности в системах ввода паролей (парольный менеджер переключает пользователя на пустой стол, куда чужие перехватчики не дотянутся) до киосков и терминалов, где оператор видит только одно приложение:

HDESK d = CreateDesktopEx(L"SecureDesk", NULL, NULL, 0,
    GENERIC_ALL, NULL, 0, NULL, 0);
HDESK old = GetThreadDesktop(GetCurrentThread());
SetThreadDesktop(d);
SwitchDesktop(d);
// ... показать интерфейс, принять ввод ...
SwitchDesktop(old);
SetThreadDesktop(old);
CloseDesktop(d);

Вторая фамилия в этой теме - оконная станция. Новоиспечённый процесс получает станцию от родителя, но наследование можно переопределить через поле стартовой информации CreateProcess. Службы, порождающие интерактивные помощники (так работают, например, диагностические агенты, показывающие окно от имени пользователя), обязаны явно выйти в WinSta0 целевой сессии, а до неё - добраться до самой сессии, потому что функционируя в нулевой, интерфейс в другую сессию напрямую не отправишь.

Отдельная заповедная зона - сторона безопасности: у каждого стола есть собственный дескриптор безопасности, и создание стола без продуманного SDDL спонтанно превращается либо в отказ в доступе для легального пользователя, либо в открытые апартаменты для соседей. Стандартная парадигма - копирование дескриптора с Default-стола и точечное добавление нужных SID.

Инструменты наблюдения и API управления сессиями

Смотреть сессии удобнее всего семейством WTS-функций: WTSEnumerateSessions выдаёт список сессий сервера с их состояниями, WTSQuerySessionInformation - подробности вроде имени пользователя, станции и времени подключения, а WTSQueryUserToken даёт маркер пользователя нужной сессии, который нужен для запуска процессов от его лица. Перечисление выглядит так:

PWTS_SESSION_INFOW list = NULL;
DWORD count = 0;

if (!WTSEnumerateSessionsW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &list, &count)) {
    return GetLastError();
}

for (DWORD i = 0; i < count; i++) {
    if (list[i].State != WTSActive) {
        continue;      // нужны только активные подключения
    }
    targetSessionId = list[i].SessionId;
}

WTSFreeMemory(list);

Значения State описывают жизненный цикл подключения, и путаница в них стоит не одного ложного срабатывания:

WTSActive        пользователь работает
WTSConnected     подключён, но ещё не вошёл
WTSConnectQuery  идёт согласование подключения
WTSShadow        сеанс наблюдения за чужим подключением
WTSDisconnected  подключение сброшено, сессия ещё жива
WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit - служебные состояния

Параллельный мир консольных программ даёт знакомые quser и qwinsta:

qwinsta
quser
query session /counter
 SESSIONNAME       USERNAME                 ID  STATE   TYPE        DEVICE
 services                                    0  Disc
 console                                     1  Conn
>                  admin                    2  Active
 rdp-tcp#5         operator                 3  Active  rdpwd

Process Explorer наглядно показывает колонку Session для каждого процесса, и это первое место, куда стоит заглядывать при вопросе, почему процесс не видит окон.

Рабочая последовательность запуска процесса в пользовательской сессии из службы уложена в легендарный сценарий:

  1. Найти целевую сессию через WTSEnumerateSessions, отфильтровав активные;
  2. Получить маркер через WTSQueryUserToken для этой сессии;
  3. Создать окружение пользователя через CreateEnvironmentBlock;
  4. Запустить процесс через CreateProcessAsUser с явным указанием станции "winsta0\default" в стартовой информации;
  5. Освободить маркер и окружение, убедившись, что у нового процесса корректная сессия.

Сам запуск пишется компактно, и критичных мест в нём ровно два: строка рабочего стола в стартовой информации и обязательное освобождение маркера:

HANDLE userToken = NULL;
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
PVOID env = NULL;

if (!WTSQueryUserToken(targetSessionId, &userToken)) {
    return GetLastError();      // служба обязана идти от SYSTEM
}

si.lpDesktop = (LPWSTR)L"winsta0\\default";

CreateEnvironmentBlock(&env, userToken, FALSE);

BOOL ok = CreateProcessAsUserW(userToken, L"C:\\App\\helper.exe", NULL,
              NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT,
              env, NULL, &si, &pi);

if (ok) {
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

DestroyEnvironmentBlock(env);
CloseHandle(userToken);

Строка winsta0\default в поле lpDesktop обязательна: без неё новый процесс окажется на невидимом столе, окно создастся, а пользователь его не увидит. Вызов WTSQueryUserToken работает только из процесса, идущего от учётной записи SYSTEM, и это вторая по частоте причина отказа после неверного номера сессии.

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

Для чего ещё используют манипуляции с сессиями

Сценарии применения накопились разнообразные. Службы удалённого администрирования стартуют агента поддержки именно в сессии пользователя, чтобы тот видел диалоги помощника. Системы киосков переключают пользователей на выделенный стол с единственным приложением и запирают SwitchDesktop обратно политикой. Сервера терминалов живут целиком на множественных сессиях: каждое подключение порождает новую, и вся подсистема балансировки опирается на перечисление WTS. Средства аудита используют информацию о сессиях, чтобы построить журнал присутствия: кто вошёл, из какого источника, как долго активен, - тема углубляется в механизмы подлинного входа слоя LSASS и протоколов упаковки учётных данных, где уже требуется отдельная статья.

Нельзя обойти вниманием и модернизацию модели. Приложения из магазинных контейнеров получили срезы AppContainer, ограничивающие доступ к чужим объектам сессии; виртуализационная безопасность изолировала ключевые процессы, такие как lsass, в защищённый режим; а сам SMSS частично распределил обязанности по специализированным службам. Тем не менее, базовая матрёшка "сессия - станция - стол" выдержала все реформы без изменений, и мастера, работавшие с Windows Server 2003, в современной системе узнают каждый механизм.

Пространство имён объектов и приватность сессии

Уровень ниже оконных станций - пространство имён объектов ядра, и оно тоже фрагментируется по сессиям. Знакомые каталоги вроде \BaseNamedObjects существуют в версии "на сессию": событие, созданное службой без префикса, живёт в её нулевой сессии и невидимо для приложений пользователя. Чтобы встретиться, компоненты договариваются через префикс Global\, который поднимает объект в общее пространство, либо через локальный префикс Local\ для строгой приватности:

// объект виден во всех сессиях, требует привилегии SeCreateGlobalPrivilege
HANDLE shared = CreateMutexW(NULL, FALSE, L"Global\\MyAgentSync");

// объект виден только внутри текущей сессии
HANDLE private = CreateMutexW(NULL, FALSE, L"Local\\MyAgentSync");

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

Из отладчика ядра ту же картину видно снизу, и расхождение между показаниями диспетчера и ядра иногда само по себе является находкой:

kd> !session
kd> !session 1
kd> !process 0 0 winlogon.exe
kd> !process 0 0 csrss.exe
kd> !object \Sessions\1\BaseNamedObjects

Смежный механизм - символические связи имён устройств. Буквы дисков, имена портов COM и LPT тоже привязаны к контексту сессии станции: сопоставление диска, сделанное пользователем, службой не видно, и наоборот. Это объясняет вечнозелёный вопрос про "пропавший сетевой диск" у служб: диск не пропал, он просто живёт в другом пространстве имён. Диагностику здесь упрощает наблюдение через просмотрщик объектов (WinObj) с явным переключением между глобальным и сессионным каталогами - видно глазами то, что иначе приходится выводить из логики.

События жизненного цикла сессии для программного кода

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

// регистрация для окна приложения
WTSRegisterSessionNotification(hWnd, NOTIFY_FOR_THIS_SESSION);

// обработка в оконной процедуре
case WM_WTSSESSION_CHANGE: {
    ULONG id = (ULONG)lParam;
    switch (wParam) {
    case WTS_SESSION_LOGON:        StartTracking(id); break;
    case WTS_SESSION_LOCK:         PauseTracking(id); break;
    case WTS_SESSION_UNLOCK:       ResumeTracking(id); break;
    case WTS_SESSION_LOGOFF:       StopTracking(id); break;
    case WTS_REMOTE_CONNECT:       NoteRemoteIn(id); break;
    case WTS_REMOTE_DISCONNECT:    NoteRemoteOut(id); break;
    }
    return 0;
}

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

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

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

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

Типовые заблуждения на тему сессий и рабочих столов

В завершение полезно собрать мифы, которые тянутся десятилетиями. Первый: будто окно нельзя показать из службы по определению - можно, но не из нулевой сессии, а маршалингом запроса в агент пользовательской сессии; попытки открыть шлюз мимо изоляции система пресекает по соображениям, которые уже обсуждались. Второй: что смена рабочего стола через SwitchDesktop как-то прячет процессы - нет, процессы остаются в той же сессии, и диспетчер задач их прекрасно видит, стол влияет только на видимость окон. Третий: что SessionId = 1 неизменно означает первого пользователя: на сервере терминалов номера выдаются по мере подключений, и единица там может быть давно отключившимся RDP-клиентом.

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

Тот, кто однажды разобрался с этой тройкой уровней, смотрит на типичную жалобу "служба перестала показывать окошко" с профессиональным спокойствием: вердикт очевиден до открытия кода. Изолированная нулевая сессия, собственные станции, выделенные столы и церемониал запуска через WTS - набор не из самых обсуждаемых, но именно на нём держится вся интерактивная идентичность Windows.

И маленькое замечание на дорогу: вывод qwinsta и снимок из Process Explorer стоит держать под рукой на каждом сервере, где ведётся разработка, потому что при первой же загадке с видимостью окон эти два инструмента рассказывают правду быстрее любого отладчика. А сама матрёшка сессий, станций и столов при этом продолжит тихо работать, как работает уже треть десятилетия подряд.