在内核里分配内存可不像在其他地方分配内存那么容易。造成这种局面的因素很多。从根本上讲,是因为内核本身不能像用户空间那样奢侈地使用内存。内核与用户空间不同,它不支持简单便捷的内存分配方式。处理内存错误对内核来说也决非易事。正是由于这些限制,再加上内存分配机制不能太复杂,所以在内核中获取内存要比在用户空间复杂得多。不过,我们会看到,也不是说内核的内存分配就困难得不得了。只是和用户空间中的内存分配不太一样而已。
页
- 内核把物理页作为内存管理的基本单位。
- 从虚拟内存的角度来看,页就是最小单位。
struct page{
page_flags_t flags;//flag域用来存放页的状态。这些状态包括页是不是脏的
atomic_t __count; //__count域存放页的引用计数—也就是这一页被引用了多少次
atomic_t __mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; //virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址
}
区
- 内核把页划分为不同的区(zone)。
- 内核使用区对具有相似特性的页进行分组
- Linux使用了三种区:
ZONE_DMA:这个区包含的页能用来执行DMA操作。
ZONE_NORMAL:这个区包含的都是能正常映射的页。
ZONE_HIGHMEM:这个区包含“高端内存”,其中的页能不永久地映射到内核地址空间。 - 这些区定义于<linux/mmzone.h>中。
-
每个区都用struct zone表示。该结构描述了page的状况,如该区还有多少个page没有被分配等信息。
获得页
- 内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>中。最核心的函数:
struct page *alloc_pages(unsigned int gfp_mask,unsigned int order)
该函数分配2的order次方个连续的物理页,并返回一个指针,该指针指向第一个页的page结构体,如果出错,就返回NULL。 - 把给定的页转换成它的逻辑地址:
void *page_address(struct page *page);
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。 - 如果你无须用到struct page,你可以调用:
unsigned long __get_free_pages(unsigned int gfp_mask,unsigned int order)
这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。
释放页
- 当你不再需要页时可以用下面的一族函数来释放它们:
void free_pages(unsigned long addr,unsigned int order)
void free_page(unsigned long addr)
释放页时要谨慎,只能释放属于你的页。传递了错误的地址,用了错误的order值,都可能导致系统崩溃。
kmalloc
- kmalloc()函数用来获取指定大小的内存;函数原型如下:
void *kmalloc(size, gfp_mask);
用它可以获得以字节为单位的一块内核内存。
kmalloc()在<linux/slab.h>中声明;
这个函数返回一个指向内存块的指针,其内存块至少要有size大小。
所分配的内存区在物理上是连续的。
在出错时,它返回NULL。 - gfp_mask标志
- Action修饰符
__GFP_WAIT: 分配器可以睡眠
__GFP_HIGH: 分配器可以访问紧急事件缓冲池.
__GFP_IO: 分配器可以启动磁盘I/O.
__GFP_FS: 分配器能够启动文件系统I/O.
__GFP_REPEAT: 失败时重新分配.
__GFP_NOFAIL: 失败时一直重新分配,直到成功.
__GFP_NORETRY: 分配器不重试. - Zone修饰符
__GFP_DMA
__GFP_HIGHMEM -
行为修饰符与区修饰符的组合
GFP_ATOMIC: 不能睡眠时使用.
GFP_NOIO: 可以阻塞,但不启动io.
GFP_NOFS: 用于文件系统中的部分代码.
GFP_KERNEL:常规分配,可能阻塞.首选
GFP_USER: 常规分配,可能阻塞.
GFP_HIGHUSER: 从高端内存分配,可能阻塞.
GFP_DMA: 从DMA区分配.
- Action修饰符
kfree
- kmalloc() 另一端就是kfree()
- kfree()声明于<linux/slab.h>中:
void kfree(const void *ptr);
kfree()函数释放由kmalloc()分配出来的内存块。 - 举个栗子
char *buf;
buf = kmalloc(BUF_SZ, GFP_KERNEL);
if (buf == NULL)
/* deal with error */
/* Do something with buf */
kfree(buf);
vmalloc
- 另外一个分配内存的函数是vmalloc()函数,如下:
void *vmalloc(unsigned long size);
vmalloc()分配的内存虚拟地址是连续的,而物理地址则无需连续。
kmalloc()函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。
vmalloc()函数只确保页在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。
很多内核代码都用kmalloc()来获得内存,而不是vmalloc()。 -
void *vmalloc(unsigned long size);
该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NULL。
函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。
要释放通过vmalloc()所获得的内存,使用下面的函数:
void vfree(void *addr)
这个函数会释放从addr开始的内存块,其中addr是以前由vmalloc()分配的内存块的地址。这个函数也可以睡眠,因此,不能从中断上下文中调用。
没有返回值。
kmalloc与vmalloc比较
- kmalloc分配的内存虚拟地址连续,物理地址也连续;vmalloc分配的虚拟地址是连续的,但是物理地址不一定连续。如果想给DMA分内存,只能用kmalloc(size, GFP_DMA)
- kmalloc分配的内存不能超过128KB,vmalloc分配的内存是没有限制
- kmalloc是可以使用在中断上下文中(原子过程),但要使用GFP_ATOMIC分配标志kmalloc(size, GFP_ATOMIC),vmalloc是不可以使用在中断上下文中的。
slab层
- 为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表。该空闲链表包含有可供使用的、已经分配好的数据结构块。
当需要一个新的数据结构时,就从空闲链表中抓取一个
当不需要这个数据结构时,就把它放回空闲链表。 - 从这个意义上说,空闲链表相当于对象高速缓存以便快速存储频繁使用的对象类型。
- 空闲链表作为高速缓冲的缺点是不能全局控制。
当可用内存变得紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放出一些内存来。 - 为了弥补这一缺陷,Linux内核提供了slab层(也就是所谓的slab分配器)。
slab层的设计
- 每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。
- 每个slab处于三种状态之一:
满: 没有空闲的对象(slab的所有对象都已被分配)。
部分满: 一些对象已分配出去,有些对象还空闲着。
空: 没有分配出任何对象(slab的所有对象都是空闲的)。 - 当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。
- 如果没有部分满的slab,就从空的slab中进行分配。
- 如果没有空的slab,就要创建一个slab了。
- 这种策略能减少碎片。
slab层设计细节
- 高速缓冲
- 每个高速缓存都是用kmem_cache结构来表示。
- 这个结构包含三个链表:
slabs_full
slabs_partial
slabs_empty
均存放在kmem_list3结构内,这些链表包含高速缓存中的所有slab。
-
slab描述符:struct slab用来描述每个slab
slab分配器的接口
- 一个新的高速缓存是通过以下函数创建:
struct kmem_cache *kmem_cache_create (const char *name,size_t size, size_t align,unsigned long flags, void (*ctor)(void *));
第一个参数是高速缓存的名字
第二个参数是高速缓存中每个元素的大小。
第三个参数是高速缓存内第一个对象的偏移。通常为0,
flags参数是可选的设置项,用来控制高速缓存的行为。通常为0
参数ctor是高速缓存的构造函数。只有在新的页追加到高速缓存时,构造函数才被调用。实际上,Linux内核的高速缓存不使用构造函数。 -
kmem_cache_create()
在成功时会返回一个指向所创建高速缓存的指针,否则,返回NULL。这个函数不能在中断上下文中调用,因为它可能会睡眠。 - 要销毁一个高速缓存,则调用:
void kmem_cache_destroy(struct kmem_cache *cachep);
调用该函数之前必须确保存在以下两个条件:
高速缓存中的所有slab都必须为空
在调用kmem_cache_destroy()期间不再访问这个高速缓存 - 该函数成功执行,返回0;否则,返回非0值。
- 创建高速缓存之后,就可以通过下列函数从中获取对象:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
该函数从给定的高速缓存cachep中返回一个指向对象的指针。
如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmern_getpages()获取新的slab.任何在新的slab中获取对象。 - 最后释放一个对象,并把它返回给原先的slab,可以使用下面这个函数:
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
- 这样就能把cachep中的对象objp标记为空闲了。
使用哪种方法在内核中分配内存
- 频繁分配和释放.
Slab
分配器. - 需要以页为单位分配内存.
alloc_pages()
- 从高端内存分配.
alloc_pages().
- 默认
kmalloc()
- 不需要连续的页.
vmalloc()