Operating Systems
The vDSO: Fast System Calls Without Entering the Kernel
Call clock_gettime() a million times a second and the vDSO turns what would be a million kernel round-trips — each burning hundreds of nanoseconds on privilege switches, register saves, and (since Meltdown) page-table swaps — into a plain user-space function call that returns in single-digit nanoseconds. The vDSO (virtual Dynamic Shared Object) is a tiny ELF shared library that the Linux kernel maps into the address space of every process at exec time, exporting a handful of hot, read-only functions that can compute their answer entirely in user mode.
Instead of trapping into ring 0 to ask the kernel "what time is it?", your program calls __vdso_clock_gettime, which reads a kernel-maintained data page and derives the answer from the CPU's timestamp counter — no mode switch, no scheduler, no TLB flush. It is the standard mechanism behind fast gettimeofday, clock_gettime, time, and getcpu on modern Linux.
- TypeKernel-mapped ELF shared library (userspace acceleration)
- Time complexityO(1) per call, ~1-30 ns vs ~100 ns-1 µs for a real syscall
- Space1-2 pages of code + 1 read-only shared data (vvar) page per process
- IntroducedLinux ~2007 (2.6.x), generalizing the earlier linux-gate/vsyscall page
- Used inglibc, databases, tracing/profilers, HFT, anything calling clock_gettime often
- Key ideaMap kernel-maintained data into userspace; compute time from the TSC without trapping
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
The problem: system calls are expensive, and some are called constantly
A system call is not a function call. To ask the kernel for a service, the CPU must switch from user mode (ring 3) to kernel mode (ring 0): it saves registers, changes stacks, validates arguments, and returns through the same gauntlet. That fixed overhead — historically ~100 ns and, after the 2018 Meltdown mitigation (KPTI) began swapping page tables on every kernel entry, often several times higher — is negligible if you make it rarely.
But some calls are made in the innermost loops of real programs. Timestamping log lines, measuring latency, computing timeouts, benchmarking, and event-loop bookkeeping all hammer gettimeofday() and clock_gettime(). A busy server might issue millions per second. Crucially, these calls are read-only and non-privileged: reading the current time does not modify kernel state or require enforcement — the kernel is just a middleman holding data the process is allowed to see.
- The insight: if the kernel can safely publish that data into userspace, the process can read it directly.
- The vehicle: a kernel-provided shared library — the vDSO — mapped into every process.
How it works: a mapped library plus a shared data page
At execve() time the kernel maps two things into the new process: the vDSO (a real, self-contained ELF shared object built into the kernel image) and a read-only vvar page holding live timekeeping data. The kernel advertises the vDSO's base address to the dynamic loader through the ELF auxiliary vector entry AT_SYSINFO_EHDR. glibc parses the vDSO's symbol table and wires up __vdso_clock_gettime, __vdso_gettimeofday, __vdso_time, and __vdso_getcpu (arm64 uses __kernel_* aliases).
The kernel updates the vvar page on each timer tick with a base wall-clock time, the TSC value at that instant, and a multiplier/shift pair. A vDSO call then, in userspace:
do {
seq = READ(vvar.seqcount); // seqlock: even = stable
if (seq & 1) continue; // writer active, retry
base = vvar.wall_time;
cycles = rdtsc() - vvar.tsc_base;
delta = (cycles * vvar.mult) >> vvar.shift;
} while (seq != READ(vvar.seqcount)); // re-read; retry if changed
return base + delta;The seqlock (seqcount) lets many lock-free readers coexist with one kernel writer: if the count changed mid-read, the reader simply retries. If the active clocksource can't be read from userspace (e.g. HPET), the vDSO transparently falls back to a real syscall.
Complexity and a worked latency trace
Every vDSO time call is O(1): a bounded read of the vvar page, one rdtsc/rdtscp, a 64-bit multiply and shift, and an add. The seqlock retry loop is O(1) amortized — a retry only happens in the vanishingly rare window where the kernel is mid-update, so expected iterations ≈ 1. Space is O(1) per process: the code pages are shared copy-nothing across all processes, plus one shared vvar page.
Concretely, compare the two paths for clock_gettime(CLOCK_MONOTONIC, &ts):
Real syscall path (no vDSO):
user->kernel trap ....... ~40-100 ns (+ KPTI CR3 swap, TLB effects)
arg check + dispatch .... ~20-40 ns
read clock + copyout .... ~20-40 ns
kernel->user return ..... ~40-100 ns
TOTAL: ~120 ns - 1 µs
vDSO path:
call __vdso_clock_gettime ~1 ns
seqlock read + rdtscp ... ~5-15 ns
mul/shift/add ........... ~2 ns
TOTAL: ~8-30 nsA ~10-40x speedup, and — critically — zero pollution of the kernel's TLB and no scheduler entry. In a loop doing 10 million timestamps, that is the difference between ~2 ms and ~200 ms of pure overhead.
Where it's used in real systems
The vDSO is invisible but ubiquitous — nearly every non-trivial Linux program hits it:
- glibc / musl:
gettimeofday,clock_gettime, andtimeroute through the vDSO automatically; you never call it by name. - Databases & storage: PostgreSQL, MySQL, and RocksDB timestamp transactions, WAL entries, and MVCC snapshots constantly; the vDSO keeps that off the syscall path.
- Observability: tracers, profilers, and metrics libraries (Prometheus client loops, latency histograms) take timestamps around every operation.
- High-frequency trading & low-latency networking: where a hundred nanoseconds is a competitive edge, vDSO timing is mandatory.
- Runtimes: the Go runtime and the JVM read the clock via the vDSO for schedulers, GC pacing, and timers.
- getcpu:
__vdso_getcpulets userspace RCU, per-CPU allocators, and sharded data structures learn their current CPU without a syscall.
Containers matter too: because the vDSO is mapped per-process from the host kernel, all containers on a machine share the same fast-path — there is no per-container time syscall tax.
Compared to the alternatives — vsyscall, raw rdtsc, and plain syscalls
The vDSO's direct ancestor is the vsyscall page (and 32-bit x86's linux-gate.so.1): a fixed-address region mapped at 0xffffffffff600000 that also served time in userspace. Its fatal flaw was the fixed address — a known executable location is an ASLR-defeating gadget source, so vsyscall is now deprecated and typically emulated via a slow trap for backward compatibility. The vDSO fixes this by being a normal, ASLR-randomized shared object located via AT_SYSINFO_EHDR.
Against a raw rdtsc: reading the timestamp counter yourself is even faster, but it gives raw, uncalibrated cycles. You get no NTP steering, no leap-second handling, no protection against TSC differences across sockets or frequency scaling, and no CLOCK_MONOTONIC semantics. The vDSO wraps rdtsc with the kernel's calibration (mult/shift, base) so you get correct, adjusted wall/monotonic time cheaply.
The tradeoff versus a plain syscall is stark but narrow: the vDSO is dramatically faster only for the specific read-only operations the kernel chose to publish. Anything that mutates kernel state — I/O, memory mapping, signals — cannot be a vDSO call and must still trap.
Pitfalls, failure modes, and why it matters
The vDSO is robust but has sharp edges:
- Clocksource fallback: if the system's clocksource isn't userspace-readable (e.g. it falls back to HPET or ACPI PM timer instead of TSC), the vDSO silently reverts to a real syscall — and your fast path quietly becomes ~20x slower. Check
/sys/devices/system/clocksource/.../current_clocksource. - Unsynchronized TSC: across NUMA sockets or older CPUs, TSCs may not be invariant/synchronized; the kernel then refuses the vDSO fast path for monotonicity safety.
- seccomp confusion: because the vDSO doesn't issue a syscall, seccomp filters and
stracecan't see these calls — great for performance, occasionally surprising when auditing or sandboxing (people wrongly assume they blockedgettimeofday). - Live migration / checkpoint-restore: tools like CRIU must carefully handle the vDSO mapping, since the code page belongs to the (possibly different) destination kernel.
Its significance is a general OS design lesson: the fastest system call is the one you never make. By publishing read-only kernel data into userspace behind a lock-free seqlock, Linux eliminated the mode-switch tax for the hottest, safest queries — a pattern echoed by io_uring's shared rings and by CPU features like rdtscp.
| Mechanism | Enters kernel? | Typical latency | Notes |
|---|---|---|---|
| Real syscall (int 0x80 / syscall) | Yes (mode switch) | ~100 ns - 1 µs+ | Register save/restore, scheduler check; KPTI page-table swap added ~significant overhead post-Meltdown |
| vsyscall page (legacy) | No (fixed address) | ~tens of ns | Deprecated: fixed address 0xffffffffff600000 breaks ASLR; now emulated (slow trap) for security |
| vDSO (__vdso_clock_gettime) | No (normal call) | ~1-30 ns | ASLR-friendly, reads vvar page, computes from TSC via seqlock; falls back to syscall if clocksource unsupported |
| Raw rdtsc in userspace | No | ~5-15 ns | Fast but gives raw cycles, not calibrated wall time; no NTP/steering; not portable across cores/sockets |
Frequently asked questions
Is the vDSO the same thing as a system call?
No — it is the opposite. A system call traps into the kernel (ring 0), while a vDSO function is ordinary user-mode code the kernel mapped into your address space. It computes its answer by reading a shared data page, with no privilege switch. The vDSO only accelerates a small set of read-only operations (time, getcpu); everything else still requires a real syscall.
How does glibc find and use the vDSO?
At program start the kernel places the vDSO's base address in the ELF auxiliary vector under AT_SYSINFO_EHDR. glibc's loader parses that in-memory ELF's dynamic symbol table and resolves symbols like __vdso_clock_gettime. When you call clock_gettime(), glibc jumps to the vDSO function if present, and falls back to a real syscall otherwise.
Why is the vDSO faster than a normal system call?
It avoids the mode switch entirely. A real syscall must save/restore registers, switch stacks, run the kernel's dispatch, and — since Meltdown/KPTI — often swap page tables (CR3), flushing TLB entries. The vDSO does none of that: it is a plain call that reads a kernel-maintained page and derives the time from the TSC, so it runs in roughly 1-30 ns versus 100+ ns.
What is the vvar page and the seqlock for?
The vvar page is a read-only page the kernel updates each tick with the base time, the TSC value at that moment, and a multiplier/shift for scaling cycles to nanoseconds. The seqlock (a seqcount) is a lock-free reader/writer protocol: readers note the count, read the data, and re-check the count; if it changed, a writer intervened and they retry. This lets unlimited userspace readers coexist safely with the single kernel writer.
How is the vDSO different from vsyscall and linux-gate.so?
vsyscall (and 32-bit linux-gate.so.1) was the predecessor: a fast-time region mapped at a FIXED address (0xffffffffff600000). That fixed, executable address weakened ASLR and became a security liability, so vsyscall is deprecated and usually emulated with a slow trap. The vDSO is a proper ASLR-randomized shared object located via AT_SYSINFO_EHDR, which is why it superseded vsyscall.
Can strace or seccomp see vDSO calls?
No. Because a vDSO call never issues a syscall instruction, strace won't log it and seccomp filters can't intercept it. This is a common gotcha: sandbox authors sometimes think they blocked gettimeofday, but the vDSO path bypasses the syscall interface entirely. If you truly need to intercept these, you must force the syscall path or disable the vDSO.