linux内核虚拟内存之高端物理内存与非连续内存分配

1、高端物理内存

         在第一章已经讲过,在usr/kernel为3:1的情况下,在一台32位的体系结构上最多只有896MB的内存可以直接访问,而超过部分就只能通过映射后进行访问。而64位机器有足够多的虚拟地址空间,就不会存在这个问题。

         结合第一章线性空间布局,内核空间映射情况如下(以本系统为例):

直接内存映射区:PAGE_OFFSET ~ PAGE_OFFSET + highmem(最大0~896M): 0xc0000000- 0xcfa00000。

动态映射区:VMALLOC_START ~ VMALLOC_END : 0xd0000000 -0xff000000

永久映射区:PAGE_OFFSET-2M ~ PAGE_OFFSET:0xbfe00000 -0xc0000000

固定映射区:FIXADDR_START ~ FIXADDR_TOP:0xfff00000 -0xfffe0000

(1)动态映射区

         通过vmalloc/vfree实现,见下一节。

(2)永久映射区

         用于将高端内存长久映射到内存虚拟地址。通过一下函数实现:

void *kmap(struct page *page)

将一个给定页映射到内核地址空间,一般用于高端内存也可以用于低端内存。如果page对应的是低端内存中的一页,函数只会单纯的返回该页的虚拟地址。如果页位于高端内存,则会建立永久映射,再返回地址。该函数可以睡眠只用于进程上下文中。

         该区域只有2M数量有限,当不再需要高端内存时,应该通过下面函数解除映射:

void kunmap(struct page *page)

(3)固定映射区

         主要解决持久映射不能用于中断处理程序而增加的临时内核映射。通过下面函数实现:

void *kmap_atomic(struct page *page)

void __kunmap_atomic(void *kvaddr)

2、非连续内存分配

         伙伴算法解决外部碎片问题,slab算法解决内部碎片问题,但是在使用过程中还是会产生碎片,从而导致申请大块连续内存将可能持续失败。因此,引入了不连续页面管理算法即vmalloc机制。

         vmalloc和kmalloc工作方式类似,只是前者虚拟地址连续而物理地址不一定连续,后者虚拟地址和物理地址都连续。vmalloc为了把物理上不连续的页转化成虚拟地址空间连续的页,必须专门建立页表项进行一个一个映射,直接导致比直接映射大的多的TLB抖动,影响性能。内核一般使用kmalloc,系统/proc/vmallocinfo可以查看当前通过vmalloc申请的映射区域:

0xbf214000-0xbf222000   57344 module_alloc_update_bounds+0xc/0x5cpages=13 vmalloc

0xbf225000-0xbf228000   12288 module_alloc_update_bounds+0xc/0x5cpages=2 vmalloc

0xd000a000-0xd004b000  266240 atomic_pool_init+0x0/0x108phys=4f500000 user

0xd004b000-0xd0057000   49152 cramfs_uncompress_init+0x30/0x64pages=11 vmalloc

从上面可以看出,加载模块时也使用vmalloc机制,但是映射的虚拟地址不在VMALLOC_START ~ VMALLOC_END,而是专门的模块地址:MODULE_VADDR~ PKMAP_BASE(PAGE_OFFSET -16M ~ PAGE_OFFSET-2M)共4M大小,其他申请的虚拟地址都在VMALLOC_START ~ VMALLOC_END。

(1)数据结构描述

         非连续内存区通过vm_struct结构体描述:

struct vm_struct {                                                                                                                               

   struct vm_struct    *next; //指向下一个vm_struct结构体                                                                                                                   

   void            *addr;  //非连续内存区的虚拟地址空间起始地址                                                                                                                     

   unsigned long       size; //申请的内存区大小+page_size(page_size保留安全间隙)                                                                                                                   

   unsigned long       flags; //映射的内存类型VM_ALLOC(vmalloc)or VM_IOREMAP(ioremap)                                                                                                                  

   struct page     **pages; //直线nr_pages数组指针,该数组由指向页描述符指针组成即每个不连续的物理页                                                                                                                     

   unsigned int        nr_pages; //页个数                                                                                                               

   phys_addr_t     phys_addr; //映射硬件设备时I/O共享内存,否则一般都为0                                                                                                                  

   const void      *caller;                                                                                                                     

};

         非连续内存区虚拟空间管理通过vmap_area结构体描述:

struct vmap_area {                                                                                                                               

   unsigned long va_start;  //虚拟空间起始地址                                                                                                                     

   unsigned long va_end;  //虚拟空间结束地址                                                                                                                      

   unsigned long flags;  //映射的内存类型                                                                                                                       

   struct rb_node rb_node;  //红黑树管理虚拟地址,根据地址排序                                                                                  

   struct list_head list;      //链表方式管理虚拟地址,根据地址排序                                                                                    

   struct list_head purge_list;    /*"lazy purge" list */                                                                                       

   struct vm_struct *vm;  //指向非连续内存区描述符                                                                                                                      

   struct rcu_head rcu_head;                                                                                                                    

};

(2)初始化

         在start_kernal的mm_init通过vmalloc_init实现初始化,mm/vmalloc.c中定义如下:

void __init vmalloc_init(void)

{  

   struct vmap_area *va;

   struct vm_struct *tmp;

   int i;

   for_each_possible_cpu(i) {                                                                                                                   

       struct vmap_block_queue *vbq;

       struct vfree_deferred *p;

   

       vbq = &per_cpu(vmap_block_queue, i);

       spin_lock_init(&vbq->lock);

       INIT_LIST_HEAD(&vbq->free);

       p = &per_cpu(vfree_deferred, i);

       init_llist_head(&p->list);

       INIT_WORK(&p->wq, free_work);

    }

   

   /* Import existing vmlist entries. */

   for (tmp = vmlist; tmp; tmp = tmp->next) {

       va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);

       va->flags = VM_VM_AREA;

       va->va_start = (unsigned long)tmp->addr;

       va->va_end = va->va_start + tmp->size;

       va->vm = tmp;

       __insert_vmap_area(va);

    }

   vmap_area_pcpu_hole = VMALLOC_END;

   vmap_initialized = true;

}

         该函数先是遍历每CPU的vmap_block_queue和vfree_deferred变量及初始化。其中vmap_block_queue是非连续内存块队列管理结构,主要是队列以及对应的保护锁;vfree_deferred是vmalloc的内存延迟释放管理,除了队列初始外,还创建了一个free_work()工作队列用于异步释放内存。接着,将已经存在于vmlist链表的各项非连续区通过__insert_vmap_area插入到非连续内存块的管理中。

(3)vmalloc解析

         vmalloc用于申请非连续区域,当有高端内存时先从高端内存申请,没有高端内存再从低端内存中申请。

void *vmalloc(unsigned long size)

{

   return __vmalloc_node_flags(size, NUMA_NO_NODE,

                    GFP_KERNEL |__GFP_HIGHMEM);

}

__vmalloc_node_flags -> __vmalloc_node-> __vmalloc_node_range是最终的实现,其源码如下:

void *__vmalloc_node_range(unsigned longsize, unsigned long align,

           unsigned long start, unsigned long end, gfp_t gfp_mask,

           pgprot_t prot, int node, const void *caller)

{

   struct vm_struct *area;

   void *addr;

   unsigned long real_size = size;

   size = PAGE_ALIGN(size);

   if (!size || (size >> PAGE_SHIFT) > totalram_pages)

       goto fail;

         /*申请虚拟地址空间,并添加到地址排序的红黑树中进行管理*/

   area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST,

                  start, end, node, gfp_mask,caller);

   if (!area)

       goto fail;

         /*申请对应的物理内存:首先计算需要的物理页数nr_pages及存储等量页面的页描述符数组空间大小pages;接着,根据页面数循环申请物理页面空间;最后对申请的页面进行页表更新 */

   addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);

   if (!addr)

       return NULL;

   clear_vm_unlist(area);

   //内存泄露监测

   kmemleak_alloc(addr, real_size, 3, gfp_mask);

   return addr;

fail:

   return NULL;

}

需要注意的一点:所有进程都共享内核空间,因此这里更新的是内核的主页表即init_mm所在的swap_pg_dir页表,进程则是拷贝它。

(4)vfree

         vfree用于释放有vmalloc申请的非连续空间,是vmalloc的倒序操作。其实现如下:

void vfree(const void *addr)

{  

   BUG_ON(in_nmi());

       

   kmemleak_free(addr);

       

   if (!addr)

       return;

   if (unlikely(in_interrupt())) {

       struct vfree_deferred *p = &__get_cpu_var(vfree_deferred);

       llist_add((struct llist_node *)addr, &p->list);

       schedule_work(&p->wq);

    }else

       __vunmap(addr, 1);

}

先撤销内存泄漏监测;如果当前释放操作在中断中,那么将释放的内存空间加入到当前的CPU的vfree_deferred管理列表中,继而通过schedule_work唤醒free_work工作队列,对内存进行异步释放操作;如果不在中断,直接通过__vunmap()进行内存释放:删除红黑树对应的虚拟地址空间管理区,释放物理页面,释放管理对象页面。

猜你喜欢

转载自blog.csdn.net/heliangbin87/article/details/78043355