zoukankan      html  css  js  c++  java
  • 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
    ", 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数据会省去许多数据上锁,唯一的要求是要禁止内核抢占。

    十二、分配函数的选择

  • 相关阅读:
    二、Blender/Python API总览
    一、Blender/Python 快速入门
    【翻译】View Frustum Culling --3 Clip Space Approach – Extracting the Planes
    【翻译】View Frustum Culling --2 Geometric Approach – Extracting the Planes
    【翻译】 View Frustum Culling --1 View Frustum’s Shape
    列表切片
    numpy--深度学习中的线代基础
    基于正则表达式用requests下载网页中的图片
    Python基础知识整理
    C++ | boost库 类的序列化
  • 原文地址:https://www.cnblogs.com/ch122633/p/11142543.html
Copyright © 2011-2022 走看看