MADV_DONTNEEDは何を捨てるのか、次のアクセスで戻る内容を分ける

左の桃色の砂は平らに整い、右の緑の砂には同心円の模様があり、紫の透明な型が浮かぶ。

大きな計算が終わると、途中で使ったメモリーが不要になることがあります。あとで同じアドレスを使いたいなら、領域の割り当てを残したまま、中身を手放す方法があります。Linuxでは、madviseにMADV_DONTNEEDを指定すると、このような使い方ができます。

その後に読める値は、メモリーの種類によって変わります。ファイルに結び付いていない、そのプロセス専用の領域ではゼロ埋めされた内容を読みます。一方、共有ファイルの領域では、元のファイルの現在の内容を読みます。以下では呼び出しが成功し、マッピングと読み取り権限が保たれ、その後に別の書き込みがない場合を見ていきます。

簡単にまとめると?

  • MADV_DONTNEEDは、割り当てたアドレスを残したまま、その範囲を当面必要としないとカーネルへ伝えます。成功後には、そこから読む内容が変わる場合があります。
  • 匿名privateの領域では次のアクセスでゼロ埋めされた内容を読み、共有ファイルの領域ではファイルの現在の内容に基づいて再び使われます。
  • 呼び出し元のRSSが減っても、システム全体で同じ量の物理メモリーがすぐ空くとは限りません。あとで必要になるデータは残しておく必要があります。

同じアドレスを残し、ページ単位で中身を手放す

madviseは、指定したアドレス範囲をどう使うか、カーネルへ伝える仕組みです。連続して読むという助言なら、先読みの調整に使われます。MADV_DONTNEEDもその指定の一つですが、処理速度だけでなく、次にアクセスしたときの内容にも関わります。

操作が及ぶ単位は「ページ」です。ページは仮想メモリーを管理するまとまりで、配列やオブジェクト一つとは限りません。開始アドレスはページ境界に合わせ、指定した長さはページサイズの倍数へ切り上げられます。小さな変数だけを手放したい場合でも、同じページに残すべきデータがあれば、まとめて対象に入ってしまいます。

アドレスの対応そのものをなくすmunmapとは違い、マッピングは残ります。ただし、同じアドレスを使えることと、以前の値を読み戻せることは別です。次の二つの例では、その違いが中身に表れます。

匿名privateの作業領域は、次に読むとゼロになる

匿名privateマッピングは、ファイルの内容を元にしない、そのプロセス専用の作業領域です。例えば、一ページに計算途中のデータがあり、先頭4バイトが65、66、67、68だとします。文字ではABCDに対応する値です。

このページにMADV_DONTNEEDを適用して成功すると、次のアクセスでは、必要に応じてゼロ埋めされたページが用意されます。先ほどと同じ位置から読む値は0、0、0、0になります。文字の「0」が四つ並ぶのではなく、各バイトの数値がゼロです。一ページの変化を、ここでは先頭4バイトに絞って見ています。

匿名privateの領域には、古い値を再び読み込む元ファイルがありません。計算途中の値をあとで使うつもりなら、この操作で手放さずに保持するか、別の場所へ保存しておく必要があります。再計算できる作業データなら、使うときに作り直す設計も考えられます。

共有ファイルの領域は、ファイルの現在の内容から戻る

sharedのファイルマッピングは、ファイルの対応部分を複数の利用者で共有できる形です。MADV_DONTNEEDの成功後にアクセスすると、対応するファイルの現在の内容に基づいてメモリーの中身が用意されます。

例えば、ファイルの対応する4バイトが87、88、89、90なら、WXYZという内容を元に読みます。捨てる直前の値を凍結して保存し、それを戻す仕組みではありません。ほかの処理がファイルを書き換える場合は、必要な時点の内容を読めるよう、読み書きの順序を同期する必要があります。

成功したMADV_DONTNEED後の次の読み取りを比較。匿名privateの65、66、67、68はゼロ4個、共有ファイルの例は現在のファイル値87、88、89、90から内容を得る。
ページ先頭4バイトを使った仕様上の例。マッピングと読み取り権限を維持し、途中の書き込みを置かない。Linuxでの実測ではない。

ここで比べたのは、匿名privateと共有ファイルの二種類です。privateのファイルマッピングや特殊なデバイス領域は、この二つだけでは説明できません。実装では、領域を作ったときの指定に合う動作を参照します。

RSSはすぐ減っても、物理ページの解放は遅れることがある

RSSは、そのプロセスの常駐メモリー量を表す指標です。Linuxのマニュアルでは、共有マッピングにMADV_DONTNEEDを適用すると、呼び出し元のRSSは直ちに減る一方、ページそのものの解放はカーネルが適切な時点まで遅らせられると説明されています。

共有ページには、ほかの利用者も関わっています。そのため、あるプロセスのRSSが減った分だけ、システム全体の空きメモリーが直ちに増えるとは限りません。再びアクセスすれば、内容を用意する処理も発生し得ます。頻繁に手放してすぐ使う場合は、保持する量を減らせる効果と、再アクセスにかかる負担を実際の環境で比べます。

また、MADV_DONTNEEDはファイルへの保存完了を保証する操作ではありません。共有ファイルから内容を読めるという説明も、電源断に耐えて保存済みであることを示してはいません。秘密データを確実に消去できたかどうかも、この指定だけでは判断できません。

ページの種類と、呼び出しの成否を確かめる

大きなページを明示的に使うHuge TLBページでは、そのページサイズに合わせて開始アドレスを整列し、範囲の長さもその倍数へ切り上げます。通常ページと同じサイズとは限りません。参照したマニュアルでは、MADV_DONTNEEDのHuge TLB対応はLinux 5.18以降です。

ロックされたページや、カーネル内部でVM_PFNMAPと記された特殊領域には適用できません。後者には、デバイスドライバーがユーザー空間へ割り当てる、通常の仮想メモリー管理の対象ではない領域などがあります。

madviseの戻り値は成功時に0、失敗時に−1で、失敗の理由はerrnoに設定されます。呼び出しただけで領域がリセットされたとは扱わず、戻り値を確認して失敗を処理します。ここで説明したのはLinuxのmadviseです。似た名前のPOSIXのposix_madviseへ、内容が変わるという動作をそのまま移すことはできません。

使い終えた作業領域を手放すときは、次に必要なのが同じアドレスなのか、そこに残した値なのかを考えると判断しやすくなります。値を再計算できる領域と、まだ保存していない唯一の結果がある領域とでは、MADV_DONTNEEDを使える場面が変わります。

出典

  1. Linux man-pages 6.19 — madvise(2)。DESCRIPTIONのConventional advice values、MADV_DONTNEED、RETURN VALUEを参照。例と図は当該仕様を説明するための独自モデル。