本篇文章主要介绍一下程序语言、应用程序、运行时库、操作系统四者之间的关系。

在现代操作系统中,应用程序在运行期间是没有多少权力来访问系统资源的。因为这些资源可能同时被多个应用程序同时访问,为了高效的利用这些资源,操作系统会统一将这些资源保护起来,尽量防止应用程序直接访问,而要想访问这些资源,应用程序必须通过操作系统对外提供的应用程序接口来实现。这样一方面是为了便于管理,另一方面也可以屏蔽不同设备资源之间的差异性。

举个常见的例子,开发人员一般是不会通过直接访问磁盘上的数据来获取文件内容的,而是通过操作系统对外提供的文件接口来间接访问。

程序运行的层次

这些接口大致可以划分为两类:

一类被称为系统调用,主要实现操作系统内核和应用程序之间的通讯。系统调用可以看作是应用程序获取操作系统服务的唯一途径,所有的应用程序都会在不同程度上依赖这些系统调用提供的接口。系统调用覆盖的功能非常广泛,大致上就是在大学期间操作系统课程中学习的那几大模块,包括处理器管理、存储器管理、文件管理、设备管理等等。系统调用中每个接口的含义、参数、行为都有明确的定义,并且非常稳定。一般不同操作系统版本之间都会实现向后兼容,以确保老的程序可以在新系统上顺利运行。

除了系统调用,还有一类接口被称为标准接口。系统调用接口涵盖了操作系统对外提供的所有功能,理论上应用程序只需要使用系统调用接口即可,但是一方面由于系统调用接口粒度上划分非常细,接口参数非常复杂,开发人员需要了解很多与操作系统相关的细节,才能正确使用;另一方面不同操作系统之间的系统调用接口差异性非常大,即使同一种操作系统不同版本之间也存在一定的差异。为了解决上述问题,“运行时库” 孕育而生。运行时库的指导原则就是计算机领域的一个万用法则:“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。

运行时库有自己的标准,它本身就是语言级别的,常被叫做标准库,而运行时库提供的接口被称为标准接口,可以配合程序语言直接使用。当我们使用这些标准接口的时候不需要考虑不同平台间的差异性,可以在最大程度上掩盖系统调用的弊端。

运行时

如上图所示,C语言的运行时库中读取文件使用的函数是 fread,而在 Windows 平台提供的 API 接口是 ReadFile,Linux 平台可能使用的却是 read 函数,如果你直接使用 ReadFile 或者 read 这两个函数很有可能会考虑不同操作系统之间区别,而如果你直接使用C语言标准中提供的 fread 函数,显然就不会存在这种问题。

如果你的应用程序完全使用标准库接口来编写,就可以被称为源码级的可移植,这也是大多数程序员所追求的。但是标准库并不是银弹,因为标准库的原则是抽象,是抽取不同操作系统之间的共性,掩盖其中的差异,所以在一定程度上,标准库肯定会损失一些原有操作系统支持的一些独特功能。它只能取不同平台之间的交集,因此如果想要支持或者使用某些操作系统独有的或者标准库不支持的功能,则必须在程序中直接调用系统调用接口(在 Windows 平台是 API 接口)来实现。

通过上面的讲解,我们大致可以知道应用程序、运行时库、操作系统三者之间的层次关系了。

最底层的就是我们计算机的硬件,也就是前面提到的设备资源。在它的上面就是操作系统层,用来管理这些设备资源。而在操作系统之上则是运行时库,运行时库的实现是通过调用一个或多个系统调用接口来实现的。而应用程序则是在运行时库之上,应用程序可以直接调用运行时库也可以直接调用操作系统提供的系统调用接口,不过为了可移植性和便捷性,大多数程序都可能更喜欢使用运行时库提供的标准接口。

大部分操作系统都是以系统调用作为应用程序最底层的接口,但是 Windows 操作系统对外最底层的接口是 Windows API。Windows API 是 Windows 编程的基础。Windows 内核虽然提供数百个系统调用,但是并没有对外公开,而是在其上建立了一个 API 接口层,开发人员在应用程序中直接调用这些 API 来完成相应功能的开发,而不是像 Linux 那样直接使用系统调用。

下面是一个C语言程序调用fwrite函数的大致流程:

fwrite_call

应用程序在 Linux 和 Windows 都是用 fwrite 函数,两个操作系统都会调用运行时库中的 write 函数。唯一的区别是二者运行时库的名字不同而已。Linux中 write 函数在 libc.so 动态库中实现的,而 Windows 则是在 msvcr90.dll 动态库中实现的。

和 Linux 不同,Window 独有一个 API 层,在运行时库中,write 函数直接调用 NtwriteFile,而对于内核层来说,二者的系统调用接口则完全不同。Linux 的接口是 sys_write,而 Windows 则是 IoWriteFile。

不知道你在开发中是否遇见过这样的现象,有时候下载别人的程序运行会弹出无法启动此程序的对话框。
system_error
根据前面相关知识的介绍,你就应该清楚之所以会弹出这样的对话框,根本原因是程序使用了标准函数,而在你的机器上,没有这个标准函数对应的动态库实现,如果想要顺利运行则需要到官方网站下载对应版本的动态库安装即可。
那怎样避免这样尴尬的情况呢?
runtime_setting
如果你没有依赖其它第三方库,默认 IDE 中指定的运行时库的方式是动态链接,这情况下,会依赖于特定版本的运行时库,具体的编译选项是 /MT、/MTd 以及 /MD 和 /MDd 。后面的小d表示 Debug 和 Release 之分,而 MT 和 MD 则是选择使用静态编译还是动态编译,如果发布程序选择 /MT 选项进行链接,则可以避免在其它机器上运行时缺失动态库的麻烦。

明白了应用程序、运行时库、操作系统这三者的基础上,这里还要简单的介绍一下程序开发语言和它们的关系。

在平常的应用开发中,其实很少会关注编译和链接的过程,因为现代 IDE 只需要一个按钮就可以将源码生成为可执行程序,这个过程通常称为构建(Build)。

程序构建流程

一个可执行程序的生成粗略的可以分为四个步骤:

  • 预处理(Prepressing),预处理主要是处理以 “#” 开头的预编译指令以及宏展开等操作。
  • 编译(Compilation),编译过程会将预处理完的文件进行一些列的操作,包括词法分析、语法分析、语义分析最终会得到一个优化后的汇编代码文件。
  • 汇编(Assembly),在这个阶段汇编器会将编译阶段产生的汇编文件翻译成机器可执行的指令,最终输出的中间文件叫做目标文件。
  • 链接(Linking),链接过程主要是处理汇编阶段产生的目标文件和应用程序依赖的其它目标文件之间的引用关系,使得各个模块之间可以正确地衔接到一起,最终形成一个可执行程序。而前面提到的运行时库其实是多个目标文件打包到一起的集合,和 RAR 压缩包的原理类似。库的目的就是为了模块化,方便程序的开发管理。

通过本篇文章的学习,你应该对编程开发有了一些粗浅的认知。在以后的开发中,应该注意下使用的函数是语言级别就支持,还是操作系统层次上支持的,这一点非常重要。