第八章 分配内存

一、Kmalloc函数的内幕

kamlloc原型:

#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
size:要分配的块的大小
falgs:标志位,多种方式控制kmalloc行为

常用的标志是GFP_KERNEL,表示内存分配是代表运行在内核空间的进程执行的。

  • GFP_ATOMIC:用于在中断处理例程或其他运行于进程上下文之外的代码中分配内存,不会休眠
  • GFP_KERNEL:内核内存的通常分配方法, 可能引起休眠
  • GFP_USER:用于为用户空间页分配内存,可能会休眠
  • GFP_HIGHUSER:类似于GFP_USER,不过如果有高端内存的话就从那里分配。
  • GFP_NOTIO:禁止任何I/O初始化
  • GFP_NOFS:分配不允许执行仍和文件系统调用
  • __GFP_DMA:标志请求分配发生在可进行DMA的内存区段中
  • __GFP_HIGHMEM:这个标志表明要分配的内存可位于高端内存
  • __GFP_COLD:内存分配器会试图返回“缓存热”页面,即可在处理器缓存中找到的页面
  • __GFP_NOWARN:很少使用,可以避免内核在无法满足分配请求时产生警告
  • __GFP_HIGH:高标志标记了一个高优先级的请求,它允许为紧急状况而小号由内核保留的最后一些页面
  • __GFP_REPEAT
  • __GFP_NOFAIL
  • __GFP_NORETRY:上面3个是分配内存遇到困难时应该采取什么行为。“再试一次“、“不返回失败,努力满足”、“请求内存不可获取就立即返回”

 

二、后备高速缓存

设备驱动常常反复的分配和使用内存块,可以增加特殊的内存池。内核确实实现了这种形式的内存池,通常称为后背高速缓存。管理有时称为“slab分配器”

相关函数和类型在<linux/slab.h>中声明。告诉缓存具有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 falgs),
    void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
size:参数指令区域的大小
name:高速缓存相关联,保管一些信息以便追踪问题
offset:参数是页面中第一个对象的偏移量,用来确保对已分配的对象进行某种特殊的对齐,常用的就是0
flags:控制如何完成分配,是一个位掩码
    SLAB_NO_REAP:保护高速缓存在系统寻找内存的时候不会被减少
    SLAB_HWCACHE_ALIGN:要求所有数据对象跟高速缓存对齐
    SLAB_CACHE_DMA:要求每个数据对象都从可用于DMA的内存区段中分配
constructor和destructor参数是可选的函数,前者用于初始化新分配的对象,而后者用于“清除”对象
kmem_cache_create

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

void *kmem_cache_alloc(kmem_cache_t *cache, int falgs);
cache:是先前创建的高速缓存
falgs:传递给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);
这个释放只有在已将从缓存中分配的所有对象都归还后才能成功。所以需要检查返回状态
如果失败,则发生了内存泄漏

 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);
使用例程

内存池

内核中有些地方的内存分配是不允许失败的。为了确保这种情况的成功分配,建立了内存池(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_fn和free_fn:分配和释放函数
pool_data:被传入alloc_fn和free_fn

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

 构造内存池的代码通常如下所示:

cache = kmem_cache_create( ... );
pool = mempool_create(MY_POOL_MINIMUM,
    mempool_alloc_slab, mempool_free_slab,
    cache);
View Code

建立内存池之后,可如下所示分配和释放对象:

void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
创建mempool时,就会多次调用分配函数为羽仙分配的对象创建内存池。

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

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
将内存池的大小调整为至少有new_min_nr个预分配对象

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

void mempool_destory(mempool_t *pool);
销毁mempool之前,必须将所有已分配的对象返回到内存池汇中,否则会导致内核oops

三、get_free_page和相关函数

分配内存页可使用下面的函数:

  • get_zeroed_page(unsigned int flags);  返回指向新页面的指针并将页面清零
  • __get_free_page(unsigned int flags);  类似于get_zeroed_page,但不清零页面
  • __get_free_pages(unsigned int flags, unisgned int order);  分配若干页面,并返回指向该内存区域第一个字节的指针,但不清零页面
    • 参数flags的作用和kmalloc中的一样

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

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
如果试图释放和先前分配数目不等的页面,内存映射关系就会被破坏,随后系统就会出错
View Code

scullp分配内蕴数量是一个或数个整页:

/* 下面分配单个量子 */
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);   
}

/* 这段代码释放整个量子集 */
for(i=0;i<qset;i++)
    if(dptr->data[i])
        free_pages((unsigned long)(dptr->data[i]),
            dptr->order);
使用例子

alloc_page接口

在需要使用高端内存的地方

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);
为了释放通过上述途径分配的页面,应使用下面的函数:
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);
alloc_pages

四、vmalloc及其辅助函数

vmalloc分配虚拟地址空间的连续区域。尽管这段区域在物理上可能是不连续的,内核确认为他们地址上是连续的。

发生错误时返回0,成功时返回一个指针,该指针指向一个线性的,大小最少为size的线性内存区域。

vmalloc函数相关函数原型,ioremap

#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void *addr);
void *ioremap(unsigned long offset, unsigned long size);
void ioremap(void *addr);

vmalloc和__get_free_pages返回的内存地址也是虚拟地址,其实际值仍然要由MMU处理才能转为物理内存地址。

/* 使用虚拟地址分配一个量子 */
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=0i<qset;i++)
    if(dptr->data[i])
        vfree(dptr->data[i]);
用vmalloc获取新内存

五、per-CPU变量

当建立一个per-CPU变量时,系统中的每个处理器都会拥有该变量的特有副本。对per-CPU的变量不需要锁定,应为每个处理器在其自己的副本上工作。

#include <linux/percpu.h>
DEFINE_PER_CPU(type, name);
name:一个数组
type:数据的维数

例子:
DEFINE_PER_CPU(int[3], my_percpu_array);
创建一个per-CPU宏

当处理器在修改某个per-CPU变量的邻接区中间,可能会被抢占,因此应该避免。

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);
网络代码对一个计数器递增操作

也有访问其他处理器的变量副本

per_cpu(variable, int cpu_id);

/* 需要某种锁定机制确保访问安全 */
void *alloc_percpu(type);
void *__alloc_percpu(size_t size, size_t align);

/* 对工台分配的per-CPU变量的访问通过per_cpu_ptr完成 */
per_cpu_ptr(void *per_cpu_var, int cpu_id);
per-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(type, name);

如果是简单的计数器,<linux/percpu_counter.h>已封装好实现
内核模块中使用

六、获取大的缓冲区

 如果的确需要联系的大块内存用作缓冲区,就最好在系统引导器件通过请求内存来分配

通过调用系列函数之一则可完成引导时的内存分配:

#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);
引导时内存分配

很少会释放引导时分配的内存,也没有任何办法可将这些内存再次拿回。

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

 

posted @ 2018-09-13 22:55  习惯就好233  阅读(304)  评论(0编辑  收藏  举报