Linux内核内存管理

2024-08-12  

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


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


主要内容:


内存的管理单元

获取内存的方法

获取高端内存

内核内存的分配方式

总结

 

1. 内存的管理单元

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


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


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


/*

 * 页中包含的成员非常多,还包含了一些联合体

 * 其中有些字段我暂时还不清楚含义,以后再补上。。。

 */

struct page {

    unsigned long flags;    /* 存放页的状态,各种状态参见 */

    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 的 4G内存来说,一个有 4*1024*1024 / 4 = 1048576 个page,


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


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


1.2 区

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


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


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


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


内存区的结构体定义也在 中。


具体参考其中 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 按页获取 - 最原始的方法,用于底层获取内存的方式

以下分配内存的方法参见:

方法

描述

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** 方法。


相应的释放内存的函数如下:也是在 中定义的


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 标志都在 中定义)


行为标志 :控制分配内存时,分配器的一些行为

区标志   :控制内存分配在那个区(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之类)

类型标志 :由上面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),或者在睡眠之前执行内存分配

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

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


kmalloc的定义在


/**

 * @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)


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层的原理如下:


可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构 task_struct 的高速缓存)

除了针对特定对象的高速缓存以外,也有通用对象的高速缓存

每个高速缓存中包含多个 slab,slab用于管理缓存的对象

slab中包含多个缓存的对象,物理上由一页或多个连续的页组成

 

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

mem_cache

2.3.2 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内核设计与实现》读书笔记(六)- 内核数据结构)


#include

#include

#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";

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。