在某些场景,比如 Rust 的应用场景下,速度是竞品的 10 倍,哪怕只是两倍都是关系到生死存亡的大问题。速度决定了这套系统在市场上的命运,跟硬件市场丝毫不差。
——Graydon Hoare
从开始使用高级语言编写操作系统至今的 50 多年来,系统编程语言已经取得了长足的进步。然而,有两个问题至今仍无法破解。
- 要写出安全的代码并不容易。想用 C 和 C++ 恰当地管理好内存特别困难。几十年来,用户已经饱受安全漏洞的侵害,至少可以追溯到 1988 年的 Morris 蠕虫病毒。
- 编写多线程代码非常困难,而多线程又是充分利用现代计算机能力的唯一方式。即便是经验丰富的程序员,在应对与线程有关的代码时也必须多加留心,因为并发会导致各式各样的新 bug,还会让普通的 bug 难以复现。
那么有这样一种语言,它不仅具有 C 和 C++ 性能,同时安全且支持并发。它就是 Rust。你可能 1 周入门 Python,两周上手 GO,但是入门 Rust,似乎让人皱了眉。不过这丝毫不影响大家对 Rust 的喜爱程度。纵然在 2020 年 Stack Overflow 的报告中 Rust 被开发者评为「最爱」的编程语言,但是 97% 的人还是没有真正使用过 Rust。
Rust 是由 Mozilla 和社区贡献者共同开发的一种新的系统编程语言。与 C 和 C++ 类似,Rust 为开发者使用内存提供了完善的控制机制,在语言的原始操作与运行它的机器的操作之间维护着一种紧密的关系,让开发者能够预测自己代码的运行成本。
Rust 承载着 C++ 之父 Bjarne Stroustrup 在他的论文“Abstraction and the C++ Machine Model”中明确提出的理想:
总的来说,C++ 实现遵循零开销原则:不用的,不必为之付出代价;要用的,也不会有代码比它更好。
在此基础之上,Rust 又追加了自己内存安全和可靠并发的目标。
Rust 实现上述所有承诺的关键在于所有权(ownership)、转移(move)和借用(borrow)机制造就的新型系统,而编译时检查和认真的设计又成就了 Rust 灵活的静态类型系统。
所有权机制为每个值规划了清晰的生命期,从而让核心语言不再需要垃圾收集,同时还为管理套接口(socket)和文件勾柄(handle)等资源提供了可靠而又灵活的接口。转移把值从一个所有者转移给另一个所有者,而借用让代码可以临时使用某个值,同时又不影响其所有权。也可能你之前从未碰到过此类特性的这种形式。同样的所有权规则也是 Rust 值得依赖的并发模型的基础。说到互斥量(mutex)与其所要保护数据的关系,大多数语言是靠注释解决问题的。而 Rust 通过编译时检查可以发现访问被锁住的互斥量的问题。大多数语言只会告诫开发者要确保不访问已经交给其他线程的数据,Rust 却能通过检查保证你没有那么做。Rust 能够在编译时防止数据争用。Rust 并非真正的面向对象语言,它只是具有一些面向对象的特征而已。Rust 也不是函数式语言,虽然它可以像函数式语言那样让计算结果更容易推断。Rust 在某种程度上类似于 C 和 C++,但 C 和 C++ 的很多惯用语法不能照搬过来在 Rust 中使用,所以典型的 Rust 代码说到底还是不像 C 或 C++ 代码。关于 Rust 到底是哪种语言,最好还是等你熟悉它之后,自己来下结论吧。为了通过真正的项目获得关于设计的反馈,Mozilla 用 Rust 开发了Servo,这是一个新的 Web 浏览器引擎。Servo 的需求与 Rust 的目标完美匹配:浏览器必须高性能,还要能安全地处理不受信的数据。Servo 利用 Rust 的安全并发最大限度地挖掘机器潜力,在某些任务上实现了 C 或 C++ 不可能实现的并行处理。Servo 与 Rust 一直并肩成长,Servo 不断应用 Rust 的最新语言特性,Rust 也基于 Servo 开发者的反馈不断改进。
Rust 是类型安全的语言,那么“类型安全”指的是什么?安全听起来不错,但要从哪里做 起呢?
以下是 C99,也就是 C 编程语言 1999 年标准中对未定义行为的定义:
根据 C99,因为这段程序访问的元素超出了数组 a
的边界,所以它的行为是未定义的。换句话说,执行这段代码可以出现任何结果。当我们在 Jim 的笔记本计算机上运行这段程序时,看到了以下输出:然后程序就崩溃了。Jim 的笔记本计算机上根本就没有一个叫 .netrc 的文件。如果你在自己的计算机上运行这段代码,很可能结果又不一样。C 编译器为这个 main
函数生成的机器码恰好把数组 a
保存到返回地址前面 3 个字的位置,因此把 0x7ffff7b36cebUL 保存到 a[3]
,会把 main
的返回地址改为指向 C 标准库中一段代码的中间,该代码会从某人的 .netrc 文件中读取密码。在 main
返回时,执行并没有恢复到 main
的调用者,而是转到了库中以下几行代码的机器码:C 编译器允许数组引用影响后续 return 语句的行为是完全符合标准的。未定义操作并非只产生意想不到的结果,事实上这种情况下程序无论做任何事情都是被允许的。为了生成更快的代码,C99 授予编译器全权。这个标准没有让编译器负责检测和处理可疑的行为(比如数组越界),而是让程序员负责保证这种情况永远不会发生。从经验来看,人类在这方面并不擅长。在犹他大学读书时,研究员李朋(Peng Li)修改了 C 和 C++ 编译器,让它们编译后的程序可以在执行某种形式的未定义行为时发送报告。他发现几乎所有程序都会发送报告,包括那些高标准严要求的备受推崇的项目。实践中,未定义行为经常会导致可被利用的安全漏洞。Morris 蠕虫病毒就是利用前面代码的原理并经过精心改造,将自己复制到不同机器的。而基于同样原理的漏洞利用在今天仍然广泛存在。基于这个例子,可以定义几个术语。如果将一个程序写得不可能在执行时导致未定义行为,那么就称这个程序为定义良好的(well defined)。如果一种语言的安全检查可以保证所有程序都定义良好,那么就称这种语言是类型安全的。如果足够用心,用 C 或 C++ 应该也能写出定义良好的程序,但 C 和 C++ 不是类型安全的:前面的程序中没有类型错误,但出现了未定义行为。相对而言,Python 是类型安全的。Python 乐意花处理器时间来检查和处理数组索引越界的操作,方式也比 C 更友好:Python 抛出了异常,这不是未定义行为:Python 文档指出,a[3] 这种赋值应该抛出 IndexError
异常(我们也看到了)。当然,ctypes 之类提供对机器无约束访问的模块可能会在 Python 代码中引入未定义行为,但其核心语言本身还是类型安全的。Java、JavaScript、Ruby 和 Haskell 在这方面都类似。
注意,类型安全与一门语言是在编译时还是在运行时检查类型无关。C 在编译时检查,但它不是类型安全的;Python 在运行时检查,但它是类型安全的。说起来还真有点尴尬,占有统治地位的系统编程语言 C 和 C++ 都不是类型安全的,大多数其他流行的语言则是类型安全的。
考虑到设计 C 和 C++ 的初衷就是用它们去实现系统的基础部分、实现可靠的安全隔离,以及操作不可信数据,类型安全对它们而言好像恰恰是最有价值的特性才对。Rust 要解决的正是这个沉淀了几十年的老问题:它既是类型安全的,又是一种系统编程语言。Rust 的设计初衷也是用于实现那些要求高性能和对资源精细控制的基础系统层,同时还能基于类型安全提供最基本的可预测性。本书后面的章节将详细介绍 Rust 是如何做到这个统一的。对多线程编程而言,Rust 特定形式的类型安全有着令人意想不到的影响。众所周知,正确实现并发在 C 和 C++ 中非常困难。开发者通常仅在单线程实在无法满足性能要求时才会考虑并发。但 Rust 可以通过编译时检查保证并发不会发生数据争用,并会捕获任何对互斥量或者其他同步原语(synchronization primitive)的错误使用。使用 Rust 编写并发程序,你再也不用担心自己的代码只有经验非常丰富的程序员才能看懂了。Rust 的安全规则也有一个“逃生阀”,用于必须使用原始指针的情形。这种情况下你写的是不安全代码,虽然绝大多数 Rust 程序用不到.与其他静态类型的语言相似,Rust 的类型除了可以防止未定义行为,还有很多优点。经验丰富的 Rust 程序员利用类型不仅可以保证安全地使用数据,也可以保证有意义地使用数据,即与应用的意图保持一致。特别地,Rust 的特型(trait)和泛型(generic),为描述一组类型的共性,乃至进一步利用这些共性提供了简洁、灵活且高效的手段。Rust 是系统编程发展史上的一个巨大进步,如果你想了解更多,这本比 Rust 官方「The Book」 还要好懂的 Rust 「螃蟹书」没准可以帮到你。这本不仅是教会你如何用 Rust 编写程序,更重要的是要教会你如何利用这门语言写出安全又得体的程序,同时还能够预测程序的执行。这是一本十分全面的 Rust 学习指南。如果你苦于怎么入门并学好 Rust,没准这是你的机会~本书由两位具有数十年经验的系统程序员撰写,他们不仅分享了自己对Rust的深刻见解,而且还提供了一些建议和操作实践,对 Rust 开发者和系统程序员十分有帮助。京东领劵价格更低
福利来了
为了回馈读者,高可用架构联合图灵教育送出5本《Rust 程序设计》,选取本文留言点赞最高 5 条,截止时间周三(11月4日)晚 23:00。
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。