小结:
内核中分配内存不容易,根本原因在于内核本身不能奢侈地使用内存,一般内核不能睡眠,且其分配机制不能太复杂。与用户空间中的内存分配不太一样。
内存管理单元(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;
void *virtual; // 页的虚拟地址,但高端内存并不永久地映射到内核地址空间,这个值为NULL
}
一个页可以由页缓存使用(此时,mapping域指向和这个页关联的address_space对象),或者作为私有数据(由private指向),或者作为进程页表中的映射。
注意:
struct zone
struct zone{
unsigned long watermark[NR_WMARK]; // 该区的最小值、最低和最高水平值
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
spintlock_t lock; // 防止该结构被并发访问,注意这个锁只保护结构,不保护这个区的所有页
struct free_area free_area[MAX_ORDER];
spinlock_t lru_lock;
struct zone_lru{
struct list_head list;
unsigned long nr_saved_scan;
} lru[NR_LRU_LISTS];
const char *name; //是一个以NULL结束的字符串表示这个区的名字。内核启动时初始化,三个区的名字为DMA、Normal、HighMem
};
我们已经对内核如何管理内存(页、区等)有所了解,现在看看内核实现分配和释放内存的接口。所有接口都以页为单位分配内存。
(1). 分配内存,最核心的函数:struct page* alloc_pages(gfp_t gfp_mask, unsigned int order)
该函数分配2order(1<void *page_address(struct page *page)
返回指向物理页当前所在的逻辑地址
(3). 或者结合(1)和(2): unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
返回第一个页的逻辑地址,因为是连续的,所以其他页也会紧随其后。
(4). 如果你只需要一个页:
struct page * alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t gfp_mask)
(5). 获得填充为0的页:unsigned long get_zeroed_page(unsigned int gfp_mask)
(6). 释放页:
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
释放页时要谨慎,只能释放属于你的页,传递错误会导致系统崩溃。记住内核是完全信赖自己的。
分配页 | 释放页 |
---|---|
alloc_page(gfp_mask) | __free_pages(page, order) |
alloc_pages(mask, order) | free_pages(addr, order) |
__get_free_page(mask) | free_page(addr) |
__get_free_pages(mask, order) | |
get_zeroed_page(mask) |
分配页时用上面方法,分配字节为单位的通常用kmalloc()
kmalloc()可以获得以字节为单位的一块内核内存,所分配的内存区在物理上是连续的。出错时返回NULL。最终可能调用alloc_pages()。
void * kmalloc(size_t size, gfp_t flags)
返回一个指向内存块的指针,其内存块大小至少有size大小。
可以分为三类:行为标识符、区标识符、类型标识符。一般使用类型标识符即可。
行为标识符:表示内核应当如何分配所需的内存,如是否可睡眠
区标识符:表示从哪里分配内存
类型标识符:组合了行为标识符和区标识符,将各种可能的组合归纳为不同类型,简化使用,如GFP_KERNEL,在内核中进程上下文可以使用
(1). 行为修饰符
标志 | 描述 |
---|---|
__GFP_WAIT | 分配器可睡眠 |
__GFP_HIGH | 分配器可以访问紧急事件缓冲池 |
__GFP_IO | 分配器可以启动磁盘I/O |
__GFP_FS | 分配器可以启动文件系统I/O |
__GFP_COLD | 分配器应该使用高速缓存中快要淘汰出去的页 |
__GFP_NOWARN | 分配器将不打印失败警告 |
__GFP_REPEAT | 分配器将在分配失败时重复进行分配,但还是存在失败的可能 |
__GFP_NOFALL | 分配器将无限地重复进行分配,分配不能失败 |
__GFP_NORETRY | 分配器在分配失败时绝对不会重新分配 |
__FGP_NO_GROW | 由slab层内部使用 |
__GFP_COMP | 添加混合页元数据,在hugetlb的代码中使用 |
(2). 区修饰符
标志 | 描述 |
---|---|
__GFP_DMA | 从ZONE_DMA分配 |
__GFP_DMA32 | 只在ZONE_DMA32分配 |
__GFP_HIGHMEM | 从ZONE_HIGHMEM或ZONE_NORMAL分配 |
不能给_get_free_pages()或kalloc()指定ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存肯能还没有映射到内存的虚拟地址空间。只有alloc_pages()才能分配高端内存。
(3). 类型标识符
标识类型 | 描述 |
---|---|
GFP_ATOMIC | 用在中断处理、下半部、持有自旋锁以及其他不能睡眠的地方 |
GFP_NOWAIT | 与GFP_ATOMIC类似,但调用不会退给紧急内存池 |
GFP_NOIO | 分配可以阻塞,但不会启动磁盘I/O |
GFP_NOFS | 在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作,这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中 |
GFP_KERNEL | 常规分配方式,可能会阻塞,内核会尽力而为 |
GFP_USER | 常规分配方式,可能会阻塞,用于为用户空间进程分配内存时 |
GFP_HIGHUSER | 从ZONE_HIGHMEM进行分配,可能会阻塞。用于为用户空间进程分配内存 |
GFP_DMA | 从ZONE_DMA进行分配,用于获取能供DMA使用的内存的设备驱动使用,一般会结合GFP_ATOMIC和GFP_KERNEL |
(4). 何时使用哪种标志
情形 | 相应标志 |
---|---|
进程上下文,可以睡眠 | 使用GFP_KERNEL |
进程上下文,不可以睡眠 | 使用GFP_ATOMIC,在你睡眠之前或之后以GFP_KERNEL执行内存分配 |
中断处理程序 | 使用GFP_ATOMIC |
软中断 | 使用GFP_ATOMIC |
tasklet | 使用GFP_ATOMIC |
需要用于DMA的内存,可以睡眠 | 使用(GFP_DMA|GFP_KERNEL) |
需要用于DMA的内存,不可以睡眠 | 使用(GFP_DMA |
kfree()释放由kmalloc()分配出来的内存块。调用kfree(NULL)是安全的。
vmalloc()类似kmalloc(), 不过vmalloc()分配的内存虚拟地址是连续的,而物理地址则无须连续。大多数情况下,只有硬件设备需要得到物理地址连续的内存,而仅供软件使用的内存块(例如与进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。
许多内核代码通过kmalloc()获得内存,这是出于性能考虑。 vmalloc()为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地映射,会导致比直接内存大得多的TLB抖动,所以vmalloc()只会在为了获得大块内存时才会使用,如模块被动态插入到内核时。
slab分配器扮演通用数据结构缓存层的角色。slab分配器从以下几个方面考虑:
(1). 频繁使用的数据结构也会被频繁分配和释放,应当缓存
(2). 频繁分配和回收会导致内存碎片
(3). 回收的对象可立即投入下一次分配。
(4). 如果分配器知道对象大小、页大小和总的高速缓存大小等,会做出更睿智的决策。
(5). 如果让部分缓存专属单个处理器,那么分配和回收就可以在不加SMP锁。
(6). 对存放的对象进行着色,防止多个对象映射到相同的高速缓存行
slab层把不同的对象划分为高速缓存组,每个高速缓存组都存放不同类型的对象。kmalloc()建立在slab层之上,使用了一组通用高速缓存。
这些高速缓存划分为slab,slab由一个或多个物理连续的页组成,一般是一个页。每个slab都包含一些对象成员。分配新对象时,从部分满的slab中分配,否则从空的slab中分配,若没有则创建。
例:inode结构,他们会被频繁创建和释放,用slab分配器来管理他们十分必要,因而struct inode就由inode_cachep高速缓存进行分配。分配时,内核从部分满的slab或空的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之外另行分配,要么放在slab自身开始的地方。
slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器进行的
( alloc_pages、__get_free_pages()是最底层的分配方式,kmalloc–>slab–>__get_free_pages())
2. 方法
kmem_getpages():空间满时,用于创建新的slab
kmem_freepages():释放slab,最终调用free_pages()
slab分配器的接口
(1). 创建新的高速缓存:kmem_cache_create(name, size, align, flags, void(*ctor)(void*));
失败返回NULL,此函数可能睡眠。
(2). flags参数:
SLAB_HWCACHE_ALIGN - slab内对象按高速缓存行对齐。
SLAB_POISON:使用slab层已知的值(a5a5a5)填充slab,有利于对未初始化内存的访问。
SLAB_RED_ZONE:在已分配内存周围插入红色警戒区以探测缓冲越界。
SLAB_PANIC: 分配失败时提醒slab层。
SLAB_CACHE_DMA:使用DMA的内存给slab分配空间。
(3). 撤销高速缓存:kmem_cache_destroy()
函数可能睡眠。
高速缓存的使用
(1). 分配:void * kmem_cache_alloc(cachep, flags)
(2). 释放:void kmem_cache_free(cachep, objp) 标记为空闲
总之,流程是先用kmem_cache_create创建高速缓存,用kmem_cache_alloc分配对象,用完后kmem_cache_free返回,全都不用了之后用kmem_cache_destroy销毁。
节省栈资源,所有局部变量之和不要超过几百字节,因为栈溢出消无声息。因此,进行动态分配是一种明智的选择。
高端内存页不能永久映射到内核地址空间,因此通过alloc_pages()以__GFP_HIGHMEM标志的页不可能有逻辑地址。
unsigned long my_percpu[NR_CPUS];
cpu = get_cpu();
my_percpu[cpu]++; //获得当前处理器,并禁止内核抢占
my_percpu[cpu]++;
put_cpu(); //激活内核抢占
上述代码不用锁,(1). 操作数据对当前处理器来说是唯一的; (2). get_cpu()时就已经禁止内核抢占。
编译时的每个CPU变量(静态分配)
DEFINE_PER_CPU(type, name);
get_cpu_var()和put_cpu_var()操作变量:返回当前处理器上的指定变量,同时禁止抢占;
per_cpu(name, cpu)++:获得别的处理器上的每个CPU数据,但不会禁止内核抢占,也不会提供锁保护。
静态创建的每个CPU变量不能在模块内使用,动态创建还是有可能的。
运行时每个CPU数据(动态分配)
void * alloc_percpu(type);
void *__alloc_percpu(size, align);
void free_percpu(const void *);
利用两个宏获取每个CPU数据,同时禁止内核抢占:
get_cpu_var(ptr)
put_cpu_var(ptr)