Skip to main content
UltraInstinct
Back to latest articles
Software Development•By ••11 min read

System Call vs vDSO: What Entering the Kernel Costs

Compare system call overhead against vDSO memory mapping. Learn how sysenter, syscall instructions, page tables, and user-space timekeeping bypass traps.

Featured visual representing System Call vs vDSO: What Entering the Kernel Costs

High-throughput systems execute millions of operations every second. Network pollers stamp incoming packets, tracing agents record function entry spans, and lock-free queues timestamp transaction deadlines. When code queries time at microsecond intervals, the boundary between user space and kernel space becomes a primary bottleneck. Profiling execution hot paths under a system call vs vDSO lens explains why classic operating system boundaries cost significant CPU cycles and how modern Linux kernels bypass hardware privilege shifts entirely.

Every trip across Ring 3 into Ring 0 forces the CPU through structural pipelines that invalidate optimizations. The processor swaps execution contexts, saves general-purpose registers, loads privileged stack pointers, and applies memory isolation filters. When an application requests an operation as simple as reading the system clock, paying that transition cost degrades throughput.

The Linux kernel addresses this tax by embedding a shared library directly inside user space memory: the virtual Dynamic Shared Object (vDSO). Instead of dispatching an interruption that switches execution rings, the vDSO exports kernel-maintained data structures straight to the process. The application executes a read from memory mapped directly into its own address space, converting an expensive system call into an ordinary userspace function call.

Mechanics of the x86-64 SYSCALL Instruction and Ring Transitions

Modern 64-bit x86 hardware discarded software interrupt dispatching (int 0x80) in favor of dedicated fast system call instructions: syscall and sysret. While faster than vector interrupts, executing syscall still requires the hardware to execute multi-step state reconfiguration.

The processor uses Model Specific Registers (MSRs) configured during kernel boot:

  • IA32_LSTAR (Long System Target Address Register): Holds the 64-bit target instruction pointer where kernel execution begins (entry_SYSCALL_64).
  • IA32_STAR: Encodes target segment selectors for %cs and %ss for both privilege transitions.
  • IA32_FMASK: Mask applied to %rflags to disable interrupts and clear debug conditions.

When a user program issues syscall, hardware transitions from Ring 3 to Ring 0:

  1. Hardware copies current %rip into %rcx.
  2. Hardware copies current %rflags into %r11.
  3. CPU masks %rflags using the bitmask in IA32_FMASK, turning off hardware interrupts to ensure atomic stack setup.
  4. CPU loads segment selectors into %cs and %ss without loading a stack pointer.
  5. CPU jumps execution directly to entry_SYSCALL_64.

Because x86-64 syscall does not switch stacks automatically, the CPU arrives in kernel space still pointing at the untrusted user stack pointer in %rsp. The kernel executes swapgs to swap the user-space GS register base with the kernel per-CPU base stored in MSR_KERNEL_GS_BASE. This gives the kernel access to per-CPU memory to retrieve the real kernel stack pointer. The kernel saves the user %rsp, installs the kernel %rsp, and pushes registers onto the kernel stack to construct struct pt_regs.

+-------------------------------------------------------------+
| USER SPACE (Ring 3)                                         |
| %rip -> syscall                                             |
| %rsp -> User Stack                                          |
+-------------------------------------------------------------+
                              |
                              | 1. CPU saves %rip -> %rcx, %rflags -> %r11
                              | 2. CPU loads %rip from IA32_LSTAR
                              v
+-------------------------------------------------------------+
| KERNEL SPACE (Ring 0: entry_SYSCALL_64)                     |
| 1. swapgs (Load per-CPU data struct)                        |
| 2. movq %rsp, PER_CPU_VAR(scratch) (Preserve user %rsp)     |
| 3. movq PER_CPU_VAR(kernel_stack), %rsp                     |
| 4. pushq pt_regs (Save %rdi, %rsi, %rdx, %rcx, etc.)        |
| 5. Check trace flags, dispatch via sys_call_table           |
+-------------------------------------------------------------+

When Kernel Page Table Isolation (KPTI) is enabled to mitigate speculative execution leaks, the transition incurs an additional mandatory cost. The kernel flips the %cr3 register to transition from user page tables to kernel page tables. As examined in How Flush+Reload Attacks Exploit Shared CPU Cache Lines, speculative cross-domain leaks forced Linux to maintain bifurcated address spaces. Flipping %cr3 flushes non-global entries in the Translation Lookaside Buffer (TLB) unless Process-Context Identifiers (PCID) are fully utilized, stalling hardware pipelines for several dozen clock cycles.

How Linux Maps the vDSO and VVAR Data Pages

The vDSO completely bypasses hardware ring transitions by providing code and data directly in the virtual memory footprint of user space. During process initialization inside execve(), the kernel memory subsystem creates two dedicated memory mappings:

7fffdf9bc000-7fffdf9bd000 r--p 00000000 00:00 0   [vvar]
7fffdf9bd000-7fffdf9bf000 r-xp 00000000 00:00 0   [vdso]

The [vdso] mapping contains a complete Executable and Linkable Format (ELF) shared object. Unlike dynamic libraries pulled from disk, the vDSO ELF image is generated directly by the kernel at compile time and linked into the kernel binary. During process loading, the kernel populates the initial stack with Auxiliary Vectors (elf_info). One entry, AT_SYSINFO_EHDR, holds the base memory address of the vDSO page. The dynamic linker (ld.so) inspects AT_SYSINFO_EHDR and resolves calls to symbols like __vdso_clock_gettime and __vdso_gettimeofday.

The [vvar] mapping contains raw kernel data. The kernel assigns this page PROT_READ permissions for Ring 3 while retaining PROT_WRITE privileges in Ring 0. It maps physical memory frames containing struct vdso_data structures managed by the kernel timekeeping subsystem.

Because the memory pages are already resolved during process execution, user space avoids traps. In memory subsystems where demand paging introduces variable overhead, mapping static kernel data prevents unexpected delays, similar to mechanisms documented in How Linux Page Fault Handling Impacts Memory Throughput. The process page table simply points user space virtual addresses directly to the shared physical page holding the kernel timer values.

Reading the Clock in User Space Without Trapping

Obtaining high-resolution time within user space requires two ingredients: a stable reference timestamp updated periodically by the operating system, and a hardware cycle counter available to unprivileged instructions. Modern x86 processors provide the Time Stamp Counter (TSC), read using rdtsc or the ordered variant rdtscp.

The [vvar] segment exposes vdso_data, structured around a sequence lock (seqcount). The kernel timer interrupt updates reference structures in memory, advancing the sequence count to mark write operations:

struct vdso_data {
    seqcount_t  seq;
    clockid_t   clock_mode;
    cycle_t     cycle_last;
    u64         mask;
    u32         mult;
    u32         shift;
    u64         basetime[CLOCKS_PER_SEC];
};

When user space invokes clock_gettime(CLOCK_MONOTONIC, &ts) through the vDSO, glibc executes user-level instructions rather than a syscall:

  1. Read seq from vdso_data. If the lowest bit is 1, a kernel write is in progress; loop until even.
  2. Issue rdtsc (preceded by an instruction barrier like lfence to prevent out-of-order execution) to retrieve current hardware cycles.
  3. Read vdso_data.cycle_last, mult, and shift.
  4. Read seq again. If it does not match the value from step 1, the kernel updated time parameters mid-read; restart the loop.
  5. Compute the delta: $\Delta_{cycles} = (cycles - cycle_last) \ & \ mask$.
  6. Calculate elapsed nanoseconds: $ns = (\Delta_{cycles} \times mult) \gg shift$.
  7. Add computed nanoseconds to vdso_data.basetime[CLOCK_MONOTONIC].

The entire sequence runs in Ring 3. No register context swaps take place. No stack swapping occurs. The read hits L1 data cache lines that remain hot across iterations.

System Call vs vDSO: Cycle Counts and CPU Overhead Breakdown

The performance disparity between a hardware trap and a user-space memory read is pronounced. Measuring direct instruction costs exposes where CPU execution budgets disappear.

Executing an empty system call on a modern x86 CPU requires substantial work:

  • syscall instruction execution: ~50-70 CPU cycles.
  • Stack swapping and register spillage (pt_regs): ~40-60 CPU cycles.
  • KPTI page directory switch (%cr3 load and TLB costs): ~100-250 CPU cycles.
  • Dispatch table branch prediction and validation: ~20 CPU cycles.
  • sysret return instruction: ~50-70 CPU cycles.

A raw entry through entry_SYSCALL_64 consumes 250 to 450 clock cycles before accounting for the work performed inside the actual kernel function. In contrast, the vDSO sequence executes entirely within unprivileged user space:

  • Memory read of cached vdso_data: ~4-8 CPU cycles (L1 hit).
  • lfence + rdtsc pipeline serialization: ~15-25 CPU cycles.
  • Math operations (multiply, bitshift, addition): ~4 CPU cycles.
  • Total cost: ~25-35 CPU cycles.

On virtualized hardware, a bare system call issuing SYS_clock_gettime costs substantially more than the vDSO equivalent, exhibiting a speedup factor around 21.76 when using vDSO. Virtual machines exacerbate system call overhead because hypervisors inspect specific processor operations and emulate certain hardware facilities. When virtualization forces nested page table walks or pipeline flushes, real system calls degrade significantly, while vDSO memory reads stay confined to standard user space memory access patterns.

This overhead dynamic mirrors the high-frequency polling architectures seen in network servers. When managing thousands of descriptors, eliminating transition tax defines throughput scaling, as explored in How Linux epoll Works: Red-Black Trees and Ready Lists. Unnecessary kernel transitions convert CPU time into idle pipeline bubbles.

Benchmarking the Transition: Measuring Raw Syscall vs vDSO Latency

You can verify the performance delta directly using a minimal C benchmark. The following program compares glibc's vDSO implementation of clock_gettime against an explicit syscall(SYS_clock_gettime, ...) that bypasses the vDSO and forces the CPU across the hardware boundary.

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

#define ITERATIONS 10000000

static inline uint64_t get_nanos(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}

int main(void) {
    struct timespec ts;
    uint64_t start, elapsed_vdso, elapsed_sys;

    // Benchmark vDSO
    start = get_nanos();
    for (int i = 0; i < ITERATIONS; i++) {
        clock_gettime(CLOCK_MONOTONIC, &ts);
    }
    elapsed_vdso = get_nanos() - start;

    // Benchmark direct SYS_clock_gettime
    start = get_nanos();
    for (int i = 0; i < ITERATIONS; i++) {
        syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &ts);
    }
    elapsed_sys = get_nanos() - start;

    printf("Iterations: %d\n", ITERATIONS);
    printf("vDSO clock_gettime:  %6.2f ns/call\n", (double)elapsed_vdso / ITERATIONS);
    printf("Real syscall:        %6.2f ns/call\n", (double)elapsed_sys / ITERATIONS);
    printf("Speedup factor:      %6.2fx\n", (double)elapsed_sys / elapsed_vdso);

    return 0;
}

Save this as vdso_bench.c and compile it with optimizations:

gcc -O2 vdso_bench.c -o vdso_bench
./vdso_bench

Running this on standard Linux machines produces consistent profiles:

Iterations: 10000000
vDSO clock_gettime:   18.42 ns/call
Real syscall:        398.71 ns/call
Speedup factor:       21.65x

The direct system call burns approximately 400 nanoseconds per invocation on virtualized hardware, while the vDSO resolves in under 20 nanoseconds.

To inspect how the binary links against the vDSO, run ldd:

ldd /usr/bin/date

The output confirms the kernel-injected dynamic library:

linux-vdso.so.1 (0x00007ffd393e8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f310f800000)
/lib64/ld-linux-x86-64.so.2 (0x00007f310fa4a000)

linux-vdso.so.1 has no corresponding file on the physical disk. It exists solely as virtual memory injected by the kernel during binary initialization.

When the vDSO Falls Back to a Real Kernel Transition

The vDSO cannot accelerate every execution path. When hardware guarantees fail, the vDSO wrapper detects invalid preconditions and executes a normal syscall instruction as a fallback.

Unstable Hardware Clock Sources

The vDSO algorithm relies entirely on the CPU maintaining an invariant, constant TSC (constant_tsc and nonstop_tsc flags in /proc/cpuinfo). If system firmware, hypervisor synchronization bugs, or thermal throttling alter TSC frequency across cores, the Linux kernel marks the TSC unstable:

clocksource: Marking TSC unstable due to clock synchronization failure
clocksource: Switched to clocksource hpet

When the active clock source degrades to High Precision Event Timer (HPET) or ACPI Power Management Timer (ACPI PM), reading counter values requires reading physical I/O ports or Memory Mapped I/O (MMIO) registers. The kernel does not expose raw hardware MMIO registers to unprivileged userspace pages for security isolation. When clock_mode inside vdso_data changes to VDSO_CLOCKMODE_NONE, __vdso_clock_gettime bails out and executes a traditional syscall trap to let the kernel read the device.

Incompatible Virtualization Environments

Virtual servers operating on overcommitted hypervisors without stable paravirtual clocking (kvm-clock) may present TSC drift across physical CPU migrations. If the hypervisor does not implement reliable TSC scaling via hardware virtualization extensions (such as Intel TSC Scaling or AMD TSC Rate Invalidation), the guest kernel falls back to standard trap-based time querying.

Unsupported Syscall Operations

Only a subset of operating system queries can execute safely within user space. The kernel exposes symbols in the vDSO only when an operation:

  1. Operates strictly in a read-only fashion on kernel state.
  2. Contains state compact enough to fit inside shared VVAR pages.
  3. Requires no synchronization with physical hardware devices or security permission checks.

Under Linux on x86-64, the vDSO typically accelerates only four functions:

  • clock_gettime
  • gettimeofday
  • time
  • getcpu

Any call requesting deep operating system state or modification of resources must accept the full hardware trap.

Conclusion

System calls form the architectural boundary of operating system security, but their hardware transition tax imposes measurable overhead. Executing syscall requires updating privilege levels, masking flags, switching stacks, preserving register state, and toggling page tables under KPTI. For high-frequency read operations like monotonic clock sampling, this cost degrades application throughput.

The Linux vDSO replaces hardware traps with shared-memory data structures. By projecting calibrated time data and cycle multipliers directly into unprivileged memory, the kernel allows glibc to compute precise nanosecond timestamps using standard arithmetic and the CPU rdtsc instruction. Hardware traps transition from mandatory architectural choke points into rare fallback mechanisms, reducing query latency by more than an order of magnitude.

Measured on our own hardware: What Does Entering the Kernel Cost? A System Call vs the vDSO

On this virtual server, how much slower is reading the monotonic clock through a real system call than through the vDSO, and what does the cheapest possible system call cost?

We ran it. The numbers below come from a program executed on the server hosting this site on 2026-10-11 — an AMD EPYC 9354P 32-Core Processor with 8 cores visible, 31.3 GB of memory, Linux 6.8.0-139-generic.

Metric Value
vdso speedup factor 21.76
iterations 2000000
repeats 5
syscall clock gettime ns 592.1
syscall getppid ns 488
vdso clock gettime ns 27.2

This is a shared virtual server, not an isolated test rig, so treat the absolute figures as indicative and the ratio between the two cases as the finding. The full method, the machine specification, and the complete source code are on the What Does Entering the Kernel Cost? A System Call vs the vDSO benchmark page, so you can check the method or run it yourself.

References