Linux内存管理--高端内存映射与非连续内存分配

系统 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:有多个内核成分使用该页表项所对应的页框。

源码分析:

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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.

二、临时内核映射

临时内核映射比较简单,在内核中,为每个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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

三、非连续内存分配

下图显示了如何使用高于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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.

__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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.

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

责任编辑:奔跑的冰淇淋 来源: ChinaUnix博客
相关推荐

2009-10-19 09:45:06

linux内存存管理

2023-10-18 13:31:00

Linux内存

2018-05-18 09:07:43

Linux内核内存

2013-10-12 11:15:09

Linux运维内存管理

2013-10-11 17:32:18

Linux运维内存管理

2011-12-20 10:43:21

Java

2009-06-03 15:52:34

堆内存栈内存Java内存分配

2021-07-14 10:00:32

Python内存测量

2024-11-07 09:37:46

2018-07-23 09:26:08

iOS内存优化

2009-06-16 11:11:07

Java内存管理Java内存泄漏

2021-10-15 08:51:09

Linux内存 Kmalloc

2023-09-05 09:36:19

2021-04-27 13:56:49

内存.映射地址

2022-03-07 10:54:34

内存Linux

2022-08-08 08:31:00

Linux内存管理

2013-03-28 09:55:37

Java对象

2017-05-18 16:30:29

Linux内存管理

2021-02-28 13:22:54

Java内存代码

2023-02-02 09:38:37

VMSTAT命令内存
点赞
收藏

51CTO技术栈公众号