从 20 倍性能差距看 Linux 的 vDSO 与 vvar 机制

从 20 倍性能差距看 Linux 的 vDSO 与 vvar 机制

在学习操作系统时,我们或许都曾在 /proc/self/maps 的输出末尾瞥见过这两个神秘的内存段:[vvar][vdso]。课堂上它们往往被一笔带过------"这是为了减少系统调用开销设计的"。今天,就让我们深入探究这对搭档是如何在用户态优雅地"绕过"内核,实现性能飞跃的。

bash 复制代码
$ cat /proc/self/maps
...
7ffdd208d000-7ffdd2091000 r--p 00000000 00:00 0                          [vvar]
7ffdd2091000-7ffdd2093000 r-xp 00000000 00:00 0                          [vdso]
7ffdd1fe7000-7ffdd2008000 rw-p 00000000 00:00 0                          [stack]

场景引入:获取时间戳

我们以 gettimeofday 这个高频系统调用为例。在 Linux 中,获取当前时间是极为常见的操作------日志打印、性能计时、超时判断都依赖它。

方式一:通过 libc 调用

c 复制代码
#include <stdio.h>
#include <sys/time.h>

int main()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    printf("seconds: %ld\n", tv.tv_sec);
    return 0;
}

glibc 的 gettimeofday 实现非常聪明:它会优先检查 __vdso_gettimeofday 是否可用。如果可用,直接跳转到 vDSO 中的实现,全程用户态执行,不陷入内核;只有在 vDSO 不可用(例如某些旧内核或特定架构)时,才会回退到传统的 syscall 路径。

我们用 strace 验证一下是否真的避开了系统调用:

bash 复制代码
$ strace -e gettimeofday ./a.out
seconds: 1779362307
+++ exited with 0 +++

输出中没有 gettimeofday 系统调用的痕迹,证明它确实走了 vDSO 路径。

方式二:直接系统调用

作为对比,我们绕过 libc,直接用 syscall() 发起系统调用:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/syscall.h>

int sys_gettimeofday(struct timeval* tv, struct timezone* tz)
{
    return syscall(SYS_gettimeofday, tv, tz);
}

int main()
{
    struct timeval tv;
    sys_gettimeofday(&tv, NULL);
    printf("seconds: %ld\n", tv.tv_sec);
    return 0;
}
bash 复制代码
$ strace -e gettimeofday ./a.out
gettimeofday({tv_sec=1779362323, tv_usec=339161}, NULL) = 0
seconds: 1779362323
+++ exited with 0 +++

这次 strace 清晰地捕捉到了 gettimeofday 系统调用。

性能对比:100 万次调用

将以上两种方案各执行 100 万次,结果令人震惊:

bash 复制代码
$ time ./libc_time     # 走 vDSO

________________________________________________________
Executed in   27.79 millis    fish           external
   usr time   27.35 millis    0.00 micros   27.35 millis
   sys time    0.44 millis  438.00 micros    0.00 millis

$ time ./syscall_time  # 走 syscall

________________________________________________________
Executed in  556.01 millis    fish           external
   usr time  326.03 millis  323.00 micros  325.71 millis
   sys time  226.55 millis  147.00 micros  226.41 millis

直接系统调用版本耗时约为 vDSO 版本的 20 倍!

差距主要来自两个方面:

  1. 上下文切换 :每次 syscall 都会触发特权级切换(用户态 → 内核态 → 用户态),涉及寄存器保存、页表切换、TLB 刷新等开销。
  2. 内核路径执行:进入内核后,需要经过系统调用分发、参数校验、VDSO 数据读取等一系列流程。

原理解析:内核如何把代码"借"给用户态

vDSO(virtual Dynamic Shared Object)和 vvar(virtual VARiables)的本质,是内核在创建进程时,将一小部分只读的代码和数据映射到用户空间的特定区域:

属性 作用
[vdso] r-xp(可读、可执行) 存放可由用户态直接执行的内核代码
[vvar] r--p(只读) 存放内核维护的、对用户态可见的变量

gettimeofday 为例,内核中的实现逻辑可以简化为:

c 复制代码
void vdso_gettimeofday(struct timeval* tv, struct timezone* tz)
{
    // 直接从 vvar 映射的内存读取时间
    tv->tv_sec = vvar_page->wall_time_sec;
    tv->tv_usec = vvar_page->wall_time_nsec / 1000;
}

内核初始化时会:

  1. vdso_gettimeofday 等函数的机器码写入 [vdso]
  2. wall_time_secwall_time_nsec 等变量映射到 [vvar]
  3. 每次时钟中断或定时更新时,内核更新 [vvar] 中的变量值

于是用户态进程调用 gettimeofday 时,实际只是在执行自己地址空间中的一小段代码,从同地址空间的只读内存中取值------没有陷入内核,没有上下文切换,就像调用普通函数一样快。

哪些系统调用享受了 vDSO 待遇?

并非所有系统调用都适合 vDSO 化。只有满足以下条件的调用才会被优化:

  • 只读操作:不会修改进程状态或内核数据结构
  • 数据由内核维护:例如时间、CPU 信息、进程 ID 等
  • 对一致性要求可控:允许极短暂的数据延迟(纳秒级)

目前 Linux vDSO 通常包含以下函数(具体取决于架构和内核版本):

  • __vdso_clock_gettime
  • __vdso_gettimeofday
  • __vdso_time
  • __vdso_getcpu
  • __vdso_clock_getres

小结

vDSO 和 vvar 是 Linux 内核向用户态伸出的"友好之手"------它用极低的成本(几个内存页),为高频率、只读型的系统调用开辟了一条"高速公路"。

对于开发者而言,好消息是:只要你通过 glibc 调用 gettimeofdayclock_gettime 等函数,就已经在享受这一优化了,无需额外操作。但理解其背后的原理,能帮助我们在编写高性能程序时,更清楚地知道"时间从哪来",以及为什么某些操作比其他操作"更轻"。

下次再看到 /proc/self/maps 中的 [vdso][vvar],希望你会心一笑:那是内核留给用户态的一份精心准备的礼物。

相关推荐
岑梓铭2 天前
考研408《操作系统》复习笔记,第四章《4.2.2 文件操作、文件共享、文件保护》
linux·考研·操作系统·文件系统·408
CS创新实验室2 天前
现代操作系统与大学教材的鸿沟:从理论抽象到工程实践的跨越
操作系统
明朝冰红茶2 天前
《30天自制操作系统》 Day1学习笔记
操作系统
小宇子2B3 天前
vm_area_struct:进程地址空间的合法区间表
操作系统
星马梦缘3 天前
操作系统计算题专项练习(15题)
操作系统·os·银行家算法·段页式
小宇子2B4 天前
物理内存的地图:node、zone、page frame 和 struct page
操作系统
阿昭L5 天前
操作系统复习(九)
操作系统
阿昭L5 天前
操作系统复习(七)
操作系统
小宇子2B6 天前
虚拟地址不是内存:Linux 如何切开一个进程的地址空间
操作系统
饼干哥哥8 天前
ChatGPT会员掉了,代充黑幕藏不住了
人工智能·操作系统·产品