页
内核管理内存的基本单位
每个物理页都由struct page
表示,位于<linux/mm_types.h>
。 假设每个结构体40byte, 物理页8KB, 4GB物理内存。则有
2
18
2^{18}
218页,结构体占用20MB,并不大。
struct page{
flags //页状态
_count//引用计数,-1当前内核没有引用这一页。
_mapcount
struct list_head lru
*virtual//虚拟地址
...
}
区
- 一些硬件只能通过直接内存访问,即只能访问特定内存地址
- 一些体系结构的内存的物理寻址范围比虚拟范围大,即有些内存不能永久映射到内核空间中。
所以Linux使用四种区:ZONE_DMA
:直接内存访问ZONE_DMA32
: 32位设备的直接内存访问ZONE_NORMAL
:正常映射的页ZONE_HIGHMEM:
高端内存,其中的页不能永久的映射到内核地址空间
实际分类与体系结构相关
结构体很大,但是内存中只有三个区,结构体
struct zone{
watermark[NR_WMARK]//持有该区的最小值,最低和最高 内存消耗基准,随着空闲内存多少而变化
lock//自旋锁,保护结构体被并发访问
pageset[NR_CPUS]
struct zone_lru{
struct list_head list
unsigned long nr_saved_scan
}lru[NR_LRU_LISTS]
*name //区名字,DMA,Normal,HighMem
...
}
获得释放低级页
以页为单位分配内存<linux/gfp.h>
,最核心函数为
struct page * alloc_pages(gfp_t gfp_mask , unsigned int order)
分配
2
o
r
d
e
r
2^{order}
2order个连续物理页,返回指向第一页struct page
的指针。
若无需用到struct page
。使用如下返回指向给定物理页的当前逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask , unsigned int order)
分配给用户空间的页为了保证数据安全,全部数据填充为0
unsigned long get_zeroed_page(unsigned int gfp_mask)
释放页
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr,unsigned)
kmalloc()
比用户控件的malloc
多了flags
参数。用于获取字节为单位的一块内核内存。<linux/slab.h>
物理地址连续
void * kmalloc(size_t size, gfp_t flags)
void kfree(const void *ptr)
若kmalloc
申请的内存早就被释放或者释放的内存不是kmalloc
申请的,则kfree
可能导致严重后果
gfp_mask标志
- 行为修饰符:如何分配所需内存
- 区修饰符: 哪个区分配内存
- 类型:组合了以上,方便
GFP_KERNEL
可让调用者睡眠、交换、刷新一些页到硬盘GFP_ATOMIC
失败率较高,调用者无法睡眠。中断处理程序、软中断和tasklet必须用到GFP_NOIO
介于以上两者GFP_NOFS
介于以上两者- …
vmalloc()
虚拟地址连续,物理地址无需连续。硬件设备需要得到连续的物理地址内存,软件使用的内存块就可以使用该类内存申请函数。很多内核代码使用kmalloc
,处于性能考虑,因为vmalloc
需要对页进行映射,因此只有在获得大块内存时才使用
void * vmalloc(unsigned long size)
void vfree(const void *addr)
slab层
若需要频繁申请和是释放数据结构块,常常想到空闲链表(包含可供使用的、已经分配好的数据结构块)。但是由于空闲链表无法全局控制且内核不知道有空闲链表,因此slab
分配器担任通用数据结构缓存层的角色
kmalloc()
接口建立在slab
层上,使用一组通用高速缓存
- 不同的对象划分不同高速缓存组,每个组存放一种对象类型
- 这些高速缓存被划分为
slab
slab
由一个或多个物理上连续的页组成- 每个
slab
处于三种状态之一:满,部分满或空。当内核的某一部分需要一个新对象时,先从部分满的slab
进行分配,若没有部分满则从空的slab
分配,若没有空的slab
则新建。
struct kmem_cache * kmem_cache_create( const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *));
//获取新的页
kmem_getpages()
//撤销高速缓存
int kmem_cache_destroy(struct kmem_cache *cachep)
//从缓存中获取对象
void * kmem_cache_alloc(struct kmem_cache * cachep, gfp_t flags)
//释放对象返回给slab
void kmem_cache_free(struct kmem_cache * cachep, void *objp)
在栈上静态分配
- 每个进程的内核栈小且固定,两个物理页大小,32位8KB 64位16KB
- 随着开机时间的增加,寻找两个连续的未分配的页开始变的困难。并且由于中断处理程序也使用内核栈,使得内核栈的约束条件越来越苛刻。因此在2.6系列早期,可使用单页内核栈,此时中断处理程序不再使用内核栈,另起炉灶使用中断栈。每个进程拥有一个中断处理程序的栈。
- 栈的内存很宝贵,所以需要控制局部变量大小不超过几百字节,由于栈溢出悄无声息,首当其冲受害即是
thread_info
结构
高端内存的映射
高端内存alloc_pages()
__GFP_HIGHMEM
获得的页没有逻辑地址
x86体系,高于896mb的物理内存的范围大都是高端内存,一但这些内存被分配必须映射到内核的逻辑地址空间上,3GB~4GB
void * kmap(struct page * page) //可睡眠,只能用在进程上下文中。低端内存返回虚拟地址,高端内存建立永久映射,再返回地址。
void kunmap(struct page * page)
void * kmap_atomic(struct page *page, enum km_type type)//临时映射, 当前上下文不能睡眠的情况,如中断处理程序
void kunmap_atomic(void *kvaddr, enum km_type type)
每个CPU数据
CPU可有自己独立的数据,存放在一个数组中。数组每一项对应上一个存在的处理器,按当前处理器号确定这个数组的当前元素。2.4内核
unsigned long my_percpu[NR_CPUS]
//访问
int cpu;
cpu=get_cpu();//该函数禁止内核抢占,因此此段代码不需要锁
my_percpu[cpu];
put_cpu();//激活抢占
2.6内核引入了新的CPU接口
DEFINE_PER_CPU(type,name)
get_cpu_var(name)//禁止内核抢占
put_cpu_var(name)//激活抢占
per_cpu(name,cpu)//获得别的处理器上的cpu数据,该函数没有同步保护!记得上锁
申请释放CPU数据
void *alloc_percpu(type);//宏,分配每个处理器一个指定类型对象的实例
void *__alloc_percpu(size_t size, size_t align);
void free_percpu(const void *);//释放所有处理器上的CPU数据
get_cpu_var(ptr)//alloc函数返回一个指针.该函数返回void类型指针指向ptr的拷贝
put_cpu_var(ptr)//完成,激活内核抢占
使用每个CPU数据的原因:
- 减少数据锁定。不再需要任何锁,只有这个处理器能访问
- 大大减少缓存失效。持续不断的缓存失效称为缓存抖动,影响性能。percpu接口缓存-对齐(cache-align)所有数据
使用每个CPU数据会省去许多(或最小化)数据上锁。唯一的要求是禁止内核抢占,不能在访问每个CPU数据过程中睡眠
总结:分配函数的选择
连续的物理页——低级页分配器或kmalloc()
常用方式
高端内存进行分配——alloc_pages()
该函数返回指向struct page
结构的指针,而非逻辑地址的指针。之后kmap()
映射后获得逻辑地址空间
仅需虚拟地址上连续的页——vmalloc()
比kmalloc
有一定性能损失
创建和撤销很多大的数据结构——slab
高速缓存