Linux 内存管理模型很是直接明了,由于 Linux 的这种机制使其具备可移植性而且可以在内存管理单元相差不大的机器下实现 Linux,下面咱们就来认识一下 Linux 内存管理是如何实现的。算法
每一个 Linux 进程都会有地址空间,这些地址空间由三个段区域组成:text 段、data 段、stack 段。下面是进程地址空间的示例。
数据段(data segment) 包含了程序的变量、字符串、数组和其余数据的存储。数据段分为两部分,已经初始化的数据和还没有初始化的数据。其中还没有初始化的数据就是咱们说的 BSS。数据段部分的初始化须要编译就期肯定的常量以及程序启动就须要一个初始值的变量。全部 BSS 部分中的变量在加载后被初始化为 0 。shell
和 代码段(Text segment) 不同,data segment 数据段能够改变。程序老是修改它的变量。并且,许多程序须要在执行时动态分配空间。Linux 容许数据段随着内存的分配和回收从而增大或者减少。为了分配内存,程序能够增长数据段的大小。在 C 语言中有一套标准库 malloc 常常用于分配内存。进程地址空间描述符包含动态分配的内存区域称为 堆(heap)。数据库
第三部分段是 栈段(stack segment)。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展。举个例子来讲,在 32 位 x86 架构的机器上,栈开始于 0xC0000000,这是用户模式下进程容许可见的 3GB 虚拟地址限制。若是栈一直增大到超过栈段后,就会发生硬件故障并把页面降低一个页面。数组
当程序启动时,栈区域并非空的,相反,它会包含全部的 shell 环境变量以及为了调用它而向 shell 输入的命令行。举个例子,当你输入缓存
cp cxuan lx1
时,cp 程序会运行并在栈中带着字符串 cp cxuan lx ,这样就可以找出源文件和目标文件的名称。数据结构
当两个用户运行在相同程序中,例如编辑器(editor),那么就会在内存中保持编辑器程序代码的两个副本,可是这种方式并不高效。Linux 系统支持共享文本段做为替代。下面图中咱们会看到 A 和 B 两个进程,它们有着相同的文本区域。
数据段和栈段只有在 fork 以后才会共享,共享也是共享未修改过的页面。若是任何一个都须要变大可是没有相邻空间容纳的话,也不会有问题,由于相邻的虚拟页面没必要映射到相邻的物理页面上。多线程
除了动态分配更多的内存,Linux 中的进程能够经过内存映射文件来访问文件数据。这个特性可使咱们把一个文件映射到进程空间的一部分而该文件就能够像位于内存中的字节数组同样被读写。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多。共享库的访问就是使用了这种机制。以下所示
咱们能够看到两个相同文件会被映射到相同的物理地址上,可是它们属于不一样的地址空间。架构
映射文件的优势是,两个或多个进程能够同时映射到同一文件中,任意一个进程对文件的写操做对其余文件可见。经过使用映射临时文件的方式,能够为多线程共享内存提供高带宽,临时文件在进程退出后消失。可是实际上,并无两个相同的地址空间,由于每一个进程维护的打开文件和信号不一样。框架
下面咱们探讨一下关于内存管理的系统调用方式。事实上,POSIX 并无给内存管理指定任何的系统调用。然而,Linux 却有本身的内存系统调用,主要系统调用以下
若是遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其余标志位,fd 是文件描述符,offset 是文件偏移量。编辑器
brk 经过给出超过数据段以外的第一个字节地址来指定数据段的大小。若是新的值要比原来的大,那么数据区会变得愈来愈大,反之会愈来愈小。
mmap 和 unmap 系统调用会控制映射文件。mmp 的第一个参数 addr 决定了文件映射的地址。它必须是页面大小的倍数。若是参数是 0,系统会分配地址并返回 a。第二个参数是长度,它告诉了须要映射多少字节。它也是页面大小的倍数。prot 决定了映射文件的保护位,保护位能够标记为 可读、可写、可执行或者这些的结合。第四个参数 flags 可以控制文件是私有的仍是可读的以及 addr 是必须的仍是只是进行提示。第五个参数 fd 是要映射的文件描述符。只有打开的文件是能够被映射的,所以若是想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从何时开始,并不必定每次都要从零开始。
内存管理系统是操做系统最重要的部分之一。从计算机早期开始,咱们实际使用的内存都要比系统中实际存在的内存多。内存分配策略克服了这一限制,而且其中最有名的就是 虚拟内存(virtual memory)。经过在多个竞争的进程之间共享虚拟内存,虚拟内存得以让系统有更多的内存。虚拟内存子系统主要包括下面这些概念。
大地址空间
操做系统使系统使用起来好像比实际的物理内存要大不少,那是由于虚拟内存要比物理内存大不少倍。
保护
系统中的每一个进程都会有本身的虚拟地址空间。这些虚拟地址空间彼此彻底分开,所以运行一个应用程序的进程不会影响另外一个。而且,硬件虚拟内存机制容许内存保护关键内存区域。
内存映射
内存映射用来向进程地址空间映射图像和数据文件。在内存映射中,文件的内容直接映射到进程的虚拟空间中。
公平的物理内存分配
内存管理子系统容许系统中的每一个正在运行的进程公平分配系统的物理内存。
共享虚拟内存
尽管虚拟内存让进程有本身的内存空间,可是有的时候你是须要共享内存的。例如几个进程同时在 shell 中运行,这会涉及到 IPC 的进程间通讯问题,这个时候你须要的是共享内存来进行信息传递而不是经过拷贝每一个进程的副本独立运行。
下面咱们就正式探讨一下什么是 虚拟内存
虚拟内存的抽象模型
在考虑 Linux 用于支持虚拟内存的方法以前,考虑一个不会被太多细节困扰的抽象模型是颇有用的。
处理器在执行指令时,会从内存中读取指令并将其解码(decode),在指令解码时会获取某个位置的内容并将他存到内存中。而后处理器继续执行下一条指令。这样,处理器老是在访问存储器以获取指令和存储数据。
在虚拟内存系统中,全部的地址空间都是虚拟的而不是物理的。可是实际存储和提取指令的是物理地址,因此须要让处理器根据操做系统维护的一张表将虚拟地址转换为物理地址。
为了简单的完成转换,虚拟地址和物理地址会被分为固定大小的块,称为 页(page)。这些页有相同大小,若是页面大小不同的话,那么操做系统将很难管理。Alpha AXP系统上的 Linux 使用 8 KB 页面,而 Intel x86 系统上的 Linux 使用 4 KB 页面。每一个页面都有一个惟一的编号,即页面框架号(PFN)。
上面就是 Linux 内存映射模型了,在这个页模型中,虚拟地址由两部分组成:偏移量和虚拟页框号。每次处理器遇到虚拟地址时都会提取偏移量和虚拟页框号。处理器必须将虚拟页框号转换为物理页号,而后以正确的偏移量的位置访问物理页。
上图中展现了两个进程 A 和 B 的虚拟地址空间,每一个进程都有本身的页表。这些页表将进程中的虚拟页映射到内存中的物理页中。页表中每一项均包含
有效标志(valid flag): 代表此页表条目是否有效
该条目描述的物理页框号
访问控制信息,页面使用方式,是否可写以及是否能够执行代码
要将处理器的虚拟地址映射为内存的物理地址,首先须要计算虚拟地址的页框号和偏移量。页面大小为 2 的次幂,能够经过移位完成操做。
若是当前进程尝试访问虚拟地址,可是访问不到的话,这种状况称为 缺页异常,此时虚拟操做系统的错误地址和页面错误的缘由将通知操做系统。
经过以这种方式将虚拟地址映射到物理地址,虚拟内存能够以任何顺序映射到系统的物理页面。
按需分页
因为物理内存要比虚拟内存少不少,所以操做系统须要注意尽可能避免直接使用低效的物理内存。节省物理内存的一种方式是仅加载执行程序当前使用的页面(这未尝不是一种懒加载的思想呢?)。例如,能够运行数据库来查询数据库,在这种状况下,不是全部的数据都装入内存,只装载须要检查的数据。这种仅仅在须要时才将虚拟页面加载进内中的技术称为按需分页。
交换
若是某个进程须要将虚拟页面传入内存,可是此时没有可用的物理页面,那么操做系统必须丢弃物理内存中的另外一个页面来为该页面腾出空间。
若是页面已经修改过,那么操做系统必须保留该页面的内容,以便之后能够访问它。这种类型的页面被称为脏页,当将其从内存中移除时,它会保存在称为交换文件的特殊文件中。相对于处理器和物理内存的速度,对交换文件的访问很是慢,而且操做系统须要兼顾将页面写到磁盘的以及将它们保留在内存中以便再次使用。
Linux 使用最近最少使用(LRU)页面老化技术来公平的选择可能会从系统中删除的页面,这个方案涉及系统中的每一个页面,页面的年龄随着访问次数的变化而变化,若是某个页面访问次数多,那么该页就表示越 年轻,若是某个呃页面访问次数太少,那么该页越容易被换出。
物理和虚拟寻址模式
大多数多功能处理器都支持 物理地址模式和虚拟地址模式的概念。物理寻址模式不须要页表,而且处理器不会在此模式下尝试执行任何地址转换。 Linux 内核被连接在物理地址空间中运行。
Alpha AXP 处理器没有物理寻址模式。相反,它将内存空间划分为几个区域,并将其中两个指定为物理映射的地址。此内核地址空间称为 KSEG 地址空间,它包含从 0xfffffc0000000000 向上的全部地址。为了从 KSEG 中连接的代码(按照定义,内核代码)执行或访问其中的数据,该代码必须在内核模式下执行。连接到 Alpha 上的 Linux内核以从地址 0xfffffc0000310000 执行。
访问控制
页面表的每一项还包含访问控制信息,访问控制信息主要检查进程是否应该访问内存。
必要时须要对内存进行访问限制。 例如包含可执行代码的内存,天然是只读内存; 操做系统不该容许进程经过其可执行代码写入数据。 相比之下,包含数据的页面能够被写入,可是尝试执行该内存的指令将失败。 大多数处理器至少具备两种执行模式:内核态和用户态。 你不但愿访问用户执行内核代码或内核数据结构,除非处理器之内核模式运行。
访问控制信息被保存在上面的 Page Table Entry ,页表项中,上面这幅图是 Alpha AXP的 PTE。位字段具备如下含义
V
表示 valid ,是否有效位
FOR
读取时故障,在尝试读取此页面时出现故障
FOW
写入时错误,在尝试写入时发生错误
FOE
执行时发生错误,在尝试执行此页面中的指令时,处理器都会报告页面错误并将控制权传递给操做系统,
ASM
地址空间匹配,当操做系统但愿清除转换缓冲区中的某些条目时,将使用此选项。
GH
当在使用单个转换缓冲区条目而不是多个转换缓冲区条目映射整个块时使用的提示。
KRE
内核模式运行下的代码能够读取页面
URE
用户模式下的代码能够读取页面
KWE
之内核模式运行的代码能够写入页面
UWE
以用户模式运行的代码能够写入页面
页框号
对于设置了 V 位的 PTE,此字段包含此 PTE 的物理页面帧号(页面帧号)。对于无效的 PTE,若是此字段不为零,则包含有关页面在交换文件中的位置的信息。
除此以外,Linux 还使用了两个位
PAGE_DIRTY
若是已设置,则须要将页面写出到交换文件中
PAGE_ACCESSED
Linux 用来将页面标记为已访问。
上面的虚拟内存抽象模型能够用来实施,可是效率不会过高。操做系统和处理器设计人员都尝试提升性能。 可是除了提升处理器,内存等的速度以外,最好的方法就是维护有用信息和数据的高速缓存,从而使某些操做更快。在 Linux 中,使用不少和内存管理有关的缓冲区,使用缓冲区来提升效率。
缓冲区缓存
缓冲区高速缓存包含块设备驱动程序使用的数据缓冲区。
还记得什么是块设备么?这里回顾下
块设备是一个能存储固定大小块信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每一个块都有本身的物理地址。一般块的大小在 512 - 65536 之间。全部传输的信息都会以连续的块为单位。块设备的基本特征是每一个块都较为对立,可以独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘
与字符设备相比,块设备一般须要较少的引脚。
缓冲区高速缓存经过设备标识符和块编号用于快速查找数据块。 若是能够在缓冲区高速缓存中找到数据,则无需从物理块设备中读取数据,这种访问方式要快得多。
页缓存
页缓存用于加快对磁盘上图像和数据的访问
它用于一次一页地缓存文件中的内容,而且能够经过文件和文件中的偏移量进行访问。当页面从磁盘读入内存时,它们被缓存在页面缓存中。
交换区缓存
仅仅已修改(脏页)被保存在交换文件中
只要这些页面在写入交换文件后没有修改,则下次交换该页面时,无需将其写入交换文件,由于该页面已在交换文件中。 能够直接丢弃。 在大量交换的系统中,这节省了许多没必要要的和昂贵的磁盘操做。
硬件缓存
处理器中一般使用一种硬件缓存。页表条目的缓存。在这种状况下,处理器并不老是直接读取页表,而是根据须要缓存页的翻译。 这些是转换后备缓冲区 也被称为 TLB,包含来自系统中一个或多个进程的页表项的缓存副本。
引用虚拟地址后,处理器将尝试查找匹配的 TLB 条目。 若是找到,则能够将虚拟地址直接转换为物理地址,并对数据执行正确的操做。 若是处理器找不到匹配的 TLB 条目, 它经过向操做系统发信号通知已发生 TLB 丢失得到操做系统的支持和帮助。系统特定的机制用于将该异常传递给能够修复问题的操做系统代码。 操做系统为地址映射生成一个新的 TLB 条目。 清除异常后,处理器将再次尝试转换虚拟地址。此次可以执行成功。
使用缓存也存在缺点,为了节省精力,Linux 必须使用更多的时间和空间来维护这些缓存,而且若是缓存损坏,系统将会崩溃。
Linux 页表
Linux 假定页表分为三个级别。访问的每一个页表都包含下一级页表
图中的 PDG 表示全局页表,当建立一个新的进程时,都要为新进程建立一个新的页面目录,即 PGD。
要将虚拟地址转换为物理地址,处理器必须获取每一个级别字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。这样重复三次,直到找到包含虚拟地址的物理页面的页框号为止。
Linux 运行的每一个平台都必须提供翻译宏,这些宏容许内核遍历特定进程的页表。这样,内核无需知道页表条目的格式或它们的排列方式。
页分配和取消分配
对系统中物理页面有不少需求。例如,当图像加载到内存中时,操做系统须要分配页面。
系统中全部物理页面均由 mem_map 数据结构描述,这个数据结构是 mem_map_t 的列表。它包括一些重要的属性
count :这是页面的用户数计数,当页面在多个进程之间共享时,计数大于 1
age:这是描述页面的年龄,用于肯定页面是否适合丢弃或交换
map_nr :这是此mem_map_t描述的物理页框号。
页面分配代码使用 free_area向量查找和释放页面,free_area 的每一个元素都包含有关页面块的信息。
页面分配
Linux 的页面分配使用一种著名的伙伴算法来进行页面的分配和取消分配。页面以 2 的幂为单位进行块分配。这就意味着它能够分配 1页、2 页、4页等等,只要系统中有足够可用的页面来知足需求就能够。判断的标准是nr_free_pages> min_free_pages,若是知足,就会在 free_area 中搜索所需大小的页面块完成分配。free_area 的每一个元素都有该大小的块的已分配页面和空闲页面块的映射。
分配算法会搜索请求大小的页面块。若是没有任何请求大小的页面块可用的话,会搜寻一个是请求大小二倍的页面块,而后重复,直到一直搜寻完 free_area 找到一个页面块为止。若是找到的页面块要比请求的页面块大,就会对找到的页面块进行细分,直到找到合适的大小块为止。
由于每一个块都是 2 的次幂,因此拆分过程很容易,由于你只需将块分红两半便可。空闲块在适当的队列中排队,分配的页面块返回给调用者。
若是请求一个 2 个页的块,则 4 页的第一个块(从第 4 页的框架开始)将被分红两个 2 页的块。第一个页面(从第 4 页的帧开始)将做为分配的页面返回给调用方,第二个块(从第 6 页的页面开始)将做为 2 页的空闲块排队到 free_area 数组的元素 1 上。
页面取消分配
上面的这种内存方式最形成一种后果,那就是内存的碎片化,会将较大的空闲页面分红较小的页面。页面解除分配代码会尽量将页面从新组合成为更大的空闲块。每释放一个页面,都会检查相同大小的相邻的块,以查看是否空闲。若是是,则将其与新释放的页面块组合以造成下一个页面大小块的新的自由页面块。 每次将两个页面块从新组合为更大的空闲页面块时,页面释放代码就会尝试将该页面块从新组合为更大的空闲页面。 经过这种方式,可用页面的块将尽量多地使用内存。
例如上图,若是要释放第 1 页的页面,则将其与已经空闲的第 0 页页面框架组合在一块儿,并做为大小为 2页的空闲块排队到 free_area 的元素 1 中
内存映射
内核有两种类型的内存映射:共享型(shared) 和私有型(private)。私有型是当进程为了只读文件,而不写文件时使用,这时,私有映射更加高效。 可是,任何对私有映射页的写操做都会致使内核中止映射该文件中的页。因此,写操做既不会改变磁盘上的文件,对访问该文件的其它进程也是不可见的。
按需分页
一旦可执行映像被内存映射到虚拟内存后,它就能够被执行了。由于只将映像的开头部分物理的拉入到内存中,所以它将很快访问物理内存还没有存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操做系统会报告这项错误。
页面错误描述页面出错的虚拟地址和引发的内存访问(RAM)类型。
Linux 必须找到表明发生页面错误的内存区域的 vm_area_struct 结构。因为搜索 vm_area_struct 数据结构对于有效处理页面错误相当重要,所以它们以 AVL(Adelson-Velskii和Landis)树结构连接在一块儿。若是引发故障的虚拟地址没有 vm_area_struct 结构,则此进程已经访问了非法地址,Linux 会向进程发出 SIGSEGV 信号,若是进程没有用于该信号的处理程序,那么进程将会终止。
而后,Linux 会针对此虚拟内存区域所容许的访问类型,检查发生的页面错误类型。 若是该进程以非法方式访问内存,例如写入仅容许读的区域,则还会发出内存访问错误信号。
如今,Linux 已肯定页面错误是合法的,所以必须对其进行处理。