【问题标题】:Why are many system calls (getpid) captured only once using strace?为什么许多系统调用 (getpid) 使用 strace 只捕获一次?
【发布时间】:2011-08-11 14:12:26
【问题描述】:

我在一个程序中多次调用getpid()(测试系统调用的效率),但是当我使用strace获取trace时,只有一个getpid( ) 调用被捕获。

代码很简单:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void print_usage(){
    printf("Usage: program count\n");
    exit(-1);
}

int main(int argc, char** argv){
    if(argc != 2)
        print_usage();
    int cnt = atoi(argv[1]);
    int i = 0;
    while(i++<cnt)
        getpid();
    return 0;
}

我用gdb 得到了这个:

(gdb) disasse
Dump of assembler code for function getpid:
0xb76faac0 <getpid+0>:  mov    %gs:0x4c,%edx
0xb76faac7 <getpid+7>:  cmp    $0x0,%edx
0xb76faaca <getpid+10>: mov    %edx,%eax
0xb76faacc <getpid+12>: jle    0xb76faad0 <getpid+16>
0xb76faace <getpid+14>: repz ret 
0xb76faad0 <getpid+16>: jne    0xb76faadc <getpid+28>
0xb76faad2 <getpid+18>: mov    %gs:0x48,%eax
0xb76faad8 <getpid+24>: test   %eax,%eax
0xb76faada <getpid+26>: jne    0xb76faace <getpid+14>
0xb76faadc <getpid+28>: mov    $0x14,%eax
0xb76faae1 <getpid+33>: call   *%gs:0x10
0xb76faae8 <getpid+40>: test   %edx,%edx
0xb76faaea <getpid+42>: mov    %eax,%ecx
0xb76faaec <getpid+44>: jne    0xb76faace <getpid+14>
0xb76faaee <getpid+46>: mov    %ecx,%gs:0x48
0xb76faaf5 <getpid+53>: ret  

我不太了解汇编代码。如果有人可以对此进行详细解释,那也会很有帮助。根据我的观察,除了第一个 getpid() 调用外,没有执行"call *%gs:0x10" (, 它跳转到 vdso),这可能是后续 的原因getpid() 调用未被捕获。但我不知道为什么。

linux内核:2.6.24-29 海合会 (GCC) 4.2.4 libc 2.7,

谢谢!

【问题讨论】:

    标签: linux-kernel system-calls libc strace


    【解决方案1】:

    Glibc 缓存结果,因为它不能在调用之间改变。例如查看源代码here

    所以真正的系统调用只执行一次。其他调用只是从缓存中读取。 (代码不是很简单,因为它负责用线程做正确的事情。)

    【讨论】:

    • 太棒了。我还有另一个问题;它可能不相关。我想知道即使是第一个 getpid() 调用也不会通过利用 vdso 触发用户内核模式切换,就像 gettimeofday 一样?
    • 另一个问题是如何使用 gdb 单步进入“调用 *%gs:0x10”?
    • 我不确定是否有唯一的答案。系统调用的处理方式因平台而异(即,即使 32 位 x86 和 64 位 x86_64 也有不同的系统调用机制)。但也许我错了 - 你可能应该为此发布一个单独的问题,并指定你感兴趣的架构,以及如果有一些你特别感兴趣的系统调用。(我不'根本不了解gdb)
    • @SIFE:我不知道,我看不出他们提供该功能的任何理由 - 进程的 PID 永远不会改变。
    • @Mat 但这并不能回答我的问题。
    【解决方案2】:

    glibc 缓存 pid 值。第一次调用 getpid 时它会向内核询问 pid,下一次它只返回从第一个 getpid 系统调用获得的值。

    glibc 代码:

    pid_t
    __getpid (void)
    {
    #ifdef NOT_IN_libc
      INTERNAL_SYSCALL_DECL (err);
      pid_t result = INTERNAL_SYSCALL (getpid, err, 0);
    #else
      pid_t result = THREAD_GETMEM (THREAD_SELF, pid);
      if (__builtin_expect (result <= 0, 0))
        result = really_getpid (result);
    #endif
      return result;
    }
    

    如果您想测试系统调用的开销,gettimeofday() 经常用于执行此操作 - 内核所做的工作非常少,编译器和 C 库都无法优化对它的调用。

    【讨论】:

    • 您不能使用gettimeofday() 函数来测量系统调用开销,因为gettimeofday 不是Linux 上的标准系统调用:它通过vDSO 机制进行了优化。在我的笔记本电脑上,标准系统调用持续约 225 ns,而 gettimeofday 仅持续 20 ns。
    【解决方案3】:

    如今,随着 pid_namespaces 的引入,以及在应用程序中检测到信号接收或通过调用 syscall() 而不是 fork() 创建子进程时检测到的大量错误, vfork()clone(),pid 不再缓存在 GLIBC 中。这点在manual中指出:

    从 glibc 版本 2.3.4 到并包括版本 2.24,
    用于 getpid() 缓存 PID 的 glibc 包装函数,目标是
    当进程调用 getpid() 时避免额外的系统调用
    反复。通常这种缓存是不可见的,但它是正确的
    操作依赖于 fork(2) 的包装函数中的支持,
    vfork(2) 和 clone(2):如果应用程序绕过了 glibc
    使用 syscall(2) 封装这些系统调用,然后调用
    在孩子中获取 getpid() 会返回错误的值(要
    精确:它将返回父进程的PID)。在
    此外,在某些情况下 getpid() 可能会返回错误
    即使通过 glibc 包装函数调用 clone(2) 时的值。
    (有关此类情况的讨论,请参阅 clone(2) 中的 BUGS。)
    此外,缓存代码的复杂性一直是
    多年来 glibc 中的一些错误的来源。

    【讨论】:

      猜你喜欢
      • 2020-05-18
      • 2017-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多