计算机系统广义上不单单是指系统软件,还包括的硬件,也就是说计算机系统=系统软件+硬件,它们共同工作来运行各种应用程序。
我们都知道计算机(系统)主要是获取一些输入,然后对获取的输入进行处理,最后对处理后的输入进行输出。
如上图所示,计算机中的所有数据都是 0 和 1 组成的一串二进制数,所以首先需要弄明白的就是信息的表示,也就是计算机是怎么知道那一串二进制数表示的是一张图片而不是一首音乐或者一段视频。
信息就是位+上下文
位 ,也叫比特位,就是二进制的取值,也就是 0 或者 1。8 个连续的位序列组成一组称作 字节。字节是计算机信息表示的基本单位,比如说对于 ASCII 码来说,计算机在读到二进制序列 “01101000 01100101 01101100 01101100 01101111” 的时候会精确的把它解码成 “hello” 字符串。
| 字符 | 十六进制数 | 二进制数 |
|---|---|---|
| h | 68 | 01101000 |
| e | 65 | 01100101 |
| l | 6C | 01101100 |
| l | 6C | 01101100 |
| o | 6F | 01101111 |
原因是计算机在解码的时候是以字节为单位进行处理的,从第一个比特位开始读取到第八个比特位后,就会认为 “01101000” 这个字节表示字符 ‘h’,以此类推,从而确定这个字节序列表示的意义。
当然,除了上面提到的 ASCII 码意外,国际通用的一般还有 UTF-8 编码等,对于音乐,图片,视频等也都有各自的编解码协议。
在不同的编解码下,同样的一串比特位序列表达的意义就是不一样的,这其实说明了一个基本思想:
计算机系统中的所有信息,包括磁盘文件,内存中的程序,内存中存放的用户数据以及网络中传输的数据等等,都是由一串比特位序列表示的。而区分不同数据对象的唯一方法就是计算系统在读取到这些数据对象时的上下文。
比如在不同的上下文中,同样的一串比特位序列就可能表示一个整数,浮点数,字符串,一种颜色,一个音调或者机器指令等。
生活中的语言其实也是一样的,比如,下面那句话在不同的语境(上下文)下面有不同的意思。
我喜欢上一个人
好了,对于上面那句话,如果你能理解出完全不同的意义出来,那这部分对你来说应该就不是问题了。
程序不同格式的转换
#include <stdio.h>int main(){printf("hello world!");return 0;}
对于上面这段程序,很明显它只是几行方便人类读写的字符组成的文本文件,但它和普通的文件又有点不一样,因为它不是随便写的,它是按照一定语法规则写的,它是可编译的文本文件,这就是源程序。
根据前面的知识,上面那段文本计算机系统是不认识的,计算机系统只认识 0 和 1。所以对于上面的每一行 C 语句都需要被其他程序转化为一系列由 0 和 1 组成的低级机器语言指令之后计算机系统才能够执行。对于 C/C++ 来说这个转换过程分为四个阶段:预处理-编译-汇编-链接。
预处理
预处理器(cpp)处理由 ‘#’ 开头的一些列预处理命令,比如 #include,#ifdefine,#if 等。最后得到的是一个不包含任何预处理指令的 C/C++ 源程序,通常以 ‘.i’ 作为预处理后的扩展名。
编译
编译器(cc1)将预处理器处理过的 ‘.i’ 文件编译成汇编格式的程序文件,也就是把 C/C++ 程序转换成了汇编语言程序。
顾名思义,汇编语言的每一条语句都对应了一条低级机器语言指令,汇编语言到机器指令就只是简单的映射,它的一个很大用处就是为不同高级语言的不同编译器提供了通用的输出。
比如,在同一个机器上,C 编译器和 Fortran 编译器的输出文件都是一样的汇编语言。
汇编
汇编(as)就是把前面的 ‘.s’ 文件再翻译成低级机器指令,并把这些指令按照一种叫做 可重定位目标程序 的格式,然后得到一个以 ‘.o’ 结尾的目标文件。
链接
注意上面的程序中有一个 printf("hello world!"); 函数调用语句,而这个函数明显不是在这个源程序中定义的。其实它是由 C 的标准库中提供的一个函数,位于printf.o这个独立的目标文件中,而我们在自己编译的 ‘xxx.o’ 文件中用到 printf.o 这个文件中提供的功能,那么 printf.o 这个文件必须以某种方式合并到我们自己编译的 ‘xxx.o’ 文件中才行,而连接器(ld)就是做这个工作的。
计算器系统的硬件组成
上面是程序执行前软件的行为,要想程序真的执行起来,还需要硬件的协助才行。
上面就是计算机硬件最主要的部分,它们之间的结构如下图:
总线
总线就是贯穿整个系统的一组电子管道,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数(也即字长)是一个基本的系统参数,基本上 32 位系统的字长一般是 4 字节,64 位系统的字长是 8 字节。
I/O 设备
I/O 设备是系统与外部世界联系的通道,我们知道仅仅对数据进行处理是没有意义的,所以对处理后的数据进行输出是非常重要的,常见的 I/O 设备包括键盘,鼠标,显示器,磁盘等。
每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连,控制器与适配器的区别就在于它们的封装方式,控制器是 I/O 设备本身或系统主板上的芯片组,而适配器则是一块插在主板插槽上的卡。
主存
主存是一个临时存储设备,在处理器执行程序时,用来临时存放程序和程序处理的数据。物理上,主存是由一组动态随机存储器芯片组成的;逻辑上,主存是一个线性的字节数组,每个字节都有它唯一的地址。
处理器
处理器是中央处理器(CPU)的简称,是解释执行存储在主存中指令的引擎。处理器的核心是一个大小为一个字的寄存器,成为程序计数器(PC),在任何时候,PC 都指向主存中的某条机器指令。
从系统通电开始,到系统断电,处理器一直不断的执行 PC 指向的指令,并更新 PC,使其指向下一条需要执行的指令。
高速缓存至关重要
前面说到处理器只解释执行主存中的指令,但是我们的程序都是存储在磁盘中的,所以程序加载前前需要先复制到内存中并在执行的时候再复制对应的指令到处理器中。这些复制动作都会带来巨大的开销,减慢了程序真正工作的时间,原因是 CPU 的处理速度比 CPU 从主存中拷贝数据的速度快的多的多。
高速缓存就是解决这个问题,它在 CPU 和主存之间加了几个称为高速缓存存储器的硬件作为信息暂时的集结区域,存放处理器近期可能会需要的信息,高速缓存存储器的访问速度几乎和寄存器的访问速度是一致。
既然这样,为什么我们不直接用高速缓存存储器来直接替代主存呢?原因是这里有一个机械性原理的限制。
机械性原理:较大的存储设备要比较小的存储设备运行的慢,而较快的存储设备的造价又远高于同类的低速设备。
根据上面的机械性原理,我们不可能直接使用高速缓存存储器来直接替代主存以加快计算机的运行速度,并且随着半导体技术的进步,这种处理器与主存之间的速度差距还在持续增大,所以高速缓存至关重要。
现代的计算机的存储都被设计成了一个层次结构,上一层会依此缓存下一层的数据或指令等信息,以此提高计算机的整体运行速度。
操作系统管理硬件
对于前面我们写的那个 hello 程序,它从加载到执行结束都没有直接和硬件打交道,虽然它本身就是存储在磁盘中,并且它的功能也是在显示器上打印一串字符串。那它是如何做到的呢?答案就是操作系统的功劳。操作系统就是在应用程序和底层硬件之间插入的一层抽象。
有了这层抽象之后,应用程序就不需要关心怎么和各种五花八门的硬件打交道了,只需要直接使用操作系统提供的统一的 API 服务即可,只要提供的 API 不变,底层硬件的更换就不会影响到上层的程序。
操作系统提供了两个基本功能:
- 防止硬件被异常的应用程序滥用
- 向应用程序提供简单一致的机制来控制复杂而又大不相同的各种低级硬件
它主要通过三个抽象来实现这两个功能:
- 进程
- 虚拟内存
- 文件
进程
进程是操作系统对一个正在运行的程序的一种抽象。在一个操作系统中可以同时运行多个进程,而每个进程都好像在独占的使用硬件。这其实是一种假象,每个处理器同一时刻只能运行一个进程,这种假象就是通过进程的概念来实现的。并发运行说的就是一个进程的指令和另一个进程的指令交替执行。这种交替执行的机制就称作上下文切换。
操作系统会保持跟踪进程运行所需要的所有状态信息,这些状态就是上下文。当操作系统决定要把控制权从当前进程转移到另一个进程的时候,就会进行一次上下文切换,即保存当前进程的上下文,创建或恢复新进程的上下文。
上下文切换的动作是由操作系统内核管理的,内核是操作系统常驻主存的那部分。内核并不是一个独立的进程,它是系统管理全部进程所用代码和数据结构的集合。线程
在现代操作系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局变量以及其他的资源。
进程是资源分配的基本单位,线程是执行的基本单位。虚拟内存
虚拟内存是一个抽象概念,它为每一个进程提供了一个假象,即每个进程都认为自己是在独占的使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。
如上图,每个进程看到的虚拟地址空间都是由大量准确定义的区(逻辑上)构成,每个区都有专门的功能。
- 代码和数据区:对所有进程来说,代码都是从同一个固定的地址开始的。紧接着就是和 C/C++ 全局变量相对应的数据位置。代码和数据区中的内存是直接根据加载到的可执行目标文件的内容初始化的。
- 堆:在代码和数据区之后,程序运行时通过 malloc/free 或者 new/delete 等分配函数动态分配,由低地址向高地址扩展。
- 共享库区:大约在地址空间的中间部分是一块用来存放可执行目标文件用到的一些共享库的代码和数据的区域,也是由低地址向高地址扩展。
- 栈:位于用户虚拟地址空间顶部的那一块区域。用来进行函数调用,保存局部变量等,在执行区间会动态扩展和收缩,由高地址向低地址扩展。
- 内核虚拟内存:地址空间顶部的一块区域,这部分是为内核保留的,不允许用户态程序读写这个区间的内容,必须由内核来读写这部分区域。
文件
文件就是字节序列,仅此而已。每个 I/O 设备,包括磁盘,键盘,显示器,甚至是网络都可以看成是文件。系统中的所有输入输出都是通过使用一组称位 Unix I/O 的系统函数调用读写文件来实现的。
文件这个抽象概念是非常强大的,它向应用程序提供了一个统一的视图来看待系统中可能包含的所有各式各样的 I/O 设备。
总结
抽象的使用是计算机科学中最为重要的概念之一。例如,为一组功能提供一个简单的 API 就是一个很好的编程习惯,其他程序员无须了解内部分具体实现即可直接使用这些代码达到他的目的。不同编程语言都提供了不同程度的抽象支持。
这里我们也介绍了三个抽象:
- 文件是对所有 I/O 设备的抽象。
- 虚拟内存是对物理存储器的抽象。
- 进程则是对一个正在运行的程序的抽象。
正所谓,没有什么事情不是加一层抽象可以解决的,如果有,那就再加一层。
