Linux内核设计与实现 总结笔记(第十二章)内存管理

内核里的内存分配不像其他地方分配内存那么容易,内核的内存分配不能简单便捷的使用,分配机制也不能太复杂。

一、页

内核把页作为内存管理的基本单位,尽管处理器最小寻址坑是是字或者字节。但是内存管理单元MMU通常以页为单位进行处理。

从虚拟内存的角度来看,页就是最小单位。大多数32位系统支持4KB的页,而64位系统结构一般会支持8KB的页。

内核用struct page结构表示系统中每个物理页,在<linux/mm_types.h>中

struct page {
    unsigned long flags;
    atomic_t         _count;
    atomic_t         _mapcount;
    unsigned long private;
    struct address_space *mapping;
    pgoff_t             index;
    struct list_head lru;
    void                  *virtual;
};
简化的struct page

flag域用来存放页的状态,flag的每一位单独表示一种状态,这些标志位定义于<linux/page-flags.h>中

_count存放页的引用计数,也就是一页被引用了多少次。如果是-1时,就说明当前内核并没有引用这一页。内核代码不应直接检查该域,而是使用page_count()函数进行检查,当页空闲时,返回0表示页空闲。

virtual域是页的虚拟地址,通常情况下,它就是页在虚拟内存中的地址。

page结构与物理页相关,而并非与虚拟页相关。

 

二、区

有些页位于内存中特定的物理地址上,不能用于其他特定的任务。由于这种限制,内核把页划分为不同的地区。

Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:

  • 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)
  • 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样就有一些内存不能永久地映射到内核空间上。

因为上面的制约条件,Linux使用了四种区:

  • ZONE_DMA:这个区包含的页能用来执行DMA操作
  • ZONE_DMA32:和ZOME_DMA类似,该区包含的页面可用来执行DMA操作;不过只能被32位设备访问。
  • ZONE_NORMAL:这个区包含的都是能正常映射的页。
  • ZONE_HIGHEM:这个区包含“高端内存”并不能永久地映射到内核地址空间,在<linux/mmzone.h>中定义。

 

区          描述        物理内存

ZONE_DMA    DMA使用的页    <16MB

ZONE_NORMAL  正常可寻址的页     16~896MB

ZONE_HIGHMEM  动态映射的页    >896MB

Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。

比如需要DMA分配所需的内存,可以在ZONE_DMA内存池分配。

区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑分组。

每个区都是struct zone表示,在<linux/mmzone.h>中定义:

struct zone {
    unsigned long watermark[NR_WMARK];
    unsigned long lowmem_reserve[MAX_NR_ZONES];
    struct per_cpu_pageset pageset[NR_CPUS];
    spinlock_t lock;
    struct free_area free_area[MAX_ORDER];
    spinlock_t lru_lock;
    struct zone_lru {
        struct list_head list;
        unsigned long nr_saved_scan;
    }lru[NR_LRU_LISTS];
    struct zone_reclaim_stat reclaim_stat;
    unsigned long pages_scanned;
    unsigned long flags;
    atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
    int prev_priority;
    unsigned int inactive_ratio;
    wait_queue_head_t *wait_table;
    unsigned long wait_table_hash_nr_entries;
    unsgined long wait_table_bits;
    struct pglist_data *zone_pgdat;
    unsigned long zone_start_pfn;
    unsigned long spanned_pages;
    unsigned long present_pages;
    const char *name;
}
struct zone

lock域是一个自旋锁,它防止该结构被并发访问。这个域只保护结构,而不保护驻留在这个区中的所有页。

watermark数组持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。

name域是一个以NULL结束的字符串表示这个区的名字。在mm/page_alloc.c中,有名字“DMA”、“Normal”和“HighMem”

三、获得页

内核使用如下接口在内核内分配和释放内存,定义于<linux/gfp.h>中。

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
alloc_pages

函数分配2order(1<<order)个连续的物理页,并返回一个指针,指向第一个页的page结构体;如果出错,就返回NULL。

返回的页指针,可以用下面函数转换成逻辑地址。该函数返回一个指针,指向给定物理页当前所在的逻辑地址。

void *page_address(struct page *page);
page_address

如果你无须用到struct page,可以调用:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
__get_free_pages

这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。

如果只需要一页,有两个封装好的函数,只不过传递给order的值为0:

struct page *alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)
alloc_page

3.1 获得填充为0的页

函数可以让返回的页内容全为0

unsigned long get_zeroed_page(unsigned int gfp_mask);
get_zeroed_page

此函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0。

alloc_page(gfp_mask):只分配一页,返回指向页结构的指针
alloc_pages(gfp_mask, order):分配2^order个页,返回指向第一页页结构的指针
__get_free_page(gfp_mask):只分配一页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask, order):分配2^order页,返回指向第一页逻辑地址的指针
get_zeroed_page(gfp_mask):只分配一页,让其内容填充0,返回指向其逻辑地址的指针
底层页分配方法表

3.2 释放页

当你不再需要页时可以用下面的函数释放它们:

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

例子:
unsigned long page:

page = __get_free_pages(GFP_KERNEL, 3);
if(!page) {
    /* 没有足够的内存:你必须处理这种错误 */
    return -ENOMEM;
}
/* "page"现在指向8个连续页中第1个页的地址... */
用完这8个页后需要释放它们:
free_pages(page, 3);

/* 页现在已经被释放了,我们不应该再访问存放在"page"中的地址了 */
free_pages

 

四、kmalloc()

kmalloc和malloc类似,但是比malloc多了一个flags。kmalloc()函数是一个简单的接口,用它可以获得字节为单位的一块内核内存,如果需要整页,那么,前面的页分配接口可能更好的选择。

kmalloc()在<linux/slab.h>中声明:

void *kmalloc(size_t size, gfp_t flags)
size:内存块至少要有size大小,所分配的内存区在物理上是连续的。
出错返回NULL
例子:
struct dog *p;
p=kamlloc(sizeof(struct dog), GFP_KERNEL);
if(!p)
    /* 处理错误 ... */
kmalloc原型

4.1 gfp_mask标志

这些标志分为三类:行为修饰符、区修饰符及类型。

行为修饰符:内核应当如何分配所需的内存。在某些特定情况下,只能使用某些特定的方法分配内存。如:中断要求分配内存过程不能睡眠。

包括行为描述符都是在<linux/gfp.h>中声明的。不过,在<linux/slab.h>中包含有这个头文件,因此一般不必直接包含引用。

__GFP_WAIT:分配器可以睡眠
__GFP_HIGH:分配器可以访问紧急事件缓冲池
__GFP_IO:分配器可以启动磁盘I/O
__GFP_FS:分配器可以启动文件系统I/O
__GFP_COLD:分配器应该使用告诉缓存中快要淘汰出去的页
__GFP_NOWARN:分配器将不打印失败警告
__GFP_REPEAT:分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能
__GFP_NOFALL:分配器将无限的重复进行分配,分配不能失败
__GFP_NORETRY:分配器在分配失败时绝不会重新分配
__GFP_NO_GROW:由slab层内部使用
__GFP_COMP:添加混合页元数据,在hugetlb的代码内部使用
行为修饰符列表

 

区修饰符:从哪儿分配内存。内核把物理内存分为多个区。

类型标志符:组合行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。

4.2 kfree()

kamlloc的另一端就是kfree(),kfree在<linux/slab.h>中:

void kfree(const void *ptr)
kfree原型

中断处理程序中分配内存的例子:

char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC);
if(!buf)
    /* 内存分配错误 */
kfree(buf);        /* 用完释放 */
kmallco例子

五、vmalloc()

vmalloc相对于kmalloc,分配的虚拟内存地址是连续的,物理地址则无需连续。

kmalloc确保页在物理地址上是连续的。

vmalloc函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与用户空间相同。

void *vmalloc(unsgined long size);
/* 该函数返回指针,指向逻辑上连续的一块内存区,大小至少为size。 */
/* 错误时返回NULL。函数可能睡眠 */

void vfree(const void *addr)
/* 释放vmalloc获得的内存 */

/* 例子 */
char *buf;
buf = vmalloc(16*PAGE_SIZE);    /* get 16 pages */
if(!buf)
    /* 错误!不能分配内存 */
/*
 * buf现在指向虚拟地址连续的一块内存区,其大小至少为16*PAGE_SIZE
 */

vfree(buf);
/* 释放 */
vmalloc

六、slab层

分配和释放数据结构是所有内核中最普遍的操作之一。为了全局控制频繁的数据分配和回收,有了slba分配器。 

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
  • 频繁分配和回收必然会导致内存碎片,所以空闲链表缓存会连续地存放。因为已释放地数据接哦古又会放回空闲链表,因此不会导致碎片。
  • 回收地对象可以立即投入下一次分配,因此,对于频繁分批和释放,空闲链表能够提高其性能。
  • 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
  • 如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
  • 如果分配器是与NUMA相关的,它就可以从相同的内粗节点为请求者进行分配。
  • 对存放的对象进行着色,以防止多个对象映射到相同的告诉缓存行。

6.1 slab层的设计

slab把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。比如一个存放进程描述符,一个存放索引节点。

每个高速缓存都是用kmem_cache结构来表示。包括三个链表:slabs_full、slabs_partial和slabs_empty。都存放在kmem_list3结构内,该结构在mm/slab.c中。

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

 

6.2 slab分配器的接口

一个新的高速缓存通过以下函数创建:

struct kmem_cache *kmem_cache_create(const char *name,
    size_t size,
    size_t align,
    unsigned long flags,
    void (*ctor)(void *));
name:高速缓存的名字
size:高速缓存每个元素的大小
align:slab内第一个对象的偏移,它用来确保在页内进行特定的对齐
falgs:参数是可选的设置项
ctor:高速缓存的构造函数。基本抛弃,设NULL
kmem_cache_create

falgs有各种参数:

  • SLAB_HWCACHE_ALIGN:这个标志命令slab层把一个slab内所有对象按高速缓存行对齐。对齐越严格,浪费内存越多
  • SLAB_POISON:slab层用已知的值(a5a5a5a5)填充slab
  • SLAB_RED_ZONE:导致slab层在已分配的内存周围插入“红色警戒区”以探测缓冲越界
  • SLAB_PANIC:当分配失败时提醒slab层。
  • SLAB_CACHE_DMA:slab层可以执行DMA的内存给每个slab分配空间。

 

要撤销一个高速缓存,则调用:

int kmem_cache_destroy(struct kmem_cache *cachep);
kmem_cache_destroy

1.从缓存中分配

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t *flags)
cachep:返回一个指向对象的指针
flags:_get_free_pages()
创建缓存

释放一个对象,

void kmem_cache_free(struct kmem_cache *cachep, void *objp)
cachep:对象objp标记为空闲
释放分配的对象

2.slab分配器的使用实例

在kernel/fork.c中,

struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct",
    sizeof(struct task_struct),
    ARCH_MIN_TASKALIGN,
    SLAB_PANIC | SLAB_NOTRACK,
    NULL);

struct task_struct *tsk;
tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
if(!tsk)
    return NULL;

kmem_cache_free(task_struct_cachep, tsk);

int err;
err = kmem_cache_destroy(task_struct_cachep);
if(err)
    /* 出错,撤销高速缓存 */
例子

 

七、在栈上的静态分配

历史上,每个进程都有两页的内核栈。因为32位和64位体系结构的页面大小分别是4KB和8KB,所以通常它们的内核栈的带线啊哦分别是8KB和16KB 

7.1 单页内核栈

中断处理程序也要放在内核栈中,但同时会把更严格的约束台哦见加在这可怜的内核栈上。

所以有一个中断栈,可以为每个进程提供一个用于中断处理程序的栈。

7.2 正大光明的工作

大量的静态分配是很危险的,因此动态分配是一种明智的选择。

八、高端村内的映射

高端内存的页被映射到3GB~4GB

8.1 永久映射

要映射一个给定page结构到内核地址空间,可以使用定义在文件<linux/highmem.h>中的函数:

void *kmap(struct page *page)
这个函数在高端内存或低端内存上都能用。
kmap

8.2 临时映射

建立一个临时映射:

void *kmap_atomic(struct page *page, enum km_type type)
type是枚举类型之一
enum km_type {
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_USER0,
    KM_USER1,
    KM_BIO_SRC_IRQ,
    KM_BIO_DST_IRQ,
    KM_PTE0,
    KM_PTE1,
    KM_IRQ0,
    KM_IRQ1,
    KM_SOFTIRQ0,
    KM_SOFTIRQ1,
    KM_SYNC_ICACHE,
    KM_UML_USERCOPY,
    KM_IRQ_PTE,
    KM_NMI,
    KM_NMI_PTE,
    KM_TYPE_NR
};
kmap_atomic

 通过下列取消映射:

void kunmap_atomic(void *kvaddr, enum km_type type)
kunmap_atomic

 

九、每个CPU的分配

这个不知道干什么:

unsigned long my_percpu[NR_CPUS]'

int cpu;
cpu = get_cpu()    /* 获得当前处理器,并进制内核抢占 */
my_percpu[cpu]++;    /* ...或者无论什么 */
printk("my_percpu on cpu=%d is %lu\n", cpu, my_percpu[cpu]);
put_cpu();            /* 激活内核抢占 */
cpu例子

 

十、新的每个CPU接口

为了方便创建和操作每个CPU数据,而引进了新的操作接口,称作percpu。 

10.1 编译时的每个CPU数据

在编译时设置每个CPU的变量很简单:

DEFINE_PER_CPU(type,name);

10.2 运行时的每个CPU数据

内核实现每个CPU数据的动态分配方法类似于kmalloc()。原型在文件<linux/percpu.h>中:

void *alloc_percpu(type);    /* 一个宏 */
void *__alloc_percpu(size_t size, size_t align);
void free_percpu(const void *);
宏alloc_percpu()

内核提供了两个宏来利用指针获取每个CPU数据

get_cpu_var(ptr);    /* 返回一个void类型指针,该指针指向处理器的ptr拷贝 */    
put_cpu_var(ptr);    /* 完成:重新激活内核抢占 */
获取每个CPU数据

使用这些函数的例子:

void *percpu_ptr;
unsigned long *foo;

percpu_ptr = alloc_percpu(unsigned long);
if(!ptr)
    /* 内存分配错误... */

foo = get_cpu_var(percpu_ptr);
/* 操作foo ... */
put_cpu_var(percpu_ptr);
get_cpu_var例子

 

十一、使用每个CPU数据的原因

使用每个CPU数据有很多好处,减少了数据锁定。

第二个好处是使用每个CPU数据可以大大减少缓存失效。

每个CPU数据会省去许多数据上锁,唯一的要求是要禁止内核抢占。

 

十二、分配函数的选择

 

posted @ 2019-07-06 14:00  习惯就好233  阅读(386)  评论(0编辑  收藏  举报