内核中的内存分配通常通过kmalloc/kfree来进行,但是也有其它的方式来获取内存,所有这些方式共同提供了内核中分配、释放内存的接口。

一、kmalloc/kfree

类似于标准C中的malloc/free,kmalloc/kfree是内核中的用于常规内存分配的接口。

kmalloc/kfree是工作在slab分配器的基础上的,在系统启动时会调用kmem_cache_init,该函数会创建多个通用缓冲池,这些缓冲池中的slab对象的大小都是2的整数倍,并且依次增大,最小的大小为1<<KMALLOC_SHIFT_LOW,最大的大小为1<<KMALLOC_SHIFT_HIGH,缓冲池的数目为KMALLOC_SHIFT_HIGH - KMALLOC_SHIFT_LOW+ 1。这些缓冲池统称为通用缓冲池,被用于kmalloc和kfree。从其实现机制可以看出,我们无法使用kmalloc申请大小大于1<< KMALLOC_SHIFT_HIGH的内存块。

当通过kmalloc申请内存时,内核会根据所请求的大小来从通用缓冲池中选择最合适的缓冲池进行内存分配,所谓的最合适就是大于等于申请大小的所有缓冲池中slab对象大小最小的那一个。

在分配时可以指定一些标记来指定分配时的行为,最常用的是GFP_KERNEL,使用该标记时,可能会休眠,另外一个是GFP_ATOMIC,分配不会休眠,用于原子上下文。还有很多不同的标记,可以参考文件“include/linux/gfp.h”该文件包含了分配标记及其含义。

二、专用缓冲池

通常情况下,通用缓冲池是够用的,但是对于有的内核部件来说,它可能需要反复申请、释放固定大小的内存块,这时也可以选择创建自己的专用缓冲池,然后从该专用池中进行申请、释放。这涉及到三个API:

2.1 创建专用缓冲池

typedef struct mempool_s {
	spinlock_t lock;
	int min_nr;		/* nr of elements at *elements */
	int curr_nr;		/* Current nr of elements at *elements */
	void **elements;

	void *pool_data;
	mempool_alloc_t *alloc;
	mempool_free_t *free;
	wait_queue_head_t wait;
} mempool_t;

内存池的设计思想是:在创建内存池时,首先申请指定数目的内存对象,并将其保存在内存池中,随后在进行分配时,首先尝试常规的分配,如果无法申请到内存,就从内存池预留的内存对象中取出一个;在释放内存时,如果内存池预留的内存对象数目小于指定的数目,则将要释放的内存放入预留内存中而不做实际的释放,如果内存池中预留内存对象的数目等于指定的数目则进行真正的释放操作。

内存池所预留的内存实际上是被浪费了,因而最好不要用它。

3.1 创建内存池

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

该函数被用于创建内存池,在创建时会首先使用alloc_fn申请min_nr个内存对象,并将其保存在内存池中。

  • min_nr:保证内存池中最少有这么多个内存对象
  • alloc_fn:用于自定义的进行真正的内存分配的函数,一般可用mempool_alloc_slab
  • free_fn:用于自定义的用于真正的内存释放的函数,一般可用mempool_free_slab
  • pool_data: 被传递给用户自定义函数(alloc_fn,free_fn)的参数

不在使用的内存池可以用voidmempool_destroy(mempool_t *pool)来销毁。

3.2 从内存池申请内存/向内存池释放内存

void *mempool_alloc(mempool_t *pool, intgfp_mask);

它用于使用标记gfp_mask来从内存池pool中申请内存,如果常规的申请失败(即调用创建该内存池时提供的用户自定义分配函数分配失败),则从预留的内存对象中返回一个。gfp_mask类似于kmalloc的flags。

void mempool_free(void *element, mempool_t*pool);

将内存element返回给内存池pool,如果pool中当前的预留内存对象数目小于内存池的min_nr,则内存被归还懂啊内存池的预留内存对象中,否则调用真正的释放函数(即调用创建该内存池时提供的用户自定义释放函数)

四、分配大块内存

如果一个内核部件需要大块的内存,则可以使用面向页面的技术(kmalloc对申请的最大大小有限制)

如果一个模块需要分配大块的内存, 它常常最好是使用一个面向页的技术

4.1 分配、释放页(使用地址指针)

get_zeroed_page(gfp_t gfp_mask);

返回一个指向新页的指针并且用零填充了该页.

__get_free_page(gfp_t gfp_mask);

类似于get_zeroed_page, 但是没有清零该页.

__get_free_pages(unsigned int gfp_mask,unsigned int order);

分配并返回一个指向一个内存区第一个字节的指针, 内存区可能是几个(物理上连

续)页长但是没有清零。

Order:为幂指数,即它指定分配多少个页,比如order为0,表示分配2的0次幂即1页。

gfp_mask:和kmalloc的flags相同

分配可能失败,因而调用者必须处理失败。

void free_page(unsigned long addr);

void free_pages(unsigned long addr,unsigned long order);

这两个函数用于释放页,注意如果指定了order,则申请时和释放时必须使用相同的值。需要注意的是这里的api无法用于分配高端内存。

4.2 分配、释放页(使用page)

static inline struct page*alloc_pages(gfp_t gfp_mask, unsigned int order)

#define alloc_page(gfp_mask)alloc_pages(gfp_mask, 0)

这两个函数也用于分配页,但是它们返回的是一个指向page数据结构的指针,而不是页面的起始地址。gfp_mask类似于kmalloc的flags。order类似于__get_free_pages的order参数。

使用它们分配的内存页应该使用下面的接口归还给系统:

void __free_page(struct page *page);

void __free_pages(struct page *page,unsigned int order);

void free_hot_page(struct page *page);

void free_cold_page(struct page *page);

这里的API适用于高端内存。

4.3 vmalloc/vfree

vmalloc用于从虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用alloc_page来获得每个页),但是内核将它们作为一个连续的地址范围来看待。

从 vmalloc 获得的内存用起来稍微低效些,因此不建议使用它。另外由vmalloc返回的地址必须经过页表才能找到真正的物理地址,因而如果内核部件需要使用真正的物理地址,则不能使用它来分配。

void *vmalloc(unsigned long size);用来申请内存

void vfree(void * addr);用来释放用vmalloc申请的内存

4.3.1 vmalloc和kmalloc以及__get_free_pages的区别:

vmalloc返回的是虚拟地址,但是实际上kmalloc和__get_free_pages及相关函数返回的也是虚拟地址,那么为什么vmalloc的效率很低,而其它两个不低呢,这是因为虽然kmalloc和__get_free_pages及相关函数虽然也返回虚拟地址,但是它们所返回的虚拟地址是不同的。

  • vmalloc所返回的地址范围在VMALLOC_START和VMALLOC_END之间,对于这部分地址需要在使用时创建页表,并且这部分地址对应的物理地址可能是不连续的,必须通过页表来访问;而kmalloc和__get_free_pages返回的虚拟地址则属于常规的内核虚拟地址空间,这部分虚拟地址空间的特点在于它和真实的物理地址之间就只有一个偏移量的差异,也就是说它们和真实的物理地址之间是一一对应的。
  • 另一方面也容易看出虽然__get_free_pages和vmalloc都可以返回大块的内存,但是vmalloc返回的内存可能是由多个不连续的物理页组成的,而且需要建立页表(vmalloc分配过程中会在用alloc_page(s)之后用map_vm_area来建立页表),而__get_free_pages则是返回连续的已经建立了页表的常规内存页。
vmalloc不能在原子上下文使用,因为它要创建页表,因而需要使用kmalloc来为页表来分配内存空间,这个过程可能会休眠。

4.4 ioremap

类似于vmalloc,使用ioremap 时也要建立新页表,不同于 vmallocd的是它实际上不分配任何内存,ioremap 的返回值是一个特殊的虚拟地址,该地址用于存取特定的物理地址范围。使用它获取的地址要用iounmap 来释放。对于ioremap返回的地址在使用时最好使用/IO读写函数而不是直接访问。

五、 获得大块连续的物理内存

在内核启动后,尤其是运行一段时间后,就很难通过上述方法来获取大块物理上连续的内存区域,因为使用上述方法可以获取大块连续的物理内存的方法就是调用__get_free_pages,但是在内核运行一段时间后,可能就很难找到大块物理上连续的内存了。如果一个内核部件确实需要大块物理上连续的内存,那么最好的方法是在启动过程中就进行分配,然后保留给自己使用。

启动过程中分配并保留内存的方法是调用如下API:

#include <linux/bootmem.h>

void *alloc_bootmem(unsigned long size);

该函数用于分配指定大小的内存区域。

void *alloc_bootmem_low(unsigned long size);

该函数用于在低端地址区域分配指定大小的内存。低端地址区域指的是小于ARCH_LOW_ADDRESS_LIMIT的地址。

void *alloc_bootmem_pages(unsigned long size);

该函数用于分配指定大小的内存区域,但是分配的地址会对其到page上。

void *alloc_bootmem_low_pages(unsigned long size);

该函数用于在低端地址区域分配指定大小的内存。低端地址区域指的是小于ARCH_LOW_ADDRESS_LIMIT的地址。但是分配的地址会对其到page上。

需要注意的是这种分配是有限制的,即使用这种分配的代码必须在系统启动时就被加载运行,模块是不可能使用这种技术的。另外使用该技术分配的内存对内存管理子系统来说是不可见的,因而它会减少系统的可用内存。

使用该技术分配的内存可以用free_bootmem释放,但是这种释放并不能把内存释放给内存管理子系统(除非你在内存管理子系统进行初始化之前将其归还给了系统)。

除了使用bootmem之外,在较新的内核中还引入了一种新的机制来在启动阶段分配预留内存,这就是memblock。memblock这个部件在初始化阶段会获取系统的所有物理内存的信息,并将它们分为两类,常规内存和保留内存,在内存刚被发现时它都是常规内存,内核部件可以通过memblock_alloc这个API来申请并预留一片内存区域,通过memblock_free可以释放相应的内存区域,它们的工作机制类似于bootmem。不同于bootmem的是:

  1. bootmem需要在bootmem分配器初始化完成后才能调用,从代码上来说,bootmem在start_kernel->setup_arch->do_init_bootmem中进行初始化,也就是直到这一步之后它才能使用。而memblock的初始化则在:early_setup->early_init_devtree->early_init_dt_scan_memory_ppc(PPC为例),在PPC中,early_setup是在start_kernel之前的。从两者的功能上来说,也可以看出其先后,bootmem是一个内存分配器,它本身就需要使用物理内存,而memblock则完成物理内存的检测,因而bootmem可用的时间点必定在memblock之后
  2. memblock用链表来维护保留区域以及常规内存,而bootmem的管理则使用位图来管理,相对而言memblock更灵活(bootmem每次分配进行搜索时可能会从上一次分配结束的地址开始找到一块满足分配的连续区域,因而可能引入碎片,而且如果有大量分配,搜索位图也比较慢,这也是这个简单的分配器的一个弊端,具体的细节可以参考代码)。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐