Системный Таймер В Windows: Большие Перемены

Поведение планировщика Windows существенно изменилось в Windows 10 2004 без каких-либо предупреждений или изменений в документации.

Это, вероятно, приведет к поломке нескольких приложений.

Этот это происходит не в первый раз , но это изменение более серьезное.

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

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

Если какие-либо из моих выводов неверны, пожалуйста, дайте мне знать.

Прерывания по таймеру и причина их существования

Системный таймер в Windows: большие перемены

Во-первых, немного контекста проектирования операционной системы.

Хотелось бы, чтобы программа имела возможность засыпать и позже просыпаться.

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

Итак, в Windows есть функция Спать — задайте ему желаемую продолжительность сна в миллисекундах, и он разбудит процесс: Сон(1); Стоит задуматься, как это реализовано.

В идеале при звонке Сон(1) Процессор переходит в спящий режим.

Но как операционная система разбудит поток, если процессор спит? Ответ — аппаратные прерывания.

ОС программирует чип — аппаратный таймер — который затем запускает прерывание, которое пробуждает процессор, а затем ОС запускает ваш поток.

Функции WaitForSingleObject и WaitForMultipleObjects также имеют значения таймаута, и эти таймауты реализуются с использованием того же механизма.

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

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

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

Когда поток вызывает Сон(н) , то ОС запланирует запуск потока сразу после первого прерывания таймера.

Это означает, что поток может проснуться немного позже, но Windows не является ОС реального времени, она вообще не гарантирует определенное время пробуждения (ядра ЦП в это время могут быть заняты), поэтому нормально проснуться немного позже.

Интервал между прерываниями таймера зависит от версии Windows и оборудования, но на всех моих машинах он равен по умолчанию составило 15,625 мс (1000 мс/64).

Это означает, что если вы позвоните Сон(1) в какой-то случайный момент времени процесс будет разбужен где-то между 1,0 и 16,625 мс в будущем, когда сработает следующее прерывание глобального таймера (или следующее, если оно сработает слишком рано).

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

Некоторым программам не нравится такой широкий разброс задержек (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, многие игры и т. д.).

К счастью, они могут решить проблему, используя функцию времяBeginPeriod , что позволяет программе запрашивать меньший интервал.

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

Десятилетия безумия Вот что безумие: времяBeginPeriod может быть вызвана любой программой и изменяет интервал прерывания таймера, а прерывание таймера является глобальным ресурсом.

Давайте представим, что процесс A находится в цикле, вызывая Сон(1) .

Это неправильно, но это правда, и по умолчанию он просыпается каждые 15,625 мс, или 64 раза в секунду.

Затем появляется процесс B и вызывает времяБегинПериод(2) .

Это приводит к тому, что таймер срабатывает чаще, и внезапно процесс A просыпается 500 раз в секунду вместо 64 раз в секунду.

Это безумие! Но именно так всегда работала Windows. В этот момент, если процесс C появился и вызвал времяБегинПериод(4) , это ничего не изменит — процесс A продолжит просыпаться 500 раз в секунду.

В такой ситуации правила устанавливает не последний звонок, а звонок с минимальным интервалом.

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

Если эта программа завершает работу или вызывает времяКонецПериод , то новый минимум вступает в силу.

Если одна программа вызывает времяБегинПериод(1) , то теперь это интервал прерывания таймера для всей системы.

Если одна программа вызывает времяБегинПериод(1) , и другие времяБегинПериод(4) , то универсальным законом становится интервал прерывания таймера в одну миллисекунду.



Системный таймер в Windows: большие перемены

Это важно, поскольку высокая частота прерываний таймера — и связанная с этим высокая частота планирования потоков — могут привести к потере значительной мощности процессора.

как обсуждалось здесь .

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

В стандарте JavaScript есть функция setTimeout , который просит браузер вызвать функцию JavaScript через несколько миллисекунд. Для реализации этой и других функций Chromium использует таймеры (в основном Ждатьфорсинглобжект с таймаутами, не Спать ).

Это часто требует увеличения частоты прерываний таймера.

Чтобы это не слишком сильно влияло на срок службы батареи, Chromium недавно был модифицирован таким образом, что при работе от батареи Частота прерывания таймера не превышала 125 Гц (интервал 8 мс) .

времяGetTime Функция timeGetTime (не путать с GetTickCount) возвращает текущее время, обновленное прерыванием таймера.

Исторически процессоры не очень хорошо сохраняли точное время (их часы намеренно колеблются, чтобы не работать в качестве FM-передатчиков, и по другим причинам), поэтому процессоры часто полагаются на отдельные тактовые генераторы для поддержания точного времени.

Чтение этих чипов требует больших затрат, поэтому Windows поддерживает 64-битный миллисекундный счетчик времени, обновляемый прерыванием таймера.

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

времяGetTime причины ЧтениеПрерыванияТик , который по сути просто считывает этот 64-битный счетчик.

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

Новая недокументированная реальность С выходом Windows 10 2004 г.

(апрель 2020 г.

) некоторые из этих механизмов немного изменились, но весьма запутанно.

Появился первым сообщения о том, что timeBeginPeriod больше не работает .

На деле все оказалось гораздо сложнее.

Ранние эксперименты дали неоднозначные результаты.

Когда я запустил программу с вызовом времяБегинПериод(2) , Что часы показал интервал таймера 2.0мс, но отдельная тестовая программа с циклом Сон(1) просыпался примерно 64 раза в секунду вместо 500 раз, как в предыдущих версиях Windows. Научный эксперимент Потом я написал пару программ для изучения поведения системы.

Одна программа( изменение_интервала.

cpp ) просто сидит в цикле и вызывает времяBeginPeriod с интервалом от 1 до 15 мс.

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

Пятнадцать строк кода.

Легко.

Другая программа ( Measure_interval.cpp ) запускает несколько тестов, чтобы увидеть, как меняется его поведение при изменении файлаchange_interval.cpp. Программа отслеживает три параметра.

  1. Он запрашивает у ОС текущее разрешение глобального таймера, используя Нткуеритимеррешолюшн .

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

  3. Он измеряет задержку Сон(1) вызывая его в цикле в течение секунды и подсчитывая количество вызовов.

    Средняя задержка — это просто обратная величина количества итераций.

@ФеликсПетрикони я проводил тесты на Windows 10 1909, а также на Windows 10 2004. Вот результаты, очищенные от случайных колебаний:

Системный таймер в Windows: большие перемены

Это означает, что времяBeginPeriod по-прежнему устанавливает интервал глобального таймера во всех версиях Windows. По результатам timeGetTime() мы можем сказать, что прерывание срабатывает с такой частотой по крайней мере на одном ядре процессора, и время обновляется.

Также обратите внимание, что 2.0 в первой строке относится к 1909 году.

также была версия 2.0 в Windows XP , затем 1.0 в Windows 7/8, а потом вроде вернулся к 2.0? Однако в Windows 10 2004 поведение планировщика кардинально меняется.

Сон(1) в любом процессе был просто равен интервалу прерывания таймера, за исключением timeBeginPeriod(1), что давало такой график:

Системный таймер в Windows: большие перемены

В Windows 10 2004 соотношение между времяBeginPeriod и задержку сна в другом процессе (что не вызвало времяBeginPeriod ) выглядит странно:

Системный таймер в Windows: большие перемены

Точная форма левой части графика неясна, но она определенно идет в направлении, противоположном предыдущему! Почему? Последствия Как отмечалось в обсуждении Reddit и hacker-news, вполне вероятно, что левая половина графика представляет собой попытку максимально точно имитировать «нормальную» задержку, учитывая доступную точность прерывания глобального таймера.

То есть при интервале прерывания в 6 миллисекунд задержка около 12 мс (два такта), а при интервале прерывания в 7 миллисекунд задержка около 14 мс (два такта).

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

Если прерывание таймера установлено на 7 мс, задержка Сон(1) на 14 мс — это даже не самый распространенный результат:

Системный таймер в Windows: большие перемены

Некоторые читатели могут винить в этом случайный шум в системе, но когда частота прерываний таймера составляет 9 мс или выше, шум равен нулю, поэтому это не может быть объяснением.

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

Особенно спорными являются интервалы прерывания таймера от 4 до 8 мс.

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

Это все очень странно, и я не понимаю ни логики, ни реализации.

Возможно, это ошибка, но я в этом сомневаюсь.

Я думаю, что за этим стоит какая-то сложная логика обратной совместимости.

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

Это не повлияет на большинство программ.

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

времяBeginPeriod .

Однако могут возникнуть следующие проблемы:

  • Программа может случайно предположить, что Сон(1) И времяGetTime то же разрешение, но это уже не так.

    Хотя такое предположение кажется маловероятным.

  • Программа может зависеть от небольшого разрешения таймера, которого она не получает. Было несколько сообщений о эта проблема возникает в некоторых играх - есть инструмент под названием Инструмент системного таймера Windows и еще один позвонил Разрешение таймера 1.2 .

    Они «исправляют» эти игры, увеличивая частоту прерываний таймера.

    Судя по всему эти фиксы уже не будут работать или не будут работать так же.

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

  • В многопроцессной программе основная программа управления может увеличить частоту прерываний таймера и затем ожидать, что это повлияет на планирование дочерних процессов.

    Раньше это было разумным предположением, но оно больше не соответствует действительности.

    Вот как я сам узнал об этой проблеме.

    Рассматриваемый продукт теперь вызывает времяBeginPeriod в каждом процессе, так всё нормально, но вот уже несколько месяцев ПО по непонятной причине плохо работает.

Жертва Тестовая программа изменение_интервала.

cpp работает только в том случае, если никто не запрашивает более высокую частоту прерываний таймера.

Поскольку и Chrome, и Visual Studio имеют привычку делать это, мне пришлось проводить большую часть экспериментов без доступа в Интернет и программирование в блокноте .

Кто-то предложил Emacs, но пришлось вмешаться этот дискуссия выше моих сил.

Теги: #Разработка для Windows #Игры и игровые приставки #Windows #Системное администрирование #задержка #системный таймер

Вместе с данным постом часто просматривают: