Содержание


Инструменты программирования в ядре: Часть 69. Таймеры ядра

Comments

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

Абсолютное время

Ядро предоставляет драйверам API для работы с таймерами, так как в системе параллельно может существовать множество экземпляров таймеров. В файле <linux/timer.h> содержится структура и ряд функций для декларации, регистрации и удаления таймеров ядра:

struct timer_list {
   struct list_head entry;
   unsigned long expires;
   void (*function)( unsigned long );
   unsigned long data;
...
};

void init_timer( struct timer_list *timer );
struct timer_list TIMER_INITIALIZER( _function, _expires, _data );
void add_timer( struct timer_list *timer );
void mod_timer( struct timer_list *timer, unsigned long expires );
int del_timer( struct timer_list *timer );

где:

  • expires— значение jiffies, наступления которого таймер ожидает для срабатывания (абсолютное время);
  • function— указатель на функцию, которая вызывается при срабатывании с data в качестве аргумента;
  • data— этот параметр чаще всего содержит преобразованный указатель на структуру.

Функция таймера в ядре выполняется в контексте прерывания, если конкретно то в контексте обработчика прерывания системного таймера, но не в контексте процесса! Что накладывает на неё дополнительные ограничения:

  1. для данной функции запрещён доступ к пользовательскому пространству, так как из-за отсутствия контекста процесса, невозможно определить путь к пользовательскому пространству, связанному с любым определённым процессом;
  2. указатель current не имеет смысла и не может быть использован, так как соответствующий код не имеет связи с процессом, который был прерван;
  3. не может быть выполнен переход в блокированное состояние и переключение контекста.

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

Код ядра может понять, работает ли он в контексте прерывания, используя макрос in_interrupt().

Таймеры высокого разрешения

Таймеры высокого разрешения появились, начиная с версии ядра 2.6.16, а структуры представления времени для них определены в файле <linux/ktime.h>. Их поддержка присутствует только в архитектурах, поддерживающих аппаратные таймеры высокого разрешения. Также для них определяется новый временной тип данных ktime_t— временной интервал в наносекундном выражении, и его представления сильно отличаются в зависимости от архитектуры. Здесь же определяются множество функций для установки значений и преобразований представлений времени (многие из них определены как макросы, но ниже показаны как прототипы):

ktime_t ktime_set(const long secs, const unsigned long nsecs);
ktime_t timeval_to_ktime( struct timeval tv );
struct timeval  ktime_to_timeval( ktime_t kt );
ktime_t timespec_to_ktime( struct timespec ts );
struct timespec ktime_to_timespec( ktime_t kt );

Сами операции с таймерами высокого разрешения определены в файле <linux/hrtimer.h> в структуре, напоминающей модель таймеров реального времени, вводимую для пространства пользователя стандартом POSIX 1003b:

struct hrtimer {
...
   ktime_t   _expires;
   enum hrtimer_restart (*function)(struct hrtimer *);
...
}

Единственным определяемым пользователем полем этой структуры является функция реакции function, прототип которой приведён ниже:

enum hrtimer_restart {
   HRTIMER_NORESTART,
   HRTIMER_RESTART,
};

void hrtimer_init( struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode );
int hrtimer_start( struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode );
extern int hrtimer_cancel( struct hrtimer *timer );
...
enum hrtimer_mode {
   HRTIMER_MODE_ABS = 0x0,   /* абсолютное значение времени */
   HRTIMER_MODE_REL = 0x1,   /* относительное значение времени */
...
};

Параметр which_clock типа clockid_t, относится к области стандарта POSIX и называется в нём временным базисом (тип задатчика времени): какую шкалу времени использовать из общего числа определённых в <linux/time.h>. Ниже перечислены возможные значения, часть из которых заимствована из POSIX, а другие расширяют существующие определения.

#define CLOCK_REALTIME            0
#define CLOCK_MONOTONIC           1
#define CLOCK_PROCESS_CPUTIME_ID  2
#define CLOCK_THREAD_CPUTIME_ID   3
#define CLOCK_MONOTONIC_RAW       4
#define CLOCK_REALTIME_COARSE     5
#define CLOCK_MONOTONIC_COARSE    6
  • CLOCK_REALTIME— системные часы со всеми их плюсами и минусами. Могут быть переведены вперёд или назад, в этой шкале могут попадаться "вставные секунды", предназначенные для корректировки неточностей представления периода системного тика. Эта шкала времени чаще всего и используется в таймерах.
  • CLOCK_MONOTONIC— подобны CLOCK_REALTIME, но отличаются тем, что, представляют собой постоянно увеличивающийся счётчик, и поэтому не могут быть изменены при переводе времени. Обычно это счётчик времени от загрузки системы.
  • CLOCK_PROCESS_CPUTIME_ID— возвращает время, затраченное процессором на работу только с данным приложением в независимости от других задач системы. Именно этот базис чаще всего используется для пользовательского адресного пространства.
  • CLOCK_THREAD_CPUTIME_ID— похоже на CLOCK_PROCESS_CPUTIME_ID, но отсчитывается время, затрачиваемое только на один текущий поток.
  • CLOCK_MONOTONIC_RAW— то же что и CLOCK_MONOTONIC, но в отличии от первого не подвержен изменению через сетевой протокол точного времени NTP.

Последние два базиса CLOCK_REALTIME_COARSE и CLOCK_MONOTONIC_COARSE были добавлены в 2009 году, и, как утверждается их авторами, они могут обеспечить гранулярность шкалы мельче, чем предыдущие базисы. Работу с различными базисами времени обеспечивают в пространстве пользователя не очень известные API вида clock_*() (clock_gettext(), clock_nanosleep(), и т.д.). Разрешение каждого из базисов можно получить вызовом:

long sys_clock_getres( clockid_t which_clock, struct timespec *tp );

Для наших примеров таймеров можно использовать базисы CLOCK_REALTIME или CLOCK_MONOTONIC. В модуле htick.c представлен пример использования таймеров высокого разрешения в периодическом режиме, хотя он и не раскрывает все возможности базисов высокого расширения. Полный код примера можно найти в архиве time.tgz в разделе "Материалы для скачивания".

Листинг 1. Пример использования системных часов в модуле ядра
#include <linux/module.h>
#include <linux/version.h>
#include <linux/time.h>
#include <linux/ktime.h>
#include <linux/hrtimer.h>

static ktime_t tout;
static struct kt_data {
   struct hrtimer timer;
   ktime_t        period;
   int            numb;
} *data;

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,19)
static int ktfun( struct hrtimer *var ) {
#else
static enum hrtimer_restart ktfun( struct hrtimer *var ) {
#endif
   ktime_t now = var->base->get_time();  // текущее время в типе ktime_t
   printk( KERN_INFO "timer run #%d at jiffies=%ld\n", data->numb, jiffies );
   hrtimer_forward( var, now, tout );
   return data->numb-- > 0 ? HRTIMER_RESTART : HRTIMER_NORESTART;
}

int __init hr_init( void ) {
   enum hrtimer_mode mode;
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,19)
   mode = HRTIMER_REL;
#else
   mode = HRTIMER_MODE_REL;
#endif
   tout = ktime_set( 1, 0 );       /* 1 sec. + 0 nsec. */
   data = kmalloc( sizeof(*data), GFP_KERNEL );
   data->period = tout;
   // используем системные часы CLOCK_REALTIME
   hrtimer_init( &data->timer, CLOCK_REALTIME, mode );
   data->timer.function = ktfun;
   data->numb = 3;
   printk( KERN_INFO "timer start at jiffies=%ld\n", jiffies );...
   hrtimer_start( &data->timer, data->period, mode );
   return 0;
}

void hr_cleanup( void ) {
   hrtimer_cancel( &data->timer );
   kfree( data );
   return;
}

module_init( hr_init );
module_exit( hr_cleanup );
MODULE_LICENSE( "GPL" );

Запустим наш модуль и изучим полученные результаты:

$ sudo insmod ./htick.ko$ dmesg | tail -n5
timer start  at jiffies=10889067
timer run #3 at jiffies=10890067
timer run #2 at jiffies=10891067
timer run #1 at jiffies=10892067
timer run #0 at jiffies=10893067
$ sudo rmmod htick

Часы реального времени (RTC)

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

Убедиться в наличии такого расширения на используемой аппаратной платформе можно по присутствию интерфейса к таймеру часов реального времени в пространстве пользователя. Такой интерфейс предоставляется через функции ioctl() драйвера присутствующего в системе устройства /dev/rtc:

$ ls -l /dev/rtc*
lrwxrwxrwx 1 root root      4 Апр 25 09:52 /dev/rtc -> rtc0
crw-rw---- 1 root root 254, 0 Апр 25 09:52 /dev/rtc0

В архитектуре Intel x86 устройство этого драйвера называется Real Time Clock (RTC). RTC предоставляет функцию для работы со 114-битовым значением в NVRAM. На входе этого устройства установлен осциллятор с частотой 32768 КГц, подсоединенный к энергонезависимой батарее. Некоторые дискретные модели RTC имеют встроенные осциллятор и батарею, тогда как другие RTC встраиваются прямо в контроллер периферийной шины (например, южный мост) чипсета процессора. RTC возвращает не только время суток, но, также является и программируемым таймером, способным посылать системные прерывания (IRQ 8). Частота прерываний варьируется от 2 до 8192 Гц. Также RTC может посылать прерывания ежедневно, наподобие будильника. Все определения находятся в файле <linux/rtc.h>:

struct rtc_time {
   int tm_sec;
   int tm_min;
   int tm_hour;
   int tm_mday;
   int tm_mon;
   int tm_year;
   int tm_wday;
   int tm_yday;
   int tm_isdst;
};

Ниже перечислены некоторые важные коды команд ioctl():

#define RTC_AIE_ON    _IO( 'p', 0x01 ) /* включение прерывания alarm  */
#define RTC_AIE__OFF  _IO( 'р', 0x02 ) /* ... отключение */
...
#define RTC_PIE_ON    _IO( 'p', 0x05)   /* включение периодического прерывания */
#define RTC_PIE_OFF   _IO( 'p', 0x06)   /* ... отключение */ 
...
#define RTC_ALM_SET   _IOW( 'p', 0x07, struct rtc_time) /* установка времени time */
#define RTC_ALM_READ  _IOR( 'p', 0x08, struct rtc_time) /* чтение времени alarm */
#define RTC_RD_TIME   _IOR( 'p', 0x09, struct rtc_time) /* чтение времени RTC */
#define RTC_SET_TIME  _IOW( 'p', 0x0a, struct rtc_time) /* установка времени RTC */
#define RTC_IRQP_READ _IOR( 'p', 0x0b, unsigned long)<> /* чтение частоты IRQ */
#define RTC_IRQP_SET  _IOW( 'p', 0x0c, unsigned long)<> /* установка частоты IRQ */

В листинге 2 приведён пример использования RTC из пользовательской программы для считывания абсолютного значения времени (полный код примера можно найти в архиве time.tgz в разделе "Материалы для скачивания"):

Листинг 2. Пример использования часов реального времени из пространства пользователя (файл rtcr.c)
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/rtc.h>

int main( void ) {
   int fd, retval = 0;
   struct rtc_time tm;
   memset( &tm, 0, sizeof( struct rtc_time ) );
   fd = open( "/dev/rtc", O_RDONLY );
   if( fd < 0 ) printf( "error: %m\n" );
   retval = ioctl( fd, RTC_RD_TIME, &tm );  // чтение времени RTC
   if( retval ) printf( "error: %m\n" );
   printf( "current time: %02d:%02d:%02d\n", tm.tm_hour, tm.tm_min, tm.tm_sec );
   close( fd );
   return 0;
}

Результат запуска данного примера:

$ ./rtcr
current time: 12:58:13
$ date
Пнд Апр 25 12:58:16 UTC 2011

В ещё одном примере в листинге 3 показывается, как часы RTC могут использоваться как независимый источник времени в программе, генерирующей периодические прерывания с высокой (значительно выше системного таймера) частотой следования:

Листинг 3. Использование часов реального времени в качестве независимого источника времени (файл rtprd.c)
#include <stdio.h>
#include <linux/rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <fcntl.h>
#include <pthread.h>
#include <linux/mman.h>
#include "libdiag.h"

unsigned long ts0, worst = 0, mean = 0; // для загрузки тиков
unsigned long cali;
unsigned long long sum = 0; // для накопления суммы
int cycle = 0;

void do_work( int n ) {
   unsigned long now = rdtsc();
   now = now - ts0 - cali;
   sum += now;
   if( now > worst ) {
      worst = now; // обновить задержку для случая максимального отклонения
      cycle = n;
   }
   return;
} 

int main( int argc, char *argv[] ) {
   int fd, opt, i = 0, rep = 1000, nice = 0, freq = 8192; // freq - RTC частота - Гц
   /* установить период частоты прерываний на 8192Hz
      это должно привести к генерации прерываний каждые 122 микросекунды */
   while ( ( opt = getopt( argc, argv, "f:r:n") ) != -1 ) {
      switch( opt ) {
         case 'f' : if( atoi( optarg ) > 0 ) freq = atoi( optarg ); break;
         case 'r' : if( atoi( optarg ) > 0 ) rep = atoi( optarg ); break;
         case 'n' : nice = 1; break;
         default :
            printf( "usage: %s [-f 2**n] [-r #] [-n]\n", argv[ 0 ] );
            exit( EXIT_FAILURE );
      }
   };
   printf( "interrupt period set %.2f us\n", 1000000. / freq );
   if( 0 == nice ) {
      // информация, относящиеся к приоритету задача при планировании
      struct sched_param sched_p;
      // изменить политику планировщика на SCHED_FIFO
      sched_getparam( getpid(), &sched_p );
      sched_p.sched_priority = 50;           // RT Priority
      sched_setscheduler( getpid(), SCHED_FIFO, &sched_p );
   }
   // постараемся избежать разбиения на страницы и неопределенности
   mlockall( MCL_CURRENT );
   cali = calibr( 10000 );
   fd = open( "/dev/rtc", O_RDONLY ); // открыть часы реального времени RTC
   unsigned long long prochz = proc_hz();
   ioctl( fd, RTC_IRQP_SET, freq );
   ioctl( fd, RTC_PIE_ON, 0 );  // разрешить периодические прерывания
   while ( i++ < rep ) {
      unsigned long data;
      ts0 = rdtsc();
      // блокировать до следующего периодического прерывания
      read( fd, &data, sizeof(unsigned long) ); 
      // выполнять периодическую работу ... измерять латентность
      do_work( i ); 
   } 
   ioctl( fd, RTC_PIE_OFF, 0 ); // запретить периодические прерывания
   printf( "worst latency was %.2f us (on cycle %d)\n", tick2us( prochz, worst ),
           cycle );
   printf( "mean latency was %.2f us\n",
           tick2us( prochz, sum / rep ) );
   exit( EXIT_SUCCESS );
}

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

Запустим данный пример и рассмотрим полученные результаты:

$ sudo ./rtprd
interrupt period set 122.07 us
worst latency was 266.27 us (on cycle 2)
mean latency was 121.93 us
$ sudo ./rtprd -f16384
interrupt period set 61.04 us
worst latency was 133.27 us (on cycle 2)
mean latency was 60.79 us
$ sudo ./rtprd -f16384 -n
interrupt period set 61.04 us
worst latency was 8717.90 us (on cycle 491)
mean latency was 79.45 us

Код пользовательского процесса, представленный в листинге 3, проясняет, как и на каких интервалах могут использоваться часы реального времени. Точный способ, используемый для считывания RTC-времени в ядре, не скрывается никакими обёртками и зависит от использованного оборудования RTC. Для наиболее используемого чипа Motorola 146818 (который в таком наименовании уже не производится и заменяется аналогами) соответствующие макросы и другую информацию можно найти в файле <asm-generic/rtc.h>:

spin_lock_irq( &rtc_lock ) ;
rtc_tm->tm_sec = CMOS_READ( RTC_SECONDS ) ;
rtc_tm->tm_min = CMOS_READ( RTC_MINUTES );
rtc_tm->tm_hour = CMOS_READ( RTC_HOURS );
...
spin_unlock_irq( &rtc_lock );

А определения, необходимые для понимания происходящего, находятся в файле <linux/mc146818rtc.h>:

#define RTC_SECONDS 0
#define RTC_SECONDS_ALARM 1
#define RTC_MINUTES2
...
#define CMOS_READ(addr) ({ \
   outb_p( (addr), RTC_PORT(0) ); \
   inb_p( RTC_PORT(1) ); \
})
#define RTC_PORT(x)     (0x70 + (x))
#define RTC_IRQ 8

Это значит, что в порт 0х70 записывается номер требуемого параметра, а по порту 0х71 считывается/записывается требуемое значение — так традиционно организуется обмен данными в памяти CMOS.

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

Заключение

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


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


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=938040
ArticleTitle=Инструменты программирования в ядре: Часть 69. Таймеры ядра
publish-date=07212013