内存管理部分的几点思考

来源:互联网 发布:倚天屠龙记 知乎 编辑:程序博客网 时间:2024/05/29 18:46

整理人:zhoumo


一、内存分配路径



上面的这张图片来自国嵌,很好的把内存管理这四个章节的内容概括了出来,

上图显示出了三条内存分配路径:

1、用户空间的内存分配

2、Kmalloc使用slab内存分配

3、vmalloc:线性地址空间连续,物理地址不一定连续

同样的,上图也很好的概括了内存这一块重要的知识点

1、伙伴系统

2、slab分配器

3、进程地址空间

二、分段的由来

在学习分段和分页的时候,我们都知道Linux是有限度的使用了分段机制,段基地址寄存器全为0,既然如此,那分段是怎么来的呢?
最早的CPU是16位的,即:CPU的寄存器是16位的,但地址线却有20位,一个CPU寄存器无法存取一个完整的地址,如何解决?
由此引入了内存分段,有如下两个原则:
1、段的起始地址必须是16的倍数
2、段的最大长度不超过64K
采用原则1,是因为段的起始地址是16的倍数,这样的数字有一个特点,最后4个二进制位全为0,那么最后的4个0就可以省略,只把10位的前16位存入寄存器
采用原则2,是因为16为的CPU最大只能寻到64K

由此,我们可以得到一个物理地址的计算公式:PA = 段寄存器的值 * 16 + 段偏移量

现代的CPU都是64位(或32位),不需要使用省略后四个零这种方式存储段基地址了,因此分段机制逐渐被忽略。
实模式:没有分页机制,分段管理与16位CPU相同。
保护模式:与实模式最大的区别是段基地址寄存器中存储的内容不一样。保护模式下段基地址寄存器存储一个地址,这个地址的指向段描述符,得到段基地址

三、物理地址扩展(PAE)

问题由来:

为什么我们使用的windows server2008(貌似是这个版本?)是32位的系统,内存却可以支持到64G,不是说只能寻到4G吗?

winserver为什么要使用32位的系统,直接用64位的不行么?

解释:

32位的系统对软件的兼容性好,winserver使用32位,可以兼容更多的软件,那么需要一个折中:要使用32位系统,还要支持更大的内存,那么需要一种新的技术

PAE:

要求使用32位的线性地址,寻到64G的页框,需要对页表项、页全局目录,页目录进行更改,以三级映射为例,线性地址的映射有如下变化:

cr3

指向一个PDPT

位31-30

指向PDPT中的4个项中的一个

位29-21

指向页目录中512项中的一个

位20-12

指向页表中512个项中的一个

位11-0

4KB页中的偏移量


与原来的区别在于,位29-21,位20-12与原来相比,都由32位变成64位,但项数没变,因此“节省出”两位,在30-31加上一级,两位可以映射4个表项,每一项都是64位,cr3负责指向这四个中的一个。

下面来计算一下,因为增加了4个64位的表项,因此,每一项都乘以2,就是16,所以内存可以扩展到4G乘以16等于64G


四、从内存角度思考堆和栈(此部分摘自网络,并非原创)

现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。
这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。
因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。
和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free 函数维护了一套内部的堆数据结构。
当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间(库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间),如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
    1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
    2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
    3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。 


从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而栈是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。


既然没有专门的指令和寄存器,计算机的对堆内存究竟是如何管理的?


从图中我们可以看出,进程的堆,并不是直接建立在Linux的内核的内存分配策略上的,而是建立在glibc的堆管理策略上的(也就是glibc的动态内存分配策略上),堆的管理是由glibc进行的。
所以我们调用free对malloc得到的内存进行释放的时候,并不是直接释放给操作系统,而是还给了glibc的堆管理实体,而glibc会在把实际的物理内存归还给系统的策略上做一些优化,以便优化用户任务的动态内存分配过程。


那么glibc的堆管理器在什么时候才把物理内存归还给系统呢?

它会从堆的最大线性地址开始,从后向前计算用户任务当前有多少空闲的堆内存(直到碰到使用中的堆内存地址为止),比如在该图中,它会认为有2048k的可释放内存,只有在该值大于某个特定的threshhold时(2.3.6上为64k),它才会把这些内存归还给系统。而在中间的“未使用”内存是不会归还给系统的,所以系统也不可能再利用这块物理内存页(我们假设系统没有swap区和swap文件),也就是说系统的内存会为此减少,除非在它之前的堆内存都用free进行释放以后,glibc的堆管理器才有可能(只是有可能)把该段内存归还给系统。
由此,我们在使用malloc/free时应该小心,特别是在初始化时分配了好多内存,但是在这之后却再也不需要这么多的内存了,而这块内存又没有达到threshhold值或者在堆的最高线性地址处有某块内存没有释放,但是它前面的所有堆内存都释放了;这种情况下,用户任务将会浪费一些物理内存,这在资源比较紧张的嵌入式系统中是不可容忍的。 


0 0