最近工作忙,业余时间也基本投入到了Unity中,OS实战笔记看着要烂尾了,提醒自己要抽时间把这个专题补上,今天先更一篇关于Cache和内存的。
本篇笔记主要复习几个点:
1. 程序局部性原理
2. CPU和内存的速度瓶颈
3. Cache是如何利用局部性原理加速的
4. Cache基本结构
5. Cache一致性问题简介
6. x86中cache开启方法
7. x86中获得内存视图的方法
程序的局部性原理
- 时间局部性 :指的是同一个内存位置,从时间维度来看,它能够在较短时间内被多次引用。
- 空间局部性 :指的是同一个内存位置,从空间维度来看,它附近的内存位置能够被引用 。
上面两种说法,大家应该不陌生,本笔记不再废话,直接通过代码来做分析。
例1: 数组元素求和
int array_sum(int *arr, int n)
{
int i;
int sum = 0;
for (i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
对于上面的例子,变量 sum会在循环中较短时间内会被多次引用,因此主要体现了时间局部性,没有体现空间局部性(因为每次都引用相同的地址,空间上没有变化)。对于数组arr对应空间的内容,从空间维度来看,每次引用的位置都会变化(地址增加),因此主要体现空间局部性。
例2 二维数据元素求和
//伪代码,仅用于说明问题
int calc_sum_vec2(int arr[M][N])
{
int i, j;
int sum = 0;
for (i = 0; i < M; i++) {
for (j = 0; j <N; j++) {
sum += arr[i][j];
}
}
return sum;
}
例2的代码,访问数组元素是一行一行地进行的
对于例2,不关注sum,我们来看arr数组的局部性,这个例子中二维数组的每个元素只会访问一次,因此不体现时间局部性,每次访问的元素地址增加4字节,具有空间局部性,由于4字节间隔不算大,因此空间局部性较好。
例3 二维数组元素求和(循环下标修改)
//伪代码,仅用于说明问题
int calc_sum_vec2(int arr[M][N])
{
int i, j;
int sum = 0;
for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
sum += arr[j][i];
}
}
return sum;
}
例3的代码,访问数组元素是一列一列进行的
例3的代码,循环中,数组元素的地址跨度和N的值相关,N较大时,每次引用的地址跨度较大(4N字节),空间局部性较差。
对于程序的局部性原理,我们还可以从指令的角度看,以上所有例子中,编译器编译后的函数的相关指令代码,都放到了连续的地址上。以上例子函数中如果循环相关的指令执行次数较多,那么代码本身的空间局部性和时间局部性都是比较好的。
各位可以思考下现实生活中时间局部性和空间局部性都不错的例子。我来说个,小时候经常被父母揍,一般是打屁股,打屁股这个事情的时间局部性和空间局部性就都很好,哈哈。
CPU访问内存的瓶颈
先上两幅图,来看看X86南北桥的变化
传统南北桥
原图地址:计算机组成,南北桥,倍频,通信,频率一致才可以通信 - shidexiao - 博客园
现代南北桥
原图地址:计算机组成,南北桥,倍频,通信,频率一致才可以通信 - shidexiao - 博客园
可以看到,传统方式下,内存控制器放在主板的北桥中,这种方式下延迟必然比现代方式里将内存控制器集成到CPU内部要大。当然,即便是在现代的CPU中放内存控制器,CPU内部的吞吐量也是DDR带宽的数倍。另外,如果CPU有多颗核,每颗核都有访问内存的需求,这也会造成竞争导致吞吐量下降。
CPU和内存之间的吞吐量差异很大,我们把CPU看成是闪电侠,内存看成是乌龟。现在有一项任务,闪电侠和乌龟要互相配合将气球夹在他们中间,然后走向终点。这时你就会发现一个尴尬的事情,为了保证气球不落地,闪电侠即使再快,也得等乌龟的脚步。因此你会看到闪电侠急得想骂娘,但也只能慢慢跟着乌龟的速度走。对于经常需要对内存数据进行操作的CPU,也是类似的情况,CPU要读写内存的时候,内存就是大爷,它啥时候回应了CPU,CPU才能继续干活。
为了解决CPU和内存之间的瓶颈,cache出现了。
Cache如何利用局部性原理提升
在最开始接触cache时,就一直有疑问,内存那么大,cache那么小,这个怎么能起到加速作用呢?这就要提到最开始说的局部性原理了。假设所有程序员写程序,都按照局部性原理保证的最差的方式来写,那么cache能起到的作用也确实不会有这么大了。
cache中缓存了内存的部分数据。当CPU要访问内存时,首先会查找cache,如果cache里存在对应内存地址的数据(cache hit),则直接从cache中取;否则发生cache miss,若没有则需要从内存中读取数据,并同时把这块数据放入cache中。对于写数据场景,情况也类似,但具体情况要根据cache的写执行策略相关。
cache由于制作工艺和材料等要求较高,成本比内存大多了,因此,容量不可能造的太大。根据局部性原理,cache其实也能够使用较小的容量提升相当大的性能,局部性较好的程序,一定时间内所用的数据并不多并且较为集中。
但cache的加入也对程序员们提出了更高的要求,如果写的程序空间和时间局部性都较差的话,那么cache miss就是家常便饭了,这样的程序即使使用cache,对性能也提升不了多少。另外,在多核系统中,cache还带来了一个麻烦的事情,就是cache一致性的问题,为了解决这个问题,又出现了cache一致性协议。下面我们先来看看cache的结构。
Cache的基本结构
基本术语
术语 | 描述 |
Cache Line | Cache是由一组称为缓存行(Cache line)的固定大小的数据块组成,其大小是以突发读或者突发写周期的大小为基础的。 |
Cache hit/miss | CPU访问的内存数据在cache中存在时,称为cache命中(cache hit),否则称为cache miss。 大多数现代CPU内部会有相关管理单元来统计cache命中率或cache miss率。 |
Write through | 写穿。当Cache hit时,更新的数据会同时写入cache和内存。cache和主存的数据始终保持一致。 |
Write back | 写回。当Cache hit时,只更新cache中的数据。每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit。主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致。 |
Read allocate | 读分配。当读数据发生Cache miss时,分配一个cache line缓存从主存读取的数据。默认情况下, cache都支持读分配。 |
Write allocate | 写分配。当写数据发生Cache miss时,如果支持写分配,先从主存中加载数据到cache line中 (相当于先做个读分配动作),然后更新cache line中的数据。如果不支持写分配,写指令只会更新主存数据,不分配cache line。 |
Ambiguity | 歧义。关于歧义,单独分一小节讲解。 |
Alias | 别名。关于别名,单独分一小节讲解。 |
VIVT | virtual index virtual tag, 虚拟地址索引,虚拟地址标签。 关于index,tag等内容,单独讲解 |
VIPT | virtual index physical tag,虚拟地址索引,物理地址标签。 关于index,tag等内容,单独讲解 |
PIPT | physical index physical tag,物理地址索引,物理地址标签。 关于index,tag等内容,单独讲解 |
Cache thrashing | Cache颠簸,cache反复被踢出,后文有介绍 |
Clean | 检查cache line 的dirty bit。如果dirty bit为1,将cache line的内容写回下一级存储,并将dirty bit置为0 |
Invalidate | 检查对应内存cache line 的valid bit.如果valid bit 为1则将其设置为0 |
Flush | 每条cache line 先clean,之后再invalid |
直接映射方式的cache结构(Direct mapped cache)
假设我们有一个cache,它的cache line为16字节,cache大小总共128字节。cache内部总共分为128 /16 = 8块(blocks)。下图是8 个 cache line存储实际数据的空间,B表示Byte:
这块空间我们称为data array,光有这块缓存内存数据的空间,我们是没有办法使用cache的。我们还需要一些其它的辅助信息来帮助实现cache的访问功能。
下面我们来看直接映射方式的cache是如何工作的。
假设CPU要访问0x015a(bit[15:0]: b'0000,0001,0101,1010)这个地址(假设地址位宽16 bits)。
首先,我们要知道这个地址具体要访问哪一个byte,假设cache命中,那么这个byte肯定会在某个cache line中,并且这个byte肯定是这个cache line里的某个byte。由于cache line是16个字节,因此最低4个bit就能索引到具体对应那个byte。最低的4个bit ([b3:b0]) 我们称为offset,用来索引cache line中的一个字节,。
然后,我们要知道用的是哪个cache line,cache line总共有8个,因此用3个bit就能索引到是哪个cache line。这样我们就是用[b6:b4]这个3个bit作为cache line的索引,我们称为index。
接下来,还要解决一个有无的问题,这个要访问的地址,cache上到底有没有将缓存对应地址的内容呢?这个问题我们可以通过增加一块表来辅助查找,这个表要能通过[b15:b7]这剩下的9个bit来进行查找,这个表还要给出cache line是否是有效的状态(valid)。剩下的9个bit我们称为tag,这个辅助的查找表我们称为tag array。
最后,如果CPU修改过cache的内容,cache的内容和内存(或下一级cache)上不一致,那么cache line中也要能有相应状态记录,我们称这种状态为dirty。
直接映射方式的cache主要结构我们就有了:
需要注意一点,图中展示的是逻辑结构,并不代表实际硬件tag array和data array是分开做的。
从图中可以看出,tag array具体有多少条数据,和cache line的数量是一一对应的。CPU访问0x015a时,可以知道,tag = 0x2, index = 0x5, offset = 0xa。首先根据index,我们就知道了这个index对应哪个cache line(下标为5),然后找到cachel line对应的tag entry(下标也是5),根据tag entry里的内容来对比tag的值,以及V bit。如果tag entry里记录的内容是地址高9bit的值(tag = 0x2),并且valid bit(V)为1,则表示cache line中缓存的是对应地址的内存数据。那么最后CPU就根据offset值去data array里去取出offset对应的Byte(B10)。
假设系统中内存地址范围是从0x0000到0x017F(总共0x180个字节,128 * 3),那么直接映射Cache方案,内存内容和cache line对应关系如下:
上图左边是RAM,每128字节为一个block,每个block可以看做和data array中的cache line组织结构一样。可以理解为我们以cache line为大小去划分了RAM, 每128字节划分8个小块(index 0 - 7),每个小块的内容如果要被缓存到cache,就直接对应到data array里相同下标的cache line。
直接映射Cache的优缺点
直接映射Cache的优点是硬件实现简单,成本更低。但是缺点也很明显,我们可以看到0x0000, 0x0080, 0x0100开始的地址,用的cache line是同一个(index为0)。假设有程序短时间内会频繁地访问0x0000, 0x0080, 0x0100的内容,那么就会发生尴尬的事情。初始状态下,所有tag array里的V位都为0。CPU先访问0x0000,发生cache miss,然后cache将0x0000开始的16个字节读取到第1个cache line(index = 0)。接着程序将使用0x0080的数据,此时cache会将第1个cache line的内容无效化,重新从内存中读取16个字节到第1个cache line。接下来程序使用0x0100的数据,同样的,第1个cache line的内容再次被替换成了0x0100开始的16个字节。这样一来,cache基本等于没用了。这种情况我们称为cache颠簸(cache thrashing)。
为了解决这个问题,出现了多路组相连cache。
两路组相连cache(Two-way set associative cache)
为了简单起见,我们用最简单的两路组相连cache作为例子。还是和前面用的cache例子一样,128字节大小,cache line为16字节。
两路组相连cache,会将data array分为两份。每份4条cache line,相应的tag array也会被分成两份。其中组(set)和路(way)的概念,参考下图:
从上图可以看出,一个组(Set)中有两个cache line和两个tag entry。一路(Way)中包含4条cache line。从左到右(仅从图示的逻辑角度看,并且实际物理结构)的两个tag entry和两个cache line组成了一个Set;从上到下的4个cache line组成了一个Way。
由于两个cache line分到了一组,因此总共有4个Set,此时我们只需要2 bits作为index就可以索引到组。并且index相同的时候,可以存放两个tag值,如图中的0x87和0x5,对应index相同的两个地址。在直接映射方案中,如果index = 1, 那么cache line 1所对应的tag只可能有一个值。在两路组相连cache中,由于index 1对应的cache line有两个,因此同一个index下可以对应连个不同的tag值。此时index也被称为set index,表示用来取哪一个set。接下来就是根据set里的tag array来找到我们该使用哪一个way。
两路组相连cache和直接映射cache最大的区别是:直接映射缓存一个地址只对应一个cache line,而两路组相连cache可以对应到两个cache line。
两路组相连cache的优缺点
回到之前直接映射cache的cache thrashing问题。对于两路组向量cache,假设程序访问的地址顺序是0x0000, 0x0040, 0x0080(由于两个cache line对应同一个index,因此地址相比于直接映射,循环重复index的地址间隔小了一半)。当访问0x0000时,可以从set 0 中拿出一路cache line存放这个地址开始的内容,假设是Way 0。当访问0x0040时,可以从set 0中拿出另一路cache line存放这个地址开始的内容,假设是Way 1。当访问0x0080时,这时,我们才要考虑替换出去一个cache line,假设我们硬件采用的是LRU策略,Way 0相比于Way 1使用的更少,那么就将Way 0(0x0000)对应的cache line替换成0x0080的内容。
可以看到,同一个index存放多个cache line,可以减少cache thrashing的发生频率。如果增加way的数量,则冲突发生的概率会进一步减少。
可以看到,直接映射缓存是组相连缓存的一种特殊情况,每个组只有一个cache line。因此,直接映射缓存也可以称作单路组相连缓存。
全相连映射cache(Full associative cache)
理解了组相连cache的结构,再来看全相连cache就比较好理解了。全相连映射cache中只有一个set,因此我们就不需要index字段了。对于任意地址(cache line size对齐的),它都可以映射到任意一个cache line上。其结构如下图所示:
全相连映射的cache,能够最大程度上避免cache thrashing。但是硬件实现的难度和成本相对较高。
关于D(Dirty)bit
当采用写回策略时,CPU写数据时,如果cache命中,则只会更新cache里的内容,此时内存中的数据和cache中的数据不一致。这种情况下对应cache line的D bit被置位。那么什么时候才将cache的内容同步到内存上呢?通常有两种情况:1. 当这个cache line需要被替换出去(evict, 踢出)的时候,如果D bit有效,则cache的数据修改会回写到内存(严谨一点是下一级存储)上;当CPU主动执行clean或flush操作时,cache line的内容会回写到内存。
VIVT,VIPT,PIPT,PIVT都是些啥?
有多种不同的cache地址组织方式,比如VIVT,VIPT,PIPT,PIVT。看到这些词是不是心里面想问候一下亲人了。其实最常见的也就是VIPT,PIPT也能见到。
首先我们拆开字母分别来看,V代表Virtual,表示虚拟地址;P代表Physical,表示物理地址;I表示index;T表示tag。以VIPT为例,表示用虚拟地址对应bits来取index值,物理地址对应bits作为tag的值。后面提到的VA指Virtual Address,PA指Physical Address。
VIVT
这种组织方式的cache,index和tag全部使用虚拟地址。
VIVT方式的cache,在CPU访问某个虚拟地址时,主要过程如下:首先根据虚拟地址得到index,通过index找到cache line(这里不纠结于是直接映射还是组相连之类的结构),然后对比tag(同样为虚拟地址)。如果cache hit,则直接从cache中取数据,如果cache miss,则将VA送至MMU转换为PA后访问主存,将对应数据放到cache line中并返回给CPU。
可以看到,如果使用VIVT进行cache访问,不需要每次读写都经过MMU将VA转成PA后再去查表,一定程度上降低了访问延迟。但VIVT也引入了两个问题:歧义(Ambiguity)和别名(Alias)。
歧义(Ambiguity)
歧义这个词,从字面上理解上就是词语本身会产生两种或两种以上可能的意思,容易让人误解。我们将“词语”换成虚拟地址,“可能的意思”换成cache缓存的物理地址的内容,那么VIVT里引入的歧义的意思就是,同一个虚拟地址,对应的cache line中缓存的内容对应到了不同的物理地址上。
上面这样说可能还是抽象了点,举个简单的例子就比较直观了。假设有两个进程A和B,它们都要访问虚拟地址0x1000, 进程A中VA 0x1000对应的是PA 0x8000;进程B中VA 0x1000对应PA是0x9000。假设进程A先调度,访问了0x1000,那么此时对应cache line中存的数据来自PA 0x8000地址;然后进程B调度后,同样去访问0x1000。由于VIVT方式都是使用VA来取index和tag,因此如果没有任何特殊处理,对cache来说,由于index和tag一样,必然会cache命中,cache就会返回PA 0x8000的内容。为了避免歧义问题,操作系统在进行进程间切换时,一般会做cache flush操作。
做cache flush是需要时间的,因此我们可以知道,VIVT的cache,频繁切换进程的过程中会有性能损失。这种损失有两方面,一方面是flush操作本身,另一方面就是切换到下一个进程后,这个进程所有的内存访问都会发生cache miss。
别名(Alias)
现实生活中,别名很好理解,比如你的外号就算是一个别名。VIVT中的别名问题,这个名字我们理解为虚拟地址,实际指代的“人”我们理解成物理地址就可以了。别名的意思就是一个PA有两个或多个VA相对应。
同样,我们来看一个例子,假设两个虚拟地址VA:0x1060和0x8030,这两个虚拟地址都对应同一个PA 0x9000。由于VIVT使用VA作为tag和index,对于这两个VA来说,就使用了两个cache line。这两个cache line存的是同一个PA的内容。如果只是读操作,问题还不大。但如果是写操作,并且cache的写策略是write back,那么问题就来了。假设程序A先对这两个VA进行了一次读操作,cache分配了两条cache line存储了相同的内容。假设程序接下来对VA 0x1060进行了写操作,由于是write back,这个数据不会立即写到PA对应的内存上。接下来程序要对0x8030进行访问,如果是读操作,则cache命中,返回的值还是原始读到的值而不是更新后的值;如果是写操作,那么这两个cache line里的内容也不一样了。
为了解决这个问题,对于不同的进程而言,进程切换时做flush cache能够解决问题。如果是同一个进程内要共享内存,通过nocache方式做映射,这样共享内存就不经过cache了,但大家也能看出,nocache方式性能损失肯定是巨大的。
PIPT
这种方式下,cache的tag和index都是按照PA来索引的,因此不存在歧义和别名问题。其逻辑结构如下图:
首先,VA经过MMU转换成PA,然后根据PA找到index和tag。这种方式虽然能解决歧义和别名问题(index和tag都是唯一的)。对于软件来说,PIPT基本做到了cache透明化。但是对硬件来讲,这种方案也有两个明显的不足:1. 每次进行cache访问时,VA必须经过MMU转换为PA,这个步骤相比于VIVT会较为繁琐。2. 硬件实现上比VIVT要复杂,成本也更高。
VIPT
VIPT的逻辑结构如下图:
VIPT方式下,使用虚拟地址的index来查找cache line,同时VA会发送到MMU转换成PA,然后通过PA取出tag去对比tag entry的值。这种方式综合了VIVT和PIPT,那么这种方式是否存在歧义和别名问题呢?
先看歧义问题,本质是因为同一个虚拟地址对应到了不同的物理地址。还是以之前的两个进程A,B为例,进程A访问0x1000对应PA是0x8000;进程B访问0x1000对应PA是0x9000。由于VIPT使用PA作为tag,因此,不会出现0x1000在不同进程中访问到了同一个cache line数据的问题。因此VIPT不存在歧义问题。
再来看别名问题,VIPT是否有别名问题会稍微复杂一点。
首先来看不会出现别名问题的场景。一般情况下,Linux的内存管理的最小单位是页,对于4KB的页大小。如果cache的大小也等于4KB,那么此时bits[11:0]作为页内偏移,实际上整体是作为转换后的PA的低12 bit偏移作用。这种情况下index本身也属于PA的一部分。这样无论使用VA查index还是PA查index,效果是一样的,这种情况和PIPT基本等价,因此也就不会有别名问题产生。如果是多路组相连cache,只要cache的大小除以way的数量的值,小于等于页的大小,也不会存在别名问题。
相对的,假设仍然是4KB的页,但是cache容量为8KB(对于多路组相连cache,一路大小为8KB)。那么别名问题就会出现了。这里假设cache line大小为256 bytes。因此cache line总共有32个,需要5 bits作为index,offset需要8 bits。因此index + tag总共占13个bits。对于4KB的页,VA和PA相同的bit有12个,因此index里多出了1 bit虚拟地址里的值。假设VA 0x50000和 0x51000都映射到了同一个PA 0x80000, 那么这个PA对应的cache line就会有多个,别名问题就出现了。
PIVT
如果说VIPT是缝合地不错的方案,它结合了PIPT和VIVT的优点。那么PIVT缝合出来的就是怪物了,没有保留到PIPT和VIVT的优点,还将两者的缺点都全收了。这种方案可以说基本不会有厂家用(网上搜索说MIPS R6000采用这种方式,很快被淘汰,没有求证过)。这里就不详细说了,感兴趣的朋友们可以自己去画图看看。
Cache一致性问题简介
虽然使用cache带来了很大的性能提升,但它也有副作用。最常见的一个问题就是cache一致性问题。本笔记会简单介绍cache一致性问题发生的原因。
先来看一个简单的双核心,3级缓存的结构:
如上图所示,Core0和Core1有分开的L1指令和数据cache和一个L2缓存。所有Core共享一个L3缓存。
cache的一致性问题,可以从三个范围来看:
1. 单个core内指令和数据cache的一致性
对于有些自修改的程序而言,比如我们修改了一个地址上的指令A,这是通过修改数据存储的操作来实现的,因此修改的是数据cache中的内容。接下来我们要读取指令A并运行,这时走的是指令cache,如果没有考虑一致性。可能出现指令cache命中,但cache的数据不是修改后的数据,导致读到旧指令的问题。解决这种问题的方法是做完数据cache对指令A的修改后,对指令cache做无效化操作,这样当再次访问指令A时会重新从内存中加载回来。
2. 多个core之间L2 cache的一致性
从图中可以看到,两个core的L2 cache共享一个L3 cache。假设Core 0访问地址A,此时Core 0 的L1,L2 cahe,以及L3 cache中就有了地址A对应的数据。如果Core 1也要访问地址A,这时,cache的硬件能够直接把Core 0中对应地址A的数据直接复制到Core 1的L1,L2 cach中。当Core 0和Core 1都缓存了地址A的内容后,此时如果Core 0对地址A进行了修改,更新到了Core 0的L1 cache中,如果没有一定的机制处理这个情况,当Core 1访问地址A时,就会拿到旧数据。因此需要实现这种同步的机制,这就是cache一致性协议所要解决的问题。
3. L3 cache和外设内存(比如PCIE设备,网络设备等具有DMA功能的外设)的一致性
写驱动的小伙伴对这个事情一定不陌生,如果外设要通过DMA读写主存,那么这段时间内CPU需要做相应的处理,比如flush cache或invalidate cache,具体和数据的流向相关。这部分内容,网上有很多资料,由于和本笔记关注的点不太吻合,因此不详细展开。
为了解决cache一致性问题,引入了cache一致性协议。常见的cache一致性协议类型有两种:snooping 和 directory 类型的一致性协议。
如果想要详细了解cache一致性协议的实现细节,可以参考我之前翻译过的《A primer on memory consistency and cache coherence》这本书,这本书的6-8章详细讲解了cache一致性协议相关内容。
x86启用cache的方法
x86平台上,cache默认是关闭的,如果要打开cache。主要关注CR0寄存器的CD(cache disable)和NW(not write thorugh)。这两个bit详细介绍参考Intel的手册:
将CD和NW都设置为0即可开启cache
mov eax, cr0
btr eax,29 ;CR0.NW=0
btr eax,30 ;CR0.CD=0
mov cr0, eax
x86获得内存视图的方法
在启动分页机制前,我们需要获得内存的布局信息。x86平台上利用BIOS的INT15h中断加上特殊的参数可以获得相关信息。代码如下:
_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个字节大小的结构,用来表示内存段的信息,其结构如下:
#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;
详细的INT15内存相关的操作,可以参考这里
INT 15h, AX=E820h - Query System Address Maphttp://www.uruk.org/orig-grub/mem64mb.html