参考:http://os.51cto.com/art/201309/411937.htm
http://dongxicheng.org/os/linux-memory-management-basic/
————————————————————————————————————————————————————
内存管理基本单元:
MMU内存管理单元,主要的作用是管理物理内存,当然涉及到虚拟内存和物理内存之间的转化。
内存管理的基本单元是物理页:尽管CPU最小的可寻址单元是字(通常是字节),但是MMU(管理内存并实现虚拟地址到物理地址的转化)通常是以也页单位进行处理的。同时从虚拟内存的角度来看,最小管理单位也是页。不同的体系架构支持的页大小是不一样的。大多数32位的体系架构支持4KB的页,而64位体系结构一般会支持8KB的页。
内核中使用struct page来表示系统中的每个物理页,一是为了知道哪些页空闲哪些被使用,二是需要知道已经分配的页被谁所拥有。
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; //经典的LRU呀
void *virtual; //物理页对应的虚拟页地址
}
page结构只与物理页相关,而并非和虚拟页相关。这个数据结构仅用来描述当前时刻,物理内存也中存放的东西,它是目的是用来描述物理内存本身,而不是其中的数据。
系统为了管理物理内存,每个物理页都必须分配这样一个结构体(假设占40字节)。例如:某64位体系架构,系统物理页大小为8KB,系统有4G的内存,那么一共有50多万个物理页面。如用struct page来描述的话,需要40字节 * 50w = 20M的空间来存放page结构,相对于4G内存而言可以说微不足道了,因此并不算浪费内存。
————————————————————————————————————————————————————
分区:
有于硬件的限制,物理页被分组为三个区:DMA、NORMAL、HIGHMEM。处在不同区的页不能用于一些特定的任务,也就是说各个区有专用的用途。而区的划分是与体系结构相关的,例如X86-32架构的上有DMA、NORMAL、HIGHMEM三个区,而X86-64架构上没有HIGHMEM区,所有的内存都能用于DMA和NORMAL。
区 | 描述 | 物理内存 |
ZONE_DMA | DMA使用的页 | <16MB |
ZONE_NORMAL | 正常可寻址的页 | 16~896MB |
ZONE_HIGHMEM | 动态映射的页 | >896MB |
————————————————————————————————————————————————————
页的获取:
内核提供了内存请求的机制,这些接口都是以页为单位来分配的。
下面的函数定义与<linux/gfp.h>中,是页分配的最核心的函数:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
将页转化为逻辑地址(其中“页地址 = 页号 * 页大小 + 页内偏移”, 只不过这里逻辑页号和物理页号不是相等的,而是它们之间是维持了一个映射的关系)
void *page_address(struct page *page);
如果不需要要得到struct page也可以调用:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
如果你只需要一页,就可以用下面的两个封装好的函数:
struct page *alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t gfp_mask);
页的释放:
当不再需要页时可以使用下面的函数释放它们:
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr);
释放页时只能释放属于自己的页,传错了struct page或地址,用错了order值,这些都可能导致系统崩溃。
下面是一个示例,这里用来获得8个页, 使用之后释放:
unsigned long page = __get_free_pages(GFP_KERNEL, 3);
//使用获取的8个页
free_pages(page, 3);
调用__get_free_pages()之后要注意进程错误检查,内核可能分配失败,因此代码必须进行检查并做相应的处理。
当你需要以页为单位的一族连续物理页时,尤其在你只需要一两页时,这些低级页函数就很有用。而对于常用的以直接为单位的分配来说,内核提供了函数kmalloc()。
字节获取:
kmalloc()函数与用户空间的malloc()一族函数非常类似,只不过它多了一个flags参数。kmalloc()函数是一个简单的接口,用来以字节为单位获取一块内核内存。如果需要的是页,有gfp类型的函数更适合。对于大多数内核分配来说,kmalloc()接口用得更多。
kmalloc()在<linux/slab.h>中声明:
void *kmalloc(size_t size, gfp_t flags);
这个函数返回一个指向内存的指针,分配的内存是连续的物理空间,并且必须至少有size个字节。
字节释放:
kmalloc()的另一端是kfree(),声明在<linux/slab.h>中:
void kfree(const void *ptr);
gfp_mask分配标志:
这些标志大致分为:行为修饰符、区修饰符、类型。行为修饰符表示内核应当如何分配说需要的内存,区修饰符指明在哪个区进行分配,类型表示符是前面两者的组合。GFP_KERNEL为类型标志,内核中进程上下文相关的代码可以使用它。
1.行为修饰符
2.区修饰符
3.类型标志
其中内核最常用的标志之一是GFP_KERNEL,这种分配可能引起睡眠,它使用的是普通优先级,因此这个标志只可用在可以重新安全调度的进程上下文中。
另外一个截然相反的标志是GFP_ATOMIC,这个标志不能引起睡眠,因此调度者在获取内存请求时受到严格限制。
在以上连个标志中间的是GFP_NOIO和GFP_NOFS,这两个标志可能会引起阻塞。GFP_NOIO分配绝不会启动任何磁盘I/O来帮助满足请求;而GFP_ONFS可能会启动磁盘I/O,但是它不会启动文件系统I/O。
vmalloc函数
vmalloc函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而后者分配的内存物理地址是练习的。这是由用户空间的分配函数工作方式决定的:
1.malloc()返回的页在进程的虚拟地址空间内是连续的,当不保证它们在物理地址上是连续的
2.kmalloc保证页在物理地址上是连续的,当然虚拟地址也是连续的
3.vmalloc返回的页确保页在虚拟地址空间中连续,在同过非连续的物理页与虚拟页映射
大多数情况下,只有硬件设备需要得到物理地址连续的内存,很多体系结构上硬件设备存在于MMC之外,它根本不理解什么是虚拟地址。因此硬件设备用到的任何内存区都必须是物理上连续的块;而供软件使用的内存页就可以只要求他们在虚拟地址上连续。当在编程过程中,基本察觉不到这种差异。
尽管某些情况下需要物理上连续的内存页,当时内核中常用kmalloc()来获取内存页,而不是vmalloc()。原因是前者性能较高,vmalloc为了把不连续的物理页转化为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,vmalloc得到的页必须一个一个的进行映射。
void *vmalloc(unsigned long size);
void vfree(const void *addr);
vmalloc得到的是虚拟地址,可能会引起睡眠,因此不能在中断的上下文中使用。
slab层介绍
上面介绍了内存管理的基本单元为页,内存获取和释放也都是以页为单位的(struct page是如何管理的,在心中形成了巨大的以为,难道是传说中的红黑树吗?)。内核提供了get_free_page、kmalloc、vmalloc,它们都是以分配页为基础的(得到的都是逻辑地址,需要映射的逻辑地址、和不需要映射的逻辑地址它们的区别是什么呢?)。
然而内核当中会频繁的分配、释放数据结构,为了便于数据的分配和释放,往往会使用类似线程池的链表结构。链表中包含:可使用的、已分配的数据结构。当需要一个新的数据结构实例,从链表结构中获取一个就可以实现;当需要释放一个数据结构时,这个数据仍然留在链表结构中,并标记为可使用。
这上面的描述看来,空闲链表相当于一个对象的高速缓存池,用于快速分配和释放对象。 内核中提供了slab层来扮演数据结构高速缓存的角色,个人喜欢把它叫做对象高速缓存池。之所以使用slab层是基于下面的理由来考虑的:
1.频繁的分配和释放数据结构,因此应当缓存它们
2.频繁分配和释放必然导致碎片
3.作为数据结构缓存,明显能提高效率
4.如果slab分配器知道对象的大小、页大小和高速缓存的大小,它会做出更优的决定
slab层设计
其核心思想是:分组 + 缓存。slab层不同对象划分为不同的高速缓存组,其中每个高速缓存组都存放不同类型的对象,每一个类型对应一个高速缓存组。这些高速缓存组又划分为不同的slab,slab由物理页组成(不同对象组成)。
其中每个slab都包含一些对象(放在页中,所以也可以说是物理页)。每个slab处于三种状态:
满 部分满 空
当内核需要获取一个对象时,先从部分满中获取;如果没有部分满就从空当中获取;如果全满那么这个时候就创建一个新的slab。(和进程池一样,应该有个上限根据实际情况调整)。以磁盘索引节点为例,由于其频繁的创建和释放,因此使用slab分配器来管理就很有必要(还是缓存的思想),struct inode就可以由inode_cachep高速缓存来分配,这种高速缓存有一个或多个slab,每个slab尽可能多的包含struct inode对象(原因是这样的对象很多)。当内核请求获取一个新的inode结构时,内核就从slab中返回一个未被使用的结构体指针;当内核使用完inode对象后,slab分配器就把该对象标记为空闲。
下图显示了高速缓存、slab以及对象之间的关系:
其中每个高速缓存都使用kmem_cache结构来表示,这个结构包含三个链表:slabs_full、slabs_partial、slabs_empty,它们均放在kmem_list3结构中。
struct kmem_cache {
struct array_cache *array[NR_CPUS];
unsigned int batchcount; //要转移本地高速缓存的大批对象的数量
unsigned int limit; //本地高速缓存中空闲对象的最大数目
unsigned int shared;
unsigned int buffer_size; //高速缓存的大小
u32 reciprocal_buffer_size;
unsigned int flags; //描述高速缓存永久属性的一组标志
unsigned int num; //封装在一个单独slab中的对象个数
unsigned int gfporder; // 一个单独slab中包含的连续页框数目的对数
gfp_t gfpflags;
size_t colour; //slab使用的颜色个数
unsigned_int colour_off; //slab中的基本对齐偏移
struct kmem_cache *slabp_cache;
unsigned int slab_size; //slab的大小
unsigned int dflags; //动态标志
void (*ctor)(void *,struct kmem_cache *,unsigned long); //构造函数
const char *name; //存放高速缓存名字的字符数组
struct list_head next; //高速缓存描述符双向链表使用的指针
...
struct kmem_list3 *nodelists[MAX_NUMNODES];//高速缓存中的slab链表
//下面三个参数待定
unsigned int objsize; //高速缓存中包含的对象的大小
unsigned int free_limit;//整个slab高速缓存中空闲对象的上限
spinlock_t spinlock;//高速缓存自旋锁
}
这些链表包含高速缓存中所有的slab,struct slab用来表述每个slab:
struct slab {
struct list_head list; //满、部分满、空
unsigned long colouroff; //slab着色的偏移量
void *s_mem; //slab中的第一个对象
unsigned int inuse; //slab中已分配的对象数
kmem_bufctl_t free; //slab中第一个空闲对象
}
slab分配器接口:
创建一个高速缓存:
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *));
第一个参数是一个字符串,存放高速缓存的名字;第二个参数代表高速缓存中每个元素的大小;第三个参数是slab内第一个对象的偏移;第四个参数可选,是用来控制高速缓存的行为;最后一个参数是高速缓存的构造参数。kmem_cache_create()函数返回一个创建高速缓存的指针,若不成功则返回NULL。这个函数不能在中断上下文中使用,因为可能会引起睡眠。
销毁一个高速缓存:
int kmem_cache_destory(struct kmem_cache *cachep);
该函数调用的两个条件:
1.高速缓存中的slab都为空
2.高速缓存不再需要
获取一个对象:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
该函数从高速缓存cachep中返回一个对象的指针,如果slab中全是满的,那么就需要使用kmem_getpages()
释放一个对象:
void *kmem_cache_free(struct kmem_cache *cachep, void *objp);
这样就把一个高速缓存中的对象标记为空。
slab分配器实例:
下面以task_struct结构为例,实现slab分配器分配对象。
struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct", sizeof(structtask_struct), ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRUCK, NULL);
应用程序通常调用fork()创建一个进程描述符:
struct task_struct *task;
task = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
当用完一个进程描述符后需要释放:
kmem_cache_free(task_struct_cachep, task);
看完用法后,注意只有当需要频繁创建某一类对象时,才需要用到slab分配器。