Go 不通过标准 C 库进行系统调用的一些原因

Go 不通过标准 C 库进行系统调用的一些原因

以下文章翻译自 Some reasons for Go to not make system calls through the standard C library

Unix 世界中的最新消息之一是,作为其一般安全性工作的一部分,OpenBSD 正朝着仅允许从 C 库而不是从任何其他代码进行系统调用的方向进行(你可以在 OpenBSD 中阅读此内容)。现在,OpenBSD 可以免除程序本身的代码,这主要是因为 Go 通常直接进行系统调用而不是通过调用 C 库进行调用,但是他们希望摆脱这种情况。其他人对 Go 进行直接系统调用并不满意。例如,在 Solaris 和 Illumos 上,进行系统调用的唯一受官方支持的方法也是通过 C 库,在这两个系统上,Go 实际上使用平台 C 库进行系统调用。

从表面上看,Go 这么做听起来不合理,你可能会问为什么它不能像其他所有 Unix 语言一样,仅通过系统的 C 库进行 Unix 系统调用。尽管我不知道为什么 Go 开发人员会选择采用这种方式,但是出于某些原因,你可能想要避免使用 Go 这样的语言编写 C 库,因为标准 C 库 Unix 系统调用 API 的指定不足(under-specified)且不方便。

对于诸如 Go 之类的东西,C 库 API 的指定不足的一个明显方法是你需要多少可用堆栈空间的问题。传统上,C 代码(甚至是线程 C 代码)会分配大型或超大型堆栈,但是 Go 希望在可能的情况下使用非常小的堆栈(数 KB 左右),以使 goroutine 保持轻量级。C 库 API 在这里没有任何承诺,因此,出于安全考虑,需要使用较大的堆栈对其进行调用。Go 调用 C 库系统调用需要堆栈空间,这是一个问题。Go 现在通过增加堆栈大小来解决此问题,但是由于所需的堆栈大小不是 C 库 API 的标准部分,因此将来可能会有破坏性变化(在任何 Unix 上,不仅仅是在 Linux 上调用 vDSO)。

(Unix 系统强烈坚持,你应该通过 C 库进行系统调用的,Unix 通常保留让那些“系统调用”库函数在背后做任何工作的权利,因为系统调用的表面上的 API 可能不是真正的内核 API。确实,Unix 强制执行此操作的原因之一就是使他们可以在不更改程序使用的“系统调用” API 的情况下更改内核 API。内部实现中的此类更改当然会导致不可预期和破坏性的变化:在函数调用期间,C 库对堆栈的需求和使用。)

对于系统调用,C 库 API 最明显的尴尬之处是如何在 errno 中返回错误的细节,errno 通常是一个全局变量。在多线程程序出现并希望从多个线程进行系统调用的日子里,使用全局变量是可以的,但是现在这显然是一个问题。使 errno 在现代环境中工作需要 C 库在这种魔幻场景下,必须使用整个 C 运行时(是的,C 具有运行时)来进行诸如设置线程本地存储,创建 OS 级别的操作线程,以便它们具有该线程本地存储,并从其 TLS 检索线程的 errno。在极端情况下,这可能需要你使用 C 库 pthreads API 创建将进行系统调用的任何线程,然后仔细计划要对这些 pthread 进行系统调用的goroutine(可能由于 C 库 API 而使用大型堆栈的问题)。所有这些在底层内核 API 中都是完全不需要的,该 API 已经直接为您提供了错误代码。

存在 C 全局 errno 是为了实现历史兼容性,并且因为 C 没有简单的方法可以返回多个值。Go 自然是现代 API,它返回结果和 errno 的方法,后者本质上是线程安全的,并且没有伪全局变量。要求所有语言都通过 C 库的常规 Unix API 进行系统调用意味着要约束所有语言以适应 C 的历史包容和限制。

(你可以为所有系统调用发明一个新的 C 库 API,该 API 将错误号直接写入调用者提供的位置,这将使生活简单得多,但是到目前为止,还没有主要的 Unix 或 C 库提议这样做。每个人都希望(或要求)你通过传统的 Unix API,errno 等所有这些。)

欢迎关注我的公众号:

发表评论

电子邮件地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据