对于32位的机器来说,高于896的物理内存在内核中属于高端内存,并没有对内存做一一的映射,系统保留了128M的线性地址空间来临时映射这些高于896M的高端物理内存,该线性地址为3G+768m~4G。返回页框线性地址的页分配函数对于高端内存是无效的,因为高端内存不会自动的映射到某个线性地址。例如__get_free_pages(GFP_HIGH_MEM,0)函数分配高端内存页框时,返回的是NULL;内核可以采用三种方式来使用高端物理内存:永久内核映射,临时内核映射和非连续内存分配。建立永久内核映射可能会阻塞当前进程的执行,这发生在没有高端内存没有空闲的页表项来做映射的情况下,因此在中断等不能阻塞的代码中不要使用永久内核映射。临时内核映射不会发生阻塞的情况,但必须保证没有其他的内核路径在使用同样的临时内核映射。
一、永久内存映射
永久内核映射使用的是内核主页表中的一个专门的页表,其地址存放在pkmap_page_table中,页表的页表项由宏LAST_PKMAP产生,页表中包含512或者1024项。
该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含了LAST_PKMAP个计数器,pkmap_page_table页表中的每项都有对应一个计数值:
计数器为0:对应的页表项是空闲可用的。
计数器为1:对应的页表项没有映射任何高端内存,但是它不能够使用,因为自从最后一次使用以来,其相应的TLB尚未被刷新。
计数器为n:有多个内核成分使用该页表项所对应的页框。
源码分析:
voidfastcall*kmap_high(structpage*page)
{
unsignedlongvaddr;
spin_lock(&kmap_lock);
vaddr=(unsignedlong)page_address(page);
if(!vaddr)
vaddr=map_new_virtual(page);
pkmap_count[PKMAP_NR(vaddr)]++;
BUG_ON(pkmap_count[PKMAP_NR(vaddr)]<2);
spin_unlock(&kmap_lock);
return(void*)vaddr;
}
staticinlineunsignedlongmap_new_virtual(structpage*page)
{
unsignedlongvaddr;
intcount;
start:
count=LAST_PKMAP;
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;
if(--count)
continue;
{
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);
if(page_address(page))
return(unsignedlong)page_address(page);
gotostart;
}
}
vaddr=PKMAP_ADDR(last_pkmap_nr);
set_pte_at(&init_mm,vaddr,
&(pkmap_page_table[last_pkmap_nr]),mk_pte(page,kmap_prot));
pkmap_count[last_pkmap_nr]=1;
set_page_address(page,(void*)vaddr);
returnvaddr;
}
二、临时内核映射
临时内核映射比较简单,在内核中,为每个cpu都保存了一组页表项,每个页表项由一个特定的内核成分使用,需要注意的是,不同的内核控制路径不应该同时使用一个页表项,这样的话,会使后一个内核控制路径将前一个内核控制路径设置页表项给冲掉。
建立临时内核映射使用kmap_atomic()函数。
void*__kmap_atomic(structpage*page,enumkm_typetype)
{
enumfixed_addressesidx;
unsignedlongvaddr;
inc_preempt_count();
if(!PageHighMem(page))
returnpage_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));
local_flush_tlb_one((unsignedlong)vaddr);
return(void*)vaddr;
}
三、非连续内存分配
下图显示了如何使用高于0xc0000000线性地址的线性地址空间:
内存区的开始部分包含的是对前896MB的RAM进行映射的线性地址,直接映射的物理内存的末尾的线性地址保存在high_memory变量中。
内存区的结尾位置包含的是固定映射的线性地址。
从PKMAP_BASE开始,是用于高端内存永久映射的线性地址。
其余的线性地址用于非连续内存区,在物理内存映射和第一个内存区间有一个8M的安全区,用于捕捉对内存的越界访问,同样道理,插入其它4KB大小的内存区来隔离非连续内存区。
非连续内存区描述符数据结构:
structvm_struct{
void*addr;
unsignedlongsize;
unsignedlongflags;
structpage**pages;
unsignedintnr_pages;
unsignedlongphys_addr;
structvm_struct*next;
};
1、分配非连续的内存区
分配函数主要是vmalloc(),vmap(),vmalloc()会去调用__vmalloc_node()函数:
void*__vmalloc_node(unsignedlongsize,gfp_tgfp_mask,pgprot_tprot,
intnode)
{
structvm_struct*area;
size=PAGE_ALIGN(size);
if(!size||(size>>PAGE_SHIFT)>num_physpages)
returnNULL;
area=get_vm_area_node(size,VM_ALLOC,node);
if(!area)
returnNULL;
return__vmalloc_area_node(area,gfp_mask,prot,node);
}
void*__vmalloc_area_node(structvm_struct*area,gfp_tgfp_mask,
pgprot_tprot,intnode)
{
structpage**pages;
unsignedintnr_pages,array_size,i;
nr_pages=(area->size-PAGE_SIZE)>>PAGE_SHIFT;
array_size=(nr_pages*sizeof(structpage*));
area->nr_pages=nr_pages;
if(array_size>PAGE_SIZE){
pages=__vmalloc_node(array_size,gfp_mask,PAGE_KERNEL,node);
area->flags|=VM_VPAGES;
}else
pages=kmalloc_node(array_size,(gfp_mask&~__GFP_HIGHMEM),node);
area->pages=pages;
if(!area->pages){
remove_vm_area(area->addr);
kfree(area);
returnNULL;
}
memset(area->pages,0,array_size);
for(i=0;i<area->nr_pages;i++){
if(node<0)
area->pages[i]=alloc_page(gfp_mask);
else
area->pages[i]=alloc_pages_node(node,gfp_mask,0);
if(unlikely(!area->pages[i])){
area->nr_pages=i;
gotofail;
}
}
if(map_vm_area(area,prot,&pages))
gotofail;
returnarea->addr;
fail:
vfree(area->addr);
returnNULL;
}
__vmalloc_node()并不触及当前进程的页表,因此当内核态进程访问非连续内存区时,会发生缺页异常,因为对应的进程的相应地址对应的页表项为空。当缺页异常发生时,异常处理程序会到内核主页表(init_mm.pgd页全局目录)中去查看是否有对应的页表项,有的话,就会修改当前进程的页表项,并继续进程的执行。
2、释放非连续的内存区
voidvfree(void*addr)
{
BUG_ON(in_interrupt());
__vunmap(addr,1);
}
void__vunmap(void*addr,intdeallocate_pages)
{
structvm_struct*area;
if(!addr)
return;
if((PAGE_SIZE-1)&(unsignedlong)addr){
printk(KERN_ERR"Tryingtovfree()badaddress(%p)\n",addr);
WARN_ON(1);
return;
}
area=remove_vm_area(addr);
if(unlikely(!area)){
printk(KERN_ERR"Tryingtovfree()nonexistentvmarea(%p)\n",
addr);
WARN_ON(1);
return;
}
debug_check_no_locks_freed(addr,area->size);
if(deallocate_pages){
inti;
for(i=0;i<area->nr_pages;i++){
BUG_ON(!area->pages[i]);
__free_page(area->pages[i]);
}
if(area->flags&VM_VPAGES)
vfree(area->pages);
else
kfree(area->pages);
}
kfree(area);
return;
}
与vmalloc()一样,该函数修改的是主内核页全局目录和它的页表表项,内核永远不会回收页全局,页上级,页中间目录,也不会回收页表,而进程的页表会指向这些表项。这样的话,假设一个内核进程访问已经释放的非连续内存,最终就会访问到已经被清空的页表表项,从而引发缺页异常,这就是一个错误。