OS实战笔记(5)-- Cache和内存

最近工作忙,业余时间也基本投入到了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 LineCache是由一组称为缓存行(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 thrashingCache颠簸,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一致性协议相关内容。

《A primer on memory consistency and cache coherence》第一版翻译本_亦枫Leonlew的博客-CSDN博客工作中出于兴趣翻译了此书,有需要的朋友可以去看看,业余时间翻译,有不懂的请结合英文原版查看内存一致性模型入门的翻译说明 - 知乎1 Introduction to Consistency and Coherence - 知乎多数现代计算机系统以及绝大多数多核芯片都支持硬件共享内存。在这种共享内存的系统中,每个处理器的核心可以读写同一个共享的地址空间。这些设计出发点是为获得不同的好处,如高性能,低功耗以及低成本。当然,在…https://zhuanlan.zhihu.com/p/4607378292https://blog.csdn.net/vivo01/article/details/123234273?spm=1001.2014.3001.5501

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 Mapicon-default.png?t=M85Bhttp://www.uruk.org/orig-grub/mem64mb.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦枫Leonlew

希望这篇文章能帮到你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值