• OD(7)之time调用与Linux-vDSO机制


    OD(7)之time调用与Linux-vDSO机制

    Author:OnceDay Date: 2024年2月19日

    漫漫长路,才刚刚开始…

    全系列文章可参考专栏: Linux实践记录_Once_day的博客-CSDN博客

    参考文章:

    1. 概述

    突然发现一个忽略的知识点,本来以前总是觉得time调用耗时很多,然后就应该用gettimeofday /clock_gettime等支持vDSO的函数。

    但定位代码时,发现原来time实现(glibc)的底层未必是SYS_time函数,也就是系统调用。当然,不同平台的情况有所区别,这里测试平台是Linux4.14(aarch64)和Linux5.10(x64)。

    1.1 vDSO介绍

    Linux操作系统中,vDSO(即虚拟动态共享对象)是一个提高系统调用性能的机制。它允许程序直接在用户空间中执行一些系统调用,而无需进行传统的、开销较大的内核空间到用户空间的上下文切换。举个例子,当你在家中通过遥控器操作电视时,遥控器的按键就像vDSO,它让你不必每次都走到电视机前去操作,大大节省了你的时间和精力。

    vDSO的由来是为了解决频繁但轻量级的系统调用问题。随着计算机科学的发展,处理器速度越来越快,而内核与用户空间的交互却成为了一个瓶颈。这种上下文切换需要时间和资源,尤其是在高频调用像获取当前时间这样的简单操作时。因此,vDSO应运而生,它将这些常用的系统调用直接映射到用户空间,减少了系统调用的开销。

    使用vDSO的好处是显而易见的。首先,它提高了系统调用的效率,特别是对于那些不需要完全权限模型的轻量级调用。其次,它减少了上下文切换,节约了处理器时间,这对于需要高性能的应用程序来说至关重要。然而,vDSO也有其局限性,它不适用于所有类型的系统调用。一些复杂或需要更多权限检查的调用仍然需要传统的系统调用机制。

    在使用场景上,vDSO通常用于高性能计算和实时系统,其中任何额外的延迟都可能导致性能下降。例如,数据库系统可能会大量依赖于vDSO来获取时间戳,或者某些应用需要快速响应用户输入。

    有趣的是,vDSO的实现是随着Linux内核版本的不同而有所变化的。对于开发者而言,这意味着需要注意内核版本的变动,以确保兼容性和最优性能。

    总结而言,vDSO是Linux中一个聪明的设计,它通过一个简单且有效的方式,使得系统调用更加高效。虽然它的使用有一定的局限,但在适当的情况下,vDSO能极大地提高软件的性能。开发者在设计系统时,应充分考虑是否利用vDSO来优化性能,尤其是在对性能要求极高的应用中。

    1.2 查看vDSO函数

    查看vDSO有多种方式,比如通过GDB或者dd直接dump内存,然后objdump反编译查看。下面介绍一个比较方便的方法。

    (1) 通过/proc/self/maps确定内存映射地址,每个进程都不一样,因此需要根据实际情况确定。

    onceday->~:# cat /proc/1/maps |grep vdso
    7fffb6f8f000-7fffb6f91000 r-xp 00000000 00:00 0                          [vdso]
    onceday->~:# cat /proc/self/maps |grep vdso
    7fff76104000-7fff76106000 r-xp 00000000 00:00 0                          [vdso]
    
    • 1
    • 2
    • 3
    • 4

    这里可以算出来其映射内存大小为0x2000,也就是8192字节 。

    (2) 通过dd直接dump对应内存到文件中。

    onceday->~:# dd if=/proc/1/mem of=/tmp/linux-vdso.so skip=$((0x7fffb6f8f000)) bs=1 count=8192
    dd: /proc/1/mem: cannot skip to specified offset
    8192+0 records in
    8192+0 records out
    8192 bytes (8.2 kB, 8.0 KiB) copied, 0.0203065 s, 403 kB/s
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这条命令的组成含义如下:

    • dd: 是一个用于转换和复制文件的命令行工具。
    • if=/proc/1/mem: 指定输入文件。在这里,/proc/1/mem 是一个特殊的文件,它代表init进程的内存空间。
    • of=/tmp/linux-vdso.so: 指定输出文件。在这里,输出文件被设置为 /tmp/linux-vdso.so
    • skip=$((7fffb6f8f000)): 跳过的块数。$((7fffb6f8f000)) 是一个算术扩展,用于将十六进制地址转换为十进制数。skip 参数需要一个十进制数来指定要跳过多少个输入块。
    • bs=1: 设置块大小为 1 字节。bs 表示块大小(Block Size),bs=1 意味着按字节进行读取和写入。
    • count=8192: 指定要复制的块数,这里是 8192 块。

    (3) 通过objdump查看里面的符号名称。

    onceday->~:# objdump -T /tmp/linux-vdso.so 
    
    /tmp/linux-vdso.so:     file format elf64-x86-64
    
    DYNAMIC SYMBOL TABLE:
    0000000000000c10  w   DF .text  0000000000000005  LINUX_2.6   clock_gettime
    0000000000000bd0 g    DF .text  0000000000000005  LINUX_2.6   __vdso_gettimeofday
    0000000000000c20  w   DF .text  0000000000000063  LINUX_2.6   clock_getres
    0000000000000c20 g    DF .text  0000000000000063  LINUX_2.6   __vdso_clock_getres
    0000000000000bd0  w   DF .text  0000000000000005  LINUX_2.6   gettimeofday
    0000000000000be0 g    DF .text  000000000000002a  LINUX_2.6   __vdso_time
    0000000000000cc0 g    DF .text  000000000000009d  LINUX_2.6   __vdso_sgx_enter_enclave
    0000000000000be0  w   DF .text  000000000000002a  LINUX_2.6   time
    0000000000000c10 g    DF .text  0000000000000005  LINUX_2.6   __vdso_clock_gettime
    0000000000000000 g    DO *ABS*  0000000000000000  LINUX_2.6   LINUX_2.6
    0000000000000c90 g    DF .text  0000000000000026  LINUX_2.6   __vdso_getcpu
    0000000000000c90  w   DF .text  0000000000000026  LINUX_2.6   getcpu
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    主要有四个函数:

    • __vdso_gettimeofday: 这个函数是 gettimeofday 系统调用的 vDSO 版本。gettimeofday 函数用于获取当前的时间(包括日期和时间)。它通常比传统的系统调用更快,因为它避免了用户空间与内核空间之间的上下文切换。
    • __vdso_clock_getres: 这是 clock_getres 系统调用的 vDSO 版本。该函数用于获取一个指定时钟 clockid_t 的分辨率(即时钟每滴答一次的时间)。
    • __vdso_time: 这个函数是 time 系统调用的 vDSO 实现。time 函数用于获取自 Unix 纪元(1970年1月1日 UTC)以来的时间,以秒为单位。
    • __vdso_clock_gettime: 这是 clock_gettime 系统调用的 vDSO 版本。clock_gettime 用于获取指定时钟的时间。与 gettimeofday 相比,它可以支持多种时钟源,并且提供了纳秒级的精度。

    在这些函数名前的 wg 标志表示函数的链接可见性:

    • g (global): 符号可被其他共享对象文件引用。
    • w (weak): 符号也是全局的,但它的定义可以被其他共享对象中相同名称的强符号(strong symbols)覆盖。

    一般以前是三个函数,没有clock_getresclock_gettime,当然,有了time的vDSO实现,也不意味着glibc库的time函数走这个系统调用

    1.3 快速系统调用

    Linux的快速系统调用(fast system calls)是一个深入Linux内核的概念,它与操作系统的基础工作方式紧密相关。系统调用是用户空间程序与内核空间进行交互的桥梁,允许程序请求内核为其提供服务,如文件操作、网络通信或进程管理等。在Linux系统的发展历程中,系统调用的效率一直是优化的焦点,因为它们的性能直接影响到整个系统的响应速度和效率。

    在早期的Linux版本中,系统调用是通过中断指令(如x86架构的int 0x80)实现的。这种方式虽然简单,但是它涉及到从用户模式切换到内核模式的过程,这个过程需要保存和恢复许多寄存器,导致较大的开销。随着对性能要求的提升,这种方法显得效率不高。

    为了解决这个问题,快速系统调用(fast system calls)应运而生。它们通过更加高效的方式实现用户空间到内核空间的转换。在x86架构上,这通常是通过sysentersysexit指令来完成的,而在x86-64架构上则是使用syscallsysret指令。这些指令减少了模式切换时的开销,因为它们不需要保存和恢复全部的寄存器状态,从而大幅提升了系统调用的速度。

    这一变化对于Linux系统来说是一次重大的性能提升。它不仅让系统调用变得更快,也使得Linux能够更好地利用现代处理器的特性,提高整体的系统性能。程序员和系统管理员通常不需要直接处理快速系统调用的细节,因为这些都是由内核自动处理的。但是,了解系统调用的工作原理对于深入理解Linux系统的性能优化有着不可忽视的价值。

    随着时间的推移,Linux内核不断地进行优化和改进。例如,内核开发者们引入了如vDSO(virtual Dynamic Shared Object)这样的机制来进一步优化那些经常被调用且开销较小的系统调用。vDSO允许某些系统调用在用户空间执行,从而避免了模式切换的开销,这对于频繁进行时间获取等操作的应用程序来说,性能提升尤为显著。

    至于快速系统调用的未来,随着硬件技术的发展和新的优化技术的出现,Linux内核的系统调用机制仍然在不断进化。这种持续的改进,保证了Linux能够在性能方面跟上现代计算环境的步伐,继续作为最受欢迎的操作系统之一。对于任何关注系统性能和想要深入理解Linux内核工作原理的人来说,快速系统调用及其发展历史都是一个值得研究的领域。

    1.4 vsyscall机制

    Linux的vsyscall(虚拟系统调用)机制是一个为了提高系统调用效率而设计的特性。在深入vsyscall之前,我们先回顾一下系统调用是什么:它是用户空间程序请求内核服务的一种方式,比如获取当前时间、进程信息等。在早期Linux版本中,系统调用通常涉及到从用户模式切换到内核模式的开销,这会导致性能降低。

    vsyscall机制的引入,正是为了优化那些轻量级的,被频繁调用的系统调用。vsyscall提供了一种机制,通过将特定的系统调用实现映射到用户空间,使得这些调用可以直接在用户空间执行,从而避免了模式切换的开销。简单来说,vsyscall就像是内核为用户程序提供的一小段代码,用户程序可以不通过完整的系统调用过程,直接执行这段代码,以快速完成某些操作。

    vsyscall最典型的使用案例是获取当前的时间(例如gettimeofday函数)。在早期,每次获取时间都需要进行一次完整的系统调用。但是通过vsyscall,这个操作可以快速完成,因为获取时间的代码已经被映射到了用户空间。

    然而,vsyscall机制也有其缺点。最显著的问题是它的安全性。由于vsyscall页面是静态的,其地址在系统启动时确定,并且在所有进程中都是相同的,这使得它成为潜在的攻击目标。攻击者可以利用这一点开展安全攻击,如缓冲区溢出攻击。

    为了解决这个安全隐患,Linux内核开发者引入了vDSO(virtual Dynamic Shared Object)来替代vsyscall。vDSO同样将经常使用的系统调用功能映射到用户空间,但它是动态的,地址在每次程序启动时都会发生变化,这大大增加了安全性。此外,vDSO比vsyscall更加灵活,它可以提供更多的功能,并且可以根据不同硬件和内核版本进行优化。

    今天,vsyscall已经被vDSO所取代,但它在当时是一个重要的里程碑,标志着Linux内核在追求高性能的同时也不断考虑系统的安全性。它的引入和最终的替换,都是Linux内核适应不断变化的技术需求和安全环境的例证。对于系统开发者和安全专家而言,vsyscall机制及其演变提供了深刻洞察Linux内核如何平衡性能与安全的实践案例。

    1.5 vvar/vDSO/vsyscall三者关系

    对于每个进程,其实不只是映射了vsdo段,还有两个类似功能的代码和数据空间,如下:

    onceday->~:# cat /proc/self/maps |grep v
    7ffff39f3000-7ffff39f7000 r--p 00000000 00:00 0                          [vvar]
    7ffff39f7000-7ffff39f9000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
    
    • 1
    • 2
    • 3
    • 4

    vsyscall最早被引入,是一种提高系统调用效率的机制。如前所述,vsyscall通过将一小部分经常使用的系统调用代码映射到用户空间来避免模式切换的开销,从而提高性能。然而,vsyscall机制的缺点是其地址是固定的,因此从安全角度来看,它可能会被恶意利用,这就需要一种更安全的替代方案。总结如下:

    • vsyscall 是 vDSO 的前身,它也是一种提供某些系统调用的机制,但是它的功能比 vDSO 有限。
    • vsyscall 为了兼容性而保留,但现代 Linux 系统中已经被 vDSO 取代。
    • vsyscall 提供了一个固定的、非常小的页面,通常只包含几个系统调用,如 gettimeofday
    • 由于安全和可维护性的问题,vsyscall 已经不再推荐使用,并且在新的内核版本中可能完全移除。

    vDSO (Virtual Dynamic Shared Object)则是对vsyscall的改进和替代。vDSO同样将一些常用的系统调用功能映射到用户空间,但它是动态的,地址不固定,这使得它比vsyscall更安全。vDSO也是一个用户空间的共享库,程序可以像使用其他共享库那样使用vDSO。此外,vDSO提供的功能也比vsyscall要多,可以根据硬件和内核版本的不同进行调整和优化。总结如下:

    • vDSO 是一种提高系统调用性能的机制。传统的系统调用需要在用户态和内核态之间进行上下文切换,这个过程相对耗时。vDSO 允许某些系统调用(如获取当前时间)在用户空间执行,避免了上下文切换的开销。
    • 它是一个内存区域,映射到每个进程的地址空间中。内核在这个区域中提供一些常用的系统服务的代码。
    • 当用户程序需要执行系统调用时,它可以直接调用 vDSO 中的代码,而无需执行传统的系统调用指令,从而提高效率。
    • vDSO 通常包括 gettimeofdayclock_gettime 等时间相关的调用。

    vvar则是与vDSO紧密相关的一个机制。vvar区域包含了一些变量,这些变量是由内核维护的,并且被映射到每个进程的地址空间中。这些变量通常是与时间相关的,比如用于实现高效时间相关功能的变量。vvar区域是只读的,并且它的地址也是动态的,这样可以防止恶意程序修改这些变量。总结如下:

    • vvar 是一个与 vDSO 类似的机制,它提供了一些变量而不是函数。这些变量通常是与时间相关的,如时钟频率或者最近的时间更新。
    • vvar 区域被映射到每个进程的地址空间中,但它是只读的。
    • vvar 通常与 vDSO 配合使用,vDSO 中的函数可能会利用 vvar 中的数据来执行它们的任务。

    目前三者关系如下:

    • vsyscall 是早期的尝试,用于减少系统调用的开销,但由于安全和灵活性问题,它被 vDSO 所取代。
    • vDSO 提供了更多的系统调用支持,并且可以动态更新,是现代系统中推荐的方式。
    • vvar 通常与 vDSO 一起使用,提供给 vDSO 函数所需的数据。
    2. 使用
    2.1 vDSO链接和执行概述

    应用程序使用vDSO(virtual Dynamic Shared Object)的过程对于应用程序开发者来说是相对透明的,因为大部分的复杂性都由操作系统内核和编译器抽象掉了。然而,理解这一过程对于深入了解性能优化和系统调用的工作原理是很有帮助的。下面是应用程序如何使用vDSO mapping的步骤:

    1. 启动时映射:当一个新的进程启动时,Linux内核会自动将vDSO映射到进程的地址空间中。内核负责选择映射的地址和权限设置。这个映射是对所有进程都可见的,它通常出现在进程地址空间的某个固定位置,但其实际位置可能因不同的内核和硬件而异。

    2. 动态链接:在程序的动态链接阶段,动态链接器(ld.so)会识别出程序中需要使用vDSO提供的函数。由于vDSO是动态映射的,链接器不需要像链接其他共享库那样操作文件系统。而是会在进程的地址空间中查找内核已经映射的vDSO,并将那些函数调用链接到vDSO中的相应地址。

    3. 编译时支持:为了使应用程序能够利用vDSO提供的函数,编译器和标准库实现(如glibc)通常会提供支持。例如,当程序调用获取当前时间的函数时(如clock_gettime),标准库会尝试首先使用vDSO中的相应实现来优化调用,而不是直接发起系统调用。

    4. 透明调用:程序员在应用程序中调用某些系统应用程序使用vDSO(virtual Dynamic Shared Object)的过程对于应用程序开发者来说是相对透明的,因为大部分的复杂性都由操作系统内核和编译器抽象掉了。然而,理解这一过程对于深入了解性能优化和系统调用的工作原理是很有帮助的。下面是应用程序如何使用vDSO mapping的步骤:

    5. 启动时映射:当一个新的进程启动时,Linux内核会自动将vDSO映射到进程的地址空间中。内核负责选择映射的地址和权限设置。这个映射是对所有进程都可见的,它通常出现在进程地址空间的某个固定位置,但其实际位置可能因不同的内核和硬件而异。

    6. 动态链接:在程序的动态链接阶段,动态链接器(ld.so)会识别出程序中需要使用vDSO提供的函数。由于vDSO是动态映射的,链接器不需要像链接其他共享库那样操作文件系统。而是会在进程的地址空间中查找内核已经映射的vDSO,并将那些函数调用链接到vDSO中的相应地址。

    7. 编译时支持:为了使应用程序能够利用vDSO提供的函数,编译器和标准库实现(如glibc)通常会提供支持。例如,当程序调用获取当前时间的函数时(如clock_gettime),标准库会尝试首先使用vDSO中的相应实现来优化调用,而不是直接发起系统调用。

    8. 隐性透明调用:程序员在应用程序中调用某些系统相关的函数(如时间获取、获取进程ID等)时,实际上可能是在调用vDSO中的实现,而不是经过传统的系统调用机制。这是由于在应用程序编译和链接时,这些函数已经被透明地重定向到vDSO。

    9. 性能优化:vDSO的主要优势是减少了系统调用的开销。传统的系统调用需要在用户空间和内核空间之间进行上下文切换,而vDSO中的函数可以直接在用户空间执行,从而避免了这种切换。这对于频繁调用某些系统服务的高性能应用程序来说是一个重要的性能优化。

    10. 后备机制:如果出于某种原因,vDSO不可用或者不支持特定的操作,标准库通常会提供后备机制。这意味着它会回退到传统的系统调用。对于应用程序开发者来说,这个过程是透明的,他们不需要编写额外的代码来处理这种情况。

    在应用程序中使用vDSO并不需要开发者有特别的操作,它们只需要按照正常方式调用系统函数即可。内核和标准库的设计者已经做了很多工作来确保这些函数调用尽可能高效。这样的设计允许开发者专注于应用逻辑,同时自动享受到操作系统提供的性能优化。

    2.2 常见系统调用方式

    vDSO、系统调用(syscall)、本地vsyscall和模拟vsyscall是Linux内核提供的不同机制,用于执行用户空间程序请求的系统服务。每种机制的效率和安全性有所不同。

    vDSO (virtual Dynamic Shared Object),是最现代且推荐的方式,它通过在用户空间程序的内存中映射一个小的共享库(vDSO页面),提供一些常用的系统服务。这样,程序可以直接调用这些服务,而无需执行完整的系统调用,从而减少了上下文切换的开销。

    #include 
    #include 
    #include 
    
    int main() {
        struct timespec ts;
        // 使用vDSO提供的函数来获取时间
        clock_gettime(CLOCK_MONOTONIC, &ts);
        printf("Time: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    syscall (系统调用),是用户空间程序和内核之间通信的传统方式。当调用系统调用时,程序会陷入内核模式,内核执行请求的服务后返回用户模式。这个过程涉及到上下文切换,相比vDSO会有更大的开销。

    #include 
    #include 
    #include 
    
    int main() {
        // 直接使用系统调用来获取进程ID
        pid_t pid = syscall(SYS_getpid);
        printf("Process ID: %d\n", pid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    native vsyscall (本地vsyscall)

    vsyscall是vDSO之前的一种技术,它在固定的内存地址提供少量的系统服务。由于安全问题,现在已经很少使用,并且在新的内核版本中可能完全不可用。

    emulated vsyscall (模拟vsyscall)

    模拟vsyscall是对老旧程序的一种兼容性支持,当程序尝试访问vsyscall页面时,内核会捕获这个行为并通过模拟来提供服务。这比本地vsyscall要慢,因为涉及到陷入内核模式。

    2.2 简易性能测试

    下面编写一个小程序来测试一下vDSO和syscall的性能比较,参考文档:

    代码整理如下:

    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char **argv)
    {
        unsigned long i       = 0;
        time_t (*f)(time_t *) = (time_t(*)(time_t *))0xffffffffff600400UL;
    
        if (argc != 2) {
            fprintf(stderr, "Usage: %s [emulated-vsyscall|vdso|vsyscall]\n", argv[0]);
            return 1;
        }
    
        if (!strcmp(argv[1], "emulated-vsyscall")) {
            for (i = 0; i < 1000000; ++i) {
                f(NULL);
            }
        } else if (!strcmp(argv[1], "vdso")) {
            for (i = 0; i < 1000000; ++i) {
                time(NULL);
            }
        } else if (!strcmp(argv[1], "vsyscall")) {
            for (i = 0; i < 1000000; ++i) {
                syscall(SYS_time, NULL);
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    编译之后运行如下:

    ubuntu->cs-test:$ time ./time-test.out vdso
    
    real    0m0.004s
    user    0m0.004s
    sys     0m0.000s
    ubuntu->cs-test:$ time ./time-test.out emulated-vsyscall
    
    real    0m0.911s
    user    0m0.373s
    sys     0m0.538s
    ubuntu->cs-test:$ time ./time-test.out vsyscall
    
    real    0m0.437s
    user    0m0.273s
    sys     0m0.164s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    数据可以证实,确实glibc中的time走的是vDSO机制,所以性能特别高,只有用户空间开销

    另外一个就是emulated-vsyscall的性能不如原生vsyscall,而且相差2倍,至于vDSO更是相差百倍。

    有趣的一点是,strace可以统计到vsyscall的系统调用次数,但是无法统计到emulated-vsyscall的调用次数。

    ubuntu->cs-test:$ strace -c ./time-test.out vsyscall
    % time     seconds  usecs/call     calls    errors syscall
    ------ ----------- ----------- --------- --------- ----------------
    100.00    7.030226           7   1000000           time
      0.00    0.000109          13         8           mmap
      0.00    0.000041          13         3           mprotect
      0.00    0.000026          13         2           openat
      0.00    0.000021           5         4           pread64
      0.00    0.000021          10         2           newfstatat
      0.00    0.000020          20         1           brk
      0.00    0.000018           9         2         1 arch_prctl
      0.00    0.000017          17         1           munmap
      0.00    0.000016           8         2           close
      0.00    0.000012          12         1         1 access
      0.00    0.000008           8         1           read
      0.00    0.000006           6         1           set_tid_address
      0.00    0.000006           6         1           set_robust_list
      0.00    0.000006           6         1           prlimit64
      0.00    0.000005           5         1           rseq
      0.00    0.000000           0         1           execve
    ------ ----------- ----------- --------- --------- ----------------
    100.00    7.030558           7   1000032         2 total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    2.3 glibc底层time具体调用函数

    最终我们探究一下这台X64虚拟机上(Linux VM-4-17-ubuntu 5.15.0-56-generic),glibc底层time采用那个系统函数实现。

    编译时需要携带符号信息,如下:

    ubuntu->cs-test:$ gcc -g -o time-test.out time-test.c
    ubuntu->cs-test:$ gdb --args time-test.out vdso
    
    • 1
    • 2

    然后按照如下操作:

    (gdb) break time
    Breakpoint 1 at 0x10b0
    (gdb) r
    Starting program: /home/ubuntu/tdata/cs-test/time-test.out vdso
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    
    Breakpoint 1, 0x00007ffff7fc1be0 in time ()
    (gdb) c
    Continuing.
    
    Breakpoint 1, 0x00007ffff7fc1be0 in time ()
    (gdb) bt
    #0  0x00007ffff7fc1be0 in time ()
    #1  0x0000555555555280 in main (argc=2, argv=0x7fffffffe418) at time-test.c:25
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时是直接命中time()函数,而且就是__vdso_time函数:

    (gdb) info symbol 0x00007ffff7fc1be0
    time in section .text of system-supplied DSO at 0x7ffff7fc1000
    (gdb) p __vdso_time
    $2 = {} 0x7ffff7fc1be0 
    • 1
    • 2
    • 3
    • 4

    位于vdso的映射范围之内:

    (gdb) info proc mappings 
    ................
          0x7ffff7fbd000     0x7ffff7fc1000     0x4000        0x0  r--p   [vvar]
          0x7ffff7fc1000     0x7ffff7fc3000     0x2000        0x0  r-xp   [vdso]
    ................
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对于一些其他平台的设备,比如下面这台aarch64设备(Linux-4.14):

    root:~# cat /proc/1/maps |grep v
    ffff96bf5000-ffff96bf6000 r--p 00000000 00:00 0                          [vvar]
    ffff96bf6000-ffff96bf7000 r-xp 00000000 00:00 0                          [vdso]
    root:~#dd if=/proc/1/mem of=/tmp/linux-vdso.so skip=$((0xffff96bf6000)) bs=1 count=4096
    dd: /proc/1/mem: cannot skip to specified offset
    4096+0 records in
    4096+0 records out
    4096 bytes (4.1 kB, 4.0 KiB) copied, 0.0629148 s, 65.1 kB/s
    root@firewall:~# strings /tmp/linux-vdso.so 
    __kernel_gettimeofday
    __kernel_clock_gettime
    __kernel_clock_getres
    __kernel_rt_sigreturn
    linux-vdso.so.1
    LINUX_2.6.39
    Linux
    .shstrtab
    .hash
    .dynsym
    .dynstr
    .gnu.version
    .gnu.version_d
    .note
    .text
    .eh_frame_hdr
    .eh_frame
    .dynamic
    .got
    .got.plt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    vDSO只有1KB大小,且没有time这个函数,因此这里time底层就是__kernel_gettimeofday这个函数

    具体架构的区别,在man手册里面说得很清楚,可以参考文档: vdso(7) - Linux manual page (man7.org)

    综上所述,在常见的aarch64和x64里面,使用clock_gettimegettimeofday比较保险,对于其他架构,可以参考手册之后再确定,当然,最稳妥的做法始终是实机测试一下。

    附录:
    附录1: /proc/pid/maps介绍

    Linux系统中的/proc目录是一个伪文件系统,它包含了运行中的内核和进程信息的接口。当我们提到/proc/[pid]/maps/proc/[pid]/smaps以及pmap工具时,我们其实是在讨论与特定进程相关的内存映射信息。

    首先,/proc/[pid]/maps文件为我们提供了进程的内存映射信息。这里的[pid]是指特定进程的进程ID。打开这个文件,我们可以看到该进程内存中的段落信息,包括起始和结束地址、权限、映射类型、文件名等。这对于理解进程的内存使用情况以及调试非常有用。

    例如,假设我们有一个正在运行的程序,其进程ID为1234。查看该程序的内存映射,我们可以使用命令cat /proc/1234/maps。这将列出程序的内存段,每个段的权限(是否可读、写或执行)和它们映射到的文件。这样,我们可以追踪程序运行时的内存分布情况。

    /proc/[pid]/smapsmaps的一个扩展版本,提供了更详细的内存使用信息,包括每个内存区域的大小、多少分页实际在物理内存中等。如果你需要对内存使用进行更深入的分析,smaps文件将是一个不错的选择。例如,可以通过cat /proc/1234/smaps来观察进程1234更加详细的内存分布和使用情况。

    pmap是一个外部工具,它提供了一种更友好的方式来查看进程的内存映射。pmap实际上就是读取/proc/[pid]/maps的信息,并以更易于阅读的格式显示出来。使用pmap时,你可以直接通过pmap 1234来获取同样的内存映射信息,但是它会以更加结构化和详细的方式来展示,包括每个段的大小和累积的内存使用信息。

    通过pmap,我们可以快速地获取进程的内存布局概览,这在分析程序的内存占用和性能调优时极其有用。不过,值得注意的是,pmap输出的信息可能会比直接查看maps文件多出一些统计数据,有时这些数据对于快速诊断内存问题是非常宝贵的。

    总的来说,/proc/[pid]/maps/proc/[pid]/smapspmap工具是Linux系统中非常强大的资源,能够帮助开发者和系统管理员理解和优化进程的内存使用。无论是在进行系统监控、性能优化还是安全分析时,它们都是不可或缺的工具。在开发和维护过程中,合理利用这些资源,将有助于提高系统的稳定性和性能。

    附录2: /proc/pid/mem介绍

    /proc/[pid]/mem是Linux中一个特殊文件,提供了一种方式来访问指定进程的物理内存。在这里,“[pid]”代表了进程的ID号。这个文件对于那些需要深入了解或者调试进程内存的专家来说很有用途,但由于它涉及到直接操作内存,所以普通用户和开发者通常不会直接使用它。

    想要读取这个文件,你得有足够的权限,通常是root权限,因为它可以暴露进程的敏感信息。在实际使用中,它往往与/proc/[pid]/maps文件结合起来使用,因为maps文件包含了进程内存段的地址范围,这些信息告诉我们哪些内存区域可以安全地读取。

    简而言之,/proc/[pid]/mem文件是一个强大的工具,它能够让你深入到Linux进程的内存中。然而,由于它的复杂性和潜在风险,普通用户应该避免使用,而是应该使用更安全、更友好的工具,比如pmap,来查看进程的内存使用情况。

    附录3: vDSO的构建过程

    可参考: Linux vDSO概述 - 知乎 (zhihu.com)

    vDSO(virtual Dynamic Shared Object)image的构建是一个涉及到编译器、链接器以及Linux内核构建系统的过程。vDSO被设计为一种特殊的共享库,它不同于普通的共享库,因为它是由内核在运行时动态映射到用户空间的。这意味着vDSO image的构建需要与内核构建过程紧密集成。下面是vDSO image构建过程的简要概述:

    1. 源代码编写:首先,内核开发者会编写vDSO的源代码,这些代码通常是用C语言编写的,并且包含了要提供给用户空间的函数实现,如高效的时间获取函数。

    2. 内核配置:在Linux内核的配置过程中,可以选择是否启用vDSO支持。如果启用,那么内核构建系统会包括vDSO的构建步骤。

    3. 编译:内核构建系统使用特定的编译器选项编译vDSO源代码,生成目标文件。这些编译选项确保生成的代码是适合在用户空间运行的,并且与普通的用户空间应用程序使用相同的调用约定。

    4. 链接:链接器会将多个目标文件链接成一个单独的vDSO image。在这个过程中,链接器使用了特定的脚本和选项,这些脚本和选项确保vDSO image拥有合适的内存布局和导出符号。生成的image是一个共享对象文件,通常是ELF(Executable and Linkable Format)格式。

    5. 内核集成:构建完成后的vDSO image会被内核集成。在Linux内核启动时,内核会将vDSO image映射到每个进程的地址空间中。这是通过在进程创建时,内核的某些部分(比如进程调度器)负责将vDSO映射到进程的内存中的适当位置。

    6. 动态映射:不同于传统的共享库,vDSO并不需要通过文件系统加载。内核会直接映射vDSO image到进程空间,这个过程对用户程序是透明的,用户程序会像调用普通函数一样调用vDSO中的函数。

    7. 使用:当用户空间的应用程序需要进行某些系统调用时,例如获取时间,它可以调用vDSO中的对应函数,从而避免了传统系统调用的开销。

    整个vDSO image的构建过程是自动化的,作为内核构建过程的一部分。内核维护者不断更新vDSO的代码,以支持更多的功能并优化性能。对于系统管理员和应用开发人员而言,通常不需要关心vDSO的构建细节,他们只需要知道在支持vDSO的系统上,某些系统调用会更加高效。而对于内核开发者来说,理解vDSO构建过程是必要的,这有助于他们在添加新的特性或调试时作出正确的决策。

    附录4: Linux内核命令行选项vdso和vsyscall

    在Linux系统中,内核命令行选项vdsovsyscall是用来控制内核行为的设置,特别是与vDSO(virtual Dynamic Shared Object)和vsyscall机制的行为有关。vDSO和vsyscall是Linux内核用来加速某些类型的系统调用的技术。下面将分别对这两个选项进行解释。

    vDSO是一种性能优化技术,它允许用户空间程序高效地执行某些系统调用。Linux内核通过将一段小的共享代码映射到每个进程的地址空间来实现这一点。这段代码提供了一些常用的系统服务,比如获取当前时间,而不需要进行完整的系统调用,这样可以减少上下文切换的开销。

    内核命令行选项vdso可以控制vDSO的行为。例如,你可以通过设置vdso=0来禁用vDSO功能。这可能出于调试的目的,或者在某些特殊的系统上,vDSO可能导致问题。

    vsyscall是一个较旧的机制,目前已经被vDSO所取代。它的目的也是为了加快某些系统调用。在老版本的Linux内核中,vsyscall页是一个固定地址的内存区域,它包含了几个用于兼容性的系统调用。这个机制与vDSO有些类似,但由于安全和可维护性的问题,现代Linux内核已经逐渐放弃使用vsyscall。

    内核命令行选项vsyscall可以用来控制vsyscall机制的行为。例如,vsyscall=none可以完全禁用vsyscall页,vsyscall=emulate则表示内核会在用户试图使用vsyscall页时进行模拟。这些选项通常用于兼容老的应用程序或者出于安全考虑。

    总的来说,内核命令行选项vdsovsyscall是高级选项,大多数用户和开发者可能永远不需要手动更改它们。它们是内核开发者和系统管理员在需要对系统的低级行为进行精细控制时使用的工具。对于大多数情况,可以信任默认设置是为当前内核版本和硬件平台优化过的。如果你不是在解决特定的兼容性或安全问题,通常不建议修改这些选项。

  • 相关阅读:
    TCP三次握手、为什么要三次握手?
    【Linux】Linux常用操作命令(三)
    【数据结构】栈和队列
    伍志宏心理平台搭建项目分析
    【分布式云储存】高性能云存储MinIO简介与Docker部署集群
    Python常用基础语法知识点大全合集,看完这一篇文章就够了
    代码随想录day动态规划回文子串
    OpenCV图像处理方法:腐蚀操作
    Axure设计之引入ECharts图表
    Nocas为什么会在SpringBoot启动完就会注册呢?
  • 原文地址:https://blog.csdn.net/Once_day/article/details/136181102