Механизмы таймера в C и Linux

99
Компьютерные новости и новости технологий на Q-Games.ru

Последнее обновление 09.01.2023 — Василий Иванов

Механизмы таймера позволяют планировать, чтобы ядро ​​ОС уведомляло приложение по истечении заданного времени. Обычно вы будете использовать их, предоставляя две части информации. Во-первых, вам нужно указать, сколько времени должно пройти таймер перед уведомлением. Во-вторых, вам нужно подготовить функцию обратного вызова, которая будет действовать при появлении этого уведомления.

Традиционный подход к таймерам

Механизмы таймера в системах на основе Linux и Unix эволюционировали, чтобы служить различным потребностям. Различные подходы могут помочь вам решить различные типы проблем. Однако вы часто будете видеть, что первая версия механизма alarm() все еще используется.

Функция будильника — самый простой способ использования таймера; вот его прототип:

unsigned int alarm(unsigned int seconds);

Используя этот метод, вы можете указать время только в целых секундах. Когда время истекло, операционная система отправляет сигнал SIGALRM вашему приложению. Чтобы обработать истечение таймера в вашем приложении, вы также должны определить функцию обратного вызова.

По теме:  Игроки Обсудить "Существенный" Моды, которые кардинально улучшают игру

Вот пример функции обработчика сигнала:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

void timer_callback(int signum)
{
time_t now = time(NULL);
printf("Signal %d caught on %li", signum, now);
}

int main()
{
signal(SIGALRM, timer_callback);
alarm(1);
sleep(3);
return 0;
}

Этот код вызывает сигнал SIGALRM через 1 секунду. Если вы хотите увеличить задержку таймера до пяти секунд, просто вызовите alarm(5). Чтобы остановить таймер, передайте значение 0: alarm(0).

Когда время истекло, используемый вами таймер не будет периодически перезапускаться. Например, если вы хотите задержаться еще на секунду, вы должны перезапустить механизм еще одним вызовом alarm().

Несмотря на простоту использования, этот метод имеет некоторые недостатки:

  • Только один таймер за раз.
  • Нет поддержки периодического таймера.
  • Вы можете указать только период времени, кратный целым секундам.
  • Невозможно узнать, сколько времени осталось на таймере.

Сохраните пример кода, указанный выше, как alarm.c. Когда вы скомпилируете и запустите его, программа вызовет функцию timer_callback через одну секунду. Затем он будет ждать оставшиеся две секунды из-за строки sleep(3), а затем завершится.

$ gcc -o alarm alarm.c
$ time ./alarm
Signal 14 caught on 1653490465
real 0m1.004s
user 0m0.000s
sys 0m0.003s

Причина использования команды времени состоит в том, чтобы иметь возможность видеть время. Но если посмотреть на результат, то общее время работы не три секунды. Это происходит из-за сигнала SIGALRM от alarm(1), когда истекает первая секунда, пока выполняется системный вызов, вызванный функцией sleep(3). При поступлении этого сигнала системный вызов, инициированный для sleep(3), прерывается.

Использование интервального таймера

Механизм интервального таймера был впервые доступен в версии 4.2 BSD. Позже он был стандартизирован POSIX. Его основные преимущества по сравнению с традиционным методом таймера на основе alarm():

  • Обеспечивает микросекундное разрешение.
  • Это позволяет более детально контролировать измерение времени в трех различных режимах.
  • Можно установить один раз и заставить работать периодически.
  • Можно узнать, как долго он присутствует в любой данный момент.

Прототипы функций, используемые для операций интервального таймера, следующие:

#include <sys/time.h>

int setitimer(int which, const struct itimerval *newValue, struct itimerval *oldValue);
int getitimer(int which, struct itimerval *value);

struct itimerval
{
struct timeval itInterval; // next value
struct timeval itValue; // current value
};

struct timeval
{
long tv_sec;
long tv_usec;
};

Если вы хотите настроить интервальный таймер, вам нужно будет использовать структуру itimerval. Вам нужно будет передать значение, используя эту структуру в качестве второго аргумента функции settimer.

Например, интервальный таймер, который будет уведомлять ваше приложение в течение 1 секунды, а затем каждые 300 миллисекунд, можно настроить следующим образом:

struct itimerval newTimer;
struct itimerval oldTimer;

newTimer.itValue.tv_sec = 1;
newTimer.itValue.tv_usec = 0;

newTimer.itInterval.tv_sec = 0;
newTimer.itInterval.tv_usec = 300 * 1000;

setitimer(ITIMER_REAL, &newTimer, &oldTimer);

Если перед установкой новых значений активен интервальный таймер, его значения передаются по адресу переменной типа itimerval, заданному в третьем параметре функции.

С помощью механизма интервального таймера можно настроить три разных типа таймеров. Укажите тип таймера в первом параметре setitimer():

Тип таймера Сигнал Пояснение
ITIMER_REAL СИГАЛРМ Независимо от времени, затраченного приложением, рассчитывается по общему прошедшему времени.
ITIMER_VIRTUAL СИГВТАЛРМ Рассчитывается за время работы приложения только в пользовательском режиме.
ITIMER_PROF СИГПРОФ Рассчитывается по сумме времени, проведенного приложением как в пользовательском, так и в системном режимах.

Из этой таблицы видно, что тип ITIMER_REAL отправляет сигнал SIGALRM, как и функция alarm().

Использование интервального таймера и alarm() в одном приложении может привести к путанице. Хотя вы можете еще раз проверить оставшееся время с помощью gettimer(), нет смысла использовать их одновременно.

Вот пример определения функции обработчика сигнала с заголовком отладки:

#include  <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include "./debug.h"

void timer_callback(int signum)
{
struct timeval now;
gettimeofday(&now, NULL);
printf("Signal %d caught on %li.%03li ", signum, now.tv_sec, now.tv_usec / 1000);
}

int main()
{
unsigned int remaining = 3;

struct itimerval new_timer;
struct itimerval old_timer;

new_timer.it_value.tv_sec = 1;
new_timer.it_value.tv_usec = 0;
new_timer.it_interval.tv_sec = 0;
new_timer.it_interval.tv_usec = 300 * 1000;

setitimer(ITIMER_REAL, &new_timer, &old_timer);
signal(SIGALRM, timer_callback);

while (sleep(remaining) != 0)
{
if (errno == EINTR)
debugf("sleep interrupted by signal");
else
errorf("sleep error %s", strerror(errno));
}

return 0;
}

Приведенный выше код использует функцию sleep() для ожидания в течение трех секунд. В это время запускается интервальный таймер сначала на одну секунду, затем с интервалом в 300 миллисекунд.

Для лучшего понимания сохраните и скомпилируйте пример кода с именем interval.c:

$ gcc -o interval interval.c
$ time ./interval
Signal 14 caught on 1653493614.325
debug: sleep interrupted by signal (main interval.c:36)
Signal 14 caught on 1653493614.625
debug: sleep interrupted by signal (main interval.c:36)
Signal 14 caught on 1653493614.925
debug: sleep interrupted by signal (main interval.c:36)
Signal 14 caught on 1653493615.225
debug: sleep interrupted by signal (main interval.c:36)
Signal 14 caught on 1653493615.525
...

Как видно из вывода после запуска таймера, он вызывает функцию обратного вызова каждые 300 миллисекунд.

Однако, подождав еще немного, вы заметите, что приложение не завершает работу. Он продолжает выполнять функцию обратного вызова каждые 300 миллисекунд. Если вы увеличите значение интервала в миллисекундах, вы увидите, что приложение завершает работу. Это связано с областью использования функции sleep().

Важность использования таймеров в Linux

Механизм таймера имеет большое значение, особенно для приложений реального времени. Это также решение, используемое для оптимизации производительности. Вы даже можете использовать его для измерения времени безотказной работы или задержки в вашем приложении. Важно использовать механизмы таймера для отслеживания прошедшего времени и событий временного перехода.