实际上编程语境中的 runtime 至少有三个含义,

  • 指「程序运行的时候」,即程序生命周期中的一个阶段。例句:「Rust 比 C 更容易将错误发现在编译时而非运行时。
  • 指「运行时库」,即 glibc 这类原生语言的标准库。例句:「C 程序的 malloc 函数实现需要由运行时提供。
  • 指「运行时系统」,即某门语言的宿主环境。例句:「Node.js 是一个 JavaScript 的运行时。

含义一:程序生命周期中的阶段

一个程序从写好代码字符串(起点)到跑完退出(终点),有一整套标准化的生命周期(流程),可以被拆分为多个阶段。这其中编译阶段是 compile time,链接阶段是 link time,那运行起来的阶段自然就是 run time 了。

含义二:运行时库(runtime library)


怎样理解 runtime library 呢?要知道 C、C++ 和 Rust 这类「系统级语言」相比于 JavaScript 这类「应用级语言」最大的特点之一,就在于它们可以胜任嵌入式裸机、操作系统驱动等贴近硬件性质的开发——而所谓 runtime library,大致就是这时候你没法用的东西。

  1. #include <stdio.h> // 1
  2. int main(void) { // 2
  3. printf("Hello World!\n"); // 3
  4. }

这里面除了最后一个括号,每行都和运行时库有很大关系,在缺少操作系统和标准库的裸机环境下,上面的代码是跑不起来的。

  1. stdio.h 里的符号是 C 标准库提供的 API,我们可以 include 进来按需使用(但注意运行时库并不只是标准库)。
  2. main 函数是程序入口,但难道可执行文件的机器码一打开就是它吗?这需要有一个复杂的启动流程,是个从 _start 开始的兔子洞。
  3. printf 是运行时库提供的符号。可这里难道不是直接调操作系统的 API 吗?实际上不管是 OS 的系统调用还是汇编指令,它们都不方便让你直接把字符串画到终端上,这些过程也要靠标准库帮你封装一下。

虽然 C 的 if、for 和函数等语言特性都可以很朴素且优雅地映射(lowering)到汇编,但必然会有些没法直接映射到系统调用和汇编指令的常用功能,比如上面介绍的那几项。对于这些脏活累活,它们就需要由运行时库(例如 Linux 上的 glibc 和 Windows 上的 CRT)来实现。

image.png
运行时库并不只是标准库,你就算不显式 include 任何标准库,也有一些额外的代码会被编译器插入到最后的可执行文件里。比如上面提到的 main 函数,它在真正执行前就需要大量来自运行时库的辅助。

总之,由于系统级语言被设计成既可以用来写操作系统上的原生应用,也可以用来写 bare metal 的裸机程序,因此这类语言需要的运行时(runtime)被设计成了可以按需使用的库(library),于是我们就自然地得到了 runtime library 这个概念。

含义三:运行时系统(runtime system)


上面介绍的运行时库,主要针对的是 C、C++ 和 Rust 这些「系统级语言」。只要将这个概念继续推广到其他高级语言,这时候的「运行时」指的就是 runtime system 了——如果讨论某门高级语言的运行时,我们通常是在讨论一个更重、更大而全的运行时库

比如 Java 的运行时是 JRE,C# 的运行时是 CLR。这两者都相当于一个需要在 OS 上单独安装的软件,借助它们来解释执行相应语言的程序(编译出的字节码)。而对 JavaScript 来说,一般「JS 引擎」是个不带 IO 支持的虚拟机,需要浏览器和 Node 这样的「JS 运行时」才能让它控制文件、网络、图形等硬件资源而真正实用。这些都是很经典的模型了。

典型的高级语言「运行时系统」里大概需要这些基础组件:

  • 一个解释执行字节码的虚拟机,多半得带个垃圾回收器。
  • 如果语言是源码解释执行,那么需要一个编译器前端做词法分析和语法分析。
  • 如果运行时支持 JIT 优化,那么还得藏着个编译器后端(动态生成机器码)。
  • IO 相关能力,比如 Node.js 的 fs.readFile 之类。