Библиотека сайта rus-linux.net
Цилюрик О.И. Модули ядра 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 реального времени.
Предыдущий раздел: | Оглавление | Следующий раздел: |
Абсолютное время | Таймеры ядра |