Всегда ли вы можете быть уверены, что от момента вызова функции delay() до возврата из нее пройдет ровно 20 миллисекунд?
Никогда.
На это есть две серьезные причины. Первая причина довольно проста: при блокировании поток изымается из очереди готовности. Это означает, что процессор может перейти к другому потоку вашего приоритета. Когда ваши 20 миллисекунд истекут, ваш поток будет помещен в конец очереди готовности по этому приоритету и будет таким образом оставлен на милость потока, выполняющегося в данный момент. Это относится также к обработчикам прерываний и к потокам более высокого приоритета — то, что ваш поток перешел в состояние READY, еще не означает, что ему сразу предоставят процессор.
Вторая причина несколько более хитрая. Чтобы понять ее смысл, посмотрите на нижеприведенный рисунок.
Флуктуации отсчета времени.
Проблема здесь состоит в том, что ваш запрос является асинхронным по отношению к источнику отсчетов. У вас нет никакой возможности синхронизировать аппаратный таймер с вашим запросом. Поэтому в итоге вы получите интервал задержки где-то в диапазоне от 20 до 30 мс — в зависимости от того, в какой момент между отсчетами аппаратных часов возник ваш запрос.
Это очень важный момент. Флуктуации отсчета времени — одна из печальных жизненных реалий. Единственный способ избавиться от этой проблемы заключается в увеличении разрешающей способности так, чтобы получающиеся погрешности укладывались в пределы установленных допусков. (Как это делается, мы рассмотрим ниже, в разделе «Опрос и установка часов реального времени».). Имейте в виду, что флуктуации проявляются только на первом отсчете таймера — задержка на 100 секунд, реализуемая с помощью таймера с разрешением в 10 мс, попадет в интервал между 100 и 100.01 секундами.
Таймер, работу которого мы только что обсудили, называют относительным таймером. Для такого таймера период ожидания задается относительно текущего времени. Если бы вы пожелали задержать выполнение вашего потока до 12 часов 4 минут 33 секунд EDT (Eastern Daylight Time — восточное поясное время — прим. ред.) 20 января 2005 года, вам пришлось бы сначала рассчитать точное число секунд от «сейчас» до выбранного вами момента и включить относительный таймер с задержкой на это число секунд. Поскольку это довольно часто встречающаяся операция, в QNX/Neutrino реализованы абсолютные таймеры, которые обеспечивают задержку до заданного времени (а не на заданное время, как в случае относительного таймера).
А что если вы захотите сделать что-нибудь полезное, пока поток ожидает наступления установленной даты? Или делать что-либо и получать «синхроимпульс» каждые 27 секунд? Здесь нельзя просто так позволить себе спать!
Как мы уже обсуждали в главе «Процессы и потоки», вы можете просто запустить другой поток, и пусть он выполняет работу, пока ваш поток спит. Однако, поскольку мы говорим сейчас о таймерах, посмотрим, как это можно сделать другим способом.
В зависимости от выбранной цели, вы можете сделать это с помощью либо периодического, либо однократного таймера. Периодический таймер — это таймер, который срабатывает периодически, уведомляя поток (снова и снова), что истек некоторый временной интервал. Однократный таймер — это таймер, который срабатывает только один раз.
Реализация этих таймеров в ядре основана на том же самом принципе, что и в случае с таймером задержки из нашего первого примера. Ядро запоминает абсолютное значение времени (если вы укажете сделать именно так) и хранит его. Обработчик прерываний таймера сравнивает сохраненное значение времени с текущим.
Однако, вместо удаления из очереди на выполнение после системного вызова, на этот раз ваш поток продолжит работу. И в момент, когда суточное время достигнет заданного вами и хранимого в памяти момента времени, ядро уведомит ваш поток о том, что назначенное время пришло.
Как получить уведомление о тайм-ауте? При использовании таймера задержки вы получаете уведомление просто посредством возвращения в состояние READY.
При использовании периодически и однократных таймеров у вас появляется выбор:
• послать импульс;
• послать сигнал:
• создать поток.
Импульсы мы уже обсуждали главе «Обмен сообщениями»; сигналы — стандартный для UNIX механизм. Здесь же мы кратко рассмотрим уведомления при помощи создания потока.
Как заполнять структуру struct sigevent
Независимо от выбранной вами схемы уведомления, вам обязательно придется заполнять структуру struct sigevent. Давайте вкратце посмотрим, как это делается.
struct sigevent {
int sigev_notify;
union {
int sigev_signo;
int sigev_coid;
int sigev_id;
void (*sigev_notify_function)(union sigval);
};
union sigval sigev_value;
union {
struct {
short sigev_code;
short sigev_priority;
};
pthread_attr_t *sigev_notify_attributes;
};
};
Обратите внимание, что в приведенной декларации используются неименованные объединения и структуры. Внимательное изучение файла заголовка покажет вам, как этот трюк проходит с компиляторами, не поддерживающими такую особенность. По существу там есть директива #define, которая заставляет именованные объединения и структуры выглядеть неименованными. Подробнее см. <sys/siginfo.h>.
Первое поле, которое вы должны заполнить, — это элемент sigev_notify, который определяет выбранный вами тип уведомления:
SIGEV_PULSE
Будет передан импульс.
SIGEV_SIGNAL, SIGEV_ SIGNAL _CODE или SIGEV_SIGNAL_THREAD
Будет передан сигнал.
SIGEV_UNBLOCK
В данном случае не используется; предназначен для тайм-аутов ядра (см. ниже в разделе «Тайм-ауты ядра»).
SIGEV_INTR
В данном случае не используется; предназначен для прерываний (см. главу «Прерывания»),
SIGEV_THREAD
Будет создан поток.
Поскольку мы намерены использовать структуру struct sigevent для таймеров, нас будут интересовать только такие значения sigev_notify как SIGEV_PULSE, SIGEV_SIGNAL* и SIGNAL_THREAD; остальные мы рассмотрим в соответствующих их применению разделах.
Уведомление при помощи импульса
Чтобы передать импульс при срабатывании таймера, присвойте полю sigev_notify значение SIGEV_PULSE и обеспечьте немного дополнительной информации:
Поле Значение и смысл
sigev_coid Идентификатор соединения (connection ID), по каналу которого которому будет передан импульс.
sigev_value 32-разрядное значение (данные импульса — см. параграф «Что внутри импульса?», глава «Обмен сообщениями» —
прим. ред.), которое будет передано по заданному полем
sigev_coid соединению.
sigev_code 8-разрядное значение (код импульса — см. параграф «Что внутри импульса?», глава «Обмен сообщениями» —
прим. ред.), которое будет передано по заданному полем
sigev_coid соединению.
sigev_priority Приоритет доставки импульса. Нулевое значение не допускается — слишком уж много людей пострадало от переключения на нулевой приоритет после получения импульса, а поскольку на этом приоритете приходится конкурировать за процессор со спецпроцессом IDLE, много процессорного времени там точно не светит :-).
Отметим, что sigev_coid может описывать соединение на любом канале (обычно, хотя и не обязательно, этот канал связан с процессом, который инициирует событие).
Уведомление при помощи сигнала
Чтобы передать сигнал, укажите в поле sigev_notify одно из нижеперечисленных значений:
SIGEV_SIGNAL
Процессу будет передан обычный сигнал.
SIGEV_SIGNAL_CODE
Процессу будет передан сигнал, содержащий 8-битный код.
SIGEV_SIGNAL_THREAD
Сигнал, содержащий 8-битный код, будет передан определенному потоку.
При выборе уведомления типа SIGEV_SIGNAL* нужно будет заполнить ряд дополнительных полей:
Поле Значение и смысл
sigev_signo Номер сигнала для передачи (берется из
<signal.h>, например, SIGALRM).
sigev_code 8-разрядный код (для уведомления типа SIGEV_SIGNAL_CODE или SIGEV_SIGNAL_THREAD). Уведомление созданием потока
Для создания потока по срабатыванию таймера установите поле sigev_notify в значение SIGEV_THREAD и заполните следующие поля: