Allocating Memory [LDD3 07]

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

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

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

第一个参数是size,第二个参数是flags,这个flags会直接影响kernel分配memory的方式,所以需要解释一下。最常用的两个flag是GFP_KERNEL和GFP_ATOMIC。前者用的更多,表示要为kernel中运行的process分配memory,使用kmalloc和GFP_KERNEL在memory资源不够的情况下,会导致process进入sleep,然后kernel会去做一些回收memory的操作,比如flush buffer到disk,或者把user mode 的memory swap到disk上去。如果需要在interrupt handler,tasklet,kernel timers等运行,那就不允许休眠,这种情况就不能使用GFP_KERNEL,而应该使用GFP_ATOMIC,这样的话就会从reserve的free pages给分配memory,直到用完最后一个page,如果所有的page都被用完,kmalloc就会返回fail。常用的flag:

//用在interrupt handlers等处,不会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,他们中的任何一个还可以与以下的这些使用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打印,几乎不用。
_ _GFP_NOWARN
//设置high priority,有可能把kernel reserve的memory也用掉。
_ _GFP_HIGH
//下面几个都是用来控制失败情况。不管。
_ _GFP_REPEAT
_ _GFP_NOFAIL
_ _GFP_NORETRY

Memory Zone

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

kernel至少需要知道三种memory zone:DMA-capable,Normal和High。

通常情况下,如果没有显示指定上面列的bitmask,都会分在normal zone。

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

High memory是为了在32位系统上使用大量memory的方法,因为32bit系统上只能寻址4G的memroy,如果有更多的memory,就必须使用High memory range做map才可以正常访问。如果本身支持大量memory的系统就没有问题,比如64bit系统。

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

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

如果指定了__GFP_HIGHMEM,这三个zone都会使用。(Note, however, that kmalloc cannot allocate high memory.) ??

kmalloc的size参数这里也做了描述,不过要具体细节要涉及到kernel的page 管理系统,所以这里没有细说。这里要记住一点,kmalloc分的memory,在物理地址上是连续的,所以分配的最大memory有限制,比如128KB。如果需要更多的memory,那么可以使用另外的方式。

Lookaside Caches

在kernel中,device driver有时候会频繁的分配和释放相同大小的struct,如果都从统一的memory里分,很容易造成内存碎片。所以kernel采用了memory pool的方式来管理这种固定大小,并且频繁分配和释放的结构体,这种管理机制又叫slab。memory的类型是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。offset用来控制在page中的offset,一般为0. flag用来控制如何分配,是一些bitmask:SLAB_NO_REAP, SLAB_HWCACHE_ALIGN, SLAB_CACHE_DMA。

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

SLAB_HWCACHE_ALIGN:会对每个data object对其到cache line. 如果运行在SMP,并且会被CPU频繁使用,那么可以设这个标,但是可能因为对齐,浪费一些memory。

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

constructor和destructor是可选的参数,用来对memory进行初始化或者销毁的额外操作。

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

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

cache就是之前创建的memory cache,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,说明发生了leak。

Memory Pool

device driver中极少用到,这里不讲了。

get_free_page and Friends

如果driver需要很多的memory,最好通过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,最多是10-11,也就是1024-2048个pages,也就是4-8M的内存。每个zone里面可用的page可以通过/proc/buddyinfo来获取。

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

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

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

和kmalloc类似,这些分配page的函数也会失败,所以driver里要做好错误处理。

分配page和kmalloc相比还有一个好处,就是这些page可以通过mmap的方式给user mode,kmalloc的就不可以。 

alloc_pages

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地址(有可能有多个)。使用alloc_pages会更方便,会在当前的NUMA node上分配memory,用到的NUMA node id就是numa_node_id。

释放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里

vmalloc

和前面的分配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的地址都是虚拟地址,区别在于page table,kmalloc/alloc_page分配出来的内存物理地址连续,在page table里就是连续的range,寻址效率比较高,而且cache容易命中。vmalloc/ioremap分配出来的虚拟地址,物理地址不连续,在page table里可能要跳转多次,而且cache命中率会降低。此外,他们返回的地址range也有所区别,在X86架构上,vmalloc分配的虚拟地址位于VMALLOC_START和VMALLOC_END之间。

vmalloc分配的是基于page table的虚拟地址,所以必须依赖于MMU才能使用,如果想要实现DMA的访问,那肯定不能使用vmalloc出来的地址。做DMA的memory,device必须也能访问才可以。因此,vmalloc的memory的使用场景比较局限,只能在software环境中,hardware是无法使用的。

kernel中一个典型的使用vmalloc的场景,是create_module,kernel把module load进来,并通过vmalloc分配内存,再用copy_from_user把module的data copy进vmalloc分配的memory里面。通过/proc/kallsyms可以看到module export出来的function是否在同样的memory range。

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

ioremap很常用的一点,就是把PCI的framebuffer map出来,这样可以直接访问video device的framebuffer。它的物理地址一般位于kernel之外,是被reserve的,因此需要通过remap才能访问。

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

Per-CPU Variables

一旦创建了per-CPU的变量,每一个CPU都是有一份这个变量的copy,虽然比较奇怪,但是还是有一定的优势。每一个CPU访问自己的那份数据时,都不用加锁,这样访问速度会很快。

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

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

在访问前和访问后,都要使用特定的函数:

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

//访问别的CPU中的variable
per_cpu(variable, int cpu_id);

get_cpu_var返回是当前processor的左值,也就是可以直接引用,甚至直接修改,上面的例子就是直接自增。使用完以后要调用put_cpu_var释放。

也可以动态的分配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不会被抢占:

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

//需要使用per_cpu_var的module要声明extern
DECLARE_PER_CPU(type, name);

Obtaining Large Buffers

如果driver想要分配比较大的memory,那么分配失败的可能性就会比较大。如果想要分配成功,最好在boot的时候就分配出来,但是driver必须是built-in driver了,也就是说driver不是以module的方式存在,而是在kernel image里面。

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

如果分配的memory用来做DMA,那么要使用low的版本,这样就不会从high memory分。如果需要free:

void free_bootmem(unsigned long addr, unsigned long size);
发布了32 篇原创文章 · 获赞 6 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/scutth/article/details/105353763