Фото від Rodion Kutsaiev на Unsplash
Чи замислювалися ви, як Linux обробляє дату і час? Давайте розберемося!
Найпростіший спосіб — набрати команду date
в оболонці, щоб перевірити поточну дату:
date
//Wed Dec 23 22:18:40 CET 2024
А що насправді стоїть за цією командою?
Давайте використаємо strace
, щоб проаналізувати цей бінарний файл:
-------
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=3052896, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 3052896, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f514a146000
close(3) = 0
clock_gettime(CLOCK_REALTIME, {tv_sec=1735554014, tv_nsec=723422572}) = 0
-------
Ми чітко бачимо, що clock_gettime
(https://man7.org/linux/man-pages/man3/clock_gettime.3.html) надає необхідну інформацію.
Linux підтримує різні системні годинники, такі як час процесу, реальний час тощо. Усі ці типи годинників описані у стандарті POSIX.1b
. У ядрі ми знаходимо заголовкові файли, що представляють ці ідентифікатори в таких макросах:
#define CLOCK_REALTIME 0
#define CLOCK_MONOTONIC 1
#define CLOCK_PROCESS_CPUTIME_ID 2
#define CLOCK_THREAD_CPUTIME_ID 3
#define CLOCK_MONOTONIC_RAW 4
#define CLOCK_REALTIME_COARSE 5
#define CLOCK_MONOTONIC_COARSE 6
#define CLOCK_BOOTTIME 7
#define CLOCK_REALTIME_ALARM 8
#define CLOCK_BOOTTIME_ALARM 9
#define CLOCK_SGI_CYCLE 10
#define CLOCK_TAI 11
#define TIMER_ABSTIME 0x01
У першому прикладі з командою date
ми отримали CLOCK_REALTIME
, який називається системним годинником реального часу або стінним часом (наприклад: Wed Dec 23 22:18:40 CET 2024).
Цікавою є також CLOCK_MONOTONIC
, який вимірює час від моменту завантаження системи. Існують також таймери, які вимірюють час процесу та потоку.
Але як Linux визначає час навіть після вимикання?
Давайте почнемо з завантаження Linux.
На материнській платі є чип, який живиться від маленької батареї.
Тут вступає в гру CMOS.
CMOS — це маленький чип, встановлений на материнській платі, що містить схему реального часу (RTC) та невелику кількість ОЗУ, що живиться від батареї CMOS, щоб зберігати дані, коли система вимкнена.
У ядрі Linux це представлено через структуру rtc_time
:
struct rtc_time {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
RTC — це маленька схема, яка:
- Використовує кристал з частотою 32.768 кГц (той самий, що й у кварцових годинниках)
- Надає базовий тактовий імпульс
- Ця частота вибрана, оскільки її легко поділити для отримання інтервалів у одну секунду
Оскільки CMOS/RTC зберігає рік у вигляді 2 цифр, перехід через 2000 рік спричинив серйозну помилку, яка показувала рік як 1900. (https://en.wikipedia.org/wiki/Year2000problem)
Linux покладається на кілька джерел годинникових сигналів.
RTC достатньо для збереження часу, коли комп'ютер вимкнений, але для точного відстеження часу нам потрібне більш точне рішення.
Перевіримо вивід з dmesg
// dmesg | grep clocksource
[0.000000] clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1910969940391419 ns
[0.000000] clocksource: hpet: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604467 ns
[0.001551] clocksource: tsc-early: mask: 0xffffffffffffffff max_cycles: 0x255c5fe5801, max_idle_ns: 440795244864 ns
[0.548897] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1911260446275000 ns
[0.890207] clocksource: Switched to clocksource tsc-early
[0.944169] clocksource: acpi_pm: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 2085701024 ns
[2.063053] tsc: Refined TSC clocksource calibration: 2591.906 MHz
[2.065628] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x255c5e314d9, max_idle_ns: 440795320690 ns
[2.067241] clocksource: Switched to clocksource tsc
Linux підтримує кілька джерел часу:
- refined-jiffies та jiffies вимірюють невизначені короткі проміжки часу (https://en.wikipedia.org/wiki/Jiffy_%28time%29)
- TSC (Time Stamp Counter) є основним джерелом часу
- HPET (High Precision Event Timer)
- PIT (Programmable Interval Timer)
Ви можете перевірити своє поточне джерело часу:
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
//Output: tsc
Що таке TSC (Time Stamp Counter)?
- це 64-бітний регістр, який є в процесорах x86 і підраховує цикли ЦП з моменту скидання.
- може бути прочитаний за допомогою інструкції
RDTSC
; rdtsc_basic.asm
section .text
global rdtsc_basic
rdtsc_basic:
rdtsc
shl rdx, 32
or rax, rdx
ret
- TSC підраховує фактичні сигнали годинника (імпульси), що надходять на пін CLK процесора
CLK pin: _|‾|_|‾|_|‾|_|‾|_
TSC: 0 1 2 3 4 5
Яка мета TSC?
- високоточне вимірювання часу
- використовується для вимірювання продуктивності
- підрахунок циклів ЦП
- джерело часу для ОС
tsc
не є гарантованим «джерелом істини» — щоб надавати надійний час, його потрібно калібрувати за допомогою іншого таймера, таким як hpet
або pit
HPET (High Precision Event Timer):
- Спеціалізований апаратний таймер
- Точніший за PIT
- Зазвичай має частоту 10 МГц
- Підходить для періодичних подій
- Використовується, коли TSC ненадійний
PIT (Programmable Interval Timer):
- Старий таймер Intel 8253/8254
- Низька роздільна здатність (приблизно 1.193 МГц)
- Використовується під час раннього завантаження
- Резервне джерело часу
- Старий ПК апаратний таймер
В Linux ви можете перевірити, яке джерело часу використовується, набравши:
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
//Output: tsc
Epoch Time
Що таке epoch time?
Коротко — epoch time — це відправна точка, від якої Linux вимірює час. 1970 рік — це рік, коли були розроблені системи Unix.
Вимірювання часу — це підрахунок секунд, і воно було обмежене апаратною архітектурою. Використання 32-бітних цілих чисел для підрахунку секунд від 1970 року дозволяло представляти дати до 2038 року, що на той час здавалося достатньо віддаленим у майбутнє.
Сучасні 64-бітні системи можуть обробляти набагато більші діапазони часу. 64-бітний таймштамп може представляти дати від 292 мільярдів років до 292 мільярдів років до або після 1970 року.
У ядрі Linux є кілька місць, де використовується EPOCH
час.
https://github.com/torvalds/linux/blob/master/kernel/time/timekeeping.c#L983
/**
* ktime_get_real_seconds - Get the seconds portion of CLOCK_REALTIME
*
* Returns the wall clock seconds since 1970.
*
* For 64bit systems the fast access to tk->xtime_sec is preserved. On
* 32bit systems the access must be protected with the sequence
* counter to provide "atomic" access to the 64bit tk->xtime_sec
* value.
*/
time64_t ktime_get_real_seconds(void)
{
Мови програмування також використовують цей підхід для повернення часу EPOCH
. Наприклад, у JavaScript, якщо ми хочемо повернути час EPOCH
, нам потрібно передати 0 секунд (це початковий час EPOCH
):
> new Date(0)
1970-01-01T00:00:00.000Z
Як обчислюються секунди
З попередніх розділів ми знаємо, що tsc
(Time Stamp Counter) підраховує цикли ЦП. Однак швидкість процесора не є сталою, що впливає на обчислення часу. Очевидно, ядро Linux повинно обробляти ці варіації.
Обробка джерел часу в ядрі
У ядрі Linux файл kernel/time/clocksource.c
містить наступну функцію:
/**
* __clocksource_update_freq_scale - Оновлює джерело часу з новою частотою
* @cs: джерело часу, яке реєструється
* @scale: множник, що множиться на частоту для отримання частоти джерела часу
* @freq: частота джерела часу (цикли за секунду), поділена на множник
*
* Це має бути викликано тільки з методу clocksource->enable().
*
* Це *НЕ ТРЕБА* викликати безпосередньо! Будь ласка, використовуйте
* допоміжні функції __clocksource_update_freq_hz() або __clocksource_update_freq_khz().
*/
void __clocksource_update_freq_scale(struct clocksource *cs, u32 scale, u32 freq)
{
u64 sec;
/*
* За замовчуванням джерела часу *особливі* і самостійно визначають свої mult/shift.
* Але ви не особливі, тому повинні вказати значення частоти.
*/
if (freq) {
/*
* Обчислюємо максимальну кількість секунд, які ми можемо працювати перед
* перезапуском. Для джерел часу, що мають маску > 32 біти,
* ми повинні обмежити максимальний час сну, щоб забезпечити хорошу
* точність перетворення. 10 хвилин — це ще розумна
* кількість часу. Це призводить до значення зміщення 24 для
* джерела часу з маскою >= 40 біт і f >= 4 ГГц. Це призводить до
* ~ 0.06ppm гранулярності для NTP.
*/
sec = cs->mask;
do_div(sec, freq);
do_div(sec, scale);
if (!sec)
sec = 1;
else if (sec > 600 && cs->mask > UINT_MAX)
sec = 600;
clocks_calc_mult_shift(&cs->mult, &cs->shift, freq,
NSEC_PER_SEC / scale, sec * scale);
}
Цей код обчислює масштабувальні фактори, необхідні для перетворення тиків джерела часу у наносекунди.
Часові операції в Linux
Оновлення часу в Linux здійснюється за допомогою переривань. Це буде детально пояснено в майбутніх статтях, тож слідкуйте за новинами!
У ядрі Linux функція update_wall_time
викликається перериваннями:
void update_wall_time(void)
{
if (timekeeping_advance(TK_ADV_TICK))
clock_was_set_delayed();
}
У межах функції timekeeping_advance
в kernel/time/timekeeping.c
:
static bool timekeeping_advance(enum timekeeping_adv_mode mode)
{
struct timekeeper *real_tk = &tk_core.timekeeper;
struct timekeeper *tk = &shadow_timekeeper;
u64 offset;
int shift = 0, maxshift;
unsigned int clock_set = 0;
unsigned long flags;
raw_spin_lock_irqsave(&timekeeper_lock, flags);
/* Переконайтеся, що ми повністю відновлені: */
if (unlikely(timekeeping_suspended))
goto out;
offset = clocksource_delta(tk_clock_read(&tk->tkr_mono),
tk->tkr_mono.cycle_last, tk->tkr_mono.mask);
Тут обчислюється offset
.
Для джерела часу TSC, інструкція асемблера rdtsc
виконується для отримання кількості циклів ЦП з моменту скидання.
На основі цієї інформації ми можемо створити формулу для обчислення секунд:
seconds = цикли, що пройшли / цикли за секунду (hz)
Приклад:
- Значення лічильника: 1,000,000 циклів
- Частота годинника: 1 GHz (1,000,000,000 Hz)
- Секунди = 1,000,000 / 1,000,000,000 = 0.001 секунди (1 мілісекунда)
У ядрі Linux ця формула оптимізована до:
seconds = (цикли * mult) >> shift
Функція clocksource_cyc2ns
в include/linux/clocksource.h
реалізує це:
static inline s64 clocksource_cyc2ns(u64 cycles, u32 mult, u32 shift)
{
return ((u64) cycles * mult) >> shift;
}
Використання множення та бітових зсувів є менш обтяжливим для обчислювальних ресурсів і швидшим, ніж виконання ділення. Ця оптимізація є критично важливою в просторі ядра, де продуктивність має велике значення.
Посилання
[
GitHub - piotrzarycki/low-level-os-study
Contribute to piotrzarycki/low-level-os-study development by creating an account on GitHub.
github.com
](https://github.com/piotrzarycki/low-level-os-study?source=post_page-----626f2cdf83f4--------------------------------)
[
Epoch (computing) - Wikipedia
From Wikipedia, the free encyclopedia In computing, an epoch is a fixed date and time used as a reference from which a…
en.wikipedia.org
](https://en.wikipedia.org/wiki/Epoch%28computing%29?source=postpage-----626f2cdf83f4--------------------------------)
[
GitHub - torvalds/linux: Linux kernel source tree
Linux kernel source tree. Contribute to torvalds/linux development by creating an account on GitHub.
github.com
](https://github.com/torvalds/linux?source=post_page-----626f2cdf83f4--------------------------------)
Перекладено з: Linux — How the Kernel Counts Time