浅谈linux内存管理

来源:互联网 发布:python exec函数 编辑:程序博客网 时间:2024/06/05 02:42
linux内存管理

    内核把物理页作为内存管理的基本单位,尽管处理器的最小可寻址单位通常为字,但是内存管理单元(MMU,将虚拟地址转换为物理地址的硬件)通常也页为单位进行处理,从虚拟内存的角度来看,页就是最小单位。
    大多数32位体系结构支持4KB的页,而64为系统支持8KB的页,这也就说,在支持4KB页大小并有1GB的物理内存机器上,物理内存会有262144个页。内核用struct page结构来系统中的每个物理页,其简要结构如下:
    struct page {
          unsigned long flags;           /*标志是否为脏页*/
          atomic_t      _count;          /*引用次数*/
          atomic_t      _mapcount;       /*页缓存使用*/
          unsigned long private;         /*页缓存使用*/
          struct address_space *mapping; /*页缓存使用*/
          pgoff_t       index;
          struct list_head lru;
          void          *virtual;        /*页的虚拟地址*/
     };

    由于一些物理页在内核中用于一些特定的用处,所以内核把页划分为不同的区,linux主要使用了四种区:
    ZONE_DMA:这个区包含的额页只能用来执行DMA(直接内存访问)操作
    ZONE_DMA32:只能被32位设备访问的DMA区,只能用来执行DMA操作
    ZONE_NORMAL:这个区包含的都是可以正常映射的页
    ZONE_HIGHEM:高端内存,其中的页不能永久的映射到内核地址空间
    其中有的设备没有ZONE_DMA,有的没有ZONE_HIGHEM区,都可以直接在ZONE_NORMAL上进行分配,在x86-32上,ZONE_DMA对应得物理内存是0~16MB,ZONE_NORMAL对应的物理内存是16~896MB,ZONE_HIGHEM(动态映射的页)对应的物理内存是大于896MB的部分。
    几个常用的函数:
    struct page* alloc_pages(gfp_t gfp_mask,unsigned int order)该函数分配2的order幂次方个连续的物理页,并返回一个指针,该指针指向第一个页的page结构,如果出错返回NULL。
    void* page_address(struct page* page)将给定的页转换成它的逻辑地址,函数返回一个指针,指向给定物理页的当前所在的逻辑地址。
    unsigned long _get_free_pages(gfp_t gfp_mask,unsigned int order)这个函数和alloc_pages()作用相同,不过它直接返回请求的第一个页的逻辑地址,因为页是连续的,所以其他页也会紧随其后。
    struct page * alloc_page(gfp_t gfp_mask)
    unsigned long _get_free_page(gfp_t gfp_mask)
    这两个函数与兄弟函数的工作方式相同,只不过传递给order的值为0
    unsigned long get_zeroed_page(unsigned int gfp_mask)该函数与_get_free_pages()工作方式相同,只不过把分配好的页都填充成了0.    
    当不需要页时需要释放他们,下面三个函数用于释放它们:
    void _free_pages(struct page* page,unsigned int order)
    void free_pages(unsigned long addr,unsigned int order)
    void free_page(unsigned long addr)
    以上是与页分配相关的函数,下面这个函数可以获得以字节为单位的一块内核内存,这个函数返回一个指向内存块的指针,其内存块至少要有size大小,所分配的内存区在物理上是连续的,在出错时返回NULL,该函数形式如下:
    void* kmalloc(size_t size,gfp_t flags)
    另一个函数vmalloc的工作方式与kmalloc一样,但是vmalloc分配的内存虚拟地址是连续的,而物理地址则无须连续,其函数形式如下:
    void* vmalloc(unsigned long size)
    与在用户空间以一样,对于分配出来的内存在不使用时需要释放,以上两个函数对应的释放函数分别如下:
    void kfree(const void* ptr)
    void vfree(const void* addr)
    在以上几个函数中,用的gfp_mask标志可以分为三类:行为修饰符、区修饰符以及类型。行为修饰符表示内核应当如何分配所需的内存,如_GFP_NUFALL表示分配器将无限制的重复分配,分配不能失败;区修饰符表示从哪儿分配内存,区修饰符指明到底从这些区中的哪一区中进行分配;类型标识组合了行为修饰符和区修饰符,在该类型中,经常用到的是GFP_KERNEL,这个分配可能引起睡眠,它使用的是普通优先级,由于调度可能睡眠,所以这个标识只用在可以重新安全调度的进程上下文中。另一个截然相反的标识是GFP_ATOMIC,这个标识表示不能睡眠的内存分配。

slab缓存
    Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。
    Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。

Linux slab 分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。

   slab 结构的最高层是 cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池。
每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:
slabs_full
    完全分配的 slab
slabs_partial
    部分分配的 slab
slabs_empty
    空 slab,或者没有对象被分配

注意 slabs_empty 列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。

    slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。这些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单位,因此如果需要对 slab 进行扩展,这也就是所扩展的最小值。通常来说,每个 slab 被分配为多个对象。
由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当所有对象都被释放之后,就从 slabs_partial 列表移动到 slabs_empty 列表中。
    slab的优势
    (1)  内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题
    (2) slab 分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化
    (3) slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能

API 函数
    现在来看一下能够创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 中对对象进行分配和释放操作的函数。
    第一个步骤是创建 slab 缓存结构,您可以将其静态创建为:struct struct kmem_cache *my_cachep;
    然后其他 slab 缓存函数将使用该引用进行创建、删除、分配等操作。kmem_cache 结构包含了每个中央处理器单元(CPU)的数据、一组可调整的(可以通过 proc 文件系统访问)参数、统计信息和管理 slab 缓存所必须的元素。
    内核函数 kmem_cache_create 用来创建一个新缓存。这通常是在内核初始化时执行的,或者在首次加载内核模块时执行。其原型定义如下:
    struct kmem_cache *
    kmem_cache_create( const char *name, size_t size, size_t align,
                       unsigned long flags;
                       void (*ctor)(void*, struct kmem_cache *, unsigned long),
                       void (*dtor)(void*, struct kmem_cache *, unsigned long));
    name 参数定义了缓存名称,proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。 size 参数指定了为这个缓存创建的对象的大小, align 参数定义了每个对象必需的对齐。 flags 参数指定了为缓存启用的选项。这些标志如表 1 所示。
    kmem_cache_create 的部分选项(在 flags 参数中指定)
        选项             说明
    SLAB_RED_ZONE    在对象头、尾插入标志,用来支持对缓冲区溢出的检查。
    SLAB_POISON    使用一种己知模式填充 slab,允许对缓存中的对象进行监视(对象属对象所有,不过可以在外部进行修改)。
    SLAB_HWCACHE_ALIGN    指定缓存对象必须与硬件缓存行对齐。
    ctor 和 dtor 参数定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象时,可以通过构造器进行初始化。
    在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内存。相反,在试图从缓存(最初为空)分配对象时,refill 操作将内存分配给它。当所有对象都被使用掉时,也可以通过相同的操作向缓存添加内存。
    内核函数 kmem_cache_destroy 用来销毁缓存。这个调用是由内核模块在被卸载时执行的。在调用这个函数时,缓存必须为空。void kmem_cache_destroy( struct kmem_cache *cachep );
    要从一个命名的缓存中分配一个对象,可以使用 kmem_cache_alloc 函数。调用者提供了从中分配对象的缓存以及一组标志:
    void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
    这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 向缓存中增加内存。 kmem_cache_alloc 的 flags 选项与 kmalloc 的 flags 选项相同。
    kmem_cache_alloc 和 kmalloc 内核函数的标志选项
    标志         说明
    GFP_USER    为用户分配内存(这个调用可能会睡眠)。
    GFP_KERNEL    从内核 RAM 中分配内存(这个调用可能会睡眠)。
    GFP_ATOMIC    使该调用强制处于非睡眠状态(对中断处理程序非常有用)。
    GFP_HIGHUSER    从高端内存中分配内存。
    内核函数 kmem_cache_zalloc 与 kmem_cache_alloc 类似,只不过它对对象执行 memset 操作,用来在将对象返回调用者之前对其进行清除操作。
    要将一个对象释放回 slab,可以使用 kmem_cache_free。调用者提供了缓存引用和要释放的对象。
    void kmem_cache_free( struct kmem_cache *cachep, void *objp );
0 0