计算机体系结构基础 -- 简单易懂

来源:互联网 发布:威海博优化纤怎么样 编辑:程序博客网 时间:2024/05/25 21:32

现代计算机都是基于Von Neumann体系结构的,不管是嵌入式系统、PC还是服务器。这种体系结构的主要特点是:CPU(CPU,Central Processing Unit,中央处理器,或简称处理器Processor)和内存(Memory)是计算机的两个主要组成部分,内存中保存着数据和指令,CPU从内存中取指令(Fetch)执行,其中有些指令让CPU做运算,有些指令让CPU读写内存中的数据。

1. 内存与地址

我们都见过像这样挂在墙上的很多个邮箱,每个邮箱有一个房间编号。

使用时根据房间编号找到相应的邮箱,然后投入信件或取出信件。内存与此类似,每个存储单元有一个地址(Address),CPU通过地址找到相应的存储单元,取其中的指令,或者读写其中的数据。与邮箱不同的是,一个地址所对应的存储单元不能存很多东西,只能存一个字节,所 以以前讲过的int、float等多字节的数据类型保存在内存中要占用多个地址,这种情况下把起 始地址当作这个数据的地址。
内存地址是从0开始编号的整数,最大编到多少取决于CPU的地址空间(Address Space)有多大。目前主流的处理器是32位或64位的,本书主要以32位的x86平台为例,所谓32位就是指地 址是32位的,从0x0000 0000到0xffff ffff。

2. CPU

CPU总是周而复始地做同一件事:从内存取指令,然后解释执行它,然后再取下一条指令,再解释执行。CPU包含以下功能单元:

  • 寄存器(Register),是CPU内部的高速存储器,像内存一样可以存取数据,但比访问内 存快得多。我们马上会讲到x86的寄存器如eax、ebp、eip等等,有些寄存器保存的数据只 能用于某种特定的用途,比如eip寄存器用作程序计数器,这称为特殊寄存器(Special- purpose Register),而另外一些寄存器保存的数据可以用在各种运算和读写内存的指令 中,比如eax寄存器,这称为通用寄存器(General-purpose Register)。
  • 程序计数器(PC,Program Counter),保存着CPU取指令的地址,每次CPU读出程序计 数器中保存的地址,然后按这个地址去内存中取指令,这时程序计数器保存的地址会自动 加上该指令的长度,指向内存中的下一条指令。 程序计数器通常是CPU的一个特殊寄存器,x86的程序计数器是特殊寄存器eip,由于地址 是32位的,所以这个寄存器也是32位的,事实上通用寄存器也是32位的,所以也可以说处 理器的位数是指它的寄存器的位数。处理器的位数也叫做字长,字(Word)这个概念用 得比较混乱,在有些上下文中指16位,在有些上下文中指32位(这种情况下16位被称为半 字Half Word),在有些上下文中指处理器的字长,如果处理器是32位那么一个字就 是32位,如果处理器是64位那么一个字就是64位。
  • 指令解码器(Instruction Decoder)。CPU取上来的指令由若干个字节组成,这些字节中有些位表示内存地址,有些位表示寄存器编号,有些位表示这种指令做什么操作,是加、 减、乘、除还是读、写,指令解码器负责解释这条指令的含义,然后调动相应的执行单元 去执行它。
  • 算术逻辑单元(ALU,Arithmetic and Logic Unit)。如果解码器将一条指令解释为运算指 令,就调动算术逻辑单元去做运算,比如加减乘除、位运算、判断一个条件是否成立等。 运算结果可能保存在寄存器中,也可能保存到内存中。
  • 地址和数据总线(Bus)。CPU和内存之间用地址总线、数据总线和控制线连接起 来,32位处理器有32条地址线和32条数据线(这个说法不够准确,你可以先这么理解,稍后在介绍MMU时再详细说明),每条线上有1和0两种状态,32条线的状 态就可以表示一个32位的数。如果在执行指令过程中需要访问内存,比如从内存读一个数到寄存器,则执行过程可以想像成这样:

访问内存读数据的过程图:


  1. CPU内部将寄存器对接到数据总线上,使寄存器的每一位对接到一条数据线,等待接收数据。
  2. CPU将内存地址通过地址线发给内存,然后通过另外一条控制线发一个读请求。
  3. 内存收到地址和读请求之后,将相应的存储单元对接到数据总线的另一端,这样, 存储单元每一位的1或0状态通过一条数据线到达CPU寄存器中相应的位,就完成了 数据传送。

往内存里写数据的过程与此类似,只是数据线上的传输方向相反。

3. 设备

CPU执行指令除了访问内存之外还要访问很多设备(Device),如键盘、鼠标、硬盘、显示器 等,那么它们和CPU之间如何连接呢?如下图所示。

有些设备像内存芯片一样连接到处理器的地址总线和数据总线,正因为地址线和数据线上可以 挂多个设备和内存芯片所以才叫“总线”,但不同的设备和内存应该占不同的地址范围。访问这种设备就像访问内存一样,按地址读写即可,和访问内存不同的是,往一个地址写数据只是给设 备发一个命令,数据不一定要保存,从一个地址读出的数据也不一定是先前保存在这个地址的 数据,而是设备的某个状态。设备中可供读写访问的单元通常称为设备寄存器(注意和CPU的 寄存器不是一回事),操作设备的过程就是对这些设备寄存器做读写操作的过程,比如向串口发送寄存器里写数据,串口设备就会把数据发送出去,读串口接收寄存器的值,就可以读取串 口设备接收到的数据。

还有一些设备是集成在处理器芯片中。在上图中,从CPU核引出的地址和数据总线有一端经总线接口引出到芯片引脚上了,还有一端没有引出,而是接到芯片内部集成的设备上,这些设备 都有各自的内存地址范围,也可以像访问内存一样访问,很多体系结构(比如ARM)采用这种 方式操作设备,称为内存映射I/O(Memory-mapped I/O)。但是x86比较特殊,x86对于设备有 独立的端口地址空间,CPU核需要引出额外的地址线来连接片内设备,访问设备寄存器时用特 殊的in/out指令,而不是和访问内存用同样的指令,这种方式称为端口I/O(Port I/O)。

从CPU的角度来看,访问设备只有内存映射I/O和端口I/O两种,要么像内存一样访问,要么用一 种专用的指令访问。其实访问设备是相当复杂的,由于计算机的设备五花八门,各种设备的性 能要求都不一样,有的要求带宽大,有的要求响应快,有的要求热插拔,于是出现了各种适应 不同要求的设备总线,比如PCI、AGP、USB、1394、SATA等等,这些设备总线并不直接 和CPU相连,CPU通过内存映射I/O或端口I/O访问相应的总线控制器,通过它再去访问挂在总 线上的设备。所以上图中标有“设备”的框,可能是实际的设备,也可能是设备总线的控制器。

在x86平台上,硬盘是ATA、SATA或SCSI总线上的设备,保存在硬盘上的程序是不能被CPU直 接取指令执行的,操作系统在执行程序时会把它从硬盘拷到内存,这样CPU才可以取指令执 行,这个过程称为加载(Load)。程序加载到内存之后,成为操作系统调度执行的一个任务,就称为进程(Process)。进程和程序不是一一对应的。一个程序可以多次加载到内存,成为同 时运行的多个进程,例如可以同时开多个终端窗口,每个窗口都运行一个Shell进程,而它们对 应的程序都是磁盘上的/bin/bash。

访问设备还有一点和访问内存不同。内存只是保存数据而不会产生新的数据,如果CPU不去读 它,它也不需要主动提供数据给CPU,所以内存总是被动地等待被读或被写。而设备往往会自 己产生数据,并且需要主动通知CPU来读这些数据,例如敲键盘产生一个输入字符,用户希望 计算机马上响应自己的输入,这就要求键盘设备主动通知CPU来读这个字符并做相应处理,给 用户响应。这是由中断(Interrupt)机制实现的,每个设备都有一条中断线,通过中断控制器连 接到CPU,当设备需要主动通知CPU时就引发一个中断信号,CPU正在执行的指令将被打断, 程序计数器会设置成某个固定的地址(这个地址由体系结构定义),于是CPU从这个地址开始 取指令(或者说跳转到这个地址),执行中断服务程序(ISR,Interrupt Service Routine),完 成中断处理之后再返回先前被打断的地方执行后续指令。比如某种体系结构规定发生中断时跳 转到地址0x0000 0010执行,那么就要事先把一段ISR程序加载到这个地址,ISR程序是由内核 代码提供的,中断处理的步骤通常是先判断哪个设备引发了中断,然后调用该设备驱动程序提 供的中断处理函数(Interrupt Handler)做进一步处理。

由于各种设备的用途各不相同,设备寄存器中每个位的定义和操作方法也各不相同,所以每种 设备都需要专门的设备驱动程序(Device Driver),一个操作系统为了支持广泛的设备就需要 有大量的设备驱动程序,事实上,Linux内核源代码中绝大部分是设备驱动程序。设备驱动程序 通常是操作系统内核里的一组函数,主要是通过对设备寄存器的读写实现对设备的初始化、 读、写等操作,有些设备还要提供一个中断处理函数供ISR调用。

4. MMU

现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需 要MMU(Memory Management Unit,内存管理单元)的支持。有些嵌入式处理器没有MMU, 则不能运行依赖于虚拟内存管理的操作系统。本节简要介绍MMU的作用和操作系统的虚拟内存 管理机制。
首先引入两个概念,虚拟地址和物理地址。如果处理器没有MMU,或者有MMU但没有启 用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(以下称为物理内存, 以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA),如下图所 示。


如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址 称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发 到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示。



注意,对于32位的CPU,从CPU执行单元这边看地址线是32条(图中只是示意性地画了4条地 址线),可寻址空间是4GB,但是通常嵌入式处理器的地址引脚不会有这么多条地址线,因为 引脚是芯片上十分有限而宝贵的资源,而且也不太可能用到4GB这么大的物理内存。事实上, 在启用MMU的情况下虚拟地址空间和物理地址空间是完全独立的,物理地址空间既可以小于也 可以大于虚拟地址空间,例如有些32位的服务器可以配置大于4GB的物理内存。我们说32位 的CPU,是指CPU寄存器是32位的,数据总线是32位的,虚拟地址空间是32位的,而物理地址 空间则不一定是32位的。物理地址的范围是多少,取决于处理器引脚上有多少条地址线,也取决于这些地址线上实际连接了多大的内存芯片。

MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4KB。 例如,MMU可以通过一个映射项将虚拟地址的一页0xb7001000~0xb7001fff映射到物理地址的 一页0x2000~0x2fff,物理内存中的页称为物理页面或页帧(Page Frame)。至于虚拟内存的哪 个页面映射到物理内存的哪个页帧,这是通过页表(Page Table)来描述的,页表保存在物理 内存中,MMU会查找页表来确定一个虚拟地址应该映射到什么物理地址。总结一下这个过程: 

  1. 在操作系统初始化或者分配、释放内存时,会执行一些指令在物理内存中填写页表,然后 用指令设置MMU,告诉MMU页表在物理内存中的什么位置。
  2. 设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换的操 作,地址转换操作完全由硬件完成,不需要用指令控制MMU去做。

我们在程序中使用的变量和函数都有各自的地址,程序被编译后,这些地址就成了指令中的地 址,指令中的地址被CPU解释执行,就成了CPU执行单元发出的内存地址,所以在启用MMU的 情况下,程序中使用的地址都是虚拟地址。一个操作系统中同时运行着很多进程,通常桌面上 的每个窗口都是一个进程,Shell是一个进程,在Shell下敲命令运行的程序又是一个新的进程, 此外还有很多系统服务和后台进程在默默无闻地工作着。由于有了虚拟内存管理机制,各进程 不必担心自己使用的地址范围会不会和别的进程冲突,比如两个进程都使用了虚拟地址0x0804 8000,操作系统可以设置MMU的映射项把它们映射到不同的物理地址,它们通过同样的虚拟地 址访问不同的物理页面,就不会冲突了。虚拟内存管理机制还会在后面进一步讨论。

MMU除了做地址转换之外,还提供内存保护机制。各种体系结构都有用户模式(User Mode)和特权模式(Privileged Mode)之分,操作系统可以设定每个内存页面的访问权限,有 些页面不允许访问,有些页面只有在CPU处于特权模式时才允许访问,有些页面在用户模式和 特权模式都可以访问,允许访问的权限又分为可读、可写和可执行三种。这样设定好之后, 当CPU要访问一个VA时,MMU会检查CPU当前处于用户模式还是特权模式,访问内存的目的 是读数据、写数据还是取指令,如果和操作系统设定的页面权限相符,就允许访问,把它转换 成PA,否则不允许访问,产生一个异常(Exception)。异常的处理过程和中断类似,只不过中 断是由外部设备产生的,而异常是由CPU内部产生的,中断产生的原因和CPU当前执行的指令 无关,而异常的产生就是由于CPU当前执行的指令出了问题,例如访问内存的指令被MMU检查 出权限错误,除法指令的除数为0等。 “中断”和“异常”这两个名词用得也比较混乱,不同的体系结构有不同的定义,有时候中断和异常 不加区分,有时候异常包括中断,有时候中断包括异常。在本书中按上述定义使用这两个名 词,中断的产生与指令的执行是异步(Asynchronous)的,异常的产生与指令的执行是同步 (Synchronous)的。

处理器模式图:

通常操作系统把虚拟地址空间划分为用户空间和内核空间,例如x86平台的Linux系统虚拟地址空间是0x0000 0000~0xffff ffff,前3GB(0x0000 0000~0xbfff ffff)是用户空间, 后1GB(0xc000 0000~0xffff ffff)是内核空间。用户程序在用户模式下执行,不能访问内核中 的数据,也不能跳转到内核代码中执行。这样可以保护内核,如果一个进程访问了非法地址, 顶多这一个进程崩溃,而不会影响到内核和其它进程。CPU在产生中断或异常时会自动切换模 式,由用户模式切换到特权模式,因此跳转到内核代码中执行中断或异常服务程序就被允许 了。事实上,所有内核代码的执行都是从中断或异常服务程序开始的,整个内核就是由各种中 断处理和异常处理程序组成。

我们已经遇到过很多次的段错误是这样产生的:

  1. 用户程序要访问的一个VA,经MMU检查无权访问。
  2. MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码中执行异常服务 程序。
  3. 内核把这个异常解释为段错误,把引发异常的进程终止掉。

访问权限也是在页表中设置的,可以设定哪些页面属于用户空间,哪些页面属于内核空间,哪些页面可读,哪些页面可写,哪些页面的数据可以当作指令执行等等。MMU在做地址转换时顺 便检查访问权限。

5. Memory Hierarchy

硬盘、内存、CPU寄存器,还有本节要讲的Cache,这些都是存储器,计算机为什么要有这么 多种存储器呢?这些存储器各自有什么特点?这是本节要讨论的问题。

由于硬件技术的限制,我们可以制造出容量很小但很快的存储器,也可以制造出容量很大但很慢的存储器,但不可能两边的好处都占着,不可能制造出访问速度又快容量又大的存储器。因此,现代计算机都把存储器分成若干级,称为Memory Hierarchy,按照离CPU由近到远的顺序依次是CPU寄存器、Cache、内存、硬盘,越靠近CPU的存储器容量越小但访问速度越快,下 图给出了各种存储器的容量和访问速度的典型值。

Memory Hierarchy图:

  • 寄存器、Cache和内存中的数据都是掉电丢失的,这称为易失性存储器(Volatile Memory),与之相对的,硬盘是一种非易失性存储器(Non-volatile Memory)。
  • 除了访问寄存器由程序指令直接控制之外,访问其它存储器都不是由指令直接控制的,有些是硬件自动完成的,有些是操作系统配合硬件完成的。
  • Cache从内存取数据时一次取一个Cache Line缓存起来,操作系统从硬盘取数据时一次取 几KB缓存起来,都是希望这些数据以后会被访问到。大多数程序的行为都具有局部性 (Locality)的特点:它们会花费大量的时间反复执行一小段代码(例如循环),或者反 复访问一个很小的地址范围中的数据(例如访问一个数组)。所以预读缓存的办法是很有 效的:CPU取一条指令,我把它相邻的指令也都缓存起来,CPU很可能马上就会取 到;CPU访问一个数据,我把它相邻的数据也都缓存起来,CPU很可能马上就会访问到。 设想有两台计算机,一台有32KB的Cache,另一台没有Cache,而内存都是512MB的, 硬盘都是100GB的,虽然多出来32KB的Cache和内存、硬盘的容量相比微不足道,但由 于局部性原理,有Cache的计算机明显会快很多。高速存储器即使容量只能做得很小也能 显著提升计算机的性能,这就是Memory Hierarchy的意义所在。