《深入理解计算机系统》csapp,全书分章节分享(一)-爱代码爱编程
简介&前言
这是一本很经典的书,它涵盖对计算机系统从硬件到软件的全面理解,包括处理器架构、存储系统、网络通信等,很适合学习计算机底层知识。由于实在太经典了,就不做过多的赘述了。
PS:本文仅作个人学习过程中的简要记录,纯原创,纯手工,有错误的地方希望大家指出,方便我后续进行更正,大家一起进步!!!o(* ̄▽ ̄*)ブ
PPS:由于整体篇幅较长将会拆分成好几个文章来分享
PPPS:希望大家都能阅读正版书籍,支持正版,一起学习~~~
目录
第1章计算机系统漫游
1.1 信息就是位+上下文
#include <stdio.h>
int main()
{
printf("hello,world\n");
return 0;
}
上述hello程序的生命周期从源程序(源文件)开始,程序员通过编辑器创建并保存的文本文件,文件名是hello.c。(hello 程序的生命周期是从一个高级C语言程序开始的)
源程序:由值0和1组成的位序列
位(又称为比特):计算机内部数据储存的最小单位
字节:8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。
ASCII码:用于表示文本字符,即用一个唯一的单字节大小的整数值来表示每个字符,下图为hello.c程序的ASCII码表示。
每个程序均以字节序列的形式储存在文件中;每个字节都有一个整数值,对应于某些字符。
文本文件:只由ASCII字符构成的文件(例如:上述的hello.c)
二进制文件:包含 ASCII 及扩展ASCII字符中编写的数据或程序指令(program instructions)的文件,可以包含任意类型的数据,例如图像、音频、视频、可执行文件、压缩文件等。
hello.c的表示方法说明的基本思想:系统中的所有信息都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文(在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令)。
C标准库:是一系列预先编写好的C语言函数的集合,即常用的头文件。(发展时间线:贝尔实验室1969~1973创建c语言→1989美国国家标准学会颁布ANSIC标准→C语言标准化与ISO)
C与Unix:从一开始就是作为一种用于Unix系统的程序语言开发出来的。大部分Unix内核(操作系统的核心部分),以及所有支撑工具和函数库都是用C语言编写的,且Unix通过C编写后可以很方便地移植到新的机器上。
1.2 程序被其他程序翻译成不同的 格式
hello程序的运行过程:C语句→低级机器语言指令→以可执行目标程序的格式打包→并以二进制磁盘文件的形式存放起来→目标程序 (可执行目标文件)
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:
linux> gcc -o hello hello.c
GCC编译器驱动程序读取源程序文件hello.c,并把它翻译(四个阶段)成一个可执行目标文件hello。
编译系统(compilationsystem):由执行翻译的四个阶段的程序构成(预处理器、编译器、汇编器和链接器)。
预处理阶段:预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。通常是以 .i 作为文件扩展名。
编译阶段:编译器(ccl)将文本文件hello.i 翻译成文本文件hello.s, 它包含一 个汇编语言程序。该程序包含函数main的定义:
main:
subq $8, %rsp
movl $.LCO, %edi
call puts
movl $0, %eax
addq $8, %rsp
ret
汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成 一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o(二进制文件,包含函数main的指令编码)中,直接用文本编辑器打开会出现乱码。
链接阶段:通过链接器(Id)将几个输入目标文件加工合并成一个输出文件。最后得到hello文件(一个可执行目标文件 (或者简称为可执行文件),可以被加载到内存中,由系统执行)。
1.3 了解编译系统如何工作是大有益处的
优化程序性能、理解链接时出现的错误、避免安全漏洞。
1.4 处理器读并解释储存在内存 中的指令
在Unix系统上执行hello可执行文件,应将它的文件名输入shell(命令行解释器),即可输出相应的结果:
linux> ./hello
hello, world
linux>
1.4.1 系统的硬件组成
总线:贯穿整个系统的是一组电子管道,携带信息字节并负责在各个部件间传递,常被设计成传送定长的字节块,即:字(word)。
I/O设备:系统与外部世界的通道。
主存:临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。一般来说,组成程序的每条机器指令都由不同数量的字节构成
物理上: | 主存是由一组动态随机存取存储器(DRAM)芯片组成的 |
逻辑上: | 存储器是一个线性的字节数组,每个字节都有其唯一的从零开始的地址(数组索引)。 |
处理器(中央处理单元、CPU):解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。
1.4.2 运行 hello程序
首先,shell会执行它的指令,并等待我们输入下一个指令,当我们在键盘上输人字符串 “./hello”后,shell 程序将字符逐一读人寄存器,再把它存放到内存中。
键入回车后,shell知道命令的输入已结束,并开始执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串“hello,world\n"。
利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。这个步骤如下所示。
当目标文件hello中的代码和数据被加载到主存,处理器开始执行hello程序的main程序中的机器语言指令。这些指令将“hello,world\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。
1.5 高速缓存至关重要
一、上述案例揭示:系统花费了大量的时间把信息从一个地方挪到另一个地方
1、hello程序的机器指令最初存放在磁盘上 → 程序加载时,被复制到主存 → 处理器运行程序时,又从主存复制到处理器
2、数据串“hello,world\n”开始时在磁盘上 → 被复制到主存 → 又复制到显示设备
∵ 复制就是开销,减慢了程序“真正”的工作
∴ 系统设计者的一个主要目标就是使这些复制操作尽可能快地完成
二、处理器与主存之间的差距(运行速度:处理器>>主存)
高速缓存存储器(cache memory, 简称为 cache 或 高速缓存):一种更小更快的存储设备,作为暂时的集结区域,存放处理器近期可能会需要的信息。
上图为一个典型系统中的髙速缓存存储器。其中,位于处理器芯片上的L1高速缓存的容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。一个容量为数十万到数百万字节的更大的L2高速缓存通过一条特殊的总线连接到处理器。进程访问L2高速缓存的时间要比访问L1高速缓存的时间长5倍,但是这仍然比访问主存的时间快5~10倍。
静态随机访问存储器(SRAM):用以实现L1和L2高速缓存。
高速缓存的局部性原理:程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。因而系统可以获得一个很大的存储器,同时访问速度也很快。
一个重要的结论:应用程序员能够利用高速缓存将程序的性能提高一个数量级。
1.6 存储设备形成层次结构
普遍观念:在处理器和加大较慢的设备(e.g:主存)之间插入一个更小更快的存储设备(e.g:高速缓存)
存储器层次结构的主要思想:上一层的存储器作为低一层存储器的高速缓存。
1.7 操作系统管理硬件
shell和hello程序始终没有直接访问键盘、显示器、磁盘或者主存,而是依靠操作系统提供的服务。
操作系统:应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统。
操作系统的两个基本功能:( 1 )防止硬件被失控的应用程序滥用( 2 )向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。
ps:操作系统通过下图几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。
其中,文件是对I/O设备的抽象表示;虚拟内存是对主存和磁盘I/O设备的抽象表示;进程则是对处理器、主存和I/O设备的抽象表示。
1.7.1 进程
操作系统的一种假象:以hello程序为例,该程序的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。
进程:操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。
并发运行:一个进程的指令和另一个进程的指令是交错执行的。
注:在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。
传统的单核处理器:一个时刻只能执行一个程序。
先进的多核处理器:能同时执行多个程序。
但无论是在单核 or 多核系统中,一个CPU看起来都像是在并发地执行多个进程。而这,则是通过处理器在进程间切换来实现的。
上下文切换:操作系统实现上述交错执行的机制。
上下文:操作系统保持跟踪进程运行所需要的所有状态信息,这种状态,即:上下文。其中包含许多信息,如:PC和寄存器文件的当前值,以及主存的内容。
在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。
上图为hello程序运行场景的基本理念。
其中,该场景含有2个并发的进程:shell进程和hello进程。
1. 初始状态:只有shell进程运行,等待命令输入。
2. 用户请求:通过shell启动hello程序。
3. 系统调用:shell发起系统调用,请求操作系统执行hello程序。
4. 上下文切换:
- 保存shell进程上下文。
- 创建hello进程及其上下文。
5. 控制权转移:操作系统将控制权交给hello进程。
6. 程序执行:hello进程运行直至结束。
7. 恢复上下文:操作系统保存hello进程状态,恢复shell进程上下文。
8. 返回控制权:控制权交回shell,等待新命令。
内核管理:负责进程间转换。
内核定义:操作系统的核心,常驻内存。
系统调用:
- 应用程序请求操作系统操作(如文件读写)。
- 执行特殊指令,传递控制权给内核。
内核功能:
- 执行请求的操作。
- 完成后返回控制权给应用程序。
内核特性:
- 不是独立进程。
- 管理所有进程的代码和数据结构集合。
ps:实现进程这个抽象概念需要低级硬件和操作系统软件之间的紧密合作。
1.7.2 线程
进程与线程:
- 进程:单一控制流的执行单元。
- 线程:进程内的多个执行单元。
(尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成)
线程的特点:
- 每个线程都运行在进程的上下文中
- 共享进程的代码和全局数据。
多线程相比多进程的优势:
- 并行处理:适用于网络服务器等需求。
- 数据共享:比多进程更易实现。
- 效率:通常比进程更高。
多处理器环境下的多线程:
- 程序运行速度提升。
编程模型:
- 多线程成为重要的编程模式。
1.7.3 虚拟内存
虚拟内存:一个抽象的概念,为每个进程提供了一个假象。即:每个进程都在独占地使用主存。
虚拟地址空间:确保每个进程的内存视图一致。
上图为Linux进程的虚拟地址空间(其他Unix系统的设计也与此类似)。
Linux地址空间:
- 顶部:操作系统代码和数据,所有进程共享。地址空间布局:
- 底部:用户进程定义的代码和数据。地址增长方向:
- 从下往上增大。
每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
虚拟地址空间结构:
- 由多个功能明确的区域组成。
从最低的地址开始,逐步向上介绍如下:
程序代码和数据:
- 对所有的进程来说,代码都是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。
- 初始大小固定,代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件hello
堆(Heap):
- 位于代码和数据区之后。
- 初始时未指定大小。
- 动态扩展:通过malloc
。
- 动态收缩:通过free
。
-内存管理:与代码和数据区的固定大小不同;堆大小在运行时可变。
共享库:
- (大约在地址空间的中间部分的一块区域)存放C标准库和数学库等共享库的代码和数据。
- 支持代码共享和重用。
栈:
- 用户栈:位于用户虚拟地址空间顶部,编译器用它来实现函数调用
- 用户栈在程序执行期间可以动态地扩展和收缩。
- 特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。
内核虚拟内存:
- 地址空间最顶部。
- 不允许 应用程序 读写这个区域的内容 或者 直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
- 虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
1.7.4 文件
文件:即字节序列。
每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为 Unix I/O 的系统函数调用读写文件来实现的。
1.8 系统之间利用网络通信
现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个 I/O设备,如下图所示。
网络通信过程中数据的流动:
-
数据复制:系统将数据从主存储器(RAM)复制到网络适配器(网卡)。
-
网络传输:复制到网络适配器的数据通过网线或无线信号在网络中传输。
-
目的地:数据的目的地是网络上的另一台计算机,而不是本地的存储设备,如硬盘驱动器。
-
双向通信:这个过程是双向的。系统不仅能发送数据到其他计算机,也能接收来自其他计算机的数据。
-
数据接收:当系统接收到来自网络的数据时,它会将这些数据复制到自己的主存储器中,以便进一步处理或存储。
全球网络的影响:互联网的出现使得全球范围内的计算机连接成为可能。
信息复制:将信息从一个计算机复制到另一个计算机,已成为计算机系统的一个核心功能。
基于网络信息复制功能的应用:电子邮件、即时通信、万维网、FTP 和 telnet
在hello示例中,我们可以利用telnet应用在一个远程主机上运行hello程序:
用本地主机的 telnet 客户端连接远程主机上的 telnet 服务器 → 登录远程主机并运行shell → 远端的 shell 等待接收输入命令 → 在远端运行如下 5 个 hello 程序的基本步骤:
在 telnet 客户端键入“hello”字符串并回车 → 客户端软件将此字符串发送到 telnet 服务器 → telnet 服务器从网络上接收到这个字符串后,把它传递给远端 shell 程序 → 远端 shell 运行 hello 程序 → 将输出行返回给 telnet 服务器 → telnet 服务器通过网络把输出串转发给 telnet 客户端 → 客户端将输出串输出到本地终端上
1.9 重要主题
1. 9.1 Amdahl 定律
1.9.2 并发
两个持久的需求:想要计算机做得更多,想要计算机运行得更快。
而当处理器能够同时做更多的事情时,上述两个需求都会得到改善。
并发(Concurrency):在多任务环境中,多个任务在同一个时间段内同时进行,但不一定同时执行。它允许多个进程或线程在宏观上看起来是同时运行的,即使在单核处理器上,也可以通过快速切换任务来实现这种效果。并发的目的是通过提高资源利用率来提高系统的整体性能。
并发的特点:
- 多任务:系统能够同时处理多个任务。
- 资源共享:不同的任务可能需要访问相同的资源。
- 独立性:每个任务独立于其他任务运行。
- 调度:操作系统负责管理任务的执行顺序和时间。
并行(Parallelism):多个任务或计算过程同时进行,它们可以在多个处理器或核心上同时执行。并行计算利用了硬件的并行能力,以提高处理速度和效率。
并行的特点:
- 同时执行:多个任务或计算过程在物理上同时运行。
- 多处理器/核心:需要多个处理器或核心来实现并行执行。
- 性能提升:通过并行处理,可以显著提高处理速度和效率。
- 任务分割:通常需要将问题分解成可以并行处理的子任务。
- 同步和通信:并行任务之间可能需要同步操作和数据通信。
并发和并行的区别:
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务
Linux、Windows等已经是并发的系统,一个进程也可同时执行多个线程(控制流)。
系统层级结构由高到低:
线程级并发:使用线程,我们能够在一个进程中执行多个控制流。
单处理器系统:一个计算机系统只包括一个运算处理器。单处理器系统下的并发是模拟出来的!
多处理器系统:多核、超线程。一个计算机系统至少包括 2 个运算处理器。
多核处理器是将多个CPU(称为“核”)集成到一个集成电路芯片上。
上图为一个典型的多核处理器的组织结构。
其中,微处理器芯片有4个CPU核,每个核都有自己的L1和 L2 高速缓存,其中的L1高速缓存分为两个部分 —— 一个保存最近取到的指令,另一个存放数据。这些核共享更高层次的高速缓存,以及到主存的接口。
超线程(同时多线程、simultaneous multi-threading):允许一个CPU执行多个控制流。
常规的处理器需要大约 20 000 个时钟周期做不同线程间的转换,而超线程的处理器可以在单个周期的基础上决定 要执行哪一个线程。这使得CPU能够更好地利用它的处理资源 。
多处理器的使用对性能的提升:1、减少了在执行多个任务时模拟并发的需要 2、可以使应用程序运行得更快。
指令级并发:同时执行多条指令;流水线、超标量
超标量处理器:达到比一个周期一条指令更快的执行速率的处理器。大多数现代处理器都支持超标量操作
单指令、多数据并行(SIMD 并行):允许一条指令产生多个可以并行执行的操作;SSE、AVR
1.9.3 计算机系统中抽象的重要性
抽象使得程序员在无需了解API或底层硬件等的内部工作原理便可以使用这些代码或相关功能。
在处理器里,指令集架构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得就好像运行在一个一次只执行一条指令的处理器上。底层的硬件远比抽象描述的要复杂精细,它并行地执行多条指令,但又总是与那个简单有序的模型保持一致。只要执行模型一样,不同的处理器实现也能执行同样的机器代码,而又提供不同的开销和性能。
⭐ 操作系统中的4个抽象:
1、文件是对 I/O 设备的抽象
2、虚拟内存是对程序存储器的抽象
3、进程是对一个正在运行的程序的抽象
4、虚拟机是对整个计算机的抽象,包括操作系统、处理器和程序
1.10 小结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。
计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是 ASCII 文本,然后被编译器和链接器翻译成二进制可执行文件。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在内存、I/O 设备和 CPU 寄存器之间复制数据,所以将系统中的存储设备划分成层次结构——CPU 寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
操作系统内核是应用程序和硬件之间的媒介。
注意四个常见的抽象。
网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种I/O设备。