前面讲过,针对于内核地址空间中后面的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 Global Directory里面对应于出错线性地址的页目录项。如果该页目录项不为空,那就会把其内容copy到进程P的Page Global 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 Upper Directories, Page Middle Directories和Page Tables。如果此时,vfree()释放了该区域,清除了这些page tables里面对应的内容。进程P再访问这个vmalloc区域时就会产生一个page fault。而page fault handler发现kernel page tables中也不包含出错地址所对应的页表项,因此就会把这次访问看作为bug。


我们前面讲过,kernel page tables不会被任何用户态或内核态进程直接使用,而是为进程的页表提供了一个参考模型。说的就是这个意思。