※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( FUJIWARA Katsunori )
受託開発主体の独立系ソフトウェアハウス数社を経て、現在フリーランス。
前職で、 HPC ( High Performance Computing ) 系システムのために Solaris 向けファイルシステムを実装したのを機に、 OpenSolaris 勉強会に参加。
「lsを読まずにプログラマを名乗るな!」、 「入門TortoiseHg+Mercurial」とか 「俺のコードのどこが悪い?―コードレビューを攻略する40のルール」、 「アセンブラで読み解くプログラムのしくみ」(電子書籍) といった書籍の執筆や、 技術系ウェブ媒体への記事の寄稿も。
ホームページ以外にも、 はてなダイアリー (id:flying-foozy) 等で情報発信中。 Twitter アカウント (@flyingfoozy) は細々と運用中。
本資料は、 "Solaris Internals" の第9章を、 各段落毎に「ワンフレーズ」化すること基本としています。
「ワンフレーズ」化対象の段落を識別するために、 以下のような識別情報表記を使用します。
[{章/節番号}/{通し段落番号}]@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" を元にしています。
なお、個人的に興味深いと思った点に関しては、 勝手に掘り下げた話を展開します。
[11/1]@p.xxx
カーネルメモリの、 使途、アドレス空間の構成、および割り当て・管理に関して説明します。
[11.1/1]@p.xxx 〜 [11.1/2]@p.xxx
カーネルも通常プロセス同様に、 仮想メモリを使い、 MMU による仮想⇒物理のアドレス変換を行います。 カーネル専用のアドレス空間があり、 Solaris の標準的なメモリアーキテクチャフレームワークを用いた、 メモリセグメントから構成されます。
カーネルがメモリに関する処理を実施している最中に、 カーネル自身のアドレス空間でページフォールトが発生した場合、 デッドロックが発生してしまうため、 大半のカーネルメモリはページアウト不可 ("wired down") になっています。 但し、ライトウェイトプロセススレッドのスタック領域など、 デッドロックを生じ得ない部位には、 ページアウト可能なメモリ領域を使用可能です。
カーネルスレッド生成 API thread_create() のコメントによると、 "If stk is NULL, the thread is created at the base of the stack and cannot be swapped" とのこと。 つまり、ページアウト可能なカーネルメモリ領域上に別途確保しておいた領域を、 新規スレッドのスタック領域として明示的に指定する必要があると思われる。
thread_create()
「ライトウェイトプロセススレッドのスタック領域」 をページアウト可能にするためには、 別途「ページアウト可能なメモリ」を用意する必要がある、 というのは、微妙に入れ子な表現な気が……
Solaris 以前の SunOS では、 現在で言う "thread" のことを "light weight process" と呼称していたけど、 POSIX による thread 仕様の策定以降も、 随所で "light weight process" の呼称は使用されている模様。 一応 "thread" と同義で使用されているものと思われる。
なお、ユーザ空間向けに、 Solaris スレッド (light weight process 由来?) 用の thr_xxxx 系 API と、 POSIX スレッド用の pthread_xxxx 系 API が用意されているが、 どちらで生成した thread も OS 上の資源としては同じ扱い (usr/src/lib/libc/port/threads/thr.c 中の _thrp_create() に集約)。
thr_xxxx
pthread_xxxx
usr/src/lib/libc/port/threads/thr.c
_thrp_create() に集約)。
に集約)。
[11.1/3]@p.xxx
カーネルメモリ空間は、 複数の物理⇒仮想マッピングを持っていて、 階層化されたメモリアロケータによって割り当てが行われます。 カーネルメモリ空間の殆どは、 ページアウト不可メモリを扱う segkmem と、 ページアウト可能メモリを扱う segkp の、 2つのセグメントドライバによって管理されます。 ラージページをサポートしている環境では、 ハードウェア TLB の効率を上げるために、 カーネルの最頻利用領域を、 4MB のラージページでマッピングした領域に読み込みます。
"a layered series of kernel memory allocators" に関しては、 "11.2 Kernel Memory Allocation" (特に 11.2.3 The Kernel Memory Slab Allocator) で踏み込んだ説明がある。
[11.1.1/1]@p.xxx 〜 [11.1.1/8]@p.xxx
カーネルアドレス空間の配置は、 MMU アーキテクチャ毎に異なります。 x86 (= 32bit) や、sun4u より前の SPARC アーキテクチャでは、 仮想アドレスの上位 256MB 〜 512MB を、 カーネルとプロセスが共有する配置であったため、 カーネルの利用可能なメモリ量が 256MB 〜 512MB に限定されていました。 sun4u アーキテクチャでは、 カーネルは独自のアドレス空間を持つため、 稼働環境の仮想アドレス全域をフルに使用できます。
カーネルアドレス空間における主要なマッピングには、 以下の様なものがあります。
unix
カーネルアドレス空間の配置は、 プラットフォーム固有です。
"shared by the process and kernel" に関する説明は、 9.4 節ではなく、9.2.2 〜 9.2.3 (図 9.3 と 図 9.5) を参照。
[11.1.2/1]@p.xxx 〜 [11.1.2/2]@p.xxx
カーネルアドレス空間の text や data 向けセグメントは、 ブートローダによるカーネル起動時の、 実行可能ファイルとしての unix からの .text (命令コード)や .data (初期化済みデータ) 読み込みの際に作成されます。
カーネルの base code の読み込み/実行を行うために、 一般的なカーネル初期化処理に先立って、 Oepn Boot PROM によってカーネルアドレス空間への .text や .data のマッピングが実施されます。 カーネルのロード後間も無く、 カーネル自身によってカーネルアドレス空間が作成され、 segkmem セグメントドライバによって、 カーネルの .text や .data 向けセグメントが作成されます。
私自身は、 各種環境におけるブートプロセスに関しては、 詳しくない(&出来れば関わりたくないw)ので、 補足していただけると助かります > 識者
[11.1.2/3]@p.xxx 〜 [11.1.2/6]@p.xxx
MMU のラージページサポートがある環境では、 カーネルの .text/.data 向けセグメントの冒頭 4MB を、 ラージページでマッピングし、 MMU の TLB エントリをロックします。 TLB エントリの使用数が大幅に減少するため、 以下の理由から大幅に性能が改善されます。
SPARC プラットフォームでは、 .text 冒頭にトラップテーブルが配置されるため、 これもラージページでマッピングされます。
昨今の多機能 OS からすると、 "4MB" のラージページ x 1 の空間は、 あまり広くない印象があるかもしれないが、 大多数の機能が loadable kernel module として実現されているため、 実行可能ファイルとしての unix のサイズは、実は数MBしかない。
例えば、AMD64 仮想環境の oi151 の unix ファイル (/platform/i86pc/kernel/amd64/uni) は、 ファイルサイズ上なら 2MB、.text セクションに限定すれば 800KB 程度。
/platform/i86pc/kernel/amd64/uni
[11.1.3/1]@p.xxx
稼動環境の HAT (Hardware Address Translation) 層向けの仮想メモリ管理データは、 カーネルのデータ向けセグメント(あるいは独立したメモリセグメント) に保持されます。
x86 の "a separate VM data structure's segment" には、 ラージページな旨が明記されていないので 4K ページ? もしかして x86 MMU の制約由来とか? まぁ、今時のメモリ使用効率にうるさいケースでは、 AMD64 だろうから別に良いのだけれどw
[11.1.4/1]@p.xxx
sun4u カーネルでは、 TLB エントリの入れ替えを、 ソフトウェアで実装する方式を採用していることから、 TLB ミス処理中に必要な全てのデータを乗せておく、 TLB ミス無しにアクセス可能なメモリ領域が必要とされます。 TLB エントリの入れ替えに使用されるデータは、 TSB (Translation Storage Buffer) と呼ばれるソフトウェアバッファから取得され、 TSB 領域は TLB マッピングが wired-down されていなければなりません。 SPARC V8 や V9 は、 nucleus と呼ばれる特別なメモリコアを実装しています。 sun4u アーキテクチャのシステムでは、 ラージページ上に配置された、 カーネルのテキスト/データ、 および付加的な TSB エリアが nucleus となります。
"a special core of memory, known as the nucleus" というのは、 特別なアドレス空間識別子 (Address Space Identifier: ASI) によるアドレス空間のこと。
スーパーバイザ実行時の暗黙の ASI である 0x09 (Supervisor Instruction) と 0x0B (Supervisor Data) (on V8)、 あるいは ASI_NUCLEUS (on V9) が nucleus 向けと思われる。
[11.1.5/1]@p.xxx
sun4u 環境では、 カーネルの map セグメント (segmap = ファイルの cache 領域) に読み込まれたローダブルカーネルモジュールの text/data に対して、 最大 256KB までは、 カーネルの text/data と同じセグメント上 (ラージページマッピング) に領域を割り当てます。
ファイルキャッシュ用の segmap は、 8K (4K on x86/AMD64) の物理ページで構成されることから、 (1) 通常の VOP_READ() 経由でファイルシステムから読み込んだ後で、 (2) 改めてカーネルのラージページマッピングされたセグメント上に「ロード」 している、ということだと思われる。
256KB 超のローダブルカーネルモジュールの text/data に対しては、 各プラットフォーム毎に割り当て挙動が異なる。
x86 の「独立したメモリセグメント」は、 Table 11.2 での「HAT 向けデータが独立セグメントに配置」 記述との突合せから導出されたもの。 Solaris Internals 原文では、 AMD64 と x86 に関する記述は同一内容となっている。
11.1.3 のテーブル 11.1 では、 AMD64 の HAT 向けデータは、 「カーネルのデータ向けセグメントと共有」とあるが、 Table 11.2 と付き合わせると、 AMD64 は「カーネルの text/data 向けセグメント/HAT 向けデータ/ ローダブルカーネルモジュールの text/data を同一セグメントでまかなう」 ということか? そうなると "up to 256KB" という上限に関する表現との矛盾が……
それとも "Virtual Memory Data Structures (required for the platform's HAT implementation)" と "HAT data structures" は別物?
[11.1.5/2]@p.xxx 〜 [11.1.5/4]@p.xxx
カーネルの text/data 向けセグメントに読み込まれたモジュールは、 modinfo コマンドを使うことで見つけることが出来ます。
modinfo
modinfo コマンドが出力する Loadaddr 情報から、 初期に読み込まれたモジュールが、 カーネルの text セグメント (ラージページ領域) に読み込まれていることが読み取れます。
カーネルの text/data 向けセグメントから溢れたモジュール分は、 32bit オフセットで収まるような専用のセグメント (segkemem32) に読み込まれます。 Solaris のカーネルは、 命令アドレスを 32bit で表現できるように、 64bit 版でも ABS32 フラグ付きでコンパイルされているためです。 ABS32 モードにより、 64bit カーネルでも 32bit カーネル並みの性能が維持できます。
命令アドレスが 32bit の範囲に収まることが保証できない場合、 関数コールや、分岐命令、switch のアドレステーブル等で、 64bit 幅を意識する必要があるため、 命令フェッチ等のコストが増えてしまう (分岐予測のコストも?)。
[11.1.5/5]@p.xxx 〜 [11.1.5/9]@p.xxx
Solaris では、 ページアウト可能領域からのカーネルへのメモリ割り当ても可能ですが、 プロセスコンテキストに直結したデータ向けの領域に限定されます。 プロセスがスワップアウトしている間、 プロセスコンテキストに直結したデータも、 スワップアウトできる (= 必要とされない)ためです。
ページアウト可能ページは、 segkp セグメントから割り当てられ、 メモリスケジューラがアクティブな場合のみスワップアウトされます。
Kindle 版だと、 なぜか 10.3.6 のリンクで 10.2.5 に飛ばされる…… orz
[11.1.6/1]@p.xxx 〜 [11.1.6/2]@p.xxx
カーネルアドレス空間は、 kas (kernel address space) 変数が参照するアドレス空間オブジェクトと、 セグメントを構成するセグメントドライバ (のインスタンス) で構成されます。
kas
カーネルアドレス空間で使用されるセグメントの多くは、 プラットフォーム特性に応じて、 手動で算出されたアドレス (= 値が機種依存ヘッダファイル中に hard code) に配置されます。
[11.2/1]@p.xxx 〜 [11.2/2]@p.xxx
Solaris カーネルにおけるメモリ割り当ては重層化されていて、 最下層のページアロケータ (page allocator) は、 未使用リスト中の未マッピングページを割り当て、 カーネルのアドレス空間にマッピングします。
例えば inode データ構造のように、 ページサイズ (SPARC なら 8KB、x86 なら 4KB) 未満の領域要求に対して、 ページ丸ごと割り当てるのは、 資源効率がよろしくありません。 Solaris では、 ページ単位のメモリアロケータ層の上位に、 オブジェクト単位のメモリアロケータ層が設けられています。 また、 各ページのマップ先管理のために、 リソースマップアロケータも提供されています。
図 11.3 では、 オブジェクト単位アロケータ (Slab Allocator) が、 segkmem セグメントに対して segkmem_getpages を直接発行しているが、 実際のコードでの「ページ獲得処理」は、 もう少し抽象化されている模様。
segkmem_getpages
[11.2.1/1]@p.xxx 〜 [11.2.1/2]@p.xxx
1ページ毎に page_create_va() を呼び出すことで、 ページアロケータから獲得した物理ページは、 カーネルの仮想アドレス空間にマッピングしなければ、 (通常の意味で)使うことはできません。 「カーネルヒープ」と呼ばれる、 カーネルアドレス空間中の領域は、 多目的なマッピング群で構成されています。
page_create_va()
カーネルヒープは、 独立した巨大な領域を持つセグメントで、 個別の用途毎に、 専用のメモリマッピング用仮想アドレス領域を割り当て可能です。 仮想アドレスの割り当て状況は、 vmem アロケータを使って管理してますが、 vmem アロケータ自身は万能な資源アロケータなので、 他の資源割り当て管理にも使用されています。
Slab Allocator (usr/src/uts/common/os/kmem.c)や vmem allocator (usr/src/uts/common/os/vmem.c) の実装ファイルのコメントには、 ZFS 実装のリーダーであった Jeff Bonwich 氏の名前が記載されている。
Wikipedia での記載 (英語版) では、 Slab アロケータの実装が、 Jeff Bonwich の業績として ZFS と併記されていることから、 界隈では知られた話である模様。
ZFS における Copy-on-Write でのブロック割り当ても、 カーネルにおけるページ/アドレス空間割り当ても、 資源割り当て的な観点では似たようなものなのかも(笑)
[11.2.2/1]@p.xxx 〜 [11.2.2/2]@p.xxx
segkmem セグメントドライバの主な用途は、 カーネルアドレス空間におけるメモリセグメントの生成と、 それらのセグメントの1つ (kernel map セグメント) を使った、 ページ単位のメモリアロケータとしての機能です。
「セグメントドライバ」としての segkmem は、 カーネルアドレス空間における、 ページアウト不可な多目的メモリ向けのセグメントを生成します。 また、HAT 層と連携して、 セグメントに対する保護設定の操作機能も提供します。
[11.2.2/3]@p.xxx 〜 [11.2.2/5]@p.xxx
ページ単位のメモリアロケータとしての segkmem ドライバは、 リソースマップアロケータと、 ページアロケータの組み合わせによって実現されています。 実装のメインとなる kmem_getpage() は、 ページアウトされないメモリの、 ページサイズ単位での割り当て機能を、 カーネル内で集約したものです。
kmem_getpage()
kmem_getpages() は、 kernelmap セグメントから、 ページサイズの領域を割り当てます。 kernelmap セグメントは、 segkmem ドライバによって生成されるセグメントの1つに過ぎませんが、 segkmem は kernelmap セグメントからしかメモリを割り当てません。
kmem_getpages()
vmem アロケータは、 仮想アドレス空間の一部を kernelmap セグメントから割り当てますが、 物理メモリの割り当ては行いません。 マップ済み物理メモリの割り当てには、 仮想アドレス空間を割り当てる vmem アロケータ以外に、 物理ページを割り当てる page_create_va() と、 指定アドレス空間に物理ページをマップする hat_memload() が併用されます。
hat_memload()
[11.2.2/6]@p.xxx
kmem_getpages() によって割り当てられたページは、 ページアウト不可であることに加えて、 論理的な vnode との対応が無いという、 Solaris における数少ない例外的なページです。 これらのページの識別には、 特別な vnode である kvp が使用されます。
anonymous page における、 swapfs との擬似的な対応関係を考えると、 仮想的な kvp との対応は、 十分 "logically associated vnode" な気がするのだけれど……
anonymous page と違って not pageable だから、 「最終的に実 vnode との関連性を持たない」 という意味での「例外」ということなのだろうか?
[11.2.3/1]@p.xxx
「slab アロケータ」として知られる汎用メモリアロケータについて、 最初に概要を説明した上で、 オブジェクトキャッシュや、 内部実装に関する詳細に関して、 順次説明していきます。
[11.2.3.1/1]@p.xxx 〜 [11.2.3.1/9]@p.xxx
Solaris が提供する、 「任意サイズのメモリ領域のアロケータ」は、 巨大なメモリの slab ("厚切り", "一切れ" 等の意) から、 小さなサイズのメモリを割り当てることから、 「slab アロケータ」と呼びます。 slab アロケータは、以下のようなメモリ割り当て要求を扱います。
slab アロケータは、 以下の特徴を持つ SVR4 UNIX の buddy アロケータを置き換えるために、 Solaris 2.4 から導入されました。
前世紀には、 SVR4 libc の malloc() の応答性能がよろしくないとのことで、 小規模 (128 とか 256 未満) のメモリ割り当て要求に対して chunk 管理してある領域から切り出す実装方式の、 BSD libc の malloc() を移植する仕事をしたことも。
malloc()
確か SVR4 libc の malloc() も、 デカイ領域から言われた順に領域を切り出す方式 (= 断片化が起き易い)だったような……
ちなみに、当時の BSD libc の malloc() には、 (確か)管理ヘッダ部分にバイトオーダ依存な実装があって、 そのままのソースをコンパイルしただけでは、 SPARC 上で動かない、などということも。
[11.2.3.1/10]@p.xxx 〜 [11.2.3.1/11]@p.xxx
slab アロケータの導入は、 散在していた類似コードの集約によって3,000行が削減されるなど、 システムの複雑さを劇的に減少させました。
SVR4 の buddy アロケータに対して、 slab アロケータは相当に高速です。
正直 "net reduction of 3,000 lines" は、 「スゲー!」という程ではない気もするが(笑)、 当時の Solaris の感覚では、かなりの規模だったのかな?
[11.2.3.1/12]@p.xxx 〜 [11.2.3.1/20]@p.xxx
slab アロケータは、 以下に列挙するような、 重要な付加的機能性も提供しています。
[11.2.3.1/21]@p.xxx 〜 [11.2.3.1/22]@p.xxx
slab アロケータでは、 「メモリ割り当ての最小単位」を object、 「object のプール」を cache、 「cache 内部の object のグループ」を slab と呼んでいます。 各 object 種別ごとに、 cache が 1 つ、 1 つ以上の slab が存在します。
異なるサイズの object が、 別々の cache に分類されることで、 断片化問題の多くが解決されます。 object 毎に cache が存在するので、 Solaris カーネル中では、 複数の cache が同時に運用されています。
個々の slab が(仮想アドレス的に)連続した page 群から構成されているのは、 object サイズと page サイズの境界整合を取るためか?
個々の slab を構成する「連続した page 群」は、 (1) あくまで仮想アドレス的に連続しているだけで、且つ (2) 必ずしも初期時点で全ての領域が使用可能である必要は無いので、 実際に物理ページが割り当てられている領域は、 歯抜け状態になっている筈 (そうでなければ、未使用領域に割り当てられた物理ページは、 完全に無駄になってしまう)。
→ 「page サイズと object サイズの最小公倍数なメモリ領域を確保して、 使用効率を目一杯上げる」的な頑張りは行わない模様。 詳細は "11.2.3.7 The Global (Slab) Layer" 参照。
[11.2.3.1/23]@p.xxx 〜 [11.2.3.1/24]@p.xxx
アロケータのフロントエンド側は object の割り当てを行い、 バックエンド側は slab を構成する page の提供をおこないます。 通常のバックエンドは、 kmem_getpages() による物理ページ割り当てですが、 それ以外にも、 任意のメモリ割り当てバックエンドが使用可能です。
cache は、 各 object 毎に kmem_cache_create() で作成され、 kmem_cache_destroy() で破棄されます。 統計情報採取やデバッグに便利なように、 cache には生成時に文字列で名前が指定されます。 cache からの object 割り当てには kmem_cache_alloc() を、 cache への返却には kmem_cache_free() を使用します。
kmem_cache_create()
kmem_cache_destroy()
kmem_cache_alloc()
kmem_cache_free()
名前を使った cache の情報採取に関しては、 以降の節で都度詳細を説明。
[11.2.3.2/1]@p.xxx 〜 [11.2.3.2/3]@p.xxx
slab アロケータは、 object の初期化・終了処理の実施を遅延させることで、 object の割り当て〜解放が頻発する状況において、 初期化・終了処理による時間消費を回避します。
割り当てられたメモリ領域= object が保持する内容は、 概ね (1) 内容に関するヘッダや解説 (description) と排他情報、 (2) そして実際のデータ (payload) の2つです。 メモリの割り当てを受けた際には、 ヘッダの書き込みや、リストへの追加、排他情報等を初期化した後に、 実際の用途に使用します。 メモリの使用を終える際には、 これらの情報をクリア(or 解放・破棄)した後に、 アロケータにメモリを返却します。 端的に言えば、メモリ使用のサイクルは、 割り当て、初期化、使用、終了処理、返却の順になります。
slab アロケータは、 メモリ解放の際には、 単に object に「未使用」マーキングを施すだけで、 初期化済みデータはそのまま維持し(= object のキャッシュ)、 メモリ割り当ての際には、 新規の object とキャッシュされてる初期化済み状態の object のいずれかを返却します(= 初期化処理の回避)。 object に対する終了処理は、 アロケータがメモリ(= page)をバックエンドに返却する (= cache の収縮)時点まで遅延されます(= 終了処理の回避)。 このような手法によって、 object が頻繁に割り当て〜解放される場合に、 初期化・終了処理に多くの時間が消費されることを防ぎます。
"the construction and deconstruction is only done when the cache needs to grow or shrink" とあるが、 "construction" に関しては、 cache の拡張(= slab の追加等)があっても、 個別の object の割り当て時点までは初期化処理が遅延される筈なので、 "only done when the cache needs to grow" という表現は微妙な気が……
これらの段落は、 逐語的に読んでいると理解しづらいので、 翻訳は大胆に順序の入れ替え等をしている。
[11.2.3.2/4]@p.xxx 〜 [11.2.3.2/6]@p.xxx
slab アロケータが、 object の初期化・終了処理実施の主導権を握るため、 kmem_cache_create() による生成時に、 初期化・終了処理関数が slab アロケータに渡されます。 初期化・終了処理関数は、 必要に応じて slab アロケータによって適宜実行されます。
初期化・終了処理関数が指定されない場合は、 単なる生メモリ領域の割り当て管理を行うことになります。
メモリの割り当て管理に関する諸々が、 メモリ利用側から slab アロケータに集約されました。 カーネルのメモリ枯渇を受けて、 slab アロケータが cache を収縮させる場合は、 未使用 object に終了処理を適用した上で、 未使用の slab (= 連続した page 群)をバックエンドに返却します。 kmem_cache_create() による生成時に、 メモリ枯渇を割り当て要求側に通知するための、 コールバック関数も指定できます。
[11.2.3.2/7]@p.xxx 〜 [11.2.3.2/8]@p.xxx
object として inode を使用する ufs 実装は、 slab アロケータ利用の良い例です。 初期化・終了処理関数を指定することで、 これらの実施は slab アロケータ側にゆだねられ、 ファイルシステム側は新規 inode が必要になる都度、 割り当て要求を実施するだけで済みます。
本文コード例での kmem_cache_create() 呼び出しの各引数は、 (1) "inode_cache" を名前に持つ cache は、 (2) object サイズが inode 構造体と同一で、 (3) object 毎の境界整合は不要、 (4) 初期化処理、(5) 終了処理、 (6) メモリ枯渇通知処理が指定されていて、 (8) バックエンドの page アロケータが、 デフォルトの segkmem であることを意味しています。
inode_cache
inode
"no alignment enforcement" とは言っても、 ポインタ等を内包する場合は 8 バイト境界整合が必要な筈 → 最小の境界整合として KMEM_ALIGN (= 8) を保証
KMEM_ALIGN
本文で解説されていない引数は、それぞれ (7) 初期化・終了・枯渇通知実施の際に渡される "private data" ポインタ、 (9) 実行制御用のフラグ引数。
[11.2.3.2/9]@p.xxx 〜 [11.2.3.2/11]@p.xxx
kstat コマンドを使うことで、 ufs 実装での inode 使用状況を見ることができます。
kstat
アロケータの API(KPI?) を表 11.7 に示します。
kmem_cache_create() での cache 生成時に指定可能な、 初期化・終了・メモリ枯渇通知のコールバックの概要を、 表 11.8 に示します。
ufs の inode 向け cache は、 作法に従った「礼儀正しい」初期化・終了処理を指定しているが、 nfs の rnode 向け cache は、 初期化・終了処理を自前(= 割り当て要求側)で実施し、 slab は単なる「生メモリの割り当て管理」にしか使用していない。
入り組んだ構造体の場合、 管理情報のクリアのような「ゼロ埋め」と、 排他・状態変数のような「固有の初期化」の混在があるので、 お作法通りの実装だと実行性能的な面で不利なのかも? (構造体のフィールドが、 利用ユースケースごとに分類されていないと、 キャッシュメモリが無効化されてしまうため、 高負荷時に性能が低下するケースも)
nfs の rnoe 管理は、 独自に LRU キャッシュを実装しているので、 初期化・終了処理の頻発は、一応回避出来ている筈。
[11.2.3.3/1]@p.xxx
(特定のキャッシュと強く結びついた) オブジェクトベースのメモリ割り当て以外にも、 任意サイズの領域獲得要求に対応する、 汎用のメモリ割り当て機能を提供しています。 kmem_alloc() 呼び出しは、 近傍サイズの cache を用いた割り当て要求により実現されています。
kmem_alloc()
::kmastat で表示される、 初期化済みキャッシュ一覧中に、 kmem_alloc_DDDD 形式の名前を持つキャッシュが列挙されている。
::kmastat
kmem_alloc_DDDD
[11.2.3.4/1]@p.xxx 〜 [11.2.3.4/2]@p.xxx
slab アロケータの実装では、 大規模な SMP システムような環境でも、 複数のスレッドからの割り当て要求が競合することなく、 slab から object を効率的に割り当てることができるように、 複数の内部レイヤを設けています。
CPU レイヤは、 各 CPU 毎に object をキャッシュすることで、 (slab を内包する) cache 全体を排他せずに、 object を割り当て・解放することができます。 並走する複数の(CPU 上で動作する)スレッドは、 お互いの排他によって待たされることがありません。
あくまで "witout having to hold a lock on the global slab cache" であって、 CPU 毎のキャッシュ管理情報へのアクセス時には、 きっちり排他を掛けている。
/* @usr/src/uts/common/os/kmem.c */ kmem_cache_alloc(kmem_cache_t *cp, int kmflag) { kmem_cpu_cache_t *ccp = KMEM_CPU_CACHE(cp); : mutex_enter(&ccp->cc_lock); : :
/* @usr/src/uts/common/sys/kmem_impl.h */ #define KMEM_CPU_CACHE(cp) \ ((kmem_cpu_cache_t *)((char *)(&cp->cache_cpu) \ + CPU->cpu_cache_offset))
メモリ枯渇時に、未使用 slab を刈り取るようなケースで、 CPU 毎の割り当て・解放処理が並走する可能性があるため、 「排他無し」での実装は無理。
[11.2.3.4/3]@p.xxx
slab アロケータは、 下層から順に slab、depot (貯蔵庫)、CPU の3層に分かれていて、 上位2層は、 object をグループ分けした magazine という単位で、 割り当て用の領域をキャッシュしてます。 CPU 層では、各 CPU 毎に複数の magazine を保持していて、 object の割り当て・解放要求には、 可能な限り保持してる magazine で対応します。 CPU 層の全ての magazine が空(or 一杯)になった場合のみ、 depot 層の充填済み(or 空) magazine を使用します。 depot 層が magazine を充填する場合、 複数の異なる slab からかき集めた object を使用します。
magazine を充填する際に、 object を複数の slab からかき集めるのは、 CPU キャッシュの衝突を回避するためか?
性能に拘るのであれば、 magazine 充填・CPU 層への提供の際に、 ローカリティグループ (lgroup) を配慮する必要もある筈だが、 コード中には特にそれらしき記述は無い。
カーネルデータはシステムワイドなデータであるため、 ローカリティグループに拘ってもあまり意味が無い、 といった判断なのかな?
[11.2.3.5/1]@p.xxx
slab アロケータの CPU 層は、 グループ分けされた object をキャッシュし、 より広い範囲の排他を必要とする下位層での処理を回避することで、 劇的に性能を向上させます。
CPU 層は、 「空」「完充填」「半充填」の3つの magazine を保持し、 「半充填」の magazine から割り当てを行います。 「半充填」の magazine が空になったら、 その magazine は depot 層(= magazine 層)に返し、 手元にある「完充填」の magazine から割り当てを行います (= magazine の「半充填」化)。 CPU 層が「半充填」の magazine 以外に、 「空」「完充填」な magazine も保持しておくことにより、 短命な object の確保・解放が magazine の境界付近で頻発した場合でも、 depot 層での magazine 生成・破棄の頻発を回避でき、 CPU 層単独で効率的な割り当て・解放が可能です。
[11.2.3.6/1]@p.xxx 〜 [11.2.3.6/2]@p.xxx
depot 層は、 object を magazine に充填します。 magazine に充填される object は、 slab を構成する object と異なり、 メモリ上で連続している必要はありません。 magazine には object へのポインタが格納されます。
magazine あたりの object 数は、 depot 層での競合 (contention) 頻度に応じて、 各 cache 毎に動的に変更されます。 magazine あたりの object 数を増やせば、 depot 層での競合は減りますが、 メモリ消費量 (= 充填に必要な object 数) は増えます。 object サイズ毎に magazine サイズの上限・下限が定められています。
[11.2.3.6/3]@p.xxx
slab アロケータの保守スレッドによって、 15 秒 (kmem_reap_internval で調整可能) 毎に適切な magazine サイズが再計算されます。 depot 層で顕著な競合が発生してる場合、 magazine サイズが拡張されます。
kmem_reap_internval
ここでの「競合」(contention) とは、 CPU 層から depot 層への要求が並走する状態を指す。
具体的な実装で言うなら、 mutex 確保に失敗する毎に、 競合回数 cache_depot_contention を増加させる。
cache_depot_contention
/* @usr/src/uts/common/os/kmem.c */ kmem_depot_alloc(kmem_cache_t *cp, kmem_maglist_t *mlp) { if (!mutex_tryenter(&cp->cache_depot_lock)) { mutex_enter(&cp->cache_depot_lock); cp->cache_depot_contention++; }
保守スレッドは、 同一管理構造体の cache_depot_contention_prev に保存しておいた、 前回確認時の cache_depot_contention 値と比較して、 競合頻度を確認する。
cache_depot_contention_prev
[11.2.3.7/1]@p.xxx 〜 [11.2.3.7/2]@p.xxx
slab 層は、 object 割り当てに使用する、 連続した page から構成される slab を、 depot 層へと供給します。 slab の新規獲得・解放が depot 層で発生しない限り、 slab 層での処理は実施されません。
アロケータがキャッシュのサイズ (= 割り当て可能な object の数) を拡張・縮小させる場合は、 slab 単位での拡張・縮小になります。 各 slab は、 仮想アドレス的に連続した1つ以上の page から構成されます。 メモリ領域は同一サイズの chunk に分割され、 構成要素の chunk のうち、 幾つが割り当て済みであるかを管理するための、 参照カウンタも持っています。
最初の段落で object 割り当て元=slabを "contiguous pages of physical memory" と書いておいて、 次の段落では slab が "one or more pages of virtually contiguous memory" であると書いているのは、 どういう意図なんだろう? 最初の段落は「物理メモリと対応関係を持っている」点を強調したかったのかな?
[11.2.3.7/3]@p.xxx
各 slab の管理情報は kmem_slab 構造体で、 slab を構成する buffer (= 割り当て前の object の領域)は kmem_bufctl 構造体で管理され(る場合があり)ます。
kmem_slab
kmem_bufctl
[11.2.3.7/4]@p.xxx
object サイズが page サイズの 1/8 以下の場合、 slab アロケータは slab 毎に page を1つ割り当てます。 管理構造を page 末尾に配置し、 残りの領域を同一サイズの buffer へと切り分けます。 このケースでの各 buffer は、 未使用リストに繋がるためのリンク以外の、 余計な管理情報を保持しないことで、 管理構造領域が buffer サイズより大きくなることを防ぎます。(続く)
本来の kmem_bufctl 構造体は、 以下の3つの情報を持っているが:
typedef struct kmem_bufctl { struct kmem_bufctl *bc_next; /* next bufctl struct */ void *bc_addr; /* address of buffer */ struct kmem_slab *bc_slab; /* controlling slab */ } kmem_bufctl_t;
「object サイズが page サイズの 1/8 以下」のケースでは、 bc_next 以外の情報は以下の要領で導出できるので、 保持しないようにしている。
bc_next
bc_addr
bc_slab
[11.2.3.7/4]@p.xxx(続き)
(続き) 解放済みメモリの不正利用では、 メモリ領域冒頭部分を書き換えるケースが圧倒的に多いため、 未使用リストのリンクポインタが buffer 末尾に格納しています。 この配置によって、 未使用リストのリンクポインタが無事な可能性が高まるため、 仮に不正利用が発生しても診断・調査ができる可能性も高まります。 buffer には更に余白領域が取られているので、 使用中の buffer を未使用リストに繋いでも、 既存のデータを壊す心配はありません。
/* @usr/src/uts/common/os/kmem.c */ static kmem_slab_t * kmem_slab_create(kmem_cache_t *cp, int kmflag) { : while (chunks-- != 0) { : bcp = KMEM_BUFCTL(cp, buf); : buf += chunksize; }
/* @usr/src/uts/common/sys/kmem_impl.h */ #define KMEM_BUFCTL(cp, buf) \ ((kmem_bufctl_t *)((char *)(buf) + (cp)->cache_bufctl))
buf
cache_bufctl
chunksize や cache_bufctl は、 kmem_cache_create() での初期化時点で、 object (= buffer) サイズや、境界整合値を元に算出される。
chunksize
[11.2.3.7/5]@p.xxx 〜 [11.2.3.7/6]@p.xxx
object サイズが page サイズの 1/8 以上の場合、 slab に割り当てた page を有効に使うために、 kmem_slab や kmem_bufctl といった管理構造体用の領域は、 page とは別に確保します (これらの領域の確保にも slab アロケータを使用します)。
cache から割り当てられた object が、 同一メモリオフセットに配置されていた場合、 CPU キャッシュの同一ライン上で競合することになります。 slab アロケータは、 各 slab 毎に異なるサイズの余白領域を slab 冒頭に設けることで、 この問題を回避しています。
/* @usr/src/uts/common/os/kmem.c */ static kmem_slab_t * kmem_slab_create(kmem_cache_t *cp, int kmflag) { : color = cp->cache_color + cp->cache_align; if (color > cp->cache_maxcolor) color = cp->cache_mincolor; cp->cache_color = color;
[11.2.3.8/1]@p.xxx
slab アロケータのチューニング用パラメータには、 表 11.11 に示すものがありますが、 変更しないことをお勧めします。
[11.2.3.9/1]@p.xxx 〜 [11.2.3.9/3]@p.xxx
slab アロケータの統計情報には、 グローバルなものと、cache 毎のものがあります。 グローバルな統計情報は、 mdb を使って参照できます。
mdb の ::kmasta コマンドを使うことで、 グローバルな統計情報のまとめが表示されます。
::kmasta
cache 毎の統計情報は、 kstat コマンドを使うことで取得できます。
[11.3/1]@p.xxx 〜 [11.3/4]@p.xxx
カーネルのメモリアロケータは、 仮想アドレス割り当て処理と、 割り当てられた仮想アドレスと物理ページの対応関係を確立する VM ルーチン群を必要とします。 旧来の仮想アドレス割り当て処理は、 アドレス空間の断片化が酷かったり、 断片化に伴う性能の低下や、 単一スレッドでの動作限定であるなど、 大規模システムに対するスケーラビリティがありませんでした。
仮想アドレスの割り当ては、 より一般的な「資源割り当て」の一面に過ぎません。 仮想アドレスは 64bit 整数のサブセット、 プロセス ID は 0 〜 30000 の整数のサブセット、 マイナーデバイス番号は 32bit 整数のサブセット、 といったように、 「資源」は「整数の集合」とも言えます。
本節では、 vmem と呼ばれる、 断片化が少なく、 固定時間処理が可能な、 汎用資源割り当て機能について説明します。
本節では、vmem の背景、目的、 インタフェース、実装詳細、性能(断片化、遅延、スケーラビリティ) の順で説明します。
[11.3.1/1]@p.xxx 〜 [11.3/4]@p.xxx
UNIX は rmalloc() という資源マップ (resouce map) アロケータを持ちます。 rmalloc(map, size) により map の一部を割り当て、 rmfree(map, size, addr) により割り当てられた領域を解放します。
rmalloc()
rmalloc(map, size)
rmfree(map, size, addr)
旧来の実装は、設計・実装の両面で、問題がありました。
[11.3.2/1]@p.xxx 〜 [11.3/6]@p.xxx
妥当な資源アロケータは、以下のような特徴を備えているべきです。
以降では、インタフェースから実装詳細へと、 vmem について掘り下げていきます。
[11.3.3/1]@p.xxx
vmem のインタフェースには、 (1) 資源を表す arena (舞台、競技場、関心領域) の生成・破棄、 (2) 資源の割り当て・解放、 (3) 別 arena からの資源の取り込み、 という、3つの基本的な機能があります。
[11.3.3.1/1]@p.xxx 〜 [11.3.3.1/5]@p.xxx
arena は単なる整数の集合を表します。 vmem 機能を使った arena の多くは、 仮想メモリのアドレス空間を表すために使用されますが、 実際には整数を資源として割り当てる任意の処理に適用できます。
arena における整数群の多くは、 "[100, 500)" のような単一の連続した範囲 (span) なので、 この「範囲」を vmem_create に指定することで arena を初期化します。 不連続な資源の場合は、 vmem_add を使って後から1つずつ追加します。
vmem_create
vmem_add
例えば、"[100, 500)" という範囲の整数を表す arena の初期化は:
foo = vmem_create("foo", 100, 400, ...); vmem_add(foo, 600, 200, VM_SLEEP);
(100 は開始値、400 はサイズです) "foo" という arena で "[600, 800)" の範囲も扱いたい場合は、 vmem_add を使って範囲を追加します。
vmem_create() に指定する arena の取り扱い単位 (qu: quantum unit) には、 プロセス ID のような単一整数値の場合は 1 が、 仮想アドレスの場合は PAGESIZE が指定されます。 資源割り当ての際には、 qu 単位での要求サイズの切り上げ・境界整合が保証されます。
vmem_create()
[11.3.3.2/1]@p.xxx 〜 [11.3.3.2/4]@p.xxx
一般的な用途では、 vmem_alloc(vmp, size, vmflag) 呼び出しにより、 vmp が指す arena から size 分の資源割り当てを、 vmem_free(vmp, addr, size) 呼び出しにより、 割り当てられた資源 ("[addr, addr + size)") の解放という、 簡単なインタフェースが使用できます。
vmem_alloc(vmp, size, vmflag)
vmem_free(vmp, addr, size)
割り当てに際して、 境界整合、位相(phase: 境界整合位置からのオフセット)、 割り当て範囲、境界横断に関する制約(ページ境界を跨がない、など) を指定したいケースには、 vmem_xalloc() が使用できます。
vmem_xalloc()
例えば、64 バイトの境界整合、 境界整合位置から 8 バイト後ろ、 且つ "[200, 300)" の範囲で 20 バイトの領域を割り当てる場合は:
addr = vmem_xalloc(foo, 20, 64, 8, 0, 200, 300, VM_SLEEP);
この例では、"[262, 282)" の領域が割り当てられます。
vmem_xalloc() 呼び出し例での第5引数の 0 は、 境界横断を禁止する nocross 引数。
nocross
nocross が非 0 の場合、 この境界値を跨ぐような割り当てが禁止される。
[11.3.3.2/5]@p.xxx 〜 [11.3.3.2/8]@p.xxx
割り当て要求の際には、以下の3つのポリシーから1つだけ選択できます。
VM_BESTFIT
VM_INSTANTFIT
VM_NEXTFIT
[11.3.3.2/9]@p.xxx
大多数の資源割り当てが、 高々単位量数個分 (1〜2ページのヒープや、マイナーデバイスIDのような単一整数値) であることから、 qcache_max 引数で指定した単位量を上限に、 quantum unit の整数倍ごとの資源を、 割り当て用にキャッシュとして切り出しておく、 quantum cache 機能も用意してあります。 quantum cache の閾値(=単位量上限)は、 管理対象資源の性質に応じて、 arena の初期化毎に指定可能です。 quantum cache を使用することで、 perfect-fit な割り当てを、 低遅延且つ高スケーラビリティに実現できます。
qcache_max
quantum cache 部分は、 slab アロケータ実装を流用してる模様 (図 11.6 も参照)。
/* @usr/src/uts/common/os/kmem.c */ vmem_create_common(const char *name, void *base, size_t size, size_t quantum, : : if (nqcache != 0) { ASSERT(!(vmflag & VM_NOSLEEP)); vmp->vm_qcache_max = nqcache << vmp->vm_qshift; for (i = 0; i < nqcache; i++) { char buf[VMEM_NAMELEN + 21]; (void) sprintf(buf, "%s_%lu", vmp->vm_name, (i + 1) * quantum); vmp->vm_qcache[i] = kmem_cache_create(buf, (i + 1) * quantum, quantum, NULL, NULL, NULL, NULL, vmp, KMC_QCACHE | KMC_NOTOUCH); } }
フラグ指定によって、 実メモリの確保/メモリ(= 資源領域)へのアクセスを抑止しているのかな?
[11.3.3.2/9]@p.xxx (続き)
ざっと見た範囲では、 以下の vmem arena で quantum cache を使用してる模様。
zfs_file_data
segkmem_zio_init
segkmem_ppa
segkmem_heap_lp_init
segkp
segkp_create
kmem_va
kmem_cache_init
kmem_metadata
kmem_init
bp_map
bp_init
他にも、名前が "_1" で終了している slab cache は、 quantam cache として使用されてると思われる。
mdb の ::kmastat コマンドの出力に、 これらの quantum cache 用の slab アロケータ (= cache) をみることができる。
"11.3.4.4 Quantum Caching" の際には、 より詳細に踏み込みたいところ。
[11.3.3.3/1]@p.xxx 〜 [11.3.3.3/2]@p.xxx
vmem_create() での初期化の際に、 別の arena と、 その arena に対する関数(資源の確保・解放の2つ)を指定することで、 他の arena から必要に応じて資源を import することができます。
segkmem_alloc() は、 物理ページをマッピングした上で、 (第1引数で)指定された arena から vmem_alloc() で確保した仮想アドレス領域の先頭を返却します。 そのため、 既存の arena と、 segkmem_alloc()、 segkmem_free() を組み合わせることで、 簡単に「物理ページがマッピングされた仮想アドレス空間」 を管理する arena を作り出すことができます。
segkmem_alloc()
vmem_alloc()
segkmem_free()
seg_kmem.c や kmem.c、vmem.c 中の vmem_create 呼び出しを見ることで、 比較的簡単に arena の階層構造を見つけることができる。
例えば、 汎用の heap_arena は、 heap_quantum (実際には PAGESIZE) の境界整合が設定されているが、 それを import する vmem_metadata_arena は、 境界整合を 8 ページに設定したり、 フラグ設定を変更したりといった具合に、 引用先に応じてカスタマイズしている。
heap_arena
heap_quantum
vmem_metadata_arena
vmem_t * vmem_init(const char *heap_name, : : heap = vmem_create(heap_name, heap_start, heap_size, heap_quantum, NULL, NULL, NULL, 0, VM_SLEEP | VMC_POPULATOR); vmem_metadata_arena = vmem_create("vmem_metadata", NULL, 0, heap_quantum, vmem_alloc, vmem_free, heap, 8 * heap_quantum, VM_SLEEP | VMC_POPULATOR | VMC_NO_QCACHE);
[11.3.4/1]@p.xxx
本節では、 vmem の実際の挙動に関して説明します。
[11.3.4.1/1]@p.xxx 〜 [11.3.4.1/4]@p.xxx
多くの malloc() 実装において、 割り当て領域の先頭に配置される、 割り当て情報格納領域 (boundary tag) は、 Knuth によって考案されたもので、 以下の問題を解決します。
free(()
free()
リソースアロケータ (= vmem) は、 実メモリ領域(= 管理情報を格納可能)を管理するわけではないので、 boundary tag の仕組みをそのまま使うことはできませんが、 外部 (external) boundary tag を使うことで、 固定時間処理を可能にします。
[11.3.4.2/1]@p.xxx 〜 [11.3.4.2/2]@p.xxx
arena が管理する空間を構成する segment は、 アドレス順に連結された segment リストで管理されると同時に、 フリーリスト (free list) か、割り当てハッシュチェーン (allocation hash chain) のいずれかに所属します (segment のリストには、 他の arena から import した領域の管理を容易にするための、 span マーカも含まれます)。
未使用 segment は、 サイズに応じて (2n 〜 2n+1) フリーリスト配列 freelist[n] で管理されるため、 要求に見合った未使用 segment を容易に引き当てることができます。 segregated fit と呼ばれるこの方法は、 選択される配列要素=free list 中の segment が good fit なので、 実際には best fit に近い結果となります (2n 管理の場合、 明らかに perfect fit の2倍の容量が必要です) 多くの実際の負荷ケースでフラグメント化を低減できるので、 best fit との同等性は魅力的です。
freelist[n]
[11.3.4.2/3]@p.xxx 〜 [11.3.4.2/6]@p.xxx
vmem_alloc に指定される以下の方針フラグに応じて、 空き segment からの割り当て方式が変更されます (割り当て要求サイズとして 2n 〜 2n+1 を仮定)
vmem_alloc
脚注 1: 固定時間処理が可能で、 実環境でのフラグメント化が少なく、 実装の容易な instant fit がお勧めです。 空き領域のサイズベース木構造管理をはじめ、 妥当な時間で処理可能な手段が、他にも色々あります。
[11.3.4.2/7]@p.xxx 〜 [11.3.4.2/9]@p.xxx
選択された segment は freelist から切り離され、 vmem_free での引き当てが容易なように、 boundary tag が割り当てハッシュテーブルに登録されます。 要求サイズと厳密一致でない場合は、 (1) segment の分割、 (2) 未使用部分の segment のための (external) boundary tag の生成、および (3) 対応する freelist への追加も実施されます。
vmem_free
vmem_free は、 対応領域の boundary tag の引き当ておよび削除を、 割り当てハッシュテーブルに対して実施します。 隣接(未使用)領域との連結を(可能であれば)実施した上で、 対応する freelist に追加します。 これらの処理は全て固定時間で実施できます。 割り当てハッシュテーブルでの検索は、 簡易な健全性チェックとしても機能します。 segment の引き当てとサイズ一致判定が共に成功しない場合は、 二重解放等のバグの可能性があります。
上記の処理に要する時間は、 割り当て領域サイズやフラグメント化状況に依存しないので、 任意サイズの割り当て・解放の固定時間実行が保証されます。
割り当てられた領域 (segment) に対して、 管理用の bondary tag (= サイズ情報や、 前後の領域の boundary tag へのリンクも持っている筈) が存在するのであれば、 vmem_free でのサイズ指定は不要な気がしたのだが、 vmem_free の際に 「該当エントリの引き当ての成否」「想定サイズとの一致」を、 健全性チェックの一環として実施するのであれば納得。
あるいは、 「現在の vmem_free では既に必要性が薄れたが、 歴史的経緯で必要であったので、 そのまま残ってしまった」可能性も?
[11.3.4.3/1]@p.xxx
各 arena の segment リスト、freelist、割り当てハッシュテーブルは、 大域ロックで保護されます。 巨大なサイズの割り当ては稀な上、 一般的な要求サイズに対しては、 quantum cache によってスケーラビリティが確保できるためです。
原文では "global lock" と謳っているが、 あくまで「(対象 arena に対する) 大域 lock」程度の意味合い。
quantum cache 部分は、 slab アロケータの CPU レイヤ部分によって、 並列性が担保されている。
import による arena の相互依存は、 上下階層のある木構造にはなっても、 循環構造にはならないので、 個別の arena 毎に lock を握ったままででも、 デッドロックの心配は無い(筈)
[11.3.4.4/1]@p.xxx
vmem の quantum cache 機能は、 slab アロケータのオブジェクトキャッシュ機能によって実現されています。 各 arena は、 取り扱い単位 (quantum unit) の整数倍、 且つ qcache_max 以下のサイズの割り当て・解放要求を、 内部的に保持している slab アロケータへの kmem_cache_alloc および kmem_cache_free へと読み替えます。
kmem_cache_alloc
kmem_cache_free
[11.3.4.4/2]@p.xxx
arena "foo" に対する vmem_alloc(foo, 3 * PAGESIZE) 呼び出しは、 kmem_cache_alloc(foo->vm_qcache[2]) と読み替えられます。 slab アロケータの depot 層 (= magazine 層) でも割り当てができない場合は、 foo 自身に対する vmem_alloc(foo, 16 * PAGESIZE) 呼び出しで確保した新規 slab 領域から、 5 つの PAGESIZE * 3 オブジェクトキャッシュを生成し、 その中から割り当てを行います。
vmem_alloc(foo, 3 * PAGESIZE)
kmem_cache_alloc(foo->vm_qcache[2])
vmem_alloc(foo, 16 * PAGESIZE)
新規 slab の確保の際の vmem_alloc(foo, 16 * PAGESIZE) は、 vm_qcache_max (= PAGESIZE * 5) よりも大きい (図 11.6 参照) ので、 quantum cache (slab アロケータ) ではなく、 segment list からの割り当てになる ⇒ 次段落での "prevent infinite recursion" に繋がる
「PAGESIZE * 16 から、 5 つの PAGESIZE * 3 オブジェクトキャッシュを生成」した場合、 「PAGESIZE * 1 の領域が無駄になる」のだけれど、 これはあくまで「アドレス空間 (≠ 物理メモリ)」の割り当てであり、 64bit のアドレス空間は領域が掃いて捨てるほどあるので、 このような無駄遣いをしても問題ない。
[11.3.4.4/3]@p.xxx
vmem の quantum cache として slab アロケータを使用する場合、 slab のサイズとして、 常に vm_qcache_max * 3 よりも大きな最初の 2n を使用します。 これは、 (1) vm_qcache_max よりも大きなサイズの要求による無限再帰呼び出しの回避 (2) slab の利用効率が perfet に近い (3) 同一 slab サイズの方が arena としてのフラグメントを低減できる(詳細後述)、 といった理由によるものです。
/* usr/src/uts/common/os/kmem.c */ kmem_cache_t * kmem_cache_create( : : if (cflags & KMC_QCACHE) bestfit = VMEM_QCACHE_SLABSIZE(vmp->vm_qcache_max);
/* usr/src/uts/common/sys/vmem_impl.h */ #define VMEM_QCACHE_SLABSIZE(max) \ MAX(1 << highbit(3 * (max)), 64)
[11.3.4.5/1]@p.xxx 〜 [11.3.4.5/3]@p.xxx
フラグメント化とは、 (再)利用不可能な小さくて非連続な領域への断片化のことです。 例えば、 1GB の領域を、1B づつ順に割り当てた上で、 偶数番目の割り当て領域のみを開放した場合、 空き領域は 500MB ですが、 2B 領域の割り当てすらできません。
フラグメント化の原因は、 異なるサイズ、異なる寿命の segment を混在させることにあると思われます。 割り当てサイズが常に同一であれば、 解放済み segment は常に次の割り当て要求に使用できます。 割り当てた領域の寿命が短時間であれば、 フラグメント化自体も短時間で解消されます。
segment の寿命を制御するのは難しいですが、 quantum cache による割り当てサイズの制御 (= 常に同一サイズの slab 領域を使用)により、 arena の segment list 中の要素の大半は、 slab サイズの chunk となります。
[11.3.4.5/4]@p.xxx 〜 [11.3.4.5/5]@p.xxx
以上の様な対応により、 segment list のフラグメント化は防げますが、 quantum cache において、 slab の一部しか使用されない(= フラグメント化)ため、 単に問題の発生場所が移動しただけのように見えるかもしれません。 不利な状況における segment list でのフラグメント化は、 再利用できない小片によるフラグメント化であるのに対して、 quantum cache 中の未使用オブジェクトは、 再利用可能であるという点で、 両者は大きく異なります。
役立つ事を照明するのは難しいですが、 事前割り当て (= slab 領域確保のことか?) は実際に上手く機能しています。 vmem 導入以降に、 深刻なフラグメント化の報告は受けたことがありません (旧来のアロケータに関するフラグメントは大量に報告されていました)し、 Solaris システムはしばしば数年に渡って稼動することもあります。
脚注 2: 「公立的なメモリ利用を保証できる、 確かなアルゴリズムは存在しないし、 その可能性もない」ということはわかっています。
[11.3.5/1]@p.xxx
vmem 設計の有効性を確認するために、 いくつかの性能検証が実施されました。
[11.3.5.1/1]@p.xxx 〜 [11.3.5.1/2]@p.xxx
フラグメント化状況に対しての、 割り当て・解放の遅延時間を表すグラフ 11.7 は、 rmalloc()/rmfree() がフラグメント状況に比例した時間を要するのに対して、 vmem_alloc()/vmem_free() は固定時間処理が可能であると述べたことを裏付けています。
rmfree()
vmem_free()
フラグメント化率が非常に低い場合には、 単純なアルゴリズムによる rmalloc() にも、 良好な性能が見られます。 フラグメント化無しで、 vmem の quantum cache を用いない場合なら、 rmalloc() 対 vmem_alloc() の性能比は、 715ns 対 1560ns になります。 quantum cache を使用した場合、 遅延は 482ns に抑えられますので、 quantum cache 部分で多くの割り当て・解放処理が完結する vmem は、 フラグメント化率が低い場合も、 rmalloc() より高速に動作します。
vmem と rmalloc のグラフが入れ替わっている?
quantum cache ありで遅延が 482ns なら、 フラグメント化率 0 で 715ns な rmalloc() よりも、 グラフ的には下回っていないと駄目なのでは?
このグラフでの vmem は「quantum cache 無し」で、 「2n サイズ毎の freelist 管理」+ instant fit による、 固定時間処理を計測したもの?
[11.3.5.2/1]@p.xxx 〜 [11.3.5.2/4]@p.xxx
低遅延・線形スケール可能な vmem による、 カーネルアドレス空間割り当ての性能改善は、 システムレベル性能の劇的な改善ももたらしました。
[11.3.6/1]@p.xxx 〜 [11.3.6/3]@p.xxx
vmem インタフェースは、 単純且つ非常に抑制された割り当てをサポートすると同時に、 import 機構を使って単純な要素から複雑な資源を扱うことも可能です。 また、 vmem 導入依頼、 Solaris の持っていた、 30 以上の特化された割り当て機能を排除できる程度には、 汎用的なインタフェースです。
vmem 実装の高速性・スケール可能性(・耐フラグメント性)は、 システムレベルの各種ベンチマークを 50% 以上向上させることで、 証明されています。
「instant fit」と「外部 boundary tag」という新しい概念により、 割り当てサイズやフラグメント化状況に関わらず 固定時間処理を保証できます。
vmem の quantum cache は、 大多数の共通サイズの割り当てに対して、 低遅延・性能スケール性を保証します。 また、arena の segment list に対しても、 フラグメント化を抑止する効果 (= 同一サイズ slab 領域を使うことによる再利用性向上) があります。
[11.4/1]@p.xxx 〜 [11.4/3]@p.xxx
slab アロケータは、 割り当て履歴をトレースする汎用的な機能を含んでおり、 カーネルシステム変数 kmem_flags の設定により有効化されます。 (デフォルトは無効) 記録対象の slab アロケータ名 + .DEBUG を名前に持つ slab キャッシュに、 割り当て時のスタックや割り当て履歴が記録されます。 トレース機能を有効化するには:
kmem_flags
[11.4.1/1]@p.xxx 〜 [11.4.1/3]@p.xxx
kmem_flags の設定後に、 大規模システム上の (slab) キャッシュ挙動をトレースする手順を例示します。 全てのキャッシュをトレースするには、 kmbd 経由での起動と kmem_flags の設定が必要です。
GRUB 経由でブートする環境の場合、 GRUB メニューエントリで -kd オプションを指定してください。
トレース情報の総量は、 監査キャッシュパラメータ設定により制限されます。
[11.4.2/1]@p.xxx 〜 [11.4.2/4]@p.xxx
:::kmastat dcmd 以外にも、 ::kmem_cache dcmd を使うことで、 kmem キャッシュの一覧を得ることが出来ます。
:::kmastat
::kmem_cache
::kmem_cache dcmd は、 個々のキャッシュに関して、 名前〜アドレス〜デバッグフラグ(FLAG カラム)の対応関係を参照できる点で有用です。 アロケータにおけるデバッグ機能の可否は、 キャッシュ毎のデバッグフラグ設定に基づいています。 フラグ設定はキャッシュ生成時の kmem_flags カーネルパラメータ値で確定されるため、 実行中に kmem_flags を変更しても、 既に作成済みのキャッシュの挙動は変化しません。
kmem_cache walker を使うことで、 kmem キャッシュの一覧を得ることもできます。
kmem_cache
kmem_cache walker は、 kmem キャッシュのカーネル内アドレス一覧を出力します。 特定の kme キャッシュの詳細を参照したい場合は、 kmem_cache dcmd を使います。
MDB では、:: で始まる dcmd と、 ::walk に指定する walker がある。 kmem_cache のように、 同名の dcmd と walker が存在する場合もある。
::
::walk
(参照系) dcmd が「詳細情報」の取得に使われる一方、 walker は「一覧」の取得に使われる。 11.4.11 での実行例にあるように、 walker の出力をパイプで結合することで、 より複雑な挙動を行わせることもできる。
[11.4.2/5]@p.xxx
kmem_cache dcmd の出力には、 デバッグで有用な BUFSIZE, FLAGS や NAME といったフィールドも含まれます。 NAME からはシステム内部における kmem キャッシュの用途、 BUFSIZE からはキャッシュのバッファサイズ、 FLAGS からは有効になっているデバッグ機能の情報が読み取れます。
本文の説明と、実行例が完全に食い違ってますね(笑)
以下、フラグ定義一覧
#define KMF_AUDIT 0x00000001 /* transaction auditing */ #define KMF_DEADBEEF 0x00000002 /* deadbeef checking */ #define KMF_REDZONE 0x00000004 /* redzone checking */ #define KMF_CONTENTS 0x00000008 /* freed-buffer content logging */ #define KMF_STICKY 0x00000010 /* if set, override /etc/system */ #define KMF_NOMAGAZINE 0x00000020 /* disable per-cpu magazines */ #define KMF_FIREWALL 0x00000040 /* put all bufs before unmapped pages */ #define KMF_LITE 0x00000100 /* lightweight debugging */ #define KMF_HASH 0x00000200 /* cache has hash table */ #define KMF_RANDOMIZE 0x00000400 /* randomize other kmem_flags */ #define KMF_DUMPDIVERT 0x00001000 /* use alternate memory at dump time */ #define KMF_DUMPUNSAFE 0x00002000 /* flag caches used at dump time */ #define KMF_PREFILL 0x00004000 /* Prefill the slab when created. */
[11.4.2/6]@p.xxx 〜 [11.4.2/8]@p.xxx
特定の kmem キャッシュ管理下のバッファに興味がある場合は、 kmem キャッシュアドレスに対して kmem walker を適用します。
kmem
kmem walker 適用の利便性から、 個々の kmem キャッシュごとに、 同名の walker が提供されています。
これまでの説明で、 カーネルメモリアロケータの内部データの参照方法と、 kmem キャッシュの主要な情報の参照方法が理解できた筈です。
[11.4.3/1]@p.xxx 〜 [11.4.3/7]@p.xxx
カーネルメモリアロケータの主要なデバッグ機能の1つが、 メモリ破壊の検出機能の統合です。 メモリ破壊が検出された場合、 カーネルは速やかにパニック状態になります。
メモリ破壊の問題をデバッグするためには、 以降で説明するメモリ破壊の検出方法を理解する必要があります。 メモリ不正使用の典型的ケースは、 以下のいずれかに分類されます。
カーネルメモリアロケータの設計の理解や、 メモリ破壊時の効率的な診断では、 上記分類が手助けになるでしょう。
[11.4.4/1]@p.xxx 〜 [11.4.4/3]@p.xxx
kmem キャッシュに KMF_DEADBEEF フラグが設定されている場合、 解放済みバッファに特定のパターン 0xdeadbeef を書き込むことで、 メモリ破壊の検出を容易にします。 多くのメモリ領域では、 「割り当て済み」と「未使用」の両方の領域が散在した状態になっています。
KMF_DEADBEEF
0x70a9add8 と 0x70a9ae28 から始める領域は、 共に 0xdeadbeef パターンで埋められた未使用領域です。 両領域の間にある 0x70a9ae00 から始まる領域は、 割り当て済み領域です。
例示されている 120 バイトの領域のうち、 ここで説明されているのは 24(= 8x3) バイト x 3 = 72 バイトだけです。 残りの領域に関する詳細は後述します。
「未使用領域」への書き出しに関しては、 11.4.10 で後述する ::kmem_verify による検出が可能だが、 読み出しに関しては原理上は検出できない。
::kmem_verify
但し、 SPARC プロセッサの場合、 0xdeadbeef が (アドレス値として) 誤用された際には、 奇数アドレス参照がアドレス境界整合エラーを生じるため、 結果的に不正アクセス検出 → カーネルパニックという流れになる。
kmem キャッシュが割り当てる領域で保持されているデータの多くが、 管理情報領域(= 他の kmem キャッシュ割り当て領域)へのポインタであり、 且つ管理情報領域の先頭は境界整合されていることが多いことを考えると、 副作用としての不正アクセス検出でも、 十分有用であると言える。
[11.4.5/1]@p.xxx 〜 [11.4.5/8]@p.xxx
バッファ中に散見される 0xfeedface パターンは、 "redzone" インジケータと呼ばれ、 カーネルメモリアロケータ (+ メモリ破壊をデバッグ中のユーザ) による、 不正コード (buggy code) によるバッファ境界領域の破壊の判定を可能にします。 redzone と付随するデバッグ用情報をまとめて buftag 領域と呼びます (デバッグ情報の詳細は後述)。
KMF_AUDIT, KMF_DEADBEEF あるいは KMF_REDZONE のいずれかのフラグが指定されている場合、 buftag 領域が作成されます。 buftag 領域の内容は KMF_AUDIT 指定の有無に依存します。
KMF_AUDIT
KMF_REDZONE
以上の知識があれば、 先述したメモリ領域例を個々のバッファに細分するのは簡単です。
0x70a9add8 と 0x70a9ae28 から始まる未割り当て領域では、 redzone が 0xfeedfacefeedface で埋められています。 バッファが未割り当て領域であることを判定する場合は、 この方法が簡単です。
0x70a9ae00 から始まる割り当て済み領域の場合は、 少々事情が異なり、 2種類の割り当て種別に分類されます。
例えば 20 バイトの割り当て要求に対しては、 kmem_alloc_24 キャッシュを使い、 要求元が使用する領域の直後に、 バッファ境界を示すマーカと、redzone が配置されます。
kmem_alloc_24
[11.4.5/9]@p.xxx 〜 [11.4.5/10]@p.xxx
アドレス 0x0a9ae18 の 0xfeedface の後ろに配置された 32ビット (= 4バイト) 値は、 一見乱数に見えますが、 実際にはバッファサイズをエンコードしたもので、 251 での除算によりバッファサイズが得られます。
この例では、0x139d / 251 = 20 で、 バッファサイズが 20 バイトであることが分かります。
脚注: サイズ値の符号化は (251 * size + 1) で実施されます。 整数除算の範囲では、剰余となる +1 は意味を持ちませんが、 (encodedsize % 251) == 1 により、 整合性の有無を確認できる利便性があります。
(251 * size + 1)
(encodedsize % 251) == 1
0xbb は Buffer Boundary の BB か?
0xbb は奇数なので、 LSB (least significant byte) 位置に書き込まれても、 想定外の利用が発生した場合にアドレス境界違反要因になる点は変わらない。
[11.4.5/11]@p.xxx 〜 [11.4.5/14]@p.xxx
メモリアロケータは、 バッファサイズ情報を復号し、 redzone バイト (0xfeedface ではなく 0xbb の方) が 20 バイトオフセット位置にあることを確認します。 redzone バイトは 0xbb です。
図 11.10 は先述したメモリ状態の全体図を示しています。
割り当て領域のサイズが、 キャッシュのバッファサイズと同一である場合、 redzone バイトは redzone 自体の先頭領域を上書きします。
上書き結果は、 実行環境のバイトオーダに応じて、 0xbbedface (MSB First) か 0xfeedfabb (LSB First) のいずれかになります。
[11.4.6/1]@p.xxx 〜 [11.4.6/3]@p.xxx
先述した例では、 redzone よりも前の領域に、 元々 0xbaddcafe が格納されていた上から、 redzone バイト 0xbb が上書きされたために、 0xbbddcafe というデータが格納されていました。 KMF_DEADBEEF フラグが設定されている場合、 「割り当て済み」且つ「未使用」な領域には、 0xbaddcafe パターンが格納されます。 領域割り当ての際に、 バッファ内を 4 バイト単位で検査し、 0xdeadbeef パターン格納の有無を確認した上で、 0xbaddcafe パターンを格納します。
未初期化領域へのアクセスが発生した場合はシステムがパニックします。
ページフォールト要因となったアドレスが 0xbaddcafe であれば、 パニック要因となった処理は、 未初期化領域中のデータを使ったことがわかります。
パニックの可能性としては、 「未初期化領域中のデータ使用」以外に、 「再割り当て済みの未初期化領域の不正使用」という原因も考えられるので、 初期化の有無だけ見ていると原因を見落とす可能性も。
後述する Memory Allocation Logging の機能を使えば、 ある程度は絞り込めるかな?
[11.4.7/1]@p.xxx 〜 [11.4.7/4]@p.xxx
カーネルメモリアロケータは、 先述したように、 失敗状況に応じたパニックメッセージを出力します。
例えば、 0xdeadbeef パターンで埋められているか否かで、 解放後の不正改変の有無を確認することができます。 想定外のパターンが検出された場合、 アロケータはシステムをパニックさせます。
他のメッセージ例としては、 バッファ末尾を越えた書き込みの検出があります。
redzone のサイズ情報から得られる位置に、 redzone バイト (0xbb) が配置されているか (= 別なデータで書き換えられていないか)確認することで、 バッファ末尾を越えた書き込みの有無を判定し、 検出された場合にはシステムをパニックさせます。
[11.4.8/1]@p.xxx
カーネルメモリアロケータのロギング機能と、 システムクラッシュ時におけるデバッグでの、 ロギング機能の使用方法について説明します。
[11.4.8.1/1]@p.xxx 〜 [11.4.8.1/5]@p.xxx
buftag 領域の後半部分には、 デバッグ用途向けや、 アロケータ内部処理用の、 特別な情報を格納するバッファへのポインタが格納されています。 これら補助データは様々な形態を持ちますが、 全てをまとめて bufctl (バッファ制御: buffer control) データと呼びます。
bufctl へのポインタ自体が、 不正な処理によって破壊される可能性があるため、 bufctl ポインタの妥当性を検証できなければなりません。 メモリアロケータは、 bufctl ポインタとそれを符号化したものの両方を格納し、 お互いをクロスチェックすることで、 bufctl ポインタの妥当性を検証します。
bufctl ポインタ (bcp) と、 bcp の XOR 値 (bxstat) が格納されているので、 両者を XOR した際に、 特定の想定値になるか否かを検証します。
これらの領域の一方または両方の破壊が検出された場合、 メモリアロケータはシステムをパニックさせます。 割り当て済み領域での XOR 値は 0xa11oc8ed (allocated)、 未使用領域での XOR 値は 0xf4eef4ee (freefree) です。
11.4.4 節で例示しているメモリ領域の buftag 領域の情報 (= bcp と bxstat) が、 適正なものであることが確認できる筈です。
"at(e)" を "8" で代替するのは、発音的な視点でのものだと思われるが、 "r" を "4" で代替するのは、図形的な視点?英語圏的には自然なのかな?
[11.4.8.1/6]@p.xxx 〜 [11.4.8.1/7]@p.xxx
buftag 領域の破壊が検出された場合、 以下の様なメッセージを出力し、 システムをパニックさせます。
bcp のみの破壊に対しては、 バッファの「割り当て済み」「未割り当て」状況に応じて、 bxstat XOR 0xa11oc8ed あるいは bxstat XOR 0xf4eef4ee の値を使うことで、 値の復旧ができます。
「bxstat を使った復旧ができる」はあくまで「可能性」の話であって、 ECC メモリとか RAID のパリティ的に使うわけではなさそう。
理屈上は、 「bcp」と「bxstat XOR 固定値」のそれぞれの参照先が、 (1) 妥当な bufctl 領域で、且つ (2) addr 値が自分自身を指しているか否かを検証する、 という手もあるが、 実装上のミスでメモリ破壊が生じている状況で、 そこまで頑張って復旧させてもなぁ、という感じが。
kmem の実装をみる限りでは、以下のようになっている。
kmem_erro
kmem_panic
[11.4.8.2/1]@p.xxx 〜 [11.4.8.2/2]@p.xxx
buftag 領域に格納された bcp (BufCtl Pointer) は、 kmem キャッシュのフラグに応じて異なる意味を持ちます。 KMF_AUDIT ビットが立っていない場合、 kmem_bufctl_t 構造体が割り当てられ、 各バッファ毎の最小限の情報が記録されます。 KMF_AUDIT ビットが立っている場合、 kmem_bufctl_audit_t 構造体が割り当てられ、 より多くの情報が記録されます。
kmem_bufctl_t
kmem_bufctl_audit_t
本節では、 KMF_AUDIT ビットが立っている場合を仮定します。 このビットが立っていないキャッシュでは、 利用可能なデバッグ情報は、 説明されているものよりも少なくなります。
[11.4.8.2/3]@p.xxx 〜 [11.4.8.2/9]@p.xxx
kmem_bufctl_audit_t 領域には、 当該バッファで発生した直近の処理が記録されます。 MDB の bufctl_audit マクロによる監査情報検証方法を、 11.4.4 の例で説明します。
redzone 領域直後のアドレス値が、 kmem_bufctl_audit_t 領域のアドレスになりますので、 このアドレスに対して bufctl_audit マクロを適用します。
addr 値は対応するバッファ領域のアドレス、 cache 値はバッファ割り当てを行った kmem_cache のアドレスです。 後者には ::kmem_cache dcmd を適用できます。
timestamp 値は直近の処理(割り当て・解放)が実施された時刻を、 gethrtime(3C) と同じ形式で記録したものです。
gethrtime(3C)
thread は直近の処理を実施したスレッド (の thread_t 管理構造体) へのポインタです。
lastlog と contents は、 メモリアロケータ (= cache 参照先?) の処理ログ領域内へのポインタです。 これらに関する詳細は 11.4.11 で説明します。
一般的に、 kmem_bufctl_audit_t 領域中で最も有用な情報は、 処理を実施した際のスタックトレース情報でしょう。 実行例では、 fork(2) の延長でメモリ割り当てが実施されています。
fork(2)
[11.4.9/1]@p.xxx
本節では、 メモリリークやデータ破壊の実施元の特定ナなどを含む、 より高度なメモリ分析のための機能について説明します。
[11.4.9.1/1]@p.xxx 〜 [11.4.9.1/3]@p.xxx
::findleaks dcmd は、 kmem の全てのデバッグ機能が有効になっているカーネルクラッシュダンプにおいて、 強力且つ効率的にメモリリークを検出できます。 ::findleaks は、 最初にカーネルダンプ中のメモリリークの検出を行い、 次に、メモリリークと割り当てスタックトレースを突合せます。 ::findleaks は、 特定されたメモリリーク毎に、 bufctl のアドレスと、呼び出し元を出力します。
::findleaks
MDB の bufctl_audit マクロを bufctl アドレスに対して適用することで、 完全なスタックトレースを参照できます。
bufctl_audit
kmem_bufctl_audit_t 領域の情報と、 呼び出しスタックトレースを使うことで、 対象バッファのメモリリーク要因となるコードパスを、 素早く特定可能です。
[11.4.9.2/1]@p.xxx 〜 [11.4.9.2/6]@p.xxx
メモリ破壊の診断では、 対象メモリ領域が他のどの箇所から参照されているか (= どこでポインタが保持されているか)が重要です。 この情報は、 解放済み領域への アクセス等での原因特定や、 対象領域の共有状態を知る上で、 非常に重要です。 対象アドレスに対して ::whatis dcmd を適用してみましょう。
::whatis
アロケータ名から、 対象領域が STREAMS mblk 向けの割り当てであることがわかります。 ::whatis -a を適用することで、 アロケータの階層構造も表示できます。
::whatis -a
出力例では、 vmem アリーナ kmem_va を import した kmem_va_8192 キャッシュから、 メモリ領域が割り当てられていることもわかります。
kmem キャッシュと vmem アリーナの一覧は、 ::kmastat dcmd で表示されます。 ::kgrep dcmd を使用することで、 特定のアドレス値を保持している領域を検索できます。 Solaris 内部では、 メモリ割り当ては階層的に実施されるので、 当該領域を割り当てた最終的な kmem キャッシュの名前から、 あるアドレスを含む領域の種別を判定できます。
::kgrep
::kgrep で特定した参照元の素性は、 ::whatis を適用することで特定できます。
この実行例では、 参照元のうち、 1つは既存のスレッドスタック、 もう1つは対応する STREAM dblk 構造体であることがわかりました。
[11.4.10/1]@p.xxx 〜 [11.4.10/3]@p.xxx
::kmem_verify dcmd は、 kmem アロケータが実行時に実施するのと、 同様のチェックを実施します。 フラグ設定に応じた全 kmem キャッシュの検証や、 特定の kmem キャッシュの検証といった用途に使用します。
::kmem_verify により、 メモリ破壊の問題を (kmem キャッシュ毎に) 分離することが可能です。
::kmem_verify 出力から、 kmem_alloc_24 キャッシュでメモリ破壊が生じていると思しきことがわかります。 特定の kmem キャッシュを指定した場合、 ::kmem_verify はより詳細な情報を出力します。
[11.4.10/4]@p.xxx 〜 [11.4.10/6]@p.xxx
問題調査の次の手順は、 メモリ破壊が生じていると思しきバッファ領域の検証です。
この例では、 0xdeadbeef であるべきバッファ領域の先頭に、 0 が書き込まれているために、 メモリ破壊とみなされていることがわかります。 kmem_bufctl_audit_t 領域の情報を使えば、 直近にこのバッファを利用した処理に関する手がかりが得られます。
他の手法としては、 ::kgrep dcmd を使って、 このバッファの先頭アドレスを保持しているデータ領域や、 スレッドスタック等を特定する手もあります。
(1) 割り当て済み領域であれば 0xdeadbeef ではなく 0xbaddcafee である筈であること、 (2) redzone バイト (0xbb) が存在しないこと、 (3) 0x703785a0 XOR 0x84d9714e = 0xf4eef4ee であることなどから、 対象領域が未使用領域であると判断できる。
[11.4.11/1]@p.xxx 〜 [11.4.11/6]@p.xxx
KMF_AUDIT が設定されている場合、 カーネルメモリアロケータは、 割り当て・解放の実施を記録します。 KMF_AUDIT と KMF_CONTENTS の両方のフラグが立っている場合、 割り当て・解放領域中の実データの一部を記録した「コンテントログ」(content log) を記録します。 コンテントログの説明は本書の範囲を超えるため、 ここでは「トランザクションログ」(transaction log)について説明します。
KMF_CONTENTS
トランザクションログに関して MDB が提供する一番単純な機能は、 kmem_log walker による、 トランザクションログの kmem_bufctl_audit_t 領域ポインタ一覧表示でしょう。
kmem_log
もう少し品の良いトランザクションログの参照方法は、 ::kmem_log dcmd によるものです。
::kmem_log
::kmem_log の出力は、 timestamp 情報の降順で整列されています。 ADDR 欄は kmem_bufctl_audit_t 領域の、 BUFADDR 欄は割り当て・解放対象バッファ領域のアドレスです。
これらの記録は、 バッファ領域に対する割り当て・解放の処理実施を表します。 特定のバッファでメモリ破壊が発生した場合、 トランザクションログ内で対応するバッファの記録を探すことで、 他にも同一バッファの割り当て・解放に関与しているスレッドが無いか、 確認することができます。 この情報を元に、 バッファの割り当て・解放前後に発生したであろう処理の流れを、 組み上げることができます。
[11.4.11/7]@p.xxx 〜 [11.4.11/11]@p.xxx
::bufctl -a dcmd は、 kmem_log walker によるトランザクションログを、 バッファアドレスでフィルタリングできます。
::bufctl -a
対象としているバッファに対する割り当て・解放が、 複数回実施されているのがわかります。
kmem のトランザクションログは、 カーネルメモリアロケータによって記録された、 不完全な情報である点に注意してください。 ログ領域を一定サイズに保つために、 古い情報は順次破棄されます。
特定スレッドにおける割り当て・解放の記録を参照する場合は、 ::allocdby や ::freedby 等の dcmd が使用できます。
::allocdby
::freedby
kmem_bufctl_audit_t 情報を使うことで、 特定のスレッドにおける直近の活動状況を知ることができます。
単純に実施スレッドでのフィルタリングなら、 ::bufctl -t THREAD で実現可能。 但し ::bufctl には、 割り当て・解放によるフィルタリング機能は無い模様。
::bufctl -t THREAD
::bufctl
[11.5/1]@p.xxx
MDB に関する説明なので割愛