Linux内存管理

对于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);//page->virtual记录了页框对应的线性地址vaddr=(unsignedlong)page_address(page);//若页框未被映射过,分配新的空闲页表项if(!vaddr)vaddr=map_new_virtual(page);//若是刚分配到了空闲页表项的话,在map_new_virtual()中其count//值被设置为了1,在这里再次++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;/*Foundausableentry*///count变为0的话,意味着当前没有空闲的页表项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);/*Re-start*/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;//将page->virtual的值设为vaddr,okset_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;//内存区的大小加上4K,4K是用来检查越界的内存unsignedlongflags;//非连续内存的类型,VM_ALLOC表示使用vmalloc分配的内存,VM_MAP表示使用vmap分配的内存,//VM_IOREMAP表示用ioremap()分配的内存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要对其为4K的整数倍,因为非连续内存区域是将各个物理页进行映射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;//计算vm_struct中pages数组的数组元素个数array_size=(nr_pages*sizeof(structpage*));//记录下物理页面的数目area->nr_pages=nr_pages;//为vm_struct中的pages数组分配内存if(array_size>PAGE_SIZE){pages=__vmalloc_node(array_size,gfp_mask,PAGE_KERNEL,node);area->flags|=VM_VPAGES;}elsepages=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);//为非连续内存进行页面的分配,每次分配一个页面,将其页框指针记录在pages数组中for(i=0;i<area->nr_pages;i++){if(node<0)area->pages[i]=alloc_page(gfp_mask);elsearea->pages[i]=alloc_pages_node(node,gfp_mask,0);if(unlikely(!area->pages[i])){/*Successfullyallocatedipages,freethemin__vunmap()*/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;//释放的地址应该是4k的整数倍if((PAGE_SIZE-1)&(unsignedlong)addr){printk(KERN_ERR"Tryingtovfree()badaddress(%p)\n",addr);WARN_ON(1);return;}//移除对应的vm_area数据描述符,解除对各个物理页面的页面映射项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);elsekfree(area->pages);}kfree(area);return;}

与vmalloc()一样,该函数修改的是主内核页全局目录和它的页表表项,内核永远不会回收页全局,页上级,页中间目录,也不会回收页表,而进程的页表会指向这些表项。这样的话,假设一个内核进程访问已经释放的非连续内存,最终就会访问到已经被清空的页表表项,从而引发缺页异常,这就是一个错误。

望着它们,我睡着了。今天已经过去——我生命中所有天中的一天,

Linux内存管理

相关文章:

你感兴趣的文章:

标签云: