Linux系统编程-计算机体系结构基础

一、概述

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

二、内存与地址

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

使用时根据房间编号找到相应的邮箱,然后投入信件或取出信件。内存与此类似,每个存储单元有一个地址(Address),CPU 通过地址找到相应的存储单元,取其中的指令,或者读写其中的数据。与邮箱不同的是,一个地址所对应的存储单元不能存储很多东西,只能存一个字节,所以类似 int,float 等多字节的数据保存在内存中要占用多个地址,这种情况下把起始地址当作这个数据的地址

内存地址是从0开始编号的整数,最大编到多少取决于CPU的地址空间(Address Space)有多大。目前主流的处理器是32位或64位,这里主要以32位的x86平台为例,所谓的32位就是指地址是32位的,从0x0000 0000 到0xffff ffff。

三、CPU

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

1. 寄存器(Register),是CPU内部的高速存储器,像内存一样可以存取数据,但比访问内存快得多。我们马上会讲到 x86的寄存器如eax、ebp、eip 等等,有些寄存器保存的数据只能用于某种特定的用途,比如eip寄存器用作程序计数器,这称为特殊寄存器(Special purpose Register),而另外一些寄存器保存的数据可以用在各种运算和读写内存的指令中,比如eax寄存器,这称为通用寄存器(General-purpose Register)。

 2. 程序计数器(PC,Program Counter),保存着CPU取指令的地址,每次CPU读出程序计数器中保存的地址,然后按这个地址去内存中取指令,这时程序计数器保存的地址会自动加上该指令的长度,指向内存中的下一条指令

程序计数器通常是CPU的一个特殊寄存器,x86的程序计数器是特殊寄存器 eip,由于地址是32位的,所以这个寄存器也是32位的,事实上通用寄存器也是32位的,所以也可以说处理器的位数是指它的寄存器的位数。处理器的位数也叫做字长,字(Word)这个概念用得比较混乱,在有些上下文中指16位,在有些上下文中指32位(这种情况下16位被称为半字Half Word),在有些上下文中指处理器的字长,如果处理器是32位那么一个字就是32位,如果处理器是64位那么一个字就是64位。

3. 指令解码器(Instruction Decoder)。CPU取上来的指令由若干个字节组成,这些字节中有些位表示内存地址,有些位表示寄存器编号,有些位表示这种指令做什么操作,是加、减、乘、除还是读、写,指令解码器负责解释这条指令的含义,然后调动相应的执行单元去执行它。

4. 算术逻辑单元(ALU,Arithmetic and Logic Unit)。如果解码器将一条指令解释为运算指令,就调动算术逻辑单元去做运算,比如加减乘除、位运算、判断一个条件是否成立等。运算结果可能保存在寄存器中,也可能保存到内存中

5. 地址和数据总线(Bus)。CPU和内存之间用地址总线、数据总线和控制线连接起来,32位处理器有32条地址线和32条数据线[24] ( 这个说法不够准确,你可以先这么理解,稍后在介绍MMU时再详细说明),每条线上有1和0两种状态,32条线的状态就可以表示一个32位的数。如果在执行指令过程中需要访问内存,比如从内存读一个数到寄存器,则执行过程可以想像成这样:

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

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

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

四、设备

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调用

五、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)。异常的处理过程和中断【中断服务程序ISR】类似,只不过中断是由外部设备产生的,而异常是由CPU内部产生的,中断产生的原因和CPU当前执行的指令无关,而异常的产生就是由于CPU当前执行的指令出了问题,例如访问内存的指令被MMU检查出权限错误,除法指令的除数为0等

“中断”和“异常”这两个名词用得也比较混乱,不同的体系结构有不同的定义,有时候中断和异常不加区分,有时候异常包括中断,有时候中断包括异常。在本书中按上述定义使用这两个名词,中断的产生与指令的执行是异步的,异常的产生与指令的执行是同步的。

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

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

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

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

六、Memory Hierarchy

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

Memeory Hierarchy

CPU寄存器

  • 位置:位于CPU执行单元中。
  • 存储容量:CPU寄存器通常只有几个到几十个,每个寄存器的容量取决于CPU的字长,所以一共只有几十到几百字节。
  • 半导体工艺:“寄存器” 这个名字就是一种数字电路的名字,它由一组触发器(Flip-flop)组成,每个触发器保存一个Bit的数据,可以做存取和移位等操作。计算机掉电时寄存器中保存的数据会丢失。
  • 访问时间:寄存器是访问速度最快的存储器,典型的访问时间是几纳秒。
  • 如何访问:使用哪个寄存器,如何使用寄存器,这些都是由指令决定的。

 Cache

  • 位置:和MMU一样位于CPU核中。
  • 存储容量:Cache 通常分为几级,最典型的是如上图所示的两级Cache, 一级Cache更 靠近CPU执行单元,二级Cache更靠近物理内存,通常一级Cache有几十到几百KB,二级Cache有几百KB到 几MB。
  • 半导体工艺:Cache 和内存都是由RAM(Random Access Memory)组成的,可以根据地址随机访问,计算机掉电时RAM中保存的数据会丢失。不同的是,Cache 通常由SRAM(Static RAM,静态RAM)组成,而内存通常由DRAM(Dynamic RAM,动态RAM)组成。DRAM电路比SRAM简单,存储容量可以做得更大,但DRAM的访问速度比SRAM慢。
  • 访问时间:典型的访问时间是几十纳秒。
  • 如何访问:Cache 缓存最近访问过的内存数据,由于Cache 的访问速度是内存的几十倍,所以有效地利用 Cache 可以大大提高计算机的整体性能。一级Cache是这样工作的:CPU执行单元要访问内存时,首先发出VA,Cache 利用VA查找相应的数据有没有被缓存,如果Cache中有就不需要访问物理内存了,是读操作就直接将Cache中的数据传给 CPU寄存器,是写操作就直接在Cache中改写数据;如果 Cache 中没有,就去物理内存中取数据,但并不是要哪个字节就取哪个字节,而是把相邻的几十个字节都取上来缓存着,以备下次用到,这称为一个Cache Line,典型的 Cache Line 大小是32~256字节。如果计算机还配置了二级缓存,则在访问物理内存之前先用PA去二级缓存中查找【在没有获取到PA之前怎么用PA查找】。一级缓存是用VA寻址的,二级缓存是用PA寻址的,这是它们的区别。Cache 所做的工作是由硬件自动完成的,而不是像寄存器一样由指令决定先做什么后做什么。

内存

  • 位置:位于CPU外的芯片,与CPU通过地址和数据总线相连。
  • 存储容量:典型的存储容量是几百MB到几GB。
  • 半导体工艺:由DRAM组成,详见上面关于 Cache 的说明。典型的访问时间是几毫秒,是寄存器的106倍。访问时间:典型的访问时间是几百纳秒。
  • 如何访问:内存是通过地址来访问的,但是在启用MMU的情况下,程序指令中的地址是 VA,而访问内存用的是 PA,并无直接关系,这种情况下内存的分配和使用由操作系统通过修改MMU的映射项来协调。

硬盘

  • 位置:位于设备总线上,并不直接和CPU相连,CPU 通过设备总线的控制器访问硬盘。
  • 存储容量:典型的存储容量是几百GB。
  • 半导体工艺:硬盘由磁性介质和磁头组成,访问硬盘时存在机械运动,磁头要移动,磁性介质要旋转,机械运动的速度很难提高到电子的速度,所以访问速度很受限制。但是保存在硬盘上的数据掉电后不会丢失。
  • 访问时间:典型的访问时间是几毫秒,是寄存器的106倍。
  • 如何访问:由驱动程序操作设备总线控制器去访问。由于硬盘的访问速度较慢,操作系统通常在一次从硬盘上读几个页面(典型值是4KB)到内存中缓存起来,如果这些数据后来都被程序访问到了,那么这一次硬盘访问的时间就可以分摊(Amortize)给多次数据访问了。

对这个表格总结如下:

1. 寄存器、Cache 和内存中的数据都是掉电丢失的,这称为易失性存储器(Volatile Memory),与之相对的,硬盘是一种非易失性存储器(Non-volatile Memory)。

2. 除了访问寄存器由程序指令直接控制之外,访问其它存储器都不是由指令直接控制的,有些是硬件自动完成的,有些是操作系统配合硬件完成的。【这句话的意思是其实内存访问不是由指令直接控制的,而是间接控制的,由寄存器、一级缓存、二级缓存到内存一层一层下发】

3. Cache 从内存取数据时一次取一个Cache Line 缓存起来,操作系统从硬盘取数据时一次取
几KB缓存起来,都是希望这些数据以后会被访问到。大多数程序的行为都具有局部性(Locality)的特点:它们会花费大量的时间反复执行一小段代码【例如循环,这里的意思是会多次执行重复的代码块,比如循环中的代码】,或者反复访问一个很小的地址范围中的数据(例如访问一个数组)。所以预读缓存的办法是很有效的:CPU取一条指令,我把它相邻的指令也都缓存起来,CPU 很可能马上就会取到;CPU 访问一个数据,我把它相邻的数据也都缓存起来,CPU很可能马上就会访问到。设想有两台计算机,一台有32KB的Cache,另一台没有Cache,而内存都是512MB的,硬盘都是100GB的,虽然多出来32KB的Cache和内存、硬盘的容量相比微不足道,但由于局部性原理,有 Cache 的计算机明显会快很多。高速存储器即使容量只能做得很小也能显著提升计算机的性能,这就是Memory Hierarchy的意义所在。

七、总结

MMU是在CPU内核中的,不是在内存的,页表是在物理内存中的,不是在MMU的中。每次执行指令的时候,获取的地址不是一个地址,而是该地址相邻的一小范围的地址都缓存起来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值