ハイメモリ
X86-32の場合でページ4Kの時、搭載できる物理メモリは4GBです。当然リニアアドレスも4GBと有することができます。それぜは、何故ハイメモ領域が必要か?ということですが、1リニアアドレス空間をユーザプロセスとカーネルが使っていることにあります。ユーザプロセスは0から0xC0000000までの3GB、カーネルはそれ以降の1GBです。カーネルは同時(?)に使えるリニア空間は1GMということです。しかもカーネルはストレートマップしています。そうなると物理アドレスの0から1GBまでしか、マッピングすることはできないからです。
カーネルもユーザプロセスと独立したリニアアドレス空間を有しているなら、ハイメモリというものは必要ありません。カーネルは4GB空間をストレートマップすることが可能だからです。
しかし、ユーザプロセスは頻度にカーネルモードと行き来します。カーネルが独自のページテーブルを有していると、そのつど、TBLの無効化が発生します。これはパフォーマンスによくありません。
なお、64ビットのプラットフォームではハイメモリというものはありません。搭載する物理メモリより十分なリニアアドレス空間を確保できるからです。
ハイメモリのページを使用する方法ですが、1GBを有するカーネル空間で、それは896MBとして、残りの128MB空間内にハイメモ領域のページを割り当てるようにエントリーを予約しています。このアロケート手法として永続的カーネルマッピング/一時的カーネルマッピングおよび非連続カーネルマッピングと方法があるようです。前者の2つはページ単位の割り当ては、後者は複数ページの割り当てる物のようです。
ここでは前者の永続的カーネルマッピングについてです。まず永続的/一時的という表現ですが、これは狭義的な意味合いで、概念的には遅延が許されるプロセスでの利用/そうでないプロセスでの利用と解釈した方が、理解しやすいのではと思います。用はワークキューとタスクレットの関係みたいなものでしょうか?
マップするpageを引数としてkmap関数がコールされます。そしてそのページがハイメモリ領域のページであるなら、kmap_high関数でハイメモとしてマップ処理が行われます。pageがハイメモ領域かどうかのチェックは、PageHighMemマクロでpage->flagsをチェックすることで行っています。zoneのZONE_HIGHMEMから取得したものは、page->flagsにPG_highmemがセットされており、そのビットをチェックすることで判定できます。
ZONE_HIGHMEMでないなら、そのページはストレートマップ内のページであり、リニアアドレスを有している事になります。従ってpage_address関数でリニアアドレスを取得してそれを返します。そうでないならハイメモリーで、リニアアドレスを有していません。kmap_high関数でその割り当てを行います。
ページがマップされていないなら、map_new_virtual関数により、ハイメモ領域のページをマップすることになります。
(言い換えると永続的マップとして一度に使えるサイズは4MBと言う事。たぶん)
count = LAST_PKMAPは、このマップするために予約しているエントリー数です。従ってここでの処理は、空いているエントリーを探し出して、そのページテーブルエントリーに、ページの物理アドレスを設定する処理になるわけです。
このエントリーを管理しているのが、pkmap_count[LAST_PKMAP]配列です。ここから空いているエントリを探します。last_pkmap_nrは、前回割り当てたpkmap_count[]内のインデックスです。まずlast_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASKとし、その次から検索します。LAST_PKMAP_MASKでマスクしているのは、LAST_PKMAP個超えたらlast_pkmap_nrを0とするためです(たぶん)。
if (!last_pkmap_nr) は、last_pkmap_nr 以降空いているエントリが無かった。ということです。この場合、flush_all_zero_pkmaps関数で、pkmap_count[]=1のマップとして有効となっているが使用していないエントリをpkmap_count[]=0として、TBLを無効化し再度検索します。
pkmap_count[]はそのエントリを参照しているカウンターです。0は未使用ということですが、1というのはTBL として参照し、プロセスからは参照されていない。ということです。プロセスがそのページを解放する時、いきなり0とするのでなく、1とします。そうすることで再度同じページをマップする場合、TBLを無効化する必要がないということです。
再度検索で、(!pkmap_count[last_pkmap_nr])はそのエントリは使用可能ということです。その場合ループを抜けます。
if (--count)で、countが0になったということは、すべてのエントリーを使用できません。ここからが永続的カーネルマップの味噌となります。この場合、このハイメモページを使おうとするタスクをウエイトさせています。タスク状態をTASK_UNINTERRUPTIBLEにし、add_wait_queue関数で、pkmap_map_waitをヘッドとするリストに、 DECLARE_WAITQUEUE(wait, current)で初期化したwaitキューを繋いでいます。そしてschedule関数で実行権を放棄し、ハイメモページを所有しているカーネルタスクが、ハイメモページを解放する時に、このwaitキューをウエイクアップし、再度実行して、ループすることで、pkmap_count[]が0のエントリーを取得できると言うことです。
ループを抜けると、設定したエントリのリニアアドレスをvaddr = PKMAP_ADDR(last_pkmap_nr)で取得し、set_pte_at関数で、マスタPTEのinit_mmに更新して、 pkmap_count[last_pkmap_nr] = 1でTBLが参照している旨の設定をし、set_page_address関数で、page->viatualにそのリニアアドレスを設定します。kmap_high関数に戻ると、pkmap_count[PKMAP_NR(vaddr)]++でこれを呼んだプロセスが参照している旨の設定を施します。
カーネルもユーザプロセスと独立したリニアアドレス空間を有しているなら、ハイメモリというものは必要ありません。カーネルは4GB空間をストレートマップすることが可能だからです。
しかし、ユーザプロセスは頻度にカーネルモードと行き来します。カーネルが独自のページテーブルを有していると、そのつど、TBLの無効化が発生します。これはパフォーマンスによくありません。
なお、64ビットのプラットフォームではハイメモリというものはありません。搭載する物理メモリより十分なリニアアドレス空間を確保できるからです。
ハイメモリのページを使用する方法ですが、1GBを有するカーネル空間で、それは896MBとして、残りの128MB空間内にハイメモ領域のページを割り当てるようにエントリーを予約しています。このアロケート手法として永続的カーネルマッピング/一時的カーネルマッピングおよび非連続カーネルマッピングと方法があるようです。前者の2つはページ単位の割り当ては、後者は複数ページの割り当てる物のようです。
ここでは前者の永続的カーネルマッピングについてです。まず永続的/一時的という表現ですが、これは狭義的な意味合いで、概念的には遅延が許されるプロセスでの利用/そうでないプロセスでの利用と解釈した方が、理解しやすいのではと思います。用はワークキューとタスクレットの関係みたいなものでしょうか?
マップするpageを引数としてkmap関数がコールされます。そしてそのページがハイメモリ領域のページであるなら、kmap_high関数でハイメモとしてマップ処理が行われます。pageがハイメモ領域かどうかのチェックは、PageHighMemマクロでpage->flagsをチェックすることで行っています。zoneのZONE_HIGHMEMから取得したものは、page->flagsにPG_highmemがセットされており、そのビットをチェックすることで判定できます。
ZONE_HIGHMEMでないなら、そのページはストレートマップ内のページであり、リニアアドレスを有している事になります。従ってpage_address関数でリニアアドレスを取得してそれを返します。そうでないならハイメモリーで、リニアアドレスを有していません。kmap_high関数でその割り当てを行います。
void *kmap(struct page *page) { might_sleep(); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); }kmap_high関数では、再度page_address関数でリニアアドレスの取得を試みます。ハイメモページはリニアアドレスを有していないのにと・・・。思えますが。他のプロセスが割り当てている場合があります。それと実はハイメモページを解放する時、ただ単に解放した旨のフラグの設定するだけです。MMUがページアドレス変換を行うのは、TBLキャッシュを通してであり、従って目的としているページが解放されたからと言っても、プロセスとして参照しないだけで、アドレス的には有効となっているケースがあるわけです。
ページがマップされていないなら、map_new_virtual関数により、ハイメモ領域のページをマップすることになります。
void *kmap_high(struct page *page) { unsigned long vaddr; spin_lock(&kmap_lock); vaddr = (unsigned long)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++; BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); spin_unlock(&kmap_lock); return (void*) vaddr; }ページをマップすると言うことですが、これはページの物理アドレスを、ページテーブルエントリー(最終的にはTBLに読み込ませる。)に設定する事を意味します。このためカーネルはこのエントリーをPKMAP_BASEからページサイズが4Kの場合、1024個すなわち領域として4MB予約しています。
(言い換えると永続的マップとして一度に使えるサイズは4MBと言う事。たぶん)
count = LAST_PKMAPは、このマップするために予約しているエントリー数です。従ってここでの処理は、空いているエントリーを探し出して、そのページテーブルエントリーに、ページの物理アドレスを設定する処理になるわけです。
このエントリーを管理しているのが、pkmap_count[LAST_PKMAP]配列です。ここから空いているエントリを探します。last_pkmap_nrは、前回割り当てたpkmap_count[]内のインデックスです。まずlast_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASKとし、その次から検索します。LAST_PKMAP_MASKでマスクしているのは、LAST_PKMAP個超えたらlast_pkmap_nrを0とするためです(たぶん)。
if (!last_pkmap_nr) は、last_pkmap_nr 以降空いているエントリが無かった。ということです。この場合、flush_all_zero_pkmaps関数で、pkmap_count[]=1のマップとして有効となっているが使用していないエントリをpkmap_count[]=0として、TBLを無効化し再度検索します。
pkmap_count[]はそのエントリを参照しているカウンターです。0は未使用ということですが、1というのはTBL として参照し、プロセスからは参照されていない。ということです。プロセスがそのページを解放する時、いきなり0とするのでなく、1とします。そうすることで再度同じページをマップする場合、TBLを無効化する必要がないということです。
再度検索で、(!pkmap_count[last_pkmap_nr])はそのエントリは使用可能ということです。その場合ループを抜けます。
if (--count)で、countが0になったということは、すべてのエントリーを使用できません。ここからが永続的カーネルマップの味噌となります。この場合、このハイメモページを使おうとするタスクをウエイトさせています。タスク状態をTASK_UNINTERRUPTIBLEにし、add_wait_queue関数で、pkmap_map_waitをヘッドとするリストに、 DECLARE_WAITQUEUE(wait, current)で初期化したwaitキューを繋いでいます。そしてschedule関数で実行権を放棄し、ハイメモページを所有しているカーネルタスクが、ハイメモページを解放する時に、このwaitキューをウエイクアップし、再度実行して、ループすることで、pkmap_count[]が0のエントリーを取得できると言うことです。
ループを抜けると、設定したエントリのリニアアドレスをvaddr = PKMAP_ADDR(last_pkmap_nr)で取得し、set_pte_at関数で、マスタPTEのinit_mmに更新して、 pkmap_count[last_pkmap_nr] = 1でTBLが参照している旨の設定をし、set_page_address関数で、page->viatualにそのリニアアドレスを設定します。kmap_high関数に戻ると、pkmap_count[PKMAP_NR(vaddr)]++でこれを呼んだプロセスが参照している旨の設定を施します。
#ifdef CONFIG_X86_PAE #define LAST_PKMAP 512 #else #define LAST_PKMAP 1024 #endif static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP; /* Find an empty entry */ 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 */ 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); 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; set_page_address(page, (void *)vaddr); return vaddr; }従って、カーネルはmap_new_virtual関数で取得した仮想メモリ(まだページが割り当てていないなら。)で、ハイメモ領域から取得したページをアクセスすることが可能となるわけです。そして、使用しなくなったらそのページを管理しているpkmap_count[]に0を設定することで、その仮想アドレスを解放し、他のカーネルスレッドのハイメモページをアロケイトを可能にさせているわけです。昔でいうところの、バンク切り替えみたいなものです。