操作系统 -- Cache和内存

程序局部原理性

CPU大多数时间在执行相同的指令或者与此相邻的指令,这就是程序局部性原理,依据此原理用来缓解CPU和内存之间的性能瓶颈。

内存

内存,从专业角度来讲,内存应该叫做DRAM,即动态随机存储器,内存存储颗粒芯片中的存储单元是由电容和相关元件做成,电容存储电荷的多、少代表数字信号0和1。
重点关注的是:内存的速度还有逻辑上内存和系统的连接方式和结构,如下图所示:

由上图可看出,控制内存刷新和内存读写的是内存控制器,而内存控制器集成在北桥芯片中。随着芯片制造工艺的提升,芯片集成度也越来越高,因此北桥芯片也集成到了CPU芯片中,这大大提升了CPU访问内存的性能。

CPU到内存性能的瓶颈

1、内存条的昂贵程度,超乎想象
2、CPU与内存条读写数据速度的差异:CPU要数据,内存一时给不了怎么办?CPU就得等,通常CPU会让总线插入等待时钟周期,直到内存准备好,就会发现,无论CPU新能多高都没用,内存才是决定系统整体性能的关键。

Cache

开头所讲到的程序的局部性原理:CPU 大多数时间在访问相同或者与此相邻的地址。就想到用一块小而块的存储器,放在CPU和内存之间,就可以利用程序的局部性原理来缓解CPU和内存之间的性能瓶颈。这小而块的存储器就是Cache,即高速缓存。

Catch中存放的是内存中的一部分数据,CPU在访问内存时先去访问Cache,若Cache中有需要的数据就直接从Cache中取出,若没有则需要从内存中读取数据,并同时写入Cache中,但也是由于程序局部性原理,在一段时间,CPU总能从Cache中读到自己想要的数据。

Cache可以集成在CPU内部,也可以做成独立的芯片放在总线上:

Cache主要由高速的静态存储器地址转换模块Cache行替换模块组成。
Cache 会把自己的高速静态储存器和内存分成大小相同的行,一行大小通常为 32 字节或者 64 字节。Cache 和内存交换数据的最小单位是一行,为方便管理,在 Cache 内部的高速储存器中,多个行又会形成一组。

Cache 大致的逻辑工作流程如下:
1、CPU 发出的地址由 Cache 的地址转换模块分成 3 段:组号行号行内偏移
2、Cache 会根据组号、行号查找高速静态储存器中对应的行。如果找到即命中,用行内偏移读取并返回数据给 CPU,否则就分配一个新行并访问内存,把内存中对应的数据加载到 Cache 行并返回给 CPU。写入操作则比较直接,分为回写和直通写,回写是写入对应的 Cache 行就结束了,直通写则是在写入 Cache 行的同时写入内存。
3、如果没有新行了,就要进入行替换逻辑,即找出一个 Cache 行写回内存,腾出空间,替换行有相关的算法,替换算法是为了让替换的代价最小化。例如,找出一个没有修改的 Cache 行,这样就不用把它其中的数据回写到内存中了,还有找出存在时间最久远的那个 Cache 行,因为它大概率不会再访问了。

Cache带来的问题

虽然Cache带来了性能方面的提升,但同时也给硬件和软件开发开来了问题,那就是数据一致性问题

X86 CPU的Cache结构图:

这是一颗最简单的双核CPU,具有三级Cache,第一级Cache是指令和数据分开的,第二级Cache是独立于CPU核心的,第三级Cache是所有CPU核心共享的

Cache的一致性问题主要包括三个方面:
1、一个 CPU 核心中的指令 Cache 和数据 Cache 的一致性问题。
2、多个 CPU 核心各自的 2 级 Cache 的一致性问题。
3、CPU 的 3 级 Cache 与设备内存,如 DMA、网卡帧储存,显存之间的一致性问题。

为解决上述问题,硬件工程师们开发了多种协议,典型的多核心Cache数据同步协议有MESI和MOESI,这里讲解下MESI协议。

Cache的MESI协议

MESI协议定义了4中基本状态:M、E、S、I,即修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)
1、M 修改(Modified)
当前 Cache 的内容有效,数据已经被修改而且与内存中的数据不一致,数据只在当前 Cache 里存在。比如说,内存里面 X=5,而 CPU 核心 1 的 Cache 中 X=2,Cache 与内存不一致,CPU 核心 2 中没有 X。

2、E 独占(Exclusive)
当前 Cache 中的内容有效,数据与内存中的数据一致,数据只在当前 Cache 里存在;类似 RAM 里面 X=5,同样 CPU 核心 1 的 Cache 中 X=5(Cache 和内存中的数据一致),CPU 核心 2 中没有 X。

3、S 共享(Shared)
当前 Cache 中的内容有效,Cache 中的数据与内存中的数据一致,数据在多个 CPU 核心中的 Cache 里面存在。例如在 CPU 核心 1、CPU 核心 2 里面 Cache 中的 X=5,而内存中也是 X=5 保持一致。

4、I 无效(Invalid)
当前 Cache 无效。前面三幅图 Cache 中没有数据的那些,都属于这个情况。

Cache 硬件,它会监控所有 CPU 上 Cache 的操作,根据相应的操作使得 Cache 里的数据行在上面这些状态之间切换。Cache 硬件通过这些状态的变化,就能安全地控制各 Cache 间、各 Cache 与内存之间的数据一致性了。

开启Cache

x86 CPU 上默认是关闭 Cache 的,需要在 CPU 初始化时将其开启。
在 x86 CPU 上开启 Cache 非常简单,只需要将 CR0 寄存器中 CD、NW 位同时清 0 即可。CD=1 时表示 Cache 关闭,NW=1 时 CPU 不维护内存数据一致性。所以** CD=0、NW=0 的组合**才是开启 Cache 的正确方法。

开启 Cache 只需要用四行汇编代码,代码如下:

mov eax, cr0
;开启 CACHE    
btr eax,29 ;CR0.NW=0
btr eax,30  ;CR0.CD=0
mov cr0, eax

获取内存视图

作为系统软件开发人员,与其了解内存内部构造原理,不如了解系统内存有多大。这个作用更大。
给出一个物理地址并不能准确地定位到内存空间,内存空间只是映射物理地址空间中的一个子集,物理地址空间中可能有空洞,有 ROM,有内存,有显存,有 I/O 寄存器,所以获取内存有多大没用,关键是要获取哪些物理地址空间是可以读写的内存。

物理地址空间是由北桥芯片控制管理的,那我们是不是要找北桥要内存的地址空间呢?当然不是,在 x86 平台上还有更方便简单的办法,那就是 BIOS 提供的实模式下中断服务,就是 int 指令后面跟着一个常数的形式。

由于 PC 机上电后由 BIOS 执行硬件初始化,中断向量表是 BIOS 设置的,所以执行中断自然执行 BIOS 服务。这个中断服务是 int 15h,但是它需要一些参数,就是在执行 int 15h 之前,对特定寄存器设置一些值,代码如下。

_getmemmap:
  xor ebx,ebx ;ebx设为0
  mov edi,E80MAP_ADR ;edi设为存放输出结果的1MB内的物理内存地址
loop:
  mov eax,0e820h ;eax必须为0e820h
  mov ecx,20 ;输出结果数据项的大小为20字节:8字节内存基地址,8字节内存长度,4字节内存类型
  mov edx,0534d4150h ;edx必须为0534d4150h
  int 15h ;执行中断
  jc error ;如果flags寄存器的C位置1,则表示出错
  add edi,20;更新下一次输出结果的地址
  cmp ebx,0 ;如ebx为0,则表示循环迭代结束
  jne loop  ;还有结果项,继续迭代
    ret
error:;出错处理

上面的代码是在迭代中执行中断,每次中断都输出一个 20 字节大小数据项,最后会形成一个该数据项(结构体)的数组,可以用 C 语言结构表示,如下。

#define RAM_USABLE 1 //可用内存
#define RAM_RESERV 2 //保留内存不可使用
#define RAM_ACPIREC 3 //ACPI表相关的
#define RAM_ACPINVS 4 //ACPI NVS空间
#define RAM_AREACON 5 //包含坏内存
typedef struct s_e820{
    u64_t saddr;    /* 内存开始地址 */
    u64_t lsize;    /* 内存大小 */
    u32_t type;    /* 内存类型 */
}e820map_t;
posted @ 2022-05-27 21:27  牛犁heart  阅读(1197)  评论(0编辑  收藏  举报