Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Внутренние механизмы ядра Вперед

Временные задержки

Обеспечение заданной паузы в выполнении программного кода — это вторая из обсуждавшихся ранее классов задач из области работы со временем. Она уже не так проста, как задача измерения времени и имеет больше разнообразных вариантов реализации, это связано ещё и с тем, что требуемая величина обеспечиваемой паузы может быть в очень широком диапазоне: от миллисекунд и ниже, для обеспечения корректной работы оборудования и протоколов (например, обнаружение конца фрейма в протоколе Modbus), и до десятков часов при реализации работы по расписанию — размах до 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):

pdelay.c :

	#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;
	};

Активные задержки:

$ 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() происходит асинхронно относительно шкалы системных тиков, и мог бы прийтись ровно перед очередным системным тиком, и тогда выдержка в один тик была бы «зачтена» потенциально нулевому интервалу;
  • следующий, второй тик пропускается именно из-за того, что величина периода системного тика чуть меньше миллисекунды (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

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

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

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

Предыдущий раздел: Оглавление Следующий раздел:
Абсолютное время   Таймеры ядра