Linuxのマイナー/メジャーページフォルト: ru_minfltとru_majfltが数えるもの
Linuxのgetrusageのru_minfltとru_majfltは、処理にI/Oが必要だったかでページフォルトを分けます。2つのカウンタが数えるものと、メジャーフォルト数をディスク読み込み回数と読めない理由をman-pages 6.19基準で整理します。
/usr/bin/time -vでコマンドを実行すると、出力の途中にMajor (requiring I/O) page faultsとMinor (reclaiming a frame) page faultsの2行が出てきます。この記事では、Linuxでこの2つの数値が何を数えているのか、そしてメジャーフォルト数をディスク読み込みの回数として読むとなぜ誤るのかを扱います。
根拠にした文書は、Linux man-pages 6.19のgetrusage(2)、proc_pid_stat(5)、mmap(2)、time(1)、Linuxカーネルドキュメント(7.3.0-rc6ビルド)のpagemapのページ、GNU time 1.10のマニュアルです。ここで説明するフィールドの意味はLinuxのものです。POSIX.1はgetrusage()を規定していますが、フィールドはru_utimeとru_stimeしか定めていないため、フォルトカウンタの意味をほかのUnix系システムにそのまま当てはめることはできません。
基準は「I/Oが必要だったか」
getrusage(2)のstruct rusageでは、2つのフィールドに次のコメントが付いています。
long ru_minflt; /* page reclaims (soft page faults) */
long ru_majflt; /* page faults (hard page faults) */
manページの説明は次のとおりです。
ru_minflt: I/Oなしで処理されたページフォルトの数。再割り当てを待つページのリストからページフレームを「回収(reclaim)」することでI/Oを避ける、と説明しています。ru_majflt: 処理にI/Oが必要だったページフォルトの数。
実行中のプロセスについては、同じ値が/proc/<pid>/statの10番目のフィールド(minflt)と12番目のフィールド(majflt)にあります。proc_pid_stat(5)はそれぞれを、ディスクからメモリページを読み込む必要がなかったマイナーフォルト、その必要があったメジャーフォルトと説明しています。11番目のcminfltと13番目のcmajfltは、このプロセスがwaitで回収した子プロセスの値です。
つまりLinuxの文書が2種類を分ける基準は1つです。そのフォルトの処理にI/O(ディスクからのページ読み込み)が必要だったかどうかです。処理に時間がかかったか、アクセスしたメモリがファイルに結び付いていたかは基準ではありません。
フォルトが起きる理由: デマンドページング
mmap()は呼び出したプロセスの仮想アドレス空間に新しいマッピングを作ります。マッピングができたからといって、その範囲のページがすべてメモリに載っているわけではありません。proc_pid_stat(5)のrssの説明は、常駐セットサイズ(RSS)にはまだデマンドロードされていないページやスワップアウトされたページが含まれないと書いています。
mmap(2)のMAP_POPULATEフラグ(Linux 2.5.46以降)は、同じ事実を反対側から示しています。このフラグはマッピングのページテーブルを事前に埋め(prefault)、ファイルマッピングならファイルの先読み(read-ahead)を起こして、後でページフォルトでブロックされることを減らします。マッピングを埋められなくてもmmap()呼び出し自体は失敗しません。こうしたフラグが別にあることから、デフォルトの動作ではmmap()が戻った時点でページが埋まっているとは期待できない、という結論になります(フラグの説明から導いた推論)。MAP_LOCKEDの説明も、事前の読み込みが失敗しうるのでメジャーフォルトが後から起こりうると述べ、メジャーフォルトを許容できない場合はmmap()とmlock(2)を併用するよう勧めています。
そのため、まだ準備ができていないページに初めてアクセスするとフォルトが起き、カーネルがそれを処理します。処理にディスクI/Oが必要だったならメジャー、そうでなければマイナーとして数えます。
メジャーフォルト数がディスク読み込み回数ではない理由
ファイル入力は別のカウンタに入ります。 struct rusageにはフォルトカウンタとは別にru_inblock(Linux 2.6.22以降)があり、getrusage(2)はこれをファイルシステムが入力を行う必要があった回数と定義しています。GNU timeはこの値を%I(ファイルシステム入力)として別に出力します。GNU timeマニュアルのgrepの実行例には、46inputsと(43major+251minor)pagefaultsが並んで出ています。2つは別々のフィールドで、文書は両者の間に1対1の関係を定めていません。read(2)でファイルを読むプログラムは、マップされたページにアクセスしてデータを得ているわけではないので、その読み込みがメジャーフォルト数に現れると仮定する根拠はありません(上の定義から導いた推論)。
ディスクから来たデータでも、すでにメモリにあればマイナーです。 基準はそのフォルトの処理にI/Oが必要だったかどうかです。ファイルマッピングのページでも、フォルトの時点でデータがすでにメモリにあり、I/Oなしで処理されたならマイナーとして数えます(定義から導いた推論)。たとえば同じプログラムを続けて2回実行し、2回目のメジャーフォルトが減っていたなら、1回目に読んだファイルのデータがまだメモリに残っていたという説明と合います。ただし数値1つで理由を確定することはできず、どのページがメモリにあったかは後で紹介する別の手段で確かめる必要があります。freeが表示するメモリ項目についてはfreeのavailableの記事で別に扱っています。
ツールごとに異なる表現
出力のラベルやツールの文書の説明は、getrusage(2)の定義と表現が少し異なります。
- time(1)のGNU 1.7の説明は、
%Fをページをディスクから読み込む必要があるフォルト、%Rを有効ではないがまだほかの仮想ページに取られていないページに対するフォルトと説明しています。この場合ページのデータはまだ有効で、システムのテーブルを更新するだけでよいとしています。 - GNU time 1.10のマニュアルは、
%FをI/Oを必要とする(I/O-requiring)フォルト、つまりページが実際に主記憶の外へ移されていた場合と説明しています。-vの出力ラベルはMajor (requiring I/O) page faultsとMinor (reclaiming a frame) page faultsです。
time(1)のBUGSの節は、測定項目の選択は主に4.2BSDや4.3BSDが提供していたデータに着想を得たと述べており、getrusage(2)も構造体の定義を4.3BSD Renoから取ったと書いています。「フレームの回収」のような表現はこの系譜から来たものと考えられます(推論)。Linuxで数値を解釈するときは、getrusage(2)とproc_pid_stat(5)の基準、つまりI/Oが必要だったかどうかを使えば足ります。
数値からわからないこと
- 時間。 2つのフィールドは回数です。
struct rusageにはフォルト処理にかかった時間を入れるフィールドがありません。メジャーフォルトが多くても、それが実行時間をどれだけ延ばしたかは別の時間計測と合わせて見る必要があります。timeのreal/user/sysの読み方はreal・user・sysの記事にまとめています。 - 区間。
/proc/<pid>/statの値は、プロセスがこれまでに起こしたフォルトの累積値です。特定の処理の影響を見るには、処理の前後で2回読んで差を取る必要があります(累積値であるという説明から導いた方法)。またgetrusage(2)によれば、リソース使用量はexecve(2)をまたいで保持されるため、exec前に積み上がった値が含まれることがあります。 - 子プロセス。
RUSAGE_CHILDRENとcminflt/cmajfltは、終了してwaitで回収された子だけを含みます。まだ実行中の子のフォルトは入りません。 - どのページか。 カウンタはどのアドレスでフォルトが起きたかを教えてくれません。マッピングのどのページが現在ページキャッシュに常駐しているかは
mincore(2)で確認できると、mmap(2)のNOTESの節が案内しています。カーネルドキュメントの/proc/<pid>/pagemapは仮想ページごとに64ビットの値を返し、63番ビットがpage present、62番ビットがpage swappedです。ただしLinux 4.0以降、物理フレーム番号(PFN)を得るにはCAP_SYS_ADMIN権限が必要です。どちらの手段も現在の状態を示すだけで、過去にフォルトが起きたかどうかはわかりません。 - スワップ。 getrusage(2)は
ru_nswapをLinuxでは使われていない(unmaintained)フィールドに分類し、こうしたフィールドはカーネルが0にすると説明しています。proc_pid_stat(5)のnswapも維持されていないフィールドです。GNU timeのマニュアルは、すべてのUnixがすべての値を測定するわけではないので一部の値が0と報告されることがあると書いています。GNU timeのSwapsの値がこのフィールドから来ているなら、Linuxでは常に0になるため、Swaps: 0をスワップが起きなかった証拠として読んではいけません。GNU timeがこの値をどこから取っているかは、この記事で確認した文書には直接書かれていないので、この部分は推論です。
自分で確かめる
コマンドを1つ実行してGNU timeで値を見る方法です。
/usr/bin/time -v ./your-program
/usr/bin/time -f '%F major, %R minor' ./your-program
bashなど一部のシェルには組み込みのtimeがあるため、これらのオプションを使うには、time(1)が例に挙げている/usr/bin/timeのようにパスを指定する必要がある場合があります。
すでに実行中のプロセスは/proc/<pid>/statから読みます。1234は対象のPIDに置き換えてください。
python3 -c 'import sys; s = open("/proc/%s/stat" % sys.argv[1]).read(); f = s[s.rindex(")") + 2:].split(); print("minflt", f[7], "majflt", f[9])' 1234
2番目のフィールドcommは括弧で囲まれた実行ファイル名なので、空白が混じることがあります。そのため空白ですぐに分割せず、最後の)の後ろから分割しています。その後の最初の値が3番目のフィールドstateなので、インデックス7が10番目のminflt、インデックス9が12番目のmajfltです。処理の前後で2回実行して差を取れば、その区間に起きたフォルト数になります。