Инструменты программирования в ядре: Часть 67. Служба времени: измерение интервалов

Comments

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

Пассивное измерение временных интервалов

Если вам необходимо измерить прошедший временной интервал в шкале системного таймера, то вся измерительная процедура реализуется простейшим образом:

   u32 j1, j2;
   j1 = jiffies;                    // начало измеряемого интервала
...                                 // выполнение действий
   j2 = jiffies;                    // завершение измеряемого интервала
   int interval = ( j2 — j1 ) / HZ; // интервал в секундах

Подобный подход используется в первом примере — модуле interv. Этот модуль замеряет интервал времени, в течении которого он был загружен в ядро (полный код модуля можно найти в архиве time.tgz в разделе "Материалы для скачивания").

Листинг 1. Пример измерения временного интервала с помощью системного таймера
#include <linux/module.h>
#include <linux/jiffies.h>
#include <linux/types.h>

static u32 j;

static int __init init( void ) {
   j = jiffies;
   printk( KERN_INFO "module: jiffies on start = %X\n", j );
   return 0;
}

void cleanup( void ) {
   static u32 j1;
   j1 = jiffies;
   printk( KERN_INFO "module: jiffies on finish = %X\n", j1 );
   j = j1 - j;
   printk( KERN_INFO "module: interval of life = %d\n", j / HZ );
   return;
}

module_init( init );
module_exit( cleanup );

Вот результат выполнения такого модуля, при этом обратите внимание на соответствие временных интервалов, замеренных в пространстве пользователя (командным интерпретатором) -16 секунд и интервальным измерением в ядре — 15 секунд:

$ date; sudo insmod ./interv.ko
Сбт Июл 23 23:18:45 EEST 2011
$ date; sudo rmmod interv
Сбт Июл 23 23:19:01 EEST 2011
$ dmesg | tail -n 50 | grep module:
module: jiffies on start = 131D080
module: jiffies on finish = 1320CCD
module: interval of life = 15

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

При последовательных считываниях счётчика jiffies может быть зафиксировано его переполнение (32 бит значение). Чтобы избежать этой ситуации, ядро предоставляет четыре однотипных макроса для сравнения двух значений счетчика импульсов таймера, которые корректно обрабатывают переполнение счетчиков. Они определены в файле <linux/jiffies.h> следующим образом:

#define time_after( unknown, known ) ( (long)(known) - (long)(unknown) < 0 )
#define time_before( unknown, known ) ( (long)(unknown) - (long)(known) < 0 )
#define time_after_eq( unknown, known ) ( (long)(unknown) - (long)(known) >= 0 )
#define time_before_eq( unknown, known ) ( (long)(known) - (long)(unknown) >= 0 )
  • unknown— это обычно значение переменной jiffies;
  • known— значение, с которым его необходимо сравнить.

Макросы возвращают значение true, если выполняются соотношения моментов времени unknown и known, в противном случае возвращается значение false.

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

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 осуществляется сложнее, чем доступ к jiffies. В то время, как на 64-х разрядных архитектурах эти две переменные являются фактически одной, то доступ к значению jiffies_64 для 32-х разрядных процессоров не атомарный. Это означает, что можно считать неправильное значение, если обе половинки переменной обновляются в момент чтения. Ядро экспортирует специальную вспомогательную функцию, выполняющую правильное чтение с блокировкой:

u64 get_jiffies_64( void );

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

Наиболее известным примером такого регистра-счётчика является TSC (timestamp counter), введённый в x86 процессоры, начиная с Pentium и с тех пор присутствующий во всех последующих моделях процессоров этого семейства, включая платформу x86_64. Это 64-х разрядный регистр, считывающий тактовые циклы процессора, к которому можно обращаться и из пространства ядра и из пользовательского пространства. После подключения файла <asm/msr.h> (заголовок для x86, означающий machine-specific registers), можно использовать один из макросов:

  • rdtsc( low32, high32 )— атомарно читает 64-х разрядное значение в две 32-х разрядные переменные;
  • rdtscl( low32 )— (чтение младшей половины) читает младшую половину регистра в 32-х разрядную переменную, отбрасывая старшую половину;
  • rdtscll( var64 )— читает 64-х разрядное значение в переменную long long;

Пример использования таких макросов:

unsigned long ini, end;
rdtscl( ini );
/* здесь выполняется какое-то действие */
rdtscl( end );
printk( "time was: %li\n", end — ini );

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

#include <linux/timex.h>
cycles_t get_cycles( void );

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

Примечание: Нулевое значение, возвращаемое get_cycles() на платформах, не предоставляющих соответствующей реализации, позволяет обеспечить переносимость грамотно написанного кода между аппаратными платформами (там, где это есть, используется get_cycles(), а там, где этой возможности нет, тот же код реализуется, опираясь на последовательность системных тиков). Подобный подход реализован в нескольких различных местах системы Linux.

Для наблюдения эффектов измерений в службе времени рассмотрим тестовое приложение из архива time.tgz, которое для подобного анализа может быть реализовано и как процесс в пространстве пользователя, так как наблюдаемые эффекты будут аналогичны:

Листинг 1. Простейшая программа clock для отслеживания времени.
#include "libdiag.h"

int main( int argc, char *argv[] ) {
   printf( "%016llX\n", rdtsc() );
   printf( "%016llX\n", rdtsc() );
   printf( "%016llX\n", rdtsc() );
   printf( "%d\n", proc_hz() );
   return EXIT_SUCCESS;
};

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

Функция rdtsc содержит «ручную» (для наглядности, на инлайновой ассемблерной вставке) реализацию счётчика процессорных циклов rdtsc() для пользовательского пространства, выполняющую те же функции, что и вызовы в ядре rdtsc(), rdtscl(), rdtscll() или get_cycles().

Листинг 2. Функция rdtsc с реализующая счётчика системных циклов
#include "libdiag.h"

unsigned long long rdtsc( void ) {
   unsigned long long int x;
   asm volatile ( "rdtsc" : "=A" (x) );
   return x;
}

Функция calibr производит калибровку затрат (процессорных тактов) на само выполнение вызова rdtsc(), путем выполнения непосредственно следующих друг за другом двух вызовов rdtsc(). Для дополнительного снижения погрешностей это значение усредняется по циклу.

Листинг 3. Функция calibr для калибровки процесса измерений
#include "libdiag.h"

#define NUMB 10
unsigned calibr( int rep ) {
   uint32_t n, m, sum = 0;
   n = m = ( rep >= 0 ? NUMB : rep );
   while( n-- ) {
      uint64_t cf, cs;
      cf = rdtsc();
      cs = rdtsc();
      sum += (uint32_t)( cs - cf );
   }
   return sum / m;
}

Функция proc_hz измеряет рабочую частоту процессора (число процессорных тактов за секундный интервал):

Листинг 4. Функция proc_hz.c
#include "libdiag.h"

unsigned long proc_hz( void ) {
   time_t t1, t2;
   uint64_t cf, cs;
   time( &t1 );
   while( t1 == time( &t2 ) ) cf  = rdtsc();
   while( t2 == time( &t1 ) ) cs  = rdtsc();
   return (unsigned long)( cs - cf - calibr( 1000 ) );
}

Функция set_rt переводит поток в режим диспетчеризации реального времени (в частности, на FIFO дисциплину), что рекомендуется сделать при любых измерениях временных интервалов:

Листинг 5. Функция set_rt.c
void set_rt( void ) { 
   struct sched_param sched_p;           // информация о приоритете планирования
   sched_getparam( getpid(), &sched_p ); // изменяем политику планировщика на  SCHED_FIFO
   sched_p.sched_priority = 50;          // приоритет реального времени
   sched_setscheduler( getpid(), SCHED_FIFO, &sched_p ); 
}

Выполним этот тест на компьютерах x86 самой разной архитектуры (1, 2, 4 ядра), времени его изготовления, производительности и версий ядра, так как подобное тестирование имеет смысл при максимально широкой тестовой базе:

$ cat /proc/cpuinfo
processor       : 0
...
model name      : Celeron (Coppermine)
cpu MHz         : 534.569
...
$ ./clock
0000005E00E366B5
0000005E00E887B8
0000005E00EC3F15
534551251
$ cat /proc/cpuinfo
processor	: 0
...
model name	: Genuine Intel(R) CPU           T2300  @ 1.66GHz
cpu MHz		: 1000.000
processor	: 1
...
model name	: Genuine Intel(R) CPU           T2300  @ 1.66GHz
cpu MHz		: 1000.000
$ ./clock
00001D4AAD8FBD34
00001D4AAD920562
00001D4AAD923BD6
1662497985
$ cat /proc/cpuinfo
processor	: 0
model name	: Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz
cpu MHz		: 1998.000
...
processor	: 1 
model name	: Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz
cpu MHz		: 2331.000
...
processor	: 2 
model name	: Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz
cpu MHz		: 1998.000
...
processor	: 3 
model name	: Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz 
cpu MHz		: 1998.000 
$ ./clock 
000000000E98F3BB 
000000000E9A75E8 
000000000E9A925F 
2320044881

Выполнение подобного тестирования на различном оборудовании — чрезвычайно полезное и любопытное занятие, но мы не станем сейчас останавливаться на детальном анализе полученных результатов, а отметим только высокую точность совпадения независимых измерений, и то, что rdtsc() (или обратная величина частоты) измеряет, собственно, не частоту работы процессора (или какого-то отдельно взятого процессора в SMP системе), а тактирующую частоту процессоров в системе.

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

Листинг 6. Модуль tick.c для вывода некоторых характеристик службы времени
#include <linux/module.h>
#include <linux/jiffies.h>
#include <linux/types.h>     

static int __init hello_init( void ) {
   unsigned long j;
   u64 i;
   j = jiffies;
   printk( KERN_INFO "jiffies = %lX\n", j );
   printk( KERN_INFO "HZ value = %d\n", HZ );
   i = get_jiffies_64();
   printk( "jiffies 64-bit = %016llX\n", i );
   return -1;
}
module_init( hello_init );

И запустим его:

$ sudo /sbin/insmod ./tick.ko
insmod: error inserting './tick.ko': -1 Operation not permitted
$ dmesg | tail -n3
jiffies = 24AB3A0
HZ value = 1000
jiffies 64-bit = 00000001024AB3A0

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=937882
ArticleTitle=Инструменты программирования в ядре: Часть 67. Служба времени: измерение интервалов
publish-date=07192013