Chinaunix首页 | 论坛 | 博客
  • 博客访问: 185843
  • 博文数量: 31
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1124
  • 用 户 组: 普通用户
  • 注册时间: 2013-09-27 10:55
文章存档

2013年(31)

我的朋友

分类: LINUX

2013-10-10 10:38:12


    对于32位的机器来说,高于896的物理内存在内核中属于高端内存,并没有对内存做一一的映射,系统保留了128M的线性地址空间来临时映射这些高于896M的高端物理内存,该线性地址为3G+768m~4G.返回页框线性地址的页分配函数对于高端内存是无效的,因为高端内存不会自动的映射到某个线性地址。例如__get_free_pages(GFP_HIGH_MEM,0)函数分配高端内存页框时,返回的是NULL;内核可以采用三种方式来使用高端物理内存:永久内核映射,临时内核映射和非连续内存分配。建立永久内核映射可能会阻塞当前进程的执行,这发生在没有高端内存没有空闲的页表项来做映射的情况下,因此在中断等不能阻塞的代码中不要使用永久内核映射。临时内核映射不会发生阻塞的情况,但必须保证没有其他的内核路径在使用同样的临时内核映射。


1永久内存映射
永久内核映射使用的是内核主页表中的一个专门的页表,其地址存放在pkmap_page_table中,页表的页表项由宏LAST_PKMAP产生,页表中包含512或者1024项。
该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含了LAST_PKMAP个计数器,pkmap_page_table页表中的每项都有对应一个计数值:
计数器为0:
对应的页表项是空闲可用的。
计数器为1:
对应的页表项没有映射任何高端内存,但是它不能够使用,因为自从最后一次使用以来,其相应的TLB尚未被刷新。
计数器为n:
有多个内核成分使用该页表项所对应的页框。
源码分析:
void fastcall *kmap_high(struct page *page)
{
unsigned long vaddr;


spin_lock(&kmap_lock);
//page->virtual记录了页框对应的线性地址
vaddr = (unsigned long)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;
}
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;


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; /* Found a usable entry */
//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 (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;
//将page->virtual的值设为vaddr,ok
set_page_address(page, (void *)vaddr);


return vaddr;
}
2临时内核映射
    临时内核映射比较简单,在内核中,为每个cpu都保存了一组页表项,每个页表项由一个特定的内核成分使用,需要注意的是,不同的内核控制路径不应该同时使用一个页表项,这样的话,会使后一个内核控制路径将前一个内核控制路径设置页表项给冲掉。
建立临时内核映射使用kmap_atomic()函数。
void *__kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;


//禁止内核抢占,以预防不同内核控制路径使用同一页表项
inc_preempt_count();
//非高端内存,不用进行高端内存映射
if (!PageHighMem(page))
return page_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((unsigned long)vaddr);


return (void*) vaddr;
}
3非连续内存分配
    下图显示了如何使用高于0xc0000000线性地址的线性地址空间:
     1内存区的开始部分包含的是对前896MB的RAM进行映射的线性地址,直接映射的物理内存的末尾的线性地址保存在high_memory变量中。
2内存区的结尾位置包含的是固定映射的线性地址。
3从PKMAP_BASE开始,是用于高端内存永久映射的线性地址。
4其余的线性地址用于非连续内存区,在物理内存映射和第一个内存区间有一个8M的安全区,用于捕捉对内存的越界访问,同样道理,插入其它4KB大小的内存区来隔离非连续内存区。
 
非连续内存区描述符数据结构:
struct vm_struct {
void *addr;//内存区第一个内存单元的线性地址
unsigned long size;//内存区的大小加上4K,4K是用来检查越界的内存
unsigned long  flags;//非连续内存的类型,VM_ALLOC表示使用vmalloc分配的内存,VM_MAP表示使用vmap分配的内存,
  //VM_IOREMAP表示用ioremap()分配的内存
struct page **pages;//非连续内存的的物理页数组
unsigned int nr_pages;//非连续内存的物理页的个数
unsigned long phys_addr;
struct vm_struct *next;//用来将各个非连续内存描述符串联起来
};
1分配非连续的内存区
分配函数主要是vmalloc(),vmap(),vmalloc()会去调用__vmalloc_node()函数:
void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot,
int node)
{
struct vm_struct *area;
//size要对其为4K的整数倍,因为非连续内存区域是将各个物理页进行映射
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
//找到一块空闲的线性地址区域,用来映射该非连续内存
area = get_vm_area_node(size, VM_ALLOC, node);
if (!area)
return NULL;


return __vmalloc_area_node(area, gfp_mask, prot, node);
}


void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node)
{
struct page **pages;
unsigned int nr_pages, array_size, i;
//计算要映射的物理页数
nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
//计算vm_struct中pages数组的数组元素个数
array_size = (nr_pages * sizeof(struct page *));
//记录下物理页面的数目
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;
} else
pages = kmalloc_node(array_size, (gfp_mask & ~__GFP_HIGHMEM), node);
area->pages = pages;
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
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);
else
area->pages[i] = alloc_pages_node(node, gfp_mask, 0);
if (unlikely(!area->pages[i])) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
//将各个物理页框映射到分配好的空闲线性区里面去
if (map_vm_area(area, prot, &pages))
goto fail;
return area->addr;


fail:
vfree(area->addr);
return NULL;
}
__vmalloc_node()并不触及当前进程的页表,因此当内核态进程访问非连续内存区时,会发生缺页异常,因为对应的进程的相应地址对应的页表项为空。当缺页异常发生时,异常处理程序会到内核主页表(init_mm.pgd页全局目录)中去查看是否有对应的页表项,有的话,就会修改当前进程的页表项,并继续进程的执行。


2释放非连续的内存区
void vfree(void *addr)
{
BUG_ON(in_interrupt());
__vunmap(addr, 1);
}
void __vunmap(void *addr, int deallocate_pages)
{
struct vm_struct *area;


if (!addr)
return;
//释放的地址应该是4k的整数倍
if ((PAGE_SIZE-1) & (unsigned long)addr) {
printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
WARN_ON(1);
return;
}
//移除对应的vm_area数据描述符,解除对各个物理页面的页面映射项
area = remove_vm_area(addr);
if (unlikely(!area)) {
printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
addr);
WARN_ON(1);
return;
}


debug_check_no_locks_freed(addr, area->size);
//需要向伙伴系统归还非连续的物理页
if (deallocate_pages) {
int i;
//将各个物理页面归还给伙伴系统
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()一样,该函数修改的是主内核页全局目录和它的页表表项,内核永远不会回收页全局,页上级,页中间目录,也不会回收页表,而进程的页表会指向这些表项。这样的话,假设一个内核进程访问已经释放的非连续内存,最终就会访问到已经被清空的页表表项,从而引发缺页异常,这就是一个错误。

阅读(2724) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~