Измерение временных промежутков

Предыдущая  Содержание  Следующая V*D*V

Ядро следит за течением времени с помощью таймера прерываний. Прерывания подробно описаны в Главе 10.

 

Прерывания таймера генерируются через постоянные интервалы системным аппаратным таймером; этот интервал программируется во время загрузки ядром в соответствии со значением HZ, которое является архитектурно-зависимой величиной, определённой в <linux/param.h> или файле подплатформы, подключаемом им. Значения по умолчанию в распространяемых исходных текстах ядра имеют диапазон от 50 до 1200 тиков в секунду на реальном оборудовании, снижаясь до 24 в программных эмуляторах. Большинство платформ работают на 100 или 1000 прерываний в секунду; значением по умолчанию для популярных ПК x86 является 1000, хотя в предыдущих версиях (вплоть до 2.4) оно было 100. По общему правилу, даже если вы знаете значение HZ, никогда не следует рассчитывать на определённое значение при программировании.

 

Те, кто хотят систему с другой частотой прерываний, могут изменить значение HZ. Если вы изменяете HZ в заголовочном файле, вам необходимо перекомпилировать ядро и все модули с новым значением. Вы можете захотеть увеличить HZ для получения более высокого разрешения в асинхронных задачах, если вы готовы платить накладные расходы от дополнительных прерываний таймера для достижения ваших целей. Действительно, повышение HZ до 1000 было довольно распространено для промышленных систем x86, использующих ядро версии 2.4 или 2.2. Однако, для текущих версий лучшим подходом к прерыванию таймера является сохранение значения по умолчанию для HZ, в силу нашего полного доверия разработчикам ядра, которые, несомненно, выбрали лучшее значение. Кроме того, некоторые внутренние расчёты в настоящее время осуществляются только для HZ в диапазоне от 12 до 1535 (смотрите <linux/timex.h> и RFC-1589).

 

Значение внутреннего счётчика ядра увеличивается каждый раз, когда происходит прерывание от таймера. Счётчик инициализируется 0 при загрузке системы, поэтому он представляет собой число тиков системных часов после последней загрузки. Счётчик является 64-х разрядной переменной (даже на 32-х разрядных архитектурах) и называется jiffies_64. Однако, авторы драйверов обычно используют переменную jiffies типа unsigned long, которая то же самое, что и jiffies_64 или её младшие биты. Использование jiffies, как правило, предпочтительнее, поскольку это быстрее, и доступ к 64-х разрядному значению jiffies_64 не обязательно является атомарным на всех архитектурах.

 

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

Использование счётчика тиков

Счётчик и специальные функции для его чтения живут в <linux/jiffies.h>, хотя вы обычно будете просто подключать <linux/sched.h>, который автоматически подключает jiffies.h. Излишне говорить, что jiffies и jiffies_64 должны рассматриваться как только читаемые.

 

Всякий раз, когда ваш код должен запомнить текущее значение jiffies, он может просто обратится к переменной unsigned long, которая объявлена как volatile (нестабильная), чтобы компилятор не оптимизировал чтения памяти. Вам необходимо прочитать текущий счётчик, когда вашему коду необходимо рассчитать будущий момент времени, как показано в следующем примере:

 

#include <linux/jiffies.h>

unsigned long j, stamp_1, stamp_half, stamp_n;

 

j = jiffies; /* читаем текущее значение */

stamp_1 = j + HZ; /* позже на 1 секунду */

stamp_half = j + HZ/2; /* пол-секунды */

stamp_n = j + n * HZ / 1000; /* n миллисекунд */

 

Этот код не имеет никаких проблем с переполнением jiffies до тех пор, пока различные значения сравниваются правильным способом. Хотя на 32-х разрядных платформах счётчик переполняется только один раз в 50 дней при HZ равном 1000, ваш код должен быть подготовлен к встрече этого события. Для сравнения вашего закэшированного значения (например, вышеприведённого stamp_1) и текущего значения, вы должны использовать один из следующих макросов:

 

#include <linux/jiffies.h>

int time_after(unsigned long a, unsigned long b);

int time_before(unsigned long a, unsigned long b);

int time_after_eq(unsigned long a, unsigned long b);

int time_before_eq(unsigned long a, unsigned long b);

 

Первый возвращает истину, когда a, как копия jiffies, представляет собой время после b, второй возвращает истину, когда время a перед временем b, а последние два сравнивают как "позже или равно" и "до или равно". Код работает преобразуя значения в signed long, вычитая их и проверяя результат. Если вам необходимо узнать разницу между двумя значениями jiffies безопасным способом, можно использовать тот же прием: diff = (long)t2 - (long)t1;.

 

Можно конвертировать разницу значений jiffies в миллисекунды простым способом:

 

msec = diff * 1000 / HZ;

 

Иногда, однако, необходимо обмениваться представлением времени с программами пользовательского пространства, которые, как правило, предоставляют значения времени структурами timeval и timespec. Эти две структуры предоставляют точное значение времени двумя числами: секунды и микросекунды используются в старой и популярной структуре timeval, а в новой структуре timespec используются секунды и наносекунды. Ядро экспортирует четыре вспомогательные функции для преобразования значений времени в jiffies в и из этих структур:

 

#include <linux/time.h>

 

unsigned long timespec_to_jiffies(struct timespec *value);

void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);

unsigned long timeval_to_jiffies(struct timeval *value);

void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);

 

Доступ к 64-х разрядному счётчику тиков не так прост, как доступ к jiffies. В то время, как на 64-х разрядных архитектурах эти две переменные являются фактически одной, доступ к значению для 32-х разрядных процессоров не атомарен. Это означает, что вы можете прочитать неправильное значение, если обе половинки переменной обновляются, пока вы читаете их. Вряд ли вам когда-нибудь понадобится прочитать 64-х разрядный счётчик, но в этом случае вы будете рады узнать, что ядро экспортирует специальную вспомогательную функцию, которая делает для вас правильное блокирование:

 

#include <linux/jiffies.h>

u64 get_jiffies_64(void);

 

В приведённом выше прототипе используется тип u64. Это один из типов, определённых в <linux/types.h>, он обсуждается в Главе 11 и представляет собой беззнаковый 64-х разрядный тип.

 

Если вам интересно, как 32-х разрядные платформы обновляют в одно и то же время 32-х разрядный и 64-х разрядный счётчик, почитайте скрипт компоновщика для вашей платформы (найдите файл, имя которого соответствует vmlinux*.lds*). Там символ jiffies определён для доступа к младшему слову 64-х разрядного значения, в зависимости от платформы используется прямой или обратный порядок битов (little-endian или big-endian). Собственно, тот же приём используется для 64-х разрядных платформ, так что переменные unsigned long и u64 доступны по одному адресу.

 

Наконец, отметим, что фактическая тактовая частота почти полностью скрыта от пользовательского пространства. Макрос HZ всегда преобразуется в 100, когда программы пользовательского пространства подключают param.h, и каждый счётчик, передаваемый в пользовательское пространство, преобразуется соответственно. Этот применимо к clock(3), times(2) и любой соответствующей функции. Единственным доказательством для пользователя значения HZ является то, насколько быстро происходят прерывания таймера, это показывается в /proc/interrupts. Например, вы можете получить HZ путём деления этого счётчика на время работы системы, сообщаемое /proc/uptime.

Процессорно-зависимые регистры

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

 

В современных процессорах острому спросу на эмпирические цифры производительности мешает собственная непредсказуемость времени выполнения в большинстве типов процессоров из-за кэш памяти, "планирования" инструкций и предсказания переходов. В ответ производители процессоров предоставили способ расчёта тактов, как простой и надёжный способ измерения временных интервалов. Таким образом, большинство современных процессоров имеет регистр счётчика, который постоянно увеличивается один раз в каждом такте. В настоящее время этот счётчик времени является единственным надёжным способом для выполнения задач, требующих хронометража с высоким разрешением.

 

Детали отличаются от платформы к платформе: регистр может быть или не быть читаемым из пространства пользователя, он может или не может быть доступен для записи и он может быть 64 или 32 бита. В последнем случае, вы должны быть готовы к обработке переполнения так же, как мы это делали со счётчиком тиков. Регистр может даже не существовать для вашей платформы, или он может быть реализован во внешнем устройстве разработчиком аппаратуры, если процессор не имеет этой функции и вы имеете дело с компьютером специального назначения.

 

Независимо от возможности обнуления этого регистра, мы настоятельно не рекомендуем его обнулять, даже когда аппаратура позволяет это. Вы, в конце концов, можете не быть в любой момент времени единственным пользователем счётчика; на некоторых платформах, поддерживающих SMP, например, ядро зависит от такого счётчика для синхронизации между процессорами. Поскольку вы всегда можете измерить разницу между значениями, пока эта разница не превышает время переполнения, вы можете выполнить работу, не претендуя на исключительное право собственности регистром, изменяя его текущее значение.

 

Наиболее известным регистром счётчика является TSC (timestamp counter, счётчик временных меток), введённый в x86 процессоры, начиная с Pentium и с тех пор присутствует во всех конструкциях процессора, включая платформу x86_64. Это 64-х разрядный регистр, который считает тактовые циклы процессора; он может быть прочитан и из пространства ядра и из пользовательского пространства.

 

После подключения <asm/msr.h> (заголовок для x86, имя которого означает “machine-specific registers”, "машинно-зависимые регистры"), вы можете использовать один из этих макросов:

 

rdtsc(low32,high32);

rdtscl(low32);

rdtscll(var64);

 

Первый макрос атомарно читает 64-х разрядное значение в две 32-х разрядные переменные; следующий макрос (“read low half”, "чтение младшей половины") читает младшую половину регистра в 32-х разрядную переменную, отбрасывая старшую половину; последний читает 64-х разрядное значение в переменную long long, отсюда и имя. Все эти макросы хранят значения в своих аргументах.

 

Чтение младшей половины счётчика достаточно для наиболее распространённых применений TSC. Процессор 1 ГГц переполняет его только каждые 4.2 секунды, так что вам не придётся иметь дело с многорегистровыми переменными, если промежуток времени, который вы измеряете, гарантированно занимает меньше времени. Однако, частоты процессоров растут с течением времени, так же как и увеличиваются требования к измерению времени, скорее всего, в будущем всё чаще будет необходимо читать 64-х разрядный счётчик.

 

В качестве примера, следующие строки непосредственно измеряют время выполнения инструкции используя только младшую половину регистра:

 

unsigned long ini, end;

rdtscl(ini); rdtscl(end);

printk("time lapse: %li\n", end - ini);

 

Некоторые другие платформы предлагают аналогичную функциональную возможность и заголовки ядра предлагают архитектурно-независимую функцию, которую можно использовать вместо rdtsc. Она называется get_cycles, определена в <asm/timex.h> (подключаемого с помощью <linux/timex.h>). Её прототипом является:

 

#include <linux/timex.h>

cycles_t get_cycles(void);

 

Эта функция определена для каждой платформы и она всегда возвращает 0 на платформах, которые не имеют регистра счётчика циклов. Тип cycles_t является соответствующим беззнаковым типом для хранения считанного значения.

 

Несмотря на наличие архитектурно-независимой функции, мы хотели бы получить благоприятную возможность показать пример встраивания ассемблерного кода. С этой целью мы реализуем функцию rdtscl для процессоров MIPS, которая работает так же, как и аналогичная для x86.

 

Мы основываем этот пример на MIPS, потому что большинство процессоров MIPS имеют 32-х разрядный счётчик как регистр 9 их внутреннего "сопроцессора 0". Для доступа к этому регистру, читаемого только из пространства ядра, вы можете определить следующий макрос, который выполняет ассемблерную инструкцию “move from coprocessor 0” ("переместить от сопроцессора 0"): (* завершающая инструкция nop необходима для защиты, чтобы компилятор не обратился к целевому регистру в инструкции сразу же после mfc0. Этот тип блокировки является типичным для процессоров RISC и компилятор всё ещё может наметить полезные инструкции в слотах задержки. В этом случае мы используем nop, потому что встраиваемый ассемблер является чёрным ящиком для компилятора и может быть выполнен без оптимизации.)

 

#define rdtscl(dest) \

__asm__ __volatile__("mfc0 %0,$9; nop" : "=r" (dest))

 

Вместе с этим макросом MIPS процессор может выполнять тот же код, показанный ранее для x86.

 

Во встраиваемом ассемблере gcc распределение регистров общего назначения остаётся компилятору. Макрос просто указывает использовать 0% для размещения "аргумента 0", который позднее указан как "любой регистр (r), используемый в качестве выходного (=)". Этот макрос также заявляет, что выходной регистр должен соответствовать выражению dest языка Си. Синтаксис для встраиваемого ассемблера является очень мощным, но довольно сложным, особенно для архитектур, имеющих ограничения на то, что может делать каждый регистр (в частности, семейство x86). Синтаксис описан в документации gcc, обычно предоставляемой в дереве документации info.

 

Короткий фрагмент кода Си, показанный в этом разделе, был запущен на x86-процессоре класса K7 и MIPS VR4181 (с помощью только что описанного макроса). Первый сообщил о промежутке времени в 11 тактов, а второй только о 2 тактах. Небольшая цифра была ожидаема, поскольку обычно RISC процессоры выполняют одну инструкцию за такт.

 

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

Предыдущая  Содержание  Следующая