1. 不连续页的分配
计算机系统中,随着使用时间的增长,内存碎片越来越严重,导致很多不连续的内存页,linux中伙伴系统能够一定程度上避免不连续的内存页。若要分配一段连续的内存页,而物理内存中又不存在这么多连续的内存页怎么办,可以把多个不连续的内存页映射到连续的虚拟地址空间中,当进程访问内存,进程看到的总是一块连续的空间。下面看看不连续页的分配。
分配不连续内存页的主要思想就是通过伙伴系统每次分配order=0的页,也就是每次分配一页,这肯定是内存中不连续页,然后将找到的页映射到内核页表的连续的地址空间上。为了管理不连续内存地址空间,实现快速的查找和分配虚拟地址,内核引入了几个数据结构。
不连续的内存页总是映射到虚拟内存空间的VMALLOC区域,每一段映射的块之间通过一个内存分隔防止不正确的访问,不连续内存页的分配通过两个数据结构和内核页表来组织。通过struct vmap_area管理 VMALLOC_START 到 VMALLOC_END 地址空间,为了快速查找是否存在有需要长度的内存地址空间,vmap_area通过红黑树管理。另一个数据结构 struct vm_struct 则管理底层的不连续的内存页和其对应的虚拟地址。
struct vmap_area {
unsigned long va_start; /* 对应的虚拟地址段的起始地址 */
unsigned long va_end; /* 对应虚拟地址段的结束地址 */
unsigned long flags;
struct rb_node rb_node; /* address sorted rbtree */
struct list_head list; /* address sorted list */
struct list_head purge_list; /* "lazy purge" list */
void *private; /* 指向对应的vm_struct结构 */
struct rcu_head rcu_head;
};
struct vm_struct {
struct vm_struct *next; /* vm_struct 本身的单链表 */
void *addr; /* 虚拟地址 */
unsigned long size;
unsigned long flags;
struct page **pages; /* 对应的底层struct page 数组 */
unsigned int nr_pages; /* struct page 的个数 */
phys_addr_t phys_addr;
void *caller;
}
下面看看内存分配的函数vmalloc,由于vmalloc是基于页的内存分配机制,因此先将要分配的内存大小对齐到页大小,然后分配不连续的页。除去中间的细节看看底层的函数
/**
* __vmalloc_node_range - allocate virtually contiguous memory
* @size: allocation size
* @align: desired alignment
* @start: vm area range start
* @end: vm area range end
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
* @node: node to use for allocation or -1
* @caller: caller's return address
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
void *__vmalloc_node_range(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, int node, 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); /* 在vmap_area树中查找size大小的地址空间,如果找到了则生成vmap_area对象并加入到红黑树中,然后分配对应的vm_struct实例,并建立vmap_area和vm_struct之间的联系 */
if (!area)
goto fail;
/* 实际从伙伴系统中分配空间,并放入vm_struct的pages数组, 然后将分配的页的虚拟地址加入到内核页表的对应项,这样就实现了从连续的虚拟地址到离散的物理内存页的映射*/
addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);
if (!addr)
return NULL;
/*
* In this function, newly allocated vm_struct is not added
* to vmlist at __get_vm_area_node(). so, it is added here.
*/
insert_vmalloc_vmlist(area);
/*
* A ref_count = 3 is needed because the vm_struct and vmap_area
* structures allocated in the __get_vm_area_node() function contain
* references to the virtual address of the vmalloc'ed block.
*/
kmemleak_alloc(addr, real_size, 3, gfp_mask);
return addr;
fail:
warn_alloc_failed(gfp_mask, 0,
"vmalloc: allocation failure: %lu bytes\n",
real_size);
return NULL;
}
下面看看上面注释的两个的函数的具体实现过程:
static struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long flags, unsigned long start,
unsigned long end, int node, gfp_t gfp_mask, void *caller)
{
struct vmap_area *va;
struct vm_struct *area;
size = PAGE_ALIGN(size);
if (unlikely(!size))
return NULL;
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);/*从slab中分配struct vm_struct,并初始化为0 */
if (unlikely(!area))
return NULL;
/*
* We always allocate a guard page.
*/
size += PAGE_SIZE; /*这里就上上面说的每段之间加入一个内存页分隔,避免内存访问错误 */
va = alloc_vmap_area(size, align, start, end, node, gfp_mask); /* 在红黑树中找是否有size个空间的虚拟内存地址空间,如果有就分配一个vm_area,并把它加入红黑树,表示这一段虚拟内存已经有映射了。这里基本上就是好多无聊黑红树操作。 */
if (IS_ERR(va)) {
kfree(area);
return NULL;
}
/*
* When this function is called from __vmalloc_node_range,
* we do not add vm_struct to vmlist here to avoid
* accessing uninitialized members of vm_struct such as
* pages and nr_pages fields. They will be set later.
* To distinguish it from others, we use a VM_UNLIST flag.
*/
if (flags & VM_UNLIST)
setup_vmalloc_vm(area, va, flags, caller);/* 建立两个结构之间如上文所讲的联系 */
else
insert_vmalloc_vm(area, va, flags, caller);
return area;
}
若上面的函数返回非空,就表示分配成功,内存分配的过程基本就上这样了,总结来说就是伙伴系统分配单页,然后从VMALLOC中找到虚拟内存地址并在内核页表中映射给分配的页就完事了。释放内存就是反过来的过程了,在vmap_area树中找到对应的虚拟地址的节点,因此就可以通过private成员找到vm_struct,然后就把页返回给伙伴系统并且删除内核页表的对应项就ok了。
关于vmalloc引发的缺页异常之前甚是不懂,看了一些ULK3上面讲内核页表时候说,内核页表在linux启动装载完内核,初始化内核页表之后,所有的进程和内核线程都不会使用内核页表,内核页表只是在init进程fork子进程的时候当做页表的模板被复制到子进程。然后不管是通过系统调用、中断进入内核或者内核线程都使用当前进程的页表,由于vmalloc只更新了内核页表,因此在内核态进程访问被映射的区域的时候在页表中找不到相应项而触发缺页异常,通过缺页异常实现内核页表和进程页表的同步。
2. 持久映射
如果需要将高端页帧长期的映射到内核地址空间,则使用持久映射。持久映射就更简单了,每次映射一个页帧,不需要管理内存区间段,因此没必要红黑树这种复杂的数据结构了,内核管理持久映射用的散列表,也就是双向溢出链表,可以通过struct page的地址和一次特定常量来查找这个在散列表上的位置。持久映射查找未被映射的地址方式很简单,维护一个pkmap_count的数组,数组的哪个地方为0就表示那个索引所对应的地址未映射。
下面看看管理数据结构:
/*
* Hash table bucket
*/
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
这就是上文说的散列表,PA_HASH_ORDER=7,通过lh成员来管理散列冲突,自旋锁保护lh的访问。
/*
* Describes one page->virtual association
*/
struct page_address_map {
struct page *page;
void *virtual;
struct list_head list;
};
这里就是从page到虚拟地址的映射,list成员链上上面散列表的lh,解决散列冲突。
从物理页帧获取虚拟地址的过程:
/**
* page_address - get the mapped virtual address of a page
* @page: &struct page to get the virtual address of
*
* Returns the page's virtual address.
*/
void *page_address(const struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page)) /* 判断页是不是低端内存的页 */
return lowmem_page_address(page);
pas = page_slot(page); /* 通过散列函数获取page对应的slot */
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
/* 在当前slot下遍历链表,找到pam的page地址和参数的page相同的项,则虚拟地址即为对应项的virtual成员,由此可以看出,持久映射内核总是可以看到,不像vmalloc只在内核页表中更新了值,这里更是直接建立了page和虚拟地址之间的联系,只需查找散列表便可找到page对应的虚拟地址 */
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
下面看看实际的映射函数:
/**
* kmap_high - map a highmem page into memory
* @page: &struct page to map
*
* Returns the page's virtual memory address.
*
* We cannot call this from interrupts, as it may block.
*/
void *kmap_high(struct page *page)
{
unsigned long vaddr;
/*
* For highmem pages, we can't trust "virtual" until
* after we have the lock.
*/
lock_kmap();
vaddr = (unsigned long)page_address(page);/* 获取page的虚拟地址,返回空表示为映射*/
if (!vaddr)
vaddr = map_new_virtual(page); /* 未映射的时候就映射page页 */
pkmap_count[PKMAP_NR(vaddr)]++; /* 增加引用计数 */
BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
unlock_kmap();
return (void*) vaddr;
}
看一下如何映射新的页:
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
/* Find an empty entry ,查找未被映射的虚拟地址,当pkmap_count相应的位置为0时表示这一项未被映射*/
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; /* Found a usable entry */
if (--count)
continue;
/*
* Sleep for somebody else to unmap their entries,没找到就进入等待状态,等待其他的进程释放页,并调度其他的进程运行
*/
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
unlock_kmap();
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
lock_kmap();
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
}
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; /* 将pkmap_count中相应项值设为1,表示此位置已经被映射,但是并未被激活,也就是说还没有添加散列表的对应项,内核还在散列表中找不到page的虚拟地址*/
set_page_address(page, (void *)vaddr); /* 将page和虚拟地址组合成page_address_map加入到散列表中,map_new_virtual整个函数在kmap_high中调用完成之后会将pkmap_count的对应项的值加1,表示这个映射被激活,可以使用*/
return vaddr;
}
上面的for循环就是从上一次映射的位置last_pkmap_nr开始找没有被映射的位置,PKMAP_ADDR将位置线性映射到持久映射段
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
然后设置内核页表,就表示映射完成了,但是并没有将page对应的page_address_map加入到散列表中,因此pkmap_count对应项为1,pkmap_count表示引用计数用了一种不常见的用法,当对应项为0时表示为映射,为1时表示已经映射但是不活动的,也就是为在散列表中加入项,大于1时表示引入计数的个数加1,若值为4,则表示对应项的引用次数为3。下面的set_page_address就是将page加入到散列表,当vaddr==0时表示从散列表中删除page对应的映射项,都是无聊的操作,把代码贴出来算了。
/**
* set_page_address - set a page's virtual address
* @page: &struct page to set
* @virtual: virtual address to use
*/
void set_page_address(struct page *page, void *virtual)
{
unsigned long flags;
struct page_address_slot *pas;
struct page_address_map *pam;
BUG_ON(!PageHighMem(page));
pas = page_slot(page);
if (virtual) { /* Add */
BUG_ON(list_empty(&page_address_pool));
spin_lock_irqsave(&pool_lock, flags);
pam = list_entry(page_address_pool.next,
struct page_address_map, list);
list_del(&pam->list);
spin_unlock_irqrestore(&pool_lock, flags);
pam->page = page;
pam->virtual = virtual;
spin_lock_irqsave(&pas->lock, flags);
list_add_tail(&pam->list, &pas->lh);
spin_unlock_irqrestore(&pas->lock, flags);
} else { /* Remove */
spin_lock_irqsave(&pas->lock, flags);
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
list_del(&pam->list);
spin_unlock_irqrestore(&pas->lock, flags);
spin_lock_irqsave(&pool_lock, flags);
list_add_tail(&pam->list, &page_address_pool);
spin_unlock_irqrestore(&pool_lock, flags);
goto done;
}
}
spin_unlock_irqrestore(&pas->lock, flags);
}
done:
return;
}