文章目录

  • 一、程序生命周期中的阶段
  • 二、运行时库(runtime library)
  • 三、运行时系统(runtime system)



编程语境中的 runtime 至少有三个含义,分别可以这样概括:

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

一、程序生命周期中的阶段

一个程序从写好代码字符串(起点)到跑完退出(终点),有一整套标准化的生命周期(流程),可以被拆分为多个阶段。这其中编译阶段是 compile time,链接阶段是 link time,那运行起来的阶段自然就是 run time 了。如果在前面的阶段预先做了通常在后面才方便做的事,我们就管这个叫 ahead of time。

二、运行时库(runtime library)

运行时库是一种被编译器用来实现编程语言内置函数,以提供该语言程序运行时支持的一种特殊的计算机程序库。运行时库由编译器决定,以面向编程语言,一般由编译器生产商提供。

通常运行时库是以LIB或DLL形式提供的,运行时库除了提供必要的库函数调用(如memcpy、printf、malloc等)之外,它提供的另一个最重要的功能是为应用程序添加启动函数。C运行时库启动函数的主要功能为进行程序的初始化,对全局变量进行赋初值,加载用户程序的入口函数。

  • main 函数是程序入口,但难道可执行文件的机器码一打开就是它吗?这需要有一个复杂的启动流程,是个从 _start 开始的兔子洞。
  • stdio.h 里的符号是 C 标准库提供的 API,我们可以 include 进来按需使用(但注意运行时库并不只是标准库)。比如 printf 就是运行时库提供的符号。可这里难道不是直接调操作系统的 API 吗?实际上不管是 OS 的系统调用还是汇编指令,它们都不方便让你直接把字符串画到终端上,这些过程也要靠标准库帮你封装一下。
  • math.h:很多精简指令集或嵌入式的低端 CPU 未必会提供做 sin 和 cos 这类三角函数运算的指令,这时它们需要软件实现。
  • string.h:你觉得硬件和操作系统会内置「比较字符串长度」这种功能吗?当然也是靠软件实现啦。
  • stdlib.h:直接通过 mmap 这类 OS 系统调用来分配内存是过于底层的,一般也需要有人帮你封装。分配内存的 malloc 虽然只是一个接受单个参数的函数,它的实现可远没有表面上的 API 那么简单。

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

我们可以把「应用程序、运行时库和 OS」三者间的关系大致按这样来理解:

runtimeService和repositoryService如何关联 runtime与developer pack_机器码


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

runtimeService和repositoryService如何关联 runtime与developer pack_机器码_02

除了加载和退出这些程序必备的地方以外,运行时库还可以在程序执行过程中被隐式而按需调用。例如 gcc 的 libgcc,这些库都是特定于编译器的。

由于系统级语言被设计成既可以用来写操作系统上的原生应用,也可以用来写 bare metal 的裸机程序,因此这类语言需要的运行时(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 之类。

可以看到相比上面 C 语言的运行时,这已经是个复杂的基础软件系统了。

稍微再展开一点,注意上面的运行时里是不包含应用程序业务逻辑的。那么拿 JavaScript 举例来说,如果我们把业务逻辑先编译成字节码,再把它和运行时一起编译成一个可执行文件,那不就相当于直接把 JavaScript 编译成机器码了吗?QuickJS 就可以这么做,但其实这时候业务逻辑解释执行的天性不会变——难道真有黑科技能把弱类型的脚本直接靠静态分析编译达到系统级语言的水平?这更多地只是概念定义上的话术而已。

因此,理论上任意的弱类型动态语言都可以基于这种形式来 AOT 编译成原生机器码,你看 Dart、Swift 和 Java 都可以直接编译成可执行文件,区别只是这个运行时的轻重量级不同——当然实际情况肯定没有这么理想化,譬如哪怕编译成了 ARM 机器码,Flutter 里的 Dart 运行时也必然需要比 C 做更多的类型检查和 stop the world 的 GC,这都是有成本的。但对于应用层开发来说,能做到这样已经够好了。所以我们甚至可以激进地认为对于 OS 上的应用程序,各种编程语言都是或多或少地需要运行时的,大家只有运行时轻重的区别。