Linux系统内存管理
文章目录
地址
相关概念
物理地址
内存是由若干个存储单元组成的,每个存储单元有一个编号,这种编号可唯一标识一个存储单元,称为内存地址(或物理地址)。我们可以把内存看成一个从0字节一直到内存最大容量逐字节编号的存储单元数组,即每个存储单元与内存地址的编号相对应。
逻辑地址
指由程序产生的与段相关的偏移地址部分。
逻辑地址的组成:是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]
源程序经过汇编或编译后,形成目标代码,每个目标代码都是以0为基址顺序进行编址的,原来用符号名访问的单元用具体的数据——单元号取代。这样生成的目标程序占据一定的地址空间,称为作业的逻辑地址空间,简称逻辑空间。
在逻辑空间中每条指令的地址和指令中要访问的操作数地址统称为逻辑地址。即应用程序中使用的地址。要经过寻址方式的计算或变换才得到内存中的物理地址。
逻辑地址就是你源程序里使用的地址,或者源代码经过编译以后编译器将一些标号,变量转换成的地址,或者相对于当前段的偏移地址。
线性地址(虚拟地址)
分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址*16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。
线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
地址映射
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量=),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线性地址,再利用其页式内存管理单元,转换为最终物理地址。
由于linux没有分段机制,所以段的基地址也是0。因此逻辑地址、线性地址在linux中其实是相同的。所以对于linux下的elf可执行文件来说,代码段的起始地址0x08048000既是逻辑地址,也是线性地址(虚拟地址)。内核的虚拟地址和物理地址,大部分只差一个线性偏移量。
虚拟地址空间
虚拟内存的容量
计算机所支持的最大内存是由该计算机的地址位数决定的,也就是计算机的最大寻址能力。
虚拟内存容量= min(2^计算机位数, 内存+外存)。
32位机的寻址能力为2的32次方,4GB。 此时虚存=4G
64位机的寻址能力= 16777216T, 此时虚存=内存+外存
空间分布
通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。
Linux 简化了分段机制,令段标识符为0,使得虚拟地址(逻辑地址)与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G(2^32)。
Linux内核将这4G字节的空间分为两部分。将最高的 1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间“。
因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
用户进程地址映射
因为用户进程拥有3G的虚拟地址空间,虚拟地址与物理地址一对一映射,所以最多只可以访问3G物理内存。
内核进程地址映射
当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址0xc0000003对应的物理地址为0×3,0xc0000004对应的物理地址为0×4,… …,逻辑地址与物理地址对应的关系为
物理地址 = 逻辑地址 – 0xC0000000
对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
假设按照上述简单的地址映射关系,那么内核逻辑地址空间访问为0xc0000000 ~ 0xffffffff,那么对应的物理内存范围就为0×0 ~ 0×40000000,即只能访问1G物理内存。若机器中安装8G物理内存,那么内核就只能访问前1G物理内存,后面7G物理内存将会无法访问,因为内核 的地址空间已经全部映射到物理内存地址范围0×0 ~ 0×40000000。即使安装了8G物理内存,那么物理地址为0×40000001的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址 的,0xc0000000 ~ 0xffffffff的地址空间已经被用完了,所以无法访问物理地址0×40000000以后的内存。
显然不能将内核地址空间0xc0000000 ~ 0xfffffff全部用来简单的地址映射。因此x86架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存,这就是内高端内存概念的由来。
内核地址空间分布
在x86结构中,三种类型的区域如下:
ZONE_DMA:内存开始的16MB
该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
ZONE_NORMAL:16MB~896MB
该区域的物理页面是内核能够直接使用的
ZONE_HIGHMEM:896MB ~ 结束
该区域即为高端内存,内核不能直接使用
高端内存HIGH_MEM地址空间范围为 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?
当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。
例如内核想访问2G开始的一段大小为1MB的物理内存,即物理地址范围为0×80000000 ~ 0x800FFFFF。访问之前先找到一段1MB大小的空闲地址空间,假设找到的空闲地址空间为0xF8700000 ~ 0xF87FFFFF,用这1MB的逻辑地址空间映射到物理地址空间0×80000000 ~ 0x800FFFFF的内存。映射关系如下:
当内核访问完0×80000000 ~ 0x800FFFFF物理内存后,就将0xF8700000 ~ 0xF87FFFFF内核线性空间释放。这样其他进程或代码也可以使用0xF8700000 ~ 0xF87FFFFF这段地址访问其他物理内存。
从上面的描述,我们可以知道高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。
用户空间(进程)是否有高端内存概念?
用户进程没有高端内存概念。只有在内核空间才存在高端内存。用户进程最多只可以访问3G物理内存,而内核进程可以访问所有物理内存。
64位内核中有高端内存吗?
目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存。
用户进程能访问多少物理内存?内核代码能访问多少物理内存?
32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。
64位系统用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。
常见内存分配算法
(1)最先适应算法
使用该算法进行内存分配时,从空闲分区链首开始查找,直至找到一个能满足其大小需求的空闲分区为止。然后再按照作业的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链中。
优点:
-
分配简单而且合并相邻的空闲区也比较容易,该算法的实质是尽可能利用存储区低地址的空闲区,而尽量在高地址部分保存较大的空闲去,以便一旦有分配大的空闲区的要求时,容易得到满足。
-
在释放内存分区时,如果有相邻的空闲区就进行合并,使其成为一个较大的空闲区。 缺点:
-
由于查找总是从表首开始,前面的空闲区北分割得很小时,能满足分配要求的可能性就较小,查找次数就较多。
-
会产生碎片,这些碎片散布在存储器的各处,不能集中使用,因而降低了存储器的利用率。
(2)循环最先适应算法
该算法是由首次适应算法演变而成的。在为进程分配内存空间时,不再每次从链首开始查找,而是从上次找到的空闲分区开始查找,直至找到一个能满足需求的空闲分区,并从中划出一块来分给作业。
该算法能使空闲中的内存分区分布得更加均匀,但将会缺乏大的空闲分区。
(3)最佳适应算法
将所有的空闲分区按照其容量递增的顺序排列,当要求分配一个空白分区时,由小到大进行查找。找到其大小与要求相差最小的空闲分区,在所有大于或者等于要求分配长度的空闲区中挑选一个最小的分区,即对该分区所要求分配的大小来说,是最合适的。
优点:
-
由于算法是在所有大于或者等于要求分配长度的空闲区中挑选一个最小的分区,所以分配后所剩余的空白块会最小。
-
平均而言,只要查找一半的表格便能找到最佳适应的空白区。
-
如果一个空白区的容量正好满足要求,则它比被选中。
缺点:
-
由于空闲区是按大小而不是按地址的顺序排列的,因此释放时,要在整个链表上搜索地址相邻的空闲去,合并后又要插入到合适的位置。
-
空白区一般不可能恰好满足要求,在分配之后的剩余部分通常会非常小,以致小到无法使用。
(4)最差适应算法
最坏适应算法的思想与最佳适应算法相反,将所有的空白分区按容量递减的顺序排列,最前面的最大的空闲分区就是找到的分区。该算法是取所有空闲区中最大的一块,把剩余的块再变成一个新小一点的空闲区。算法基本不留下小空闲分区,但较大的空闲分区不会被保留。
优点:分配的时候只需查找一次就可以成功,分配的算法很快。
缺点:最后剩余的分区会越来越小,无法运行大程序。
Linux内存分配算法:伙伴系统
基本概念
伙伴系统是一种经典的内存管理方法。Linux伙伴系统的引入为内核提供了一种用于分配一组连续的页而建立的一种高效的分配策略,并有效的解决了外碎片问题。
它要解决的问题是频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足请求。
假设要请求一个256 个页框的块(即1MB)。算法先在256 个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512 个页框的链表中找一个空闲块。如果存在这样的块,内核就把512 的页框分成两等份,一半用作满足请求,另一半插入到256 个页框的链表中。如果在512 个页框的块链表中也没找到空闲块,就继续找更大的块 —— 1024个页框的块。如果这样的块存在,内核把1024个页框块的256 个页框用作请求,然后从剩余的768 个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错信号。
组织结构
Linux中的内存管理的“页”大小为4KB。把所有的空闲页分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该块大小的整数倍。
结构如图所示:第i个块链表中,num表示大小为(2^i)页块的数目,address表示大小为(2^i)页块的首地址。
内存分配及释放
当向内核请求分配(2^(i-1),2^i]数目的页块时,按照2^i页块请求处理。如果对应的块链表中没有空闲页块,则在更大的页块链表中找。当分配的页块中有多余的页时,伙伴系统根据多余的页框大小插入到对应的空闲页块链表中。
当释放单页的内存时,内核将其置于CPU高速缓存中,对很可能出现在cache的页,则放到“快表”的列表中。在此过程中,内核先判断CPU高速缓存中的页数是否超过一定“阈值”,如果是,则将一批内存页还给伙伴系统,然后将该页添加到CPU高速缓存中。 v 当释放多页的块时,内核首先计算出该内存块的伙伴的地址。内核将满足以下条件的三个块称为伙伴:(1)两个块具有相同的大小,记作b。(2)它们的物理地址是连续的。(3)第一块的第一个页的物理地址是2*b的倍数。如果找到了该内存块的伙伴,确保该伙伴的所有页都是空闲的,以便进行合并。内存继续检查合并后页块的“伙伴”并检查是否可以合并,依次类推。
反碎片机制
内核将已分配页分为以下三种不同的类型:
-
不可移动页:这些页在内存中有固定的位置,不能够移动。
核心内核分配的大多数内存属于该类别。
-
可回收页:这些页不能移动,但可以删除。内核在回收页占据了太多的内存时或者内存短缺时进行页面回收。
例如,映射自文件的数据属于该类别kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收.
-
可移动页:这些页可以任意移动,用户空间应用程序使用的页都属于该类别。它们是通过页表映射的。当它们移动到新的位置,页表项也会相应的更新。
属于用户空间应用程序的页属于该类别. 它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事
由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块.
在内存子系统初始化期间,所有的页都被标记为可移动的。在启动期间,核心内核分配的内存是不可移动的。此时内核的策略是分配一个尽可能大的连续内存块,将其从可移动列表转换到不可移动列表。分配一个尽可能大的连续内存块而不是选择更小的满足要求的内存块的原因如下:
例如:现有一块可移动的具有16页的连续空闲内存块。当内核需要分配1页不可移动内存页时。分配器选择后8页(而不是一页)转移到不可移动列表,然后从不可移动列表中选择1页用于分配。如果内核又需要分配1页不可移动内存页则从不可移动列表中选择一页用于分配,下图显示了进行4次分配后内存的分布。
但如果内核只选择1页用于分配将其加入到不可移动列表,当下次需要分配时又选择一页加入到不可移动列表,下图显示了按照这种方式进行4次分配后内存的分布。
因此,当没有满足可用于分配的不可移动空闲块时,分配器会在可移动列表中迁移一个尽可能大的连续内存块给不可移动列表。这样避免了启动期间内核分配的内存散列到物理内存各处,从而使其他类型的内存分配免受碎片的干扰。
伙伴算法的优缺点
优点
-
较好的解决外部碎片问题
-
当需要分配若干个内存页面时,用于DMA的内存页面必须连续,伙伴算法很好的满足了这个要求
-
只要请求的块不超过512个页面(2K),内核就尽量分配连续的页面。
-
针对大内存分配设计。
缺点
-
合并的要求太过严格,只能是满足伙伴关系的块才能合并,比如第1块和第2块就不能合并。
-
碎片问题:一个连续的内存中仅仅一个页面被占用,导致整块内存区都不具备合并的条件
-
浪费问题:伙伴算法只能分配2的幂次方内存区,当需要8K(2页)时,好说,当需要9K时,那就需要分配16K(4页)的内存空间,但是实际只用到9K空间,多余的7K空间就被浪费掉。
-
算法的效率问题: 伙伴算法涉及了比较多的计算还有链表和位图的操作,开销还是比较大的,如果每次2^n大小的伙伴块就会合并到2^(n+1)的链表队列中,那么2^n大小链表中的块就会因为合并操作而减少,但系统随后立即有可能又有对该大小块的需求,为此必须再从2^(n+1)大小的链表中拆分,这样的合并又立即拆分的过程是无效率的。
Linux内存分配算法:slab机制
slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。
slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。
比如我需要一个100字节的连续物理内存,那么内核slab分配器会给我提供一个相应大小的连续物理内存单元,为128字节大小(不会是整好100字节,而是这个档的一个对齐值,如100字节对应128字节,30字节对应32字节,60字节对应64字节),这个物理内存实际上还是从伙伴系统获取的物理页;当我不再需要这个内存时应该释放它,释放它并非把它归还给伙伴系统,而是归还给slab分配器,这样等再需要获取时无需再从伙伴系统申请,这也就是为什么slab分配器往往会把最近释放的内存(即所谓“热”)分配给申请者,这样效率是比较高的。
slub机制
SLUB 分配器特点是简化设计理念,同时保留 SLAB 分配器的基本思想:每个缓冲区由多个小的 slab 组成,每个 slab 包含固定数目的对象。SLUB 分配器简化了kmem_cache,slab 等相关的管理数据结构,摒弃了SLAB 分配器中众多的队列概念,并针对多处理器、NUMA 系统进行优化,从而提高了性能和可扩展性并降低了内存的浪费。为了保证内核其它模块能够无缝迁移到 SLUB 分配器,SLUB 还保留了原有 SLAB 分配器所有的接口 API 函数。
Swap交换分区
交换分区(swap space)是一定磁盘空间(分区或文件),用于将部分内存中的数据换下来,以腾出内存空间用于其他需求。在一个系统中,物理内存的容量是有限的,当物理内存快使用完时,操作系统会使用交换分区(如果有的话)。当系统内存使用紧张时,操作系统根据一定的算法规则,将一部分最近没使用的内存页面保存到交换分区,从而为需要内存的程序留出足够的内存空间;在SWAP中的内存页面被访问到时,系统会将其重新载入到物理内存中去运行。
swap in (换入):是指页面从交换分区转移到内存之中;
swap out (换出):是指页面从从内存转移到交换分区中。
(这里的in/out是站在内存的角度来说的,就好理解了。)
在Windows、Unix、Linux系统中都有交换分区的概念。
查看Swap分区大小
可以用“cat /proc/swaps”、“swapon -s”、“free”等命令查看Swap分区的情况。
Swap分区大小设置
系统的Swap分区大小设置多大才是最优呢? 关于这个问题,应该说只能有一个统一的参考标准,具体还应该根据系统实际情况和内存的负荷综合考虑,像ORACLE的官方文档就推荐如下设置,这个是根据物理内存来做参考的。
分页机制
分页存储管理是将作业的逻辑地址划分为一系列同等大小的部分,称为页。并为各页加以编号,每个作业的页的编号都是从0开始的。与之类似,把可用的物理内存也划分为同样大小的连续的部分,称为块或页框。
在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address).
若虚拟地址空间很大而每页比较小,则页表太长,这时可采用Linux内核使用四级分页机制.原因如下:
如果一级页表中的一个页表条目为空,那么其所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的。
对于不同的体系结构,Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。
比如对于4G的空间,如果每页的大小为4K
-
在单级分页下:每个进程需要生成1M个页标项(无论用不用都需要,系统不知道哪个地址是否会被访问)每个进程都需要1M*4byte=4M的页表
-
在二级分页下:对于非4M的地址,不需要分配二级页表。所以每个进程需要一个页目录 + 1个页表(比如只使用4M的情况)就足够了。页目录占1页,1个页表占1页,共8KB(2页)
四级分页机制
四级分页机制包括:
页全局目录(Page Global Directory):页全局目录包含若干页上级目录的地址;
页上级目录(Page Upper Directory):页上级目录又依次包含若干页中间目录的地址;
页中间目录(Page Middle Directory):页中间目录又包含若干页表的地址;
页表(Page Table):每一个页表项指向一个页框。页表具有逻辑地址到物理地址映射的作用。
分页>分段
分段方法存在一个严重的问题:内存的使用效率低。分段法要求换入换出磁盘的空间是整个程序的所需空间,而根据程序的局部性原理,程序运行时需要的数据只是很小的一部分。而请求分段存储管理机制实现复杂。请求分页机制实现简单,可以解决分段方法的内存使用效率问题,更加有效地利用内存。
请求分页机制
在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
页面淘汰算法
先进先出页面淘汰算法(FIFO)
置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。但是该算法会淘汰经常访问的页面,不适应进程实际运行的规律,目前已经很少使用。
中断次数为6,缺页中断率为9/12*100% = 75%。
最近最少使用页面淘汰算法(LRU)
置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
LRU算法普偏地适用于各种类型的程序,但是系统要时时刻刻对各页的访问历史情况加以记录和更新,开销太大,因此LRU算法必须要有硬件的支持。
中断次数为6,缺页中断率为7/12*100% = 58.3%。
最不经常用算法(LFU)
择近期最少访问的页面作为被替换的页面。这种算法实现起来非常困难,它要为每个页面设置一个很长的计数器,并且要选择一个固定的时钟为每个计数器定时计数。在选择被替换页面时,要从所有计数器中找出一个计数值最大的计数器。
注意LFU和LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
Belady异常
一般来说,分配给进程的物理块越多,运行时的缺页次数应该越少,使用FIFO时,可能存在相反情况,分配4个物理块的缺页竟然比3个物理块的缺页次数还多!
例如:进程访问顺序为0、2、1、3、0、2、4、0、2、1、3、4。 M=3时,缺页中断9次。缺页中断率9/12*100% = 75%。
在1G内存的计算机中能否malloc(1.2G)
malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。程序运行时,堆空间只是程序向操作系统申请划出来的一大块虚拟地址空间。应用程序通过malloc申请空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
本题要申请空间的大小为1.2G=2^30×1.2 Byte,转换为十六进制约为4CCC CCCC,这个数值已经超过了int类型的表示范围,但还在unsigned的表示范围。幸运的是 malloc 函数要求的参数为unsigned。见下面的示例代码。
|
|
malloc能够申请的空间到底能达到多大,还真是一个比较复杂的问题。想知道在一台机器上malloc能够申请的最大空间到底是多少,可以使用下面的程序进行测试。
|
|
在Linux和Winidows环境下运行,返回结果约等于2.9G和1.9G,结果相差很大,为什么?
下面以Linux操作系统为例对其进行分析。
一个文件编译、链接后形成的可执行映像(32位系统上)如图所示:
-
当请求分配的内存小于或等于128kb时,malloc()会调用系统调用brk()把指向堆起始地址的brk从数据段(.data)结束地址往高地址调整,分配的内存在堆区域。
-
当请求的内存大于128kb,malloc()会调用mmap()分配一块内存,mmap是在进程的虚拟地址空间中(一般是堆和栈中间)找一块空闲的区域。
从上图也可以看出,malloc()所分配的最大空间不仅与整个用户虚拟地址空间的大小有关(Linux是3GB,Windows是2GB),还与栈的大小,以及该程序数据段和代码段等占用的空间,以及连接程序链接方式等有关。
-
malloc分配的内存在虚拟地址空间,与物理内存没有直接的关系,当程序的第一条指令执行时,发生缺页中断,操作系统负责分配物理内存,并建立虚拟内存和物理内存之间的映射关系。因此,不管是在32位的Windows下还是Linux下,mallo(1.2G)都会分配成功。
如果一个操作系统没有虚拟内存管理,malloc(1.2GB)会发生什么情况?
比如实时操作系统μLinux,就是 针对没有内存管理单元的 CPU 设计的, 所以不能使用处理器的虚拟内存管理技术。 虽然 μLinux 仍然采用存储器的分页管理,系统在启动时把实际存储器进行分页,在加载应用程序时按页加载,但由于没有 MMU 管理,所以 μLinux 采用实存储器管理策略。
μLinux 系统对内存的访问是直接的, 所有程序中访问的地址都是实际的物理地址。 操作系统对内存空间没有保护,各个进程没有独立的地址转换表,共享一个运行空间。一个进程在执行前,系统必须为进程分配足够的连续地址空间, 然后全部载入主存储器的连续空间中,这种情况下,在只有1G物理内存的计算机上malloc(1.2G)就会失败。如果其他的实时操作系统不支持虚拟内存管理,那么同样的情况发生。
转载
http://blog.csdn.net/tommy_wxie/article/details/17122923/
文章作者 Forz
上次更新 2017-06-25