Linux下的内存管理

本文探讨了早期计算机系统中内存管理的局限性,如进程地址空间无法隔离、内存效率低下以及程序运行地址不确定等问题。通过引入虚拟内存,现代Linux实现了进程的地址空间隔离和更高效的内存利用,包括分段和分页技术。虚拟内存通过MMU进行虚拟地址到物理地址的转换,并利用交换空间和延迟分配策略提高系统性能。
摘要由CSDN通过智能技术生成

下面举一个早期的计算机系统中,内存分配管理的例子,以便于大家理解。

加入我们有三个程序,程序1,2,3.程序1运行的过程中需要10M内存,程序2运行的过程中需要100M内存,而程序3运行的过程中需要20M内存。如果系统同时需要运行程序A和B,那么早期的内存管理过程大概是这样的,将物理内存的前10M分配给A, 接下来的10M-110M分配给B。这种内存管理的方法比较直接,好了,假设我们这个时候想让程序C也运行,同时假设我们系统的内存只有128M,显然按照这种方法程序C由于内存不够是不能够运行的。大家知道可以使用虚拟内存的技术,内存空间不够的时候可以将程序不需要用到的数据交换到磁盘空间上去,已达到扩展内存空间的目的。下面我们来看看这种内存管理方式存在的几个比较明显的问题。就像文章一开始提到的,要很深层次的把握某个技术最好搞清楚其发展历程。



1. 进程地址空间不能隔离


由于程序直接访问的是物理内存,这个时候程序所使用的内存空间不是隔离的。举个例子,就像上面说的A的地址空间是0-10M这个范围内,但是如果A中有一段代码是操作10M-128M这段地址空间内的数据,那么程序B和程序C就很可能会崩溃(每个程序都可以系统的整个地址空间)。这样很多恶意程序或者是木马程序可以轻而易举的破快其他的程序,系统的安全性也就得不到保障了,这对用户来说也是不能容忍的。

2.   内存使用的效率低


如上面提到的,如果我们要像让程序A、B、C同时运行,那么唯一的方法就是使用虚拟内存技术将一些程序暂时不用的数据写到磁盘上,在需要的时候再从磁盘读回内存。这里程序C要运行,将A交换到磁盘上去显然是不行的,因为程序是需要连续的地址空间的,程序C需要20M的内存,而A只有10M的空间,所以需要将程序B交换到磁盘上去,而B足足有100M,可以看到为了运行程序C我们需要将100M的数据从内存写到磁盘,然后在程序B需要运行的时候再从磁盘读到内存,我们知道IO操作比较耗时,所以这个过程效率将会十分低下。

3. 程序运行的地址不能确定


程序每次需要运行时,都需要在内存中非配一块足够大的空闲区域,而问题是这个空闲的位置是不能确定的,这会带来一些重定位的问题,重定位的问题确定就是程序中引用的变量和函数的地址,如果有不明白童鞋可以去查查编译原理方面的资料。



现在的内存管理方法就是在程序和物理内存之间引入了虚拟内存这个概念。虚拟内存位于程序和物理内存之间,程序只能看见虚拟内存,再也不能直接访问物理内存。每个程序都有自己独立的进程地址空间,这样就做到了进程隔离。这里的进程地址空间是指虚拟地址。顾名思义既然是虚拟地址,也就是虚的,不是现实存在的地址空间。

既然我们在程序和物理地址空间之间增加了虚拟地址,那么就要解决怎么从虚拟地址映射到物理地址,因为程序最终肯定是运行在物理内存中的,主要有分段和分页两种技术。

分段(Segmentation):这种方法是人们最开始使用的一种方法,基本思路是将程序所需要的内存地址空间大小的虚拟空间映射到某个
物理地址空间。





每个程序都有其独立的虚拟的独立的进程地址空间,可以看到程序A和B的虚拟地址空间都是从0x00000000开始的。我们将两块大小相同的虚拟地址空间和实际物理地址空间一一映射,即虚拟地址空间中的每个字节对应于实际地址空间中的每个字节,这个映射过程由软件来设置映射的机制,实际的转换由硬件来完成。

这种分段的机制解决了文章一开始提到的3个问题中的进程地址空间隔离和程序地址重定位的问题。程序A和程序B有自己独立的虚拟地址空间,而且该虚拟地址空间被映射到了互相不重叠的物理地址空间,如果程序A访问虚拟地址空间的地址不在0x00000000-0x00A00000这个范围内,那么内核就会拒绝这个请求,所以它解决了隔离地址空间的问题。我们应用程序A只需要关心其虚拟地址空间0x00000000-0x00A00000,而其被映射到哪个物理地址我们无需关心,所以程序永远按照这个虚拟地址空间来放置变量,代码,不需要重新定位。

无论如何分段机制解决了上面两个问题,是一个很大的进步,但是对于内存效率问题仍然无能为力。因为这种内存映射机制仍然是以程序为单位,当内存不足时仍然需要将整个程序交换到磁盘,这样内存使用的效率仍然很低。那么,怎么才算高效率的内存使用呢。事实上,根据程序的局部性运行原理,一个程序在运行的过程当中,在某个时间段内,只有一小部分数据会被经常用到。所以我们需要更加小粒度的内存分割和映射方法,此时是否会想到Linux中的Buddy算法和slab内存分配机制呢,哈哈。另一种将虚拟地址转换为物理地址的方法分页机制应运而生了。

分页机制:

分页机制就是把内存地址空间分为若干个很小的固定大小的页,每一页的大小由内存决定,就像Linux中ext文件系统将磁盘分成若干个Block一样,这样做是分别是为了提高内存和磁盘的利用率。试想以下,如果将磁盘空间分成N等份,每一份的大小(一个Block)是1M,如果我想存储在磁盘上的文件是1K字节,那么其余的999K字节是不是浪费了。所以需要更加细粒度的磁盘分割方式,我们可以将Block设置得小一点,这当然是根据所存放文件的大小来综合考虑的,好像有点跑题了,我只是想说,内存中的分页机制跟ext文件系统中的磁盘分割机制非常相似。

Linux中一般页的大小是4KB,我们把进程的地址空间按页分割,把常用的数据和代码页装载到内存中,不常用的代码和数据保存在磁盘中





我们可以看到进程1和进程2的虚拟地址空间都被映射到了不连续的物理地址空间内(这个意义很大,如果有一天我们的连续物理地址空间不够,但是不连续的地址空间很多,如果没有这种技术,我们的程序就没有办法运行),甚至他们共用了一部分物理地址空间,这就是共享内存。

进程1的虚拟页VP2和VP3被交换到了磁盘中,在程序需要这两页的时候,Linux内核会产生一个缺页异常,然后异常管理程序会将其读到内存中。

这就是分页机制的原理,当然Linux中的分页机制的实现还是比较复杂的,通过了也全局目录,也上级目录,页中级目录,页表等几级的分页机制来实现的,但是基本的工作原理是不会变的。

分页机制的实现需要硬件的实现,这个硬件名字叫做MMU(Memory Management Unit),他就是专门负责从虚拟地址到物理地址转换的,也就是从虚拟页找到物理页。


二 虚拟内存的实现机制

    首先呢,提一个概念,交换空间(swap space),这个大家应该不陌生,在重装系统的时候,会让你选择磁盘分区,就比如说一个硬盘分几个部分去管理。其中就会分一部分磁盘空间用作交换,叫做swap space。其实就是一段临时存储空间,内存不够用的时候就用它了,虽然它也在磁盘中,但省去了很多的查找时间啊。当发生进程切换的时候,内存与交换空间就要发生数据交换一满足需求。所以啊,进程的切换消耗是很大的,这也说明了为什么自旋锁比信号量效率高的原因。

    那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;就是说一个函数可能只在物理内存中呆了一会,用完了就被清除出去了,虽然在虚拟地址空间还在。(不过虚拟地址空间不是事实上的存储,所以只能说这个函数占据了一段虚拟地址空间,当你访问这段地址时,就会产生缺页处理,从交换区把对应的代码搬到物理内存上来)

三 物理内存与虚拟内存的布局

左边是物理地址分配,与实际的CPU相关。4KB的这些都是一些控制器所占有,比如lcdc sd卡,他们的寄存器地址就是这样定死的。但是呢,我们要访问这些寄存器的时候,还是不能直接用,要使用内存管理的规则,使用虚拟地址去访问它,所以在驱动等内核程序中需要使用虚拟地址访问寄存器。如果有人直接使用物理地址访问寄存器,那么唯一的解释就是没有开mmu。不过这样你的进程就没有4G内存可以用了。

物理地址分布:

这是偷的别人的图啦,物理地址有896M直接映射到虚拟地址的内存空间,这是一一对应的映射,只有起始地址不一样,偏移是一样的。这个大小大多是固定的,哪怕你的内存超过一个G,太小了就另外说了。注意:用户区的代码也是放在这段物理地址里面的,就是说物理地址可以进行二次映射。但不管怎么样,这段物理地址都是受内核管理。当你内存很大的时候,超过896M时,剩余的那些内存怎么办呢?这多出来的叫做高端内存,如果你使用vmalloc申请空间,就会在高端内存中分配,如果你使用kmalloc申请空间,就会在小于896的内存中分配。所以还是很讲究的啊!!如果你的程序需要使用高端内存,就要调用内核API来分配,所以高端内存并不是想用就能用的哦。不过通过系统把一些应用常住在高端内存到是个好注意。不过前提是你的内存灰常大啊。

为什么要这样做呢?先看看这里面放些什么?

虚拟地址分布:

关于0-3G用户空间内存的分布:

谈到段式分布,就要说说逻辑地址,线性地址与物理地址的关系:

linux通过段机制把逻辑地址转换为虚拟地址(就是线性地址),再通过页机制把虚拟地址转换为物理地址。所谓分段就是基址不同,偏移一样,比如说32位,一般程序里面都不会使用这么多的位,可以把前12位用作基址,后20位用作偏移,这样在特定段就可以只使用偏移寻址了。寻址很方便,不过linux页基址做的更好。

最后呢再说几个点:

1 线性地址空间:指linux系统中的虚拟地址空间。

2 cpu寻址是属于物理地址。所以在使用cpu寻址前要把地址转换好。

3 物理内存中的高端内存是DDR减去896M后多出来的那一段。虚拟地址里面的高端内存是指用于映射高端内存的虚拟地址空间。不过高端内存被映射到用户空间,那就是另外一回事了吧。

4 内核空间是可以访问用户空间的,级别高就是好啊。不过不是通过虚拟地址直接访问的。

Linux虚拟内存的实现,需要几种不同的机制来实现:
 
地址映射机制
 
内存的分配与回收
 
请页机制
 
交换机制
 
内存共享机制
 
在具体的读 源码 之前.我们先根据我们以前学过的操作系统知识.和C语言等知识.来考虑一下,这几个机制如何实现.现自己设计一下.在看别人是怎样实现的.找到自己想不到.或者对效率空间有损的地方.这样才有进步.我不止一次的说.操作系统的某一部分,就起实现来说,非常简单.它的难点是如何将大量的功能集成出一个kernerl.
 
地址映射机制,说白了,就是在虚拟内存与物理内存上的一个桥梁.它要做的事情可能就是通过几个不同的表.把虚拟地址转换成物理地址,把物理地址转化虚拟地址.
 
我们以前说过.因为有系统与用户之分,它必须也要有不同的数据结构.为了解决速度等问题.它会有一个硬件的缓冲区
 
对于它的数据结构.我们可以先想一下.如虚拟地址的信息,虚拟地址在那个区域等等
 
至于请页机制,更好理解.因为linux是页式存储的.因此必然会存在空白页和使用页.既然是页.就必然会存在页溢出.页无效(是不是在win98 下经常出现类似错误,当然linux的内存管理不可能和windows一样,可基本道理相同).因此.在每一个页出错.或者该页存不下多余的数据时.就要要求内核分配新的页面
 
同时.当时用fork() 产生一个新的进程时.也需要分配新的叶面.这一部分大概讲的就是进程如何向内和描述自己需要怎么样的和多少页
 
在我们学习<<数据结构>>是我们学了,很多内存分配方式,如首次拟和.最佳拟和,最差拟和等等.但是我们可以想象.linux 大概不会用他们.那就一定是伙伴系统了.因此我们可以对于伙伴系统的分配,回收的基本算法.回想一下.这样在读者一部分源码时,回有意象不到的收获.
 
至于交换机制.我们也可以现想一想.内存中总与很多使用者的页.如果这些也已经把所有的页都用完了.再分配时必须把其中的某些页释放.释放那些页,需要考虑.如最近不用页.近期少用页,等等都可以在考虑之中.
 
这个算法,大概就是计算内存中使用的页,什么时候可以换处.说白了就是为所有的使用页计算一个”权”,而这个”权”就决定了他什么时候被释放以换如它的内容.需要想的是对于经常使用的页.可以把它放入cahe.(尽管这一部分对程序员是透明的,但我们应该理解他的原理).
 
最后的一部分共享内存,我想和我门初学linux 编程 时,进程通讯里面的共享内存没有区别.大概也就是在它的数据结构中加入可以允许不同进程访问的tag 就行了.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值