※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( FUJIWARA Katsunori )
受託開発主体の独立系ソフトウェアハウス数社を経て、現在フリーランス。
前職で、 HPC ( High Performance Computing ) 系システムのために Solaris 向けファイルシステムを実装したのを機に、 OpenSolaris 勉強会に参加。
「lsを読まずにプログラマを名乗るな!」、 「入門TortoiseHg+Mercurial」とか 「俺のコードのどこが悪い?―コードレビューを攻略する40のルール」、 「アセンブラで読み解くプログラムのしくみ」(電子書籍) といった書籍の執筆や、 技術系ウェブ媒体への記事の寄稿も。
ホームページ以外にも、 はてなダイアリー (id:flying-foozy) 等で情報発信中。 Twitter アカウント (@flyingfoozy) は細々と運用中。
本資料は、 "Solaris Internals" の第12章を、 各段落毎に「ワンフレーズ」化すること基本としています。
「ワンフレーズ」化対象の段落を識別するために、 以下のような識別情報表記を使用します。
[{章/節番号}/{通し段落番号}]@p.{ページ番号}
例えば、"[9.2.6/2]@p.465" は、 以下で始まる 「465 ページに配置されている、9.2.6 節における第2段落」 を指します。
As shown in the example, the program's address space comprises ...
列挙の各項目も1段落として扱います。 単一の列挙項目が複数の段落から構成されている場合は、 構成要素の段落を、それぞれ個別の段落として扱います。
識別情報の構成要素は、 紙媒体の "Solaris Internals Second Edition" を元にしています。
なお、個人的に興味深いと思った点に関しては、 勝手に掘り下げた話を展開します。
書籍の正誤情報は、 Solaris Internals サイトの、 Errata 情報を参照してください。
[12/1]@p.xxx
ハードウェアアドレス変換層は、 仮想・物理アドレス間対応付けの生成・破棄や、 MMU の調査・制御のインタフェースを実装します。 また、 ページフォルトをはじめとする、 メモリ関連例外に対する処理も実装します。
[12.1/1]@p.xxx
HAT 層は、 セグメントドライバ視点での仮想・物理変換実現のために、 プラットフォーム固有な MMU ハードウェアを隠蔽することから、 実装の異なる HAT が複数存在します。 アドレス変換に関する最上位の情報は、 プラットフォーム固有の hat 構造体で、 各アドレス空間毎 (= as 構造体毎) に保持されます。 各ページ毎に、 HAT 固有のアドレス変換情報を保持することも可能です。
hat
as
hat 構造体は、プラットフォーム毎に以下で定義されている。
「各ページ毎の HAT 固有のアドレス変換情報」は、 page 構造体の、 以下のフィールドが使用される。
page
void *p_mapping; /* hat specific translation info */
[12.1/2]@p.xxx 〜 [12.1/3]@p.xxx
例えば仮想・物理アドレスのマッピングが必要になった場合、 セグメントドライバによって、 アドレス領域と処理種別指定を伴って HAT 層が提供する関数が呼び出されます。 HAT 層の提供する関数の引数は、 「仮想アドレス」「領域長」「page 構造体ポインタ」「保護モード」といった、 プラットフォーム依存性のないものなので、 MMU レベルの詳細を意識せずに利用できます。
HAT 層が提供する関数の一覧を表 12.1 に示します。
hat_alloc() は、 as 構造体初期化で呼び出されるもの。 通常のページ生成・破棄は、 hat_memload()/hat_unload() が使用される。
hat_alloc()
hat_memload()
hat_unload()
as 構造体初期化は、以下の様な契機で実施:
fork
as_dup()
exec
exec_args()
shmctl
shmem_lock()
start_init_common()
ちなみに、 SPARC 向け HAT 層実装では、 カーネル用の as 構造体 (kas) 初期化の際にも hat_alloc() が使用されるが、 x86 向け実装では hat_alloc() が使用されていない。
kas
ざっと見た範囲では、 C 実装は kas が初期化されている前提のコードのみなので、 アセンブラ実装側で初期化している? C 実装にできないのは、起動時 MMU 挙動上の違い由来?
[12.2/1]@p.xxx
本節では、 UltraSPARC 向けの HAT 実装に関して説明します。
[12.2.1/1]@p.xxx 〜 [12.2.1/3]@p.xxx
UltraSPARC は、 プロセッサ内 MMU を使って、 仮想・物理アドレス変換を即時実行します。 MMU は、 Translation Lookaside Buffer (TLB) と呼ばれるテーブルで、 変換情報を管理します。 HAT 層は、 仮想・物理アドレスの対応関係を識別するエントリを使って、 TLB を制御します。
TLB に格納可能な情報量には、 ハードウェア上のサイズ上限があるため、 主記憶領域(= TLB 用領域よりも低速)中に、 より大きな仮想・物理変換情報を格納します。 UltraSPARC では、 このテーブルは Translation Store Buffer (TSB) と呼ばれますが、 他の CPU アーキテクチャでは、 ページテーブルと呼ばれる場合があります。 実際の仮想・物理変換では、 最初に TLB が検索(= ハードウェアレベルでの検索)され、 該当エントリが無い(= TLB miss)場合には、 主記憶領域中のテーブルが検索されます。
UltraSPARC では、 TLB miss 発生時に、 主記憶領域中の TSB を検索し、 TLB の入れ替えを行うためのプログラムが実行されます。
[12.2.1/4]@p.xxx
(1) ユーザ空間における malloc() 呼び出しで、 (2) 仮想メモリのアドレスが返却されたケースでは、 (3) 当該メモリへの最初のアクセスの際に、 仮想メモリ層では、 (7) 未使用リストからの物理メモリページの新規割り当てを、 システムに要求します。 仮想アドレスシステムのソフトウェアは、 malloc() で返却された領域に対応するページの仮想アドレスと、 新規に割り当てられた物理メモリページの、 物理アドレス空間における物理アドレスを関連付ける変換エントリを作成します。 この新規変換エントリは、 (8) TSB に格納された上で、 (9) TLB の利用可能(≠ 空き)領域に格納されます。 また、 プロセスが属するアドレス空間の管理情報にも関連づけられます。 ユーザプログラムが当該仮想メモリ領域にアクセスした際に、 上記処理で追加された新規 TLB エントリが、 TLB 内にまだ保持(= 他の処理によって破棄される可能性もあります されている場合は、 仮想・物理アドレス変換が即時実施されます。 当該 TLB エントリが破棄されている場合は、 TLB ミスにより例外が発生し、 TSB 中のエントリが検索されます。
malloc()
原文では、 TLB ミス例外の説明を後回しにする都合なのか、 「ユーザ空間でのメモリアクセス」から一気に「物理メモリページの要求」 が実施されるかのような記述だが、 実際には以下の様な手順で処理が進む筈。 訳文中の番号は、 以下の手順の番号と対応させている。
[12.2.1/5]@p.xxx
TSB 自体にもソフトウェア上のサイズ上限が存在するため、 極端な実行状況で TSB miss が発生した場合は、 プロセスの管理情報を元にした、 低速な検索処理が実施されます。
TSB のサイズ上限に関する記述は、 SFMMU_SET_TSB_MAX_GROWSIZE マクロ定義に対する、 以下のコメントに見ることができる。
SFMMU_SET_TSB_MAX_GROWSIZE
* Restrict tsb_max_growsize to make sure that: * 1) TSBs can't grow larger than the TSB slab size * 2) TSBs can't grow larger than UTSB_MAX_SZCODE.
ちなみに、UTSB_MAX_SZCODE の定義は:
UTSB_MAX_SZCODE
#ifdef sun4v #define UTSB_MAX_SZCODE TSB_256M_SZCODE /* max. supported TSB size */ #else /* sun4u */ #define UTSB_MAX_SZCODE TSB_1M_SZCODE /* max. supported TSB size */ #endif /* sun4v */
"the software structures linked to the process" は、 前段落の "The entry is also kept in software, linked to the address space of the process to which it belongs" に相当するものと思われる。
[12.2.2/1]@p.xxx 〜 [12.2.2/4]@p.xxx
UltraSPARC の hat 構造体は、 例えば、 プロセスのコンテキストID (aka コンテキスト番号) や、 as 構造体へのポインタ、 TSB へのポインタ等の、 各プロセス毎のアドレス空間に関する、 HAT 層固有情報を関連付ける用途で使用されます。
例えば、実際に稼動している sh プロセスの hat 構造体を取得するには、 該当プロセスの proc 構造体を取得する必要があります。
sh
proc
他の手段としては、 出力に proc 構造体アドレスが含まれる ::ps dcmd と使用する方法もあります。
::ps
proc 構造体アドレスを元に、 図 12.4 の構造体リンクを辿ります。
以下に、実行例での手順をまとめる。
PID::pid2proc
PROC::print proc_t p_as
AS::print struct as a_hat
HAT::print -t struct hat
struct proc は proc_t で typedef されているが、 struct as に対応する as_t は定義されていない。 struct hat に対応する hat_t は、 i86pc 向け HAT 層でのみ定義されている。
struct proc
proc_t
typedef
struct as
as_t
struct hat
hat_t
[12.2.2/5]@p.xxx 〜 [12.2.2/12]@p.xxx
hat 構造体は、 以下の様なフィールドを持ちます。
sfmmu_xhat_provider
sfmmu_cpusran
sfmmu_as
sfmmu_ttecnt[]
sfmmu_ismttecnt[]
sfmmu_iblkp
sfmmu_imentp
[12.2.2/13]@p.xxx 〜 [12.2.2/21]@p.xxx
sfmmu_free
sfmmu_ismhat
sfmmu_ctxflushed
sfmmu_rmstat
hat_stats_enable()
uchar_t
a_vbits
sfmmu_clrstart
sfmmu_clrbin
sfmmu_cnum
sfmmu_ctxs
sfmmu_flags
sfmmu_tsb
[12.2.2/22]@p.xxx 〜 [12.2.2/27]@p.xxx
sfmmu_ismblkpa
sfmmu_tsb_cv
sfmmu_cext
sfmmu_mflags
sfmmu_mcnt
sfmmu_pgsz[]
sfmmu_reprog_pgsz_arr()
UltraSPARC IV+/Panther に関する言及は、 u3_common_mmu.c に見られる。 u3_common_mmu.c は UltraSPARC III 以降に共通の実装? us3_* 系実装ファイルには、 cheetah (UltraSPARC III) と jalapeno (UltraSPARC III+) の名前が見られる。
[12.2.3/1]@p.xxx
変換テーブルやページテーブルは様々な方法で実現可能です。 sun4m や sun4d のような 32bit の旧式 SPARC アーキテクチャでは、 "SPARC Reference MMU specification" によって、 第1レベルは 256 エントリ、 第2第3レベルは 64 エントリを保持する、 最大3レベルのページテーブル構造が規定されています。
SPARC アーキテクチャ/MMU 仕様の詳細は、 SPARC International, Inc. の Specifications Download ページから入手可能。
V8 の MMU 仕様は "Appendix H. SPARC Reference MMU Architecture"、 V9 の MMU 仕様は "Appendix F. SPARC-V9 MMU Requirements"。
Solaris Internals 原文では、 「第2レベルの各テーブルは、 64個の第3レベルテーブルをポイント」する旨の記載があるが、 SPARC V8 の仕様上、 テーブル要素である Page Table Descriptor は:
これらを識別する「種別情報」を持っているので、 必ずしも空間全体に対して、 第3レベルテーブルまで必要とされているわけではない。
第1〜第2レベルテーブルにおける「ページテーブル要素」の出現は、 「ラージページの使用」を意味する。
[12.2.3/2]@p.xxx 〜 [12.2.3/3]@p.xxx
固定長のページテーブルは、 マッピングされないメモリ領域の分も管理領域が必要なため、 疎なマッピングの場合に効率が悪くなるのが、 多段ページテーブルの問題点です。 32ビット空間全体ののマッピングには、 1M 個のページテーブル終端要素が必要なので、 ページテーブル領域の総計は 4M バイトになります。 テキスト・データ・スタックで合計3ページ (12KB) しか消費しないプロセスも、 4M バイトの管理用ページテーブル領域が必要になります。
多段ページテーブルでは、 8KB ページによる 64bit アドレス空間のマッピングへの対処が難しくなります。 ページサイズを拡大すれば問題は解消しますが、 割り当て粒度が大きくなるため、 メモリ使用効率が低下します。
「ページテーブル領域の総計は 4M バイト」の導出過程は以下の通り。 現行 Solaris はページサイズ 8KB な sun4u 以降のアーキテクチャを想定しているが、 この段落で参照している 32bit アーキテクチャのページサイズは 4KB。
先述したように、 SPARC V8 の MMU では第3レベルのページテーブルは必須ではないため、 本文の記述は多分前提条件が間違っている。
ページテーブル領域に 4MB を要するワーストケースは、 以下の条件を満たす必要から、 「256K 毎に1ページ(=4K)マッピング」となるので、 最低でも 4K (mapping/table) × 16K (table) = 64MB (mapping) は必要な筈。
なお、 この場合の「ページテーブル領域に 4MB」は、 第3レベルページテーブル限定の消費量となるが、 第2レベルページテーブルの総消費量は 1/64 の 64KB とぐっと少なくなるので、 それほど不正確な値でもない
[12.2.3/4]@p.xxx 〜 [12.2.3/7]@p.xxx
64bit 空間での疎なマッピングへの対応の先鞭は、 物理ページ毎に情報を持たせる、 IBM System/38 での Inverted Page Table (IPT) です。
sun4u アーキテクチャでは、 仮想アドレス等を使ったハッシュ値でエントリリストを引き当てる、 Hashed Page Table (HPT) と呼ばれる方法を採用しています。 引き当てたエントリリストから、 仮想アドレスとコンテキストIDが一致するものを探します。
Solaris では、 有効なマッピングの追跡にしようされる、 hme_blk 構造体と関連データを元に、 変換テーブルが実現されています。 カーネル向けと、 ユーザプロセス(群)向けの、 2つの変換テーブルが存在します。 各アドレス空間における個々の仮想アドレス領域から物理メモリへのマッピングが、 hme_blk 構造体によって定義されます。 ハッシュ関数には、 アドレス空間識別子・仮想アドレス・領域サイズが使用されます。 TSB miss 発生時には、 ハッシュ値で引き当てた hme_blk リストを線形探索し、 該当する hme_blk があれば、 それを元に TLB/TSB を充填します。
hme_blk
以降の節で、 ハッシュテーブルに関連する構造体や機能の詳細を説明します。
V8 の場合、 Context Table Pointer レジスタが指す Context Table 中のエントリが、 第1レベルのページテーブルを指す形式なので、 ページテーブルはプロセス毎に独立している。
検索条件に page size を加える手法の話は、 仮想空間管理でも採用している (特定のアドレスに対して、「ページ先頭アドレスとサイズ」対は導出可能である)。
[12.2.3.1/1]@p.xxx
TLB の各要素は 仮想・物理のマッピングと、 詳細な属性情報を保持する Translation Table Entry (TTE) から構成されています。 TTE は、 タグ (64bit 長) と変換データ (64bit 長) で構成され、 sun4m アーキテクチャにおける Page Table Entry (PTE) 相当ともみなせます。 TTE タグは、 コンテキストIDと加工した仮想アドレスを、 TTE データは、 変換に関する属性情報と、 対応する物理アドレスを保持します。 アドレス空間を識別する 13bit 長のコンテキストID により、 別アドレス空間の同一アドレスに関するマッピングであっても、 TLB 内で両立できます。 TTE は、 8KB, 64KB, 512KB, 4MB (UltraSPARC V+ は 32MB, 256MB も) 長の連続したメモリ領域をマッピングしますので、 サイズ情報は最も重要な属性情報の1つです。 書き込み・実行の許可や、 物理・仮想アドレスベースでのキャッシュ可否なども、 属性情報として保持されます。
マッピングサイズが変わっても(= ラージページマッピングでも) 下位層における仮想ページの管理は、 page 構造体を使って 8K 単位で実施される。 詳細は "9.10.1 System View of a Large Page" 参照。
考察の続き
コンテキストIDは、 hat.sfmmu_ctxs[] で保持される。 配列保持なのは、 システム上に複数のコンテキストドメイン (= MMU) があるケースを想定したもの。
hat.sfmmu_ctxs[]
例えば sfmmu_ctxs[CPU_MMU_IDX(CPU)] といった形式で、 各ドメイン=MMU毎のコンテキストIDを管理している。 同一プロセスの複数のスレッドが、 それぞれ MMU が異なる CPU 上で稼動した場合、 同一仮想アドレス空間を構成する物理メモリが、 MMU毎に異なるコンテキストIDでマッピングされるようなケースも。
sfmmu_ctxs[CPU_MMU_IDX(CPU)]
単一CPU・単一MMUの場合は、 実質的に PID が コンテキストID に相当するが、 プロセス数の最大値 (= PID の最大値) は 999999 (MAX_MAXPID) である一方、 コンテキストID は 13bit 長 (= 最大 8192 - 1) なので、 両者の管理は独立している必要がある。
MAX_MAXPID
→ (MMU 毎の) コンテキスト ID は、 PID とは独立して割り当て・解放されている模様 (次々段落での情報を元に推測)。
また、 init プロセスやカーネル空間向けの分を除くと、 どんなに大量にメモリを積んで、 各プロセスのフットプリントを減らしても、 メモリ上に存在可能なユーザプロセス数は 8192 - (1 + N) に限定されることになるが、 この認識で合っているのか?
→ UltraSPARC 系は、 MMU あたりの TLB 数がせいぜい 1024 以下程度らしいので、 上記のような仮想空間と独立した管理により、 13bit (= 8192) のコンテキスト ID でも、 十分運用できるのではないかと思われる (例: コアごとの命令・データ TLB エントリ数は、 UltraSPARC T1 が 64 エントリ、 UltraSPARC III は 512 エントリ)。
[12.2.3.1/2]@p.xxx
TLB に設定された TTE のどれかに対して、 仮想アドレス (のページ内オフセット除外部分) と、 (プロセスに払い出された) コンテキスト ID の両方が一致した場合、 「TLB ヒット」 (TLB hit) となります。 複数の TLB エントリによるアドレスエイリアス (address aliasing) は、 同一物理メモリを複数仮想アドレスで共有することはできますが、 同一仮想アドレスに複数の物理メモリが割り当てられた場合の挙動は未定義です。 TLB ミスのトラップ発生時には、 (ハードウェア実装される TLB に対して) ソフトウェア管理のキャッシュである TSB からの読み込みが発生します。
[12.2.3.1/3]@p.xxx
Solaris 9 以前のカーネル (sun4u) では、 全ての MMU コンテキスト (= プロセス+カーネル) が単一 TSB を共有していたため、 TSB エントリ (= TSB TTE タグ) はコンテキスト ID を保持する必要がありました。 しかし Solaris 10 以後は、 仮想空間毎 (※ 原文では「プロセス」) に TSB を保持するようになったため、 TSB TTE タグはコンテキスト ID を保持しなくなりました。 仮想アドレス (のページ内オフセット除外部分) と、 TSB TTE タグ中の仮想アドレスが合致した場合、 「TSB ヒット」(TSB hit) となります。
例えば、TLB ミスが発生した場合の TLB エントリ充填手順は:
なお、以下のような手順を踏むことで、 MMU 毎の「利用可能コンテキスト ID」管理を回避している模様 (sfmmu_ctx_wrap_around() も参照)。
sfmmu_ctx_wrap_around()
世代番号更新の際に、 割り当て済みコンテキストIDに対応する TLB エントリは全て破棄されるので、 コンテキストIDが再利用されても、対応する TLB 破棄の必要はない。
[12.2.3.2/1]@p.xxx 〜 [12.2.3.2/2]@p.xxx
UltraSPARC sun4u の HAT 層では、 HAT マッピングエントリ (HAT mapping entry/HME) と呼ばれる sf_hment 構造体を使って、 仮想-物理のアドレス変換を管理しています。
sf_hment
sf_hment 構造体は、 対応する物理ページを、 page 構造体へのポインタとして保持します。 物理ページは、TTE とは一対一ですが、 仮想ページとは一対多なので、 同一物理ページを参照する一連の sf_hment を辿れるように、 hme_prev/hme_next によって双方向にリンクされています (NULL 終端)。
hme_prev
hme_next
sf_hment 構造体は、 hme_blk 構造体の hbk_hme 配列要素に相当する。 こういう重要な情報を、「ソース読め!」的に省略しないで欲しいなぁ(笑)
hbk_hme
page 構造体自体は「物理ページ」概念に相当するが、 アーキテクチャ非依存な構造体なので、 仮想〜物理マッピングのような HAT 依存な情報は保持していない。
sf_hment 構造体の「双方向リンク」は、 スワップアウト等での物理ページ刈り取りの際に、 関連する仮想アドレス領域のマッピングをまとめて解消するようなケースで有用。
[12.2.3.2/3]@p.xxx 〜 [12.2.3.2/11]@p.xxx
実システム上で稼動している bash プロセスの例を示します。
::ps dcmd で取得した proc 構造体アドレス情報を使って、 ある bash プロセスにおける仮想アドレス 0x10028 番地に対応する、 物理メモリのマッピング情報を取得してみましょう。 まずは proc 構造体からアドレス空間情報を取得した上で、 ::sfmmu_vtop dcmd で仮想〜物理のマッピングを取得します。
::sfmmu_vtop
仮想アドレス 0x10028 番地は、 物理アドレス 0xb490028 にマッピングされていることが判明しました。 実行例の ::sfmmu_vtop dcmd は以下のような情報も表示しています。
sfmmup
hmebp
hmehash_bucket
hmeblkp
tte
pfn
pp
[12.2.3.2/12]@p.xxx 〜 [12.2.3.2/14]@p.xxx
対応する仮想ページの page 構造体情報を見てみましょう。
p_pagenum フィールドが保持する値は、 ::sfmmu_vtop が表示した物理ページフレーム番号と一致しているので、 正しい page 構造体を参照していることが確認できます。 物理メモリの共有数を表す p_share 値が 5、 先述の ::ps 実行例で表示される bash プロセスが五つなので、 稼動アドレス 0x10028 番地は、 bash バイナリのテキスト領域であろうことが推測できます。
p_pagenum
p_share
page 構造体に対応する sf_hment 構造体のアドレスは、 p_mapping フィールド (void* 型) 経由でリンクされています。
p_mapping
void*
2つ目の実行例により、 全てのマッピング (sf_hment) が同一の物理ページ (page 構造体) を参照していることが確認できます。
図 12.5 における、 page と hme_blk.hblk_hmeのリンクは、 「単一の page」に対して、 「複数の hme_blk.hblk_hme」が関連付けられていることを意味する。 page からの参照先は、 常に双方向リストの先頭 sf_hment を指す。
hme_blk.hblk_hme
[12.2.3.3/1]@p.xxx 〜 [12.2.3.3/2]@p.xxx
「起点アドレス」と「範囲」が示す連続した仮想アドレス領域と、 物理メモリのマッピングの管理に、 hme_blk 構造体が使用されます。
64KB, 512KB, 4MB ページを管理する場合は、 hme_blk 構造体末尾の sf_hment 構造体配列 hblk_hme は要素を1つしか持ちませんが、 8K ページを管理する場合は、 7要素分の領域を追加して合計8要素が保持されます。
hblk_hme
「起点アドレス」と「範囲」は、 それぞれ以下の領域に格納されている。
hblk_tag.hblk_tag_un.hblk_basepg
hblk_span
なお、 「起点アドレス」は仮想アドレスであるため、 HAT (≒ アドレス空間) 情報と組み合わせて初めて意味がある。 hme_blk 構造体と関連付けられた hat 構造体は、 hblk_tag.hblk_tag_un.hblk_id 経由で入手できる (書籍では hblk_id ではなく sfmmup と表記)。
hblk_tag.hblk_tag_un.hblk_id
hblk_id
8K ページのマッピング管理時の hme_blk 領域拡張に関しては、 本文中の前後の文脈では特に言及が無いが、 おそらくは管理領域の細分化を回避するためか?
[12.2.3.3/3]@p.xxx 〜 [12.2.3.3/4]@p.xxx
以下の例では、 0x3000722a750 番地の hme_blk 構造体は、 有効な hblk_hme 領域を4つ保持しています。
::array dcmd を使うことで、 配列中の各 hblk_hme 領域の先頭アドレスを列挙できます。
dcmd を使うことで、 配列中の各 hblk_hme 領域の先頭アドレスを列挙できます。
本文中では、 「hme_blk 末尾の配列サイズが可変」という説明と、 「hblk_hmecount の値から、 hblk_hme 配列の要素数は4つ」および、 配列4要素分のアドレス列挙の実行例が連続しているため、 まるで「hme_blk 末尾の配列要素数は1〜8で可変」 であるかのような紛らわしさがあるが、 ヘッダファイル等を見る限りでは、 下記のような情報しか読み取れない。
hblk_hmecount
[12.2.3.3/5]@p.xxx
sf_hment 構造体配列 hblk_hme 中の要素のうち、 (1) 「ページマッピングリストにリンクされている」ものの数が hblk_hmecntに、 (2) 「有効な TTE 値を持っている」ものの数が hblk_vcnt に保持されています。 これらの参照カウンタの増減は、 cas (Compare-And-Swap) 命令によって厳密性が保証されており、 hme_blk 領域の解放可否の判定に使用されます。 これらとは別に用意されている「TTE 毎参照カウント機能」は、 物理メモリのフレーム番号を利用する physio 処理の間、 物理メモリを保持し続ける際に使用されます。
hblk_hmecnt
hblk_vcnt
本文中で説明されているフィールド名は、 実際にはプリプロセッサによるマクロ名 (hme_blk 構造体定義の引用末尾を参照)なので、 例えば本文中の mdb 実行例などでは、 マクロ名ではなく正式な(長い)名前を使用している。
本文で言及している 「TTE 毎参照カウント機能」 (per-TTE lock count) は、 sf_hment.hme_tte.tte_bit.rsv 中の、 6bit 分の lockcnt フィールドを使用していたみたいだが、 「複数 physio による参照カウント増加」で桁あふれの問題を起こしたためなのか、 現在は hme_blk.hblk_lckcnt を使用するようになっている模様。
sf_hment.hme_tte.tte_bit.rsv
hme_blk.hblk_lckcnt
8つの sf_hment 要素を一括管理する 8KB マッピングでは、 hme_blk 毎の参照カウントは駄目な気がするが、 物理メモリを固定する physio 的な挙動なら、 64KB ページ等のメモリ確保をするでしょ?的な割り切りなのかな?
ヘッダのコメントを見るに、 「TTE 毎の参照カウント」に戻す気はある模様(笑)
@usr/src/uts/sfmmu/vm/hat_sfmmu.h * The hmeblk now also has per tte lock cnts. This is required because * the counts can be high and there are not enough bits in the tte. When * physio is fixed to not lock the translations we should be able to move * the lock cnt back to the tte. See bug id 1198554.
[12.2.3.3/6]@p.xxx 〜 [12.2.3.3/9]@p.xxx
hmeblk_tag 構造体中で特に重要なフィールドは、 以下の2つです。
hmeblk_tag
hblk_basepg
hblk_rehash
hmeblk_tag 関連のマクロを例示します。
hblk_rehash はビットフィールドとして保持しているので、 実質的にアドレスの合致判定は、想定サイズの合致判定を兼ねていることになる。
現行の定義では書籍での記述と異なり、 sfmmu_t *sfmmup ではなく void* hblk_id と定義されている
sfmmu_t *sfmmup
void* hblk_id
ちなみに(現在は)、 sfmmu_t は struct hat の typedef。
sfmmu_t
書籍に掲載されている UltraSPARC では、 TLB TTE データとして必要な物理アドレスは 42..13 ビット (図 12.6 参照) なので、 63..43 の 21 ビットは破棄されることになるが、 現行の T 系や M 系ではもっと広い空間が有効になっているのかな?
マッピング検索の開始サイズが 64KB なのは、 8KB ページのマッピングの場合、 先述したように連続8エントリをまとめて管理することから、 実質的には64KBが最小管理単位であるため。
[12.2.3.3/10]@p.xxx
hme_blk 構造体による管理の利点は、 広大な仮想空間が間歇的にマッピングされる際の(管理用)メモリの効率です。 但し、 特定のアドレス空間に関するマッピングを全て破棄するようなケースでは、 当該アドレス空間内部を全走査 (走査単位は 64KB) して、 引き当てた hme_blk を順次破棄するしかありません。 このような処理を高速化するために、 shadow HME ブロックが導入されました。
TSB はプロセス毎に用意されるが、 hme_blk のハッシュテーブルは、 ユーザ用/カーネル用にしか分離されていないため、 「あるプロセス P のアドレス空間に関する全マッピング」的な引き当てができない。
「プロセスのアドレス空間内部を全走査」とは言っても、 プロセス毎のアドレス空間管理において、 セグメント毎の範囲は限定されているので、 本当の意味で「アドレス空間全て」が走査される必要は無い筈 (それでも相当にコストは掛かるが……)
[12.2.3.4/1]@p.xxx
各プロセスへの hme_blk 割り当てには、 上限 4MB (UlrtaSPARC V+ は 256MB) のより広範な領域に対する、 シャドウ hme_blk と呼ばれるものが追加されます。 シャドウ hme_blk 領域には、そのことを示すフラグが設定されます。 例えば、UltraSPARC IV+ 以前の環境では、 64KB 領域割り当てに対応する hme_blk には、 当該領域を含む 512KB 領域に対応するシャドウ hme_blk が、 更にその領域を含む 4MB 領域に対するシャドウ hme_blk が割り当てられます。 シャドウ hme_blk は、 より下位サイズの領域をマッピングする hme_blk の存在の有無を、 ビットマスクで保持します。 先述の例では、 4MB のシャドウ hme_blk は 8 個分の 512KB 領域のマッピングの有無をビットマスクで管理します。 当該領域にマッピングが存在する場合、 対応する hme_blk は 512KB 領域を直接マッピングする hme_blk か、 あるいは下位(= 64KB)サイズの領域マッピングの有無を管理する、 シャドウ hme_blk となります。
シャドウ hme_blk を表すフラグは hme_blk.hblk_misc.shadow_bit、 ビットマスクは hme_blk.hblk_un.hblk_shadow_mask で格納。
hme_blk.hblk_misc.shadow_bit
hme_blk.hblk_un.hblk_shadow_mask
64KB 領域の一括マッピングを管理する hblk1_t や、 8KB x8 領域の個別マッピングを管理する hblk8_t は、 char[sizeof(....)] ベースで定義されている。
hblk1_t
hblk8_t は、 char[sizeof(....)] ベースで定義されている。
char[sizeof(....)]
typedef struct { char h8[HME8BLK_SZ]; } hblk8_t; typedef struct { char h1[HME1BLK_SZ]; } hblk1_t;
[12.2.3.4/2]@p.xxx 〜 [12.2.3.4/3]@p.xxx
特定範囲中のマッピングの存在の有無は、 まず 4MB (or 256MB) 単位の領域先頭アドレスで HME ブロックハッシュを検索し、 hme_blk が存在しなければ、 当該領域にはマッピングが無いことが確認できます。 もしも、シャドウ hme_blk が存在した場合、 より下位のサイズでのマッピングが存在する可能性があります。
仮想アドレス空間中にマッピングがまばらに存在するケースでは、 広範な領域におけるマッピングの不在が、 シャドウ hme_blk の不在により、 最大マッピングサイズでの一度の検索で判定できます。 しかし、 シャドウ hme_blk が存在する場合は、 より下位サイズのマッピングが存在するため、 当該範囲における通常 hme_blk の引き当てを行います。
シャドウ hme_blk の存在に対して、 第2段落では「(裏で invalidate されている可能性もあるので) 有効な実マッピングを引き当てできるとは限らない」(not garanteeed) と言いつつ、 第3段落では「最低1つは下位サイズのマッピングが存在する筈」(must) という矛盾する記述が(笑)。
「hme_blk 自体は存在するけど、valid とは限らない」 ということで一応辻褄は合う? 前述した hblk_hmecnt や hblk_vcnt あたりは、 排他ではなく CAS (Compare And Swap) 命令で保護されているらしいので、 これらが 0 = invalid を意味する?
[12.2.3.4/4]@p.xxx
シャドウ hme_blk の導入により、 マッピング有無の判定は、 4MB (or 256MB) 単位の検索と、 必要に応じた下位サイズマッピングの掘り下げで済みます。 しかし、 広大なアドレス空間では、この方法でも低速です。 4MB 空間の探索数がハッシュテーブルサイズ (UHMEHASH_SZ) を超える場合は、 ハッシュチェーンの線形走査をせざるを得ません。
UHMEHASH_SZ
ユーザ空間向けのハッシュテーブルサイズは、 結構面倒な算出処理で計算している模様。 詳細は calc_hmehash_sz()@usr/src/uts/sun4/vm/sfmmu.c 等を参照のこと。
calc_hmehash_sz()@usr/src/uts/sun4/vm/sfmmu.c
[12.2.3.5/1]@p.xxx
hme_blk は sfmmu_hblk_alloc() によって割り当てられます。 内部的な sfmmu_shadow_hcreate() 呼び出しにより、 必要なシャドウ hme_blk も間接的に割り当てられます。 通常の割り当てには、 sfmmu8_cache (8KB マッピング用) および sfmmu1_cache (64KB マッピング用) の2つの slab キャッシュが使用されます。 カーネル空間向けマッピングでの kmem_cache_alloc() 呼び出しは KM_NOSLEEP ですが、 ユーザ空間向けでは KM_SLEEP です。 segkmem セグメントが利用可能になるまでのブート過程では、 事前確保された静的領域からの割り当てとなります。 この「事前確保された静的領域」(nucleus hme blk)は、 sfmmu_init_nucleus_hblks() で初期化されます。
sfmmu_hblk_alloc()
sfmmu_shadow_hcreate()
sfmmu8_cache
sfmmu1_cache
kmem_cache_alloc()
KM_NOSLEEP
KM_SLEEP
sfmmu_init_nucleus_hblks()
本文では「sfmmu8_cache は hat_memload_arena を、 sfmmu1_cache は kmem_default_arena を使用」 する旨が言及されているが、現行の実装だと、 「sfmmu1_cache は、 独自 arena の hat_memload1_arena を使用」している模様。
hat_memload_arena
kmem_default_arena
hat_memload1_arena
hat_memload_arena は 内部処理で VM_MEMLOAD フラグが付加される点が、 hat_memload1_arena との差異になる模様。
VM_MEMLOAD
* VM_MEMLOAD is for use by the HAT to avoid infinite recursion.
[12.2.3.5/2]@p.xxx 〜 [12.2.3.5/3]@p.xxx
SPARC の HAT 層では、 8KB マッピング用の空き hme_blk 領域プールを、 freehblkp で参照しています。 sfmmu_hblk_allloc() は、 通常プロセス向けに sfmmu8_cache からの確保が成功した際に、 プールが充填されていなければ、 確保した領域のプールへの追加と、領域再確保を繰り返します。
freehblkp
sfmmu_hblk_allloc()
空き領域枯渇によって キャッシュから hme_blk が確保できない場合、 sfmmu_hblk_steal() 呼び出しにより、 ハッシュリスト中の未使用又は未ロック中の hme_blk を横取りします。 使用中のものは、対応するアドレス空間から横取りされます (対応する物理メモリページも横取りされる?) ユーザプロセス向け HME ハッシュから横取りできないケースでは、 カーネル空間向け HME ハッシュから、 使用可能 hme_blk を探します。 最も最悪のケースでは、 空き領域が見つかるまで sfmmu_hblk_steal() はループを繰り返しますが、 初期化時に nucleus hme blk として十分な量を確保しますし、 hme_blk は動的に追加されますので、 このような事態になることはありません。
sfmmu_hblk_steal()
[12.2.3.5/4]@p.xxx
sfmmu_hblk_alloc() では、 hme_blk 確保後の検証処理として、 HME ハッシュに利用可能な既存のものの有無を確認します。 hblk_reserve (後述) ではないものが存在するなら、 確保した hme_blk を解放し、既存のものを利用します。 ユーザ空間向け確保での確保済み hme_blk は、 (前述した) プール領域への追加によって解放されます。 プール領域が一杯の場合、 確保済み領域は segkmem に返却されます。 カーネル空間向け確保の場合、 8KB 向け hme_blk (hblk8) は問答無用でプール領域に追加することで、 segkmem への返却処理が省略されます。 hme_blk 確保過程での kmem_cache_free() が、 内部でのメモリ確保のために hme_blk を必要とすることで、 再帰呼び出しが無限に発生する可能性を回避するためです。 64KB 向け hme_blk (hblk1) では segkmem への返却は発生しません。
hblk_reserve
kmem_cache_free()
「segkmem への返却処理」の有無が、 hat_memload_arena と hat_memload1_arena での VM_MEMLOAD フラグの有無の差。
[12.2.3.5/5]@p.xxx
8KB 向け hme_blk (hblk8) を、 sfmmu8_cache から確保する場合、 slab キャッシュ自体が新たなメモリ空間マッピングを要する可能性があるため、 HAT 層実装は、再帰呼び出しの無限発生に留意する必要があります。 sfmmu8_cache 自体のマッピングのための sfmmu_hblk_alloc() 呼び出しでは、 最初にプール領域からの hme_blk 確保が試みられます。 プール領域が空の場合、 あらかじめ割り当てられた特別な hme_blk である hblk_reserve が引き当てられます。 hblk_reserve を引き当てた場合、 実行中のスレッドを「オーナー」スレッドとして登録 (hblk_reserve_thread) し、 排他 (hblk_reserve_lock) を獲得することで、 他のスレッドによる hblk_reserve 利用を防止します。 hblk_reserve の「オーナー」が、 更に別の 8KB 向け hme_blk (hblk8) を確保しようとする可能性がありますので、 「オーナー」にのみ使用を許す hme_blk を プール領域にあらかじめ HBLK_RESERVE_MIN だけ確保するようにしています。 これらを使い切った場合は、パニックによりシステム終了します。
hblk_reserve_thread
hblk_reserve_lock
[12.2.3.5/6]@p.xxx 〜 [12.2.3.5/7]@p.xxx
hblk_reserve の「オーナー」による sfmmu_hblk_alloc() 呼び出しで、 sfmmu8_cache からの 8KB 向け hme_blk (hblk8) が確保できた場合、 確保された hme_blk と hblk_reserve を交換した上で、 保留されている割り当て処理を消化します。
sfmmu_hblk_alloc() での検証過程で、 HME ハッシュテーブル中に hblk_reserve が存在し、 且つ自身が「オーナー」ではない場合、 hblk_reserve_lock が解放されるまで割り当て処理を休止します。 自身が「オーナー」の場合は、 新規に確保した hme_blk により、 使用中の hblk_reserve を置き換えます。
[12.2.3.6/1]@p.xxx 〜 [12.2.3.6/2]@p.xxx
sun4u カーネルでは、 カーネル空間向けの hme_blk ハッシュテーブル (khme_hash) と、 ユーザ空間向けのハッシュテーブル (uhme_hash) が存在します。 ハッシュテーブルは hmehash_bucket の配列で、 カーネル空間向け (khmehash_num) と、 ユーザ空間向け (uhmehash_num) で独立したサイズ設定になります。
khme_hash
uhme_hash
khmehash_num
uhmehash_num
hmehash_bucket は2つの排他機構を持ちます。 hmehash_mutex は、 ハッシュチェーンへの操作を単一スレッドに限定するためのもので、 HAT 層に対する「仮想アドレス+アドレス空間」を伴う要求は、 この排他を獲得します。 走査中の一貫性を保つために、 TSB ミスハンドラはこの排他を獲得しますが、 hmehash_mutex の排他を獲得したスレッドが 再び TLB/TSB ミスを生じた場合、 デッドロック等の危険があります。 そのような状況を回避するため、 TSB ミスハンドラ、 sfmmu_vatopfn()、 およびハッシュチェーンに対する hme_blk の追加/削除の間は、 hmehash_listlock を使用します。 hmehash_listlock の使用中は、 TLB ミスが発生しないことを保証するように、 HAT 層が実装されています。
hmehash_mutex
sfmmu_vatopfn()
hmehash_listlock
書籍/ヘッダファイル中のコメントでは hmehash_listlock だが、 実際に定義では hmeh_listlock。 しかも、全ソース (*.s 含む) 中で当該シンボルを参照している箇所は無いので、 どのように「保証」されているのかは不明。
hmeh_listlock
[12.2.3.6/3]@p.xxx
ユーザ向けハッシュテーブルは、 物理メモリ量の HMEHASH_FACTOR 倍ベースに、 平均チェーン長が HMENT_HASHAVELEN になるような、 2の累乗サイズとなります。 ユーザ空間向けハッシュテーブルが必要とするカーネルメモリ量を制限するため、 テーブルサイズ値 uhmehash_num は、 MAX_UHME_BUCKETS が上限となっています。 カーネル向けハッシュテーブルは、 物理メモリ量ベースの2の累乗サイズとすることで、 平均チェーン長を1にしています。 カーネル向けテーブルサイズは MAX_KHME_BUCKETS を上限とすると同時に、 MIN_KHME_BUCKETS が下限になっています。
HMEHASH_FACTOR
HMENT_HASHAVELEN
MAX_UHME_BUCKETS
MAX_KHME_BUCKETS
MIN_KHME_BUCKETS
書籍での MAX_UHME_BUCKETS や MAX_KHME_BUCKETS は 2M だが、 ヘッダファイル上は (0x1 << 30) なので、 桁が違う気が……
(0x1 << 30)
「古い版では 20 ビットシフト」という情報を入手。 ちなみに、現在辿れる範囲の履歴では、 最初のソースコード追加の時点で 30 ビットシフトだった模様 > hat_sfmmu.h
[12.2.3.6/4]@p.xxx
システム起動処理 startup() を起点にした startup_memlist() → alloc_hmehash() 呼び出しの過程で、 nucleus データ領域にハッシュテーブルが確保されます。 利用可能な物理メモリ量を元に、 MAX_UHME_BUCKETS や MAX_KHME_BUCKETS マクロ定義値を上限として、 ハッシュテーブル領域を確保します。
startup() を起点にした startup_memlist() → alloc_hmehash() 呼び出しの過程で、 nucleus データ領域にハッシュテーブルが確保されます。 利用可能な物理メモリ量を元に、 MAX_UHME_BUCKETS や MAX_KHME_BUCKETS マクロ定義値を上限として、 ハッシュテーブル領域を確保します。
を起点にした startup_memlist() → alloc_hmehash() 呼び出しの過程で、 nucleus データ領域にハッシュテーブルが確保されます。 利用可能な物理メモリ量を元に、 MAX_UHME_BUCKETS や MAX_KHME_BUCKETS マクロ定義値を上限として、 ハッシュテーブル領域を確保します。
startup_memlist()
alloc_hmehash()
現行ソースは原文からかなり乖離している模様:
ndata_alloc_hat()
alloc_hme_buckets()
MAX_NUCUHME_BUCKETS
MAX_NUCKHME_BUCKETS
startup_memlist() 中のコメントによると、 "starcat"/"opl" アーキテクチャの場合、 実行される "FCode" が 32bit 仮想空間しか扱えないので、 追加の初期化を行う plat_startup_memlist() を呼び出しているとの事だが、 "starcat"/"opl" って CPU アーキテクチャ?システムアーキテクチャ?
plat_startup_memlist()
[12.2.3.6/5]@p.xxx 〜 [12.2.3.6/9]@p.xxx
ハッシュテーブルのインデックス参照には、 HAT 構造体アドレス (= アドレス空間を識別)、 当該アドレス空間における仮想アドレス、 およびマッピングページサイズの指定を伴う HME_HASH_FUNCTION() マクロが使用されます。
HME_HASH_FUNCTION()
hme_blk 検索は以下の手順で実施されます。
[12.2.3.6/10]@p.xxx 〜 [12.2.3.6/14]@p.xxx
線形探索補助のためのマクロが3つ定義されています。
HME_HASH_SEARCH
HME_HASH_SEARCH_PREV
HME_HASH_FAST_SEARCH
::sfmmu_vtop -v コマンドを使用することで、 mdb 上でも hme_blk 検索が可能です。
::sfmmu_vtop -v
HME_HASH_SEARCH マクロ定義では、 hblk_vcnt と hblkp->hblk_hmecnt による有効性確認&無効エントリの破棄を行っているので、 "removes empty hme_blk structures from the linked list ..." 記述における "empty" は、 どちらかというと "invalid" な気が……
hblkp->hblk_hmecnt
HME_HASH_FAST_SEARCH は、 「無効要素破棄」の操作がない点で FAST な模様。
[12.2.4/1]@p.xxx 〜 [12.2.4/2]@p.xxx
TLB ミスの都度、HME ハッシュを検索するのは、コストが高いので、 Solaris では TTE 情報を TSB でキャッシュします。 Solaris10 では、プロセス毎に最大2つのTSBに対して、 割り当て、領域拡張/縮小が適宜実施されます。 それぞれの TSB 領域は対応する tsb_info で管理され、 実行中プロセスが使用中の tsb_info の一覧が HAT により管理されます。
tsb_info
各プロセスの hat 構造体の sfmmu_tsb フィールドが、 tsb_info 領域へのポインタを保持しています。
[12.2.4/3]@p.xxx 〜 [12.2.4/13]@p.xxx
tsb_va
tsb_pa
tsb_next
tsb_szc
tsb_flags
tsb_ttesz_mask
tsb_tte
tsb_sfmmu
tsb_cache
tsb_vmp
tsb_info 領域への ::tsbinfo mdb コマンド適用で、 TSB の情報と内容を表示できます。
::tsbinfo
[12.2.4/14]@p.xxx 〜 [12.2.4/18]@p.xxx
プロセス生成時は 8KB TSB を1つ保持し、第2TSBは後になって作成されます。
アドレス空間生成時は、 hat_alloc() → sfmmu_tsbinfo_alloc() → sfmmu_init_tsbinfo() 呼び出しの過程で、 tsb_info 構造体領域が初期化されます。 この時点では、 TSB 領域自体は確保されません。 最初の MMU ミス 〜 sfmmu_tsbmiss_exception() 呼び出しの延長で sfmmu_init_tsbinfo() が呼び出された際に、 実際の TSB 領域が割り当てられます。 以下の条件を満たすため、 TSB は常に境界整合付きで物理連続に配置されます。
sfmmu_tsbinfo_alloc()
sfmmu_init_tsbinfo()
sfmmu_tsbmiss_exception()
ここでの記述も、実装からかなり乖離している模様。
sfmmu_init_tsbinfo() 自体には、 TSB 領域向けの実メモリを kmem_cache_alloc() 等で確保する実装が含まれているが、 hat_alloc() 経由での呼び出しの場合、 割り当てを指示するフラグ TSB_ALLOC が設定されないため、 本文での "At this point, memory for the TSB itself is not allocated" 記述通り、TSB 向け実メモリの割り当ては実施されない。 ----@
TSB_ALLOC
[12.2.4/19]@p.xxx
ページマッピング操作後に、 各ページサイズ毎に TTE エントリキャッシュ数を確認し、 TSB 占有率が一定以上であった場合、 TSB 領域を拡張します。 デフォルト設定では、8KB TSB なら 75% が埋まった時点で拡張することで、 TSB 上での衝突を回避しています。 TSB 拡張の際に、 空きメモリが枯渇 (freemem <= desfree) していたり、 TSB によるメモリ使用が tsb_alloc_hiwater を越えている場合は、 拡張は実施されません。
freemem <= desfree
tsb_alloc_hiwater
"rss factor" 周りの判定は、 実装を見る限りでは、以下の様な処理になっている模様:
8K TSB ページ使用時における 75% 格納状態の TTE 数を、 グローバルな tsb_rss_factor に格納。 この値との比較で超過が検出された時に、改めて TSB 領域拡張の必要性を検証する。
tsb_rss_factor
浮動小数点演算 (* 0.75) の都度実施を回避するためか?
正式な判定処理では、 int 化済みの tsb_rss_factor 値をビットシフトすることで、 各ページ毎の推奨最大エントリ数を算出している模様 (sfmmu_select_tsb_szc())。
sfmmu_select_tsb_szc()
一旦 tsb_rss_factor 越え条件を満たしてしまうと、 以後は TSB へのキャッシュの都度、 sfmmu_select_tsb_szc() での検証が走ってしまう気が……
[12.2.4/20]@p.xxx 〜 [12.2.4/23]@p.xxx
メモリ枯渇が発生している場合や、 TSB によるメモリ使用が tsb_alloc_hiwater を越えている場合は、 ページマッピング解消の際にTSB メモリの回収可否が判定されます。 仮想空間中のページ解放によって、 有効マッピング数 x 2 が tsb_rss_factor を下回った場合、 TSB サイズが縮小されます。 「有効マッピング数 x 2」を境界とすることで、 境界値前後で縮小〜拡張が頻発することを防げます。
プロセス中の 4MB マッピングの使用数が、 tsb_sectsb_threshold を越えた場合、 全ての 4MB マッピングのキャッシュ用に、 第2TSBが作成されます。 tsb_sectsb_threshold 値を大きくすることで、 第2TSBの作成を抑止できます。
tsb_sectsb_threshold
ユーザ空間向け TSB の上限サイズは、 ハードウェアサポートサイズおよび tsb_slab_size を上限に、 tsb_max_growsize で指定されます。 カーネル空間向け TSB の場合は、 ソフトウェア実装を用いることで、 ハードウェアサポートサイズの上限を超えることができます。
tsb_slab_size
tsb_max_growsize
tsb_alloc_hiwater は、 デフォルトでシステム物理メモリの 1/32 となっています。 メモリ枯渇が発生している場合や、 TSB によるメモリ使用が tsb_alloc_hiwater を越えている場合は、 TSB メモリの割り当て処理が減速されます。 後述する DR (dynamic reconfiguration ?) イベントの際には、 physmem や tsb_alloc_hiwater_factor を元に、 tsb_alloc_hiwater が再計算されます。 システムハングを防ぐために、 tsb_alloc_hiwater よりも、 swapfs_minfree や segspt_minfree が十分大きくなるようにしてください。 tsb_alloc_hiwater を小さくするのは、 常に安全に実施できます。
physmem
tsb_alloc_hiwater_factor
swapfs_minfree
segspt_minfree
[12.2.4.1/1]@p.xxx
sfmmu_init_tsbinfo() における TSB 領域の割り当て方法は、 TSB 領域サイズ(と基準ページサイズとの比較)と、 メモリ枯渇状況等を元に、以下の手順で決定されます。
if (ラージページ割り当て(> 8KB)) { if (ラージページ割り当て(> 4MB)) { kmem_bigtsb_default_arena vmem 経由での NOSLEEP 割り当て (kmem_bigtsb_arena 〜 heap_arena ベース/slabsize=256MB) } else { kmem_tsb_default_arena vmem 経由での NOSLEEP 割り当て (kmem_tsb_arena 〜 heap_arena ベース/slabsize=4MB) } } else if (メモリ枯渇 || 強制割り当て要求) { sfmmu_tsb8k_cache からの SLEEP 割り当て(失敗なし/待ちなし?) (static_arena ベース) } else { sfmmu_tsb_cache からの NOSLEEP 割り当て (kmem_tsb_default_arena ベース) }
実際の実装は、 locality group (lgrp) を意識したメモリ割り当てになっているが、 lgrp センシティブの有無を判定する tsb_lgrp_affinity フラグ変数のコメント曰く:
tsb_lgrp_affinity
観測可能な効果が認められないので、デフォルト値は「lgroup 毎分割しない」設定
「色々頑張ってみたけど、性能向上には寄与しなかったよ…… orz 」 ということなのかな?(笑)
[12.2.4.1/2]@p.xxx 〜 [12.2.4.1/6]@p.xxx
TSB 系 arena からの割り当てでは、 長時間のページ割り当て待ちによるブロッキングを回避するために、 NOSLEEP での割り当て要求を行います。
8KB TSB の割り当て(= デフォルトサイズ)に使用される sfmmu_tsb_cache は kmem_tsb_default_arena vmem 経由で、 sfmmu_tsb8k_cache は static_arena vmem 経由で、 8KB 単位の TSB 領域を割り当てます。 sfmmu_tsb8k_cache は「magazine なし」で作成されるので、 TSB 領域解放(= プロセス終了)の際には、 即座にシステムにメモリが返却されます。
sfmmu_tsb_cache
kmem_tsb_default_arena
sfmmu_tsb8k_cache
static_arena
8KB より大きい TSB 領域の割り当て頻度は、 8KB のものより大幅に少ないので、 専用の vmem から直接(= キャッシュなしで)割り当てます。
kmem_tsb_default_arena vmem は、 kmem_tsb_arena 〜 heap_arena 由来のメモリ割り当てを実施します。 多段階層の vmem は無駄に見えるかもしれませんが、 割り当て領域の境界整合のためには、 heap_arena からのではなく、 kmem_tsb_arena を経由する必要があります。
kmem_tsb_arena
heap_arena
割り当てにおける kmem キャッシュ経由の有無に関わらず、 領域割り当て後の処理は、いずれのサイズでも共通です。 割り当てられた TSB 領域の仮想/物理アドレスが、 tsb_info に保持され、 後の TSB 再配置に備えたコールバックが登録されます。
[12.2.4.2/xxxx]@p.xxx 〜 [12.2.4.4/xxxx]@p.xxx
kmem_tsb_default_arena vmem の quantm size は、 メモリフラグメントの最小化と、 ユーザ空間向けの大量の TSB へのアクセスにおける、 TLB ミス発生の最小化のために、 ラージページサイズが選択されます。 sun4u システムでの一般的な値は 4MB です (tsb_slab_size に格納)。 このサイズは、 MMU ハードウェアがサポートするサイズでさえあれば、 他には特に制約はありません。 メモリの少ない環境 (例: 搭載メモリが1GB以下のシステム) でメモリ消費を節約したい場合は、 起動中に (起動の間は?) tsb_slab_size を 512KB に設定してください。 TSB に対するマップ要求は、 最大でも 512KB 以下の筈です。
現時点では、 カーネル空間中でラージページを動的に割り当てるための、 汎用の I/F は提供されていません。 そのため、 kmem_tsb_default_arena vmem 経由で割り当てるための物理メモリは、 下記のラージページ割り当てに特化した I/F 経由で獲得する必要があります。 これらの限定的な I/F は、 ラージページによるマッピングを行います。 下位レイヤとの連携上、 全ての割り当ては、 サイズはページサイズの倍数、 且つページ先頭はページ境界に整合していなければなりません。 これらの I/F は、 安定的に提供されるものではないので、 カーネル内部での汎用メモリ割り当てには適していません。
segkmem_page_create()
sfmmu_tsb_page_create()
segkmem_xalloc()
sfmmu_tsb_xalloc()
sfmmu_tsb_segkmem_alloc()
sfmmu_tsb_segkmem_free
page_create_va()
page_create_va_large()
[12.2.4.3/1]@p.xxx 〜 [12.2.4.3/3]@p.xxx
TSB 領域はカーネル cage の外に確保されるので、 Dynamic Reconfigure (DR) イベントや cage 拡張の際に、 再配置可能でなければなりません。 再配置処理はプロセス実行とは非同期的に実施されるので、 再配置中の物理アドレスキャッシュ経由での TSB アクセスを回避する必要があります。 マッピング保留中(= 再配置中)の仮想アドレス領域へのアクセスは、 フォルト発生で回避されるため、 仮想アドレス経由での TSB アクセスは回避の必要がありません。
再配置の際には、 hat_page_relocate() 経由で pre-relocation および post-relocation のコールバックが呼び出されます。 物理アドレスレベルの再配置の場合は、 トラップハンドラが TSB にアクセスする際に使用する、 locked TTE の更新が必要です。
hat_page_relocate()
TSB 向けの pre-relocation および post-relocation コールバックは、 それぞれ sfmmu_tsb_pre_relocator() と sfmmu_tsb_post_relocator() として定義されています。 例えば、 ISM セグメントの破棄が再配置と並走した場合に、 再配置前の TSB からはエントリが破棄されたのに、 再配置後の TSB にはエントリが残る(⇒ 不正なメモリ領域へのアクセス) といった事態が発生する危険があります。 sfmmu_tsb_pre_relocator() は、 hat_lock 獲得後に、 当該領域が再配置中であることを示す TSB_RELOC_FLAG フラグを tsbinfo 構造体にセットすることで、 このような事態を回避します。 sfmmu_tsb_post_relocator() は、 hat_lock 獲得後に TSB_RELOC_FLAG フラグをクリアします。
sfmmu_tsb_pre_relocator()
sfmmu_tsb_post_relocator()
hat_lock
TSB_RELOC_FLAG
tsbinfo
コールバックの登録は hat_register_callback() で実施。 sfmmu 系以外には、 PCI モジュールが DMA 処理向けにコールバックを登録している (usr/src/uts/sun4u/io/pci/pci_reloc.c)
hat_register_callback()
[12.2.4.4/1]@p.xxx 〜 [12.2.4.4/12]@p.xxx
Solaris 10 の HAT 層は、 プロセスの開始 (exec() ?)、 スワップイン、 TSB の拡張および縮小のすべてを、 同様に (replacement として) 扱います。
exec()
TSB の入れ替えでは、 tsbinfo 構造体と TSB 領域の両方が、 丸々入れ替えられます。 「丸々入れ替える」ことにより、 TSB の入れ替え(や拡張)の間、 競合状態回避のために処理を停止する必要がなくなります。
TSB の入れ替えは、以下の手順で実施されます。
INVALID_CONTEXT
[12.2.4.4/13]@p.xxx 〜 [12.2.4.4/17]@p.xxx
TSB 入れ替えは、 新規の tsbinfo 構造体と TSB 領域の確保で始まります。 デッドロックを避けるため、 排他獲得無しで実施されます。
tsbinfo 管理リスト更新の間、 他のスレッドによるリスト参照や、 要素の追加/削除が発生しないように、 hat_lock を獲得します。 プロセスのコンテキストを INVALID_CONTEXT に設定することで、 TSB 入れ替え中の当該プロセス実行により TSB アクセスが発生しても、 トラップで中断されるようになります。
※ xxxxxx 段落 15 〜 17 は未校
[12.2.4.5/1]@p.xxx 〜 [12.2.4.5/2]@p.xxx
アクセス対象の仮想アドレスから物理アドレスへ MMU による変換ができない場合、 CPU はトラップを発生させます。 データのロード/ストアの場合は fast_data_access_MMU_miss、 命令読み込みの場合は fast_instruction_access_MMU_miss を発生させ、それぞれトラップハンドラ DTLB_MISS() および ITLB_MISS() によって処理されます。
fast_data_access_MMU_miss
fast_instruction_access_MMU_miss
DTLB_MISS()
ITLB_MISS()
DTLB_MISS() は (1) MMU の Tag Access レジスタと、8K ページ向け TSB ポインタレジスタを、 それぞれ汎用レジスタに取り出し、 (2) Tag Access レジスタ値から取り出したフォルト発生元のコンテキストIDを、 (3) INVALID_CONTEXT と比較します。 (4) 一致した場合、カーネル向け TLB ミス処理である sfmmu_kdtlb_miss() に分岐します。 INVALID_CONTEXT なコンテキストでのトラップ発生は、 特殊ケースとして扱います。
sfmmu_kdtlb_miss()
INVALID_CONTEXT なコンテキストでのトラップ発生が、 特殊ケース扱いなのは、 前節で説明されている TLB Replacement との繋がり。
[12.2.4.5/3]@p.xxx
ユーザプロセスでのトラップ発生時は、 (5) 8K ページ向け TSB ポインタレジスタの最上位ビットを確認し、 (6) ビットが立っている (= 2nd TSB が存在する) 場合は sfmmu_udtlb_slowpath() に分岐します。 (7) それ以外の場合、 フォルトアドレスを元に、 TSB タグと対応する TTE を TSB から(アトミックに)取り出し、 (8) Tag Access レジスタが保持するフォルト発生元アドレスと TSB タグを比較し、 (9) 一致していたなら TSB ヒットとみなし、 TSB から得た TTE を dTLB に格納した上で、 (10) フォルト発生元の命令を再実行します。 (11) それ以外の場合は、 sfmmu_tsb_miss() を実行します。
sfmmu_udtlb_slowpath()
sfmmu_tsb_miss()
「8K ページ向け TSB ポインタレジスタ」の値は、 「TSB 領域の先頭を参照するレジスタ」(TSB Regiter) と 「フォルト発生元に関する情報を保持するレジスタ」(Tag Access) から自動的に合成される。 つまり、フォルト発生元アドレスを使って、 TSB 領域をインデクシングする手間を、 ハードウェア的に低減している。
コメント続き
MMU レジスタのアクセスは、 ASI (Address Space Identifier) を使ったアクセスで実現される。 以下は、 "UltraSPARC User's Manual - UltraSPARC-I/-II" で明記されている、 MMU レジスタアクセスに使用される UltraSPARC 固有の MMU 関連 ASI 定義の抜粋。 各レジスタの詳細は "6.9 MMU Internal Registers and ASI Operations" を参照のこと。
from "8.3.2 UltraSPARC (Non-SPARC-V9) ASI Extensions" ----+---------------------------+-----------+--+--------------------------- ASI |name |addr |RW| ----+---------------------------+-----------+--+--------------------------- 0x58|ASI_DMMU 0x00 |R |Tag Target Register 0x58|ASI_DMMU 0x08 |RW|Primary Context Register 0x58|ASI_DMMU 0x10 |RW|Secondary Context Register 0x58|ASI_DMMU 0x18 |RW|Synch. Fault Status Register 0x58|ASI_DMMU 0x20 |R |Synch. Fault Address Register 0x58|ASI_DMMU 0x28 |RW|TSB Register 0x58|ASI_DMMU 0x30 |RW|TLB Tag Access Register 0x58|ASI_DMMU 0x38 |RW|VA Data Watchpoint Register 0x58|ASI_DMMU 0x40 |RW|PA Data Watchpoint Register ----+---------------------------+-----------+--+--------------------------- 0x59|ASI_DMMU_TSB_8KB_PTR_REG 0x00 |R |TSB 8K Pointer Register 0x5A|ASI_DMMU_TSB_64KB_PTR_REG 0x00 |R |TSB 64K Pointer Register 0x5B|ASI_DMMU_TSB_DIRECT_PTR_REG 0x00 |R |TSB Direct Pointer Register 0x5C|ASI_DTLB_DATA_IN_REG 0x00 |W |TLB Data In Register 0x5D|ASI_DTLB_DATA_ACCESS_REG 0x00..0x1F8|RW|TLB Data Access Register 0x5E|ASI_DTLB_TAG_READ_REG 0x00..0x1F8|R |TLB Tag Read Register 0x5F|ASI_DMMU_DEMAP 0x00 |W |TLB demap 6.9.10 ----+---------------------------+-----------+--+---------------------------
[12.2.4.5/4]@p.xxx 〜 [12.2.4.5/8]@p.xxx
ITLB_MISS() は DTLB_MISS() と同様に振る舞いますが、 以下の様な点が異なります。
sfmmu_kitlb_miss()
exec_fault()
sfmmu_uitlb_slowpath()
[12.2.4.5/9]@p.xxx 〜 [12.2.4.5/11]@p.xxx
Kernel TLB Miss Handling sfmmu_kdtlb_miss() は、 フォルト発生元の仮想アドレスをインデックスに使って、 1st TSB から 8KB/64KB/512KB マッピングの引き当てを行います。 2nd TSB が存在する(可能性のある)場合のみ、 2nd TSB からの引き当てが実施されます。
kpm_vbase
本文の説明は非常に回りくどいが、 要するに「kpm_vbase よりもアドレス高位には、 segkpm 以外にカーネルページのマッピングは行わない」 と仮定している、と考えると話は早い。
kpm_vbase の値は、 SPARC64 Olympus アーキテクチャや、 UltraSPARC III 系が 0x80000000.00000000 を使用している一方で、 同じ sun4u 系でも spitfire アーキテクチャ (UltraSPARC I/II 系)は、 0xfffffa00.00000000 を使用している。 Solaris Internal の Appendix A に掲載されている、 カーネルアドレス空間図(図A.2)では、 segkpm の位置として後者の値が使われており、 本文との整合性が取れていない(笑)
ちなみに、 sun4v 系アーキテクチャでは、 基本的には 0x80000000.00000000 が使用されているが、 利用可能仮想アドレス領域に hole が存在する Niagra アーキテクチャ向けに、 hole 領域を回避するコードが仕込まれている模様 (usr/src/uts/sun4v/os/fillsysinfo.c 参照)
[12.2.4.5/12]@p.xxx 〜 [12.2.4.5/13]@p.xxx
segkpm 領域に対する TSB ミスの場合、 segkpm でのラージページ使用状況に応じて、 sfmmu_kpm_dtsb_miss_small() あるいは sfmmu_kpm_dtsb_miss() へと分岐します。 それ以外の TSB ミスは、sfmmu_tsb_miss() で処理されます。
sfmmu_kpm_dtsb_miss_small()
sfmmu_kpm_dtsb_miss()
非 nucleus 領域(= TLB ロックされない) の命令格納ページは 8KB ページでマッピングされるため、 sfmmu_kitlb_miss() は、 1st TSB からの引き当てのみを行います。 TSB ヒットの場合は、 実行権限のチェックと、iTLB への TTE 格納が実施されます。 それ以外の場合は、 sfmmu_tsb_miss() で処理されます。
[12.2.4.5/14]@p.xxx 〜 [12.2.4.5/16]@p.xxx
Multiple TSB Probes Solaris 10 以降から、 各プロセスは最大2つの TSB 領域を保持します。 sun4u アーキテクチャでは、 1st TSB は 8KB ページ向けエントリのみを格納しますが、 複数の 8KB ページ向けエントリを束ねることで、 64KB/512KB ページ向けエントリの同時管理を実現しています。 2nd TSB には 4MB ページ向けエントリが格納されますが、 UltraSPARC IV+ 等の場合は、 4MB ページ向けエントリを複数束ねることで、 32MB/256MB ページ向けエントリの同時管理を実現しています。 将来的により大きな TSB をソフトウェアのみでサポートする可能性はありますが、 sun4u アーキテクチャが提供する MMU 支援機能の都合上、 1st TSB のサイズは 1MB に制限されています。
TLB トラップ処理時の最短経路では、 TSB 8KB ポインタレジスタの最上位ビットにより、 2nd TSB 存在の有無を判定しています。 通常のケースでは 2nd TSB が存在しないので、 このレジスタ値はそのまま使用可能です。 2nd TSB が存在する場合は、 分岐先の sfmmu_udtlb_slowpath() ないし sfmmu_uitlb_slowpath() において、 GET_1ST_TSBE_PTR() および GET_2ND_TSBE_PTR() マクロを使って、 2つの TSB 領域中のそれぞれにおける、 フォルト発生元アドレスに対応するエントリのアドレスを算出します。 slow-path ルートでは、 1st TSB → 2nd TSB の順で引き当てを行い、 共に引き当てができなかった場合は、 sfmmu_tsb_miss() で処理されます。
GET_1ST_TSBE_PTR()
GET_2ND_TSBE_PTR()
Solaris の 64bit プロセスでは、 ISM セグメントは 8GB よりも高位の仮想アドレスに配置される可能性が高いです。 フォルト発生元アドレスが 8GB よりも高位アドレスで、 且つ最上位ビットが立っていない場合の DTLB ミスは、 ISM ページにおける可能性が高いと推測し、 TSB からの引き当ては、 2nd TSB (for 4MB) → 1st TSB (for 8KB) の順で実施されます。
同一 TSB 内で、 複数サイズのページマッピングの管理を行う点や、 ISM ページ向けの引き当て順序逆転に関しては、 "9.10.6 HAT Support" においても言及されている。
64bit ユーザ空間における、 8GB より高位アドレスのセグメント配置に関しては、 図 9.4 も参照のこと。
----@
[12.2.4.6/1]@p.xxx 〜 [12.2.4.6/2]@p.xxx
sfmmu_tsb_miss() は、 (1) TLB にも TSB にもキャッシュされていない 「仮想→物理」対応情報をマッピング情報から検索する以外に、 (2) 権限フォルトやページフォルトの一部、 (3) INVALID_CONTEXT での TLB ミスの処理を行います。
TSB ミスの処理の間のキャッシュミスを回避するために、 CPU 毎の tsbmiss 領域が使用されます。 各 tsbmiss 領域は、 TSB ミス処理に必要な情報(の複製)の格納や、 一時作業用の変数格納領域として使用する tsbmiss 構造体を保持します。 ハッシュテーブル中の TTE 引き当てには、 GET_TTE() マクロが使用されます。
GET_TTE()
GET_TTE() の定義は、 ガッツリ書かれた SPARC アセンブラのマクロ (usr/src/uts/sfmmu/ml/sfmmu_asm.s) なので、 あまり細かい話は突っ込まないでください(笑)
[12.2.4.6/3]@p.xxx 〜 [12.2.4.6/4]@p.xxx
ISM セグメント以外でのフォルト時は、 当該プロセスの tsbmiss 領域から読み込まれた hat 構造体アドレス (as hatid) と、 8KB/64KB のマッピング検索を示す TTE64K (as hasno) が、 GET_TTE() に指定されます。 HME ハッシュ上に該当する 8KB/64KB のマッピングがない場合、 512KB のマッピング検索を意味する TTE512K (as hasno) を使って、 再度 GET_TTE() が実施されます。
hatid
TTE64K
hasno
TTE512K
512KB 以上のマッピングに関しては、 当該プロセスの tsbmiss 領域中の HAT フラグ情報 (uhat_tteflags または uhat_rtteflags) を使って、 仮想空間中の当該ページサイズマッピングの有無を確認した上で、 必要な場合のみ GET_TTE() を実施することで、 ユーザプロセス向け TSB 処理を最適化しています。 該当するマッピングが見つからない場合は、 サポート対象ページサイズの上限まで、 上記の手順を繰り返します。
uhat_tteflags
uhat_rtteflags
本文の記述だと、 512KB マッピングのみが特別扱いされるかのような記述だが、 より大きなページでのマッピングの検索に対する "as above" は、 「プロセス空間中の当該ページサイズによるマッピングの有無の確認」 も含んだ "as above" な模様。
対象のアドレス空間中に、 「あるページサイズのマッピングが存在する」からといって、 「フォルト発生元アドレスに対応する、当該サイズのマッピングが見つかる」 とは限らない。
uhat_tteflags または uhat_rtteflags の情報更新は、 以下の位置等で実施される。
[12.2.4.6/5]@p.xxx 〜 [12.2.4.6/7]@p.xxx
ISM セグメントでのフォルト時は、 ISM 用の hat 構造体アドレス (as hatid) と、 セグメント内オフセット値 (as tagacc) が、 GET_TTE() に指定されます。 ISM セグメントは、 4MB ページサイズ (or それ以上?) 向けに最適化されているので、 HME ハッシュの検索は、 サポート対象ページサイズの上限から、 降順で実施されます。
tagacc
HME ハッシュの検索により、 有効なマッピングが見つかった場合は、 TSB_UPDATE_TL() によって TSB 中のエントリが更新されます。 マッピングサイズが 4MB 未満の場合は 1st TSB が、 4MB 以上の場合(且つ既に存在するなら) 2nd TSB が更新対称になります。 最終的には TLB が更新された上で、 フォルト発生元の命令を再実行します。
TSB_UPDATE_TL()
UltraSPARC IV+ の ITLB ミス処理は、 32MB/256MB のデータページへの命令列書き込み+実行を行うプログラム (e.g. Java) のために、 4MB ページを使ったエミュレーションで 32MB/256MB ページをサポートします。 TSB_UPDATE_TL_PN() は、 生成した 4MB の PFN (Page Frame Number) ビットを、 TSB 中の 32MB/256MB ページ向け TTE に保存します (4MB PFN オフセットビットはハードウェアにより無視)。
TSB_UPDATE_TL_PN()
UltraSPARC IV+ ハードウェア自体は 32MB/256MB ページをサポートしていない? ⇒ Oracle SPARC Processor Documentation によると、 UltraSPARC IV+ 自体は 32MB/256MB の TTE サイズをサポートしているっぽいが、 UltraSPARC Architecture 2005 では 32MB (+ 512KB) が Reserved 扱いになっている模様。
[12.2.4.6/8]@p.xxx 〜 [12.2.4.6/10]@p.xxx
HME ハッシュでの検索で該当エントリが見つからない場合、 トラップレベルに応じて挙動が異なりますが、 DTLB_MISS() と ITLB_MISS() での違いはありません。
カーネルでの TLB ミスの場合、 トラップレベルが 1 以下ならば、 カーネル処理中のカーネル空間におけるフォルトなので、 sfmmu_pagefault() で処理されます。 トラプレベルが 1 より大きいならば、 ptl1_panic() により PANIC 処理が実施されます。 スタック領域でのフォルトの場合も、 ptl1_panic() により PANIC 処理が実施されます (スタック不正によりトラップハンドラが正常に機能しないと思われるため)。
sfmmu_pagefault()
ptl1_panic()
ユーザ空間での TLB ミスの場合、 トラップレベルが 1 より大きいならば、 レジスタウィンドウのオーバーフロー/アンダーフロー処理中の TLB ミスなので、 sfmmu_window_trap() で処理されます。 トラップレベルが 1 ならば、 sfmmu_pagefault() で処理されます。
sfmmu_window_trap()
CPU_DTRACE_NOFAULT フラグが立っている場合、 実際のフォルト処理を行うのではなく、 フォルト要因 (CPU_DTRACE_BADADDR 等) をフラグに設定し、 フォルト発生元命令は「実行完了」(done) 扱いになる = 「再実行」(retry)しない。
CPU_DTRACE_NOFAULT
CPU_DTRACE_BADADDR
CPU_DTRACE_BADADDR 以外にも、 以下のような要因フラグが定義されている。
@usr/src/uts/common/sys/cpuvar.h #define CPU_DTRACE_BADALIGN 0x0008 /* DTrace fault: bad alignment */ #define CPU_DTRACE_DIVZERO 0x0010 /* DTrace fault: divide by zero */ #define CPU_DTRACE_ILLOP 0x0020 /* DTrace fault: illegal operation */ #define CPU_DTRACE_NOSCRATCH 0x0040 /* DTrace fault: out of scratch */ #define CPU_DTRACE_KPRIV 0x0080 /* DTrace fault: bad kernel access */ #define CPU_DTRACE_UPRIV 0x0100 /* DTrace fault: bad user access */
[12.2.5/1]@p.xxx 〜 [12.2.5/3]@p.xxx
自身の仮想アドレス空間中に、 何らかの共有メモリセグメントを配置しているプロセスは、 共有対象の物理メモリをアドレス空間にマップするためのページテーブル構造体を、 プロセス毎に保持しなければなりません。 特定のメモリセグメントを共有するプロセスは、 マッピング情報を格納した sf_hment 構造体の内容が同一であるにも関わらず、 メモリセグメント共有のために、 hme_blk および sf_hment 構造体を、 各プロセス毎に管理しているのです。 多くの商用データベースソフトのような、 巨大な共有メモリセグメントを大量のプロセスが共有するようなケースでは、 このような手法はカーネルメモリを無駄遣いしてしまいます。 この問題を解決するために、 共有先プロセス間でページテーブル情報を共有する、 Solaris は Intimate Shared Memory (ISM) を提供しています。 ISM の基本的な部分に関しては、 4.4.2 節により詳細な説明があります。
異なるアドレス空間で hme_blk を共有するには、 HME ハッシュチェーンでの hme_blk 検索用に、 (共通で且つ)一意な tag 情報が必要になります。 しかし、 tag 情報 (= hmeblk_tag) は、 hatid (= hat 構造体アドレス)、 仮想アドレスおよびページサイズを元に構築されます。 各プロセスは、 それぞれ異なるアドレスに共有セグメントを配置できるため、 tag 情報生成における共通の情報は、 ページサイズしかありません。 この問題を解決するために、 プロセス毎の hat 構造体の代わりとして、 ISM セグメント毎のダミー hat 構造体アドレスを、 仮想アドレスの代わりに、 ISM セグメント内でのオフセット値を利用して、 tag 情報を構築します。
各プロセスの仮想空間における ISM セグメントのマッピングは、 ism_ment 構造体で表現され、 対応する ISM hat を共有(= 同一 ISM セグメントを共有) する全てのプロセスの hat がリンクされます。 page 構造体の p_mapping が、 当該ページをマッピングする sf_hment のリンクに使用されるのと同じ構図です。 あるプロセスが ISM セグメントを配置する場合、 当該プロセスが利用する各 ISM セグメントの管理情報である ism_blk の配列が、 hat から参照参照されます。
ism_ment
ism_blk
[12.2.5/4]@p.xxx 〜 [12.2.5/7]@p.xxx
ISM は2つのセグメントドライバ segspt および segspt_shm によってサポートされます。 ユーザプロセスに配置された各 ISM セグメントにはそれぞれ1つの segspt_shm セグメントインスタンスが、 各 ISM 空間にはそれぞれ1つの segspt セグメントインスタンスが存在します。 2つのプロセスが1つの ISM 空間を共有する単純なケースでは、 各プロセスは segspt_shm を1つずつ保持し、 これらは ISM マッピングのためのメモリ/スワップ情報を管理している、 同一の segspt セグメントを参照します。
segspt
segspt_shm
shmat は 当該 ISM に対応する segspt セグメントの割り当てを確認し、 必要であれば sptcreate() を経由して as_alloc() (当該 ISM のための as および hat の確保) と、 as_map() (segspt セグメントの確保、 および ISM 空間中の SEGSPTADDR = 0 アドレス位置へのセグメントの配置) が呼び出されます。 segspt_create は、 vnode を確保した上で、 ISM 空間に物理メモリを割り当てるために anon_map_createpages() を呼び出します。
shmat
sptcreate()
as_alloc()
as_map()
SEGSPTADDR
segspt_create
vnode
anon_map_createpages()
anon_map_createpages() は 指定されたアドレス領域に対して、 可能な限り大きなページサイズの anonymous 向けの(物理)メモリ領域を割り当てます。 次に呼び出される hat_memload_array は、 ISM 空間に割り当てられた物理ページが、 SEGSPTADDR 位置へのマッピングを行うために、 マッピングに対応する hme_blk を HME ハッシュチェーンに追加します。 hat_memload_array には、 hblk_lckcnt の使用を指示するための HAT_LOAD_LOCK フラグと、 ISM 使用が禁止されていない範囲で最大サイズのページの使用を指示するための HAT_LOAD_SHARE フラグが指定されます。
hat_memload_array
hblk_lckcnt
HAT_LOAD_LOCK
HAT_LOAD_SHARE
ISM 領域でのラージページ使用の可否は、 disable_ism_large_pages フラグで制御します。 1 (= ビット 0 が 1)は「全ラージページの使用禁止」を、 ビット 1 〜 5 はそれぞれ 64KB/512KB/4MB/32MB/256MB の使用禁止に相当します。 ISM 向けに 512KB ページが禁止されていない場合、 ユーザプロセスによる 512K ページへのアクセスで、 不定期にページフォルトが発生するでしょう。
disable_ism_large_pages
ISM で 512KB ページがサポートされない理由が見当たらない。 2nd TSB のサポート範囲である over 4MB サイズページで運用した方が効率が良い、 というだけの話? であれば 64KB ページに関しても言及されてしかるべきな気が……
[12.2.5/8]@p.xxx 〜 [12.2.5/9]@p.xxx
その後、 shmat() は segspt_shm セグメントインスタンスを確保した上で、 ユーザプロセス空間中のマッピング先アドレスと、 segspt_shmattach() 関数を伴って、 as_map() を呼び出すことで、 ユーザプロセス空間に当該セグメントを配置します。 segspt_shmattach() は、 セグメントのプライベートデータ領域 shm_data を確保/初期化した上で、 ism_blk 領域の確保および hat へのリンクを行う hat_share() を呼び出します。 マッピング先アドレス指定が 0 だった場合、 shmat() は妥当な空き領域を割り当てます。 64bit アドレス空間の場合、 ISM セグメントは PEDISM_BASE 〜 PREDISM_BOUND (= 2^33 〜)の範囲に配置されることが推奨されます。 アドレス変換を効率化するために、 このアドレス範囲を対象セグメント特定のヒントとして使用します。 但し、このアドレス範囲は推奨レベルであって、 この範囲の外に ISM セグメントが配置される可能性もあれば、 この範囲に非 ISM セグメントが配置される可能性もあります。 ISM セグメントとプロセスのヒープ領域の衝突を回避するために、 shmat() は可能であれば、 PREDISM_1T_BASE より高位のアドレスに ISM セグメントを配置するようにします。
shmat()
segspt_shmattach()
shm_data
hat_share()
PEDISM_BASE
PREDISM_BOUND
PREDISM_1T_BASE
segspt セグメントは、 最大で availrmem - segspt_minfree までのメモリを割り当てます。 ISM でのメモリ消費が availrmem の 90% を越えると、 システム性能が低下する可能性がありますが、 大量にメモリを積んだ環境では、 より多くのメモリを使用可能です。 そのため、 segspt_minfree は、 ISM 領域へのページ割り当て完了後でも、 システムに残るページ数で、 sptcreate() の初回起動時に その時点での availrmem の 5% に設定されます。 この挙動は、 明示的な segspt_minfree 設定で回避できます。
availrmem - segspt_minfree
availrmem
"D"ISM の "D" は、Dynamic を意味する模様。 Solaris9 から導入された DISM は、 ISM の機能に加えて「動的サイズ変更」(拡張/解放の両方)などが可能になった模様。
availrmem は 「現時点で利用可能な pagable 物理メモリページ数」なので、 上限算出には availrmem_init あたりを使用した方が良い気が……
availrmem_init
[12.2.6/1]@p.xxx 〜 [12.2.6/2]@p.xxx
HAT 層が正しく機能するために、相当量の同期機構が必要とされます。 新規コンテキストのためのTLBフラッシュ待ちや、 利用可能コンテキスト一覧の保護、 ハンドラによる TSB アクセス中の TSB 破棄の防止、 別スレッドによる TSB 物理メモリの移動待ちなどで、 同期機構が使用されます。
Solaris 10 の HAT 層における同機構は、 スケーラビリティを向上させ、 ボトルネックを極小化すると同時に、 複雑さを低減するなど、 それ以前の版のものよりも大幅に改善されています。
[12.2.6.1/1]@p.xxx 〜 [12.2.6.1/3]@p.xxx
hat_lock は、 アダプティブ(adaptive) な mutex lock である kmutex_t の配列で、 ユーザプロセスの hat 構造体アドレスによる hatid のハッシュ値で参照されます。 sfmmu_flags との組み合わせにより、 旧来の hat.sfmmu_mutex による排他を置き換えています。
kmutex_t
hat.sfmmu_mutex
TSB のマップ/アンマップ操作や、TSB の移動、 TSB のフラッシュ(整合性維持のため)等は排他される必要があるため、 hat_lock による HAT 層における tsbinfo リストの変更保護は便利です。
HAT 層での hat_lock による同期は、 sfmmu_hat_enter で開始され、 sfmmu_hat_exit で終了します。 前者が返却する hatlock_t ポインタは、 後者の呼び出しに使用されます。
sfmmu_hat_enter
sfmmu_hat_exit
hatlock_t
[12.2.6.1/4]@p.xxx 〜 [12.2.6.1/6]@p.xxx
キャッシュ不可一時 (TNC: Temporary NonCacheable) エントリ の設定のような、 仮想アドレスキャッシュがフラッシュされるケースでは、 どのアドレス空間が影響を受けるのか、事前には分かりません。 排他順序の制約上、 仮想アドレスキャッシュ情報を管理する下位層関数の呼び出し前に、 sfmmu_hat_enter() を呼び出す必要が有ります。 旧実装での ctx_lock 相当の実現には、 全 hat_lock 要素に対する一括排他/解放が必要ですが、 ユーティリティとして sfmmu_hat_[un]lock_all() が提供されています。
sfmmu_hat_enter()
ctx_lock
sfmmu_hat_[un]lock_all()
あるプロセスに関する hat_lock 排他獲得が、 他のプロセス向けの処理に対するボトルネックにならないように、 完了までに時間を要する処理 (例: TSB 物理メモリの移動) は、 hat や tsbinfo 中のフラグを使うことで、 排他待ちによる処理の停止を回避しています。
プロセス間での共有セグメントのマップ/アンマップを同期する新機構として、 hat_lock の上位に sfmmu_ismhat_enter が位置します。 (広い領域を使用する)ISM セグメントのアンマップなどは、 完了までに長時間を要することから、 排他競合による不要な CPU 消費を招かないように、 sfmmu_flags 操作を操作します。
sfmmu_ismhat_enter
TNC 云々のあたりは、翻訳に自信無し……orz
hat_lock 配列のサイズは、 ソース埋め込みマクロ値による 128 固定であるため、 一定規模以上のシステムにおいてはプロセス間での hat_lock エントリ共有が無いとは言えないが、 全てのプロセスで ctx_lock を共有するような、 単一ボトルネックになるよりはマシ、 という判断なのだろう。
sfmmu_ismhat_enter() 処理自身が、 「フラグによるボトルネック回避」の典型例で、 HAT_BUSY フラグ検出時は、 他のスレッドによりフラグがクリアされるのを cv_wait() で待ち合わせる。 この場合、当該スレッドは sleep 状態になるため CPU は消費されない。
sfmmu_ismhat_enter()
HAT_BUSY
cv_wait()
[12.2.6.2/1]@p.xxx 〜 [12.2.6.2/2]@p.xxx
排他順序に関する重要な点として、 hat_lock を必要とする操作が呼ばれる前に、 マッピングリストロック (= mml_table) が獲得されるケースが多々あることが上げられます。 hat_lock 獲得状態でのカーネルメモリ割り当ては、 デッドロックを生じてしまいますので、 全てのメモリ割り当て処理は hat_lock 獲得無しで実行される必要があります。
mml_table
カーネル空間向け sfmmu_t に対しては、 sfmmu_hat_enter や sfmmu_hat_exit は NOP 相当の挙動になるため、 ユーザプロセス向けの hat_lock 獲得状態から、 上記のようなデッドロックが発生する心配はありません。
[12.2.6.3/1]@p.xxx 〜 [12.2.6.3/2]@p.xxx
旧来の Solaris では、 あるプロセスのコンテキストIDを横取りして他のプロセスで再利用する際には、 同一TSBエントリが予期せず複数のプロセス間で共有されないように、 コンテキストIDが一致するTSB中の全てのエントリを一旦破棄 (= unmapping)する必要があります。 コンテキストID再利用時向けに、 TSBエントリ一括破棄機能が提供されていました (= TSBエントリ毎への破棄操作の実施は不要)。
アドレス空間毎 TSB を採用している現行 Solaris では、 TSB エントリにはコンテキストID情報が格納されませんので、 コンテキストIDの再利用に伴う、 TSBエントリの一括破棄処理は必要ありません (そのための機能提供もありません)。 この手法は、 コンテキストIDの再利用における、 大幅な処理の削減になります。 ページフォルトの延長で、 アドレス空間に対してコンテキストIDが割り当てられますが、 それまでに TSB にキャッシュされた TTE 情報はそのままで、 処理が継続されます。 些少なトレードオフではありますが、 コンテキストの活性によらず、 TSB エントリに対する破棄操作は、 エントリ毎に実施する必要があります。
12.2.3.1 The Translation Table Entry での考察でも触れているが、 HAT 層で言うところの「コンテキストID」は、 以下の要領で「コンテキストID+世代番号」で管理されている。
このため、 「コンテキストIDが再利用される」=「TLB の全エントリ破棄が実施済み」となり、 原文で言うところの "the invalid context (= 再利用される既存のコンテキスト) is never allowed to have TTEs in the TLB" であることが保証される。
[12.2.6.3/3]@p.xxx 〜 [12.2.6.3/11]@p.xxx
現行実装では、 以下のケースで TSB エントリが破棄されます。
以下のケースでは、TSB 中の全エントリが破棄されます。
TSB エントリ全破棄を実現する sfmmu_inv_tsb (Invalidate TSB) は、 VIS (Visual Instruction Set) 命令によるブロック転送を使う実装になっています。 VIS 命令の利用は、 TSB エントリの破棄性能の大幅な向上と同時に、 level-1 キャッシュのデータを無効化するという、 顕著な副作用も持っています。 TSB の全エントリ破棄は、 ISM 領域のアンマッピングや、 TSB 領域の初回割り当ての際に実施されますが、 アドレス空間の解放時には実施されません。 そのため、 プロセス終了時や exec() における貴重なCPU消費を、 低減させることができます。
sfmmu_inv_tsb
VIS 命令は「FPU がある場合のみ利用可能」らしいが、 「FPU が無い SPARC」実装は有り得る(予定されていた)のか?
「FPU trap 処理中等で、FPU が使用できないケースに対応」の可能性も? ⇒ 初期化処理以外での代入処理は見当たらない。 アセンブラ実装も ld のみで st 実施は見当たらない。
SPARC T1 などは、 「4 thread/core x (4, 6 or 8) コア」を搭載するプロセッサに対して、 全コアで1つの FPU を共有する形態なので、 実質的に使用できない(すべきではない?)と考えた方が良いのかも? 「T1 の頃は使えないものがあった」⇒「使えない」の意味が違うのでは?(笑) T2 は各コア毎に FPU が搭載された模様。
※ 表のみなので割愛
[12.2.8/1]@p.xxx
カーネル統計情報 sfmmu_tsbsize_stat は TSB サイズ割り当てに関しての、 sfmmu_global_stat は HAT 層に関しての統計情報を保持しています。
sfmmu_tsbsize_stat
sfmmu_global_stat
[12.3.1/1]@p.xxx 〜 [12.3.1/2]@p.xxx
新しい x64 プロセッサは、 OS のメモリ管理向けに、 「セグメンテーション」と「ページング」の2つの方式を提供していますが、 Solaris におけるカーネルとアプリケーションのメモリ分離による保護には、 セグメンテーションではなくページテーブルモデルを採用しています。 メモリ管理における基本的なページサイズは 4KB です。 「ページング」モード有効化後は、 全てのメモリ参照におけるアドレスが、 ページテーブルを介して仮想アドレスから物理アドレスに変換されます。 制御レジスタ CR3 が、最上位のページテーブルアドレスを参照します。 仮想アドレス中の各ビットが、 物理アドレス引き当てのためのページテーブル参照に使用されます。
現行の x64 プロセッサ実装では、 64bit 仮想アドレスのうち、 48 〜 63 ビットは「全ての 0」あるいは「全て 1」でなければなりません。 この領域は利用不可能な hole 領域ですが、 何TBにも及ぶ十分な仮想アドレス空間が利用可能です。
[12.3.1/3]@p.xxx 〜 [12.3.1/7]@p.xxx
Solaris の仮想アドレス管理で使用される個々の機能や構成は、 プロセッサ種別や実行モードで異なります。
Solaris のソースコード中では、 ページテーブルの各レベルに対する Intel/AMD 固有の呼称を使用せず、 レベル番号の 0 (= Page Table), 1 (= Page Directory), 2 (= Page Directory Pointer), 3 (= Page Map) を使用しています。
[12.3.2/1]@p.xxx 〜 [12.3.2/7]@p.xxx
Solaris では、 単一の struct mmu 型変数に対して、 MMU の構成情報が起動時に格納されます。
struct mmu
pt_nx
pt_global
highset_pfn
num_level
max_level
num_level - 1
max_page_level
ASSERT(max_level <= max_page_level)
[12.3.2/8]@p.xxx 〜 [12.3.2/13]@p.xxx
ptes_per_table
top_level_count
pae_hat
pte_size
hole_start
hole_end
pte_size_shift
pte_size == 1 << pte_size_shift
[12.3.2/14]@p.xxx 〜 [12.3.2/17]@p.xxx
level_size[n]
level_shift[n]
1 << level_shift[n] == level_size[n]
level_offset[n]
level_size[n] - 1
level_mask[n]
~level_offset[n]
[12.3.3/1]@p.xxx 〜 [12.3.3/3]@p.xxx
64bit 環境では、 アプリケーションのトップレベルページテーブル中の数エントリを、 カーネル空間ページテーブルと同じ値で初期化することで、 アプリケーション/カーネル間で仮想空間を共有していますが、 ページテーブルエントリ (PTE) の権限ビットにより、 アプリケーションからのカーネル空間へのアクセスは抑止されています。 更に、PTE 中で Global Bit が利用できる場合は、 全てのカーネルマッピングで当該ビットを設定することで、 アプリケーション間のコンテキスト切り替えの際に、 カーネル空間向けの TLB エントリが破棄されることを防ぎます。
32bit 環境では、 物理メモリ量に応じて page_t 領域を確保する必要があるため、 物理メモリを多く搭載している程、 カーネル空間とユーザ空間の境界はアドレス低位に位置します。
page_t
64bit 環境では、 全てのローダブルカーネルモジュールを収めるための、 2GB のカーネル向け text/data 用領域 ("core heap") を確保しています。
「Global Bit 利用可能」=「カーネル空間の TLB エントリは固定」?
カーネル向け領域とユーザ向け領域の境界は、 kernelbase 変数で参照可能。
kernelbase
"32bit" 環境への言及は、PAE モード込みの話なので、 4GB 超の物理メモリへの配慮が含まれている。
64bit 環境での toxic 領域のサイズは、 一応 toxic_size で管理されているので、 理論上は初期化時可変(現在は固定値 1GB で初期化)。
toxic_size
[12.3.3/4]@p.xxx 〜 [12.3.3/5]@p.xxx
64bit 環境では、 I/O デバイスの制御レジスタをマッピングするために、 toxic_addr から始まる 1GB () を確保しています。 32bit 環境では、 仮想アドレス空間サイズの制限があるため、 I/O デバイスマッピング領域は、 ビットマップで管理しています。
toxic_addr
64bit 環境では、 segkpm (Kernel Physical Mapping セグメントドライバ) によって、 全物理メモリがマッピングされた仮想アドレス領域が提供されるています。 kpm_vbase に物理アドレスを追加した値を使うことで、 対応するメモリ領域に、仮想アドレス経由で簡単にアクセスできます。 ページテーブルエントリ (PTE) 量を低減させるため、 segkpm は可能な限り大きなページサイズでマッピングします。 32bit 環境では、 仮想空間サイズが制限されるため、 必要に応じて一時的な仮想アドレスマッピングを生成する必要があります。
32bit 環境での toxic 領域の管理の "stricter virtual address limitations" は、 「まとめてマッピングする余裕がないから、 ヒープ領域中に散在している状況をビットマップで管理する」 というニュアンスっぽい (usr/src/uts/i86pc/os/startup.c 中のコメントで言及)。
usr/src/uts/i86pc/os/startup.c
kpm_vbase 自体は、 SPARC 環境でも定義されている。
[12.3.4/1]@p.xxx
カーネルの text および data が最初に読み込まれます。 初期化処理の早期の段階で、 カーネルメモリアロケータ等が必要とする他のデータと共に、 初期 page_t 向け領域が割り当てられます。 Solaris カーネルが利用する AMD64 メモリアドレスモデルでは、 全てのカーネル text が単一の 2GB 領域に収まっている必要があるため、 ローダブルカーネルモジュール向けに core heap 領域が確保されます。 segkpm による物理メモリの直接マッピングや、 巨大なカーネルヒープの確保等で、 Solaris に十分なメモリを提供しつつ、 64bit アプリケーションに利用可能な最大の仮想メモリが割り当てできるように、 このメモリ配置が選択されました。
アドレス空間配置の詳細は、 usr/src/uts/i86pc/os/startup.c 中のコメントでも参照可能。
[12.3.5/1]@p.xxx 〜 [12.3.5/2]@p.xxx
Linux と異なり、 Solaris のカーネル text/data は、 アドレス高位に配置されます。 この配置にした場合、 kernelbase をアドレス低位に下げれば、 カーネルイメージを再リンクせずに、 多くの物理メモリを搭載した環境でも、 必要な page_t 領域を確保することが可能です。
32bit アプリケーションが 64bit カーネル上で動作する場合は、 0xFE000000 以下の空間は全てアプリケーションによって使用されます (最初期の x64 プロセッサのいくつかは、エラッタ回避のために、 0xC0000000 以下を使用します)。 この場合、32bit アプリケーションは 32bit Solaris 上での実行よりも、 約 1GB 余分に使うことができます。
「最初期の x64 プロセッサのいくつか」は、 OPTERON_ERRATUM_95 で識別される模様 (usr/src/uts/i86pc/os/mp_startup.c 参照)。
usr/src/uts/i86pc/os/mp_startup.c
[12.3.6/1]@p.xxx 〜 [12.3.6/4]@p.xxx
x64 での HAT 層の主要なデータ構造は以下の通りです
struct htable
hat_ht_hash
struct hment
hat_t における struct htable ハッシュ管理のフィールド名は、 誤: hat_htable ⇒ 正: hat_ht_hash
hat_htable
x86 向けは hment_t、 SPARC 向けは sf_hment。
hment_t
[12.3.6.1/1]@p.xxx 〜 [12.3.6.1/4]@p.xxx
hat_pages_mapped
hat_vlp_ptes
64bit (又は 32bit PAE) 環境の場合、 32bit ユーザプロセスに対しては、LEVEL 0/1 のページテーブルのみを生成します。 CPU 毎に用意された上位レベルのページテーブルエントリから、 hat_t の hat_vlp_ptes に複製されます。 この手法により、 32bit プロセス毎の 4K x 2 ページ消費を抑止できます。 32bit プロセスの場合は、 hat_vlp_ptes が LEVEL 2 PTE に相当します。
LEVE 0/1 のページテーブルの網羅範囲が 30bit = 1GB なので、 32bit プロセスのために必要な LEVEL2 PTE 数 = VLP_NUM_PTES は 4 つ。
VLP_NUM_PTES
LEVEL 2 ページテーブルは、 vlp_page (@usr/src/uts/i86pc/vm/hat_i86.c) 領域を、 システムワイドで共有。 各 CPU 上で実行中の HAT (= プロセス)が切り替わる都度、 対応する CPU 専用の領域が、 hat_vlp_ptes 中の PTE で上書きされる。
vlp_page
LEVEL 3 の "per-cpu set of upper-level page table" は、 起動時 (or CPU online 時) に hat_vlp_setup() で確保される。 各 CPU 上で実行中の HAT が切り替わる都度、 レジスタ CR3 がこの領域を参照するように上書きされる。
hat_vlp_setup()
詳細は hat_switch() @usr/src/uts/i86pc/vm/hat_i86.c を参照。
hat_switch()
[12.3.6.2/1]@p.xxx 〜 [12.3.6.2/5]@p.xxx
必要なページテーブル毎に、 管理用の htable 構造体が存在します。 特定のアドレスマッピングに対応する htable は、 ::vatopfn と ::pte dcmd の組み合わせで特定可能です。
htable
::vatopfn
::pte
::pte コマンドは、 ページテーブルエントリ (PTE) の内容(ビットフィールド等)を、 人が読める形式で表示します。
実行例では、 仮想アドレス 0xffffffff80aa8000 が物理アドレス 0x7fc78000 にマップされ、 global/writable なカーネルページであることがわかります。
物理ページから、 当該ページを利用中の HAT (= 仮想空間)を参照する場合、 ::report_maps コマンドを使用します。
::report_maps
実行例では、 物理ページ 7fc78 (ページサイズが 4K = 0x100 なので 0x7fc78000) が、 0xffffffff8067f438 に存在する htable に対応するページテーブル領域であることがわかります。 当該ページは、0xfffffe80bf800000 (segkpm によるマッピング) から始まる空間と、 0xfffffe80aa800000 から始まる空間からもマッピングされています。
[12.3.6.2/6]@p.xxx 〜 [12.3.6.2/12]@p.xxx
ht_next
ht_hat
ht_pfn
ht_vaddr
ht_level
ht_valdi_cnt
ht_parent
「主要フィールド」扱いはされていないが、 逆向きのリンク参照用の ht_prev も定義されているので、 ハッシュテーブルからのリンクは双方向。
ht_prev
構造体定義上、ht_next と ht_prev が離れて定義されているのは、 「アクセス時のキャッシュ効率への配慮 (= 単なる参照では prev へのアクセスが稀)」なのかな?と思ったら、 やはり「htable_lookup() の高速化上の配慮」とのこと。
htable_lookup()
@usr/src/uts/i86pc/vm/htable.h * The fields have been ordered to make htable_lookup() fast. Hence, * ht_hat, ht_vaddr, ht_level and ht_next need to be clustered together.
[12.3.6.2/13]@p.xxx 〜 [12.3.6.2/14]@p.xxx
通常のページテーブル管理では、 仮想アドレスではなくページフレーム番号 (PFN) を使用しつつ、 仮想アドレスでのアクセスの必要に応じて、 一時マッピング(32bitカーネル)か segkpm (64bitカーネル) を使用します。 ページテーブルの PFN がわかっている場合は、 MDB の :ptable コマンドでページテーブルの内容を参照可能です。
:ptable
実行例では、 有効なエントリに対して、 インデックス値、 マッピング対象の仮想アドレス、 PTE 値、 PTE 内容の可読形式が表示されます。 実行例のものは、 カーネルヒープ領域のマッピングなので、 全てのマッピングにおいて、 グローバルビットと NX ビットが立った状態になっています。
[12.3.6.3/1]@p.xxx 〜 [12.3.6.3/4]@p.xxx
HAT では、 与えられた物理ページから、 マッピング先のアドレス空間/仮想アドレスを引き当てる情報も管理しています。 メモリ消費低減のため、 マッピング情報が page_t に直接埋め込まれる場合があります (以下 PFN 0x6f3c8 での例)。
p_embed
p_mlentry
p_embed は x86 系に固有のフィールド。 SPARC 系固有フィールドの p_vcolor (10章の "10.2.7 Page Coloring" 参照) と排他的に定義される。
p_vcolor
[12.3.6.3/5]@p.xxx 〜 [12.3.6.3/6]@p.xxx
複数のマッピングが存在する場合、 マッピングの引き当てにリンクリストを使用するために、 先述のフィールド値は異なる用途で用いられます。
p_embed が 0 の場合、 p_mapping は hment_t 構造体を参照します。 hment_t 構造体は、 マッピングに対応するページテーブルを管理する htable (hm_htable) と、 当該ページテーブル内でのインデックス値 (hm_entry) を保持します。 同一物理ページを複数マッピングしている場合、 双方向リンクで管理されます。 多くの物理メモリは単一マッピングであるため、 マッピングに関する情報の引き当ては、 page_t から直接得ることができます。
hm_htable
hm_entry
SPARC HAT の場合、 p_mapping は常に sf_hment へのリンクを参照している (図 12.5 等を参照) が、 "most memory in processes are not shared" は SPARC では成立していないのか?
あるいは 「TSB 充填の関係上、仮想⇒物理マッピングに対応する sf_hment が必ず必要」な SPARC に対して、 「ページテーブルが、 仮想⇒物理マッピング管理のマスター情報」な x86 系は、 複数マッピング時等で必要に迫られない限り、 マッピングに対応する hment_t 領域は確保されない? それなら、 冒頭で "to save memory usage" と言及しているのも納得が行く。
[12.3.6.4/1]@p.xxx 〜 [12.3.6.4/6]@p.xxx
未使用物理メモリを管理するリストは、 過去の PC アーキテクチャにおける DMA 対象範囲に応じて、 4つのアドレス範囲に分割されています。
DMA 範囲が限定されるデバイスへは、 対応する範囲のメモリが割り当てられます。 それ以外の場合は、 可能な限り上位領域のメモリが割り当てられます。 DMA 向けに物理メモリがカーネル空間にマッピングされたなら、 以後の DMA に対する allocate()/free() 処理において、 I/O システムは高速化のために 2^N (16MB, 32MB, 64MB...) 単位でメモリを管理します。
allocate()
free()
0 〜 16MB は、80286 + ISA 時の DMA コントローラ (8237) での制約由来か? ネット上では「FD 等のサポート向けに必要」との言及も見られるが……
32bit 空間が、 0 〜 2GB と 2GB 〜 4GB に区切られているのは、 ネット上で「31bit アドレスしか認識できないドライバ or ファーム」 の存在に関する言及がみられるデバイス (RAID カード系) への対応のためか?
ここで言う「アドレス」は、物理アドレスの話? 未使用物理メモリを管理する freelist (実際は page_freelists) 周りを見るに、 サイズベースでの分類はしていても、 アドレスベースでの分類をしているようには見えないのだが……
freelist
page_freelists
MDB に関する説明なので割愛