今天开始学习《深入理解计算机系统学习笔记这本书》,希望自己能有所长进。

第一章

计算机系统由硬件和系统软件组成。

我们通过研究 hello 程序的生命周期,正式开启对计算机系统的学习之旅。

#include<stdio.h>

int main() {
    printf("hello world");
    return 0;
}

hello.c

信息是位+上下文

hello程序的生命周期是由源程序开始,即通过编辑器创建保存的文本文件,也就是我们熟知的hello.c文件。源程序实际是由0和1组成的位序列。我们知道8位是一个字节,每个字节就可以表示程序中的某些文本字符。

大部分计算机系统使用ASCII码来表示文本字符,这样我们可以使用唯一的单字节大小的整数值来表示每个字符。在hello.c文件中每个字符对应一个整数值,比如第一个字节的整数值是35,它对应的就是字符‘#’。第二个字节的整数值是105,对应的就是‘i'。其中每个文本行都是以’\n'来结束,它对应的整数值是10。像 hello.c 这样只由 ASCII 字符构成 的文件称为文本文件,所有其他文件都称为二进制文件。

程序被其他程序翻译成不同的格式

C语言的形式方便程序员读懂,但是想要在系统上运行hello.c程序,需要把c语句转化为一系列低级机器语言指令。这些指令按称为可执行目标程序的格式打好包,以二进制磁盘文件的形式存放起来,最后的这个目标程序就是可执行目标文件。

在Unix系统上,从源文件到目标文件的转化由编译器驱动程序完成。

linux>

gcc -o hello hello.c

这个命令实际上在后台执行了四个步骤:

预处理(展开宏、头文件等):hello.c(源程序) -> hello.i(修改了的源程序)

编译(将 C 代码转换为汇编代码):hello.i -> hello.s(汇编程序)

汇编(将汇编代码转换为目标文件 .o):hello.s -> hello.o(可重定位目标程序)

链接(将目标文件与库链接成可执行文件):hello.o -> hello(可执行目标程序)

下面来详细讲下这四个阶段:
预处理:处理源代码中以 # 开头的指令(如 #include、#define),展开宏定义,生成 .i 文件。比如第一行的#include<stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并插入到程序文本中,结果就得到了另一个C程序,通常以.i作为程序扩展名。

编译:将预处理后的代码翻译成汇编语言,生成 .s 文件。hello.s中包含一个汇编语言程序,程序中包含函数main的定义:

2-7行的每一条语句以一种文本格式描述了一条低级机器语言指令。

汇编阶段:将汇编代码转换为机器能看懂的二进制机器码(目标文件),生成 .o 文件。hello.o是二进制文件,包含的17个字节是函数main的指令编码。

链接阶段:将生成的目标文件与系统所需的库文件(如 C 标准库)链接在一起,最终生成可以在当前平台上直接运行的可执行文件 hello。比如hello程序调用了printf函数,printf函数存在于名为printf.o的单独的预编译好的目标文件中,这个文件将以某种方式合并到hello.o程序中。链接器就负责这种合并,最后得到hello文件。

编译系统如何工作大有益处

笔者简单的将书中的观点记录下。

  1. 优化程序性能
  2. 理解链接时出现的错误
  3. 避免安全漏洞

处理器读并解释储存在内存中的指令

hello.c源程序已经被翻译成可执行目标文件hello,距离在Unix系统上运行该文件,还需要将文件名输入到shell应用程序中。

Linux:

./hello

这段指令在linux中表示运行的是当前目录下的 hello 文件

输出结果:

hello, world

系统的硬件组成

总线:贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。

通常总线被设计成传送定长的字节块,即字。字中的字节数是基本的系统参数,各个系统不尽相同。一般是4(32位)或8(64位)字节数。

I/O设备:I/O设备是系统与外部世界的联系通道。

每个I/O设备通过一个控制器或适配器与I/O总线相连。控制器与适配器的区别主要是它们的封装方式。控制器是 I/0 设备本身或者系统的主印制电路板(通常称作 主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。

主存:主存是临时存储设备,在执行器处理程序时用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。

处理器:中央处理单元(CPU), 简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器 (PC) 。在任何时刻, PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。 

从系统通电到系统断电,处理器不断执行pc指向的指令,再更新程序计数器,使其指向下一个指令。整个过程中指令按照严格的顺序执行,处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,更新PC,使其指向下一条指令,其中这条指令不一定与刚刚执行的指令相连。

这些简单操作主要围绕主存,寄存器文件和算术/逻辑单元(ALU)进行。

寄存器文件是很小的存储设备,由一些单个字长的寄存器组成,每个寄存器有唯一的名字。ALU计算新的数据和地址值。

下面是一些简单操作:

  1. 加载:从主存复制一个字节或一个字到寄存器,以覆盖寄存器原来的内容。
  2. 存储:从寄存器复制一个字节或一个字到主存的某个位置,以覆盖原来的内容。
  3. 操作:把两个寄存器的内容复制到ALU,ALU 对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。
  4. 跳转:从指令本身抽取一个字,将这个字复制到程序计数器中,覆盖程序计数器中原来的值。

运行hello程序

初始时shell程序执行它的指令,等待我们输入命令。输入"./hello"后,shell将字符逐一读入寄存器,然后存放到内存中。

敲回车确认结束输入后,shell执行一系列指令来加载可执行的hello文件,这些指令将hello文件中的代码和数据从磁盘复制到主存,数据包括最终会被输出的字符“hello world\n"。

也可以通过直接存储器存取(DMA)技术,不通过处理器直接从磁盘到达主存。

当目标文件hello中的代码和数据确认到达主存,处理器开始执行hello中的main程序中的机器语言指令,即将 "hello, world\n" 字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上 。 

我们可以发现,系统做的就是将信息从一个地方转移到另一个地方,那其中的复制就是减慢了工作的开销。有什么方法可以有效减少开销??

最明显的问题是存储器极快但是容量小,造价昂贵。主存速度中等,容量较小,成本适中。针对两者的差异设计出来高速缓存储存器(cache),L1cache直接集成在处理器内部,访问速度几乎和寄存器一样快,L2高速缓存容量更大,通过一条特殊的总线连接到处理器,比L1高速缓存慢5倍。更新的还有三级高速缓存即L1,L2,L3,主要原理就是告诉缓存里存放经常访问的数据,大部分的内存操作可以在高速缓存中快速完成。

操作系统与硬件

操作系统是硬件与应用程序中插入的一层软件,起到沟通二者的作用。操作系统可以防止应用程序失控导致硬件被滥用,也可以提供简单一致的机制控制复杂且不同的硬件设备。

进程与线程

进程是操作系统对正在运行的程序的一种抽象。一个系统可以运行多个进程,进程可以占用计算机的资源,我们常说的并发执行是指进程和进程交错执行。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。处理器在进程中来回切换看上去cpu好像在并发执行多个进程,操作系统实现这种交错执行的机制叫做上下文切换。

上下文指的是操作系统跟踪进程运行所需要的所有状态信息,这里由许多信息包括PC 和寄存器文件的当前值,以及主存的内容。当操作系统决定把控制权从当前进程转移到下一个进程,就会进行上下文切换。保存当前进程的上下文,恢复新进程的上下文,将控制权交给新进程,新进程就会从上次停止的地方开始。在hello函数中的体现就是shell进程运行等待命令输入,当它运行hello程序时shell调用函数来将控制权交给操作系统,操作系统保存shell进程的上下文,创建一个新的 hello 进程及其上下文,然后将控制权传给新的hello进程。 hello进程终止后,操作系统恢复 shell 进程的上下文,并将控制权传回给它, shell进程会继续等待下一个命令行输入。

一个进程通常由多个线程组成,每个线程运行在进程的上下文中,共享该进程的资源。

虚拟内存

虚拟内存是指让每个进程以为自己独占主存。每个进程看到的内存是一致的,称为虚拟地址空间。我们将虚拟地址空间分为5个区,每个区有专门功能,从下往上地址增大。这里从下往上讲解:

代码段(Text Segment / Code Segment):
位于最底部,存放的是编译后的机器指令(即程序代码)。为了防止程序在运行时意外修改自身的逻辑,这个区域通常是只读的。

数据段(Data Segment):
紧跟在代码段之后,用于存放程序中已初始化的全局变量和静态变量。

BSS段(BSS Segment):
通常与数据段相邻,用于存放未初始化的全局变量。操作系统会自动将这块区域的内存清零。
堆(Heap):
位于数据段/BSS段之上。这是程序在运行时动态分配的内存区域(例如通过 C 语言的 malloc 或 free 函数)。堆的内存空间是不固定的,它会随着程序的动态分配需求向上(高地址方向)生长。
栈(Stack):
位于用户空间的最顶部(靠近内核空间)。它由操作系统自动管理,专门用于处理函数调用时的局部变量、函数参数和返回地址。与堆相反,栈是向下(低地址方向)生长的。

内核虚拟内存:位于虚拟地址空间最顶部的区域。隔离与保护:用户程序完全不可见,也绝对不允许直接读写这个区域,更不能直接调用内核函数。如果用户程序试图越权访问,CPU 会触发异常(如段错误 Segmentation Fault)。共享性:与每个进程都有自己独立的“用户空间”不同,内核空间在所有进程中是共享的。当程序需要执行文件读写、网络通信等底层操作时,必须通过“系统调用”陷入内核态,由操作系统代为执行。

并发和并行

并发指一个同时有多个活动的系统,并行指用并发使一个系统运行更快。

线程级并行 (Thread-Level Parallelism, TLP)
核心概念:TLP 是指计算机在同一时间内可以执行两条以上的线程。与指令级并行不同,TLP 是显式表示的,程序通过创建多个独立的执行流(线程)来实现本质上固有的并行。每个线程拥有自己独立的指令、数据、程序计数器和寄存器状态。

主要实现方式:多核处理器在单个芯片上集成多个处理器核心,每个线程在独立的物理核心上并发运行。同步多线程 (SMT)多个线程共享同一个处理器的执行单元和大型寄存器池,通过更高效的资源分配来减少硬件闲置,提高利用率。

指令级并行(Instruction-Level Parallelism, ILP)

核心概念:ILP 是一种在处理器内部开发的隐式并行技术。它的目标是通过并行执行多条无相关性(无数据依赖)的指令,来充分利用处理器的计算能力,提高程序执行效率。这种并行性不需要程序员显式处理,而是由硬件和编译器自动发掘。

单指令多数据并行 (SIMD)

核心概念:SIMD 是一种数据级并行(DLP)技术。其核心原理是通过一个中央控制器向多个处理单元发送同一条指令,使这些单元在同一时钟周期内对多个不同的数据元素执行完全相同的操作。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐