内存分配

本文详细介绍了Linux内核中的内存分配,包括kmalloc、后备高速缓存(SLAB)、内存池、get_free_page和vmalloc等机制。kmalloc基于slab实现,用于分配物理地址连续的小内存,而get_free_page分配连续的物理页面。内存池为紧急状态下提供内存,避免分配失败。vmalloc分配虚拟地址空间,虽然物理上不连续,但在地址上是连续的。

一、概述

  • linux内核中很多内存分配器,使用这些内存分配器,就能得到想要的内存大小和内型。内核中有像kmalloc,slab,get_free_page,mempool,vmalloc,buddy system等等各种不同的内存分配器。android下推出来了一个更强大的内存分配器,叫做ion,ion相当一个内存分配器容器,它相当于整合了上面大部分的内存分配器,使用ion可以分配到连续的物理内存,也可以分配虚拟内存,还可以分配到DMA内存等等,有兴趣的大家可以去了解下,这里暂不描述。

二、kmalloc

  • kmalloc函数特性
  1. kmalloc函数如果不被阻塞,本身是运行很快的,也意味着kmalloc函数在获取不到空闲内存时会睡眠
  2. kmalloc函数所获取的内存空间清零,也就是说,分配给它的区域仍然保持这原有的数据。意味着我们需要将内存显示地清空,尤其是可能导出给用户空间或者写入设备的内存
  3. kmalloc函数分配的区域在物理内存也是连续的。kmalloc函数分配的是物理内存,并且是连续的。
  4. kmalloc是基于slab实现的,用于分配物理地址连续的小内存,__get_free_page() 分配连续的物理地址,用于整页分配
  • kmalloc函数原型
    void *kmalloc(size_t size, int flags);

  • kmalloc参数

  1. flags参数
    @)flags分配标志,它能够以多种方式控制kmalloc的行为,最常用的标志是 GFP_KERNEL,它表示内存分配是代表运行在内核空间的进程执行的。换句话说,这意味着调用它的函数正代表某个进程执行的系统调用(最终总是调用get_free_pages来实现实际的分配,这就是GFP的由来)。所以kmalloc的底层实现就是get_free_pages。
    @@)使用GFP_KERNEL允许kmalloc在空闲内存较少时把当前进程转入休眠以等待一个页面。所以调用kmalloc以GFP_KERNEL标志分配内存函数必须是可重入的。
    @@@)GFP_KERNEL并不总是适用,有时kmalloc是在进程上下文之外被调用的,例如在中断上下文,tasklet或者内核定时器中调用。这种情况下current进程就不应该睡眠,驱动程序则应该使用GFP_ATOMIC标志。内核通常会为原子性的分配预留一些空闲页面。使用GFP_ATOMIC标志时,kmalloc甚至会用掉最后一个空闲页面。不过如果连最后一页都没有了, 分配就返回失败。

  2. flags常见类型
    GFP_ATOMIC
    用于在中断处理例程或其他运行于进程上下文之外的代码中分配内存,不会休眠。

    GFP_KERNEL
    内核内存的通常分配方法,可能引起休眠。

    GFP_USER
    用于为用户空间页分配内存,可能会休眠。

    上面列出的分配标志可以和下面的标志"或"起来使用。下面这些标志控制如何进行分配。
    __GFP_DMA
    该标志请求分配发生在可进行DMA的内存区段中。

    __GFP_HIGHMEM
    这个标志表明要分配的内存可位于高端内存。

    __GFP_HIGH
    这个标志标记了一个高优先级的请求,它允许为紧急情况而消耗由内核保留的一些页面。

    __GFP_REPEAT
    __GFP_NOFAIL
    __GFP_NORETRY
    这三个标志告诉分配器在满足分配请求而遇到困难时应该采取何种行为。第一个表示努力再试一次。第二个表示始终不返回失败,会努力满足分配请求。最后一个表示如果请求的内存不可获得,立即返回。

  3. 内存区段
    1)Linux内核把内存分为三个区段:可用于DMA的内存、常规内存以及高端内存。通常的内存分配都发生在常规内存区,但通过设置上面介绍过的标志也可以请求在其他区段中的分配。
    2)可用于DMA的内存指存在于特别地址范围内的内存,外设可以利用这些内存执行DMA访问。外设只要做DMA映射,这里所说的外设指的是CPU核以外的外设,比如LCD控制器,USB控制器等,都是在ARM芯片内存,CPU核以外。将这些外设内存(寄存器地址)映射到DDR中
    3)高端内存是32位平台为了访问(相对)大量的内存而存在的一种机制。如果不首先完成一些特殊的映射,我们就无法从内核中直接访问这些内存。当一个新页面为满足kmalloc的要求被分配时,内核会创建一个内存区段的列表以供搜索。如果指定了__GFP_DMA标志,只只有DMA区段会被搜索;如果低地址段上没有可用内存,分配就会失败。如果没有指定特殊标志,则常规区段和DMA区段都会被搜索;而如果设置了__GFP_HIGHMEM标志,则所有三个区段都会被搜索以获取一个空闲页(然而要注意的是,kmalloc不能分配高端内存)

  4. size参数
    1)Linux内核负责管理系统物理内存,物理内存只能按页面进行分配,一页的大小一般为4KB(PAGE_SIZE)。其结果是kmalloc和典型的用户空间的malloc在实现上有很大的差别。简单的基于堆的内存分配技术会遇到麻烦,因为页面边界的处理成为棘手的问题,因此内核使用了特殊的基于页的分配技术,以最佳地利用系统RAM。
    2)Linux处理内存的方法是,创建一系列的内存对象池每个池中的内存块大小是固定一致的(但是池与池之间的内存块大小是不一致的)。处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。
    3)内核只能分配一些预定义的、固定大小的字节数组。如果申请任意数量的内存,那么得到的很可能会多一些,最多会到申请数量的两倍。另外,程序员应该记住,kmalloc能处理的最小的内存块是32或64,到底是哪个则取决于当前体系结构使用的页面大小。
    4)对kmalloc能够分配的内存块大小,存在一个上限。这个限制随着体系架构的不同以及内核配置选项的不同而变化。如果我们希望代码具有完整的可移植性,则不应该分配大于128KB的内存。

     static __always_inline void *kmalloc(size_t size, gfp_t flags)
     {
     	struct kmem_cache *cachep;
     	void *ret;
    
     	if (__builtin_constant_p(size)) {
     		int i;
    
     		if (!size)
     			return ZERO_SIZE_PTR;
    
     		if (WARN_ON_ONCE(size > KMALLOC_MAX_SIZE))
     			return NULL;
    
     		i = kmalloc_index(size);
    
     #ifdef CONFIG_ZONE_DMA
     		if (flags & GFP_DMA)
     			cachep = kmalloc_dma_caches[i];
     		else
     #endif
     			cachep = kmalloc_caches[i];
    
     		ret = kmem_cache_alloc_trace(cachep, flags, size);
    
     		return ret;
     	}
     	return __kmalloc(size, flags);
     }
     
     void *__kmalloc(size_t size, gfp_t flags)
     {
     	return __do_kmalloc(size, flags, 0);
     }
     
     static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
     			  unsigned long caller)
     {
     	struct kmem_cache *cachep;
     	void *ret;
    
     	/* If you want to save a few bytes .text space: replace
     	 * __ with kmem_.
     	 * Then kmalloc uses the uninlined functions instead of the inline
     	 * functions.
     	 */
     	cachep = kmalloc_slab(size, flags);
     	if (unlikely(ZERO_OR_NULL_PTR(cachep)))
     		return cachep;
     	ret = slab_alloc(cachep, flags, caller);
     
     	trace_kmalloc(caller, ret,
     		      size, cachep->size, flags);
    
     	return ret;
     }
    

三、后备高速缓存

  1. SLAB的高速缓存中有普通高速缓存和专用高速缓存,平时kmem_cache_create创建的是专用高速缓存,比如存放task_struct,mm_struct的高速缓存。普通高速缓存主要供kmalloc使用。

  2. 所谓后备高速缓存,也就是专用高速缓存,是指内核中的一组拥有同一大小内存块的内存池,主要用于反复分配同一大小的内存块。

  3. Linux内核的高速缓存管理有时称为"slab分配器",slab分配器实现的高速缓存具有kmem_cache_t类型,可以通过kmem_cache_create创建:

     struct kmem_cache *
     kmem_cache_create(const char *name, size_t size, size_t align,
     		  unsigned long flags, void (*ctor)(void *))
    
  4. 该函数创建一个新的高速缓存对象,其中可以容纳任意数目的内存区域,这些区域的大小都相同,由size参数指定。参数name与这个高速缓存相关联,其功能是保管一些信息以便追踪,通常被设置为高速缓存的结构类型的名字,高速缓存保留指向该名称的指针,而不是复制其内容。offset是页面中第一个对象的偏移量,flags控制如何完成分配。

  5. 一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc从中分配内存对象:

     void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
    

    这里参数cache是先前创建的高速缓存;参数flags和传递给kmalloc的相同

  6. 释放一个内存对象时使用kmem_cache_free:

     void kmem_cache_free(kmem_cache_t *cache, const void *obj);
    
  7. 如果驱动和高速缓存有关额部分已经处理完了,比如模块被卸载了,这时驱动程序应该释放它的高速缓存,如下:

     int kmem_cache_destroy(kmem_cache_t * cache);
    

    这个释放操作只有在已将从缓存中分配的所有对象都归还后才能成功。所以,模块应该检查kmem_cache_destroy的返回状态;如果失败,则表面模块中发生了内存泄露(因为有一些对象被漏掉了)。使用后备式缓存带来的另一个好处是内核可以统计高速缓存的使用情况。高速缓存的使用统计情况可以从/proc/slabinfo中获取。

四、内存池

  1. 内核中有些地方的内存分配是不允许失败的。为了确保这种情况下的成功分配,内核开发者建立了一种称为内存池(或者“mempool”)的抽象。内存池其实就是某种形式的后备高速缓存,它试图始终保存空闲的内存,以便在紧急状态下使用。

  2. 内存池对象的类型为mempool_t,可使用mempool_create来建立内存对象:

     mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
     						mempool_free_t *free_fn, void *pool_data)
    

    min_nr参数表示的是内存池应始终保持的已分配对象的最少数目。对象的实际分配和释放由alloc_fn和free_fn函数处理,mempool_create的最后一个参数即pool_data被传入alloc_fn和free_fn中。我们可以为mempool编写特定用途的函数来处理内存分配。但是,通常我们仅会让内核的slab分配器为我们处理这个任务。内核中有两个函数(mempool_alloc_slab和mempool_free_slab),它们的原型和上述内存池分配原型匹配,并利用kmem_cache_alloc和kmem_cache_free处理内存分配和释放。alloc_fn和free_fn用mempool_calloc_salb和mempool_free_slab替换

  3. 在建立内存池之后,可如下所示分配和释放内存对象

     void *mempool_alloc(mempool_t *pool, int gfp_mask);
     void mempool_free(void *element, mempool_t *pool);
    

    在创建mempool时,就会多次调用分配函数为预先分配的对象创建内存池。之后,对mempool_alloc的调用将首先通过分配函数获得该对象;如果分配失败,就会返回预先分配的对象。如果使用mempool_free释放一个对象,则如果预先分配的对象数目小于要求的最低数目,就会将对象保留在内存中;否则,对象会返回给系统

     mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
     					mempool_free_t *free_fn, void *pool_data)
     {
     	return mempool_create_node(min_nr,alloc_fn,free_fn, pool_data,
     				   GFP_KERNEL, NUMA_NO_NODE);
     }
     EXPORT_SYMBOL(mempool_create);
     
     mempool_t *mempool_create_node(int min_nr, mempool_alloc_t *alloc_fn,
     			       mempool_free_t *free_fn, void *pool_data,
     			       gfp_t gfp_mask, int node_id)
     {
     	mempool_t *pool;
     	pool = kmalloc_node(sizeof(*pool), gfp_mask | __GFP_ZERO, node_id);
     	if (!pool)
     		return NULL;
     	pool->elements = kmalloc_node(min_nr * sizeof(void *),
     				      gfp_mask, node_id);
     	if (!pool->elements) {
     		kfree(pool);
     		return NULL;
     	}
     	spin_lock_init(&pool->lock);
     	pool->min_nr = min_nr;
     	pool->pool_data = pool_data;
     	init_waitqueue_head(&pool->wait);
     	pool->alloc = alloc_fn;
     	pool->free = free_fn;
    
     	/*
     	 * First pre-allocate the guaranteed number of buffers.
     	 */
     	while (pool->curr_nr < pool->min_nr) {
     		void *element;
     
     		element = pool->alloc(gfp_mask, pool->pool_data);
     		if (unlikely(!element)) {
     			mempool_destroy(pool);
     			return NULL;
     		}
     		add_element(pool, element);
     	}
     	return pool;
     }
     
     static void add_element(mempool_t *pool, void *element)
     {
     	BUG_ON(pool->curr_nr >= pool->min_nr);
     	pool->elements[pool->curr_nr++] = element;
     }
     
     void * mempool_alloc(mempool_t *pool, gfp_t gfp_mask)
     {
     ...
     	element = pool->alloc(gfp_temp, pool->pool_data);
     	if (likely(element != NULL))
     		return element;
     
     	spin_lock_irqsave(&pool->lock, flags);
     	if (likely(pool->curr_nr)) {
     		element = remove_element(pool);
     		spin_unlock_irqrestore(&pool->lock, flags);
     		/* paired with rmb in mempool_free(), read comment there */
     		smp_wmb();
     		return element;
     	}
     	...
     }
    
     void mempool_free(void *element, mempool_t *pool)
     {
     	...
     	if (pool->curr_nr < pool->min_nr) {
     		spin_lock_irqsave(&pool->lock, flags);
     		if (pool->curr_nr < pool->min_nr) {
     			add_element(pool, element);
     			spin_unlock_irqrestore(&pool->lock, flags);
     			wake_up(&pool->wait);
     			return;
     		}
     		spin_unlock_irqrestore(&pool->lock, flags);
     	}
     	pool->free(element, pool->pool_data);
     }
    
  1. 从以上可以看出pool->pool_data即通过kmem_cache_create创建的高速缓存对象,pool->alloc和pool->free为传入的mempool_alloc_slab和mempol_free_slab函数。可以知道mempool_create创建的内存池是如何来的,首先通过kmem_cache_create创建一个后备高速缓存对象,然后接着通过while循环创建pool->min_nr个内存块,创建函数为mempool_alloc_slab ==>kmem_cache_alloc,然后通过add_element将创建的element加入到pool->elements数组中。
  2. mem_alloc分配对象的原则是先使用mempool_alloc_slab分配内存对象,如果失败,就返回预先分配的对象(在mempool_create中创建的)。而使用mempool_free_alloc释放对象时,如果内存池中的内存块对象个数小于最小数目时,则释放的对象返回到内存池中,否则就是返回给系统了,这样就可以控制mempool中的内存对象数目数目在min_nr附近
  1. 还可以通过下面的函数来调整mempool的大小:

     int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
    
  2. 如果不再需要内存池,可以使用下面的函数将其返回给系统:

     void mempool_destroy(mempool_t *pool);
    
  3. mempool会分配一些内存块,空闲且不会真正得到使用。因此,使用mempool很容易浪费大量内存。几乎在所有的情况下,最好不使用mempool除了处理可能的失败。

五、get_free_page和相关函数

  1. 如果模块需要分配大块的内存,使用面向页面的分配技术会更好些。分配页面的函数如下:

     get_zeroed_page(unsigned int flags);
     返回指向新页面的指针并将页面清零
     __get_free_page(unsigned int flags);
     类似于get_zeroed_page,但不清零页面
     __get_free_pages(unsigned int flags, unsigned int order);
     分配若干(物理连续的)页面,并返回指向该内存区域第一个字节的指针,但不清零页面
    
  2. flags与kmalloc一样,通常会使用GFP_KERNEL或GFP_ATOMIC,也会还会加上__GFP_DMA或者__GFP_HIGHMEM

  3. order是要申请或释放的页面数的以2为底的对数(log2N)。比如你要申请8个页面,那么order = log2/8=3,所以order为0表示1个页面,order为3表示8个页面,如果order太大,就会返回失败。可允许的order最大为10或者11,这依赖于体系结构,1024页也就是4096KB,也就是4MB,还是挺大的。

  4. 当程序不再需要使用页面时,使用free_page或free_pages释放,如果试图释放和先前分配数目不等的页面,内存映射就会遭到破坏,随后系统就会出错。

  5. 使用get_free_page性能并没有提高很多,因为kmalloc已经够快了,而是在于更有效地使用了内存。按页分配不会浪费内存空间,而用kmalloc函数则会因为分配粒度的原因而浪费一定数量的内存。

  6. 分析kmalloc函数的实现可以知道,kmalloc() 函数本身是基于 slab 实现的。slab 是为分配小内存提供的一种高效机制。但 slab 这种分配机制又不是独立的,它本身也是在页分配器的基础上来划分更细粒度的内存供调用者使用。也就是说系统先用页分配器分配以页为最小单位的连续物理地 址,然后 kmalloc() 再在这上面根据调用者的需要进行切分。关于以上论述,我们可以查看 kmalloc() 的实现,kmalloc()函数的实现是在 __do_kmalloc() 中,可以看到在 __do_kmalloc()代码里最终调用了 __cache_alloc() 来分配一个 slab,其实kmem_cache_alloc() 等函数的实现也是调用了这个函数来分配新的 slab。我们按照 __cache_alloc()函数的调用路径一直跟踪下去会发现在 cache_grow() 函数中使用了 kmem_getpages()函数来分配一个物理页面,kmem_getpages() 函数中调用的alloc_pages_node() 最终是使用 __alloc_pages() 来返回一个struct page 结构,而这个结构正是系统用来描述物理页面的。这样也就证实了上面所说的,slab 是在物理页面基础上实现的。kmalloc() 分配的是物理地址。

  7. __get_free_page() 是页面分配器提供给调用者的最底层的内存分配函数。它分配连续的物理内存。__get_free_page() 函数本身是基于 buddy 实现的。在使用 buddy 实现的物理内存管理中最小分配粒度是以页为单位的。关于以上论述,我们可以查看__get_free_page()的实现,可以看到 __get_free_page()函数只是一个非常简单的封状,它的整个函数实现就是无条件的调用 __alloc_pages() 函数来分配物理内存。

  8. struct page是内核用来描述单个内存页的数据结构,Linux页分配器的核心代码是称为alloc_pages_node函数

     struct page *alloc_pages_node(int nit, unsigned int flags, unsigned int order);
    

    这个函数有两个变种,大多数情况下我们使用这两个宏:

     struct page *alloc_pages(unsigned int flags, unsigned int order);
     struct page *alloc_page(unsigned int flags);
    

六、vmalloc及辅助函数

  1. vmalloc分配的是虚拟地址空间的连续区域,在物理上是不连续的,因此要访问其中每个页面都必须独立地调用函数alloc_page,内核认为它们在地址上是连续的。vmalloc也是Linux内存分配的机制,通过vmalloc分配的内存使用起来效率不高。vmalloc可以获得的地址在VMALLOC_START和VMALLOC_END之间。
  2. kmalloc和__get_free_pages返回的内存地址也是虚拟地址,其实际值需要MMU的处理才能转为物理内存地址,vmalloc在如何使用硬件上没有区别,却别在于内核如何执行分配任务上。kmalloc和__get_free_pages使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量PAGE_OFFSET的一个偏移。这两个函数不需要为该地址段修改页表。但另一方面,vmalloc和ioremap使用的的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立(虚拟)内存区域。
  3. vmalloc分配的地址是不能在处理器之外使用的,因为它们只在处理器的内存管理单元才有意义。当驱动程序需要真正的物理地址时,就不能使用vmalloc了。使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。注意vmalloc的开销要比__get_free_pages大,因为它不但获取内存,还要建立页表。因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。
  4. vmalloc申请的需要使用vfree释放,ioremap映射的需要iounmap来取消映射,和vmalloc一样,ioremap也建立新的页表,但和vmalloc不同的是,ioremap并不实际分配内存。ioremap的返回值是一个特殊的虚拟地址,可以用来访问指定的物理内存区域。ioremap多用于映射(物理)缓冲区地址到虚拟的内核空间,比如帧缓冲区。不应该将ioremap返回的地址昂做指向内存的指针而直接访问,相反应该使用readb或其他I/O函数
  5. vmalloc函数的一个小缺点是不能在原子上下文中使用,它的内部实现调用了kmalloc(GFP_KERNEL)来获七、获取大的缓冲区
    取页表的存储空间,因而可能休眠。

七、获取大的缓冲区

  • 如果的确需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。在引导时就进行分配是获得大量连续内存页面的唯一方法,它绕过了__get_free_pages函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。在引导时分配缓冲区有点“脏”,因为它通过保留私有内存池而跳过了内核的内存管理策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值