最快 Linux 系统调用

Fastest Linux system call

在支持 syscallsysret 的 x86-64 Intel 系统上,来自原始内核上的 64 位用户代码的 "fastest" 系统调用是什么?

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

这样的调用可用于估计原始 syscallsysret 开销,独立于调用完成的任何工作。


1 特别是,这排除了看似系统调用但在 VDSO 中实现(例如,clock_gettime)或由运行时缓存的内容(例如,getpid).

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

我怀疑这些 VDSO system calls (e.g. time(2), ...) 是最快的。您可以声称没有 "real" 系统调用。

顺便说一句,你可以 add a dummy system call to your kernel (e.g. some system call always returning 0, or a hello world system call, see also this) 并测量它。

我怀疑(没有对其进行基准测试)getpid(2) should be a very fast system call, because the only thing it needs to do is fetch some data from the kernel memory. And AFAIK, it is a genuine system call, not using VDSO techniques. And you could use syscall(2) 避免由您的 libc 进行缓存并强制执行真正的系统调用。

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

一个不存在的,因此 returns -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:

this benchmark by Brendan Gregg (linked from this blog post 中,推荐阅读有关该主题的有趣内容)close(999)(或其他一些未使用的 fd)。

使用无效的系统调用号,因此调度代码只需 returns 和
eax = -ENOSYS 而不是分派到 system-call 处理函数。

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


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

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


我的 问答概述了从兼容模式到 x86-64 内核 (entry_64_compat.S) 的 system-call 入口点的内核端。这个答案只是取了其中的相关部分。

该答案中的链接和此链接指向 Linux 4.12 来源,其中不包含 Meltdown 缓解措施 page-table 操作,因此这将是 重要 额外开销。

int 0x80sysenter 有不同的入口点。您正在寻找 entry_SYSENTER_compat。 AFAIK,sysenter 总是在那里,即使你在 64 位 user-space 进程中执行它。 Linux的入口点压入一个常量__USER32_CS作为保存的CS值,所以在32位模式下它总是return到user-space。

压入寄存器在内核栈上构造一个struct pt_regs后,有一个TRACE_IRQS_OFF钩子(不知道总共有多少条指令),然后call do_fast_syscall_32写在C.(本机 64 位 syscall 调度直接从 asm 完成,但 32 位兼容系统调用始终通过 C 调度)。

do_syscall_32_irqs_on in arch/x86/entry/common.c 很漂亮 light-weight:只是检查进程是否被跟踪(我认为这就是 strace 可以通过 ptrace 挂钩系统调用的方式),然后

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

    syscall_return_slowpath(regs);
}

AFAIK,内核可以在这个函数 returns 之后使用 sysexit

因此,无论 EAX 是否具有有效的系统调用号,return 路径都是相同的,显然 return 根本不分派是通过该函数的最快路径,尤其是在具有 Spectre 缓解功能的内核,其中函数指针 table 上的间接分支将通过 retpoline 并始终预测错误。

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

您可能还想修改 ABI 以在寄存器中传递 return 地址(就像 syscall 自己做的那样)而不是保存在 user-space 堆栈中Linux 当前的 sysenter ABI 是这样做的;它必须 get_user() 才能读取它应该 return 到的 EIP 值。


如果所有这些开销都是您想要测量的一部分,那么您肯定已经准备好了一个 eax 给您 -ENOSYS;在最坏的情况下,如果基于正常的 32 位系统调用,分支预测器对于该分支是热的,那么您将从 range-check 中获得一个额外的分支未命中。