【问题标题】:Fastest Linux system call最快的 Linux 系统调用
【发布时间】:2018-02-21 18:34:06
【问题描述】:

在支持syscallsysret 的 x86-64 Intel 系统上,在 vanilla 内核上从 64 位用户代码中“最快”的系统调用是什么?

特别是,它必须是一个执行syscall/sysret用户内核转换1的系统调用,但除此之外所做的工作量最少。它甚至不需要自己执行系统调用:某种类型的早期错误永远不会分派到内核端的特定调用,只要它不会因此而走慢路。

此类调用可用于估算原始 syscallsysret 开销,而与调用完成的任何工作无关。


1 特别是,这不包括看似系统调用但在 VDSO 中实现(例如,clock_gettime)或由运行时缓存(例如,getpid)的事物。

【问题讨论】:

  • 你为什么要问,你为什么关心?你的问题缺乏动力!
  • 你为什么关心我为什么关心?就我个人而言,我不坚持任何问题都需要有详细动机的想法,只要它足够清楚 - 这是 SO 的一个令人讨厌的方面,一个特定的子组几乎用“你为什么要关心?XY 问题”来回答每个问题, ETC”。无论如何,尽管我对这件事有感觉,但我什至预先考虑了动机,因为我认为有人会问:这样的调用可用于估计原始 sysenter 和 sysret 开销,而与调用所做的任何工作无关。
  • 您是否排除了开发人员创建并添加到内核中的无操作系统调用的可能性?
  • 我看到您在评论中回复了关于未修改内核的评论。这确实应该在问题中得到体现。
  • @MichaelPetch - 是的,它应该是现代内核上最快的现有调用。无论如何,我怀疑无操作调用并不是最快的:最快的可能只是一个错误路径,例如,“系统调用编号太高”,它甚至永远不会离开入口代码。公平地说,我认为这暗示着“添加你自己的系统调用”应该被隐含地排除在 Linux 问题上,除非另有说明。否则,任何“我如何或可以在 Linux 上做 X”都可以简单地通过“添加您自己的系统调用来做”来回答(然后尝试说服每个人使用您的自定义内核?)。

标签: linux performance x86-64 microbenchmark


【解决方案1】:

一个不存在的,因此很快返回 -ENOSYS。

来自arch/x86/entry/entry_64.S:

#if __SYSCALL_MASK == ~0
    cmpq    $__NR_syscall_max, %rax
#else
    andl    $__SYSCALL_MASK, %eax
    cmpl    $__NR_syscall_max, %eax
#endif
    ja  1f              /* return -ENOSYS (already in pt_regs->ax) */
    movq    %r10, %rcx

    /*
     * This call instruction is handled specially in stub_ptregs_64.
     * It might end up jumping to the slow path.  If it jumps, RAX
     * and all argument registers are clobbered.
     */
#ifdef CONFIG_RETPOLINE
    movq    sys_call_table(, %rax, 8), %rax
    call    __x86_indirect_thunk_rax
#else
    call    *sys_call_table(, %rax, 8)
#endif
.Lentry_SYSCALL_64_after_fastpath_call:

    movq    %rax, RAX(%rsp)
1:

【讨论】:

  • 这是 syscall 从 64 位模式的入口点,而不是 sysenter。 compat 系统调用(系统调用号仅从 C 代码检查)有更多开销,入口点位于entry_64_compat.S。但是,是的,超出范围的系统调用号似乎是最快的。
  • @PeterCordes 和 Tim - 很抱歉造成混淆。我应该一直在谈论syscall(即在64位代码中进行系统调用的最佳方式以及应该在VDSO thunk中的方法),但我错误地在问题中写了sysenter/sysretsysret 至少是正确的)。那么鉴于这是正确的切入点吗?
  • @BeeOnRope 是的,它是正确的入口点。但请注意,启用 Meltdown 缓解后,实际入口点是 entry_SYSCALL_64_trampoline,因此内核可以避免通过 IDT 暴露 kernel-ASLR 偏移量(即使在用户空间中也必须映射,因此可以被 Meltdown 读取)
  • @peter 你知道 pti=off 是否会改变蹦床的行为吗?
  • @BeeOnRope:可能?我认为它可以,但IDK 如果它可以。您可以通过sudo perf record 了解内核中哪些指令得到计数?
【解决方案2】:

使用无效的系统调用号,以便调度代码简单地返回
eax = -ENOSYS,而不是调度到系统调用处理函数。

除非这会导致内核使用iret 慢速路径而不是sysret / sysexit。这可能解释了the measurements 显示一个比@​​987654331@ 慢17 个周期的无效数字,因为glibc 错误处理(设置errno)可能无法解释它。但是从我对内核源代码的阅读中,我看不出有任何理由为什么它在返回 -ENOSYS 时仍不会使用 sysret


此答案适用于sysenter,而不是syscall。这个问题最初是说sysenter/sysret(这很奇怪,因为sysexitsysenter 一起使用,而sysretsyscall 一起使用)。我根据sysenter 回答了 x86-64 内核上的 32 位进程。

本机 64 位 syscall 在内核内部得到更有效的处理。 (更新;使用 Meltdown / Spectre 缓解补丁,它仍然是 4.16-rc2 中的 dispatches via C do_syscall_64)。


我的What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? Q&A 概述了从兼容模式到 x86-64 内核 (entry_64_compat.S) 的系统调用入口点的内核方面。这个答案只是采取了相关部分。

该答案中的链接和此链接指向 Linux 4.12 源,其中不包含 Meltdown 缓解页表操作,因此这将是显着额外开销。

int 0x80sysenter 有不同的入口点。您正在寻找entry_SYSENTER_compat。 AFAIK,sysenter 总是去那里,即使你在 64 位用户空间进程中执行它。 Linux 的入口点推送一个常量__USER32_CS 作为保存的 CS 值,所以它总是会在 32 位模式下返回到用户空间。

在压入寄存器以在内核堆栈上构造struct pt_regs 之后,有一个TRACE_IRQS_OFF 钩子(不知道相当于多少条指令),然后是call do_fast_syscall_32,它是用C 编写的。(本机64 位@ 987654355@ 调度是直接从 asm 完成的,但 32 位兼容系统调用总是通过 C) 调度。

do_syscall_32_irqs_on in arch/x86/entry/common.c 非常轻量级:只需检查是否正在跟踪进程(我认为这就是strace 可以通过ptrace 挂钩系统调用的方式),然后

   ...
    if (likely(nr < IA32_NR_syscalls)) {
        regs->ax = ia32_sys_call_table[nr]( ... arg );
    }

    syscall_return_slowpath(regs);
}

AFAIK,这个函数返回后内核可以使用sysexit

因此,无论 EAX 是否具有有效的系统调用号,返回路径都是相同的,并且显然完全不分派返回是通过该函数的最快路径,特别是在具有 Spectre 缓解的内核中,其中间接分支在表上的函数指针会通过 retpoline 并且总是错误预测。

如果您想真正测试 sysenter/sysexit 而没有额外的开销,您需要修改 Linux 以放置一个更简单的入口点,而无需检查跟踪或推送/弹出所有寄存器。

您可能还想修改 ABI 以在寄存器中传递返回地址(就像 syscall 自己做的那样),而不是保存在 Linux 当前的 sysenter ABI 所做的用户空间堆栈中;它必须 get_user() 才能读取它应该返回的 EIP 值。


如果所有这些开销都是您想要测量的一部分,那么您肯定已经设置了一个给您-ENOSYS 的 eax;在最坏的情况下,如果基于正常的 32 位系统调用,分支预测器是否对该分支是热的,那么您会从范围检查中得到一个额外的分支未命中。

【讨论】:

  • 您认为使用无效的系统调用号会更快,不是吗?但是,它比我的系统上的 syscall(SYS_getpid) 慢了大约 17 个周期:也许是因为 glibc 中的 syscall() 包装器必须在错误返回上做额外的工作(例如,设置 errno)?分支预测在这里不是问题,因为我在循环中对此进行基准测试。
  • 你应该忽略上面的数字,我终于意识到我只是在计时用户模式周期。实际时间约为 1800 个周期,所以我需要仔细检查我的工作,因为这似乎太慢了。大部分成本是注册 0x48 的wrmsr,我认为这是一个 Spectre 缓解措施。我不知道如何关闭它。
  • 并非所有缓解措施都有关闭开关,您可以在启动时使用 noibrsnoibpb 关闭它们,就像 KPTI 一样。事实上,您可以在启动后在/sys/kernel 文件系统中动态执行此操作,只要您的发行版将其包含在补丁中即可。禁用这些功能后,系统调用成本会降至至少约 160 个周期。
  • 请注意,/sys/kernel 开关,描述为here,显然仅在 RHEL 派生内核上可用,至少目前如此。但是,您仍然可以使用引导参数在几乎任何主线派生的内核上禁用它们(但是,使用和不使用选项进行背靠背测试会更烦人)。
  • 不,不是。我确实在某些 cmets 或答案中放置了一些数字,但我不会真正认为它们具有权威性:它们可能是特定于发行版的,而且我没有深入研究我看到的一些奇怪之处,例如,在禁用缓解措施时会产生更糟糕的结果引导命令行(即,缓解打开比缓解关闭更快)。结果显示了相当大的放缓,就像任何崩溃之前的 100 个周期到之后的 700 个周期。
【解决方案3】:

建议在 Brendan Gregg 的 this benchmark 中(链接自 this blog post,这是关于该主题的有趣读物)close(999)(或其他一些未使用的 fd)。

【讨论】:

  • 感谢您的链接! close(999) 似乎比 getuid() 慢了大约 20 个周期。大约 50 个周期,而我的系统在启用 KPTI 的情况下是 70 个。
【解决方案4】:

有些系统调用甚至不经过任何用户->内核转换,请阅读vdso(7)

我怀疑这些VDSO 系统调用(例如time(2),...)是最快的。您可以声称没有“真正的”系统调用。

顺便说一句,您可以 add 对内核进行虚拟系统调用(例如,某些系统调用总是返回 0,或 hello world 系统调用,另请参见 this)并对其进行测量。

我怀疑(没有对其进行基准测试)getpid(2) 应该是一个非常快速的系统调用,因为它唯一需要做的就是从内核内存中获取一些数据。而且 AFAIK,它是一个真正的系统调用,而不是使用 VDSO 技术。您可以使用syscall(2) 来避免由您的libc 完成缓存并强制执行真正的系统调用。

我保持我的立场(在对您最初问题的评论中给出):没有实际动机,您的问题没有任何具体意义。然后我仍然认为syscall(2)getpid 正在测量进行系统调用的典型开销(我猜你真的很关心那个)。实际上,几乎所有系统调用都比getpid(或getppid)做更多的工作。

【讨论】:

  • 确实如此,但我特别排除了不进入内核的调用(我会在问题中更清楚地说明这一要求)。分界线是必须有用户/内核转换(即,sysenter 调用)。关于虚拟调用,我希望它可以在未修改的内核上运行,与仅使用一些现有的快速调用相比,这是大量的工作。
猜你喜欢
  • 2012-06-30
  • 1970-01-01
  • 1970-01-01
  • 2012-12-30
  • 2016-05-12
  • 2016-03-02
  • 1970-01-01
  • 2021-05-11
  • 2015-12-22
相关资源
最近更新 更多