Skip to content

Latest commit

 

History

History
 
 

time

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Время в UNIX

API для работы со временем

Текущее время

Время в UNIX-системах определяется как количество секунд, прошедшее с 1 января 1970 года, причем часы идут по стандартному гринвичскому времени (GMT) без учета перехода на летнее время (DST - daylight saving time).

32-разрядные системы должны прекратить своё нормальное существование 19 января 2038 года, поскольку будет переполнение знакового целого типа для хранения количества секунд.

Функция time возвращает количество секунд с начала эпохи. Аргументом функции (в который можно передать NULL) является указатель на переменную, куда требуется записать результат.

В случае, когда требуется более высокая точность, чем 1 секунда, можно использовать системный вызов gettimeofday, который позволяет получить текущее время в виде структуры:

struct timeval {
  time_t      tv_sec;  // секунды
  suseconds_t tv_usec; // микросекунды
};

В этом случае, несмотря на то, что в структуре определено поле для микросекунд, реальная точность будет составлять порядка 10-20 миллисекунд для Linux.

Более высокую точность можно получить с помощью системного вызова clock_gettime.

Разложение времени на составляющие

Человеко-представимое время состоит из даты (год, месяц, день) и времени суток (часы, минуты, секунды).

Это описывается структурой:

struct tm { /* время, разбитое на составляющие */
  int tm_sec; /* секунды от начала минуты: [0 -60] */
  int tm_min; /* минуты от начала часа: [0 - 59] */
  int tm_hour; /* часы от полуночи: [0 - 23] */
  int tm_mday; /* дни от начала месяца: [1 - 31] */
  int tm_mon; /* месяцы с января: [0 - 11] */
  int tm_year; /* годы с 1900 года */
  int tm_wday; /* дни с воскресенья: [0 - 6] */
  int tm_yday; /* дни от начала года (1 января): [0 - 365] */
  int tm_isdst; /* флаг перехода на летнее время: <0, 0, >0 */
};

Для преобразования человеко-читаемого времени в машинное используется функция mktime, а в обратную сторону - одной из функций: gmtime или localtime.

Daylight Saving Time

Во многих странах используется "летнее время", когда стрелки часов переводятся на час назад.

История введения/отмены летнего времени, и его периоды хранится в базе данных IANA.

База данных представляет собой набор правил в текстовом виде, которые компилируются в бинарное представление, используемое библиотекой glibc. Наборы файлов с правилами перехода на летнее время для разных регионов хранятся в /usr/share/zoneinfo/.

Когда значение tm_isdst положительное, то применяется летнее время, значение tm_isdst - зимнее. В случае, когда значение tm_isdst отрицательно, - используются данные из timezone data.

Reentrant-функции

Многие функции POSIX API разрабатывались во времена однопроцессорных систем. Это может приводить к разным неприятным последствиям:

struct tm * tm_1 = localtime(NULL);
struct tm * tm_2 = localtime(NULL); // opps! *tm_1 changed!

Проблема заключается в том, что некоторые функции, например localtime, возвращает указатель на структуру-результат, а не скалярное значение. При этом, сами данные структуры не требуется удалять, - они хранятся в .data-области библиотеки glibc.

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

struct tm tm_1; localtime_r(NULL, &tm_1);
struct tm tm_2; localtime_r(NULL, &tm_2); // OK

Использование повторно входимых функций является обязательным (но не достаточным) условием при написании многопоточных программ.

Некоторые reentrant-функции уже не актуальны в современных версиях glibc для Linux, и помечены как deprecated. Например, реализация readdir использует локальное для каждого потока хранение данных.

Виды часов

Время в UNIX системе представляется в виде двух чисел: количества секунд и количества наносекунд, но это не означает, что точность часов сопоставимо с одной наносекундой. В современных компьютерах архитектуры несколько типов часов:

  • аппаратные часы, которые работают от отдельной батарейки даже при отключения питания;
  • счетчик в ядре операционной системы, который периодически обновляется отдельным аппаратным таймером;
  • счетчик тактов процессора.

Аппаратные часы

Аппаратные часы обычно работают на базе стандартного часового кварца, обеспечивающего частоту 32.768КГц, и имеющие точность, сопоставимую с точностью обычных бытовых часов. Эти часы могут хранить дату как в формате UTC (стандарт, принятый в UNIX-системах), так и локальное время (принято в Windows).

В Linux аппаратные часы доступны в виде символьного устройства /dev/rtc, доступ к которому есть только у пользователя root.

Это устройство может быть открыто только для чтения, после чего из него можно читать 32-битные значения - информацию о прерываниях. Настройка поведения часов осуществляется с помощью системного вызова ioctl и передачей одной из команд, относящихся к rtc(4).

Прерывания могут быть:

  • каждую секунду, если часы настроены на ежесекундное срабатывание RTC_UIE
  • с частотой от 2 до 8192 Гц, причем частота должна быть степенью двойки RTC_PIE
  • срабатывание в определенное время RTC_AIE.

Ежесекундное прерывание с использованием часов реального времени:

#include <sys/ioctl.h>       // системный вызов ioctl
#include <linux/rtc.h>       // константы RTC_*

int rtc = open("/dev/rtc", O_RDONLY);
if (-1==rtc) { perror("open /dev/rtc"); exit(1); } // только root может открыть

ioctl(rtc, RTC_UIE_ON, 0);   // включаем прерывания каждую секунду
while (1) {
    int interrupt_mask;
    // системный вызов read блокируется до следующего прерывания
    read(rtc, &interrupt_mask, sizeof(interrupt_mask));
    puts("Tick");
}

Аппаратные часы хранят информацию о текущей дате и текущем времени с точностью до секунды, причем это время может отличаться от системного.

Получение времени из аппаратных часов:

#include <sys/ioctl.h>       // системный вызов ioctl
#include <linux/rtc.h>       // константы RTC_*, а ещё структура rtc_time

int rtc = open("/dev/rtc", O_RDONLY);
if (-1 == rtc) { perror("open /dev/rtc"); exit(1); }

struct rtc_time t = {};
// чтение текущего времени из аппаратных часов
ioctl(rtc, RTC_RD_TIME, &t);

printf("RTC time: %02d : %02d : %02d \n",
       t.tm_hour, t.tm_min, t.tm_sec);

Синхронизация системного времени с аппаратными часами осуществляется при загрузке системы и завершении работы (выключении или перезагрузке).

Команда hwclock позволяет взаимодействовать с часами реального времени, в том числе и для синхронизации.

> hwclock -r     # прочитать и вывести время из RTC
> hwclock -w     # сохранить системное время в RTC (обычно при выключении)
> hwclock -s     # установить системное время из RTC (при загрузке)

По умолчанию подразумевается, что аппаратные часы используют время UTC, но можно если компьютер используется совместно с системой Windows (двойная загрузка), то можно синхронизировать локальное время, для этого используется опция -l. Многие дистрибутивы Linux на этапе установки позволяют указать, какое именно время хранится в часах реального времени.

Системное время и источники времени

Отсчет времени начинается с момента загрузки ядра, и хранится в виде целого количества тиков (jiffies), продолжительность которых определяется параметром компиляции ядра CONFIG_HZ, и может принимать одно из значений: 100, 250, 300 или 1000 Гц. Для текущего ядра это можно выяснить в файле /boot/config-ВЕРСИЯ_ЯДРА.

Более высокая частота подразумевает большую нагрузку на процессор, но бывает полезна в некоторых применениях, когда требуется повысить отзывчивость системы.

Доступные источники времени зависят от архитектуры процессора и конфигурации ядра, узнать в Linux их можно командой:

> cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
#^   ^    ^ 
#|   |    Legacy-драйвер           
#|   Системный таймер высокой точнсти, обычно работает на частоте от 10Мгц
#Регистр Time-Step Counter в самом процессоре

Текущий способ определения точного времени хранится в current_clocksource:

> cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

Наиболее точным источником времени, в то же время с минимальным временем доступа, - это счетчик тактов в самом процессоре Time-Step Counter, значение которого, для архитектуры x86 можно получить с помощью команды RDTSC. Поскольку получение высокоточных значений времени используется при эксплуатации уязвимостей процессоров Meltdown и Spectre, то этот способ может быть принудительно отключен в системе.

Для получения текущего времени из источника времени используется системный вызов clock_gettime:

#include <time.h>

struct timespec {
    time_t tv_sec;  // время в секундах
    long   tv_nsec; // доля времени в наносекундах
};

int clock_gettime(clockid_t id, /* out: */ struct timespec *tp);

Первый параметр системного вызова - это целочисленное значение, определяющее, какой именно счетчик или таймер нужно использовать. Для большинства UNIX-систем определены таймеры:

  • CLOCK_REAL - значение астрономического времени, где за точку отсчета принимается начало эпохи - 1 января 1970 года;
  • CLOCK_MONOTONIC - значение времени с момента загрузки ядра, исключая то время, пока система находилась в спящем режиме;
  • CLOCK_PROCESS_CPUTIME_ID - значение времени, затраченного на выполнение текущего процесса;
  • CLOCK_THREAD_CPUTIME_ID - значение времени, затраченного на выполнение текущего потока.

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

В современных версиях Linux происходит обращение к регистру TSC, либо опрос системного таймера, который возвращает текущее значение с высокой точностью. Часы CLOCK_REAL_COARSE и CLOCK_MONOTONIC_COARSE возвращают время с точностью до одного тика, как в старых версиях.

В системе FreeBSD предусмотрены два вида часов - точные, с суффиксом _PRECISE, которые опрашивают системный таймер, и быстрые, с суффиксом _FAST, которые возвращают значения с точностью до тика. POSIX-совместимым названиям часов соответствуют _FAST-версии.

Системный вызов clock_gettime реализован в виде vdso(7)-функции, которая доступна в адресном пространстве пользователя. В случае, если происходит опрос часов с низкой точностью (например CLOCK_REAL_COARSE в Linux или CLOCK_REAL_FAST в FreeBSD), то время вычисляется в адресном пространстве пользователя по значению из счетчика, ранее проставленного планировщиком задач. Если же требуется получить время с высокой точностью и не используется TSC, то может потребоваться настоящий системный вызов для опроса системного таймера.