Skip to content

Latest commit

 

History

History
346 lines (257 loc) · 24.4 KB

README.md

File metadata and controls

346 lines (257 loc) · 24.4 KB

Измерение времени

Для представления интервалов времени в 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.