Содержание


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

Comments

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

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

Тем не менее, в ядре абсолютное UTC время (время эпохи UNIX — отсчитываемое от 1 января 1970г.) хранится как:

struct timespec xtime;

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

#include <linux/time.h>
struct timespec {
   time_t tv_sec; /* секунды */
   long tv_nsec;  /* наносекунды */
}
...
struct timeval {
   time_t       tv_sec;  /* секунды */
   suseconds_t  tv_usec; /* микросекунды */
};
...
#define NSEC_PER_USEC   1000L             
#define USEC_PER_SEC    1000000L
#define NSEC_PER_SEC    1000000000L

В виду неатомарности xtime, его нельзя напрямую использовать в коде, но существует некоторый набор API ядра для преобразования хронологического времени в одну из форм представления и обратно:

  • превращение хронологического времени в значение единиц jiffies:
    #include <linux/time.h>
    unsigned long mktime( unsigned int year, unsigned int mon, unsigned int day,
                          unsigned int hour, unsigned int min, unsigned int sec );
  • текущее время с разрешением до тика:
    #include <linux/time.h>
    struct timespec current_kernel_time( void );
  • текущее время с разрешением меньше тика (если аппаратная платформа поддерживает такую возможность):
    #include <linux/time.h>
    void do_gettimeofday( struct timeval *tv );

Временные задержки выполнения

Обеспечение заданной паузы при выполнении программного кода — это вторая из обсуждавшихся ранее классов задач из области работы со временем. Она уже не так проста, как задача измерения времени, и имеет больше различных вариантов реализации. Сложность подобной задачи связана и с тем, что требуемая продолжительность паузы может находиться в очень широком диапазоне: от миллисекунд для обеспечения корректной работы оборудования и протоколов до десятков часов при реализации работы по расписанию — размах до 6-7 порядков величины.

Основное требование к функции временной задержки сформулировано в стандарте POSIX, в его расширении реального времени POSIX 1003.b: заказанная временная задержка может быть при выполнении сколь угодно более продолжительной, но не может быть ни на какую величину и не при каких условиях — короче. Но это условие не так легко выполнить!

Реализация временной задержки может выполняться одним из двух способов: активное ожидание и пассивное ожидание (блокирование процесса). Активное ожидание осуществляется выполнением процессором «пустых» циклов на протяжении установленного интервала, а пассивное — переводом потока выполнения в блокированное состояние. Существует предубеждение, что активное ожидание — это менее эффективный и даже менее профессиональный подход, в отличии от пассивного ожидания. Тем не менее всё определяется конкретным контекстом использования, и каждый подход имеет свои сильные и слабые стороны в разных ситуациях. Например, любой переход в блокированное состояние — это очень трудоёмкая операция со стороны системы (переключения контекста, смена адресного пространства и множество других действий), и реализация коротких пауз способом активного ожидания может оказаться эффективнее (как мы и увидим при рассмотрении примитивов синхронизации: семафоры и спинблокировки). Кроме того, в ядре во многих ситуациях (в контексте прерывания и, в частности, в таймерных функциях) просто запрещено переходить в блокированное состояние (это закончится крахом всей системы!).

Активное ожидание может быть реализовано теми же механизмами, что и измерение временных интервалов. Например, это может быть код, основанный на шкале системных тиков, как показано ниже:

/* вычисляется значение тиков для окончания задержки */
unsigned long j1 = jiffies + delay * HZ; 
while ( time_before( jiffies, j1 ) )
   cpu_relax();

где:

  • time_before()— рассмотренный ранее макрос, вычисляющий разницу 2-х значений с учётом возможных переполнений;
  • cpu_relax()— макрос, говорящий, что процессор ничем не занят, и в гипер-триэдинговых системах могущий (в некоторой степени) занять процессор ещё чем-то;

В конечном счёте возможна и такая запись активного ожидания:

while ( time_before( jiffies, j1 ) );

Для коротких задержек в файле <linux/delay.h> определены несколько функций активного ожидания с прототипами:

void ndelay( unsigned long nanoseconds );
void udelay( unsigned long microseconds );
void mdelay( unsigned long milliseconds );

Эти же функции определены и как макросы:

#ifndef mdelay
#define mdelay(n) (                             \
{                                               \
        static int warned=0;                    \
        unsigned long __ms=(n);                 \
        WARN_ON(in_irq() && !(warned++));       \
        while (__ms--) udelay(1000);            \
})
#endif
#ifndef ndelay
#define ndelay(x)       udelay(((x)+999)/1000)
#endif

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

#include <linux/sched.h>
while( time_before( jiffies, j1 ) ) {
   schedule();
}

В пассивное ожидание можно переключиться функцией:

#include <linux/sched.h>
signed long schedule_timeout( signed long timeout );

где timeout— число тиков для задержки. Эта функция возвращает значение 0, если возврат произошёл перед истечением указанного времени ожидания (в ответ на сигнал). Функция schedule_timeout()требует, чтобы до её вызова было установлено текущее состояние процесса, допускающее прерывание сигналом, поэтому её типичный вызов выглядит следующим образом:

set_current_state( TASK_INTERRUPTIBLE );
schedule_timeout( delay );

В файле <linux/delay.h> определено несколько функций для выполнения задержки, не использующих активное ожидание:

void msleep( unsigned int milliseconds );
unsigned long msleep_interruptible( unsigned int milliseconds );
void ssleep( unsigned int seconds );

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

Рассмотрим разницу между реализацией задержи с помощью активного и пассивного ожидания. Так как различия между этими подходами проявляются одинаково и в ядре и в пользовательском процессе, то наши примеры будут организованы на основе выполнения процесса пространства пользователя. Полный код примеров можно найти в архиве time.tgz в разделе "Материалы для скачивания".

Листинг 1. Процесс, использующий пассивное / активное ожидание
#include "libdiag.h"

int main( int argc, char *argv[] ) {
   long dl_nsec[] = { 10000, 100000, 200000, 300000, 500000, 
                      1000000, 1500000, 2000000, 5000000 };
   int c, i, j, bSync = 0, bActive = 0, cycles = 1000,
       rep = sizeof( dl_nsec ) / sizeof( dl_nsec[ 0 ] );
   while( ( c = getopt( argc, argv, "astn:r:" ) ) != EOF ) 
      switch( c ) { 
         case 'a': bActive = 1; break; 
         case 's': bSync = 1; break; 
         case 't': set_rt(); break; 
         case 'n': cycles = atoi( optarg ); break;
         case 'r': if( atoi( optarg ) > 0 && atoi( optarg ) < rep ) 
                      rep = atoi( optarg ); break;
         default:
            printf( "usage: %s [-a] [-s] [-n cycles] [-r repeats]\n", argv[ 0 ] );
            return EXIT_SUCCESS;
   }
   char *title[] = { "passive", "active" };
   printf( "%d cycles %s delay [millisec. == tick !] :\n", cycles,
           ( bActive == 0 ? title[ 0 ] : title[ 1 ] ) );
   unsigned long prs = proc_hz();
   printf( "processor speed: %d hz\n", prs );
   long cali = calibr( 1000 );
   for( j = 0; j < rep; j++ ) {
      const struct timespec sreq = { 0, dl_nsec[ j ] }; // наносекунды для timespec
      long long rb, ra, ri = 0;
      if( bSync != 0 ) nanosleep( &sreq, NULL );
      if( bActive == 0 ) {
         for( i = 0; i < cycles; i++ ) {
            rb = rdtsc();
            nanosleep( &sreq, NULL );
            ra = rdtsc();
            ri += ( ra - rb ) - cali;
         }
      }
      else {
         long long wpr = (long long) ( ( (double) dl_nsec[ j ] ) / 1e9 * prs );
         for( i = 0; i < cycles; i++ ) {
            rb = rdtsc() + cali;
            while( ( ra = rdtsc() ) - rb < wpr  ) {}
            ri +=  ra - rb;
         }
      }
      double del = ( (double)ri ) / ( (double)prs );
      printf( "set %5.3f => was %5.3f\n", 
              ( ( (double)dl_nsec[ j ] ) / 1e9 ) * 1e3, del * 1e3 / cycles );
   }
   return EXIT_SUCCESS;
};

Запустим наш процесс с опцией имитации активных задержек (опция -a) продолжительностью 100 тиков:

$ sudo nice -n-19 ./pdelay -n 1000 -a
1000 cycles active delay [millisec. == tick !] :
processor speed: 1662485585 hz
set 0.010 => was 0.010
set 0.100 => was 0.100
set 0.200 => was 0.200
set 0.300 => was 0.300
set 0.500 => was 0.500
set 1.000 => was 1.000
set 1.500 => was 1.500
set 2.000 => was 2.000
set 5.000 => was 5.000

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

$ uname -r
2.6.18-92.el5
$ sudo nice -n-19 ./pdelay -n 1000
1000 cycles passive delay [millisec. == tick !] :
processor speed: 534544852 hz
set 0.010 => was 1.996
set 0.100 => was 1.999
set 0.200 => was 1.997
set 0.300 => was 1.998
set 0.500 => was 1.999
set 1.000 => was 2.718
set 1.500 => was 2.998
set 2.000 => was 3.889
set 5.000 => was 6.981

Хотя цифры при малых задержках и могут показаться неожиданными, они объяснимы и совпадают с результатами, наблюдаемыми в других POSIX операционных системах. Увеличение задержки на два системных тика (3 миллисекунды при заказе 1-й миллисекунды) нисколько не противоречит упоминавшемуся требованию стандарта POSIX 1003.b (и даже сделано для его обеспечения) и объясняется следующим:

  • период первого тика после вызова не может «идти в зачёт» выдержки времени, потому как вызов nanosleep() происходит асинхронно относительно шкалы системных тиков, и мог бы прийтись ровно перед очередным системным тиком, и тогда выдержка в один тик была бы «зачтена» потенциально нулевому интервалу (интервал до 1-го тика всегда пропускается);
  • следующий, второй тик пропускается именно из-за того, что величина периода системного тика чуть меньше миллисекунды (0.999847мс, как это обсуждалось выше), и вот этот остаток «чуть» и приводит к ожиданию ещё одного очередного, не исчерпанного тика.

Более необъяснимыми (хотя и более ожидаемыми по житейской логике) будут результаты, полученные на новых архитектурах и ядрах:

$ uname -r
2.6.32.9-70.fc12.i686.PAE
$ sudo nice -n-19 ./pdelay -n 1000
1000 cycles passive delay [millisec. == tick !] :
processor speed: 1662485496 hz
set 0.010 => was 0.090
set 0.100 => was 0.182
set 0.200 => was 0.272
set 0.300 => was 0.370
set 0.500 => was 0.571
set 1.000 => was 1.075
set 1.500 => was 1.575
set 2.000 => was 2.074
set 5.000 => was 5.079

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

В любом случае, мы можем сделать несколько заключений из полученных результатов:

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

Заключение

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


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


Похожие темы


Комментарии

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

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