文章目录
- 1 伙伴算法
- 页框操作
- alloc_pages()
- 2 slab
- slab机制要解决的问题
- 使用高速缓存
- 3 内存管理函数
- kmalloc
- kzalloc
- vmalloc
- vzalloc
- 区别
- 参考文章
内核使用struct page
结构体描述每个物理页,也叫页框。内核在很多情况下,需要申请连续的页框,而且数量不定,比如4个、5个、9个等。如果频繁地请求和释放不同大小的一组连续的页框,必然导致在已分配的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足请求。为了避免这种情况,Linux内核引入了伙伴算法。
1 伙伴算法
伙伴算法把所有的空闲页框分为11个块链表,每块链表中分布包含特定的连续页框内存空间,在第i条链表中,每个链表元素包含2的i次方个连续页框。
假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,如果仍然没有,则返回错误。页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。
从上面可以知道Buddy算法一直在对页框做拆开合并拆开合并的动作。Buddy算法牛逼就牛逼在运用了世界上任何正整数都可以由2^n的和组成。这也是Buddy算法管理空闲页表的本质。
页框操作
alloc_pages()
static inline struct page *
alloc_pages(unsigned int gfp_mask, unsigned int order);
该函数分配2的order次方个连续的页框,并返回一个指针,该指针指向第一个页page结构体,如果出错,返回NULL。可以使用下面这个函数把给定的页转为它的逻辑地址:
void *page_address(struct page *page);
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。
其他页框操作可以看这篇文章:https://blog.csdn.net/qq_41683305/article/details/123966721
2 slab
在Linux中,伙伴算法是以页为单位管理和分配内存。但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧!那此不是严重浪费内存。那么该如何分配呢?slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以字节为单位。但是slab分配器并没有脱离伙伴算法,而是基于伙伴算法分配的大内存进一步细分成小内存分配。
我们先来看一张图:
kmem_cache是cache_chain上的一个元素,kmem_cache描述了一个高速缓存,每个高速缓存包含了一个slabs的列表,这通常是一段连续的内存块。存在3种slab:
- slabs_full:slab都已经分配完
- slabs_partial:slab部分分配
- slab_empty:空slab或者没有对象被分配。
kmem_cache高速缓存以缓存对象的大小来区分,所包含的三种slab都是链表,里面有一个或多个slab,每个slab由一个或多个连续的物理页组成,在物理页上保存的才是对象。
slab是slab分配器的最小单位,在实现上一个slab由一个或多个连续的物理页组成(通常只有一页)。单个slab可以在slab链表之间移动,例如如果一个半满slab被分配了对象后变满了,就要从slabs_partial中被删除,同时插入到slabs_full中去。
slab机制要解决的问题
- 减少伙伴算法在分配小块连续内存时所产生的内部碎片
- 将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销
- 通过着色技术调整对象以更好的使用硬件高速缓存
使用高速缓存
一个新的高速缓存是通过以下函数创建的:
kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags,void (*ctor)(void *, kmem_cache_t *, unsigned long),void (*dtor)(void *, kmem_cache_t *, unsigned long));
第一个参数是字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素的大小,第三个参数就是高速缓存内第一个对象的偏移,这用来确保在页内进行特定的对齐,通常情况,0就可以满足要求,也就是标准对齐。flags是可选的设置项,用来控制高速缓存的行为。
我们使用cat /proc/slabinfo可以看到系统所有的高速缓存:
创建高速缓存之后,就可以通过下列函数从中获取对象:
void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);
该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给__get_free_pages()。
最后释放一个对象,并把它返回给原先的slab,可以使用下面的函数:
void kmem_cache_free(kmem_cache_t *cachep,void *objp);
这样就能把高速缓存cachep中的对象objp标记为空闲了。
要销毁一个高速缓存,则调用:
int kmem_cache_destroy(kmem_cache_t *cachep);
同样,也不能从中断上下文中调用这个函数,因为它也可能会睡眠。调用该函数之前必须确保以下两个条件:
- 高速缓存中的所有slab都必须为空
- 在调用kmem_cache_destroy()期间,不能再访问这个高速缓存
3 内存管理函数
kmalloc
void *kmalloc(size_t size, int flags);
- size:是指要分配的内存的字节数。
- flags:是分配标志,它提供了多种kmalloc( )的行为
这个函数返回一个指向内存块的指针,其内存块至少要有size大小,所分配的内存区在物理上是连续的,在出错时,它返回NULL,除非没有足够的内存可用,否则内核总能分配成功。在使用kmalloc()时,必须检查返回值是不是NULL。
kzalloc
void *kzalloc(size_t size, int flags);
kzalloc的功能比kmalloc多了一步,会将申请到连续物理内存数据置为0
vmalloc
void *vmalloc(unsigned long size);
该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下使用。
vzalloc
void *valloc(unsigned long size);
vzalloc比vmalloc步骤多了一步,将申请的逻辑地址连续的内存数据置为0。
区别
kmalloc和kzalloc申请的内存在物理上连续,vmalloc和vzalloc申请的内存在物理上不需要连续,它们在逻辑上连续。kmalloc和kzalloc申请的内存可由kfree函数释放
void kfree(const void *ptr);
vmalloc和vzalloc申请的内存可由vfree函数释放
void vfree(void *addr);
参考文章
https://zhuanlan.zhihu.com/p/36140017
https://www.cnblogs.com/cherishui/p/4246133.html