写在前面
作为一名身在EE心在CS的不合格EE新生1,CMU的神课Computer Systems:A Programmer’s Perspective一直令我神往,故而下载了教材阅读,并顺便记录笔记供自查与分享。
文中可能会有各种难以理解的名词,它们会在随后的章节中被解释,我之所以不在这里解释是因为我也还不会(。
信息=二进制位+语境
#include <stdio.h>
int main(void){
printf("Hello, World!");
}这是一个最简单的”Hello, World”C程序,计算机在存储它时通过ASCII标准将其中的字符与换行转化为ASCII码,并以ASCII码的形式储存。存储在计算机中的这段程序其实只是一串有顺序的二进制位,这些二进制位的每八位组成一个”字节”,每个字节都代表其中的一个字符。 其他数据也采用了相同的表达方式:在不同的语境中,一串相同的字节可能是整数、浮点、字符串、机器指令、etc。
程序被其他程序翻译为各种形式
C语言是一种高级语言,这也就是说计算机并不能直接执行,而需要其他程序将其翻译为计算机能够执行的机器语言。机器语言以二进制文件储存在磁盘中,以”目标程序”的形式储存。 这个过程叫做编译,由被称为编译系统的一系列程序构成。编译系统主要包括四部分:预处理器、编译器、汇编器和链接器;分别对应编译过程的四步。下面以前面的”Hello, World”为例分别介绍这四步。
预处理阶段
预处理器检测C程序中#开头的部分并生成处理过的新C程序文件,例如在这一程序的编译中,预处理器cpp会检测到
#include<stdio.h>并将stdio.h直接插入到原程序文件中,这一步的处理结果一般会带有.i后缀。编译阶段
编译器会将预处理得到的.i文件翻译为一个汇编语言程序,汇编语言是一种低级语言(但是人类依然还能理解一部分),使用不同的编译器编译不同的语言程序都会先得到汇编程序。汇编程序一般带有.s后缀。
汇编阶段
汇编器会将汇编程序.s文件翻译为二进制形式的机器指令,并将其封装在一个”可重定位目标文件”中,这类文件一般带有.o后缀,人类已经无法直接理解它了。
链接阶段
我们的Hello, World程序中调用了printf函数,但在生成的hello.o文件中并不包括printf的实现。链接器会将存储在系统中的printf.o与汇编得到的hello.o链接在一起,生成最终可供执行的”可执行目标程序”。
编译原理值得了解
理解编译原理有利于: 1. 优化程序性能 2. 理解链接错误 3. 避免安全漏洞
处理器读取并处理内存中存储的指令
我们可以利用Shell来运行我们编写的Hello程序。Shell是一种命令行解释程序,这意味着它接受命令行输入并执行。我们在Shell中输入./hello, Shell会检测到hello是一个可执行文件,并将它加载到内存中后执行。程序执行完成后,Shell会继续请求命令行输入。
计算机系统的硬件组成
总线
总线是沿着整个系统运行的一系列电子线路的集合。总线可以将信息位在元件之间来回传输, 总线一般所传输的信息位数是固定的, 这些字节的组块被称作“语词”, “语词”长度一般为4字节(也就是32位)或8字节(也就是64位)
I/O 设备
I/O设备以输入或输出的方式建立计算机与外界的连接。I/O设备通过控制器或者适配器连接到I/O总线。控制器是主板上的专门芯片,适配器则是一些连接在主板上的设备。
主内存
主内存是一种临时储存设备,在处理器执行程序时会将程序与对应的数据暂存进内存。内存一般采用DRAM(Dynamic Random Access Memory,动态随机访问存储)技术。在软件层面内存会被抽象为一系列线性的数组,并从0开始编号,这一编码被称为内存地址。
处理器
这里的处理器是一般说的中央处理器(CPU),是处理并执行内存中存储的指令的元件。在其中有一个单个语词长度的寄存器,被称作PC(Program Counter,程序计数器)。PC在运行中指向内存中的某条机器语言指令(也就是现在要运行的)。 只要计算机接电,处理器就会不断执行程序计数器所指的那条指令,程序会主动更新程序计时器以便执行下一条命令。处理器根据其指令集(ISA)的不同,具有其独有且唯一的指令执行模型,其中包括执行指令的顺序与执行指令的一系列步骤。 CPU能执行的指令实际上只有简单的几条,它们都围绕着内存、寄存器与ALU(算术/逻辑单元)展开,例如Load(从内存向寄存器加载指令),Store(从寄存器向内存写入数据),Operate(利用ALU执行)及Jump(利用PC加载下一条指令)
Hello程序的执行
向Shell输入./hello后,shell会将程序中的指令与数据加载到内存中。 通过DMA技术,数据可以不经CPU直接写入内存。 在程序加载进内存后,处理器会开始执行内存里的机器码。具体来说,这些用机器码表示的指令会把hello,world\n放进寄存器,然后再从寄存器输出到显示设备。
缓存的重要性
在磁盘与内存之间移动数据往往会消耗很长时间,甚至成为程序运行的性能瓶颈;因此优化这些拷贝工作的耗时非常重要。 由于物理规律的限制,往往容量越大的存储设备速度就会越慢;而快速的存储器不但容量小,造价也十分昂贵。例如硬盘可能有千倍于内存的容量,但同时速度可能只有内存的千万分之一;而处理器的内置寄存器则容量更小,但是速度比起内存还要快上百倍,而且由于处理器性能的发展,这个比例可能还会继续增长。 这就导致内存与寄存器之间产生了巨大的鸿沟,为了填补这个鸿沟,我们引入了缓存作为内存与寄存器之间的缓冲区。缓存一般有2-3级,级数越高越大(并且慢);通过将常用或者即将要再次使用的数据与指令暂存在更靠近处理器、速度比内存快得多的缓存之中,可以使大部分的内存操作都在处理器片内执行,性能大大提高。
操作系统管理硬件
软件并不能直接访问硬件,而要通过操作系统的中介。操作系统作为中间层有两层好处:1.阻挡软件错误使用硬件 2.包装抽象的底层硬件,提高软件通用性。
进程
操作系统将程序抽象为进程来运行,在进程中,每个程序都以为自己是当前系统运行的唯一程序,自己的数据是内存中仅有的数据。一个系统里通常都会有很多进程,它们“见缝插针”地交错运行。 传统系统中,一个CPU理论上只能执行一个线程的任务。而现代操作系统通过“上下文切换”,可以在多个进程之间切换,并保存其各自实时状态。 操作系统的内核是一系列一直在内存里待命的代码,应用程序可以通过System Call来调用它,让它完成读写文件之类的工作。
线程
现代处理器的执行单元被称为线程,一般有很多个。进程正是在线程中运行,每个线程内部都有共享的代码与全局数据。线程也可以看作核心的抽象,但线程间通信明显会比核心间通信方便不少;在多核心系统中,面向线程进行程序设计可以提高运行效率。
虚拟内存
操作系统会将内存抽象为虚拟地址空间。虚拟地址空间被划分为不同的区域,这些区域从下到上分别是:程序的代码与数据、堆、共享库、栈与内核虚拟内存。
文件
(类)Unix系统里,各种I/O设备都被抽象为文件,对它们的操作全部通过文件读写实现,这部分System Call叫作Unix I/O。
系统间的网络通讯
网络也可以被看做一个Unix I/O设备来操作。 两个终端系统中间通过服务器传输数据。 ## 一些重要的概念 ### 阿姆达尔定律
假设系统的一个部分占用系统运行时间的比例为\(\alpha\),而这一部分的性能提升到了原来的k倍,则提速倍数(原耗时/新耗时) \(S=1/((1-\alpha)+\alpha/k)\)
并发与并行
线程级别的并发
传统的并发是在单CPU上实现的,处理器不断地在任务之间切换,模拟出了并发执行的效果,仅通过这种技术便可以实现多任务。 而近年来我们则有了更多选择。一是处理器的核心开始变多,而每个核心都有自己的独立L1、L2缓存,可以真正意义上同时运行多个任务。 二是超线程技术。这种技术可以让一个核心执行多个控制流,传统处理器经过数万个周期才能切换到另一个线程,而带有超线程的CPU核心每个周期都可以选择执行哪个线程的人物,这使得运算单元的空闲时间得到重复呢利用,有效提升了效率。 如果程序编写时针对多线程做了优化,那么即使仅运行这一个程序,并发也会带来性能提升。
指令级别的并行
现代处理器可以在一个周期内同时执行多条指令。
单指令、多数据(SIMD)的并行
现代处理器也可以同时对多组数据进行同样的运算,然后同时得出结果。
抽象的重要性
处理器方面:指令集 操作系统方面: I/O抽象为文件、内存抽象为虚拟地址空间、程序抽象为进程等等
(Finished on Friday 31, October 2025)
碎碎念
今天是2025年10月31号,离我最初开始写这篇笔记似乎已经过去了整整一年。过去的一年里,我从电子信息大一新生变成了AI大二学生;我开了无数个像这篇文章一样的坑,又弃了无数个这样的坑。我无数次地迷茫、无数次地无所适从;无数次地精神崩溃、无数次地怨天尤人。在无数多的坏情绪下走到了今天。 直到最近,我终于下定决心:该从AI与Sys中选一个方向去做了。我花了很多天,告诉自己AI是坑、是泡沫等等等等;但是沉下心来想,任何人都能看出这两个方向现在究竟哪个更吃香,我想的那些理由不过是想合理化自己的喜好。不过我也因此明白了,我真正喜欢的就是Sys方向,我要从我去年看了几眼就放下的CSAPP开始,一点一点补起我的知识体系。 过去一年我走了很多弯路,我现在又重新上路了,我不知道这次是不是弯路,但是这次,我不想再停下来了。
Footnotes
情况发生了一些变化。请参照最后一段。↩︎