Kernel那些事儿之内存管理(13) --- 内核映射(下)

前面讲过,针对于内核地址空间中后面的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不会被任何用户态或内核态进程直接使用,而是为进程的页表提供了一个参考模型。说的就是这个意思。

时间: 2024-10-25 20:59:57

Kernel那些事儿之内存管理(13) --- 内核映射(下)的相关文章

Kernel那些事儿之内存管理(11) --- 内核映射(上)

前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射. 我们先从32位系统开始. 在32位系统中,线性地址空间的大小为 2^32,即4GB.Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分: 0~3GB 用户地址空间 3GB~4GB 内核地址空间. 用户地址空间的管理和映射是个大的topic.我们后面再详细讲述. 内核地址空间只有1GB大小,最多只能映射1GB的物理内存.那问题来了:1GB之外物理内存怎么办? Kernel 给出的解决办法是,把1GB

Kernel那些事儿之内存管理(12) --- 内核映射(中)

内核地址空间中后面这128MB的最后一部分,是固定映射 (fixed mappings). 固定映射是什么意思?为什么要有固定映射?Kernel源代码的注释里有一句话,可谓一语中的:The point is to have a constant address at compile time, but to set the physical address only in the boot process. 一个固定映射的线性地址是个常量,例如0xffffc000,且该常量在编译阶段就可以确定.

Kernel那些事儿之内存管理(9) --- Slab(下)

有了Slab的数据结构和操作方法之后,就可以创建一个指定大小的cache,然后在这个cache中申请和释放object.这个做法很适合这种应用场景:频繁地使用一固定大小的内存空间. 如果只是偶尔使用某个大小的内存空间,为此新建一个cache就有点得不偿失.针对于这种应用场景,内核提供了一系列 general caches.今天我们就来看一看这些general caches是怎么工作的. 其思想很简单:这些general caches会在系统初始化时提前创建好,且其大小按照2的幂次方由小到大分布,

Kernel那些事儿之内存管理(1)

有人的地方就有江湖.要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起. 在NUMA结构中,物理内存首先被分成若干nodes.每一个node进一步被分成若干zones.每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符. 不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames.这三者的关系和地位大体可以用下图来描述(该图取自"Professional Linux Kernel Arc

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(7) --- Slab(上)

前面讲的buddy system算法,分配内存的最小单位是一个页面(例如 4K).这对于大的内存申请比较适用.可是实际生活中,Kernel经常需要分配小的内存空间,比如几十个字节,这个时候怎么办呢? 不同的人可能会想到不同的解决办法. 对于财大气粗的富人一族,办法很简单:申请一个页面,只用其中的几十字节,剩下的直接丢掉. 对于锱铢必较的穷人一族,就不敢这么挥霍了:申请一个页面,然后记录好页面内哪些内存区用了,哪些还没用,没用的部分可以用来满足其他的内存分配请求.总之务必做到物尽其用. 很不幸,K

Kernel那些事儿之内存管理(2) --- 百闻不如一见

上次介绍了物理内存管理中三位主要人物中的node 和zone.这两位是当官的,一个是县长,一个是里长,不敢不先介绍啊.接下来出场的就是我们的老百姓了 --- page frame. Page frame是物理内存的基本组成单位,在Kernel中由结构体 struct page 来描述. struct page {     unsigned long flags;          atomic_t _count;          union {         atomic_t _mapcou

Kernel那些事儿之内存管理(4) --- 未雨绸缪

上次讲的buddy system算法虽然效率很高,但是要从buddy system中分配出一个内存页块来,还是要做不少工作的,有时想想都会觉得很累. 在系统运行过程中,Kernel经常会有单个页面的申请和释放操作.为了进一步提高性能,也为了让生活变得轻松一点,Kernel采用了这样一种cache机制: Memory zone为每个CPU定义了page frame cache.Kernel会在适当的时机提前从buddy system中分配好若干单页,放在这些cache中.以后Kernel若要申请单