Драйвер, который забыл договориться с системой о питании, узнаёт об этом от пользователей самым неприятным образом: ноутбук не засыпает, планшет не просыпается, а после выхода из гибернации устройство притворяется незнакомцем. Инфраструктура управления питанием 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, что позволяет потом размотать последовательность без дополнительного вмешательства. Сессия сбора поднимается одной командой: