编辑
2025-01-22
工作知识
0
请注意,本文编写于 135 天前,最后修改于 135 天前,其中某些信息可能已经过时。

目录

一、什么是vDSO
二、系统的vDSO
2.1 ldd查看
2.2 maps查看
三、vDSO的位置
四、程序实验
4.1 vdso测试
4.2 syscall测试
五、结论

linux系统中有一个很有意思的共享库,名字为linux-vdso.so.1,这个库我们在rootfs中找不到实体,但是每个elf文件都需要链接它。之前和同事讨论的时候,同事想要了解elf的运行原理,我顺便提出了vDSO的这个东西,elf不必多说,相信大家都清楚,本文本着普及了解vDSO的目的,介绍一下什么是vDSO,以及深入了解vDSO。

一、什么是vDSO

vDSO是virtual dynamic shared object,也就是虚拟的动态链接库。

关于vDSO的解释,第一次看到的时候是如下文章,讲解的很仔细,可以看看:

https://www.kernel.org/doc/Documentation/ABI/stable/vdso

对于更详细的文章,可以看如下:

https://lwn.net/Articles/615809/

根据链接的意思,对于每个应用程序,会主动加载vDSO程序到进程空间,这样提供高度优化的syscall方案,也就是加快了系统的syscall的调用性能。

关于arm的实现,我们可以查看如下ppt

image.png

二、系统的vDSO

对于系统中的vDSO,我们两个地方可以查看。以systemd为例

2.1 ldd查看

# ldd /usr/bin/systemd linux-vdso.so.1 (0x0000007f8888a000)

这里我们看到程序未运行时,默认有一个linux-vdso.so.1加载地址

2.2 maps查看

# cat /proc/1/maps | grep "vdso\|vvar" 7f93766000-7f93768000 r--p 00000000 00:00 0 [vvar] 7f93768000-7f93769000 r-xp 00000000 00:00 0 [vdso]

可以看到systemd运行的时候,实际的重定向后的地址是0x7f93768000,这个地址小于ld-2.31,大于其他动态链接库。

7f93736000-7f93737000 rw-p 0000b000 b3:04 143061 /usr/lib/aarch64-linux-gnu/libdrm-cursor.so.1.0.0 7f93737000-7f93738000 rw-p 00000000 00:00 0 7f93738000-7f93759000 r-xp 00000000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so 7f93759000-7f93765000 rw-p 00000000 00:00 0 7f93766000-7f93768000 r--p 00000000 00:00 0 [vvar] 7f93768000-7f93769000 r-xp 00000000 00:00 0 [vdso] 7f93769000-7f9376a000 r--p 00021000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so 7f9376a000-7f9376c000 rw-p 00022000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so

所以这里我们可以获取两个信息:

  • vdso是一个常规动态链接库
  • vdso是ld加载后主动加载的动态库

三、vDSO的位置

带着上面的结论,我们可以知道,这个文件应该是在内核的,所以其位置如下:

arch/arm64/kernel/vdso/vdso.so

因为其是动态链接文件,所以是标准的elf文件,我们可以如下查看:

# readelf -l vdso.so Elf 文件类型为 DYN (共享目标文件) Entry point 0x320 There are 4 program headers, starting at offset 64 程序头: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000990 0x0000000000000990 R E 0x10 DYNAMIC 0x0000000000000860 0x0000000000000860 0x0000000000000860 0x0000000000000110 0x0000000000000110 R 0x8 NOTE 0x00000000000002c8 0x00000000000002c8 0x00000000000002c8 0x0000000000000054 0x0000000000000054 R 0x4 GNU_EH_FRAME 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x8 Section to Segment mapping: 段节... 00 .hash .dynsym .dynstr .gnu.version .gnu.version_d .note .text .eh_frame .dynamic .got .got.plt 01 .dynamic 02 .note 03

我们看看其中的符号,如下

# readelf -s vdso.so Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LINUX_2.6.39 2: 0000000000000780 108 FUNC GLOBAL DEFAULT 7 __kernel_clock_getres@@LINUX_2.6.39 3: 00000000000007f0 8 NOTYPE GLOBAL DEFAULT 7 __kernel_rt_sigreturn@@LINUX_2.6.39 4: 00000000000005c0 424 FUNC GLOBAL DEFAULT 7 __kernel_gettimeofday@@LINUX_2.6.39 5: 0000000000000320 664 FUNC GLOBAL DEFAULT 7 __kernel_clock_gettime@@LINUX_2.6.39

可以发现,这个so就提供了四个函数符号。

也就是说,如果程序调用这四个符号,则默认优先调用vdso,而不是直接系统调用

四、程序实验

为了测试验证vDSO的功能,我们以gettimeofday为例,编写程序,用于测试vDSO,如下是代码

#include <sys/syscall.h> #include <sys/time.h> #include <sys/auxv.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { struct timeval tv; int i; unsigned int loop; unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR); if(argc==1 || (strcmp(argv[1], "--help")==0)){ printf("Usage:\n"); printf("\t %s %s %s %s\n", argv[0], "vdso|syscall", "count", "loop"); return 0; } if (argc == 3) loop = atoi(argv[2]); else { loop = 1000; } printf("pid=%d sysinfo_ehdr(vdso_addr)=%#lx \n", getpid(), sysinfo_ehdr); if (strcmp(argv[1], "vdso") == 0) { int (*ptr)(struct timeval *, void *) = gettimeofday; printf("gettimeofday addr=%p \n", ptr); for (i = 0; i < loop; i++){ gettimeofday(&tv, NULL); } } else if (strcmp(argv[1], "syscall") == 0){ for (i = 0; i < loop; i++) syscall(__NR_gettimeofday, &tv, NULL); } if(argc == 4 && (strcmp(argv[3], "loop")==0)){ while(1){ sleep(60); } } return 0; }

默认此程序如下提示:

# ./test_vdso Usage: ./test_vdso vdso|syscall count loop

我们可以进行两项基准测试:

vdso syscall

count代表循环的次数,loop代表是否进入死循环。

4.1 vdso测试

我们以1次的vdso测试,如下:

# ./test_vdso vdso 1 loop pid=53329 sysinfo_ehdr(vdso_addr)=0x7f96689000 gettimeofday addr=0x7f966895c0

我们拿到了两个地址,一个是vdso_addr=0x7f96689000,一个是函数符号地址 gettimeofday=0x7f966895c0

此时我们可以查看maps,如下:

# cat /proc/$(pidof test_vdso)/maps | grep "\[vdso\]" 7f96689000-7f9668a000 r-xp 00000000 00:00 0 这里看到0x7f96689000能够对应上AT_SYSINFO_EHDR

此时我们查看gettimeofday的符号地址如下:

00000000000005c0 424 FUNC GLOBAL DEFAULT 7 __kernel_gettimeofday@@LINUX_2.6.39

可以发现其计算如下:

0x7f966895c0 = 0x7f96689000 + 00000000000005c0

我们使用ltrace定位如下:

# ltrace ./test_vdso vdso 1 __libc_start_main(0x55756a0a9c, 3, 0x7fddd9a658, 0x55756a0cc0 <unfinished ...> getauxval(33, 0, 0x7fddd9a678, 0x55756a0a9c) = 0x7f80bba000 strcmp("vdso", "--help") = 73 atoi(0x7fddd9b4f9, 0x55756a0d61, 118, 45) = 1 getpid() = 59791 printf("pid=%d sysinfo_ehdr(vdso_addr)=%"..., 59791, 0x7f80bba000pid=59791 sysinfo_ehdr(vdso_addr)=0x7f80bba000 ) = 48 strcmp("vdso", "vdso") = 0 printf("gettimeofday addr=%p \n", 0x7f80bba5c0gettimeofday addr=0x7f80bba5c0 ) = 32 gettimeofday(0x7fddd9a4e8, 0) = 0 __cxa_finalize(0x55756b2008, 0x55756a0a50, 0x11d20, 1) = 0x7f80b4ec70 +++ exited (status 0) +++

这里看到ltrace调用能够定位到其调用了动态库的gettimeofday函数。我们strace查看调用如下:

strace ./test_vdso vdso 1 2>&1 | grep "gettimeofday("

可以发现vdso的时候,调用gettimeofday并不会产生系统调用。

至此,我们可以知道,代码里gettimeofday(&tv, NULL);的调用就是调用的vdso.so里面的__kernel_gettimeofday@@LINUX_2.6.39

此时我们将count放大为1亿次调用,统计时间如下:

# time ./test_vdso vdso 100000000 pid=58465 sysinfo_ehdr(vdso_addr)=0x7f8e1b8000 gettimeofday addr=0x7f8e1b85c0 real 0m3.946s user 0m3.940s sys 0m0.007s 可以发现,用时3.946s

4.2 syscall测试

我们以1次的syscall测试,通过strace查看系统调用,如下:

# strace ./test_vdso syscall 1 2>&1 | grep "gettimeofday(" gettimeofday({tv_sec=1734333991, tv_usec=512630}, NULL) = 0

可以发现其通过syscall下发的gettimeofday。

我们尝试看看ltrace的信息

# ltrace ./test_vdso syscall 1 2>&1 __libc_start_main(0x558d780a9c, 3, 0x7fe207c958, 0x558d780cc0 <unfinished ...> getauxval(33, 0, 0x7fe207c978, 0x558d780a9c) = 0x7fb95b3000 strcmp("syscall", "--help") = 70 atoi(0x7fe207d4f9, 0x558d780d61, 115, 45) = 1 getpid() = 63596 printf("pid=%d sysinfo_ehdr(vdso_addr)=%"..., 63596, 0x7fb95b3000pid=63596 sysinfo_ehdr(vdso_addr)=0x7fb95b3000 ) = 48 strcmp("syscall", "vdso") = -3 strcmp("syscall", "syscall") = 0 syscall(169, 0x7fe207c7e8, 0, 0x11b033b440000) = 0 __cxa_finalize(0x558d792008, 0x558d780a50, 0x11d20, 1) = 0x7fb9547c70 +++ exited (status 0) +++

可以发现ltrace这里没有gettimeofday。

此时我们将count放大为1亿次调用,统计时间如下:

# time ./test_vdso syscall 100000000 pid=64134 sysinfo_ehdr(vdso_addr)=0x7fb55c0000 real 0m16.279s user 0m3.927s sys 0m12.352s

可以发现,用时16.279s,主要耗时在syscall上。

五、结论

我们可以发现,对于vDSO而言,linux设计了一个动态库,使其默认通过vDSO的共享地址调用函数,而不需要使用系统调用,其在1亿次为基准的情况下能够是syscall的5-6倍的性能提升。