天天看点

linux内核内存分配类型和方法获得大量缓冲

函数功能描述:

struct page alloc_pages(unsigned int flags, unsigned int order);函数以gfp_mask分配方式分配2的order次方(1<<order)个*连续的物理页*。

分配的页可以通过void page_address(struct page page)函数获得该page对应的逻辑地址指针。如果无需用到struct page可以直接用__get_free_pages(unsigned int flags, unsigned int order)分配并返回一个指向一个内存区第一个字节的指针, 内存区是几个(*物理上连续)页长但是*没有清零*。还可以使用get_zeroed_page(unsigned int flags);返回一个指向新页的指针并且用*零填充了该页*.

输入参数说明:

gfp_mask:是分配标志,内核分配内存有多种方式,该参数告诉内核如何分配以及在哪分配所需的内存,内存分配最终总是调用 _get_free_pages( ) 来实现,这也是 GFP 前缀的由来。其中分配标志(gfp_mask)可以取以下各值:

GFP_KERNEL 该分配方式最常用,是内核内存的正常分配,它可能睡眠。

GFP_ATOMIC 该分配方式常用来从中断处理和进程上下文之外的其他代码中分配内存,从不睡眠。

GFP_USER 用来为用户空间分配内存页,可能睡眠。

GFP_HIGHUSER 类似GFP_USER,如果有高端内存,就从高端内存分配页。

GFP_NOIO

GFP_NOFS 功能类似于GFP_KERNEL,但是为内核分配内存的工作增加了限制。具有GFP_NOFS 的分配不允许执行任何文件系统调用,而 GFP_NOIO 禁止任何 I/O 初始化。它们主要用在文件系统和虚拟内存代码,那里允许分配休眠,但不应发生递归的文件系统调用。

有的标志用双下划线做前缀,他们可与上面标志“或”起来使用,以控制分配方式:

_GFP_DMA 要求分配可用于DMA的内存。

_GFP_HIGHMEM 分配的内存可以位于高端内存。

_GFP_NOWARN 当一个分配无法满足,阻止内核发出警告(使用 printk )。

_GFP_HIGH 高优先级请求,允许为紧急状况消耗被内核保留的最后一些内存页。

_GFP_REPEAT

_GFP_NOFAIL

_GFP_NORETRY 告诉分配器当满足一个分配有困难时,如何动作。_GFP_REPEAT 表示努力再尝试一次,仍然可能失败;__GFP_NOFAIL告诉分配器尽最大努力来满足要求,始终不返回失败,不推荐使用;__GFP_NORETRY 告知分配器如果无法满足请求,立即返回。

order:指要分配的物理页数,其取值为2的order次方个。

返回参数说明:

alloc_pages( )函数返回page结构体指针,指向所分配的物理页中的第一个页,如果分配不成功,则返回NULL。

void *kmalloc(size_t size, int flags),分配物理上连续的虚拟内存,最终调用__get_free_pages函数进行分配内存,基于slab实现。slab不能使用dma和highmem内存。那么kmalloc使用的也就只有normal了。

输入参数说明:

给 kmalloc 的第一个参数是要分配的块的大小. 内核管理系统的物理内存, 这些物理内存只是以页大小的块来使用. 结果是, kmalloc 看来非常不同于一个典型的用户空间 malloc 实现. 一个简单的, 面向堆的分配技术可能很快有麻烦; 它可能在解决页边界时有困难. 因而, 内核使用一个特殊的面向页的分配技术来最好地利用系统 RAM.Linux 处理内存分配通过创建一套固定大小的内存对象池. 分配请求被这样来处理, 进入一个持有足够大的对象的池子并且将整个内存块递交给请求者. 内存管理方案是非常复杂, 并且细节通常不是全部设备驱动编写者都感兴趣的.然而, 驱动开发者应当记住的一件事情是, 内核只能分配某些预定义的, 固定大小的字节数组. 如果你请求一个任意数量内存, 你可能得到稍微多于你请求的, 至多是 2 倍数量. 同样, 程序员应当记住 kmalloc 能够处理的最小分配是 32 或者 64 字节, 依赖系统的体系所使用的页大小.kmalloc 能够分配的内存块的大小有一个上限. 这个限制随着体系和内核配置选项而变化. 如果你的代码是要完全可移植, 它不能指望可以分配任何大于 128 KB. 如果你需要多于几个 KB, 但是, 有个比 kmalloc 更好的方法来获得内存, 也就是前面讨论的page分配函数。

第二个参数, 分配标志,同alloc_pages函数的flags参数一样。 非常有趣, 因为它以几个方式控制 kmalloc 的行为.最一般使用的标志, GFP_KERNEL, 意思是这个分配((内部最终通过调用 _get_free_pages 来进行, 它是 GFP 前缀的来源) 代表运行在内核空间的进程而进行的. 换句话说, 这意味着调用函数是代表一个进程在执行一个系统调用. 使用 GFP_KENRL 意味着 kmalloc 能够使当前进程在少内存的情况下睡眠来等待一页. 一个使用 GFP_KERNEL 来分配内存的函数必须, 因此, 是可重入的并且不能在原子上下文中运行. 当当前进程睡眠, 内核采取正确的动作来定位一些空闲内存, 或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存.GFP_KERNEL 不一直是使用的正确分配标志; 有时 kmalloc 从一个进程的上下文的外部调用. 例如, 这类的调用可能发生在中断处理, tasklet, 和内核定时器中. 在这个情况下, 当前进程不应当被置为睡眠, 并且驱动应当使用一个 GFP_ATOMIC 标志来代替. 内核正常地试图保持一些空闲页以便来满足原子的分配. 当使用 GFP_ATOMIC 时, kmalloc 能够使用甚至最后一个空闲页. 如果这最后一个空闲页不存在, 但是, 分配失败。

纠正一个错误,由于kmalloc分配的内存大小有限制,当需要大块的连续物理内存时,使用前面讨论的page分配函数(alloc_pages)很可能分配失败,所以需要在系统启动请求内存分配。

获得大量缓冲

我们我们已经在前面章节中注意到的, 大量连续内存缓冲的分配是容易失败的. 系统内存长时间会碎片化, 并且常常出现一个真正的大内存区会完全不可得. 因为常常有办法不使用大缓冲来完成工作, 内核开发者没有优先考虑使大分配能工作. 在你试图获得一个大内存区之前, 你应当真正考虑一下其他的选择. 到目前止最好的进行大 I/O 操作的方法是通过发散/汇聚操作, 我们在第 1 章的"发散-汇聚 映射"一节中讨论了.

在启动时获得专用的缓冲

如果你真的需要一个大的物理上连续的缓冲, 最好的方法是在启动时请求内存来分配它. 在启动时分配是获得连续内存页而避开 __get_free_pages 施加的对缓冲大小限制的唯一方法, 不但最大允许大小还有限制的大小选择. 在启动时分配内存是一个"脏"技术, 因为它绕开了所有的内存管理策略通过保留一个私有的内存池. 这个技术是不优雅和不灵活的, 但是它也是最不易失败的. 不必说, 一个模块无法在启动时分配内存; 只有直接连接到内核的驱动可以这样做.

启动时分配的一个明显问题是对通常的用户它不是一个灵活的选择, 因为这个机制只对连接到内核映象中的代码可用. 一个设备驱动使用这种分配方法可以被安装或者替换只能通过重新建立内核并且重启计算机.

当内核被启动, 它赢得对系统种所有可用物理内存的存取. 它接着初始化每个子系统通过调用子系统的初始化函数, 允许初始化代码通过减少留给正常系统操作使用的 RAM 数量, 来分配一个内存缓冲给自己用.

启动时内存分配通过调用下面一个函数进行:

#include <linux/bootmem.h>

void *alloc_bootmem(unsigned long size);

void *alloc_bootmem_low(unsigned long size);

void *alloc_bootmem_pages(unsigned long size);

void *alloc_bootmem_low_pages(unsigned long size);

这些函数分配或者整个页(如果它们以 _pages 结尾)或者非页对齐的内存区. 分配的内存可能是高端内存除非使用一个 _low 版本. 如果你在为一个设备驱动分配这个缓冲, 你可能想用它做 DMA 操作, 并且这对于高端内存不是一直可能的; 因此, 你可能想使用一个 _low 变体.

很少在启动时释放分配的内存; 你会几乎肯定不能之后取回它, 如果你需要它. 但是, 有一个接口释放这个内存:

void free_bootmem(unsigned long addr, unsigned long size);

注意以这个方式释放的部分页不返回给系统 -- 但是, 如果你在使用这个技术, 你已可能分配了不少数量的整页来用.

如果你必须使用启动时分配, 你需要直接连接你的驱动到内核. 应当如何完成的更多信息看在内核源码中 Documentation/kbuild 下的文件.

void *vmalloc(unsigned long size);分配物理上不连续的虚拟内存,最终调用__get_free_pages函数进行分配内存,不基于slab实现。从highmem内存进行分配。

输入参数说明:size,分配内存的大小。

vmalloc 分配的地址不能用于微处理器之外, 因为它们只在处理器的 MMU 之上才有意义. 当一个驱动需要一个真正的物理地址(例如一个 DMA 地址, 被外设硬件用来驱动系统的总线), 你无法轻易使用 vmalloc. 调用 vmalloc 的正确时机是当你在为一个大的只存在于软件中的顺序缓冲分配内存时. 重要的是注意 vamlloc 比 __get_free_pages 有更多开销, 因为它必须获取内存并且建立页表. 因此, 调用 vmalloc 来分配仅仅一页是无意义的。在内核中使用 vmalloc 的一个例子函数是 create_module 系统调用, 它使用 vmalloc 为在创建的模块获得空间. 模块的代码和数据之后被拷贝到分配的空间中, 使用 copy_from_user. 在这个方式中, 模块看来是加载到连续的内存. 你可以验证, 同过看 /proc/kallsyms, 模块输出的内核符号位于一个不同于内核自身输出的符号的内存范围.

在内核中使用 vmalloc 的一个例子函数是 create_module 系统调用, 它使用 vmalloc 为在创建的模块获得空间. 模块的代码和数据之后被拷贝到分配的空间中, 使用 copy_from_user. 在这个方式中, 模块看来是加载到连续的内存. 你可以验证, 同过看 /proc/kallsyms, 模块输出的内核符号位于一个不同于内核自身输出的符号的内存范围.vmalloc 的一个小的缺点在于它无法在原子上下文中使用。

slab后备缓存,当你需要使用很多类型相同的数据结构或变量时,可以使用slab,它会尽量减少内存分配和释放的性能消耗。

一个设备驱动常常以反复分配许多相同大小的对象而结束. 如果内核已经维护了一套相同大小对象的内存池, 为什么不增加一些特殊的内存池给这些高容量的对象? 实际上, 内核确实实现了一个设施来创建这类内存池, 它常常被称为一个后备缓存slab. 设备驱动常常不展示这类的内存行为, 它们证明使用一个后备缓存是对的, 但是, 有例外; 在 Linux 2.6 中 USB 和 SCSI 驱动使用缓存.

Linux内核的缓存管理者有时称为"slab分配器". 因此, 它的功能和类型在 <linux/slab.h> 中声明. slab 分配器实现有一个 kmem_cache_t 类型的缓存; 使用一个对 kmem_cache_create 的调用来创建它们:

kmem_cache_t *kmem_cache_create(const char *name, size_t size,

size_t offset,

unsigned long flags,

void (*constructor)(void *, kmem_cache_t *,

unsigned long flags), void (*destructor)(void *, kmem_cache_t *, unsigned long flags));

这个函数创建一个新的可以驻留任意数目全部同样大小的内存区的缓存对象, 大小由 size 参数指定. name 参数和这个缓存关联并且作为一个在追踪问题时有用的管理信息; 通常, 它被设置为被缓存的结构类型的名子. 这个缓存保留一个指向 name 的指针, 而不是拷贝它, 因此驱动应当传递一个指向在静态存储中的名子的指针(常常这个名子只是一个文字字串). 这个名子不能包含空格.

offset 是页内的第一个对象的偏移; 它可被用来确保一个对被分配的对象的特殊对齐, 但是你最可能会使用 0 来请求缺省值. flags 控制如何进行分配并且是下列标志的一个位掩码:

SLAB_NO_REAP 设置这个标志保护缓存在系统查找内存时被削减. 设置这个标志通常是个坏主意; 重要的是避免不必要地限制内存分配器的行动自由.

SLAB_HWCACHE_ALIGN 这个标志需要每个数据对象被对齐到一个缓存行; 实际对齐依赖主机平台的缓存分布. 这个选项可以是一个好的选择, 如果在 SMP 机器上你的缓存包含频繁存取的项. 但是, 用来获得缓存行对齐的填充可以浪费可观的内存量.

SLAB_CACHE_DMA 这个标志要求每个数据对象在 DMA 内存区分配.

还有一套标志用来调试缓存分配; 详情见 mm/slab.c. 但是, 常常地, 在用来开发的系统中, 这些标志通过一个内核配置选项被全局性地设置

函数的 constructor 和 destructor 参数是可选函数( 但是可能没有 destructor, 如果没有 constructor ); 前者可以用来初始化新分配的对象, 后者可以用来"清理"对象在它们的内存被作为一个整体释放回给系统之前.构造函数和析构函数会有用, 但是有几个限制你必须记住. 一个构造函数在分配一系列对象的内存时被调用; 因为内存可能持有几个对象, 构造函数可能被多次调用. 你不能假设构造函数作为分配一个对象的一个立即的结果而被调用. 同样地, 析构函数可能在以后某个未知的时间中调用, 不是立刻在一个对象被释放后. 析构函数和构造函数可能或不可能被允许睡眠, 根据它们是否被传递 SLAB_CTOR_ATOMIC 标志(这里 CTOR 是 constructor 的缩写).为方便, 一个程序员可以使用相同的函数给析构函数和构造函数; slab 分配器常常传递 SLAB_CTOR_CONSTRUCTOR 标志当被调用者是一个构造函数.

一旦一个对象的缓存被创建, 你可以通过调用 kmem_cache_alloc 从它分配对象.

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

这里, cache 参数是你之前已经创建的缓存; flags 是你会传递给 kmalloc 的相同, 并且被参考如果 kmem_cache_alloc 需要出去并分配更多内存.

为释放一个对象, 使用 kmem_cache_free:

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

当驱动代码用完这个缓存, 典型地当模块被卸载, 它应当如下释放它的缓存:

int kmem_cache_destroy(kmem_cache_t *cache);

这个销毁操作只在从这个缓存中分配的所有的对象都已返回给它时才成功. 因此, 一个模块应当检查从 kmem_cache_destroy 的返回值; 一个失败指示某类在模块中的内存泄漏(因为某些对象已被丢失.)

使用后备缓存的一方面益处是内核维护缓冲使用的统计. 这些统计可从 /proc/slabinfo 获得.

Qingsheng Shen wrote:

函数功能描述:

struct page alloc_pages(unsigned int flags, unsigned int order);函数以gfp_mask分配方式分配2的order次方(1<<order)个*连续的物理页*。

分配的页可以通过void page_address(struct page page)函数获得该page对应的逻辑地址指针。如果无需用到struct page可以直接用__get_free_pages(unsigned int flags, unsigned int order)分配并返回一个指向一个内存区第一个字节的指针, 内存区是几个(*物理上连续)页长但是*没有清零*。还可以使用get_zeroed_page(unsigned int flags);返回一个指向新页的指针并且用*零填充了该页*.

通过alloc_pages分配的内存对于低端地址可以用page_address函数获得该page对应的逻辑地址的指针,但是对于高于896MB的高端内存,需要使用kmap或者kmap_atomic函数来建立映射。

永久映射--kmap

这个函数在高端内存和低端内存上都可以使用,当内存是低端内存是它会调用page_address函数返回逻辑地址,当是高端内存时,则需要先建立永久映射,然后返回逻辑地址。这个函数可能会睡眠,所以只能用在进程上下文中。

临时映射(原子映射)--kmap_atomic

当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射。有一组保留的映射,他们可以存放新创建的临时映射。内核可以原子的把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝对不会阻塞。

同样它也禁止内核抢占,这个非常必要,因为映射对每个处理器都是唯一的,调度可能对哪个处理器执行哪个进程做变动。

临时映射可以用在低端内存和高端内存,当内存是低端内存是它会调用page_address函数返回逻辑地址,当是高端内存时,才会真正的使用临时映射机制。¶

每-CPU 变量是一个有趣的 2.6 内核的特性. 当你创建一个每-CPU变量, 系统中每个处理器获得它自己的这个变量拷贝. 这个可能象一个想做的奇怪的事情, 但是它有自己的优点. 存取每-CPU变量不需要(几乎)加锁, 因为每个处理器使用它自己的拷贝. 每-CPU 变量也可存在于它们各自的处理器缓存中, 这样对于频繁更新的量子带来了显著的更好性能.

一个每-CPU变量的好的使用例子可在网络子系统中找到. 内核维护无结尾的计数器来跟踪有每种报文类型有多少被接收; 这些计数器可能每秒几千次地被更新. 不去处理缓存和加锁问题, 网络开发者将统计计数器放进每-CPU变量. 现在更新是无锁并且快的. 在很少的机会用户空间请求看到计数器的值, 相加每个处理器的版本并且返回总数是一个简单的事情.

每-CPU变量的声明可在 <linux/percpu.h> 中找到. 为在编译时间创建一个每-CPU变量, 使用这个宏定义:

DEFINE_PER_CPU(type, name);

如果这个变量(称为 name 的)是一个数组, 包含这个类型的维数信息. 因此, 一个有 3 个整数的每-CPU 数组应当被创建使用:

DEFINE_PER_CPU(int3, my_percpu_array);

每-CPU变量几乎不必使用明确的加锁来操作. 记住 2.6 内核是可抢占的; 对于一个处理器, 在修改一个每-CPU变量的临界区中不应当被抢占. 并且如果你的进程在对一个每-CPU变量存取时将, 要被移动到另一个处理器上, 也不好. 因为这个原因, 你必须显式使用 get_cpu_var 宏来存取当前处理器的给定变量拷贝, 并且当你完成时调用 put_cpu_var. 对 get_cpu_var 的调用返回一个 lvalue 给当前处理器的变量版本并且禁止抢占. 因为一个 lvalue 被返回, 它可被赋值给或者直接操作. 例如, 一个网络代码中的计数器时使用这 2 个语句来递增的:

get_cpu_var(sockets_in_use)++;

put_cpu_var(sockets_in_use);

你可以存取另一个处理器的变量拷贝, 使用:

per_cpu(variable, int cpu_id);

如果你编写使处理器涉及到对方的每-CPU变量的代码, 你, 当然, 一定要实现一个加锁机制来使存取安全.

动态分配每-CPU变量也是可能的. 这些变量可被分配, 使用:

void *alloc_percpu(type);

void *__alloc_percpu(size_t size, size_t align);

在大部分情况, alloc_percpu 做的不错; 你可以调用 __alloc_percpu 在需要一个特别的对齐的情况下. 在任一情况下, 一个 每-CPU 变量可以使用 free_percpu 被返回给系统. 存取一个动态分配的每-CPU变量通过 per_cpu_ptr 来完成:

per_cpu_ptr(void *per_cpu_var, int cpu_id);

这个宏返回一个指针指向 per_cpu_var 对应于给定 cpu_id 的版本. 如果你在简单地读另一个 CPU 的这个变量的版本, 你可以解引用这个指针并且用它来完成. 如果, 但是, 你在操作当前处理器的版本, 你可能需要首先保证你不能被移出那个处理器. 如果你存取这个每-CPU变量的全部都持有一个自旋锁, 万事大吉. 常常, 但是, 你需要使用 get_cpu 来阻止在使用变量时的抢占. 因此, 使用动态每-CPU变量的代码会看来如此:

int cpu;

cpu = get_cpu()

ptr = per_cpu_ptr(per_cpu_var, cpu);

put_cpu();

当使用编译时每-CPU 变量时, get_cpu_var 和 put_cpu_var 宏来照看这些细节. 动态每-CPU变量需要更多的显式的保护.

每-CPU变量能够输出给每个模块, 但是你必须使用一个特殊的宏版本:

EXPORT_PER_CPU_SYMBOL(per_cpu_var);

EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);

为在一个模块内存取这样一个变量, 声明它, 使用:

DECLARE_PER_CPU(type, name);

DECLARE_PER_CPU 的使用(不是 DEFINE_PER_CPU)告知编译器进行一个外部引用.

如果你想使用每-CPU变量来创建一个简单的整数计数器, 看一下在 <linux/percpu_counter.h> 中的现成的实现. 最后, 注意一些体系有有限数量的地址空间变量给每-CPU变量. 如果你创建每-CPU变量在你自己的代码, 你应当尽量使它们小.

继续阅读