├── .gitignore ├── README.md └── notes ├── CSAPP 存储器层次结构.md ├── CSAPP 计算机系统漫游.md ├── CSAPP-优化程序性能.md ├── CSAPP-信息的表示和处理.md ├── CSAPP-处理器体系结构.md ├── CSAPP-并发编程.md ├── CSAPP-异常控制流.md ├── CSAPP-程序的机器级表示.md ├── CSAPP-系统级I-O.md ├── CSAPP-网络编程.md ├── CSAPP-虚拟存储器.md └── CSAPP-链接.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macOS 3 | 4 | ### macOS ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | # End of https://www.gitignore.io/api/macOS 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CSAPP 2 | 3 | 《深入理解计算机系统》学习笔记 + 习题 4 | 5 | ### 学习笔记 6 | 7 | 1. [计算机系统漫游](https://www.liuin.cn/2018/01/23/CSAPP%20%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E6%BC%AB%E6%B8%B8/) 8 | 2. [信息的处理和表示](https://www.liuin.cn/2018/01/24/CSAPP-%E4%BF%A1%E6%81%AF%E7%9A%84%E8%A1%A8%E7%A4%BA%E5%92%8C%E5%A4%84%E7%90%86/) 9 | 3. [程序的机器级表示](https://www.liuin.cn/2018/01/25/CSAPP-%E7%A8%8B%E5%BA%8F%E7%9A%84%E6%9C%BA%E5%99%A8%E7%BA%A7%E8%A1%A8%E7%A4%BA/) 10 | 4. [处理器体系结构](https://www.liuin.cn/2018/01/26/CSAPP-%E5%A4%84%E7%90%86%E5%99%A8%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/) 11 | 5. [优化程序性能](https://www.liuin.cn/2018/01/27/CSAPP-%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD/) 12 | 6. [存储器层次结构](https://www.liuin.cn/2018/01/28/CSAPP%20%E5%AD%98%E5%82%A8%E5%99%A8%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84/) 13 | 7. [链接](https://www.liuin.cn/2018/01/29/CSAPP-%E9%93%BE%E6%8E%A5/) 14 | 8. [异常控制流](https://www.liuin.cn/2018/01/30/CSAPP-%E5%BC%82%E5%B8%B8%E6%8E%A7%E5%88%B6%E6%B5%81/) 15 | 9. [虚拟存储器](https://www.liuin.cn/2018/01/31/CSAPP-%E8%99%9A%E6%8B%9F%E5%AD%98%E5%82%A8%E5%99%A8/) 16 | 10. [系统级别I/O](https://www.liuin.cn/2018/02/01/CSAPP-%E7%B3%BB%E7%BB%9F%E7%BA%A7I-O/) 17 | 11. [网络编程](https://www.liuin.cn/2018/02/02/CSAPP-%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/) 18 | 12. [并发编程](https://www.liuin.cn/2018/02/03/CSAPP-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/) 19 | 20 | 21 | -------------------------------------------------------------------------------- /notes/CSAPP 存储器层次结构.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 存储器层次结构 3 | date: 2018-01-28 17:47:19 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第6章笔记 9 | 10 | 11 | 12 | 存储器系统(memory system)是一个具有不同容量、成本和访问时间的存储设备的层次结构。CPU 寄存器保存着最常用的数据。靠近 CPU 的晓得、快速的高速缓存存储器(cache memory)作为一部分存储在相对慢速的主存储器(main memory)中的数据和指令的缓冲区域。主存暂时存放在容量较大的、慢速磁盘上的数据,而这些磁盘常常又作为存储在通过网络连接的其他机器的磁盘或磁带上的区域的缓冲区域。 13 | 14 | 具有良好局部性的程序倾向于一次又一次地访问相同的数据项集合,或者是倾向于访问邻近的数据项集合 15 | 16 | ## 存储技术 17 | 18 | ### 随机访问存储器 19 | 20 | 随机访问存储器 (Random-Access Memory, RAM)分为两类:静态的和动态的。SRAM 比 DRAM 更快,但也贵得多。SRAM 用来作为高速缓存存储器,既可以在 CPU 芯片上,也可以在片下。DRAM 用来作为主存以及图形系统的帧缓冲区。 21 | 22 | ### 磁盘存储 23 | 24 | 磁盘构造 25 | 26 | ![enter description here][39] 27 | 28 | 磁盘容量:一个磁盘能够存储的最大位数,由以下因素决定: 29 | * 记录密度(recoding density),磁道一英寸的段可以放入的位数 30 | * 磁道密度 31 | * 面密度 32 | 33 | 磁盘操作:用一个连接到传动臂的读写头来读写存储在磁性表面的位 34 | 35 | 对扇区的访问时间由三个部分组成:寻道时间(seek time)、旋转时间(rotational latency)、传送时间(transfer time) 36 | 37 | ### 固态硬盘(Solid State Disks) 38 | 基于闪存(flash memory)的存储技术 39 | 40 | ![enter description here][40] 41 | 42 | 43 | ### 存储技术趋势 44 | 45 | * 不同的存储技术有不同的价格和性能的折中 46 | * 不同存储技术的价格和性能属性以截然不同的速率变化着 47 | * DRAM和磁盘访问的时间滞后于CPU时钟周期时间 48 | 49 | ## 局部性 50 | 51 | 一个编写良好的计算机程序常常具有良好的局部性(locality)。也就是说,它们倾向于引用临近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。这种倾向性,被称为局部性原理(principle of locality),是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。 52 | 53 | 局部性通常有两种不同的形式:时间局部性(temporal locality)和空间局部性(spatial locality)。有良好局部性的程序比局部性差的程序运行得更快。 54 | 55 | * 重复引用同一个变量的程序有良好的时间局部性 56 | * 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好 57 | * 对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好 58 | 59 | ## 存储器层次结构 60 | 61 | ![enter description here][41] 62 | 63 | ### 在存储器层次结构中的缓存 64 | 65 | 一般而言,高速缓存(cache)是一个小而快速的存储设备。使用高速缓存的过程称为缓存(caching)。 66 | 67 | 存储器层次结构的中心思想是,对于每个 k,位于 k 层的更快更小的存储设备作为位于 k+1 层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一次都缓存来自较低一层的数据对象。 68 | 69 | 数据总是以块大小为传送单元(transfer unit)在第 k 层和第 k+1 层之间来回拷贝的。虽然在层次结构中任何一对相邻的层次之间块大小是固定的,但是其他的层次对之间可以用不同的块大小。一般而言,层次结构较低的层(离 CPU 较远)的设备访问时间较长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。 70 | 71 | ![enter description here][42] 72 | 73 | 缓存命中: 当程序需要第 k+1 层的某个数据对象 d 时,它首先在当前存储的第 k 层的一个块中查找 d。如果 d 刚好缓存在第 k 层中,那么就是**缓存命中**(cache hit)。 74 | 75 | 缓存不命中:如果第 k 层中没有缓存数据对象 d,那么就是缓存不命中(cache miss)。当发生 cache miss 时,会从下一次取出包含 d 的那个块,如果第 k 层的缓存已经满了的话,可能就会覆盖现存的一个块。 76 | 77 | 覆盖一个现存的块的过程称为替换(replacing)或驱逐(evicting)。被驱逐的看这个块有时也称为牺牲块(victim block)。决定该替换那个块是由缓存的替换策略(replacement policy)来控制的。(LRU, LFU 等等替换策略在这里可以使用) 78 | 79 | 缓存不命中的种类: 一个空的缓存有时称为冷缓存(cold cache),此类不命中称为compulsory miss 或 cold miss。 80 | 81 | ### 存储器层次结构概念小结 82 | 83 | 存储器层次结构行之有效,因为较慢的设备比较快的设备更便宜,还因为程序偏向于展示局部性: 84 | * 利用时间局部性,同一数据对象可能会被多次使用 85 | * 利用空间局部性,块通常包含有多个数据对象 86 | 87 | ![enter description here][43] 88 | 89 | ## 高速缓存存储器 90 | 91 | 早期计算机系统的存储器结构只有三层:CPU 寄存器、DRAM 主存储器和磁盘存储。不过,由于 CPU 和主存之间逐渐增大的差距,系统设计者被迫在 CPU 寄存器文件和主存之间插入了一个小的 SRAM 高速缓存存储器,称为 L1 高速缓存。之后又插入了一个更大的高速缓存,称为 L2 高速缓存,之后还有 L3 高速缓存。周期数:L1(2~4), L2(~10), L3(~30~40) 92 | 93 | ### 通用的高速缓存存储结构 94 | 95 | ![enter description here][44] 96 | 97 | ### 直接映射高速缓存 98 | 99 | 每个组只有一行(E=1)的高速缓存被称为直接映射高速缓存 100 | 101 | ![enter description here][45] 102 | 103 | ![enter description here][46] 104 | 105 | ### 组相联高速缓存 106 | 107 | 直接映射高速缓存中冲突不命中造成的问题是源于每一个组只有一行,组相联高速缓存(set associative cache)放松了这条限制,所以每个组都保存了有多于一行的高速缓存 108 | 109 | ![enter description here][47] 110 | 111 | ![enter description here][48] 112 | 113 | ![enter description here][49] 114 | 115 | ### 全相联高速缓存 116 | 117 | 由一个包含所有高速缓存行的组(E = C/B)组成 118 | 119 | ![enter description here][50] 120 | 121 | ![enter description here][51] 122 | 123 | ![enter description here][52] 124 | 125 | ### 有关写的问题 126 | 127 | 更新写命中的缓存的方法: 128 | 1. 直写(write-throuth),立即将w的高速缓存块写回到存储器中,优点是简单,缺点是每条存储指令都会引起总线上面的一个写事务 129 | 2. 写回(write-back),尽可能推迟存储器的更新,只有当替换算法要驱逐已更新块时,才写入存储器中,优点是能够显著减少总线事务的数量,缺点是增加复杂性,需要额外维护一个修改位(dirty bit) 130 | 131 | 处理写不命中的方法: 132 | 1. 写分配(write-allocate),加载相应的存储器块到高速缓存中,然后更新这个高速缓存块 133 | 2. 非写分配(not-write-allocate),避开高速缓存,直接把字写到存储器中 134 | 135 | ### 指令高速缓存和统一高速缓存 136 | 137 | 只保存指令的高速缓存成为i-cache,只保存程序数据的高速缓存称为d-cache,即保存指令又保存程序数据的高速缓存称为统一的高速缓存(unified cache) 138 | 139 | ![enter description here][53] 140 | 141 | ### 高速缓存参数的性能影响 142 | 143 | 衡量性能的指标: 144 | * 不命中率 145 | * 命中率 146 | * 命中时间 147 | * 不命中处罚 148 | 149 | 影响: 150 | 1. 高速缓存大小 151 | 2. 块大小 152 | 3. 相联度 153 | 4. 写策略 154 | 155 | ## 编写高速缓存友好的代码 156 | 157 | 1. 让最常见的情况运行得快 158 | 2. 在每个循环内部使缓存不命中数量小 159 | 160 | ## 综合:高速缓存对程序性能的影响 161 | 162 | 存储器山(memory mountain) 163 | ![enter description here][54] 164 | 165 | ## 小结 166 | 167 | 程序员可以通过编写有良好空间和时间局部性的程序来显著地改进程序的运行时间。利用基于 SRAM 的高速缓存存储器特别重要。 168 | 169 | [39]: https://data2.liuin.cn/story-writer/2018_1_28_1517109327779.jpg 170 | [40]: https://data2.liuin.cn/story-writer/2018_1_28_1517110224118.jpg 171 | [41]: https://data2.liuin.cn/story-writer/2018_1_28_1517110431384.jpg 172 | [42]: https://data2.liuin.cn/story-writer/2018_1_28_1517110523087.jpg 173 | [43]: https://data2.liuin.cn/story-writer/2018_1_28_1517110831456.jpg 174 | [44]: https://data2.liuin.cn/story-writer/2018_1_28_1517111205440.jpg 175 | [45]: https://data2.liuin.cn/story-writer/2018_1_28_1517111234451.jpg 176 | [46]: https://data2.liuin.cn/story-writer/2018_1_28_1517111247641.jpg 177 | [47]: https://data2.liuin.cn/story-writer/2018_1_28_1517111425945.jpg 178 | [48]: https://data2.liuin.cn/story-writer/2018_1_28_1517111461520.jpg 179 | [49]: https://data2.liuin.cn/story-writer/2018_1_28_1517111477325.jpg 180 | [50]: https://data2.liuin.cn/story-writer/2018_1_28_1517111577074.jpg 181 | [51]: https://data2.liuin.cn/story-writer/2018_1_28_1517111592957.jpg 182 | [52]: https://data2.liuin.cn/story-writer/2018_1_28_1517111605869.jpg 183 | [53]: https://data2.liuin.cn/story-writer/2018_1_28_1517112120679.jpg 184 | [54]: https://data2.liuin.cn/story-writer/2018_1_28_1517112396218.jpg -------------------------------------------------------------------------------- /notes/CSAPP 计算机系统漫游.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 计算机系统漫游 3 | date: 2018-01-23 17:47:19 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第1章笔记 9 | 10 | 11 | 12 | ## 信息就是位+上下文 13 | 整个计算机系统中的所有信息都可以用一串比特串的形式表示,区分不同的数据对象的唯一方法就是我们读到的这些对象时的上下文(context) 14 | 15 | ## 程序被其他程序翻译成不同的格式 16 | 一个高级语言写的程序(这里以C语言为例),从源代码到最终的机器中的可执行文件会经过一下几个阶段: 17 | 1. 预处理阶段,处理源码的中的预处理语句(比如说#include) 18 | 2. 编译阶段,将c语言编译成汇编语言 19 | 3. 汇编阶段,把汇编语言翻译成机器指令 20 | 4. 链接阶段,把在程序中调用的库函数的相关文件引入 21 | 22 | ## 了解编译系统如何工作室大有益处的 23 | 促使程序员要知道编译系统是如何工作的原因: 24 | 1. 优化程序性能,我们需要对汇编语言以及编译器如何将不同的C语句转化为汇编语言有基本的了解 25 | 2. 理解链接时出现的错误 26 | 3. 避免安全漏洞,其中一个比较典型的是缓冲区溢出错误 27 | 28 | ## 处理器读并解释存储在存储器中的指令 29 | 30 | 一个计算机系统的硬件主要由以下几个部分组成: 31 | 1. 总线,负责携带信息字节并在各个部件之间进行传输 32 | 2. I/O设备,负责系统和外界的联系 33 | 3. 主存,运行程序时存放程序以及程序中含有的数据 34 | 4. 处理器,解释(或执行)存储在主存中的指令 35 | 36 | 执行一个hello程序的过程有一下几部: 37 | * shell程序执行其指令,等待我们输入命令 38 | * 我们输入完命令以后,shell执行一系列指令,将hello程序的代码以及其数据加载到主存中 39 | * 处理器开始执行hello程序中的机器指令,将“hello world”输出到屏幕上 40 | 41 | ## 高速缓存 42 | 43 | 我们使用的存储设备通常是较大的存储设备比较小的存储设备运行地要慢,所以就使用一个较小的速度较快的存储设备作为CPU和Main Memory交换数据的桥梁,这个设备就是高速缓存(cache memories) 44 | 45 | ## 形成层次结构的存储结构 46 | 47 | 在计算机系统的存储设备被组织成了一个金字塔形的存储层次模型,其中从上到下,设备速度越来越慢,空间越来越大,每字节的造价越来越便宜。 48 | 49 | ![enter description here][1] 50 | 51 | ## 操作系统管理硬件 52 | 53 | 操作系统可以看成是一个应用程序和硬件之间的一个软件,其有两个基本功能: 防止硬件被失控的程序滥用; 为应用程序提供控制硬件的简单一致的方法 54 | 55 | ### 进程 56 | 进程可以看成是操作系统对正在运行的程序的一种抽象,在一个系统中可以运行多个进程,这些进程对外表现好像是独占硬件,实际上是通过不同进程之间进程的交互执行实现的,这个过程叫上下文切换(context switch) 57 | 58 | ### 线程 59 | 一个进程可以由多个线程组成,运行在一个上下文环境中,共享代码以及全局数据。因为共享数据,使得其比一般的进程更加高效(花在context switch的时间少)。 60 | 61 | ### 虚拟存储器 62 | 给进程提供的一个好像自己独占主存的假象,对于进程的所使用的虚拟存储器可以分成一下几个部分: 63 | * 程序代码和数据 64 | * 堆,可以动态扩展或者收缩,供像malloc和free这样的C语言中的库进行调用 65 | * 共享库 66 | * 栈,可以动态扩展或者收缩,用于编译器的函数调用 67 | * 内核虚拟存储器 68 | 69 | ![enter description here][2] 70 | 71 | ### 文件 72 | 文件可以看成字节序列,每一个I/O设备从本质上来看都可以看成是文件 73 | 74 | ## 利用网络系统和其他系统进行通信 75 | 76 | ## 导图 77 | 78 | ![enter description here][3] 79 | 80 | 81 | [1]: https://data2.liuin.cn/story-writer/2018_1_24_1516786032387.jpg 82 | [2]: https://data2.liuin.cn/story-writer/2018_1_24_1516786535550.jpg 83 | [3]: https://data2.liuin.cn/story-writer/2018_1_24_A%20Tour%20of%20Computer%20System.png 84 | -------------------------------------------------------------------------------- /notes/CSAPP-优化程序性能.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 优化程序性能 3 | date: 2018-01-27 22:54:56 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第5章笔记 9 | 10 | 11 | 12 | 编写高效程序可以考虑两个方面:选择好的算法和数据结构;编写编译器能够有效优化以转换为高效可执行代码的源码。 13 | 14 | 程序员通常需要在实现和维护程序的简单性与他的运行速度之间做出权衡折中。 15 | 16 | 程序优化的第一步就是消除不必要的内容,让代码尽可能有效地执行它期望的工作。这包括消除不必要的函数调用、条件测试和存储器引用。这些优化不依赖于目标机器的任何具体属性。 17 | 18 | 研究程序的汇编代码表示,是理解编译器,以及产生的代码如何运行的最有效的手段之一。仔细研究内循环的代码是一个很好的开端。 19 | 20 | ## 优化编译器的能力和局限性 21 | 22 | 编译器优化程序的能力受到几个因素的制约: 23 | 1. 要求决不能改变正确程序的行为 24 | 2. 对程序的行为、使用的环境有所了解 25 | 3. 需要很快地完成编译工作 26 | 27 | 阻碍优化的几个因素: 28 | * 存储器别名的使用,编译器必须假设不同的指针可能会指向存储器中的同一个位置 29 | * 函数调用,函数可能会有副作用(改变全局程序状态的一部分) 30 | 31 | ## 表示程序性能 32 | 33 | 我们引入度量标准每元素的周期数(Cycles Per Element, CPE)作为一种表示性能并指导我们改进代码的方法。处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹(GHz),即十亿周期每秒来表示。CPE 越小越好。 34 | 35 | ## 消除循环的低效率 36 | 37 | 优化前 38 | 39 | ![enter description here][34] 40 | 41 | 优化后 42 | 43 | ![enter description here][35] 44 | 45 | 以上的代码移动(code motion)是一种优化。这类优化包括识别要执行多次(例如在循环里)但是计算结果不会改变的计算。因而可以将计算移动到代码前面不会被多次求值的部分。 46 | 47 | 编程时一个常见的问题就是一个看上去无足轻重的代码片段有隐藏的渐进低效率(asymptotic inefficiency) 48 | 49 | ## 减少过程调用 50 | 51 | 过程调用会代码相当大的开销,而且妨碍大多数形式的程序优化。我们可以直接访问数组,而不是利用函数调用并加上边界检查: 52 | 53 | ![enter description here][36] 54 | 55 | 对于性能至关重要的程序来说,为了速度,可能需要损害一些对象的模块性和抽象性 56 | 57 | ## 消除不必要的存储器引用 58 | 59 | 累加过程中其实没有必要每次都把结果写入到 dest 中,可以使用一个临时变量,消除不必要的存储器引用: 60 | 61 | ![enter description here][37] 62 | 63 | ## 理解现代处理器 64 | 65 | 不依赖目标机器的优化,只能简单通过降低过程调用开销、以及消除一些重大的“妨碍优化因素”来实现。 66 | 67 | 要想获得充分提高的性能,需要仔细地分析程序,同时代码的生成也要针对目标处理器进行调整。在实际的处理器中,是同时对多条指令求值,这个现象称为指令级并行。现代微处理器取得的了不起的功绩之一是:它们采用复杂而奇异的微处理器结构,其中,多条指令可以并行地执行,同时又呈现一种简单地顺序执行指令的表象。 68 | 69 | 两种下界描述了程序的最大性能。当一系列操作必须按照严格顺序执行时,就会遇到延迟界限(latency bound),因为在下一条指令开始之前,这条指令必须结束。当代码中的数据相关限制了处理器利用指令级并行的能力时,延迟界限能够限定程序性能。吞吐量界限(throughput bound)刻画了处理器功能单元的原始计算能力。这个界限是程序性能的终极限制。 70 | 71 | ### 整数操作 72 | 73 | Nehalem 微体系结构是 20 世纪 90 年代以来,许多制造商生产的典型的高端处理器。在工业界称为超标量(superscalar),意思是可以在每个时钟周期执行多个操作,而且是乱序的(out-of-order),意思就是指令执行的顺序不一定要与它们在机器级程序中的顺序一致。整个设计有两个主要部分:指令控制单元(Instruction Control Unit, ICU)和执行单元(Execution Unit, EU)。前者负责从存储器中读出指令序列,并根据这些指令序列生成一组针对程序数据的基本操作;而后执行这些操作。 74 | 75 | ICU 从指令高速缓存(instruction cache)中读取指令。指令高速缓存是一个特殊的高速缓存存储器,它包含最近访问的指令。通常,ICU 会在当前正在的指令很早之前取指,这样它才有足够的时间对指令译码,并把操作发送到 EU。不过,一个问题是党程序遇到分支时,程序有两个可能的前进方向。一种可能会选择分支,控制被传递到分支目标。另一种可能是,不选择分支,控制被传递到指令序列的下一条指令。现代处理器采用了一种称为**分支预测(branch prediction)的技术,处理区会猜测是否会选择分支,同时还预测分支的目标地址**。使用投机执行(speculative execution)的技术,处理器会开始取出位于它预测的分支会跳到的地方的指令,并对指令译码,甚至在它确定分支预测是否正确之前就开始执行这些操作。如果过后确定分支预测错误,会将状态重新设置到分支点的状态,并开始取出和执行另一个方向上的指令。 76 | 77 | ### 功能单元的性能 78 | 79 | 每个运算都是由两个周期计数值来刻画的:一个是延迟(latency),它表示完成运算所需要的总时间;另一个是发射时间(issue time),它表示两个连续的同类型运算之间需要的最小时钟周期数。随着字长的增加,对于更复杂的数据类型,对于更复杂的运算,延迟也会增加。 80 | 81 | ### 处理器操作的抽象模型 82 | 83 | 我们会使用程序的数据流(data-flow)表示,作为分析在现代处理器上执行的机器级程序性能的一个工具,这是一种图形化的表示方法,展现了不同操作之间的数据相关是如何限制它们的执行顺序的。这种限制形成了图中的**关键路径**(critical path),这是执行一组机器指令所需时钟周期数的一个下界。 84 | 85 | ![enter description here][38] 86 | 87 | 88 | ## 循环展开(loop unrolling) 89 | 90 | 循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。其思想是在一次迭代中访问数组并做乘法,这样得到的程序需要更少的迭代,从而降低循环的开销。 91 | 92 | 循环展开能够从两个方面改善程序的性能。首先,它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。其次,它提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。 93 | 94 | ## 提高并行性 95 | 96 | ### 循环分割(loop splitting) 97 | 98 | 对于一个可结合可交换的合并操作来说,比如说整数的乘法和加法,我们可以通过将一组数据合并分割成两个或者更多部分,并在最后合并结果来提高性能 99 | 100 | ### 寄存器溢出(register spilling) 101 | 102 | 循环并行性的好处就是受描述计算的汇编代码的能力限制,如果我们有并行度p超过了可用的寄存器数量,这种情况性能就会急剧下降。 103 | 104 | 作为一条通用原则,无论何时当一个编译了的程序显示出来在某个频繁使用的内循环中有寄存器溢出的迹象时,都会偏向于重写代码,使之需要较少的临时值。 105 | 106 | ## 确定和消除性能瓶颈 107 | 108 | Amdahl定律 109 | 110 | 当我们加快系统一部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。 111 | 112 | 主要观点:想要大幅度提高整个系统的速度,我们必须提高系统很大一部分的速度。 113 | 114 | ## 小结 115 | 116 | 没有任何编译器能用一个好的算法或数据结构代替低效率的算法或数据结构,因此程序设计时的这些方面仍然应该是程序员主要关心的。 117 | 118 | 119 | [34]: https://data2.liuin.cn/story-writer/2018_1_27_1517062357118.jpg 120 | [35]: https://data2.liuin.cn/story-writer/2018_1_27_1517062333115.jpg 121 | [36]: https://data2.liuin.cn/story-writer/2018_1_27_1517062508161.jpg 122 | [37]: https://data2.liuin.cn/story-writer/2018_1_27_1517062838432.jpg 123 | [38]: https://data2.liuin.cn/story-writer/2018_1_27_1517063266274.jpg 124 | -------------------------------------------------------------------------------- /notes/CSAPP-信息的表示和处理.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 信息的表示和处理 3 | date: 2018-01-24 21:59:14 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第2章笔记 9 | 10 | 11 | 12 | ## 信息存储 13 | 14 | 大部分计算机使用8位的块(或者字节)来作为最小的可寻址的存储器单元。机器级程序将存储器视为一个非常大的字节数组,称之为虚拟存储器。存储器中的每一个字节由唯一的一个地址(address)来标识,所有可能地址的集合称之为虚拟地址空间(virtual address space) 15 | 16 | ### 十六进制表示法 17 | 一个字节有8位,用二进制表示就是 \00000000(2)-11111111(2) ,用十进制表示是0(10)-256(10),用十六进制表示是00(16)-FF(16) 18 | 19 | ### 字 20 | 每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(nominal size),字长决定系统中最重要的参数就是虚拟地址空间的最大大小 21 | 22 | ### 数据大小 23 | 计算机和编译器使用不同的方式来编码数字,比如说不同长度的整数和浮点数,从而支持多种数据格式 24 | ![enter description here][4] 25 | 26 | 程序员应该力图使他们的程序在不同的计算机和编译器上可移植,可移植的其中一个方面就是**使程序对不同的数据类型的确切大小不那么敏感** 27 | 28 | ### 寻址和字节顺序 29 | 30 | 对于跨多个字节的程序对象,我们必须建立两个规则:这个对象的地址是什么;我们怎样在存储器中对这些字节进行排序 31 | 32 | > 小端法 33 | > 从低有效字节到高有效字节的顺序存储对象 34 | 35 | > 大端法 36 | > 从高有效字节到低有效字节的顺序存储对象 37 | 38 | 对于一个十六进制的数0x01234567: 39 | ![enter description here][5] 40 | 41 | 几种机器所使用的字节顺序会成为问题的情况: 42 | 43 | * 在不同类型的机器之间通过网络传送二进制数据。 44 | * 当阅读表示整数数据的字节序列时,字节顺序也很重要。 45 | * 当编写规避正常的类型的系统时。 46 | 47 | ### 表示字符串 48 | C中的字符串被编码为一个以null(其值为0)字符结尾的字符数组 49 | 50 | ### 表示代码 51 | 不同的机器类型使用的是不同的并且不兼容的指令和编码方式,所以最后的二进制代码是有很强的平台依赖性的,其很少能够在不同的操作系统和机器之间进行移植 52 | 53 | ### C中的位级运算 54 | 包括按位与、按位或、异或运算,位运算的一个常见应用就是实现掩码运算(从一个字中选出一个组位) 55 | 56 | ### C中的逻辑运算 57 | 包括逻辑或、逻辑与、逻辑非 58 | 逻辑运算表达式中,第一个参数能够确定表达式的结果的时候,逻辑运算表达式就不会计算第二个参数的值 59 | 60 | ### C中的移位运算 61 | 向左移位运算右端补0 62 | 向右移位运算包含两种形式:逻辑移位(左端补0)和算数移位(左端补最高有效位) 63 | 64 | ## 整数表示 65 | 66 | ### 整型数据类型 67 | 68 | ![enter description here][6] 69 | 70 | ### 无符号和二进制补码编码 71 | 72 | 假设一共有 w 位,每个介于 0 ~ 2^w -1 之间的数都有唯一一个 w 位的值编码,即这个函数映射是一个双射。 73 | 74 | 补码表示的是字的最高有效位解释为负权(negative weight)。 75 | 76 | ### 有符号数和无符号数之间的转换 77 | ![enter description here][7] 78 | 79 | ### C中的有符号数和无符号数 80 | 81 | C 语言允许无符号数和有符号数之间的转换。转换的原则是底层的位表示保持不变。 82 | 83 | ### 扩展一个数字的位表示 84 | 零扩展: 将一个无符号数转换为一个更大的数据类型,在开头加0 85 | 符号扩展: 将一个二进制补码转化为一个更大的数据类型,在开头加最高有效位 86 | 87 | ### 截断数字 88 | 截断一个数字可能会改变它的值——溢出的一种形式 89 | 90 | ### 有关有符号数和无符号数的建议 91 | 92 | 有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的,程序员经常忽视了它的影响。 93 | 94 | 避免这类错误的一种方法就是绝**不使用无符号数**。实际上,除了 C 以外,很少有语言支持无符号整数。 95 | 96 | ## 整数运算 97 | 98 | ### 无符号加法 99 | 每个数都能表示为 w 位无符号数字。如果计算它们的和,表示这个和可能需要 w + 1位。无符号运算可以被视为一种模运算形式。 100 | ![enter description here][8] 101 | 102 | ### 二进制补码加法 103 | 必须确定当结果太大(为正)或者太小(为负)时,应该做些什么。 104 | ![enter description here][9] 105 | 106 | ### 二进制补码的乘法 107 | ![enter description here][10] 108 | 109 | ### 乘以2的幂 110 | 111 | 在大多数机器上,整数乘法指令相当慢,需要 10 个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运算和移位)只需要 1 个时钟周期。因此,编译器使用了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。 112 | 113 | ### 除以2的幂 114 | 115 | 在大多数机器上,整数除法要比整数乘法更慢——需要 30 个或者更多的周期。除以 2 的幂也可以用移位运算右移来实现,无符号和补码数分别使用逻辑移位和算术移位来达到目的。 116 | 117 | ## 浮点 118 | 119 | ### 二进制小数 120 | ![enter description here][11] 121 | 122 | ### IEEE浮点表示 123 | 124 | 用 V = (-1)^s * M * 2^E 的形式来表示一个数: 125 | * 符号(sign) s决定这个数是负数(s=1)还是正数(s=0),对于数值 0 的符号位解释作为特殊情况处理。 126 | * 尾数(significand) M 是一个二进制小数,它的范围是 1 ~ 2 - ε,或者是 0 ~ 1 - ε。 127 | * 阶码(exponent) E 的作用是对浮点数加权,这个权重是 2 的 E 次幂(可能是负数) 128 | 129 | 将浮点数的位表示划分为三个字段,分别对这些值进行编码: 130 | * 一个单独的符号位 s 直接编码符号 s。 131 | * k 位的阶码字段 exp = e(k-1)…e(1)e(0) 编码阶码 E。 132 | * n 位小数字段 frac = f(n-1)…f(1)f(0) 编码尾数 M,但是编码出来的值也依赖于阶码字段的值是否等于 0。 133 | 134 | ### 舍入 135 | 因为表示方法限制类浮点数的范围和精度,浮点运算只能近似地表示实数运算。因此,对于值 x,我们一般想用一种系统的方法,能够找到“最接近的”匹配值,这就是舍入运算的任务。 136 | 137 | 常见的舍入方式有:向偶数舍入、向零舍入、向下舍入、向上舍入 138 | ![enter description here][12] 139 | 140 | ### 浮点运算 141 | 142 | 浮点加法不具有结合性。浮点乘法在加法上不具备分配性。对于科学计算程序员和编译器编写者来说,这是很严重的问题,即使为了在三维空间中确定两条线是否交叉而写代码这样看上去很简单的任务,也可能成为一个很大的挑战。 143 | 144 | ### C语言的浮点数 145 | 146 | float 和 double。在 int、float 和 double 格式之间进行强制类型转换时,程序改变数值和位模式的原则如下(假设 int 是 32 位的): 147 | * 从 int 转换成 float,不会溢出,可能被舍入。 148 | * 从 int 或 float 转换成 double,能够保留精确的数值。 149 | * 从 double 转换成 float,可能溢出成为正无穷或负无穷,也可能被舍入。 150 | * 从 float 或者 double 转换成 int,值会向零舍入。例如 1.999 将被转换成 1。 151 | 152 | 153 | [4]: https://data2.liuin.cn/story-writer/2018_1_24_1516797063776.jpg 154 | [5]: https://data2.liuin.cn/story-writer/2018_1_24_1516797382468.jpg 155 | [6]: https://data2.liuin.cn/story-writer/2018_1_24_1516799034435.jpg 156 | [7]: https://data2.liuin.cn/story-writer/2018_1_24_1516799175770.jpg 157 | [8]: https://data2.liuin.cn/story-writer/2018_1_24_1516800136471.jpg 158 | [9]: https://data2.liuin.cn/story-writer/2018_1_24_1516800211055.jpg 159 | [10]: https://data2.liuin.cn/story-writer/2018_1_24_1516800273052.jpg 160 | [11]: https://data2.liuin.cn/story-writer/2018_1_24_1516800421224.jpg 161 | [12]: https://data2.liuin.cn/story-writer/2018_1_24_1516800599058.jpg 162 | -------------------------------------------------------------------------------- /notes/CSAPP-处理器体系结构.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 处理器体系结构 3 | date: 2018-01-26 21:49:11 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第4章笔记 9 | 10 | 11 | 12 | 一个处理器支持的指令和指令字节编码称为它的ISA(Instruction-set Architecture)。这一章主要讲以Y86指令集体系结构为例讲了处理器中指令的执行流程以及流水的原理和实现。 13 | 14 | ## Y86指令集体系结构 15 | 16 | Y86 程序中的每条指令都会读取或修改处理器状态的某些部分,这称为程序员(用汇编代码写程序的人或机器级代码的编译器)可见状态: 17 | 18 | ![enter description here][27] 19 | 20 | Y86指令集 21 | 22 | ![enter description here][28] 23 | 24 | 指令编码:每条指令的第一个字节表示指令的类型,这个字节分为两个部分,每个部分四位:高四位是代码(code)部分,低四位是功能(function)部分。 25 | 26 | ![enter description here][29] 27 | 28 | CISC vs RISC 29 | 30 | ![enter description here][30] 31 | 32 | ## 逻辑设计和硬件控制语言HCL 33 | 34 | 硬件设计中,电子电路是用来计算位的函数(function on bits),以及在各种存储器元素中存储位。实现一个数字系统主要有三个部分:计算位的函数的组合逻辑、存储位的存储元素,以及控制存储元素更新的时钟信号。 35 | 36 | ### 逻辑门 37 | 38 | 逻辑门是数字电路的基本计算元素。它们产生的输出,等于它们输入位值的某个布尔函数 39 | 40 | ### 组合电路和HCL布尔表达式 41 | 42 | 很多的逻辑门组合成一个网,就能构建计算块(computational block),称为组合电路(combinational circuits)。构建这些网有两条限制: 43 | * 两个或多个逻辑门的输出不能连接在一起。否则它们可能会使线上的信号矛盾,可能会导致一个不合法的电压或电路故障 44 | * 这个网必须是无环的。也就是在网中不能有路径经过一系列的门而形成一个回路,这样的回路会导致该网络计算的函数有歧义。 45 | 46 | ### 字级的组合电路和HCL整数表达式 47 | 48 | 字级的组合电路: 对数据字(data word)进行操作的电路,在HCL中,将所有的字级的信号都声明为int,而不指定字的大小 49 | 50 | ### 集合关系(set membership) 51 | 52 | 实现将一个信号和众多可能的信号做比较,判断正在处理的某些指令是否属于一类指令代码 53 | 54 | ### 存储器和时钟控制 55 | 56 | 为了产生时序电路,必须引入按位存储信息的设备,考虑两种: 57 | * 时钟寄存器存储单个位或字 58 | * 随机访问存储器存储多个字 59 | 60 | ## Y86的顺序(sequential)实现 61 | 62 | ### 将处理组织成阶段 63 | 64 | 处理一条指令包含很多操作,我们把其组织成某些特殊的阶段序列,使得即使指令的动作差异很大,但是所有的指令都遵守统一的序列: 65 | * 取指(fetch),取指阶段,从存储器中读入指令,地址为程序计数器(PC)的值 66 | * 解码(decode),解码阶段从寄存器中读入最多两个操作数,得到其值 67 | * 执行(execute),算术逻辑单元(ALU)要么执行指令指明的操作,计算存储器引用的有效地址,要么增加或者减少栈指针。 68 | * 访存(memory),可以将数据写入存储器,或者从存储器中读入数据 69 | * 写回(write back),最多可以写两个结果到寄存器文件 70 | * 更新PC(update PC),将PC设置为下一条PC的地址 71 | 72 | SEQ的硬件结构、时序、阶段实现 因为比较复杂,可以参考书上的内容 73 | 74 | ## 流水线的通用原理 75 | 76 | 流水线化的系统有一些通用的属性和原理,在流水线系统中,待执行的任务被划分成若干个相互独立的阶段。 77 | 78 | ### 计算流水线 79 | 80 | 由一些执行计算的逻辑以及保存计算结果的寄存器组成。时钟信号控制在每个特定的时间间隔加载寄存器。 81 | 82 | ![enter description here][31] 83 | 84 | ### 流水线操作的详细说明 85 | 86 | 三段流水线的时序 87 | 88 | ![enter description here][32] 89 | 90 | 流水线操作的一个时钟周期 91 | 92 | ![enter description here][33] 93 | 94 | ### 流水线的局限性 95 | 96 | * 不一致的划分 97 | * 流水线过深,收益反而下降(由寄存器延迟造成的) 98 | 99 | ### 带反馈的流水线系统 100 | 101 | 可能产生的相关:数据相关(data dependency)、顺序相关(sequential dependency)、控制相关(control dependency) 102 | 103 | ## Y86的流水线实现 104 | 105 | ### 插入流水线寄存器 106 | 107 | 在SEQ+的各个阶段之间插入流水线寄存器,并对信号重新做排列 108 | 109 | ### 对信号做重新排列和标号 110 | 111 | 在流水线化的设计中,对应正在进过系统的各个指令,对指令中处理的值进行重新排列和标号 112 | 113 | ### 预测下一个PC 114 | 115 | ### 流水先冒险(hazard) 116 | 117 | 数据相关和控制相关导致的流水线产生的计算错误,成为冒险(hazard)。同样,冒险也分为数据冒险和控制冒险两大部分。 118 | 119 | ### 用暂停(stalling)来避免数据冒险 120 | 121 | 暂停时,处理器会停止流水线中一条或多条指令,知道冒险不再满足 122 | 123 | ### 用转发(forwarding)来避免数据冒险 124 | 125 | ## 小结 126 | 127 | 指令集体系结构(ISA)在处理器行为(就指令集合以及其编码而言)和如何实现处理器之间提供了一层抽象。 128 | 129 | 流水线化通过让不同的阶段并行操作,改进系统的吞吐量性能。 130 | 131 | 处理器设计的几个重要经验: 132 | * 管理复杂性是首要问题 133 | * 我们不需要直接实现ISA 134 | * 硬件设计人员必须谨慎小心,一旦芯片被制造出来,就几乎不可能改正任何错误了。 135 | 136 | [27]: https://data2.liuin.cn/story-writer/2018_1_26_1516971186027.jpg 137 | [28]: https://data2.liuin.cn/story-writer/2018_1_26_1516971281093.jpg 138 | [29]: https://data2.liuin.cn/story-writer/2018_1_26_1516971475052.jpg 139 | [30]: https://data2.liuin.cn/story-writer/2018_1_26_1516971547830.jpg 140 | [31]: https://data2.liuin.cn/story-writer/2018_1_26_1516973076361.jpg 141 | [32]: https://data2.liuin.cn/story-writer/2018_1_26_1516973204553.jpg 142 | [33]: https://data2.liuin.cn/story-writer/2018_1_26_1516973260280.jpg 143 | -------------------------------------------------------------------------------- /notes/CSAPP-并发编程.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 并发编程 3 | date: 2018-02-03 18:58:52 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第12章笔记 9 | 10 | 11 | 12 | 13 | 如果逻辑流在时间上重叠,那么它们就是并发(concurrent)的。并发性不仅仅局限于内核,他也可以在应用中也扮演着重要的角色: 14 | * 在多处理器上进行并行计算 15 | * 访问慢速I/O设备 16 | * 与人交互 17 | * 通过推迟工作以减少执行时间 18 | * 服务多个网络客户端 19 | 20 | 现代操作系统提供了三种基本的构造并发程序的方法: 21 | * 进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(interprocess communication, IPC)机制。 22 | * I/O 多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。 23 | * 线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像 I/O 多路复用一样共享同一个虚拟地址空间。 24 | 25 | ## 基于线程的并发编程 26 | 27 | 构造并发程序最简单的方法就是用进程,使用那些大家都很熟悉的函数,像 fork, exec 和 waitpid。 28 | 29 | 对于在父、子进程间共享状态信息,进程有一个非常清晰的模型:**共享文件表,但是不共享用户地址空间**。进程有独立的地址空间既是优点也是缺点。这样一来,一个进程不可能不小心覆盖另一个进程的虚拟存储器,这就消除了许多令人迷惑的错误。 30 | 31 | 另一方面,**独立的地址空间使得进程共享状态信息变得更加困难**。为了共享信息,它们必须使用显式的 IPC 机制。基于进程的设计的另一个缺点是,它们往往比较慢,因为进程控制和 IPC 的开销很高。 32 | 33 | ## 基于I/O多路复用的并发编程 34 | 35 | I/O 多路复用可以用作并发**事件驱动**(event-driven)程序的基础,在事件驱动程序中,流是因为某种事件而前进的。一般概念是将逻辑流模型化为**状态机**。不严格地说,一个状态机(state machine)就是一组状态(state)、输入事件(input event)和转移(transition),其中转移就是将状态和输入事件映射到状态。每个状态都将一个(输入状态,输入事件)对映射到一个输出状态。自循环(self-loop)是同一组输入和输出状态之间的转移。通常把状态机花城有向图,其中节点表示状态,有向弧表示转移,而弧上的标号表示输入事件。一个状态机从某种初始状态开始执行。每个输入事件都会引发一个从当前状态到下一状态的转移。 36 | 37 | 事件驱动设计的一个优点是,它比基于进程的设计给了程序员更多的对程序行为的控制。另一个优点是在流之间共享数据变得很容易,而且事件驱动设计常常比基于进程的设计要高效得多,因为它们不需要进程上下文切换来调度新的流。 38 | 39 | 事件驱动设计的一个明显的缺点就是编码复杂,另一重大缺点时它们不能充分利用多核处理器。 40 | 41 | ## 基于线程的并发编程 42 | 43 | 一个线程(thread)就是运行在一个进程上下文中的一个逻辑流。每个线程都有它自己的**线程上下文**(thread context),包括一个唯一的整数线程ID(Thread ID, TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。 44 | 45 | 基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特点 46 | 47 | ### 线程执行模型 48 | 49 | ![enter description here][110] 50 | 51 | ## 多线程程序中的共享变量 52 | 53 | 线程很有吸引力的一个方面就是多个线程很容易共享相同的程序变量 54 | 55 | ### 线程存储器模型 56 | 57 | 一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文,包括线程 ID、栈、栈指针、程序计数器、条件码和通用目的寄存器。每个线程和其他线程一个共享进程上下文的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成的。 58 | 59 | 从实际操作的角度来说,让一个线程去读写另一个线程的寄存器是不可能的。寄存器不是共享的,而虚拟存储器是共享的。 60 | 61 | ### 将变量映射到存储器 62 | 63 | 线程化的 C 程序中变量根据它们的存储类型被映射到虚拟存储器: 64 | * 全局变量:在运行时,虚拟存储器的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用 65 | * 本地自动变量:定义在函数内部但是没有 static 属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例 66 | * 本地静态变量:定义在函数内部并有 static 属性的变量,和全局变量一样 67 | 68 | ## 用信号量同步线程 69 | 70 | 共享变量十分方便,但是它们也引入了同步错误(synchronization error)的可能性。 71 | 72 | ### 进度图 73 | 74 | 一个进度图( progress graph)将个并发线程的执行模型化为一条维笛卡儿空间中的轨线。 每条轴k对应于线程k的进度。每个点(1,2…,n)代表线程k(k=1,,n)已经完成了指令I(k)这一个状态 75 | 76 | ![enter description here][111] 77 | 78 | 一个进度图将指令执行模型化为一个从一种状态到另一种状态的转換( transition)。一个转换被 表示为一条从一点到相邻点的有向边。合法的转换是向右(线程1中的一条指令完成)或者向上(线 程2中的一条指令完成)的。两个指令不能在同一时刻完成一一对角线转換是不允许的。程序决不 会反向运行,所以向下或者向左移动的转换也是不合法的。 79 | 80 | ![enter description here][112] 81 | 82 | 环绕不安全区的轨线叫做**安全轨线**( safe trajectory)。相反,接触任何不安全区的轨线就叫做**不安全轨线**( unsafe trajectory)。 83 | 84 | ![enter description here][113] 85 | 86 | 任何安全轨迹都将正确地更新共享计数器。 87 | 88 | ### 利用信号量访问共享变量 89 | 90 | 一种经典的解决同步不同执行线程问题的方法:基于一种叫做信号量( semaphore)的特殊类型变量的。信号量s是具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作称为P和V。 91 | 92 | * P(s):如果s是非零的,那么P将s减1,并且立即返回。如如果s为零,那么就挂起进程, 直到s变为非零,并且该进程被一个V操作重启。在重启之后,P操作将s减1,并将控制 返回给调用者。 93 | * V(s):操作将s加1。如果有任何进程阻塞在P操作等待s变成非零,那么V操作会重启 这些进程中的一个,然后该进程将s减1,完成它的P操作 94 | 95 | P和和V的定义确保了一个运行程序绝不可能进入这样一种状态,也就是一个正确初始化了的信 号量有一个负值。这个属性称为信号量不变性( semaphore invariant),为控制并发程序的轨线而避 免不安全区提供了强有力的工具。 96 | 97 | 基本的思想是将每个共享变量(或者相关共享变量集合)与一个信号量s(初始为1)联系起来, 然后用P(s)和W(s)操作将相应的临界区包围起来。以这种方式来保护共享变量的信号量叫做二进制 信号量( binary semaphore),因为它的值总是0或者1。 98 | 99 | ### 利用信号量来调度共享资源 100 | 101 | 信号量另一个重要的作用是调度对共享资源的访问,一个线程用信号量来通知另一个线程,程序状态中的某个量已经为真了。 102 | 103 | ![enter description here][114] 104 | 105 | ## 其他并发性问题 106 | 107 | ### 线程安全 108 | 109 | 当我们用线程编写程序时,我们必须小心地编写那些具有称为线程安全性( thread safety)属性 的函数。一个函数被称为线程安全的( thread- safe),当且仅当被**多个并发线程反复地调用时,它会 一直产生正确的结果**。如果一个函数不是线程安全的,我们就说它是线程不安全的( thread-unsafe)。 我们能够定义出四类(有相交的)线程不安全函数: 110 | * 不保护共享变量的函数 111 | * 保持跨越多个调用的状态的函数 112 | * 返回指向静态变量的指针函数 113 | * 调用线程不安全函数的函数 114 | 115 | ### 可重入性 116 | 117 | 有一类重要的线程安全函数,叫做**可重入函数**( reentrant function),其特点在于它们具有这样 种属性:**当它们被多个线程调用时,不会引用任何共享数据。** 118 | 119 | ![enter description here][115] 120 | 121 | ### 竞争 122 | 123 | 当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时, 就会发生**竞争**(race)。通常发生竟争是因为程序员假定线程将按照某种特殊的轨线穿过执行状态空 间,而忘记了另一条准则规定定:多线程程序必须对任何可行的轨线都正确工作。 124 | 125 | ### 死锁 126 | 127 | 一组线程被阻塞了,等待一个永远也不会为真的条件 128 | 129 | ![enter description here][116] 130 | 131 | ## 小结 132 | 133 | 无论哪种并发机制,同步对于共享数据的并发访问都是一个困难的问题。提出对信号的 P 和 V 操作就是为了帮助解决这个问题。信号量操作可以用来提供对共享数据的互斥访问,也对诸如生产者-消费者程序中有限缓冲区和读者-写者系统中的共享对象这样的资源访问进行调度。 134 | 135 | 并发也引入了其他一些困难的问题。被线程调用的函数必须具有一种称为线程安全的属性。竞争和死锁是并发程序中出现的另一些困难的问题。当程序员错误地假设逻辑流该如何调度时,就会发生竞争。当一个流等待一个永远不会发生的事件时,就会产生死锁。 136 | 137 | [110]: https://data2.liuin.cn/story-writer/2018_2_3_1517653797354.jpg 138 | [111]: https://data2.liuin.cn/story-writer/2018_2_3_1517654269007.jpg 139 | [112]: https://data2.liuin.cn/story-writer/2018_2_3_1517654474783.jpg 140 | [113]: https://data2.liuin.cn/story-writer/2018_2_3_1517654599444.jpg 141 | [114]: https://data2.liuin.cn/story-writer/2018_2_3_1517654937600.jpg 142 | [115]: https://data2.liuin.cn/story-writer/2018_2_3_1517655306363.jpg 143 | [116]: https://data2.liuin.cn/story-writer/2018_2_3_1517655428227.jpg 144 | -------------------------------------------------------------------------------- /notes/CSAPP-异常控制流.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 异常控制流 3 | date: 2018-01-30 17:01:24 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第8章笔记 9 | 10 | 11 | 12 | 处理器按照一定的序列的地址执行对应的指令,从这一个地址过渡到下一个地址成为控制转移,这样的控制转移序序列称为处理器的控制流(flow control) 13 | 14 | 系统必须能够对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定要和程序的执行相关。比如,一个硬件定时器定期产生信号,这个事件必须得到处理。当子进程终止时,创造这些子进程的父进程必须得到通知。 15 | 16 | 线代系统通过使控制流发生突变来对这些情况做出反应。一般而言,我们把这些突变称为**异常控制流**(Exceptional Control Flow, ECF)。异常控制流发生在计算机系统的各个层次。比如,在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序。在操作系统层,内核通过上下文转换将控制从一个用户进程转移到另一个用户进程。在应用层,一个进程可以发送信号到另一个进程,而接受者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应。 17 | 18 | ## 异常 19 | 20 | 异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。因为它们有一部分是由硬件实现的,所以具体细节将随系统的不同而有所不同。然而,对于每个系统而言,基本的思想都是相同的。 21 | 22 | 异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。如下图所示: 23 | 24 | ![enter description here][61] 25 | 26 | 在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做**异常表**(exception table)的跳转表,进行一个间接过程调用(异常),到一個专门设计用来处理这类事件的操作系统子程序(异常处理程序, exception handler) 27 | 28 | 当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种: 29 | * 处理程序将控制返回给当前指令 I(curr),即当事件发生时正在执行的指令。 30 | * 处理程序将控制返回给 I(next),即如果没有发生异常将会执行的下一条指令。 31 | * 处理程序被中断的程序 32 | 33 | ### 异常处理 34 | 35 | 系统中可能的每种类型的异常都分配了一个唯一的非负整数的**异常号**(exception number)。其中一些号码是由处理器的设计者分配的,其他号码是由操作系统内核的设计者分配的。前者的示例包括被零除、缺页、存储器访问违例以及算术溢出。后者的示例包括系统调用和来自外部 I/O 设备的信号。 36 | 37 | 在系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得条目 k 包含异常 k 的处理程序的地址。 38 | 39 | ![enter description here][62] 40 | 41 | 异常号是到异常表中的索引,异常表的起始地址放在一个叫做**异常表基址寄存器**(exception table base register)的特殊 CPU 寄存器里。 42 | 43 | ![enter description here][63] 44 | 45 | ### 异常的类别 46 | 47 | 异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort) 48 | 49 | ![enter description here][64] 50 | 51 | > 中断 52 | 53 | 中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序通常称为中断处理程序(interrupt handler) 54 | 55 | ![enter description here][65] 56 | 57 | 剩下的异常类型(陷阱、故障和终止)是同步发生的,是执行当前指令的结果。我们把这类指令叫做故障指令(faulting instruction)。 58 | 59 | > 陷阱 60 | 61 | 陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做**系统调用**。 62 | 63 | ![enter description here][66] 64 | 65 | > 故障 66 | 67 | 故障由错误情况引起,它可能被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序 68 | 69 | ![enter description here][67] 70 | 71 | 一个经典的故障示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在存储器中,因此必须从磁盘中取出时,就会发生故障。就像我们将在第 9 章中看到的那样,一个页面就是虚拟存储器的一个连续的块。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在存储器中了,指令就可以没有故障地运行完成了。 72 | 73 | > 终止 74 | 75 | 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM 或者 SRAM 位被损坏时发生的奇偶错误。终止程序从不将控制返回给应用程序。 76 | 77 | ![enter description here][68] 78 | 79 | ### Intel处理器中的异常 80 | 81 | ![enter description here][69] 82 | 83 | ## 进程 84 | 85 | 异常是允许操作系统提供**进程**(process)的概念所需要的基本构造块,进程是计算机可续重最深刻最成功的概念之一。当我们在一个现代系统上运行一个程序时,会得到一个假象,就好像我们的程序是系统中当前运行着的唯一的程序。 86 | 87 | 进程的经典定义就是一个**执行中的程序的实例**。系统中的每个程序都是运行在某个进程的**上下文**(context)中的。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。 88 | 89 | 每次用户通过向外壳输入一个可执行目标文件的名字,并运行一个程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,且在这个新进程的上下文中运行它们自己的代码或其他应用程序。 90 | 91 | ### 逻辑控制流 92 | 93 | 即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象的指令。这个 PC 值的序列叫做**逻辑控制流**。 94 | 95 | ![enter description here][70] 96 | 97 | 进程是轮流使用处理器的,每个进程执行它流中的一部分,然后被抢占(preempted)(暂时挂起),与此同时其他进程开始执行。 98 | 99 | ### 并发流 100 | 101 | 一个逻辑流的执行在时间上与另一个流重叠,称为并发流(concurrent flow),这两个流被称为并发地运行。更准确地说,流 X 和 Y 互相并发,当且仅当 X 在 Y 开始之后和 Y 结束之前开始,或者 Y 在 X 开始之后和 X 结束之前开始。 102 | 103 | 多个流并发地执行的一般现象称为**并发**(concurrency)。一个进程和其他进程轮流运行的概念称为**多任务**(multitasking)。一个进程执行它的控制流的一部分的每一时间段叫做**时间片**(time slice)。因此,多任务也叫做时间分片(time slicing) 104 | 105 | 注意,并发的思想与流运行的处理器核数或者计算机无关。如果两个流再时间上重叠,那么它们就是并发的,即使它们是运行在同一个处理器上的。如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它们为并行流(parallel flow)。 106 | 107 | ### 私有地址空间 108 | 109 | 进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。尽管和每个私有地址空间相关联的存储器的内容一般是不同的,但是每个这样的空间都有相同的通用结构,如下图所示。 110 | 111 | ![enter description here][71] 112 | 113 | ### 用户模式和内核模式 114 | 115 | 为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。 116 | 117 | 处理器通常是用某个控制寄存器中的一个**模式位**(mode bit)来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位,进程就运行在内核模式(超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。 118 | 119 | 没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行**特权指令**(priviledged instruction),比如停止处理器、改变位模式,或者发起一个 I/O 操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。 120 | 121 | Linux 提供了一种聪明的机制,叫做 /proc 文件系统,它允许用户模式进程访问内核数据结构的内容。/proc文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。 122 | 123 | ### 上下文切换 124 | 125 | 操作系统内核使用一种称为**上下文切换**(context switch)的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在8.1节中那些较低层异常机制之上的。 126 | 127 | 内核为每个进程维持一个**上下文**(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描绘地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。 128 | 129 | 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做**调度**(schedule),是由内核中称为**调度器**(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们就说内核调度了这个进程。 130 | 131 | ![enter description here][72] 132 | 133 | ## 系统调用和错误处理 134 | 135 | Unix提供大量的系统调用,当应用程序想向内核请求服务时,可以使用这些系统调用。 136 | 137 | 标准C库提供一组针对最常用系统调用的方便的包装(wrapper)函数。 138 | 139 | 通过使用错误处理包装(error-handling wrapper)函数,我们可以进一步简化我们的代码。 140 | 141 | ## 进程控制 142 | 143 | ### 获取进程 144 | 145 | 每个进程都有一个唯一的正数进程 ID(PID)。`getpid` 函数返回调用进程的 PID。`getppid` 函数返回它的父进程的 PID。 146 | 147 | ``` cpp 148 | #include 149 | #include 150 | 151 | pid_t getpid(void); 152 | pit_t getppid(void); 153 | ``` 154 | 155 | ### 创建和终止进程 156 | 157 | 从程序员的角度,我们可以认为进程总是处于下面三种状态之一: 158 | * 运行。进程要么在 CPU 上执行,要么在等待被执行且最终会被内核调度。 159 | * 停止。进程的执行被挂起(suspend),且不会被调度。当收到 SIGSTOP、SIGTSTP、SIDTTIN 或者 SIGTTOU 信号时,进程就停止,并且保持停止直到它收到一个 SIGCONT 信号,在这个时刻,进程再次开始运行。 160 | * 终止。进程永远地停止了。进程会因为三种原因终止:1)收到一个默认行为是终止进程的信号,2)从主程序返回,3)调用 exit 函数 161 | 162 | 该程序无返回值,`exit` 函数以 status 退出来终止进程。 163 | 164 | ``` cpp 165 | #include 166 | 167 | void exit(int status); 168 | ``` 169 | 170 | 父进程通过调用 `fork` 函数创建一个新的运行子进程,子进程返回0,父进程返回子进程的 PID,如果出错则为 -1。 171 | 172 | ``` cpp 173 | #include 174 | #include 175 | 176 | pid_t fork(void); 177 | ``` 178 | 179 | 新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和 bss 段、以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。父进程和新创建的子进程最大的区别在于他们有不同的 PID。 180 | 181 | `fork` 函数只被调用一次,却会返回两次(父进程与子进程)。因为子进程的 PID 总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。 182 | 183 | 使用`fork`创建一个新进程: 184 | ``` cpp 185 | 1 #include "csapp.h" 186 | 2 187 | 3 int main() 188 | 4 { 189 | 5 pid_t pid; 190 | 6 int x = 1; 191 | 7 192 | 8 pid = Fork(); 193 | 9 if (pid == 0) { /* Child */ 194 | 10 printf("child : x=%d\n", ++x); 195 | 11 exit(0); 196 | 12 } 197 | 13 198 | 14 /* Parent */ 199 | 15 printf("parent: x=%d\n", --x); 200 | 16 exit(0); 201 | 17 } 202 | ``` 203 | 204 | 这个例子有一些微妙的方面: 205 | * 调用一次,返回两次 206 | * 并发执行。顺序不能保证 207 | * 相同但是独立的地址空间,所以变量是分别独立的 208 | * 共享文件,输出是指向同一个地方 209 | 210 | ### 回收子进程 211 | 212 | 当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一中已终止的状态中,直到被它的父进程回收(reap)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个终止了但还未被回收的进程称为**僵死进程**(zombie)。 213 | 214 | 如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排 init 进程来回收它们。init 进程的 PID 为 1,并且是在系统初始化时由内核创建的。长时间运行的程序,比如 shell 或者服务器,总是应该回收它们的僵死子进程。即使僵死子进程没有运行,它们仍然小号系统的存储器资源。 215 | 216 | 一个进程可以通过调用 `waitpid` 函数来等待它的子进程终止或者停止。如果成功,则返回子进程的 PID,如果 WHOHANG ,则为 0,如果其他错误,则为 -1。 217 | 218 | ### 让进程休眠 219 | 220 | `sleep` 函数让一个进程挂起一段指定的时间。返回还要休眠的秒数。 221 | 222 | 如果请求的时间量已经到了,`sleep`返回 0,否则返回还剩下要休眠的秒数。我们会发现很有用的另一个函数是 `pause` 函数,该函数让调用函数休眠,直到该进程收到一个信号。总是返回 -1。 223 | 224 | ### 加载并运行程序 225 | 226 | `execve` 函数在当前进程的上下文中加载并运行一个新程序。如果成功则不返回,如果错误,则返回 -1。 227 | 228 | ``` cpp 229 | #include 230 | 231 | int execve(const char *filename, const char *argv[], const char *envp[]); 232 | ``` 233 | 234 | ## 信号 235 | 236 | 一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。 237 | 238 | ![enter description here][73] 239 | 240 | ### 信号术语 241 | 242 | 传送一个信号到目的进程是由两个不同步骤组成的: 243 | 244 | * **发送信号**。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号可以用如下两个原因:1)内核检测到一个系统事件,比如被零除错误或者子进程终止。2)一个进程调用 kill 函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。 245 | * **接收信号**。当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。如下图所示 246 | 247 | 248 | 一个只发出而没有被接收的信号叫做待处理信号(pending signal)。在任何时刻,一种类型至多只会有一个待处理信号。如果一个进程有一个类型为 k 的待处理信号,那么任何接下来发送到这个进程的类型为 k 的信号都不会排队等待,它们只是被简单地丢弃。一个进程可以有选择地阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。 249 | 250 | 一个待处理信号最多只能被接收一次。内核为每个进程在 pending 位向量中维护着待处理信号的集合,而在 blocked 位向量中维护着被阻塞的信号集合。只要传送了一个类型为 k 的信号,内核就会设置 pending 中的第 k 位,而只要接收了一个类型为 k 的信号,内核就会清除 pending 中的第 k 位。 251 | 252 | ### 发送信号 253 | 254 | Unix 系统提供了大量向进程发送信号的机制。所有这些机制都是基于进程组(process group)这个概念的。 255 | 256 | 进程组:每个进程都只属于一个进程组,进程组是由一个正整数进程组 ID 来标识的。`getpgrp` 函数返回当前进程的进程组 ID。 257 | 258 | 默认的,一个子进程和它的父进程同属一个进程组。一个进程可以通过使用 `setpgid` 函数来改变自己或者其他进程的进程组,成功则返回 0,否则返回 -1。 259 | 260 | 用 /bin/kill 程序发送信号 261 | 262 | 从键盘发送信号 263 | 264 | 用 kill 函数发送信号 265 | 266 | 用 alarm 函数发送信号 267 | 268 | ### 接受信号 269 | 270 | 当内核从一个异常处理程序返回,准备将控制传递给进程 p 时,它会检查进程 p 的未被阻塞的待处理信号的集合(pending&~blocked)。如果这个集合为空(通常情况下),那么内核将控制传递到 p 的逻辑控制流中的下一条指令 271 | 272 | ### 信号处理问题 273 | 274 | 当一个程序要补货多个信号时,一些细微的问题就产生了: 275 | * 待处理信号被阻塞 276 | * 待处理信号不会排队等待 277 | * 系统调用可以被中断 278 | 279 | 不可以用信号来对其他进程中发生的事件计数。 280 | 281 | ### 可移植的信号处理 282 | 283 | 不同系统之间,信号处理语义的差异是 Unix 信号处理的一个缺陷。为了处理这个问题,Posix 标准定义了` sigaction` 函数,它允许用户明确指定他们想要的信号处理语义。 284 | 285 | ### 显式地阻塞信号 286 | 287 | 使用 `sigprocmask` 函数 288 | 289 | ## 非本地跳转 290 | 291 | C 语言提供了一种用户级一场控制流形式,称为**非本地跳转**(nonlocal jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用——返回序列,通过 `setjmp` 和 `longjmp` 函数来提供的。 292 | 293 | 非本地跳转的另一个重要应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。 294 | 295 | ## 操作进程的工具 296 | 297 | Linux 系统提供了大量的监控和操作进程的有用工具: 298 | * STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。用 -static 编译你的程序,能得到一个更干净的、不带有大量与共享库相关的输出的 trace 299 | * PS:列出当前系统中的进程(包括僵死进程) 300 | * TOP:打印出关于当前进程资源使用的信息 301 | * PMAP:显示进程的存储器映射 302 | * /proc:一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容,用户可以读取这些内容 303 | 304 | ## 小结 305 | 306 | 异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。 307 | 308 | 在硬件层,异常是由处理器中的事件触发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。 309 | 310 | 有四种不同类型的异常:中断、故障、终止和陷阱。 311 | 312 | 在操作系统层,内核用 ECF 提供进程的基本概念。进程提供给应用两个重要的抽象:1)逻辑控制流,它提供给每个程序一个假象,好像它是在独占地使用处理器,2)私有地址空间,它提供给每个程序一个假象,好像它是在独占地使用主存。 313 | 314 | 在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待它们的子进程停止或者终止,运行新的程序,以及不活来自其他进程的信号。信号处理的语义是微妙的,并且随着系统不同而不同。然而,在与 Posix 兼容的系统上存在着一些机制,允许程序清楚地指定期望的信号处理语义。 315 | 316 | [61]: https://data2.liuin.cn/story-writer/2018_1_30_1517298145936.jpg 317 | [62]: https://data2.liuin.cn/story-writer/2018_1_30_1517299098759.jpg 318 | [63]: https://data2.liuin.cn/story-writer/2018_1_30_1517299181611.jpg 319 | [64]: https://data2.liuin.cn/story-writer/2018_1_30_1517299300222.jpg 320 | [65]: https://data2.liuin.cn/story-writer/2018_1_30_1517299374198.jpg 321 | [66]: https://data2.liuin.cn/story-writer/2018_1_30_1517299528855.jpg 322 | [67]: https://data2.liuin.cn/story-writer/2018_1_30_1517299611720.jpg 323 | [68]: https://data2.liuin.cn/story-writer/2018_1_30_1517299685578.jpg 324 | [69]: https://data2.liuin.cn/story-writer/2018_1_30_1517299747975.jpg 325 | [70]: https://data2.liuin.cn/story-writer/2018_1_30_1517299971390.jpg 326 | [71]: https://data2.liuin.cn/story-writer/2018_1_30_1517300174690.jpg 327 | [72]: https://data2.liuin.cn/story-writer/2018_1_30_1517300757505.jpg 328 | [73]: https://data2.liuin.cn/story-writer/2018_1_30_1517301875073.jpg 329 | -------------------------------------------------------------------------------- /notes/CSAPP-程序的机器级表示.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 程序的机器级表示 3 | date: 2018-01-25 22:10:07 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第3章笔记 9 | 10 | 11 | 12 | 高级语言通过编译变成汇编语言,汇编代码则与特定的机器密切相关。汇编代码中包含了管理存储器(memory)和执行计算的低级指令的一些细节(写高级程序的人员一般不需要考虑的)。编译器基于编程语言的原则、目标机器的指令集和操作系统遵循的规则,经过一系列的阶段产生机器代码。 13 | 14 | ## 程序编码 15 | 16 | 正如之前所说的,从源代码到机器可执行代码会经过以下几个过程:预处理-> 编译器-> 汇编器 -> 链接器 17 | 18 | ### 机器级代码 19 | 20 | 对于机器级代码来说,有两种抽象非常重要。第一种是机器级程序的格式和行为,定义为**指令集体系结构**(Instruction set architecture, ISA),它定义了处理器状态、指令的格式,以及每条指令对状态的影响。第二种抽象是,机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。 21 | 22 | 汇编代码和原始的C代码相差比较大,一些通常对C语言程序员隐蔽的处理器状态是可见的: 23 | * 程序计数器(PC,用 %eip 表示)指示将要执行的下一条指令在存储器中的地址。 24 | * 整数寄存器文件包含 8 个命名的位置,分别存储 32 位的值。这些寄存器可以存储地址(对应于 C 语言的指针)或证书数据。有的寄存器被用来记录某些重要的程序状态,而其他的寄存器则用来保存临时数据。 25 | * 条件码(codition code)寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化。 26 | * 一组浮点寄存器存放浮点数据。 27 | 28 | C语言中的聚焦数据类型,例如数组和结构,在汇编中是用连续的字节表示的。汇编代码不区分有符号或无符号整数,不区分各种类型的指针,甚至不区分指针和整数。 29 | 30 | 程序存储器(program memory)包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块。同时OS负责管理虚拟地址空间,将虚拟地址转换为物理地址。 31 | 32 | 一条指令只执行一个非常基本的操作。例如,将存放在寄存器中的两个数字相加,在存储器和寄存器之间传送数据,或是条件分支转移到新的指令地址。编译器必须产生这些指令的序列,从而实现(像算术表达式求值、循环或过程调用和返回这样的)程序结构。 33 | 34 | ### 关于格式的注解 35 | 36 | 所有以 . 开头的行都是指导汇编器和链接器的命令(对程序的解释),我们通常可以忽略这些行。 37 | 38 | 39 | ## 数据格式 40 | 41 | ![enter description here][14] 42 | 43 | ## 访问信息 44 | 45 | 一个IA32的CPU中有8个32位的寄存器用来存储整数数据和指针,在过程(procedures)处理中,对前三个寄存器(%eax, %ecx, %edx)的保存和恢复惯例不同于接下来的三个寄存器(%ebx, %edi, %esi)。最后两个寄存器(%ebp, %esp)保存着指向程序栈中重要位置的指针。只有根据栈管理的标准惯例才能修改这两个寄存器中的值。 46 | 47 | ### 操作数指示符 48 | 49 | 大多数指令有一个或多个操作数(operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。操作数可能被分为三种类型: 50 | * 立即数(immediate),也就是常数值 51 | * 寄存器(register),表示某个寄存器的内容 52 | * 存储器(memory)引用,它会根据计算出来的地址访问某个存储器位置 53 | 54 | ![enter description here][15] 55 | 56 | ### 数据传送指令 57 | 58 | ![enter description here][16] 59 | 60 | ## 算术和逻辑操作 61 | 62 | 给出的每个指令类都有对字节、字和双字数据进行操作的指令。这些操作被分为四组:加载有效地址、一元操作、二元操作和移位。 63 | 64 | ### 加载有效地址 65 | 66 | 加载有效地址(load effective address)指令 leal 实际上是 movl 指令的变形。它的指令形式是从存储器读数据到寄存器,但实际上它根本就没有引用存储器。它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。 67 | 68 | ![enter description here][17] 69 | 70 | ### 一元和二元操作 71 | 72 | 一元操作:一个操作数既是源又是目的 73 | 74 | 二元操作:第二个操作数既是源又是目的 75 | 76 | ### 移位操作 77 | 78 | 先给出移位的量,然后是待移位的值 79 | 80 | ## 控制 81 | 82 | ### 条件码 83 | 84 | CPU 维护着一组单个 bit 的条件码(condition code) 寄存器,他们描述了最近的算术或逻辑操作的属性 85 | 86 | ![enter description here][18] 87 | 88 | ![enter description here][19] 89 | 90 | ### 访问条件码 91 | 92 | 两种最常见的访问条件码的方法不是直接读取,常用的使用方法有三种: 93 | * 可以根据条件码的某个组合,将一个字节设置为 0 或者 1 94 | * 可以条件跳转到程序的某个其他的部分 95 | * 可以有条件地传送数据 96 | 97 | ![enter description here][20] 98 | 99 | ### 跳转指令和他们的编码 100 | 101 | 跳转指令会导致执行切换到程序中的一个全新的位置 102 | 103 | ![enter description here][21] 104 | 105 | ### 翻译条件分支 106 | 107 | 将条件表达式和语句从 C 语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转 108 | 109 | ![enter description here][22] 110 | 111 | ### 循环 112 | 113 | 汇编中没有相应的循环指令,将条件测试和跳转组合起来可以实现循环的效果 114 | 115 | ![enter description here][23] 116 | 117 | ### switch语句 118 | 119 | 通过一种称为跳转表(jump table)的数据结构使得实现更加高效,相比使用一组很长的if-else语句,使用跳转表的优点是执行开关语句的时间和开关情况(switch cases)的数量无关。 120 | 121 | 一般在开关情况数量比较多,并且值的范围跨度比较小的时候使用跳转表 122 | 123 | ## 过程 124 | 125 | 一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。大多数机器,包括 IA32,只提供转移控制到过程和从过程转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。 126 | 127 | ### 栈帧结构 128 | 129 | IA32 程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后回复,以及本地存储。为单个过程分配的那部分栈称为**栈帧**(stack frame)。 130 | 131 | ![enter description here][24] 132 | 133 | ### 转移控制 134 | 135 | ![enter description here][25] 136 | 137 | ### 寄存器使用惯例 138 | 139 | 程序寄存器组是唯一能够被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程调用另一个过程时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。 140 | 141 | 根据惯例,寄存器 %eax、%edx、%ecx 被划分为**调用者保存寄存器**(caller save)。当过程 P 调用 Q 时,Q 可以覆盖这些寄存器,而不会破坏任何 P 所需要的数据。另一方面, 寄存器 %ebx、%esi、%edi 被划分为**被调用者保存寄存器**(callee save)。 142 | 143 | ## 数组的分配和访问 144 | 145 | C 语言一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算。 146 | 147 | 优化编译器非常善于简化数组索引所使用的地址计算。不过这使得 C 代码和它机器代码的翻译之间的对应关系有些难以理解。 148 | 149 | ### 指针运算 150 | 151 | C 语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。也就是说,如果 p 是一个指向类型为 T 的数据的指针,p 的值为 xp,那么表达式 p+i 的值为 xp+L\*i,这里 L 是数据类型 T 的大小。 152 | 153 | ### 数组与循环 154 | 155 | 在循环代码中,对数组的引用通常有非常规则的模式,优化编译器会使用这些模式 156 | 157 | ### 嵌套数组 158 | 159 | > int A[5][3]; 160 | 161 | 等价于下面的声明 162 | 163 | > typedef int row3_t[3]; 164 | > row3_t A[5]; 165 | 166 | ### 固定大小的数组 167 | 168 | ### 动态分配的数组 169 | 170 | ## 异类的数据结构 171 | 172 | ### 结构(structure) 173 | 将可能不同类型的对象聚合到一个对象中。结构的各个组成部分用名字来引用。类似于数组的实现,结构的所有组成部分都存放在存储器中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段(field)的字节偏移。它以这些偏移作为存储器引用指令中的位移,从而产生对结构元素的引用。 174 | 175 | ### 联合(union) 176 | 177 | 提供了一种方式,能够规避 C 语言的类型系统,允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差比较大。它们是用不同的字段来引用相同的存储器块。 178 | 179 | ## 对齐(alignment) 180 | 许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值 K(通常是 2、4、8)。这种对齐限制简化了形成处理器和存储器系统之间接口的硬件设计 181 | 182 | ## 综合:理解指针 183 | 184 | 指针是 C 语言的一个重要特征。它们以一种统一方式,对不同数据结构中的元素产生引用。这里介绍一些指针和它们映射到机器代码的关键原则: 185 | * 每个指针都对应一个类型。这个类型表明指针指向哪一类对象。 186 | * 每个指针都有一个值。这个值是某个指定类型对象的地址。特殊的 NULL(0) 值表示该指针没有指向任何地方 187 | * 指针用 & 运算符创建。这个运算符可以应用到任何 lvalue 类的 C 表达式上。 188 | * 操作符用于指针的间接引用。其结果是一个值,它的类型与该指针的类型相关。间接引用是通过存储器引用来实现的,要么是存储到一个指定的地址,要么是从指定的地址读取。 189 | * 数组与指针紧密联系。一个数组的名字可以像一个指针变量一样引用(但是不能修改)。数组引用与指针运算和间接引用有一样的效果。数组引用和指针运算都需要用对象大小对偏移量进行伸缩。 190 | * 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。强制类型转换的一个效果是改变指针运算的伸缩。来看一个例子,如果 p 是一个 char* 类型的指针,那么表达式(int)p+7 计算为 p+28, 而(int)(p+7)计算为 p+7。 191 | * 指针也可以指向函数。这提供了一个很强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用。 192 | 193 | ## 存储器的越界引用和缓冲区溢出 194 | 195 | C 对于数组引用不进行任何边界检查,而局部变量和状态信息,都存放在栈中。这两种情况结合到一起就可能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行 ret 指令时,就会出现很严重的错误。 196 | 197 | 缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输入和程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码(exploit code),另外还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么执行 ret 指令的效果就是跳转到攻击代码。 198 | 199 | 一种攻击形式,攻击代码会使用系统调用启动一个外壳程序,给攻击者提供一组操作系统函数。另一种攻击形式是,攻击代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行 ret 指令,(表面上)正常返回给调用者。 200 | 201 | ## 小结 202 | 203 | 机器级程序和它们的汇编代码表示,与 C 程序的差别很大。在汇编语言程序中,各种数据类型之间的差别很小。程序是以指令序列来表示的,每条指令都完成一个单独的操作。部分程序状态,如寄存器和运行时栈,对程序员来说是直接可见的。 204 | 205 | C 语言中缺乏边界检查,使得许多程序容易出现缓冲区溢出。虽然最近的运行时系统提供了安全保护,而且编译器帮助使得程序更加安全,但是这已经使许多系统容易收到入侵者的恶意攻击。 206 | 207 | 208 | [14]: https://data2.liuin.cn/story-writer/2018_1_25_1516882675143.jpg 209 | [15]: https://data2.liuin.cn/story-writer/2018_1_25_1516883245706.jpg 210 | [16]: https://data2.liuin.cn/story-writer/2018_1_25_1516883311416.jpg 211 | [17]: https://data2.liuin.cn/story-writer/2018_1_25_1516883471796.jpg 212 | [18]: https://data2.liuin.cn/story-writer/2018_1_25_1516883691695.jpg 213 | [19]: https://data2.liuin.cn/story-writer/2018_1_25_1516883745587.jpg 214 | [20]: https://data2.liuin.cn/story-writer/2018_1_25_1516883932824.jpg 215 | [21]: https://data2.liuin.cn/story-writer/2018_1_25_1516884044007.jpg 216 | [22]: https://data2.liuin.cn/story-writer/2018_1_25_1516884153857.jpg 217 | [23]: https://data2.liuin.cn/story-writer/2018_1_25_1516884258395.jpg 218 | [24]: https://data2.liuin.cn/story-writer/2018_1_25_1516884747521.jpg 219 | [25]: https://data2.liuin.cn/story-writer/2018_1_25_1516884832280.jpg 220 | -------------------------------------------------------------------------------- /notes/CSAPP-系统级I-O.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 系统级I/O 3 | date: 2018-02-01 18:19:48 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第10章笔记 9 | 10 | 11 | 12 | 输入/输出(I/O)是在主存(memory)和外部设备之间拷贝数据的过程。输入数据是从I/O设备拷贝数据到主存,输出数据是从主存拷贝数据到I/O设备。 13 | 14 | ## Unix I/O 15 | 16 | 所有的的I/O设备都模型化为文件,而所有的输入和输出都当作相应文件的读和写来执行。 17 | 18 | 所有的输入输出都以一种统一且一致的方式来执行: 19 | * 打开文件 20 | * 改变当前的文件位置 21 | * 读写文件 22 | * 关闭文件 23 | 24 | ## 打开和关闭文件 25 | 26 | open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符 27 | 28 | ## 读和写文件 29 | 30 | ![enter description here][94] 31 | 32 | `read`函数从描述符为fd的当前文件位置拷贝至多n个字节到存储器位置buf。 33 | 34 | `write` 函数从存储器位置拷贝至多n个字节到描述符fd的当前文件位置 35 | 36 | 通过调用`lseek`函数,应用程序能够显式地修改当前文件的位置 37 | 38 | ## 共享文件 39 | 40 | 可以用许多不同的方式来共享 Unix 文件。内核用三个相关的数据结构来表示打开的文件: 41 | * **描述符表**(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表指向文件表中的一个表项。 42 | * **文件表**(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项包括当前的文件位置、引用计数(reference count),以及一个指向 v-node 表中对应表项的指针。 43 | * **v-node 表**(v-node table)。同文件表一样,所有的进程共享这张表。每个表项包含 stat 结构中的大多数信息。 44 | 45 | ![典型的打开文件的内核数据结构][95] 46 | 47 | 多个描述符也可以通过不同的文件表表项来引用同一个文件 48 | 49 | ![enter description here][96] 50 | 51 | 子进程继承父进程打开文件 52 | 53 | ![enter description here][97] 54 | 55 | ## I/O重定向 56 | 57 | ![enter description here][98] 58 | 59 | ## 我们该使用哪些I/O函数 60 | 61 | ![enter description here][99] 62 | 63 | ## 小结 64 | 65 | Unix 提供了少量的系统级函数,它们允许应用程序打开、关闭、读和写文件,提取文件的元数据,以及执行 I/O 重定向。 Unix 的读和写操作会出现不足值,应用程序必须能正确地预计和处理这种情况。应用程序不应直接调用 Unix I/O 函数,而应该使用 RIO 包,RIO 包通过反复执行读写操作,直到传送完所有的请求数据,自动处理不足值。 66 | 67 | Unix 内核使用三个相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向 v-node 表中的表项。 68 | 69 | 标准 I/O 库是基于 Unix I/O 实现的,并提供了一组强大的高级 I/O 例程。对于大多数应用程序而言,标准 I/O 更简单,是优于 Unix I/O 的选择。然而,因为对标准 I/O 和网络文件的一些相互不兼容的限制,Unix I/O 比标准 I/O 更适用于网络应用程序。 70 | 71 | 72 | [94]: https://data2.liuin.cn/story-writer/2018_2_1_1517479720893.jpg 73 | [95]: https://data2.liuin.cn/story-writer/2018_2_1_1517479945102.jpg 74 | [96]: https://data2.liuin.cn/story-writer/2018_2_1_1517480013346.jpg 75 | [97]: https://data2.liuin.cn/story-writer/2018_2_1_1517480058326.jpg 76 | [98]: https://data2.liuin.cn/story-writer/2018_2_1_1517480099251.jpg 77 | [99]: https://data2.liuin.cn/story-writer/2018_2_1_1517480139110.jpg 78 | -------------------------------------------------------------------------------- /notes/CSAPP-网络编程.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 网络编程 3 | date: 2018-02-02 11:27:46 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第11章笔记 9 | 10 | 11 | 12 | 网络应用随处可见。有趣的是,所有的网络应用都是基于相同的基本编程模型,有着相似的整体逻辑结构,并且一来相同的编程接口。 13 | 14 | 网络应用依赖于很多在系统研究正已经学习过的概念,例如,进程、信号、字节顺序、存储器映射以及动态存储分配,都扮演着重要的角色。 15 | 16 | ## 客户端-服务器编程模型 17 | 18 | 每一个网络应用都是基于`客户端-服务器模型`的,采用这个模型,一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。 19 | 20 | 客户端-服务器模型中的基本操作时**事务(ttansaction)**。 21 | 22 | ![enter description here][100] 23 | 24 | 认识到客户端和服务器是进程,而不是机器或者主机,这一点很重要。 25 | 26 | ## 网络 27 | 28 | 客户端和服务器通常运行在不同的主机上,并且通过**计算机网络**的硬件和软件资源来通信。对于一台主机而言,网络只是又一种 I/O 设备,作为数据源和数据接收方,如下图所示。 29 | 30 | ![enter description here][101] 31 | 32 | 物理上而言,网络是一个按照地理远近组成的层次系统。最底层是 LAN(Local Area Network, 局域网),在一个建筑或者校园范围内。迄今为止,最流行的局域网技术是以太网(Ethernet)。 33 | 34 | 每个以太网适配器都有一个全球唯一的 48 位地址,一台主机可以发送一段位,称为帧(frame),到这个网段内的其他任何主机。每个帧包括一些固定数量的头部(header)位,用来标识此帧的源和目的地址以及此帧的长度,伺候紧随的就是数据位的有效载荷。每个主机适配器都能看到这个帧,但是只有目的主机实际读取它。 35 | 36 | 使用一些电缆和叫做网桥(bridge)的小盒子,多个以太网段可以连接成较大的局域网,称为**桥接以太网**(bridged Ethernet),如下图所示: 37 | 38 | ![enter description here][102] 39 | 40 | 在层次更高的级别中,多个不兼容的局域网可以通过叫做**路由器**(router)的忒书计算机连接起来,组成一个internet(互联网络)。 41 | 42 | ![enter description here][103] 43 | 44 | internet(互联网络)至关重要的特性是,它能采用完全不同和不兼容技术的各种局域网和广域网组成。 45 | 46 | 在互联网中,数据是如何从一台主机传送到另一台主机的: 47 | 48 | ![enter description here][104] 49 | 50 | ## 全球IP因特网 51 | 52 | 协议软件消除了不同网络之间的差异,必须具备两种基本能力:命名机制和传送机制。 53 | 54 | 每台因特网主机都运行实现 **TCP/IP 协议**(Transmission Control Protocol/Internet Protocol)的软件,几乎每个现代计算机系统都支持这个协议。 55 | 56 | ![enter description here][105] 57 | 58 | TCP/IP 实际上是一个协议族,其中每一个都提供不同的功能。从程序员角度,我们可以把因特网看做一个世界范围的主机集合,满足以下特性: 59 | 60 | * 主机集合被映射为一组 32 位的 IP 地址 61 | * 这组 IP 地址被映射为一组称为因特网域名(Internet domain name)的标识符 62 | * 因特网主机上的进程能够通过连接(connection)和任何其他因特网主机上的进程通信 63 | 64 | ### IP 地址 65 | 66 | 一个 IP 地址就是一个 32 位无符号整数。IP 地址通常是以一种称为点分十进制表示法来表示的,这里,每个字节由它的十进制值表示,并且用句点和其他字节间分开。 67 | 68 | ### 因特网域名 69 | 70 | 域名集合形成了一个层次结构,每个域名编码了它在这个层次中的位置。 71 | 72 | ![enter description here][106] 73 | 74 | 因特网定义了域名集合和IP地址集合之间的映射,这个映射是通过分布世界范围内的数据库——DNS(域名系统)来维护的。 75 | 76 | ### 因特网连接 77 | 78 | 因特网客户端和服务器通过在连接上发送和接收字节流来通信。从连接一对进程的意义上而言,连接是点对点的。从数据可以同时双向流动的角度来说,它是全双工的。并且由源进程发出的字节流最终被目的进程以它发出的顺序收到它的角度来说,它是可靠的。 79 | 80 | **一个套接字是连接的一个端点**。每个套接字都有相应的套接字地址,是由一个因特网地址和一个 16 位的整数端口组成的,用地址:端口来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口(ephemeral port)。然而,服务器套接字地址中的端口通常是某个知名的端口,是和这个服务相对应的。例如,Web 服务器通常使用端口 80,而电子邮件服务器使用端口 25。在 Unix 机器上,文件 /etc/services 包含一张这台机器提供的服务以及它们的知名端口号的综合列表。 81 | 82 | 一个连接由它两端的套接字地址唯一确定。这对套接字地址叫做套接字对(socket pair),由下列元组来表示: 83 | 84 | ![enter description here][107] 85 | 86 | ## 套接字接口 87 | 88 | **套接字接口**(socket interface)是一组函数,它们和 Unix I/O 函数结合起来,用以创建网络应用。 89 | 90 | ![enter description here][108] 91 | 92 | ## Web服务器 93 | 94 | ### Web基础 95 | 96 | Web 客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做 HTTP(Hypertext Transfer Protocol)。HTTP 是一个简单的协议。一个 Web客户端打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。 97 | 98 | ### Web内容 99 | 100 | 对于 Web 客户端和服务器而言,内容是一个与 MIME(Multipurpose Internet Mail Extensions)类型相关的字节序列。 101 | 102 | ![enter description here][109] 103 | 104 | Web 服务器以两种不同的方式向客户端提供内容: 105 | * 取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容(static content),而返回文件给客户端的过程称为服务静态内容(serving static content)。 106 | * 运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为动态内容(dynamic content),而运行程序并返回它的输出到客户端的过程称为服务动态内容(serving dynamic content)。 107 | 108 | 每条由 Web 服务器返回的内容都是和它管理的某个文件相关联的。这些文件中的每一个都有一个唯一的名字,叫做 URL(Universal Resource Locator)。 109 | 110 | 关于服务器如何解释一个 URL 的后缀,以下几点需要理解: 111 | * 确定一个 URL 指向的是静态内容还是动态内容没有标准的规则。每个服务器对它所管理的文件都有自己的规则。一种常见方法是,确定一组目录,例如 cgi-bin,所有的可执行文件都必须存放这些目录中。 112 | * 后缀中的最开始的那个 / 不表示 Unix 的根目录。相反,它表示的是被请求内容类型的主目录。例如,可以将一个服务器配置成这样:所有的静态内容存放在目录 /usr/httpd/html 下。 113 | * 最小的 URL 后缀是 / 字符,所有服务器将其扩展为某个默认的主页,例如 /index.html。这解释了为什么在浏览器中键入一个域名就可以取出一个网站的主页。浏览器在 URL 后添加缺失的 /,之后服务器把 / 扩展到某个默认的文件名。 114 | 115 | ### HTTP事务 116 | 117 | 因为 HTTP 是基于在因特网连接上传送的文本行的,我们可以使用 Unix 的 TELNET 程序来和因特网上的任何 Web 服务器执行事务。 118 | 119 | ## 小结 120 | 121 | Web 服务器使用 HTTP 协议和它们的客户端彼此通信。浏览器向服务器请求静态或者动态内容。CGI 标准提供了一组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其他信息传递给子进程,以及子进程如何将它的输出发送会客户端 122 | 123 | [100]: https://data2.liuin.cn/story-writer/2018_2_2_1517540316932.jpg 124 | [101]: https://data2.liuin.cn/story-writer/2018_2_2_1517540421619.jpg 125 | [102]: https://data2.liuin.cn/story-writer/2018_2_2_1517540470497.jpg 126 | [103]: https://data2.liuin.cn/story-writer/2018_2_2_1517540536223.jpg 127 | [104]: https://data2.liuin.cn/story-writer/2018_2_2_1517540792401.jpg 128 | [105]: https://data2.liuin.cn/story-writer/2018_2_2_1517540858886.jpg 129 | [106]: https://data2.liuin.cn/story-writer/2018_2_2_1517540938762.jpg 130 | [107]: https://data2.liuin.cn/story-writer/2018_2_2_1517541075440.jpg 131 | [108]: https://data2.liuin.cn/story-writer/2018_2_2_1517541130564.jpg 132 | [109]: https://data2.liuin.cn/story-writer/2018_2_2_1517541249812.jpg 133 | -------------------------------------------------------------------------------- /notes/CSAPP-虚拟存储器.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 虚拟存储器 3 | date: 2018-01-31 21:51:20 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第9章笔记 9 | 10 | 11 | 12 | 一个系统中的进程是与其他进程共享 CPU 和主存资源的。然而,共享主存会形成一些特殊的挑战。随着对 CPU 需求的增长,进程以某种合理的平滑方式慢了下来。但是如果太多的进程需要太多的存储器,那么它们中的一些就根本无法运行。当一个程序没有空间可用时,那就是它运气不好了。存储器还很容易被破坏。如果某个进程不小心写了另一个进程使用的存储器,它就可能以某种完全和程序逻辑无关的令人迷惑的方式失败。 13 | 14 | 为了更加有效地管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做**虚拟存储器**(VM)。虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟存储器提供了三个重要的能力:1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。2)它为每个进程提供了一致的地址空间,从而简化了存储器管理。3)它保护了每个进程的地址空间不被其他进程破坏。 15 | 16 | 虚拟存储器是计算机系统最重要的概念之一。它成功的一个主要原因就是因为它是沉默地、自动地工作的,不需要应用程序员的任何干涉。 17 | 18 | ## 物理和虚拟寻址 19 | 20 | 计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的**物理地址**(Physical Address, PA)。第一个字节的地址为 0,接下来的字节地址为 1,再下一个为 2,以此类推。给定这种简单的结构,CPU 访问存储器的最自然的方式就是使用物理地址。我们把这种方式称为**物理寻址**(physical addressing)。 21 | 22 | ![enter description here][74] 23 | 24 | 现代处理器使用的是一种称为**虚拟寻址**(virtual addressing)的寻址形式 25 | 26 | ![enter description here][75] 27 | 28 | ## 地址空间 29 | 30 | **地址空间**(address space)是一个非负整数地址的有序集合:{0, 1, 2, …}。如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space)。 31 | 32 | 地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。 33 | 34 | 允许每个数据对象有多个独立的地址,其中每一个地址都选自一个不同的地址空间。这就是虚拟存储器的基本思想 35 | 36 | ## 虚拟存储器作为缓存的工具 37 | 38 | 概念上而言,虚拟存储器(VM)被组织为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的。VM 系统通过将虚拟存储器分割为虚拟页(Virtual Page, VP)的大小固定的块来处理这个问题。每个虚拟页的大小为 P=2^p 字节。类似地,物理存储器被分割为物理页(Physical Page, PP),大小也为 P 字节(物理页也称为页帧(page frame))。 39 | 40 | 在任意时刻,虚拟页面的集合部分都分为三个不相交的子集: 41 | * 未分配的:VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。 42 | * 缓存的:当前缓存在物理存储器中的已分配页。 43 | * 未缓存的:没有缓存在物理存储器中的已分配页。 44 | 45 | ![enter description here][76] 46 | 47 | ### DRAM高速缓存的组织结构 48 | 49 | 在存储层次结构中,DRAM缓存的位置对于他的组织结构有很大的影响。DRAM 缓存的组织结构完全是由巨大的不命中开销驱动的。因为大的不命中处罚和访问第一字节的开销,虚拟页往往很大,典型地是4KB-2MB。由于大的不命中处罚,DRAM 缓存是全相连的,也就是说,任何虚拟页都可以放置在任何的物理页中。不命中时的替换策略也很重要,因为替换错了虚拟页的出发也非常高。因此,与硬件对 SRAM 缓存相比,操作系统对 DRAM 缓存使用了更复杂精密的替换算法。最后,因为对磁盘的访问时间很长,DRAM 缓存总是使用写回(write back),而不是直写。 50 | 51 | ### 页表 52 | 53 | 同任何缓存一样,虚拟存储器系统必须有某种方法来判定一个虚拟页是否存放在 DRAM 中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理存储器中选择一个牺牲页,并将虚拟页从磁盘拷贝到 DRAM 中,替换这个牺牲页。 54 | 55 | 这些功能是由许多软硬件联合提供的,包括操作系统软、MMU(存储器管理单元)中的地址翻译硬件和一个存放在物理存储器中叫做**页表**(page table)的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表。操作系统负责维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。 56 | 57 | 下图展示了一个页表的基本组织结构。页表就是一个页表条目(Page Table Entry, PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。 58 | 59 | ![enter description here][77] 60 | 61 | ### 页命中 62 | 63 | ![enter description here][78] 64 | 65 | ### 缺页 66 | 67 | 在虚拟存储器的习惯说法中,DRAM不命中称为**缺页**(page fault)。缺页异常调用内核中缺页异常处理程序,该程序会选择一个牺牲页。在磁盘和存储器之间传送页的活动叫做**交换**(swapping)或者**页面调度**(paging)。 68 | 69 | ![enter description here][79] 70 | 71 | ![enter description here][80] 72 | 73 | ### 分配页面 74 | 75 | ![enter description here][81] 76 | 77 | ### 局部性再次搭救 78 | 79 | 尽管在整个运行过程中程序引用的不同页面的总数可能超出物理存储器总的大小,但是局部性原则保证了在任意时刻,程序往往在一个较小的活动页面(active page)集合上工作,这个集合叫做**工作集**(working set)或者**常驻集**(resident set)。 80 | 81 | 如果工作集的大小超出了物理存储器的大小,那么程序将产生一种不幸的状态,叫做颠簸(thrashing),这时页面将不断的换进换出。 82 | 83 | ## 虚拟存储器作为存储管理的工具 84 | 85 | OS为每个进程提供一个独立的页表,就是一个独立的虚拟地址空间。多个虚拟页面可以映射到同一个共享物理页面上。 86 | 87 | ![enter description here][82] 88 | 89 | ## 虚拟存储器作为存储器保护的工具 90 | 91 | 任何现代计算机系统都必须为操作系统提供手段来控制对存储器系统的访问。提供独立地址空间使得分离不同进程私有存储器变得容易。地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。 92 | 93 | ![enter description here][83] 94 | 95 | 96 | ## 地址翻译 97 | 98 | 地址翻译的基础知识: 99 | 100 | ![enter description here][84] 101 | 102 | ![enter description here][85] 103 | 104 | ![enter description here][86] 105 | 106 | ### 结合高速缓存和虚拟存储器 107 | 108 | ![enter description here][87] 109 | 110 | ### 利用TLB加速地址翻译 111 | 112 | TLB是一个小的、虚拟地址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相连性。 113 | 114 | ![enter description here][88] 115 | 116 | ### 多级页表 117 | 118 | ![enter description here][89] 119 | 120 | ## 存储器映射 121 | 122 | Linux通过将一个 虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程叫存储器映射(memory mapping) 123 | 虚拟存储器区域可以映射到两种类型的对象: 124 | 1. Unix文件系统的普通文件 125 | 2. 匿名文件 126 | 127 | ### 再看共享对象 128 | 129 | 存储器映射的概念来源于一个聪明的发现:如果虚拟存储器系统可以集成到传统的文件系统中,那么就能提供一种简单而高效的把程序和数据加载到存储器中的方法。 130 | 131 | 一个对象可以被映射到虚拟存储器中的一个区域,要么作为共享对象,要么作为私有对象。另一个方面,对于一个映射到私有对象的区域所做的改变,对于其他进程来说是不可见的,而且进程对这个区域所做的任何写操作都不会反映在磁盘的对象中。 132 | 133 | ![enter description here][90] 134 | 135 | 私有对象是使用一种写时拷贝(copy-on-write)的巧妙技巧被映射虚拟存储器中的。 136 | 137 | ![enter description here][91] 138 | 139 | ## 动态存储器分配 140 | 141 | 一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。 142 | 143 | ![enter description here][92] 144 | 145 | 显式分配器要求应用显式地释放任何已经分配的块 146 | 147 | 隐式分配器要求检测何时一个已分配块不再被使用,然后就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector) 148 | 149 | ### 为什么要使用动态存储器分配 150 | 151 | 经常直到程序运行时,才知道某些数据结构的大小 152 | 153 | ### 碎片 154 | 155 | 造成堆利用率低的主要原因是碎片(fragmentation),当虽然有未使用的存储器但是不能用来满足分配请求时,就发生这种现象。有两种碎片形式:**内部碎片**(internal fragmentation)和**外部碎片**(external fragmentation): 156 | * 内部碎片是在一个已分配块比有效载荷大时发生的。 157 | * 外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。 158 | 159 | ## 垃圾收集 160 | 161 | 垃圾收集器(garbage collector)是一种动态存储分配器,它自动释放程序不在需要的已分配块。 162 | 163 | ### 垃圾收集器的基本要素 164 | 165 | 垃圾收集器将存储器视为一张有向可达图(reachability graph) 166 | 167 | ![enter description here][93] 168 | 169 | ## C程序中常见的与存储器相关的错误 170 | * 间接引用坏指针 171 | * 读未初始化的存储器 172 | * 允许栈缓冲区溢出 173 | * 假设指针和他们指向的对象是相同大小的 174 | * 造成错位错误 175 | * 引用指针而不是他们指向的对象 176 | * 误解指针运算 177 | * 引用不存在的变量 178 | * 引用空闲堆块中的数据 179 | * 引起存储器泄露 180 | 181 | ## 小结 182 | 183 | 虚拟存储器是对主存的一个抽象,支持虚拟存储器的处理器通过一种叫做虚拟寻址的间接引用来引用主存 184 | 185 | [74]: https://data2.liuin.cn/story-writer/2018_1_31_1517403129993.jpg 186 | [75]: https://data2.liuin.cn/story-writer/2018_1_31_1517403182759.jpg 187 | [76]: https://data2.liuin.cn/story-writer/2018_1_31_1517403393083.jpg 188 | [77]: https://data2.liuin.cn/story-writer/2018_1_31_1517403685361.jpg 189 | [78]: https://data2.liuin.cn/story-writer/2018_1_31_1517403740507.jpg 190 | [79]: https://data2.liuin.cn/story-writer/2018_1_31_1517403893776.jpg 191 | [80]: https://data2.liuin.cn/story-writer/2018_1_31_1517403911509.jpg 192 | [81]: https://data2.liuin.cn/story-writer/2018_1_31_1517403947855.jpg 193 | [82]: https://data2.liuin.cn/story-writer/2018_1_31_1517404196815.jpg 194 | [83]: https://data2.liuin.cn/story-writer/2018_1_31_1517404420646.jpg 195 | [84]: https://data2.liuin.cn/story-writer/2018_1_31_1517404559355.jpg 196 | [85]: https://data2.liuin.cn/story-writer/2018_1_31_1517404669578.jpg 197 | [86]: https://data2.liuin.cn/story-writer/2018_1_31_1517404692161.jpg 198 | [87]: https://data2.liuin.cn/story-writer/2018_1_31_1517404781626.jpg 199 | [88]: https://data2.liuin.cn/story-writer/2018_1_31_1517404914871.jpg 200 | [89]: https://data2.liuin.cn/story-writer/2018_1_31_1517404952385.jpg 201 | [90]: https://data2.liuin.cn/story-writer/2018_1_31_1517405449818.jpg 202 | [91]: https://data2.liuin.cn/story-writer/2018_1_31_1517405536089.jpg 203 | [92]: https://data2.liuin.cn/story-writer/2018_1_31_1517405624725.jpg 204 | [93]: https://data2.liuin.cn/story-writer/2018_1_31_1517406251592.jpg 205 | 206 | -------------------------------------------------------------------------------- /notes/CSAPP-链接.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSAPP 链接 3 | date: 2018-01-29 17:56:56 4 | tags: CSAPP 5 | categories: 读书笔记 6 | --- 7 | 8 | 《深入理解计算机系统》第7章笔记 9 | 10 | 11 | 12 | 链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载到存储器并执行。链接可以执行于编译时(compile time),也可以执行于加载时(load time),甚至执行于运行时(run time)。 13 | 14 | 链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。 15 | 16 | ## 编译器驱动程序 17 | 18 | 大多数编译系统提供**编译驱动程序**(compiler driver),为用户根据需求调用语言预处理器、编译器、汇编器和链接器 19 | 20 | ## 静态链接 21 | 22 | 以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。 23 | 24 | ![enter description here][55] 25 | 26 | 为了创建可执行文件,链接器必须完成两个主要任务: 27 | * 符号解析(symbol resolution),将一个符号引用和一个符号定义结合起来 28 | * 重定位(relocation),编译器和汇编器生成从地址 0 开始的代码和数据节。链接器通过把每个符号定义域一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。 29 | 30 | 目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些则包含程序数据,而其他的则包含指导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少。产生目标文件的编译器和汇编器已经完成了大部分工作。 31 | 32 | ## 目标文件 33 | 34 | 目标文件有三种形式: 35 | * 可重定位目标文件,包含二进制文件和代码,其形式在编译时和其他可重定位目标文件合并起来,创建一个可执行目标文件 36 | * 可执行目标文件,其形式可被拷贝到存储器并执行 37 | * 共享目标文件,一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载到存储器并链接。 38 | 39 | 编译器和汇编器生成可重定位目标文件(包括共享目标文件),链接器生成可执行目标文件 40 | 41 | ## 可重定位目标文件 42 | 43 | 典型的 ELF 可重定位目标文件的格式。ELF 头(ELF header)以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。 44 | 45 | ![enter description here][56] 46 | 47 | ## 符号和符号表 48 | 49 | 每个重定位的目标模块m都有一个符号表,包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号: 50 | 1. 有m定义并能够被其他模块引用的全局符号 51 | 2. 由其他模块定义并被m所引用的全局符号 52 | 3. 只被m定义和引用的本地符号 53 | 54 | C 程序员使用 static 属性在模块内部隐藏变量和函数声明。 55 | 56 | ## 符号解析 57 | 58 | 链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。 59 | 60 | 当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,它会假设符号是在其他模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理。 61 | 62 | ### 链接器如何解析多处定义的全局符号 63 | 64 | 编译器输出每个全局符号给汇编器,或者是强(stong),或者是弱(week),而汇编器把这些信息隐含得编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。 65 | 66 | 根据强弱符号的定义,Unix使用一下规则来处理多处定义的符号: 67 | 1. 不允许有多个强符号 68 | 2. 如果有一个强符号和多个弱符号,则选择强符号 69 | 3. 如果有多个弱符号,则从中随机选择一个 70 | 71 | ### 与静态库链接 72 | 73 | 所有编译系统都提供了一种机制,将所有相关的目标模块打包成一个单独的文件,成为静态库(static library),它也可以作为链接器的输入。 74 | 75 | 在Unix系统中,静态库以一种存档(archive)的特殊文件格式存储在磁盘中。 76 | 77 | ![enter description here][57] 78 | 79 | ## 重定位 80 | 81 | 一旦链接器完成符号解析这一步,就把代码中每一个符号的引用和确定的一个符号定义结合起来。链接器就知道他输入目标模块中的代码节和数据节的确定大小。后面就是重定位的步骤了, 82 | 83 | 重定位由两个部分组成: 84 | 1. 重定位节和符号定义 85 | 2. 重定义节中的符号引用,链接器修改代码节和数据节中的每一个符号引用,使得他们指向正确的地址 86 | 87 | ### 重定向表目 88 | 89 | 无论何时汇编器遇到对最终位置未知的目标引用,他就会生成一个重定位表目(relocation entry),告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。 90 | 91 | 有两种最基本的重定位类型: 92 | * R_386_PC32 : 重定位一个使用32位PC相关的地址引用 93 | * R_386_32 : 重定位一个使用32位绝对地址的引用 94 | 95 | ### 重定位符号表引用 96 | 97 | ## 可执行目标文件 98 | 99 | 一个典型的ELF可执行文件中的各类信息: 100 | 101 | ![enter description here][58] 102 | 103 | ## 加载可执行文件 104 | 105 | 每个Unix程序都有一个运行时的存储器映像: 106 | 107 | ![enter description here][59] 108 | 109 | ## 动态链接共享库 110 | 111 | 共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器的程序来执行的。 112 | 113 | 共享库也称为共享目标(shared object),在 Unix 系统中通常用 .so 后缀来表示。微软的操作系统大量地利用了共享库,它们称为 DLL。 114 | 115 | ![使用共享库来动态链接][60] 116 | 117 | 118 | ## 小结 119 | 120 | 链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件,它又三种不同的形式:可重定位的、可执行的和共享的。可重定位的目标文件由静态链接器合并成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序调用 dlopen 库的函数时。 121 | 122 | 链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。 123 | 124 | 静态链接器是由像 GCC 这样的编译驱动器调用的。它们将多个可重定位目标文件合并成一个单独的可执行目标文件。多个目标文件可以定义相同的符号,而链接器用来悄悄地解析这些多重定义的规则可能在用户程序中引入的微妙错误。 125 | 126 | 多个目标文件可以被连接到一个单独的静态库中。链接器用库来解析其他目标模块中的符号引用。许多链接器通过从左到右的顺序扫描来解析符号引用,这是另一个引起迷惑的链接时错误来源。 127 | 128 | 加载器将可执行文件的内容映射到存储器,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的程序和数据的未解析的引用。在加载时,加载器将部分链接的可执行文件映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。 129 | 130 | 被编译为位置无关代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。为了加载、链接和访问共享库的函数和数据,应用程序还可以在运行时使用动态链接器。 131 | 132 | [55]: https://data2.liuin.cn/story-writer/2018_1_29_1517214954215.jpg 133 | [56]: https://data2.liuin.cn/story-writer/2018_1_29_1517215240554.jpg 134 | [57]: https://data2.liuin.cn/story-writer/2018_1_29_1517216233165.jpg 135 | [58]: https://data2.liuin.cn/story-writer/2018_1_29_1517217108531.jpg 136 | [59]: https://data2.liuin.cn/story-writer/2018_1_29_1517217187444.jpg 137 | [60]: https://data2.liuin.cn/story-writer/2018_1_29_1517217296974.jpg 138 | --------------------------------------------------------------------------------