图片 6

Linux内核——内存管理

来想这样一个问题:在表空间级别有碎片吗?

转自:

Linux内核——内存管理

答案是:有,但也要看情况。

内核的内存使用不像用户空间那样随意,内核的内存出现错误时也只有靠自己来解决(用户空间的内存错误可以抛给内核来解决)。

内存管理

在统一区大小表空间中,因为区的大小一致,不会出现碎片问题。但在系统管理区中,由于区的大小不一致,仍会存在碎片。

所有内核的内存管理必须要简洁而且高效。

内核把物理页作为内存管理的基本单位;内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理。MMU以页大小为单位来管理系统中的页表。从虚拟内存的角度看,页就是最小单位。

32位系统:页大小4KB

64位系统:页大小8KB

在支持4KB页大小并有1GB物理内存的机器上,物理内存会被划分为262144个页。内核用
struct page 结构表示系统中的每个物理页。

struct page {

page_flags_t flags; /* 表示页的状态,每一位表示一种状态*/

atomic_t _count; /* 存放页的引用计数,0代表没有被引用 */

atomic_t _mapcount;

unsigned long private;

strcut address_space *mapping;

pgoff_t index;

struct list_head lru;

void *virtual; /* 页在虚拟内存中的地址,动态映射物理页 */

}

下面,我们来解释下其中的重要字段。

flags:这个字段用于存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。
flag
的每一位单独表示一种状态,所以,它至少可以同时表示出32种不同的状态。

_count:这个字段存放页的使用计数,也就是这个页被引用了多少次。很奇怪,技术值变为
-1
时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它,注意,这个字段使用的是
-1 代表未使用,而不是 0 。

virtual:这个字段是页的虚拟地址。

mapping:这个域指向和这个页关联的address_space 对象。

private:这个根据名字就可以看得出,它指向私有数据。

内核通过这样的数据结构管理系统中所有的页,因为内核需要知道一个页是否空闲,谁有拥有这个页。拥有者可能是:用户空间进程、动态分配的内核数据、静态内核代码、页高速缓存等等。系统中每一个物理页都要分配这样一个结构体,进行内存管理。

比如说,有很多个64KB 的区,互相不连续,分布在数据文件的各个角落。当需要
1MB、 8MB 大小的区时,这些不连续的 64KB
区无法被重用,这就是典型的碎片了。但是,这种情况很少出现。因为区不会被频繁地分配、释放。

主要内容:

由于硬件的限制,内核并不能对所有的页一视同仁。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:

1)一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。

2)一些体系结构其内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。

由于存在这种限制,内核把具有相似特性的页划分为不同的区(ZONE):

1)ZONE_DMA——这个区包含的页能用来执行DMA操作。

2)ZONE_NORMAL——这个区包含的都是能正常地映射网页。

3)ZONE_DMA32——同上,不过只能被32位设备访问

4)ZONE_HIGHMEM——这个区包含“高端内存”,其中的页并能不永久地映射到内核地址空间。

Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配。注意,区的划分没有任何物理意义,这只是内核为了管理页而采取的一种逻辑上的分组。用于DMA的内存必须从ZONE_DMA中进行分配,但是一般用途的内存却既能从ZONE_DMA分配,也能从ZONE_NORMAL分配。

一个表创建之后,很少会去对它进行 Drop、 Truncate 操作。

  • 内存的管理单元
  • 获取内存的方法
  • 获取高端内存
  • 内核内存的分配方式
  • 总结

获得页

内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>。最核心的函数是:

structpage *alloc_pages( unsigned int gfp_mask, unsigned int order );

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

void*page_address( struct page *page );

把给定的页转换成它的逻辑地址。如果无须用到 struct page,可以调用:

unsignedlong __get_free_pages( unsigned int gfp_mask, unsigned int
order );

这个函数与alloc_pages
作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,因此其他页也会紧随其后。

如果只需要一页,可以用以下两个函数:

structpage *alloc_page( unsigned int gfp_mask );

unsignedlong _get_free_page( unsigned int gfp_mask );

如果需要让返回页的内容全为0,可以使用下面这个函数

unsignedlong get_zeroed_page(unsigned int gfp_mask );

方法

描述

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,返回指向其逻辑地址的指针

当不再需要页时可以使用以下函数来释放它。

void__free_pages( struct page *page, unsigned int order );

voidfree_pages( unsigned long addr, unsigned int order );

voidfree_page( unsigned long addr );

释放页时要谨慎,只能释放属于你的页。传递了错误的 struct page
或地址,用了错误的 order
值都可能导致系统崩溃。请记住,内核是完全依赖自己的。

没有频繁的分配、释放操作,碎片也就很少出现了。所以,在表空间层,碎片已经是一个可以忽略的问题了。当然,在表层、索引层,还有可能存在碎片。

 

kmalloc()

kmalloc 与 malloc 一族函数非常类似,只不过它多了一个 flags
参数。kmalloc在<linux/slab.h>中声明:

void*kmalloc( size_t size, int flags );

这个函数返回一个指向内存块的指针,其内存块至少要有 size
大小。所分配的内存正在物理上是连续的。在出错时,它返回
NULL。除非没有足够的内存可用,否则内核总能分配成功。在对 kmalloc
调用之后,你必须检查返回的是不是 NULL,如果是,要适当地处理错误。

在低级页分配函数还是 kmalloc
中,都用到了gfp_mask(分配器标志)。这些标志可分为三类:行为修饰符、区修饰符及类型。

1)行为修饰符表示内核应当如何分配所需的内存。在某些特定情况下,只能使用某些特定的方法分配内存。例如,中断处理程序就要求内核在分配内存的过程中不能睡眠(因为中断处理程序不能被重新调度)。

2)区修饰符指明到底从哪一区中进行分配。

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

kmalloc 的另一端就是 kfree,kfree声明于<linux/slab.h>中

voidkfree( const void *ptr );

kfree 函数释放由 kmalloc分配出来的内存块。调用 kfree( NULL ) 是安全的。

1. 内存的管理单元

内存最基本的管理单元是页,同时按照内存地址的大小,大致分为3个区。

 

vmalloc()

vmalloc 的工作方式是类似于
kmalloc,只不过前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是这并不保证他们在物理RAM中也是连续的。kmalloc()函数确保页在物理地址上是连续。vmalloc函数值确保在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,在修订页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。

大多数情况下,只有硬件设备需要得到物理地址连续的内存,因为硬件设备存在内存管理单元以外,它根本不理解什么是虚拟地址。尽管仅仅在某些情况下才需要物理上连续的内存块,但是很多内核都有kmalloc()来获取内存,而不是vmalloc()。这主要出于性能方面的考虑。vmalloc()函数为了把物理上不连续的页转换成虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地进行映射。因为这些原因,一般是在为了获得大块内存时,例如当模块被动态插入内核时,就把模块装载到由vmalloc()分配的内存上。

void *vmalloc(unsigned long size)

该函数返回一个指针,指向逻辑上连续的一块内存,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此么不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。

释放通过vfree()函数

void vfree(const void *addr)

1.1 页

页的大小与体系结构有关,在 x86 结构中一般是 4KB或者8KB。

可以通过 getconf 命令来查看系统的page的大小:

[wangyubin@localhost ]$ getconf -a | grep -i 'page'

PAGESIZE                           4096
PAGE_SIZE                          4096
_AVPHYS_PAGES                      637406
_PHYS_PAGES                        2012863

以上的 PAGESIZE 就是当前机器页大小,即 4KB

 

页的结构体头文件是: <linux/mm_types.h>
位置:include/linux/mm_types.h

图片 1

/*
 * 页中包含的成员非常多,还包含了一些联合体
 * 其中有些字段我暂时还不清楚含义,以后再补上。。。
 */
struct page {
    unsigned long flags;    /* 存放页的状态,各种状态参见<linux/page-flags.h> */
    atomic_t _count;        /* 页的引用计数 */
    union {
        atomic_t _mapcount;    /* 已经映射到mms的pte的个数 */
        struct {        /* 用于slab层 */
            u16 inuse;
            u16 objects;
        };
    };
    union {
        struct {
        unsigned long private;        /* 此page作为私有数据时,指向私有数据 */
        struct address_space *mapping;    /* 此page作为页缓存时,指向关联的address_space */
        };
#if USE_SPLIT_PTLOCKS
        spinlock_t ptl;
#endif
        struct kmem_cache *slab;    /* 指向slab层 */
        struct page *first_page;    /* 尾部复合页中的第一个页 */
    };
    union {
        pgoff_t index;        /* Our offset within mapping. */
        void *freelist;        /* SLUB: freelist req. slab lock */
    };
    struct list_head lru;    /* 将页关联起来的链表项 */
#if defined(WANT_PAGE_VIRTUAL)
    void *virtual;            /* 页的虚拟地址 */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
    unsigned long debug_flags;    /* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
    /*
     * kmemcheck wants to track the status of each byte in a page; this
     * is a pointer to such a status block. NULL if not tracked.
     */
    void *shadow;
#endif
};

图片 2

物理内存的每个页都有一个对应的 page
结构,看似会在管理上浪费很多内存,其实细细算来并没有多少。

比如上面的page结构体,每个字段都算4个字节的话,总共40多个字节。(union结构只算一个字段)

 

那么对于一个页大小 4KB 的 4G内存来说,一个有 4*1024*1024 / 4 = 1048576
个page,

一个page 算40个字节,在管理内存上共消耗内存 40MB左右。

 

如果页的大小是 8KB 的话,消耗的内存只有 20MB 左右。相对于 4GB
来说并不算很多。

 

slab层

为了便于数据的频繁分配和回收,Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。

slab层把不同的对象划分为高速缓存,其中每个高速缓存组中存放的都是不同类型的数据结构对象。例如,一个高速缓存用于存放进程描述符,另一个高速缓存用于存放i节点。这些高速缓存又被划分为slab,slab由一个或多个物理上连续的页组成。一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。

每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于三种状态之一:满、部分满或空。当内核的某一部分需要一个对象时,就要由slab分配了,首先考虑的是部分满的slab,如果不存在部分满的slab则去空的slab分配,如果也不存在空的slab,则内核需要申请页重新分配高速缓存。下图描述了高速缓存、slab及对象之间的关系,来自

图片 3

< 喎?” target=”_blank”
class=”keylink”>vcD4KPHA+1fu49nNsYWKy47XE1K3A7cjnz8KjujwvcD4KPHA+MS6/ydLU1NrE2rTm1tC9qMGiuPfW1rbUz/O1xLjfy9m7urTmKLHIyOe9+LPMw+jK9s/gudi1xL3hubkgdGFza19zdHJ1Y3QKtcS438vZu7q05ik8L3A+CjxwPjIus/3By9XrttTM2LaottTP87XEuN/L2bu6tObS1M3io6zSstPQzajTw7bUz/O1xLjfy9m7urTmPC9wPgo8cD4zLsO/uPa438vZu7q05tbQsPy6rLbguPYgc2xhYqOsc2xhYtPD09q53MDtu7q05rXEttTP8zwvcD4KPHA+NC5zbGFi1tCw/LqstuC49ru6tOa1xLbUz/OjrM7vwO3Jz9PJ0rvSs7vytuC49sGs0Pi1xNKz1+mzyTwvcD4KPHA+w7+49rjfy9m7urTmtrzKx9PDa21lbV9jYWNoZV9zIL3hubnAtLHtyr6ho9XiuPa94bm5sPy6rMj9uPbBtLHtIHNsYWJzX2Z1bGyjrHNsYWJzX3BhcnRpYWy6zSBzbGFic19lbXB0eaOsvvm05rfF1Noga21lbV9saXN0cyC94bm5xNqho9Xi0KnBtLHtsPy6rLjfy9m7urTm1tC1xMv509BzbGFioaNzbGFiw+jK9rf7IHN0cnVjdHNsYWIg08PAtMPoyvbDv7j2c2xhYqO6PC9wPgo8cD5zdHJ1Y3Qgc2xhYiB7PC9wPgo8cD4gICAgc3RydWN0IGxpc3RfaGVhZCBsaXN0OyAgICAgICAgIC8qIML6oaKyv7fWwvq78r/VwbSx7SAqLzwvcD4KPHA+ICAgIHVuc2lnbmVkIGxvbmcgY29sb3Vyb2ZmOyAgLyogc2xhYiDXxcmrtcTGq9LGwb8gICAqLzwvcD4KPHA+ICAgIHZvaWQgKnNfbWVtOyAgICAgICAgICAgICAgICAgIC8qINTaIHNsYWIg1tC1xLXa0ru49rbUz/MgKi88L3A+CjxwPiAgICB1bnNpZ25lZCBpbnQgaW51c2U7ICAgICAgICAgIC8qINLRt9bF5LXEttTP88r9ICAgICAgICAqLzwvcD4KPHA+ICAgIGttZW1fYnVmY3RsX3QgdHJlZTsgICAgICAgICAvKiC12tK7uPa/1bzkttTP86OoyOe5+9PQtcS7sKOpICovPC9wPgo8cD59OzwvcD4KPGgyPnNsYWK31sXkxve1xL3Tv9o8L2gyPgo8cD7W99Kq09DLxLj2PC9wPgo8cHJlIGNsYXNzPQ==”brush:java;”>1.
高速缓存的创建

struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
  1. 从高速缓存中分配对象

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

  1. 释放对象,返回给原先的slab

    void kmem_cache_free(struct kmem_cache cachep, void objp)

4.高速缓存的销毁

void kmem_cache_destroy(struct kmem_cache *cachep)

1.2 区

页是内存管理的最小单元,但是并不是所有的页对于内核都一样。

内核将内存按地址的顺序分成了不同的区,有的硬件只能访问有专门的区。

 

内核中分的区定义在头文件 <linux/mmzone.h>
位置:include/linux/mmzone.h

内存区的种类参见 enum zone_type 中的定义。

 

内存区的结构体定义也在 <linux/mmzone.h> 中。

具体参考其中 struct zone 的定义。

 

其实一般主要关注的区只有3个:

描述

物理内存

ZONE_DMA DMA使用的页 <16MB
ZONE_NORMAL 正常可寻址的页 16~896MB
ZONE_HIGHMEM 动态映射的页 >896MB

 

某些硬件只能直接访问内存地址,不支持内存映射,对于这些硬件内核会分配
ZONE_DMA 区的内存。

某些硬件的内存寻址范围很广,比虚拟寻址范围还要大的多,那么就会用到
ZONE_HIGHMEM 区的内存,

对于 ZONE_HIGHMEM 区的内存,后面还会讨论。

对于大部分的内存申请,只要用 ZONE_NORMAL 区的内存即可。

 

slab解决内存碎片

内存碎片存在的方式有两种:a.内部碎片 b.外部碎片

内部碎片的产生:因为所有的内存分配必须起始于可被 4、8 或 16
整除(视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。假设当某个客户请求一个
43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得
44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片。
外部碎片的产生:
频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。假设有一块一共有100个单位的连续空闲内存空间,范围是0~99。如果你从中申请一块内存,如10个单位,那么申请出来的内存块就为0~9区间。这时候你继续申请一块内存,比如说5个单位大,第二块得到的内存块就应该为10~14区间。如果你把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。现在整个内存空间的状态是0~9空闲,10~14被占用,15~24被占用,25~99空闲。其中0~9就是一个内存碎片了。如果10~14一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了,变成外部碎片。

解决方法:

slab机制,因为slab预先分配了特定数据结构大小的内存,所以没有内部碎片或者外部碎片。

2. 获取内存的方法

内核中提供了多种获取内存的方法,了解各种方法的特点,可以恰当的将其用于合适的场景。

 

slab与传统内存管理模式比较:

与传统的内存管理模式相比, slab
缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab
缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab
分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab
分配器还可以支持硬件缓存对齐和着色,这防止错误的共享(两个或两个对象尽管位于不同的内存地址,但映射到相同的告诉缓冲行),这可以提高性能,但以增加内存浪费为代价。

2.1 按页获取 – 最原始的方法,用于底层获取内存的方式

以下分配内存的方法参见:<linux/gfp.h>

方法

描述

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,返回指向其逻辑地址的指针

 

alloc** 方法和 get**
方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。

如果无须直接操作物理页结构体的话,一般使用 get** 方法。

 

相应的释放内存的函数如下:也是在 <linux/gfp.h> 中定义的

extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
extern void free_hot_page(struct page *page);

在请求内存时,参数中有个 gfp_mask
标志,这个标志是控制分配内存时必须遵守的一些规则。

gfp_mask 标志有3类:(所有的 GFP 标志都在 <linux/gfp.h> 中定义)

  1. 行为标志 :控制分配内存时,分配器的一些行为
  2. 区标志   :控制内存分配在那个区(ZONE_DMA, ZONE_NORMAL,
    ZONE_HIGHMEM 之类)
  3. 类型标志 :由上面2种标志组合而成的一些常用的场景

 

行为标志主要有以下几种:

行为标志

描述

__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 的代码内部使用

 

区标志主要以下3种:

区标志

描述

__GFP_DMA 从 ZONE_DMA 分配
__GFP_DMA32 只在 ZONE_DMA32 分配 (注1)
__GFP_HIGHMEM 从 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2)

注1:ZONE_DMA32 和 ZONE_DMA
类似,该区包含的页也可以进行DMA操作。 
         唯一不同的地方在于,ZONE_DMA32 区的页只能被32位设备访问。 
注2:优先从 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 没有多余的页则从
ZONE_NORMAL 分配。

 

类型标志是编程中最常用的,在使用标志时,应首先看看类型标志中是否有合适的,如果没有,再去自己组合
行为标志和区标志。

类型标志

实际标志

描述

GFP_ATOMIC __GFP_HIGH 这个标志用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 0 与 GFP_ATOMIC 类似,不同之处在于,调用不会退给紧急内存池。 
这就增加了内存分配失败的可能性
GFP_NOIO __GFP_WAIT 这种分配可以阻塞,但不会启动磁盘I/O。 
这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,可能会导致递归
GFP_NOFS (__GFP_WAIT | __GFP_IO) 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作。 
这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中
GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS ) 这是常规的分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。 
为了获得调用者所需的内存,内核会尽力而为。这个标志应当为首选标志
GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS ) 这是常规的分配方式,可能会阻塞。用于为用户空间进程分配内存时
GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS )|__GFP_HIGHMEM) 从 ZONE_HIGHMEM 进行分配,可能会阻塞。用于为用户空间进程分配内存
GFP_DMA __GFP_DMA 从 ZONE_DMA 进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志 
通常与以上的某个标志组合在一起使用。

 

以上各种类型标志的使用场景总结:

场景

相应标志

进程上下文,可以睡眠 使用 GFP_KERNEL
进程上下文,不可以睡眠 使用 GFP_ATOMIC,在睡眠之前或之后以 GFP_KERNEL 执行内存分配
中断处理程序 使用 GFP_ATOMIC
软中断 使用 GFP_ATOMIC
tasklet 使用 GFP_ATOMIC
需要用于DMA的内存,可以睡眠 使用 (GFP_DMA|GFP_KERNEL)
需要用于DMA的内存,不可以睡眠 使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠之前执行内存分配

 

在栈上的静态分配

内核栈大小固定。我们在进程时要注意节省栈资源,要控制函数内的局部变量,尽量不要出现大型数组或大型结构体。尤其对于内核栈,一旦造成溢出,就会影响到内核数据(如thread_info)。所以应当优先考虑动态分配。另外一个进程的内核栈和中断栈是分开的,这样可以减轻内核栈的负担(一个内核栈只占1页或2页)。

2.2 按字节获取 – 用的最多的获取方法

这种内存分配方法是平时使用比较多的,主要有2种分配方法:kmalloc()和vmalloc()

kmalloc的定义在 <linux/slab_def.h> 中

图片 4

/**
 * @size  - 申请分配的字节数
 * @flags - 上面讨论的各种 gfp_mask
 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
#+end_src

vmalloc的定义在 mm/vmalloc.c 中
#+begin_src C
/**
 * @size - 申请分配的字节数
 */
void *vmalloc(unsigned long size)

图片 5

kmalloc 和 vmalloc 区别在于:

  • kmalloc 分配的内存物理地址是连续的,虚拟地址也是连续的
  • vmalloc 分配的内存物理地址是不连续的,虚拟地址是连续的

 

因此在使用中,用的较多的还是 kmalloc,因为kmalloc 的性能较好。

因为kmalloc的物理地址和虚拟地址之间的映射比较简单,只需要将物理地址的第一页和虚拟地址的第一页关联起来即可。

而vmalloc由于物理地址是不连续的,所以要将物理地址的每一页都和虚拟地址关联起来才行。

 

kmalloc 和 vmalloc 所对应的释放内存的方法分别为:

void kfree(const void *)
void vfree(const void *)

 

高端内存的映射

因为32位的处理器能够寻址达到4GB。一旦这些页被分配,就必须映射到内核的虚拟内存空间上。

高于896MB的所有物理内存的范围大都是高端内存,它不会永久或自动的映射到内核虚拟地址空间。

内核地址的虚拟内存大小为1G,其中0-896M的内存与物理内存一一映射,即线性映射。而896MB~1024MB的虚拟内存如果也与物理内存线性映射,那么内核态只能使用1G的物理内存,即使物理内存大于1G(比如4G),这样的话就没有充分利用物理内存了。所以内核虚拟内存中的896MB~1024MB与高端内存不会一一映射。具体的映射方式如下:

当内核态需要访问高端物理内存时,在内核虚拟内存空间中的896-1024MB找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想要访问的那段物理内存,临时用一会,用完后归还。这样当进程后面又需要访问其他的高端物理内存时,仍然可以用这段逻辑地址空间。

高端内存的最基本思想:在内核虚拟空间896MB~1024MB的内存中借一段地址空间,建立与高端物理内存的临时地址映射,用完后释放虚拟空间,达到这段虚拟地址空间可以循环使用,访问所有物理内存。

高端内存映射有三种方式:

1、映射到“内核动态映射空间”

这种方式很简单,因为通过 vmalloc()
,在”内核动态映射空间“申请内存的时候,就可能从高端内存获得页面(参看
vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间“ 中。
2、永久内核映射
如果是通过alloc_page() 获得了高端内存对应的
page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START
,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M
之间。这个空间起叫“内核永久映射空间”或者“永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是
swapper_pg_dir,对普通进程来说,通过 CR3
寄存器指向。通常情况下,这个空间是 4M
大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table
寻找这个页表。
3、临时映射

当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(也就是原子映射)。有一组保留的映射,他们可以存放新创建的临时映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。

2.3 slab层获取 – 效率最高的获取方法

频繁的分配/释放内存必然导致系统性能的下降,所以有必要为频繁分配/释放的对象内心建立缓存。

而且,如果能为每个处理器建立专用的高速缓存,还可以避免
SMP锁带来的性能损耗。

 

每个CPU数据

SMP环境下加锁过多的话,会严重影响并行的效率,如果是自旋锁的话,还会浪费其他CPU的执行时间。所以内核中才有了按CPU分配数据的接口。按CPU分配数据之后,每个CPU自己的数据不会被其他CPU访问,虽然浪费了一点内存,但是会使系统更加的简洁高效。

按CPU来分配数据主要有2个优点:

1.最直接的效果就是减少了对数据的锁,提高了系统的性能

2.由于每个CPU有自己的数据,所以处理器切换时可以大大减少缓存失效的几率。因为如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动,对系统性能影响很大。

内存管理 页
内核把物理页作为内存管理的基本单位;内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)…

2.3.1 slab层实现原理

linux中的高速缓存是用所谓 slab
层来实现的,slab层即内核中管理高速缓存的机制。

整个slab层的原理如下:

  1. 可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构
    task_struct 的高速缓存)
  2. 除了针对特定对象的高速缓存以外,也有通用对象的高速缓存
  3. 每个高速缓存中包含多个 slab,slab用于管理缓存的对象
  4. slab中包含多个缓存的对象,物理上由一页或多个连续的页组成

 

高速缓存->slab->缓存对象之间的关系如下图:

图片 6

 

2.3.2 slab层的应用

slab结构体的定义参见:mm/slab.c

图片 7

struct slab {
    struct list_head list;   /* 存放缓存对象,这个链表有 满,部分满,空 3种状态  */
    unsigned long colouroff; /* slab 着色的偏移量 */
    void *s_mem;             /* 在 slab 中的第一个对象 */
    unsigned int inuse;         /* slab 中已分配的对象数 */
    kmem_bufctl_t free;      /* 第一个空闲对象(如果有的话) */
    unsigned short nodeid;   /* 应该是在 NUMA 环境下使用 */
};

图片 8

 

slab层的应用主要有四个方法:

  • 高速缓存的创建
  • 从高速缓存中分配对象
  • 向高速缓存释放对象
  • 高速缓存的销毁

图片 9

/**
 * 创建高速缓存
 * 参见文件: mm/slab.c
 * 这个函数的注释很详细,这里就不多说了。
 */
struct kmem_cache *
kmem_cache_create (const char *name, size_t size, size_t align,
    unsigned long flags, void (*ctor)(void *))

/**
 * 从高速缓存中分配对象也很简单
 * 函数参见文件:mm/slab.c
 * @cachep - 指向高速缓存指针
 * @flags  - 之前讨论的 gfp_mask 标志,只有在高速缓存中所有slab都没有空闲对象时,
 *           需要申请新的空间时,这个标志才会起作用。
 *
 * 分配成功时,返回指向对象的指针
 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

/**
 * 向高速缓存释放对象
 * @cachep - 指向高速缓存指针
 * @objp   - 要释放的对象的指针
 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)

/**
 * 销毁高速缓存
 * @cachep - 指向高速缓存指针 
 */
void kmem_cache_destroy(struct kmem_cache *cachep)

图片 10

 

我做了创建高速缓存的例子,来尝试使用上面的几个函数。

测试代码如下:(其中用到的 kn_common.h 和 kn_common.c
参见之前的博客《Linux内核设计与实现》读书笔记(六)-
内核数据结构)

图片 11

#include <linux/slab.h>
#include <linux/slab_def.h>
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");

#define MYSLAB "testslab"

static struct kmem_cache *myslab;

/* 申请内存时调用的构造函数 */
static void ctor(void* obj)
{
    printk(KERN_ALERT "constructor is running....n");
}

struct student
{
    int id;
    char* name;
};

static void print_student(struct student *);


static int testslab_init(void)
{
    struct student *stu1, *stu2;

    /* 建立slab高速缓存,名称就是宏 MYSLAB */
    myslab = kmem_cache_create(MYSLAB,
                               sizeof(struct student),
                               0,
                               0,
                               ctor);

    /* 高速缓存中分配2个对象 */
    printk(KERN_ALERT "alloc one student....n");
    stu1 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb1";
    print_student(stu1);

    printk(KERN_ALERT "alloc one student....n");
    stu2 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu2->id = 2;
    stu2->name = "wyb2";
    print_student(stu2);

    /* 释放高速缓存中的对象 */
    printk(KERN_ALERT "free one student....n");
    kmem_cache_free(myslab, stu1);

    printk(KERN_ALERT "free one student....n");
    kmem_cache_free(myslab, stu2);

    /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */
    return 0;
}

static void testslab_exit(void)
{
    /* 删除建立的高速缓存 */
    printk(KERN_ALERT "*************************n");
    print_current_time(0);
    kmem_cache_destroy(myslab);
    printk(KERN_ALERT "testslab is exited!n");
    printk(KERN_ALERT "*************************n");

    /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */
}

static void print_student(struct student *stu)
{
    if (stu != NULL)
    {
        printk(KERN_ALERT "**********student info***********n");
        printk(KERN_ALERT "student id   is: %dn", stu->id);
        printk(KERN_ALERT "student name is: %sn", stu->name);
        printk(KERN_ALERT "*********************************n");
    }
    else
        printk(KERN_ALERT "the student info is null!!n");    
}

module_init(testslab_init);
module_exit(testslab_exit);

图片 12

 

Makefile文件如下:

图片 13

# must complile on customize kernel
obj-m += myslab.o
myslab-objs := testslab.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

图片 14

 

执行测试代码:(我是在 centos6.3 x64 上实验的)

图片 15

[root@vbox chap12]# make
[root@vbox chap12]# insmod myslab.ko 
[root@vbox chap12]# dmesg | tail -220 
# 可以看到第一次申请内存时,系统一次分配很多内存用于缓存(构造函数执行了多次)
[root@vbox chap12]# cat /proc/slabinfo | grep test #查看我们建立的缓存名在不在系统中
testslab               0      0     16  202    1 : tunables  120   60    0 : slabdata      0      0      0
[root@vbox chap12]# rmmod myslab.ko #卸载内核模块
[root@vbox chap12]# cat /proc/slabinfo | grep test #我们的缓存名已经不在系统中了

图片 16

 

3. 获取高端内存

高端内存就是之前提到的 ZONE_HIGHMEM 区的内存。

在x86体系结构中,这个区的内存不能映射到内核地址空间上,也就是没有逻辑地址,

为了使用 ZONE_HIGHMEM 区的内存,内核提供了永久映射和临时映射2种手段:

 

3.1 永久映射

永久映射的函数是可以睡眠的,所以只能用在进程上下文中。

图片 17

/* 将 ZONE_HIGHMEM 区的一个page永久的映射到内核地址空间
 * 返回值即为这个page对应的逻辑地址
 */
static inline void *kmap(struct page *page)

/* 允许永久映射的数量是有限的,所以不需要高端内存时,应该及时的解除映射 */
static inline void kunmap(struct page *page)

图片 18

 

3.2 临时映射

临时映射不会阻塞,也禁止了内核抢占,所以可以用在中断上下文和其他不能重新调度的地方。

图片 19

/**
 * 将 ZONE_HIGHMEM 区的一个page临时映射到内核地址空间
 * 其中的 km_type 表示映射的目的,
 * enum kn_type 的定义参见:<asm/kmap_types.h>
 */
static inline void *kmap_atomic(struct page *page, enum km_type idx)

/* 相应的解除映射是个宏 */
#define kunmap_atomic(addr, idx)    do { pagefault_enable(); } while (0)

图片 20

以上的函数都在 <linux/highmem.h> 中定义的。

 

4. 内核内存的分配方式

内核的内存分配和用户空间的内存分配相比有着更多的限制条件,同时也有着更高的性能要求。

下面讨论2个和用户空间不同的内存分配方式。

 

4.1 内核栈上的静态分配

用户空间中一般不用担心栈上的内存不足,也不用担心内存的管理问题(比如内存越界之类的),

即使出了异常也有内核来保证系统的正常运行。

 

而在内核空间则完全不一样,不仅栈空间有限,而且为了管理的效率和尽量减少问题的发生,

内核栈一般都是小而且固定的。

 

在x86体系结构中,内核栈的大小一般就是1页或2页,即 4KB ~ 8KB

内核栈可以在编译内核时通过配置选项将内核栈配置为1页,

配置为1页的好处是分配时比较简单,只有一页,不存在内存碎片的情况,因为一页是本就是分配的最小单位。

当有中断发生时,如果共享内核栈,中断程序和被中断程序共享一个内核栈会可能导致空间不足,

于是,每个进程除了有个内核栈之外,还有一个中断栈,中断栈一般也就1页大小。

 

查看当前系统内核栈大小的方法:

[xxxxx@localhost ~]$ ulimit -a | grep 'stack'
stack size              (kbytes, -s) 8192

 

4.2 按CPU分配

与单CPU环境不同,SMP环境下的并行是真正的并行。单CPU环境是宏观并行,微观串行。

真正并行时,会有更多的并发问题。

 

假定有如下场景:

图片 21

void* p;

if (p == NULL)
{
/* 对 P 进行相应的操作,最终 P 不是NULL了 */
}
else
{
/* P 不是NULL,继续对 P 进行相应的操作 */
}

图片 22

在上述场景下,可能会有以下的执行流程:

  1. 刚开始 p == NULL
  2. 线程A 执行到 [if (p == NULL)] ,刚进入 if 内的代码时被线程B
    抢占 
      由于线程A 还没有执行 if 内的代码,所以 p 仍然是 NULL
  3. 线程B 抢占到CPU后开始执行,执行到 [if (p == NULL)]时, 发现 p 是
    NULL,执行 if 内的代码
  4. 线程B 执行完后,线程A 重新被调度,继续执行 if 的代码 
      其实此时由于线程B 已经执行完,p 已经不是 NULL了,线程A
    可能会破坏线程B 已经完成的处理,导致数据不一致

 

在单CPU环境下,上述情况无需加锁,只需在 if 处理之前禁止内核抢占,在 else
处理之后恢复内核抢占即可。

而在SMP环境下,上述情况必须加锁,因为禁止内核抢占只能禁止当前CPU的抢占,其他的CPU仍然调度线程B
来抢占线程A 的执行

 

SMP环境下加锁过多的话,会严重影响并行的效率,如果是自旋锁的话,还会浪费其他CPU的执行时间。

所以内核中才有了按CPU分配数据的接口。

按CPU分配数据之后,每个CPU自己的数据不会被其他CPU访问,虽然浪费了一点内存,但是会使系统更加的简洁高效。

 

4.2.1 按CPU分配的优势

按CPU来分配数据主要有2个优点:

  1. 最直接的效果就是减少了对数据的锁,提高了系统的性能
  2. 由于每个CPU有自己的数据,所以处理器切换时可以大大减少缓存失效的几率
    (*注1)

 

注1:如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个

处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动,对系统性能影响很大。

 

4.2.2 编译时分配

可以在编译时就定义分配给每个CPU的变量,其分配的接口参见:<linux/percpu-defs.h>

/* 给每个CPU声明一个类型为 type,名称为 name 的变量 */
DECLARE_PER_CPU(type, name)
/* 给每个CPU定义一个类型为 type,名称为 name 的变量 */
DEFINE_PER_CPU(type, name)

注意上面两个宏,一个是声明,一个是定义。

其实也就是 DECLARE_PER_CPU 中多了个 extern 的关键字

 

分配好变量后,就可以在代码中使用这个变量 name 了。

DEFINE_PER_CPU(int, name);      /* 为每个CPU定义一个 int 类型的name变量 */

get_cpu_var(name)++;            /* 当前处理器上的name变量 +1 */
put_cpu_var(name);              /* 完成对name的操作后,激活当前处理器的内核抢占 */

 

通过 get_cpu_var 和 put_cpu_var
的代码,我们可以发现其中有禁止和激活内核抢占的函数。

相关代码在 <linux/percpu.h> 中

#define get_cpu_var(var) (*({                
    extern int simple_identifier_##var(void);    
    preempt_disable();/* 这句就是禁止当前处理器上的内核抢占 */    
    &__get_cpu_var(var); }))
#define put_cpu_var(var) preempt_enable()  /* 这句就是激活当前处理器上的内核抢占 */

 

4.2.3 运行时分配

除了像上面那样静态的给每个CPU分配数据,还可以以指针的方式在运行时给每个CPU分配数据。

动态分配参见:<linux/percpu.h>

图片 23

/* 给每个处理器分配一个 size 字节大小的对象,对象的偏移量是 align */
extern void *__alloc_percpu(size_t size, size_t align);
/* 释放所有处理器上已分配的变量 __pdata */
extern void free_percpu(void *__pdata);

/* 还有一个宏,是按对象类型 type 来给每个CPU分配数据的,
 * 其实本质上还是调用了 __alloc_percpu 函数 */
#define alloc_percpu(type)    (type *)__alloc_percpu(sizeof(type), 
                               __alignof__(type))

图片 24

 

动态分配的一个使用例子如下:

图片 25

void *percpu_ptr;
unsigned long *foo;

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

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

图片 26

 

5. 总结

在众多的内存分配函数中,如何选择合适的内存分配函数很重要,下面总结了一些选择的原则:

应用场景

分配函数选择

如果需要物理上连续的页 选择低级页分配器或者 kmalloc 函数
如果kmalloc分配是可以睡眠 指定 GFP_KERNEL 标志
如果kmalloc分配是不能睡眠 指定 GFP_ATOMIC 标志
如果不需要物理上连续的页 vmalloc 函数 (vmalloc 的性能不如 kmalloc)
如果需要高端内存 alloc_pages 函数获取 page 的地址,在用 kmap 之类的函数进行映射
如果频繁撤销/创建教导的数据结构 建立slab高速缓存

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注