最快的Linux系统调用

8
在支持syscallsysret的x86-64英特尔系统上,从64位用户代码到原始内核的最快系统调用是什么?
具体而言,它必须是一种通过syscall/sysret用户<->内核转换来实现的系统调用1,但除此之外不需要做太多的工作。它甚至不需要自己执行系统调用:只要出现某种早期错误,就可以避免在内核侧分派到特定调用,只要它不因此走上某些缓慢的路径。
这样的调用可用于估算与任何调用所做的工作无关的原始syscallsysret开销。

1具体而言,这排除了在VDSO中实现的似乎是系统调用但不是系统调用的内容(例如,clock_gettime)或由运行时缓存的内容(例如,getpid)。


2
你为什么问这个问题,你为什么在意呢?你的问题缺乏很多动机! - Basile Starynkevitch
6
你为什么关心我为什么关心?就个人而言,我不坚持认为任何问题都需要有详细的动机,只要足够清楚即可——SO的一个特定子组似乎会用“你为什么关心?XY问题”等方式回答几乎每个问题,这是很烦人的一点。无论如何,尽管我对此持有感受,但我提前包含了动机,因为我想到有人会问:这样的调用可以用来估计纯sysenter和sysret开销,而与调用所执行的任何工作无关。 - BeeOnRope
1
你是否排除了开发人员创建并添加到内核中的“什么也不做”的系统调用的可能性? - Michael Petch
1
我看到你在评论中提到了未修改的内核。这个信息应该被包含在问题中。 - Michael Petch
1
@MichaelPetch - 是的,在现代内核上,它应该是现有调用中最快的。无论如何,我怀疑一个空调用并不是最快的,最快的可能是一个错误路径,例如“syscall号码太高”,它甚至从入口代码中都没有离开。公平地说,我认为“添加自己的系统调用”在Linux问题中应该被默认排除,除非有其他迹象表明。否则,任何“如何或可以在Linux上做X”的问题都可以简单地回答为“添加自己的系统调用来实现它”(然后尝试说服每个人使用您的自定义内核?)。 - BeeOnRope
显示剩余3条评论
4个回答

9

这是一个不存在的函数,因此会快速返回-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:

这是64位模式下syscall的入口点,而不是sysenter的入口点。兼容系统调用有一些额外的开销(仅从C代码检查系统调用号),入口点在entry_64_compat.S中。但是,是的,超出范围的系统调用号似乎是最快的。 - Peter Cordes
1
@PeterCordes和Tim - 对于混淆我感到抱歉。整个时间我应该在谈论syscall(即,在64位代码中进行系统调用的最佳方法,也应该是VDSO thunk中的方法),但我错误地在问题中写了sysenter/sysret(至少sysret是正确的)。所以,鉴于这是正确的入口点吗? - BeeOnRope
@BeeOnRope 是的,这是正确的入口点。但请注意,在启用Meltdown缓解措施的情况下,实际的入口点是entry_SYSCALL_64_trampoline,因此内核可以避免通过IDT(即使在用户空间中也必须映射)暴露内核ASLR偏移量,从而可以被Meltdown读取。 - Peter Cordes
@peter 你知道 pti=off 是否会改变跳板行为吗? - BeeOnRope
@BeeOnRope:可能吗?我认为它可能,但我不知道它是否确实。您可以使用sudo perf record找出内核中哪些指令计数。 - Peter Cordes
这是正确的答案。我之前报告它比像getuid这样的简单系统调用慢,但这是错误的:我只测量了用户时间,而不是内核时间(差异在于错误处理,如此处所讨论)。一旦纠正,这就是最快的,大约 ~117个周期,而最快的系统调用为 ~130个周期,一旦禁用所有Meltdown和Spectre内容,并直接使用syscall从asm中调用它。 - BeeOnRope

5
使用无效的系统调用号,这样分派代码就会直接返回 eax = -ENOSYS,而不是调度到任何系统调用处理函数。除非这导致内核使用 iret 慢路径而不是 sysret / sysexit。这可能解释了 测量结果,显示无效号码比 syscall(SYS_getpid) 慢 17 个周期,因为 glibc 错误处理(设置 errno)可能无法解释它。但是从我阅读内核源代码的情况来看,我没有看到为什么在返回 -ENOSYS 的同时不使用 sysret

这个答案是针对sysenter而不是syscall。原来的问题中提到了sysenter/sysret(这很奇怪,因为sysexitsysenter配合使用,而sysretsyscall配合使用)。我基于在x86-64内核上32位进程的sysenter回答了这个问题。

本机64位syscall在内核中处理更有效率。(更新:通过Meltdown / Spectre缓解补丁,它仍然通过C do_syscall_64在4.16-rc2中分派)。


我的如果在64位代码中使用32位int 0x80 Linux ABI会发生什么?问答概述了从兼容模式到x86-64内核(entry_64_compat.S)的系统调用入口点的内核方面。本答案只涉及相关部分。

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

int 0x80sysenter有不同的入口点。您需要查找entry_SYSENTER_compat。据我所知,即使在64位用户空间进程中执行它,sysenter也始终会进入那里。Linux的入口点将常量__USER32_CS作为保存的CS值推送,因此它将始终以32位模式返回到用户空间。
在将寄存器推入内核堆栈上的struct pt_regs来构造后,有一个TRACE_IRQS_OFF钩子(不知道它包含多少条指令),然后是call do_fast_syscall_32,它是用C编写的。(本机64位syscall分派直接从asm执行,但32位兼容系统调用始终通过C进行分派)。

arch/x86/entry/common.c 中的 do_syscall_32_irqs_on 很轻量级:只是检查进程是否被跟踪(我认为这就是 strace 如何通过 ptrace 钩取系统调用),然后...

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

    syscall_return_slowpath(regs);
}

据我所知,内核在该函数返回后可以使用 sysexit

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

如果你想真正测试 sysenter/sysexit 而没有所有这些额外开销,那么你需要修改 Linux,将一个简化得多的入口点放入其中,而无需检查跟踪或推送/弹出所有寄存器。

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


如果你想测量所有这些开销,那么使用eax并获得-ENOSYS就足够了;最坏的情况是,如果分支预测器对该分支进行热处理,基于正常的32位系统调用,你将会多出一个分支未命中的范围检查。

1
你会认为使用无效的系统调用号码会更快,不是吗?但在我的系统上,它比如 syscall(SYS_getpid) 慢了约17个周期,可能是因为 glibc 中的 syscall() 包装器在错误返回时需要额外的工作(例如设置 errno)?由于我正在循环中进行基准测试,所以分支预测并不是一个问题。 - BeeOnRope
2
你应该忽略上面的数字,我最终意识到我只计时了用户模式周期。实际时间大约为1800个周期,所以我需要仔细检查我的工作,因为这似乎太慢了。大部分成本是wrmsr到寄存器0x48,我认为这是一种Spectre缓解措施。我不知道如何关闭它。 - BeeOnRope
2
并非所有的缓解措施都有关闭开关,您可以在启动时使用 noibrsnoibpb 将它们关闭,就像 KPTI 一样。实际上,只要您的发行版包含了该补丁程序,您甚至可以在 /sys/kernel 文件系统中动态地在启动后禁用它们。如果这些功能被禁用,系统调用成本将下降到约 160 个周期的最低值。 - BeeOnRope
2
请注意,/sys/kernel 开关在此处描述的仅适用于 RHEL 派生内核,目前至少是这样。但是,您仍然可以使用引导参数在几乎任何主线派生内核上禁用它们(但是这样做会使有和没有选项进行连续测试更加麻烦)。 - BeeOnRope
1
不,其实不是。我确实在某些注释或答案中放了一些数字,但我并不认为它们是权威的:它们可能是特定于发行版的,而且我没有深入研究我看到的一些奇怪现象,例如在引导命令行上禁用缓解措施时(即,开启缓解措施比关闭缓解措施更快)。结果显示出相当大的减速,从任何熔断之前的100个周期到之后的700个周期左右。 - BeeOnRope
显示剩余7条评论

3

2
谢谢提供链接!close(999)似乎比getuid()慢了约20个周期。在我的系统上启用KPTI后,它大约是50个周期与70个周期之间的差距。 - BeeOnRope

2
一些系统调用甚至不需要经过任何用户->内核转换,请参阅vdso(7)。我怀疑这些VDSO系统调用(例如time(2)等)是最快的。你可以说没有“真正”的系统调用。
顺便说一句,你可以向内核添加一个虚拟系统调用(例如,始终返回0的某个系统调用,或者一个hello world系统调用,请参见此处),然后对其进行测量。
我怀疑(没有进行基准测试)getpid(2)应该是一个非常快的系统调用,因为它唯一需要做的就是从内核内存中获取一些数据。而且据我所知,这是一个真正的系统调用,不使用VDSO技术。你可以使用syscall(2)来避免libc对其进行缓存并强制使用真正的系统调用。
我保持我的立场(在你最初的问题的评论中给出):没有实际动机,你的问题没有任何具体意义。然后我仍然认为syscall(2)执行getpid正在测量使系统调用的典型开销(我猜你确实关心这个)。在实践中,几乎所有的系统调用都比这样的getpid(或getppid)做更多的工作。

确实如此,但我特别排除不进入内核的调用(我将在问题中明确这一要求)。分界线是必须有用户/内核转换(即sysenter调用)。关于虚拟调用,我希望它能在未经修改的内核上工作,并且与使用现有快速调用相比,这需要大量的工作。 - BeeOnRope

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接