存储管理(二)

Linux的伙伴算法把所有的空闲页面分为10个块组,每组中块的大小是2次方页面,例如,第0组中块的大小都为20 1个页面),第1组中块的大小为都为212个页面),第9组中块的大小都为29512个页面)。也就是说,每一组中块的大小是相同的,且这同样大小的块形成一个链表。

伙伴算法把满足以上条件的两个块合并为一个块,该算法是迭代算法,如果合并后的块还可以跟相邻的块进行合并,那么该算法就继续合并。

我们通过一个简单的例子来说明该算法的工作原理。

假设要求分配的块其大小128个页面(由多个页面组成的块我们就叫做页面块)。该算法先在块大小为128个页面的链表中查找,看是否有这样一个空闲块。如果有,就直接分配;如果没有,该算法会查找下一个更大的块,具体地说,就是在块大小为256个页面的链表中查找一个空闲块。如果存在这样的空闲块,内核就把这256个页面分为两等份,一份分配出去,另一份插入到块大小为128个页面的链表中。如果在块大小为256个页面的链表中也没有找到空闲页块,就继续找更大的块,即512个页面的块。如果存在这样的块,内核就从512个页面的块中分出128个页面满足请求,然后从384个页面中取出256个页面插入到块大小为256个页面的链表中。然后把剩余的128个页面插入到块大小为128个页面的链表中。如果512个页面的链表中还没有空闲块,该算法就放弃分配,并发出出错信号。

以上过程的逆过程就是块的释放过程,这也是该算法名字的来由。满足以下条件的两个块称为伙伴: 

      两个块的大小相同;

    两个块的物理地址连续。

伙伴算法把满足以上条件的两个块合并为一个块,该算法是迭代算法,如果合并后的块还可以跟相邻的块进行合并,那么该算法就继续合并

 

linux通过伙伴算法管理和分配页,但由于硬件的原因,内存中的不同区域会有不同的特性。主要有以下两个问题:
    一些硬件只能用某些特定的内存地址来执行DMA;
    一些体系结构中有一些内存不能永久映射到内核空间上。
因此某些内存必须从特定区域上分配,不能由单一的伙伴系统管理。为了区分这些内存区域,Linux使用了3个Zone,每个Zone由一个自己的伙伴系统来管理,如下:
    Zone_DMA包含可以用来执行DMA操作的内存;
    Zone_NORMAL包含可以正常映射到虚拟地址的内存区域;
    Zone_HIGHMEM包含不能永久映射到内核地址空间的内存区域。
 
这些区域的划分和具体的体系结构有关,例如,某些体系结构中Zone_NORMAL覆盖了所有的内存区域,而另外两个区均为空。在X86系统中,Zone_DMA为0~16MB的内存范围,Zone_HIGHMEM包含了所有高于896MB的物理内存,Zone_NORMAL覆盖区域部分。
这些区域的划分被分别管理,而在分配时,不同的任务会从不同的区域分配,例如,DMA任务只能从Zone_DMA中分配内存,而普通的内存请求则会依次尝试从Zone_NORMAL、Zone_HIGHMEM和Zone_DMA中分配。在分配器看来,这3个区只是3个不同的内存池对象,用相同的方法即可进行处理。
 
 
获得内存页面
 
系统中对于物理页有大量的需求,当程序映像加载到内存中时,操作系统需要分配页;当程序结束执行并卸载时,操作系统需要释放这些页。另外,为了存放相关的数据结构(如页表自身),也需要物理页。这种用于分配和回收也的机制和数据结构对于维护虚拟内存子系统的效率是非常重要的。
 
系统中的所有物理页都使用page数据结构来描述。每一个屋里也对应一个page变量。一个zone的所有page变量集合形成数组,由zone的zone_mem_map成员指针指向数组的起始地址,页数组的初始化在系统启动时完成。
 
页分配器的算法是在伙伴系统之上的,伙伴系统将内存区域组织为以页为单位的块,n称为该块的“级别”,具有相同级别的块用链表接在一起。每次分配必须指定一个级别,并以该级别块大小为单位。
在分配时,依次从级别n到最大级别开始搜索,直到找到一个非空的块为止。如果这个非空块级别不是n,则将其拆成两份,一份放到其对应的级别的空闲块中,另一份如果还不是级别n就继续拆分,直到最后返回到那个级别为n的块。
在回收时,首先计算处被回收的块的伙伴,然后查看你这个伙伴是否在级别为n的空闲链中。如果找到了,则将这个块与这个伙伴合并(伙伴从空闲链删除,并整“当前块”的位置即可),然后n=n+1,继续这个合并过程。当伙伴不在该空闲链中时,合并过程结束。
 
slab分配器
 
Linux内核中有许多内存动态分配的需求,而其中的对象大小也参差不齐,Linux内核提供了slab层,扮演了通用数据结构缓存层的角色。slab层根据对象的类型来分组不同的Cache,每个Cache存放不同类型的对象,例如,一个Cache被用来存储task_struct,而另一个存放Inode等,这些Cache包含几个slab,而slab由一个活多个物理上连续的page组成。对于一般的数据结构,每个slab只有一个页即可。
 
每个slab都包含一些对象成员,即被管理的数据结构。系统分配对象时就从slab中取得。首先从这个Cache中部分满的slab中分配,如果没有这样的slab,便从空的slab中分配,如果也没有这样的slab,便从空的slab中分配,如果也没有,就创建一个新的slab来分配即可。
 
因为每个slab是包含同一种对象的Cache块,它对对象的分配和释放会变得更为容易和快速。另外由于每个对象在释放时几乎处于分配好并且初始化好的状态,还可以节省不少初始化的时间。例如分配inode变量,首先需要一块malloc(sizeof(inode))大小的内存,然后初始化Inode中的数据成员,在使用完毕后用free释放分配的内存在free之后内存中的内容和刚刚初始化时差不多,例如Inode的引用计数Count一定是降为零。
 
Kernel有许多数据结构都是还原为初始化时的状态后才会free掉,例如一个Mutexlock初始化时和释放时都处于Unlock状态。因此只要在Cache初始化时蛮久将对象置于合法状态,以后每次分配对象的这些field一定是确定的,从而不必重复初始化,可以节省不少开销。Linux的kmem_cache_create中有一个参数ctor初始化函数,可以被用作这一目的,但Linux似乎并没有使用slab的这一特性(因为它有调用ctor函数)。
 
 
kmalloc
 
核心的kmalloc内存分配函数和应用层malloc函数很相似,只是这个函数运行得很快-除非它被阻塞。kmalloc函数不清零它获得的内存空间分配给它的区域仍存放着原有的数据。
 
系统物理内存的管理是由内核负责的,物理内存只能按页大小进行分配。这就需要一个面向页的分配技术以取得计算机内存管理上最大的灵活性。类似malloc函数的简单的线性分配技术不再有效了。在像Linux内核这样的面向页的系统中,如果内存使用线性分配的策略就很难维护。空洞的处理很快就会成为一个问题,会导致内存浪费,降低系统的性能。
 
Linux是通过维护页面池来处理kmalloc的分配要求的,这样页面就可以很容易地放进或取出页面池。为了能够满足超过PAGE_SIZE字节数大小的内存分配请求,mm/slab.c文件维护页面簇的列表。每个页面簇都存放着连续若干页,可用于DMA分配。Linux所使用的分配策略的最终方案是,内核只能分配一些预定义的固定大小的字节数组。如果申请任一大小的内存空间,那么很可能系统会多分配一点。
 
这些预定义的内存大小一般“稍小于2的某次方”(而在更新的实现中系统管理的内存大小恰好为2的某次方)。如果能记住这一点,就可以更有效地使用内存。例如需要一个2000B左右的缓冲区,最好还是申请2000B,而不要申请2048B。申请恰好是2的幂次的内存空间是最糟糕的情况了——内核会分配两倍于申请空间大小的内存。
 
 
高端内存
 
一般情况下,Linux在初始化时总是尽可能地将所有的物理内存映射到内核地址空间中去。如果内核地址空间起始于0xC0000000,为vmalloc保留的虚拟地址空间是128MB,那么最多只能有(1GB-128MB)的物理内存直接映射到内核空间中,内核可以直接访问。如果还有更多的物理内存,就称为高端内存,内核不能直接访问,只能通过修改页表映射后才能进行访问。
 
内存分区可以使内核页分配更加合理。当系统物理内存大于1GB时,内核不能将所有的物理内存都预先映射到内核空间中,这样就产生了高端内存,高端内存最适合于映射到用户进程空间中。预映射的部分可直接用于内核缓冲区,其中有一小块可用与DMA操作的内存,留给DMA操作分配用,一般不会轻易分配。内存分区还可以适应不连续的物理内存分布,是非一致性内存存取体系(NUMA)的基础。
 

硬件已经趋向使用多条系统总线,每条系统总线为一小组处理器提供服务。每组处理器都有自己的内存,并可能有自己的 I/O  通道。但是,每个 CPU 都可以通过一致的方式访问与其他组关联的内存。每个组称为一个“NUMA 节点”。NUMA 节点中的 CPU 数量取决于硬件供应商。访问本地内存比访问与其他 NUMA 节点关联的内存快。这就是“非一致性内存访问体系结构”名称的由来。

在32位机器上,页表通常只可以存储在低端内存中。低端内存只限于物理内存的前896MB,同时还要满足内核其余的大部分要求。在应用程序使用了大量进程并映射了大量内存的情况下,低端内存可能很快就不够用了。在2.6内核中有一个配置选项称为Highmem PTE,让页表条目可以存放在高端内存中,释放出更多的低端内存区域给那些必须放在这里的其他内核数据结构,使用这些页表条目的进程会稍微慢一些。不过,对于那些有大量进程在运行的系统来说,将页表存储到高端内存中可以再低端内存区域挤出更多的内存。
 
 
虚拟内存的申请和释放
 
申请和释放较小且连续的内存空间时,使用kmalloc和kfree函数在物理内存中进行分配,使用较大的内存空间时们可以使用vmalloc函数。由vmalloc函数申请的内存空间在虚拟内存中是连续的,它们映射到物理内存时,可以使用不连续的物理页面,而且仅把当前访问的部分放在物理页面中。
 
vmalloc
 
尽管这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数__get_free_page),内核却认为它们在地址上是连续的。分配的内存空间被映射到内核数据段中,用户空间是不可见的,这一点与其他分配技术不同。vmalloc发生错误时返回0(NULL地址),成功时返回一个指向一个大小为size的线性地址空间的指针。vmalloc函数在核心中所分配的内存由vm_struct结构的链表所支持。
 
与其他内存分配函数不同的是,vmalloc返回很“高”的地址值——这些地址要高于物理内存的顶部。由于vmalloc对页表调整后允许用连续的“高”地址访问分配得到的页面,因此处理器是可以访问返回得到的内存区域的。内核能和其他地址一样使用vmalloc返回的地址,但程序中用到的这个地址与地址总线上的地址并不相同。
用vmalloc分配得到的地址是不能在微处理器之外使用的,因为他们只有在处理器的分页单元上才有意义。当驱动程序需要真正的物理地址时(像外设用于驱动系统总线的DMA地址),这样的地址是不能通过vmalloc函数分配的。正确使用vmalloc函数的场合是为软件分配一大块连续的用于缓冲的内存区域。注意,vmalloc的开销要比__get_free_page大,因为它处理获取内存还要建立页表。因此,不值得用vmalloc函数只分配一页的内存空间。
 
vmalloc分配的内核虚拟内存与kmalloc/__get_free_page分配的内核逻辑内存位于不同的区间,不会重叠。因为内核空间被分区管理,各司其职。用户空间被分配在0~3GB之间,3GB之后紧跟着物理内存映射区间,然后才是vmalloc_start开始的用于vmalloc分配内存的地址空间。
 
使用vmalloc函数的一个例子函数式create_module系统调用,它利用vmalloc函数来获取被创建模块需要的内存空间。而在insmod调用重定位模块代码后,将会调用memcpy_fromfs函数把模块本身复制进分配而得的空间内。
 
用vmalloc分配得到的内存空间用vfree函数来释放,这就像是要用kfree函数来释放kmalloc函数分配得到的内存空间。
 
和vmalloc一样,ioremap也建立新的页表,但和vmalloc不同的是,ioremap实际上并不分配内存,ioremap的返回值是一个虚拟地址,可以用来访问指定的物理内存区域,得到的这个虚拟地址最后要调用vfree来释放掉。
 
ioremap用于将高内存空间的PCI缓冲区映射到用户空间。例如如果VGA设备的帧缓冲区被映射到地址0xf0000000(典型的一个值)后,ioremap就可以建立正确的页表,从而让让让处理器可以访问。而系统初始化时建立的页表只是用于访问低于物理地址空间的内存区域。系统的初始化过程并不检测PCI缓冲区,而是由各个驱动程序自己负责管理自己缓冲区。
 
如果希望驱动程序能在不同的平台间移植,那么使用ioremap时就要小心。在一些平台上是不能直接将PCI内存区域映射到处理器的地址空间的,如在Alpha上就不行。此时就不能像普通内存区域那样对重映射区域进行访问,而应该用reeadb函数或其他一些I/O函数。这些函数可以再不同平台间移植。
 
对vmalloc和ioremap函数可分配的内存空间大小并没有什么限制,但为了能检测到程序员犯的一些错误,vmalloc不允许分配超过物理内存大小的内存空间。但是vmalloc函数请求过多的内存空间会产生一些和调用kmalloc函数时相同的问题。
 
ioremap和vmalloc函数都是面向页的(它们都会修改页表),因此分配或释放的内存空间实际上都会上调为最近的一个页边界。而且,ioremap函数并不考虑如何重映射不是页边界的物理地址。
posted @ 2013-01-06 21:54  雪中飞  阅读(1388)  评论(2编辑  收藏  举报