Для представления интервалов времени в POSIX системах существуют два типа данных.
Некоторые системные вызовы используют тип struct timeval
, определенный следующим образом:
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
Тип suseconds_t
- это 32-битный знаковый тип, достаточный для представления числа микросекунд
в секунде (от 0 до 999999). Если поле tv_usec
содержит значение, выходящее из этого интервала,
системные вызовы вернут ошибку EINVAL
.
Некоторые системные вызовы используют тип struct timespec
, определенный следующим образом:
struct timespec
{
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Поле tv_nsec
должно содержать значение в интервале от 0 до 999999999.
При использовании этих структур секундная часть требуемого интервала времени записывается в поле tv_sec
,
а остаток меньший секунды - в поле tv_usec
или tv_nsec
.
В любом случае в приложениях, выполняющихся не с приоритетом реального времени, не следуюет ожидать точности измерения или реакции выше, чем частота системного таймера, с которой вызывается планировщик процессов. Частота системного таймера составляет от 100 до 1000 Гц, причем может меняться со временем. Кроме того, система может переводиться в спящий режим, в котором системный таймер может быть приостановлен.
Во многих ситуациях удобнее всего хранить в программе и обрабатывать время, представленное как
число миллисекунд от условной точки начала отсчета (например, от эпохи Unix). Для хранения такого времени
достаточно 64-битной целой знаковой переменной. При использовании системных вызовов, требующих
структуры struct timespec
или struct timeval
интервал времени в миллисекундах очевидным образом
пересчитывается в требуемые значения полей структур.
Текущее астрономическое время можно получить с помощью функции gettimeofday
:
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
Текущее астрономическое время сохраняется по указателю tv
. Не стоит ожидать, что возвращено будет время с точностью до микросекунды.
Точность измерения времени будет не выше, чем частота системного таймера.
В данном разделе рассматривается интерфейс таймеров, реализованный с помощью системного вызова setitimer
.
Несмотря на то, что он стандартный для POSIX систем, в современной версии стандарта он считается устаревшим (deprecated).
Таймер может настраиваться на однократное или периодическое срабатывание. Гарантируется, что таймер не сработает раньше истечения требуемого интервала времени, но может сработать позже, если система сильно загружена.
Каждому процессу доступны три независимых таймера: таймер реального времени (ITIMER_REAL
), таймер виртуального времени (ITIMER_VIRTUAL
),
таймер профилирования (ITIMER_PROF
).
Таймер реального времени (ITIMER_REAL
) измеряет интервалы астрономического времени. По истечению таймера в процесс
посылается сигнал SIGALRM
.
Таймер виртуального времени (ITIMER_VIRTUAL
) измеряет интервалы процессорного времени, когда процесс работает в пользовательском
режиме. По истечению таймера в процесс посылается сигнал SIGVTALRM
.
Профилировочный таймер (ITIMER_PROF
) измеряет интервалы процессорного времени, когда процесс работает и в пользовательском
режиме, и в режиме ядра. По истечению таймера в процесс посылается сигнал SIGPROF
.
Для установки таймера используется функция setitimer
.
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
Тип struct itimerval
определен следующим образом:
struct itimerval
{
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
Параметр which
задает тип таймера (ITIMER_REAL
, ITIMER_VIRTUAL
или ITIMER_PROF
). Параметр new_value
задает новое значение таймера, а в параметр old_value
возвращается текущее значение.
Чтобы установить таймер на однократное срабатывание, в поле it_value
нужно записать интервал времени до срабатывания,
а поле it_interval
- обнулить. Для установки таймера на периодическое срабатывание в поля it_value
и
it_interval
нужно записать период срабатывания таймера. Для сброса таймера (его остановки)
нужно обнулить поля it_interval
и it_value
.
Например, пусть переменная timeout
хранит интервал времени в миллисекундах до срабатывания таймера.
Тогда таймер реального времени может быть настроен следующим образом:
int64_t timeout;
// ...
struct itimerval to = {}; // структура инициализирована нулями
to.it_value.tv_sec = timeout / 1000;
to.it_value.tv_usec = (timeout % 1000) * 1000;
setitimer(ITIMER_REAL, &to, NULL);
При использовании механизма signalfd можно использовать ввод-вывод с файловыми дескрипторами для уведомлении о поступлении сигнала срабатывания таймера.
Но по аналогии с signalfd в Linux реализованы системные вызовы, которые позволяют работать с
таймерами средствами работы с файловыми дескрипторами. В отличие от механизма itimer
процесс может создать несколько таймеров одного типа. С другой стороны, при использовании
механизма timerfd
доступен только таймер реального времени.
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
Системный вызов timerfd_create
возвращает файловый дескриптор для работы с таймерами.
Параметр clockid
может быть равен CLOCK_REALTIME
для работы с часами реального времени,
для которых не гарантируется монотонность, и CLOCK_MONOTONIC
для работы с монотонно возрастающим временем,
CLOCK_BOOTTIME
для работы с монотонно возрастающим временем с учетом периодов времени, когда
работа системы приостановлена.
В параметре flags
можно указать флаг TFD_NONBLOCK
для неблокирующего ввода-вывода и флаг
TFD_CLOEXEC
для автоматического закрытия файлового дескриптора при выполнении системного вызова exec.
В системном вызове timerfd_settime
используется структура struct itimerspec
,
определенная следующим образом:
struct itimerspec
{
struct timespec it_interval; /* Interval for periodic timer */
struct timespec it_value; /* Initial expiration */
};
Она аналогична структуре struct itimerval
, использующейся в setitimer
, но интервалы времени
задаются с помощью структуры struct timespec
.
Системный вызов timerfd_settime
позволяет запустить или остановить таймер, связанный с файловым
дескриптором fd
. В параметре flags
можно указать флаг TFD_TIMER_ABSTIME
, означающий,
что параметр new_value
задает не интервал времени для таймера, а абсолютное время срабатывания таймера,
то есть таймер превращается в будильник.
Срабатывание таймера можно определить по готовности файлового дескриптора fd
на чтение.
Если было хотя бы одно срабатывание таймера, с помощью системного вызова read
можно считать
64-битное беззнаковое целое число, хранящее счетчик срабатываний таймера после предыдущего
чтения состояния таймера с помощью read
.
Готовность файлового дескриптора fd
к чтению из него можно ждать и с помощью системного
вызова read
, который заблокирует процесс до наступления готовности (если только
таймер не открыт в неблокирующем режиме), и с помощью системных вызовов select, poll, epoll.
Механизм уведомлений (notification) или семафоров можно реализовать
с помощью неименованных каналов (pipe), используя семантику системных вызовов read
и write
.
Однако Linux предлагает специализированный интерфейс, также использующий файловые дескрипторы.
Файловый дескриптор для уведомлений создается с помощью системного вызова eventfd
.
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
Файловый дескриптор, который возвращает системный вызов eventfd
предоставляет доступ
к объекту ядра Linux, в котором хранится 64-битное целое беззнаковое значение (счетчик), которое можно использовать для уведомлений
или как значение семафора в зависимости от режима создания flags
.
Параметр initval
позволяет задать начальное значение счетчика. Системный вызов позволяет задать
только 32 бита, хотя в ядре хранится 64-битное значение.
Параметр flags
позволяет задать флаг EFD_CLOEXEC
для автоматического закрытия файлового дескриптора при exec
,
EFD_NONBLOCK
для работы в неблокирующем режиме.
Для использования файлового дескриптора в режиме семафора в flags
необходимо передать флаг EFD_SEMAPHORE
.
В системные вызовы read
и write
, работающие с таким файловым дескриптором должны передаваться буферы
размера (как минимум) 8 байт, то есть указатели на переменные 64-битного целого беззнакового типа.
Если файловый дескриптор был создан eventfd
в режиме уведомлений (то есть без указания флага EFD_SEMAPHORE
),
то системные вызовы read
и write
работают следующим образом:
Если значение счетчика не равно 0, то read
записывает в переданный буфер значение этого счетчика, которое после этого сбрасывается в 0.
Если значение счетчика равно 0, то read
приостанавливает процесс до момента, пока значение счетчика не изменится.
Системный вызов write
прибавляет переданное в системном вызове значение к текущему значению счетчика.
Однако, если результат сложения окажется больше, чем максимальное значение счетчика 0xfffffffffffffffe,
то процесс будет заблокирован до момента, когда сложение можно будет выполнить без превышения результатом
максимального значения счетчика.
Если файловый дескриптор был создан в режиме EFD_SEMAPHORE
, то поведение read
меняется следующим образом:
если значение счетчика не равно 0, то read
записывает в переданный буфер значение 1, а текущее значение счетчика
уменьшается на 1. Если значение счетчика равно 0, то read
приостанавливает процесс до момента, пока значение счетчика не изменится.
Таким образом, использовать файловый дескриптор в режиме семафора можно следующим образом:
fd = eventfd(1, EFD_SEMAPHORE); // начальное значение 1
// lock semaphore
uint64_t rval;
read(fd, &rval, sizeof(rval));
// unlock semaphore
uint64_t wval = 1;
write(fd, &wval, sizeof(wval));
Если процесс одновременно работает с несколькими файловыми дескрипторами, практически всегда возникает
необходимость отслеживать состояние нескольких файловых дескрипторов одновременно. Например,
программа-сервер может обрабатывать подключение новых клиентов с помощью accept
и выполнять обмен
данными с уже подключившимися клиентами с помощью read
и write
. Каждый из системных вызовов
может заблокировать процесс на неопределенное время, что недопустимо, так как пока процесс
заблокирован на работе с одним файловым дескриптором, в других файловых дескрипторах могут
появиться данные.
В таких случаях обработка операций ввода-вывода разбивается на две стадии. На первой стадии процесс ждет готовности интересующих его файловых дескрипторов, и на второй стадии выполняет операции ввода-вывода с готовыми файловыми дескрипторами.
Ожидание готовности файловых дескрипторов может выполняться одним из нескольких системных
вызовов: select
, pselect
, poll
, ppoll
. Мы рассмотрим механизм epoll ядра Linux.
epoll хорош тем, что множество файловых дескрипторов хранится в ядре, и таким образом
не требуется каждый раз указывать интересующие файловые дескрипторы при подготовке
аргументов системного вызова.
Работа с интерфейсом epoll ведется с помощью файлового дескриптора. Чтобы создать объект
ядра для последующего ожидания готовности файловых дескрипторов используется
системный вызов epoll_create1
.
#include <sys/epoll.h>
int epoll_create1(int flags);
В параметре flags
можно передать флаг EPOLL_CLOEXEC
, означающий автоматическое закрытие
файлового дескриптора при выполнении exec
.
Системный вызов возвращает файловый дескриптор, который нужно использовать при последующих операциях.
Изменять множество отслеживаемых файловых дескрипторов можно с помощью системного вызова epoll_ctl
.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
В параметре epfd
передается файловый дескриптор, полученный с помощью epoll_create1
.
В параметре op
указывается операция над множеством файловых дескрипторов:
EPOLL_CTL_ADD
- добавить файловый дескриптор в множество отслеживаемых,
EPOLL_CTL_MOD
- модифицировать режим отслеживания файлового дескриптора,
EPOLL_CTL_DEL
- удалить файловый дескриптор из множества отслеживаемых.
В параметре fd
передается файловый дескриптор, операцию с которым нужно выполнить.
Тип struct epoll_event
определен следующим образом:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
В поле events
структуры можно указать один или несколько флагов,
задающих готовность к каким операциям должна отслеживаться.
Константа EPOLLIN
задает, что должна отслеживаться готовность
к операции чтения, константа EPOLLOUT
задает, что должна отслеживаться
готовность к операции записи.
Таким образом, чтобы добавить файловый дескриптор fd
в множество
файловых дескрипторов, для которых отслеживается готовность к чтению,
можно выполнить следующие операции.
struct epoll_event eev = {};
eev.events = EPOLLIN;
eev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &eev);
Ожидание готовности файловых дескрипторов выполняется с помощью
системного вызова epoll_pwait
.
#include <sys/epoll.h>
int epoll_pwait(
int epfd,
struct epoll_event *events,
int maxevents,
int timeout,
const sigset_t *sigmask);
Параметр epfd
- это файловый дескриптор, созданный с помощью epoll_create1
.
В параметре maxevents
передается размер массива, а в параметре events
передается
адрес начала массива, в который в результате выполнения системного вызова
будет записана информация о готовых файловых дескрипторах.
В параметре timeout
указывается максимальное время ожидания в миллисекундах.
Если передано значение -1, ожидание будет выполняться без ограничения времени.
Если передано значение 0, то будет возвращена информация о файловых
дескрипторах, готовых к выполнению соответствующей операции немедленно.
В параметре sigmask
передается маска сигналов, которая заместит на время работы
системного вызова текущую маску заблокированных сигналов процесса
(аналогично системному вызову sigsuspend
). Если модифицировать
маску заблокированных сигналов не нужно, в параметре sigmask
нужно передать NULL.
Системный вызов epoll_pwait
возвращает -1 в случае ошибки. Тогда нужно проверять
переменную errno
, чтобы определить причину ошибки (например, EINTR).
Возвращается 0, если истекло время ожидания, но готовые файловые дескрипторы отсутствуют.
В противном случае возвращается количество готовых к выполнению запрошенной операции
файловых дескрипторов (не более чем maxevents), а сами готовые файловые дескрипторы
записываются в массив events
.