※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( 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" を元にしています。
なお、個人的に興味深いと思った点に関しては、 勝手に掘り下げた話を展開します。
[10/1]@p.xxx
グローバルな中央集約されたプールで管理される物理メモリが、 Solaris システム内でのメモリ消費に対して、 どのように割り当てられるのかを説明します。
[10.1/1]@p.xxx
Solaris での物理メモリ割り当ては、 中央集約された管理プールと、 システム内の各種消費主体との間でやり取りされます。 page scanner デーモンは、 メモリ不足を防ぐために、 割り当てに介入します。
[10.1.1/1]@p.xxx 〜 [10.1.1/2]@p.xxx
システム起動時の物理メモリは、 最重要の物理メモリプールである freelist に、 ページサイズ毎チャンク形式で保持され、 主に以降で述べる3つの形式で割り当てられます。
(1) プロセスへの anonymous メモリ割り当て: もっとも典型的な freelist からの割り当ては、 各プロセスにおけるヒープやスタック、 共有メモリに使用される anonymous メモリとしての割り当てです。 anonymous メモリは退避(= pageout)可能で、 マッピングが解除されるか、 page scanner デーモンによって回収されることで、 freelist に返却されます。
『カーネルスレッド用スタックとしての割り当ても、 少量の anonymous メモリ割り当て扱い』とのことだが、 別にここで言及しなくても良い気がしないでもない。
後述する『freelist に返却されない』カーネル用途への割り当てと、 使用後(=スレッド実行終了)には『freelist に返却される』点での、 対比として記述する必要があったのか?
[10.1.1/3]@p.xxx 〜 [10.1.1/6]@p.xxx
(2) ファイルシステムの『ページキャッシュ』: ZFS 以外のファイルシステムでは、 ファイル内容のキャッシュのために、 ページキャッシュ(page cache)が使用されます。 利用可能な物理メモリを消費して、 ページサイズ毎チャンクでの、 ファイルデータ内容のキャッシュを行います。 freelist から割り当てられ、 ファイルからデータが読み込まれたページは、 segmap キャッシュ/プロセスアドレススペースへのマッピング/cachelist のどれか1つに属します。
cachelist はページキャッシュの要で、 アドレススペースにマッピングされていないファイルのページは、 cachelist に属します。
segmap は、 第1レベルの高速ファイルシステム read/write キャッシュです。 freelist から割り当てられ、 ファイルからデータが読み込まれたページは、 segmap によるキャッシュとして、 read/write システムコール実行時に利用されます。 segmap キャッシュ中のページは、 segmap キャッシュに空きを設けるために、 いずれは cachelist に移動されます。
SPARC システムでは、 cachelist は物理メモリの 12% 程度で、 segmap と共に、 ファイルデータをキャッシュするために使用されます。 read/write システムコール発行時には、 物理メモリ中のファイルデータの最大 12% が segmap に、 残りが cachelist に属します。
ZFS では、 継続的な read/write システムコール受付と並行して、 トランザクション書き出し用に別途ファイル内容を維持する必要がある点が、 『other than ZFS』となっている理由と思われる。
read/write システムコールにおいて、 ページキャッシュ(相当)を利用しているのは、 他のファイルシステムと共通の筈。
『The cachelist is typically 12% of the physical memory size』と、 『up to 12% of the physical memory file data resides in ....』 の関係は、上記翻訳で合っている?
[10.1.1/7]@p.xxx 〜 [10.1.1/8]@p.xxx
プロセスアドレススペースへのマッピング (=メモリマップドファイル)の場合も、 freelist から割り当てられ、 ファイルからデータが読み込まれたページは、 アンマップ (明示的な unmap あるいは madvise) されれば cachelist へ、 システムのメモリ不足が発生すれば freelist に移動されます。
freelist が枯渇した場合、 最古のページが cachelist から割り当てられます。 これにより、 空きメモリ領域をキャッシュとして有効に利用できると同時に、 他の用途への柔軟な流用が可能になります。
『最大キャッシュサイズを設定』する方式の場合、 たとえメモリに空きがあって、 最大サイズ以上にはキャッシュできないため、 メモリ搭載が I/O 性能向上に寄与しない可能性がある。
更に『ファイルキャッシュ』用の領域が、 それ以外の用途向け領域から独立している場合は、 キャッシュ消費が少ない状況であっても、 他の用途に流用できない可能性もある。
[10.1.1/9]@p.xxx 〜 [10.1.1/10]@p.xxx
(3) カーネルへの割り当て: 『プロセス一覧』のような、 カーネル内部での情報管理のためのメモリ消費は、 vmem や slab といった独自アロケータを使って、 freelist から割り当てます。 プロセス (=ヒープ/スタック) やファイル (=ページキャッシュ) への割り当てと異なり、 カーネルへの割り当てでは freelist への返却は滅多に行わず、 割り当て/解放は、 サブシステムとカーネルアロケータの間で実施されます。
カーネルに割り当てられたメモリは、 大概が退避(pageout)不可なので、 page scanner デーモンによる管理対象にはなりません。 システムのメモリが不足した場合は、 カーネルアロケータによって、 freelist への返却が行われます。
通常のユーザプログラムが、 malloc(3)/free(3) を使用して、 sbrk(2) の直接呼び出しや、 ヒープ領域の解放を行ったりしない点との相似性がある。
malloc(3)
free(3)
sbrk(2)
[10.2/1]@p.xxx 〜 [10.2/3]@p.xxx
本節では、 Solaris における物理メモリ管理の基本単位となる『ページ』(page)に関して、 ページの構成、検索、freelist における管理方法について説明します。 物理メモリは、ページ単位に分割されます。 Solaris において、 アクティブ (=有効なデータを保持している) なページとは、 『ファイル(vnode)とメモリとのマッピング』を表しており、 『vnode へのポインタ』と『ファイル内 offset』の2つの値で識別されます。
HAT およびアドレス空間(address space)層は、 物理ページと仮想アドレス空間の対応付けを管理します。 vnode/offset 対でのページ管理では、 メモリ内容を backing store と同期すれば、 ページを他用途に再利用可能になる、 という再利用上の利便性があります。
例えば、 ヒープ領域に使用されていた物理メモリの再利用では、 swap へのヒープ内容の書き出しが、 通常ファイル向けページキャッシュに使用されていた物理メモリの再利用では、 ページ内容が更新されていた場合に限り、 対応するファイル (= backing store) への内容書き出しが実施されます。
本文中では、 "the vnode/offset pair is the backing store for ..." という表現をしているが、 Chapter 9: Virtual Memory における "9.8.1 swapfs Implementation" で説明したように、 anonymous メモリの場合は、 vnode が必ずしもデバイス上のファイルと直接結びついているわけではない。
『backing store への書き出し』における 『ページ内容の更新』判定の最適化に関しては、 Chapter 14: File System Framework"での VMODSORT の話題も。
[10.2.1/1]@p.xxx 〜 [10.2/2]@p.xxx
ページは、 vnode/offset 対を識別子とした、 グローバルなハッシュリストで管理されます。 ページ検索関数は、 vnode/offset を引数に、 検索結果のページ構造体 (page_t) へのポインタを返却します。 『グローバルなハッシュリスト』は、 リンクリストへのポインタから成る配列です。 page_hash 配列を起点に、 vnode/offset 対を識別子としてページ構造体を検索します。
page_t
page_hash
anonymous memory layer での、 仮想 vnode 割り当てにおいて、 複数の vnode に分散させていたのには、 グローバルハッシュでのハッシュ値の分散を、 担保する役割もあるものと思われる。
[10.2.1/3]@p.xxx 〜 [10.2.1/5]@p.xxx
page_find() によるページ検索は以下の流れで実現されます。
PAGE_HASH_FUNC()
PAGE_HASH_SEARCH()
PAGE_HASH_SEARCH() マクロの定義は、 usr/src/uts/common/vm/vm_page.c に移動。 統計情報採取の有無で、 マクロ定義を切り替えている模様。
usr/src/uts/common/vm/vm_page.c
[10.2.2/1]@p.xxx 〜 [10.2.2/2]@p.xxx
ページ構造体の定義は、引用コードの通りです。
ページ構造体には、 page scanner が被参照性や更新有無を判定するためのフラグビットや、 HAT固有情報を参照するためのフィールドもあります。
論理的な機能上の問題ではないけれど、 ページ構造体の先頭に、 vnode/offset や、 ハッシュテーブルやリスト管理用のリンクフィールドが固まっていることで、 引き当て/追加/削除における参照での、 キャッシュ衝突→実行性能低下となるケースが回避できる。 (リスト要素が少なくても、これが意外と侮れない影響を持っている)
[10.2.3/1]@p.xxx 〜 [10.2.2/3]@p.xxx
freelist と cachelist は、 アドレス空間にマッピングされておらず、 且つ page_free() によって解放済みのページを管理します。 これらで管理されるページは、 vnode/offset 対と結びついた有効なデータを持つ 『キャッシュ』として機能するものの、 vmstat(1M) 出力上は free 欄に計上されます cachelist 中のページは有効なファイルデータを保持しているので、 本当の意味での free ページではありませんが、 freelist が枯渇した場合には、 データが破棄され freelist に移動されます。 cachelist は、メモリのキャッシュ利用に関する良い例です。
page_free()
vmstat(1M)
freelist で管理されるページは、 破棄と、vnode ハッシュリストからの削除が済んでいて、 vnode/offset 対との関連付けを持ちません。 ユーザプロセスやカーネルから参照されなくなったページでも、 vnode/offset 対との関連付けが維持されるケースが多いため、 freelist のサイズは通常あまり大きくありません。 ヒープ/スタック等の anonymous メモリ用のページが、 ユーザプロセス終了契機で freelist に追加されるケースが典型的です。
cachelist は、 有効な vnode/offset 対を保持したページの、 ハッシュリストです。 cachelist 上のページは、 vnode/offset 対を引数とする page_lookup() により検索され、 引き当てられた場合は cachelist から除外されます。 cachelist から取り戻された (reclaim) ページは、 vmstat(1M) 出力上は re 欄に計上されます。
page_lookup()
[10.2.4/1]@p.xxx
Solaris カーネルでは、 『連続した物理メモリ』を管理する、 複数のセグメントをリスト管理しています (近年のハードウェアは、非連続な物理メモリを提供しています)。 物理メモリセグメントは、 起動時の登録以外にも、 動的に追加/取り外しが行われます。
NUMA (Non Uniform Memory Architecture) での locality group (lgrp) 周りの話だと思われる。
NUMA 構成のシステムの場合、 各コントローラにぶら下がるメモリの物理アドレスは、 スロットやシステムボード毎で固定されているのかな?
[10.2.5/1]@p.xxx
Soalris の仮想メモリシステムは、 ページ管理/操作機能を、 中枢機能として実装しています。 これらの機能は、 セグメントドライバやファイルシステムにおいて、 ページの作成/削除/変更に使用されます。
page_create_va() 関数は、 引数で指定された割り当てページ数に従い、 freelist から確保したページ群を、 リンクリスト形式で返します。 また、引数で指定された仮想アドレスにより、 ページのカラーリング (coloring) を実現することもできます (詳細は後述)。
page_create_va()
『ページのカラーリング』は、 chapter 9 の口頭説明の際にも触れた、 旧関数 page_create() での以下のコードによる、 アドレスの分散のことを指していると思われる。
page_create()
page_t * page_create(vnode_t *vp, u_offset_t off, size_t bytes, uint_t flags) { : : random_vaddr = (caddr_t)(((uintptr_t)vp >> 7) ^ (uintptr_t)(off >> PAGESHIFT)); kseg.s_as = &kas; return (page_create_va(vp, off, bytes, flags, &kseg, random_vaddr)); }
[10.2.6/1]@p.xxx
カーネル内部でのメモリ獲得では、 PG_WAIT フラグ付きで page_create_va() が呼び出された場合、 フリーなメモリが throttlefree 値 (デフォルト値は minfree と同値)を上回るまで、 メモリ割り当ては保留され、 呼び出しはブロックされます(= ページスロットル)
PG_WAIT
throttlefree
minfree
lotsfree
desfree
[10.2.7/1]@p.xxx 〜 [10.2.7/2]@p.xxx
キャッシュ上で他のページと領域が重複した場合、 キャッシュ効率が低下し、 ホットスポットとなり得ます。
キャッシュにおけるページの最適配置は、 アプリケーションのメモリアクセスパターンに依存します。 ランダムアクセスかもしれませんし、 ストライドアクセス(行列の列アクセスのような一定間隔のアクセス) かもしれません。 Solaris では、 複数の配置アルゴリズムが選択可能で、 デフォルトアルゴリズムは、 全般的なケースでの最適配置を目指すものです。
[10.2.7/3]@p.xxx
UltraSPARC-I/-II の場合、 仮想アドレスベースの L1 キャッシュ(16K バイト) 物理アドレスベースの L2 キャッシュ(512K 〜 8Mバイト) から構成されます。 L2 キャッシュは 64 バイト単位の「ライン」で構成され、 データ転送もライン単位(64バイト)で実施されます。 L1 キャッシュサイズは vac_size、 L2 キャッシュサイズは ecache_size カーネル変数で参照できます。
vac_size
ecache_size
SPARC 系は usr/src/uts/sun4/os/mlsetup.c で以下のものが定義されている。
dcache_size
_linesize
icache_size
x86 系は usr/src/uts/i86pc/os/startup.c で以下のものが定義されている (L1 cache 系情報は無し?)
l2cache_sz
_linesz
[10.2.7/4]@p.xxx 〜 [10.2.7/5]@p.xxx
L2 キャッシュは、 ページサイズ単位に分割した有限の「スロット」から構成されます。 メモリ配置によるキャッシュ効率の増減を説明するに当たり、 8K バイトページサイズのアーキテクチャにおける、 32K バイトの L2 キャッシュの保有、 つまり4つの「スロット」を仮定します。 メモリ〜キャッシュにおける実際の読み書き単位は、 8K バイトではなく 64 バイト単位なので、 512 の「スロット」で構成されます。
L2 キャッシュは物理アドレスベースなので、 32K バイト境界での物理アドレス(例えばアドレス 0 と 32678)アクセスは、 同一のキャッシュラインへのアクセスとなります。 同一キャッシュラインへのアクセスは、 フラッシング(flushing)/ピンポン(ping-pong) と呼ばれる現象を生じさせるため、 キャッシュよりも 10 〜 20倍低速な実メモリの性能まで、 実効性能が低下します。 例示で用いたアクセスパターンでは、 L2 全体サイズが 32K バイトであるのに対して、 限定的なキャッシュラインのみしか有効に使用できず、 性能に顕著な影響を生じます。
メモリ〜スワップ間での pagein/pageout 頻発では、 「スラッシング」(thrashing: (罰などとして)何度も打つ[たたく, 殴る]こと) とも呼ばれるが、 flusing や ping-pong とどちらが一般的なのかな?
[10.2.7/6]@p.xxx 〜 [10.2.7/7]@p.xxx
先述した例では、 物理アドレスから見た「一定パターンでのアクセス」を見ましたが、 実際のプログラムは、 物理アドレスではなく仮想アドレスベースで実装されます。 そのため、 仮想アドレス〜物理アドレスのマッピングの際に OS が配慮しないと、 先述した例のような性能劣化が生じます。
特別な配慮無しで実装した場合、 ページの要求のあった仮想アドレスに対して、 freelist に列挙された物理ページの順に、 物理アドレスが割り当てられることになります。 (1) OS起動時点では、 整列されたページからの順次割り当てになるため、 容易に ping-pong 現象を生じさせます。 (2) 稼働後に一定時間が経過した後は、 freelist 中の並びもランダムになりますし、 個別のアプリケーション毎に異なるページ配置になりますから、 実行性能傾向は都度異なります。 初期の Solaris では、 実行の都度、 最大 30% 程度の性能差が生じていました。
[10.2.7/8]@p.xxx 〜 [10.2.711]@p.xxx
性能の向上と一貫性のために、 Solaris での仮想アドレスに対する物理ページ割り当ての際には、 「ページ色分け」(page coloring)アルゴリズムを使用します。 ランダムな物理ページ割り当てではなく、 予め決めておいた仮想アドレスと割り当て物理ページの対応付けを使って、 ページの割り当てを行います: freelist は、 (物理アドレスを使う)キャッシュの各「スロット」に対応する色(color) を持った複数の容器(bin)から構成されます。 bin の数は(物理アドレスを使う)キャッシュサイズを、 ページサイズで割った値です。
freelist へのページ返却の際に、 page_free() は物理アドレスを元に、 対応する色の bin に物理ページを割り当てます。 freelist からのページ割り当ての際には、 仮想アドレス〜物理アドレスの対応付けによって選択された色の bin から割り当てられます。 対応付けでの必要上、 物理ページ割り当て関数の引数には、 仮想アドレスが必要になります。
新規の物理ページは、 page_create_va() によって割り当てられます。 この関数の引数には、 割り当てられた物理ページのマッピング先仮想アドレスが指定されます。 この引数を使って bin の色が算出されます。
旧来の page_create() 関数は残してあるので、 これを使用するサードパーティ製カーネルモジュールは機能し続けますが、 bin の色分けに乱数を使う page_create() は、 性能劣化要因となるため、 新規コードは page_create_va() を使うべきです。
[10.2.7/12]@p.xxx 〜 [10.2.7/15]@p.xxx
アプリケーション毎にメモリアクセスパターンが異なるため、 全てのアプリケーションに適した単一のアルゴリズムは存在しません。 Solaris の色分けアルゴリズムは、 シミュレーション、ベンチマーク、顧客からのフィードバックを広範囲に行うことで、 改善を続けてきました。 1つのデフォルトアルゴリズムと、 2つのオプションアルゴリズムが提供されています。 デフォルトアルゴリズムは、 以下の観点で選択されたものです。
[10.2.7/16]@p.xxx 〜 [10.2.7/17]@p.xxx
デフォルトアルゴリズムでは、 可能な限りページをキャッシュに散在させるために、 ハッシュアルゴリズムを使用しています。
カーネルパラメータ consistent_coloring によって、 アルゴリズムを選択可能です。
consistent_coloring
「仮想アドレス ⇒ bin の色」変換は、 AS_2_BIN() マクロで定義されている。
AS_2_BIN()
usr/src/uts/sun4/vm/vm_dep.h
usr/src/uts/i86pc/vm/vm_dep.h
x86 系は、 アルゴリズムの切り替え(consistent_coloring)は無く、 ハッシュアルゴリズムのみの一択
[10.2.7/18]@p.xxx
ページの色分けは、 メモリアクセスが頻発する科学技術計算系処理等でないと、 アルゴリズムによる性能差が顕著には表れず、 一般的なアプリケーションであれば、 デフォルトアルゴリズムで充分な性能が出ます。 実行性能が要求される科学技術計算系処理を実施する場合、 複数のアルゴリズムから最適なものを選択してください。 また、 同一アルゴリズムでも、 実行毎に性能傾向が異なる場合があるので、 可能な限り多くの実測結果を参照してください。
以下、page_get_freelist() での処理の流れの概要:
page_get_freelist()
/* 仮想アドレスに応じた bin の算出 */ AS_2_BIN(as, seg, vp, vaddr, bin, szc); while(mnode = lgrp_memnode_choose()){ page_get_mnode_freelist(mnode, bin, ....); DTRACE_PROBE4(page__get, lgrp, mnode, bin, flags); } if (szc == 0 && ((flags & PGI_PGCPSZC0) == 0)){ return NULL; } if (!(flags & PG_LOCAL)) { /* 他の lgrp からの割り当て */ while ((mnode = lgrp_memnode_choose(&lgrp_cookie)) >= 0) { page_get_mnode_freelist(mnode, bin, ....); DTRACE_PROBE4(page__get, lgrp, mnode, bin, flags); }
[10.3/1]@p.xxx 〜 [10.3/6]@p.xxx
メモリ不足が発生した場合、 ページスキャナ(page scanner)は、 直近のアクセスが無いメモリをアドレス空間から横取り(steal)し、 内容をバックストアと同期(anonymous メモリの場合は swapに退避)した上で、 メモリを解放します。
ページスキャナは、 プロセス毎のメモリ消費パターンやワーキングセットには、 関与しません。 物理メモリの参照状況のみで判断します。 この方針は "global page replacement" と呼ばれ、 プロセスベースの管理方針は "local page placement" と呼ばれます。
Solaris カーネルにおけるメモリ管理方針は、 これまでに2回だけ大きな変更がありました。
fastscan
handspreadpages
詳細は、ページスキャナ実装に関する節で説明します。
[10.3.1/1]@p.xxx 〜 [10.3.1/3]@p.xxx
ページスキャナによる各ページの監視では、 ハードウェア MMU 機能が各ページ毎に持っている、 最後のビットクリア以後の参照/変更の有無を判定する2つのビットによって、 直近アクセスの有無を実現しています。
ページスキャナはカーネルスレッドとして実行され、 利用可能ページが閾値を下回った契機で起動されます。 全物理ページのリストの末尾と冒頭を繋げて円環状にした上で、 物理ページを順次走査し、 直近のアクセスがないページの内容をバックストアに退避した上で、 当該ページを解放します。
二針時計(two handed clock)アルゴリズムと呼ばれる走査方法では、 時計回りに移動する二つの針のうち、 前側が参照/変更監視ビットをクリアし、 少し遅れて前側の針を追う後側の針が、 両ビットを検証します。 参照/変更ビットがセットされてないページは、 バックストアへの内容の退避/同期の上で解放されます。
ページスキャナ起動の閾値は lotsfree で管理。 デフォルトは総メモリの 1/64 or 512K。
ページリスト上での各針の進行度合いはシステムの空きメモリ量で、 二つの針の間隔は動的に算出される handspreadpages パラメータで管理。
[10.3.2/1]@p.xxx 〜 [10.3.2/2]@p.xxx
ページアウトアルゴリズムを制御するパラメータには、 システム起動時に確定するものもあれば、 実行状況に応じて動的に算出されるものもあります。
先述した二針時計アルゴリズムの二つの針に関するパラメータ群は、 ページ走査の量と、二つの針の開き具合を制御します。
[10.3.2/3]@p.xxx 〜 [10.3.2/4]@p.xxx
Solaris 9 からは、 ページスキャナに対して、 毎秒あたりの最大走査ページ数の上限が算出されるようになりました。
二針時計の針の開き具合(ページ数)は、 handspreadpages に格納されます。 ページスキャナ起動時に算出され、 pageout_new_spread に格納された値と、 ページスキャナの実行状況から上限値が算出されます。
pageout_new_spread
パラメータチューニングで、 明示的な handspreadpages 指定が無い場合の初期値は、 「64MB 相当のページ数分の針の開き」
/* * @usr/src/uts/i86pc/sys/vm_machparam.h * @usr/src/uts/sun4/sys/vm_machparam.h */ #define MAXHANDSPREADPAGES ((64 * 1024 * 1024) / PAGESIZE)
ページスキャン終了時に、 pageout_new_spread が 0 の場合は、 以下で再初期化した上で、 関連パラメータの再算出。
/* * pageout_scanner()@usr/src/uts/common/os/vm_pageout.c */ pageout_rate = (hrrate_t)pageout_sample_pages * (hrrate_t)(NANOSEC) / pageout_sample_etime; pageout_new_spread = pageout_rate / 10; setupclock(1);
備考の続き
実際に各種チューニングパラメータの算出を行っている usr/src/uts/common/os/vm_pageout.c の setupclock() のコメントに、 方針等が詳しく記述されている。
setupclock()
setupclock() の呼び出しは、 (1) 起動時、(2) ページスキャン終了時に pageout_new_spread が 0 だった場合以外に、 (3) メモリの動的増減が発生した場合も呼び出される模様: usr/src/uts/common/os/mem_config.c の kphysm_add_memory_dynamic() および kphysm_del_cleanup()。
kphysm_add_memory_dynamic()
kphysm_del_cleanup()
[10.3.2.1/1]@p.xxx 〜 [10.3.2.1/2]@p.xxx
空きメモリが lotsfree (+ バッファ分の deficit) を下回るのを契機に、 ページスキャナが動作します。 初期の deficit 値は0かそれに近い値ですが、 大量のメモリ割り当ての都度、 以後のメモリ枯渇に対する先手を打つために deficit 値が更新されます。
deficit
初期走査率は、 最も低速な slowscan ですが、 メモリ不足(= lotsfree の下回り具合)の度合いに応じて、 より高速な走査を行います。 最大走査ページ数は pageout_new_spread で初期化され、 fastscan に格納されます。
slowscan
説明の簡略化のためだと思われるが、 fastscan に対する maxfastscan、 slowscan に対する maxslowscan の関係が説明されていない。
maxfastscan
maxslowscan
コメントの続き
以下、 usr/src/uts/common/os/vm_pageout.c の setupclocks() における fastscan の算出処理:
setupclocks()
if (init_mfscan == 0) { if (pageout_new_spread != 0) maxfastscan = pageout_new_spread; else maxfastscan = MAXHANDSPREADPAGES; } else { maxfastscan = init_mfscan; } if (init_fscan == 0) fastscan = MIN(looppages / loopfraction, maxfastscan); else fastscan = init_fscan; if (fastscan > looppages / loopfraction) fastscan = looppages / loopfraction;
looppages は総ページ数、 loopfraction の初期値は 2 なので、 どんなに高速なページ走査実行を指示されても、 全ページを網羅するには最低2秒を要する (fastscan は秒あたりのページ走査数)。
looppages
loopfraction
slowscan の算出は以下の通り:
/* * maxslowscan のデフォルト値は 100 */ if (init_sscan == 0) slowscan = MIN(fastscan / 10, maxslowscan); else slowscan = init_sscan; if (slowscan > fastscan / 2) slowscan = fastscan / 2;
fastscan の半分(= 全ページ数の 1/4)以下を強制されるので、 低速での全ページ網羅には最低でも4秒を要する。
[10.3.2.1/3]@p.xxx 〜 [10.3.2.1/5]@p.xxx
ページの走査は、 空きページ数の lotsfree からの下回り具合に応じて、 最低速の slowscan から、 最高速の fastscan の間で実施されます。 空きメモリがゼロになることはありませんが、 処理上では空きメモリの範囲を 0 から lotsfree の間で算出しています。
搭載メモリ量を 1GB、 deficit を 0 と仮定した場合、 空きメモリが 16MB を下回ることでページスキャナが起動され、 毎秒 100 ページを走査します。 slowscan は固定値ですが、 fastscan は動的に算出されます。 空きメモリが 12MB を下回ると、 (提示された式に従い)走査率が増加されます。
fastscan 値は mdb で確認可能です。
以下、デフォルト値ベースでの各種パラメータ値:
MIN(fastscan / 10, maxslowscan)
[10.3.2.1/6]@p.xxx 〜 [10.3.2.1/7]@p.xxx
(ページサイズ8KB の SPARC ベースで)ページ数表記にした場合、 空きページ数 1,536 (12MB) と、 lotsfree 値 2,048 (16MB) から、 秒間の走査ページ数は 31,994 ページとなります。
空きメモリ不足時は、毎秒4回の走査が実施されますが、 空きメモリが minfree を下回った場合、 ページ確保要求の都度走査が実施され、 最低でも minfree の空きメモリの維持に勤めるようになっています。
[10.3.2.2/1]@p.xxx 〜 [10.3.2.2/2]@p.xxx
二針時計アルゴリズムにおける前側針と後側針の時間差は、 前後針の距離(ページ数)と秒あたりのページ走査数で変動します。 最もアクセス時期の古いページのみを横取りしたいので、 非常に活発なページのみが居残る(間隔が短い)よりも、 長時間に渡ってアクセスのないページのみが横取りされる(間隔が長い) 方が理想的な挙動です。
走査ページ数によって、 前後の針の間隔は、 数秒から数時間に及びます。
[10.3.3/1]@p.xxx
ページスキャナに追加された最適化により、 広範に共有されているライブラリからの横取りを防ぎます。 ページ毎の参照カウントを確認し、 閾値以上のプロセスからの参照がある場合は、 ページアウト対象から除外されます。 閾値を保持する変数 po_share は 8 で初期化され、 走査が一巡した際のページ解放の有無に応じて、 8 〜 134217728 の間で増減します。
po_share
pageout_scanner() ⇒ checkpage() ⇒ hat_page_checkshare() において、 指定された page_t と po_share を使って比較 (hat_page_checkshare() は CPU アーキテクチャ毎実装)。
pageout_scanner()
checkpage()
hat_page_checkshare()
ページ解放の有無による po_share の増減は、 二倍(po_share <<= 1)か、 半減(po_share >>= 1)を実施。
po_share <<= 1
po_share >>= 1
[10.3.3.1/1]@p.xxx
走査率に対するCPU利用上限によって、 ページアウトデーモンが過剰に CPU 時間を消費するのを防止します。 2つの内部パラメータ min_percent_cpu(空きメモリが lotsfree: 4%)から max_percent_cpu (空きメモリが無い: 80%)の間で、 ページスキャナが消費する(単一CPUの)CPU時間が制御されます。
min_percent_cpu
max_percent_cpu
min_percent_cpu から導出した min_pageout_ticks、 max_percent_cpu から導出した max_pageout_ticks を使って、 想定消費 CPU 時間 pageout_ticks を算出。
min_pageout_ticks
max_pageout_ticks
pageout_ticks
/* * schedpaging()@usr/src/uts/common/os/vm_pageout.c */ pageout_ticks = min_pageout_ticks + (lotsfree - vavail) * (max_pageout_ticks - min_pageout_ticks) / nz(lotsfree);
[10.3.4/1]@p.xxx
maxpgio パラメータは、 swap デバイスに対する I/O 要求頻度を制御し、 swap デバイスの飽和を防ぎます。 毎秒 I/O 数のデフォルトは、 x86 で 40、 SPARC で 60 です。
maxpgio
maxpgio = (DISKRPM * 2) / 3;
usr/src/uts/i86pc/sys/vm_machparam.h でも usr/src/uts/sun4/sys/vm_machparam.h でも、 DISKRPM マクロの定義は共に 60 なので、 maxpgio は (60 * 2) / 3 = 40。
DISKRPM
変更履歴情報上も、 該当行が更新された形跡は無いので、 (1) 誤植か、 (2) オープンソース化以前に変更されたかのいずれかか?
[10.3.4/2]@p.xxx
ページアウトデーモンは、 ページ走査中に検出された、 未書き出しのファイルキャッシュを保持するページも、 書き出し対象にするため、 maxpgio パラメータは、 間接的にファイルシステムのスループットも制限します。 ファイルシステムの I/O 要求は、 ユーザプロセスによってキューへの投入/書き出しが実施されるため、 maxpgio による制限対象にはなりません。 しかし、 大量のページ書き出しが発生し、 多数のダーティページがメモリ上に保持された場合、 ページアウトデーモンがダーティページを検出し、 I/O をキューへの投入します。 結果として、 ファイルシステムのスループットが maxpgio によって制限される可能性があります。
[10.3.4.1/1]@p.xxx
ページアウトデーモンを制御するパラメータは以下の通りです。
[10.3.5/1]@p.xxx 〜 [10.3.5/2]@p.xxx
ページスキャナは、 プロセス番号 2 の "pageout" プロセスに属する、 ページの走査を行うスレッドと、 swap デバイスへの I/O をキューに投入するスレッドから構成されています。 空きメモリが不十分な場合に、 カーネルの callout 機能(19.2 節で詳細を説明)を使用して、 ページ走査スレッドが起動されます。
ページスキャナの schedpaging() 関数は、 callout 機能経由で毎秒4回呼び出され、 空きメモリが閾値を下回っていないか確認し、 必要に応じてページ走査スレッドをアクティブにします。 callout 契機でのアクティブ化以外に、 空きメモリが throttlefree を下回った場合も、 ページ走査スレッドがアクティブになります。
schedpaging()
callout 機能は、 usr/src/uts/common/os/callout.c で実装されている模様。
schedpaging() 末尾での timeout() 呼び出しにより、 schedpaging() 自身を callout テーブルに登録している。
timeout()
throttlefree 値に関する説明:
カーネル内部でのメモリ獲得では、 PG_WAIT フラグ付きで page_create_va() が呼び出された場合、 フリーなメモリが throttlefree 値 (デフォルト値は minfree と同値)を上回るまで、 メモリ割り当ては保留され、 呼び出しはブロックされます(= ページスロットル) from "10.2.6 The Page Throttle"
[10.3.5/3]@p.xxx 〜 [10.3.5/4]@p.xxx
schedpaging() 関数は、 走査対象ページ数(desscan: 10.3.2.1 節参照)と、 ページ走査スレッドが消費可能な CPU 時間 (pageout_ticks: 10.3.3.1 節参照) を算出し、 条件変数(conditional variable) 経由でページ走査スレッドをアクティブにします。
desscan
ページ走査スレッドは、 二針時計アルゴリズムに従い、 参照/変更ビットをクリアする前側針を1つ進め、 次に変更状況を検証する後側針を1つ進めます。 ページが変更されている場合は、 ページアウトスレッドによって処理される、 ダーティページがキューに投入されます。 (変更が無く且つ)ページが参照されていない場合は、 当該ページを解放します。
現状の実装では、 「ページ走査スレッドをアクティブ」にするための条件変数は、 wakeup ではなく、 proc_pageout->p_cv。
wakeup
proc_pageout->p_cv
usr/src/uts/common/os/vm_pageout.c の変更履歴情報上も、 wakeup なる条件変数があった痕跡は見られない。
前後の針を1つ進める毎に、 当該ページに対して checkpage() を呼び出し、 ページの参照/変更状況をチェックしている。 本文中の "check_page()" は誤植と思われる (図版は checkpage() と記載)。
check_page()
「参照/変更ビットをクリアする前側針を1つ進め」の時点で、 変更ビットが立っている場合は、 書き出しが必要な筈?
/* * checkpage()@usr/src/uts/common/os/vm_pageout.c */ /* * If the page is currently dirty, we have to arrange * to have it cleaned before it can be freed. */ if ((ppattr & P_MOD) && pp->p_vnode) { : : if (!queue_io_request(vp, offset)) {
[10.3.5/5]@p.xxx
ダーティページがキューに投入され、 独立した page-out スレッドによって、 バックストアに内容が退避されます。 内容退避は独立スレッドによって実施されるため、 swap への書き出し待ちでも 走査スレッドがブロックされることはありません。 page-out スレッドにおける非同期 I/O 待ちは、 async_request_size (デフォルト値 256) だけ事前確保された要素を持つリストで管理されます。 async_request_size 以上の I/O 待ちや、 maxpgio を超える頻度での I/O 待ちの投入は、 ブロックされます。
async_request_size
原文では、 "deadlock can't occur while the system is waiting to swap a page out" とあるが、意図しているのは deadlock ではなく blocking の事だと思われる。
I/O 待ち要素最大数の制御パラメータは、 async_request_size ではなく async_list_size。 変更履歴上も、 最初期から async_list_size を使用している。
async_list_size
「async_request_size 以上の I/O 待ち」 をブロックするのは queue_io_request()、 「maxpgio を超える頻度での I/O 待ち」 をブロックするのは pageout()。
queue_io_request()
pageout()
queue_io_request() でブロックされたページは、 「未解放」扱いになる(けれど modified ビットの復元は行わない?)
/* * checkpage()@usr/src/uts/common/os/vm_pageout.c */ if (!queue_io_request(vp, offset)) { VN_RELE(vp); return (0); ※ 投入失敗時ルート } return (1); ※ 参照はキュー側が引き継ぐので VN_RELE() 実施無し
[10.3.5/6]@p.xxx
page-out スレッドは、 I/O 待ちキューからエントリを取り出し、 VOP_PUTPAGE() 呼び出しにより I/O を発行します。 Solaris における anonymous ページの場合、 swapfs 層での swap 書き出しを行う swapfs_putpage() が呼び出されます。 swapfs 層では、 klustsize パラメータで指定される複数ページを、 一括して書き出すために、 書き出しを遅延させます。
VOP_PUTPAGE()
swapfs_putpage()
klustsize
SPARC 向けの klustsize 定義は ページサイズベース("MMU_PAGESIZE * 16")なのに、 x86 向けの定義はサイズベース ("56 * 1024")なのは理由があるのか? あるいは単にヘッダファイル構成の関係?
MMU_PAGESIZE * 16
56 * 1024
klustsize 分のまとめ待ちで I/O を遅延させる場合、 「発行済み I/O 数」を保持する pushes (maxpgio 上限との比較向け)を、 swap_putpage() 側で減算している。 つまり、実 I/O が発行されなければ、 「サイズまとめ待ち」が多発しても、 maxpgio 制限にはひっかからない。
pushes
swap_putpage()
[10.3.6/1]@p.xxx 〜 [10.3.6/2]@p.xxx
メモリ消費を低減させるために、 対象プロセスのスレッド構造体群やプライベートメモリページの退避と、 プロセステーブルへの「swap 退避済み」フラグ設定が、 page-out 処理とは独立して、 CPU スケジューラ/ディスパッチャによって実施されます。 プロセス全体の swap 退避自体は特にコスト高な処理ではありませんが、 プロセスの実行性能には大きく影響するので、 継続的にメモリが不足する場合のみ実施されます。
システム起動時に立ち上げられたメモリスケジューラは、 平均未使用メモリが desfree を下回る状態が30秒以上継続した場合、 全体を swap に退避可能なプロセスを探します。
Memory Scheduler の実装は、 usr/src/uts/common/os/sched.c の sched()。
sched()
[10.3.6.1/1]@p.xxx
平均未使用メモリが desfree を下回る状態が、 30秒以上続いた場合、 メモリスケジューラは、 最低 maxslp 秒以上休眠しているプロセスを探し、 当該プロセス全体を swap に退避します。
maxslp
[10.3.6.2/1]@p.xxx 〜 [10.3.6.2/5]@p.xxx
以下の条件が全て成立する場合、hard swapping が実施されます。
page-out + page-in > maxpgio
hard swapping では、 非アクティブなカーネルモジュールに対して、 モジュールの unload とキャッシュの放棄を要求した上で、 必要な空きメモリが確保できるまで、 swap へのプロセス退避を実施します。
[10.4/1]@p.xxx
MDB に関する説明なので割愛