Build
[python3 | py3 | <nothing>] ./cmd.py build [project name]
Run
[python3 | py3 | <nothing>] ./cmd.py run [project name]
计算机系统 – Computer System A Programmer’s Perspective (CSAPP)
Hello 观众朋友们大家好,这个Repo是我在做《深入理解计算机系统》(CSAPP)视频时所用到的代码,还包括一本设计手册,设计手册还在慢慢更新。代码、手册以及视频都是为了给大家介绍CSAPP这本书,帮助大家阅读和理解最基础的计算机系统知识。特别是本科低年级的同学(大一大二),准备转专业的同学,以及跨专业考研的同学,通常会掌握一些基本的编程语言,但是可能对计算机系统本身并不足够了解。我的视频、代码以及手册就是希望帮助大家学习,尽量通过代码实现CSAPP上的知识点,然后再通过视频讲解,将基础的计算机系统知识连贯起来。
下面是目录:
Chapter 02 – Representing and Manipulating Information
计算机可以看作对Bit串的翻译器,为了了解计算机系统,我们首先需要了解Bit在计算机中是如何被表达与解释的,简单了解这些位运算的操作,背后的代数结构。
2020-09-29 关于有符号与无符号整数,以及它们背后的代数结构。我设计了一张环形图,方便观众理解Bit串到有符号与无符号整数的映射。这张环形图在手册里更加精美: bilibili-video
2020-10-01 我们第一次动手写C语言代码。一些简单的位操作运算,例如判断一个十六进制数中是否全部都是字母,使用卡诺图对布尔代数进行化简。一个基于位运算的数据结构:树状数组。bilibili-video
2020-10-16 浮点数的四种类型:规格化、非规格化、无穷大、非数。uint32_t
到float
的数值转换,特别是关于约分的规定:bilibili-video
Chapter 03 – Machine-Level Representation of Program
拥有Bit构成的基本数据类型以后,我们开始考虑构造一台原始的冯诺依曼结构的计算机。其实,我们是在做一个计算机的模拟器。为此,我们需要模拟CPU中的寄存器与内存等硬件,在这些硬件的基础上编写指令集。我们的指令集是通过字符串写成的,就像我们通常所写的高级语言代码一样,其实都是文本。我们用一些编译的基础知识来解析字符串指令集,将它们翻译成指令的数据类型,然后按照操作符与操作数解释执行这些代码。
在这里,我们其实做了一层抽象:文本与字符串指令就像一台虚拟机,类似于JVM中的字节码,CLR中的IL,我们解释执行的是虚拟机指令。字符串指令集上,我们实现一些简单的指令,例如add
,sub
,mov
,最重要的是关于过程控制,也就是函数调用的几个指令:call
,push
,pop
,ret
,我们对内存栈的控制蕴含在这几条指令之中。
2020-10-22 CPU寄存器的模拟器,这是我们模拟器的第一行代码。我们利用结构体与联合等异构的数据类型来描述寄存器:bilibili-video
2020-10-31 指令的数据结构,用来描述一条汇编指令的操作符,源操作数以及目的操作数,包括操作数中的立即数、寄存器等:bilibili-video
2020-11-05 一个CPU的指令周期,我们第一次执行了一条汇编指令:bilibili-video
2020-11-06 模拟器的运行若干条指令,形成一个程序。为了访问内存资源,我们需要设计内存访问接口。完成call
指令:bilibili-video, git-commit
2020-11-18 递归与树与栈之间的关系,实现push
指令:bilibili-video, git-commit
2020-12-04 完成pop
与ret
指令,至此,我们已经在内存栈上实现了函数的过程控制。我们开始第一次代码重构,将in-memory的结构体指令数组转换为字符串指令数组,并且开始解析字符串指令:bilibili-video, git-commit
2020-12-08 为了解析字符串指令,特别是其中所要用到的立即数,我们开始写字符串到十进制与十六进制数的转换。我们使用确定有限自动状态机去扫描字符串,设计状态机的状态转移:bilibili-video, git-commit
2020-12-18 解析字符串操作数中的内存与寄存器格式:bilibili-video, git-commit
2020-12-27 完成字符串指令的解析,并且在字符串指令上运行了程序。设计状态码,支持条件判断:bilibili-video, git-commit
2021-01-04 实现jmp
,jne
,leave
指令。到此为止,我们已经可以模拟函数的递归调用了:bilibili-video, git-commit
2021-01-15 总结与回顾我们到此为止所做的一切工作,关于数据类型和冯诺依曼计算机,一个简化的计算模型——URM:bilibili-video
2021-01-23 除了冯诺依曼计算机,我们还有另一种计算模型:λ演算。冯诺依曼计算机中,函数是数据;λ演算中,数据是函数:bilibili-video
Chapter 07 – Linking
模拟了计算机的硬件基础以后,我们准备开始在模拟器上运行程序。因此,我们需要设计自己的程序。考虑到我们将指令集用字符串实现,因此我们的程序也是字符串的汇编指令。这一步工作可以由编译器为我们实现,我们需要做的是完成一个简陋的链接器,将可执行的文件链接起来。
正如我们在上一部分所说的,字符串指令可以看作一种在字符串与文本上实现的虚拟机,因此我们的链接也是在.txt
文本上实现的,一个可执行可链接的文本格式,.elf.txt
。为了设计这个文本格式,我们需要理解Linux中是如何处理ELF文件的。
2021-02-06 ELF文件从它的Header开始,我们用readelf
与hexdump
去看ELF文件的二进制数据,按照Byte去理解ELF文件的Header:bilibili-video
2021-02-21 Section Header Table的结构:bilibili-video
2021-02-28 ELF的符号表,以及什么是符号,它们在编译器和链接器视角中的差异:bilibili-video
2021-03-07 符号表中的Bind, Type以及Index,C语言.c
源文件中不同位置的申明与定义是如何在ELF文件中描述的,特别是COMMON节等容易混淆的地方:bilibili-video
2021-03-14 设计可执行与可链接的文本格式,.elf.txt
。从磁盘上的文件中读取到内存:bilibili-video, git-commit
2021-03-21 解析.elf.txt
的节头表与符号表:bilibili-video, git-commit
2021-03-27 静态链接的第一步:符号解析,设计内部的符号表作为维护符号关系的数据结构:bilibili-video, git-commit
2021-04-04 静态链接的第二步:将ELF的Section合并为EOF的Segment,并且计算Segment的运行时起始地址:bilibili-video, git-commit
2021-04-10 静态链接的第三步:将ELF文件中的符号引用重定位到它们在EOF中的符号:bilibili-video, git-commit
2021-04-18 完成静态链接,计算符号的运行时地址。动态链接:GOT与PLT:bilibili-video, git-commit
2021-04-23 链接部分的总结。C语言的.c
源文件如何被预处理,编译,链接,然后加载到内存中执行。在运行时,进程怎样使用动态链接库:bilibili-video
Chapter 06 – The Memory Hierarchy
编译与链接的知识可以帮助我们理解进程的内存模型。从这里开始,我们要真正开始讨论内存问题了,主要包括内存本身的实现以及虚拟内存与物理内存的转换。我们首先讨论内存体系,理解缓存思想在内存体系中的地位。实际上,计算机想要实现高性能的应用,也大量依赖缓存的技术。一旦涉及缓存,或者说共享的资源,我们就无法回避一致性的问题。缓存背后的局部性,如何设计缓存,如何分析缓存一致性带来的性能问题,是我们这一阶段的核心主题。
2021-04-27 简单介绍一下关于内存的许多问题,这也是我们学习计算机系统的最核心知识。内存的层次、局部性、内存与缓存的一致性,以及我们怎么考虑虚拟内存。 bilibili-video
2021-05-09 关于cache的基本结构:cache视角下的物理地址,组相连的cache。bilibili-video, git-commit
2021-05-15 一个简单的LRU cache模拟器,讨论cache的写回/写分配、直写/不写分配的不同状态转移。模拟总线上读写一个cache line,以及LRU cache的读写。bilibili-video, git-commit
2021-05-22 验证LRU cache的正确性,用一个Python脚本对比cache hit, miss, evict。对复杂度为O(n^3)的矩阵乘法的常数级优化,利用时间、空间局部性,写cache-friendly代码。bilibili-video, git-commit
2021-05-24 线程级并行计算时,多处理器之间维护cache的一致性(coherence)。经典的MESI协议的实现:M – Exclusively Modified; E – Exclusively Clean; S – Shared Clean; I – Invalid. bilibili-video, git-commit
2021-06-06 完善MESI协议的实现。拥有了多处理器之间的cache一致性,我们可以开始考虑多处理器多线程的资源共享问题。资源共享是各种问题的根源,但即便我们不考虑资源是否被正确地共享,由于MESI等cache协议,不恰当的共享会立即给性能带来极大的消耗。我们做一个简单的实验,多线程并行计算时,对同一个、相邻的、相距很远的物理地址进行读写,会得到不同的运算时间。这就是经典的伪共享(False Sharing)问题。 bilibili-video, git-commit
Chapter 09 – Virtual Memory
2021-06-14 开始考虑虚拟内存(Virtual Machine)。对于先前的va2pa
映射,我们考虑四种维护虚拟地址到物理地址的映射,并且要求都是O(1)时间复杂度:1. 使用%
,对物理内存范围取余数,这也是目前为止,我们代码所采用的映射方法;2. 使用Hash Map,维护一个地址到地址的映射;3. 使用段表(Segment Table);4. 使用页表(Page Table)。使用页表时,页表本身的存放也要占据物理内存,因此我们需要想办法减少页表本身占据的空间,否则我们要分配2^36条页表项,维护每一个虚拟页(Virtual Page)与物理页(Physical Page)之间的映射。这个方法就是多级页表。 bilibili-video, git-commit
2021-06-20 使用多级页表实现va2pa
的映射。为此,我们首先需要确定每一个页表项(Page Table Entry)都储存怎样的信息。每一个VP与PP都占据4KB的大小,而一整张页表也同样这么大。4KB的一张页表维护512个次级页表的物理页号(Physical Page Number),每一项大小为8 Bytes。这8 Bytes,64 Bits中,几乎每一个Bit都有自己的含义,用于标识与控制。至此,我们大概了解了页表的数据结构,其实本质上是一个512叉树。对于这512叉树,它的根节点PPN通过控制寄存器CR3
索引确定。 bilibili-video, git-commit
2021-06-27 开始实现四级页表的地址翻译,Page Walk。Page Walk从进程(Process)保存的CR3
寄存器开始,这些寄存器信息也被称为进程的上下文(Context)。也就是说,进程的上下文其实包含了这个进程的地址空间(指针,四级页表还是被内核Kernel分配在物理内存上的,并且不会被Swap Out到磁盘)。从最顶级的Page Global Directory开始,按照VPN索引当前页表,走向下一级页表。这个过程其实和树的深度遍历类似。如果发生了缺页,我们需要触发Page Fault Exception,应用程序请求内核处理缺页异常。当然,目前我们并没有实现中断处理(Interrupt),因此我们暂时把异常处理直接写在代码里。bilibili-video, git-commit
2021-07-11 开始实现Page Fault处理。我们考虑内核在什么时候发现Page Fault。在Page Walk的过程中,如果页表项最低位的present
比特为0
,那么就发生了缺页。和SRAM的LRU Cache一样,我们需要从现有的物理内存选择一个Page,用来存放缺少的这个Page。可是,如果所有的物理内存都已经被占用,没有空闲的物理页,那么我们就要按照LRU的策略选择一个倒霉蛋的物理页[i]
,牺牲它的数据,并且将目标数据写进这个物理页[i]
。但是,倒霉蛋[i]
可能正在被其他进程[j]
所使用,如果我们要牺牲倒霉蛋,就必须通知进程[j]
,将倒霉蛋[i]
的数据存放到其他位置,才能方便进程[j]
之后回复当前物理页[i]
的数据。
这就是Swap的过程。物理内存作为磁盘的缓存,如果当前的进程[t]
发生了缺页,内核决定牺牲进程[j]
物理页[i]
,那么就要将物理页[i]
的数据写入磁盘,然后才将进程[t]
的数据写入物理页[i]
。同时,内核需要修改进程[j]
的页表,使得[j]
仍能通过页表索引到磁盘上的数据。也就是说,我们需要通过物理页的页号(Page Number)[i]
查找到进程[j]
的页表项,这个过程就是页表的反向映射(Reversed Mapping),是从物理地址到虚拟地址的映射。我们通过一个数组来维护这个映射。bilibili-video, git-commit
2021-07-17 大概介绍一下整个虚拟内存系统。主要有两个映射,一个是虚拟地址到物理地址的正向映射,另一个是从物理地址到虚拟地址的反向映射。当然,实际情况会复杂许多。例如我们先前讨论过动态链接,一个动态共享库在运行时可以被多个程序共同引用,但它只占有一份物理内存。那么,共享库的物理地址要反向映射到哪一个进程的虚拟地址呢?这个问题就涉及到了内存映射(Memory Mapping),内核将共享库的文件加载到内核时,反向映射并不是很直白简单地实现的。
另一种情况是匿名的内存页,也就是Stack与Heap,是进程所私有的。与Readonly的.text
段不同,Stack与Heap是动态创建和管理的,无法与一个磁盘上的文件对应,因此它们被视为交换磁盘的缓存。它们不通过文件,而通过另一种方式实现反向映射。
我们讨论虚拟内存系统,主要集中在正向映射和反向映射,这样才能对Page Fault有比较清楚的了解。从进程的数据结构出发,到地址空间的数据结构,再到四级页表,虚拟内存区域(Virtual Memory Area),到物理内存的数据结构page_t
,到它们之间的映射关系。bilibili-video
2021-07-31 粗略了解了虚拟内存,我们才能开始实现Page Fault的简单处理。特别注意,这里的Page Fault处理只是我们的第一步,较为完整的Page Fault实现需要和进程的内存管理结合起来实现。为了处理Page Fault,我们需要实现Swap,也就是交换空间。Swap将物理内存(DRAM)看作磁盘(SSD或HDD)的上一级缓存,因此我们要按照SRAM Cache的方式,用LRU的策略实现Page的Swap In与Swap Out。bilibili-video, git-commit
2021-08-08 Page Table被看作对disk的缓存,那么,我们可以再加一层缓存用来加速。这就是TLB(Translate Lookaside Buffer)的作用。TLB是CPU内的硬件模块,用来做地址翻译的硬件加速,缓存了虚拟地址与物理地址之间的映射关系。它在代码中的实现与我们之前实现的SRAM cache很相似。不同的是,TLB没有采用LRU的替换策略,而是随机选择一个受害者牺牲。bilibili-video, git-commit
2021-08-21 到此为止,我们已经大概讨论了内核中的内存管理。接着,我们简单讨论一下用户态中的动态内存管理。用户地址空间中,.text
, .data
, 以及stack,是不需要特别管理的。只有heap,也就是动态内存,需要我们特别维护,以免发生内存泄漏,这一点对服务器等长时间运行的程序尤为重要。用户态的Heap可以看作一个内存池,一个缓存,它一次性向内核申请若干物理页以供分配,然后按请求提供。我们将整个heap划分成若干block,用链表进行管理。这就是implicit free list。bilibili-video
2021-08-28 实现implicit free list,以及相应的malloc()
与free
。implicit free list的原理比较简单,但是很多细节,特别是地址对齐,非常容易搞错,所以要细心一些。bilibili-video, git-commit
发表回复