内存分配

来源:互联网 发布:js实现动态树形结构 编辑:程序博客网 时间:2024/05/16 12:31

http://s.click.taobao.com/t?e=m%3D2%26s%3DBVM0kzNLmxocQipKwQzePDAVflQIoZepLKpWJ%2Bin0XJRAdhuF14FMULXpKGyEMu%2Flovu%2FCElQOvlaW6Pnw5RI9HO8a%2BfejaflyMnEnsOycKnA1HiGjbvTiI4FDKO59pQ

到目前为止,我们已经使用过kmalloc和kfree来分配秋释放内存,但Linux内核为我们提供了更加丰富的内存分配原语集。本章我们将介绍设备驱动程序中使用内存的一些其他方法。

kmalloc函数内幕

    1. 除非被阻塞,否则这个函数可运行得很快
    2. 不对所获取的内存空间清零,也就是分配给它的区域仍然保持着原有的数据
    3. 它分配的区域在物理内存中也是连续的

flags参数

记住kmalloc的原型是:

#include <linux/slab.h>

void *kmalloc(size_t size, int flags);

kmalloc的第一个参数是要分配的块大小,第二个参数是分配标志,它能够以多种方式控制kmalloc的行为。

最常用的标志是GFP_KERNEL,它表示内存分配(最终总是调用get_free_pages来实现的分配,这就是GFP_前缀的由来)是代表运行在内核空间的进程执行的。换句话说,这意味着调用它的函数正代表某个进程执行系统调用。使用GFP_KERNEL允许kmalloc在空闲内存较少的时候把当前进程转入休眠以等待一个页面。

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

以上两个标志基本上可以满足大多数驱动程序的需要了。不过还有其它一些标志可用于替换和补充这两个标志。所有标志都定义在<linux/gfp.h>中,还有一些符号表示这些标志的常用组合。

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

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——通常,内存分配器会试图返回“缓存热(cache warm)”页面,好可在处理器缓存中找到的页面。相反,这个标志请求尚未使用的“冷”页面。对用于DMA读取的页面分配,可使用这个标志,因为这种情况下,页面存在于处理器缓存中没有多大帮助。

__GFP_NOWARN——该标志很少使用。它可以避免内核在无法满足分配请求时产生警告

__GFP_HIGH——该标志标记了一个高优先级的请求,它允许为紧急状况而消耗由内核保留的最后一些页面。

__GFP_REPEAT、__GFP_NOFAIL、__GFP_NORETRY——这些标志告诉分配器在满足分配请求而遇到困难时应该采取体积行为。__GFP_REPEAT表示努力再尝试一次,它会重新尝试分配,但仍有可能失败。__GFP_NOFAIL标志告诉分配器始终不返回失败,它会努力满足分配请求。我们不鼓励使用__GFP_NOFAIL标志,因为在设备驱动程序中,从没有理由需要使用这个标志。最后,__GFP_NORETRY告诉分配器,如果所请求的内存不可获得,就立即返回。

内存区段

__GFP_DMA和__GFP_HIGHMEM的使用与平台相关,尽管在所有平台上都可以使用这两个标志。

Linux内核把内存分为三个区段:可用于DMA的内存、常规内存以及高端内存。通常的内存分配都发生在常规内存区。但通过设置上面介绍的标志也可请求在其他区段中分配。如果指定了__GFP_DMA标志,则只有DMA区段会被搜索:如果低地址段上没有可用内存,分配就会失败。如果没有指定特定标志,则常规区和DMA区段都会被搜索;而如果设置了__GFP_HIGHMEM标志,则所有三个区段都会被搜索以获取一个空闲页(注意,kmalloc不能分配高端内存)

内存区段的背后机制在mm/page_alloc.c中实现,区段的初始化是平台相关的,通常在对应的arch树下的mm/init.c中。第十五章还会再次讨论这个问题。

size参数

内核负责管理系统物理内存,物理内存只能按页面进行分配。因简单的基于堆的内存分配技术在处理页面边界很棘手,所以内核使用了特殊的基于页的分配技术,以最佳地利用系统RAM。

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

驱动程序开发者应该记住一点,就是内核只能分配一些预定义的,固定大小的字节数组。如果申请任意数量的内存,那么得到的很可能会多一些,最多会到申请数量的两倍。别处,程序员应该记住,kmalloc能处理的最小的内存块是32或者64,到底是哪个则取决于当前体系结构使用的页面大小。

对kmalloc能够分配的内存快大小,存在一个上限。这个限制随着系统架构的不同以及内核配置选项的不同而变化。如果我们希望代码具有完整的可移植性,则不应该分配大于128KB的内存。但是如果希望得到多于几千字节的内存,则最好使用除kmalloc之外的内存获取方法。我们将在稍后描述这些方法。

后备高速缓存

 设备驱动程序常常会反复地分配很多同一大小的内存块。既然内核已经维护了一组拥有同一大小内存块的内存池,那为什么不为这些反复使用的块增加某些特殊的内存池呢?实际上,内核的确实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)。设备驱动程序通常不会涉及这种使用后备高速缓存的内存行为,但也有例外,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与这个高速缓存相关联,其功能是保管一些信息以便追踪问题,它通常设置为将要高速缓存的结构的名字。高速缓存保留指向该名称的指针,而不是复制内容,因此,驱动程序应该将指向静态存储(通常可取直接字符串)的指针传递给这个函数。名称中不能包含空白。

offset参数是页面中的第一个对象的偏移量,它可以用来确保对已分配的对象进行某种特殊的对齐,但是最常用的就是0,表示默认值。flags控制如何完成分配,是一个位掩码,可取的值如下:

SLAB_NO_REAP——设置这个标志可以保护高速缓存在系统寻找内存的时候不会被减少。设置该标志通常不是好主意,因为我们不应该对内存分配器的自由做一些人为的、不必要的限制。

SLAB_HWCACHE_ALIGN——这个标志要求所有数据对象跟高速缓存行对齐;实际的操作则依赖于主机平台的硬件高速缓存布局。如果在SMP机器上,高速缓存中包含有频繁访问的数据项的话,则该标志将是非常好的选择。但是,为了满足高速缓存行的对齐需求,必要的填白可能浪费大量的内存。

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

还有一些标志可用于高速缓存分配的调度,详情请见mm/slab.c文件。但通常这些标志只在开发系统中通过内核配置选项而全局的设置。

constructor和destructor参数是可选参数(但是不能只有des而没有con);前者用于初始化新分配的对象,而后者用于“清除”对象——在内存空间被整个释放给系统之前。

一旦某个对象的高速缓存被创建,就可以调用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获得。

基于slab高速缓存的scull:scullc

与scull使用kmalloc不同的是,scullc使用内存高速缓存。数据对象大小可以编译或加载时修改,但是不能在运行时修改——那样需要创建一个新的高速缓存,而这里不必处理那些不必要的细节问题。

scullc可以用于测试slab分配器。它和scull只有几行代码不同。我们必须首先声明自己的slab高速缓存:
/*声明一个高速缓存指针,它将用于所有的设备*/
kmem_cache_t *scullc_cache;

slab高速缓存的创建代码如下所示(在模块装载阶段):

/*scullc_init:为我们的量子创建一个高速缓存*/
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
        0, SLAB_HWCACHE_ALIGN, NULL, NULL);/*没有ctor和dtor*/
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]);

最后,在模块卸载期间,我们必须将高速缓存返回给系统:
/*scullc_cleanup:释放量子使用的高速缓存*/
if(scullc_cache)
    kmem_cache_destroy(scullc_cache);

和scull相比,scullc的最主要差别是运行速度略有所提高,并且对内存的利用率更佳。由于数据对象是从内存池中分配的,而内存池中的所有内存块大小都具有同样的大小,所以这些数据在内存中的位置排列达到了最大程度的密集,相反的,scull的数据对象则会引入不可预测的内存碎片。

内存池

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

内存池对象的类型为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_fn和free_fn函数处理,其原型如下:

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,被传入alloc_fn和free_fn。

如有必要,我们可以为mempool编写特定用途的函数来处理内存分配。但是,通常我们仅会让内核的slab分配器为我们处理这个任务。内核中有两个函数(mempool_alloc_slab和mempool_free_slab),它们的原型和上述内存池分配原型匹配,并利用kmem_cache_alloc和kmem_cache_free处理内存分配和释放。因此,构造内存池的代码通常如下所示:

cache = kmem_cache_create(...);
pool = mempool_create(MY_POOL_MINIMUM,
                      mem_pool_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的调用将首先通过分配函数获得该对象;如果该分配失败,就会返回预先分配的对象(如果存在的话)。如果使用mempool_free释放一个对象,则如果预先分配的对象数目小于要求的最低数目(min_nr),就会将该对象保留在内存池中;否则,该对象会返回给系统。

我们可以利用下面的函数来调整mempool的大小:

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);//gfp_mask同kmalloc的标志一样

如果对该函数调用成功,将反内存池的大小调整为至少有new_min_nr个预分配对象。

如果不再需要内存池,可使用下面的函数将其返回给系统:

void mempool_destroy(mempool_t *pool);

在销毁mempool之前,必须将所有已分配的对象返回到内存池中,否则会导致内核oops。

如果读者计划在自己的驱动程序中使用mempool,则应该记住下面这点:mempool会分配一些内存块,空闲且不会真正得到使用。因此,使用mempool很容易浪费大量内存。几乎在所有情况下,最好不要使用mempool而是处理可能的分配失败。如果驱动程序存在某种方式可以响应分配的失败,而不会导致对系统一致性的破坏,则应该使用这种方式,也就是说,应尽量避免在驱动程序代码中使用mempool。

get_free_page和相关函数

 如果模块要分配大块的内存,使用面向页的分配技术会更好些。整页的分配还有其他优点,以后会在第十五章介绍。

分配页面可使用正面的函数:

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

 __get_free_page(unsigned int flags); //类似于get_zeroed_page,但不清零页面。

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

参数flags的作用和kmalloc中的一样;通常使用GFP_KERNEL或GFP_ATOMIC,也许还会加上__GFP_DMA标志(申请可用于ISA直接内存访问操作的内存)或者__GFP_HIGHMEM标志(使用高端内存)。参数order是要申请或释放的页面数的以2为底的对数。例如为0表示一个页面,为3表示8个页面。如果order太大,而又没有那么大的连续区域可以分配,就会返回失败。get_order函数返回order值。可允许的最大order值是10或者11(对应于1024或2048个页),这依赖于体系结构。

如果读者对此好奇,/proc/buddyinfo可告诉你系统中每个内存区段上每个除数下可获得的数据块数目。

当程序不再需要使用页面时,它可以通过使用下列函数之一来释放它们。第一个函数是一个宏,展开后就是对第二个函数的调用:

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

如果试图释放和先前分配数目不等的页面,内存映射关系就会被破坏,随后系统就会出错。

值得强调的是,只要符合和kmalloc同样的规则,get_free_pages和其他函数可以在任何时间调用。某些情况下函数分配内存会失败,特别是在使用了GFP_ATOMIC的时候。因此,调用了这些函数的程序在分配出错时都应该提供相应的处理。

尽管kmalloc(GFP_KERNEL)在没有空闲内存时有时会返回失败,但内核总会尽可能满足它。因此,如果分配太多内存,系统的响应性能就很容易降下来。例如往scull设备写入大量数据,计算机很可能就会死掉;当系统为满足kmalloc分配请求而试图掏出尽可能多的内存页时,就会变得很慢。所有资源都被贪婪的设备所吞噬,计算机很快就会变得无法使用了;此时甚至已经无法为解决这个问题而生成新的进程。我们没有在scull中提到这个问题是因为它是个例子,并不会真正在多用户系统中使用。但作为一个编程者必须要小心,因为模块是特权代码,会带来新的系统安全漏洞,例如很可能会造成DoS(denail-of-service,拒绝服务攻击)安全漏洞。

使用整页的scull:scullp(实际测试页面分配)

scullp分配的内存数量是一个或数个整页:scullp_order变量默认为0,但可以在编译或加载时更改。

下列代码说明了它如何分配内存:

/*下面分配单个量子*/
if(!dptr->data[s_pos]){
    dptr->data[s_pos] =
        (void *)__get_free_pages(GFP_KERNEL, dptr->order);
    if(!dptr->data[s_pos])
        goto nomem;
    memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
}

scullp中释放内存的代码如下:

for(i = 0; i < qset; i++)
    if(dptr->data[i])
        free_pages((unsigned long)(dptr->data[i]),
            dptr->order);

从用户角度来看,可以感觉到的差别就是速度快了一些,并且内存利用率更高了,因为不会有内存碎片。但性能提高并不多,因为kmalloc已经运行得很快。基于页的分配策略的优点实际不在速度上,而在于更有效地使用了内存。按页分配不会浪费内存空间,而用kmalloc函数则会因分配粒度的原因而浪费一定数量的内存。

使用__get_free_page函数的最大优点是这些分配的页面完全属于我们自己,而且在理论上可以通过适当地调整页表将它们合并成一个线性区域。例如,可以允许用户进程对这些单一的并互不相关的页面分配得到的内存区域进行mmap。

alloc_pages接口

 为完整起见,本节将介绍内存分配的另一个接口,但是在十五章才会使用这个接口。现在,我们只要知道struct page是内核用来描述单个内存页的数据结构就足够了。我们将看到,在内核中有许多地方要使用page结构,尤其在需要使用高端内存(高端内存在内核中没有对应不变的地址)的地方。

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

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);

这个函数具有两个变种(它们只是简单的宏),大多数情况下我们使用这两个宏:

struct page *alloc_pages(unsigned int flags, unsigned int order);

struct page *alloc_page(unsigned int flags);

核心函数alloc_pages_node要求传入三人参数。nid是NUMA节点的ID号,表示要在其中分配内存,flags是通常的GFP_分配标志,而order是要分配的内存大小。该函数返回指向第一个page结构(可能返回多个页)的指针,它描述了已分配的内存;或者在失败时返回NULL。

alloc_pages通过在当前的NUMA节点上分配内存而简化了alloc_pages_node函数,它将numa_node_id的返回值作为nid参数而调用了alloc_pages_node函数。另外,alloc_page函数显然忽略了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在发生错误时返回0(NULL地址),成功时返回一个指针,该指针指向一个线性的、大小最少为size的线性内存区域。

我们在这里描述vmalloc的原因是,它是Linux内存分配机制的基础。但是,我们要注意在大多数情况下不鼓励使用vmalloc。通过vmalloc获得的内存使用起来效率不高,而且在某些体系架构上,用于vmalloc的地址空间总量相对较小。如果希望将使用vmalloc的代码提交给内核主线代码,则可能会受到冷遇。如果可能,应该直接和单个页面打交道,而不是使用vmalloc。

虽然这么说我们还是来看看如何使用vmalloc。该函数的原型及其相关函数(ioremap,并不是严格的分配函数,将在本节后面讨论)如下所示:

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(内存管理单元,通常是CPU的组成部分)处理才能转为物理内存地址(注:有些体系结构保留虚拟地址范围,用于寻址物理内存)。vmalloc在如何使用硬件上没有区别,区别在于内核如何执行分配任务。

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

可以通过比较内存分配函数返回的指针来发现这种差别。某些平台上(如x86),vmalloc返回的地址仅仅比kmalloc返回的地址高一些;而其它平台上(如MIPS和IA-64),它们就完全属于不同的地址范围了。vmalloc可以获得的地址范围在VMALLOC_START到VMALLOC_END的范围中。这两个符号都在<asm/pgtable.h>中定义。

用vmalloc分配得到的地址是不能在微处理器这外使用的,因为它们只在处理器的内存管理单元上才有意义。当驱动程序需要真正的物理地址时(像外设用驱动系统总线的DMA地址),就不能使用vmalloc了。使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中才存在的、用于缓冲的内存区域的时候。注意vmalloc的开销要比__get_free_pages大,因为它不但获取内存,还要建立页表。因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。

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

用vmalloc分配得到的空间要用vfree函数来释放。

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

ioremap更多用于(物理的)PCI缓冲区地址到(虚拟的)内核空间。例如,可以用来访问PCI视频设备的帧缓冲区;该缓冲区通常被映射到高物理地址,超出了系统初始化时建立的页表地址范围。

要注意,为了保持可移植性,不应把ioremap返回的地址当作指向内存的指针而直接访问。相反,应该使用readb或者其他I/O函数,这是因为,在如Alpha的一些平台上,由于PCI规范和Alpha处理器在数据 传输方式上的差异,不能直接把PCI内存区域映射到处理器的地址空间。

ioremap和vmalloc函数都是面向页的(它们都会修改页表),因此重新定位或分配的内存空间实际上都会上调到最近的一个页边界。ioremap通过把重新映射的地址向下下调到页边界,并返回在第一个重新映射页面中的偏移量的方法模拟了不对齐的映射。

vmalloc函数的一个小缺点是它不能在原子上下文中使用,因为它内部实现调用了kmalloc(GFP_KERNEL)来获取页表的存储空间,因而可能休眠。但这不是什么问题,因为如果__get_free_page函数都还不能满足中断处理例程的需求的话,那应该修改软件的设计了。

使用虚拟地址的scull:scullv

scullv模块使用了vmalloc。和scullp一样,这个模块也是scull的一人缩减版本,只是使用了不同的分配函数来获取内存空间。

该模块每次分配16页的内存。这里的内存分配使用了较大的数据块以获取比scullp更好的性能,并且展示了为什么使用其他分配技术会更耗时。用__get_free_pages函数来分配一页以上的内存空间容易出错,而且即使成功了也会比较慢。在前面我们已经看到,用vmalloc分配几个页时比其他函数要快一些,但由于存在建立页表的开销,所不当只分配一页时会慢一些。scullv设计得和scullp很相似。order参数指定每次要分配的内存空间的“阶数”,默认为4。scullv和scullp的唯一差别是在分配管理上。下面的代码用vmalloc获取新内存:

/*使用虚拟地址分配一个量子*/
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);
}

scullv中释放内存的代码如下:

for(i = 0; i < qset; i++)
    if(dptr->data[i])
        vfree(dptr->data[i]);

per-CPU变量

per-CPU(每CPU)变量是2.6内核的一个有趣的特性。当建立一个per-CPU变量时,系统中的每个处理器都会拥有该变量的特有副本。这看起来有些奇怪,但它有其优点。对per-CPU变量的访问(几乎)不需要锁定,因为每个处理器在其自己的副本上工作。per-CPU变量还可以保存在对应处理器的调整缓存中,这样,就可以在频繁更新时获得更好的性能。

关于per-CPU变量使用的例子可见于网线子系统中。内核维护着大量计数器,这些计数器跟踪已接收到的各类数据包数量,而这些计数器每秒可能被更新上千次。网络子系统的开发者将这些统计用的计数器放在了per-CPU变量中,这样,他们就不需要处理缓存和锁定问题,而更新可在不用锁定的情况下快速完成。在用户空间偶尔请求这些计数器的值时,只需要将每个处理器的版本相加并返回合计值即可。

关于per-CPU变量的声明可见于<linux/percpu.h>中。要在编译期间创建一个per-CPU变量,可使用下面的宏:

DEFINE_PER_CPU(type, name);

如果该变量是一个数组,需要在type中包含数组的维数。这样,具有三个整数的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的调用将返回当前处理器变量版本的1value值,并禁止抢占。因为返回的是1value,因此可直接赋值或者操作。例如,网络代码对一个计数器的递增使用了下面的两条语句:

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);

在大多数情况下可使用alloc_percpu完成分配工作;但如果需要特定的对齐,则应该调用__alloc_percpu函数。不管使用哪个函数,可使用free_percpu将per-CPU变量返回给系统。对动态分配的per-CPU变量的访问通过per_cpu_ptr完成:

per_cpu_ptr(void *per_cpu_var, int cpu_id);

这人宏返回指向对应于给定cpu_id的per_cpu_var版本的指针。如果打算读取该变量的其他CPU版本,则可以引用该指针进行相关操作。但是,如果正在操作当前处理器的版本,则应该首先确保自己不会被切换到其他处理器上运行。如果对per-CPU的整个访问发生在拥有某个自旋锁的情况下,则不会出现任何问题。但是,在使用该变量的时候通常需要使用get_cpu来阻塞抢占。这样使用动态per-CPU变量的代码类似下面所示:

int cpu;

cpu = get_cpu();
ptr = per_cpu_ptr(per_cpu_var, cpu);
/*使用ptr*/
put_cpu();

per-CPU变量可以导出给模块,但是必须使用下面的宏:

EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);

要在模块中访问这样一个变量,则应该将其声明如下:

使用DECLARE_PER_CPU(而不是DEFINE_PER_CPU),将告诉编译器要使用一个外部引用。

如果打算使用per-CPU变量来建立简单的整数计数器,可参考<linux/percpu_counter.h>中已封装好的实现。最后要注意,在某些体系架构上,per-CPU变量可使用的地址空间是受限制的。因此,如果要创建per-CPU变量,则应该保持这些变量较小。

获取大的缓冲区

我们在前面的小节中提到,大的、连续内存缓冲区的分配易流于失败。系统内存会随着时间的流逝而碎片化,这导致无法获得真正的大内存区域。因为可以有其它途径来完成工作而不需要大的缓冲区,所以内核开发者并没有将大缓冲区的分配工作作为高优先级的任务来计划。在试图获得大的缓冲区前我们应该仔细考虑是否有其他的实现途径。到目前为止,执行大的I/O操作的最好方式是通过离散/聚集操作。我们将在第十章中讨论这各操作。

在引导时获得专用缓冲区

如果的确需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。在引导时就进行分配是获得大量连续内存页面的唯一方法,它绕过了__get_free_jpages函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。在引导时分配缓冲区有点“脏”,因为通过保留私有内存池而跳过了内核的内存管理策略。这种技术比较粗暴也很不灵活,但也是最不容易失败的。显然,模块不能在引导时分配内存,而只有直接链接到内核的设备驱动程序才能在引导时分配内存。

还有一个值得注意的问题是,对于变通用户来说引导时分配不是一个切实可用的的选项,因为这种机制只对链接到内核映像中的代码可用。要安装或替换使用了这种分配技术的驱动程序,就只能重新编译内核并重新启动计算机。

内核被引导时,它可访问系统所有的物理内存,然后调用各个子系统的初始化函数进行初始化,它允许初始化代码分配私有的缓冲区,同时减少了留给系统操作的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操作,而高端内存并不总是支持DMA操作;这样,我们可能需要使用上述函数的一个_low变种。

很少会释放引导时分配的内存,而且也没有任何办法可将这些内存再次拿回。但是内核还是提供了一种释这种内存的接口:

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

注意,通过上述函数释放的部分页面不会返回给系统——但是,如果我们使用这种技术,则其实已经分配得到了一些完整的页面。

如果必须使用引导时的分配,则应该将驱动程序直接链接到内核。关于直接链接到内核的实现细节,可参阅内核源代码中Documentation/kbuild目录下的文件。


原创粉丝点击