lectrue20 比较用C和HLL实现OS的优劣

注:基于这位大佬的中文翻译笔记学习,有大段对翻译笔记的直接引用,以及一部分询问AI和结合自己理解的一些内容。写这份笔记只是为了方便自己复习和理解。

https://github.com/huihongxiao/MIT6.S081


C语言实现OS的优劣

引入:学生和开发者经常质疑,为什么非要用C,如果换种语言,是否能避免一堆烦人的bug。虽然社区对此争论不休,但缺乏实际的数据支持。本节课讨论的论文旨在通过构建一个真实的内核,提供量化的数据和深入的分析,而非一个简单的是或否。

实验对象------Biscuit内核:使用一种带有自动内存管理的高级编程语言编写,这意味着程序员不需要手动调用free,从而消除了一大类内存Bug。架构风格遵循传统的Monolithic UINX风格(类似Linux,xv6),以确保对比的公平性。不同于xv6的教学简化实现,Biscuit采用了高性能的数据结构和算法,旨在构建一个真正可用的高性能内核。

C的优势:尽管存在争议,但目前主流OS几乎全是用C写的。这是因为C语言拥有独特的、适合底层的特性:

优势维度 详细说明
极致的控制权 C 允许程序员完全控制内存的分配(malloc)和释放(free)。内核开发往往需要这种精细的资源管理能力。
代码透明度 C 语言几乎没有"隐藏代码"。程序员在读 C 代码时,脑海中几乎能直接映射出对应的汇编指令和机器行为。
直接硬件访问 C 指针可以轻松读写内存中的任意位置,包括页表项(PTE)的标志位、设备寄存器等。这对于驱动开发和内存管理至关重要。
极简运行时 (Runtime) C 不需要庞大的运行时环境(Runtime Environment)。正如在 XV6 启动过程中看到的,只需几行汇编设置好栈,就可以直接跳进 C 代码运行。

运行时:简单来说,运行时就是为了让你的代码能够运行,而必须额外附带的一些支撑代码。在C语言中,运行时需要在main()开始前,建立好栈,处理命令行参数,之后就开始执行你写的代码。而在高级语言中,通常拥有庞大的运行时。比如Go,在你写的第一行代码执行之前,Runtime已经忙活半天了,它的工作如下。一个庞大的运行时会带来不可控的时延,并且本身会有一定内存开销。同时,庞大的运行时还会带来黑盒的问题,在C语言中你完全知道free()发生了什么。但在高级语言中,内存管理是自动的(黑盒),内核开发者很难精确控制物理内存的布局。

  • 垃圾回收器 (GC):启动一个后台线程,专门负责盯着内存,随时准备回收垃圾。

  • 调度器 (Scheduler):Go 有自己的协程调度器,它得先启动起来,决定把哪个协程放到哪个 CPU 线程上。

  • 类型检查与反射:加载类型信息。

  • 栈管理:准备动态扩容的栈。

C的劣势:几十年的经验证明,人类很难写出安全的C代码。C的灵活性也是其安全漏洞的根源。

  • 缓冲区溢出 (Buffer Overrun):

    • 数组越界访问、栈溢出等。这是 C 语言中最臭名昭著的问题,攻击者常利用它来注入恶意代码。
  • 释放后使用 (Use-After-Free):

    • 内存被 free 后,指针依然指向该地址,且后续代码再次使用了这个指针。这会导致数据损坏或安全漏洞。
  • 并发内存管理难题:

    • 在多线程环境下,很难判断一块共享内存何时不再被任何线程使用,从而安全地释放它。

现实世界:CVE数据库显式,仅 2017 年就有 40 个 Linux 内核 Bug 允许攻击者完全接管机器。这些严重漏洞大多源于 buffer overrun 或其他内存安全问题。在xv6的lab cow中,许多同学都遭遇了 use-after-free Bug,这在很多时候是因为难以追踪页面的引用计数。


高级编程语言实现操作系统的优劣势

高级语言(HLL)的好处:

  • 内存安全 (Memory Safety):这是最大的卖点。

    • 自动检查数组越界(Bounds Checking)。

    • 自动检查空指针引用。

    • 结果:上一节提到的那些可怕的 CVE 漏洞(如缓冲区溢出)在高级语言中根本无法编译通过,或者在运行时会安全地 Panic,而不是被黑客利用。

  • 类型安全 (Type Safety):强类型系统防止了数据被错误解读。

  • 自动内存管理 (GC):垃圾回收机制消除了 use-after-freedouble-free 这类难以调试的 Bug。

  • 并发与抽象:提供了更高级的并发原语(如 Go 的 Channel)和更好的模块化特性(类、接口),让代码更易读、更易维护。

高级语言的劣势:

A. 性能代价 (The HLL Tax)

高级语言的安全性不是免费的,它需要消耗 CPU 指令来维持,这被称为 "High Level Language Tax":

  • 运行时检查:每一次数组访问都要检查边界,每一次指针解引用都要检查是否为空,类型转换也需要检查。这些指令累积起来会拖慢速度。

  • GC 开销:垃圾回收器需要扫描内存、标记对象,这会占用 CPU 时间并可能导致不可预测的停顿(Pause)。

B. 内核不兼容性 (Incompatibility)

内核是一个特殊的程序,它需要直接操作硬件,而高级语言的设计初衷往往是屏蔽硬件细节,这就产生了矛盾:

  • 缺乏直接内存访问:内核需要直接读写物理地址(如页表、设备寄存器),但这违反了高级语言的类型安全原则。

  • 汇编集成困难:内核在启动、上下文切换(Context Switch)时必须使用汇编,而高级语言通常很难优雅地内嵌汇编。

  • 并发模型冲突:内核中会有一些特殊的锁操作(如线程 A 加锁,传递给线程 B 释放),高级语言的用户态并发模型可能不支持这种操作。

研究的痛点------缺乏公平的对比:教授指出,虽然历史上有很多用高级语言写的内核(如 Java OS, Singularity),但它们并没有回答"性能损失到底有多少"这个问题。以前的 HLL 内核往往采用了全新的架构(New Architecture)。这导致当它们比 Linux 慢时,我们不知道是因为语言慢,还是因为新架构设计得不好。我们需要保持架构不变(Control the variables),只改变语言(Variable),从而精确测量语言带来的性能损耗。

实验设计:为了得到可信的结论,我们将会把Biscuit内核和Linux对比,xv6本身只是一个未优化的C内核,所以我们将会挑战Linux,真正高度优化的生产级内核。Linux是由成千上万名工程师经过几十年打磨出来的,一个小研究团队不可能重写一个完整的Linux。所以我们采用折中方案。

  • 构建 Biscuit 内核:使用高级语言(Go)编写。

  • 功能对等:实现 Linux 核心功能的子集(Subset),保留 Monolithic 架构。

  • 性能优化:尽力优化 Biscuit,使其性能接近 Linux(In the ballpark)。

  • 测量:在功能和性能基准线相近的情况下,去量化高级语言带来的额外开销(Tax)。


控制变量法:为了公平地评估高级语言在内核开发中的优劣,研究团队设计了一个严格的对比实验。

  • 实验组:Biscuit(用 Golang 编写的新内核)。

  • 对照组:Linux(用 C 编写的成熟内核)。

  • 控制变量:

    • 相同的应用程序:两者都运行 NGINX(Web 服务器)。

    • 相同的接口:Biscuit 实现了 Linux 系统调用的一个子集。NGINX 发出的系统调用参数、频率在两个内核上是完全一致的。

    • 相同的硬件:运行在同样的机器上。

为什么是Golong:

理由 详细说明
1. 静态编译 (Static Compilation) Go 和 Python 不同,它没有解释器。代码直接编译成机器码。这意味着它天生具备高性能的基础,且不需要在内核里塞进一个庞大的解释器。
2. 系统编程基因 Go 被设计用于系统级编程。它能非常方便地调用汇编代码(这对于内核启动和上下文切换至关重要),也能轻松链接外部 C 代码。
3. 优秀的并发支持 内核是高度并发的程序。Go 语言的 Goroutine 和 Channel 机制非常适合处理并发任务。
4. 灵活性 Go 是一种通用的、灵活的语言,适合构建复杂的内核逻辑。
5. 垃圾回收 (GC) 这是最关键的一点。研究的目标之一就是测量"自动内存管理在内核中的代价"。Go 自带高效的 GC,非常适合用来回答"GC 到底会不会拖垮内核"这个问题。

为什么不是Rust:虽然如今Rust在操作系统开发中非常火(比如Linux已经合并了Rust支持),但在论文启动时Rust不够流行和稳定。且Rust的核心特性是通过所有权系统在编译期间内解决内存问题,而不依赖GC。如果用Rust写Biscuit,确实能得到高性能和安全性,但就无法测量GC在内核中的开销了。

注:如果用Rust写内核,即使通过极致优化,也最多和C内核持平。因为C语言已经足够底层,几乎直接对应机器指令。Rust 能做到的底层优化,C 都能做。所以上限是一样的,只是 Rust 更容易写出安全的代码。

注------托管运行时:托管运行时是现代高级编程语言(如 Java, Python, Go, C#)与传统系统级语言(如 C, C++, Rust)区别的关键。简单来说,托管运行时是一个在你的程序运行期间,始终在后台默默工作的管家系统。

所谓托管是指计算机资源的生命周期由运行时环境接管,而不是由程序员直接负责。非托管如C/C++需要自己申请内存,自己释放内存。其代码直接编译成机器码,直接在OS上跑。而托管只需要创建对象,运行时负责回收垃圾(GC)。其代码运行在一个中间层之上,这个中间层负责管理一切。


Heap exhaustion(内核堆耗尽):xv6中没有内核堆,所有内核对象都在编译时静态分配了固定大小的数组,这很简单,但灵活性差。现代内核都使用动态堆分配,当打开文件,创建socket或fork进程时,内核调用malloc动态申请内存。优点是灵活性高,按需分配。但如果有大量进程疯狂申请资源,堆内存会被耗尽。

当内核堆没有空闲空间,而新的请求又到来时,内核必须做出应对。对于Biscuit来收,这个问题比C语言内核更加严重。C语言中,malloc失败会返回NULL,开发者至少可以检查返回值。而Go语言的runtime设计假设内存是无限的或自动管理的。调用 new 通常不返回错误,如果分配失败,runtime 可能会直接 Panic。这导致 Biscuit 很难直接复用 C 内核的处理逻辑。

方案一:直接崩溃 (Panic)
  • 做法:如果分配不到内存,内核直接 Panic(死机)。

  • XV6 的例子:XV6 的 buffer cache 如果满了,就会 panic。

  • 缺陷:对于一个生产级系统,因为内存不足就导致整个系统崩溃是不可接受的。

方案二:等待 (Wait/Sleep)
  • 做法:在内存分配器中睡眠等待,直到有其他进程释放内存。

  • 缺陷:极易导致死锁 (Deadlock)。

    • 场景:进程 A 持有锁 L,并请求内存(进入等待);进程 B 想要释放内存,但为了完成释放操作,它需要获取锁 L

    • 结果:互相等待,系统挂起。即使不是大内核锁,细粒度的锁也容易陷入这种依赖循环。

方案三:返回错误并回退 (Bail out / Error Handling)
  • 做法:malloc 返回 NULL,调用者检查错误并终止当前操作。

  • 缺陷:代码复杂性极高,难以做对。

    • 回滚难题 (Unwinding):一个复杂的系统调用(如 exec 或文件系统操作)通常包含多个步骤。如果在第 5 步申请内存失败,你必须精确地撤销前 4 步的所有操作(释放已申请的内存、恢复磁盘状态、解锁等)。

    • 现实情况:这在工程上非常容易出错,导致资源泄漏或状态不一致。

    • 引用:Linux 社区有关于 "Too small to fail" 的讨论,说明即使是 Linux 也很难完美处理这种复杂的回退逻辑。


Heap exhaustion solution

预留机制:Biscuit并没有试图在内存分配失败时进行恢复,而是选择完全避免分配失败。工作流程如下:

  • 入口检查:当应用程序发起系统调用(如 read, fork)时,在真正执行任何内核逻辑之前,先调用 reserve() 函数。

  • 计算与预留:reserve() 会计算该系统调用在最坏情况下可能需要的内存总量,并尝试从全局堆中预留这部分内存。

  • 成功路径:如果内存充足,预留成功,系统调用开始执行。由于内存已经预留,后续代码中的所有 new 操作保证不会因为 OOM(内存不足)而失败。

  • 失败路径(等待):如果内存不足,进程会在 reserve() 函数内部等待。

  • 清理与归还:系统调用结束后,未使用的预留内存返还给系统。

为什么在这个位置等待是安全的:

  • 无锁状态:reserve() 发生在系统调用的最开始。此时,内核还没有为该进程获取任何锁,也没有持有任何关键资源。

  • 避免死锁:因为没有持有资源,所以在这个位置"挂起"等待是绝对安全的。内核可以在此期间运行内存回收机制(如杀死 OOM 进程)来腾出空间,一旦空间足够,唤醒等待的进程即可。

优势:这种方案完美契合Go语言,且带来了三个巨大的工程优势。

  • 无需错误检查:内核代码中不需要在每次 newmake 后检查是否为 nil(这也符合 Go 的语法习惯)。

  • 无需回滚逻辑:既然分配不会失败,就不需要编写复杂的 bail out 和资源清理代码,大大降低了 Bug 出现的概率。

  • 消除死锁风险:避开了在持有锁的深层调用栈中等待内存分配的死锁陷阱。

静态分析:这个方案的难点在于如何精确计算一个系统调用到底需要多少内存。如果预留太多,会严重限制并发度,如果预留太少,会导致运行时崩溃。Biscuit利用Go强大的静态分析工具解决这个问题。

A. 调用图分析 (Call Graph Analysis)
  • 利用 Go 编译器内部的包,分析系统调用的函数调用链(Call Graph)。

  • 计算公式:S = \\sum (\\text{所有路径上 new 对象的大小})

  • 最坏情况原则:总是计算调用深度最深、分配内存最多的那条路径。

B. 逃逸分析 (Escape Analysis)
  • 问题:函数 h 分配的内存可能会返回给函数 g 继续使用。

  • 处理:分析器必须识别这种"逃逸"行为,将子函数中存活下来的内存加到调用者的配额中。

处理动态边界:静态分析不是万能的,遇到动态行为时需要人工介入:

动态场景 问题 解决方案
循环 (Loops) 循环次数可能取决于参数(如 read(n)),静态无法确定。 人工标注:开发者在代码中标注该循环的最大次数(Max Loop Count),分析器读取标注进行计算。
递归 (Recursion) 递归深度无法静态预测。 禁止递归:Biscuit 内核代码中避免使用递归,或者特殊处理。
切片扩容 (Slice) append 操作可能导致底层数组扩容(内存翻倍)。 人工标注:标注切片的最大容量(Max Capacity)。

Biscuit的堆耗尽解决方案是"静态分析+运行时预留"的结合。这也展示了高级语言的一个隐藏优势:强大的工具链可以辅助解决系统编程中的硬核难题。

  • 它利用了 Go 语言易于分析的特性,通过工具计算出每个系统调用的最大内存开销。

  • 它将"等待内存"的时机提前到了无锁的入口处,从而巧妙地规避了复杂的死锁和错误处理问题。

  • 虽然需要一定的人工标注工作量(Cody 花了几天时间),但这使得用高级语言编写无 Panic 的内核成为可能。


使用Go带来的收益

首先声明,Biscuit是一个高性能内核,他并非xv6那样的教学玩具,而是实现了很多Linux级别的优化。高级语言没有阻碍开发者实现这些复杂的底层优化。

下面从三个方面证明高级语言带来的收益是真实的,而不是通过作弊获得的。

问题一:有没有"作弊"?(Feature Usage)

质疑:Biscuit 是否为了性能,故意避开了 Golang 中那些开销大但好用的高级特性(如 GC、Interface、Channel),把 Go 当作"带垃圾回收的 C"来写?

验证方法:

  • 使用静态分析工具,对比 Biscuit 内核与两个大型 Go 项目(Go Runtime 和 Moby/Docker)的代码特征。

  • 统计每 1000 行代码中高级特性的使用频率。

结果:

  • 常用特性:new (分配内存), map (哈希表), slice (动态数组), 多返回值, 闭包, 接口 (Interface), 类型断言。

  • 少用特性:channel (在内核和 Docker 中都用得少)。

  • 数据对比:Biscuit 对高级特性的使用频率与标准 Go 项目高度一致,没有明显区别。

结论:没有作弊。Biscuit 充分利用了 Golang 的高级特性,没有刻意回避。

问题二:高级语言是否简化了代码?(Simplification)

定性评估:是的,代码更简单了。

  • GC 的功劳:不需要像 XV6 那样在 exit 时编写复杂的资源释放逻辑,GC 会自动回收 VMA 和结构体。

  • 数据结构:直接使用内置的 map 替代了 C 语言中手写的线性扫描或复杂的哈希表实现。

核心案例:并发数据共享(无锁读)

  • 场景:多个线程共享数据(如链表),且读多写少。为了高性能,读操作不应该加锁。

  • C 语言的困境:

    • 如果读者正在读一个节点,写者把它删了并 free 了,读者就会触发 Use-After-Free。

    • Linux 的解法 (RCU):非常聪明但极其复杂。必须遵守严格规则(如读临界区不能睡眠、不能切换上下文),编程难度极大,容易出错。

    • 另一种解法 (引用计数):原子操作维护计数,性能开销大。

  • Go 语言的解法:

    • 直接读取:使用 atomic_load 读取指针,完全不需要锁。

    • 自动回收:当写者把节点从链表中移除(pop)后,写者不需要(也不应该)手动释放内存。

    • GC 兜底:GC 会自动检测到该节点不再被任何线程(包括正在读的线程)引用时,才会真正释放内存。

结论:GC 彻底消除了复杂的生命周期管理难题,使得无锁并发编程变得极其简单且安全。

问题三:能否防御漏洞?(Safety / CVEs)

验证方法:

  • 选取了 2017 年 Linux 内核的 65 个 CVE (通用漏洞披露)。

  • 逐一分析:如果同样的逻辑用 Go 写,这个 Bug 还会存在吗?

结果:

  • 逻辑 Bug (Logic bugs):Go 救不了。比如权限检查逻辑写错了,Go 和 C 表现一样。

  • 内存安全 Bug (Memory-safety bugs):共有 40 个。

    • 8 个直接消失:例如 Use-After-Free,因为有 GC,这类问题在 Go 中根本不存在。

    • 32 个转化为 Panic:例如数组越界(Out-of-bound)。在 C 中这会导致缓冲区溢出攻击(黑客接管机器);在 Go 中这会导致内核 Panic(死机)。

结论: 虽然 Panic 导致服务中断(Availability 损失)也不完美,但相比于被黑客利用漏洞获取 Root 权限(Security 彻底沦陷),Panic 显然是更好的结果。高级语言成功消除了所有内存安全类的漏洞利用可能。

总之,使用高级语言实现内核的收益是巨大的。

  • 开发效率:功能实现不打折,代码更简洁。

  • 并发简化:GC 让无锁编程变得"傻瓜式"简单。

  • 安全性:从根源上消灭了 Use-After-Free,并将缓冲区溢出降级为 Panic,大幅提升系统安全性。


HLL performance cost1

高级语言的性能代价:总体吞吐量上,Biscuit虽然性能不及Linux,但是没有数量级的差距,而是同一水平线上。在Mailbench上,慢约10%,NGINX/Redis上慢约10%-15%。使用高级语言构建的内核在性能上是完全可用的,并没有想象中那么糟糕。

HLL Tas拆解:Biscuit比Linux慢的部分到底慢在哪。

开销类型 占比 说明
Prologue (函数序言) 最高 Golang 为了支持动态栈增长(Goroutine 栈很小,需要自动扩容),在每个函数开头都要检查栈空间是否足够。这是最大的单一开销来源。
GC (垃圾回收) ~3% 在标准测试中,GC 的开销惊人地低。这说明虽然 GC 在运行,但因为内存分配合理,并未占用大量 CPU。
Safety (安全检查) 2%-3% 包括数组边界检查、空指针检查等。这是为了换取安全性所必须付出的代价,占比较小。
Write Barrier (写屏障) 极低 GC 为了跟踪指针变化而插入的代码(类似于并发 GC 中的着色标记)。

令人惊讶的是,GC并不是性能杀手,反而是函数调用的栈检查消耗了更多时间。

GC与内存大小的关系:GC的性能高度依赖于空闲内存的大小。研究团队发现,在内存紧张时,GC被迫频繁运行,CPU开销多达34%,系统性能严重下降。而内存较宽裕时,CPU开销降至约9%。

结论:教授总结了一个关于GC性能的黄金法则------为了让GC的性能开销保持在可接受范围(<10%),需要配置大约3倍于存活对象大小的物理内存。在"内存越来越便宜"(好像不是真的)的今天,浪费一点内存来换取开发效率和安全性(使用带 GC 的语言)是完全划算的。


HLL performance cost2

这一节通过两个更细致的维度继续评估 HLL(高级语言)的性能代价:垃圾回收导致的延迟(GC Pause) 以及 纯粹的语言指令开销。

1. GC 暂停时间 (GC Pause Latency)

除了吞吐量,延迟 (Latency) 也是衡量内核性能的关键指标。Go 的 GC 是并发的,但在进行 Write Barrier(写屏障) 处理时,仍需要极短的"Stop the World"(暂停应用程序)。

  • 测试场景:Web Server 负载。

  • 测量数据:

    • 最大单次暂停:115 微秒 (\\mu s)。

      • 原因:主要是扫描 TCP 连接表(TCP Connection Table)时的标记工作。
    • 单次 HTTP 请求的最大总延迟:582 微秒。

      • 这是处理一个请求过程中所有 GC 微小暂停的累加和。
    • 频率:超过 100 微秒的暂停非常罕见(< 0.3%)。

  • 评价:

    • 参考 Google 的经典论文《The Tail at Scale》,通常对于长尾延迟的容忍度在 毫秒 (ms) 级别。

    • Biscuit 的 582 微秒 远低于毫秒级阈值,完全在可接受的预算范围内。

    • 这证明了 Golang 的 GC 实现得非常出色,且随着版本更新不断优化。

2. 终极公平对比:C vs Go 微基准测试 (Microbenchmark)

为了消除"Biscuit 和 Linux 功能实现不同"这一变量,研究团队做了一个极端的控制变量实验:编写两个完全相同的微型内核,一个用 C,一个用 Go。

  • 实验内容:管道往返 (Pipe Round-trip)。

    • 将一个字节从管道一端传到另一端。

    • 特点:这个过程不涉及内存分配(因此没有 GC 干扰),纯粹测试语言本身的指令执行效率。

  • 代码对比:

    • Go:1.2K 行代码。

    • C:1.8K 行代码。

    • 团队仔细检查了汇编代码和最耗时的 10 个热点,确保两者的逻辑完全一致。

  • 性能结果:

    • Golang 比 C 慢约 15% (操作数/秒)。
  • 原因分析 (HLL Tax 验证):

    • 分析 Go 的汇编代码发现,函数序言 (Prologue) 和 安全检查 (Safety-check) 相关的指令大约占了总指令数的 16%。

    • 结论:性能的差距(15%)与额外指令的开销(16%)几乎完美吻合。

核心总结:

  • GC 延迟不可怕:Go 的 GC 暂停时间在微秒级,对于大多数网络服务 SLA 来说完全不是问题。

  • 性能差距来源明确:Go 比 C 慢约 15%,这并不是因为 Go 编译的代码质量差,而是因为 Go 为了安全性(栈检查、边界检查)必须执行额外的指令。

  • 竞争力:15% 的性能损失换取内存安全和开发效率,在内核开发中是一个非常有竞争力的交易(Trade-off)。这不是 2 倍或 10 倍的差距,而是工业界可以接受的范围。


我们应该在新的内核中使用HLL吗?

核心结论:教授并未给出非黑即白的答案,而是给出了一个基于需求的决策框架:

  • 选择 C 语言的情况:

    • 极致性能:如果你无法接受 10% - 15% 的性能损耗(HLL Tax)。

    • 内存极度受限:如果你在微控制器或极小内存环境下工作,GC 的内存开销(需要 3 倍冗余)是不可接受的。

  • 选择高级语言 (HLL) 的情况:

    • 安全性优先:如果你希望从根源上杜绝 Use-After-Free、缓冲区溢出等内存安全漏洞。

    • 通用场景:在大多数不追求极致压榨硬件的场景下,用 15% 的性能换取高安全性和高开发效率,是非常合理的选择。

最终感悟:编程语言只是工具。Biscuit 的成功证明了,语言不会限制你做什么,你可以用 Go 写内核,也可以用 C 写。操作系统的本质是对硬件的抽象和管理,而语言只是实现这一目标的手段。只要我们愿意支付一点性能税并处理好Runtime和裸机的适配,高级语言完全有能力构建出安全,高效且复杂的操作系统内核。

为什么xv6选择C:C的透明度更好。而Go隐藏了太多细节,而操作系统课程的目的是揭示线程如何创建,内存如何管理。所以需要C所写即所得的特性。

相关推荐
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.2 小时前
Keepalived VIP迁移邮件告警配置指南
运维·服务器·笔记
ling___xi3 小时前
《计算机网络》计网3小时期末速成课各版本教程都可用谢稀仁湖科大版都可用_哔哩哔哩_bilibili(笔记)
网络·笔记·计算机网络
中屹指纹浏览器4 小时前
中屹指纹浏览器底层架构深度解析——基于虚拟化的全维度指纹仿真与环境隔离实现
经验分享·笔记
Hello_Embed4 小时前
libmodbus 移植 STM32(基础篇)
笔记·stm32·单片机·学习·modbus
无聊的小坏坏5 小时前
实习笔记:用 /etc/crontab 实现定期数据/日志清理
笔记·实习日记
香芋Yu5 小时前
【机器学习教程】第04章 指数族分布
人工智能·笔记·机器学习
深蓝海拓6 小时前
PySide6从0开始学习的笔记(二十六) 重写Qt窗口对象的事件(QEvent)处理方法
笔记·python·qt·学习·pyqt
中屹指纹浏览器6 小时前
中屹指纹浏览器多场景技术适配与接口封装实践
经验分享·笔记
BugShare8 小时前
Obsidian 使用指南:从零开始搭建你的个人知识库
笔记·obsidian
深蓝海拓9 小时前
PySide6从0开始学习的笔记(二十五) Qt窗口对象的生命周期和及时销毁
笔记·python·qt·学习·pyqt