[置顶] linux设备驱动(十四)–内存分配

linux内核为设备驱动程序提供了一致的内存管理接口,所以模块不涉及到分段、分页的问题。

kmalloc函数

kmalloc与malloc相似,除非被阻塞,否则这个函数可运行的很快,而且不对获取的空间清零,也就是说,分配给它的区域仍然保持着原有数据。它分配的内存在物理内存中是连续的。原型如下:

#include <linux/slab.h>

void *kmalloc(size_t size,int flags);

最常用的标志是:GFP_KERNEL,它表示内存分配(最终总是调用get_free_page来实现的,这就是GFP_的由来)是代表运行在内核空间的进程执行的。这意味着调用它的函数正代表某个进程执行系统调用。使用GFP_KERNEL允许kmalloc在空闲空间较少时把当前进程转入休眠以等待一个页面。当调用kmalloc进入休眠时,内核会采取适当的行动,或者是把缓存区的内容写到硬盘上,或者是从一个用户进程换出内存,以获取一个内存页面。

GFP_KERNEL标志并不是始终适用,有时kmalloc是在进程下上文以外被调用的,例如在中断处理例程、tasklet以及内核定时器中调用。这种情况下current进程就不应该休眠,驱动程序应该调用GFP+ATOMIC标志。内核通常会为原子性的分配预留一些空间页面。使用GFP_ATOMIC,kmalloc甚至可以用掉最后一个空闲页面,如果最后一页也没有则会返回失败。

几个常用的标志:

GFP_ATOMIC:用于在中断处理例程或者其他运行于进程上下文以外的代码中分配内存,不会休眠。

GFP_KERNEL:通常的内存分配方法,可能会休眠。

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

GFP_HIGHUSER:同上,不过会从高端内存部分分配。

GFP_NOIO GFP_NOFS功能类似 GFP_KERNEL,但是为内核分配内存的工作增加了限制。具有GFP_NOFS 的分配不允许执行任何文件系统调用而GFP_NOIO 禁止任何 I/O 初始化。它们主要用在文件系统和虚拟内存代码。那里允许分配休眠,但不应发生递归的文件系统调用。上面列出的分配标志可以和下面的标志“或”起来使用。__GFP_DMA:该标志请求分配发生在可进行DMA的内存区段中。具体的含义是平台相关的。__GFP_HIGHMEM:这个标志标明要分配的内存可位于高端内存.__GFP_COLD:T通常,内存分配会试图返回”缓存热“页面,即可在高速缓存中找到的页面。相反这个标志使用冷页面,对用于DMA读取的页面分配,可使用这个标志。__GFP_HIGH:该标志标记一个高优先级的请求,他允许为紧急情况而消耗最后一个页面。__GFP_REPEAT __GFP_NOFAIL __GFP_NORETRY

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

内存分配布局:linux内存分为三个区段,可用于DMA的内存,常规内存以及高端内存。通常的内存分配都发生在常规内存区域,但通过设置上面介绍过的标志也可以请求在其他区段中分配。可用于DMA的内存中存在于特别地址范围内的内存,外设可以利用这些内存执行DMA请求。高端内存是32位平台为了访问(相对)大量的内存而存在的一种机制。如果不首先完成一些特殊的映射,我们就无法从内核直接访问这些内存。,但是如果驱动程序要使用大量的内存,那么在能够使用高端内存的大系统上可以工作的很好。当要分配一个满足kmalloc要求的新页时, 内核会建立一个内存区段的列表以供搜索。若指定了 __GFP_DMA, 只有可用于DMA的内存区段被搜索;若没有指定特别的标志, 常规和 可用于DMA的内存区段都被搜索; 若 设置了 __GFP_HIGHMEM,所有的 3 个区段都被搜索(注意:kmalloc 不能分配高端内存)。

内存区段背后的机制在 mm/page_alloc.c 中实现, 且区段的初始化时平台相关的, 通常在 arch 目录树的 mm/init.c中。

SIZE参数

物理内存只能按页进行分配,其结果是kmalloc和典型的用户空间malloc在实现上有很大的差别。简单的基于堆的内存分配技术会遇到麻烦,因为页面边界称为一个棘手的问题。因此内核使用了特殊的基于页的分配技术,以最佳地利用系统RAM。

Linux分配内存的方法是创建一系列的内存缓存池,每个池中的内存块大小固定一致的,处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。

内核只能分配一些预定义的,固定大小的字节数组。如果申请任意数量的内存,如果申请任意数量的内存,那么得到的可能会多一些,最多会是申请数量的两倍。kmalloc能够分配的最小内存快是32或者64,到底是哪个则取决于当前体系结构使用的页面大小。

对kmalloc能够分配的内存块的大小,存在一个上限。这个限制随着体系结构的不同以及内核配置不同而不同,如果我们希望有好的移植性,则不应该分配大于128KB的内存。否则运用其他的方法。

后备高速缓存:

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与这个高速缓存相关联,其功能是报关一些信息以便追踪问题,通常被设置为将要高速缓存的结构类型的名字。高速缓存保留指向该名称的指针,而不是复制其内容。因此,驱动程序应该将指向静态存储的指针传递给这个函数,名称中不能包含空白。

offset参数是页面中第一个对象的偏移量,它可以用来确保对已分配的对象进行某种形式的对齐,默认取零。

flags控制任何分配完成,是一个掩码:

SLAB_NO_REAP 保护缓存在系统查找内存时不被削减,不推荐。

SLAB_HWCACHE_ALLGN 这个标志要求所有数据对象跟高速缓存行对齐;实际的操作则依赖与主机平台的硬件高速缓存布局。如果在SMP机器上,高速缓存中包含有频繁访问的数据项的话,则该选择这项。但为了保持对齐,会浪费很多内存。

SLAB_CACHE_DMA 这个标志要求每个对象都从可用DMA的内存区段分配。

constructor和destructor参数是可选参数,但必须成对出现。constructor 函数在分配一组对象的内存时被调用,由于内存可能持有几个对象,所以可能被多次调用。我们不能认为分配一个对象后随之就会调用constrcuctor。同理,destructor不是立刻在一个对象被释放后调用,而可能在以后某个未知的时间中调用。 根据它们是否被传递 SLAB_CTOR_ATOMIC 标志( CTOR 是 constructor 的缩写),控制是否允许休眠。由于当被调用者是constructor函数时,slab 分配器会传递 SLAB_CTOR_CONSTRUCTOR 标志。为了方便,它们可通过检测这个标志以使用同一函数。

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

void *kmem_cache_alloc(kmem_cache_t *cache,int flags);(先创建高速缓存,再分配)

参数cache是先前创建的高速缓存,flags标志和kmalloc相同。

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

表示从缓存池里,删除对象。

如果驱动程序代码中和高速缓存有关的部分已经处理晚了(例如驱动模块被卸载的时候),这时驱动程序应该释放它的高速缓存:

int kmem_cache_destory(kmem_cache_t *cache);

这个释放只有在已将从缓存中分配的所有对象都归还后才能成功。所以模块应该检查kmem_cacha_destory的返回状态;如果失败,则表明模块中发生了内存泄露(因为一些对象被漏掉了)。

使用后备高速缓存的一个好处是内核会统计后备高速缓存的使用,统计情况可从 /proc/slabinfo 获得。

基于slab的scullc

首先,我们必须声明自己的slab分配器:

kmem_cache_t *scullc_cache; //声明一个高速缓存指针,将用于所有设备

slab高速缓存创建代码如下:

scullc_cache = kmem_cache_create(“scullc”,scullc_quantum,0,NULL,NULL);

if(!scullc_cache){

scullc_cleanup();

return -ENOMEM;

分配内存量子:

if(!dptr->data[s_pos]){

dptr->data[s_pos] = kmem_cache_alloc(scullc_cache,GFP_KERNEL);

if(!dptr->data[s_pos])

goto nomem;

memset(dptr->data[s_pos],0,scullc_quantum);

}

释放内存:

for(i=0;i<qset;i++){

if(dptr->data[i])

kmem_cache_free(scullc_cache,dptr->data[i]);

最后在模块卸载之前,将申请的高速缓存返还给系统:

if(scullc_cache)

kmem_cache_destory(scullc_cache);

和使用kmalloc的scull相比,scullc的最主要差别是运行速度略有提高,并且对内存的利用率更佳。由于数据对象是从内存池中分配的,而内存池中的所有对象具有相同的大小,所以这些数据对象在内存中的位置排列达到了最大程度的密集。

内存池

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

所以使用时必须注意: mempool 会分配一些内存块,使其空闲而不真正使用,所以容易消耗大量内存 。而且不要使用 mempool 处理可能失败的分配。应避免在驱动代码中使用 mempool。

内存池的类型为mempool_t(在<linux/mempool.h>中定义),可使用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_t和free_t来完成,原型如下:

typedef void *(mempool_alloc_t)(int gfp_mask,void *pool_data); 用于返回分配了的内存池指针。

typedef void (mempool_free_t)(void *element,void *pool_data); 释放内存池元素。

mempool_create的最后一个元素,pool_data被传入到mempool_alloc_t和mempool_free_t。

你可编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器为你处理这个任务:mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用 kmem_cache_alloc 和 kmem_cache_free处理内存的分配和释放。构造内存池的代码:

cache = kmem_cache_create(“mempool”,scullc_quantum,0,NULL,NULL); //分配内存

pool = mempoll_create(MY_POOL_MINOR,mempool_alloc_slab,mempool_free_slab,cache); //建立内存池

这里仅仅是建立了一个内存池,用一下函数来分配和释放对象:

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

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

在创建mempool时,分配函数(mempool_alloc_slab)将被调用多次来创建预先分配的对象。因此,对 mempool_alloc 的调用是试图用分配函数请求额外的对象,如果失败,则返回预先分配的对象(如果存在)。用 mempool_free 释放对象时,若预分配的对象数目小于最小量,就将它保留在池中,否则将它返回给系统。

可用以下函数重定义mempool预分配对象的数量:

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);/*若成功,内存池至少有 new_min_nr 个对象*/

(3)若不再需要内存池,则返回给系统:

void mempool_destroy(mempool_t *pool); /*在销毁 mempool 之前,必须返回所有分配的对象,否则会产生 oops*/

get_free_page和相关函数

如果模块需要分配大量的内存,则应该使用面向页的分配技术。

get_zeroed_page(unsigned int flags); 返回指向新页面的指针并清零

__get_free_page(unsigned int flags); 同上,但是不清零

__get_free_pages(unsigned int flags,unsigned int order); 分配若干连续的界面,并返回指向该区域第一个字节的指针,但不清零。

当程序不再需要使用页面时,它可以使用一下函数来释放:

void free_page(unsigned long addr);

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

其实__get_free_page和free_page也是对__get_free_pages和free_pages的调用,是order为0时。

kmalloc其实也是对get_free_page的调用,get_free_page和其他函数可以在任何时间调用。某些情况下函数分配内存失败,特别是使用了GFP_ATOMIC时,所以调用这些函数的程序都应提供相应的出错处理。

尽管kmalloc(GFP_KERNEL)在没有空闲内存时可能失败,但内核总会尽可能来满足这个请求。因此如果分配太多内存,系统的响应性能就很容易将下来;当系统为满足kmalloc分配请求而试图换出尽可能多的内存页时,就会变的很慢,所以不应该用kmalloc来分配大量内存。

使用整页的scull:scullp

if(!dptr->data[i]){

dptr->data[i] = (void *)get_free_pages(GFP_KERNEL,dptr->order);

if(!dptr->data[i])

goto nomem;

memset(dptr->data[i],0,PAGE_SIZE<<dptr->order);

}

释放代码:

for(i=0;i<qset;i++){

if(dptr->data[i])

free_pages((unsigned long)(dptr->data[i]),dptr->order);

}

从用户的角度,可感觉到的区别主要是速度提高和更好的内存利用率(因为没有内部的内存碎片)。但主要优势实际不是速度, 而是更有效的内存利用。基于页的分配策略的优点是更有效的使用内存,按页分配不会浪费空间,而用kmalloc函数则会因分配粒度的原因而浪费一定的内存。 __get_free_page 函数的最大优势是获得的页完全属于调用者, 且理论上可以适当的设置页表将其合并成一个线性的区域。

alloc_pages接口

struct page是内核用来描述单个内存页的数据结构,内核在许多地方使用了page结构,<linux/Mm_types.h>尤其在高端内存(高端内存在内核空间没有对应不变的地址)。

Linux 页分配器的核心是称为 alloc_pages_node 的函数:

struct page *alloc_pages_node(int nid, unsigned int flags,unsigned int order);/*以下是这个函数的 2 个变体(是简单的宏):*/struct page *alloc_pages(unsigned int flags, unsigned int order);struct page *alloc_page(unsigned int flags);/*他们的关系是:*/#define alloc_pages(gfp_mask, order) /alloc_pages_node(numa_node_id(), gfp_mask, order)#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

参数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);void __free_pages(struct page *page, unsigned int order);void free_hot_page(struct page *page);void free_cold_page(struct page *page);/*若知道某个页中的内容是否驻留在处理器高速缓存中,可以使用 free_hot_page (对于驻留在缓存中的页) 或 free_cold_page(对于没有驻留在缓存中的页) 通知内核,帮助分配器优化内存使用*/

vmalloc及其辅助函数:

vmalloc,它分配的虚拟地址空间的连续区域,尽管这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数alloc_page)。应当注意的是: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返回的内存地址也是虚拟地址。vmalloc在如何使用硬件上没有区别,关键在于如何执行分配任务上。

kmalloc和__get_free_pages使用的虚拟地址范围与物理内存是一一对应的,可能会有基于常量PAGE_OFFSET的一个偏移。这两个函数不需要为该地址段修改页表,但是vmalloc与ioremap使用的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立内存区域。

用vmalloc分配的内存不能在微处理器之外使用,因为他们只在处理器的内存管理单元上才有意义。当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA),就不能使用vmalloc了。vmalloc适合在分配一大块连续的,只在软件中存在的,用于缓存区域的时候。vmalloc的开销要比__get_free_pages大的多,因为它不仅需要获得内存,还需要建立页表。

使用vmalloc的一个例子是create_module系统调用,它利用vmalloc函数来获得装载模块所需的内存空间,在调用insmod重新定义模块代码以后,接着会调用copy_from_user函数把模块代码和数据复制到分配的空间内。这样,模块看起来像是在连续内存空间内。但通过/proc/ksyms文件就能发现模块导出的内核符号和内核本身导出的符号分布在不同的内存范围内。

和vmalloc一样,ioremap也建立新的页表,但和vmalloc不同的是,ioremap并不实际分配内存。ioremap的返回时一个特殊的虚拟地址,可以用来访问指定的物理内存区域,这个虚拟地址最后要调用iounmap来释放掉。

为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。

ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。

vmalloc的一个缺点就是不能再原子上下文中使用,因为其内部调用的是kmalloc(GFP_KERNEL),来获取页表的存储空间,因而可能休眠。

使用虚拟地址的scullv

if(!dptr->data[s_pos]){

dptr->data[s_pos] =(void *)vmalloc(PAGE_SIZE << dptr->order);

if(!dptr->data[s_pos])

goto nomem;

memset(dptr->data{s_pos],0,PAGE_SIZE << dptr->order);

}

for(i=0;i<qset;i++){

if(dptr->data[i])

vfree(dptr->data[i]);

}

vmalloc要建立新的页表。

per-CPU变量

per-CPU 变量是一个有趣的 2.6 内核特性,定义在 <linux/percpu.h> 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能。

per-CPU 变量是一个有趣的 2.6 内核特性,定义在 <linux/percpu.h> 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能。

在编译时间创建一个per-CPU变量使用如下宏定义:

DEFINE_PER_CPU(type, name);/*若变量( name)是一个数组,则必须包含类型的维数信息,例如一个有 3 个整数的per-CPU 数组创建如下: */DEFINE_PER_CPU(int[3], my_percpu_array);

虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。 对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:

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);void *__alloc_percpu(size_t size, size_t align);/*需要一个特定对齐的情况下调用*/void free_percpu(void *per_cpu_var); /* 将per-CPU 变量返回给系统*//*访问动态分配的per-CPU变量通过 per_cpu_ptr 来完成,这个宏返回一个指向给定 cpu_id 版本的per_cpu_var变量的指针。若操作当前处理器版本的per-CPU变量,必须保证不能被切换出那个处理器:*/per_cpu_ptr(void *per_cpu_var, int cpu_id);/*通常使用 get_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);

/*要在模块中访问这样一个变量,声明如下:*/DECLARE_PER_CPU(type, name);

注意:在某些体系架构上,per-CPU变量的使用是受地址空间有限的。若在代码中创建per-CPU变量, 应当尽量保持变量较小.per-CPU变量就是一个自身可以进行原子操作的变量,但在支持抢占的内核中,需要用特定的函数调用来完成对per-CPU的操作。获取大的缓冲区大量连续内存缓冲的分配是容易失败的,在试图获得大的内存区之前,我们应该仔细考虑其他的实现途径。到目前止执行大 I/O 操作的最好方法是通过离散/聚集操作 。若真的需要大块连续的内存作缓冲区,最好的方法是在引导时来请求内存来分配。在引导时分配是获得大量连续内存页(避开 __get_free_pages 对缓冲大小和固定颗粒双重限制)的唯一方法。但是得到的缓存区会有点脏,因为它通过保留私有内存池而跳过了内核的内存的管理机制。一个模块无法在引导时分配内存,只有直接连接到内核的驱动才可以。 而且这对普通用户不是一个灵活的选择,因为这个机制只对连接到内核映象中的代码才可用。要安装或替换使用这种分配方法的设备驱动,只能通过重新编译内核并且重启计算机。在引导时获得专用缓冲区要通过调用下面函数进行:

#include <linux/bootmem.h>/*分配不在页面边界上对齐的内存区*/void *alloc_bootmem(unsigned long size); void *alloc_bootmem_low(unsigned long size); /*分配非高端内存。希望分配到用于DMA操作的内存可能需要,因为高端内存不总是支持DMA*//*分配整个页*/void *alloc_bootmem_pages(unsigned long size); void *alloc_bootmem_low_pages(unsigned long size);/*分配非高端内存*//*很少在启动时释放分配的内存,但肯定不能在之后取回它。注意:以这个方式释放的部分页不返回给系统*/void free_bootmem(unsigned long addr, unsigned long size);

不甚酒力,体会不了酒的美味,但却能感受知已的妙处。

[置顶]
        linux设备驱动(十四)–内存分配

相关文章:

你感兴趣的文章:

标签云: