OS体系结构

526 阅读23分钟

冯诺依曼模型

冯诺依曼模型定义了计算机的基本结构,由5部分组成:存储器、运算器、控制器、输入与输出设备,运算器、控制器是在中央处理器CPU里的,存储器就是常见的内存,输入输出设备则是计算机外接的设备,存储单元和输入输出设备要与中央处理器打交道离不开总线(传输数据需要数据线)

内存

存储数据的基本单位是字节(byte),1 字节等于 8 位(8 bit)。内存地址从 0 开始编号自增,顺序存储,随机读写

CPU

中央处理器,32 位和 64 位,通常称为 CPU 的位宽, 最主要区别在于一次能计算多少字节数据:32 位 CPU 一次可以计算 4 个字节;64 位 CPU 一次可以计算 8 个字节

CPU 这样设计是为了能计算更大的数值,如果是 8 位的 CPU,那么一次只能计算 1 个字节 0~255 范围内的数值,这样就无法一次完成计算 10000 * 500 ,于是为了能一次计算大数的运算,CPU 需要支持多个 byte 一起计算,所以CPU 位宽越大,可以计算的数值就越大,如 32 位 CPU 能计算的最大整数是4294967295

CPU 内部还有一些组件,常见的有寄存器、控制单元和逻辑运算单元。控制单元负责控制 CPU 工作,逻辑运算单元负责计算,寄存器负责充当cache缓存提升IO性能,常见的寄存器种类:

  • 通用寄存器,用来存放需要进行运算的数据比如需要进行加和运算的两个数据
  • 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址
  • 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里

总线

总线是用于 CPU 和内存以及其他设备之间的通信,总线可分为 3 种:

  • 地址总线,用于指定 CPU 将要操作的内存地址
  • 数据总线,用于读写内存的数据
  • 控制总线,用于发送和接收信号,如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线

当 CPU 要读写内存数据时,一般需要这三个总线:首先要通过「地址总线」来指定内存的地址;然后通过「控制总线」控制是读或写命令;最后通过「数据总线」来传输数据

线路位宽与 CPU 位宽

数据线路通过操作电压传输数据,低电压表示 0,高压电压则表示 1。如果构造了高低高这样的信号,其实就是 101 二进制数据,十进制则表示 5,如果只有一条线路,就意味着每次只能传递 1 bit 的数据,即 0 或 1,那么传输 101 这个数据,就需要 3 次才能传输完成,这样的效率非常低。这样一位一位传输的方式,称为串行,下一个 bit 必须等待上一个 bit 传输完成才能进行传输。当然,想一次多传一些数据,增加线路即可,这时数据就可以并行传输。为了避免低效率的串行传输的方式,线路的位宽最好一次就能访问到所有的内存地址。

CPU 要想操作的内存地址就需要地址总线:

  • 如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种地址,所以 CPU 能操作的内存地址最大数量为 2(2^1)个(能访问的最大内存地址,不是同时能操作 2 个内存地址);
  • 如果地址总线有 2 条,那么能表示 00、01、10、11 这四种地址,所以 CPU 能操作的内存地址最大数量为 4(2^2)个

那么,想要 CPU 操作 4G 大的内存就需要 32 条地址总线, 2 ^ 32 = 4G

CPU 的位宽最好不要小于线路位宽,如 32 位 CPU 控制 40 位宽的地址总线和数据总线的话,工作起来就会非常复杂且麻烦,所以 32 位的 CPU 最好和 32 位宽的线路搭配,因为 32 位 CPU 一次最多只能操作 32 位宽的地址总线和数据总线

如果用 32 位 CPU 去加和两个 64 位大小的数字,就需要把这 2 个 64 位的数字分成 2 个低位 32 位数字和 2 个高位 32 位数字来计算,先加个两个低位的 32 位数字,算出进位,然后加和两个高位的 32 位数字,最后再加上进位,就能算出结果了,可以发现 32 位 CPU 并不能一次性计算出加和两个 64 位数字的结果。 64 位 CPU 就可以一次性算出加和两个 64 位数字的结果,因为 64 位 CPU 可以一次读入 64 位的数字,并且 64 位 CPU 内部的逻辑运算单元也支持 64 位数字的计算。但是并不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少应用需要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来

现代OS大多数为64位架构,32 位 CPU 最大只能操作 4GB 内存,就算装了 8 GB 内存条也没用。 64 位 CPU 寻址范围理论最大空间为2^64

程序执行基本过程

程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU ,CPU 执行程序过程如下:

  1. CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」
  2. CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行
  3. CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。自增大小由 CPU 位宽决定,如 32 位的 CPU指令是 4 个字节,「程序计数器」的值会自增 4

一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 CPU 的指令周期

a = 1 + 2 进一步分析:

CPU 不认识 a = 1 + 2 ,需要先把整个程序翻译成汇编语言,这个过程称为编译成汇编代码,针对汇编代码,还需要用汇编器翻译成机器码,这些机器码是由 0 和 1 组成的机器语言,这一条条机器码,就是一条条的计算机指令

程序编译过程中,编译器通过分析代码,发现 1 和 2 是数据,于是程序运行时,内存会有专门的区域来存放这些数据,这个区域就是「数据段」

  • 数据 1 被存放到 0x100 位置;
  • 数据 2 被存放到 0x104 位置;

数据和指令是分开区域存放的,存放指令区域的地方称为「正文段」,编译器会把 a = 1 + 2 翻译成 4 条指令,存放到正文段中

  • 0x200 的内容是 load 指令将 0x100 地址中的数据 1 装入到寄存器 R0
  • 0x204 的内容是 load 指令将 0x104 地址中的数据 2 装入到寄存器 R1
  • 0x208 的内容是 add 指令将寄存器 R0R1 的数据相加,并把结果存放到寄存器 R2
  • 0x20c 的内容是 store 指令将寄存器 R2 中的数据存回数据段中的 0x108 地址中,这个地址也就是变量 a 内存中的地址;

编译完成后,具体执行程序时程序计数器会被设置为 0x200 地址,然后依次执行 4 条指令。上面的例子中,由于是在 32 位 CPU 执行的,因此一条指令是占 32 位大小,所以每条指令间隔 4 个字节。而数据的大小是根据在程序中指定的变量类型,如 int 类型的数据占 4 个字节,char 类型的数据占 1 个字节

指令

指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容,不同的 CPU 有不同的指令集,也就是对应着不同的汇编语言和不同的机器码

以MIPS 指令集为例,MIPS 的指令是一个 32 位的整数,高 6 位代表着操作码,表示这条指令是一条什么样的指令,剩下的 26 位不同指令类型所表示的内容也就不相同,主要有三种类型R、I 和 J。

  • R 指令,用在算术和逻辑操作,里面有读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有位移操作的「位移量」,而最后的「功能码」则是在前面的操作码不够的时候,扩展操作码来表示对应的具体指令的
  • I 指令,用在数据传输、条件分支等。这个类型的指令就没有了位移量和功能码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或一个常数
  • J 指令,用在跳转,高 6 位之外的 26 位都是一个跳转后的地址;

接下来,把前面例子的这条指令:「add 指令将寄存器 R0R1 的数据相加,并把结果放入到 R2」,翻译成机器码。

加和运算 add 指令是属于 R 指令类型:

  • add 对应的 MIPS 指令里操作码是 000000,以及最末尾的功能码是 100000,这些数值都是固定的,查询 MIPS 指令集手册可知;
  • rs 代表第一个寄存器 R0 的编号,即 00000
  • rt 代表第二个寄存器 R1 的编号,即 00001
  • rd 代表目标的临时寄存器 R2 的编号,即 00010
  • 因为不是位移操作,所以位移量是 00000

把上面这些数字拼在一起就是一条 32 位的 MIPS 加法指令了,用 16 进制表示的机器码则是 0x00011020

编译器在编译程序的时候,会构造指令,这个过程叫做指令的编码。CPU 执行程序的时候,就会解析指令,这个过程叫作指令的解码现代大多数 CPU 都使用来流水线的方式来执行指令,所谓的流水线就是把一个任务拆分成多个小任务,于是一条指令通常分为 4 个阶段,称为 4 级流水线:取指→译码→执行→回写,也称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始

  1. CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令)
  2. CPU 对指令进行解码,这个部分称为 Decode(指令译码)
  3. CPU 执行指令,这个部分称为 Execution(执行指令)
  4. CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写)

事实上,不同的阶段其实是由计算机中的不同组件完成的:

指令类型

指令从功能角度划分,可以分为 5 大类:

  • 数据传输类型指令,如 store/load 是寄存器与内存间数据传输的指令,mov 是将一个内存地址的数据移动到另一个内存地址的指令;
  • 运算类型指令,如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据;
  • 跳转类型指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的 if-elseswitch-case、函数调用等。
  • 信号类型指令,如发生中断的指令 trap
  • 闲置类型指令,如指令 nop,执行后 CPU 会空转一个周期;

指令执行速度

CPU 的硬件参数都会有 GHz 这个参数,如一个 1 GHz 的 CPU,指的是时钟频率是 1 G,代表着 1 秒会产生 1G 次数的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期对于 CPU 来说,在一个时钟周期内,CPU 仅能完成一个最基本的动作,时钟频率越高,时钟周期就越短,工作速度也就越快。

大多数指令不能在一个时钟周期完成,通常需要若干个时钟周期,不同的指令需要的时钟周期是不同的,程序执行时间=指令数CPI时钟周期

  • 指令数,表示执行程序所需要多少条指令,以及哪些指令。这个层面基本靠编译器来优化,同样的代码在不同的编译器下编译出来的计算机指令会有各种不同的表示方式。
  • 每条指令的平均时钟周期数 CPI,表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU 时钟周期数尽可能的少;
  • 时钟周期时间,表示计算机主频,取决于计算机硬件。有的 CPU 支持超频技术,打开了超频意味着把 CPU 内部的时钟给调快了,于是 CPU 工作速度就变快了,CPU 跑的越快,散热的压力就会越大,CPU 会很容易奔溃

存储器

当 CPU 需要访问内存中某个数据的时候,如果寄存器有这个数据,CPU 就直接从寄存器取数据即可,如果寄存器没有这个数据,CPU 就会查询 L1 高速缓存,如果 L1 没有,则查询 L2 高速缓存,L2 还是没有的话就查询 L3 高速缓存,L3 依然没有的话,才去内存中取数据,不同的存储器之间性能差距很大,构造存储器分级很有意义,分级的目的是要构造缓存体系

寄存器

寄存器是最靠近 CPU 的控制单元和逻辑计算单元的存储器,它使用的材料速度也是最快的,因此价格也是最贵的,那么数量不能很多。每个寄存器可以用来存储一定的字节(byte)的数据:

  • 32 位 CPU 中大多数寄存器可以存储 4 个字节;
  • 64 位 CPU 中大多数寄存器可以存储 8 个字节。

寄存器的访问速度非常快,一般要求在半个 CPU 时钟周期内完成读写,CPU 时钟周期跟 CPU 主频息息相关,比如 2 GHz 主频的 CPU,那么它的时钟周期就是 1/2G,也就是 0.5ns(纳秒)。CPU 处理一条指令的时候,除了读写寄存器,还需要解码指令、控制指令执行和计算。如果寄存器的速度太慢,则会拉长指令的处理周期,从而给用户的感觉就是电脑「很慢」

CPU Cache

CPU Cache 用的是一种叫 SRAM(Static Random-Access Memory,静态随机存储器) 的芯片。SRAM 之所以叫「静态」存储器,是因为只要有电,数据就可以保持存在,而一旦断电,数据就会丢失了,用作高速缓存。

在 SRAM 里面,一个 bit 的数据通常需要 6 个晶体管,所以 SRAM 的存储密度不高,同样的物理空间下,能存储的数据是有限的,不过也因为 SRAM 的电路简单,所以访问速度非常快。CPU 的高速缓存,通常可以分为 L1、L2、L3 这样的三层高速缓存,也称为一级缓存、二级缓存、三级缓存。

  • L1 高速缓存的访问速度几乎和寄存器一样快,通常只需要 2~4 个时钟周期,而大小在几十 KB 到几百 KB 不等。每个 CPU 核心都有一块属于自己的 L1 高速缓存,指令和数据在 L1 是分开存放的,所以 L1 高速缓存通常分成指令缓存数据缓存
  • L2 高速缓存同样每个 CPU 核心都有,但是 L2 高速缓存位置比 L1 高速缓存距离 CPU 核心 更远,它大小比 L1 高速缓存更大,CPU 型号不同大小也就不同,通常大小在几百 KB 到几 MB 不等,访问速度则更慢,速度在 10~20 个时钟周期
  • L3 高速缓存通常是多个 CPU 核心共用的,位置比 L2 高速缓存距离 CPU 核心 更远,大小也会更大些,通常大小在几 MB 到几十 MB 不等,具体值根据 CPU 型号而定。访问速度相对也比较慢一些,访问速度在 20~60个时钟周期:

内存

内存用的芯片和 CPU Cache 有所不同,它使用的是一种叫作 DRAM (Dynamic Random Access Memory,动态随机存取存储器) 的芯片。相比 SRAM,DRAM 的密度更高,功耗更低,有更大的容量,而且造价比 SRAM 芯片便宜很多。DRAM 存储一个 bit 数据,只需要一个晶体管和一个电容就能存储,但是因为数据会被存储在电容里,电容会不断漏电,所以需要「定时刷新」电容,才能保证数据不会被丢失,这就是 DRAM 之所以被称为「动态」存储器的原因,只有不断刷新,数据才能被存储起来。

DRAM 的数据访问电路和刷新电路都比 SRAM 更复杂,所以访问的速度会更慢,内存速度大概在 200~300 个 时钟周期之间。

SSD/HDD 硬盘

SSD(Solid-state disk) 就是常说的固态硬盘,结构和内存类似,但是它相比内存的优点是断电后数据还是存在的,而内存、寄存器、高速缓存断电后数据都会丢失。内存的读写速度比 SSD 大概快 10~1000 倍。

HDD就是机械硬盘(Hard Disk Drive, HDD),它是通过物理读写的方式来访问数据的,因此它访问速度是非常慢的,比内存慢 10W 倍左右。由于 SSD 的价格快接近机械硬盘了,因此机械硬盘已经逐渐被 SSD 替代了。

中断

中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。中断是一种异步的事件处理机制,可以提高系统的并发处理能力

操作系统收到了中断请求,会打断其他进程的运行,所以**中断请求的响应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度地影响。**而且,中断处理程序在响应中断时,可能还会「临时关闭中断」,这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就说中断有可能会丢失,所以中断处理程序要短且快

Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」

  • 上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
  • 下半部用来延迟处理上半部未完成的工作,一般以「内核线程」的方式运行。

举一个常见的网卡接收网络包的例子。

网卡收到网络包后,通过 DMA 方式将接收到的数据写入内存,接着会通过硬件中断通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来处理该事件,这个事件的处理也是会分成上半部和下半部。

上部分要做的事情很少,会先禁止网卡中断,避免频繁硬中断,而降低内核的工作效率。接着,内核会触发一个软中断,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。

所以,中断处理程序的上部分和下半部可以理解为:

  • 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
  • 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;

还有一个区别,硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,名字通常为「ksoftirqd/CPU 编号」,比如 0 号 CPU 对应的软中断内核线程的名字是 ksoftirqd/0

不过,软中断不只是包括硬件设备中断处理程序的下半部,一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁(内核里常用的一种锁)等。

OS内核

计算机是由各种外部硬件设备组成的,如内存、cpu、硬盘等,如果每个应用都要和这些硬件设备对接通信协议就太麻烦了,所以这个中间人就由内核来负责,让内核作为应用连接硬件设备的桥梁,应用程序只需关心与内核交互,不用关心硬件的细节。

现代操作系统,内核一般会提供 4 个基本能力:

  • 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力;
  • 管理内存,决定内存的分配和回收,也就是内存管理的能力;
  • 管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力;
  • 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。

内核具有很高的权限,可以控制 cpu、内存、硬盘等硬件,而应用程序具有的权限很小,因此大多数操作系统把内存分成了两个区域:

  • 内核空间,这个内存空间只有内核程序可以访问;
  • 用户空间,这个内存空间专门给应用程序使用;

用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。因此,当程序使用用户空间时,常说该程序在用户态执行,而当程序使内核空间时,程序则在内核态执行。应用程序如果需要进入内核空间,就需要通过系统调用

内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作。

内核的架构一般有三种类型:

  • 宏内核,包含多个模块,整个内核像一个完整的程序;
  • 微内核,有一个最小版本的内核,一些模块和服务则由用户态管理,每个基础应用服务可以分配一个微内核进行处理;
  • 混合内核,是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有一个小型的内核,其他模块就在这个基础上搭建,整个内核是个完整的程序;

Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用了混合内核。 Linux 可执行文件格式叫作 ELF,Windows 可执行文件格式叫作 PE。

Reference

转载自小林coding:xiaolincoding.com/os/1_hardwa…