7.内存管理 2010-01-15 17:10 216人阅读 评论(0) 收藏
前面提到,0xc0000000以上空间给内核用,其中又有末尾128MB固定它用。之前提到的前8MB映射完成后,pagetable_init()函数会建立全局页目录的表项,完成最终内核页表。若RAM小于896MB时,地址空间0xc0000000以上有1G,送去最后的128MB剩下896MB,足以对物理RAM寻址;RAM大小在896~4096MB之间时,内核只能根据页表寻址到其中的896MB,初始化阶段就将896MB的一个窗口线性地映射到内核地址空间。若内核要寻址其它的RAM,要进行动态重映射(马上说)。在这两种情况下,内核都是线性映射的,即直接减法。而那剩下的128M保留空间用于非连续内存分配与固定映射。固定映射是指每个固定的线性地址不要一定要对应它减去0xc0000000,而可以任意建立。不论如何RAM某些部分已永久给内核,剩下的叫动态内存。
再回顾硬件高速缓存。80x86引入了“行”的单位,以脉冲突发模式在DRAM与SRAM间传送数据以实现调整缓存。直接映射时,主存的一行就是cache中相同位置的一行,充分关联时,主存中一行可存于cache中任意一行,N路关联时指主存中一行可以存在cache中N行中任一行。Cache控制器的表项数组中的表项有一标签,通过这若干位的标签识别物理内存单元。物理地址的高几位用于标签,中间用作cache控制器的子集索引,低几位用于提出行内偏移量。访问一RAM时,CPU从物理地址中取出中间的子集索引,把Cache控制器对应子集的每表项的行标签与物理地址高几位比较,若相等,说明缓存命中。原理上,就是从DRAM地址中根据规则提出信息,根据信息去SRAM中找数据,免得每次对DRAM操作慢。
为管理内存,内核必须知道每个页框的状态。内核用数据结构“页描述符”page来表示,它们在mem_map数组中。每个描述符32字节,用宏将页框号与描述符关联。对内存的访问有非一致访问和一致访问,它们是按cpu访问不同内存是否花时间不同决定。若是非一致的情况,则将物理内存分为若干节点,节点范围内部访问时间相同。每节点在内核中有对应的数据结构pg_data_t描述符。但它们不以数组组织,以链表串起来。虽然80x86体系结构使用一致访问,但Linux还是使用节点,不过仅有唯一的一个节点。这样移植性好一点。
概念上,内存的任意页框可存放任意数据页,但80x86体系有如下约束:1.ISA总线的DMA处理器只可对RAM前16MB寻址(因为它I/O总线只有24位);2.大容量RAM中,4G的线性空间使得不可直接访问所有RAM,为解决这问题,Linux将节点内物理内存再分为3个管理区。80x86一致访问中管理区为:ZONE_DMA,它包括低16MBRAM;ZONE_HIGHMEM,大于896MB的页框;ZONE_NORMAL,包括中间的页框。这三个管理区有自己的内存管理区描述符。至此,有节点描述符、管理区描述符、页描述符三种数据结构从大到小来表示物理内存。
请求内存时,如果有空闲,就立即分配,若没有,则要回收一些。回收意味着将当前的内核控制路径阻塞,直到有内存被释放。但前面说过,即使抢占式内核,软硬中断是不可阻塞的。可用原子操作来解决分配,若不成功则不阻塞,直接返回失败。老失败也不行,内核为原子分配请求保留了一个页框区域,在内存不足时使用。大小一般>128&&<65535。ZONE_DMA与ZONE_NORMAL区各将一定数量页框献出来作保留页框。对于每个内存管理区,有三种主要的内存分配形式:
一、请求连续的页框:
它由内核子系统“分区页框分配器”处理。其中的“管理区分配器”完成动态内存的分配与释放。请求页框有适当的函数与宏,它们的参数可指出搜索管理区的顺序。请求成功则返回对应的线性地址,问题来了,对ZONE_HIGHMEM的请求成功如何返回呢?线性地址映射前896M用完了,如果成功又不可通过线性地址来引用它,就意味着内存丢了。解决方法是不返回线性地址,返回所得的内存第一页框的页描述符地址。但你拿到了它的描述符地址仍不可对它访问,因为没有方法取得它的物理地址,物理地址只能从页表中查,这就要用到前面所说的“最后128M线性地址”,它有一部分用于暂时映射高端内存的页框。内核可能采用三种不同机制:永久映射,建立高端页框到内核地址空间的长期映射,使用主内核页表中的专门页表项,引入专门的数据结构来描述它的页描述符(返回)、物理地址(页表)、线性地址(高128MB)的关系。分配成功后返回的是页描述符的指针,根据它到一个hash表中,找出最终的映射结果,即线性地址。如果其它情况的映射,但在高端内存已无空页时会引起阻塞,所以在软硬中断中不可行。临时映射,不会要求阻塞,相反它要求不能阻塞,因为怕其它内核控制路径用同一窗口来映射其它高端内存。不论哪种技术都不可同时对整个RAM寻址。对于临时内核映射,高端内存任一页框可以通过一个“窗口”(为此保留的一个页表项)映射到内核地址空间。每个CPU都有自己的13个窗口集合。
解决了怎么分配,接下来考虑分配策略。由于分配连续的页框,若策略不好会产生大量外碎片。解决外碎片的常见两种思路是将不连续的空页框映射到连续的线性地址或允许一种技术尽量提高分配效率。考虑到如老式DMA等服务需要连续页框且连续的页框可提高牛快表特性,一般都会第二种方法。Linux用伙伴系统来解决外碎片。它将所有空闲页框分为11个块链表,每个链表串起1、2、4……1024个连续的页框。分配时,从小到大扫描,如果需要大小的链表中空了,那么找上一个,将它划出一半,一半返回,另一半插入原来的空链表。这两半就是“伙伴”的含义。因为最大的1024个页框充满了一个页表(1024个项),所以再大就不合适了,此时还未找到则返回出错。每个内存管理区有各自的伙伴系统。C++中STL的vector内存分配策略与此类似。至此连续页框的请求处理、页框组织、申请释放已大致了解。
所有内核申请页框操作的最上层是“管理区分配器”,它负责保护保留的页框池,内存不足且允许阻塞时引发页框回收算法,并尽可能保存小而珍贵的DMA区。
二、分配连续内存块但非页框
如果给它简单分整个页框,显然浪费。但伙伴系统只作用于页框,这时对于几十个字节的一请求怎么办?必须引入额外的数据结构描述页框内部的内存使用情况。这样不可避免会引入内碎片。典型解决方法也是早期Linux使用的是返回内存大小是2的幂次,这样内碎片可控制在50%内,再用一个链表串起每页框内的空闲内存区。
伙伴系统在此时不可用,Sun的Solaris2.4使用了slab分配器模式,它将内存区看作对象(object),有构造析构。Slab分配器不丢弃已分配对象,而是释放之让其保存在内存中,需要时就不用对它再次初始化。内核运行中要高频地建立释放如进程描述符这样的内存区,slab分配器把那个页框存在高速缓存中。
Slab分配器将对象分组放入调整缓存,包含调整缓存的主存分为多个slab,每slab由一个或多个连续页框组成,slab内有多个对象,包括已分配的,也有空闲的。相关的数据结构为高速缓存描述符、slab描述符,每个对象也有描述符,但它是一个简单的无符号整数,放在slab中slab描述符之后(如果slab描述符本身存在slab中)、对象之前。Slab着色用于改善高速缓存使用性能。
三、通过连续线性地址访问非连续页框
虽然这样访问高速缓存利用率低,但也有好处,如避免了外碎片。它必须打乱页表,可用于请求不频繁的情况。回忆0xc0000000开始的1G线性地址的分布。一开始是用于前896M的映射,最后是固定映射与永久内核映射的线性地址。它们中间的可用于非连续内存。在物理映射末与第一内存之间加入8MB的安全区用于捕获越界访问。同样,在区与区之间插入4K大小安全隔离带。非连续内存区也有对应的描述符。分配时先分描述符,找线性地址区间。之后再分页框,将分得的不连续页框的页描述符存在area->pages数组中,最后再更新内核使用的页表项。
总之,内核以相当直接的方法获取动态内存。__get_free_pages()或alloc_pages()从分区页框分配器中获得连续页框;kmem_cache_alloc()或kmalloc()用slab分配器为专用或通用对象分配块;vmalloc()或vmalloc_32()获得非连续内存区。可使用如此直接的步骤是因为内核是OS中优先级最高的部分。若某内核函数请求动态内存,不会推迟,且内核函数被认为是正确的,这就不用出错处理了。
版权声明:本文为博主原创文章,未经博主允许不得转载。