Allocating Memory [LDD3 08]

16 篇文章 0 订阅
14 篇文章 1 订阅

Table of Contents

8.1. The Real Story of kmalloc

8.1.1. The Flags Argument

8.1.1.1 Memory zones

8.1.2. The Size Argument

8.2. Lookaside Caches

8.2.1. A scull Based on the Slab Caches: scullc

8.2.2. Memory Pools

8.3. get_free_page and Friends

8.3.1. A scull Using Whole Pages: scullp

8.3.2. The alloc_pages Interface

8.4. vmalloc and Friends

8.4.1. A scull Using Virtual Addresses: scullv

8.5. Per-CPU Variables

8.6. Obtaining Large Buffers

8.6.1. Acquiring a Dedicated Buffer at Boot Time


本章讨论从kernel分配memory的几种方法,并尽可能与架构无关。

8.1. The Real Story of kmalloc


kmalloc相对来说,是比较简单的分配memory的方式,因为用法和user mode的malloc非常相似。kernel的kmalloc分配的memory在物理地址上是连续的,并且里面包含了随机的数据,需要使用者自己清零。

8.1.1. The Flags Argument

这个是kmalloc的函数原型:

#include <linux/slab.h>
void *kmalloc(size_t size, int flags);

第一个参数是size,也就是要分配的内存大小,单位为byte;第二个参数是flags,这个flags会直接影响kernel分配memory的方式,所以需要解释一下。

flags中,最常用的两个flag是GFP_KERNEL和GFP_ATOMIC。GFP_KERNEL表示要分配memory的主体是用户态的process,也就说这次memory的分配是来自于用户态的系统调用,此时如果内存资源不够,kernel会把这个process sleep,然后去做一些回收memory的操作,比如flush buffer到disk,或者把user process 的memory swap到disk上去。

因为设置GFP_KERNEL可能会导致休眠,所以除非是允许休眠的进程或者线程,否则分配memory就不能设置这个flag。比如,如果需要在interrupt handler,tasklet,kernel timers等运行,那进程或者线程就不允许休眠,这种情况就不能使用GFP_KERNEL,而应该使用GFP_ATOMIC,这样的话即便内存不够,也不会休眠。如果设置了GFP_ATOMIC,kernel会从reserve的free pages里分配memory,直到用完最后一个page,如果所有的page都被用完,kmalloc就会返回fail。常用的flag有以下这些:

//用在interrupt handlers,以及没有process context等处,不会sleep
GFP_ATOMIC
//从kernel memory中分配,可能会sleep
GFP_KERNEL
//给user space分配page,可能会sleep
GFP_USER
//从high memory中给user space分配,可能会sleep
GFP_HIGHUSER
//不常用,主要是filesystem中使用
GFP_NOIO
GFP_NOFS

除了以上的flag,他们中的任何一个还可以与以下的这些flag使用or组成bitmask,来控制kmalloc的行为: 

//从DMA-capable的memory中分配
_ _GFP_DMA
//如果有必要,从high memory中分配
_ _GFP_HIGHMEM
//通常情况下,kmalloc分配都是warm page,也就是cache有可能在processor cache里的,如果设置了COLD,就不会分配这类的memory。多用于DMA的memory。
_ _GFP_COLD
//no warning打印,会把warning的打印关掉,所以几乎不用。
_ _GFP_NOWARN
//设置high priority,有可能把kernel reserve的memory也用掉。
_ _GFP_HIGH
//下面几个都是用来控制失败情况。不管。
_ _GFP_REPEAT
_ _GFP_NOFAIL
_ _GFP_NORETRY

8.1.1.1 Memory zones

__GFP_DMA和__GFP_HIGHMEM虽然很多platform上都有,但是内部的实现还是和架构相关的。

kernel里,至少存在三种memory zone:DMA-capable,Normal和High,通常情况下,如果没有显示指定上面列的bitmask,都会分在normal zone。

DMA-capable的memory,有些外设可以通过这些memory做DMA,在大部分系统上,所有的memory都是DMA-capable。在X86系统上,前面16M是DMA zone,legacy ISA driver只能在这16M里才能做DMA。PCI device没有限制,所有的memory range都可以做DMA。

High memory是为了在32位系统上使用大量memory,这些high memory kernel不能直接访问,必须做过map才可以正常访问。如果本身支持大量memory的系统就没有问题,比如64bit系统。关于DMA更多的内容,可以参看15章。

在每次分配memory时,kernel都有一个zone list,memory就是从这些zone list中分配出来的。

在分配memory时,如果指定了__GFP_DMA,就只会从DMA中分配memory,如果DMA zone中没有足够的memory,就会fail。

如果没有指定特殊的flag,就会从normal和DMA中分配memory。

如果指定了__GFP_HIGHMEM,这三个zone都会使用。注意,kmalloc不能从high memory中分配内存,因为kmalloc分配的内存都是kernel可以直接访问的,而high memory要做过特殊的map才能被kernel 直接使用。

8.1.2. The Size Argument

kmalloc的size参数这里也做了描述,不过要具体细节要涉及到kernel的page 管理系统,所以这里没有细说。

这里要记住一点,kernel对物理内存的管理,是以page(4096Byte)为单位来管理的,而且会预先有一个memory pool,里面也都是固定大小的block,当需要分配内存时,就从pool里来分。从使用者的角度看,有两个问题需要注意:1,实际分配出来的内存可能会被要求的要多,但是kmalloc有分配的最低要求,比如32或者64 byte,取决于当前架构的配置;2, kmalloc分的memory,在物理地址上是连续的,所以分配的最大memory有限制,可能限制为128KB。如果需要更多的memory,那么可以使用另外的方式,后面会有介绍。

8.2. Lookaside Caches


在kernel中,device driver有时候会频繁的分配和释放相同大小的memory,如果都从统一的memory里分,很容易造成内存碎片,所以kernel采用了memory pool的方式来管理这种固定大小,并且频繁分配和释放的结构体,这种管理机制又叫slab。memory cache的数据结构类型是kmem_cache_t,使用的分配函数:

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

返回的kmem_cache_t就是memory pool,它可以容纳任意多个大小为size的内存对象。name就是一个字符串,用于trace用,通常设置为和device driver定义的kmem cache结构体的名字。offset是page中第一个object的偏移,可以用来做指定的alignment,不过一般设置为0. flag用来控制如何分配,是一些bitmask:SLAB_NO_REAP, SLAB_HWCACHE_ALIGN, SLAB_CACHE_DMA。

SLAB_NO_REAP: 禁止kernel在内存不够的情况下减小memory pool的内存占用,一般不设置。

SLAB_HWCACHE_ALIGN:会对每个data object对其到当前platform的cache line. 如果运行在多核系统上,并且会被多个CPU频繁使用,那么可以设这个flag,这样有助于提高performance,但是可能因为对齐,浪费一些memory。

SLAB_CACHE_DMA:所有的memory cache都从DMA zone里分配。

除了上面的这些debug以外,kernel还提供了一些debug用的flag,不过一般通过kernel的configuration来配置,device driver无需关心。

constructor和destructor是两个可选的参数,用来对memory进行预先的初始化或者销毁的额外操作。构造和析构可以是同一个函数,通过参数flag来区分即可:如果是构造过程,flag会被kernel设置上SLAB_CTOR_CONSTRUCTOR,否则就不设置flag。

在memory cache创建完成以后,就可以从里面创建object:

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

cache就是之前创建的memory cache,flags和kmalloc用的参数一样,一旦cache里没有足够的memory,kernel就通过kmalloc再分配,所以需要这个flags参数,用来控制kmalloc的分配方式。

如果要free object:

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

如果memory cache不再使用,比如driver要unload,那么调用destory来释放memory cache:

int kmem_cache_destroy(kmem_cache_t *cache);

注意,只有memory cache里的所有object都释放了以后,kmem_cache_destroy才会被释放,否则就会返回error。所以driver需要check 返回值,如果有fail,说明发生了object 的leak。

使用memory cache的一个好处是,kernel会对内存的使用进行统计,并且在/proc/slabinfo中可以看到统计信息。

8.2.1. A scull Based on the Slab Caches: scullc

这里是一个例子,展示如何使用kernel memory cache:

/* declare one cache pointer: use it for all devices */
kmem_cache_t *scullc_cache;

//module load time
/* scullc_init: create a cache for our quanta */
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
        0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* no ctor/dtor */
if (!scullc_cache) {
    scullc_cleanup(  );
    return -ENOMEM;
}

//从cache中分配memory object
/* Allocate a quantum using the memory cache */
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);
}

//释放已经分配的cache object
for (i = 0; i < qset; i++)
if (dptr->data[i])
        kmem_cache_free(scullc_cache, dptr->data[i]);

//在module driver unload的时候
/* scullc_cleanup: release the cache of our quanta */
if (scullc_cache)
    kmem_cache_destroy(scullc_cache);

8.2.2. Memory Pools

在某些场景下,分配memory不允许失败,为了满足这个需求,kernel抽象除了一个memory pool(mempool)完成这个任务,mempool其实就是kmem cache,其中保存了一些free的list,用来给紧急情况下使用。

memory pool使用数据结构mempool_t来表示,可以使用下面的函数创建一个mempool:

mempool_t *mempool_create(int min_nr, 
                          mempool_alloc_t *alloc_fn,
                          mempool_free_t *free_fn, 
                          void *pool_data);

min_nr表示mempool中保存的最少object个数;真正的allocation和free操作是由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的参数。

一般情况下,device driver不需要实现自己的alloc_fn和free_fn,而是使用kernel提供的:mempool_alloc_slab和mempool_free_slab。所以,一般使用mempool的code大概是这个样子:

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

在mempool创建出来以后,通过下面的接口来create object以及free object:

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

当mempool创建出来的时候,kernel会调用多次alloc_fn来对object进行初始化,因为要创建的object是min_nr个,所以会调用多次。当device driver调用mempool_alloc分配memory时,优先从kernel分配,如果分配失败了,就会从这个mempool里返回一个object;而当device driver调用mempool_free时,如果当前mempool中的object小于min_nr个,free的这个object就会被保存在mempool中,返回就直接返回给kernel。

mempool可以被resize,使用下面的接口:

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

这个函数调用成功之后,mempool中将会至少包含new_min_nr个free的object。

mempool不再使用时,调用destroy把memory返还给kernel:

void mempool_destroy(mempool_t *pool);

在调用mempool_destroy之前,需要把pool中已经分配的object全部释放才可以,否则kernel报错。

mempool的优缺点显而易见,缺点就是任何时刻,都有memory被reserve掉,其他任何人都无法使用,对于device driver而言,更应该处理内存分配失败的情况,而不是使用mempool来reserve memory。

8.3. get_free_page and Friends


如果driver需要很多的memory,最好通过page的方式来分配,而不是使用kmalloc。分配page的函数如下:

//分配一个page,并且page memory初始化为0
get_zeroed_page(unsigned int flags);
//分配一个page,但是不初始化
__get_free_page(unsigned int flags);
//分配2^order个物理地址连续的page,返回指向起始地址的指针
__get_free_pages(unsigned int flags, unsigned int order);

这里的flag和kmalloc用的flag一样,不再赘述。order表示要分配的page的个数,按照2的N次方分配,比如order是0,就是2^0也是1个page,如果是1,就是2^1,也就是2个page,order最多是10-11,也就是1024-2048个pages,也就是4-8M的内存。通过使用get_order可以计算某个整形值对应的order,如果要分配的order非常大,可能get page会返回fail。

这里有个注释挺奇怪:

[2] Although alloc_pages (described shortly) should really be used for allocating high-memory pages, for reasons we can't really get into until Chapter 15.

也就说,alloc_pages分配的memory都是从high memory分配的吗??有待研究和确认。

每个zone里面可用的page可以通过/proc/buddyinfo来获取。

如果需要释放之前分配的page,使用:

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

如果释放的memory和之前分配的不一致,比如order值和之前分配的不一样,kernel的memory就会corrupt。

和kmalloc类似,这些分配page的函数也会失败,尤其是设置GFP_ATOMIC的情况下,所以driver里要做好错误处理。

8.3.1. A scull Using Whole Pages: scullp

这里是一个展示分配page和释放page的例子:

/* Here's the allocation of a single quantum */
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);
}

/* This code frees a whole quantum-set */
for (i = 0; i < qset; i++)
    if (dptr->data[i])
        free_pages((unsigned long)(dptr->data[i]),
                dptr->order);

8.3.2. The alloc_pages Interface

这里有列举了一些分配page的接口,但是后面才会用到。struct page是kernel中一个非常重要的结构体,用来描述一个物理的page。在用来描述high memory时尤其有用,因为high memory在kernel中没有固定的地址来访问。

kernel里分配page的核心函数是:

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

并且有几个变体,主要是macro:

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

这里主要讲解alloc_pages_node。alloc_page_node有三个参数,第一个是nid,NUMA node ID,表明从哪里分memory;flags就是之前的GFP flag,order表示size。返回值指向分出来的page首地址(后面可能是一个page list),失败返回NULL。使用alloc_pages会更方便,会在当前的NUMA node上分配memory,用到的NUMA node id就是numa_node_id。这里说一下nid,在多处理器架构上,每一个CPU可能都有自己的memory,成为local memory,另外有一个memory,是nonlocal memory,从local memory上分配自然是最快的,而且performance更好,因此这里通过nid来支持从local分配memory。

释放page:

void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page); //page的内容还在CPU的cache里
void free_cold_page(struct page *page); //page的内容不在CPU的cache里

8.4. vmalloc and Friends


和前面的分配memory的方式有所区别,vmalloc分出来的是虚拟地址连续的内存,但物理地址不一定连续,连续的虚拟地址对应离散的多个page,其中的每一个page最后都是从alloc_page分出来的。在device driver中,并不推荐使用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);

严格来讲,ioremap并不是分配内存的方式,但是它返回的也是连续的虚拟地址,所以放到一起,不过暂时先不讨论它。

kmalloc/alloc_page/vmalloc这些函数分配出来的memory的地址都是虚拟地址,但是又有区别。kmalloc/alloc_page返回的地址和真实的物理地址是一一映射的关系,也就是虚拟地址和物理地址只是相差一个偏移,比如PAGE_OFFSET,因此这两个函数甚至不需要修改page table,因为通过虚拟地址可以直接找到物理地址。而vmalloc和ioremap就不一样了,他们对应了完全离散的page,必须通过设置正确的page table才能访问到物理地址。此外,他们返回的地址range也有所区别,在X86架构上,vmalloc分配的虚拟地址位于VMALLOC_START和VMALLOC_END之间,这个地址空间和kmalloc/alloc_page的地址有很大区别。

vmalloc分配的是基于page table的虚拟地址,所以必须依赖于MMU才能使用,因此就不能脱离CPU来使用,如果想要实现DMA的访问,那肯定不能使用vmalloc出来的地址。做DMA的memory,device必须也能访问才可以。因此,vmalloc的memory的使用场景比较局限,只能在software环境中,hardware是无法直接使用的。而且,通过vmalloc分配的memory还需要设置page table,所以performance比较差。

kernel中一个典型的使用vmalloc的场景,是create_module,kernel把module load进来,并通过vmalloc分配内存,再用copy_from_user把module的data copy进vmalloc分配的memory里面。通过/proc/kallsyms可以看到module export出来的function和kernel自己export出来的symbol地址也有很大差别。

ioremap也会build page table,但是和vmalloc不同的是,ioremap不会分配物理内存,它只会返回一个虚拟地址,这个地址指向之前已经分配好的物理地址,通过这个虚拟地址就可以直接访问那段memory。iounmap可以释放这个虚拟地址。

ioremap很常用的一点,就是把PCI的framebuffer map到kernel的space里来,这样可以通过kernel的virtual address访问video device的framebuffer。framebuffer的物理地址一般位于kernel之外,是在high memory空间里面,当kernel bootup的时候,是没有创建对应的page table的,因此在使用的时候,要通过ioremap创建page table entry,然后再访问。

另外,书里说不推荐直接使用ioremap返回的虚拟地址,因为在某些架构上,这些虚拟地址可能并不对应真正的物理内存。应该使用readb或者device适用的I/O 接口来访问。

vmalloc和ioremap都是面向page的,因为要build page table,此外,vmalloc不能用于atomic context,因为为PTE分配物理内存的时候,调用了kmalloc,并且传递的flag是GFP_KERNEL,因此可能会sleep。

8.4.1. A scull Using Virtual Addresses: scullv

这里是一个使用vmalloc来使用内存的例子:

/* Allocate a quantum using virtual addresses */
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);
}

/* Release the quantum-set */
for (i = 0; i < qset; i++)
    if (dptr->data[i])
        vfree(dptr->data[i]);

8.5. Per-CPU Variables


per-CPU的变量听上去有些奇怪,因为device driver或者其他的driver中并不多见。如果创建一个per-CPU的变量,每个CPU上都会有这个变量自己的拷贝,当需要访问这个变量时,每个CPU都是访问的自己的那份拷贝。使用per-CPU变量的好处是,不用加锁,如果非常频繁的访问这个变量,就能显著的提高performance。

kernel中一个典型的应用,就是网络子系统的数据包个数统计。这个个数就是一个per-CPU的变量,每个CPU都只读写自己的那份计数,这样会非常的快速,每秒可能需要访问上千次,如果通过加锁互斥访问,那么性能会非常非常低。当user space需要直到这个计数时,driver只需要把每个CPU里的计数加和就好了。

静态的声明一个per-CPU的变量:

<linux/percpu.h>
//初始化单个变量
DEFINE_PER_CPU(type, name);
//初始化变量数组
DEFINE_PER_CPU(int[3], my_percpu_array);

读写per-CPU的数据几乎不需要加锁,但是在访问前和访问后,都要使用特定的函数,以防止CPU被抢占(get cpu会关闭抢占):

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);

get_cpu_var返回是当前变量的左值,也就是可以直接引用,甚至直接修改,上面的例子就是直接自增。使用完以后要调用put_cpu_var释放。也可以指定要访问哪个CPU上的变量:

per_cpu(variable, int cpu_id);

当然,如果你需要访问别的CPU中的数据,那就要做好互斥访问,防止竞争条件的产生。

除了静态的初始化per-CPU变量,也可以动态的分配per-CPU的变量:

void *alloc_percpu(type);
//和alloc_percpu类似,只不过可以自己指定alignment
void *__alloc_percpu(size_t size, size_t align);
//上面两个函数分配的per-CPU变量都可以用per_cpu_ptr来释放
free_percpu(void *per_cpu_var)
//通过动态分配的方式分配的per-CPU可以通过per_cpu_ptr访问
per_cpu_ptr(void *per_cpu_var, int cpu_id);

通过这种方式访问的时候,要先确保你的code不会被抢占,仍然需要调用get cpu来关闭抢占:

int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu); /* work with ptr */
put_cpu( );

调用get_cpu可以block当前CPU的抢占,防止下面的code在执行的时候CPU被抢占而导致变量被修改。如果你是通过静态声明的方式使用per-CPU的变量,这些事情get_cpu_var and put_cpu_var已经帮你搞定了。

如果per-CPU的变量想要export出去给别人用:

EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);

//需要使用别的module中export出来的per cpu变量,需要声明
DECLARE_PER_CPU(type, name);

8.6. Obtaining Large Buffers


如果driver想要分配比较大的memory,那么分配失败的可能性就会比较大。对于device driver而言,最好的方式是通过scatter/gather的方式里分配大量的内存。

8.6.1. Acquiring a Dedicated Buffer at Boot Time

如果想要分配成功,最好在boot的时候就分配出来,但是driver必须是built-in driver了,也就是说driver不是以module的方式存在,而是在kernel image里面。在系统启动阶段,kernel会调用各个子系统的初始化函数,各个子系统里会通过kernel的接口分配一些memory:

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

以_page结尾的函数分配的都是按照page为单位的memory,否则就不是page为boundary;如果指定了low memory,也就是以_low结尾,就从low memory里分配,否则可能会从high memory里分。如果分配的memory用来做DMA,那么要使用low的版本,这样就不会从high memory分。如果需要free:

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

再次强调,如果要在bootup的阶段分配memory,要把driver变成built-in的才可以。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值