当前位置:   article > 正文

linux内核学习1:内存地址(1)_linux内核分析x86内存地址映射

linux内核分析x86内存地址映射

一、了解地址

1. 程序反汇编

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。

我们写个最简单的hello world程序,用gccs编译,再反编译后会看到以下指令:

mov 0x80495b0, %eax

这里的内存地址0x80495b0 就是一个逻辑地址,必须加上隐含的DS 数据段的基地址,才能构成线性地址。也就是说 0x80495b0 是当前任务的DS数据段内的偏移

2. 地址分类

当使用80x86微处理器时,必须区分以下三种不同的地址:

  1. 逻辑地址(logical address),每一个逻辑地址都由一个段(segment)和偏移量(offset或者displacement)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
  2. 线性地址(linear address),也称虚拟地址(virtual address),是一个32bit无符整数,可以用来表示4G的地址,通常由16制数字表示。可以认为是cpu执行程序过程中的一种中间地址。
  3. 物理地址(physical address),用于内存芯片级内存单元寻址,与地址总线相对应。他们与从微处理器的地址引脚范松到内存总线上的电信号相对应,物理地址由32bit或36bit无符号整数表示。

逻辑地址——>【分段单元】——>线性地址——>【分页单元】——>物理地址

2.1 实模式和保护模式

具体区别查看:https://zhuanlan.zhihu.com/p/42309472
实模式的"实"更多地体现在其地址是真实的物理地址。
保护模式,实现更大空间的,更灵活也更安全的内存访问。

2.2.逻辑地址转换成线性地址

在 80386中,有6个16位的段寄存器,但是,这些段寄存器中存放的不再是某个段的基地址,而是某个段的选择符(Selector)。因为16位的寄存器 无法存放32位的段基地址,段基地址只好存放在一个叫做描述符表(Descriptor)的表中

我们在寻址的时候,一般是从段寄存器拿到段选择符,然后再根据选择符的索引号,找到段描述符,然后从段描述符中取出段基址,加上偏移就形成了我们要访问的地址

段选择符

段选择符(或称段选择子)是段的一个十六位标志符,段选择符用来表示指向哪个段描述符,即用来在段描述符中寻址

索引号:给出了描述符在GDT或LDT表中的索引项号。
TI : TI([2])TI = 0 ,表示描述符在GDT中,TI=1,表示描述符在LDT中。
RPL :请求特权级,装在cs寄存器中,值为0代表最高优先级,为3代表最低优先级。linux中只用哪0级和3级,分别称之为内核态和用户态

在这里插入图片描述

为了更方便的找到段选择符,处理器提供了段寄存器,段寄存器的唯一目的就是存放段选择符。

段寄存器

段寄存器:有6个段寄存器,分别为:
cs(代码段寄存器)——包含程序指令的段
ss(栈段寄存器)——包含当前程序栈的段
ds(数据段寄存器)——包含静态数据或者全局数据的段
es、fs和gs,用于存放段选择符。——一般用途,指向任意的数据段

cs除了是包含程序指令的段,还包含前面提到的请求特权级(RPL),值为0代表最高优先级,为3代表最低优先级。linux中只用哪0级和3级,分别称之为内核态和用户态

段描述符

段描述符8个字节,描述了段的特征。段描述符存放在全局描述符(GDT)或者局部描述符(LDT)中

GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正在被使用的LDT的地址和大小放在ldtr控制寄存器中。

段描述符具有64位在GDT表中结构如下:
在这里插入图片描述

P位:段描述符中高4字节中的15位P位是最重要的1位,如果P=0代表这个段描述符不用看了无效是个无效描述符

          P=1代表这个段描述符有效(如果P=0其它的所有操作比较啊都没有意义)

G位:是粒度G=0代表20位的Limit的单位是BYTE,G=1代表20位的Limit的单位是4KB一个页

        G=0或1只会决定能访问的大小,不会决定段的大小,只决定Limit的范围(而Limit决定你能访问段的大小)

D/B位:会在后面进行讲解比较复杂

DPL:在段描述符的高4字节的13-14位,表示段描述符特权级,只有想要加载这个段描述符的选择子的请求特权级别高于

        或者等于DPL才能加载这个段(也就是你达到了女方要求的条件了)RPL(权限大于)>=DPL只是条件之一,下一篇帖子

        段权限检查会给大家全面的讲述,加上实践检验

S位: S=0代表这个描述符是系统段描述符,S=1代表这个段描述符是代码段描述符或者数据段描述符

      GDT中的描述符分为两类一类是系统段描述符,另一种数据段描述符或者代码段
Type:描述段的类型特征和它的存取权限,4-bit

还有其他的这里不详细讲,看上面的图
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

常见的几种段描述符Type(即上面Type字段):

  • 代码段描述符(Code Segment Descriptor),代表一个代码段,可以放在GDT或者LDT中,该描述符S标志为1(非系统段)。、-
  • 数据段描述符(Data Segment Descriptor),代表一个数据段,可以放在GDT或者LDT中,S标志为1。
  • 任务状态段描述符(Task State Segment Descriptor,TSSD),代表一个任务状态段(Task State Segment,TSS),也就是说这个段用于保存处理器寄存器的内容,只能出现在GDT中。
  • 局部描述符表描述符(Local Descriptor Table Descriptor,LDTD),代表一个包含LDT的段,只出现在GDT中,S标志为0。

段描述符的地址 = GDT/LDT的值(保持在gdtr/ldtr寄存器) + (段选择符的索引值 * 8)
能够保持在GDT中的段描述符的最大数目是8191,即2^13 -1

分段单元工作步骤

下图显示了一个逻辑地址是怎样转换成对应的线性地址的。

① 检查段选择符的TI字段,以确定段描述符保存在GDT还是LDT中

② 段描述符地址 = gdtr / ldtr + 段选择符index字段 * 8(每个段描述符8B)

③ 线性地址 = 段描述符base字段 + 逻辑地址的偏移量

在这里插入图片描述

对上面的各种地址的阶段性总结如下:

CPU将一个虚拟地址空间的地址转换为物理地址,需要进行两步:首先将给定的逻辑地址,即[段标识符:段内偏移量]这样的形式,利用段式管理单元,转化为线性地址,然后利用页式内存管理单元,转化为最终的物理地址。
  参考https://blog.csdn.net/baidu_35679960/article/details/80463445
   https://blog.csdn.net/wxzking/article/details/5905214

在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”所以如果做linux下内核开发,对于上述的x86的段式管理可以完全不用理会,我们可以认为linux根本没有用intel弄出来的这个段式管理,而是以页式管理完成了所有的内存管理工作。
这个在下一个介绍
在这里插入图片描述

内核以页框为基本单位管理物理内存,分页单元中,页指一组数据,而存放这组数据的物理内存就是页框,当这组数据被释放后,若有其他数据请求访问此内存,那么页框中的页将会改变。

二、 分页

分页单元用来把线性地址转换成物理地址。
它的一个主要的任务就是根据线性地址的访问权限检查请求的访问类型。如果访问的内存不合法,它会产生一个页面错误异常(参考第4章和第8章)。

出于性能考虑,线性地址被分成固定大小的组,叫做页面;一个页面中连续的线性地址被映射到连续的物理地址上。如此,内核可以对一个页面进行权限控制,而不需要针对其中的每个线性地址。一般情况下,我们说页面时,就是指一个线性地址集合和这些地址包含的数据。

分页单元把所有RAM分成固定长度的页帧(物理页)。每个页帧包含一个页面,也就是页帧的大小和页面大小一样。页帧是主内存的组成部分,因此也是一块存储区域。区分页帧和页面很重要,页面是一块数据,它被存储在任意一个页帧上或者磁盘上。

把线性地址映射到物理地址的数据结构叫做页表;它存储在主存中,并被内核初始化(启用分页单元的情况下)。

从80386开始,所有80x86处理器支持分页;通过设置cr0寄存器的PG标志位可以启用它。当PG=0时,线性地址等于物理地址。

2.1常规分页

从80386开始,页面的大小为4KB。
32位线性地址被分成三部分:

  • 目录
    最高10位有效位
  • 页表
    中间10位
  • 偏移
    最低12位有效位

线性地址的转换分为两步,每一步基于一种转换表。第一个转换表叫做页目录,第二个叫做页表。
在这里插入图片描述

2.2 分页举例

假设内核给进程分配0x20000000和0x2003ffff之间的地址空间,包含64个页面。我们不关心页面对应页帧的物理地址;实际上,它们甚至不一定在主存中,这里,我们只关心页表节点的其他字段。

假设我们现在需要读取线性地址0x20021406处的内容,分页单元的处理逻辑如下:
0x20021406换成二进制为0010 0000 0000 0010 0001 0100 0000 0110

  • 前10位为0010 0000 00,即0x80,即128(10)
  • 中间10位为00 0010 0001,即0x21
  • 后面12位为0100 0000 0110,即0x406


1、目录字段0x80用来选择页目录的第0x80号节点,它指向相应的页表;

2、表字段0x21用来选择页表的第0x21号节点,它指向包含目标页的页帧;

3、最后,偏移字段0x406用来选择页帧中0x406处的字节。
在这里插入图片描述

与intel处理器的分段相比,Linux更喜欢使用分页方式
因为基本不使用分段机制,所以,GDT容量(8K条)足够用。
内核是不使用LDT的
以下是Linux常用的段:
在这里插入图片描述

因为从上节的分段管理中,我们知道,段基址+逻辑地址=线性地址,上面列举的四项可以看出,段首地址基本相同,都是0x00000000,所以,Linux下逻辑地址和线性地址是一样的。

2.3 linxu为什么“不需要”分段了?

因为只用分页就足够了。我们把线性地址整合到进程里就可以了,这实际上与分段是一个道理的。
例如,在分页机制里,我们直接把页表的地址分配给进程不就好了吗?一个新的进程被加载时,系统分配一个页表地址保存在进程的PCB里,这与分配一个段基址如出一辙。之后调用进程时,把页表地址读进寄存器,逻辑地址提供页表索引和页内偏移不就可以找到任意内存位置了吗?为不同的进程分配不同的起始页表地址,因此避免了混淆。当然,这个例子是我自己想的,可能思想是对的,但细节不一定正确。

https://www.cnblogs.com/thrillerz/p/6031561.html

2.4 为什么需要分段和分页

想象一下,假如没有分段和分页机制的情况是什么样的? 这种情况下相当于直接操作内存,那么程序员在写代码的时候要自己考虑并写死用哪些物理地址!而且程序的运行一定需要连续的地址来一次装入程序!

上面的情况将引来三个问题:

1、地址空间不隔离

 如何理解地址空间不隔离?

  举个例子,假设我有两个程序,一个是程序A,一个是程序B。程序A在内存中的地址假设是0x00000000~0x00000099,程序B在内存中的地址假设是0x00000100~x00000199。那么假设你在程序A中,本来想操作地址0x00000050,不小心手残操作了地址0x00000150,那么,不好的事情或许会发生。你影响了程序A也就罢了,你把程序B也搞了一顿。

2、程序运行时候的地址不确定

 如何理解程序运行时候的地址不确定?

  因为我们程序每次要运行的时候,都是需要装载到内存中的,假设你在程序中写死了要操作某个地址的内存,例如你要地址0x00000010。但是问题来了,你能够保证你操作的地址0x00000010真的就是你原来想操作的那个位置吗?很可能程序第一次装载进内存的位置是0x00000000~0x00000099,而程序第二次运行的时候,这个程序装载进内存的位置变成了0x00000200~0x00000299,而你操作的0x00000010地址压根就不是属于这个程序所占有的内存。

3、内存使用率低下

 如何理解内存使用率低下呢?

  举个例子,假设你写了3个程序,其中程序A大小为10M,程序B为70M,程序C的大小为30M你的计算机的内存总共有100M。

  这三个程序加起来有110M,显然这三个程序是无法同时存在于内存中的。

  并且最多只能够同时运行两个程序。可能是这样的,程序A占有的内存空间是0x00000000~0x00000009,程序B占有的内存空间是0x00000010~0x00000079。假设这个时候程序C要运行该怎么做?可以把其中的一个程序换出到磁盘上,然后再把程序C装载到内存中。假设是把程序A换出,那么程序C还是无法装载进内存中,因为内存中空闲的连续区域有两块,一块是原来程序A占有的那10M,还有就是从0x00000080~0x00000099这20M,所以,30M的程序C无法装载进内存中。那么,唯一的办法就是把程序B换出,保留程序A,但是,此时会有60M的内存无法利用起来,很浪费对吧。

而分页的引入解决了第三个问题。分页机制和分段机制是非常类似的,他们都实现了隔离保护,分页是不同进程使用不同的页映射关系(页表),因此不同进程间互不影响。而且分页也不需要考虑如何操作物理地址了,分页后的地址称为线性地址,其关系是通过页表实现虚拟地址到物理地址的一一对应,这个对应是由MMU硬件实现的,不用程序员操心!而且分页和分段对于虚拟地址的映射方式基本一致,分段是通过段表GDT,LDT中的段描述符来确定段的属性和物理地址范围; 而分页是通过页表项(页表描述符)来记录页框的属性和物理地址位置!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

为什么要有分段和分页,分段和分页解决了什么,可以参考:
https://www.cnblogs.com/myseries/p/12487211.html

2.5 分段和分页的区别

分段解决了:

  • 地址空间不隔离
  • 程序运行时候的地址不确定

但是没有解决内存使用率低下的问题

分页解决了内存使用率低下的问题 使用了更加小粒度的内存分割和映射方法

在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。

分段和分页的比方:

打个比方,比如说你去听课,带了一个纸质笔记本做笔记。笔记本有100张纸,课程有语文、数学、英语三门,对于这个笔记本的使用,为了便于以后复习方便,你可以有两种选择。

第一种是,你从本子的第一张纸开始用,并且事先在本子上做划分:第2张到第30张纸记语文笔记,第31到60张纸记数学笔记,第61到100张纸记英语笔记,最后在第一张纸做个列表,记录着三门笔记各自的范围。这就是分段管理,第一张纸叫段表。

第二种是,你从第二张纸开始做笔记,各种课的笔记是连在一起的:第2张纸是数学,第3张是语文,第4张英语……最后呢,你在第一张纸做了一个目录,记录着语文笔记在第3、7、14、15张纸……,数学笔记在第2、6、8、9、11……,英语笔记在第4、5、12……。这就是分页管理,第一张纸叫页表。你要复习哪一门课,就到页表里查寻相关的纸的编号,然后翻到那一页去复习

分页的机制:

操作系统为该程序创建一个4GB的进程虚拟地址空间,把程序不用的部分交换到磁盘,当CPU要访问程序中这个虚拟地址时,CPU发现该地址并没有相关联的物理地址,CPU会认为该虚拟地址所在的页面是个空页面,CPU会认为这是个页错误(Page Fault),然后会产生产生一个缺页异常,然后异常管理程序会把放在磁盘的部分读到内存中,操作系统于是为该页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为PE文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。

Linux 简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的 1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统 内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

2.6 Linux的分页

Linux直到2.6.10,使用三级分页,2.6.11之后使用四级分页。

  • 全局页目录
  • 顶层页目录
  • 中层页目录
  • 页表

图2-12. Linux分页模型
在这里插入图片描述
全局页目录包含若干顶层页目录的地址,顶层页目录又包含若干中层页目录的地址,中层页目录又包含一些页表的地址。每个页表节点指向一个页帧。因此,线性地址可以分成五个部分。图2-12没有展示它们的位数,因为它们的长度取决于计算机架构。

对于没有启用PAE的32位系统,两级分页就足够了。Linux实际上会忽略顶层页目录和中层页目录。但是为了同时兼容32位和64位系统,顶层页目录和中层页目录的位置依然保存着,Linux定义它们的节点数为1,并使其映射到全局页目录的某个节点。

对于开启了扩展分页的32位系统,使用三级分页:Linux的全局页目录对应80x86的页目录指针表,顶层页目录被移除,中层页目录对应80x86的页目录,页表对应80x86的页表。

64位系统使用三级或四级分页。

三、浅析CPU高速缓存(cache)

CPU高速缓存是为了解决CPU速率和主存访问速率差距过大问题

程序员为何需要学习CPU cache?

作为一个程序员,我们需要理解存储器层次结构和CPU cache缓存原理,因为它们对程序性能有着巨大的影响。比如访问CPU寄存器中的数据,只需要一个时钟周期;访问高速缓存中的数据,大概需要几十个时钟周期;如果访问的数据在主存中,需要大概上百个周期;而访问磁盘中的数据则需要大约几千万个周期!因此我们应该了解存储器层次结构,让我们的程序尽可能得高效执行。

存储器层次结构

一种典型的存储器层级结构如下:
在这里插入图片描述
几个关键的层次访问速度:在这里插入图片描述
除了本文主角CPU高速缓存外,计算机系统还有很多利用“缓存”的地方:
在这里插入图片描述

SRAM和DRAM的区别:

  • DRAM用作内存比较多,SRAM用作cache比较多
  • SRAM与DRAM的区别只在于一个是静态一个是动态。由于SRAM不需要刷新电路就能够保存数据,所以具有静止存取数据的作用。而DRAM则需要不停地刷新电路,否则内部的数据将会消失。而且不停刷新电路的功耗是很高的,在我们的PC待机时消耗的电量有很大一部分都来自于对内存的刷新。
  • SRAM存储一位需要花6个晶体管,而DRAM只需要花一个电容和一个晶体管。cache追求的是速度所以选择SRAM,而内存则追求容量所以选择能够在相同空间中存放更多内容并且造价相对低廉的DRAM。

各种存储器类型的区别参考:https://www.amobbs.com/thread-5477455-1-1.html

程序运行的局部性

在了解了存储器层次结构后,我们考虑一个问题:如何用缓存提高程序运行效率?

计算机程序运行遵循局部性原则。局部性原理是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。局部性原理又表现为:时间局部性和空间局部性。

  • 时间局部性 是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
  • 空间局部性 是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。具有良好局部性的程序比差的程序更多的倾向于从存储器层次结构较高层次处访问数据,因此运行的更快,尤其是执行大数据量的算术运算。了解局部性原理,有利于提高我们程序的运行效率。

下面引用《深入理解计算机系统》中的一个经典例子,来理解程序局部性对效率的影响。
程序A:

int sum_arry(int a[m][n])
{
	int i, j, sum = 0;
	
	for (i = 0; i < m; i++) {
		for (j = 0; j< n; j++)
			sum += a[i][j];
	}
	return sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

程序B

int sum_arry(int a[m][n])
{
	int i, j, sum = 0;
	
	for (j = 0; j < n; j++) {
		for (i = 0; i< m; i++)
			sum += a[i][j];
	}
	return sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

两个程序都是对一个二维数组矩阵求和,不同的是第一种是先求第一行的和,然后第二行,依次类推,这也是最常见的写法。第二种i j换了位置,从矩阵上看是按列求和。

第一种顺序访问矩阵中的每个元素(存储顺序),具有步长为1的引用模式,这种模式被称为顺序引用模式。显然这种模式具有很好的空间局部性。顺序引用模式是程序空间局部性的重要来源。第二个程序局部性很差,并未按照二维数组在内存中的顺序来访问。

多级缓存

在前面介绍的存储器层级结构中,我们知道高速缓存是插在CPU寄存器和主存之间的缓存存储器,称为L1高速缓存,基本是由SRAM(static RAM)构成,访问时大约需要4个始终周期。刚开始只有L1高速缓存,后来CPU和主存访问速度差距不断增大,在L1和主存之间增加了L2高速缓存,可以在10个时钟周期内访问到。现代CPU又增加了一个更大的L3高速缓存,可以在大约50个时钟周期内访问到它。三个level的高速缓存用处依据具体SOC架构而定,下面给出一个典型的英特尔core:Intel Core i7

在这里插入图片描述
L1、L2缓存在CPU核内部独有,L1分D-cache(数据) 和 I-cache(指令)。多核共享L3 cache。

高速缓存控制器

一般高速缓存只有几十KB,而主存有几GB甚至更多,系统就是通过高速缓存控制器来管理缓存映射内存。
在这里插入图片描述
高速缓存是由硬件来管理的,一般OS不需要参与(具体情况后面会分析)。高速缓存控制器存放一个表项数组,每一个表项对应高速缓存中的line行。表项由标签tag(n位)和表示状态的几个flag组成(图5 b)。标签能让高速缓存控制器辨别 line行映射的内存单元。在访问存储单元时,把物理地址高几位和物理地址子集提取的行的标签对比,相同则表示命中高速缓存。读操作简单,写操作会涉及写高速缓存+DRAM 和 只写 高速缓存两种情况,较为复杂,一致性问题以后再分析。现代处理器都有多级缓存cache,多级间一致性由硬件处理,Linux只当一级处理。

随机访问存储器(RAM)分为静态随机访问存储器(Static Random Access Memory - SRAM)和动态随机存取存储器(Dynamic Random Access Memory -DRAM)

高速缓存原理

在x86体系中,由一个新单位:line,行。高速缓存换入换出的单元就是line。由几十个连续字节组成,用来在DRAM和SRAM间传送,实现高速缓存。主存中任意一行和高速缓存中N个行的任意一行相关联。

假设这样一个系统,主存地址有m位,共有M=2^m个不同地址;高速缓存分成S个组,每个组E行,每行B个字节:

在这里插入图片描述
每行由一个 B=2^b字节的数据块组成,一个有效位指明这个行是否包含有意义信息,还有t=m-(b+s)个标记位(tag bit)用于唯一标识存储在这个高速缓存行中的块。缓存总大小为C=BxExS。

这种缓存结构被称为:组相联高级缓存,

  • E等于1的时候称之为“直接映射高速缓存”,
  • E等于C/B即一个组包含所有行的时候称之为“全相联高速缓存”

每个组中行数E越多,硬件设计越复杂,成本就越高。但好处是标记内存地址的t位会越多,可以映射更多内存,即越不容易出现cache thrashing。

下面以一个“直接映射高速缓存”例子,来分析高速缓存原理。假设我们的缓存line为64字节(26),共512个line(29),一共32KB。

在这里插入图片描述
那么32K的大小怎么进行对4G的内存进行映射呢?高速缓存读物理内存的位置不是任意的,而是固定的。从RAM0地址开始,每32KB需要512行映射一次,这样固定的地址会被固定的行映射。假如CPU要访问一个地址:“0x0003 057E”,缓存映射关系和判断该地址是否命中缓存过程如下:
在这里插入图片描述
缓存确定一个请求是否命中,抽取被请求字的过程分三步:
在这里插入图片描述
【1】高速缓存抽取的地址是物理地址还是虚拟地址?

处理器在进行存储器访问时,处理器访问的是虚拟地址,经过TLB和MMU的映射,最终变成了物理地址。高速缓存在判断是否命中时使用的内存地址是虚拟内存还是物理内存?根据标记域(tag)和索引域(s位和b位)使用地址不同分为下面三种:
1)VIVT(Virtual Index Virtual Tag):使用虚拟地址索引域和虚拟地址的标记域:
简单理解是:高速缓存在判断是否命中时使用的内存地址是虚拟地址。虚拟地址直接送到高速缓存控制器,如果cache hit。直接从cache中返回数据给CPU。如果cache miss,则把虚拟地址发往MMU,经过MMU转换成物理地址,根据物理地址从主存(main memory)读取数据。
这种缓存优点是硬件设计简单,刚开始时很多CPU都是采用这种方式。但这种方式会引入很多软件使用上的问题。 操作系统在管理高速缓存正确工作的过程中,主要会面临两个问题。歧义(ambiguity)和别名(alias)。为了保证系统的正确工作,操作系统负责避免出现歧义和别名。
2)VIPT(Virtual Index Physical Tag):使用虚拟地址的索引域和物理地址的标记域:
VIPT使用虚拟地址的索引域和物理地址的标记域来查找cache line,它可以避免VIVT出现的别名(alias)问题。
以Linux kernel为例,它是以4kb为页面进行管理的,那么对于一个页来说,虚拟地址和物理地址的低12bit是相同的,所以不同的虚拟地址映射到同一个物理地址的时候,这些虚拟地址的低12bit也是相同的,在这种情况下,如果索引域在0~12bit之内,那么这些虚拟地址的cache组是相同的,同样的它们的Physical Tag也是相同的,那么这些不同的虚拟地址的cache line在cache里面是同一个,因此不会产生一致性问题。
3)PIPT(Physical Index Physical Tag):使用物理地址的索引域和物理地址的标记域
这种方式下索引域和标记域都采用物理地址,这种方式也可以避免缓存别名问题,不过性能方面要差一些,因为要先经过地址翻译的过程。
不同level的缓存,根据缓存大小、效率要求可能采用不同的虚拟/物理地址策略。VIVT Cache问题太多,软件维护成本过高,是最难管理的高速缓存,现在基本不再采用这种方式。

四、TLB和MMU

4.1 MMU介绍

MMU,全称内存管理单元,一般是CPU里的硬件电路,也可单独集成电路,主要功能是把虚拟地址转换为物理地址。通过段机制和页机制完成转换

在这里插入图片描述
至此,MMU可以完成地址转换,通过建立页表,把虚拟地址通过页表查找,得到最终的物理地址。例如,当需要访问内存中的一个数据,通过这个数据的虚拟地址查找页表,一旦在页表中找到(hit),就通过找到的物理地址寻址到内存中的数据。如果页表中没有找到(miss),表示页表中没有建立这个数据虚拟地址到物理地址的映射,通过缺页异常,建立这个页表映射项。

但是有个问题,当我们经常使用一些不变的数据,时间浪费在查找页表上了,尽管我们上次已经找过这个数据和它的页表项了 。
于是为了加快速度,减少不必要的重复,TLB出现了。

4.2.TLB介绍

TLB,俗称快表,因为它确实快。TLB是MMU的一部分,实质是cache,它所缓存的是最近使用的数据的页表项(虚拟地址到物理地址的映射)。他的出现是为了加快访问数据(内存)的速度,减少重复的页表查找。当然它不是必须要有的,但有它,速度就更快。在这里插入图片描述
TLB很小,因此,缓存的东西也不多。主要缓存最近使用的数据的数据映射。TLB结构如下图:在这里插入图片描述
如果一个需要访问内存中的一个数据,给定这个数据的虚拟地址,查询TLB,发现有(hit),直接得到物理地址,在内存根据物理地址取数据。如果TLB没有这个虚拟地址(miss),那么就只能费力的通过页表来查找了。

关于cpu如何读取内存中的一个数据,下面流程图说明:
在这里插入图片描述

4.3.TLB刷新与上下文切换

当进程地址空间进行了切换,比如现在是进程1运行,TLB中放的是进程1的相关数据的地址;突然切换到进程2,TLB中原有的数据不是进程2相关的,此时TLB需要刷新数据。怎么刷新数据?
目前两种方法:一,全部刷新。二,部分刷新。
全部刷新很简单,但花销大,很多不必刷新的数据也进行刷新,增加了无畏的花销。
部分刷新是根据标志位,刷新需要刷新的数据,保留不需要刷新的数据。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/811249
推荐阅读
相关标签
  

闽ICP备14008679号