内存

内存是DRAM,即动态随机存储器。使用电容保存电荷来记录0或1,需要周期性刷新。

这里只需要了解内存的速度以及逻辑上内存和系统的链接方式。 控制内存刷新和读写的是内存控制器,集成在北桥芯片上。传统方式下,北桥芯片存在于主板上,随着芯片集成度的提高,现代CPU将北桥芯片集成在CPU中。

从逻辑上来说,我们只需要将内存看作一个巨大的字节数组即可。

CPU到内存的性能瓶颈

由于成本的原因,CPU与内存采用的工艺和技术是不同的,也就造成了CPU和内存的速度要差几个数量级

很多时候CPU需要数据,但是内存一时给不了,就造成CPU空转,浪费了CPU的性能。所以通常内存决定了系统整体的性能。

为了解决这个问题,就出现了cache。

cache

cache的出现是依赖程序的局部性原理的,CPU 大多数时间在执行相同的指令或者与此相邻的指令。

因此,在CPU和内存之间添加一块小而块的存储器,即Cache,就能够利用程序的局部性原理来缓解CPU和内存之间的性能瓶颈。

现在Cache与北桥一样都是集成在CPU内部的。

Cache由三部分组成:高速的静态存储器、地址转换模块和Cache行替换模块组成。

cache与内存交换数据的最小单位是一行,一行通常为32字节或者64字节,多个行又会形成一组。Cache行中除了数据,还有一些标志位,如脏位、回写位、访问位等。会被替换模块所使用。

Cache的工作流程如下:

  1. CPU发出的地址由Cache地址转换模块分为三部分:组号、行号、行内偏移。
  2. 根据组号、行号在静态存储器中找到对应的行。如果命中则用行内偏移读取并返回给CPU,没有命中则分配一个新行并访问内存加载数据到Cache后再返回给CPU。对于写操作,分为回写和直通写,回写是写入Cache就结束了,直通写需要写入Cache和内存。
  3. 没有新行了,则根据行替换算法来选择一个行进行替换。

Cache是由硬件实现的,对软件是透明的。

Cache带来的数据不一致问题

Cache在带来性能提升的同时,也带来了数据不一致的新问题。这与Cache的设计有关。

如上图,可以看出来Cache分为三级。一级Cache分为指令Cache和数据Cache,二级Cache是CPU核所私有的,三级Cache是所有CPU核共享的。

这带来了三种数据不一致的问题:

  1. 一个CPU中指令Cache和数据Cache不一致问题。对自修改代码(即修改运行中的代码的指令数据,变成新的指令,例如Java运行时编译器)。被修改的指令是作为数据写入数据Cache的,如果我们接下来执行修改的指令,那么从指令Cache中取出的可能仍然是之前旧的指令,造成了不一致。
  2. 多个CPU核心中各自二级Cache不一致问题。如果CPU1从内存取得了A地址的内容存放到cache中,那么CPU2就没有必要从内存中读取,而是直接CPU1的Cache中复制A地址的内容到自己的第2、1等级cache中。但是这个时候修改了CPU1的A地址的数据,就造成了两个CPU中的cache的数据不一致。
  3. 三级Cache与设备内存,如DMA、显卡之间的一致性问题。

为了解决多核CPU的缓存同步问题,有了MESI协议。

Cache的MESI协议

MESI其实是Cache中数据的4种状态。

  • Modified修改(M):数据在当前Cache中有效并且已经修改与内存中数据不一致。
  • Eclusive独占(E):数据在当前Cache有效并且与内存中数据一致,也就是没有修改过,并且在其它Cache中不存在此数据。
  • Shared共享(S):数据在当前Cache中有效并且没有修改,而且在其它CPU的Cache中也存在。
  • Invalid无效(I):数据在当前Cache中无效。

Cache硬件能够通过Cache的操作,让Cache中对应的数据行在这些状态中切换,根据这些状态来控制各Cache间、各Cache与内存间的数据一致。

详细内容等待后续的内存知识篇详细介绍。

开启Cache

x86 CPU开启Cache非常简单,将CR0寄存器的CD、NW位置为0即可。

CD=1表示Cache关闭,NW=1表示CPU不维护内存数据一致性。

所以CD=0,NW=0是开启cache的正确方式

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

获取内存视图

作为系统开发人员,要获取哪些物理地址空间是可以读写的内存。 在x86上,利用BIOS提供的实模式下中断服务即可。中断服务为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;