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

Как роли и события питания делятся между системой и драйвером

В модели Windows есть два независимых пространства состояний. Системное, от S0 (работа) до S4 (гибернация) и S5 (выключено), и устройственное, от D0 (полностью включено) до D3 (полностью выключено). Система руководствуется планами питания и решениями пользователя, устройство - потребностью в работе и таймаутами бездействия. Мост между ними строит PnP-менеджер: он рассылает IRP с запросами системного и устройственного состояния, а драйвер обязан честно их обработать: сохранить контекст, выключить части устройства, а при пробуждении поднять всё обратно.

В WDM всё это делалось вручную, и количество способов выстрелить себе в ногу исчислялось десятками: нужно было корректно пропускать IRP вниз по стеку, помнить про ожидание нижележащих слоёв, не трогать оборудование вне D0. Инфраструктура KMDF забрала эту механику на себя, оставив драйверу колбэки состояний: EvtDeviceD0Entry, EvtDeviceD0Exit, EvtDevicePrepareHardware, EvtDeviceSelfManagedIoInit и другие. Драйвер описывает, что делает, фреймворк решает, когда это звать и в каком порядке. Роль владельца политики питания, power policy owner, при этом обычно берёт на себя функциональный драйвер устройства: именно он имеет право голоса в вопросах бездействия и пробуждения.

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

Политика бездействия S0 и как её настроить

Ключевая часть собственной политики - правила ухода в сон на ходу, когда система в целом работает. Драйвер задаёт таймаут бездействия, целевое состояние (обычно D2 или D3) и условие, при котором уход в сон разрешён: наличие или отсутствие возможности будить себя самостоятельно. Объявление политики выглядит компактно:

WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS idle;
WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT(&idle,
    IdleCanWakeFromS0);
idle.IdleTimeout       = 5000;   // миллисекунды бездействия
idle.IdleTimeoutType   = SystemManagedIdleTimeoutWithHint;
idle.UserControlOfIdleSettings = IdleAllowUserControl;
idle.Enabled           = WdfUseDefault;
WdfDeviceAssignS0IdleSettings(device, &idle);

WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS wake;
WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS_INIT(&wake);
wake.UserControlOfWakeSettings = WakeAllowUserControl;
wake.Enabled  = WdfUseDefault;
WdfDeviceAssignWakeSettings(device, &wake);

После такой регистрации фреймворк сам считает бездействие: если к устройству не поступает запросов заданное число миллисекунд и открытых заявок нет, оно посылается в целевое состояние, а драйвер получает своё D0Exit. Выбор IdleCanWakeFromS0 вместо IdleCannotWakeFromS0 меняет требования к аппаратуре: девайс обязан уметь инициировать сигнал пробуждения, иначе его засыпание превращается в одностороннее. Тип таймаута с подсказкой позволяет системе объединять таймеры бездействия нескольких устройств и будить процессор одной толпой вместо россыпи; для батарейных платформ это ощутимый вклад в выносливость.

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

// работа началась: просим фреймворк не усыплять устройство
status = WdfDeviceStopIdle(device, FALSE);   // FALSE - не будить принудительно
if (!NT_SUCCESS(status)) {
    return status;
}

// ... критическая секция, обмен с железом ...

// работа закончилась: возвращаем устройству право уснуть
WdfDeviceResumeIdle(device);

Пара StopIdle и ResumeIdle обязана быть строго сбалансированной по всем веткам выхода, включая ветки ошибок, иначе устройство остаётся в состоянии бодрствования навсегда и политика бездействия просто перестаёт работать. Второй параметр WdfDeviceStopIdle отвечает за немедленный переход в D0, если устройство в момент вызова уже спало.

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

Вторая половина политики отвечает на вопрос, смеёт ли устройство будить спящую систему. Аппаратная основа здесь - линия сигнала пробуждения, в современной терминологии это сигналы через шину (Power Management Event на PCI Express, встраиваемые линии на SoC). Драйвер заявляет о возможности пробуждения в структуре возможностей устройства и регистрирует колбэки постановки на охрану: EvtDeviceArmWakeFromS0, EvtDeviceArmWakeFromSx и соответствующие пары разоружения. После сигнала система поднимается по цепочке: устройство будит контроллер, контроллер - мост, мост - сокет, и обработчики драйвера получают свой D0Entry с признаком источника пробуждения.

Практических нюансов тут хватает. Настройки пользователя могут запрещать пробуждение конкретным устройством, и политика обязана уважать выбор (галочка в свойствах адаптера знакома всем). Для систем с современным режимом ожидания (Modern Standby) привычная философия S3 заменяется на платформу, которая дремлет в S0 low power idle, и устройство там живёт жизнью коротких циклов сна и пробуждения; драйвер, спроектированный под классический сон, но заявляющий политику без переосмысления, будет мешать платформе засыпать целиком, и счёт за это предъявит время автономной работы. Отдельно отслеживают цепочку возможностей: возможность пробуждения должна быть заявлена на каждом уровне стека, несогласованные стеки, где нижний фильтр отрицает то, что заявляет верхний, дают причудливые баги.

Диагностика отказов сна, как доказать системными средствами, кто мешает машине заснуть

Сладкая парочка вопросов эксплуатации - почему машина не засыпает и почему она просыпается сама. Ответы ищутся системными средствами, и их стоит знать наизусть:

powercfg /requests
powercfg /requests override DRIVER \FileSystem\myport.sys
powercfg /waketimers
powercfg /lastwake
powercfg /energy /duration 60 /output C:\PerfLogs\energy.html
powercfg /sleepstudy /output C:\PerfLogs\sleepstudy.html
powercfg /systempowerreport /output C:\PerfLogs\report.html
powercfg /devicequery wake_armed

Команда /requests показывает активные заявки на бодрствование, типичный вывод выглядит так:

DISPLAY:
[PROCESS] \Device\HarddiskVolume3\Windows\explorer.exe
SYSTEM:
[DRIVER] \FileSystem\myport.sys
[DRIVER] USB Root Hub (USB 3.0)
AWAYMODE:
None.
EXECUTION:
None.
PERFCOUNT:
None.

Строка с именем собственного драйвера в разделе SYSTEM и есть ответ на вопрос, кто не пускает систему в сон. Ключ /devicequery wake_armed печатает устройства, которым разрешено будить машину, а /sleepstudy собирает отчёт по циклам современного режима ожидания. Журнал событий System с источником Power-Troubleshooter добавляет историю переходов, а из отладчика состояние политики конкретного устройства показывает расширение фреймворка:

kd> !wdfkd.wdfdevice ffffd10a`12345060
kd> !wdfkd.wdfdevicepowerpolicy ffffd10a`12345060
kd> !wdfkd.wdfdriver ffffd10a`0a112000
kd> !devstack ffffd10a`12345060

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

CODE5 На стенде убедительно выглядит связка из таймера, который опрашивает количество запросов SysInfo, и наблюдения за WPP-потоком: видно вживую, как политика, написанная по уму, усыпляет и будит устройство ровно тогда, когда задумывалось: без паники и без заседаний в D0 по пустякам.

Self-managed IO и другие расширенные возможности фреймворка

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

WDF_PNPPOWER_EVENT_CALLBACKS pnp;
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnp);

pnp.EvtDeviceD0Entry                = EvtDeviceD0Entry;
pnp.EvtDeviceD0Exit                 = EvtDeviceD0Exit;
pnp.EvtDevicePrepareHardware        = EvtDevicePrepareHardware;
pnp.EvtDeviceSelfManagedIoInit      = EvtDeviceSelfManagedIoInit;
pnp.EvtDeviceSelfManagedIoSuspend   = EvtDeviceSelfManagedIoSuspend;
pnp.EvtDeviceSelfManagedIoRestart   = EvtDeviceSelfManagedIoRestart;
pnp.EvtDeviceSelfManagedIoCleanup   = EvtDeviceSelfManagedIoCleanup;

WdfDeviceInitSetPnpPowerEventCallbacks(deviceInit, &pnp);

Порядок вызовов при переходе вниз по питанию ثابت, и его полезно держать перед глазами при разборе логов:

уход в сон:      EvtDeviceD0Exit -> EvtDeviceSelfManagedIoSuspend
выход из сна:    EvtDeviceSelfManagedIoRestart -> EvtDeviceD0Entry
выгрузка:        EvtDeviceSelfManagedIoCleanup

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

Для устройств с "хранительной" частью есть и близкие по духе возможности: удержание устройства в рабочем состоянии вокруг периода активности через механизм заявок на активность, раздельные состояния для функциональных частей составного устройства и интеграция с runtime power management на шинах, которые поддерживают посегментное питание. USB-мир, к примеру, одарил разработчиков изящным сценарием selective suspend: драйвер функции может усыпить свой конец устройства, пока распорядитель шины деятельно работает, и именно на это рассчитана часть стандартных настроек питания USB, которые администраторы видят в схемах электропитания.

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

Типовые ошибки проектирования политики питания

Печальный опыт массовых рассылок драйверов устаканил список промахов, который стоит проговорить дословно:

  1. Контекст регистров восстанавливается только в EVT_D0_ENTRY, однако часть сопутствующих структур (DMA, очереди) всё ещё инициализируются в стартовом пути, и после выхода из S3 устройство срабатывает с мусором;
  2. Таймаут бездействия выставлен под копирку у всех устройств семейства, независимо от реальной частоты обращений, и на холодных устройствах цикл сна-пробуждения начинает греть процессор без пользы;
  3. Возможность пробуждения заявлена, а реальная проводка сигнала пробуждения на плате отсутствует - баг вылезает только на конкретных моделях ноутбуков;
  4. Драйвер возвращает из D0Exit STATUS_UNSUCCESSFUL по малейшему поводу, чем приводит к неожиданному удалению устройства из стека;
  5. Собственная активность не регистрируется через StopIdle, и прерывания приезжают к спящему устройству, а вся симптоматика при этом выглядит как аппаратная аномалия.

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

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

От каркаса к зрелой политике

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