Время в 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
.
Во многих странах используется "летнее время", когда стрелки часов переводятся на час назад.
История введения/отмены летнего времени, и его периоды хранится в базе данных IANA.
База данных представляет собой набор правил в текстовом виде, которые компилируются в бинарное представление, используемое библиотекой glibc. Наборы файлов с правилами перехода на летнее время для разных регионов хранятся в /usr/share/zoneinfo/
.
Когда значение tm_isdst
положительное, то применяется летнее время, значение tm_isdst
- зимнее. В случае, когда значение tm_isdst
отрицательно, - используются данные из timezone data.
Многие функции 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, то может потребоваться настоящий системный вызов для опроса системного таймера.