Лінукс — Як ядро рахує час

pic

Фото від 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.

pic

На материнській платі є чип, який живиться від маленької батареї.

Тут вступає в гру CMOS.

pic

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);  
 }

Цей код обчислює масштабувальні фактори, необхідні для перетворення тиків джерела часу у наносекунди.

pic

Часові операції в 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

Leave a Reply

Your email address will not be published. Required fields are marked *