在之前分析clock_gettime的文章中接触到了VDSO,本篇文章是对VDSO的学习总结,借鉴了很多前人的经验。
1. 什么是VDSO
vDSO:virtual DSO(Dynamic Shared Object),虚拟动态共享库,内核向用户态提供了一个虚拟的动态共享库。在 Linux 众多的系统调用中,有一部分存在以下特点:
- 系统调用本身很快,主要时间花费在
trap
过程 - 无需高特权级别权限
这部分系统调用如果能够直接在用户空间中执行,则能够对性能有较大的改善。gettimeofday
就是一个典型的例子,它仅仅只是读取内核中的时间信息,而且对于许多应用程序来说,读取系统时间是必要的同时也是频率很高的行为。
例如在ARM64平台到处的接口如下:
aarch64 functions
The table below lists the symbols exported by the vDSO.
symbol version
──────────────────────────────────────
__kernel_rt_sigreturn LINUX_2.6.39
__kernel_gettimeofday LINUX_2.6.39
__kernel_clock_gettime LINUX_2.6.39
__kernel_clock_getres LINUX_2.6.39
vdso在不同平台的命名略有不同, 如下:
user ABI vDSO name
─────────────────────────────
aarch64 linux-vdso.so.1
arm linux-vdso.so.1
ia64 linux-gate.so.1
mips linux-vdso.so.1
ppc/32 linux-vdso32.so.1
ppc/64 linux-vdso64.so.1
riscv linux-vdso.so.1
s390 linux-vdso32.so.1
s390x linux-vdso64.so.1
sh linux-gate.so.1
i386 linux-gate.so.1
x86-64 linux-vdso.so.1
x86/x32 linux-vdso.so.1
因为vdso本身是内核提供的机制,被编译进内核,所以并没有具体的文件路径,以上名称是C库访问时需要用到。
vdso和vsyscall的对比以及vdso引入linux kernel的时间可以参考
The VDSO on arm64
2. 使用VDSO
使用VDSO的方式有三种
- 使用 C 标准库
- 使用 dlopen 获取函数地址
- 使用 getauxvel 获取函数地址
具体可以参考这篇文章:articles/20220717-riscv-syscall-part3-vdso-overview.md · 泰晓科技/RISCV-Linux - Gitee.com
3. VDSO实现原理
a. vdso的编译以及如何集成到内核
可直接参考链接:泰晓科技 / RISCV-Linux
这里附上文章中的图片:
b. vdso的几个问题
vdso的初始化同样在上面的文章中讲得很详细了,我们按照如下思路再捋一遍。
1) vdso.so不是给内核用的,但是被内核包含,用户态如何调用到vdso中的代码呢?
2) 内核如何更新数据,数据放在哪里让用户态可以获取到呢
3)用户态通过vdso.so中的代码如何访问到内核中的数据呢?
c. vdso中的代码如何共享给用户态
vdso被包含进内核,而不是链接进内核,这是因为vdso.so中的代码段是给用户态进程使用的,那么很显然用户态进程需要映射代码段的地址到进程的地址空间。
首先,在vdso.S(/arch/arm64/kernel/vdso)中,vdso_start,vdso_end定义了vdso代码段的起始地址和结束地址
.globl vdso_start, vdso_end
.section .rodata
.balign PAGE_SIZE
vdso_start:
.incbin "arch/arm64/kernel/vdso/vdso.so"
.balign PAGE_SIZE
vdso_end:
.previous
vDSO 内核中代码部分地址初始化的时候,vdso_code_start和 vdso_code_end分别被赋值了 vdso_start和 vdso_end,在__vdso_init函数中,使用vdso_info[abi].cm->pages记录了代码段的物理页信息,如下:
/* Grab the vDSO code pages. */
pfn = sym_to_pfn(vdso_info[abi].vdso_code_start);
for (i = 0; i < vdso_info[abi].vdso_pages; i++)
vdso_pagelist[i] = pfn_to_page(pfn + i);
vdso_info[abi].cm->pages = vdso_pagelist;
有了物理页信息,那么用户态进程访问代码段,只需要建立物理页与进程虚拟地址空间的映射即可,用户态进程execve解析elf文件时,在内核会调用arch_setup_additional_pages,__setup_additional_pages则会从vdso_info中取出代码段和数据段的page进行映射,从而用户进程就可以访问代码段和数据段的数据了。
ret = _install_special_mapping(mm, vdso_base, VVAR_NR_PAGES * PAGE_SIZE,
VM_READ|VM_MAYREAD|VM_PFNMAP,
vdso_info[abi].dm);
if (IS_ERR(ret))
goto up_fail;
if (IS_ENABLED(CONFIG_ARM64_BTI_KERNEL) && system_supports_bti())
gp_flags = VM_ARM64_BTI;
vdso_base += VVAR_NR_PAGES * PAGE_SIZE;
mm->context.vdso = (void *)vdso_base;
ret = _install_special_mapping(mm, vdso_base, vdso_text_len,
VM_READ|VM_EXEC|gp_flags|
VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC,
vdso_info[abi].cm);
用户态映射后的示意图:
图片来自:杂谈:vdso原理 - 知乎
d. 内核如何更新vdso数据,以及用户态如何访问
有了上面访问代码段的机制,用户态访问数据的机制自然不用再说了,需要注意的是dm
的初始化在 vvar_fault
函数中实现。vvar_fault
是 dm
缺页中断的回调函数。那么内核态如何更新vsdo数据呢,主要通过update_vsyscall更新vdso_data变量
用户态调用vdso函数,以 gettimeofday
为例分析 vDSO 函数调用流程,libc 调用 vsdo.so
中 __kernel_gettimeofday
函数, __kernel_gettimeofday
访问 vvar
数据。除了第一次访问会触发 Page Fault
(实测开销大于syscall),整个过程不会陷入内核态。
gettimeofday->__kernel_gettimeofday=> special_mapping_fault
__kernel_gettimeofday->__arch_get_vdso_data=> special_mapping_fault->vvar_fault
__arch_get_hw_counter //从硬件 timer 读取 cntvct_el0 寄存器得到距离上次更新vdso_data的时间差,加上 vdso_data 里的时间得到最终时间
参考资料:
The vDSO on arm64
泰晓科技 / RISCV-Linux
杂谈:vdso原理 - 知乎