- [Перевод] Системный таймер в Windows: большое изменение
- Прерывания таймера и смысл их существования
- Десятилетия безумия
- timeGetTime
- Новая недокументированная реальность
- Научный эксперимент
- Последствия
- Исследование точности отсчёта временных интервалов в системе Windows
- Братуха М.А., Шевченко О.Г.
- ГВУЗ Донецкий национальный технический университет , г. Донецк, Украина
- astr0n@ukr.net
- Введение
- 1 Способы измерения временных интервалов
- 2 Программа тестирования таймера
- Выводы
- Литература
[Перевод] Системный таймер в Windows: большое изменение
Если вкратце, то вызовы timeBeginPeriod из одного процесса теперь влияют на другие процессы меньше, чем раньше, хотя эффект ещё присутствует.
Думаю, что новое поведение — это по сути улучшение, но оно странное, и заслуживает того, чтобы быть задокументированным. Честно предупреждаю — у меня только результаты собственных экспериментов, поэтому могу только догадываться о целях и каких-то побочных эффектах этого изменения. Если какие-либо из моих выводов неверны, пожалуйста, дайте знать.
Прерывания таймера и смысл их существования
Во-первых, немного контекста о дизайне операционных систем. Желательно, чтобы программа могла засыпать, а позже — просыпаться. На самом деле это не следует делать очень часто — потоки обычно ждут событий, а не таймеров, — но иногда необходимо. Итак, в Windows есть функция Sleep — передайте ей желаемую продолжительность сна в миллисекундах, и она разбудит процесс:
Стоит подумать о том, как это реализуется. В идеале при вызове Sleep(1) процессор переходит в спящий режим. Но как операционная система разбудит поток, если процессор спит? Ответ — аппаратные прерывания. ОС программирует микросхему — аппаратный таймер, который затем запускает прерывание, которое пробуждает процессор, и ОС затем запускает ваш поток.
Функции WaitForSingleObject и WaitForMultipleObjects также имеют значения таймаута, и эти таймауты реализуются с использованием того же механизма.
Если много потоков ждут таймеров, то ОС может запрограммировать аппаратный таймер на индивидуальное время для каждого потока, но это обычно приводит к тому, что потоки просыпаются в случайное время, а процессор так нормально и не засыпает. Энергоэффективность CPU сильно от времени его сна (нормальное время от 8 мс ), и случайные пробуждения тому не способствуют. Если несколько потоков синхронизируют или объединяют свои ожидания таймера, то система становится более энергоэффективной.
Существует множество способов объединения пробуждений, но основной механизм в Windows — глобальное прерывание таймера, тикающего с постоянной скоростью. Когда поток вызывает Sleep(n), то ОС запланирует запуск потока сразу после первого прерывания таймера. Это означает, что поток может в конечном итоге проснуться немного позже, но Windows — это не ОС реального времени, она вообще не гарантирует определённое время пробуждения (в любом случае, в это время ядра процессора могут быть заняты), поэтому вполне нормально проснуться чуть позже.
Интервал между прерываниями таймера зависит от версии Windows и железа, но на всех моих машинах он по умолчанию составлял 15,625 мс (1000 мс/64). Это означает, что если вызвать Sleep(1) в какое-то случайное время, то процесс будет разбужен где-то между 1,0 мс и 16,625 мс в будущем, когда сработает следующее прерывание глобального таймера (или через одно, если это сработало слишком рано).
Короче говоря, природа задержек таймера такова, что (если только не используется активное ожидание процессора, а его, пожалуйста, не используйте ) ОС может пробуждать потоки только в определённое время с помощью прерываний таймера, а Windows использует регулярные прерывания.
Некоторым программам не подходит такой большой разброс в задержках ожидания (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, многие игры и т. д.). К счастью, они могут решить проблему с помощью функции timeBeginPeriod , которая позволяет программе запросить меньший интервал. Есть также функция NtSetTimerResolution , которая позволяет устанавливать интервал меньше миллисекунды, но она редко используется и никогда не требуется, поэтому не буду больше её упоминать.
Десятилетия безумия
Вот сумасшедшая вещь: timeBeginPeriod может вызвать любая программа, и она изменяет интервал прерывания таймера, при этом прерывание таймера — это глобальный ресурс.
Представим, что процесс А находится в цикле с вызовом Sleep(1). Это неправильно, но это так, и по умолчанию он просыпается каждые 15,625 мс, или 64 раза в секунду. Затем появляется процесс B и вызывает timeBeginPeriod(2). Это заставляет таймер срабатывать чаще, и внезапно процесс А просыпается 500 раз в секунду вместо 64 раз в секунду. Это безумие! Но именно так всегда работала Windows.
В этот момент, если бы появился процесс C и вызвал timeBeginPeriod(4), это ничего бы не изменило — процесс A продолжал бы просыпаться 500 раз в секунду. В такой ситуации правила устанавливает не последний вызов, а вызов с минимальным интервалом.
Таким образом, вызов timeBeginPeriod от любой работающей программы может установить глобальный интервал прерывания таймера. Если эта программа завершает работу или вызывает timeEndPeriod , то вступает в силу новый минимум. Если одна программа вызывает timeBeginPeriod(1), то теперь это интервал прерывания таймера для всей системы. Если одна программа вызывает timeBeginPeriod(1), а другая timeBeginPeriod(4), то всеобщим законом становится интервал прерывания таймера в одну миллисекунду.
Это имеет значение, потому что высокая частота прерываний таймера — и связанная с ней высокая частота планирования потоков — может впустую расходовать значительную мощность CPU, как обсуждалось здесь .
Одним из приложений, которому необходимо планирование на основе таймера, является веб-браузер. В стандарте JavaScript есть функция setTimeout , которая просит браузер вызвать функцию JavaScript через несколько миллисекунд. Для реализации этой и других функций Chromium использует таймеры (в основном WaitForSingleObject с таймаутами, а не Sleep). Это часто требует повышенной частоты прерываний таймера. Чтобы это не слишком сказывалось на времени автономной работы, Chromium недавно модифицировали таким образом, чтобы при работе от батареи частота прерываний таймера не превышала 125 Гц (интервал 8 мс) .
timeGetTime
Функция timeGetTime (не путать с GetTickCount) возвращает текущее время, обновлённое прерыванием таймера. Процессоры исторически не очень хороши в ведении точного времени (их часы специально колеблются, чтобы не служить FM-передатчиками, и по другим причинам), поэтому для поддержания точного времени CPU часто полагаются на отдельные генераторы тактовых импульсов. Чтение с этих чипов стоит дорого, поэтому Windows поддерживает 64-битный счётчик времени в миллисекундах, обновляемый прерыванием таймера. Этот таймер хранится в общей памяти, поэтому любой процесс может дёшево считывать оттуда текущее время, не обращаясь к генератору тактовых импульсов. timeGetTime вызывает ReadInterruptTick, который по сути просто считывает этот 64-битный счётчик. Все просто!
Поскольку счётчик обновляется прерыванием таймера, мы можем его отследить и найти частоту прерывания таймера.
Новая недокументированная реальность
С выпуском Windows 10 2004 (апрель 2020 года) некоторые из этих механизмов слегка изменились, но очень запутанным образом. Сначала появились сообщения, что timeBeginPeriod больше не работает . На самом деле всё оказалось куда сложнее.
Первые эксперименты дали смешанные результаты. Когда я запустил программу с вызовом timeBeginPeriod(2), то clockres показал интервал таймера 2,0 мс, но отдельная тестовая программа с циклом Sleep(1) просыпалась около 64 раз в секунду вместо 500 раз, как в предыдущих версиях Windows.
Научный эксперимент
Тогда я написал пару программ для изучения поведения системы. Одна программа ( change_interval.cpp ) просто сидит в цикле, вызывая timeBeginPeriod с интервалами от 1 до 15 мс. Она удерживает каждый интервал в течение четырёх секунд, а затем переходит к следующему, и так по кругу. Пятнадцать строк кода. Легко.
Другая программа ( measure_interval.cpp ) запускает несколько тестов для проверки, как её поведение изменяется при изменении change_interval.cpp. Программа отслеживает три параметра.
- Она спрашивает ОС, каково текущее разрешение глобального таймера, используя NtQueryTimerResolution .
- Она измеряет точность timeGetTime, вызывая его в цикле до тех пор, пока возвращаемое значение не изменится — и отслеживая величину, на которую оно изменилось.
- Она измеряет задержку Sleep(1), вызывая его в цикле в течение секунды и подсчитывая количество вызовов. Средняя задержка является просто обратной величиной числа итераций.
@FelixPetriconi провёл для меня тесты на Windows 10 1909, а я провёл тесты на Windows 10 2004. Вот очищенные от случайных флуктуаций результаты:
Это означает, что timeBeginPeriod по-прежнему устанавливает интервал глобального таймера во всех версиях Windows. Из результатов timeGetTime() можно сказать, что прерывание срабатывает с такой скоростью по крайней мере на одном ядре процессора, и время обновляется. Обратите также внимание, что 2.0 в первой строке для 1909 года тоже было 2.0 в Windows XP , затем 1.0 в Windows 7/8, а затем вроде опять вернулось к 2.0?
Однако поведение планировщика резко меняется в Windows 10 2004. Ранее задержка для Sleep(1) в любом процессе просто равнялась интервалу прерывания таймера, за исключением timeBeginPeriod(1), давая такой график:
В Windows 10 2004 соотношение между timeBeginPeriod и задержкой сна в другом процессе (который не вызывал timeBeginPeriod) выглядит странно:
Точная форма левой части графика неясна, но она определённо уходит в противоположную сторону от предыдущего!
Последствия
Как было указано в обсуждении reddit и hacker-news, вероятно, левая половина графика представляет собой попытку максимально точно имитировать «нормальную» задержку, учитывая доступную точность глобального прерывания таймера. То есть с интервалом прерывания 6 миллисекунд задержка происходит примерно на 12 мс (два цикла), а с интервалом прерывания 7 миллисекунд — примерно на 14 мс (два цикла). Однако измерение фактических задержек показывает, что реальность ещё более запутанна. При прерывании таймера, установленном на 7 мс, задержка Sleep(1) в 14 мс даже не самый распространённый результат:
Некоторые читатели могут обвинить случайный шум в системе, но когда частота прерывания таймера 9 мс и выше, шум равен нулю, так что это не может быть объяснением. Попробуйте сами запустить обновлённый код . Особенно противоречивыми кажутся интервалы прерывания таймера от 4 мс до 8 мс. Вероятно, измерения интервалов следует выполнять с помощью QueryPerformanceCounter, поскольку на текущий код беспорядочно влияют изменение правил планирования и изменение точности таймера.
Всё это очень странно, и я не понимаю ни логики, ни реализации. Может, это и ошибка, но я в этом сомневаюсь. Думаю, что за этим стоит сложная логика обратной совместимости. Но самый эффективный способ избежать проблем совместимости — это документировать изменения, желательно заранее, а здесь правки сделаны без какого-либо уведомления.
Это не повлияет на большинство программ. Если процесс хочет более быстрое прерывание таймера, то он сам должен вызвать timeBeginPeriod. Тем не менее, могут возникнуть следующие проблемы:
- Программа может случайно предположить, что у Sleep(1) и timeGetTime одинаковое разрешение, а это теперь не так. Хотя, такое предположение кажется маловероятным.
- Программа может зависеть от маленького разрешения таймера, которого не получает. Было несколько сообщений о такой проблеме в некоторых играх — есть инструмент под названием Windows System Timer Tool и ещё один под названием TimerResolution 1.2 . Они «исправляют» эти игры, повышая частоту прерываний таймера. Видимо, эти исправления больше не будут работать или будут работать не так хорошо. Возможно, это заставит разработчиков игр выпустить правильные патчи, но до тех пор изменение вызывает проблемы обратной совместимости.
- В многопроцессной программе главная управляющая программа может повысить частоту прерываний таймера, а затем ожидать, что это повлияет на планирование дочерних процессов. Раньше это было разумное предположение, но теперь оно не работает. Именно так я сам узнал об этой проблеме. Продукт, о котором идёт речь, теперь вызывает timeBeginPeriod в каждом процессе, так что всё в порядке, но несколько месяцев программное обеспечение плохо работало по непонятной причине.
Исследование точности отсчёта временных интервалов в системе Windows
Братуха М.А., Шевченко О.Г.
ГВУЗ Донецкий национальный технический университет , г. Донецк, Украина
astr0n@ukr.net
Братуха М.А., Шевченко О.Г. Исследование точности отсчёта временных интервалов в системе Windows. Исследованиеразрешающей способности системного таймере Windows и его стойкости при нагрузках системы. В основе исследования лежат WinAPI-функции, которые позволяют программировать аппаратный таймер. В качестве объекта исследования выступают «ожидаемые таймеры».
Введение
Управление процессом предоставления ресурсов системы задачам, нитям, процедурам обработки прерываний и т.д. является одной из основных функций любой операционной системы и осуществляется при помощи такого механизма, как планирование. В системах реального времени планирование должно также гарантировать предсказуемое поведение, безопасность, возможность длительной, безотказной работы, выполнение задач в установленных временных рамках. От метода планирования во многом зависит успешная работа системы в целом.
Характерной особенностью операционной системы реального времени является управление ресурсами компьютера таким образом, чтобы определенные действия выполнялись периодически с одинаковым интервалом времени. Например, несвоевременное перемещение детали механизма, лишь по той причине, что это позволяют ресурсы системы, может привести к катастрофическим результатам, так же, как и невозможность осуществления перемещения этой детали вследствие занятости системы.
В данной работе исследуется возможность использования ОС Windows, как модельной среды при разработке и отладке ПО систем реального времени.
1 Способы измерения временных интервалов
Документированный набор функций Windows предоставляет несколько инструментов для задания и измерения временных интервалов:
1) timeGetTime() – возвращает время в миллисекундах с момента старта операционной системы («мультимедийный таймер»). Возвращаемое значение обнуляется каждые 49,71 дней. Точность измерения около 1-5 мс.
2) GetTickCount() – возвращает время в миллисекундах с момента старта операционной системы (обнуление каждые 49,71 дней). Является достаточно быстрой функцией, однако, точность измерения порядка 55 мс.
3) QueryPerformanceCounter() – «таймер высокого разрешения». Возвращает «набежавшее» количество тиков. Частота работы этого таймера не меняется во время работы системы (порядка 1 МГц и более). Частота определяется с помощью функции QueryPerformanceFrequency().
4) SetTimer() – задаёт временной интервал и обладает небольшой точностью. Позволяет выполнять пользовательскую функцию при каждом срабатывании, однако нет возможности организовывать отложенные вызовы.
5) WaitableTimer() – «ожидаемый таймер». Обладает высокой точностью (порядка 1 мс). При срабатывании таймера возможен вызов пользовательской функции.
Из перечисленных функций интерес представляю те, которые позволяют задать желаемый интервал времени, по истечении которого необходимо выполнить определенные действия. В связи с этим в качестве объекта исследования был выбран, так называемые «ожидаемый таймер» (waitable timer). Программный интерфейс таймера предоставляет возможность указать пользовательскую функцию, которая по каждому тику должна запускаться на исполнение.
Ожидаемый таймер являются объектом ядра, что немаловажно для его точности и адекватности поведения. Данный объект может находиться в одном из двух состояний – быть занятым или свободным, при этом в первое из указанных состояний объект переходит сразу же после своего создания. Для сообщения таймеру момента перехода в свободное состояние используется функция SetWaitableTimer(). Переход таймера в свободное состояние означает завершение временного интервала. Согласно интерфейсу соответствующих API-функций, при программировании ожидаемого таймера может быть предусмотрена передача управление пользовательской (так называемой callback) функции. Вызываемая callback-функция ставиться в APC (asynchronous procedure call) очередь, а это означает, что каждый запрос на ее вызов будет обслужен системой. Однако, если длительность выполнения АРС функции превышает величину временного интервала, отсчитываемую ожидаемым таймером, то следующий вызов этой функции будет выполнен только по завершению работы текущей. Для систем, выполнение действий в которых зависит от временных факторов, это может быть серьёзной проблемой. В Windows данная ситуация разрешается путем привлечения стандартных механизмов организации многопоточных приложения – ресурсоемкая часть callback функции локализуется в рамках одного или нескольких потоков.
По-умолчанию разрешающая способность системного таймера в Windows на большинстве рабочих станций составляет около 15,6 мс. Это означает, что даже высокоточный «ожидаемый таймер» не сможет измерять с точностью в 1мс на таких машинах, поскольку он также реализован на базе системного таймера. Обойти это ограничение можно с помощью специальных WinAPI-функций timePeriodBegin() и timerPeriodEnd(), которые позволяют задать точность системного таймера вплоть до 1 мс.
Поскольку системы класса Windows (исключая CE) изначально не являются системами реального времени, поэтому задачей исследования было выяснить – можно ли реализовать высокоточные измерения интервалов времени в Windows и что это за собой повлечёт, а также, какие условия должны при этом выполнятся.
2 Программа тестирования таймера
Для проведения полномасштабного тестирования возможностей системного таймера, была разработана программа, которая использовала WinAPI-функцию WaitableTimer в качестве связующего звена с системным таймером и позволяла тестировать таймер в различных условиях c учетом степени нагрузки и приоритетов приложений. Разработанная программа предоставляет следующие возможности:
· Задание разрешающей способности таймера;
· Установку временных интервалов между тиками таймера;
· Указание количества тиков таймера (т.е. задание длительности выполнения программы);
· Регулировку приоритетов нагрузочных потоков;
· Тестирование с учётом приоритета текущего потока;
· Моделирование степени нагрузки системы.
Программа разработана с консольным интерфейсом на языке C++. Тестирование проводилось в ОС Windows 7 SP1 и Windows XP SP3.
В ОС Windows предусмотрено 32 уровня приоритета потоков, которые составляют 6-ть классов приоритетов процессов, для тестирования таймера было выбрано три из них: низкий, высокий и нормальный.
Моделирование степени загрузки системы («легкая», «средняя», «тяжелая») осуществлялось путем увеличения количества функционирующих потоков – 0, 100, 400 соответственно. В качестве легкой степени загрузки принимается состояние системы на момент запуска однопоточного теста.
Для наглядности анализа результаты экспериментов представлены в виде графиков. Они позволяют оценить:
– количество ошибочных срабатываний таймера (то есть таких, при которых последующий тик таймера приходит позже установленного интервала);
– среднее время величины интервала.
На рисунке 1 видно три линии, которые показывают зависимость количества ошибочных значений от количества выполненных тиков, каждая линия соответствует процессу с определённым приоритетом, в данном случае верхняя линия соответствует процессу с низким приоритетом, средняя – со средним приоритетом и нижняя – с высоким приоритетом. Представленные результаты позволяют сделать вывод о том, что качественные характеристики таймера зависят от приоритета выполняемой программы.
Рисунок 1 – График зависимости ошибочных срабатываний таймера от их общего количества
Рисунок 2 демонстрирует зависимость среднего времени интервала от количества таких интервалов.
Рисунок 2 – График зависимоти среднего времени интервала от общего количества таких интервалов
В ходе проведения экспериментов на системе Windows 7 было обнаружено, что если система сама устанавливает разрешающую способность таймера ниже, чем 15,6 мс, то увеличить ее не представляется возможным, пока сама ОС не сделает этого.
Выводы
Анализируя полученные данные, можно с определенной уверенностью сказать, что точность таймера зависит от приоритета главного потока, приоритетов нагрузочных потоков, а также от количества таких потоков, то есть от общей нагрузки системы.
Поскольку на Windows XP не удается установить желаемую разрешающую способность таймера (даже используя функцию timePeriodBegin), то рекомендуется задавать интервал между тиками кратным времени, которое устанавливает система, в таком случае точность таймера будет высокой и количество ошибочных интервалов значительно уменьшиться.
Не смотря на то, что во время тестирования некоторые тесты приводили к эффекту «зависания» системы, а другие выполнялись слишком долго (в 10 раз больше, чем было задано), результаты тестов говорят о том, что на базе систем Windows можно реализовать достаточно высокоточные измерения и использовать ОС Windows, как модельную среду при разработке и отладке ПО для систем реального времени.
Литература
1. Руссинович М. и Соломон Д. Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP и Windows 2000. Мастер-класс. / Пер. с англ. – 4-е изд. – М.: Издательство «Русская Редакция»; СПб.: Питер, 2008. – 992 стр.: ил.
2. Рихтер Дж. Windows для профессионалов: создание эффективных Win32-приложений с учётом специфики 64-разрядной версии Windows/Пер. с англ. – 4-е изд. – СПб.: Питер; М.: Издательско-торговый дом «Русская Редакция», 2004. – 749 с.: ил.