分类: LINUX
2013-02-21 05:00:11
原文地址:ldd3学习之八:内存分配 作者:leon_yu
size 参数
内核管理系统的物理内存,物理内存只能按页面进行分配。kmalloc 和典型的用户空间 malloc 在实际上有很大的差别,内核使用特殊的基于页的分配技术,以最佳的方式利用系统 RAM。Linux 处理内存分配的方法:创建一系列内存对象集合,每个集合内的内存块大小是固定。处理分配请求时,就直接在包含有足够大内存块的集合中传递一个整块给请求者。
必须注意的是:内核只能分配一些预定义的、固定大小的字节数组。kmalloc 能够处理的最小内存块是 32 或 64 字节(体系结构依赖),而内存块大小的上限随着体系和内核配置而变化。考虑到移植性,不应分配大于 128 KB的内存。若需多于几个 KB的内存块,最好使用其他方法。
flags 参数
内存分配最终总是调用 __get_free_pages 来进行实际的分配,这就是 GFP_ 前缀的由来。
所有标志都定义在
主要的标志常被称为分配优先级,包括:
最常用的标志,意思是这个分配代表运行在内核空间的进程进行。内核正常分配内存。当空闲内存较少时,可能进入休眠来等待一个页面。当前进程休眠时,内核会采取适当的动作来获取空闲页。所以使用 GFP_KERNEL 来分配内存的函数必须是可重入,且不能在原子上下文中运行。
GFP_ATOMIC内核通常会为原子性的分配预留一些空闲页。当当前进程不能被置为睡眠时,应使用 GFP_ATOMIC,这样kmalloc 甚至能够使用最后一个空闲页。如果连这最后一个空闲页也不存在,则分配返回失败。常在中断处理,和进程上下文之外的其他代码中用来分配内存,从不睡眠。
GFP_USER用来为用户空间分配内存页,可能睡眠。
GFP_HIGHUSER类似 GFP_USER,如果有高端内存,就从高端内存分配。
GFP_NOIOGFP_NOFS功能类似 GFP_KERNEL,但是为内核分配内存的工作增加了限制。具有GFP_NOFS 的分配不允许执行任何文件系统调用,而 GFP_NOIO 禁止任何 I/O 初始化。它们主要用在文件系统和虚拟内存代码。那里允许分配休眠,但不应发生递归的文件系统调。
Linux 内核把内存分为 3 个区段: 可用于DMA的内存(位于一个特别的地址范围的内存, 外设可以在这里进行 DMA 存取)、常规内存和高端内存(为了访问(相对)大量的内存而存在的一种机制)。目的是使每种计算机平台都必须知道如何将自己特定的内存范围归类到这三个区段中,而不是所有RAM都一样。 当要分配一个满足kmalloc要求的新页时, 内核会建立一个内存区段的列表以供搜索。若指定了 __GFP_DMA, 只有可用于DMA的内存区段被搜索;若没有指定特别的标志, 常规和 可用于DMA的内存区段都被搜索; 若 设置了 __GFP_HIGHMEM,所有的 3 个区段都被搜索(注意:kmalloc 不能分配高端内存)。 内存区段背后的机制在 mm/page_alloc.c 中实现, 且区段的初始化时平台相关的, 通常在 arch 目录树的 mm/init.c中。 |
有的标志用双下划线做前缀,他们可与上面标志“或”起来使用,以控制分配方式:
要求分配可用于DMA的内存。
__GFP_HIGHMEM分配的内存可以位于高端内存.
__GFP_COLD通常,分配器试图返回“缓存热(cache warm)”页面(可在处理器缓存中找到的页面)。 而这个标志请求一个尚未使用的“冷”页面。对于用作 DMA 读取的页面分配,可使用此标志。因为此时页面在处理器缓存中没多大帮助。
__GFP_NOWARN当一个分配无法满足,阻止内核发出警告(使用 printk )。
__GFP_HIGH高优先级请求,允许为紧急状况消耗被内核保留的最后一些内存页。
__GFP_REPEAT__GFP_NOFAIL__GFP_NORETRY告诉分配器当满足一个分配有困难时,如何动作。__GFP_REPEAT 表示努力再尝试一次,仍然可能失败; __GFP_NOFAIL 告诉分配器尽最大努力来满足要求,始终不返回失败,不推荐使用; __GFP_NORETRY 告知分配器如果无法满足请求,立即返回。
2.后备高速缓存
内核为驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookaside cache)。 设备驱动程序通常不会涉及后备高速缓存,但是也有例外:在 Linux 2.6 中 USB 和 SCSI 驱动。Linux 内核的高速缓存管理器有时称为“slab 分配器”,相关函数和类型在
(1)创建一个新的后备高速缓存
kmem_cache_t *kmem_cache_create(const char *name, size_t size,size_t offset, void (*destructor)(void *, kmem_cache_t *, unsigned long flags)); |
*name: 一个指向 name 的指针,name和这个后备高速缓存相关联,功能是管理信息以便追踪问题;通常设置为被缓存的结构类型的名字,不能包含空格。
size:每个内存区域的大小。
offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。
flags:控制分配方式的位掩码:
其他标志详见 mm/slab.c。但是,通常这些标志在只在开发系统中通过内核配置选项被全局性地设置。
参数constructor 和 destructor 是可选函数(不能只有destructor,而没有constructor ),用来初始化新分配的对象和在内存被作为整体释放给系统之前“清理”对象。
constructor 函数在分配一组对象的内存时被调用,由于内存可能持有几个对象,所以可能被多次调用。同理,destructor不是立刻在一个对象被释放后调用,而可能在以后某个未知的时间中调用。 根据它们是否被传递 SLAB_CTOR_ATOMIC 标志( CTOR 是 constructor 的缩写),控制是否允许休眠。由于当被调用者是constructor函数时,slab 分配器会传递 SLAB_CTOR_CONSTRUCTOR 标志。为了方便,它们可通过检测这个标志以使用同一函数。
(2)通过调用 kmem_cache_alloc 从已创建的后备高速缓存中分配对象:
void *kmem_cache_alloc(kmem_cache_t *cache, int flags); |
(3)使用 kmem_cache_free释放一个对象:
void kmem_cache_free(kmem_cache_t *cache, const void *obj); |
(4)当驱动用完这个后备高速缓存(通常在当模块被卸载时),释放缓存:
int kmem_cache_destroy(kmem_cache_t *cache); |
使用后备高速缓存的一个好处是内核会统计后备高速缓存的使用,统计情况可从 /proc/slabinfo 获得。
3.内存池为了确保在内存分配不允许失败情况下成功分配内存,内核提供了称为内存池( "mempool" )的抽象,它其实是某种后备高速缓存。它为了紧急情况下的使用,尽力一直保持空闲内存。所以使用时必须注意: mempool 会分配一些内存块,使其空闲而不真正使用,所以容易消耗大量内存 。而且不要使用 mempool 处理可能失败的分配。应避免在驱动代码中使用 mempool。
内存池的类型为 mempool_t ,在
(1)创建 mempool :
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, |
你可编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器为你处理这个任务:mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用 kmem_cache_alloc 和 kmem_cache_free 处理内存的分配和释放。
典型的设置内存池的代码如下:
cache = kmem_cache_create(. . .); |
(2)创建内存池后,分配和释放对象:
void *mempool_alloc(mempool_t *pool, int gfp_mask); |
可用一下函数重定义mempool预分配对象的数量:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask); |
(3)若不再需要内存池,则返回给系统:
void mempool_destroy(mempool_t *pool); |
4.get_free_page 相关函数
如果一个模块需要分配大块的内存,最好使用面向页的分配技术,驱动常用到。__get_free_page(unsigned int flags); |
get_order 函数可以用来从一个整数参数 size(必须是 2 的幂) 中提取 order,函数也很简单:
/* Pure 2^n version of get_order */ |
通过/proc/buddyinfo 可以知道系统中每个内存区段上的每个 order 下可获得的数据块数目。
当程序不需要页面时,它可用下列函数之一来释放它们。
void free_page(unsigned long addr); |
它们的关系是:
#define __get_free_page(gfp_mask) \ |
若试图释放和你分配的数目不等的页面,会破坏内存映射关系,系统会出错。
注意:只要符合和 kmalloc 的相同规则, __get_free_pages 和其他的函数可以在任何时候调用。这些函数可能失败(特别当使用 GFP_ATOMIC 时),因此调用这些函数的程序必须提供分配失败的处理。
从用户的角度,可感觉到的区别主要是速度提高和更好的内存利用率(因为没有内部的内存碎片)。但主要优势实际不是速度, 而是更有效的内存利用。 __get_free_page 函数的最大优势是获得的页完全属于调用者, 且理论上可以适当的设置页表将起合并成一个线性的区域。
5.alloc_pages 接口struct page 是一个描述一个内存页的内部内核结构,定义在
Linux 页分配器的核心是称为 alloc_pages_node 的函数:
struct page *alloc_pages_node(int nid, unsigned int flags, |
nid: 是要分配内存的 NUMA 节点 ID,
flags: 是 GFP_ 分配标志,
order: 是分配内存的大小.
返回值是一个指向第一个(可能返回多个页)page结构的指针, 失败时返回NULL。
alloc_pages 通过在当前 NUMA 节点分配内存( 它使用 numa_node_id 的返回值作为 nid 参数调用 alloc_pages_node)简化了alloc_pages_node调用。alloc_pages 省略了 order 参数而只分配单个页面。
释放分配的页:
void __free_page(struct page *page); |
5.vmalloc 和 ioremap
vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:
#include <linux/vmalloc.h> |
kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。
而vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END 的范围中,定义在
ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。
ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。
6.per-CPU 的变量
per-CPU 变量是一个有趣的 2.6 内核特性,定义在
DEFINE_PER_CPU(type, name); |
get_cpu_var(sockets_in_use)++; |
per_cpu(variable, int cpu_id); |
void *alloc_percpu(type); |
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
|
#include <linux/bootmem.h> |