Learning Platform
Глоссарий Troubleshooting
Урок 13.04 · 22 мин
Средний
vDSOSyscallsPerformanceLinuxKernel

vDSO — быстрые ‘syscall’ без перехода в kernel

clock_gettime() в горячем цикле выполняется ~10 миллионов раз в секунду на современном CPU. getpid() — около 5 миллионов. Это в 50 раз быстрее, чем должно быть, если бы каждый вызов был реальным syscall. Магия в vDSO (virtual Dynamic Shared Object) — маленькой библиотеке, которую kernel мapпит в каждый процесс и которая позволяет выполнить определённые «syscall» БЕЗ перехода в Ring 0.

В этом уроке — как это работает физически, какие syscall превращены в vDSO-функции, чем vDSO отличается от устаревшего vsyscall, и почему именно эти оптимизации могут drastically улучшить latency-sensitive приложений.


Зачем нужен vDSO

Истинный syscall стоит ~100-500 наносекунд только на overhead перехода (с KPTI после Meltdown — ближе к верхней границе). Некоторые syscall вызываются очень часто:

  • gettimeofday / clock_gettime — любое логирование, метрики, profiling, тайм-ауты.
  • getpid — часто для логирования.
  • getcpu — в numa-aware коде.

Если ваш high-frequency trading или нагруженный web-сервис делает миллион clock_gettime в секунду, при честном syscall это будет ~500 мс на overhead каждой секунды — 50% CPU только на switching. С vDSO — ~10 мс. Это 50х разница.

Поэтому Linux давно (с 2.6) реализует vDSO для нескольких syscall.


Что такое vDSO

vDSO — это shared object (.so), который kernel автоматически мapпит в адресное пространство каждого процесса при exec. Это маленькая библиотека (несколько килобайт), содержащая реализации некоторых syscall в user-space.

Содержимое vDSO видно через /proc/PID/maps:

cat /proc/self/maps | grep -E 'vdso|vvar'
# 7ffd...000-7ffd...000 r--p 00000000 00:00 0  [vvar]
# 7ffd...000-7ffd...000 r-xp 00000000 00:00 0  [vdso]

Две страницы:

  • [vdso] — исполняемая страница с кодом функций. r-xp = read + execute, private.
  • [vvar] — read-only страница с данными (timestamp, etc), которую kernel периодически обновляет.

Программа в user-space может прыгать в код [vdso] и читать данные [vvar] — никакого syscall.

vDSO: где живёт быстрый clock_gettime
User codeВаш Python/C код. Зовёт clock_gettime
libc clock_gettimelibc обёртка. Проверяет, доступна ли vDSO-версия. Если да -- прыгает в vdso
[vdso] pageMapped kernel page с user-space реализацией. Просто читает timestamp из [vvar]
read
[vvar] pagePage с глобальным таймером. Kernel обновляет периодически (по тику HPET/TSC)
Result returnedВозврат в libc, потом в код приложения. Никакого Ring switch, ~5-30 ns
No syscall!Всё произошло в user mode. Никакого перехода в Ring 0, нет KPTI overhead, нет cache pollution

Что реализовано в vDSO

На Linux x86_64 типичный vDSO содержит:

ФункцияПокрытый syscallСкорость vs syscall
__vdso_clock_gettimeclock_gettime10-30x быстрее
__vdso_gettimeofdaygettimeofday10-30x быстрее
__vdso_timetime10-30x быстрее
__vdso_getcpugetcpu10x быстрее
__vdso_clock_getresclock_getres10x быстрее

В современных kernel могут быть добавлены и другие (__vdso_rt_sigreturn для сигналов).

getpid исторически НЕ в vDSO, но с Linux 5.x в некоторых конфигурациях добавлен. Зависит от kernel и архитектуры.


Что физически делает vDSO для clock_gettime

Упрощённая модель того, что находится в vDSO:

// Псевдокод __vdso_clock_gettime:
int __vdso_clock_gettime(clockid_t clk_id, struct timespec *ts) {
    struct vdso_data *vd = (struct vdso_data*)VVAR_ADDRESS;
    // Loop в случае race -- kernel может обновлять данные параллельно:
    do {
        uint32_t seq = vd->seq;
        if (clk_id == CLOCK_REALTIME) {
            // Прочитать TSC (Time Stamp Counter -- регистр CPU):
            uint64_t tsc = read_tsc_rdtsc();
            // Конвертировать в секунды используя коэффициенты, посчитанные kernel:
            uint64_t ns = (tsc - vd->cycle_last) * vd->mult >> vd->shift;
            ts->tv_sec = vd->wall_time_sec + ns / 1e9;
            ts->tv_nsec = vd->wall_time_nsec + ns % 1e9;
        }
        // Проверить, не обновил ли kernel данные во время чтения:
    } while (vd->seq != seq);
    return 0;
}

Ключевые моменты:

  1. TSC (Time Stamp Counter) — регистр процессора, который инкрементируется на каждом такте. Читается одной инструкцией rdtsc (~10-20 циклов).
  2. vdso_data — структура в [vvar], которую kernel периодически обновляет: текущее «настоящее» время + множители для конвертации tsc -> ns.
  3. Sequence counter — защита от race: пока читаем, kernel мог обновить. Если seq изменился — читаем заново.

Никакого syscall. Никакого перехода. Просто rdtsc + арифметика + чтение нескольких полей.


История: vsyscall (deprecated)

Раньше существовал ещё один механизм — vsyscall. Это было ещё проще: kernel мapпит fixed page в каждый процесс по фиксированному адресу (0xffffffffff600000), там лежали обработчики для gettimeofday, time, getcpu.

Проблемы vsyscall:

  1. Фиксированный адрес — предсказуемая точка для эксплойтов (ROP-цепочки на vsyscall-странице).
  2. Только эти три функции — не расширяется.
  3. Не учитывал KASLR (Kernel Address Space Layout Randomization).

В Linux 3.5 (2012) vsyscall был помечен deprecated. В современных kernel вместо реального кода там «emulated vsyscall» — при попытке выполнить инструкцию там, kernel перехватывает page fault, делает обычный syscall, возвращает. Это safety net для старого ПО, но никакого выигрыша по скорости.

# Посмотреть, есть ли vsyscall в вашем процессе:
cat /proc/self/maps | grep vsyscall
# ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0  [vsyscall]
# Если есть -- legacy режим. Современный kernel может работать без него.

vDSO заменил vsyscall полностью: больше функций, рандомизированный адрес, прозрачно для приложений (вызов через libc).

NOTE

В современных дистрибутивах (с Linux 5.x) vsyscall часто отключён в kernel config (CONFIG_LEGACY_VSYSCALL_NONE=y). Это правильно: меньше attack surface. Если у вас старая программа, использующая старый vsyscall ABI — обновите её или включите CONFIG_LEGACY_VSYSCALL_EMULATE.


Как libc решает, использовать vDSO или syscall

Когда glibc стартует, она через ELF auxv (вспомогательный вектор) получает указатель на [vdso]. Парсит его символы. Дальше при вызове clock_gettime:

// Упрощённо, реальная glibc сложнее:
int clock_gettime(clockid_t clk_id, struct timespec *ts) {
    if (vdso_clock_gettime != NULL) {
        // Используем vDSO -- быстро:
        return vdso_clock_gettime(clk_id, ts);
    }
    // Fallback -- реальный syscall:
    return syscall(SYS_clock_gettime, clk_id, ts);
}

Программе не нужно ничего знать — библиотека сама выбирает быстрый путь.

Проверить, использует ли ваша clock_gettime vDSO:

# strace должен НЕ показать clock_gettime syscall, если идёт через vDSO:
strace -e clock_gettime python3 -c "
import time
for i in range(100):
    time.time()
" 2>&1 | tail
# Если ничего нет -- через vDSO.
# Если есть -- system без vDSO или libc не использует.

Замер скорости

import time

N = 10_000_000

# clock_gettime через vDSO:
t = time.perf_counter()
for _ in range(N):
    time.time()  # под капотом clock_gettime через vDSO
dt = time.perf_counter() - t
print(f"time.time: {dt*1e9/N:.0f} ns each")

# getpid -- реальный syscall:
import os
t = time.perf_counter()
for _ in range(N):
    os.getpid()
dt = time.perf_counter() - t
print(f"os.getpid: {dt*1e9/N:.0f} ns each")

Типичные результаты на современном x86_64 (3 GHz):

time.time: 30 ns each       <- vDSO
os.getpid: 250 ns each      <- real syscall

Разница ~8x. На системах с KPTI разница может быть и 20x.


Альтернативные пути ускорения syscall

vDSO — не единственный механизм:

1. io_uring (Linux 5.1+). Новый async I/O API, который минимизирует syscalls для I/O. Можно пакетировать тысячи операций в одном syscall. Прорыв для high-perf I/O.

2. eBPF. Запуск user-defined программ внутри kernel. Не «уменьшает» syscall, но позволяет выполнять часть логики на стороне kernel, избегая returning data в user space.

3. SCHED_FIFO / busy-wait. Real-time приложения могут делать busy-wait на shared memory, избегая epoll_wait syscall. Сжигают CPU, но получают latency ~ns.

4. DPDK / userspace networking. Сети без kernel — сетевая карта мapпится в user space, пакеты обрабатываются без syscall.

vDSO — самый универсальный и автоматический. Для всех остальных нужны изменения в коде. Но vDSO покрывает только timer-related вызовы.

Иерархия памяти: почему vDSO выигрывает у syscall именно из-за кэша

Реальный пример: high-frequency timestamps

Logging-фреймворк, который пишет timestamp на каждую запись, может стать bottleneck на нагрузке:

# Сколько мы можем сделать timestamp'ов в секунду:
import time
N = 5_000_000
t = time.perf_counter()
for _ in range(N):
    ts = time.time()
dt = time.perf_counter() - t
print(f"timestamps/sec: {N/dt:.0f}")
# На современном CPU: ~30M/sec

С реальным syscall (если бы каждый clock_gettime был syscall) было бы ~3M/sec в лучшем случае. vDSO даёт 10x.

В высокопроизводительных системах (HFT, ad-tech) часто еще используют rdtsc напрямую:

// inline assembly -- сырая инструкция rdtsc:
static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ __volatile__("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

Стоимость ~10-20 ns — ещё в 2-3 раза быстрее clock_gettime через vDSO. Минус: нужно самим конвертировать в время.


Cross-architecture: vDSO на ARM64, RISC-V

vDSO — не только x86_64. На современных архитектурах он тоже есть, с теми же функциями:

# Посмотреть vDSO на ARM64:
cat /proc/self/maps | grep vdso
# 7fff8a... r-xp ... [vdso]

# Содержимое vDSO символы:
LD_BIND_NOW=1 ldd /bin/ls | grep linux-vdso
# linux-vdso.so.1 (0x00007fff...)

# Файл linux-vdso.so.1 виртуальный, не существует на диске.
# Анализ символов:
objdump -T <бинарь> | grep VDSO
# Or extract vDSO content:
sudo dd if=/proc/self/mem of=/tmp/vdso bs=1 skip=$VDSO_OFFSET count=4096

Каждая архитектура определяет свои vDSO-функции в arch/*/kernel/vdso/. Имена символов могут отличаться (__vdso_clock_gettime на x86_64, __kernel_clock_gettime на ARM64), но семантика та же.


Попробуй сам

# 1. Найти vDSO в своём процессе:
cat /proc/self/maps | grep -E 'vdso|vvar'

# 2. Адрес vDSO рандомизирован на каждый запуск:
for i in 1 2 3; do cat /proc/self/maps | grep vdso; done
# Разные адреса -- ASLR

# 3. Замерить разницу в Python:
python3 -c "
import time, os

N = 1_000_000
start = time.perf_counter()
for _ in range(N):
    time.time()
print(f'time.time (vDSO): {(time.perf_counter()-start)*1e9/N:.0f} ns')

start = time.perf_counter()
for _ in range(N):
    os.getpid()
print(f'os.getpid (syscall): {(time.perf_counter()-start)*1e9/N:.0f} ns')
"

# 4. Проверить через strace, что clock_gettime не идёт в kernel:
strace -e clock_gettime,gettimeofday python3 -c "
import time
for i in range(1000):
    time.time()
" 2>&1 | grep -c clock_gettime
# Должно быть 0 -- идёт через vDSO

# 5. Сравнить с getpid:
strace -e getpid python3 -c "
import os
for i in range(100):
    os.getpid()
" 2>&1 | grep -c getpid
# Будут 100 -- это реальный syscall

# 6. Посмотреть символы vDSO:
LD_BIND_NOW=1 ldd /bin/ls | grep vdso
# Покажет что-то вроде: linux-vdso.so.1 (0x...)

# 7. Содержимое vvar (требует прав, опасно):
# Не делайте на production!
# В Python:
# import mmap, os
# with open('/proc/self/maps') as f:
#     ... (найти адрес vvar)

# 8. Минимальный C-код с inline rdtsc (если есть gcc):
cat > rdtsc_test.c << 'EOF'
#include <stdio.h>
#include <stdint.h>

static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ __volatile__("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

int main(void) {
    uint64_t a = rdtsc();
    uint64_t b = rdtsc();
    printf("rdtsc cost: %lu cycles\n", b - a);
    return 0;
}
EOF
gcc rdtsc_test.c -o rdtsc_test && ./rdtsc_test

Проверка знанийKnowledge check
HFT-команда замеряет: каждый log.info() занимает 800 наносекунд. Профилирование показывает, что 60% времени уходит на 'datetime.now()' для timestamp. Что нужно объяснить и какие 3 уровня оптимизации возможны?
ОтветAnswer
Здесь два эффекта: накладные расходы Python и накладные расходы получения времени. Объясняю по слоям. datetime.now() в Python включает в себя: 1. Python-объект создаётся (Datetime, аллокация в heap) 2. Под капотом вызывается time.time() или clock_gettime(REALTIME) 3. Конверсия в datetime structure (год, месяц, день, час, минута, секунда, микросекунды) 4. Wrapping в Python object с заполнением полей Уже на этих шагах 100+ Python-операций. Сам clock_gettime через vDSO занимает 30-50 ns, остальное -- Python overhead. 3 уровня оптимизации: УРОВЕНЬ 1 -- использовать time.time() вместо datetime.now(): ```python import time ts = time.time() # float seconds since epoch # Конвертация в строку только при выводе: formatted = f"{ts:.6f}" ``` time.time() возвращает float напрямую из clock_gettime через vDSO. ~50-80 ns. Создание datetime-объекта дорогое из-за struct fields, format'инга, etc. Это даёт x5-10 ускорение. УРОВЕНЬ 2 -- batch timestamps: ```python # Если пишете лог-batch'ами (Kafka producer), брать один timestamp на batch: batch_ts = time.time() for msg in batch: log_with_ts(msg, batch_ts) ``` Если у вас 1000 сообщений в batch и точность ~1ms допустима -- один clock_gettime на batch вместо 1000. Это даёт x1000 в этой части. Подходит, когда экстремальная точность не нужна. УРОВЕНЬ 3 -- raw rdtsc (для extreme HFT): ```c // В C/C++ через inline asm: static inline uint64_t rdtsc(void) { uint32_t lo, hi; __asm__ __volatile__("rdtscp" : "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) | lo; } // ~10-20 ns -- быстрее vDSO clock_gettime. ``` В Python через ctypes можно вызывать функции из shared object с rdtsc. Конверсия в реальное время делается offline (TSC frequency known). Что выбрать: - Если время в логе нужно для дебага (ms точность OK) -- УРОВЕНЬ 1 (time.time + format в строку только при flush). - Если высокая нагрузка и batch processing -- УРОВЕНЬ 2 (один timestamp на batch). - Если HFT с ns-точностью на event -- УРОВЕНЬ 3 (raw rdtsc), но нужен C/C++ или Rust. Дополнительные оптимизации логирования: - Async log appender (queue + dedicated writer thread). - Лог-format через f-string или просто id+args, не format() который медленный. - Binary log format (Protocol Buffers, MessagePack) вместо JSON. - Меньше log.info, больше log.debug которые отключаются в production. Часто Python-overhead доминирует над clock_gettime/datetime. vDSO даёт ns-уровень, но Python преобразования всё равно микросекундные. Если вам нужны nano-seconds -- Python неподходящий язык, переход на C/C++/Rust может быть необходим.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что такое vDSO в контексте Linux?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4