三、内存管理

简介: 三、内存管理

三、内存管理



内存管理功能:
1. 内存空间的分配与回收。由操作系统完成主存储器空间的分配与管理。
2. 地址转换。逻辑地址与物理地址的转换
3. 内存空间的扩充。利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
4. 内存保护。保证各道作业在各自的存储空间内运行,互补干扰。


1. 内存管理



1. 程序的装入和链接


创建进程首先要将程序和数据装入内存。将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:
1. 编译。由编译程序将用户源代码编译成若干目标模块。
2. 链接。由链接程序将编译后形成的一组目标模块及所需的库函数链接在一起,形成一个完整的装入模块。
3. 装入。由装入程序将装入模块装入内存运行。



2. 覆盖与交换


覆盖与交换技术是在多道程序环境下用来扩充内存的两种方法。
覆盖:由于程序运行时并非任何时候都要访问程序及数据的各个部分,因此可把用户空间分成一个固定区和若干个覆盖区。将经常活跃的部分放在固定区,其余部分按调用关系分段。首先将那些即将访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。
交换:把处于等待状态的程序从内存移到外存,把内存空间腾出来,叫换入;把准备好竞争CPU运行的程序从外存移到内存,叫换入。


3. 连续分配管理方式


单一连续分配、固定分区分配、动态分区分配。
1. 单一连续分配
内存分为系统区和用户区,系统区仅供操作系统使用,通常在低地址部分。
该方式无需进行内存保护,因为内存中永远只有一道程序,因此肯定不会干扰其他程序。
优点:简单、无外部碎片,可以采用覆盖技术,不需要额外的技术支持。
缺点:只能用于单用户。单任务的操作系统,有内部碎片,存储器的利用率极低。
2. 固定分区分配
最简单的多道程序存储管理方式,将用户内存划分为若干个大小固定的区域,一个分区装入一道程序。
两种划分方法:大小相等的分区,大小不等的分区
缺点:一是程序可能太大不能装入任何一个分区,只能用覆盖技术解决;而是程序小于分区大小的话,产生内部碎片,造成空间浪费。
优点:无外部碎片,最简单的多道程序设计方法
3. 动态分区分配(可变分区分配)
不预先划分内存,而是在进程装入内存时,根据进程的大小动态地建立分区。分区大小和数目不定。
缺点:会产生外部碎片,可以采用紧凑技术解决。
动态分配算法:
(1)首次适应算法(First Fit)
(2)最佳适应算法(Best Fit)
(3)最坏适应算法(Worst Fit)
(4)邻近适应算法(Next Fit)


4. 非连续分配管理方式


分页存储管理方式、分段存储管理方式
在分页存储管理方式中,又根据运行作业时是否要把作业的所有页面都装入内存才能运行,分为基本分页存储、请求分页存储。


1、基本分页存储管理方式


固定分区会产生内部碎片,动态分区会产生外部碎片,这两种技术对内存的利用率都比较低。就引入了分页的思量:把内存空间划分为大小相等且固定的块,块相对较小,作为主存的基本单位。


分页存储的几个基本概念:
(1)页面和页面大小
(2)地址结构。  页号 + 页内偏移量
(3)页表。记录了页面在内存中对应的物理块号,页表一般存放在内存中。  页号 + 物理内存中的块号
//页表项的第二部分与地址的第二部分共同组成物理地址。





上面的介绍地址变换过程可知,若页表全部放在内存中,则存取一个数据或指令至少要访问两次内存:
第一次:访问页表,确定所存取的数据/指令的物理地址
第二次:根据该地址存取数据/指令
所以引入查找快的高速缓冲存储器:快表(TLB)




2. 虚拟内存


传统内存管理的特征:
1. 一次性。作业必须一次性全部装入内存才能开始运行。
导致两个问题:
(1)作业太大无法装入,该作业无法运行
(2)只能装入一部分作业,导致多道程序度下降
2. 驻留性。作业被装入内存后,就一直驻留在内存中,其任何部分都不会被换出,直至作业运行结束。运行中的进程会因等待I/O而被阻塞,可能处于长期等待状态。


局部性原理:
1. 时间局部性。程序中的某条指令一旦执行,不久后该指令可能再次执行;某数据被访问过,不久后该数据可能再次被访问。
2. 空间局部性。一旦程序访问了某个存储单元,不久后其附近的存储单元也将被访问,//即程序在一段时间内所访问的地址可能集中在一定的范围内。
基于局部性原理,引入虚拟存储器。
之所以将其称为虚拟存储器,是因为这种存储器实际上不存在,只是由于系统提供了部分装入、请求调入和置换功能后,给用户的感觉是好像存在一个比实际物理内存大得多的存储器。
虚拟存储器三特征:
(1)多次性
(2)对换性
(3)虚拟性   


1. 虚拟内存的实现方式?


1. 请求分页存储管理
2. 请求分段存储管理
3. 请求段页式存储管理


1、请求分页管理方式


只要求将当前需要的一部分页面装入内存,不是全部装入。




2、内存分段;分段机制下,虚拟地址和物理地址如何映射?


内存分段:
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段的形式把这些段分离出来。


问:分段机制下,虚拟地址和物理地址如何映射?
虚拟地址由两部分组成:段选择子和段内偏移量
段选择子:保存在段寄存器里面。
  段选择子里面最重要的是段号,用作段表的索引。
  段表里面保存的是这个段的基地址、段的界限和特权等级等。
段内偏移量:位于0和段界限之间。
    如果段内便宜量是合法的,就将段基地址加上段内偏移量得到物理内存地址。



内存分段存在的问题:
1. 内存碎片问题
2. 内存交换的效率低的问题
解决外部内存碎片的问题就是内存交换
外部交换空间在Linux系统里常见的是Swap空间,这个空间是从硬盘划分出来的
因为硬盘的访问速度比内存慢,如果内存交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿
为了解决内存分段的内存碎片和内存交换效率低的问题,就出现了内存分页


3、内存分页;分页机制下,虚拟地址和物理地址是如何映射的?


分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。
由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片的原因。
而采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。
如果内存空间不够,操作系统会把一些内存页面释放掉,叫做换出。
把需要的加载进来,叫做换入。
在Linux下,每一页的大小为4KB。


问:分页机制下,虚拟地址和物理地址是如何映射的?
答:在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,基地址与页内偏移的组合形成了物理内存地址。



内存地址转换步骤:
1. 把虚拟内存地址,切分成页号和偏移量
2. 根据页号,从页表里面,查询对应的物理页号
3. 物理页号+偏移量就得到物理内存地址


4、简单的分页有什么缺陷?


页表缺陷,页表可能很大。
32位环境下每个页表项需要4个字节大小来存储,4GB空间的映射需要4MB的内存存储页表
而且每个进程都有自己的虚拟地址空间,就是说有自己的页表;100个进程就需要400MB的内存存储页表。
所以引入多级页表


5、段页式内存管理


实现方式:
1. 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制
2. 接着再把每个段分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的项
这样地址结构就由段号、段内页号、页内位移三部分组成


段页式地址变换中要得到物理地址需经过三次内存访问:
1. 第一次访问段表,得到页表起始地址
2. 第二次访问页表,得到物理页号
3. 第三次将物理页号与页内位移组合,得到物理地址


6、说一说页表寻址


页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。


7、分页和分段的区别?


1. 段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
2. 段的大小不固定,由它所完成的功能决定;页的大小固定,由固定决定。
3. 段向用户提供二维地址空间;页向用户提供的是一维地址空间。
4. 段是信息的逻辑单位,便于存储保护和信息的共享;页的保护和共享受到限制。


2. 虚拟内存是什么?好处?代价?


虚拟内存:
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data 段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如 malloc 时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。


虚拟内存好处:
1. 扩大地址空间;
2. 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。
           虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3. 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4. 当进程通信时,可采用虚存共享的方式实现。
5. 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存。
6. 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高。
7. 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。


代价:
1. 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存。
2. 虚拟地址到物理地址的转换,增加了指令的执行时间。
3. 页面的换入换出需要磁盘I/O,这是很耗时的。
4. 如果一页中只有一部分数据,会浪费内存。


3. 说一说操作系统中的内存结构



一个程序本质上都是由BSS段、data段、text段三个组成的。
可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
1. BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。
   BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。
2. 数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配
3. 代码段:存放程序执行代码的一块内存区域。
   这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量
4. text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。
   bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
   data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。
数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
可执行程序在运行时又多出两个区域:栈区和堆区。
1. 栈区:由编译器自动释放,存放函数的参数值、局部变量等。
   * 每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间。
   * 每调用一个函数一个新的栈就会被使用。
   * 栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
2. 堆区:用于动态分配内存,位于BSS和栈中间的地址区域,由程序员申请分配和释放。
   * 堆是从低地址位向高地址位增长,采用链式存储结构。
   * 频繁的malloc/free造成内存空间的不连续,产生碎片。
   * 当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。


4. 用户空间分布情况



1. 程序文件段,包括二进制可执行代码
2. 已初始化数据段,包括静态常量
3. 未初始化数据段,包括未初始化的静态变量
4. 堆段,包括动态分配的内存,从低地址开始向上增长
5. 文件映射段,包括动态库、共享内存等,从低地址开始向上增长
6. 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB。


5. 请你说一说内存溢出和内存泄漏


1、内存溢出:指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存溢出原因:
(1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据
(2)集合类中有对对象的引用,使用完后未清空,使得不能回收
(3)代码中存在死循环或循环产生过多重复的对象实体
(4)使用的第三方软件中的 BUG
(5)启动参数内存值设定的过小


内存泄漏:指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类:
1、堆内存泄漏(Heap leak)。堆内存指的是程序运行中根据需要分配通过malloc、realloc、new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
2、系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如Bitmap、handle、SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。


6. 什么是缓冲区溢出?有什么危害?


缓冲区为暂时置放输出或输入资料的内存。
缓冲区溢出是指:当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
造成缓冲区溢出的主要原因:程序中没有仔细检查用户输入是否合理。
有什么危害:程序崩溃导致拒绝服务;跳转并且执行一段恶意代码。


7. 物理地址、逻辑地址、虚拟内存的概念


1. 物理地址:它是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址从主存中存取,是内存单元真正的地址。
2. 逻辑地址:是指计算机用户看到的地址。例如:当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间,指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址为起始地址+4。
实际上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
3. 虚拟内存:是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。


8. 常见内存分配错误


1. 内存分配未成功,却使用了它。
应该使用assert(p != NULL)或if(p == NULL)进程防错处理。
2. 内存分配成功,但未初始化就引用它。
3. 内存分配成功且初始化成功,但操作内存越界。(数组的索引越界)
4. 忘记了释放内存,造成内存泄漏
new-delete,malloc-free
5. 释放了内存却继续使用它
使用free或delete释放了内存后,没有将指针设置为NULL,导致产生“野指针”。


9. 内存交换中,被换出的进程保存在哪里?


保存在磁盘中,也就是外存中。
具有对换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式。对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式。总之,对换区的I/O速度比文件区的更快。


10. 抖动(颠簸现象)是什么?


刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度称为抖动(颠簸)。
产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)。
//为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率
相关文章
|
1月前
|
存储 程序员 编译器
C++:内存管理
C++:内存管理
43 1
|
2月前
|
存储 C语言 C++
C++:C/C++内存管理
C++:C/C++内存管理
35 0
|
6月前
|
存储 C语言 C++
|
1月前
|
存储 程序员 编译器
C++内存管理
C++内存管理
|
1月前
|
存储 程序员 C语言
【C++】| C/C++内存管理
【C++】| C/C++内存管理
|
1月前
|
存储 编译器 程序员
【C++】内存管理
【C++】内存管理
|
8月前
|
C语言 C++
C/C++内存管理
C/C++内存管理
43 0
|
4月前
|
C语言 C++ Windows
【C++】:内存管理
【C++】:内存管理
35 0
|
4月前
|
存储 Java C语言
c++内存管理
c++内存管理
38 0
|
8月前
|
C++ 容器
C++ 内存管理 基本部分
不能直接调用构造函数,但是可以直接调用析构函数
55 0