Термин "операционная система реального времени" в последние годы стал употребляемым с такой лёгкостью, что уже приобретает некоторый оттенок неприличия. Свою операционную систему как "реал-тайм" не пропагандирует только крайне ленивый. При подготовке этого материала я "ради смеха" задал для поиска "операционная система реального времени" поисковой системе www.yandex.ru: 7867 страниц на "не менее чем" на 233 серверах.
Тем не менее, технические требования к системам реального времени "в строгом смысле" достаточно хорошо исследованы. Вот некоторые формулировки, которые я позаимствовал из разных источников:
"Система должна обеспечивать гарантированный отклик в гарантированное время" [1].
"Система должна обладать способностью к параллельной обработке нескольких событий. Если несколько событий наступают одновременно, система должна успеть обработать все события в соответствующих каждому событию временных рамках, независимо от количества событий, порядка их поступления и соотношения их временных рамок" [1].
"ОС должна поддерживать вытесняющую многопоточность (preemptive multi-threading) и мультипроцессорные архитектуры" [4].
"Поведение самой ОС после системных вызовов и наступления событий должно быть предсказуемо и известно заранее. Это означает, что разработчики ОС должны специфицировать такие временные характеристики, как "задержка обработки прерывания" (interrupt latency), максимальное время маскировки прерываний а также максимальное время исполнения всех системных вызовов" [4].
Показатели и требования систем реального времени - это сама по себе огромная и неисчерпаемая тема для рассмотрения, и мы не станем сейчас ею заниматься. Я привёл эти формулировки для того, чтобы стало понятнее: само понятие "реального времени" и его критерии оценки - как на фундаменте зиждется на службах времени операционной системы: измерении и задании временных интервалов системы в шкале текущего времени. Ведь нельзя рассуждать о сущностях, которые с необходимой достоверностью не умеешь измерять!
Собственно в этих заметках мы и займёмся изучением того, какие способы программного задания, измерения и контроля временных интервалов доступны в операционной системе QNX, и какие точности и дискретности измерения временных параметров можно ожидать в QNX.
Фиксация временных интервалов (тайм-ауты, тайм-ауты ядра, интервальные таймеры и др.) и хронометраж выполнения участков кода для OS "жёсткого реального времени" QNX RTP на порядок более критичны, чем для универсальных OS общего применения (например, все системы семейства Windows, а все рассуждения об Windows NT и производных от неё как системе реального времени, я бы склонен был рассматривать как не очень удачный каламбур).
Для детальное понимание функционирования служб времени в QNX требуется тщательно рассмотреть:
Задание любых временных интервалов с использованием функций sleep(), usleep(), nanosleep(), delay(), alarm(), select(), nanospin(), sigaction() - я сознательно привожу максимально полный перечень "временных" функций, как подсказку по их использованию;
Все механизмы и функции, связанные с созданием и использованием интервальных таймеров: все функции группы timer_*();
Измерение временных характеристик выполнения (хронометрирование) критических участков программного кода - clock(), time(), times() и др.;
Модель временной шкалы QNX. Модель и поведение службы времени QNX наиболее полно, пожалуй, изложена Р.Кёртеном в [3], но степень детализации изложения - определённо недостаточна.
Итак, что мы имеем "в предпосылках":
Микроядро QNX RTP "живёт" в дискретной "сетке" времени;
Каждый единичный момент времени для микроядра - это такт или "тик" системного времени;
Какие либо изменения состояний времени фиксируются микроядром только в узлах этой дискретной шкалы времени;
Микроядро "не различает" (не разрешает) два временных события, если они происходят между соседними "тиками" системного времени (с интервалом, естественно, меньшим, чем интервал системного тика).
Собственно, всё достаточно естественно и разумно: прикладывая линейку для измерения длины, мы имеем "дискретную сетку" с разрешением в 1мм, и в утверждениях типа "длина составляет 13.55мм" вот эти ".55" - это "от лукавого", и мы должны говорить либо о 13-ти, либо о 14-ти миллиметрах.
Начальная (после загрузки системы) длительность тика определяется соглашениями, принятыми для той или иной версии RTP. Из документации следует, что для RTP 6.1 при частоте процессора >40МГц тик составляет 1мс, и 10мс - при меньших частотах процессора. Значение тика для RTP 6.1 программно может быть переустановлено вызовом ClockPeriod(), но не ниже 500мксек. (всё дальнейшее рассмотрение проводится для 6.1 с процессором выше 40Мгц). Для RTP 6.2 (последняя стабильная версия) анонсировано минимальное значение системного тика в 10мксек, и скоро мы увидим, что это действительно так.
В архитектуре PC x86 часы реального времени работают от задающего кварцевого генератора, сигнал от которого далее поступает на делитель. Из-за различий в 5-6 знаке (коэффициент деления должен быть целым числом) реально период тика чуть меньше 1мсек (для значения, установленного по умолчанию, или чуть меньше переустанавливаемого значения). Точное значение времени тика можем получить так:
_clockperiod clcold;
ClockPeriod( CLOCK_REALTIME, NULL, &clcold, 0 );
cout << "ClockPeriod=" << clcold.nsec << endl;
Структура _clockperiod имеет вид:
struct _clockperiod {
uint64_t nsec, // наносекунды
fract // фемтосекунды, рассчитано на будущие развития
// и должно быть равным 0!
};
Результат (в моей системе): 999847 - это наносекунды. Почему "чуть меньше" (а не больше)? Это существенно важно: стандарт POSIX 1003.1b-1993/1003.1i-1995 (расширение POSIX для реального времени) требует, чтобы все детерминированные на временной шкале события (завершения ожидания, тайм-ауты и т.д.) наступали не раньше истечения установленного временного интервала. Т.е. позже - пожалуйста, а вот раньше - нельзя. Это тоже понятно: программный поток "уснувший" на 1сек. может и вообще не "пробудиться", если за время выдержки этого временного интервала в системе появится другой поток с более высоким приоритетом, т.е. временной интервал в 1сек. совершенно корректно может превратиться в бесконечность. Отметим пока, что POSIX накладывает ограничения только "снизу", и мы ещё неоднократно будем с этим сталкиваться.
Дальше посмотрим, как можно переустановить значение тика в системе:
const int TICK = 500000;
_clockperiod clcnew = { TICK, 0 }, clcold;
ClockPeriod( CLOCK_REAKTIME, &clcnew, &clcold, 0 ); // переустановить
ClockPeriod( CLOCK_REAKTIME, NULL, &clcold, 0 ); // проверить
cout << "ClockPeriod=" << clcold.nsec << endl;
Получаем: ClockPeriod=499504.
В документации QNX RTP [7] утверждается, что "при попытках установить значение ниже минимального предела, будет установлено стандартное системное значение". Реально картина несколько отличается, как мы это скоро увидим, что приводит к некоторым существенным последствиям. Пробуем установить TICK меньше нижней границы - 500мксек в системе QNX 6.1. Не получается, но при этом (в отличии от того, что говорит HELP) будет сохранено последнее установленное в системе значение тика (в нашем случае - 499504нсек), а не значение по умолчанию.
Внимание! После завершения вашей задачи, которая делает переустановку системного тика, все временные масштабы в системе будут изменены. По-моему, лучшее решение в этой ситуации: в любой задаче изменяющей системный тик перед завершением восстанавливать значение, сохранённое при её старте.
Теперь очень коротко о том, как мы будем измерять "грязное" хронологическое время с высокой точностью в экспериментах. Для этого мы можем использовать аппаратный счётчик циклов процессора x86. Тактовую частоту используемого процессора получаем так:
#include
#include
uint64_t cps = SYSPAGE_ENTRY( qtime )->cycles_per_sec;
Дальше "обкладываем" интересующий нас фрагмент "засечками", их разница - полное астрономическое время выполнения фрагмента в системе:
uint64_t bg = ClockCycles();
// ... делаем что-то
uint64_t fn = ClockCycles();
cout << "Draft time in seconds = "
<< (float)( fn - bg ) / cps << endl;
Таким способом тактовая частота используемого процессора устанавливается с точностью до 6-7 знаков, а измерения астрономического времени производится с чрезвычайно высокой точностью (мною использовался процессор 233Мгц, что соответствует ~4нсек). Измеренное таким образом астрономическое время - "грязное", его никаким образом невозможно связать с временным интервалом, выделенным этому процессу системой. Мы будем использовать эту шкалу как датчик точного времени.
Напишем программу, которая будет в качестве параметра командной строки принимать численное значение (в наносекундах) и устанавливать его в качестве значения системного тика (я исключил из текста для его сокращения все директивы #include):
#include "Consts.h"
int main( int argc, char *argv[] ) {
long T = 1000000;
if( argc > 1 && atol( argv[ 1 ] ) > 0 ) T = atol( argv[ 1 ] );
_clockperiod clcnew = { T, 0 }, clcold, clcout;
ClockPeriod( CLOCK_REALTIME, &clcnew, &clcold, 0 );
ClockPeriod( CLOCK_REALTIME, NULL, &clcout, 0 );
uint64_t cps = SYSPAGE_ENTRY( qtime )->cycles_per_sec;
cout << "ClockPeriod: old=" << clcold.nsec
<< " new=" << clcout.nsec << endl
<< "ClockPerSec=" << CLOCKS_PER_SEC << " "
<< "Processor: " << cps << " cycles/sec." << flush << endl;
return EXIT_SUCCESS;
};
Эта программа использует переопределённую операцию вывода в поток 64-битовых целочисленных значений, которая в файле Consts.h определена так:
ostream& operator <<( ostream&, uint64_t );
которую вы без труда сможете воссоздать при необходимости (или использовать printf), поэтому её исходный текст не приводится. Испытаем нашу программу:
/root # tick 100000
ClockPeriod: old=999847 new=99733
ClockPerSec=1000000 Processor: 233274700 cycles/sec.
Всё прекрасно. А теперь вот так:
/root # tick 10000
...но не тут-то было. Программа tick не завершается (не выводит своего сообщения), а операционная система глухо "виснет" (и это в высшей степени устойчивая QNX, которую я ещё никогда не наблюдал в столь "глухом" состоянии: ничего, кроме RESET не помогает!). Что происходит? Могу только предположить (достоверно могли бы сказать только разработчики системы): на каждый временной тик система обязана "обойти" все выполняющиеся в системе программные потоки и модифицировать их временные отметки, кроме того, она обязана проделать то же самое со всеми созданными в программах таймерами. Что происходит, если вся совокупность этих операций требует для своего выполнения времени большего, чем системный тик? Правильно, создаются вложенные (рекурсивные) обработчики прерываний от системного таймера, число незавершённых экземпляров обработчиков лавинно нарастает, дальше Е крах системы (здесь уже не важна детальная причина: то ли стек переполняется, то ли полностью исчерпались ресурсы системы). Если это так, то вблизи критических значений системного тика полезная производительность операционной системы должна очень резко падать - все вычислительные ресурсы задействованы на обновление временных отметок. Проверяем:
/root # tick 50000
ClockPeriod: old=999847 new=49447
ClockPerSec=1000000 Processor: 233262800 cycles/sec.
(Интересно, обратите внимание, на изменение измеренного значения тактовой частоты процессора - это измерение от предыдущего отделяет несколько часов. Это лишний раз говорит о той "тщательности", с которой работает техника её измерения в QNX). Смотрим, что произошло с системой. Для этого запустим PhAB (не потому, что собираемся строить новый программный проект, а потому, что нам просто нужно приложение, активно нагружающее процессор в момент старта). Приложение ведёт себя как Е те "горячие эстонские парни": размеренно и протяжно. Смена окон Photon требует нескольких секунд, а индикатор System Monitor при этих операциях "зашкаливает" под предел. Восстанавливаем значение тика до 500000, и работа графической системы приходит в норму. Определим то минимальное значение тика (для данного экземпляра процессора, а это - P233MMX) при котором система ещё сохраняет работоспособность. Это - 47000 (наносекунд), при чём, это значение не зависит, определяем ли мы его в графической системе Photon, либо в "голой" текстовой консоли. Интересно проследить, как эта нижняя граница зависит от типа процессора. Celeron 533 на MicroStar BXMaster - 10000, без заметного на глаз снижения производительности! Ниже значение не может быть установлено (помните, выше я описывал как система препятствует установке системного тика ниже граничной величины, в 6.2 10 микросекунд и есть этой границей). Проделываем то же на P166: при 12000 система сохраняет работоспособность без, по крайней мере, катастрофической потери производительности, при 11000 - ещё "шевелится", при 10000 - "висит". Стройная система разрушилась! Первое предположение состояло в том, что критическое значение тика выявляет зависимость от способа установки QNX (в FAT32 или в отдельный QNX раздел диска: моя система на P233MMX установлена в раздел FAT32, а на P166 - в QNX раздел диска. Файловые операции в том и другом случае, при прочих равных условиях отличаются по скорости в 2-2.5 раза - это проверялось раньше). Но достаточно объёмные эксперименты не подтвердили такой зависимости. Очевидно, такая, несколько странная, зависимость объясняется сложной совокупностью процессора, системной платы и RAM компьютера, на котором производятся измерения.
Движемся далее. На более детальное (и экспериментальное) изучение организации и поведения службы времени QNX меня, в заметной мере, подвигла статья Brian Stecher [2], представленная разработчиком OS QNX фирмой QSSL. С одной стороны, она вносила определённую ясность в предмет, а с другой - вызывала подсознательное ощущение некого внутреннего противоречия, в ней содержащегося. Автор приводит фрагмент программного кода:
void OneSecondPause() {
for ( i=0; i<1000; i++ ) delay( 1 );
}
И спрашивает: "Выполнится ли этот код за одну секунду?". И отвечает:
- К сожалению (здесь я не понял - почему это должно вызывать сожаление), нет, этот код не вернет управление через одну секунду на IBM PC. Скорее всего, он выполнится за три секунды. А почему это функция выполняется ровно за три секунды? Так как мы вызываем delay() асинхронно с прерыванием системного таймера, это означает, что мы должны добавить один такт, чтобы убедиться, что мы отмеряем время корректно (представьте себе, что мы этого не делаем, и задержка в один такт была запрошена до прерывания таймера). Обычно это добавляет половину миллисекунды каждый раз, но в приведенном примере мы должны синхронизироваться с таймером каждый раз, что приводит к миллисекундной задержке каждый раз. Это занимает 2 секунды, откуда берется еще одна? Это проблема возникает из-за того, что когда мы запрашиваем задержку в одну миллисекунду, мы получаем задержку меньше - это зависит от системного таймера. На IBM PC длительность одного такта 999,847 наносекунд. В итоге каждый раз delay (1) в действительности ожидает: 999847ns * 3 = 2999541ns
Фрагмент Brian Stecher действительно выполняется строго 3 секунды - можете поверить. Но для уточнения деталей происходящего, сделаем ещё один тест:
// время будем мерять процессорными тактами 233Мгц:
static uint64_t cps = SYSPAGE_ENTRY( qtime )->cycles_per_sec;
int main( int argc, char *argv[] ) {
if( argc > 1 ) {
// можем менять приоритет задачи из командной строки
int n = atoi( argv[ 1 ] );
if( n >= 0 ) setprio( 0, n );
};
// набор задержек:
const unsigned long TT[] = { 100, 300, 500, 1000 };
const int CYCLE = 1000, STEP = 5;
cout << "Current priority=" << getprio( 0 ) << endl;
uint64_t cycle1 = ClockCycles(), cycle2;
for( int k = 0; k < sizeof( TT ) / sizeof( *TT ); k++ ) {
float summa = 0., delta;
// гистограмма по временам срабатывания задержки
// (с шагом гистограммы по 200мксек при STEP = 5):
int nHist[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
for( int i = 0; i < CYCLE; i++ ) {
cycle1 = ClockCycles();
usleep( TT[ k ] ); // вот она, родёмая, задержка ...
cycle2 = ClockCycles();
summa += ( delta = float( cycle2 - cycle1 ) );
int ind = (int)floor( delta / cps * 1000. * STEP );
nHist[ ind ] +=1; // разнесение по гистограмме
}
cout << "Mean time for usleep(" << TT[ k ] << ") = "
<< summa / cps / CYCLE * 1000. << " msec."
<< " : " << endl;
for( int i = 0; i < sizeof( nHist ) / sizeof( *nHist ); i++ ) {
cout << nHist[ i ];
if( i != sizeof( nHist ) / sizeof( *nHist ) - 1 )
cout << "|";
else cout << endl;
}
}
return EXIT_SUCCESS;
};
А теперь смотрим ... (задержки в скобках указаны в микросекудах, значение системного тика - по умолчанию 999847 наносекунд, т.е. чуть меньше миллисекунды, производим несколько последовательных запусков с различными приоритетами задачи):
Current priority=1
Mean time for usleep(100) = 2.05821 msec. :
0|0|0|0|0|5|12|1|1|888|73|0|0|9|3|0|0|0|0|0|0|0|0|0|0|0|0|0|2|0
Mean time for usleep(301) = 2.04473 msec. :
0|0|0|0|0|0|14|4|1|897|64|0|1|8|1|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0
Mean time for usleep(500) = 2.03649 msec. :
0|0|0|0|0|2|14|2|1|894|68|0|0|9|1|0|0|0|0|0|0|0|0|0|0|0|0|1|2|0
Mean time for usleep(1000) = 3.07173 msec. :
0|0|0|0|0|0|0|0|0|0|4|8|3|2|902|65|0|0|3|0|0|0|0|0|0|0|0|0|0|0
Current priority=10
Mean time for usleep(100) = 1.99678 msec. :
0|0|0|0|0|0|1|3|1|898|90|1|2|0|1|0|0|2|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(300) = 1.99235 msec. :
0|0|0|0|0|0|1|4|0|893|97|1|2|0|0|0|1|0|0|0|0|0|0|0|0|0|0|1|0|0
Mean time for usleep(500) = 1.99039 msec. :
0|0|0|0|0|0|1|2|1|919|74|1|1|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0
Mean time for usleep(1000) = 2.99171 msec. :
0|0|0|0|0|0|0|0|0|0|1|3|3|1|881|104|2|2|1|0|0|0|1|0|0|0|0|0|0|0
Current priority=25
Mean time for usleep(100) = 1.98879 msec. :
0|0|0|0|0|0|0|1|0|974|25|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(300) = 1.98753 msec. :
0|1|0|0|0|0|0|0|0|966|33|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(500) = 1.98764 msec. :
0|1|0|0|0|0|0|0|0|972|27|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(1000) = 2.9869 msec. :
0|0|0|0|0|0|1|0|0|0|0|0|0|0|981|18|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Current priority=63
Mean time for usleep(100) = 1.98873 msec. :
0|0|0|0|0|0|0|0|1|993|6|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(300) = 1.98759 msec. :
0|1|0|0|0|0|0|0|0|984|15|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(500) = 1.98743 msec. :
0|1|0|0|0|0|0|0|0|990|9|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Mean time for usleep(1000) = 2.98663 msec. :
0|0|0|0|0|0|1|0|0|0|0|0|0|0|982|17|0|0|0|0|0|0|0|0|0|0|0|0|0|0
Интересно? Мне - очень! При определении всякой задержки (с тайм-аутами, нужно полагать, то же - нет никаких оснований использовать при этом другой механизм) производится следующее:
в счётчик таймера заносится значение заказанной временной задержки плюс значение системного тика (это - для удовлетворения требования POSIX относительно предотвращения преждевременного срабатывания таймера);
при каждом системном тике значение счётчика таймера декрементируется на величину системного тика;
если значение счётчика таймера становится после этого отрицательным (или "0" тоже?), то таймер считается сработавшим, и фиксируется событие, приводящее к перепланированию микроядром активности потоков в системе.
Что бросается в глаза? Во-первых, хорошо видно как возрастает чёткость перепланирования микроядром текущего потока при возрастании его приоритета (значения времён активизации потока после срабатывания таймера всё более концентрируются вокруг одного значения). Во-вторых (последняя строка), то что даже при максимальном приоритете (63), когда подавляющее большинство активизаций потока (982) приходится на 3-й системный тик, существует "хвост" (17) более поздних активизаций потока, т.е. даже в этом случае поток "не выбирает" 100% процессорного времени. Но самое интересное - это вот те "1", которые стоят в "ранних" интервалах гистограммы, но к ним мы вернёмся чуть позже.
И ещё один эксперимент, с паузами usleep(...), для последовательности задержек в 100, 300, 500, 999 и 1000 микросекунд (вывод сделан несколько модифицированной программой, которая разделителем "." отделяет соседние 0.2 миллисекундные участки гистограммы, а "|" - границы, соответствующие целым значениям миллисекунд).
Current priority=25
Histogramm (step=0.2msec.):
Mean time for usleep(100) = 1.98846 msec. :
0.0.0.0.0|0.1.0.0.973|26.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(300) = 1.98761 msec. :
0.1.0.0.0|0.0.0.0.981|18.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(500) = 1.98765 msec. :
0.1.0.0.0|0.0.0.0.988|11.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(999) = 1.98762 msec. :
0.1.0.0.0|0.0.0.0.981|18.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(1000) = 2.98712 msec. :
0.0.0.0.0|0.1.0.0.0|0.0.0.0.969|30.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Т.е. usleep(999) - обеспечивает реальную задержку в 2 тика (1.999694 т.е. ~2 миллисекунды), а usleep(1000) - в 3 тика (2.999541 т.е. ~3 миллисекунды). Я подчёркиваю это не для того, чтоб сказать, что это плохо, напротив - "так есть", и при задании временных интервалов нужно отчётливо представлять картину того, что происходит.
А теперь вернёмся к тем "уродцам" - "1" в ранних интервалах гистограмм, на которые я акцентировал внимание раньше? Что это? Какие-то артефакты, связанные с ошибочным поведением программы? Я долго не мог понять этого, но картина, при случайном её характере в деталях, повторялась с завидной регулярностью. Вот то единичное срабатывание за время существенно короче остальных 999 - это и есть то единственное асинхронное (относительно последовательности системных тиков) срабатывание хронологически первого usleep(), как это и должно быть "по жизни". Все остальные - синхронизированы с последовательностью системных тиков, и синхронизацию эту мы вносим искусственно циклом "for". В циклах вида (что довольно часто случается в реальных программных фрагментах):
for( int i = 0; i < CYCLE; i++ ) {
cycle1 = ClockCycles();
usleep( TT[ k ] );
cycle2 = ClockCycles();
.......
}
шкала системных тиков:
Каждый очередной цикл завершается по системному тику, а новый цикл начинается с задержкой на время выполнение последнего ClockCycles(), т.е. практически сразу же после этого тика! Сказанное демонстрируется на графической схеме.
Текстуально изменим структуру цикла, в котором осуществляется задержка:
_clockperiod clcout;
ClockPeriod( CLOCK_REALTIME, NULL, &clcout, 0 );
....
timespec bg;
bg.tv_sec = 0; // ... на всякий случай
for( int i = 0; i < CYCLE; i++ ) {
// вносим случайную десинхронизацию в диаппазоне тика
bg.tv_nsec = (long)floor( ( (float)rand() / RAND_MAX ) *
clcout.nsec
// rand % clcout.nsec - работает плохо: некоррелированность
// случайных велечин ещё вовсе не означает некоррелированность
// их младших разрядов (зависит от типа генератора)!
nanospin( &bg ); // рассинхронизация!!!
// nanosleep( &bg, NULL ); - интересно, см.ниже!
cycle1 = ClockCycles();
usleep( TT[ k ] );
cycle2 = ClockCycles();
.......
}
И получаем следующие результаты:
Current priority=63
Histogramm (step=0.2msec.):
Mean time for usleep(100) = 1.41298 msec. :
0.0.0.0.107|205.183.191.164.150|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(300) = 1.4342 msec. :
0.0.0.0.107|189.167.188.183.166|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(500) = 1.42146 msec. :
0.0.0.0.119|189.165.187.175.165|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(999) = 1.41889 msec. :
0.0.0.1.118|179.205.181.158.153|1.0.0.1.0|1.0.1.0.1|0.0.0.0.0|0.0.0.0.0
Mean time for usleep(1000) = 2.42189 msec. :
0.0.0.0.0|0.0.0.0.135|176.175.158.195.157|0.0.0.0.2|1.0.0.1.0|0.0.0.0.0
Хорошо видно, что времена задержек "рассинхронизированы", размазаны случайным образом на интервале в 1мсек. в сторону уменьшения. Теперь можно сказать, что delay(1), вызываемая в программе асинхронно (что гораздо естественнее в реальной жизни и программах), создаёт задержку "от 2-х до 3-х тиков"!
Ещё один эффект - что рассинхронизация не получается с вызовом функции nanosleep() - см. комментарий в тексте программы. Почему интересно - потому, что её не раз вспоминает Р.Кёртен как функцию активного ожидания, не блокирующую вызывающий поток. Похоже это не так: все функции delay, sleep, usleep, nanosleep - блокирующие. В отличии от nanospin, вот об ней из HELP: "The nanospin function busy-waits for the amount of time specified in when without blocking the calling thread. ... The nanospin*() functions are designed for use with hardware that requires short time delays between accesses". Так что и по предписанию от разработчиков: для обеспечения критических для оборудования точных временных интервалов - изволь использовать неблокирующие: nanospin(), nanospin_count(), nanospin_ns() ...
И на последок: какое значение времени срабатывания таймера мы должны ожидать, заказывая исчезающе малое значение, асимптотически приближающееся к нулю (другими словами - какую минимальную задержку можно обеспечить в принципе)? Если установка временного интервала синхронизирована с началом интервала системного тика - то 2 системных тика, если не синхронизирована - то от 1-го до 2-х системных тиков
Измерение временных характеристик. Вот теперь, более или менее разобравшись с моделью временной шкалы QNX и методологией задания временных интервалов, мы можем перейти к вопросу об измерении временных характеристик в QNX. Из изложенного выше уже должно быть понятно, что:
любые временные интервалы могут быть установлены или измерены только в единицах целых значений установленного в системе тика;
любые численные значения, в которых будут фигурировать избыточное число значащих цифр, или которые будут указывать на значения, не являющиеся целым числом системных тиков - в этой части своих значащих цифр являются "мусором", не имеющим никакого осмысленного значения.
Теперь сосредоточимся на рассмотрении того, "как" и "каким инструментом" мы можем измерять временные интервалы в системе:
Способ 1. О нём уже было сказано выше - используем аппаратный счётчик циклов процессора:
#include
#include
uint64_t cps = SYSPAGE_ENTRY( qtime )->cycles_per_sec;
uint64_t bg = ClockCycles();
// ... делаем что-то
uint64_t fn = ClockCycles();
cout << "Draft time in seconds = "
<< (float)( fn - bg ) / cps << endl;
Этим способом мы можем замерять только "грязное" хронометрическое время без привязки к выполняемому процессу.
Способ 2. Использование time_t clock( void ). Дословно из HELP: "The clock() function returns the number of clock ticks of processor time used by the programm since it started executing. You can convert number of tics in seconds by dividing by the value CLOCK_PER_SEC.". Это - чистое процессорное время, диспетчированное потоку:
clock_t bg, fn;
bg = clock();
// ... делаем что-то
fn = clock();
cout << "Time in seconds = "
<< (long)( ( fn - bg ) / CLOCK_PER_SEC ) << endl;
посмотреть значение константы CLOCK_PER_SEC, установленное в системе:
CLOCK_PER_SEC = 1000000
Способ 3,. Возьмём информацию о времени исполнения процесса непосредственно из системной информации исполняемого процесса [5] (аналогичным способом мы можем "достать" и временные интервалы, относящиеся к отдельным потокам процесса):
#include
#include
// .....
pid_t nProc = getpid(); // pid текущего процесса
char sBuf[ 100 ];
sprintf( sBuf, "/proc/%d/as", nProc );
int fd = open( sBuf, O_RDONLY );
proc_info info;
// читаем proc_info:
if( devctl( fd, DCMD_PROC_INFO, &info, sizeof( info ) ) != EOK ) {
// ... ERROR ...
};
uint64_t put1, put2, pst1, pst2;
put1 = info.utime;
pst1 = info.stime;
// ... делаем что-то
// снова читаем proc_info
if( devctl( fd, DCMD_PROC_INFO, &info, sizeof( info ) ) != EOK ) {
// ... ERROR ... };
put2 = info.utime;
pst2 = info.stime;
cout << "User time: " << ( put2 - put1 )
<< " System time: " << ( pst2 - pst1 ) << endl;
В файле sys/debug.h в комментариях к структуре _debug_process_info (proc_info из sys/procfs - это просто её синоним) читаем: "utime - user running time in nsec, stime - system running time in nsec", т.е., нужно полагать, это - пользовательские и системные времена "чистого" выполнения процесса в наносекундах.
Численно эти значение очень близки к полученным в "способ 2", умноженные на 1000.
Способ 4. Используем стандартную службу времени UNIX - time(): для контроля, чтоб увязать одни цифры с другими - это "грязные" хронологические времена, так же, как в способе 1.
time_t t1, t2;
time( &t1 );
// ... делаем что-то
time( &t2 );
cout << "Difference time: " << difftime( t2, t1 ) << endl;
Здесь происходит грубое усечение хронологического времени до целых секунд из значения счётчика процессорных циклов, которое мы видели в "способ 1".
Способ 5. Используем times() - POSIX 1003.1 (это должно быть "чистое" время процесса в режимах юзера и системы):
struct tms ts1, ts2;
times( &ts1 );
// ... делаем что-то
time( &ts2 );
cout << "User time: " << ts2.tms_cutime - ts1.tms_cutime << endl
<< "System time: " << ts2.tms_cstime - ts1.tms_cstime << endl;
С использованием этого способа у меня в QNX были определённые проблемы, которые до сих пор толком не понимаю: возможно я недостаточно гибко толкую понимаемые результаты, возможно, в реализации QNX есть ошибка… В любом случае, этот вопрос должен будет получить более детальное развитие в последующих редакциях этого документа, а пока я в итоговых таблицах не включаю результаты этого способа измерений.
Способ 6. Используем периодический интервальный таймер, возбуждающий сигнал, скажем SIGUSR1 (прошу прощения за длительное цитирование кода, если кто знает, как это сделать короче - пусть покажет):
static long long ttime = 0 // это и будет счётчик времени
//создаём обработчик сигнала SIGUSR1:
static void catcher( void ) { ttime++; };
static void setSignal( void ) {
static struct sigaction act;
act.sa_handler = catcher;
sigfillset( &(act.sa_mask ) );
sigaction( SIGUSR1, &act, NULL );
};
// . . . .
// теперь подготовку завершили и в главной программе делаем таймер
struct sigevent event;
SIGEV_SIGNAL_INIT( &event, SIGSR1 );
timer_t timerid;
struct itimerspec timer;
// я хотел иметь timer на 100 мксек., что получилось - посмотрим
timer.it_value.tv.sec = 0;
timer.it_value.tv.nsec = 100000;
timer.it_interval.tv.sec = 0;
timer.it_interval.tv.nsec = 100000;
timer_create( CLOCK_REALTIME, &event, &timerid );
// привязали обработчик сигнала ... :
setSignal();
// собственно запуск таймера (flag=0 - относительный):
timer_settime( timerid, 0, &timer, NULL );
// заодно проверим установленный период тика (можно и переустановить попозже):
_clockperiod clcnew, clcold;
ClockPeriod( CLOCK_REALTIME, NULL, &clcold, 0 );
cout << "ClockPeriod=" << clcold.nsec << endl;
// . . . . . .
// а теперь, собственно тот фрагмент, который меряем:
ttime = 0;
for( int i = 0 .... )
// . . . делаем что-то
long long tim = ttime;
cout << "Timer time: " << tim << endl;
Относительно последнего фрагмента и его возможного развития и использования, нужно сделать следующее замечание: хотя классические UNIX сигналы описываются в литературе в привязке к процессам, в которых они обрабатываются, новые POSIX определения привязывают сигналы и их обработку к потокам, что делает их использование намного более универсальным.
Вот некоторые частные результаты измерения времени выполнения 3-х различных участков программного кода:
№ способа
1 17.1388 16.9859 16.9343
2 6 999 3 000 0
3 6 998 929 2 999 541 0
4 17 17 17
5 ... с этим нужно отдельно разбираться! ...
6 17 117 16 984 16 934
Неожиданные, но, по неспешному размышлению, понятные и разумные результаты, обеспечиваемые интервальным таймером: т.е. время срабатывание - "грязное", по принципу "солдат спит - служба идёт". В то время когда поток вытеснен - таймерные интервалы продолжают считаться.
Системное профилирование. Возможности и техника системного профилирования программного кода, как способа сравнительной оценки времени выполнения различных фрагментов и выявления потенциальных неэффективностей, конечно, не имеет никакого касательства к вопросам измерения временных интервалов реального времени. Это, скорее, составная часть технологии компиляции - сборки программных проектов. Тем не менее, эта техника, в ряде случаев, приходит на помощь в исследовании производительности кода, и временами делает вообще не нужным реальное хронометрирование, об определённых сложностях которого говорилось выше. Именно поэтому ниже включена короткая врезка об технике системного профилирования.
1. Посмотрим:
use QCC
options:
. . . . .
-p compile with profiling
2. Компилируем программу (назовём её XXX) с опцией -p, с ней же и линкуем. После запуска программы (точнее, после её отработки) появляется файл gmon.out.
3. Для получения листинга профайлера выполняем:
gprof XXX gmon.out > prof.lst.
Системный профайлер обрабатывает в качестве профилируемых единиц кода (по принципу "начало - конец") участки, оформленные в виде отдельных функций. Я мог бы привести здесь образец листинга профайлера (например, названный выше prof.lst), но он достаточно прост для толкования и без разъяснений.
Источники информации:
[1] - Евгений Хухлаев "Операционные системы реального времени и Windows NT", Открытые системы, №05, 1997г., http://www.osp.ru/os/1997/05/48.htm
[2] - Brian Stecher, "Концепция времени в Neutrino", перевод с английского qnx.org.ru-team: http://qnx.org.ru/docs-devel/quantization.html
[3] - Роберт Кёртен, "Введение в QNX Neutrino 2", СПБ. "Петрополис", 2001, 480 стр.
[4] - И. Н. Коваленко "Проект Neutrino: В поисках Святого Грааля", http://qnxworld.main.ru/doc/Kovalenko_nto.html
[5] - Дмитрий Алексеев, "Получение системной информации": http://qnx.org.ru/docs-devel/text_sysinfo.html
[6] - Олег Цилюрик, Дмитрий Алексеев "ОС QNX сегодня", Компьютерное Обозрение #18-19, 15 - 21 мая 2002, http://itc.ua/article.phtml?ID=9884&pid=108
[7] - Интерактивная HELP-подсистема OS QNX RTP.
На любые возникшие по материалу вопросы автор с готовностью ответит по электронной почте: Олег Цилюрик, фирма "LOT Ltd.", г. Харьков, Украина, <olej@lot.kharkov.ua>. |