前面讲过,针对于内核地址空间中后面的128MB空间,Kernel提供了三种机制来映射物理内存。之前讲过了两种,即持久内核映射和临时内核映射。这两种机制的目的都是一样的:使Kernel能够访问到高端内存。
今天讲一下第三种机制:非连续内存分配,也就是vmalloc。这个机制同样可以使Kernel能够访问到高端内存,不过这不是该机制的主要目的。该机制的主要目的是:把物理上不连续的页面映射到连续的内核线性地址空间中。
非连续内存区域管理
既然是映射,肯定会涉及到三个元素:集合L,集合P,映射M。
集合L:线性地址空间
vmalloc映射到的线性地址空间,位于内核地址空间后面128MB中的开始部分,范围是 VMALLOC_START ~ VMALLOC_END。
该空间的开始,与物理内存直接映射的空间的结尾(即3GB+896MB)之间,有一个8MB的安全间隔。
为了管理这段空间,Kernel必须知道其中哪些区域已经使用了,哪些还未使用。为此,Kernel提供了一个数据结构,来描述已经使用了的非连续内存区域。
25 struct vm_struct { 26 /* keep next,addr,size together to speedup lookups */ 27 struct vm_struct *next; 28 void *addr; 29 unsigned long size; 30 unsigned long flags; 31 struct page **pages; 32 unsigned int nr_pages; 33 unsigned long phys_addr; 34 };
- addr:该区域的起始地址;
- size:该区域的大小,加上4096(区域之间的安全间隔)。
- flags:该区域的标志。
- VM_ALLOC: 该区域是由 vmalloc 创建的。
- VM_MAP: 该区域是由 vmap 创建的。
- VM_IOREMAP: 由 ioremap 使用。
- pages:指向一个page指针数组的指针。每一项元素都表示一个映射到该区域的物理页面。
- nr_pages:该区域映射的物理页面的个数。
- phys_addr:由ioremap使用。
所有的 vm_struct 结构体都链接在一个链表上,链表的头为vmlist。该链表由锁vmlist_lock保护。
24 DEFINE_RWLOCK(vmlist_lock); 25 struct vm_struct *vmlist;
Kernel提供了函数 get_vm_area() 来寻找一块空闲区域,并创建一个vm_struct结构体。该函数最终调用函数 __get_vm_area_node() 来完成工作。
169 static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags, 170 unsigned long start, unsigned long end, 171 int node, gfp_t gfp_mask) 172 { 173 struct vm_struct **p, *tmp, *area; 174 unsigned long align = 1; 175 unsigned long addr; ... 188 addr = ALIGN(start, align); 189 size = PAGE_ALIGN(size); 190 if (unlikely(!size)) 191 return NULL; 192 193 area = kmalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); 194 195 if (unlikely(!area)) 196 return NULL; 197 198 /* 199 * We always allocate a guard page. 200 */ 201 size += PAGE_SIZE; 202 203 write_lock(&vmlist_lock); 204 for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) { 205 if ((unsigned long)tmp->addr < addr) { 206 if((unsigned long)tmp->addr + tmp->size >= addr) 207 addr = ALIGN(tmp->size + 208 (unsigned long)tmp->addr, align); 209 continue; 210 } 211 if ((size + addr) < addr) 212 goto out; 213 if (size + addr <= (unsigned long)tmp->addr) 214 goto found; 215 addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align); 216 if (addr > end - size) 217 goto out; 218 } 219 220 found: 221 area->next = *p; 222 *p = area; 223 224 area->flags = flags; 225 area->addr = (void *)addr; 226 area->size = size; 227 area->pages = NULL; 228 area->nr_pages = 0; 229 area->phys_addr = 0; 230 write_unlock(&vmlist_lock); 231 232 return area; 233 234 out: 235 write_unlock(&vmlist_lock); 236 kfree(area); 237 if (printk_ratelimit()) 238 printk(KERN_WARNING "allocation failed: out of vmalloc space - use vmalloc=<siz e> to increase size.\n"); 239 return NULL; 240 }
参数 start 和 end 分别设为 VMALLOC_START 和 VMALLOC_END。
193行调用kmalloc分配一个vm_struct结构体。
201行把size增加一页大小。增加的这一页大小用来作为区域之间的安全间隔。
204 ~ 218 遍历链表 vmlist,寻找一个大小至少为 size+PAGE_SIZE 的空闲区域。如果没找到,则释放前面分配的vm_struct结构体,返回NULL。
220 ~ 232 初始化找到的空闲区域,并把该区域的vm_struct结构体添加到链表vmlist中,并返回该结构体的地址。
集合P:不连续的物理页面
vmalloc要映射的物理页面,当然也是从buddy system中分配出来的。该工作由函数__vmalloc_area_node()来完成。
426 void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, 427 pgprot_t prot, int node) 428 { 429 struct page **pages; 430 unsigned int nr_pages, array_size, i; 431 432 nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT; 433 array_size = (nr_pages * sizeof(struct page *)); 434 435 area->nr_pages = nr_pages; 436 /* Please note that the recursion is strictly bounded. */ 437 if (array_size > PAGE_SIZE) { 438 pages = __vmalloc_node(array_size, gfp_mask | __GFP_ZERO, 439 PAGE_KERNEL, node); 440 area->flags |= VM_VPAGES; 441 } else { 442 pages = kmalloc_node(array_size, 443 (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO, 444 node); 445 } 446 area->pages = pages; 447 if (!area->pages) { 448 remove_vm_area(area->addr); 449 kfree(area); 450 return NULL; 451 } 452 453 for (i = 0; i < area->nr_pages; i++) { 454 if (node < 0) 455 area->pages[i] = alloc_page(gfp_mask); 456 else 457 area->pages[i] = alloc_pages_node(node, gfp_mask, 0); 458 if (unlikely(!area->pages[i])) { 459 /* Successfully allocated i pages, free them in __vunmap() */ 460 area->nr_pages = i; 461 goto fail; 462 } 463 } 464 465 if (map_vm_area(area, prot, &pages)) 466 goto fail; 467 return area->addr; 468 469 fail: 470 vfree(area->addr); 471 return NULL; 472 }
432行确定要映射的物理页面的个数。
433 ~ 451 分配页面指针数组pages。
453 ~ 463 从buddy system中申请物理页面。这里需要注意的一点是,物理页面是按单个页面,逐次分配出来的。这正是vmalloc的一个核心思想所在,正因如此,vmalloc才能够把物理上不连续的页面映射到连续的线性地址空间中。
映射M:kernel page tables
至此,我们有了一块连续的线性地址空间,也有了足够的物理页面,那接下来的工作就是把两者映射起来。该工作是由函数map_vm_area()完成的。
148 int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages) 149 { 150 pgd_t *pgd; 151 unsigned long next; 152 unsigned long addr = (unsigned long) area->addr; 153 unsigned long end = addr + area->size - PAGE_SIZE; 154 int err; 155 156 BUG_ON(addr >= end); 157 pgd = pgd_offset_k(addr); 158 do { 159 next = pgd_addr_end(addr, end); 160 err = vmap_pud_range(pgd, addr, next, prot, pages); 161 if (err) 162 break; 163 } while (pgd++, addr = next, addr != end); 164 flush_cache_vmap((unsigned long) area->addr, end); 165 return err; 166 }
该函数通过修改页表来完成具体的映射操作。这里需要注意的一点是,该函数只是修改了kernel page tables, 而当前进程的页表并没有改变。
好了,把以上三个元素组装起来,就是函数vmalloc()的实现了。该函数最终通过函数__vmalloc_node()来完成工作。
490 static void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot, 491 int node) 492 { 493 struct vm_struct *area; 494 495 size = PAGE_ALIGN(size); 496 if (!size || (size >> PAGE_SHIFT) > num_physpages) 497 return NULL; 498 499 area = get_vm_area_node(size, VM_ALLOC, node, gfp_mask); 500 if (!area) 501 return NULL; 502 503 return __vmalloc_area_node(area, gfp_mask, prot, node); 504 }
释放函数:vfree
函数vfree()用来释放一个vmalloc区域。它最终调用函数__vunmap()来完成工作。
322 static void __vunmap(void *addr, int deallocate_pages) 323 { 324 struct vm_struct *area; 325 326 if (!addr) 327 return; 328 329 if ((PAGE_SIZE-1) & (unsigned long)addr) { 330 printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr); 331 WARN_ON(1); 332 return; 333 } 334 335 area = remove_vm_area(addr); 336 if (unlikely(!area)) { 337 printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n", 338 addr); 339 WARN_ON(1); 340 return; 341 } 342 343 debug_check_no_locks_freed(addr, area->size); 344 345 if (deallocate_pages) { 346 int i; 347 348 for (i = 0; i < area->nr_pages; i++) { 349 BUG_ON(!area->pages[i]); 350 __free_page(area->pages[i]); 351 } 352 353 if (area->flags & VM_VPAGES) 354 vfree(area->pages); 355 else 356 kfree(area->pages); 357 } 358 359 kfree(area); 360 return; 361 }
它的具体实现也是由三步来完成,正好与vmalloc()一一对应。
第一步:找到要释放区域的描述符,并解除映射。该操作是由335行上的函数remove_vm_area()完成的。
284 static struct vm_struct *__remove_vm_area(void *addr) 285 { 286 struct vm_struct **p, *tmp; 287 288 for (p = &vmlist ; (tmp = *p) != NULL ;p = &tmp->next) { 289 if (tmp->addr == addr) 290 goto found; 291 } 292 return NULL; 293 294 found: 295 unmap_vm_area(tmp); 296 *p = tmp->next; 297 298 /* 299 * Remove the guard page. 300 */ 301 tmp->size -= PAGE_SIZE; 302 return tmp; 303 }
解除映射由函数unmap_vm_area()完成。正如vmalloc(),这里只会修改kernel page tables,而当前进程的页表不会改变。
第二步:把该区域所映射的物理页面释放回buddy system,并释放页表指针数组pages。345 ~ 357
第三步:释放vm_struct结构体所占用的内存。359行。
后记
刚才我们强调过,函数vmalloc()只是修改了kernel page tables,当前进程的page tables并没有改变。因此,当一个处于内核态的进程P访问vmalloc区域时,就会产生一个page fault,因为在进程P的页表中,该vmalloc区域所对应的页表项为空。然而,page fault handler会检查master kernel Page Tables里面对应于出错线性地址的页表项。如果该页表项不为空,那它就会把该页表项的内容copy到进程P的页表对应的页表项中(实际上,copy的是Page Middle Directory中的页表项)。这样,page fault handler返回后,进程P就可以继续执行并正常访问该vmalloc区域了。
正如函数vmalloc(),函数vfree()也是只修改了kernel page tables,而并没有修改进程的页表。那它是怎么工作的呢?
假设,一个处于内核态的进程P,访问一个vmalloc区域。按照我们上面讲的,page fault handler会把kernel page tables中对应的页表项copy到进程P的页表对应的页表项中,从而使得进程P可以正常访问该vmalloc区域。也就是说,针对于该vmalloc区域中的地址,进程P的Page Global Directory和master kernel Page Global Directory最终指向相同的的page tables。如果此时,vfree()释放了该区域,清除了这些page tables里面对应的内容。进程P再访问这个vmalloc区域时就会产生一个page fault。而page fault handler发现kernel page tables中也不包含出错地址所对应的页表项,因此就会把这次访问看作为bug。
我们前面讲过,kernel page tables不会被任何用户态或内核态进程直接使用,而是为进程的页表提供了一个参考模型。说的就是这个意思。