Tutorials

为什么不用 Go 编写操作系统?

Read this post in other languages:

本文由外部贡献者撰写。

Aniket Bhattacharyea

热爱计算机和软件的数学研究生。

网站

操作系统是计算机系统的心脏和灵魂。 操作系统管理计算机的硬件和软件资源,并为用户提供与计算机交互的手段。 传统上,C 语言和汇编等语言因其低开销和“接近机器”的特性而被用于编写操作系统。 但是,越发流行的 Go 等高级语言新引入了一些特性,可以使操作系统等复杂软件更易开发。 例如,类型安全、错误处理和并发等特性有利于操作系统编写。 因此,Go 这样的高级语言应该会成为操作系统开发的自然选择。 所以,为什么不是呢?

在本文中,您将了解为什么 C 语言这样的语言在操作系统开发中占有重要地位,以及使用 Go 编写操作系统是否可行。

为什么操作系统或内核是使用 C 语言和汇编编写的?

用于编写特定操作系统的语言可能难以确定,因为每个操作系统都是使用多种语言的组合编写而成。

操作系统的组件具有不同职责,可能使用不同的语言编写。 操作系统的核心组件是负责与硬件交互的内核,内核几乎从来都是使用 C 语言或汇编语言编写。 面向用户的组件,例如 GUI 应用,可以使用任何语言编写。 举例来说,Android 将 Java 用于用户空间组件,比如 GUI 框架和“相机”、“电话”等系统应用。 相比之下,内核使用的是 C 语言和汇编,而低级系统组件(例如库)使用的是 C++。

本文特别关注 Go 是否适合编写操作系统内核。 几乎所有的主流内核都使用 C 语言编写,中间夹杂着一些汇编。 C 语言统治内核领域的几个原因:

  1. 直接内存管理:C 语言的卖点是它允许程序员直接处理内存。 虽然在编写内核时手动内存管理通常具有挑战性,但您会想尽可能多地控制内存,而 C 语言恰恰可以满足这样的需求。 内存映射 I/ODMA 控制器、页表、交换等概念都需要内存操作,使用 C 语言可以实现。
  2. 没有抽象:C 语言没有哈希、树或链表这样的复杂数据结构,程序员可以根据需要自己实现。 这提供了对代码更精细的控制,因为程序员可以调整实现的详细信息来提高效率。
  3. 不需要运行时:与 Java 和 Python 不同,C 语言不需要运行时。 只需要一个调用 main() 函数的机制,C 语言就能够顺畅运行。 这意味着您可以直接在硬件上运行 C 语言程序,而不必纠结于内存管理、进程管理等问题。
  4. 可移植性:C 语言已被移植到多种 CPU 架构,是编写支持多架构内核的绝佳选择。

不过,C 语言本身通常不足以编写整个内核,某些情况下您需要编写汇编代码:

  1. 手动编写的汇编可能比编译器生成的更好。 某些操作可能难以在 C 语言中实现,并且编译器生成的机器码可能较为复杂或效率低下。 在这种情况下,手动编写汇编是更好的选择。
  2. 某些代码无法使用 C 语言编写。例如,在 C 语言中切换任务时,无法将寄存器保存到堆栈,也不能将堆栈指针保存到任务控制块,因为 C 语言不提供对堆栈指针的直接访问。

为什么 Go 可以成为操作系统开发的替代语言?

Go 这样的高级语言提供了一些优良特性,从表面上看,它们似乎是操作系统开发的绝佳选择:

  1. 某些类型的 bug 在高级语言中不太可能出现。 缓冲区溢出use-after-free bug 在 Go 等语言中几乎是不可能的。 即使是由专业程序员精心编写的 C 语言代码也会无意中包含这样的 bug。 use-after-free bug 十分常见,因此 Linux 内核包含内存检查器,用于在运行时检测一些 use-after-free 和缓冲区溢出 bug。
  2. 在高级语言中并发更容易处理,因为几乎每种高级语言都内置了处理并发所需的机制。
  3. Go 这样的语言的类型安全可以防止 C 语言的宽松类型强制

为什么 Go 没有用于操作系统/内核开发?

虽然 Go 提供了优良的特性,可以让操作系统开发者的工作更加轻松,但它也有一些局限性。

作为具有垃圾回收的语言,Go 并不真正适合操作系统开发。 使用 Go 编写内核意味着仔细编写代码,最大程度减少堆使用来小心绕过 Go 的垃圾回收。 如这篇 Reddit 帖子中所述,鼠标延迟可能是因为中断处理程序分配了触发垃圾回收的内存。

Go 还需要大量运行时才能执行,不能直接在硬件上运行。 尽管 TinyGo 可以将 Go 编译为在裸机上运行,但与 C 语言相比,它只支持少量的架构,而 C 语言几乎可以在任何架构上运行。

另一个相关问题是系统调用构成了典型 Go 运行时中的大量操作。 运行时为各种操作与操作系统通信,例如写入文件或启动线程。 但是,编写操作系统时,您必须在裸机上实现这些操作,并修改 Go 运行时来调用实现。 然后问题归结为,您是否真的想花这么多时间和精力来修改运行时,毕竟 C 语言等其他语言允许您立即开始编写操作系统。

正如您所看到的,使用 Go 编写操作系统并非不可能。 不过,编写可供普通用户使用的非小型操作系统几乎不可能。 编写一个在单一架构上启动并进入 shell 的操作系统很容易,但是使用 Go 编写一个在多种架构上运行、支持不同设备(如显卡或网卡)并且可能符合 POSIX 的操作系统将非常具有挑战性。 当然一些特性可以省略,但这会限制操作系统的可行性。

使用 Go 编写的操作系统

虽然 Go 不是最适合操作系统开发的选择,但这并不意味着使用 Go 编写操作系统是不可能的,许多研究项目正在探索如何做到这一点。

Biscuit 是使用 Go 编写的操作系统,在 64 位 X86 架构上运行。 它使用经过修改的 Go 1.10 运行时实现,其中添加了更多汇编代码来处理系统调用和中断处理程序的引导和进入/退出。 使用汇编编写的引导块加载 Go 运行时和“shim”层。 Go 运行时期望与底层内核通信来提供各种功能。 由于没有底层内核,“shim”层提供了这些功能。

Biscuit 为用户进程提供 POSIX 接口,支持 fork、exec 等。 它实现了支持核心 POSIX 文件系统调用的文件系统。 Biscuit 为使用 Go 编写的 Intel PCI-Express 以太网 NIC 实现了 TCP/IP 堆栈和驱动程序。 使用 POSIX 接口,Biscuit 可以在不修改源代码的情况下运行许多 Linux C 程序。

不过,Biscuit 缺少许多功能,例如调度优先级、换出页面或磁盘,以及许多安全功能,例如用户、访问控制列表和地址空间随机化。

有关 Biscuit 的更多详细信息,请参阅白皮书

gopher-os 是另一个使用 Go 编写的概念验证内核。 与 Biscuit 一样,它使用汇编来设置 Go 运行时和加载内核。 不过,它仍处于开发的早期阶段,自 2018 年以来没有获得任何更新。 它在最新版本的 Go 上也无法运作。

Clive 是另一个使用 Go 编写的单内核操作系统,但它不在裸机上运行,并且使用经过修改的 Go 编译器来编译 Clive 软件。

gVisor 是使用 Go 编写的应用程序内核,它在沙盒容器中实现 Linux 系统 API。

结论

虽然 C 语言在操作系统开发中占主导地位,但 Go 提供了类型安全、自动内存管理和并发等特性,有潜力成为操作系统开发的绝佳选择。 不过,由于缺乏对运行时和语言不同方面的微调控制,以及 C 语言的受欢迎程度,Go 很难在操作系统开发领域站稳脚跟。 但是,一些研究操作系统已经使用 Go 编写,我们可以期待在不久的将来使用 Go 编写消费者友好型操作系统。

本博文英文原作者:

Sue

Sergey Kozlovskiy

Discover more

了解 Go 中的模糊测试

本文由外部贡献者撰写。 Hrittik Roy Hrittik 是一位作者和软件工程师,专门研究云原生生态系统。 他曾参与过许多大型项目,在云计算的技术和商业方面都有经验。 他经常在会议上发言,并写过许多关于软件开发和分布式系统的文章。 在他的空闲时间,他喜欢散步。 LinkedIn Twitter         试图为所有可能的用户与您软件的交互手动创建测试用例即使不是不可能的,也仍令人望而却步。 模糊测试,或称 "Fuzzing",是一种自动化的软件测试,可以帮助发现潜在的错误和安全漏洞。 该测试涉及将随机数据(或 “Fuzz”)注入被测软件中。 这种测试框架可以帮助发现可能导致崩溃或其他安全问题的未定义行为。 虽然不可能用模糊测试找到所有的错误,但它可以成为发现和修复许多常见类型问题的有效方法。 它经常被用来测试那些处理来自不受信任来源的输入或可能有意外输入的程序。 Go 测试综合指南 模糊测试通常是自动化的,可用于测试功能和安全缺陷。 功能性模糊测试包括向程序输入无效数据以检查意外行为。 安全性模糊测试包括向程序输入恶意数据以发现安全漏洞。 Go有一个很棒的软件包生态系统,比如开源的模糊测试软件包 gofuzz 和 go-fuzz。它们使用简单,可以集成到您的测试工作流程中。 然而,从 Go 1.18 开始,语言中出现了原生的模糊测试支持,这意味着您