内存管理机制负责对内存架构进行管理,使程序在内存架构的任何一个层次上的存放对于用户来说都是一样的。用户无须担心自己的程序是存储在缓存、主存、磁盘还是磁带上,反正运行、计算、输出的结果都一样。而内存管理实现这种媒介透明的手段就是虚拟内存。用我们多次讲过的“抽象”来说,虚拟内存就是操作系统提供给用户的另一个“幻象”。这个幻象构建在内存架构的顶端,给用户提供一个比物理主存空间大许多的地址空间。
虚拟内存系统负责为程序提供一个巨大的、稀疏的、私有的地址空间的假象,其中保存了程序的所有指令和数据。操作系统在专门硬件的帮助下,通过每一个虚拟内存的索引,将其转换为物理地址,物理内存根据获得的物理地址去获取所需的信息。操作系统会同时对许多进程执行此操作,并且确保程序之间互相不会受到影响,也不会影响操作系统。整个方法需要大量的机制(很多底层机制)和一些关键的策略。
常用概念:
- 页框:内存中固定长度的块
- 页:固定长度的数据块,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。
- 段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合)
内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有:
- 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。
- 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。
- 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
- 存储保护:保证各道作业在各自的存储空间内运行,互不干扰。
进程对应的内存空间中所包含的6种不同的数据区:
-
代码段(code segment):用来存放程序运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。
-
数据段(data segment): 存储初始化的全局变量和初始化的static变量。数据段中的数据的生存期是随程序持续性(随进程持续性):进程创建就存在,进程死亡就消失。
-
BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0.这里很奇怪,一般的书上都会说全局变量和静态变量是会自动初始化的,怎么突然来了个未初始化的变量?其实变量的初始化可以分为显式初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话也会被初始化,那就是不管什么类型都初始化为默认值,这种没有显示初始化的就是这里所说的未初始化。例如整数型的全局变量未初始化的默认隐式初始化的值为0,都是0就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)。BSS的全称是Block Started by Symbol,它属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存能在运行时分配并被有效的清零。BSS节在应用程序的二进制映像文件中并不存在,既不占用磁盘空间,而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。
-
rodata段(read only data):常量区,存放只读数据。比如程序中定义为const的全局变量,“Hello Word”的字符串常量。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。在有的嵌入式系统中,rodata放在ROM中,运行时直接读取,不须加载到RAM内存中。所以在嵌入式开发中,常将已知的常量系数,表格数据等加以const关键字。存放在ROM中,避免占用RAM空间。
-
堆(heap): 其中所有的申请和释放操作都由程序员显式地完成。 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
-
栈(stack): 它的申请和释放操作是编译器来隐式管理的,所以有时也称为自动(automatic)内存。 栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束就自动回收空间。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。
所谓虚拟存储技术是指:当进程运行时,先将其一部分装入内存,另一部分暂留在磁盘,当要执行的指令或访问的数据不在内存时,由操作系统自动完成将它们从磁盘调入内存的工作。 虚拟地址空间即为分配给进程的虚拟内存。 虚拟地址是在虚拟内存中指令或数据的位置,该位置可以被访问,仿佛它是内存的一部分。
在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch时,之前内存里的内容怎么办?简单粗暴的方式就是先dump到磁盘上,然后再从磁盘上restore之前dump的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。
操作系统需要提供一个易用(easy to use)的物理内存抽象。这个抽象叫作地址空间(address space),是运行的程序看到的系统中的内存。 一个进程的地址空间包含运行的程序的所有内存状态。比如:程序的代码(code,指令)必须在内存中,因此它们在地址空间里。当程序在运行的时候,利用栈(stack)来保存当前的函数调用信息,分配空间给局部变量,传递参数和函数返回值。最后,堆(heap)用于管理动态分配的、用户管理的内存,就像你从C语言中调用malloc()或面向对象语言(如C ++或Java)中调用new 获得内存。当然,还有其他的东西(例如,静态初始化的变量),但现在假设只有这3个部分:代码、栈和堆。
一个16KB的地址空间可能长这样:
Stack和Heap中间有一块free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。
Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。
在讨论进程空间细节前,这里先要澄清下面几个问题:
-
4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
-
用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。
-
每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。
大多数内存管理方案都假定操作系统占据内存中的某些固定部分,而内存中的其余部分则供多个用户进程使用。管理用户内存空间的最简单的方案就是对它分区,以形成若干边界固定的区域。
固定分区分为两种:
-
使用大小相等的分区:此时小于等于分区大小的任何进程都可以装入任何可用的分区中。若所有分区都已满且没有进程处于就绪态或运行态,则操作系统可以换出一个进程的所有分区,并装入另一个进程,使得处理器有事可做。但是大小相等的分区有两个难点:
-
程序可能太大而不能放到一个分区中。此时程序员必须使用覆盖技术设计程序,使得任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。
-
内存的利用率非常低。任何程序,即使很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。
-
-
使用大小不等的分区:大小不等的分区可以缓解上面的两个问题,但是不能完全解决。
虽然大小不等的分区带来了一定的灵活性,但是分区的数量在系统生成阶段已经确定,因而限制了系统中活动(未挂起)进程的数量。而且由于分区大小是在系统生成阶段事先设置的,因而小作业不能有效的利用分区空间。
为了克服固定分区的确定,提出了动态分区。对于动态分区,分区长度和数量是可变的。程序装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。但是对于一个64MB的内存,如果装入前三个进程已经占用了很大一部分,剩下的部分对于第四个进程来说又太小,这样在内存的末尾就剩下一个“空洞”。而等第二个进程结束后腾出的足够的空间来装入第四个进程,但是由于第四个进程比第二个进程小,所有这里又形成了另一个小"空洞"。这样最终在内存中就会形成许多小空洞。随着时间的推移,内存中形成了越来越多的碎片,内存的利用率随之下降。这种现象称为外部碎片(external fragmentation),指在所有分区外的存储空间变成了越来越多的碎片,这与前面所讲的内部碎片正好对应。
克服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。
固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 它是一种经典的内存分配方案,是一种特殊的“分离适配”算法。主要思想是将内存按2的幂进行划分,组成若干空闲块链表,查找该链表找到能满足进程需求的最佳匹配块。
算法:
- 首先将整个可用空间看做一块:2的幂,这里假设一共有1M的内存。
- 假设进程申请的空间大小为s,这里假设为100k,如果满足2的u-1次幂 < s <= 2的u次幂,则分配整个块,否则,将块划分为两个大小相等的伙伴,大小为2的u-1次幂,那这两个大小相等的伙伴就是伙伴关系。等内存回收后,具有伙伴关系的两块还可以合并到一起,组成一个更大的内存块。
- 1M内存分为两块。
- 512k、512k仍然大于100k,将第一块再分为两块。
- 256k、256k、512k,而第一块256k仍然大于100k,第一块256k再分配。
- 128k、128k、256k、512k。这个时候64k < 100k < 128k,所以把第一个128k分配给当前申请100k内存的进程。
- 等该进程的内存回收后,前两个128k的内存具有伙伴关系,还可以合并成256k。合并后和后面的256k又是一个伙伴,又合并成512k,发现和后面的512k又是一个伙伴,就又合并成了1M的内存块。
在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的某个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型:
- 逻辑地址(logical address)是指与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。
- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。用户程序经过编译、汇编后形成目标代码,目标代码通常采用相对地址的形式,其首地址为0,其余地址相对于首地址而编址。
- 物理地址(physical address)或绝对地址是数据在内存中的实际位置。
系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。通常情况下,被加载进程中所有内存访问都相对于程序的开始点。因此,在执行包括这类访问的指令时,需要有把相对地址转换为物理内存地址的硬件机制。这类地址转换就需要一个特殊的处理器寄存器(基址寄存器),其内容是程序在内存中的起始地址。还有一个界限寄存器指明程序的终止位置。
将逻辑地址转换为物理地址的操作就叫做重定位。而把地址进行转换的功能部件就叫做内存管理单元(MMU, Memory Management Unit)。
如果要使多个应用程序同时运行在内存中,必须要解决两个问题:保护
和 重定位
。第一种解决方式是用保护密钥标记内存块
,并将执行过程的密钥与提取的每个存储字的密钥进行比较。这种方式只能解决第一种问题(破坏操作系统),但是不能解决多进程在内存中同时运行的问题。
还有一种更好的方式是创造一个存储器抽象:地址空间(the address space)
。就像进程的概念创建了一种抽象的 CPU 来运行程序,地址空间也创建了一种抽象内存供程序使用。
在多道编程环境下,由于程序加载到内存的地址不是固定的(有多个地方可加载),我们必须对地址进行翻译。那么如何来翻译呢?我们看到,一个程序是加载到内存里事先划分好的某片区域,而且该程序是整个加载进去。该程序里面的虚地址只要加上其所占区域的起始地址即可获得物理地址。因此,我们的翻译过程非常简单:物理地址=虚拟地址+程序所在区域的起始地址
程序所在区域的起始地址称为(程序)基址。另外,由于有多个程序在内存空间中,我们需要进行地址保护。由于每个程序占用连续的一片内存空间,因此只要其访问的地址不超出该片连续空间,则为合法访问。因此,地址保护也变得非常简单,只要访问的地址满足下列条件即为合法访问:
程序所在区域的起始地址≤有效地址≤程序所在区域的起始地址+程序长度
由此可见,我们只需要设置两个端值:基址和变址,即可达到地址翻译和地址保护的目的。 这两个端值可以由两个寄存器来存放,分别称为基址寄存器和变址寄存器。在固定分区下,基址就是固定内存分区中各个区域的起始内存地址,而变址则是所加载程序的长度(记住,不是内存各个分区的上限)。
这样,每次程序发出的地址需要和极限比较大小;如果大于极限,则属非法访问。在这个时候我们将陷入内核,终止进程(在个别操作系统上,也可能进入一个异常处理的过程);否则将基址加上偏移获得物理地址,就可以合法访问这个物理地址。
动态重定位(dynamic relocation)
技术,它就是通过一种简单的方式将每个进程的地址空间映射到物理内存的不同区域。
- 基址寄存器:存储数据内存的起始位置
- 变址寄存器:存储应用程序的长度。
每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将基址值
添加到进程生成的地址中,然后再将其发送到内存总线上。同时,它检查程序提供的地址是否大于或等于变址寄存器
中的值。如果程序提供的地址要超过变址寄存器的范围,那么会产生错误并中止访问。
在动态重定位的过程中,只有很少的硬件参与,但获得了很好的效果。 一个基址寄存器将虚拟地址转换为物理地址。 一个界限寄存器确保这个地址在进程地址空间的范围内。 它们一起提供了既简单又高效的虚拟内存机制。这种基址寄存器配合界限寄存器的硬件结构是芯片中的(每个CPU一对)。有时我们将CPU的这个负责地址转换的部分统称为内存管理单元(Memory Management Unit,MMU)。
大小不等的固定分区和大小可变的分区技术在内存的使用上都是低效的,前者会产生内部碎片,后者会产生外部碎片。但是,如果内存被划分成大小固定、相等的块,切块相对比较小,每个进程也被分成同样大小的小块,那么进程中称为页的块可以分配到内存中称为页框的可用块。这样在使用分页技术时,每个进程在内存中浪费的空间,仅仅是进程最后一页的一小部分形成的内部碎片。没有任何外部碎片。
分页系统的核心就是将虚拟内存空间和物理内存空间皆划分为大小相同的页面,如4KB、8KB或16KB等,并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。这样,由于物理空间是页面的整数倍,并且空间分配以页面为单位,将不会再产生外部碎片。同时,由于一个虚拟页面可以存放在任何一个物理页面里,空间增长也容易解决:只需要分配额外的虚拟页面,并找到一个闲置的物理页面存放即可。在分页系统下,一个程序发出的虚拟地址由两部分组成:页面号和页内偏移值。
这样操作系统需要为每个进程维护一个页表(page table)。页表给出了该进程的每页所对应页框的位置。在程序中每个逻辑地址包括一个页号和该页中的偏移量。在分页中,逻辑地址到物理地址的转换仍然由处理器硬件完成,给出逻辑地址(页号,偏移量)后,处理器使用页表产生物理地址(页框号,偏移量)。
采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区不需要是连续的。
为了使分页方案更加方便,规定页和页框的大小必须是2的幂,以便容易的表示出相对地址。
从上面的分析可以看出,分页系统要能够工作的前提是:对于任何一个虚拟页面,系统知道该页面是否在物理内存中,如果在的话,其对应的物理页面是哪个;如果不在的话,则产生一个系统中断(缺页中断),并将该虚页从磁盘转到内存,然后将分配给它的物理页面号返回。也就是说,页面管理系统要能够将虚拟地址转换为物理地址。
因此,分页系统的核心是页面的翻译,即从虚拟页面到物理页面的映射。而这个翻译过程由内存管理单元(MMU)完成。MMU接收CPU发出的虚拟地址,将其翻译为物理地址后发送给内存。内存单元按照该物理地址进行相应访问后读出或写入相关数据
内存管理单元对虚拟地址的翻译只是对页面号的翻译,即将虚拟页面号翻译成物理页面号。而对于偏移值,则不进行任何操作。这是因为虚拟页表和物理页表大小完全一样,虚拟页面里的偏移值和物理页面里的偏移值完全一样,因此无须翻译。那么内存管理单元是通过什么手段完成这种翻译的呢?当然是查页表。对于每个程序,内存管理单元都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。每当为一个虚拟页面寻找到一个物理页面后,就在页表里面增加一个记录来保留该虚拟页面到物理页面的映射关系。随着虚拟页面进出物理内存,页表的内容页不断发生变化。
在程序发出一个虚拟地址给内存管理单元后,内存管理单元首先将地址里面页号部分的字位分离出来,然后判断该虚拟页面是否有效,是否存放在内存,是否受到保护。如果页面无效,即该虚拟页面不存在或没有在内存,也就是说该虚拟页面在物理内存里面没有对应。如果该页面受到保护,即对该页面的访问被禁止,则产生一个系统中断来处理这些特殊情况。对于无效页面访问,需要终止发出该无效访问的进程。对于合法但不在物理内存中的页面,我们通过缺页中断将该虚拟页面放进物理内存。对于受保护的页面,同样终止该进程。
页表在分页内存管理系统的地位十分关键。页表的根本功能是提供从虚拟页面到物理页面的映射。因此,页表的记录条数与虚拟页面数相同。
内存管理单元依赖页表来进行一切与页面有关的管理活动。这些活动包括判断某一页面号是否在内存里,页面是否受到保护,页面是否非法空间等。因此,页表除了提供虚拟页面到物理页面的映射外,还记录这些相关信息。
分页系统不会产生外部碎片,一个进程占用的内存空间可以不是连续的,并且一个进程的虚拟页面在不需要的时候可以放在磁盘上。这样,在分页系统下,进程空间的增长和虚拟内存的实现都解决了。除此之外,分页系统的另一个优点是可以共享小的地址,即页面共享。我们只需要在对应给定页面的页表项里做一个相关的记录即可。当然,分页系统也存在缺点。第一个缺点就是页表很大,占用了大量的内存空间。例如,对于32位寻址、页面尺寸为4KB的分页系统来说,页表将有1048576个记录。每个记录又要占用多个字节,这样,一个页表所占的内存空间相当大。那么有没有办法减少页表的尺寸呢?可以选择多级页表的方式。既然一个程序可以分解为一个个页面,并将部分页面存放在磁盘上而降低其内存占用空间,页表也可以采用同样的方式处理,即将页表分为一个个页面,不需要的页面也放在磁盘上。内存里只存放需要的页面。
在多级页表结构下,页表根据存放的内容可分为:顶级页表、一级页表、二级页表、三级页表等。顶级页表里面存放的是一级页表的信息,一级页表里面存放的是二级页表的信息,以此类推,到最后一级页表存放的才是虚拟页面到物理页面的映射。一个程序在运行时其顶级页表常驻内存,而次级页表则按需要决定是否存放在物理内存。
例如,如果使用两层页表的话,虚拟地址的前10位可作为顶级页表的索引,中间10位可作为次级页表的索引,最后12位可作为页内偏移值。这样,当CPU发出一个虚拟地址时,我们将虚拟地址一分为三,用最前面10位的值找到顶级页表的对应记录,得到所需要的次级页表。用中间10位的值作为索引在刚才获得次级页表里找到对应的记录,得到对应的物理页面号。然后将物理页面号与页内偏移值链接起来获得最后的物理地址。
多级页表为什么占用的内存空间少呢?因为大部分次级页表会放到磁盘上,而放在内存里面的页表较少。因此,内存占用少。多级页表有什么缺点呢?它降低了系统的速度。因此每次内存访问都变成多次内存访问。对于二级页表,一次内存访问变成了三次内存访问。如果次级页表不在内存,还需要加上一次磁盘访问。这样,系统的速度将大为下降。对于级数更多的页表来说,内存访问速度额下降将更加明显。
地址翻译因增加了内存访问次数而降低了系统效率。如果只使用单级页表,则每次内存访问变为两次内存访问,速度的下降还尚可以忍受。但如果使用多级页表或反转页表,则每次内存访问将变为多于两次的内存访问,这样效率的下降将非常明显。由于内存访问是每条指令都需要执行的操作,这样将造成整个系统效率的下降。那有没有什么办法改善翻译的速度呢?有。因为程序的运行呈现所谓的时空局域性,即在一段时间内,程序所要访问的地址空间有一定的空间局域性。如果一个页面被访问,则该页面的其他地址可能也将被随后访问。这样,我们可以将该页面的翻译结果存放在缓存里,而无须在访问该页面的每个地址时再翻译一次。这样就可以大大提高系统的执行效率。
这种存放翻译结果的缓存称为翻译快表(Translation Look-Aside Buffer,TLB)。TLB里面存放的是从虚拟页面到物理页面的映射,其记录的格式与内容和正常页表的记录格式与内容一样。这样,在进行地址翻译时,如果TLB里有该虚拟地址记录,即“命中”,就从TLB获得其对应的物理页面号,而无须经过多级页表或反转页表查找,从而大大提高翻译速度。如果TLB里面没有该虚拟页面号,即“未命中”,则需要按正常方式读取页表内容。在TLB未命中的情况下,我们既可以将页表相应记录存入TLB,然后再由TLB来满足地址翻译需求(即重新启动指令),也可以直接由页表来满足翻译请求,同时将翻译结果存入TLB。这两种方式提供的抽象是不一样的。前者提供的抽象是所有翻译皆由TLB完成,而后者提供的抽象则是翻译过程既看到TLB,又看到正常页表。方式的差异将影响模块化的设计思路。
TLB通常由CPU制造商提供,但TLB的更换算法则有可能由操作系统提供。
分页内存管理它克服了交换系统的所有缺点,但它自己有什么缺点吗?页表太大?这个缺点用多级页表克服了。多级页表速度慢?这个问题用TLB解决了绝大部分。页面来回更换?这个缺点用页面更换算法解决了大部分。固定页面大小呢?这不应该算是一个缺点,因为可变页面大小的操作系统不仅难以选择最优的页面大小,而且会变得很复杂。内部碎片算是一个小小的缺憾,但总比交换系统的外部碎片强,一个进程的内部碎片所浪费的空间平均起来只有半个页面,相对于分页系统的诸多优点来说,这个缺点似乎微不足道。那么分页系统还有其他缺陷吗?有。其中的一个是共享困难。虽然在理论上可以按页进行共享,似乎粒度很细,但实际上这根本就是不现实的。原因是一个页面的内容很可能既包括代码又包括数据,即很难使一个页面只包含需要共享的内容或不需要共享的内容。只要一个页面里面有一行地址是不能共享的,这个页面就不能共享。而一个页面里面存在至少一行不能共享的地址是完全可能的。这样,想自由地共享任何内容几乎就变得不可能了。
如果说上述缺点尚可以容忍,但有一个缺点却是无法容忍的,同时也是分页系统无法解决的。这个缺点就是一个进程只能占有一个虚拟地址空间。在此种限制下,一个程序的大小至多只能和虚拟空间一样大,其所有内容都必须从这个共同的虚拟空间内分配。
我们来看一个例子:编译器的工作过程。我们知道编译器在工作时需要保持多个数据结构:词法分析树、常数表、代码段、符合表、调用栈。保持多个数据结构本身并无任何问题。问题出在这些数据结构可以独立增长和收缩。即在编译器扫描过程中,这些数据结构里面数据可以增多或减少,从而造成该数据结构所需内存空间的变化。
如果编译器只在一个虚拟空间活动,则所有的数据结构或表格均在一个虚拟空间分配,这样就必然发生不同的数据结构占据虚拟空间的不同部分的情况。这样,当一个数据结构空间需要增长时,就会碰到处于其上的其他数据结构而造成无法增长。那么如果某个数据结构无法增长,例如词法树所占空间无法增长,则编译过程将失败。
当然,编译器在工作时会为每个数据结构多分配一些空间来容纳其随后的增长。而且这些空间的大小都经过经验数据测试,可以应对大多数情况下数据结构增长的需要。但是,谁也不能保障不会碰到一个很大的程序,以致某个数据结构的增长空间不够的情况。例如,如果一个程序里定义的变量奇多无比,则符号表所占的空间将大幅增加。
那么怎么解决这个问题呢?记住,这里的碰撞可是在虚拟空间中的碰撞,而不是物理空间的碰撞。解决的办法自然是增加虚拟空间的容量。但是虚拟空间的大小受寻址宽度的限制而无法增长。剩下的办法就只能让一个程序使用多个虚拟空间!由于分页系统只使用一个虚拟地址空间,自然解决不了这个问题。那么要解决这个问题显然需要一种新的内存管理模式。这种新的模式就是分段管理系统。
分段管理就是将一个程序按照逻辑单元分成多个程序段,每一个段使用自己单独的虚地址空间。例如,对于编译器来说,我们可以给其5个段,占用5个虚地址空间。
这样,一个段占用一个虚地址空间,就不会再发生空间增长时碰撞到另一个段的问题。从而避免因空间不够而造成编译失败的情况。如果某个数据结构对空间的需求超过整个虚地址能够提供的空间,则编译仍将失败。不过出现这种可能的概率恐怕不会比太阳从西边出来的概率高出多少。
一个程序占据多个虚拟地址空间,那么不同的段有可能有同样的虚拟地址空间。如果要区分一个虚拟地址所在的段,我们必须在虚拟地址前面冠以一个前缀,即该地址所在的段号。也就是说,在分段情况下,一个虚拟地址将由段号和段内偏差两个部分构成,
采用分段技术,可以把用户进程地址空间按程序的自身的逻辑关系和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。同样内存空间也会被动态的划分为若干长度不相同的取悦,称为物理段,每个物理段由起始地址和长度确定。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 以段为单位进行分配,每段在内存中占据连续空间,但各段之间可以不相邻。
由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。
采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再是简单的对应关系。类似于分页,在简单的分段方案中,每个进程都有一个段表,系统也会维护一个内存中的空闲块列表。每个段表都必须给出相应段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。
如果说分页存储器管理方式引入的目的是提高内存的利用率,那么分段存储器管理方式的引入便是为了方便用户的使用。引入分段存储器管理方式主要是为了满足用户方便编程、分段共享和分段保护等要求。
分段存储管理方式要求每个程序的地址空间按照自身的逻辑关系划分成若干段,比如主程序段、子程序段、数据段、堆栈段等,每个段都有自己的名字。为了简单,通常可用一个段号来代替段名,每个段都从0开始独立编址,段内地址连续。段的长度由相应的逻辑信息组的长度决定,因而各段的长度不等。分配内存时,为每个段分配一连续的存储空间,段间地址空间可以不连续。
分页和分段的两个特点:
- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时把动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。
- 一个进程可划分为许多块(页和段),在执行过程中,这些块不需要连续的位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能。
由于上面这两个特点的存在,那么在一个进程的执行过程中,该进程不需要所有页或段都在内存中。如果内存中保存有待取的下一条指令所在块(段或页)及待访问的下一个数据单元所在块,那么执行至少可以暂时继续下去。我们用术语“块”来表示页或段。处理器在需要访问一个不在内存中的逻辑地址时,会产生一个中断,这表明出现了内存访问故障。操作系统会把被中断的进程置于堵塞态。要继续执行这个进程,操作系统必须把包含引发访问故障的逻辑地址的进程块读入内存。为此操作系统产生一个磁盘I/O读请求。产生I/O请求后,在执行磁盘I/O期间,操作系统可以调度另一个进程运行。在需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统则把由于缺少该块而被堵塞的进程置为就绪态。这样的话就会有两种提高系统利用率的方法:
- 在内存中保留多个进程。由于对任何特定的进程都仅装入它的某些块,因此有足够的空间来放置更多的进程。这样,在任何时刻这些进程中至少有一个处于就绪态,于是处理器就得到了更有效的利用。
- 进程可以比内存的全部空间还大。程序占用的内存空间的大小是程序设计的最大限制之一。没有这种方案时,程序员必须清楚的知道有多少内存空间可用。若编写的程序太大,程序员就必须设计出能把程序分成块的方法,这些块可按某种覆盖策略分别加载。通过基于分页或分段的虚拟内存,这项工作可由操作系统和硬件完成。对程序员而言,他所处理的是一个巨大的内存,大小与磁盘存储器有关。操作系统在需要时会自动的把进程块装入内存。
由于进程只能在内存中执行,因此这个存储器成为实存储器(real memory),简称实存。但程序员或用户感觉到的是一个更大的内存,且通常分配在磁盘上,这称为虚拟内存(virtual memory),简称虚存。虚存支持更有效的系统并发度,并能解除用户与内存之间没有必要的紧密约束。
逻辑分段的优点十分明显。首先,每个逻辑单元可单独占一个虚拟地址空间,这样使得编写程序的空间大为增长,几乎可以编写出没有尺寸限制的程序来。其次,由于段是按逻辑关系而分,共享起来就非常方便。不会出现分页系统下一个页面里面可能同时包含数据和代码而造成共享不便的问题。由于不同的逻辑段使用不同的基址与极限对,我们可以对不同的段采用不同的保护措施。最后,对于空间稀疏的程序来说,分段管理将节省大量的空间。因为空余的部分不用分配任何虚拟空间;而在分页下,空余的空间仍然需要分配虚拟页面,只不过该页面非法而已。而在上下文切换时要更换的内容很简单:段表。因为段表很小,这种切换非常容易。
分段管理的缺点也十分明显。既然是分段,就存在前面基本内存管理时介绍过的缺点:外部碎片和一个段必须全部加载到内存。一般来说,一个程序的所有逻辑段是同时需要的。如果实行分段管理,则由于每个段必须全部加载到内存,这就造成了我们在基本内存管理时已经遇到过的一个问题:一个程序必须同时全部加载到内存才能执行。当然,解决这个问题可以使用重叠(overlay)。但我们已经尝过分页系统好处后再使用overlay显然有“优汰劣胜”的味道。
那么我们的解决办法是什么呢?分页。但这次的分页不是前面的直接对程序进程分页,而是对程序里面的段进行分页。这就形成了所谓的段页式内存管理模式。这种段内分页是前面讲过的页式内存管理的否定之否定。
段页式管理就是将程序分为多个逻辑段,在每个段里面又进行分页,即将分段和分页组合起来使用。这样做的目的就是想同时获得分段和分页的好处,但又避免了单独分段或单独分页的缺陷。如果我们将每个段看做一个单独的程序,则逻辑分段就相当于同时加载多个程序。
综合页式、段式方案的优点,克服两者的缺点。用户进程先按段划分,每一段再按页面划分。内存分配还是以页为单位进行分配。
每个进程访问自己的私有虚拟地址空间(virtual address space)(有时称为地址空间,address space),操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响到其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。但实际情况是,物理内存是由操作系统管理的共享资源。
面对越来越大的程序,常常产生程序>内存的问题,为解决这种问题,虚拟内存的概念得到普及.
虚拟内存是一种内存分配方案,是一项可以用来辅助内存分配的机制。我们知道,应用程序是按页装载进内存中的。但并不是所有的页都会装载到内存中,计算机中的硬件和软件会将数据从 RAM 临时传输到磁盘中来弥补内存的不足。如果没有虚拟内存的话,一旦你将计算机内存填满后,计算机会对你说呃,不,对不起,您无法再加载任何应用程序,请关闭另一个应用程序以加载新的应用程序。对于虚拟内存,计算机可以执行操作是查看内存中最近未使用过的区域,然后将其复制到硬盘上。虚拟内存通过复制技术实现了 妹子,你快来看哥哥能装这么多程序 的资本。复制是自动进行的,你无法感知到它的存在。
要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。
虚拟内存机制使得期望运行大于物理内存的程序成为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。
虚拟内存的中心思想是将物理主存扩大到便宜、大容量的磁盘上,即将磁盘空间看做主存空间的一部分。用户程序存放在磁盘上就相当于存放在主存内。用户程序既可以完全存放在主存,也可以完全存放在磁盘上,当然也可以部分存放在主存、部分存放在磁盘。而在程序执行时,程序发出的地址到底是在主存还是在磁盘则由操作系统的内存管理模块负责判断,并到相应的地方进行读写操作。事实上,我们可以更进一步,将缓存和磁带也包括进来,构成一个效率、价格、容量错落有致的存储架构。即虚拟内存要提供的就是一个空间像磁盘那样大、速度像缓存那样高的主存储系统
虚拟内存的基本思想是:每个程序都拥有自己的地址空间,这个空间被分割成多个块,每个块被成为一页或页面.
程序运行时,并不是所有页都在物理内存中:
- 当程序引用一部分在物理内存的地址空间时,由硬件直接执行必要的映射;
- 当程序引用一部分不在物理内存的地址空间时,由操作系统将缺失的页装入物理内存,并重新运行
虚拟内存基于分段和分页这两种基本技术或基于这两种技术中的一种。虚拟内存机制允许程序以逻辑方式访问存储器,而不考虑物理内存上可用的空间数量。虚存的构想是为了满足有多个用户作业同时驻留在内存中的要求,因此在一个进程被写出到副主存储器中且后续进程被读入时,连续的进程执行之间将不会脱节。进程大小不同时,若处理器在很多进程间切换,则很难把它们紧密的压入内存,因此人们引入了分页系统。在分页系统中,进程由许多固定大小的块组成。这些块成为页。程序通过虚地址(virtual address)访问,虚地址由页号和页中的偏移量组成。进程的每页都可置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址(real address)或物理地址之间的动态映射。
- CPU中包含MMU内存管理单元,用于管理虚拟地址空间到物理内存地址的映射.
- 假设物理内存地址大小为32k,每4k为一个页框.虚拟地址空间分页,每个页面大小等于一个页框
- 当程序想要访问一个虚拟地址x
- 指令将x送到MMU
- MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中
- 若在,MMU将x转化为物理内存地址y
- 若不在,则进行缺页中断,操作系统在物理内存中找到一个使用较少的页面回收掉,将需要访问的页面读到被回收的页面处,再将x转化为物理内存地址访问
读取策略决定某页何时取入内存,常用的方法有如下两种:
-
请求分页
只有当访问到某页中的一个单元时才将该页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。
-
预先分页
对于预先分页,读取的页并不是缺页中断请求的页。预先分页利用了大多数辅存设备(如磁盘)的特性,这些设备有寻道时间和合理的延迟。若一个进程的页连续存储在辅存中,则一次读取许多连续的页要比隔一段时间读取一页有效。当然,若大多数额外读取的页未引用到,则这个策略是低效的。进程首次启动时,可采用预先分页策略,此时程序员须以某种方式制定需要的页。
放置策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。
如果访问的页面不在内存中,则系统将产生缺页中断。缺页中断服务程序将负责把位于磁盘上的数据加载到物理内存来。如果物理内存还有空闲页面,那就直接使用空闲的页面。但如果物理内存已满,则需要挑选某个已经使用过的页面进行替换。那么挑选哪个页面合适呢?当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。所有策略的目标都是移出最近最不能访问的页。
最优算法
在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用
。然而,它可以作为衡量其他算法的标准。NRU
算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。FIFO
会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。第二次机会
算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。时钟
算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。LRU
算法是一个非常优秀的算法,但是没有特殊的硬件(TLB)
很难实现。如果没有硬件,就不能使用 LRU 算法。NFU
算法是一种近似于 LRU 的算法,它的性能不是非常好。老化
算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择- 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。
WSClock
是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。
总之,「最好的算法是老化算法和WSClock算法」。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。
在更换页面时,如果更换的页面是一个很快就会被再次访问的页面,则在此次缺页中断后很快又会发生新的缺页中断。在最坏情况下,每次新的访问都是对一个不在内存的页面进行访问,即每次内存访问都产生一次缺页中断,这样每次内存访问皆变成一次磁盘访问,而由于磁盘访问速度比内存可以慢一百万倍,因此整个系统的效率急剧下降。这种现象就称为内存抖动,或者抽打、抽筋(tras-hing)。
对于分页式虚拟内存,在准备执行时,不需要也不可能把一个进程的所有页都读入内存。因此,操作系统必须决定读取多少页,即决定给特定的进程分配多大的内存空间。
与读取策略相反,清除策略用于确定何时将已修改的一页写回辅存。通常有两种选择:
-
请求式清除
只有当一页被选择用于置换时才能被写回辅存。
-
预约式清除
将这些已修改的多页在需要使用他们所占据的页框之前成批写回辅存。
加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间花费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。
进程通过一个系统调用(mmap)将一个文件(或部分)映射到其虚拟地址空间的一部分,访问这个文件就像访问内存中的一个大数组,而不是对文件进行读写。
在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时,页面才会被每次一页的读入(虚拟内存的缺页处理),磁盘文件则被当做后备存储。
当进程退出或显式地解除文件映射时,所有被修改页面会写回文件。
在运行一个C程序的时候,会分配两种类型的内存。第一种称为栈内存,它的申请和释放操作是编译器来隐式管理的,所以有时也称为自动(automatic)内存。
C中申请栈内存很容易。比如,假设需要在func()函数中为一个整形变量x申请空间。为了声明这样的一块内存,只需要这样做:
void func() {
int x: // declares an integer on the stack
}
编译器完成剩下的事情,确保在你进入 func() 函数的时候,在栈上开辟空间。当你从该函数退出时,编译器释放内存。因此,如果你希望某些信息存在于函数调用之外,建议不要将它们放在栈上。
就是这种对长期内存的需求,所以我们才需要第二种类型的内存,即所谓的堆(heap)内存,其中所有的申请和释放操作都由程序员显式地完成。毫无疑问,这是一项非常艰巨的任务!这确实导致了很多缺陷。但如果小心并加以注意,就会正确地使用这些接口,没有太多的麻烦。下面的例子展示了如何在堆上分配一个整数,得到指向它的指针:
void func() {
int *x = (int *)malloc(sizeof(int));
}
关于这一小段代码有两点说明。首先,你可能会注意到栈和堆的分配都发生在这一行:首先编译器看到指针的声明(int * x)时,知道为一个整型指针分配空间,随后,当程序调用malloc()时,它会在堆上请求整数的空间,函数返回这样一个整数的地址(成功时,失败时则返回NULL),然后将其存储在栈中以供程序使用。因为它的显式特性,以及它更富于变化的用法,堆内存对用户和系统提出了更大的挑战。
忘记释放内存另一个常见错误称为内存泄露(memory leak),如果忘记释放内存,就会发生。在长时间运行的应用程序或系统(如操作系统本身)中,这是一个巨大的问题,因为缓慢泄露的内存会导致内存不足,此时需要重新启动。因此,一般来说,当你用完一段内存时,应该确保释放它。请注意,使用垃圾收集语言在这里没有什么帮助:如果你仍然拥有对某块内存的引用,那么垃圾收集器就不会释放它,因此即使在较现代的语言中,内存泄露仍然是一个问题。在某些情况下,不调用free()似乎是合理的。例如,你的程序运行时间很短,很快就会退出。在这种情况下,当进程死亡时,操作系统将清理其分配的所有页面,因此不会发生内存泄露。虽然这肯定“有效”(请参阅后面的补充),但这可能是一个坏习惯,所以请谨慎选择这样的策略。长远来看,作为程序员的目标之一是养成良好的习惯。其中一个习惯是理解如何管理内存,并在C这样的语言中,释放分配的内存块。即使你不这样做也可以逃脱惩罚,建议还是养成习惯,释放显式分配的每个字节。
***补充:***为什么在你的进程退出时没有内存泄露当你编写一个短时间运行的程序时,可能会使用malloc()分配一些空间。程序运行并即将完成:是否需要在退出前调用几次free()?虽然不释放似乎不对,但在真正的意义上,没有任何内存会“丢失”。原因很简单:系统中实际存在两级内存管理。第一级是由操作系统执行的内存管理,操作系统在进程运行时将内存交给进程,并在进程退出(或以其他方式结束)时将其回收。第二级管理在每个进程中,例如在调用malloc()和free()时,在堆内管理。即使你没有调用free()(并因此泄露了堆中的内存),操作系统也会在程序结束运行时,收回进程的所有内存(包括用于代码、栈,以及相关堆的内存页)。无论地址空间中堆的状态如何,操作系统都会在进程终止时收回所有这些页面,从而确保即使没有释放内存,也不会丢失内存。因此,对于短时间运行的程序,泄露内存通常不会导致任何操作问题(尽管它可能被认为是不好的形式)。如果你编写一个长期运行的服务器(例如Web服务器或数据库管理系统,它永远不会退出),泄露内存就是很大的问题,最终会导致应用程序在内存不足时崩溃。当然,在某个程序内部泄露内存是一个更大的问题:操作系统本身。这再次向我们展示:编写内核代码的人,工作是辛苦的...
当内存严重不足的时候,页分配器在多次尝试直接页回收失败以后,就会调用内存耗尽杀手(OOM killer, OOM是“Out of Memory”的缩写),选择进程杀死,释放内存。
Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下:
-
ASHMem(Anonymous Shared Memory):这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。
-
Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。
-
Low Memory Killer:andorid基于OOM Killer原理所扩展的一个多层次OOM Killer。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOM kille会选择性杀掉进程,到OOM的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOM Killer,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。这些都是由活动管理器来负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供者,据此可判断该进程的重要程度。
Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定其oom_adj。/proc//oom_adj:
- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill
- 该设置参数的存在是为了和旧版本的内核兼容
让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。
Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用物理内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。
- 邮箱 :[email protected]
- Good Luck!