今天给各位分享虚拟内存是什么的知识,其中也会对虚拟内存是什么进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!

本文导读目录:

1、(深入浅出)详解虚拟内存

2、虚拟内存是什么

3、计算机体系结构 -- 虚拟内存

  我们都知道一个进程是与其他进程共享CPU和内存资源的。正因如此,操作系统需要有一套完善的内存管理机制才能防止进程之间内存泄漏的问题。   为了更加有效地管理内存并减少出错,现代操作系统提供了一种对主存的抽象概念,即是虚拟内存(Virtual Memory)。虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。   理解不深刻的人会认为虚拟内存只是“使用硬盘空间来扩展内存“的技术,这是不对的。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,使得程序的编写难度降低。并且,把内存扩展到硬盘空间只是使用虚拟内存的必然结果,虚拟内存空间会存在硬盘中,并且会被内存缓存(按需),有的操作系统还会在内存不够的情况下,将某一进程的内存全部放入硬盘空间中,并在切换到该进程时再从硬盘读取(这也是为什么Windows会经常假死的原因...)。   虚拟内存主要提供了如下三个重要的能力:它把主存看作为一个存储在硬盘上的虚拟地址空间的高速缓存,并且只在主存中缓存活动区域(按需缓存)。它为每个进程提供了一个一致的地址空间,从而降低了程序员对内存管理的复杂性。它还保护了每个进程的地址空间不会被其他进程破坏。   介绍了虚拟内存的基本概念之后,接下来的内容将会从虚拟内存在硬件中如何运作逐渐过渡到虚拟内存在操作系统(Linux)中的实现。   好文推荐:   万字讲解你写的代码是如何跑起来的?   什么是Linux内核,如何搞懂Linux内核?(Linux内核学习笔记合集来了!)   字节终面:CPU 是如何读写内存的?   全网最牛Linux内核分析--Intel CPU体系结构   一文让你读懂Linux五大模块内核源码,内核整体架构设计(超详细)   嵌入式前景真的好吗?那有点悬!   一文教你如何使用GDB+Qemu调试Linux内核   Linux内核必读五本书籍(强烈推荐)   全网独一无二Linux内核Makefle系统文件详解(一)(纯文字代码)   带你深度了解Linux内核架构和工作原理!   如何读懂GDB底层实现原理(从这几点入手~)   一文彻底理解Memory barrier(内存屏障)   一篇文带你搞懂,虚拟内存、内存分页、分段、段页式内存管理(超详细)   内存通常被组织为一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址(Physical Address PA),作为到数组的索引。CPU访问内存最简单直接的方法就是使用物理地址,这种寻址方式被称为物理寻址。   现代处理器使用的是一种称为虚拟寻址(Virtual Addressing)的寻址方式。使用虚拟寻址,CPU需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。   虚拟寻址需要硬件与操作系统之间互相合作。CPU中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件,它的功能是将虚拟地址转换为物理地址。MMU需要借助存放在内存中的页表来动态翻译虚拟地址,该页表由操作系统管理。   虚拟内存空间被组织为一个存放在硬盘上的M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的虚拟地址,作为到数组的索引(这点其实与物理内存是一样的)。   操作系统通过将虚拟内存分割为大小固定的块来作为硬盘和内存之间的传输单位,这个块被称为虚拟页(Virtual Page, VP),每个虚拟页的大小为字节。物理内存也会按照这种方法分割为物理页(Physical Page, PP),大小也为字节。   CPU在获得虚拟地址之后,需要通过MMU将虚拟地址翻译为物理地址。而在翻译的过程中还需要借助页表,所谓页表就是一个存放在物理内存中的数据结构,它记录了虚拟页与物理页的映射关系。   页表是一个元素为页表条目(Page Table Entry, PTE)的集合,每个虚拟页在页表中一个固定偏移量的位置上都有一个PTE。下面是PTE仅含有一个有效位标记的页表结构,该有效位代表这个虚拟页是否被缓存在物理内存中。   虚拟页、、、被缓存在物理内存中,虚拟页和被分配在页表中,但并没有缓存在物理内存,虚拟页和还没有被分配。   在进行动态内存分配时,例如函数或者其他高级语言中的关键字,操作系统会在硬盘中创建或申请一段虚拟内存空间,并更新到页表(分配一个PTE,使该PTE指向硬盘上这个新创建的虚拟页)。   由于CPU每次进行地址翻译的时候都需要经过PTE,所以如果想控制内存系统的访问,可以在PTE上添加一些额外的许可位(例如读写权限、内核权限等),这样只要有指令违反了这些许可条件,CPU就会触发一个一般保护故障,将控制权传递给内核中的异常处理程序。一般这种异常被称为“段错误(Segmentation Fault)”。页命中   如上图所示,MMU根据虚拟地址在页表中寻址到了,该PTE的有效位为1,代表该虚拟页已经被缓存在物理内存中了,最终MMU得到了PTE中的物理内存地址(指向)。缺页   如上图所示,MMU根据虚拟地址在页表中寻址到了,该PTE的有效位为0,代表该虚拟页并没有被缓存在物理内存中。虚拟页没有被缓存在物理内存中(缓存未命中)被称为缺页。   当CPU遇见缺页时会触发一个缺页异常,缺页异常将控制权转向操作系统内核,然后调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果牺牲页已被修改过,内核会先将它复制回硬盘(采用写回机制而不是直写也是为了尽量减少对硬盘的访问次数),然后再把该虚拟页覆盖到牺牲页的位置,并且更新PTE。   当缺页异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送给MMU。由于现在已经成功处理了缺页异常,所以最终结果是页命中,并得到物理地址。   这种在硬盘和内存之间传送页的行为称为页面调度(paging):页从硬盘换入内存和从内存换出到硬盘。当缺页异常发生时,才将页面换入到内存的策略称为按需页面调度(demand paging),所有现代操作系统基本都使用的是按需页面调度的策略。   虚拟内存跟CPU高速缓存(或其他使用缓存的技术)一样依赖于局部性原则。虽然处理缺页消耗的性能很多(毕竟还是要从硬盘中读取),而且程序在运行过程中引用的不同虚拟页的总数可能会超出物理内存的大小,但是局部性原则保证了在任意时刻,程序将趋向于在一个较小的活动页面(active page)集合上工作,这个集合被称为工作集(working set)。   根据空间局部性原则(一个被访问过的内存地址以及其周边的内存地址都会有很大几率被再次访问)与时间局部性原则(一个被访问过的内存地址在之后会有很大几率被再次访问),只要将工作集缓存在物理内存中,接下来的地址翻译请求很大几率都在其中,从而减少了额外的硬盘流量。   如果一个程序没有良好的局部性,将会使工作集的大小不断膨胀,直至超过物理内存的大小,这时程序会产生一种叫做抖动(thrashing)的状态,页面会不断地换入换出,如此多次的读写硬盘开销,性能自然会十分“恐怖”。所以,想要编写出性能高效的程序,首先要保证程序的时间局部性与空间局部性。   我们目前为止讨论的只是单页表,但在实际的环境中虚拟空间地址都是很大的(一个32位系统的地址空间有,更别说64位系统了)。在这种情况下,使用一个单页表明显是效率低下的。   常用方法是使用层次结构的页表。假设我们的环境为一个32位的虚拟地址空间,它有如下形式:虚拟地址空间被分为4KB的页,每个PTE都是4字节。内存的前2K个页面分配给了代码和数据。之后的6K个页面还未被分配。再接下来的1023个页面也未分配,其后的1个页面分配给了用户栈。   下图是为该虚拟地址空间构造的二级页表层次结构(真实情况中多为四级或更多),一级页表(1024个PTE正好覆盖4GB的虚拟地址空间,同时每个PTE只有4字节,这样一级页表与二级页表的大小也正好与一个页面的大小一致都为4KB)的每个PTE负责映射虚拟地址空间中一个4MB的片(chunk),每一片都由1024个连续的页面组成。二级页表中的每个PTE负责映射一个4KB的虚拟内存页面。   这个结构看起来很像是一个,这种层次结构有效的减缓了内存要求:如果一个一级页表的一个PTE是空的,那么相应的二级页表也不会存在。这代表一种巨大的潜在节约(对于一个普通的程序来说,虚拟地址空间的大部分都会是未分配的)。只有一级页表才总是需要缓存在内存中的,这样虚拟内存系统就可以在需要时创建、页面调入或调出二级页表(只有经常使用的二级页表才会被缓存在内存中),这就减少了内存的压力。   【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639   从形式上来说,地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。   下图为MMU利用页表进行寻址的过程:   页表基址寄存器(PTBR)指向当前页表。一个n位的虚拟地址包含两个部分,一个p位的虚拟页面偏移量(Virtual Page Offset, VPO)和一个(n - p)位的虚拟页号(Virtual Page Number, VPN)。   MMU根据VPN来选择对应的PTE,例如代表、代表....因为物理页与虚拟页的大小是一致的,所以物理页面偏移量(Physical Page Offset, PPO)与VPO是相同的。那么之后只要将PTE中的物理页号(Physical Page Number, PPN)与虚拟地址中的VPO串联起来,就能得到相应的物理地址。   多级页表的地址翻译也是如此,只不过因为有多个层次,所以VPN需要分成多段。假设有一个k级页表,虚拟地址会被分割成k个VPN和1个VPO,每个都是一个到第i级页表的索引。为了构造物理地址,MMU需要访问k个PTE才能拿到对应的PPN。   页表是被缓存在内存中的,尽管内存的速度相对于硬盘来说已经非常快了,但与CPU还是有所差距。为了防止每次地址翻译操作都需要去访问内存,CPU使用了高速缓存与TLB来缓存PTE。   在最糟糕的情况下(不包括缺页),MMU需要访问内存取得相应的PTE,这个代价大约为几十到几百个周期,如果PTE凑巧缓存在L1高速缓存中(如果L1没有还会从L2中查找,不过我们忽略多级缓冲区的细节),那么性能开销就会下降到1个或2个周期。然而,许多系统甚至需要消除即使这样微小的开销,TLB由此而生。   TLB(Translation Lookaside Buffer, TLB)被称为翻译后备缓冲器或翻译旁路缓冲器,它是MMU中的一个缓冲区,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引与标记字段是从VPN中提取出来的,如果TLB中有个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。   下图为地址翻译的流程(TLB命中的情况下):   第一步,CPU将一个虚拟地址交给MMU进行地址翻译。第二步和第三步,MMU通过TLB取得相应的PTE。第四步,MMU通过PTE翻译出物理地址并将它发送给高速缓存/内存。第五步,高速缓存返回数据到CPU(如果缓存命中的话,否则还需要访问内存)。   当TLB未命中时,MMU必须从高速缓存/内存中取出相应的PTE,并将新取得的PTE存放到TLB(如果TLB已满会覆盖一个已经存在的PTE)。   Linux为每个进程维护了一个单独的虚拟地址空间。虚拟地址空间分为内核空间与用户空间,用户空间包括代码、数据、堆、共享库以及栈,内核空间包括内核中的代码和数据结构,内核空间的某些区域被映射到所有进程共享的物理页面。   Linux也将一组连续的虚拟页面(大小等于内存总量)映射到相应的一组连续的物理页面,这种做法为内核提供了一种便利的方法来访问物理内存中任何特定的位置。   Linux将虚拟内存组织成一些区域(也称为段)的集合,区域的概念允许虚拟地址空间有间隙。一个区域就是已经存在着的已分配的虚拟内存的连续片(chunk)。   例如,代码段、数据段、堆、共享库段,以及用户栈都属于不同的区域,每个存在的虚拟页都保存在某个区域中,而不属于任何区域的虚拟页是不存在的,也不能被进程所引用。   内核为系统中的每个进程维护一个单独的任务结构(task_struct)。任务结构中的元素包含或者指向内核运行该进程所需的所有信息(PID、指向用户栈的指针、可执行目标文件的名字、程序计数器等)。   mm_struct:描述了虚拟内存的当前状态。pgd指向一级页表的基址(当内核运行这个进程时,pgd会被存放在CR3控制寄存器,也就是页表基址寄存器中),mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。vm_starts:指向这个区域的起始处。vm_end:指向这个区域的结束处。vm_prot:描述这个区域内包含的所有页的读写许可权限。vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的以及一些其他信息。vm_next:指向链表的下一个区域结构。   Linux通过将一个虚拟内存区域与一个硬盘上的文件关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。   这种将虚拟内存系统集成到文件系统的方法可以简单而高效地把程序和数据加载到内存中。   一个区域可以映射到一个普通硬盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页的初始内容。由于按需页面调度的策略,这些虚拟页面没有实际交换进入物理内存,直到CPU引用的虚拟地址在该区域的范围内。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。   一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。当CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就先将它写回到硬盘,之后用二进制零覆盖牺牲页并更新页表,将这个页面标记为已缓存在内存中的。   简单来说:普通文件映射就是将一个文件与一块内存建立起映射关系,对该文件进行IO操作可以绕过内核直接在用户态完成(用户态在该虚拟地址区域读写就相当于读写这个文件)。匿名文件映射一般在用户空间需要分配一段内存来存放数据时,由内核创建匿名文件并与内存进行映射,之后用户态就可以通过操作这段虚拟地址来操作内存了。匿名文件映射最熟悉的应用场景就是动态内存分配(malloc()函数)。   Linux很多地方都采用了“懒加载”机制,自然也包括内存映射。不管是普通文件映射还是匿名映射,Linux只会先划分虚拟内存地址。只有当CPU第一次访问该区域内的虚拟地址时,才会真正的与物理内存建立映射关系。   只要虚拟页被初始化了,它就在一个由内核维护的交换文件(swap file)之间换来换去。交换文件又称为交换空间(swap space)或交换区域(swap area)。swap区域不止用于页交换,在物理内存不够的情况下,还会将部分内存数据交换到swap区域(使用硬盘来扩展内存)。   虚拟内存系统为每个进程提供了私有的虚拟地址空间,这样可以保证进程之间不会发生错误的读写。但多个进程之间也含有相同的部分,例如每个C程序都使用到了C标准库,如果每个进程都在物理内存中保持这些代码的副本,那会造成很大的内存资源浪费。   内存映射提供了共享对象的机制,来避免内存资源的浪费。一个对象被映射到虚拟内存的一个区域,要么是作为共享对象,要么是作为私有对象的。   如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。相对的,对一个映射到私有对象的区域的任何写操作,对于其他进程来说是不可见的。一个映射到共享对象的虚拟内存区域叫做共享区域,类似的,也有私有区域。   为了节约内存,私有对象开始的生命周期与共享对象基本上是一致的(在物理内存中只保存私有对象的一份副本),并使用写时复制的技术来应对多个进程的写冲突。   只要没有进程试图写它自己的私有区域,那么多个进程就可以继续共享物理内存中私有对象的一个单独副本。然而,只要有一个进程试图对私有区域的某一页面进行写操作,就会触发一个保护异常。   在上图中,进程B试图对私有区域的一个页面进行写操作,该操作触发了保护异常。异常处理程序会在物理内存中创建这个页面的一个新副本,并更新PTE指向这个新的副本,然后恢复这个页的可写权限。   还有一个典型的例子就是函数,该函数用于创建子进程。当函数被当前进程调用时,内核会为新进程创建各种必要的数据结构,并分配给它一个唯一的PID。   为了给新进程创建虚拟内存,它复制了当前进程的、和页表的原样副本。并将两个进程的每个页面都标为只读,两个进程中的每个区域都标记为私有区域(写时复制)。   这样,父进程和子进程的虚拟内存空间完全一致,只有当这两个进程中的任一个进行写操作时,再使用写时复制来保证每个进程的虚拟地址空间私有的抽象概念。   虽然可以使用内存映射(函数)来创建和删除虚拟内存区域来满足运行时动态内存分配的问题。然而,为了更好的移植性与便利性,还需要一个更高层面的抽象,也就是动态内存分配器(dynamic memory allocator)。   动态内存分配器维护着一个进程的虚拟内存区域,也就是我们所熟悉的“堆(heap)”,内核中还维护着一个指向堆顶的指针brk(break)。动态内存分配器将堆视为一个连续的虚拟内存块(chunk)的集合,每个块有两种状态,已分配和空闲。已分配的块显式地保留为供应用程序使用,空闲块则可以用来进行分配,它的空闲状态直到它显式地被应用程序分配为止。已分配的块要么被应用程序显式释放,要么被垃圾回收器所释放。   本文只讲解动态内存分配的一些概念,关于动态内存分配器的实现已经超出了本文的讨论范围。如果有对它感兴趣的同学,可以去参考dlmalloc[1]的源码,它是由Doug Lea(就是写Java并发包的那位)实现的一个设计巧妙的内存分配器,而且源码中的注释十分多。   造成堆的空间利用率很低的主要原因是一种被称为碎片(fragmentation)的现象,当虽然有未使用的内存但这块内存并不能满足分配请求时,就会产生碎片。有以下两种形式的碎片:内部碎片:在一个已分配块比有效载荷大时发生。例如,程序请求一个5字(这里我们不纠结字的大小,假设一个字为4字节,堆的大小为16字并且要保证边界双字对齐)的块,内存分配器为了保证空闲块是双字边界对齐的(具体实现中对齐的规定可能略有不同,但对齐是肯定会有的),只好分配一个6字的块。在本例中,已分配块为6字,有效载荷为5字,内部碎片为已分配块减去有效载荷,为1字。外部碎片:当空闲内存合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大到可以来处理这个请求时发生。外部碎片难以量化且不可预测,所以分配器通常采用启发式策略来试图维持少量的大空闲块,而不是维持大量的小空闲块。分配器也会根据策略与分配请求的匹配来分割空闲块与合并空闲块(必须相邻)。   分配器将堆组织为一个连续的已分配块和空闲块的序列,该序列被称为空闲链表。空闲链表分为隐式空闲链表与显式空闲链表。隐式空闲链表,是一个单向链表,并且每个空闲块仅仅是通过头部中的大小字段隐含地连接着的。显式空闲链表,即是将空闲块组织为某种形式的显式数据结构(为了更加高效地合并与分割空闲块)。例如,将堆组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱节点的指针与后继节点的指针。   查找一个空闲块一般有如下几种策略:首次适配:从头开始搜索空闲链表,选择第一个遇见的合适的空闲块。它的优点在于趋向于将大的空闲块保留在链表的后面,缺点是它趋向于在靠近链表前部处留下碎片。下一次适配:每次从上一次查询结束的地方开始进行搜索,直到遇见合适的空闲块。这种策略通常比首次适配效率高,但是内存利用率则要低得多了。最佳适配:检查每个空闲块,选择适合所需请求大小的最小空闲块。最佳适配的内存利用率是三种策略中最高的,但它需要对堆进行彻底的搜索。   对一个链表进行查找操作的效率是线性的,为了减少分配请求对空闲块匹配的时间,分配器通常采用分离存储(segregated storage)的策略,即是维护多个空闲链表,其中每个链表的块有大致相等的大小。   一种简单的分离存储策略:分配器维护一个空闲链表数组,然后将所有可能的块分成一些等价类(也叫做大小类(size class)),每个大小类代表一个空闲链表,并且每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小(例如,某个大小类的范围定义为(17~32),那么这个空闲链表全由大小为32的块组成)。   当有一个分配请求时,我们检查相应的空闲链表。如果链表非空,那么就分配其中第一块的全部。如果链表为空,分配器就向操作系统请求一个固定大小的额外内存片,将这个片分成大小相等的块,然后将这些块链接起来形成新的空闲链表。   要释放一个块,分配器只需要简单地将这个块插入到相应的空闲链表的头部。   在编写C程序时,一般只能显式地分配与释放堆中的内存(与),程序员不仅需要分配内存,还需要负责内存的释放。   许多现代编程语言都内置了自动内存管理机制(通过引入自动内存管理库也可以让C/C++实现自动内存管理),所谓自动内存管理,就是自动判断不再需要的堆内存(被称为垃圾内存),然后自动释放这些垃圾内存。   自动内存管理的实现是垃圾收集器(garbage collector),它是一种动态内存分配器,它会自动释放应用程序不再需要的已分配块。   垃圾收集器一般采用以下两种(之一)的策略来判断一块堆内存是否为垃圾内存:引用计数器:在数据的物理空间中添加一个计数器,当有其他数据与其相关时(引用),该计数器加一,反之则减一。通过定期检查计数器的值,只要为0则认为是垃圾内存,可以释放它所占用的已分配块。使用引用计数器,实现简单直接,但缺点也很明显,它无法回收循环引用的两个对象(假设有对象A与对象B,它们2个互相引用,但实际上对象A与对象B都已经是没用的对象了)。可达性分析:垃圾收集器将堆内存视为一张有向图,然后选出一组根节点(例如,在Java中一般为类加载器、全局变量、运行时常量池中的引用类型变量等),根节点必须是足够“活跃“的对象。然后计算从根节点集合出发的可达路径,只要从根节点出发不可达的节点,都视为垃圾内存。   垃圾收集器进行回收的算法有如下几种:标记-清除:该算法分为标记(mark)和清除(sweep)两个阶段。首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。标记-清除算法实现简单,但它的效率不高,而且会产生许多内存碎片。标记-整理:标记-整理与标记-清除算法基本一致,只不过后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。复制:将程序所拥有的内存空间划分为大小相等的两块,每次都只使用其中的一块。当这一块的内存用完了,就把还存活着的对象复制到另一块内存上,然后将已使用过的内存空间进行清理。这种方法不必考虑内存碎片问题,但内存利用率很低。这个比例不是绝对的,像HotSpot虚拟机为了避免浪费,将内存划分为Eden空间与两个Survivor空间,每次都只使用Eden和其中一个Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一个Survivor空间上,然后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor的大小比例为8:1,只有10%的内存空间会被闲置浪费。分代:分代算法根据对象的存活周期的不同将内存划分为多块,这样就可以对不同的年代采用不同的回收算法。一般分为新生代与老年代,新生代存放的是存活率较低的对象,可以采用复制算法;老年代存放的是存活率较高的对象,如果使用复制算法,那么内存空间会不够用,所以必须使用标记-清除或标记-整理算法。   虚拟内存是对内存的一个抽象。支持虚拟内存的CPU需要通过虚拟寻址的方式来引用内存中的数据。   CPU加载一个虚拟地址,然后发送给MMU进行地址翻译。地址翻译需要硬件与操作系统之间紧密合作,MMU借助页表来获得物理地址。首先,MMU先将虚拟地址发送给TLB以获得PTE(根据VPN寻址)。如果恰好TLB中缓存了该PTE,那么就返回给MMU,否则MMU需要从高速缓存/内存中获得PTE,然后更新缓存到TLB。MMU获得了PTE,就可以从PTE中获得对应的PPN,然后结合VPO构造出物理地址。如果在PTE中发现该虚拟页没有缓存在内存,那么会触发一个缺页异常。缺页异常处理程序会把虚拟页缓存进物理内存,并更新PTE。异常处理程序返回后,CPU会重新加载这个虚拟地址,并进行翻译。   虚拟内存系统简化了内存管理、链接、加载、代码和数据的共享以及访问权限的保护:简化链接,独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。简化加载,虚拟内存使向内存中加载可执行文件和共享对象文件变得更加容易。简化共享,独立的地址空间为操作系统提供了一个管理用户进程和内核之间共享的一致机制。访问权限保护,每个虚拟地址都要经过查询PTE的过程,在PTE中设定访问权限的标记位从而简化内存的权限保护。   操作系统通过将虚拟内存与文件系统结合的方式,来初始化虚拟内存区域,这个过程称为内存映射。应用程序显式分配内存的区域叫做堆,通过动态内存分配器来直接操作堆内存。   首页 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛   转载地址:(深入浅出)详解虚拟内存 - 圈点 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛  什么是虚拟内存?在使用电脑过程真,我们会接触到虚拟内存这个概念。比如电脑内存不足的时候,就可以设置虚拟内存。那么,什么是虚拟内存呢?虚拟内存简单来说,它就是是用来暂时存放东西,当东西没用的时候即可以清楚,等下次要再用时重新暂时存放,说白了就相当于是那个缓存!   当实际RAM满时(实际上,在RAM满之前),虚拟内存就在硬盘上创建了。当物理内存用完后,虚拟内存管理器选择最近没有用过的,低优先级的内存部分写到交换文件上。这个过程对应用是隐藏的,应用把虚拟内存和实际内存看作是一样的。   如果计算机缺少运行程序或操作所需的随机存取内存 (RAM),则 Windows 使用虚拟内存(Virtual Memory)进行补偿。   虚拟内存将计算机的 RAM 和硬盘上的临时空间组合在一起。当 RAM 运行速度缓慢时,虚拟内存将数据从 RAM 移动到称为“分页文件”的空间中。将数据移入与移出分页文件可以释放 RAM,以便完成工作。   一般而言,计算机的 RAM 越多,程序运行得越快。如果计算机的速度由于缺少 RAM 而降低,则可以尝试增加虚拟内存来进行补偿。但是,计算机从 RAM 读取数据的速度要比从硬盘读取数据的速度快得多,因此增加 RAM 是更好的方法。   虚拟内存是把一部分硬盘空间作为内存功能来使用以缓解物理内存空间有限的局面。当内存条中有闲置状态的进程时,系统就会把闲置状态的进程放入硬盘的虚拟内存空间内,以便腾出物理内存空间给正在活动的进程使用。   虚拟内存使用技巧   不要将虚拟内存设置在系统分区中;   将虚拟内存设置在另一块高速磁盘中;   将虚拟内存设置在使用NTFS文件格式的分区中;   将最大值和初始大小设置为一个相同的值;   将初始大小设置为物理内存的1.5倍;   不要在同一块硬盘中设置“分页文件”。   如何设置虚拟内存   虚拟内存怎么设置最好   合理设置虚拟内存   虚拟内存的设定主要根据你的物理内存大小和电脑的用途来设定,在桌面上用鼠标右击“我的电脑”,选择“属性”,就可以看到内存了。根据微软公司的建议,虚拟内存设为物理内存容量的1.5--3倍,例如512MB的内存,虚拟内存设定为768--1536MB;1G的内存,虚拟内存设定为1536--3072MB。也可让Windows来自动分配管理虚拟内存,它能根据实际内存的使用情况,动态调整虚拟内存的大校在虚拟内存设置页面下方有一个推荐数值,如果确实不知道设置多少为最佳,建议虚拟内存就设为推荐的数值。虚拟内存有初始大小与最大值两个数值,最好把初始大小和最大值设为相同,以避免系统频繁改变页面文件的大小,影响电脑运行。内存容量2GB或以上的,如果不运行大型文件或游戏,也可以关闭虚拟内存。   虚拟内存就是在你的物理内存不够用时把一部分硬盘空间作为内存来使用,不过由于硬盘传输的速度要比内存传输速度慢的多,所以使用虚拟内存比物理内存效率要慢。个人实际需要的值应该自己多次调整为好。设的太大会产生大量的碎片,严重影响系统速度,设的太小就不够用,于是系统就会提示你虚拟内存太校   我的电脑--属性--高级--性能设置--高级--更改--选中C区,在初始大小和最大值处输入0点设置取消C盘的虚拟内存,然后点D(或者其他盘,前提要空间是最大)盘,在初始大小输入实际内存的1.5-2倍大小,在最大值处输入实际内存的3倍,然后点设置-确定完成设置,这样设置最合理。   以上就是虚拟内存的相关教程,想要了解更多电脑教程,请关注。  在计算机发展的早期,CPU是直接操作物理内存(Physical Memory)的。直接操作物理内存有以下一些问题:不同的程序的地址空间很难完全互相隔离。如果一个程序修改了另一个程序的内存空间,可能会导致程序崩溃,安全性低。可用内存大小受到实际物理内存大小的限制,如果一个程序使用的内存超过了物理内存大小就会崩溃。   为了解决这些问题,人们引入了虚拟内存(Virtual Memory)。如下图所示,每一个进程都有独立的虚拟地址空间,不同进程的虚拟地址空间互相不干扰,提高了安全性。在每个进程看来,就像它自己独享了整个内存似的。当物理内存不够是,可以将一部分不常使用的内存块换出(Swap-out)到磁盘中,下次使用时再换入到内存中(Swap-in),这样能允许程序使用超过实际物理内存大小的空间。应用虚拟内存之后,程序中看到的地址都是虚拟地址。在访存时,虚拟地址首先会被转换成物理地址,然后再访问实际物理内存。虚拟内存   虚拟地址到物理地址的映射由操作系统和硬件完成。在本文中,我们以linux系统和x86架构为例,对虚拟内存和物理内存的映射机制进行简单介绍。其中部分内容参考了下面这篇知乎回答:如何理解虚拟地址空间?   虚拟内存到物理内存的映射,主要有分段和分页两种方式。进程的地址空间由多个逻辑分段组成,包括代码段,数据段,堆段和栈段等,每个逻辑段在虚拟地址空间中都看作一个连续的内存块,   分段映射机制就是以这些逻辑分块为单位,将进程的虚拟地址空间中的各个段,分别映射到连续的物理地址空间。分段机制下,虚拟地址被两部分组成:段选择子和段内偏移。操作系统会维护一个段表,每一个段表项由段基地址、段边界和特权级组成。操作系统根据段选择子从段表中检索出对应的段表项,然后将段内偏移和段基地址相加,得到物理地址。具体描述可以参考下面这篇文章::https://zhuanlan.zhihu.com/p/479968208   因为进程各个段的size通常较大,分段的映射机制的映射粒度太粗,引起了很多问题,比如内存碎片化、交换开销大等。在现代处理器和操作系统中,已经基本不采用纯粹的分段映射了,转而采用更加细粒度的分页(pageing)映射机制。   在分页机制下,虚拟内存和物理内存都被切分成固定大小的子块,称为页(Page)。虚拟页和物理页之间通过页表(Page Table)建立映射关系,连续的虚拟页可以被映射到任意不连续的物理页。在页内部,连续的虚拟地对应着连续的物理地址。内存的申请、释放、换入、换出都是以页为基本单位,有助于解决碎片化,提高交换效率。分页内存映射机制   分页机制下,虚拟地址被分成两部分:页号(Page Number)和页内偏移(Offset)。每一个页表项(Page Table Entry, PTE)包含物理页基地址和各种状态标志位。在地址转换时,首先根据页号找到对应的页表项,得到其中的物理页基地址,然后将物理页基地址和页内偏移相加得到物理地址。   在linux系统中,不考虑large page的情况,一个页大小通常为4KB。在页内部要索引一个字节,需要12位(2^12=4K)地址,所以页内偏移通常为12位。在32位系统中,虚拟地址中剩下的20位是页号,可以索引1M(2^20=1M)个页表项。我们可以结合下图的示例理解一下:虚拟地址为0x00003500,其中高20位被作为页号,低12位被作为页内偏移。根据页号,我们可以索引到页表中的第0x00003个页表项。该页表项中存储的物理页基地址为0x70000,该基地址对应到物理内存中的Page 4。在Page 4中,根据页内偏移500可以定位到具体物理地址。页表检索过程示意图   32位系统中,每个进程需要1M个页表项才能表达整个虚拟地址空间,每个页表项32位,则每个页表占据4MB空间。如果我们开了100个进程,则需要400MB的内存,这显然是一个很大的内存开销。为了解决这个问题,人们引入了多级页表(Multi-level Page Table)以优化空间占用。   多级页表的设计基于局部性原理。进程的虚拟地址空间很大,但根据根据局部性原理,通常只有一小部分虚拟地址空间会被使用到,我们只需要为实际使用到的虚拟页分配页表项即可。我们以两级页表为例,介绍多级页表的实现原理。   在两级页表中,虚拟地址的页号部分被分为页目录号(Page Directory Number)和二级页号,页表也被分成了页目录(Page Directory)和二级页表。每一个页目录项(Page Directory Entry, PDE)包含页表物理基地址和各种状态标志位。其中的页表物理基地址指向了对应的二级页表首地址。在地址转换时,首先根据页目录号找到对应的页目录项,得到其中的页表物理基地址,然后根据页表物理基地址找打二级页表,然后使用二级页表号找到对应的页表项,然后将页表项中的物理页基地址和页内偏移相加得到物理地址。   结合下图,更加形象地理解一下:虚拟地址为0x00003500,其中高10位被作为页目录号,中间10位被作为二级页号,低12位被作为页内偏移。根据页目录号,我们可以索引到页目录中的第0x00个PDE。该PDE中存储的二级页表物理机地址时0x2ffaa,然后根据此地址,我们找到对应的二级页表。然后,根据页号,我们可以索引到二级页表中的第003个页表项。该页表项中存储的物理页基地址为0x70000,该基地址对应到物理内存中的Page 4。在Page 4中,根据页内偏移500可以定位到具体物理地址。多(二)级页表检索过程示意图   二级页表能节省多少空间呢?还是以32位系统为例,虚拟地址空间是4GB,我们只需要为4GB空间分配页目录。每个PDE大小32位,则整个页目录需要4*2^10=4KB内存。然后,假设程序实际只使用了100M虚拟内存,那么我们只需要为这100M空间分配二级页表,大概为100MB/4KB*4B=100KB。总共,需要100KB+4KB=104KB内存,相比于单级页表的4MB,减少了很多。   在64 位的系统中,因为虚拟地址空间更大,因此需要更多级数来减少内存。在64位linux中,通常使用四级页表:全局页目录项(Page Global Directory, PGD)、上层页目录项(Page Upper Directory, PUD)、中间页目录项 (Page Middle Directory, PMD)、页表项(Page Table Entry, PTE)[1]。64位linux的4级页表结构   前面提到,PDE和PTE中除了保存二级页表或物理页的物理机地址之外,还有一些状态标志位。在linux系统中,PDE和PTE的状态标志位定义可以参考下面这篇文章,本文不再赘述。https://zhuanlan.zhihu.com/p/67053210   页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程,TLB的作用我们后面在讲。   这里大家可能会有一个疑问:页表是在内存中,MMU怎么知道它在内存中的具体位置呢?其实,CPU中有个页表基寄存器(Page Table Base Register, PTBR),其中保存了页表的物理基地址(对多级页表来说,就是页目录的首地址)。也正因此,页表在物理内存中必须是连续的,否则MMU无法仅通过一个基地址索引整个页表[3]。当发生上下文切换时,操作系统负责把将要允许的进程的页表基地址写进PTBR。在多核心CPU中,每个核心都有独立的寄存器组和单独的PTBR,所以多个进程可以在多个核心上并行运行。   在介绍多级页表时,我们提到只会为实际使用到的虚拟内存创建页表,从而节省空间。所以,当我们首次访问某一个虚拟页时,页表中肯定没有与之对应的页表项。TWU在检索页表时,如果没有找到与当前虚拟地址对应的有效页表项,就会发生缺页异常(Page Fault)[4][5]。   在发生缺页异常时,进程会从用户态切换到内核态,进入操作系统的缺页异常处理流程。操作系统会为当前的虚拟地址创建页表项,然后检查物理页是否已经存在于物理内存中,如果已经存在,则直接填充好页表项,如果不存在,则重新分配一页物理内存,并填充好页表项。整个处理过程中不涉及到硬盘操作,耗时相对较少,因此称为Minor Page Fault。   前面提到,当物理内存不足时,操作系统可能将一部分物理页换出到硬盘中[6]。如果被访问的虚拟地址对应的物理页已经被换出,则也会发生缺页异常。此时,操作系统会先分配一页物理内存,然后将磁盘中对应的内容读入内存中。像这种涉及到硬盘访问的过程,比较耗时,因此称为Major Page Fault。   应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。   为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。关于TLB的结构,可以参考这篇文章,本文就不再赘述:https://zhuanlan.zhihu.com/p/108425561   TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]。   前面提到,发生缺页异常时,操作系统会更新页表,包括创建新的页表项等。页表更新之后,TLB中的数据可能会失效。比如页表中本来存在A-B的映射,后来被删掉,改成了A->C的映射,则TLB中原本的A->B的映射就失效了,为了保证缓存数据的正确性,我们需要flush TLB,这会降低性能。所以,缺页异常对与性能是有严重影响的。   上面介绍了这么多原理性的内容,也顺带介绍了一些有关性能方面的考量,比如TLB优化、Page Fault对性能的影响等。那么学以致用,下面我们通过一个简单的实验,直观的感受虚拟内存对程序性能的影响。   实验用的C++代码如下所示,其中定义了execute0(),execute1(),execute2()和execute3()四个函数。在这四个函数中,我们模拟了典型的“分配内存 -> 使用内存 -> 释放内存”的workload,并将这个workload循环执行1000次。但这四个函数的内存分配和释放模式不相同:execute0在循环外pre-malloc一块大小为32MB的内存,并在循环结束后释放内存execute1在每次循环内交替malloc两块大小为16MB的内存,并在循环内释放内存execute2在每次循环内malloc一块大小为32MB的内存,并在循环内释放内存execute3在每次循环内连续malloc两块大小为16MB的内存,并在循环内释放内存   运行下面cmd,统计运行时间、系统调用次数[10]、page fault数量、cache/TLB miss[11][12]等性能指标:   测试用的硬件平台为i9 desktop,具体配置如下:   统计结果如下表所示:execute0()execute1()execute2()execute3()malloc/free calls1200010001000memory write count32000 MB32000 MB32000 MB32000 MBmmap3738103646munmap2310014brk6763003futex1997199419751998minor page fault8479847381932828160350cache miss546,434,644158,888,547684,188,390694,544,457tlb miss53,99735,0975,428,3836,332,598execution time4088.92 ms4072.8 ms5927.06 ms5931.52 ms   在分析实验数据之前,先简单了解一下glibc中malloc和free函数的实现[13]。具体实现细节蛮复杂的,但大致可以归纳成下面几点:malloc函数是基于mmap和brk系统调用实现的。malloc函数会预先申请一片虚拟内存,将其切分成小块,返回给用户使用。当堆空间不够时,malloc会调用brk拓展堆空间。当申请的内存大于某个阈值时,会调用mmap()申请内存。mmap和brk只是申请虚拟内存,只有当真正访问这些虚拟内存时,引发缺页异常,才会申请物理内存并创建对应的页表项。free函数是基于munmap和brk系统调用实现的。free函数在释放malloc却分出的小块儿内存时,并不会调用munmap或者brk将其归还给操作系统。当释放的内存满足某些条件时,free函数会调用brk()缩减堆空间当释放的内存满足某些条件时,free函数会调用munmap将内存归还给操作系统。munmap和brk都有可能更新页表。因为它们都是将内存归还给操作系统。如果它们释放的虚拟内存有对应的物理内存,那么对应的页表项会被删除[14],然后TLB会被刷新。   结合glibc中malloc和free函数的实现原理以及上述的关于虚拟内存的知识,我们可以大致分析出execute2和execute3存在的性能问题如下:在某些特定的条件下(execute2和execute3的内存分配模式正好hit了这些条件),每次malloc、free都会调用mmap、munmap或者brk来分配/释放虚拟内存,这些系统调用导致用户态和内核态之间的切换,影响性能。malloc中调用mmap、brk做内存分配时,返回的都是虚拟内存地址,没有实际分配物理内存。因此首次访问虚拟地址时,会发生minor page fault。操作系统需要重新分配物理内存,更新页表,影响性能。free中调用munmap、brk做内存释放,会将物理内存归还给操作系统,并且删除对应的页表项,影响性能。频繁的mmap、munmap和brk导致频繁的重新分配和释放物理内存,导致cache miss增加,影响性能。频繁的mmap、munmap和brk导致的频繁的页表更新会flush TLB,导致TLB miss增加,影响性能。   既然不同的内存分配模式之间存在性能差异,那我们当然可以通过优化分配模式来提升程序性能。比如在深度学习推理引擎中,对于静态模型,通常采用类似于execute0的静态内存分配策略,提前计算出所需要的全部内存,一次性分配好并一直复用。对于一些动态模型,没法提前分配好所有内存,那么我们也可以使用性能更好的内存分配器,比如jemalloc[15]、tcmalloc[16]等。在深度学习框架中,也有很多关于内存分配模式的优化[17]。   在计算机发展初期,CPU都是直接操作物理内存的。为了解决安全性等问题,人们引入了虚拟内存。虚拟内存通过分段、分页等方式映射到物理内存。其中,分页映射机制在现代系统和架构中被广泛使用。分页机制下,虚拟内存和物理内存都被划分成一个个的页,然后通过页表维护虚拟页和物理页之间的映射。为了优化页表的存储空间,引入了多级页表。页表的遍历依靠MMU硬件完成,为了优化查表时间,MMU中加入了TLB cache。当MMU查不到需要的页表项时,就会发生缺页异常,操作系统负责处理缺页异常。处理缺页异常时,可能会涉及到物理内存分配,磁盘读取,TLB清空等操作,往往堆性能影响较大。通过实验,我们也能验证这些推论。在实际编程中,我们可以通过优化内存分配模式来降低缺页异常的发生概率,从而提高程序性能。   虚拟内存很有用,但凡是都有两面性。为了使用虚拟内存,硬件和软件都变得更加复杂。对于很多低端单片机或者嵌入式MCU(著名的51单片机,Arm cortex-M系列等)来说,它们的应用场景相对比较简单,资源又比较有限,为了降低成本,它们通常没有使用虚拟内存。   最后感叹一下,架构和内核的水真是太深了~ 本文仅仅是对虚拟内存相关知识的简单梳理。在整理这篇文章时,我参考了很多大佬们的文章,谢谢大佬们的分享。下面简单列举了一些,很多本文没有介绍到知识可以从中找到:   关于linux虚拟内存:兰新宇:虚拟地址转换[一] - 基本流程兰新宇:虚拟地址转换[二] - 具体实现兰新宇:虚拟地址转换[三] - 多级页表兰新宇:虚拟地址转换[四] - large page/hugepage兰新宇:虚拟地址转换[五] - paging structure caches兰新宇:Linux中的页表实现兰新宇:page fault时发生了什么兰新宇:Linux的进程地址空间[二] - VMAHow The Kernel Manages Your MemoryCSDN:浅谈程序的内存布局   关于缺页异常处理和内存交换:Swap ManagementLinux页表中虚拟内存地址如何映射到硬盘数据块地址?兰新宇:page fault时发生了什么Linux进程的内存管理之缺页异常 - 墨天轮兰新宇:Linux中的Anonymous Pages和Swap [一]兰新宇:Linux中的Page Cache [一]   关于mmap()/munmap()/brk()等内存管理相关的linux系统调用:https://www.codeleading.com/article/17125575276/Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程https://people.cs.pitt.edu/~lol16/CS1550_fall18/data/CS1550_Lab3.pdf   关于malloc和free实现,何柄融:malloc 的实现原理 内存池 mmap sbrk 链表月踏:内存管理:malloc主释放过程_int_freeCPP加油站:深入理解 glibc malloc:内存分配器实现原理Bowen Xiao:分析 Glibc 中的malloc/free 实现linux:深入理解glibc malloc:malloc() 与 free() 原理图解
虚拟内存是什么的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于虚拟内存是什么虚拟内存是什么的信息别忘了在本站进行查找喔。

未经允许不得转载! 作者:谁是谁的谁,转载或复制请以超链接形式并注明出处

原文地址:http://www.9qk.net/post/25362.html发布于:2026-05-14