Linux内核-内存管理

Posted by keys961 on March 29, 2019

1. 页

内存管理的基本单位:Page。MMU(vaddr -> addr)也用页进行管理。

内核用struct page表示一个物理页,定义于<linux/mm_types.h>中:

struct page {
    unsigned long flags; //页状态
    atomic_t _count; //引用计数, -1表示没有引用,可被分配
    atomic_t _mapcount;
    unsigned long private; //存放私有数据
    struct address_space *mapping; //由页缓存使用
    pgoff_t index;
    struct list_head lru;
    void *virtual; //页虚拟地址
    //...
}

注意,这里struct page与物理页相关,而非与虚拟页相关。

2. 区

内核把页划分到不同的区内,对相似特性的页进行分组。

主要使用4种区:

  • ZONE_DMA:包含的页执行DMA操作

  • ZOME_DMA32:和ZONE_DMA类似,但只能被32位设备访问

  • ZONE_NORMAL:正常映射的页

  • ZONE_HIGHEM:包含“高端内存”,不能永久映射到内核地址空间

32位x86机器上,内核空间分为:

  • ZONE_DMA: <16MB
  • ZONE_NORMAL: 16~896MB
  • ZONE_HIGHEM: >896MB

而x86-64上没有ZONE_HIGHEM

分配时,可从多个区中获取页,但不能同时在2个区中分配。

每个区用struct zone表示,定义在<linux/mmzone.h>中。(内包含一个自旋锁,但只保护结构,不保护整个区)

# 3. 获得页

获取页核心函数是:

  • struct page* alloc_page(gfp_t gfp_mask, unsigned int order),它可分配$2^{order}$个连续的物理页,返回第一个页的page指针

若获取全为0的页,可调用:

  • unsigned long get_zeroed_page(unsigned int gfp_mask)

释放页是,可调用下面的函数,但要谨慎,只能释放属于你的页(内核完全信赖自己,若操作非法,内核就会挂起):

  • void __free_pages(struct page *page, unsigned int order)
  • void free_pages(unsigned long addr, unsigned int order)
  • void free_page(unsigned long addr)

4. kmalloc()

从内核中申请一块内存,物理地址连续,和malloc()相比,多了一个flag参数:

  • void *kmalloc(size_t size, gfp_t flags),定义在<linux/slab.h>

其中flag是一个gfp_mask标志,定义在<linux/gfp.h>,分为3类:

  • 行为修饰符:指定分配时的行为
  • 区修饰符:指定分配的区域
  • 类型标识:结合上面两种修饰,配置内核内存分配

释放内存可调用:

  • void kfree(const void *ptr),只能释放kmalloc()开辟的内存。若释放其它部分内存或释放已经释放的内存,会导致严重后果。

    kfree(NULL)是安全的。

5. vmalloc()

可以分配虚拟地址连续的内存,物理地址无需连续(和malloc()很像)。它定义于<linux/vmalloc.h>

  • void* vmalloc(unsigned long size)

释放调用void vfree(const void *addr)

两个函数可以睡眠,不能在中断上下文中调用。

6. slab层

Linux内核使用slab分配器,扮演通用数据结构缓存层,避免过多的内存申请和释放。

6.1. slab层设计

每个高速缓存对应一个对象类型,由若干个slab组成,slab由一个或多个连续物理页面组成。

slab三种状态:满、部分满或者空。

分配时,先从部分满的分配,再考虑空的slab,最后再考虑创建一个slab。

高速缓存使用struct kmem_cache表示,包含3个链表:slabs_full, slabs_partial, slabs_empty

而slab描述符用struct slab描述:

struct slab {
    struct list_head list; // 满、部分满或空链表
    unsigned long colouroff; // slab着色的偏移量
    void *s_mem; // slab中第一个对象
    unsigned int inuse; //已分配对象数
    kmem_bufctl_t free; //第一个空闲对象
}

slab分配器可创建新的slab,实际上最终通过__get_free_pages()低级内核页分配器进行的:

  • 入口是static void* kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid),分配大小是2的幂次方

  • 释放则调用kmem_freepages()

6.2. slab分配器的接口

高速缓存可通过下面的函数创建:

struct kmem_cache *kmem_cache_create(const char *name, //缓冲名
                                    size_t size, //每个对象大小
                                    size_t align, //第一个对象偏移
                                    unsigned long flags,
                                    void (*ctor)(void*));//构造函数

撤销可调用,但必须让里面所有slab为空,且之后不能再访问:

int kmem_cache_destroy(struct kmem_cache *cachep)

从缓存中分配和释放对象,可调用:

void *kmem_cache_alloc(struct kmem_cache *cachep, gft_t flags)
void kmem_cache_free(struct kmem_cache *cachep, void *objp)

7. 栈上静态分配

内核栈空间小且固定(不能动态增长),大小通常与体系架构和编译选项有关。

7.1. 单页内核栈

2.6内核早期,栈空间只占用一个页。

好处在于:

  • 减少内存消耗,
  • 避免因为内存碎片的增多,难以寻找未分配的连续的页

7.2. 节省内核栈的内存

避免在内核栈上大量静态分配,否则栈会溢出,导致堆末端的内容被覆盖。

因此申请内存尽量动态分配,让函数所有的局部变量占用空间之和不超过几百字节。

8. 高端内存的映射

8.1. 永久映射

永久映射的高端内存页数是有限的,映射时,调用:

  • void *kmap(struct page *page)

不需要使用时,要解除映射:

  • void kumap(struct page *page)

函数可以睡眠,不能在中断上下文中使用。

8.2. 临时映射

内核可原子地将高端内存中的一个页映射到某个保留映射中,它可以用于不能睡眠的上下文中(如中断上下文),因此它不会阻塞。

建立和解除映射可调用:

  • void *kmap_atomic(struct page *page, enum km_type type)
  • void kunmap_atomic(void *kvaddr, emun km_type type)

9. 每个CPU的内存分配

2.6内核为了方便获取每个CPU的数据,引入新接口percpu,定义在<linux/percpu.h>

9.1. 获取CPU

  • get_cpu():获得当前处理器ID,并禁止内核抢占
  • put_cpu():激活内核抢占

9.2. 静态给每个CPU分配变量

DECLARE_PER_CPU(type, name):创建类型为type,名字为name的变量

然后可通过:

  • get_cpu_var(name):获得该name变量,并禁止内核抢占
  • put_cpu_var(name):完成,重新激活内核抢占

也可以获得其它处理器的变量,但不会禁止内核抢占和锁保护:

  • per_cpu(name, cpu)

9.3. 运行时给每个CPU分配变量

可通过下面的宏/函数进行分配和释放:

  • void *alloc_percpu(type)(宏)
  • void *__alloc_percpu(size_t size, size_t align)
  • void free_percpu(const void *)

然后依旧可以像9.2.中一样访问变量。

9.4. 使用每个CPU数据的原因

  • 减少数据锁定(只需禁止内核抢占即可,代价小的多)
  • 减少缓存失效

但注意,访问每个CPU数据时,不能睡眠,否则醒来可能到其它处理器上。

10. 分配函数的选择

  • 若需要连续物理内存:kmalloc()
  • 若需要连续虚拟内存:vmalloc(),性能比kmalloc()差些
  • 若从高端内存进行分配:alloc_pages()&kmap()
  • 若频繁创建和释放许多大的数据结构:建立slab高速缓存