页框管理
内核对整个物理内存进行分页,每页大小为4KB或者4MB(大小无所谓,不同os都可能不一样),一般认为Linux的页大小为4KB,内核必须记录好每个页框的信息,所以linux内核把所有的页框都用struct page来描述,page也叫做页描述符,所有的page形成数组,放在mem_map变量中,mem_map的大小占用整个RAM(内存)的1%左右!记住以下两个宏定义
- virt_to_page(addr):把线性地址addr转化为页描述符地址
- pfn_to_page(pfn):把页框号转化为页描述符地址
页描述符page的字段主要包括flags、_count、index等,具体看书。
非一致内存访问NUMA和一致内存访问UMA
自己查资料,我们现在用的电脑一般为UMA。
内存管理区
Linux把整个内存分为三个区,分别为:
- ZONE_DMA:包含了低于16MB的内存页框,一般用于DMA,也可以用于别的用途。
- ZONE_NORMAL:包含了高于16MB且低于896MB的内存页框
- ZONE_HIGHMEM:包含了高于896MB的内存页框
保存的页框池
为了避免在申请页框的时候内存不够而导致的阻塞,系统会预留一些内存,这些内存被叫做保存的页框池,存放在min_free_kbytes变量中。
分区页框分配器
整个分配器如下组成:
管理区分配器可以接收动态内存的分配和释放请求,拿到请求后,可以根据具体的内存管理区去执行内存的分配和释放。每个内存管理区使用了伙伴系统来处理。下面十几个具体的函数
- 请求页框
- alloc_pages(gfp_mask,order):请求2的order次方个连续的页框,并返回第一个页框的页框描述符地址,如果失败返回NULL;
- alloc_page(gfp_mask):等价于alloc_pages(gfp_mask,0)
- __get_free_pages(gfp_mask,order):请求2的order次方个连续的页框,并返回第一个页的线性地址,如果失败返回NULL;
- get_zeroed_page(gfp_mask):请求1个初始化为0的页框,并返回线性地址,如果失败返回NULL;
- __get_dma_pages(gfp_mask,order):在DMA区域请求2的order次方个连续的页框,并返回第一个页的线性地址,如果失败返回NULL;
注意:请求的时候可以指定具体的区域,比如DMA区域,一般设置在gfp_mask中!
- 释放页框
- __free_pages(page,order):page是一个页框描述符,将其以及其后的2的order次方的描述符中count字段减一,减到0的会释放掉该页。
- free_pages(addr,order):addr是一个线形地址page,将其描述符中count字段减一,减到0的会释放掉该页。
- __free_page(page):不解释。
- free_page(addr):不解释。
高端内存页框的内核映射
针对ZONE_DMA和ZONE_NORMAL的页框请求和释放都很简单,这就是简单的线形映射,内核空间的前896MB都是这样线形映射的,所以很简单,但是高端内存(ZONE_HIGHMEM)就不一样了,因为32位的内核空间只有3-4GB,总长就1GB,前面以及占用了896MB了,所以只剩下128MB可以去映射高端内存,所以这一节着重记录一下高端内存的映射。
注意:64位的系统就没这么麻烦了,64位系统的用户空间、内核空间都远远大于物理内存,直接使用就行了!
《深入理解LINUX内核》这本书里面举了个例子,我觉得挺好:
例如,内核调用了__get_dma_pages(GFP_HIGHMEM,0)在高端内存中分配一个页框,此时如果有一个高端页框也不能反回其线性地址,因为根本不存在,所以返回NULL。
此时就要用到alloc_pages和alloc_page这两个函数了,获取到高端页框的描述符地址,然后为了让内核能访问到,必须使用内核空间的后128MB去映射。内核可以使用三种机制来映射高端内存,分别是永久内核映射、临时内核映射以及非连续内存分配。
永久内核映射
永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它的页表存在于128MB之中,用变量pkmap_page_table变量保存。页表的表项数由LAST_PKMAP宏产生,一般就是512或1024项,不多,一共也就2MB或4MB。且该段页表映射的线形地址从PKMAP_BASE开始。Linux为每一个页表项都设置了一个计数器,存放在pkmap_count数组中,计数器的含义:
- 计数器为0,则代表该页表项没有映射任何高端内存,是可用的。
- 计数器为1,则代表该页表项没有映射任何高端内存,但不可用,因为从他最后一次被使用后,相应的TLB表还没有被刷新。
- 计数器大于1,由n-1个内核成分正在使用它。
此外,内核还用了page_address_htable散列表来映射物理页框和线性地址的关系,key为页框page,高端页框、低端页框都适用!函数page_address(page)就是用来查page_address_htable找出page对应的线性地址,如果page是低端页框,直接物理地址-3GB**就得到线性地址了,如果page是高端页框,就用散列表,三列表中如果不存在就返回NULL。
**kmap()**函数用来建立永久内核映射,代码等价如下:
void *kmap(struct page *page){
if(!PageHighMem(page))//如果是低端内存,就直接返回映射address。
return page_address(page);//上面的
return kmap_high(page);
}
其中kmap_high()函数等价于:
void *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[(vaddr-PKMAP_BASE)>>PAGE_SHIFT]++;
spin_unlock(&kmap_lock);
return (void *)vaddr;
}
kmap_high使用了自旋锁,保证多处理器上的并发问题,同时它没有关中断,因为中断处理函数里不能调用kmap()。map_new_virtual函数才是真的去做映射,其代码等价于:
for(;;){
int count;
DECLARE_WAITQUEUE(wait, current);//将当前进程包装成一个等待结构体,wait就是该结构体指针,是个宏定义!相见之前做的笔记
for(count = LAST_PKMAP;count > 0; --count){
last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP-1);//这里的last_pkmap_nr是全局变量,便于从上次调用map_new_virtual产生的last_pkmap_nr接着扫描。
if(!last_pkmap_nr){
flush_all_zero_pkmap();//扫描,回收count=1的页表项,置count=0并删除其page对应的散列项、并刷新TLB。
count = LAST_PKMAP;
}
if(!pkmap_count[last_pkmap_nr]){
unsigned long vaddr = PKMAP_BASE + (last_pkmap_nr<<PAGE_SHIFT);
set_pte(
&(pkmap_page_table[last_pkmap_nr]),
mk_pte(page, __pgprot(0x63))
);//设置页表!!!
pkmap_count[last_pkmap_nr] = 1;
set_page_address(page, (void *)vaddr);//在散列表中建立散列项
return vaddr;
}
}
current->state = TASK_UNITERRUPTIBLE;//无可用页表,就去睡把!
add_wait_queue(&pkmap_map_wait,&wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait,&wait);
spin_lock(&kmap_lock);
if(page_address(page))
return (unsigned long) page_address(page);
//否则继续执行死循环。
}
**kunmap()**函数用来撤销映射:
void kunmap(struct page *page){
spin_lock(&kmap_lock);
if(--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT]==1)
if(waitqueue_active(&pkmap_map_wait))
wake_up(&pkmap_map_wait);
spin_unlock(&kmap_lock);
}
临时内核映射
临时内核映射更加简单,它不阻塞当前进程,可以用于中断处理程序和可延迟函数内部,但他必须保证当前没有其他的内核控制路径在使用同样的映射,所以它不能被阻塞。高端内存任何一页都可以通过一个窗口映射到内核空间,但是这些窗口非常少,每个CPU都有13个窗口,用枚举enum km_type表示如下,最后一个KM_TYPE_NR仅仅表示前面有多少个(13个),这些个窗口都是固定映射的(见第二章的“固定映射的线性地址”),他们的开始和结束都由enum fixed_addresses数据结构中的FIX_KMAP_BEGIN和FIX_KMAP_END指定,如下所示,其中KM_TYPE_NR就是13,NR_CPUS是cpu数目。
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
};
enum fixed_addresses {
FIX_HOLE,
FIX_VDSO,
...
FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
...
__end_of_fixed_addresses
};
变量kmap_pte表示可以FIX_KMAP_BEGIN的页表项地址,使用fix_to_virt(FIX_KMAP_BEGIN)调用获取并赋值给kmap_pte进行初始化。为了建立临时内核映射,内核调用**kmap_atomic()**函数实现,代码等价于:
void *kmap_atomic(struct page *page, enum km_type type){
enum fixed_addresses idx;
unsigned long vaddr;
current_thread_info()->preempt_count++;//禁止调度、禁止强占
if(!PageHighMem(page))//如果是低端内存,就直接返回映射address。
return page_address(page);//上面的
idx = type + KM_TYPE_NR*smp_processor_id();//通过type和cpu号 获取具体的index
vaddr = fix_to_virt(FIX_KMAP_BEGIN + idx);
set_pte(kmap_pte+idx,mk_pte(page, __pgprot(0x63)));//设置页表!!!
__flush_tlb_single(vaddr);//刷新tlb项
return (void *)vaddr;
}
内核使用kunmap_atomic(struct page *page)来撤销临时内核映射,在该函数中会preempt_count–,这是一种禁止调度方式,所以这里直接改preempt_count,就是禁止本cpu调度,这样的话整个kmap_atomic和kunmap_atomic之间就不能被强占,保证了当前没有其他的内核控制路径在使用同样的映射,就可以避免同一个临时内核映射页表项被多个进程使用!!
伙伴系统
Linux为了解决碎片的问题,可以有两种办法:
- 利用分页,把碎片页框映射到连续的线性地址上即可。
- 使用伙伴系统。
Linux是使用伙伴系统来解决碎片问题。Linux把所有空闲页分为11个块链表,每个块链表分别包含大小为1、2、4、8、16、32、64、128、256、512、1024个连续页框。对于1024个页框就是4MB。
使用到的数据结构
Linux的伙伴系统有三种,分别针对三个内存区域,每个伙伴系统都使用了以下数据结构:
- mem_map数组,每个伙伴系统所使用的页框都是mem_map的子集,伙伴系统的第一个页框以及页框个数使用zone_mem_map和size字段指定。
- 一个数组,包含了11个(对应前面的1、2、4…一共11个)类型为free_area的元素,每个free_area里包含了一个free_list,用于链接所有的同样大小的空闲块。
free_area数组的第k个元素表示了所有大小为2的k次方的空闲快,free_area的free_list字段是一个链接空闲块的双向循环链表的头,该链表的元素实际上就是空闲块首个页框的描述符(即page的lru字段,lru在页框空闲时用于此);free_area的nr_free表示当前链表中有多少个元素(即空闲块)。此外空闲块的第一个page的private字段放了k,用于表示它后面块个数为2的k次方。
分配快
分配快使用__rmqueue()函数,需要传入两个参数:管理去描述符zone和order(即需要快的大小为2的order次方)。如果分配成功则返回第一个页框的页描述符,否则返回NULL。该函数默认调用者以及禁止了本地中断和获取了zone->lock自旋锁。该函数大概内容如下:
...
struct free_area *area;
unsigned int current_order;
for(current_order=order;current_order<11;current_order++){
//不断地向上找,找到就跳转到block_found,找不到返回NULL
area = zone->free_area + current_order;
if(!list_empty(&area->free_list))
goto block_found;
}
return NULL;
...
block_found代码如下:
block_found:
page = list_entry(area->free_list.next,struct page, lru);//这是一个宏定义,用到了container_of宏
list_del(&page->lru);
ClearPagePrivate(page);
page->private = 0;
area->nr_free--;
zone->free_pages -= 1UL << order;
此时如果current_order大于order的话就说明分配到了一个更大的块,所以需要讲剩余块插入到别的链表中去:
size = 1 << current_order;
while(current_order>order){
area--;
current_order--;
size >>= 1;
buddy = page + size;
list_add(&buddy->lru, &area->free_list);
area->nr_free++;
buddy->private = current_order;
SetPagePrivate(buddy);
}
释放块
释放块用函数__free_pages_bulk(),它需要三个参数:zone、page、order,具体含义不解释了。该函数默认调用者以及禁止了本地中断和获取了zone->lock自旋锁。该函数大概内容如下:
{
struct page *base = zone->zone_mem_map;
unsigned long buddy_idx, page_idx = page - base;
struct page *buddy, *coalesced;
int order_size = 1 << order;
zone->free_pages += order_size;//增加zone的page计数器
while(order<10){
//不断地去找伙伴
buddy_idx = page_idx ^ (1<<order);//找到伙伴的page编号
buddy = base + buddy_idx