全部博文(23)
2014年(23)
分类: 嵌入式
2014-09-21 20:25:05
因为内核为驱动提供了一个统一的内存管理接口,所以驱动程序无需涉及分段、分页等问题中。
1、kmalloc函数内幕
kmalloc是一个功能强大且高速(除非它阻塞)的工具。它分配的内存在物理内存中连续且保留原有的内容(不清零)。
1.1、flags 参数
kmalloc原型:
#include
void *kmalloc(size_t size,int flags);
说明:
size指明了要分配的内存块大小
flags为分配标志,可以用来控制kmalloc的行为
因为内存分配都是通过内部调用 __get_free_pages来实现的,所以分配标志带有GFP_ 前缀。所有的标志定义在
GFP_ATOMIC
用来在中断处理和进程上下文之外的其他代码中分配内存,从不睡眠。当前进程不应当被置为睡眠,这时驱动程序应当使用GFP_ATOMIC标志来调用kmalloc。内核通常地会保留一些空闲页来满足原子分配。当使用 GFP_ATOMIC 标志时,kmalloc 能够使用甚至最后一个空闲页。但是如果这最后一个空闲页不存在,则分配失败。
GFP_KERNEL
正常分配内核内存,可能睡眠。GFP_KERNEL是最常用的分配标志,意思是这个分配是代表运行在内核空间的进程而进行的。在内存紧缺时,以GFP_KERNRL调用kmalloc 的进程会进入睡眠状态来等待有一页可分配的内存。因此,一个使用GFP_KERNEL来分配内存的函数必须是可重入的(可中断的)并且不能在原子上下文中运行。
GFP_USER
用来为用户空间分配内存,可能睡眠。
GFP_HIGHUSER
如同GFP_USER,但是从高端内存分配,如果有高端内存。
GFP_NOIO
GFP_NOFS
这个标志功能如同GFP_KERNEL,但是为了满足需求,它们增加了对内核能做的行为的限制。GFP_NOFS分配不允许进行任何文件系统调用,而 GFP_NOIO则不允许进行任何I/O初始化。它们主要用在文件系统和虚拟内存代码,那里允许在无内存可分配时使进程睡眠,但是不应该发生递归的文件系统调用。
下列标志可以与上面列出的分配标志相或来控制内存的分配方式:
__GFP_DMA
要求分配的可用于DMA的内存区。
__GFP_HIGHMEM
这个标志指示分配的内存可以位于高端内存。
__GFP_COLD
通常内存分配器会尽量分配"缓冲热"的页(可能在处理器缓冲中找到的页)。相反,这个标志请求的是一个"冷"页,该页已有一段时间没被使用。在为DMA读缓冲分配内存页时,它很有用的,此时在处理器缓冲中出现的页反而是无用的(关于怎么分配DMA缓冲区详见第15章)。
__GFP_NOWARN
这个标志用于阻止内核发出警告(通过printk ),当一个分配无法满足时。
__GFP_HIGH
这个标志标识了一个高优先级请求,它可以使用甚至被内核保留给紧急状况时使用的最后的内存页。
__GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY
这些标志告知分配器在满足一个分配有困难时,该怎么做。__GFP_REPEAT 意思是通过重复尝试来"try a little harder" ,但是分配可能仍然失败。__GFP_NOFAIL标志告诉分配器分配只能成功不能失败,让分配器尽最大努力来满足要求(强烈建议不要使用__GFP_NOFAIL)。__GFP_NORETRY告知分配器立即放弃如果得不到请求的内存。
1.1.1、内存区
尽管__GFP_DMA和__GFP_HIGHMEM对所有平台都可有效,但它们都有一个平台相关的角色。
Linux 内核将内存分为3个内存区域:
1)、可用于DMA的内存:位于一个特别的地址范围,外设可以在这里进行DMA存取。
2)、常规内存。通常我们分配到的内存都位于常规内存,除非我们指定要分配特定内存区的标志(__GFP_DMA和__GFP_HIGHMEM)。
3)、高端内存:一个允许在32位平台存取(相对地)大量内存的机制。如果没有先建立一个特殊的映射,内核是无法直接访问这个内存的并且这个内存通常比较难使用。然而,如果你的驱动使用大量内存,并且它能够使用高端内存,那么在大系统中它会工作得更好(高端内存详细介绍见第15章的1.3节"高端和低端内存")。
通常地分配都发生于普通区,通过设置与请求内存区相关的标志可以分配到不同的内存区。
在分配一个新页来满足一个内存分配请求时,内核建立一个用于搜索的内存区列表。如果设置了__GFP_DMA标志,那么只搜索可用于DMA的内存区;如果在低端没有内存可用,则分配失败。如果没有指定分配特定内存区的标志,常规内存区和可用于DMA的内存区都会被搜索。如果设置了__GFP_HIGHMEM标志,3种内存区都会被搜索(但注意kmalloc不能分配高端内存)。
mm/page_alloc.c中包含了内存区的实现机制,而内存区的初始化则在平台特定的文件中,常常在arch目录树的mm/init.c。
1.2、size 参数
内核管理系统的物理内存,但物理内存按页分配。这导致kmalloc与用户空间malloc非常不同。内核使用一个特殊的面向页的分配技术来最好地利用系统RAM。
Linux 处理内存分配方法:创建一系列固定大小的内存对象池。处理分配请求时,就直接在持有足够大的内存对象的池子中传递一个内存对象(一整块内存)给请求者。
注意,内核只能分配某些预定义的,固定大小的字节数组。如果你请求一个任意数量的内存,分配到内存可能会多于实际请求的。
kmalloc能够分配最小的内存是32或64 字节(依赖于系统体系所使用的页大小),而kmalloc能够分配的内存块大小的上限随着体系和内核配置选项而变化。若要使代码完全可移植,则不应分配任何大于128KB的内存。若需要多于几个KB的内存卡,最好使用别的方法。
2、后备高速缓存
驱动程序常常需要反复分配许多大小相同的内存区,内核为这一情况增加了一些特殊的内存池,称为后备高速缓存。设备驱动通常不会涉及后背高速缓存,但是也有例外:在Linux 2.6中USB和SCSI驱动。
Linux 内核的高速缓存管理者有时称为" slab 分配器",相关函数和类型在<linux/slab.h> 中声明。slab分配器实现了kmem_cache_t类型的高速缓存,使用方法如下:
1)、创建高速缓存
|
2)、分配对象
一旦创建好高速缓存,就可以通过调用kmem_cache_alloc从已创建的高速缓存中分配对象:
/* 功能:从已创建的高速缓存中分配对象
* 参数 cache: 要从中分配对象的高速缓存
* flags: 与kmalloc的flags标志相同
* 返回值:成功时返回指向已分配好的对象的指针;否则返回NULL
*/
void *kmem_cache_alloc(kmem_cache_t *cache,int flags);
3)、释放对象
通过kmem_cache_free释放一个对象:
/* 功能:释放一个对象
* 参数 cache:该对象所属的高速缓存
* obj: 要释放的对象
*/
void kmem_cache_free(kmem_cache_t *cache,const void *obj);
4)、释放高速缓存
当驱动程序用完高速缓存后(通常在模块被卸载时),应当释放高速缓存:
/* 功能:释放一个高速缓存
* 参数 cache:要释放的高速缓存
* 返回值:仅当从该高速缓存中分配的所有对象都已返回给它时才成功。因此,我们应当检查kmem_cache_destroy返回值:失败时指示驱动模块存在内存泄漏(因为已丢失某些对象)
*/
int kmem_cache_destroy(kmem_cache_t *cache);
2.1、内存池
在内核中有不少地方内存分配不允许失败。为确保在这些情况下成功分配内存,内核建立了称为内存池("mempool")的抽象。它其实就是某种的后备高速缓存,尽力保持一些空闲内存供紧急时使用。使用mempool时必须注意:mempools会分配一些空闲、非真正要使用的内存块(预分配对象),所以容易消耗大量的内存。在驱动代码中,应当避免使用mempools。
内存池类型为mempool_t (在
1)、创建内存池:
|
典型地创建内存池代码:
cache = kmem_cache_create(...);
pool = mempool_create(MY_POOL_MINIMUM, mempool_alloc_slab,
mempool_free_slab, cache);
2)、分配和释放对象
一旦创建好内存池,就可以分配和释放对象了:
|
在内存池创建时,分配函数将被多次调用来创建一个预先分配的对象池。因此,对 mempool_alloc的调用试图用分配函数请求额外的对象;如果分配失败,返回预先分配的对象(如果存在)。用mempool_free释放对象时,如果预分配对象的数目少于最小量,将它保留在池中;否则,它将被返回给系统(真的释放对象)。
一个 mempool 可被重新定大小,使用:
|
用完内存池后,应将它返回给系统:
/* 功能:释放指定内存池,仅当free_fn休眠时该函数才会休眠。在所有分配对象都返回给内存池(即释放),才能调用该函数;否则会产生内核oops
* 参数 pool:要释放的内存池
*/
void mempool_destroy(mempool_t *pool);
3、get_free_page及相关函数
如果一个模块需要分配大块的内存,最好是使用面向页的分配技术。
1)、分配内存页
通过/proc/buddyi
nfo可知系统中每个内存区中的每个order有多少块可用。
/* get_zeroed_page返回一个指向新页的指针并用零填充了该页(清零) */
get_zeroed_page(unsigned int flags);
/* __get_free_page返回一个指向新页的指针但不清零该页 */
__get_free_page(unsigned int flags);
/* 分配若干个物理上连续的内存页并返回指向该内存区第一个字节的指针,不清零 */
__get_free_pages(unsigned int flags,unsigned int order);
/* 参数 flags:同kmalloc的flags参数
* order:要请求的或释放的页数,以2为底的对数。如果order太大(没有那么大的连续区可用),则分配失败。get_order函数,接收一个整型参数size(我们要分配的页数,必须是2的幂次)并从该size 中提取order(size的以2为底的对数)
*/
2)、释放内存页
用完内存页后,可以使用下列函数之一来释放它们。第一个函数是一个调用第二个函数的宏:
void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order);
如果你试图释放与你分配的页数不同的内存页,会破坏内存映射关系,系统会出错。
注意,只要符合和kmalloc 的相同规则,__get_free_pages 和其他的函数可以在任何时候调用。这些函数可能失败,特别当使用GFP_ATOMIC时。因此,调用这些分配函数的程序必须提供分配失败的处理。
在用户级别,可感觉到的区别主要是速度的提高和更好的内存使用,因为没有内部的内存碎片。页分配技术的主要优势实际上不是速度,而是更有效地使用内存。按页分配不浪费内存,而使用 kmalloc 由于分配的粒度会浪费无法预测数量的内存。__get_free_page函数的最大优势是获得的页完全属于调用者,且理论上可以通过适当的设置页表来将这些页使用成一个线性区域。
3.1、alloc_pages 接口
struct page是一个描述一个内存页的内部内核结构。
Linux页分配器的真正核心是一个称为alloc_pages_node的函数:
|
void __free_page(struct page *page);
void __free_pages(struct page *page,unsigned int order);
/* 若知道某个页面的内容是否驻留在处理器高速缓存中,应当使用free_hot_page (对于驻留在缓存的页)或者free_cold_page(对于没有驻留在缓存的页)通知内核,帮助内存分配器优化内存的使用。 */
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
4、vmalloc及相关函数
vmlloc是一个基本的Linux内存分配机制,它在虚拟内存空间分配一块连续的内存区。尽管这些页在物理内存中不连续(其中的每个页都通过单独调用alloc_page来获得),但内核认为它们地址是连续的(虚拟连续)。不推荐使用vmalloc,vmalloc分配的内存用起来效率比较低,并且在某些体系上,留给vmalloc的地址空间的数量相对较小。vmalloc函数原型及相关函数:
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void * addr);
void *ioremap(unsigned long offset,unsigned long size);
void iounmap(void * addr);
注意,kmalloc和_get_free_pages返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc在使用硬件上与kmalloc和__get_free_page没有什么不同,不同的是内核如何进行分配任务:kmalloc和__get_free_pages使用的(虚拟)地址范围和物理内存是一对一映射的,可能会偏移一个常量PAGE_OFFSET值,无需修改页表;vmalloc和ioremap使用的地址范围是完全地合成的,并且每次分配内存都要通过适当地设置页表来建立(虚拟)内存区域。可供vmalloc分配的内存地址范围: VMALLOC_START~VAMLLOC_END (定义在
注意,vamlloc比__get_free_pages开销更大,因为它必须获取内存并建立页表。因此,调用vmalloc来分配仅仅一页是不值得的。在内核中使用 vmalloc 的一个例子函数是create_module 系统调用,它使用 vmalloc 为在创建的模块获得空间。
使用vmalloc分配的内存由vfree释放。
如同vmalloc,ioremap也要建立新页表,不同的是它实际上不分配任何内存。ioremap的返回值是一个特殊的虚拟地址可用来存取特定的物理地址范围;通过调用iounmap来释放ioremap获得的虚拟地址。为了可移植性,不应当像内存指针那样直接存取由ioremap返回的虚拟地址,而应当使用I/O函数(见第9章)来访问这些虚拟地址。
ioremap和vmalloc是面向页的(它通过修改页表来工作)。因此,重定位或者分配的大小都会被上调到最近的页边界。ioremap模拟一个非对齐的映射通过下调被重映射的地址并返回第一个被重映射页内的偏移。
vmalloc的缺点:无法在原子上下文中使用。因为它内部使用kmalloc(GFP_KERNEL)来获取页表的存储空间,因此可能休眠。
5、Per-CPU 的变量
Per-CPU变量是一个有趣的2.6内核特性,定义在
/* 功能:在编译时创建一个Per-CPU变量
* 参数 name:变量名
* type:变量类型
*/
DEFINE_PER_CPU(type,name);
如果变量name是一个数组,必须在type参数里包含维数信息。例如,创建一个有3个整数的Per-CPU 数组:
DEFINE_PER_CPU(int[3],my_percpu_array);
虽然操作Per-CPU变量几乎不必使用明确的加锁,但必须记住2.6内核是可抢占的。对于一个处理器来说,应当避免在修改一个Per-CPU变量的临界区中被抢占,并且还要避免进程在对一个Per-CPU变量存取时被移动到另一个处理器上运行。因此,必须显式使用get_cpu_var宏来存取当前处理器的Per-CPU变量副本,并在存取结束后调用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);
当代码涉及到多处理器的per-CPU变量,就必须实现一个加锁机制来保证访问安全。
/* 动态分配Per-CPU变量 */
void *alloc_percpu(type);
/* 动态分配Per-CPU变量,并指定对齐方式 */
void *__alloc_percpu(size_t size,size_t align);
/* 释放动态分配的Per-CPU变量 */
void free_percpu(void *per_cpu_ptr);
/* per_cpu_ptr用于访问一个动态分配的Per-CPU变量,该宏返回一个指针指向该Per-CPU变量的指定(cpu_id)CPU的副本 */
per_cpu_ptr(void *per_cpu_var,int cpu_id);
若只是简单地读另一个CPU的这个变量的副本,则可以解引用这个指针并且用它来完成。若在操作当前处理器的变量副本,则必须首先保证不能被切换出那个处理器。如果存取这Per-CPU变量的整个过程都持有一个自旋锁,那万事大吉。但通常还是需要使用get_cpu 来阻止在使用Per-CPU变量时被抢占。因此,使用动态Per-CPU变量的典型代码如下:
int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var,cpu);
/* work with ptr */
put_cpu();
当使用编译时的Per-CPU 变量时,get_cpu_var和put_cpu_var宏会处理这些细节;而动态Per-CPU变量,则需要更多的显式保护。
Per-CPU变量可以导出给模块,但是必须使用一个特殊的宏版本:
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
要在模块内存取Per-CPU变量,需先通过DECLARE_PER_CPU来声明该变量:
DECLARE_PER_CPU(type,name);
DECLARE_PER_CPU(不是 DEFINE_PER_CPU)告知编译器进行一个外部引用。
若希望通过Per-CPU变量来创建一个简单的整数计数器,看一下在
注意,一些体系留给Per-CPU变量的地址空间有限。若在代码中创建Per-CPU变量,应当尽量保持变量较小。
6、获得大量缓冲
大量连续内存缓冲的分配是容易失败的。到目前止进行大I/O操作的最好方法是通过发散/汇聚操作(见第15章)。
6.1、在引导时获得专用的缓冲区
如果真的需要一大块物理上连续的内存区,最好的方法是在引导时请求内存来分配它。在引导时分配是获得大量连续内存页,避开__get_free_pages对缓冲大小限制(最大允许大小和限制大小的选择)的唯一方法。它也是最不易失败的。在引导时分配内存是一个"脏"技术,因为它通过保留一个私有的内存池来绕开所有的内存管理策略。一个模块无法在引导时分配内存; 只有直接连接到内核的驱动才可以。
对于普通用户来说,它不是一个灵活的选择,因为这个机制只对连接到内核映象中的代码可用。要安装或者替代使用这种分配方法的设备驱动,只能通过重新编译内核并且重启计算机。
当内核被引导时,它可以访问系统中所有可用的物理内存,接着调用每个子系统的初始化函数,允许初始化代码通过减少留给常规系统操作使用的RAM数量,来分配一个私有的内存缓冲给自己用。
调用下面任一函数,可在引导时分配专用内存:
#include <linux/bootmem.h>
/* _pages 结尾:以页为单元分配内存区
* 非_pages 结尾:分配非页对齐的内存区
* 非_low 版本:分配到的内存可能是高端内存(高端内存不一定总支持DMA,若希望分配到可用于DMA的内存,则应使用_low版本函数)
*/
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);
/* free_bootmem用于释放引导时分配的内存。很少在引导时释放分配的内存,但肯定不能在引导后取回它。注意,以这个方式释放的部分页不返回给系统 */
void free_bootmem(unsigned long addr,unsigned long size);