历时 4 个月,这本书终于看完了,大概从 6 月中旬开始阅读,直到 10 月中旬,这本书终于结束了。
很久没读过这么厚的书了,而且读完了,真不错,书的内容值得推荐,这书属于程序员必读基础书籍之一。
此书共计 730 页,每章节后面都有一些练习题,除去练习题,大概真正的内容是 500 页左右,当然练习题也很好,能帮助我们巩固知识。
如果对计算机底层没有深入的兴趣,可以省去练习题,只看书的内容,也能有非常多的收获。
从头到尾介绍了计算机「CPU、存储器、固态硬盘、缓存、文件执行流程、共享文件、异常处理、进程、上下文切换、虚拟内存、链接、I/O、通信」等等多个关键知识点。
我把书的内容精简,整理成文章,我们来回顾一下本书的内容:
在 CPU 中存在一种特殊的寄存器「栈指针寄存器」,CPU 的栈管理和我们平时程序里写的栈数据结构一样,都是「后进先出」原则。
CPU 把要执行的「指令、变量、返回地址」入栈,并分配栈空间,给栈空间分配一个栈指针 ID,在 64 位系统中,每增加一条栈空间,相应的「栈 ID – 8」,如果栈指针 ID 太多,则会从地址空间中回绕,从最大值开始减少。
不过,如果程序写的栈层级太深或者无限循环,就会触发「栈溢出」,因为这个时候已经没有新的栈空间可供分配了。
程序计数器
在 CPU 中,程序计数器代表的是程序应该执行哪条指令的地址,当它执行完以后会更新,指向下一条要执行的指令。
我们通过「调用者」和「被调用者」来阐述,把它们类比成函数,每个函数都有一个计数器(整型),来标识自己在内存中的起始地址。当「被调用者」执行完成后,从栈中弹出这个地址,跳转到「调用者」继续执行,以此类推。
缓冲区溢出
在编译阶段,我们定义了一个字符串的内存大小,如果我们在执行程序时,写入一个非常大的字符串,而内存空间不够,就会发生缓冲区溢出。
如果是无意的缓冲区溢出还可控,如果是恶意的,那就会受到安全攻击,利用溢出的数据可以用来控制「程序计数器」执行的下一条指令,执行恶意代码。
CPU 可以通过「栈随机化」和「金丝雀值」来规避这样的安全问题。
通过一个时间周期内流程来讲述 CPU 是如何循环往复的执行指令的。
首先它使用程序计数器来定位指令在内存中的地址,并从当前地址读取要执行的指令。接着解析指令数据,获得指令结果,根据结果来计算或跳转。
执行完指令后,执行结果通过写回逻辑,把值写回到内存中去,并获取到下一条程序计数器的值。
在一个时间周期内执行完一组指令,如果这个时间周期是 3 秒,那下一个指令执行就需要等待 3 秒,效率很低。
为了提高吞吐量,而不是等待一个完整的周期,CPU 引入了流水线工作模式。
将 CPU 均衡地分解成多个模块,每个模块处理特定的任务,并通过程序来控制任务之间的切换,当第一条指令在第 1 模块执行完成后,第二条指令立即进入第 1 模块执行,从而实现连续的执行。
为了最大化效率,模块的划分要均衡,要保证每块任务的工作时间几乎相同,另外就是流水线不能太深,太深会增加切换成本。
在计算机存储层次结构包括「寄存器、高速缓存存储器、主存、磁盘」等。
寄存器存储容量最小,但提供了最快的访问速度,其造价也是最昂贵的;
高速缓存存储器介于寄存器和主存之间,具有中等的存储容量和访问速度,它是为了缓解主存和寄存器之间访问速度差异;
主存就是我们常说的「内存条」,它有更大的容量,造价较为适中,但访问速度稍慢;
以上三种断电后,数据都会消失,所以应运而生,磁盘出现了,提供了巨大的存储空间,但访问速度是最慢的,造价也属于存储器层次里最经济的。
磁盘包括但不限于「硬盘、固态硬盘、U 盘、光盘」等。
它是一种大容量的数据存储硬件,它包含多个盘面,数据被存储在盘面的扇区中。一个特殊的手臂移动到适当的位置来读取或写入数据。
磁盘的容量是根据它可以写入的最大字节数量来确定的,通常以 GB 或 TB 为单位来表示。
固态硬盘简称 SSD,是一种基于闪存技术的数据存储设备。
在固态硬盘内部,存储空间被分成多个 B 块,每个 B 块包含多个 P 页,每次进行读写时,会锁定特定的 P 页进行读或写操作。
对于固态硬盘来说,读操作通常比写操作快,因为每次写操作之前都需要擦除已存在的信息,在写入过程中,固态硬盘会经历一个「写放大」的现象,首先将数据复制到新的 P 页,然后再开始执行写操作。
固态硬盘的造价相比于旋转磁盘更「贵」,更耐摔,并提供了更高的访问速度。
「寄存器 – 高速缓存存储 – 内存 – 磁盘」不同的存储器有不同的性能,为了在多种设备间找到平衡,中间增加缓存,以减少从慢速存储器加载数据的需要。
这种策略得益于「局部性原理」的概念,分为「时间局部性和空间局部性」。
时间局部性意味着:一个数据被访问了,在近期内有很高的概率被再次访问;
空间局部性意味着:一个内存位置被访问后,在近期内附近的内存位置大概率也会被访问;
在实际应用中,缓存的概念应用于浏览器缓存、CDN 服务器、数据库缓存、路由器的查表等等,应用很广。
为了桥接各个存储器之间的访问速度差异,在这种层次结构中,按照「块」来组织,形成金字塔结构。
每一个较上层的存储器都缓存了下一层的部分内容,而从上到下,每一层缓存的数据容量都会逐渐增大。
假设需要访问变量 k,首先会在顶层检查缓存,若缓存命中,数据直接使用;若缓存不命中,则继续再向下层寻找,依次类推,直到检查到最底层。
其实就是充分利用「局部性原理」来编写程序,程序在执行的时,大部分时间都集中在「核心代码」上,尤其是「循环」结构,所以我们可以针对循环做一些优化。
例如声明变量时,在作用域内如果可以重复使用同一变量,那将提高缓存的利用率,这得益于时间局部性;而涉及循环时,如果步长设置为 1 ,则可以最大化地利用「空间局部性」提高代码效率。
链接是软件开发中一个重要的步骤,类似于我们拼图,将各个独立的部分连接成一个完整的整体。
在程序开发中,链接会把我们编写的文件通过「编译、合并」最终形成一个「可执行文件」,将这个文件加载到内存,就可以由「加载器」启动并执行。
这个过程与打包工具的「依赖分析」相似,它们都是将所有需要的模块正确地连接在一起。
在计算机中,当多个项目 A 和 B 都依赖于一个「函数库」,如何高效地管理这个函数库就变得至关重要。
如果两个项目都存在一份「函数库」,这当然是最简单的方案,但升级函数库的时候,就需要同时更新两份代码。
另一种方案是放在固定的内存位置,但如果 C 项目不需要这个库,那么这块内存对于 C 来讲,就是浪费了。
所以产生了「位置无关代码」,它可以随意放在内存哪个位置,不受约束,这为动态共享和加载提供了便利,多个进程可以共享单个内存中的代码副本。
在计算机系统存在一张「异常表」,这里会罗列所有的异常码,每种异常码对应着不同的异常处理程序,包含中断、陷井、故障、终止等。
系统正常执行时,如果遇到问题会根据异常表码去执行异常处理程序,如果程序执行完后可以恢复那会恢复程序的继续执行,如果不能恢复程序,那就会终止程序。
这张异常表 CPU 会预定义一部分基础的异常或错误码,通常和硬件操作相关。而操作系统可以定义与系统相关的错误码,来应对相应的错误处理。
进程是计算机中的一个基本概念,用来管理和执行程序,可以把它理解成流水线,每条流水线都在制作不同的产品。
每个进程都有一个独立的空间、代码和数据,都有自己独立的 Process ID。进程分为三种状态「运行中、已终止、已死亡」。
子进程就像主流水线分出来的小流水线一样。它能做和父进程一样的任务,它们是新的、独立的,主进程和子进程除了 ID 不一样,其余都一致。
子进程的回收都是通过父进程来实现的,如果父进程被意外杀死了,那它里面的子进程就会变成孤岛子进程,只能等待系统的进程回收方法回收孤岛子进程。
我们看到的假象是「计算机专为我服务」,其实计算机可以同时处理多个程序,而不是只处理一个,它可以同时服务多个程序。
当进程进行切换时,首先会保存当前进程的信息「寄存器状态、程序计数器 ID」等,然后恢复加载另一个进程的信息,以此来达到进程间切换。
假设 A 程序正在启动中,它需要去磁盘读取一个文件,需要 10 ms,这个时候计算机就会利用这个空间去运行 B 程序,来保证 CPU 的最大利用效率。
虚拟内存是计算机内存管理的一种技术,允许计算机使用硬盘空间来充当额外的内存,可以使得计算机在内存不足的情况下,运行更多的程序。
每个进程都需要访问真实的内存地址,对于计算机来说,真实的内存地址是有限的,那可以通过「虚拟内存」的方式来扩充、管理、翻译虚拟地址。
虚拟内存的存在和「缓存」的概念很相似,在访问真实内存地址之前,先经过地址翻译,看当前虚拟内存中是否含有需要的数据,如果有直接使用,如果没有,则去真实的内存地址中获取,虽然这样会有访问速度上的牺牲,但扩充了程序的运行数量 。
输入「input」是通过「鼠标、键盘、外设」等设备把数据传输到计算机的过程,输出「output」是指计算机将处理的数据发送给「鼠标、键盘、外设」的过程。
对于 Unix 系统来说,有「一切皆文件」的哲学思想,这意味着所有的设备「硬盘、键盘、虚拟文件」都被抽象成「文件」。
Unix I/O 提供了标准的 I/O 库,帮助我们方便地使用「open、write、read、close」等输入输出操作,而 C 语言在标准库的基础上,又封装了一层称为高级 I/O 库,扩展了更多的底层操作方法。
客户端 – 服务端模型,客户端呈现内容,服务端处理数据,它们之间通过「套接字 Sockets」来建立通信链路,通常采用 HTTP 协议规范数据的传输方式。
在多用户环境中,服务端通常会并行处理来自不同客户端的请求,而每个请求相当于是一个独立的进程,通过这种方式,服务端可以高效地响应多个客户端。
多进程:每个进程都有独立的虚拟地址空间,基于进程的设计往往比较慢,它们之间如果想通信,需要使用 IPC 技术,通信成本很高。
多路复用:单进程内,任务通过调度器来切换,实现并发执行。它优化了就绪任务的处理,但无法解决通道阻塞的问题,在实际的处理中,仍然是顺序执行。
多线程:结合了进程和多路复用的优点,线程间通信成本低,多个线程可以并行的处理多个 I/O 请求,实现了真正的并行执行。
终于 … 整理完了。
内容优化:ChatGPT
内容来源:《深入理解计算机系统》
原创文章,作者:小道研究,如若转载,请注明出处:https://www.sudun.com/ask/34575.html