最快 Linux 系统调用
Fastest Linux system call
在支持 syscall
和 sysret
的 x86-64 Intel 系统上,来自原始内核上的 64 位用户代码的 "fastest" 系统调用是什么?
特别是,它必须是执行 syscall
/sysret
用户 <-> 内核转换 1 的系统调用,但执行最少除此之外的工作量。它甚至不需要自己执行系统调用:某种类型的早期错误永远不会分派到内核端的特定调用就可以了,只要它不会因此而走慢路。
这样的调用可用于估计原始 syscall
和 sysret
开销,独立于调用完成的任何工作。
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
(这很奇怪,因为 sysexit
与 sysenter
对应,而 sysret
与 syscall
对应)。我根据 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 0x80
和 sysenter
有不同的入口点。您正在寻找 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 中获得一个额外的分支未命中。
在支持 syscall
和 sysret
的 x86-64 Intel 系统上,来自原始内核上的 64 位用户代码的 "fastest" 系统调用是什么?
特别是,它必须是执行 syscall
/sysret
用户 <-> 内核转换 1 的系统调用,但执行最少除此之外的工作量。它甚至不需要自己执行系统调用:某种类型的早期错误永远不会分派到内核端的特定调用就可以了,只要它不会因此而走慢路。
这样的调用可用于估计原始 syscall
和 sysret
开销,独立于调用完成的任何工作。
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
(这很奇怪,因为 sysexit
与 sysenter
对应,而 sysret
与 syscall
对应)。我根据 sysenter
对 x86-64 内核上的 32 位进程进行了回答。
本机 64 位 syscall
在内核中得到更有效的处理。 (更新;使用 Meltdown / Spectre 缓解补丁,它在 4.16-rc2 中仍然 dispatches via C do_syscall_64
)。
我的 entry_64_compat.S
) 的 system-call 入口点的内核端。这个答案只是取了其中的相关部分。
该答案中的链接和此链接指向 Linux 4.12 来源,其中不包含 Meltdown 缓解措施 page-table 操作,因此这将是 重要 额外开销。
int 0x80
和 sysenter
有不同的入口点。您正在寻找 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 中获得一个额外的分支未命中。