vmalloc
Rev.2を表示中。最新版はこちら。
vmalloc関数は、複数ページのサイズのリニアアドレス空間を必要とする時にコールされます。獲得する複数ページは物理的に連続である必要はありません。ある意味この区間はストレートマップでないとも言えるわけです。実装の概略は、カーネルメモリー区間から必要サイズのリニアメモリー空間を割り当て、必要ページを1ページづつ確保した後、そのページをページグローバルテーブル/ページアッパーテーブル/ページミドルテーブル/ページエントリーテーブルとするページテーブルに設定していくだけです。ここで問題となるのが、カーネル動作時のメモリー空間です。カーネルは独自のメモリー空間を有していません。カレントプロセスの0xC0000000以降のメモリー空間で動作します。このリニアメモリー空間/ページテーブルはすべてのプロセスで同じ内容となっているからです。
そうなると、vmalloc関数で設定したリニアメモリー空間/ページテーブルをすべてのプロセスに反映させる必要があります。いちいち全プロセスにvmalloc関数毎に反映させていたら大変です。そこで、vmalloc関数で設定するリニアメモリー空間/ページテーブルは動作しているプロセスのそれに反映させるのでなく、init_mmとするマスタカーネルページに施します。プロセスがその動的メモリーをアクセスすると、そのプロセスにはその設定がされていないため、例外が発生します。その例外処理の中で、init_mmとするマスタカーネルページにその設定がされているなら、その設定を動作しているプロセスに反映させるというものです。マスタカーネルページに設定されていない場合、カーネルのバグということです。
上でリニアメモリー空間/ページテーブルはすべてのプロセスで同じ内容。と言いましたが、実は各プロセスに反映させるのはページテーブルだけです。リニアメモリー空間は論理的に管理する内容であり、カーネルがその空間を必要とする場合、カーネルのリニア空間も管理しているvmlistから取得すればいいからです。(たぶん)
vmalloc関数は所定の引数で__vmalloc_node関数をコールします。まず要求サイズをページアライメントに調整し、そのページ数が物理ページ数より小さいかのチェックをします。そして__get_vm_area_node関数で、カーネル用リニア区間を管理するvmlistから、利用可能なリニア空間をvm_struct *areaとして取得した後、__vmalloc_area_node関数で実際のページを獲得します。
static void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot, int node, void *caller) { struct vm_struct *area; size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > num_physpages) return NULL; area = __get_vm_area_node(size, VM_ALLOC, VMALLOC_START, VMALLOC_END, node, gfp_mask, caller); if (!area) return NULL; return __vmalloc_area_node(area, gfp_mask, prot, node, caller); }__vmalloc_area_node関数では、nr_pages に必要とするページ数、array_sizeにページの物理アドレスをセットするnr_pagesの配列を用意したときのサイズをセットします。array_sizeはstruct page **pagesにページの物理アドレスをセットするのですが、それが1ページで収まるか、収まらないかチェックするためです。if (array_size > PAGE_SIZE)で1ページに収まらなければ、__vmalloc_node関数で、収まればkmalloc_node関数でその配列を確保しているようです。
struct page **pagesを使うのは、非連続での物理ページを割り当てるためです。連続であるならこのバッファーは必要ありません。先頭物理ページとその個数だけでいいわけですから。
そして必要ページでループするfor (i = 0; i < area->nr_pages; i++)内で、alloc_pageないしalloc_pages_node関数で1ページを確保し、その物理アドレスをarea->pages[i]に設定します。このループを抜けると必要として確保した全ページが、area->pagesに設定されたわけです。
あとはこれをページテーブルに反映させるだけです。それを行うのがmap_vm_area関数です。
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, pgprot_t prot, int node, void *caller) { struct page **pages; unsigned int nr_pages, array_size, i; nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT; array_size = (nr_pages * sizeof(struct page *)); area->nr_pages = nr_pages; /* Please note that the recursion is strictly bounded. */ if (array_size > PAGE_SIZE) { pages = __vmalloc_node(array_size, gfp_mask | __GFP_ZERO, PAGE_KERNEL, node, caller); area->flags |= VM_VPAGES; } else { pages = kmalloc_node(array_size, (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO, node); } area->pages = pages; area->caller = caller; if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } for (i = 0; i < area->nr_pages; i++) { struct page *page; if (node < 0) page = alloc_page(gfp_mask); else page = alloc_pages_node(node, gfp_mask, 0); if (unlikely(!page)) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } area->pages[i] = page; } if (map_vm_area(area, prot, &pages)) goto fail; return area->addr; fail: vfree(area->addr); return NULL; }map_vm_area関数の引数のstruct vm_struct *areaは、割り当てるメモリーのリニアアドレス空間です。addrにはその先頭アドレスを、endに終了アドレスをセットして、先頭アドレスを引数にしてpgd_offset_kマクロで、そのアドレスに対するページグロバルテーブル内のインデックスアドレスを求めます。
それを、pgd++としながら、addr != endまで、while ループで、vmap_pud_range関数そのページグローバルテーブルに対する、ページアップテーブルを設定していきます。vmap_pud_range関数でも同じように今度はミドルテーブルを、そしてミドルテーブル処理ではページエントリーテーブルをそして最後に、そのエントリにページの物理アドレスを設定して、この物理ページがvm_struct *areaのリニアアドレスで利用できるわけです。
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages) { pgd_t *pgd; unsigned long next; unsigned long addr = (unsigned long) area->addr; unsigned long end = addr + area->size - PAGE_SIZE; int err; BUG_ON(addr >= end); pgd = pgd_offset_k(addr); do { next = pgd_addr_end(addr, end); err = vmap_pud_range(pgd, addr, next, prot, pages); if (err) break; } while (pgd++, addr = next, addr != end); flush_cache_vmap((unsigned long) area->addr, end); return err; }ここで味噌となるのが、ページグローバルテーブルのエントリーを求めるのにpgd_offsetでなく、pgd_offset_kとなっているところです。pgd_offset_kマクロはpgd_offsetマクロの引数に、init_mmを引数にして展開しているというろころです。その設定はマスタカーネルページに施していることです。
#define pgd_offset_k(address) pgd_offset(&init_mm, (address)) #define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))