内核中的第二语言

邮件原文:https://lkml.org/lkml/2021/4/14/1023

众所周知,在内核中引入一种新语言有巨大成本和风险。因为这会分散精力,也会提升对内核某些部分的知识门槛。

最重要的是,引入新语言之后,如果该语言不再被支持了,那用这门语言写的模块之后很难被替换。

无论如何,我们相信,即使在今天,使用Rust也是利大于弊。这些后面会解释。

请注意,我们说的Rust支持,指的是支持用Rust写驱动以及类似的“叶”模块,至少在可见的将来是这样。特别地,并不是要重写内核的核心代码,也不是要重写内核的重要子系统(比如kernel/mmsched/,等等)。对Rust的支持会建立在这些之上。

目标

通过在Linux内核中使用Rust,我们希望达成如下目标。

  • 用Rust写的新代码能降低内存安全隐患、数据争用和整体的逻辑错误,这些是由下面提到的语言特性决定的。
  • 维护者更有信心重构和接收对模块的补丁,这利益于下面提到的Rust安全能力。
  • 新驱动和模块写起来会更容易,因为Rust基于现代语言特性的抽象更容易推断,而且有详尽的文档可供查阅。
  • 更多人可以参与内核开发,因为使用现代语言的人更多。
  • 利用Rust工具生态的优势,可以强制保证之前我们制定的文档规范落地。比如,要求所有公共API、安全前置条件、unsafe块和类型不变量都必须有文档说明。

为什么选Rust

Rust是系统编程语言,就Linux内核而言,拥有几个C所不具备的重要优势。

  • 使用其安全特性时不存在未定义行为(when unsafe code is sound),包括内存安全和不存在数据急用。
  • 类型系统更严格,进一步减少逻辑错误。
  • 可以清晰地区分安全与unsafe代码。
  • 特性丰富:sum类型、模式匹配、泛型、RAII、生命期、共享&专有引用、模块&可见性、强大的清洁能力(hygienic),以及过程式的宏,等等。
  • 大量独立的标准库:ResultOption等词表类型、迭代器、格式化、受控/丰富/包装的整数计算,等等。
  • 开箱即用的工具,如文档生成器、格式化、代码纠错,全部内置在编译器中。

总的来说,Rust这门语言成功吸引了数十年来系统编程乃至函数式编程的经验,而且增加了生命期和借用检查机制。

为什么不用

相对于C,就Linux内核而言,Rust也有一些不足。

  • 多年来围绕内核投入了非常多的人力开发C工具,包括编译器插件、坏码检查(sanitizer)、Coccinelle、lockdep、sparse等。不过,随着Rust在内核上的应用得越来越多,这些问题有可能改善。
  • 基于LLVM的唯一实现。有人正着手解决这个问题,比如GCC前端、基于Cranelift的rustc后端和旨在缩短自举流程的编译器mrustc。欢迎对任何这些项目提供帮助。
  • 没有标准化。尽管还不清楚标准化是否对内核有益,但有几点可以把风险降到最低:Rust稳定性承诺、丰富的文档、WIP参考,还有详尽的RFC,等等。
  • 编译通常比较慢,一方面语言特性比较复杂,另一方面当前的编译器也施加了诸多限制。
  • 目前,还需要一些最新的特性。换句话说,就是稳定版编译器还不支持的特性。无论如何,我们计划在一年内扫清这些限制,要么让rustc稳定版支持它们,要么在我们的代码中去掉相关特性。这里有我们维护的一个报告:https://github.com/Rust-for-Linux/linux/issues/2
  • 当前文本量比预期大,原因是corealloc这两个Rust标准库里有一些未用到的部分。我们计划逐步解决这个问题。

上述问题多数缘于Rust还是一个相对年轻、应用较少的语言。不过,相信Rust很有希望成为系统编程大家庭中的重要成员,就像过去几十年的C一样。因此,多数上述问题都会随着工业界将更多资源倾注于Rust而缓解。

设计

有几个重要的设计决定。

首先,Rust内核模块要求共享一些代码,可以通过一个配置项生效(CONFIG_RUST)。这样可以让个别模块更小。对Rust的支持包括:

  • Rust标准库。目前是corealloc,未来可能只需要core的子集。这些基本上相当于C标准库的那些独立子集。
  • 封装内核API的抽象。代码位于rust/kernel。目的是让它们尽可能安全,因而让用Rust写的模块尽可能少地依赖unsafe代码。
  • 其他,比如module!过程式宏、编译器内置能力、生成的绑定及辅助函数,等等。

支持Rust占据比较大的空间,当然把Rust标准库中那些未用代码删掉会变小。

以下是我们在CI中使用一个小x86_64配置的例子:

   text    data     bss      dec

7464833 1492128 2301996 11258957 vmlinux (without Rust support)
7682527 1709252 2301996 11693775 vmlinux (with    Rust support)
7682527 1721540 2301996 11706063 vmlinux (plus overflow checks)

   2224       0      16     2240 samples/rust/rust_semaphore_c.o
   3694       0      10     3704 samples/rust/rust_semaphore.o
   2367     768      16     3151 samples/rust/rust_semaphore_c.ko
   3829     768      10     4607 samples/rust/rust_semaphore.ko

  80554    5904   20249   106707 drivers/android/binder.o
  12365    1240       9    13614 drivers/android/binder_alloc.o
  92818       8      16    92842 drivers/android/rust_binder.o

在启动溢出检查的vmlinux中,文本(text)多出3%,整体多出4%。模块本身与对应的C代码接近。

上表也比较了Binder和Rust对应的原型实现。注意,虽然Rust版还达不到原始模块的要求,但可以进行大致的估计。如表所见,C Binder的文本总和少于Rust驱动,但加一块更大。

其次,Rust写的模块永远都不应该使用C内核API。因为在内核中使用Rust最主要的目的是提供安全抽象,让模块更容易推断,进而更容易评审、重构,等等。

另外,与内核C端的绑定是通过bindgen(Rust官方工具)动态生成的。这样可以不用在Rust端更新绑定。

宏还是需要手工处理,有些函数会被行内化,对此我们要创建辅助函数从Rust里调用。

第三,在Rust代码中,多数文档都是跟源代码写在一块的,格式为Markdown。我们也遵循了这个惯例,因而虽然一些通用文档在Documentation/rust/里,但实际上多数文档都在源代码文件中。

为了方便阅读文档,Rust提供了一个生成HTML文档的工具,类似于Sphinx/kernel-doc,只不过面向的是Rust代码和语言特性。

此外,正如上面解释的,我们也借这个机会推动了文档策略。同时也推动了代码的自动格式化,一套Clippy代码纠错工具。这里面都是按照Rust的惯例来的,比如保持rustfmt这个默认值。比如,代码缩进是4个空格,而不是制表符宽度。如果需要的话我们也乐意修改,关键在于自动格式化。

最后,为避免将GPL符号暴露为非GPL(即使是间接暴露),我们将Rust支持的所有符号都作为GPL导出到了内核中。

状态

这里展示的Rust支持是试验性的,因此很多内核API及抽象都看不到。要覆盖全部API需要很长时时间开发和成熟。其他实现细节也在进行中。

不过,这个支持已经足够好,可以用来实现模块原型了。本RFC包括对已有模块的可用移植,主要是Binder和Android IPC机制。尽管还不能在产品中使用,但它展示了可以做到哪一步,以及将来的Rust模块可能是什么样的。

说到编译器,我们支持Clang构建的内核,以及可能情况下的LLVM=1构建(比如,只要ClangBuiltLinux支持)。此外我们也维护了GCC构建内核的配置,不过现在已经不建议使用了。以bindgen为GCC后端很适合改进对这些构建的支持。

至于架构,我们已经支持了x86_64arm64ppc64le。要支持其他变体如riscvs390mips也不会太麻烦。

我们也加入了linux-next(签署了特别弃权状)。当前,我们的支持是通过!COMPILE_TEST控制的,因为不想由于错误破坏任何产品CI,但如果对本RFC的反馈是正向的,那我们会去掉这个限制。

上游计划

一如既往,早点介入维护是发现缺失细节的最佳方式,因此我们会在合并窗口一到就发送这些变更。

审阅本RFC

我们希望得到模块作者的意见。特别是关于补丁9的示例和补丁13的Binder。也就是说,身为模块作者,看到那里的Rust代码你会作何感想?你觉得自己将来也会写类似的Rust代码,考虑安全/无UB吗?

对Rust抽象本身及本RFC其他细节的意见我们也欢迎,但请注意这些都在进行中,还没最终完成。

另一个我们希望得到反馈的主题是Rust “原生”文档,即写在代码里的那些注释。已经上传到这里了: https://rust-for-linux.github.io/docs/kernel/

我们这种生成的文档还挺好的。请大家也看一眼,告诉我们你的想法。

测试本RFC

如果想测试一下,请遵循Documentation/rust/quick-start.rst中的Quick Start。其中包括如何设置Rust以及其他构建和测试本RFC所需的工具。

本文写作时,本RFC系列与仓库是对应的,但要想及时跟进,请检出我们主干中的rust分支:https://github.com/Rust-for-Linux/linux.git

致谢

(略)

翻译本文的目的是尝试给出ECMAScript规范中核心术语的译法,供同好品评。

原文链接:https://timothygu.me/es-howto/

摘要

ECMAScript语言规范(又称JavaScript规范或ECMA-262)是学习JavaScript复杂工作原理的一手资料。然而,其浩繁的卷帙一开始总会让人无从下手、望而生畏。本文旨在降低阅读这一最佳JavaScript语言参考的阅读门槛。

目录

1. 前言
1.1 为什么应该阅读ECMAScript规范
1.2 什么属于ECMAScript规范,什么不属于
1.3 先别急,ECMAScript规范在哪里?
1.4 查阅规范

2. 运行时语义
2.1 算法步骤
2.2 抽象操作
2.3 什么是[This]
2.3.1 Record的字段
2.3.2 JavaScript对象的内部栏位
2.3.3 JavaScript对象的内部方法
2.4 完成记录:?!
2.5 JavaScript对象
2.6 示例:String.prototype.substring()
2.7 示例:Boolean()String()可以抛异常吗?
2.8 示例:typeof操作符

术语表
常见抽象操作

参考
参考资料

1. 前言

你肯定知道,每天读一点ECMAScript规范有益健康。无论它是你新年的小目标,还是仅仅为了遵照医嘱,反正欢迎阅读本文!

注意:本文只在指称规范是使用“ECMAScript”,其他地方使用“JavaScript”。不过,这两个词指的是同一个东西。(历史上,ECMAScript和JavaScript有一些区别,不过那不是本文的重点,大家可以自行搜索。)

1.1 为什么应该阅读ECMAScript规范

ECMAScript规范是实现JavaScript的权威资料,不管是在浏览器中,在Node.js中,还是在IoT设备中。所有JavaScript引擎的开发者都依靠这份文档确保自己的新特性能够跟其他JavaScript引擎一样如期工作。

不过必须得说明一下,规范的效用绝对不仅仅限于被称为“JavaScript引擎开发者”的那些神秘人物。实际上,规范对你我这样的普通JavaScript程序员一样有用,只是你还不知道罢了。

假如有一天你发现了下面这个问题:

> Array.prototype.push(42)
1
> Array.prototype
[ 42 ]
> Array.isArray(Array.prototype)
true
> Set.prototype.add(42)
TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
    at Set.add (<anonymous>)
> Set.prototype
Set {}

并且想不通为什么一个方法可以对自己的原型起作用,而另一个方法却不能对自己的原型起作用。可惜谷歌总是在你最需要帮助时失灵,而一直给力的Stack Overflow居然也无能为力。

看规范可以找到答案。

或者,你可能就是想知道臭大街的松散相等操作符(==)到底怎么起作用。作为曾经勤奋好学的软件工程师,你在MDN上查到几段解释,但看来看去只会越来越晕。

看规范可以找到答案。

话说回来,我也不推荐JavaScript新手看ECMAScript规范。如果你才开始接触JavaScript,那还是先写几个网页或者Web应用吧!要不就写个基于JavaScript的保姆摄像头!别的什么东西也行!等你体会到足够多的JavaScript缺点,或者已经足够有钱而不必再担心JavaScript了,再回来看这篇文章不迟。

好啦,现在大家知道规范对于理解语言或平台的工作机制非常有帮助了。但要阅读ECMAScript规范,到底应该从哪里入手呢?

1.2 什么属于ECMAScript规范,什么不属于

1.3 先别急,ECMAScript规范在哪里?

谷歌“ECMAScript规范”,会看到很多结果,都说是合法规范。那应该看哪一个?

长话短说,tc39.es/ecma262/这个规范最可能是你想看的。

ECMAScript语言规范由具有不同背景的一群人共同制定,这群人就是TC39(即Ecma International Technical Committee 39)。TC39在tc39.es上面维护着ECMAScript语言的最新规范。

问题在于,TC39每年都会在某个时间点将规范的一个快照变成该年度的ECMAScript Language标准,并给它分配一个版本号。比如,ECMAScript® 2019 Language Specification (ECMA-262, 10th edition)(常被称为ES10或ES2019)就是2019年6月份在tc39.es上看到的规范,把它放福尔马林里那么一泡,一塑封并给出个PDF版,就永久封存了。

为此,除非你只想让自己的Web应用在2019年6月份之前发布的浏览器上跑,否则就应该只看tc39.es上最新的规范。但如果你想(或必须)支持旧版本浏览器或Node.js,那可能老版本规范有用。

ISO/IEC也把ECMAScript语言标准重新发布为ISO/IEC 22275。不过别担心,因为该标准基本上就是一个指向ECMAScript规范的超链接。

1.4 查阅规范

ECMAScript规范涉及的内容极多。即使规范的作者全力将它分成有逻辑的部分,仍然可以用卷帙浩繁来形容。

从个人角度,我喜欢将规范分成五部分。

  • 约定与基础(“什么是Nubmer?规范中说「抛出TypeError异常」时意味着什么?”)
  • 语言的文法产生式(“程序员应该怎么写for-in循环?”)
  • 语言的静态语义(“怎么确定var语句中的变量名?”)
  • 语言的运行时语义(如何执行for-in循环?
  • API(“String.prototype.substring()能做什么?”)

不过规范并不是这么组织的。事实上,它把第一个项目符号放在了“§5 表示方式约定”到“§9 普通与异质对象行”中,接下来三个项目符号穿插放在了“§10 ECMAScript语言:源代码”到“§15 ECMAScript语言:脚本与模块”中,比如:

  • §13.6 if语句文法产生式

    • §13.6.1-6 静态语义
    • §13.6.7 运行时语义
  • §13.7 迭代语句文法产生式

    • §13.7.1 共享的静态和运行时语义
    • §13.7.2 do-while语句

      • §13.7.2.1-5 静态语句
      • §13.7.2.6 运行时语义
    • §13.7.3 while语句
    • ……

而AP则分布在条款“§13 全局对象”到“§27 反射”中。

现在,我想说明一下,绝对没有人从头到尾地读规范。而是只会看与自己想寻找的答案相关的部分,而在该部分中则只看那些需要的地方。因此要学会判断你的问题与上述五大部分中的哪些相关。如果想不出来,可以问自己一个问题:“这个(你想确认的东西)是什么时间被求值的?”,应该有帮助。别担心,查规范查得越多就会越容易的。

2. 运行时语义

语言和API的运行时语义是规范中最大一部分,通常也是人们问题最多的地方。

总体来看,规范中这些部分都很直观明了。不过,规范使用的很多简写形式,让刚开始阅读的人(至少让我)感到很讨厌。接下来会解释一些这种约定,然后通过分析几个特性的原理来看看怎么使用它们。

2.1 算法步骤

ECMAScript中的大多数运行时语义都是通过一系列算法步骤规定的,不像伪代码,但更精确。

示例1

算法步骤的示例:

  1. a为1;
  2. ba+a
  3. b为2,则

    1. 哇!没算错。
  4. 否则

    1. 去!

继续阅读:“§5.2 算法约定”

2.2 抽象操作

有时候,你会看到一个类似函数调用的东西。Boolean()函数的第一步是:

2.3 什么是[This]

2.3.1 Record的字段

2.3.2 JavaScript对象的内部栏位

2.3.3 JavaScript对象的内部方法

2.4 完成记录:?!

2.5 JavaScript对象

2.6 示例:String.prototype.substring()

2.7 示例:Boolean()String()可以抛异常吗?

2.8 示例:typeof操作符

翻译本文的目的是尝试给出ECMAScript规范中核心术语的译法,供同好品评。

原文链接:https://v8.dev/blog/understanding-ecmascript-part-4

环球同此凉热

Mozilla的Jason Orendorff写了一篇深入分析JS诡异语法的文章。虽然实现细节上有差异,但每个JS引擎在这些诡异的细节上都会面对同样的问题。

包含文法

这篇文章将深入探讨包含文法(cover grammar)。包含文法是为那些乍一看模棱两可的语法构造规定文法的一种方式。

为简单起见,我们跳过下标[In, Yield, Await],因为对本文不重要。可以参考第三篇文章,了解它们的含义和用法。

有限前查

通常,解析器在有限前查(finite lookhead,跟进固定个数的标记)基础上决定使用哪个产生式。

有时候,下一个标记可以毫无歧义地决定要使用的产生式。例如

UpdateExpression :
  LeftHandSideExpression
  LeftHandSideExpression ++
  LeftHandSideExpression --
  ++ UnaryExpression
  -- UnaryExpression

- 阅读剩余部分 -

翻译本文的目的是尝试给出ECMAScript规范中核心术语的译法,供同好品评。

原文链接:https://v8.dev/blog/understanding-ecmascript-part-3

这一次我们深入ECMAScript语言及其语法的定义。如果你不太熟悉上下文无关文法,应该先补补课,至少先弄懂一些基本概念。因为规范中使用了上下文无关文法定义语言。

ECMAScript文法

ECMAScript规范定义了4种文法。

  1. 词法文法:描述怎么把Unicode码点(code point)翻译为输入元素(标记、行终止符、注释、空白)序列。
  2. 语法文法:定义标记(token)怎么构成语法正确的程序。
  3. 正则文法:描述怎么把Unicode码点翻译为正则表达式。
  4. 数值字符串文法:描述怎么把String翻译成数字值。

每种文法都用上下文无关文法来定义,都包含一组产生式。

不同的文法使用了不同的表示方式。语法文法表示为LeftHandSideSymbol :,词法文法和正则文法表示为LeftHandSideSymbol ::,而数值字符串文法表示为LeftHandSideSymbol :::。(以冒号的多少来区分。——译者注)

接下来我们详细分析一下词法文法和语法文法。

- 阅读剩余部分 -

翻译本文的目的是尝试给出ECMAScript规范中核心术语的译法,供同好品评。

原文链接:https://v8.dev/blog/understanding-ecmascript-part-2

要理解规范,可以拿一个我们知道的JavaScript特性,看看它是怎么规定的。

注意,本文包含从2020年2月的ECMAScript规范中复制的算法,请以正式规范为准。

我们知道,访问对象的属性需要走查(walk)原型链。如果对象上没有要读的属性,就会沿原型链逐级查找,直到找到这个属性(或者找到一个没有原型的对象)。(这个过程我们可以称其为原型链走查或走查原型链。——译者注)

比如:

const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99

这种原型走查是在哪里定义的?

最好的起点是对象内部方法

有两个与查找属性相关的内部方法:[[GetOwnProperty]][[Get]]。我们感兴趣的是不限制自有(own)属性的,所以就搜[[Get]]吧。

可是,属性描述符规范类型(Property Descriptor)也有一个字段叫[[Get]]。因此搜索的时候要注意从它们不同的用法来区分。

- 阅读剩余部分 -