从 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],希望你会心一笑:那是内核留给用户态的一份精心准备的礼物。

相关推荐
fakerth1 天前
【OpenHarmony】communication_ipc模块
操作系统·openharmony
Coisinier1 天前
RHCE中shell脚本基础(磁盘剩余空间监控,Web 服务状态检查,curl 访问 Web 服务并返回状态)
linux·运维·服务器·前端·nginx·操作系统
小宇子2B3 天前
free 完再 malloc 同样大小,为什么常拿回刚还回去的那块?
操作系统
触底反弹4 天前
拷个 .exe 到新电脑就跑不起来?你缺的不是文件,是对链接的理解
c++·windows·操作系统
杊页5 天前
第一板块:Android 系统基石与运行原理 | 第二篇:Android 编译、打包与安装机制
android·操作系统
壮Sir不壮5 天前
GO语言——GMP调度模型
linux·开发语言·golang·go·操作系统·线程·协程
Surest5 天前
OpenHarmony 技术拆解(二):从 capability 看懂分布式软总线与任务迁移
操作系统
OpenAnolis小助手5 天前
如何利用 AI Agent 实现热补丁的自动化生成
人工智能·安全·ai·操作系统·agent·龙蜥
小宇子2B7 天前
缺页中断不是“出错”,是内核最忙的一条正常路径
操作系统
小宇子2B7 天前
内存不够时,内核怎么把"冷"页踢出去——swap 与页面回收
操作系统