为您推荐:
精华内容
最热下载
问答
  • 4星
    679B cuihao1995 2021-03-27 18:05:19
  • 5星
    6.88MB qq8864 2021-06-03 09:04:31
  • 5星
    20.8MB l1028386804 2021-05-18 11:29:38
  • 5星
    2.9MB weixin_44573410 2021-01-27 18:28:43
  • 在嵌入式实时系统内存管理根据不同的系统,有不同的策略,对于有些系统支持的虚拟内存管理机制,对于另外一些系统,可能只有flat式的简单内存管理机制。2、内存管理机制:大体上来说,嵌入式系统所用到的内存...

    1、概述

    操作系统的内存管理功能用于向操作系统提供一致的地址映射功能和内存页面的申请、释放操作。在嵌入式实时系统中,内存管理根据不同的系统,有不同的策略,对于有些系统支持的虚拟内存管理机制,对于另外一些系统,可能只有flat式的简单内存管理机制。

    2、内存管理机制:

    大体上来说,嵌入式系统所用到的内存管理机制主要有以下两种:

    (1)  虚拟内存管理机制:

    有一些嵌入式处理器提供了MMU,在MMU具备内存地址映射和寻址功能,它使操作系统的内存管理更加方便。如果存在MMU ,操作系统会使用它完成从虚拟地址到物理地址的转换, 所有的应用程序只需要使用虚拟地址寻址数据。 这种使用虚拟地址寻址整个系统的主存和辅存的方式在现代操作系统中被称为虚拟内存。MMU 便是实现虚拟内存的必要条件。

    虚拟内存的管理方法使系统既可以运行体积比物理内存还要大的应用程序,也可以实现“按需调页”策略,既满足了程序的运行速度,又节约了物理内存空间。

    在L inux系统中,虚拟内存机制的实现实现为我们提供了一个典型的例子:在不同的体系结构下, 使用了三级或者两级页式管理,利用MMU 完成从虚拟地址到物理地址之间的转换。基于虚拟内存管理的内存最大好处是:由于不同进程有自己单独的进程空间,十分有效的提高了系统可靠性和安全性。

    (2)  非虚拟内存管理机制

    在实时性要求比较高的情况下,很多嵌入式系统并不需要虚拟内存机制:因为虚拟内存机制会导致不确定性的 I/O阻塞时间, 使得程序运行时间不可预期,这是实时嵌入式系统的致命缺陷;另外,从嵌入式处理器的成本考虑,大多采用不装配MMU 的嵌入式微处理器。所以大多嵌入式系统采用的是实存储器管理策略。因而对于内存的访问是直接的,它对地址的访问不需要经过MMU,而是直接送到地址线上输出,所有程序中访问的地址都是实际的物理地址;而且,大多数嵌入式操作系统对内存空间没有保护,各个进程实际上共享一个运行空间。一个进程在执行前,系统必须为它分配足够的连续地址空间,然后全部载入主存储器的连续空间。

    由此可见,嵌入式系统的开发人员不得不参与系统的内存管理。从编译内核开始,开发人员必须告诉系统这块开发板到底拥有多少内存;在开发应用程序时,必须考虑内存的分配情况并关注应用程序需要运行空间的大小。另外,由于采用实存储器管理策略,用户程序同内核以及其它用户程序在一个地址空间,程序开发时要保证不侵犯其它程序的地址空间,以使得程序不至于破坏系统的正常工作,或导致其它程序的运行异常;因而,嵌入式系统的开发人员对软件中的一些内存操作要格外小心。

    UCOS就是使用非虚拟内存管理的一个例子,在UCOS中,所有的任务共享所有的物理内存,任务之间没有内存保护机制,这样能够提高系统的相应时间,但是任务内存操作不当,会引起系统崩溃。

    3、内存在系统中的生命期:

    对于内存在整个嵌入式运行过程中,以3中方式存在:

    1)、 在bootstraping 阶段,内存以临时内存分配的形式出现,当完成系统启动后,这些内存会回收供以后系统使用。

    2)、 在正常运行阶段,内存以两种方式存在:

    ( 1) 系统为代码,数据分配的永久内存,这些内存在系统运行过程中是不会改变的,有的硬件的I/O等外设也把相应的地址映射到固定的内存空间。

    ( 2) 动态内存分配空间:这些内存不会固定分配,而是根据系统需要而动态分配的,如果利用非虚拟内存管理机制,一般需要改造动态内存分配机制以提高性能。

    4、内存管理的具体应用:

    A、Linux系统的内存管理机制

    Linux内存管理机制中,X86体系结构是利用虚拟内存管理的典型,在i386CPU上,首先要进行段式映射,Linux 没有用到段式管理,它的做法是把GDT中段描述符段的大小定义为4GB, 也就是说只分了一段, 从而使段式映射没有起作用。在页式映射中,对于嵌入式i386芯片来说,实际上是两层映射, 跳过中间的PMD层次。对于程序来说,并非所有虚存都映射都到物理空间了,而是动态映射,如果程序运行时内核发现虚拟页面没有映射或映射的是磁盘页面, 会作相应的缺页处理——分配内存页面并建立映射,然后恢复程序运行。

    在程序运行的过程中,涉及到的内存操作主要有内存分配、内存使用、内存回收、内存页面换出、页面换入。内存分配会在管理区的空闲区进行, 通过Buddy算法在管理区的free_area 中获得需要的内存块。如果内存不足, 则会启动Kswapd这个守护进程腾出部分物理内存。除了被调用, Kswapd 进程还会定时启动。Kswapd 的工作分两部分:

    ( 1) 检测物理内存剩余的情况, 如果短缺, 则按LRU策略断开active_list 队列中部分可交换页面的映射, 使页面变为不活跃状态,链入inactive_clean_list队列或者inactive_dirty_list队列, 为换出做准备。

    ( 2) 每次都执行, 把inactive_ dirty_list 中的页面写入交换设备,并且回收一部分inactive_clean_list 中的页面。

    Linux系统虚拟内存机制的屏蔽

    由于虚拟内存在时间上的不可预期性,对于实时性要求很高的系统,必须屏蔽虚拟内存机制。在uCLinux中就利用了这种技术一保证系统的实时性,下面是屏蔽虚拟内存机制的思路:

    为了满足在工业控制中一些任务的实时性要求,必须屏蔽内核的虚拟内存管理机制以增强Linux的实时性。当要更改内核的某项机制时,一般不必大规模的改写代码,可采用条件编译的方法。思路是用#ifdef或 #ifndef屏蔽现有语句,在#else宏编译语句中包括自己编写的代码。实现虚拟内存的机制有:地址映射机制、内存分配和回收机制,缓存和刷新机制、请页机制、交换机制、内存共享机制,将实现这些机制的数据结构和函数屏蔽或修改,还要修改与之相关的文件。需要改动的文件主要在 /include/linux、/mm、/drivers/char、/fs、/ipc/kernel、/init目录下。

    主要的改动如下:与虚存有关的主要的数据结构是vm_area_struct,将进程的mm_struct结构中的vm_area_struct去掉,vm_area_struct利用了vm_ops来抽象出对虚拟内存的处理方法,屏蔽与虚拟内存操作有关的函数。内存映射主要由do_mmap()实现,改写此函数的代码。取消交换操作,屏蔽用于交换的结构和函数声明,以及实现交换的代码。取消内核守护进程kswapd。

    B、UCOS的内存管理:

    UCOS的内存管理与大多数嵌入式系统一样,是flat内存,但在此flat内存的基础上进行了优化,使在动态内存分配的时候,减少了内存粹片,提高了系统性能。

    UCOS的具体方法是:把连续的大块内存进行分区,每个分区包含整数个大小相同的内存块,在一个系统中有多个不同内存大小的分区。这样,应用程序根据不同的需求,从不同大小的内存分区中分配相应大小的内存。对于不用的内存,又重新释放回原来的分区。通过这样的内存管理算法,解决了内存粹片的问题,提高了系统性能。UCOS具体的实现可以参看源码。主要由以下几个函数实现:

    OSMemCreate():创建内存分区。

    OSMenGet() :为应用程序分配一段内存。

    OSMenPut():回收应用程序不再使用的内存。

    5、结论:

    内存管理是嵌入式系统的一个重要方面,虚拟内存管理机制在为进程安全提供很好保证的同时,也为开发人员提供了一个管理内存的方法,使开发人员更多的关注其他的方面。但是它也带来了时间不确定性的缺陷。根据不同的系统需求,我们可以选取相应的内存管理策略。在现在大多数的实时系统中,非虚拟内存管理机制用得比较多,这样保证了系统的实时性,但是增加了开发的难度,任务内存操作不当,可能引起系统崩溃。

    由于虚拟内存管理的请求换页机制在很大程度上影响了系统的实时性能,现在有些开发人员提出了一个折衷方案,即不用虚拟内存管理的请求换页机制,只考虑进程保护、内存映射、共享虚拟内存等功能,这样既能提高系统的实时性,又能提高系统的安全性,具体请参考文献4。

    参考文献:

    1、基于Linux的嵌入式系统在测控系统中的设计.http://article.ednchina.com/Embeded/20070630090542.htm

    2、The Linux kernel Primer.

    3、嵌入式L inux 操作系统的研究.  刘文峰, 李程远, 李善平

    4、嵌入式软件虚拟内存管理技术的研究和实现. 钱 静, 芦东昕, 谢 鑫, 徐立锋

    5、Understanding the Linux kernel.

    作者:Bluestorm's Space

    来源:博客园

    展开全文
    weixin_33548923 2021-05-30 18:31:36
  • 内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入到主板上的,因此对于一台计算机...带着这些疑问我们来看看计算系统内存管理那些事。 内存的交换技术..

    内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入到主板上的,因此对于一台计算机来说,由于物理限制,它的内存不可能无限大的。我们知道我们写的代码最终是要从磁盘被加载到内存中的,然后再被CPU执行,不知道你有没有想过,为什么一些大型游戏大到10几G,却可以在只有8G内存的电脑上运行?甚至在玩游戏期间,我们还可以聊微信、听音乐...,这么多进程看着同时在运行,它们在内存中是如何被管理的?带着这些疑问我们来看看计算系统内存管理那些事。

    内存的交换技术

    如果我们的内存可以无限大,那么我们担忧的问题就不会存在,但是实际情况是往往我们的机器上会同时运行多个进程,这些进程小到需要几十兆内存,大到可能需要上百兆内存,当许许多多这些进程想要同时加载到内存的时候是不可能的,但是从我们用户的角度来看,似乎这些进程确实都在运行呀,这是怎么回事?

    这就引入要说的交换技术了,从字面的意思来看,我想你应该猜到了,它会把某个内存中的进程交换出去。当我们的进程空闲的时候,其他的进程又需要被运行,然而很不幸,此时没有足够的内存空间了,这时候怎么办呢?似乎刚刚那个空闲的进程有种占着茅坑不拉屎的感觉,于是可以把这个空闲的进程从内存中交换到磁盘上去,这时候就会空出多余的空间来让这个新的进程运行,当这个换出去的空闲进程又需要被运行的时候,那么它就会被再次交换进内存中。通过这种技术,可以让有限的内存空间运行更多的进程,进程之间不停来回交换,看着好像都可以运行。 

     如图所示,一开始进程A被换入内存中,所幸还剩余的内存空间比较多,然后进程B也被换入内存中,但是剩余的空间比较少了,这时候进程C想要被换入到内存中,但是发现空间不够了,这时候会把已经运行一段时间的进程A换到磁盘中去,然后调入进程C。 

    内存碎片

    通过这种交换技术,交替的换入和换出进程可以达到小内存可以运行更多的进程,但是这似乎也产生了一些问题,不知道你发现了没有,在进程C换入进来之后,在进程B和进程C之间有段较小的内存空间,并且进程B之上也有段较小的内存空间,说实话,这些小空间可能永远没法装载对应大小的程序,那么它们就浪费了,在某些情况下,可能会产生更多这种内存碎片。

     如果想要节约内存,那么就得用到内存紧凑的技术了,即把所有的进程都向下移动,这样所有的碎片就会连接在一起变成一段更大的连续内存空间了。 

     但是这个移动的开销基本和当前内存中的活跃进程成正比,据统计,一台16G内存的计算机可以每8ns复制8个字节,它紧凑全部的内存大概需要16s,所以通常不会进行紧凑这个操作,因为它耗费的CPU时间还是比较大的。

    动态增长

    其实上面说的进程装载算是比较理想的了,正常来说,一个进程被创建或者被换入的时候,它占用多大的空间就分配多大的内存,但是如果我们的进程需要的空间是动态增长的,那就麻烦了,比如我们的程序在运行期间的for循环可能会利用到某个临时变量来存放目标数据(例如以下变量a,随着程序的运行是会越来越大的):

    var a []int64
    for i:= 0;i <= 1000000;i++{
      if i%2 == 0{
       a = append(a,i) //a是不断增大的
      }
    }
    

    当需要增长的时候:

    1. 如果进程的邻居是空闲区那还好,可以把该空闲区分配给进程
    2. 如果进程的邻居是另一个进程,那么解决的办法只能把增长的进程移动到一个更大的空闲内存中,但是万一没有更大的内存空间,那么就要触发换出,把一个或者多个进程换出去来提供更多的内存空间,很明显这个开销不小。

    为了解决进程空间动态增长的问题,我们可以提前多给一些空间,比如进程本身需要10M,我们多给2M,这样如果进程发生增长的时候,可以利用这2M空间,当然前提是这2M空间够用,如果不够用还是得触发同样的移动、换出逻辑。

    空闲的内存如何管理

    前面我们说到内存的交换技术,交换技术的目的是腾出空闲内存来,那么我们是如何知道一块内存是被使用了,还是空闲的?因此需要一套机制来区分出空闲内存和已使用内存,一般操作系统对内存管理的方式有两种:位图法链表法

    位图法

    先说位图法,没错,位图法采用比特位的方式来管理我们的内存,每块内存都有位置,我们用一个比特位来表示:

    1. 如果某块内存被使用了,那么比特位为1
    2. 如果某块内存是空闲的,那么比特位为0

    这里的某块内存具体是多大得看操作系统是如何管理的,它可能是一个字节、几个字节甚至几千个字节,但是这些不是重点,重点是我们要知道内存被这样分割了。

     位图法的优点就是清晰明确,某个内存块的状态可以通过位图快速的知道,因为它的时间复杂度是O(1),当然它的缺点也很明显,就是需要占用太多的空间,尤其是管理的内存块越小的时候。更糟糕的是,进程分配的空间不一定是内存块的整数倍,那么最后一个内存块中一定是有浪费的。

    如图,进程A和进程B都占用的最后一个内存块的一部分,那么对于最后一个内存块,它的另一部分一定是浪费的。

    链表法

    相比位图法,链表法对空间的利用更加合理,我相信你应该已经猜到了,链表法简单理解就是把使用的和空闲的内存用链表的方式连接起来,那么对于每个链表的元素节点来说,他应该具备以下特点:

    1. 应该知道每个节点是空闲的还是被使用的
    2. 每个节点都应该知道当前节点的内存的开始地址和结束地址

    针对这些特点,最终内存对应的链表节点大概是这样的:

     p代表这个节点对应的内存空间是被使用的,H代表这个节点对应的内存空间是空闲的,start代表这块内存空间的开始地址,length代表的是这块内存的长度,最后还有指向邻居节点的pre和next指针

    因此对于一个进程来说,它与邻居的组合有四种:

    1. 它的前后节点都不是空闲的
    2. 它的前一个节点是空闲的,它的后一个节点也不是空闲的
    3. 它的前一个节点不是空闲的,它的后一个节点是空闲的
    4. 它的前后节点都是空闲的

    当一个内存节点被换出或者说进程结束后,那么它对应的内存就是空闲的,此时如果它的邻居也是空闲的,就会发生合并,即两块空闲的内存块合并成一个大的空闲内存块。

    ok,通过链表的方式把我们的内存给管理起来了,接下来就是当创建一个进程或者从磁盘换入一个进程的时候,如何从链表中找到一块合适的内存空间?

    首次适应算法

    其实想要找到空闲内存空间最简单的办法就是顺着链表找到第一个满足需要内存大小的节点,如果找到的第一个空闲内存块和我们需要的内存空间是一样大小的,那么就直接利用,但是这太理想了,现实情况大部分可能是找到的第一个目标内存块要比我们的需要的内存空间要大一些,这时候呢,会把这个空闲内存空间分成两块,一块正好使用,一块继续充当空闲内存块。

     一个需要3M内存的进程,会把4M的空间拆分成3M和1M。

    下次适配算法

    和首次适应算法很相似,在找到目标内存块后,会记录下位置,这样下次需要再次查找内存块的时候,会从这个位置开始找,而不用从链表的头节点开始寻找,这个算法存在的问题就是,如果标记的位置之前有合适的内存块,那么就会被跳过。

     一个需要2M内存的进程,在5这个位置找到了合适的空间,下次如果需要这1M的内存会从5这个位置开始,然后会在7这个位置找到合适的空间,但是会跳过1这个位置。

    最佳适配算法

    相比首次适应算法,最佳适配算法的区别就是:不是找到第一个合适的内存块就停止,而是会继续向后找,并且每次都可能要检索到链表的尾部,因为它要找到最合适那个内存块,什么是最合适的内存块呢?如果刚好大小一致,则一定是最合适的,如果没有大小一致的,那么能容得下进程的那个最小的内存块就是最合适的,可以看出最佳适配算法的平均检索时间相对是要慢的,同时可能会造成很多小的碎片。

     假设现在进程需要2M的内存,那么最佳适配算法会在检索到3号位置(3M)后,继续向后检索,最终会选择5号位置的空闲内存块。

    最差适配算法

    我们知道最佳适配算法中最佳的意思是找到一个最贴近真实大小的空闲内存块,但是这会造成很多细小的碎片,这些细小的碎片一般情况下,如果没有进行内存紧凑,那么大概率是浪费的,为了避免这种情况,就出现了这个最差适配算法,这个算法它和最佳适配算法是反着来的,它每次尝试分配最大的可用空闲区,因为这样的话,理论上剩余的空闲区也是比较大的,内存碎片不会那么小,还能得到重复利用。

     一个需要1.5M的进程,在最差适配算法情况下,不会选择3号(2M)内存空闲块,而是会选择更大的5号(3M)内存空闲块。

    快速适配算法

    上面的几种算法都有一个共同的特点:空闲内存块和已使用内存块是共用的一个链表,这会有什么问题呢?正常来说,我要查找一个空闲块,我并不需要检索已经被使用的内存块,所以如果能把已使用的和未使用的分开,然后用两个链表分别维护,那么上面的算法无论哪种,速度都将得到提升,并且节点也不需要P和M来标记状态了。但是分开也有缺点,如果进程终止或者被换出,那么对应的内存块需要从已使用的链表中删掉然后加入到未使用的链表中,这个开销是要稍微大点的。当然对于未使用的链表如果是排序的,那么首次适应算法和最佳适应算法是一样快的。

    快速适配算法就是利用了这个特点,这个算法会为那些常用大小的空闲块维护单独的链表,比如有4K的空闲链表、8K的空闲链表...,如果要分配一个7K的内存空间,那么既可以选择两个4K的,也可以选择一个8K的。

     它的优点很明显,在查找一个指定大小的空闲区会很快速,但是一个进程终止或被换出时,会寻找它的相邻块查看是否可以合并,这个过程相对较慢,如果不合并的话,那么同样也会产生很多的小空闲区,它们可能无法被利用,造成浪费。

    虚拟内存:小内存运行大程序

    可能你看到小内存运行大程序比较诧异,因为上面不是说到了吗?只要把空闲的进程换出去,把需要运行的进程再换进来不就行了吗?内存交换技术似乎解决了,这里需要注意的是,首先内存交换技术在空间不够的情况下需要把进程换出到磁盘上,然后从磁盘上换入新进程,看到磁盘你可能明白了,很慢。其次,你发现没,换入换出的是整个进程,我们知道进程也是由一块一块代码组成的,也就是许许多多的机器指令,对于内存交换技术来说,一个进程下的所有指令要么全部进内存,要么全部不进内存。看到这里你可能觉得这不是正常吗?好的,别急,我们接着往下看。

    后来出现了更牛逼的技术:虚拟内存。它的基本思想就是,每个程序拥有自己的地址空间,尤其注意后面的自己的地址空间,然后这个空间可以被分割成多个块,每一个块我们称之为(page)或者叫页面,对于这些页来说,它们的地址是连续的,同时它们的地址是虚拟的,并不是真正的物理内存地址,那怎么办?程序运行需要读到真正的物理内存地址,别跟我玩虚的,这就需要一套映射机制,然后MMU出现了,MMU全称叫做:Memory Managment Unit,即内存管理单元,正常来说,CPU读某个内存地址数据的时候,会把对应的地址发到内存总线上,但是在虚拟内存的情况下,直接发到内存总线上肯定是找不到对应的内存地址的,这时候CPU会把虚拟地址告诉MMU,让MMU帮我们找到对应的内存地址,没错,MMU就是一个地址转换的中转站。

    程序地址分页的好处是:

    1. 对于程序来说,不需要像内存交换那样把所有的指令都加载到内存中才能运行,可以单独运行某一页的指令
    2. 当进程的某一页不在内存中的时候,CPU会在这个页加载到内存的过程中去执行其他的进程。

    当然虚拟内存会分页,那么对应的物理内存其实也会分页,只不过物理内存对应的单元我们叫页框。页面和页框通常是一样大的。我们来看个例子,假设此时页面和页框的大小都是4K,那么对于64K的虚拟地址空间可以得到64/4=16个虚拟页面,而对于32K的物理地址空间可以得到32/4=8个页框,很明显此时的页框是不够的,总有些虚拟页面找不到对应的页框。

    我们先来看看虚拟地址为20500对应物理地址如何被找到的:

    1. 首先虚拟地址20500对应5号页面(20480-24575)
    2. 5号页面的起始地址20480向后查找20个字节,就是虚拟地址的位置
    3. 5号页面对应3号物理页框
    4. 3号物理页框的起始地址是12288,12288+20=12308,即12308就是我们实际的目标物理地址。

    但是对于虚拟地址而言,图中还有红色的区域,上面我们也说到了,总有些虚拟地址没有对应的页框,也就是这部分虚拟地址是没有对应的物理地址,当程序访问到一个未被映射的虚拟地址(红色区域)的时候,那么就会发生缺页中断,然后操作系统会找到一个最近很少使用的页框把它的内容换到磁盘上去,再把刚刚发生缺页中断的页面从磁盘读到刚刚回收的页框中去,最后修改虚拟地址到页框的映射,然后重启引起中断的指令。

    最后可以发现分页机制使我们的程序更加细腻了,运行的粒度是页而不是整个进程,大大提高了效率。

    页表

    上面说到虚拟内存到物理内存有个映射,这个映射我们知道是MMU做的,但是它是如何实现的?最简单的办法就是需要有一张类似hash表的结构来查看,比如页面1对应的页框是10,那么就记录成hash[1]=10,但是这仅仅是定位到了页框,具体的位置还没定位到,也就是类似偏移量的数据没有。不猜了,我们直接来看看MMU是如何做到的,以一个16位的虚拟地址,并且页面和页框都是4K的情况来说,MMU会把前4位当作是索引,也就是定位到页框的序号,后12位作为偏移量,这里为什么是12位,很巧妙,因为2^12=4K,正好给每个页框里的数据上了个标号。因此我们只需要根据前4位找到对应的页框即可,然后偏移量就是后12位。找页框就是去我们即将要说的页表里去找,页表除了有页面对应的页框后,还有个标志位来表示对应的页面是否有映射到对应的页框,缺页中断就是根据这个标志位来的。

    可以看出页表非常关键,不仅仅要知道页框、以及是否缺页,其实页表还有保护位修改位访问位高速缓存禁止位

    • 保护位:指的是一个页允许什么类型的访问,常见的是用三个比特位分别表示执行
    • 修改位:有时候也称为脏位,由硬件自动设置,当一个页被修改后,也就是和磁盘的数据不一致了,那么这个位就会被标记为1,下次在页框置换的时候,需要把脏页刷回磁盘,如果这个页的标记为0,说明没有被修改,那么不需要刷回磁盘,直接把数据丢弃就行了。
    • 访问位:当一个页面不论是发生读还是发生写,该页面的访问位都会设置成1,表示正在被访问,它的作用就是在发生缺页中断时,根据这个标志位优先在那些没有被访问的页面中选择淘汰其中的一个或者多个页框。
    • 高速缓存禁止位:对于那些映射到设备寄存器而不是常规内存的页面而言,这个特性很重要,加入操作系统正在紧张的循环等待某个IO设备对它刚发出的指令做出响应,保证这个设备读的不是被高速缓存的副本非常重要。

    TLB快表加速访问

    通过页表我们可以很好的实现虚拟地址到物理地址的转换,然而现代计算机至少是32位的虚拟地址,以4K为一页来说,那么对于32位的虚拟地址,它的页表项就有2^20=1048576个,无论是页表本身的大小还是检索速度,这个数字其实算是有点大了。如果是64位虚拟的地址,按照这种方式的话,页表项将大到超乎想象,更何况最重要的是每个进程都会有一个这样的页表

    我们知道如果每次都要在庞大的页表里面检索页框的话,效率一定不是很高。而且计算机的设计者们观察到这样一种现象:大多数程序总是对少量的页进行多次访问,如果能为这些经常被访问的页单独建立一个查询页表,那么速度就会大大提升,这就是快表,快表只会包含少量的页表项,通常不会超过256个,当我们要查找一个虚拟地址的时候。首先会在快表中查找,如果能找到那么就可以直接返回对应的页框,如果找不到才会去页表中查找,然后从快表中淘汰一个表项,用新找到的页替代它。

    总体来说,TLB类似一个体积更小的页表缓存,它存放的都是最近被访问的页,而不是所有的页。

    多级页表

    TLB虽然一定程度上可以解决转换速度的问题,但是没有解决页表本身占用太大空间的问题。其实我们可以想想,大部分程序会使用到所有的页面吗?其实不会。一个进程在内存中的地址空间一般分为程序段、数据段和堆栈段,堆栈段在内存的结构上是从高地址向低地址增长的,其他两个是从低地址向高地址增长的。

    可以发现中间部分是空的,也就是这部分地址是用不到的,那我们完全不需要把中间没有被使用的内存地址也引入页表呀,这就是多级页表的思想。以32位地址为例,后12位是偏移量,前20位可以拆成两个10位,我们暂且叫做顶级页表和二级页表,每10位可以表示2^10=1024个表项,因此它的结构大致如下:

     对于顶级页表来说,中间灰色的部分就是没有被使用的内存空间。顶级页表就像我们身份证号前面几个数字,可以定位到我们是哪个城市或者县的,二级页表就像身份证中间的数字,可以定位到我们是哪个街道或者哪个村的,最后的偏移量就像我们的门牌号和姓名,通过这样的分段可以大大减少空间,我们来看个简单的例子:

    如果我们不拆出顶级页表和二级页表,那么所需要的页表项就是2^20个,如果我们拆分,那么就是1个顶级页表+2^10个二级页表,两者的存储差距明显可以看出拆分后更加节省空间,这就是多级页表的好处。

    当然我们的二级也可以拆成三级、四级甚至更多级,级数越多灵活性越大,但是级数越多,检索越慢,这一点是需要注意的。


    最后

    为了便于大家理解,本文画了20张图,肝了将近7000多字,创作不易,各位的三连就是对作者最大的支持,也是作者最大的创作动力。

    微信搜一搜【假装懂编程】,加入我们,与作者共同学习,共同进步。

     

    往期精彩:

    展开全文
    shanwu1 2021-11-04 09:26:35
  • 内存管理架构 内存管理子系统架构可以分为:用户空间、内核空间及硬件部分3个层面,具体结构如下所示: 1、用户空间:应用程序使用malloc()申请内存资源/free()释放内存资源。 2、内核空间:内核总是驻留在内存,...

    一、内存管理架构

    内存管理子系统架构可以分为:用户空间、内核空间及硬件部分3个层面,具体结构如下所示:
    1、用户空间:应用程序使用malloc()申请内存资源/free()释放内存资源。
    2、内核空间:内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
    3、硬件:处理器包含一个内存管理单元(Memory Management Uint,MMU)的部件,负责把虚拟地址转换为物理地址。

    二、虚拟地址空间布局架构

    上面的用户空间和内核空间所指的都是虚拟地址,物理地址没有用户和内核之分。每个项目的物理地址对于进程不可见,谁也不能直接访问这个物理地址。操作系统会给进程分配一个虚拟地址。所有进程看到的这个地址都是一样的,里面的内存都是从 0 开始编号。
    所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
    在程序里面,指令写入的地址是虚拟地址。例如,位置为 10M 的内存区域,操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
    当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。
    32位处理器使用32位虚拟地址,而64位处理器却不是使用64位虚拟地址。因为目前应用程序没有那么大的内存需求,所以ARM64和X86_64处理器不支持完全的64位虚拟地址,而是使用了48位。我们计算一下,如果是 32 位,有 2^32 = 4G 的内存空间都是我的,不管内存是不是真的有 4G。如果是 64 位,在 ARM64和X86_64 下面,其实只使用了 48 位,那也挺恐怖的。48 位地址长度也就是对应了 256TB 的地址空间。我都没怎么见过 256T 的硬盘,别说是内存了。
    在这里插入图片描述

    这么大的虚拟空间一切二,一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间。
    用户空间其实包含以下几个区域,我们最低位开始排起:
    1.代码段,数据段,未初始化的数据段(bss)
    2.存放动态生成数据的堆,堆是往高地址增长的
    3.动态库的代码段,数据段和未初始化的数据段(bss)
    4.存放局部变量和实现函数调用的栈
    5.把文件映射到虚拟地址空间的内存映射区
    6.存放在栈底的环境变量和参数字符串

    三、物理内存体系架构

    目前多处理器系统有两种体系结构:

    1)一致内存访问(Uniform Memory Access,UMA),所有处理器访问内存花费的时间是相同。

    在这里插入图片描述
    这种结构的CPU 是通过一条通用总线连接到北桥,北桥中的内存控制器链接着内存。这种设计中,瓶颈马上出现了。第一个瓶颈与设备对RAM 的访问有关。早期,所有设备之间的通信都需要经过 CPU,结果严重影响了整个系统的性能。为了解决这个问题,有些设备加入了直接内存访问(DMA)的能力。DMA 允许设备在北桥的帮助下,无需 CPU 的干涉,直接读写 RAM。到了今天,所有高性能的设备都可以使用 DMA。虽然 DMA 大大降低了 CPU 的负担,却占用了北桥的带宽,与 CPU 形成了争用。所以现在很少使用了。

    2)非一致内存访问(Non-Unit Memory Access,NUMA):指内存被划分成多个内存节点的多处理器系统,访问一个内存节点花费的时间取决于处理器和内存节点的距离。在这里插入图片描述
    采用这样的架构,系统里有几个处理器,就可以有几个内存库。系统仍然要让所有内存能被所有处理器所访问,导致内存不再是统一的资源。处理器能以正常的速度访问本地内存(连接到该处理器的内存)。但它访问其它处理器的内存时,却需要使用处理器之间的互联通道。

    四、内存结构

    由于现在我接触的基本都是使用UMA的结构,所以下面说的都是这种:
    内存管理子系统使用节点(node),区域(zone)、页(page)三级结构描述物理内存。节点是基于哪个cpu,一般多少核的cpu就有多少个节点node;zone是每个cpu,也就是每个节点会把内存分为高端内存,低端内存,DMA区域等等的内存区域;页就是物理内存的最小单位了,也是虚拟内存映射到物理内存的最小单位。最后,在NUMA内存架构中, Linux定义了一个 pglist_data 的结构体来管理所有的内存节点.

    1. 内存节点(node)

    在NUMA体系的内存节点是根据处理器和内存的距离划分的,而在具有不连续内存的NUMA系统中,表示比区域的级别更高的内存区域,根据物理地址是否连续划分,每块物理地址连续的内存是一个内存节点。内存节点结构体在linux内核include/linux/mmzone.h文件中,

    /*
     * On NUMA machines, each NUMA node would have a pg_data_t to describe
     * it's memory layout. On UMA machines there is a single pglist_data which
     * describes the whole memory.
     *
     * Memory statistics and page replacement data structures are maintained on a
     * per-zone basis.
     */
    struct bootmem_data;
    typedef struct pglist_data {
    	struct zone node_zones[MAX_NR_ZONES];//内存区域数组
    	struct zonelist node_zonelists[MAX_ZONELISTS];//备用区域数组
    
    	int nr_zones;//该节点包含的内存区域数量
    #ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
    	struct page *node_mem_map;//指向物理页描述符数组
    #ifdef CONFIG_PAGE_EXTENSION
    	struct page_ext *node_page_ext;//页的扩展属性
    #endif
    #endif
    #ifndef CONFIG_NO_BOOTMEM
    	struct bootmem_data *bdata;//早期内存管理器
    ......
    } pg_data_t;
    
    1. 内存区域(zone)

    每一个节点分成一个个区域 zone,放在数组 node_zones 里面。这个数组的大小为 MAX_NR_ZONES。我们来看区域的定义。内存节点被划分为内存区域,内存区域结构体在linux内核include/linux/mmzone.h文件中

    struct zone {
    	unsigned long watermark[NR_WMARK];
    	unsigned long nr_reserved_highatomic;
    	long lowmem_reserve[MAX_NR_ZONES];
    #ifdef CONFIG_NUMA
    	int node;
    #endif
    	struct pglist_data	*zone_pgdat;
    	struct per_cpu_pageset __percpu *pageset;
    #ifndef CONFIG_SPARSEMEM
    	unsigned long		*pageblock_flags;
    #endif /* CONFIG_SPARSEMEM */
    	unsigned long		zone_start_pfn;
    	unsigned long		managed_pages;
    	unsigned long		spanned_pages;
    	unsigned long		present_pages;
    	const char		*name;
    #ifdef CONFIG_MEMORY_ISOLATION
    	unsigned long		nr_isolate_pageblock;
    #endif
    #ifdef CONFIG_MEMORY_HOTPLUG
    	seqlock_t		span_seqlock;
    #endif
    	int initialized;
    	ZONE_PADDING(_pad1_)
    	struct free_area	free_area[MAX_ORDER];//内存区域数组,用于伙伴分配器进行页分配
    	unsigned long		flags;//内存区域的属性,定义在下面
    	spinlock_t		lock;
    	ZONE_PADDING(_pad2_)
    	unsigned long percpu_drift_mark;
    #ifdef CONFIG_COMPACTION
    	unsigned int		compact_considered;
    	unsigned int		compact_defer_shift;
    	int			compact_order_failed;
    #endif
    #if defined CONFIG_COMPACTION || defined CONFIG_CMA
    	bool			compact_blockskip_flush;
    #endif
    	bool			contiguous;
    	ZONE_PADDING(_pad3_)
    	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
    	atomic_long_t		vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
    } ____cacheline_internodealigned_in_smp;
    
    //struct zone 的 flags 参数
    enum zone_type {
    #ifdef CONFIG_ZONE_DMA
    	ZONE_DMA,
    #endif
    #ifdef CONFIG_ZONE_DMA32
    	ZONE_DMA32,
    #endif
    	ZONE_NORMAL,
    #ifdef CONFIG_HIGHMEM
    	ZONE_HIGHMEM,
    #endif
    	ZONE_MOVABLE,
    #ifdef CONFIG_ZONE_DEVICE
    	ZONE_DEVICE,
    #endif
    	__MAX_NR_ZONES
    };
    

    ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接内存存取)的内存。DMA 是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过 CPU 控制完成,但是这会占用 CPU,影响 CPU 处理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,这样就可以解放 CPU。对于 64 位系统,有两个 DMA 区域。除了上面说的 ZONE_DMA,还有 ZONE_DMA32。在这里你大概理解 DMA 的原理就可以,不必纠结,我们后面会讲 DMA 的机制。
    ZONE_NORMAL 是直接映射区,就是上一节讲的,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。ZONE_HIGHMEM 是高端内存区,就是上一节讲的,对于 32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域。
    ZONE_MOVABLE 是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。
    这里你需要注意一下,我们刚才对于区域的划分,都是针对物理内存的。

    1. 内存页(page)

    了解了区域 zone,接下来我们就到了组成物理内存的基本单位,页的数据结构 struct page。这是一个特别复杂的结构,里面有很多的 union,union 结构是在 C 语言中被用于同一块内存根据情况保存不同类型数据的一种方式。这里之所以用了 union,是因为一个物理页面使用模式有两种。第一种模式,仅需分配小块内存,Linux 系统采用了一种被称为 slab allocator的技术,下一节会讲。
    第二种模式,要用就用一整页。这一整页的内存,或者直接和虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File)。
    每个物理页对应一个page结构体,称为页描述符,内存节点的pglist_data实例的成员node_mem_map指向该内存节点包含的所有物理页的页描述符组成的数组。内存区域结构体在linux内核include/linux/mm_types.h文件中

    struct page {
    	unsigned long flags;		/* Atomic flags, some possibly
    					 * updated asynchronously */
    ......
    } _struct_page_alignment;
    

    其结构大概如下图所示:
    在这里插入图片描述

    五、内存模型

    内存模型是其实就是从cpu的角度看,其物理内存的分布情况,在linux kernel中,使用什么的方式来管理这些物理内存。
    内存管理子系统支持3种内存模型:
    1)平坦内存(Flat Memory):内存的物理地址空间是连续的,没有空洞。
    如果从系统中任意一个processor的角度来看,当它访问物理内存的时候,物理地址空间是一个连续的,没有空洞的地址空间,那么这种计算机系统的内存模型就是Flat memory。这种内存模型下,物理内存的管理比较简单,每一个物理页帧都会有一个page数据结构来抽象,因此系统中存在一个struct page的数组(mem_map),每一个数组条目指向一个实际的物理页帧(page frame)。在flat memory的情况下,PFN(page frame number)和mem_map数组index的关系是线性的(有一个固定偏移,如果内存对应的物理地址等于0,那么PFN就是数组index)。因此从PFN到对应的page数据结构是非常容易的,反之亦然,具体可以参考page_to_pfn和pfn_to_page的定义。此外,对于flat memory model,节点(struct pglist_data)只有一个(为了和Discontiguous Memory Model采用同样的机制)。需要强调的是struct page所占用的内存位于直接映射(directly mapped)区间,因此操作系统不需要再为其建立page table。

    #define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 
    

    2)不连续内存(Discontiguous Memory):内存的物理地址空间存在空洞,这种模型可以高效地处理空洞。
    如果cpu在访问物理内存的时候,其地址空间有一些空洞,是不连续的,那么这种计算机系统的内存模型就是Discontiguous memory。一般而言,NUMA架构的计算机系统的memory model都是选择Discontiguous Memory,不过,这两个概念其实是不同的。NUMA强调的是memory和processor的位置关系,和内存模型其实是没有关系的,只不过,由于同一node上的memory和processor有更紧密的耦合关系(访问更快),因此需要多个node来管理。Discontiguous memory本质上是flat memory内存模型的扩展,整个物理内存的address space大部分是成片的大块内存,中间会有一些空洞,每一个成片的memory address space属于一个node(如果局限在一个node内部,其内存模型是flat memory)。因此,这种内存模型下,节点数据(struct pglist_data)有多个,宏定义NODE_DATA可以得到指定节点的struct pglist_data。而,每个节点管理的物理内存保存在struct pglist_data 数据结构的node_mem_map成员中(概念类似flat memory中的mem_map)。这时候,从PFN转换到具体的struct page会稍微复杂一点,我们首先要从PFN得到node ID,然后根据这个ID找到对于的pglist_data 数据结构,也就找到了对应的page数组,之后的方法就类似flat memory了。

    #define __pfn_to_page(pfn)            \ 
    ({    unsigned long __pfn = (pfn);        \ 
        unsigned long __nid = arch_pfn_to_nid(__pfn);  \ 
        NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\ 
    })
    

    3)稀疏内存(Space Memory):内存的物理地址空间存在空洞,如果要支持内存热插拔,只能选择稀疏内存模型。
    Memory model也是一个演进过程,刚开始的时候,使用flat memory去抽象一个连续的内存地址空间(mem_maps[]),出现NUMA之后,整个不连续的内存空间被分成若干个node,每个node上是连续的内存地址空间,也就是说,原来的单一的一个mem_maps[]变成了若干个mem_maps[]了。一切看起来已经完美了,但是memory hotplug的出现让原来完美的设计变得不完美了,因为即便是一个node中的mem_maps[]也有可能是不连续了。其实,在出现了sparse memory之后,Discontiguous memory内存模型已经不是那么重要了,按理说sparse memory最终可以替代Discontiguous memory的,这个替代过程正在进行中,4.4的内核仍然是有3中内存模型可以选择。
    为什么说sparse memory最终可以替代Discontiguous memory呢?实际上在sparse memory内存模型下,连续的地址空间按照SECTION(例如1G)被分成了一段一段的,其中每一section都是hotplug的,因此sparse memory下,内存地址空间可以被切分的更细,支持更离散的Discontiguous memory。此外,在sparse memory没有出现之前,NUMA和Discontiguous memory总是剪不断,理还乱的关系:NUMA并没有规定其内存的连续性,而Discontiguous memory系统也并非一定是NUMA系统,但是这两种配置都是multi node的。有了sparse memory之后,我们终于可以把内存的连续性和NUMA的概念剥离开来:一个NUMA系统可以是flat memory,也可以是sparse memory,而一个sparse memory系统可以是NUMA,也可以是UMA的。对于经典的sparse memory模型,一个section的struct page数组所占用的内存来自directly mapped区域,页表在初始化的时候就建立好了,分配了page frame也就是分配了虚拟地址。但是,对于SPARSEMEM_VMEMMAP而言,虚拟地址一开始就分配好了,是vmemmap开始的一段连续的虚拟地址空间,每一个page都有一个对应的struct page,当然,只有虚拟地址,没有物理地址。因此,当一个section被发现后,可以立刻找到对应的struct page的虚拟地址,当然,还需要分配一个物理的page frame,然后建立页表什么的,因此,对于这种sparse memory,开销会稍微大一些(多了个建立映射的过程)。

    #define __pfn_to_page(pfn)    (vmemmap + (pfn))
    
    #define vmemmap            ((struct page *)VMEMMAP_START - \
                     SECTION_ALIGN_DOWN(memstart_addr >> PAGE_SHIFT))
    

    六、虚拟地址和物理地址的转换

    cpu读写指令和数据都需要用到内存,而我们程序操作的都是虚拟内存,大家觉得,为什么系统设计者要引入虚拟地址呢?
    设想一下,如果一台计算机的内存中只运行一个程序 A,因为程序 A 的地址在链接时就可以确定,例如从内存地址 0x8000 开始,每次运行程序 A 都装入内存 0x8000 地址处开始运行,没有其它程序干扰。现在改变一下,内存中又放一道程序 B,程序 A 和程序 B 各自运行一秒钟,如此循环,直到其中之一结束。这个新场景下就会产生一些问题,当然这里我们只关心内存相关的这几个核心问题。

    1. 谁来保证程序 A 跟程序 B 没有内存地址的冲突?换句话说,就是程序 A、B 各自放在什么内存地址,这个问题是由 A、B 程序协商,还是由操作系统决定。
    2. 怎样保证程序 A 跟程序 B 不会互相读写各自的内存空间?这个问题相对简单,用保护模式就能解决。
    3. 如何解决内存容量问题?程序 A 和程序 B,在不断开发迭代中程序代码占用的空间会越来越大,导致内存装不下。
    4. 还要考虑一个扩展后的复杂情况,如果不只程序 A、B,还可能有程序 C、D、E、F、G……它们分别由不同的公司开发,而每台计算机的内存容量不同。这时候,又对我们的内存方案有怎样的影响呢?

    想完美地解决以上最核心的 4 个问题,一个较好的方案是:让所有的程序都各自享有一个从 0 开始到最大地址的空间,这个地址空间是独立的,是该程序私有的,其它程序既看不到,也不能访问该地址空间,这个地址空间和其它程序无关,和具体的计算机也无关。事实上,计算机科学家们早就这么做了,这个方案就是虚拟地址。
    虚拟地址
    正如其名,这个地址是虚拟的,自然而然地和具体环境进行了解耦,这个环境包括系统软件环境和硬件环境。
    事实上,所有的应用程序开始的部分都是这样的。这正是因为每个应用程序的虚拟地址空间都是相同且独立的。那么这个地址是由谁产生的呢?答案是链接器,其实我们开发软件经过编译步骤后,就需要链接成可执行文件才可以运行,而链接器的主要工作就是把多个代码模块组装在一起,并解决模块之间的引用,即处理程序代码间的地址引用,形成程序运行的静态内存空间视图。只不过这个地址是虚拟而统一的,而根据操作系统的不同,这个虚拟地址空间的定义也许不同,应用软件开发人员无需关心,由开发工具链给自动处理了。由于这虚拟地址是独立且统一的,所以各个公司开发的各个应用完全不用担心自己的内存空间被占用和改写。
    物理地址
    虽然虚拟地址解决了很多问题,但是虚拟地址只是逻辑上存在的地址,无法作用于硬件电路的,程序装进内存中想要执行,就需要和内存打交道,从内存中取得指令和数据。而内存只认一种地址,那就是物理地址。
    什么是物理地址呢?物理地址在逻辑上也是一个数据,只不过这个数据会被地址译码器等电子器件变成电子信号,放在地址总线上,地址总线电子信号的各种组合就可以选择到内存的储存单元了。
    但是地址总线上的信号(即物理地址),也可以选择到别的设备中的储存单元,如显卡中的显存、I/O 设备中的寄存器、网卡上的网络帧缓存器。不过如果不做特别说明,我们说的物理地址就是指选择内存单元的地址。
    虚拟地址到物理地址的转换
    明白了虚拟地址和物理地址之后,我们发现虚拟地址必须转换成物理地址,这样程序才能正常执行。要转换就必须要转换机构,它相当于一个函数:p=f(v),输入虚拟地址 v,输出物理地址 p。
    那么要怎么实现这个函数呢?用软件方式实现太低效,用硬件实现没有灵活性,最终就用了软硬件结合的方式实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。
    MMU一个工具,我们通过mmu去读取地址关系转化表,再根据虚拟地址空间地址找到物理地址所在区域,可以看图:
    在这里插入图片描述
    下面我们不妨想一想地址关系转换表的实现. 如果在地址关系转换表中,这样来存放:一个虚拟地址对应一个物理地址。那么问题来了,32 位地址空间下,4GB 虚拟地址的地址关系转换表就会把整个 32 位物理地址空间用完,这显然不行。
    系统设计者最后采用一个这样的方案,即把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB,这样就进入了现代内存管理模式——分页模型。于是mmu的功能就是这样的了:
    在这里插入图片描述
    结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。
    MMU 页表
    现在我们开始研究地址关系转换表,其实它有个更加专业的名字——页表。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。为了增加灵活性和节约物理内存空间(因为页表是放在物理内存中的),所以页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表。.
    在这里插入图片描述
    上图中 CR3 就是 CPU 的一个的寄存器,MMU 就是根据这个寄存器找到页目录的。所以,每个进程都有一个页表基地址,我们每次切换进程都会把当前cpu寄存器的值入栈,这叫环境保护,等cpu再次切换回来的时候出栈,恢复cpu寄存器大值,这叫环境恢复。

    七、内存映射原理分析

    内存映射即在进程的虚拟地址空间中创建一个映射,分为两种:
    (1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,
    数据源是存储设备上的文件。
    (2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,
    没有数据源。

    创建内存映射时,在进程的用户虚拟地址空间中分配一个虚拟内存区域。内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页。如果是匿名映射,就分配物理页,然后在页表中把虚拟页映射到物理页。
    我们的内存空间是分成一段段的,这叫分段机制。分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。段选择子就保存在咱们前面讲过的段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。虚拟地址中的段内偏移量应该位于 0 和段界限之间。如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
    其实 Linux 倾向于另外一种从虚拟地址到物理地址的转换方式,称为分页(Paging)。对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫做换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。这个换入和换出都是以页为单位的。页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。

    在这里插入图片描述
    虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
    32 位环境下,虚拟地址空间共 4GB。如果分成 4KB 一个页,那就是 1M 个页。每个页表项需要 4 个字节来存储,那么整个 4GB 空间的映射就需要 4MB 的内存来存储映射表。如果每个进程都有自己的映射表,100 个进程就需要 400MB 的内存。对于内核来讲,有点大了 。
    页表中所有页表项必须提前建好,并且要求是连续的。如果不连续,就没有办法通过虚拟地址里面的页号找到对应的页表项了。
    那怎么办呢?我们可以试着将页表再分页,4G 的空间需要 4M 的页表来存储映射。我们把这 4M 分成 1K(1024)个 4K,每个 4K 又能放在一页里面,这样 1K 个 4K 就是 1K 个页,这 1K 个页也需要一个表进行管理,我们称为页目录表,这个页目录表里面有 1K 项,每项 4 个字节,页目录表大小也是 4K。
    页目录有 1K 项,用 10 位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项,也即 4K 的页表项。每个页表项也是 4 个字节,因而一整页的页表项是 1K 个。再用 10 位就可以表示访问页表项的哪一项,页表项中的一项对应的就是一个页,是存放数据的页,这个页的大小是 4K,用 12 位可以定位这个页内的任何一个位置。
    这样加起来正好 32 位,也就是用前 10 位定位到页目录表中的一项。将这一项对应的页表取出来共 1k 项,再用中间 10 位定位到页表中的一项,将这一项对应的存放数据的页取出来,再用最后 12 位定位到页中的具体位置访问数据。如下图所示:
    在这里插入图片描述
    你可能会问,如果这样的话,映射 4GB 地址空间就需要 4MB+4KB 的内存,这样不是更大了吗? 当然如果页是满的,当时是更大了,但是,我们往往不会为一个进程分配那么多内存。
    比如说,上面图中,我们假设只给这个进程分配了一个数据页。如果只使用页表,也需要完整的 1M 个页表项共 4M 的内存,但是如果使用了页目录,页目录需要 1K 个全部分配,占用内存 4K,但是里面只有一项使用了。到了页表项,只需要分配能够管理那个数据页的页表项页就可以了,也就是说,最多 4K,这样内存就节省多了。
    当然对于 64 位的系统,两级肯定不够了,就变成了四级目录,分别是全局页目录项 PGD(Page Global Directory)、上层页目录项 PUD(Page Upper Directory)、中间页目录项 PMD(Page Middle Directory)和页表项 PTE(Page Table Entry)。
    在这里插入图片描述

    展开全文
    sinat_22338935 2021-05-26 12:10:29
  • 关于Python内存管理,下列说法错误的是答:变量无须先创建和赋值而直接使用虽然在脑海里想了很事情,但是不能改变观点或是不能影响行为的做法,叫作大脑()答:空转有关死锁的叙述正确的是???????????? 。答:死锁...

    关于Python内存管理,下列说法中错误的是

    答:变量无须先创建和赋值而直接使用

    虽然在脑海里想了很多事情,但是不能改变观点或是不能影响行为的做法,叫作大脑()

    答:空转

    有关死锁的叙述正确的是???????????? 。

    答:死锁的产生既与资源分配的策略有关,也与进程并发执行的速度有关 对资源采用按序分配策略就不会出现循环等待资源的情况了

    依据契约,在销售季末,零售商可以以一定的价格把未售出的产品全部退回给供应商,该类型供应契约属于( )

    答:回购契约

    社会主义法律的基本属性()

    答:平等

    为帮助学生理解文章,教师在学生阅读课文之前先呈现了一段引导材料,这段材料能帮助学生从总体上把握全文,我们通常把这段材料称作“先行组织者”,以下对“先行组织者”理解不正确的是( )。

    答:“先行组织者”比较适合阅读和词汇的教学

    中国大学MOOC: 天疱疮、类天疱疮、交界性大疱性表皮松懈症都与哪一种细胞连接紧密相关( )

    答:桥粒连接

    1879 年在莱比锡大学创立了世界上第一个心理实验室,标志着心理学的成立

    答:冯特

    患者男,72岁。临床诊断为慢性肺源性心脏病。因心前区不适,做心电图检查。 其心电图表现下列哪一项是 错误 的

    答:V1导联R/S≥1

    交易产品的类型主要包括哪几种:

    答:云服务 解决方案 API 数据包

    庆祝喜庆的事宜和不吉庆的事宜,在外包装的包装方法上也是不同的。

    答:对

    清华美院为西藏地区设计的生态厕所案例属于哪一种设计扶贫模式?

    答:美丽乡村风貌

    历史经验表明,经济危机往往孕育着新的科技革命。1857年世界经济危机引发了电气革命,推动人类社会从蒸汽时代进入电气时代。1929年的世界经济危机引发了电子革命,推动人类社会从电气时代进入电子时代。由此证明: 社会实践的需要是科技发展的强大动力|科学技术是社会形态更替的根本标志|科技创新能够推动社会经济跨越式发展|科技革命是摆脱社会危机的根本出路

    答:社会实践的需要是科技发展的强大动力 科技创新能够推动社会经济跨越式发展

    当劳动L的总产量下降时

    答:MP为负

    光电直读光谱仪中,若光源为ICP,测定时的试样是 ( )

    答:溶液

    中华文明能够绵延几千年的奥秘是什么?

    答:多元文化并存 有丰富多彩的民族文化 兼容并蓄,博采众长

    计算机网络中的服务器是指__________。

    答:为网络提供资源,并对这些资源进行管理的计算机

    与直接筹资相比,间接筹资具有灵活便利、规模经济、提高资金使用效益的优点

    答:×

    公共关系客体准确的表述是指( ?)

    答:公众

    绷带的长宽度正确的是()

    答:宽5厘米 长10厘米 宽8厘米 长10厘米

    中国大学MOOC: 活称人结是被攀岩者青睐的一种结实的称人结。

    答:对

    关于Python内存管理,下列说法中错误的是

    展开全文
    weixin_39942400 2021-04-27 02:49:49
  • weixin_46291251 2021-04-16 21:34:06
  • qq_44833321 2021-02-17 15:54:23
  • weixin_53472920 2021-08-16 22:50:44
  • ych9527 2021-03-10 18:24:10
  • little_rookie__ 2021-01-08 22:06:17
  • weixin_42109925 2021-05-19 17:26:33
  • qq_40989769 2021-01-23 15:24:18
  • weixin_36386044 2020-12-31 09:16:38
  • aaalswaaa1 2021-04-03 22:07:29
  • weixin_42473904 2021-07-17 00:23:39
  • singxsy 2020-12-22 15:12:03
  • qq_45740349 2021-07-18 15:32:44
  • weixin_33917217 2021-06-23 06:39:17
  • qq_40989769 2021-01-12 17:38:51
  • weixin_27012987 2021-06-28 06:03:46
  • weixin_42619061 2021-03-14 18:16:23
  • whbing1471 2020-12-26 23:37:29
  • qq_41899026 2021-05-27 15:53:28
  • REPCHAOCHAO 2021-09-05 22:53:00
  • lady_killer9 2021-07-10 11:23:07
  • weixin_34452086 2021-07-29 00:17:19
  • wo541075754 2021-04-22 07:48:12
  • weixin_55626853 2021-11-09 20:49:18
  • qq_44096670 2021-08-28 22:09:36
  • theflybrid 2021-02-01 22:20:41
  • qq_45892987 2021-12-13 20:13:32

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 618,449
精华内容 247,379
关键字:

多道技术中的内存管理