计算机系统导论(持续更新)

计算机系统导论(持续更新),第1张

文章目录
  • 概览
    • 理念
    • 五个基本事实
      • 数据表示与计算:int不是整数,float不是实数
      • 机器级原理:你必须懂汇编语言
      • 储存器很重要
      • 性能:不仅仅是渐进复杂度
      • 计算机系统的高级功能
    • 课程内容
  • 计算机系统漫游
    • 信息就是位+上下文
    • 程序被其他程序翻译成不同的格式
    • 处理器读取并解释储存在他内存中的指令
      • 系统的硬件组成
      • Hello World的执行过程
    • 高速缓存与储存器层次
    • *** 作系统管理硬件
      • 进程
      • 线程
      • 虚拟储存器
      • 文件
    • 网络通信
    • 重要概念
      • Amdahl定律(系统性能计算)
      • 并发和并行(Concurrency and Parallelism)
        • 线程级并发
        • 指令级并行
        • SMID并行
      • 抽象
      • 响应时间与吞吐量
      • 执行时间探究
        • 性能与相对性能
        • 测量执行时间
        • CPU时间
        • MIPS与性能度量

概览 理念

计算机科班同学最应该学习的课程:计算机体系结构。

这门课的内容为,如何把硬件(处理器、内存、磁盘驱动器、网络基础设施)和软件( *** 作系统、编译器、库、网络协议)组合起来来支持应用程序的执行,以及程序员如何利用这些特征让程序更高效。

这门课看起来和应用没有关系,但是应用是运行在系统上的,其不可避免地会受到系统的影响,比如出明明你的程序从逻辑上来说一点问题都没有,但是就是会出bug,卡顿之类的,这个时候如果没有计算机体系结构知识,就会对这些问题无能为力,这种感觉是很难受的。

从更长远的角度看,不论是开发岗还是算法岗,学习体系结构知识可以帮助你学习编译器、 *** 作系统、网络、计算机体系结构、嵌入式系统、存储系统知识,让你学的更快,学新框架,新技术的时候一眼就可以看出来原理,因为思想都是互通的。开发岗学了体系知识,就不容易被优化,更容易变成架构师。算法岗/研究人员学了,学习框架,配置环境,优化,加速的时候就更得心应手。

简言之,这门课不会帮助你更好的写代码,但是会让你深刻地认识到计算机体系中各个部件的构成,配合,可能出现的问题,以及如何优化,可以让你在面对各种问题的时候都可以从容解决。

本文脱胎于《深入理解计算机系统》,这本教材是卡耐基梅隆大学的教材,写的貌似是可以的,就是看翻译给不给力了,如果翻译不太好应当考虑读英文版。另外,理论上这门课应该在大二开,但是考虑到在北理工大一基本没学计算机知识,所以北理工大三开或许和卡耐基梅隆的大二开没啥区别。

五个基本事实 数据表示与计算:int不是整数,float不是实数

如果学过数据的具体表示,就能明白int和float都是有范围的,既然有范围,就不是数学意义上的数了,只能说int和float是计算机用二进制表示出来的,有限的数字。

学过底层数据表示以后,就可以理解第一张图的溢出,以及第二张图的浮点数截断问题。


计算机计算的原理是加法器,如果学过数字逻辑,就知道加法器的电路做法,基于加法器,产生乘法,减法,除法。

因为计算机计算的原理+数字储存的有限性,造成了计算机中的数字系统仅仅是对现实世界的一种模拟,这种模拟毕竟不是现实世界,就会与现实有一点差距,这一点差距平时无关痛痒,但是如果工作内容与这些误差有关,那就需要明确地学习。

机器级原理:你必须懂汇编语言

其实现在的年代,你基本用不上汇编,或者说绝大部分人不会用汇编去写程序,编译器比你做的更好。

但是汇编语言是对机器码(0101)的直接封装,理解汇编语言是理解机器级执行模式的关键,你可以从机器级别了解程序的运行原理,理解程序效率的影响因素,可以个性化地进行性能优化。
所以那一小部分用汇编的人,都是 *** 作系统的设计者,开发语言的设计者,一般来说都是顶级工程师。

还有就是,学计算机的大概都有对信息安全的兴趣,空闲时间做个自娱自乐的黑客也是有趣的事情。

储存器很重要

首先是内存RAM。
虽然在程序中没有规定内存的使用范围,但是内存实际上,在物理上是有限制的,所以编程的时候必然涉及到对内存的分配和管理。

其次是各种储存器,Cache之类的。
不同的储存器性能差距很大,会显著影响程序性能,根据储存系统的特点调整程序可以极大地优化程序性能。

关于内存引用错误,这种错误一般出现在C语言级别的语言中,因为C语言为了追求性能,将系统暴露给了程序员,不做任何的内存保护。内存引用错误一般都很隐晦,很难被找出来,经典错误:

  1. 数组引用超界 Out of bounds array references
  2. 不合法的指针值 Invalid pointer values
  3. 分配和释放内存滥用 Abuses of malloc/free

那数组引用越界举例,引用越界可以导致本不属于数组的数据被数组数据覆盖。进而引发储存的错误,偏偏还不会报错,只会在运行的时候被程序员注意到,怎么这个数就错了呢?

拿着个图举例,结构体里有a数组和一个浮点数b。a数组占据2个地址,浮点数也占2个地址,因为在结构体里,所以是紧挨的。


如果给b赋值3.14,这时给a[0],a[1]赋值都不会影响b,但是给a[2]赋值,给a[3]赋值,就会导致越界,把b占据的内存覆盖一部分,如果进一步越界,可能就会触及到某些不知名空间,导致程序崩溃。

以上只是给出了一种错误情况与其影响,其实还可能有各种错误:

  1. 破坏不知名对象,甚至是程序,系统
  2. 产生一个延迟影响,当时看不出来,过一段时间才出现

要避免这种错误,如下:

  1. 采用Java、Ruby、Python、ML等编程,这些语言都对内存管理的功能进行了封装,但是相应的,还没有比C语言更高效的。
  2. 要么就是用C语言死磕到底,理解可能会发生什么相互影响 Understand what possible interactions may occur
  3. 使用或开发工具来检测引用错误(例如Valgrind) Use or develop tools to detect referencing errors (e.g. Valgrind)
性能:不仅仅是渐进复杂度

性能的影响因素是很多的,从上到下都有,算法,数据表示,过程,循环,以及系统,底层。需要注意的是,其实常数也是有很大影响的,只不过在渐进计数法中忽略了罢了。
所以要精准预测性能是不可能的,比如不同的写代码方式就可以导致10被性能差距。

要想实现对性能的大幅度优化,理解系统是必不可少的,你需要理解:

  1. 程序是如何编译和执行的 How programs compiled and executed
  2. 如何测量程序性能和识别瓶颈 How to measure program performance and identify bottlenecks
  3. 如何改进性能同时不破坏代码的模块性和通用性 How to improve performance without destroying code modularity and generality

比如下面这个图,从逻辑上讲,这两个代码是一模一样的,但是就是切换了一下内外循环,就会导致20倍的速度差距,如果不明白底层原理,不明白内存的读写,是无法看懂为什么的。

计算机系统的高级功能

计算机除了计算,还可以执行多种功能:

  1. I/O
  2. 并发 *** 作
  3. 网络通信
  4. 跨平台兼容
课程内容

课程以计算机体系为核心,采取程序员视角(应用者),而非设计者视角,适合加深理解,虽然配一些Lab,但是真正需要动手开发的并不是很多,难度也不会很高。

下面是课程不同章节的内容与作业:



计算机系统漫游

本章跟踪hello world程序的生命周期,对系统进行一个全流程的简单解析。

首先看一眼注册表。Windows系统有注册表,注册表是按照文件目录结构组织的,很多人不知道注册表有什么用,其实就如他的名字一样,凡是你用过的软硬件设备,凡是经计算机管理的软硬件,都要被记录在注册表中。

具体说,HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Enum里包含了系统控制的各种设备,包括USB,DISPLAY显示器等等,里面记录了这些设备的信息。

可以说,注册表里记录的信息就是物理硬件的抽象。所谓抽象,就是用一些数值表示物理硬件,就如同用一个公式表示物理规律一样。

注册表这里图一乐就好,言归正传。
程序的生命周期是从写代码开始,直到系统调用完成,退出程序。

信息就是位+上下文

所有程序都会有源代码,即源文件。
源文件就是我们平常说的代码,这些源文件记录了程序整体的逻辑,虽然不能直接执行,但是是执行的源头。

#include 

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

你看到的程序是一个一个的文本,一个一个的字符,但是在计算机实际的储存中,很多机器是用ASCII码存的,一个字节储存一个字符:

可以看到,其中不仅仅有代码,还有换行,空格等字符。

进一步讲,计算机储存所有数据都是用0101的二进制来实现的。

这里就可以解释文本文件和二进制文件的区别了,实际上,他们的底层存储都是二进制码,但是文本文件的二进制码都是ASCII编码,而其他文件有的就不是ASCII编码,或者说人家就不是用来表示文本的,所以这些就叫二进制文件。

这里还会有一个疑问,相同的01序列,有可能表示两个不同的东西,是如何区分这一串二进制码表示哪个数据对象的呢?你怎么知道这个文件是二进制文件,还是文本文件呢?

那就是上下文。具体是如何的,可以暂时理解为类似于前后缀的东西,比如0101010和10101001,开头为0就代表文本,开头是1就是二进制(这个例子是我的假想)

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

C语言源代码是程序的高级表示,是人能读懂的,要想机器执行,就要转换成机器可以读懂的格式,即机器语言指令。将这些指令按照某种规格打包,就形成了可执行文件(比如windows的exe)。当然,这个可执行文件仍然是二进制磁盘文件。

具体过程由四步组成,这四步共同构成了编译系统。这四步每一步都把文件进行转化,生成一个中间文件,后缀不同,直到最终生成可执行文件。

  1. 预处理阶段。将C语言中所有预处理指令都处理了,生成.i文件,这个文件相当于一份完整的C语言源代码。C语言中,带#符号的指令都是预处理器指令,比如#include就是导入指令,这个预处理指令把对应的文件直接复制粘贴到对应位置。还有其他指令,比如#define 这个是进行宏替换。
  2. 编译阶段。编译器.i文件中的C语言转化成汇编语言,生成.s。汇编语言是对所有机器统一的,但是编程语言不是。对于不同的平台,不同的语言,可以使用不同的编译器把源代码转换成统一的汇编语言,比如C和Fortran的Hello world程序写法不同,但是汇编代码一样。
  3. 汇编阶段。将汇编语言转化成机器指令。生成.o目标文件
  4. 链接阶段。会便于这里的机器指令还不完善,因为#include只是告诉你去哪里找printf函数,但是printf函数具体怎么用机器语言执行,你是不知道的。在指定的地方,存在着预编译好的printf.o的文件,所以要把printf.o文件拼接到前面的.o文件中,补全.o文件,补全后一打包就成了可执行文件。

这一系列流程,现在都已经被整合,封装的很完善了,比如你在vs里写代码,直接F5就可以运行,不需要你去手动调用编译系统,他一下都执行完了。那我们为什么还要理解编译系统呢?

  1. 优化程序性能。就比如前面那个内外循环,虽然编译器可以为我们优化,但是优化的程度毕竟是有限的,更多时候需要我们思考怎么做才能让编译器优化到最好,换句话说,现在我们是将军,要指挥士兵。
  2. 理解链接错误。刚学C语言时,经常会写一大堆bug,最尴尬的是连编译都过不了,这个时候看报错就可能会有一些诸如“未解析的引用”,之类的错误,此时就需要链接相关的知识了。
  3. 避免安全漏洞。大部分网络安全漏洞出现在缓冲区溢出上,缓冲区是底层知识。
处理器读取并解释储存在他内存中的指令

此时已经有一个exe文件,里面都是二进制的指令信息,而这个exe是放在磁盘的。
在了解后续流程之前,需要先了解一点微机知识。

系统的硬件组成

这个知识我不会细说了,因为都在我的另一个文章里写了:

汇编语言与接口技术笔记

我这里只是简单的复述一下,再补充一点关于CPU指令的知识:

  1. 总线。总线是在主板上各个部件之间通信的通道,这个通道的宽度是固定bit数,比如32(4字节),64(8字节),不同的硬件有不同的宽度,但是宽度都称作字长,而一个字长的数据块就是一个“字”。
  2. I/O设备。磁盘严格意义上来说是算外部设备的,所以要通过通道加载到内存里。
  3. 主存。一般叫内存,是一种临时储存设备,这是缺点,优点就是速度快。从物理上来说,他是由一系列DRAM芯片组成的,DRAM可以实现动态随机存取,这就是他的速度来源,逻辑上把这些DRAM合并为一条字节数组,每一个字节都有地址。
  4. 处理器。负责计算,解释,执行指令。处理器内部也有储存设备,叫寄存器,很小,但是速度是和CPU计算同频的,没有更快的了。CPU的核心是一个叫程序计数器(PC)的储存器,PC的任务就是指向某条机器语言指令(PC存了个指针)。
  5. 从通电开始,CPU就在不断执行PC指向的指令。这时你可能会问,PC怎么更新,不更新就永远在重复执行一个指令了。更新的原理也很简单,就是PC指向的指令,不仅仅有当前指令需要做的01码,还有与PC地址切换相关的01码,在执行完当前指令相关的步骤后,这个指令还会令PC指向下一条指令,这样就无穷无尽,直到断电为止。
  6. 说到指令,指令就是一些关于主存,寄存器,计算,逻辑相关的01码,指令的数量并不多,有的也就100-200,加起来叫指令集。
  7. 具体说,指令可以执行加载(内存到寄存器),储存(寄存器到内存), *** 作(把两个寄存器的内容放到ALU上计算,并放回到指定寄存器上),跳转(从指令中抽取一个字,更新PC)
  8. 现代指令集已经很复杂了,所以把指令集的逻辑架构和实际实现分开了,逻辑上,指令集架构只需要描述一条指令可以干什么,理解为接口定义,而实际上,微体系结构描述了指令集如何实现

Hello World的执行过程

首先读入./hello指令(这是个加载指令)。
这个指令告诉CPU我要执行hello程序。
读入的顺序为:输入设备——桥芯片——寄存器——内存。
我是比较奇怪为什么不直接放到内存里?但是可能这两个之间没有直接通路,又或者CPU还需要处理一下。
实际上是要处理一下。

之后加载。
就是把磁盘中的二进制可执行程序(比如exe文件)以及可执行程序需要用到的数据,加载到内存中,开始执行。
这一步是磁盘直接到主存的,是通过DMA技术实现的。

最后执行。
执行的时候,CPU从内存中读取指令执行,同时不断更新PC,跳转指令。
这一步的输入是内存,输出有很多,比如内存,显示器(比如printf函数)

这里提一点,显示器在显示内容之前,要先把内容加载到显存之内。

高速缓存与储存器层次

在程序执行中,底层会执行大量的数据传输工作,那么数据传输,储存速度就制约了系统的性能。
再加上CPU越来越快,数据储存传输速度也就显得越发慢了。

在现有技术背景下,因为大容量的必然就慢(想一想CSDN写文章的时候,文章越长,就越卡),而想要提高速度,成本就又会提升,快速材料很贵。所以自然而然就产生了分级储存+缓存的想法。就比如CSDN写文章,长文章不直接写,而是先写到缓存文章中,然后粘贴到主文章去。

总而言之,外部和主板有差距,主板和CPU也有差距,且越是计算要求高的部位,越是频繁存取的部位,对容量的要求反而就没那么高,所以从外向内,容量越来越小,但是速度是越来越快。

  1. CPU里面的寄存器和CPU同频
  2. CPU里的Cache L1高速缓存,以及与CPU以特殊方式相连的L2高速缓存稍微慢一些,这L1-3的Cache采取SRAM技术实现,相比于DRAM(对应主存),S代表Static,比Dynamic更快。
  3. 更外面就是主存
  4. 之后是磁盘,也就是外行经常说的“内存”,比如512G的电脑,就是指磁盘,实际上严格意义上来说应该叫外存。
  5. 最后就是云计算储存资源,从本地到云要走网络,更慢。

*** 作系统管理硬件

前面执行hello程序其实是在另一个程序中的,即shell程序。
shell本身也是个程序,但shell,以及hello其实都不直接和硬件打交道,在应用程序与硬件之间还插了一个 *** 作系统, *** 作系统定义了软件与硬件之间的接口,防止一些物理性的破坏,比如烧了机器等等。

在应用程序看来,他是接触不到处理器和主存以及I/O设备的,这些都被 *** 作系统用另一种概念包装起来了,或者说提供给用户(软件)一种视图。

  1. 一切I/O都看做是文件,对文件的读写就是I/O的传输
  2. 把涉及到数据传输的部件:主存+I/O都封装成虚拟内存,总是就是都可以存数据
  3. 把一个程序要用到的所有的硬件接口都选择性地包装在进程中,一方面方便用户使用,同时也会限制用户做一些有害于硬件的 *** 作。


之后就对这些抽象视图进行解释

进程

现代 *** 作系统把一个又一个任务包装到一个又一个进程中,比如hello就是一个任务,也是一个进程。

进程可以同时有多个,这就是并发。就像你可以同时听音乐+写文章。

虽然看起来是同时运行的,但是那么多任务(至少100个),处理器却只有那么几个(比如我现在的8核电脑),也就是说一个CPU上会同时有好几个任务。但是实际上CPU是单线程的,CPU不能同时做两个任务,那为什么CPU看起来是并发的呢?只能有一种解释了:不同的任务在CPU上交错执行,比如先执行A的一步,再切到B执行一步,再切回A。

进程交错切换的技术叫上下文切换技术

上下文储存了程序执行的状态。这很好理解,就像游戏里的存档,没有上下文,你切回来的时候怎么恢复原来的状态? *** 作系统每一次切换进程的时候,都是先保存当前上下文(存档),然后恢复新进程的上下文(读档)

线程

把进程切分,就变成了线程。

进程可以理解为,不同的人干不同的任务。而线程,是一群人干一个任务。具体技术比较麻烦,后面会讲。

虚拟储存器 文件 网络通信

云计算本质上就是多进程通过网络通信形成的并发。

重要概念 Amdahl定律(系统性能计算)

现代程序运行在一个大的系统上,因此系统的各个部分都可以影响程序性能。

并发和并行(Concurrency and Parallelism)
  1. 并发:同时(concurrency)+多个活动。这是个通用的概念
  2. 并行:用并发技术使系统更快,更多的指平行(parallelism)
线程级并发

一个CPU上可以实现进程级并发。

一个进程一般是有很多步骤的,包括计算+I/O

在最开始,当一个进程占据CPU的时候,程序还是顺序执行的。假如一个程序要先计算再储存,那么在程序计算的时候,IO还是空闲的。(此时因为CPU被占据,其他进程还在休眠)

如果可以把一个程序拆开,这就是线程。可以想到,一个程序拆了是很不容易的,但是至少可以把计算和IO分离,计算作为主线程,IO作为子线程。

在进程+线程的背景下,执行程序就变成了一个进程对应一群人。进程之间的切换就是一群人之间的切换。在一个进程占据CPU的时候,一群人同时做一种事情,但是内部还有具体分工,计算的计算,IO的IO,这就是线程。再回归进程级别,进程切换也可以理解为一些线程切换到另一些线程。

多核出现以后,伴随超线程。

指令级并行

CPU是顺序执行的,但是并不代表一次不能执行多条指令。

CPU一次性执行多条指令也算一种并行。

在指令集并行出现之前,一般来说一个指令需要好多个时钟周期,但是有了指令集并发之后,从宏观来说,一条指令可能只需要一个时钟周期就执行完了。(微观来说,虽然一个人干的慢,但是同时有3个人干,即指令集并发,效果上就相当于一个人有了原来三倍的速度,即效果上加速一条指令的速度)

是否还能加速呢?当加速到一条指令都不需要一个时钟周期就可以执行完的时候,就叫做超标量处理器了。

SMID并行

单指令,多数据流并行。

这个技术通常在GPU上有,所以GPU的并行能力是很强的。

抽象

抽象是计算机中最重要的概念。

抽象是一个很广泛的概念,和我们中国的抽象还不是特别一样。计算机中的抽象更多的是一种封装,把底层细节包装起来,封装了以后再暴露接口(视图)。

之所以重要,是因为从最基本的01,不断抽象,形成现在的计算机世界,其中工作量之大,单纯用01实现是不可能的,只有逐层封装抽象,才能让系统开发更有效率。

从底层01到计算机系统应用有几层抽象:

  1. 指令集体系结构提供实际处理器硬件的抽象 the instruction set architecture provides an abstraction of the actual processor hardware.
  2. *** 作系统提供三个抽象:文件作为I/O设备抽象、虚存作为程序内存的抽象、进程作为运行程序的抽象 OS provides three abstractions: files as an abstraction of I/O devices, virtual memory as an abstraction of program memory, and processes as an abstraction of a running program.
  3. 新抽象:虚拟机提供整个计算机的抽象,包括OS、处理器和程序 a new one: the virtual machine, providing an abstraction of the entire computer, including the operating system, the processor, and the programs.
响应时间与吞吐量
  1. 响应时间。完成任务消耗的时间
  2. 吞吐量。吞吐量根据场景不同具有不同的意义,大致理解为执行速度,比如CPU吞吐可以理解为单位时间完成进程/事务的数量,网络传输吞吐可能就是2G/s这种。

吞吐速度的提升,本质上就是性能的提升,速度的提升。

吞吐速度上去了,响应时间自然就快了,也就不卡了。

执行时间探究 性能与相对性能

衡量程序性能一般用时间的倒数。很朴素。

相对性能就是性能之比,就是运行时间的反比。

测量执行时间

那问题来了,时间怎么测量?

简单用time类去测是不准确的。因为time类是软件部分,在软件执行之前还有系统硬件的各种 *** 作,并且进程/线程之间也是有互斥的,time进程(线程)甚至可能被搁置,休眠。

实际上,程序经历的时间包含了很多方面,计算系统时间是一个复杂的工作。

CPU时间

CPU时间是最规则的,就是时钟。

因为时钟频率是固定的,直接用时钟周期数/频率就是任务消耗在CPU上的时间。

所以性能改进可以减少时钟周期数量,也可以提高CPU频率。

这不是一个解方程问题。
时钟频率=时钟数量/时钟时间
A时钟频率=1个单位的时钟周期/10秒
B时钟频率=1.2个单位的时钟周期/6秒
所以B时钟频率/A时钟频率=2,所以B的频率就是 2 × 2 G H z 2\times 2GHz 2×2GHz

前面说时钟周期数/频率,那么问题来了,时钟周期数怎么算?

时钟周期数=指令数×CPI

这个CPI其实是一个平均值,因为一个指令集里面的指令与指令需要消耗的时钟周期是不同的。

所以要么减少指令数,要么就减少CPI,即加快指令处理速度。


A的周期小,但是单指令消耗周期多,B的周期大,但是单指令消耗周期小。

比较AB的速度,可以直接比执行一条指令消耗的时间=CPI×时钟周期

进一步了解CPI。

CPI是一个平均值,但是加权平均其实是更加精确的,比如一个任务里某个指令执行的出现率很高,就应该给他高的权值。权值=该指令出现次数/所有指令的次数总和

下图给出两个不同的任务,分别计算器CPI。

最后进行总结:

CPU时间=每个程序的指令数 X 每条指令的时钟周期数 X 每个时钟周期的时间
本质上就是这三个在影响,从写代码,到编译,到指令集架构,到CPU硬件时钟周期,都可以影响CPU时间。

MIPS与性能度量

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/2991625.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-23
下一篇 2022-09-23

发表评论

登录后才能评论

评论列表(0条)

保存