这篇文章是csapp第一章阅读笔记。
1. 信息就是:位+上下文
下面是一个简单的hello程序:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
这个程序中所有的字符都是ASCII字符,那么类似这样只有ASCII字符的文件,就是文本文件(text file)。
其它的文件,就是二进制文件(binary file)。
上面的hello.c
文件展示了一个计算机中的基本想法:
All information in a system, is represented as a bunch of bits. The only thing that distinguishes different data objects is the context in which we view them.
也就是说计算机中,不管是磁盘文件、内存中的程序或者网络中传输的数据,都是由一个个比特位(bits)组成的。用来区别它们的唯一方式是所处的上下文(context)。
所处的上下文不同,那么相同的一串比特信息可以构成不同的信息。
2. 程序被其他程序翻译成不同的格式
为了执行上面的hello程序,我们需要将它编译成一个可执行程序:
gcc -o hello hello.c
这个编译过程经过了如下的几个阶段:
2.1 Preprocessing Phase
预处理阶段:预处理器(cpp)修改原始的C程序hello.c
,根据#所包含的头文件,预处理器将所包含的头文件中的内容直接插入到C程序中,得到hello.i
文件,这也是一个文本文件。
2.2 Compilation Phase
编译阶段:编译器(cc1)将上一阶段的结果hello.i
文件转换成另一个文本文件hello.s
,这里的hello.s
文件包含的是汇编程序。比如:
main:
subq $8, %rsp
movl $.LC0, %edi
call puts
movl $0, %eax
addq $8, %rsp
ret
汇编语言很有用,因为它为不同的高级语言提供了一个通用的编译结果。比如C语言编译器和Fortran编译器都可以生成同样的汇编语言文件。
hello.s
还是一个文本文件。
2.3 Assembly Phase
汇编阶段:汇编器(assembler)将上一阶段的结果hello.s
文件转换成机器语言指令,然后包装成可重定位目标程序(relocatable object program),将结果保存在hello.o
文件中,这里,hello.o
就是一个二进制文件了。
2.4 Linking Phase
链接阶段:在上面的hello程序中,我们使用了printf
函数,这个函数是C语言标准库提供的,这个函数保存在另一个二进制文件printf.o
中,为了使用这个函数,我们需要把上一阶段的结果hello.o
和这个printf.o
合并到一起。
这就是链接器(ld)的工作。输出的结果hello
文件,就是一个可执行的二进制文件了,可以加载到内存中然后执行了。
3. 了解下编译系统是如何工作的很重要
通过上面的过程,我们知道了一个hello.c
文本文件是如果通过编译变成一个可执行二进制文件hello
的了。
理解这个过程对于程序员是很重要的。原因有一下几点:
- 理解编译的过程可以优化程序性能
- 可以理解链接时错误
- 避免一些安全问题的坑
总之,理解底层的原理,终归是有好处的。
4. 处理器读并解释存在内存中的指令
为了执行上面的hello
程序,只需要;
./hello
即可,就是这么简单。
那么计算机是如何执行存储在hello
文件中的指令的呢?首先看一下计算机的整体结构:
4.1 系统的硬件组成
上图展示了系统的硬件组织,主要有下面几个部分:
4.1.1 Buses
总线。总线用来在不同的部分之间传输数据。就像城市里的公路有四车道八车道一样,总线也有一个衡量的大小。总线有一个固定大小的指标:字(word),一个字就是总线能传输数据的最小单位,可以用bytes来表示。
不同的系统字的大小不同,也就是字包含的字节数不一样。32位系统一个字有4个字节;64位系统一个字有8个字节。
4.1.2 I/O Devices
输入输出设备。这就是计算机系统和外界的连接点了,比如键盘、鼠标、显示器和磁盘等。
每一个输入输出设备可以通过controller或adapter连接到系统中。
4.1.3 Main Memory
内存。内存就是dynamic random access memory(DRAM)的集合。
4.1.4 Processor
中央处理器,CPU。这个是用来执行指令的,有逻辑处理单元(arithmetic/logic unit, ALU)、程序计数器(program counter, PC)和寄存器文件(register file)。
CPU通过执行一些简单的指令就可以完成大量的工作:
- Load
- Store
- Operate
- Jump
4.2 执行hello
介绍完计算器的硬件组成,来看看计算机是如何执行hello
这个程序的。
当我们将./hello
通过键盘输入之后,这个信息就被存储到寄存器文件中了。这个过程就是读指令。如图:
当我们敲下回车键之后,计算机就知道指令输入完了,接下来就是执行了。shell将可执行的hello
文件从磁盘加载进内存中。从上图可以看出,hello
程序的数据先被送到了CPU中的寄存器文件中,然后送到内存中。
通过DMA(direct memory access)技术,磁盘中的数据可以直接送到内存中。如图:
当hello
文件中的指令和数据加载到内存之后,计算机就可以执行程序中的指令了,并将数据输出到屏幕上:
5. 缓存至关重要
从上面可以看到数据在磁盘、内存和CPU中不断穿梭,穿梭的过程中就浪费了一些时间。
所以为了降低时间,一个重要的组成部分就是缓存(cache)。
比如CPU中可以加上L1和L2缓存(static random access memory, SRAM)。
当然,在别的地方也可以加缓存。在计算机系统中,我们会经常看到缓存的身影。
6. 层级结构的存储设备
7. 操作系统管理硬件
当我们执行hello
程序的时候,并不是这个程序完成了所有的工作,它依赖于另一个程序来完成大部分的工作,这个程序就是操作系统。操作系统用来管理硬件。结构如下:
7.1 Processes
进程,是操作系统提供的一个抽象。它让运行在这个进程中的程序感觉,只有它自己在使用整个计算机系统。
进程的抽象如下:
为了支持多个进程,需要使用上下文切换(context switching)的机制。说白了就是计算机的使用权在不同的进程中“反复横跳”:
除了用户进程,有一个内核进程。这个内核进程可以管理所有的进程。
7.2 Threads
线程的粒度更低一些,一个进程可以有多个线程,所有的线程共享这一个进程的代码和数据。
多线程编程的代价比多进程编程的代价低一些。
7.3 Virtual Memory
虚拟内存。就像进程是一个抽象一样,虚拟内存也是一个抽象。它给每个进程一个幻觉,好像自己独占了所有的内存一样。
虚拟内存的结构如下:
主要包括:代码段、数据段、堆、共享库、栈和内核虚拟内存。
7.4 Files
文件就是比特串。在linux中,所有的IO设备也被认为是文件。
8. 系统间使用网络通信
上面介绍的都是在一个计算机系统中。那么不同的计算机系统之间怎么通信呢?
答案是使用网络:
网络也可以认为是一个I/O设备,当然也可以当做是一个文件。
9. 重要的主题
9.1 Amdahl’s Law
主旨就是:当我们提升系统中一部分的效率时,那么整体的效果取决于,那部分在整个系统中的重要性以及那部分提升效率的具体值。
令$T_{old}$作为系统升级前所需时间,$α$作为升级部分在系统中的比例,性能提升系数是$k$,那么系统提升后所需的时间:
$$
T_{new}=(1-α)T_{old}+(αT_{old})/k
=T_{old}[(1-α)+α/k]
$$
所以系统整体性能提升的系数$S$是:
$$
S=\frac{1}{(1-α)+α/k}
$$
比如要升级一个系统中占60%的子系统(α=0.6),性能提升系数是3(k=3)。那么整体提升的性能系数是1/[0.4+0.6/3]=1.67。
有意思的是,当k无穷大时,系统整体的性能提升系数是:
$$
S_{∞}=\frac{1}{1-α}
$$
拿上面的例子来说就是2.5。
所以我们在做系统优化的时候,对于一个子系统不一定非要提升得多么多么好,从而白白浪费精力也没有给整个系统带来可观的性能提升。
9.2 Concurrency and Parallelism
并发与并行。在整个计算机的发展过程中,我们主要有两个目的:让计算机做得更多以及,做得更快。
使用并发(concurrency)这个术语来指系统同时做多个任务;使用并行(parallelism)来指系统通过并发执行得更快。
9.2.1 线程级并发
9.2.2 指令级并行
就是处理器同时执行多条指令。
在一个时钟周期内执行多条指令的处理器叫做超标量处理器(superscalar processors)。
9.2.3 单条指令,多个数据并行
最低一层,就是在一个指令中,处理多个数据。这叫做single-instruction, multiple-data(SIMD)。
9.3 计算机系统中抽象的重要性
计算机系统中的一个重要概念就是抽象了,比如API就是一个抽象。
下面就是一些这章内容介绍的抽象: