CSAPP要点总结 第1章 计算机系统漫谈
前言
本来想一次把《Effective C++》系列做完的,但是在关于异常处理部分,搜索相关资料,发现我如果能掌握汇编知识就更容易理解了,包括C++语言本身,如果学习汇编相关,因此,虽然现在时间紧迫,还是先把CSAPP啃完吧。
CSAPP为CMU的教材,其地位和价值毋庸置疑,英文有第三版,中文目前是第二版
资源:
书中插图 | 书中源码 | 书中实验练习 |
实验练习包括:
- 数据试验
- 二进制炸弹试验
- 缓冲区溢出试验
- 体系结构试验
- 性能试验
- 外壳试验
- malloc试验
- 代理试验
程序的生命周期,从源码到可执行文件
对于$ gcc -o hello hello.c
而言,从文本格式(ASCII)的源文件转换为机器码的目标文件。
该过程分为四个步骤:
hello.c hello.i hello.s hello.o hello
------>[预处理器]----->[编译器]------->[汇编器]-------->[链接器]------>
(cpp) (ccl) (as) (ld)
其中:
hello.c
: 源代码
预处理阶段
hello.i
: 经过预处理后的源代码,依然是文本格式。
编译阶段:生成通用的汇编语言
hello.s
: 汇编程序,依然是文本格式
汇编阶段:汇编器将hello.s翻译成机器语言指令,将其打包为可重定位目标程序(relocation object program)
hello.o
: 目标程序,二进制格式
链接阶段:
hello
: 最终的可执行程序。
系统硬件组成
1. 总线
传输数据用,传送固定长度的字节(byte)块,称之为字(word),通常是4字节或者8字节。总线每次只传送1个word。
2. I/O 设备
输入设备和输出设备,键盘、鼠标、硬盘等。每个I/O设备通过一个控制器或者适配器与I/O总线相连。 控制器通常位于I/O设备本身或者印刷电路板上的芯片组。 适配器则是插在主板上的卡(例如显卡)
3. 主存
主存是由一组动态随机存取存储器(DRAM)芯片组成,逻辑上DRAM是线性的字节数组,每个字节拥有唯一的地址(数组索引),地址从零开始。
需要说明的是,通常机器指令都有不同数量的字节byte构成,例如,对于运行在Linux的IA32机器上的C程序,short需要2字节,int、float、和long需要4字节,double需要8字节。
4. 处理器
中央处理器CPU的核心是一个word字长的存储设备(或寄存器),称为程序计数器(PC)。任何时候PC都指向主存中某条机器指令(即内含机器指令的地址)。
执行过程是:CPU从PC所指向的程序那里读取指令,解释指令上的位,执行该指令的简单操作,更新PC执行下一条指令(不一定在物理内存中相连)。
指令围绕主存、寄存器文件(register file)和算术/逻辑单元(ALU)进行。
寄存器文件: 小的存储设备,有一些1字长的寄存器组成(注意,一个寄存器1字长,多个寄存器构成寄存器文件),每个寄存器都有唯一的名字。
例如以下简单操作:
加载:把一个byte或一个word从主存复制到寄存器,覆盖寄存器原有内容。 存储:把一个byte或一个word从寄存器复制到主存的某个位置,覆盖主存原有内容。 操作:把两个寄存器的内容复制到ALU,ALU对这两个word(注意是word)作算术操作,结果存放到一个寄存器,覆盖其原有内容。 跳转:从指令本身抽取一个word,将这个word复制到程序计算器(PC)中,以覆盖PC原来的值。
补充:1个word字长通常是32位或者64位,程序计数器PC就是用来指位置的。
以上的简单操作只是示意性的,实际过程现代处理器使用非常复杂的机制优化加速。
hello的执行过程
我们首先在键盘上输入:$ ./hello
, shell会检测我们的输入,将其存于内存并显示在屏幕上,当输入回车符时,shell知道我们结束命令输入。
其后shell执行一系列指令加载可执行文件hello,将hello中的代码和数据从磁盘复制到内存。(利用DMA,直接存储器存取技术,可以不经过CPU直接将数据从磁盘经过I/O桥到内存)
当文件hello的代码和数据完全加载到主存后,处理器开始执行hello程序的main程序的机器指令。指令将”hello,world\n”字符串中的字节byte从主存复制到寄存器文件,再从寄存器文件复制到显示设备。
高速缓存至关重要
以上过程揭示了一个问题,就是系统花费大量时间将信息从一个地方挪到另一个地方。因此,系统设计的一个目标是使这些复制操作尽快完成。
又由于对于存储设备,储量越大,越慢且造价越便宜,例如硬盘。 储量越小,越快、造价越贵,例如寄存器。
基于以上两点,考虑一个存储器层级结构金字塔: 上一级作为下一级的高速缓存。
大致结构是:
寄存器 <- L1缓存 <- L2缓存 <- L3缓存 <- 内存 <- 磁盘 <- 远程存储
高速缓存的位置如下图所示
操作系统管理硬件
操作系统作用有二,一是防止应用程序恶意破环,而是在应用与各种硬件之间建立统一的桥梁。
操作系统有几个基本的抽象概念: 进程,虚拟存储器,文件。
文件: 1. I/O设备 的抽象 虚拟存储器:1. I/O设备 2. 主存 的抽象 进程: 1. I/O设备 2. 主存 3. 处理器 的抽象
进程
进程是操作系统对一个正在运行的程序的抽象,一个系统上可以同时运行多个进程,但好像每个进程能独占地使用硬件。
CPU看上去再并发执行多个进程,实际上进程数通常多于CPU核心数,因此,这只是假象,这依靠上下文切换来实现。
操作系统保持跟踪进程运行所需的所有状态信息,这就是上下文。当操作系统决定把控制权从当前进程转移给某个新进程,就会进行上下文切换。
线程
尽管通常我们认为一个进程只有一个控制流,但现代操作系统中,一个进程实际由多个称为线程的执行单元组成。每个线程都运行在进程的上下文中,共享同样的代码和全局数据,
虚拟存储器
虚拟存储器是一个抽象概念,给进程提供一种独占内存的假象。
每个进程看到的是一致的存储器,称为虚拟地址空间。
每个进程看到的虚拟地址空间都准确划分为不同区域:
程序代码和数据
程序代码以及静态的数据,分为 [1. 只读代码和数据] [2. 可读写数据]
堆
用于 [3. 运行时动态分配的数据],malloc free等的数据
共享库
[4. 共享库] 例如存放C标准库和数学库的代码和数据区域
栈
[5 用户栈] 用于实现函数调用,位于用户虚拟地址空间顶部(上面就是用户代码不可见的区域了,包括操作系统的代码和数据)
内核虚拟存储器
[6 内核虚拟存储器] 内核总是驻留在内存中,是操作系统的一部分,位于地址空间顶部。不允许应用程序读写。
文件
文件就是字节序列,仅此而已。每个I/O设备都可看做是文件。包括磁盘、键盘、显示器、网络等。
为应用程序提供统一的视角看待不同I/O设备。
系统之间利用网络通信
略
重要主题
并发和并行
并发(concurrency)指一个同时具有多个活动的系统。例如同时运行两个程序。
并行(parallelism)指用并发来使得一个系统运行得更快。
并行可以在计算机系统不同抽象层次上运用,主要包括:
1. 线程级并发
在较高层级抽象上,现代处理器允许一个进程有多个线程同时运行。
什么是超线程
使用线程,可以在一个进程中执行多个控制流。即超线程(hyperthreading)或者说同时多线程。它涉及某些硬件有多个备份。例如备份PC和register file,而其他硬件部分只有一份,比如执行浮点计算的单元。超线程处理器在单个时钟周期基础上决定执行哪个线程,这使得CPU更好地利用资源。
意识就是说,我现在有两个任务排队需要电脑,但是只有一个电脑(指的是ALU,例如浮点计算)。那么超线程就决定那个任务可以用“电脑”。发现一个任务此时还没准备好,那么就执行另一个。这样时间就节省了,更好地利用了CPU资源。
每个任务管理员(PC)不同,所以需要多个PC(程序计数器) 每个任务都要保持自己的临时数据,因此也需要多个register file) 但是“电脑”(ALU)可以通用。
2. 指令级并行
在较低层级抽象上,现代处理器可以同时执行多条指令
通过流水线(pipeling)技术,现代处理器每次输入多条指令,像流水线一样,将每条指令组织成不同子步骤。
打个比方,假设要炒一盘菜需要1个小时,但发现炒每盘菜都要先洗菜,那么把洗菜步骤单独拿出来,放好几个盆,几个菜放在各个盆子里一起洗,洗完再进入下一个步骤,这样分配到每个菜上的时间就省下来了,也就小于1小时。处理器使用非常聪明的技巧同时处理多达100条的指令
本来每条指令大约要20,甚至更多时钟周期,通过流水线的精心设计,实际分配到每条指令可能半个时钟周期。如果一个指令比时钟周期还快,成为超标量(superscaler)处理器。
3. 单指令、多数据并行SIMD
在最低层次上,现代处理器拥有特殊的硬件,允许一条指令产生多个可并行执行的操作。这种方式成为单指令、多数据流SIMD并行。例如。较新的Intel或AMD可以同时对4个float做加法运算。
个人理解,貌似就是ALU单元比较大,同时处理了4个32位,也就是128bit的数据。
如何实现? 有些编译器试图从C程序中抽取SIMD并行。但更可靠的方法是使用编译器支持的特殊向量数据类型来写程序(这样即使编译器不能自动SIMD并行也有效)。