Linux系统开发专栏

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

      内存管理,不用多说,言简意赅。在内核里分配内存还真不是件容易的事情,根本上是因为内核不能想用户空间那样奢侈的使用内存。

      先来说说内存管理。内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常是字,但是,内存管理单元MMU通常以页为单位进行处理。因此,从虚拟内存的交代来看,页就是最小单位。内核用struct  page(linux/mm.h)结构表示系统中的每个物理页:

struct page {
         unsigned long flags;                                                      
         atomic_t count;                
         unsigned int mapcount;          
         unsigned long private;          
         struct address_space *mapping;  
         pgoff_t index;                  
         struct list_head lru;  
	 union{
	 	struct pte_chain;
		pte_addr_t;
	 }         
         void *virtual;                  
};

      flag用来存放页的状态,每一位代表一种状态,所以至少可以同时表示出32中不同的状态,这些状态定义在linux/page-flags.h中。count记录了该页被引用了多少次。mapping指向与该页相关的address_space对象。virtual是页的虚拟地址,它就是页在虚拟内存中的地址。要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述是短暂的。内核仅仅用这个结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。

      在linux中,内核也不是对所有的也都一视同仁,内核而是把页分为不同的区,使用区来对具有相似特性的页进行分组。Linux必须处理如下两种硬件存在缺陷而引起的内存寻址问题:

1.一些硬件只能用某些特定的内存地址来执行DMA
2.一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。
为了解决这些制约条件,Linux使用了三种区:
1.ZONE_DMA:这个区包含的页用来执行DMA操作。
2.ZONE_NOMAL:这个区包含的都是能正常映射的页。
3.ZONE_HIGHEM:这个区包"高端内存",其中的页能不永久地映射到内核地址空间。

      区的实际使用与体系结构是相关的。linux 把系统的页划分区,形成不同的内存池,这样就可以根据用途进行分配了。需要说明的是,区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑上的分组。尽管某些分配可能需要从特定的区中获得页,但这并不是说,某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占用其他可用去的内存。下表给出每个区及其在X86上所占的列表:

     image

      每个区都用定义在linux/mmzone.h中的struct zone表示,如下:

struct zone {
         spinlock_t              lock;
         unsigned long           free_pages;
         unsigned long           pages_min, pages_low, pages_high;
         unsigned long           protection[MAX_NR_ZONES];
         spinlock_t              lru_lock;       
         struct list_head        active_list;
         struct list_head        inactive_list;
         unsigned long           nr_scan_active;
         unsigned long           nr_scan_inactive;
         unsigned long           nr_active;
         unsigned long           nr_inactive;
         int                     all_unreclaimable; 
         unsigned long           pages_scanned;    
         struct free_area        free_area[MAX_ORDER];
         wait_queue_head_t       * wait_table;
         unsigned long           wait_table_size;
         unsigned long           wait_table_bits;
         struct per_cpu_pageset  pageset[NR_CPUS];
         struct pglist_data      *zone_pgdat;
         struct page             *zone_mem_map;
         unsigned long           zone_start_pfn;

         char                    *name;
         unsigned long           spanned_pages;  
         unsigned long           present_pages;  
};

      其中的lock域是一个自旋锁,这个域只保护结构,而不是保护驻留在这个区中的所有页。没有特定的锁来保护单个页。free_pages域是这个区中空闲页的个数。内核尽可能的保护有pages_min个空闲页可用。name域是一个以NULL结束的字符串,表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page_alloc.h中,三个区的名字分别是"DMA","Normal","HighMem"。

内核提供了一种请求内层的底层机制,并提供了对它进行访问的几个接口。所有这些接口都是以页为单位进行操作的。下表给出所有底层的页分配方法:

     image

      当你不再需要页时可以用下列函数释放它们,只是提醒:仅能释放属于你的页,否则可能导致系统崩溃。内核是完全信任自己的,如果有非法操作,内核会开心的把自己挂起来,停止运行。列表如下:

      image

      上面提到都是以页为单位的分配方式,那么对于常用的以字节为单位的分配来说,内核通供的函数是kmalloc(),和mallloc很像吧,其实还真是这样,只不过多了一个flags参数。用它可以获得以字节为单位的一块内核内存。如果需要的是页----尤其是在你的需求总量接近2的幂次方的时候----那么,前面讨论的页分配接口可能是更好的选择。

      接下来,注意的话,可能会发现无论是页分配接口还是kmalloc都有一个分配器标志(如GFP_KERNEL这样的)。这些标志可分为三类:行为修饰符,区修饰符及类型.下面就来讨论个问题.

      1.行为修饰符(linux/gfp.h):表示内核应当如何分配所需的内存。在某些特定的情况下,只能使用某些特定的方法分配内存。可以同时使用这些标志,用|链接。列表如下:

       image

      2.区分配符:它只关心去应当从何处分配。通常,分配可以从任何区开始。不过,内核优先从ZONE_NORMAL开始,这样可以确保其他区在需要时有足够的空闲页可以使用。区修饰符如下:

       image

       不能给_get_free_pages()指定ZONE_HIGHMEM,因为这个函数返回都是逻辑地址,而不是page结构。这两个函数分配的内存当前可能有可能还没有映射到内核的虚拟地址空间,因此,也可能根本就没有逻辑地址。只有alloc_pages()才能分配高端内存。实际上,大多数ZONE_NORMAL就已经足够了。

      3.类型标志:指定所需的行为和区描述符以完成特殊类型的处理。正因为这点,内核代码趋向于使用正确的类型标志,而不是一味地指定它可能需要用到的多个描述符。下面两个表分别给出了类型标志的列表和每个类型标志与哪些修饰符相关联:

      image                image

      上表中,左边是类型标志,右边是每种类型标志后隐含的修饰符列表。在编写的大多数代码中,用到的要么是GFP_KERNEL,要么是GFP_ATOMIC。下表是通常情形和所用标志的列表,不管使用那种分配类型,你都必须进行检查,并对错误进行处理:

     image

      有了kmalloc,当然就有kfree()(linux/slab.h),释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的内存早就被释放了,在这种情况下调用这个函数会导致严重的后果。特别说明kfree(NULL)是安全的。

      vmalloc()和kmalloc是一样的作用,不同在于前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式,如malloc().kmalloc()可以保证在物理地址上都是连续的(当然,虚拟地址当然也是连续的)。vmalloc()函数只确保页在虚拟机地址空间内是连续的。它通过分配非联系的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。但很显然这样会降低处理性能,因为内核不得不做“拼接”的工作。所以这也是为什么不得已才使用vmalloc()的原因(比如获得大内存时)。大多数情况下,只有硬件设备需要得到物理地址连续的内存。硬件设备存在于内存管理单元以外,它根本不懂什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚地址连续的块。最后需要说明的是,vmalloc()可能睡眠,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。释放时必须使用vfree().

      分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,常常会用到一个空间链表。它就相当于对象高速缓存以便快速存储频繁使用的对象类型。在内核中,空闲链表面临的主要问题之一是不能全局控制。当可用内存变得紧张的时候,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放一些内存来。实际上,内核根本不知道有这样的空闲离岸边。为了弥补这一缺陷,也为了是代码更加稳固,linux内核提供了slab层(也就是所谓的slab分类器),slab分类器扮演了通用数据结构缓存层的角色。slab分配器试图在如下几个原则中寻求一种平衡:

1.频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
2.频繁分配和回收必然会导致内存碎片。为了避免这种情况,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,不会导致碎片。
3.回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
4.如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
5.对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。

      slab层把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在slab层上,使用了一组通用高速缓存。这些缓存又被分为slabs,slab由一个或多个物理上连续的页组成,一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构,每个slab处于三种状态之一:满,部分满,空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。下图给出高速缓存,slab及对象之间的关系:

      image

      上图中的每个cache由kmem_cache_s结构表示,这个结构包含三个链表slabs_full,slab_partial和slabs_empty,均存放在kmem_list3结构内,这些链表包含高速缓存中的所有slab,slab描述符struct slab:

struct slab {
        struct list_head  list;       /*满,部分满或空链表*/
        unsigned long     colouroff;  /*slab着色的偏移量*/
        void              *s_mem;     /*在slab中的第一个对象*/
        unsigned int      inuse;      /*已分配的对象数*/
        kmem_bufctl_t     free;       /*第一个空闲对象*/
};

      slab描述符要么在slab之外另行分配,要么就在slab自身最开始的地方。如果slab很小或者slab内核有足够的空间容纳slab描述符,那么描述符就存放在slab里面.slab分配器创建新的slab是通过__get_free_pages()低级内存分配器进行的:

static inline void * kmem_getpages(kmem_cache_t *cachep, unsigned long flags)
{
        void *addr;
        flags |= cachep->gfpflags;
        addr = (void*)__get_free_pages(flags, cachep->gfporder);
        return addr;
}

      上面的是一个描述原理的简化版。接着,调用kmem_freepages()释放内存,而对给定的高速缓存页,kmem_freepages()最终调用的是free_pages().当然,slab层的关键就是避免频繁分配和释放页。由此可知,slab页只有当给定的高速缓存中既没有部分满也没有空的slab时候才会调用页分配函数。而只有在下列情况下才会调用释放函数:当可用内存变得紧缺时,系统试图释放出更多内存以供使用,或者当高速缓存显式地被销毁时。slab层的管理是在每个高速缓存的基础上,通过提供个整个内核一个简单的接口来完成的。通过接口就可以创建和销毁新的高速缓存,并在高速缓存内分配和释放对象。高速缓存及slab的复杂管理完全通过slab层的内部机制来处理。当创建一个高速缓存后,slab层所起的作用就像一个专用的分配器,可以为具体的对象类型进行分配。一个新的高速缓存是通过一下接口进行创建的:

kmem_cache_t * kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,
                                 void (*ctor)(void*, kmem_cache_t *, unsigned long),
                                 void (*dtor)(void*, kmem_cache_t *, unsigned long));
      有关这个函数的说明,我就省略了,需要的网上一大堆。这个函数成功时会返回一个执行所创建高速缓存的指针,否则,返回空。这个函数由于会睡眠,因此不能在中断上下文中使用。要销毁一个高速缓存,调用:int kmem_cache_destroy(kmem_cache_t *cachep),同样,也是不能在中断上下文中使用。调用该函数之前必须确保存在以下两个条件:
1.高速缓存中的所有slab都必须为空。
2.在调用kmem_cache_destory()期间不再访问这个高速缓存,调用者必须确保这种同步。

      创建了高速缓存以后,就可以通过下列函数从中获取对象:void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)。该函数从高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给__get_free_pages().最后,释放一个对象,并把它返回给原来的slab,可以使用下面的函数:

void kmem_cache_free(kmem_cache_t *cachep,void *objp)

      这样就能把cachep中的对象objp标记为空闲了,关于slab分配器的使用实例,参考资料上有,我就不说了。相比较以前的用户空间栈而言,内核栈是非常小的。每个进程都有自己的内核栈进程在内核执行期间的整个调用链必须放在自己的内核栈上。中断处理程序也使用被它们打断的进程的堆栈。这就意味着,在最恶劣的情况下,8kB的内核栈可能会由多个函数的嵌套调用链和几个中断处理程序来共享。显然,深度的嵌套会导致溢出。

      根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。一旦这些页被分配,就必须映射到内核的逻辑地址空间上。要映射一个给定的page结构到内核地址空间,可以使用void *kmap(struct page *page) 这个函数在高端内存或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址,如果页位于高端内存,则会建立一个永久映射,在返回地址。这个函数可以睡眠,所以kmap()只能用在进程上下文中。当不再需要内存映射的时候,就用下列函数进行解除映射:

void kunmem(struct page* page)

      当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时睡眠(也就是原子睡眠)。只要有一组保留的永久映射,它们就可以临时持有新创建的一个映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方。建立临时映射:void *kmap_atomic(struct page *page,enum km_type type).参数type是下列枚举类型之一,描述了临时映射的目的,如下:

     image

      这个函数不会阻塞,它也禁止内核抢占,通过函数void *kunmap_atomic(void *kvaddr,enum km_type type).这个函数还是不会映射。

      最后,我们总结一下,说说分配函数的选择吧,总结如下:

1.如果需要连续的物理页,就可以使用某个低级页分配器或kmalloc().
2.如果想从高端内存进行分配,使用alloc_pages().
3.如果不需要物理上连续的页,而仅仅是虚拟地址上连续的页,那么就是用vmalloc
4.如果要创建和销毁很多大的数据结构,那么考虑建立slab高速缓存。
      好了,有关内存管理的也说完了,其实也不算我说,有很多都是参考书上资料的。
posted on 2011-07-28 15:42  ☆&寒 烟☆  阅读(10084)  评论(3编辑  收藏  举报