内存管理子系统是操作系统的重要部分。从计算机发展早期开始,就存在对于大于系统中物理能力的内存需要。为了克服这种限制,开发了许多种策略,其中最成功的就是虚拟内存。虚拟内存通过在竞争进程之间共享内存的方式使系统显得拥有比实际更多的内存。
虚拟内存不仅仅让你的计算机内存显得更多,内存管理子系统还提供:
Large Address Spaces(巨大的地址空间)操作系统使系统显得拥有比实际更大量的内存。虚拟内存可以比系统中的物理内存大许多倍。
Protection(保护)系统中的每一个进程都有自己的虚拟地址空间。这些虚拟的地址空间是相互完全分离的,所以运行一个应用程序的进程不会影响另外的进程。另外,硬件的虚拟内存机制允许对内存区写保护。这可以防止代码和数据被恶意的程序覆盖。
Memory Mapping(内存映射)内存映射用来将映像和数据映射到进程的地址空间。用内存映射,文件的内容被直接连结到进程的虚拟地址空间。
Fair Physics Memory Allocation(公平分配物理内存)内存管理子系统允许系统中每一个运行中的进程公平地共享系统的物理内存。
Shared Virtual Memory(共享虚拟内存)虽然虚拟内存允许进程拥有分离(虚拟)的地址空间,有时你也需要进程之间共享内存。例如,系统中可能有多个进程运行命令解释程序bash。虽然可以在每一个进程的虚拟地址空间都拥有一份bash的拷贝,更好的是在物理内存中只拥有一份拷贝,所有运行bash的进程共享代码。动态连接库是多个进程共享执行代码的另一个常见例子。共享内存也可以用于进程间通讯(IPC)机制,两个或多个进程可以通过共同拥有的内存交换信息。Linux系统支持系统V的共享内存IPC机制。
3.1 An Abstract Model of Virtual Memory(虚拟内存的抽象模型)
在考虑Linux支持虚拟内存的方法之前,最好先考虑一个抽象的模型,以免被太多的细节搞乱。
在进程执行程序的时候,它从内存中读取指令并进行解码。解码指令也许需要读取或者存储内存特定位置的内容,然后进程执行指令并转移到程序中的下一条指令。进程不管是读取指令还是存取数据都要访问内存。
在一个虚拟内存系统中,所有的地址都是虚拟地址而非物理地址。处理器通过操作系统保存的一组信息将虚拟地址转换为物理地址。
为了让这种转换更简单,将虚拟内存和物理内存分为适当大小的块,叫做页(page)。页的大小一样(当然可以不一样,但是这样一来系统管理起来比较困难)。Linux在Alpha AXP系统上使用8K字节的页,而在Intel x86系统上使用4K字节的页。每一页都赋予一个唯一编号:page frame number(PFN 页编号)。在这种分页模型下,虚拟地址由两部分组成:虚拟页号和页内偏移量。假如页大小是4K,则虚拟地址的位11到0包括页内偏移量,位12和以上的位是页编号。每一次处理器遇到虚拟地址,它必须提取出偏移和虚拟页编号。处理器必须将虚拟页编号转换到物理的页,并访问物理页的正确偏移处。为此,处理器使用了页表(page tables)。
图3.1 显示了两个进程的虚拟地址空间,进程X 和进程Y ,每一个进程拥有自己的页表。这些页表将每一个进程的虚拟页映射到内存的物理页上。图中显示进程X 的虚拟页号0 映射到物理页号1 ,而进程Y 的虚拟页编号1 映射到物理页号4 。理论上页表每一个条目包括以下信息: 有效标志表示页表本条目是否有效; 本页表条目描述的物理页编号; 访问控制信息描述本页如何使用:是否可以写?是否包括执行代码?
页表通过虚拟页标号作为偏移来访问。虚拟页编号5 是表中的第6 个元素(0 是第一个元素)。 要将虚拟地址转换到物理地址,处理器首先找出虚拟地址的页编号和页内偏移量。使用2 的幂次的页尺寸,可以用掩码或移位简单地处理。再一次看图3.1 ,假设页大小是0x2000 (十进制8192 ),进程Y 的虚拟地址空间的地址是0x2194 ,处理器将会把地址转换为虚拟页编号1 内的偏移量0x194 。
文章图片
处理器使用虚拟页编号作为索引在进程的页表中找到它的页表的条目。如果该条目有效,处理器从该条目取出物理的页编号。如果本条目无效,就是进程访问了它的虚拟内存中不存在的区域。在这种情况下,处理器无法解释地址,必须将控制权传递给操作系统来处理。
处理器具体如何通知操作系统进程在访问无法转换的无效的虚拟地址,这个方式是和处理器相关的。处理器将这种信息(page fault)进行传递,操作系统得到通知,虚拟地址出错,以及出错的原因。
假设这是一个有效的页表条目,处理器取出物理页号并乘以页大小,得到了物理内存中本页的基础地址。最后,处理器加上它需要的指令或数据的偏移量。
再用上述例子,进程Y的虚拟页编号1映射到了物理页编号4(起始于0x8000 , 4x 0x2000),加上偏移0x194,得到了最终的物理地址0x8194。
通过这种方式将虚拟地址映射到物理地址,虚拟内存可以用任意顺序映射到系统的物理内存中。例如,图3.1 中,虚拟内存X的虚拟页编号映射到了物理页编号1而虚拟页编号7虽然在虚拟内存中比虚拟页0要高,却映射到了物理页编号0。这也演示了虚拟内存的一个有趣的副产品:虚拟内存页不必按指定顺序映射到物理内存中。
3.1.1 Demand Paging
因为物理内存比虚拟内存少得多,操作系统必须避免无效率地使用物理内存。节省物理内存的一种方法是只加载执行程序正在使用的虚拟页。例如:一个数据库程序可能正在数据库上运行一个查询。在这种情况下,并非所有的数据必须放到内存中,而只需要正被检查的数据记录。如果这是个查找型的查询,那么加载程序中增加记录的代码就没什么意义。这种进行访问时才加载虚拟页的技术叫做demand paging。
当一个进程试图访问当前不在内存中的虚拟地址的时候处理器无法找到引用的虚拟页对应的页表条目。例如:图3.1中进程X的页表中没有虚拟页2 的条目,所以如果进程X试图从虚拟页2中的地址读取时,处理器无法将地址转换为物理地址。这时处理器通知操作系统发生page fault。
如果出错的虚拟地址无效意味着进程试图访问它不应该访问的虚拟地址。也许是程序出错,例如向内存中任意地址写。这种情况下,操作系统会中断它,从而保护系统中其他的进程。
如果出错的虚拟地址有效但是它所在的页当前不在内存中,操作系统必须从磁盘映像中将相应的页加载到内存中。相对来讲磁盘存取需要较长时间,所以进程必须等待直到该页被取到内存中。如果当前有其他系统可以运行,操作系统将选择其中一个运行。取到的页被写到一个空闲的页面,并将一个有效的虚拟页条目加到进程的页表中。然后这个进程重新运行发生内存错误的地方的机器指令。这一次虚拟内存存取进行时,处理器能够将虚拟地址转换到物理地址,所以进程得以继续运行。
Linux使用demand paging技术将可执行映像加载到进程的虚拟内存中。当一个命令执行时,包含它的文件被打开,它的内容被映射到进程的虚拟内存中。这个过程是通过修改描述进程内存映射的数据结构来实现,也叫做内存映射(memory mapping)。但是,实际上只有映像的第一部分真正放在了物理内存中。映像的其余部分仍旧在磁盘上。当映像执行时,它产生page fault,Linux使用进程的内存映像表来确定映像的那一部分需要加载到内存中执行。
3.1.2 Swapping(交换)
如果进程需要将虚拟页放到物理内存中而此时已经没有空闲的物理页,操作系统必须废弃物理空间中的另一页,为该页让出空间。
如果物理内存中需要废弃的页来自磁盘上的映像或者数据文件,而且没有被写过所以不需要存储,则该页被废弃。如果进程又需要该页,它可以从映像或数据文件中再次加载到内存中。
但是,如果该页已经被改变,操作系统必须保留它的内容以便以后进行访问。这种也叫做dirty page,当它从物理内存中废弃时,被存到一种叫做交换文件的特殊文件中。因为访问交换文件的速度和访问处理器以及物理内存的速度相比很慢,操作系统必须判断是将数据页写到磁盘上还是将它们保留在内存中以便下次访问。
如果决定哪些页需要废弃或者交换的算法效率不高,则会发生颠簸(thrashing)。这时,页不断地被写到磁盘上,又被读回,操作系统过于繁忙而无法执行实际的工作。例如在图3.1中,如果物理页号1经常被访问,那么就不要将它交换到硬盘上。进程正在使用的也叫做工作集(working set)。有效的交换方案应该保证所有进程的工作集都在物理内存中。
Linux使用LRU(Least Recently Used最近最少使用)的页面技术来公平地选择需要从系统中废弃的页面。这种方案将系统中的每一页都赋予一个年龄,这个年龄在页面存取时改变。页面访问越多,年纪越轻,越少访问,年纪越老越陈旧。陈旧的页面是交换的好候选。
3.1.3 Shared Vitual Memory(共享虚拟内存)
虚拟内存使多个进程可以方便地共享内存。所有的内存访问都是通过页表,每一个进程都有自己的页表。对于两个共享一个物理内存页的进程,这个物理页编号必须出现在两个进程的页表中。
图3.1显示了两个共享物理页号4的进程。对于进程X虚拟页号是4,而对于进程Y虚拟页号是6。这也表明了共享页的一个有趣的地方:共享的物理页不必存在共享它的进程的虚拟内存空间的同一个地方。
3.1.4 Physical and Vitual Addressing Modes(物理和虚拟寻址模式)
对于操作系统本身而言,运行在虚拟内存中没有什么意义。如果操作系统必须维护自身的页表,这将会是一场噩梦。大多数多用途的处理器同时支持物理地址模式和虚拟地址模式。物理寻址模式不需要页表,处理器在这种模式下不需要进行任何地址转换。Linux核心运行在物理地址模式。
Alpha AXP处理器没有特殊的物理寻址模式。它将内存空间分为几个区,将其中两个指定为物理映射地址区。核心的地址空间叫做KSEG地址空间,包括从0xfffffc0000000000向上的所有地址。为了执行连接在KSEG的代码(核心代码)或者访问那里的数据,代码必须在核心态执行。Alpha 上的Linux核心连接到从地址0xfffffc0000310000执行。
文章图片
3.1.5 Access Control(访问控制)
页表条目也包括访问控制信息。当处理器使用页表条目将进程的虚拟地址映射到物理地址的时候,它很容易利用访问控制信息控制进程不要用不允许的方式进行访问。
有很多原因你希望限制对于内存区域的访问。一些内存,比如包含执行代码,本质上是只读的代码,操作系统应该禁止进程写它的执行代码。反过来,包括数据的页可以写,但是如果试图执行这段内存应该失败。大多数处理器有两种执行状态:核心态和用户态。你不希望用户直接执行核心态的代码或者存取核心数据结构,除非处理器运行在核心态。
访问控制信息放在PTE(page table entry)中,而且和具体处理器相关。图3.2显示了Alpha AXP的PTE。各个位意义如下:
V 有效,这个PTE是否有效
FOE “Fault on Execute” 试图执行本页代码时,处理器是否要报告page fault,并将控制权传递给操作系统。
FOW “Fault on Write” 如上,在试图写本页时产生page fault
FOR “fault on read” 如上,在试图读本页时产生page fault
ASM 地址空间匹配。用于操作系统清除转换缓冲区中的部分条目
KRE 核心态的代码可以读本页
URE 用户态的代码可以读本页
GII 间隔因子,用于将一整块映射到一个转换缓冲条目而非多个。
KWE 核心态的代码可以写本页
UWE 用户态的代码可以写本页
Page frame number 对于V位有效的PTE,包括了本PTE的物理页编号;对于无效的PTE,如果不是0,包括了本页是否在交换文件的信息。
以下两位由Linux定义并使用
_PAGE_DIRTY 如果设置,本页需要写到交换文件中。
_PAGE_ACCESSED Linux 使用,标志一页已经访问过
3.2 Caches(高速缓存)
如果你用以上理论模型来实现一个系统,它可以工作,但是不会太高效率。操作系统和处理器的设计师都尽力让系统性能更高。除了使用更快的处理器、内存等,最好的方法是维护有用信息和数据的高速缓存,这会使一些操作更快。Linux使用了一系列和高速缓存相关的内存管理技术:
Buffer Cache: Buffer cache 包含了用于块设备驱动程序的数据缓冲区。这些缓冲区大小固定(例如512字节),包括从块设备读出的数据或者要写到块设备的数据。块设备是只能通过读写固定大小的数据块来访问的设备。所有的硬盘都是块设备。块设备用设备标识符和要访问的数据块编号作为索引,用来快速定位数据块。块设备只能通过buffer cache存取。如果数据可以在buffer cache中找到,那就不需要从物理块设备如硬盘上读取,从而使访问加快。
参见fs/buffer.c
Page Cache 用来加快对磁盘上映像和数据的访问。它用于缓存文件的逻辑内容,一次一页,并通过文件和文件内的偏移来访问。当数据页从磁盘读到内存中时,被缓存到page cache中。
参见mm/filemap.c
Swap Cache 只有改动过的(或脏dirty)页才存在交换文件中。只要它们写到交换文件之后没有再次修改,下一次这些页需要交换出来的时候,就不需要再写到交换文件中,因为该页已经在交换文件中了,直接废弃该页就可以了。在一个交换比较厉害的系统,这会节省许多不必要和高代价的磁盘操作。
参见mm/swap_state.c mm/swapfile.c
文章图片
Hardware Cache: 硬件高速缓存的常见的实现方法是在处理器里面:PTE 的高速缓存。这种情况下,处理器不需要总是直接读页表,而在需要时把页转换表放在缓存区里。 CPU里有转换表缓冲区(TLB Translation Look-aside Buffers),放置了系统中一个或多个进程的页表条目的缓存的拷贝。
当引用虚拟地址时,处理区试图在TLB中寻找。如果找到了,它就直接将虚拟地址转换到物理地址,进而对数据执行正确的操作。如果找不到,它就需要操作系统的帮助。它用信号通知操作系统,发生了TLB missing。一个和系统相关的机制将这个异常转到操作系统相应的代码来处理。操作系统为这个地址映射生成新的TLB条目。当异常清除之后,处理器再次尝试转换虚拟地址,这一次将会成功因为TLB中该地址有了一个有效的条目。
高速缓存的副作用(不管是硬件或其他方式的)在于Linux必须花大量时间和空间来维护这些高速缓存区,如果这些高速缓存区崩溃,系统也会崩溃。
3.3 Linux Page Tables(Linux页表)
Linux假定了三级页表。访问的每一个页表包括了下一级页表的页编号。图3.3显示了一个虚拟地址如何分为一系列字段:每一个字段提供了在一个页表中的偏移量。为了将虚拟地址转换为物理地址,处理器必须取得每一级字段的内容,转换为包括该页表的物理页内的偏移,然后读取下一级页表的页编号。重复三次直到包括虚拟地址的物理地址的页编号找到为止。然后用虚拟地址中的最后一个字段:字节偏移量,在页内查找数据。
Linux运行的每一个平台都必须提供转换宏,让核心处理特定进程的页表。这样,核心不需要知道页表条目的具体结构或者如何组织。通过这种方式,Linux成功地使用了相同的页表处理程序用于Alpha和Intel x86处理器,其中Alpha使用三级页表,而Intel使用二级页表。
参见include/asm/pgtable.h
3.4 Page Allocation and Deallocation (页的分配和回收)
系统中对于物理页有大量的需求。例如,当程序映像加载到内存中的时候,操作系统需要分配页。当程序结束执行并卸载时需要释放这些页。另外为了存放核心相关的数据结构比如页表自身,也需要物理页。这种用于分配和回收页的机制和数据结构对于维护虚拟内存子系统的效率也许是最重要的。
系统中的所有的物理页都使用mem_map数据结构来描述。这是一个mem_map_t结构的链表,在启动时进行初始化。每一个mem_map_t(容易混淆的是这个结构也被称为page 结构)结构描述系统中的一个物理页。重要的字段(至少对于内存管理而言)是:
参见include/linux/mm.h
count 本页用户数目。如果本页由多个进程共享,计数器大于1。
Age 描述本页的年龄。用于决定本页是否可以废弃或交换出去。
Map_nr mem_map_t描述的物理页编号。
页分配代码使用free_area向量来查找空闲的页。整个缓冲管理方案用这种机制来支持。只要用了这种代码,处理器使用的页的大小和物理页的机制就可以无关。
每一个free_area 单元包括页块的信息。数组中的第一个单元描述了单页,下一个是 2页大小的块,下一个是4页大小的块,以此类推,依次向上都是2 的倍数。这个链表单元用作队列的开头,有指向mem_map 数组中页的数据结构的指针。空闲的页块在这里排队。Map 是一个跟踪这么大小的页的分配组的位图。如果页块中的第N 块空闲,则位图中的第N 位置位。
图3.4显示了free_area结构。单元0有一个空闲页(页编号0),单元2有2个4页的空闲块,第一个起始于页编号4,第二个起始于页编号56。
3.4.1 Page Allocation (页分配)
参见mm/page_alloc.c get_free_pages()
Linux使用Buddy算法有效地分配和回收页块。页分配代码试图分配一个由一个或多个物理页组成的块。页分配使用2的幂数大小的块。这意味着可以分配1页大小,2页大小,4页大小的块,依此类推。只要系统有满足需要的足够的空闲页(nr_free_pages > min_free_pages),分配代码就会在free_area中查找满足需要大小的一个页块。Free_area中的每一个单元都有描述自身大小的页块的占用和空闲情况的位图。例如,数组中的第2个单元拥有描述4页大小的块的空闲和占用的分配图。
这个算法首先找它请求大小的内存页块。它跟踪free_area数据结构中的list单元队列中的空闲页的链表。如果请求大小的页块没有空闲,就找下一个尺寸的块(2倍于请求的大小)。继续这一过程一直到遍历了所有的free_area或者找到了空闲页块。如果找到的页块大于请求的页块,则该块将被分开成为合适大小的块。因为所有的块都是2的幂次的页数组成,所以这个分割的过程比较简单,你只需要将它平分就可以了。空闲的块则放到适当的队列,而分配的页块则返回给调用者。
文章图片
例如在图3.4中,如果请求2页的数据块,第一个4页块(起始于页编号4)将会被分为两个2页块。起始于页号4的第一个2页块将会被返回给调用者,而第二个2页块(起始于页号6)将会排在free_area数组中的单元1中2页空闲块的队列中。
3.4.2 Page Deallocation(页回收)
联成大的页块。其实分配页块的过程中将大的页块分为小的页块,将会使内存更为零散。页回收的代码只要可能就把页页块的大小很重要(2的幂数),因为这样才能很容易将页块组成大的页块。
只要一个页块回收,就检查它的相邻或一起的同样大小的页块是否空闲。如果是这样,就把它和新释放的页块一起组成以一个新的下一个大小的空闲页块。每一次两个内存页块组合成为更大的页块时,页回收代码都要试图将页块合并成为更大的块。这样,空闲的页块就会尽可能的大。
例如,在图3.4,如果页号1释放,那么它会和已经空闲的页号0一起组合并放在free_area的单元1中空闲的2页块队列中。
3.5 Memory Mapping (内存映射)
当一个映像执行时,执行映像的内容必须放在进程的虚拟地址空间中。对于执行映像连接到的任意共享库,情况也是一样。执行文件实际并没有放到物理内存,而只是被连接到进程的虚拟内存。这样,只要运行程序引用了映像的部分,这部分映像就从执行文件中加载到内存中。这种映像和进程虚拟地址空间的连接叫做内存映射。
每一个进程的虚拟内存用一个mm_struct 数据结构表示。这包括当前执行的映像的信息(例如bash)和指向一组vm_area_struct结构的指针。每一个vm_area_struct的数据结构都描述了内存区域的起始、进程对于内存区域的访问权限和对于这段内存的操作。这些操作是一组例程,Linux用于管理这段虚拟内存。例如其中一种虚拟内存操作就是当进程试图访问这段虚拟内存时发现(通过page fault)内存不在物理内存中所必须执行的正确操作,这个操作叫做 nopage 操作。Linux请求把执行映像的页加载到内存中的时候用到nopage操作。
当一个执行映像映射到进程的虚拟地址空间时,产生一组vm_area_struct数据结构。每一个vm_area_struct结构表示执行映像的一部分:执行代码、初始化数据(变量)、未初始化数据等等。Linux支持一系列标准的虚拟内存操作,当vm_area_struct数据结构创建时,一组正确的虚拟内存操作就和它们关联在一起。
【【Linux】Linux Kernel--Memory Management(一)】
推荐阅读
- 服务器|一文玩转 Docker
- 笔记|共享内存 设计原理-shm
- 服务器|闲置安卓手机改造linux服务器
- Linux/Android——input子系统核心
- Linux/Android——input_handler之evdev
- Linux/Android——输入子系统input_event传递
- Linux/Android——usb触摸屏驱动 - usbtouchscreen
- Linux/Android多点触摸协议
- linux硬核知识点