本文首先介绍和内存管理相关的一些概念如page,zone,然后介绍多种获得内存的方式,最后介绍Linux的slab层(slab分配器)。
页是Linux内核进行内存管理的基本单元。MMU和虚拟内存也都使用页作为基本管理单元。不同的架构有不同的页大小,绝大部分的情况是32-bit使用4KB页大小,64-bit使用8KB页大小。这意味着如果一台机器的物理内存为1GB大小,大小为4KB,那么它的物理内存会被分为262144个页。
页使用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;
void *virtual;
}
内核使用page结构体来管理物理内存中的所有page,内核需要知道一个page是否空闲(即page是否被分配出去了),如果page不是空闲的,那么内核需要知道page的所有者是谁。
page结构体只表示物理内存的page,不是虚拟内存的page。一个虚拟page不同时间可能会对应不同的物理page,即使这个虚拟page存储的数据没有变化(swapping等原因造成的)。
物理内存中的每一个page都会有一个对应的struct page实例,即有多少个页,在内存中就有多少个struct page实例,无论该page是否被使用。听起来是一个很大的内存开销,但是实际上并不大,比如一个有4GB物理内存的系统,使用8KB大小的page。那么总共需要524288个struct page实例,这总共需要20M的内存,相对于4GB的物理内存来说,开销其实很小。
内核将这些page划分成具有不同各自特征的区(zone),不同区的页有着不同的特性。
Linux主要有4种内存区
不了解高端内存的话,可以看一下这篇博客linux 用户空间与内核空间——高端内存详解,作者写得很详细
不同区的实际使用和布局是体系架构相关的 下表是在x86-32架构中不同区域的描述和大小。
区域 | 地址范围(物理地址) |
---|---|
ZONE_DMA | <16MB |
ZONE_NORMAL | 16~896MB |
ZONE_HIGHMEM | > 896MB |
x86-64架构中,只有ZONE_DMA 和 ZONE_NORMAL两个区域
内核使用strcut zone 来表示区域,该结构体定义在
内核在\
分配页:
函数接口 | 功能描述 |
---|---|
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) | 分配2^order个连续的物理页,返回指向第一个页的指针 |
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) | 分配2^order个连续的物理页,返回第一个页的逻辑地址 |
struct page * alloc_page(gfp_t gfp_mask) | 分配单个物理页,返回指向该页的指针 |
unsigned long __get_free_page(gfp_t gfp_mask) | 分配单个物理爷,并返回该页的逻辑地址 |
unsigned long get_zeroed_page(unsigned int gfp_mask) | 分配单个物理页,将页内容置为0,并返回该页的逻辑地址 |
内核还提供了一个函数,用于将页指针转换为逻辑地址
void * page_address(struct page *page),传入参数为页指针,返回参数为逻辑地址。
释放页:
函数接口 | 功能描述 |
---|---|
void __free_pages(struct page *page, unsigned int order) | 释放2^order个物理页,传入参数为指向第一个页的指针 |
void free_pages(unsigned long addr, unsigned int order) | 释放2^order个物理页,传入参数为指向第一个页的逻辑地址 |
void free_page(unsigned long addr) | 释放单个页,传入采纳数为指向第一个页的逻辑地址 |
在
void * kmalloc(size_t size, gfp_t flags)
kmalloc返回一段连续的物理内存,大小至少为size,这里说至少是因为内核内存分配是以页为单位的,所以当size的大小不到n个页大小,但是又大于n-1个页大小时,内核会分配n个页的内存。返回地址是第一个页的逻辑地址。
和kmalloc对应的释放内存的函数是void kfree(const void *ptr);
获取物理内存页的底层接口和kmalloc()函数中都包含了一个gfp_t类型的参数,gfp是get free page的缩写,这个参数由3种flag组成,分别为action modifier, zone modifier,type。
actions modifier 规定了内核应该如何分配内存(比如中断处理程序获取内存时要求不能睡眠)
下表是不同action modifier及其功能,就不一一翻译了
Flag | Description |
---|---|
__GFP_WAIT | The allocator can sleep. |
__GFP_HIGH | The allocator can access emergency pools. |
__GFP_IO | The allocator can start disk I/O. |
__GFP_FS | The allocator can start filesystem I/O. |
__GFP_COLD | The allocator should use cache cold pages. |
__GFP_NOWARN | The allocator does not print failure warnings. |
__GFP_REPEAT | The allocator repeats the allocation if it fails, but the allocation can potentially fail. |
__GFP_NOFAIL | The allocator indefinitely repeats the allocation. The allocation cannot fail. |
__GFP_NORETRY | The allocator never retries if the allocation fails. |
__GFP_NOMEMALLOC | The allocator does not fall back on reserves. |
__GFP_HARDWALL | The allocator enforces “hardwall” cpuset boundaries. |
__GFP_RECLAIMABLE | The allocator marks the pages reclaimable. |
__GFP_COMP | The allocator adds compound page metadata (used internally by the hugetlb code). |
不同的modifier可以通过或的方式合并使用,如ptr = kmalloc(size, __GFP_WAIT | __GFP_IO | __GFP_FS);
zone modifyer 规定了从哪个区域(zone)分配内存
Flag | Description |
---|---|
__GFP_DMA | Allocates only from ZONE_DMA |
__GFP_DMA32 | Allocates only from ZONE_DMA32 |
__GFP_HIGHMEM | Allocates from ZONE_HIGHMEM or ZONE_NORMAL |
未指定zone modifier | 默认从ZONE_DMA或者ZONE_NORMLA分配,会优先选择ZONE_NORMAL |
需要注意的是不能给__get_free_page()、 __get_free_pages()或者kmalloc()指定 __GFP_HIGHMEM,因为从高端内存中分配的页可能没有映射到到内核地址空间中,因此没有逻辑地址。不能返回一个有效的逻辑地址。
type flags实际上是前两者的组合,是为了方便某些场合下规定特定的flags而设计的,简化了flags的指定,减少错误的发生。
Flag | Description |
---|---|
GFP_ATOMIC | The allocation is high priority and must not sleep. This is the flag to use in interrupt handlers, in bottom halves, while holding a spinlock, and in other situations where you cannot sleep. |
GFP_NOWAIT | Like GFP_ATOMIC, except that the call will not fallback on emergency memory pools. This increases the liklihood of the memory allocation failing. |
GFP_NOIO | This allocation can block, but must not initiate disk I/O. This is the flag to use in block I/O code when you cannot cause more disk I/O, which might lead to some unpleasant recursion. |
GFP_NOFS | This allocation can block and can initiate disk I/O, if it must, but it will not initiate a filesystem operation. This is the flag to use in filesystem code when you cannot start another filesystem operation. |
GFP_KERNEL | This is a normal allocation and might block. This is the flag to use in process context code when it is safe to sleep. The kernel will do whatever it has to do to obtain the memory requested by the caller. This flag should be your default choice. |
GFP_USER | This is a normal allocation and might block. This flag is used to allocate memory for user-space processes. |
GFP_HIGHUSER | This is an allocation from ZONE_HIGHMEM and might block. This flag is used to allocate memory for user-space processes. |
GFP_DMA | This is an allocation from ZONE_DMA. Device drivers that need DMA-able memory use this flag, usually in combination with one of the preceding flags. |
其中最常使用的是GFP_KERNEL、GFP_ATOMIC、GFP_DMA.
type flags一样可以通过或运算来进行参数组合,如(GPF_DMA| GPF_KERNLE)表明需要DMA_ZONE的内存,进程可以进入睡眠
vmalloc()声明在
void * vmalloc(unsigned long size)
vmalloc()分配指定大小的内存,这些内存在逻辑上是连续的,在物理上不一定来连续。因为这些页在物理上不一定连续,所以vmalloc()需要设置page table表,建立映射关系,使得这些内存在逻辑上是连续的。因为需要为每个页简历一个映射关系,会导致TLB更容易抖动,从而降低性能。因此内核中大部分情况下使用kmalloc(),vmalloc只有不得不用的时候才会使用(比如分配超大块的内存)。
和vmalloc()对应的释放内存的函数为:
void vfree(const void *addr);
Slab层是linux内核提供的通用数据结构cache层,主要作用是为一些频繁分配和释放的数据结构(对象)建立对象缓存,当对象不再需要时,slab回收该对象,将其放在缓存中,而不是直接销毁。当程序申请新的对象时,直接从该对象的缓存中返回一个可用对象。而不是重新分配内存。
Linux的slab层设计与实现遵循下面的原则:(不一一翻译了):
Slab层将对象按照类型分成不同的cache,每个cache中只包含一类对象,一种类型的对象只存在与一个特定的cache中。
cahce被分为多个slab,slab由一个或者多个物理上连续的页组成(通常一个slab只有一个页)。
每个slab包含一些对象(这些对象就是缓存的对象)
简单来说cache由slab组成,slab由对象组成。
每个slab都处于一下三种状态中的一个:
当内核的某一个部分请求一个新的对象时,会优先从partial状态的slab中分配,如果没有partial状态的slab,就会从empty状态的slab中分配,如果也没有empty状态的slab,一个新的empty slab会被创建出来。
cache对象使用struct kmem_cache表示,该结构体中包含3个链表,分别是slabs_full, slabs_partial和slabs_emtpy,分别存储3中状态的slab对象。
slab对象使用struct slab来表示。
struct slab {
struct list_head list; /* full, partial, or empty list */
unsigned long colouroff; /* offset for the slab coloring */
void *s_mem; /* first object in the slab */
unsigned int inuse; /* allocated objects in the slab */
kmem_bufctl_t free; /* first free object, if any */
};
当需要分配一个新的对象,但是cache中没有partial状态slab,也没有empty状态的slab对象时,slab分配器通过函数static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)来为cachep指向的cache创建一个新的empty slab。
kmem_getpages将会调用__get_free_pages()来获得连续的物理内存页。
当内存不足,并且系统试图释放内存时或者当cache被销毁时,slab分配器通过调用kmem_freepages(struct kmem_cache *cachep, void *addr)函数来释放cachep中的由逻辑地址参数addr指定的slab
kmem_freepages()将会调用free_pages()来释放物理内存页。
slab层为调用者提供了一组接口,通过该接口可以
1) 创建某个类型对象的cache
2) 销毁某个类型对象的cache
3) 从cache中获得对象
4) 将对象返还给cache
通过这些接口,slab alloctor隐藏了内部实现(如寻找partial slab,创建新的slab,销毁empty slab),表现为为一个专用的对象分配器。
下表列出了slab分配器提供的主要接口:
函数接口 | 功能描述 |
---|---|
struct kmem_cache * kmem_cache_create(const char name, size_t size, size_t align, unsigned long flags, void (*ctor)(void )) | 创建一个cache对象,name是cache的名字,size是对象的大小,flags定义了对象的行为,比如是否要在ZONE_DMA中创建每个slab等。 |
int kmem_cache_destroy(struct kmem_cache *cachep) | 销毁cachp指向的cache,调用者需要确保该cache中的slab都是空的 |
void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) | 从cache中获得一个可用的对象 |
void kmem_cache_free(struct kmem_cache *cachep, void *objp) | 释放objp指向的对象,这将会使cachep中的objp被标记为空闲 |
《Linux Kernel Development 3rd Edition》
《Understanding The Linux Kernel 3rd Edition》