前言
这两天在读《深入理解计算机系统》(CSAPP)这本书,它从程序员的视角系统地讲解了计算机系统的底层原理,是计算机科学领域公认的一本神书,也是卡内基梅隆大学(CMU)的镇校之作。
读完了第一章,确实感到其名不虚传。
第一章计算机系统漫游从章节标题来看好像是对计算机系统的简单介绍,但它带给我的感觉绝不仅仅是简单的介绍,夸张一点来讲它甚至构建了整个计算机系统的世界观。读完这一章,最大的收获在于加深了我对计算机底层的三个核心的理解:信息的本质、系统的抽象、存储的层次。
本篇文章作为我的一篇读书笔记,将会以上面的三个核心为主,以编译系统和 Amdahl 定律为辅,尝试着整理出计算机系统的基础脉络。
1. 信息的本质
这是全书开篇提出的第一个颠覆性的概念,也是理解计算机底层表示的基础:“信息 = 位 + 上下文”。
在计算机中,无论是我们写的代码,存储的数据,还是网络传输的包,本质上都只是位,即 0 和 1 组成的序列。
我们在平时写代码时其实对数据类型是非常敏感的,但在计算机底层,所有信息本质上都是无意义的 0 和 1 序列,真正赋予它们意义的是解读这串序列时的上下文。同样的字节序列,在文本上下文中可能表示字符'a',但在整数上下文中就表示数值97。
理解了这一点,我们其实可以回想一下 C 语言中的强制类型转换这个概念,为什么会出现乱码,其实本质上就是我们在用错误的上下文去解读一串位序列。
1.1 什么是上下文
正如我们上面所讲,计算机硬件只认识位序列,它并不会知道这串位序列表示的是浮点数还是整数。而区分不同数据对象的唯一方法就是我们读到这个数据对象时的上下文。
上下文一般都包括:
- 数据类型:这是特定的编程语言或者编译器指定的。
- 存储位置:这串数据是放在栈,堆,还是代码段里面。
- 字节序:应该以大端还是小端的方式来解读这串数据。
- 执行环境:程序是在 x86-64 架构下,还是 ARM 架构下运行的。
没有了上下文,位序列就毫无意义。
我们试想一个简单的场景,你现在要阅读别人的一份代码,你通常习惯于使用英文来理解变量名的含义,而他的代码中变量名恰好是使用拼音来命名的,好在我们作为人类是具有思考能力的,琢磨一会也许就反应过来了。
但是对于计算机,他并没有思考能力,当你不明确指定一串序列的上下文时,它就完全无法知道这串序列是什么意思。当你指定了一个错误的上下文时,他也会按照这个上下文去解读这串序列,但这样做得到的结果自然不是我们期待的。计算机并没有分辨是非的能力,它需要引导,这就是上下文存在的意义。
1.2 示例
这里举个简单的例子,请看下面代码:
#include<stdio.h>intmain(){inti=97;char*c=(char*)&i;printf("i的地址:%p\n",&i);printf("指针c:%p\n",c);printf("作为整数: %d\n",i);printf("作为字符: %c\n",*c);return0;}代码中定义了一个int类型的局部变量i并初始化为97,然后我们将变量i的地址强制转换成char *类型并传给指针c。
接着我们使用两个printf打印出i的地址和指针变量c的值,后面程序运行时我们可以看到打印出的这两个值其实是相同的,这里的强制类型转换只是让计算机以char来解读该地址存放的值。
然后再用两个printf分别以整数和字符的格式将这个地址的值打印出来。
运行结果如下:
可以看到打印出来的确实是同一个地址。对这个地址中的值以不同的上下文进行解读,我们得到的是完全不同的结果,这里没有乱码是因为97刚好是字符a的ASCII码。
我们将i的值改为250再试试:
可以看到,打印出乱码了。
2. 系统的抽象
第一章最精彩的部分在于解释了操作系统到底在做什么,简单来说,操作系统通过抽象在硬件和软件之间搭建了沟通的桥梁。
如果程序员直接操作硬件,难免会出现粗心大意的情况,不仅复杂而且危险。为了保护硬件和方便编程,操作系统提供了三个最基本的抽象,给程序制造了幻觉。
2.1 文件 — 对 I/O 设备的抽象
所有的 I/O 设备,比如磁盘、键盘、显示器、网络,都被视为一串字节流。
不同的设备有完全不同的物理特性。
但是我们不需要了解具体的硬件技术细节,只需要通过 I/O 接口read和write读写文件即可。在 Linux 中,一切皆文件,这一设计极大地简化了输入输出操作。
2.2 虚拟内存 — 对主存和磁盘的抽象
每个程序都以为自己独占了主存,拥有一个巨大的,连续的地址空间。
但是实际上,物理内存可能很小,且被多个程序共享。
虚拟内存是操作系统最复杂的抽象之一,它为每个进程提供了一个一致的虚拟地址空间结构。从下往上依次是:
- 程序代码和数据:从可执行文件中加载。
- 堆:程序运行时动态申请内存。
- 共享库:比如 C 标准库。
- 栈:函数调用链。
- 内核虚拟内存:操作系统驻留的地方。
这种抽象不仅简化了链接和加载,还保护了各进程的地址空间互不干扰。
2.3 进程 — 对CPU、主存和I/O的总和抽象
每个进程都以为自己独占了 CPU。
但是实际上,操作系统通过上下文切换机制,让 CPU 在多个进程间快速切换,因为切换速度极快,看起来就像是多个程序在同时运行。
现代系统中,一个进程内部还可以有多个线程。线程是运行在进程上下文中的执行单元,它们共享代码和全局数据,相比进程更加轻量级。
3. 存储的层次
这部分是连接理论与实践的桥梁。
3.1 核心矛盾
摩尔定律揭示了 CPU 的发展速度极其迅猛,但内存 DRAM 的访问速度提升却相对缓慢。如果 CPU 每执行一条指令(纳秒级)都要等待主存读取数据(百纳秒级别),CPU 将大部分时间处于空转等待状态,如此系统的性能将被内存拖垮。
3.2 解决方案
为了解决这个核心矛盾,计算机系统设计了一个类似金字塔的层次结构。核心思想是:利用造价便宜、容量大的存储设备,作为速度快、造价高、容量小的存储设备的“缓存”。
下面这个金字塔模型是从书里面截取下来的:
3.3 局部性原理
为什么加了缓存就变快了呢?这是因为程序运行具有局部性:
- 时间局部性:刚访问过的数据,很可能马上再次被访问。
- 空间局部性:访问了某个地址的数据,很可能马上访问它旁边的数据。
理解了这一层级结构之后,我们在写代码时就应该注重对缓存友好,这往往比降低算法的时间复杂度带来的性能提升更加直接。
比如在 C 语言中,按行遍历二维数组通常比按列遍历快得多,就是因为按行遍历缓存命中率更高,利用了空间局部性。
4. 其他重要的概念
第一章中还有两个概念值得一提,它们贯穿了后续章节。
4.1 编译系统的四个阶段
下面这个流程图是从书中截取的:
- 预处理:插入头文件,替换宏。
- 编译:翻译成汇编语言。
- 汇编:翻译成二进制机器代码。
- 链接:合并库函数。
4.2 Amdahl 定律
这是一个关于系统性能提升的定律,它和木桶效应具有一定的相似之处。
当我们对系统的一个部分的性能进行提升时,对系统整体性能的影响取决于该部分被使用的频率。
就拿这张图来说吧:如果我们不断增加完好的木板的长度,桶里面能装的水的量并不能提升,因为我们增加的那部分根本不会被使用到,但是如果我们增加断开的木板的长度,那么储水量将会大大提升。
虽然 Amdahl 定律主要讲的是加速比,但用木桶效应来理解瓶颈决定系统上限也是非常直观的。
第一章的漫游就到此为止,我们下一篇文章再见。