Системный таймер — что это
На самом деле, часы — это не есть сам таймер. Просто они работают с помощью него. Сам таймер просто с определенной частотой вырабатывает прерывание INT 8h, приблизительно 18,2 раза в секунду (точное значение — 1193180/65536 раз в секунду) . С помощью него работают и другие устройства компьютера — от контроллера харда до генератора звуковых сигналов. Используется, в основном, при программировании (например, при генерации случайных чисел).
Остальные ответы
Часы в системном блоке (работающие от литиевой батарейки) , время можно выставить в Bios-е
Что такое системный таймер? Что будет с системой, если он не работает (нет дров) ? Может ли это влиять на работу USB 3?
Был сбой напряжения в сети, комп перезагрузился. В итоге в диспетчере устройств появился восклицательный знак — нет драйвере для системного таймера. Кроме того, перестал видеться внешний диск WD My Book на 2 тб, подключенный по USB 3 (по USB 2 диск виден и работает) . Это может быть связано со сбоем системного таймера? С поиском драйвера не помогли ни DriverPack Solution, ни Driver Genius, Гугл с Яндексом пишут «по запросу ничего не найдено». Обновил чипсет, толку никакого. Что же делать?
Дополнен 10 лет назад
Компьютеру нет и полгода, системная плата ASRock Z68 Extreme4 Gen3
Лучший ответ
Эти драйверы идут в поставке винды, а не производителя железа, при сбое, видимо, с ними система взаимодействовала, и не успела записать корректно на жесткий диск. Самое простое переставить винду.
Остальные ответы
Просто отчет времени и работы всех программ и системы вроде! просто узнать скок времени был включен комп и так далее
STM32F4: GNU AS: Прерывания, Системный таймер (SysTick) (Часть 6)
В первых публикациях — мы тактировали микроконтроллер от внутреннего тактового генератора (HSI)… ну если быть совсем точным, то мы вообще не настраивали тактирование микроконтроллера, и пользовались тем состоянием которое было у микроконтроллера при старте (включении питания, сбросе). Для первых программ это вполне допустимо, но для будущих проектов этого не достаточно, поэтому в пятой части публикации я предложил модуль настройки тактирования микроконтроллера (sysclk) на его «родную» (определенную производителем) частоту в 168 МГц.
Теперь организуем задержку на заданное количество миллисекунд при помощи системного таймера (SYSTICK).
До настоящей публикации, в своей первой программе «мигалка», для того чтобы организовать задержку при мигании светодиодом мы делали цикл с достаточно большим периодом повторения:
DELAY: LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз. Delay_loop: SUBS R3, R3, 1 BNE Delay_loop BX LR
Такой подход более менее допустим при тактовой частоте микроконтроллера в 10-20 МГц и при отсутствии требования о жестком задании временных интервалов и их соответствии каким то общеупотребимым значениям (например долям секунды).
Теперь же (после настройки тактирования микроконтроллера на 168 МГц) переменную цикла, для сохранения прежней частоты мигания светодиодом (значение регистра R3), нужно увеличить примерно в 10 раз — значение должно быть равно, примерно, 0x00119999. На глаз — частота мигания светодиода станет прежней, но точно подобрать интервал примерно равный полу- или одной секунде, что называется «на глаз», без большой погрешности просто не возможно… В программе мигалка это не важно, но в дальнейшем такой подход использовать нельзя, и нужно использовать другое решение:
В микроконтроллерах STM32F4 есть системный таймер на основе которого получается хороший способ генерации задержек.
Описание регистров системного таймера (SYSTICK) приведено в PM214 Programming manual, стр. 245 (и далее), можно поискать в интернете, например, вот таким запросом в googlе.ru: systick stm32f4 пример, я лишь немного резюмирую информацию по регистрам и «переложу» все это в код программы на языке ассемблера:
У системного таймера есть несколько управляющих регистров:
—STK_CTRL — регистр управления, смещение относительно базы — 0х00 
Расшифровка управляющих битов:
- COUNTFLAG Бит устанавливается в «1» если счетчик досчитал до нуля и этот бит еще не проверялся
- CLKSOURCE — Бит выбора источника для тактирования SYSTICK:
0: AHB/8
1: AHB
Частота AHB равна тактовой частоте процессора, после работы модуля тактирования частота AHB будет равна 168 МГц - TICKINT — Бит включения прерывания таймера при достижении нуля:
0: При достижении «0» прерывание не генерируется
1: При достижении «0» генерируется прерывание - ENABLE — Включить системный таймер
—STK_LOAD — регистр первоначального значения, смещение относительно базы — 0х04 
В регистре содержится первоначальное значение счетчика, значение 24-ех битное, от 1 до 0xFFFFFF (16777215). Стартовое значение равное «0» не имеет смысла, так как бит COUNTFLAG регистра STK_CTRL выставляется в «1» при уменьшении значения счетчика с «1» на «0».

—STK_VAL — регистр текущего значения, смещение относительно базы — 0х08
Значение счетчика так же как и значение регистра STK_LOAD 24-ех битное. При чтении регистра — считывается его текущее значение, при любой записи регистра — значение регистра сбрасывается в «0»

—STK_CALIB — регистр калибровки, смещение относительно базы — 0х0C
В этом регистре не разбирался. Судя по описанию калибровочное значение нужно при использовании частоты такта AHB/8.
- Установить значение счетчика STK_LOAD
- Установить управляющее значение регистра STK_CTRL:
- Выбрать частоту таймера, CLKSOURCE=1
- Выбрать необходимость генерации прерывания, TICKINT=1
- Включить таймер, ENABLE=1
Почему загружаем значение (168 000 -1) а не просто 168 000? — потому что загрузка значения STL_LOAD в STK_VAL происходит при следующем цикле после установки значения счетчика равным «0», и следующее уменьшение значения еще при следующем такте — таким образом количество тактов как бы увеличивается на «1» относительно значения заданного STK_LOAD.
Оформим алгоритм настройки, приведенный выше, программой на языке ассемблера:
.global SYSTICK_START SYSTICK_START: LDR R0 , =SYSTICK_BASE @ установка значения пересчета для получения частоты 1000 гц LDR R1 , =(168000 - 1) STR R1 , [ R0 , STK_LOAD] @ источник частоты AHB (168 мгц) + прерывания + включаем SYSTICK LDR R1 , =(STK_CTRL_CLKSOURSE + STK_CTRL_TICKINT + STK_CTRL_ENABLE) STR R1 , [ R0 , STK_CTRL]
После включения таймера произойдет следующее:
1) значение из STK_LOAD будет записано в STK_VAL
2) значение STK_VAL будет уменьшаться с частотой заданной CLKSOURSE [1: 168 мгц / 0: 21 мгц (168/8)], в примере выше CLKSOURSE=1 (такт системного таймера от AHB =168 МГц)
3) как только значение STK_VAL будет уменьшаться с 1 на 0 произойдет:
а) установка флага COUNTFLAG=1
б) если бит настройки TICKINT=1, то произойдет вызов подпрограммы обработки прерывания системного таймера
в) значение из STK_LOAD будет загружено в STK_VAL
Думаю с настройкой системного таймера (SYSTICK) теперь все более менее понятно (за исключением калибровки), теперь посмотрим как строится обработчик прерываний системного таймера.

Расположение векторов таблицы прерываний можно посмотреть на стр. 37 PM0214 Programming manual
Эти указатели размещаются сразу после указателя стека загружаемого при старте микроконтроллера.
Я поместил эту таблицу указателей в файл isr_vector.inc
@ Таблица исключений для микроконтроллеров STM32F40x - F41x .word int_vect_terminator+1 @ NMI .word int_vect_terminator+1 @ Hard Fault .word int_vect_terminator+1 @ MPU Fault .word int_vect_terminator+1 @ Bus Fault .word int_vect_terminator+1 @ Usage Fault .word 0 @ Reserved .word 0 @ Reserved .word 0 @ Reserved .word 0 @ Reserved .word int_vect_terminator+1 @ SVCall .word int_vect_terminator+1 @ Debug Monitor .word 0 @ Reserved .word int_vect_terminator+1 @ PendSV .word ISR_SYSTICK+1 @ SysTick .section .text @ Заглушка для любых необрабатываемых прерываний int_vect_terminator: B int_vect_terminator
Как я описывал в первой части статьи — все вектора прерываний должны быть увеличены на 1.
Поскольку сейчас нам не нужно обрабатывать никакие прерывания кроме SYSTICK — то все вектора заполним адресом перехода на подпрограмму-заглушку int_vect_terminator, которая представляет собой просто зацикленный сам на себя переход B
Включение таблицы векторов прерываний выглядит как .include в секции .vectors после указателя стека и вектора сброса (RESET)
.section .vectors @ таблица векторов прерываний .word 0x20020000 @ Вершина стека .word Start+1 @ Вектор сброса .include "isr_vector.inc" @ таблица указателей векторов прерываний
Теперь более четко определим логику работы с системным таймером (SYSTICK)
В прерывании мы будем проверять некий счетчик (размещенный в SRAM) на равность нулю, и если его значение не равно «0», то будем уменьшать это значение на единицу (в текущем прерывании) и выходить… Поступая таким образом мы добьемся уменьшения значения счетчика на 1 каждые 1/1000 секунду (1 мс).
В программе: объявляем переменную в SRAM, согласно карты компоновки (stm32f40_def.ld) это секция .bss:
.section .bss @ Переменная в ОЗУ SYSTICK_COUNTER: .word 0 @ Счетчик задержки
Еще один интересный момент: нам нужно загрузить адрес переменной SYSTICK_COUNTER в R1 (см. далее в программе), для этого мы создаем в секции .asmcode или .rodata константу ADR_SYSTICK_COUNTER со значением адреса переменной,
ADR_SYSTICK_COUNTER: .word SYSTICK_COUNTER
и уже это значение загружаем в R1, после того как в R1 мы загрузили адрес счетчика — мы можем загрузить в R0 значение этого счетчика
LDR R1 , ADR_SYSTICK_COUNTER LDR R0 , [R1 , 0]
Теперь нам нужно проверить значение R0 на ноль, и если R0<>0, то уменьшить его на 1:
ORRS R0 , R0 , 0 @ Проверка R0 на 0 ITT NE @ Если R0<>0 уменьшаем его на 1 SUBNE R0 , R0 , 1 STRNE R0 , [R1 , 0]
Способ проверки и изменения прост: сначала мы делаем логическое OR регистра R0 к самому себе, соответственно его значение не меняется, но вот суффикс «S» после команды указывает, что необходимо обновить флаги. Соответственно если R0 = 0, то после выполнения ORR R0, R0, 0 будет установлен флаг «Z«. Далее идет блок IT который резервирует после себя две команды которые будут выполнены при сброшенном флаге «Z» (в нашей программе в случае если R0<>0).
Это команды:
— Уменьшения R0 на «1» (при сброшенном флаге «Z» — суффикс исполнения «NE«)
— Записи нового значения R0 (так же при сброшенном флаге «Z» — суффикс исполнения «NE«)
Поскольку в нашем прерывании мы используем два регистра R0 и R1 — которые возможно имели какие то значения в прерываемой программе- то необходимо их сохранить при входе в прерывание и восстановить при выходе из него — это делается командами PUSH / POP соответственно.
Весь код обработчика прерываний будет выглядеть следующим образом:
.section .bss @ Переменная в ОЗУ SYSTICK_COUNTER: .word 0 @ Счетчик задержки .section .asmcode ADR_SYSTICK_COUNTER: .word SYSTICK_COUNTER @ Прерывание уменьшает значение счетчика SYSTICK_COUNTER на "1" (в случае если @ значение счетчика больше "0" .global ISR_SYSTICK ISR_SYSTICK: PUSH < R0 , R1 >LDR R1 , ADR_SYSTICK_COUNTER LDR R0 , [R1 , 0] ORRS R0 , R0 , 0 @ Проверка R0 на 0 ITT NE @ Если R0<>0 уменьшаем его на 1, и сохраняем в SYSTICK_COUNTER SUBNE R0 , R0 , 1 STRNE R0 , [R1 , 0] POP < R0 , R1 >BX LR
К этому моменту у нас есть:
1) Настройка системного таймера
2) Прерывание генерируемое 1000 раз в секунду
3) Уменьшаемый с частотой 1000 раз в секунду счетчик.
Осталось написать процедуру задержки на заданное время:
.global SYSTICK_DELAY SYSTICK_DELAY: PUSH LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик STR R0, [R2, STK_VAL] LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика STR R0, [R1 , 0] @ сохраним начальное значение DELAY_LOOP: LDR R0, [R1 , 0] @ ждем обнуления счетчика ORRS R0, R0 , 0 BNE DELAY_LOOP POP
Что мы делаем:
— сохраняем используемые регистры
— сбрасываем текущий счетчик STK_VAL в «0» (посмотрите описание, счетчик сбрасывается ПРИ ЛЮБОЙ ЗАПИСИ в него!)
PUSH LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик STR R0, [R2, STK_VAL]
-сохраняем входное значение задержки в R0 в SYSTICK_COUNTER
LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика STR R0, [R1 , 0] @ сохраним начальное значение
— Ждем в цикле пока счетчик не обнулится — это будет означать что [R0]-миллисекунд прошло и можно выходить из подпрограммы задержки
DELAY_LOOP: LDR R0, [R1 , 0] @ ждем обнуления счетчика ORRS R0, R0 , 0 BNE DELAY_LOOP POP

Весь код модуля мы разобрали, осталась лишь одна тонкость:
Дело в том что в микроконтроллерах STM32 существует возможность задания приоритетов прерываний. В AVR микроконтроллерах если процессор обрабатывает прерывание, то все остальные, возникшие за это время прерывания, ожидают завершения обработки текущего прерывания и потом будут исполняться в определенном порядке. В STM32 если при выполнении прерывания с приоритетом например 15, возникает прерывание с приоритетом меньшим — то процессор перейдет от выполнения текущего прерывания к выполнению прерывания с более высоким приоритетом. Насколько часто могут возникнуть подобные ситуации я не берусь сказать, скорее всего все зависит от контекста задачи. Но коль такой механизм есть, то настроим низший приоритет для нашего прерывания (низший приоритет — более высокое значение приоритета!).
Для того чтобы определить какой регистр к какому прерыванию задает приоритет нужно посмотреть на стр. 217 PM0214 Programming manual:

Для нашего системного таймера определен регистр SHPR3:
Для максимального приоритета задается номер «0», для минимального 0xFF, правда есть оговорка что принимаются только старшие 4 бита приоритета — таким образом у нас есть возможность задать старшими 4-мя битами 16 приоритетов (от 0x00 до 0xF0).
Я использовал системный таймер и без задания приоритета прерывания — так что можно особо не разбираться в этом механизме сейчас, но чтобы не забыть что это нужно настраивать — задаем приоритет 15 (самый низший)
Полный текст модуля под раздельную компиляцию
@GNU AS .syntax unified @ синтаксис исходного кода .thumb @ тип используемых инструкций Thumb .cpu cortex-m4 @ процессор .fpu fpv4-sp-d16 @ сопроцессор @ определение констант .equ SCB_BASE ,0xE000ED00 @ System control block (SCB) .equ SHPR3 ,0x20 @ System handler priority registers (SHPRx) .equ SYSTICK_BASE ,0xE000E010 @ System timer .equ STK_CTRL ,0x00 @ Регистр статуса и управления .equ STK_LOAD ,0x00000004 @ Значение для перезагрузки счетчика .equ STK_VAL ,0x00000008 @ Текущее значение счетчика .equ STK_CTRL_CLKSOURSE ,0x00000004 @ (RW) источник тактирования: 0: AHB/8; 1: AHB .equ STK_CTRL_TICKINT ,0x00000002 @ (RW) при установке генерирует прерывание при переходе в 0 .equ STK_CTRL_ENABLE ,0x00000001 @ (RW) включает счетчик. @ **************************************************************************** @ * Обработчик прерывания системного таймера SysTick * @ **************************************************************************** .section .bss @ Переменная в ОЗУ SYSTICK_COUNTER: .word 0 @ Значение необходимой задержки .section .asmcode @ Прерывание уменьшает значение счетчика SYSTICK_COUNTER на "1" (в случае если @ значение счетчика больше "0" .global ISR_SYSTICK ISR_SYSTICK: PUSH LDR R1 , ADR_SYSTICK_COUNTER LDR R0 , [R1 , 0] ORRS R0 , R0 , 0 @ Проверка R0 на 0 ITT NE @ Если R0<>0 уменьшаем его на 1 SUBNE R0 , R0 , 1 STRNE R0 , [R1 , 0] POP ADR_SYSTICK_COUNTER: .word SYSTICK_COUNTER @ **************************************************************************** @ * Инициализация системного таймера SysTick * @ **************************************************************************** @ Для частоты AHB=168 Мгц @ Частота счета 1000 Гц @ @ Включение SysTick .global SYSTICK_START SYSTICK_START: PUSH LDR R0 , =SYSTICK_BASE @ установка значения пересчета для получения частоты 1000 гц LDR R1 , =168000 - 1 STR R1 , [R0 , STK_LOAD] @ источник частоты AHB (168 мгц) + прерывания + включаем SYSTICK LDR R1 , =(STK_CTRL_CLKSOURSE + STK_CTRL_TICKINT + STK_CTRL_ENABLE) STR R1 , [R0 , STK_CTRL] @ установка приоритета прерываний от SysTick LDR R0 , =SCB_BASE LDR R1 , [R0, SHPR3] ORR R1 , R1 , 0xF0 << 24 STR R1 , [R0 , SHPR3] POP @ **************************************************************************** @ * Задержка средствами системного таймера SysTick * @ **************************************************************************** @ Входной параметр: R0 - задержка в милисекундах @ Выходной параметр: R0 = 0 @ Изменение других регистров: нет .global SYSTICK_DELAY SYSTICK_DELAY: PUSH LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик STR R0, [R2, STK_VAL] LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика STR R0, [R1 , 0] @ сохраним начальное значение DELAY_LOOP: LDR R0, [R1 , 0] @ ждем обнуления счетчика ORRS R0, R0 , 0 BNE DELAY_LOOP POP
Использование модуля в программе «мигалка»
Я не буду приводить весь текст программы приведу только текст процедуры DELAY которая отвечает за задержку:
DELAY: PUSH < R0, LR >MOV R0, 250 @ задержка 250 мс. BL SYSTICK_DELAY POP
В переменной R0 мы задаем желаемую задержку в миллисекундах, и потом вызываем SYSTICK_DELAY из получившегося у нас модуля systick. Удобство в том, что теперь мы можем задавать период мигания светодиода с точностью до 1 мс, а не подбирать «на глаз» 🙂
Проект с использованием модулей sysclk и systick можно скачать здесь
Дополнительно в скрипт компиляции я внес небольшое усовершенствование: теперь при включении файлов директивой .include путь задается относительно .asm файла в котором делается «включение». Так что теперь, если в модуль входят несколько файлов, то можно делать .include просто по имени включаемого файла при условии, что он «лежит» рядом с .asm файлом — это удобно — так как не нужно знать относительный путь папки модуля в проекте. К слову, старый способ включения файлов с указанием относительного пути тоже работает, можете делать так как вам удобно.
P.s. если в статье где-то не очень понятно выразился — то вы всегда можете написать мне в «личку» здесь на хабре или на gorbukov @ «тот кто знает все !»
Системный таймер в Windows: большое изменение
Поведение планировщика Windows значительно изменилось в Windows 10 2004 без каких-либо предупреждений и изменения документации. Вероятно, это поломает несколько приложений. Такое происходит не первый раз, но эта перемена посерьёзнее.
Если вкратце, то вызовы 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), вызывая его в цикле в течение секунды и подсчитывая количество вызовов. Средняя задержка является просто обратной величиной числа итераций.

Это означает, что 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 в каждом процессе, так что всё в порядке, но несколько месяцев программное обеспечение плохо работало по непонятной причине.
Жертва
Тестовая программа change_interval.cpp работает только в том случае, если никто не запрашивает более высокую частоту прерываний таймера. Поскольку и Chrome, и Visual Studio имеют привычку делать это, мне пришлось проделать большую часть моих экспериментов без доступа в интернет и программируя в блокноте. Кто-то предложил Emacs, но ввязываться в эту дискуссию выше моих сил.
- Системное администрирование
- Разработка под Windows
- Игры и игровые консоли