操作系统对内存管理

操作系统 专栏收录该内容
2 篇文章 0 订阅

为什么叫内存的抽象?

如果看过设计模式的人可能会知道,设计模式中提到最多的概念之一就是抽象,纯虚的基类作为接口就是对各种派生类对象的抽象。调用接口的用户,并不知道内部如何实现,因此内部实现的方法可能也有多种。地址空间也可以这样理解,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

说了这么多,我们还是不清楚为什么用虚拟内存就能实现多个程序同时运行,并且切换性能很高呢?

我们刚刚讲了,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

  • 9
    点赞
  • 1
    评论
  • 16
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

文将 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。 追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。 不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求: 确定您是否有足够的内存来处理数据。 从可用的内存中获取一部分内存。 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。 实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。 回页首 C 风格的内存分配程序 C 编程语言提供了两个函数来满足我们的三个需求: malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。 free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。 物理内存和虚拟内存 要理解内存在程序中是如何分配的,首先需要理解如何将内存操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。 只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一应关系的数学术语 —— 当内存的虚拟地址有一个应的物理地址来存储内存内容时,该内存将被映射。) 基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用: brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。 mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 mun
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值