第12章 内存管理
内核空间分配空间比较复杂,但是从程序开发角度来说,只是和用户空间中内存分配不太一样而已。
页
内核把物理页作为内存管理的基本单元。
内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)也通常以页为单位来处理。
内核用struct page结构表示系统中的每个物理页,位于<linux/mm_types.h>,简化定义如下:
struct page
{
/* 页的状态,脏的,锁定在内存中,每一位一个状态,位于<linux/page-flags.h> */
unsigned long flags;
/* 页的引用计数,被使用了多少次,-1表示没被引用,空闲,使用page_count进行检查 */
atomic_t _count;
atomic_t _map_count;
unsigned long private;
struct address_sapce *mapping;
pgoff_t index;
struct list_head lru;
/* 页的虚拟地址 */
void *virtual;
};
page结构和物理页相关,而并非和虚拟页先关。
每个页都需要一个结构体来进行管理,其实消耗掉的内存是很小的一部分。
区
由于硬件的限制,内核并不能对所有的页一视同仁,有些页位于内存中特定的物理地址上,不能将其用于一些特定的任务。
ZONE_DMA
ZONE_DMA32
ZONW_NORMAL
ZONE_HIGHMEM
区的实际使用和体系结构相关的。
Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。
区的划分没有任何物理意义,是内核为了管理页而采取的一种逻辑上的分组。
获得页
以页为单位进行获取,<linux/gfp.h>。
/* 申请2^order(1<<order)个连续的物理页 */
struct page* alloc_page(gfp_t gfp_mask,unsigned int order);
/* 将页转换成逻辑地址 */
void* page_address(struct page *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);
/* 页被填充为零,信息可能并不是完全随机的,有可能包含敏感数据。用户空间的页在返回之前,所有数据必须填充为0 */
unsigned long get_zerod_page(unsigned int gfp_mask);
释放页
__free_pages();
free_pages();
free_page();
只能释放属于自己的页。
kmalloc()
获得一块以字节为单位的内核内存。
gfp_mask标志
可分为三类。
行为修饰符,如何分配所需内存,如能否睡眠。
区修饰符,从哪分配内存。
类型标志,前面两种的组合。
GFP_KERNEL
GFP_ATOMIC
kfree()
释放kmalloc申请的内存。
vmalloc()
只确保页在逻辑地址空间内是连续的,也是用户空间malloc的工作方式。而kmalloc在物理上是连续的(虚拟地址因此也是连续的)。
通过修改页表实现。为了将不连续的物理页转换为虚拟地址空间上连续的页,必须建立页表项,必须一个个进行映射,TLB抖动较大。
仅在不得已时候用,比如为了获得大块内存的时候使用。当模块动态插入到内核时,就把模块装载到vmalloc分配的内存上。
vfree释放。
slab层
空闲链表包含可以使用的、已经分配好的数据结构块。需要的时候从里面取,不需要的时候放进去。减少申请和释放的次数,对象高速缓存。但是不能全局控制。
slab分配器扮演了通用数据结构缓存层的角色。
slab层的设计
slab层把不同的对象划分为高速缓存组,其中每个高速缓存组存放不同类型的对象。进程描述符,索引节点对象。kmalloc建立在slab之上,使用了一组通用高速缓存。
slab由一个或多个物理上连续的页组成。三个状态,满、部分满、空,分配顺序部分满 优先,空其次。没有空的话,需要创建一个slab。可以减少碎片内存。
inode结构。
高速缓存用kmem_cache结构表示。
创建新的slab是通过__get_free_pages()实现的。
slab分配器的接口
创建
/* 名字,每个元素的大小,第一个对象的偏移(用于对齐) */
struct kmem_cache* kmem_cache_create(const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void*));
撤销
int kmem_cache_destory(struct kmem_cache *cachep);
- 从缓冲中分配
/* 获取对象 */
void kmem_cache_allockmem_cache *cachep,gfp_t flags);
/* 释放对象 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
- 实例
进程描述符,kernel/fork.c。
一个指向高速缓存的全局变量指针,fork_init()中会创建高速缓存,元素大小为sizeof(struct task_struct)。每次fork时,从高速缓存中获取,进程结束在将其释放。
进程描述符是内核核心组成部分,不应该也不能去撤销。
slab层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。如果需要频繁创建很多相同类型的对象,应该使用slab高速缓存,不要自己去实现空闲链表。
在栈上的静态分配
与用户态不同,内核栈小而且固定。
历史上,每个进程都有两页的内核栈。
单页内核栈
让每个进程减少内存消耗。
随着时间进行,内存碎片化,寻找两个连续的页比较困难。
中断处理程序和中断进程共享一个内核栈。为了优化,添加了中断栈。
在栈上光明正大的工作
在任意一个函数中,必须尽量节省占空间。在具体的函数中让所有局部变量所占空间不要超过几百字节。在栈上进行大量静态分配时很危险的。
高端内存的映射
在高端内存中的页不能永久地映射到内核地址空间上。
永久映射
void *kmap(struct page *page);
void kunmap(struct page *page);
高端或者低端都可以用。可以睡眠,只能用在进程上下文中。
永久映射的数量是有限的。
临时映射
必须创建一个映射而且不能睡眠时,使用临时映射(原子映射)。有一组保留的映射,可以存放新创建的临时映射。
void* kmap_atomic(struct page *page,enum km_type tpye);
void kunmap_atomic(void* kvaddr,enum km_type type);
每个CPU的分配
一般来说,每个CPU的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器。
get_cpu();
put_cpu();
新的每个CPU接口
percpu();
编译时的每个CPU数据
静态创建
DEFINE_PER_CPU(type,name);
DECLARE_PER_CPU(type,name);
get_cpu_var(name)++;
put_cpu_var(name);
per_cpu(name,cpu)++;
运行时每个CPU数据
动态,类似于kmalloc()。
alloc_percpu();
__alloc_percpu();
free_percpu();
使用每个CPU数据的原因
减少了数据锁定,每个处理器访问特定位置的数据。
减少缓存失效。
安全要求是禁止内核抢占,比上锁代价下。在中断和进程上下文中都很安全,但是过程中不能睡眠(挂起),因为唤醒后可能在其他cpu上。
分配函数的选择
连续的物理页,kmalloc。
高端内存,alloc_pages()+kmap()。
仅仅需要虚拟地址上连续的页,vmalloc()。
创建和撤销很多大的数据结构,slab高速缓存。