/COMP/ Intel: HPET

Jan 11, 2006 03:21



Волею судеб занесло меня на интересную
страничку о новейших веяниях в таймеростроении
. Почитал
я её и, как ильфопетровский Ромуальдыч, тяжело задумался...
Или я не понимаю Высших Целей которые достигались
составлением Этого, или одно из двух, но почему кажется, что
всё было намеренно усложнено значительно больше чем
необходимо?

Некоторое предисловие. Что требуется в достаточно
современной ОС от механизма счёта времени и генерации
периодических событий по этому счёту? Это три основные вещи:

1. Собственно считать время. То есть, имея некоторые
внушённые "свыше" (CMOS память, NTP сервер, PPS (Pulse Per
Second) сигнал от GPS приёмника, или просто админа нажавшего
Enter на команде/форме установки времени) данные вида "в
такой-то момент было столько-то на часах", для любого
момента после этого "внушения" дать ответ о времени в этот
момент, с хорошей точностью. "Хорошая точность" это не
секунды, это как минимум миллисекунды, в идеале -
микросекунды. Дальше стремиться, как правило, бессмысленно:
развитие компьютеров уже упёрлось в несколько потолков
скоростей, и интервалы меньше микросекунд местами
несущественны даже на системной шине.

2. Не слишком редко порождать прерывание. Прерывание служит
средством "оживления" системы разделения времени, которыми
хотя бы в основе являются все современные ОС: по таймерному
прерыванию они производят пересчёт планирования процессов,
останавливают прожор, давая возможность поработать другим,
запускают очистительные процессы, останавливают затянувшиеся
ожидания несбыточных мечтаний вроде прихода пакета с давно
умершего TCP-соединения, и прочая, и прочая, и прочая. Как
часто должно порождаться такое прерывание? Большинство
современных ОС настраивают "железо" на посылку прерывания
100 раз в секунду, хотя FreeBSD7, по слухам, перешла на 1000
раз в секунду. Впрочем, 100 или 1000 - не так важно, обе
цифры вполне в пределах разумного, если не навешивать на
таймерное прерывание слишком много обработки.

3. Порождать прерывание достаточно близко к таймауту
какого-либо ожидания. В большинстве реализаций интервал
между таймерными прерываниями постоянен, поэтому среднее
отклонение будет составлять 1/2 периода между прерываниями.
Интересный вариант здесь представляет Windows NT с
потомками, где частота прерываний берётся как максимум из
запрошенных всеми драйверами; драйвер при загрузке может
заказать частоту повыше, чтобы точнее подгадать к нужному
моменту.

Собрав эти три требования, посмотрим на традиционные
реализации. В первую очередь это старый добрый i8254,
логическая схема которого повторяется во всех чипсетах
начиная ещё, наверно, с комплектов для 80386. Устройство его
тривиально - в режиме 3, который нас интересует, счётчик
декрементируется до 0 по входящей тактовой частоте, в этот момент
генерируется прерывание, в счётчик загружается значение из
регистров начального значения и всё повторяется. Пока нет
желания узнать текущее время точнее чем до периода
прерывания, всё просто - прерывание инкрементирует счётчик,
который мы читаем когда хотим узнать время. А если с
точностью до микросекунды (тактовая частота на входе в PC
равна 1193182Гц)? Надо прочитать текущее значение счётчиков;
это возможно. Но если счётчики близки к 0 (неважно, с какой
стороны - будь то 0x0001 или 0xFFFD), то мы чуть поспешили
или чуть опоздали? Чтобы корректно прочитать значение,
приходится прибегать, например, к следующим пляскам с бубном:

- прочитать счётчик прерываний (разрешив прерывания после
этого! это условие обязательно)
- прочитать значения счётчиков канала i8254 (предварительно
запомненных в регистрах-защёлках)
- снова прочитать счётчик прерываний, если значение не
совпало с тем что на первом чтении - пропустили
прерывание, начинаем всё сначала

Но и даже это не проблема. Проблема - что если кто-то
запретит прерывания на период более одного цикла таймера?
Чем чаще скорость прерываний, тем это вероятнее. Железо
капризное и медленное, реагирует тяжело, порой легче сделать
задержку, чем отдавать процессор другой задаче и требовать
его возврата через пару миллисекунд...

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

Оценим количественные параметры этого счётчика. Установив
следующие ограничения (по принципу "меньше уже нельзя"):
- тактовая частота не менее 1 МГц,
- разрядность не менее 32 бит,
- полный цикл не менее 4 секунд,

получим 1050(!)-кратный запас по параметрам. Если частоту
принять за любимую PC'шную 1193182Гц (вообще-то это 1/3 так
называемой f(sc) телестандарта NTSC), и сохранить
разрядность, то полный цикл будет достигаться за 3599.5
секунд... в общем за час:) Можно поднять частоту...

Фактически, по этому принципу построен отличный ACPI-fast
timer стандарта ACPI (в девичестве это был PIIX timer южных
мостов интеловских чипсетов). Ну разрядность у него
подкачала - только 24 (это уже какая-то нелепая экономия на
спичках), зато частота в 3 раза выше, полный цикл - 4.5
секунды. Что ж, достаточно чтобы не заботиться о пропуске
прерываний.

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

Идеально красивой эта система получится, конечно, при
одинаковых частотах на входе двух счётчиков - времени и
интервала прерывания - и при одинаковых размерностях. Можно
и на часок уснуть, если системе делать нечего:) Но даже при
разных параметрах, если они в пределах допустимого,
осложнение - только несколько умножений/делений.

Ограничение на параметры счётчика интервала должно следовать
одному - ограничению возможного периода прерывания. От 10мкс
до 4с с шагом 10мкс - заведомо выше чем нужно 99.99% задач,
а иначе всё равно потребуется спец.железо:) Так что не будем
требовать большего.

Знакомые с особенностями аппаратных прерываний могли
заметить, что я сказал про 1) отрицательное значение
старшего бита счётчика интервала, а не переход через 0, 2)
срабатывание по уровню, а не по фронту. Возможно было,
конечно, и требовать срабатывание по фронту. Если контроллер
прерываний никогда их не теряет - не страшно. Но
срабатывание по фронту должно включать в себя случай
загрузки отрицательного значения в момент загрузки, иначе...
ну неустойчиво оно получается:)

Наконец, можно захотеть не счётчик интервала, а значение, с
которым сравнивается счётчик времени. (Это то, откуда растут
идеи про обязательность идентичных частоты и размерности.)
Но, тогда прерывание должно генерироваться если значение
счётчика времени больше либо равно "как беззнаковое"
чем значение регистра "образца" (то есть их разность имеет
старший бит 0). А иначе жди беды, если программа просто не
успела загрузить вычисленное значение в регистр образца.

Эту схему можно расширять, например, несколькими счётчиками
интервала (пусть генерируют разные прерывания, если система
этого хочет - примерно как BSD системы имеют обычное
таймерное прерывание и профилирующее прерывание). Но даже в
варианте ACPI-fast timer для счёта плюс i8254 для генерации
- получается система устойчивая и соответствующая всем
желаниям.

Но тут приходит Intel (малиновые штаны, все приседают и
делают три раза "ку") и показывает всем новый мегарулез
HPET. Посмотрим на него.

Детали размещения данных в памяти мы, конечно, пропустим.
Что существенного?

- До 8 таймерных блоков (у каждого своя основная частота), в
каждом до 32 таймеров (это чтобы все прерывания занять и
больше ни на что не осталось; совмещать же таймерные
прерывания с другими на одной линии - грязный не-кошер, с
этим даже я согласен). Что ж, грандиозно - как пирамида
Хеопса в роли пляжного топчана.

- Минимальная тактовая частота на входе таймеров - 10МГц.
(Очевидно, подавая выход генератора на любимом PC'шном NTSC
4f(sc) == 14_318_182Hz, удовлетворяем это требование, а
дальше никто кроме Особых Монстроидальных Извращенцев не
будет давать больше. Считать только с такими числами тяжело,
но это не страшно - все уже давно привыкли, а процессоры у
нас быстрые.

- Очень много описания раутинга прерываний. Ладно, у Intel
это больная тема; сначала надо было вжиматься в 8
прерываний, потом в 16, теперь - прогресс! - в 32; остаётся
в упор непонятным, почему аппаратные прерывания,
ловушки/исключения и программные прерывания вроде сисколлов
должны размещаться в одной и той же IDT??? Думаю, лет через
5 Intel нам преподнесёт "расщепление" IDT на несколько
таблиц или её увеличение за пределы нынешних 256 векторов,
как всегда - в максимально извращённом и неудобном для
понимания стиле. Сделать же просто и понятно - религия не
позволит.

- О, уже ближе к теме: таймерный интервал в фемтосекундах
(10^-15 секунды). Наша любимая 4f(sc) даст 69841269 оных
фемтосекунд. Что ж, "Внушаить" (tm)

- Main Counter Register. Его можно останавливать. Зачем? Всё
что нужно грамотно написанному драйверу - 1 (прописью: один)
раз прочитать его значение (при старте ядра или, так уж и
быть, при выходе из suspend//hibernate), запомнить в своих
данных и дальше считать от него. Останавливать - должно быть
табу.

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

Авторы сего концепта осознали связанные с подобным дизайном
проблемы, о чём таки упомянули: "WARNING: Software
developers must be careful when programming the comparator
registers. If the value written to the register is not
sufficiently set far enough ahead of the current register
value, then the counter may pass the value before it reaches
the register and the interrupt will be missed." Что ж,
честность - лучшая политика, как сказал герой О'Генри,
продававший государственные посты направо и налево. Как
можно это обойти? После загрузки значения в "регистр
компаратора", драйвер должен прочитать значение регистра
таймера и если оно "выше" (вспомним команды ja, jae, jb, jbe
на x86) значения регистра компаратора - он сам себе должен
вызовать прерывание. Самурай бы сам себе уже сделал сеппуку
от такой жизни, но драйвер не человек - как его индус
закодировал, так он и будет работать. Что ж, надеемся что
хоть сравнение значений там будет сделано аккуратно (раза с
4-го, не раньше).

- Режим периодических прерываний. О, это совсем Песня. Можно
загрузить в регистр компаратора значение, которое будет
использовано как обычно - когда регистр счётчика дойдёт до
него, сгенерируется прерывание. Ура. А дальше - в момент
генерации прерывания - к текущему значению регистра
компаратора добавится то, которое в него записывали! И тут
же пояснение для чайников вроде меня:

- в счётчике 0
- в компаратор записали 123
- счётчик досчитал до 123, генерируется прерывание, к
компарат(ор)у прибавляется снова 123 (хранящееся в невидимом
регистре), получаем 246. Ждём пока счётчик дойдёт до него,
"намылить голову... повторить" (анекдот про программиста в
ванне). Теперь включаем голову. Если задача стояла - с
момента запуска системы запустить таймер на, например, 1000
прерываний в секунду и больше ни о чём не думать - да,
система хороша. А если надо поменять частоту (вспоминаем
драйверную модель NT)? Нам популярно разъясняют, как это
делать:

- останавливаем весь счёт (ой! а как же мы точное время
будем знать?)
- сбрасываем в 0 основной счётчик
- перепрограммируем все компараторы (потому что их работа
была завязана на значения основного счётчика, и если другой
компаратор перестанет на длительный период генерировать свои
прерывания - хорошо не будет) - операция на микросекунды
- запускаем счёт времени

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

Недостатков у периодического режима получается столько, что
лучше его не использовать. Честно-честно. Подсчитать
"вручную" (в драйвере) эти значения получится на порядок
меньше мороки.

Естественно, извечный русский вопрос напоследок - "Какую
траву они курят?" И как на любой извечный русский вопрос,
ответа не будет.

У меня всё больше создаётся впечатление, что в тенденциях
катастрофического усложнения современной вычислительной
техники основную роль давно уже несут не авторы программ
(как Microsoft), а авторы железа (как Intel). В HPET видна в
первую очередь их кошмарная лапа: стиль построения, стиль
нагромождения концепций, изначальная катастрофическая
непродуманность концепций; наворачивание уровней там, где
они не нужны в принципе, и уход от проблемы там, где она
есть. Практически все новомодные инновации Intel'а страдают
этим; великолепные примеры - ACPI, которое за 5-6 лет было
таки додавлено до состояния, когда оно "в общем работает"
(ценой включения в ОС целого драйверного стека Intel для
работы с ним; а спецификацию ACPI поймёт даже в общих
чертах, мягко говоря, только человек значительно выше
среднего по отрасли ума); EFI, которое грандиозно
планировалось на замену нынешним BIOS и за 4 года совершило
громкий пшик, дав единственную пользу - новый формат таблицы
разделов диска. HPET - ещё одно великое творение.
Чего следует ожидать от его судьбы? Наверно, того же что и у
остальных - хорошие идеи выживут, плохие - подохнут, но
если даже ребёнку понятно что идея крива, то Intel это
поймёт только года через 4 когда пройдёт 2-3 полных цикла
разработки новых поколений материнских плат:

- >99.9% производителей будут использовать только 1 блок таких таймеров; планы наличия до 8 блоков по размаху превосходят даже наполеоновские - плата с 2 блоками будет редкостью большей чем кенгуру в Балтийском море. Аналогично, больше трёх (ну ладно, четырёх) компараторов не будет.
- полностью заменять i8254+ACPI-fast никто не захочет пока Intel не начнёт исключать его из чипсетов (как Самую Вредную Компоненту, занимающую аж пару тысяч транзисторов! конечно, фантатическая затрата по сравнению с кэшами и блоками предсказания переходов) Соответственно, ОС поддержат саму возможность такой замены только через K лет (кстати, все заметили, что первый публичный выпуск этой спецификации - уже 5.5 лет назад?)
- ОС >95% компьютеров общего пользования - Microsoft Windows 2000/XP/Server2003/etc. - будут игнорировать кривые возможности периодического режима. Сделать одно сложение и одну запись в память легче, чем перепрограммировать все компараторы...
- Intel будет в N+1-й раз проклята любителями ассемблера. Ей не привыкать, но на этот раз это будет 100% поделом.

И это не исправит даже жестокий пятый прокуратор Иудеи
всадник Понтий Пилат.

UPDATE: ну конечно! linux/arch/i386/kernel/time_hpet.c:

/*
* Some systems seems to need two writes to HPET_T0_CMP,
* to get interrupts working
*/
hpet_writel(tick, HPET_T0_CMP);
hpet_writel(tick, HPET_T0_CMP);

А ведь это всё то же интеловское хозяйство.

Previous post Next post
Up