为什么在用户空间重新实现的内核功能比在内核空间更快?

为什么在用户空间重新实现的内核功能比在内核空间更快?

我正在读一本博客文章并注意到下面这句话:

然后他说了一些非常令人惊讶的话:在 Seastar HTTP 框架中,他们编写了自己的 TCP 堆栈,这使得一切都快了好几倍。什么?!

我试图理解为什么出于性能原因会在用户空间中重新实现内核功能。我假设内核中存在的功能正是因为它们执行(许多)特权指令而存在于内核中,否则该功能可以简单地实现为用户空间程序。因此,如果要在用户空间中重新实现内核特性或功能,例如网络堆栈(这就是 gVisor 对其所做的事情)网络堆栈例如),您是否最终不必将许多系统调用返回到内核中,从而导致大量开销?

这种用户空间重新实现传统上属于内核一部分的功能是否能够避免进行许多系统调用?如果是这样,那么它对于例如网络堆栈如何工作,因为我可以想象,您可能必须例如send()或经常这样做。recv()

我确实了解在用户空间中重新实现功能的两个潜在优势是:

  • 您不依赖于添加到内核中的内容(这似乎是一个艰巨的过程)
  • 如果在用户空间中重新实现的传统内核功能中发现漏洞,那么它“只是”一个非特权用户空间进程

但我对这个问题的性能方面更感兴趣。

答案1

其中一些是避免一些跨越系统调用边界的行程。

确实如此,但另一方面,Linux 系统调用接口既非常通用(即必须处理许多不同类型的应用程序和系统),又非常狭窄(系统调用参数仅专门处理当前请求)。内核通常几乎不知道你的代码接下来要做什么。

我们举find个例子。它花费大量时间在系统调用上,例如getdentsopendir。您可以使用它做很多事情,find但这是一个典型的命令行:

find . -name 'report_201[89].txt' -print -quit

find程序将打开很多目录并读取很多文件名。它将把这些文件名提供给用户空间函数,fnmatch以查明它们是否是report_2018.txtreport_2019.txt.

但是,我们假设这.是在某些现代文件系统中。这些目录实际上是 B 树或哈希表。如果内核知道我们正在寻找哪个文件名,我们就可以节省大量处理过程。

假设我们看的是git status.如果你跟踪它的系统调用,它会发出大量lstat调用。但它真正想要弄清楚的是用户是否更改了文件系统? 内核基本上知道答案,但是没有办法git告诉内核它想知道什么。因此它必须自己检查一切(尽管它以相当聪明的方式这样做)。

这里的总体主题是,如果内核 API 是特定于应用程序的,那么事情会更加高效。但从设计角度来看,这很疯狂,因为有很多不同的应用程序。维护更广泛的内核接口可能具有超线性的复杂性。但这就是为什么通过在用户空间解决更多问题(对于某些问题)可以提高效率。

答案2

简而言之,内核必须处理许多不同的情况/应用程序/硬件。当您了解硬件和/或应用程序通信需求时,即可完成堆栈的重新实现。然后,您可以只为该宇宙编写代码。

假设您有一个小型传感设备通过 UDP 定期发送数据。您可以创建 UDP/IP 数据包,其中大部分值都是固定的,以便它到达服务器(您知道您的 IP、端口、目标端口和地址、消息长度、标志...您只需更改的阅读)。

仅仅为此而运行完整的 IP 内核堆栈将是矫枉过正、速度较慢,甚至可能不可行(在简单的 Arduino 中运行例如)。

但更广泛的答案更复杂,所以我建议这篇文章:为什么我们使用 Linux 内核的 TCP 堆栈链接在您引用的文章的底部。

相关内容