Linux内核剖析 之 内存管理
1. 内存管理区
为什么分成不同的内存管理区?
ISA总线的DMA处理器有严格的限制:仅仅能对物理内存前16M寻址。
内核线性地址空间仅仅有1G,CPU不能直接訪问全部的物理内存。
ZONE_DMA 小于16M内存页框
ZONE_NORMAL 16M~896M内存页框
ZONE_HIGHMEM 大于896M内存页框
ZONE_DMA和ZONE_NORMAL区域包括的页框,通过线性的映射到内核线性地址空间。内核能够直接訪问(对应的内核页表早已经建立)。
ZONE_HIGHMEN 动态映射到内核线性地址空间。
为什么既有线性映射又有动态映射?
线性映射。保持了内核页表的稳定性,降低了TLB miss。动态映射使得内核能够訪问到全部的物理内存。
内核将页框的信息保存在类型为struct page的页描写叙述符中。全部的页描写叙述符存放在数组mem_map(下标即为页框号)中。
struct page {
unsigned long flags; //标志,同一时候高位用来指向页框所在的管理区,包括多达32个页框的状态标志
//比如:PG_highmem 页框至于ZONE_HIGHMEM管理区
// PG_slab 页框包括在slab中
atomic_t _count; //页框引用计数
atomic_t _mapcount; //页框中的页表项数目
unsigned long private; //在不同情况下,意义不同.当页框为空暇时,由伙伴系统使用
struct address_space *mapping;
unsigned long index; //在不同情况下,意义不同
struct list_head lru; //双向链表指针
};
每一个管理区都有自己的描写叙述符:
struct zone {
unsigned long free_pages; //管理区中空暇页数目
unsigned long pages_min //保留页框的数目 (保留页框池)
unsigned long pages_low;
unsigned long pages_high; //两个watermark,与回收页框,管理区分配器有关
struct per_cpu_pageset pageset[]; //(每CPU页框快速缓存)
spinklock_t lock; //保护该描写叙述符
unsigned long lowmem_reserve[] //在内存不足的临界情况下,管理区必须保留的页框数
struct free_area free_area[]; //管理区空暇页框块(伙伴系统)
struct page *zone_mem_map; //指向管理区第一个页框
unsigned long zone_start_pfn; //第一个页框的下标
char *name; //管理区名字(DMA, NORMAL, HighMem)
..........
}
保留的页框池
有内存分配请求时,假设有足够多的空暇内存可用,请求就被立马满足,否则,必须回收一些内存,堵塞当前执行的内核代码(内核控制路径),直到有内存被释放。
可是。中断或临界区内代码不能被堵塞。原子内存分配请求。
为了降低原子内存分配请求失败,内核保留了一个页框池。
保留池大小 = [sqrt(16*直接映射内存)] (KB)
ZONE_DMA、ZONE_NORMAL按各自大小比例贡献。
struct zone中的unsigned long pages_min //保留页框的数目 (保留页框池)。
2.分区页框分配器
被称为分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。
管理区分配器:接收动态内存分配和释放的请求。(搜索能满足请求的内存管理区,触发页框回收。
。。)
每CPU页框快速缓存:快速满足本地CPU发出的单一页框请求。
伙伴系统:解决外碎片问题。
2.1 请求和释放页框
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
gfp_mask:一组标志,指明怎样寻找空暇的页框
order:请求分配2order个页框
若成功。返回第一个分配页框的页描写叙述符地址
区修饰符:
__GFP_DMA
__GFP_HIGHMEM
行为修饰符:
__GFP_WAIT 同意内核堵塞等待空暇页框的当前内核控制路径
__GFP_HIGH 同意内核訪问保留页框池
__GFP_COLD 所请求页框可能为”冷的“(每CPU快速缓存)
__GFP_REPEAT 内核重试内存分配直到成功为止
__GFP_NOFALL 与 __GFP_REPEAT 同样
__GFP_NORETRY 一次内存分配后失败后不再重试
void __free_pages(struct page *page, unsigned int order)
释放页框函数
2.2 伙伴系统
频繁的请求和释放不同大小的连续页框,必定导致在已分配页框的块内分散了很多小块的空暇页框。
由此产生的问题是:即使有足够的空暇页框能够满足请求。但当要分配一个大块的连续页框时,无法满足请求。
这就是著名的内存管理问题:外碎片问题。
linux採用伙伴系统算法来解决外碎片的问题。
把全部的空暇页框分组为11个块链表。
每一个块链表包括大小为1,2,4,8,16,32,64,128,256,512,1024个连续的页框。每一个块的第一个页框的物理地址必须是该块大小的整数倍。
比如,大小为16个页框的块,其起始地址为16x212的倍数。
算法原理:
通过一个简单的样例说明该算法的工作原理——
假设请求一个256个页框的块,先在256个页框的链表内检查是否有一个空暇的块。
假设没有这种块,算法会查找下一个更大的块,在512个页框的链表中找一个空暇块。假设存在这种块,内核就把512的页框分成两半,一半用作满足请求,还有一半插入256个页框的链表中。假设512个页框的块链表也没有空暇块,就继续找更大的块,1024个页框的块。
假设这种块存在。内核把1024个页框的256个页框用作请求。然后从剩余的768个中拿出512个插入512个页框的链表中。把最后256个插入256个页框的链表中。
页框块的释放过程:
内核试图把大小为b的一对空暇伙伴块合并为一个大小为2b的单独块。
满足下面条件的两个块成为伙伴:
两个块具有同样的大小:b。它们的物理地址连续。
第一个页框的物理地址为2xbx212的倍数。
该算法是迭代的,假设成功合并所释放的块。它会试图合并2b的块。
数据结构:
分配块:
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
struct free_area * area;
unsigned int current_order;
struct page *page;
unsigned int index;
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = zone->free_area + current_order;
if (list_empty(&area->free_list))
continue; //从order開始,搜索第一个满足要求的页框块
page = list_entry(area->free_list.next, struct page, lru);
list_del(&page->lru); //将找到的页框块的第一个页框描写叙述符从链表删除
index = page - zone->zone_mem_map;
if (current_order != MAX_ORDER-1)
MARK_USED(index, current_order, area);
zone->free_pages -= 1UL << order; //管理区的内存页框块降低
return expand(zone, page, index, order, current_order, area);
//假设current_order>order,把剩余未分配的页框块,插入合适的链表
}
return NULL;
}
static inline struct page * expand(struct zone *zone, struct page *page, unsigned long index, int low, int high, struct free_area *area)
{//曾经面提到的样例为例
unsigned long size = 1 << high;
while (high > low) {
area--;
high--;
size >>= 1;
BUG_ON(bad_range(zone, &page[size]));
list_add(&page[size].lru, &area->free_list);
MARK_USED(index + size, high, area)。
}
return page;
}
释放块:
page—须要释放页框块的首个页框的页描写叙述符
base—管理区首个页框的页描写叙述符
order—页框块页框个数的对数
area—页数为2order的块的链表
static inline void __free_pages_bulk (struct page *page, struct page *base,
struct zone *zone, struct free_area *area, unsigned int order)
{
unsigned long page_idx, index, mask;
if (order)
destroy_compound_page(page, order);
mask = (~0UL) << order;
page_idx = page - base; //块中第一个页框的索引號,相对于管理区的首个页框
if (page_idx & ~mask) //页框块的起始物理地址必须是2orderx212 所以page_idx须要是2order的倍数
BUG();
index = page_idx >> (1 + order);
zone->free_pages += 1 << order;
while (order < MAX_ORDER-1) {
struct page *buddy1, *buddy2;
BUG_ON(area >= zone->free_area + MAX_ORDER);
if (!__test_and_change_bit(index, area->map)) //检查伙伴块是否已经被分配
/* //??
?
?
???
* the buddy page is still allocated.
*/
break;
/* Move the buddy up one level. */
buddy1 = base + (page_idx ^ (1 << order)); //伙伴块的索引號
buddy2 = base + page_idx;
BUG_ON(bad_range(zone, buddy1));
BUG_ON(bad_range(zone, buddy2));
list_del(&buddy1->lru);
mask <<= 1;
order++;
area++;
index >>= 1;
page_idx &= mask; //合并后块中第一个页框的索引
}
list_add(&(base + page_idx)->lru, &area->free_list);
}
2.3 每CPU页框快速缓存
内核常常请求和释放单个页框。
为了提高性能。每一个内存管理区定义了每CPU页框快速缓存。当中包括一些预先分配的页框它们被用于满足本地CPU发出的单一页框请求。
每一个内存管理区为每一个CPU提供了两个快速缓存:热快速缓存和冷快速缓存。
热快速缓存:它存在的页框中所包括的内容可能就在CPU硬件快速缓存中。
假设进程在分配页框后。就向页框中写,那么从热快速缓存中获得页框对性能有利。
假设页框将被DMA操作。则从冷快速缓存中获得页框。对性能有利(节约了热快速缓存)
数据结构:
struct zone {
struct per_cpu_pageset pageset[];
}
struct per_cpu_pageset {
int count; //快速缓存中页框的个数
int low; //下界,快速缓存须要补充
int high; //上界,快速缓存须要释放到伙伴系统中
int batch; //每次加入或释放的个数
struct list_head list //快速缓存中包括的页框描写叙述符链表
}
struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags);
释放页框到每CPU页框快速缓存:
void fastcall free_hot_cold_page(struct page *page, int cold);
2.4 管理区分配器
管理区分配器是内存页框分配器的前端。必须找到一个包括足够多空暇页框的内存区,满足内存请求。
注意:
管理区分配器保护保留的页框池;
当内存不足,且同意堵塞当前内核控制路径时,触发页框回收算法;
保存小而珍贵的ZONE_DMA区域。
管理区分配器的核心:__alloc_pages()
3.高端内存页框的内核映射
到眼下为止所介绍的函数。都是获得一块连续的内存页框,要想訪问物理内存,必需要将页框映射到线性地址空间中。
过程:(1)分配物理页框。(2)得到未映射到不论什么物理页框的线性地址;(3)更新页表,使得线性地址映射到物理页框。
对于ZONE_DMA和ZONE_NORMAL区域,线性映射到内核地址空间前896M。
依据物理地址能够直接得到对应的线性地址。而且此部分的内核页表在系统初始化时已经建立。
__va((unsigned long)(page-mem_pag) << 12)
而且这样的映射是临时的,通过反复使用线性地址,使得整个高端内存可以在不同的时间被訪问。
出于不同的目的。内核使用三种不同的机制将页框映射到高端内存。
*永久内核映射
*暂时内核映射(固定映射)
*非连续内存分配
后128M的线性地址,被分为例如以下三个部分:
3.1 永久内核映射
永久内核映射同意内核建立高端页框到内核线性地址空间的长期映射。其可能堵塞当前进程,不能在中断处理程序等不同意堵塞的代码中调用。
永久内核映射使用内核页表中的一个专门的页表。页表的地址存放在pkmap_page_table变量中。页表的表项数目:(LAST_PKMAP 1024)
该页表映射的线性地址空间PKMAP_BASE 0xff800000UL (3G+1016M) ——>0x3fe页全局文件夹的第873项。
也就是,内核通过永久内核映射。最多同一时候訪问4M高端内存。
页表中每一项都有一个计数器:
static int pkmap_count[LAST_PKMAP];
计数器为0
相应的页表项没有映射到不论什么高端内存页框,而且是可用的。
计数器为1
相应的页表项没有映射到不论什么高端内存页框,但不能使用,由于自从它最后一次使用以来,其相应的TLB表项还未被刷新。
计数器为n
相应的页表项映射到一个高端内存页框。
void *kmap(struct page *page)
{
might_sleep();
if (page < highmem_start_page)
return page_address(page); //不在高端内存中,直接返回映射的线性地址
return kmap_high(page);
}
void fastcall *kmap_high(struct page *page)
{
unsigned long vaddr;
spin_lock(&kmap_lock);
vaddr = (unsigned long)page_address(page);
if (!vaddr)
vaddr = map_new_virtual(page);
pkmap_count[PKMAP_NR(vaddr)]++;
if (pkmap_count[PKMAP_NR(vaddr)] < 2)
BUG();
spin_unlock(&kmap_lock);
return (void*) vaddr;
}
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
/* Find an empty entry */
for (;;) {
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
if (!last_pkmap_nr) {
flush_all_zero_pkmaps();
count = LAST_PKMAP;
}
if (!pkmap_count[last_pkmap_nr])
break; /* Found a usable entry */
if (--count)
continue;
/*
* Sleep for somebody else to unmap their entries
*/
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
spin_lock(&kmap_lock);
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
}
vaddr = PKMAP_ADDR(last_pkmap_nr);
set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
//建立内核页表
pkmap_count[last_pkmap_nr] = 1;
set_page_address(page, (void *)vaddr);
return vaddr;
}
3.2 暂时内核映射
FIXADDR_START (3G + 1020M)
FIXADDR_TOP (0xfffff000UL) (除去最后一页)
enum km_type {
KM_BOUNCE_READ,
KM_SKB_SUNRPC_DATA,
KM_SKB_DATA_SOFTIRQ,
KM_USER0,
KM_USER1,
KM_BIO_SRC_IRQ,
KM_BIO_DST_IRQ,
KM_PTE0,
KM_PTE1,
KM_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_TYPE_NR
};
同一个窗体永不会被两个不同的控制路径同一时候使用
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
inc_preempt_count(); //禁止内核抢占
if (page < highmem_start_page)
return page_address(page);
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); //获得线性地址
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot)); //建立页表
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
3.3 非连续内存区管理
主要特点:通过连续的线性地址訪问非连续的页框。
长处:非连续的页框,避免了外碎片;
缺点:打乱了内核页表。
Linux在为活动的交换区分配数据结构, 为模块分配空间某些I/O驱动程序等等方面都用到了非连续内存区
从high_memory(896M)到PKMAP_BASE(1016M)之间的线性地址空间用于非连续内存区。
high_memory与VMALLOC_START之间有8M(VMALLOC_OFFSET)的安全区。为了捕获对内存的越界訪问。
vmalloc区之间的4KB的安全区也是出于相同的考虑。
数据结构:
每一个非连续内存区都相应一个类型为 vm_struct 的描写叙述符
struct vm_struct {
void *addr; //内存区起始线性地址
unsigned long size; //内存区的大小加4096(内存区之间安全区的大小)
unsigned long flags;
struct page **pages; //页描写叙述符指针数组
unsigend int nr_pages; //指针数组长度(内存区页的个数)
unsigned long phys_addr; //该字段为0
struct vm_struct *next; //指向下一个vm_struct
}
分配非连续内存区
void *vmalloc(unsigend long size):
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
struct vm_struct *area;
struct page **pages;
unsigned int nr_pages, array_size, i;
size = PAGE_ALIGN(size); //(((addr)+PAGE_SIZE-1)&PAGE_MASK)
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = get_vm_area(size, VM_ALLOC);
//在VMALLOC_START和 VMALLOC_END之间寻找大小为(szie+4096)的空暇连续地址空间
if (!area)
return NULL;
nr_pages = size >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
area->pages = pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
memset(area->pages, 0, array_size);
for (i = 0; i < area->nr_pages; i++) {
area->pages[i] = alloc_page(gfp_mask);
//vmalloc基于伙伴系统。 每次从伙伴系统分配一页
if (unlikely(!area->pages[i])) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
if (map_vm_area(area, prot, &pages)) //建立页表
goto fail;
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
前面讲的几种动态映射都会改动内核页表。
永久内核映射与非连续内存区改动内核页表后,进程页表中内核线性地址空间部分须要与其保持一致。
那么内核怎样确保对内核页表的改动,能传递到各个进程页表中?
===>>>
当内核建立新页表。
内核态的进程訪问内核线性地址。相应的页文件夹项为空。缺页发生。
缺页处理程序检查这个缺页的线性地址是否在主内核页表中。一旦缺页处理程序发现主内核页表对应的页文件夹项不为空。
就把页文件夹项的值复制到进程的对应位置,并恢复进程的运行。
当内核撤销线性地址到物理页框的映射,把页表项设为0;
内核态进程訪问线性地址,对应页表项为0。缺页发生,缺页处理程序觉得这种訪问是一个错误。