转自:https://blog.csdn.net/shanghairuoxiao/article/details/70256247
为什么叫内存的抽象?
如果看过设计模式的人可能会知道,设计模式中提到最多的概念之一就是抽象,纯虚的基类作为接口就是对各种派生类对象的抽象。调用接口的用户,并不知道内部如何实现,因此内部实现的方法可能也有多种。地址空间也可以这样理解,32位机上,创建进程时操作系统为进程分配4GB的独立地址空间,用户可以使用这4GB的独立地址空间。但是,反过来一想,给每个进程都分配4GB地址空间,对于8GB内存的计算机而言岂不也就能同时运行两个进程。对于现代计算机而言,这显然是不可能的。所以实际上,用户能使用的4GB地址空间并不是对应物理内存的4GB,具体怎么实现被封装了,所以叫内存抽象。
多道程序实现
现代操作系统能够同时运行多个程序,程序被运行时,需要占用内存的一块空间,如果同时运行的程序太多,物理内存装不下了怎么办?因此出现了两种技术,交换技术和虚拟内存。
交换技术
交换技术:就是指当内存满了以后,就将一个程序从内存换出,将另一个程序放入内存,换出的内存数据保存在硬盘上,当该程序再次被换入的时候,就将硬盘上的数据拷贝到内存。
如下图,蓝色区域表示空闲的内存,绿色区域表示被某个进程占用的内存。刚开始装入A进程,然后装入B进程,再装入C进程。对于进程B而言假设其地址空间为0x0000-0xFFFF,对于其地址0x0000而言,对应的物理地址肯定不是0x0000,那应该是进程A的首地址。所以如果进程B要访问其首地址0x0000,就必须加上一个偏移量,而这样偏移量保存在一个基址寄存器中,除了基址寄存器还有一个界限寄存器防止访问越界。
再接着上面的说,如果这时候来了一个进程D,剩余的内存放不下进程D了,这时候可以选择将进程B交换出去,将进程B的数据保存到硬盘上,将进程D装入内存运行,如果CPU重新调度到进程B然后再采用同样的方式将某个进程交换出去,保存到硬盘上,把进程B装入内存中,这样的过程就叫交换技术。
虚拟内存
交换技术似乎解决了多道程序运行的问题,但是实际上如果每次交换一整个进程的数据,CPU需要花费数秒的时间来处理,这显然是不能被容忍的。因此,需要提出虚拟内存的概念。
虚拟内存:操作系统为了管理内存,给每个进程都分配独立的地址空间,对32位的系统而言,这个空间的大小是4GB。这4GB并不是实际的物理内存,实际上并不存在,因此有虚拟内存这一名称。
虚拟地址空间的地址称为逻辑地址,实际物理内存(就是内存条的大小)的地址空间称为物理地址。虚拟地址空间被分割成多个大小相同的页面(比如4k为一个页面),物理地址空间被分割成同样大小的页框。虚拟地址的页面通过一个页表映射物理内存的页框,页表中保存着两者的对应关系。逻辑地址是CPU使用的地址,当进程要访问该进程地址空间里的某个地址时时候,将该地址的值传递给CPU,CPU访问该地址时,会经过MMU将逻辑地址转换为物理地址,之前说的页表就保存在MMU中,操作系统为每个进程都维护一个页表。
说了这么多,我们还是不清楚为什么用虚拟内存就能实现多个程序同时运行,并且切换性能很高呢?
我们刚刚讲了,MMU把虚拟地址空间的页表和物理地址空间的页框关联起来了,如果页表中所有的数据都在页框中有对应项,那虚拟地址就没有任何意义了。实际上,程序运行的时候只需要部分数据存在内存中就可以了,因此只有部分页面和页框有对应值,其余的页表的数据保存在硬盘一块固定的地方(在Linux里叫swap分区,window里保存在C盘里)。当访问到某个页面在物理内存中没有对应的页框时就会发生缺页中断,这时候操作系统就将该页面保存在硬盘中的数据拷贝到物理内存中,并更新页表建立该页面和对应页框之间的映射关系。
这样做就实现了每次交换的代价很小,但是物理地址空间还是可能不够用,因此操作系统交换一些数据进物理内存的时候,也会从物理内存中移除部分页框数据到硬盘上,那到底该移出谁呢?这就涉及到页面交换算法了。
Linux内存管理
以Linux系统为例谈谈操作系统对内存的管理,一点皮毛,用于梳理自己的思路,使得面试的时候能够思路更清晰。
前面讲了进程具有独立的地址空间,对于32位的系统而言,该地址空间的大小是4GB。Linux将这4GB的地址空间分为两部分,一个是用户地址空间,一个是内核地址空间。内核地址空间的地址范围范围为3G到4G,用户地址空间的地址范围为0G到3G。这里所讲的0G到4G都是虚拟地址,也称为逻辑地址。
Linux对内核空间和用户空间是分别管理的,因为进程要么运行在用户态,要么运行在内核态,进程通过系统调用陷入内核态。
(借用网上一张图片说明一下,侵删)
内核空间
内核空间的逻辑地址范围在3GB到4GB,并且内核空间是线性映射到物理空间的。何为线性映射,举例说明,内核空间逻辑地址0xc0000000对应的物理地址是0x00000000,逻辑地址0xc0000001对应的物理地址是0x00000001,也就是说逻辑地址到物理都减了一个0xc0000000的偏移量。如果1GB都是这样映射的话,那么内核空间能使用物理地址范围在0x00000000到0x40000000之间,不能访问所有的物理地址了。
为了解决这个问题,内核空间就将物理内存分为三个区:ZONE_DMA,ZONE_NORMAL,ZONE_HIHGEM。DMA区是用于一些特殊设备的,我们不过多追究。主要讨论高端内存(ZONE_HIHGEM),对于内核空间而言高于896M的空间称为高端内存,低于896M的自然就可以称为低端内存了,低端内存的范围上,逻辑地址与物理地址是线性映射的。对于内核空间896M以上剩余的128M是用来访问高端内存的。这128M里的页面到物理页框随机映射的,和用户空间的映射是一样的。低端内存是自动永久映射的,高端内存可以永久映射也可以零时映射。
前面两端主要将的是对页表页框的管理,后面再将如何分配内存,也就是如果内核需要一定大小的内存的时,在3GB到4GB的范围里取出拿一块给它。内核空间分配内存可以按页分配,采用alloc_pages()和free_pages()函数分配多个连续页大小的内存,也可以通过kmalloc()分配指定大小的内存。
内核分配内存时很多时候都是分配固定大小的内存块,比如为每一个进程维护的task_struct结构体等。频繁分配这样的小块,很容易造成内存碎片,自然想到用内存池的方法来解决内存碎片的问题,只不过在Linux中给其取了一个更高大上的名字,叫高速缓存cache与slab层。一个高速缓存中有多个slab,分为三类:满的,部分满,和空的。每个slab就是一个链表,链表的每个节点就是一块固定大小的内存。和内存池是一样的。
用户空间
看过操作系统书的人肯定看到过下面这样图。
这张图解释了,一个进程将数据分为代码段,数据段,BSS段,堆和栈。实际上这些数据分享了0GB到3GB的地址范围。Linux管理这些段采用分区的结构,为每一个段维护一个vm_area_struct的结构体。这些结构体中保存了指向下一个指针因此形成了链表,还有另外一个指针使其构成红黑树,用户快速查找。
对于用户空间不得不谈到malloc函数,malloc函数是动态分配内存,内存来自用户空间的堆区。操作系统通过链表的形式将堆区的空间贯穿起来,当需要动态分配内存时就去查询该链表,找到空闲块,如果堆区满了,就调用sbrk函数扩大堆的范围。
当分配内存时,操作系统去查询该链表,找到一块能容纳下的地方放进去,将剩余的返还给空闲表。如何找到这个容纳的地方有出现了多种算法:首次适配算法,第二次首次适配,最佳适配算法,最差适配算法。这四个算法你可以去细讲差别,首次适配第一次找到第一个大于需要的地址空间的块,每次都从表头开始找,第二次着从上次找到的位置开始找,最佳适配找一个和需要大小最接近比需要的大的,最差每次早最大的。实际上对于进程内部堆的分配,页可以采取同样类似的办法。具体可以去看malloc的源码。
另外还有一点很重要的是内存映射文件,内存映射文件通过mmap函数实现,将文件映射到内存中,读写文件通过操作指针就能实现。实际上,内存映射文件并不是调用mmap的时候就将该文件拷贝到内存中,而是建立逻辑地址到文件地址之间的映射关系,但访问这段内存的数据时还是引发缺页中断,然后将该页的数据换到物理地址上。可以直接使用mmap实现进程间内存共享,XSI的内存共享实现的原理也是基于mmap,只是映射一种特殊文件系统的文件到内存中,该文件不能通过read和write调用来访问。
最后用一张图来结束本篇文章
如有错误欢迎指正!
参考文章:
http://blog.csdn.net/yusiguyuan/article/details/12045255#comments
http://www.secretmango.com/jimb/Whitepapers/slabs/slab.html