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()进行内存释放:删除红黑树对应的虚拟地址空间管理区,释放物理页面,释放管理对象页面。