<<Linux内核设计与实现>>读书笔记(十二)-内存管理

来源:互联网 发布:mac u盘 没有退出选项 编辑:程序博客网 时间:2024/06/15 21:38

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

所有内核的内容管理必须简洁而且高效.

主要内容:

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


1.内存的管理单元

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


1.1 页

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

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

[wangyubin@localhost ]$ getconf -a | grep -i 'page'PAGESIZE                           4096PAGE_SIZE                          4096_AVPHYS_PAGES                      637406_PHYS_PAGES                        2012863

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

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

/* * 页中包含的成员非常多,还包含了一些联合体 * 其中有些字段我暂时还不清楚含义,以后再补上。。。 */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};

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

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

那么对于一个页大小4KB 的 4GB内存来说,一共有4*1024*1024/4=1048576个page,
一个page算40字节,在管理内存上共消耗内存40MB左右.

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


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区的内存即可.

2.获取内存的方法

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


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_NOFAIL 分配器将无限的重复进行分配.分配不能失败 __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_ATPMIC __GFP_HIGH 这个标志用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方 GFP_NOWAIT 0 与GFP_ATOMIC类似,不同之处在于,调用不会退给紧急内存池.
这就增加了内存分配失败的可能性 GFP_NOIO __GFP_WAIT 这种分配可以阻塞,但不会启动磁盘IO.
这个标志在不能引发更多磁盘IO时能阻塞IO代码,可能会导致递归 GFP_NOFS (__GFP_WAIT|__GFP_IO) 这种分配在必要时可能阻塞,也可能启动磁盘IO,但不会启动文件系统操作.
这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中 GFP_KERNEL (__GFP_WAIT|__GFP_IO|GFP_FS) 这是常规的分配方式,可能会阻塞.这个标志在睡眠安全时用在进程上下文代码中.
为了获得调用者所需的内存,内核会尽力而为.这个标志应当为首选标志 GFP_USER (__GFP_WAIT|__GFP_IO|__GFP_FS) 这是常规的分配方式,可能会阻塞.用于为用户空间进程分配内存时 GFP_HIGHUSER ((__GFP_WAIT|__GFP_IO|__GFP_GS)|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),或者在睡眠之前执行内存分配


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

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

kmalloc的定义在 linux/slab_def.h中

/** * @size  - 申请分配的字节数 * @flags - 上面讨论的各种 gfp_mask */static __always_inline void *kmalloc(size_t size, gfp_t flags)#+end_srcvmalloc的定义在 mm/vmalloc.c 中#+begin_src C/** * @size - 申请分配的字节数 */void *vmalloc(unsigned long size)

kmalloc 和 vmalloc 区别在于:

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


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

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

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


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

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


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

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

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


2.3.1 slab层实现原理

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

整个slab层的实现原理如下:

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


高速缓冲->slab->缓存对象之间的关系如下图:
这里写图片描述


2.2.3 slab层的应用

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

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 环境下使用 */

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

  • 高速缓存的创建
  • 从高速缓存中分配对象
  • 从高速缓缓存中释放对象
  • 高速缓存的销毁
/** * 创建高速缓存 * 参见文件: 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)

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

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

0 0