物理メモリ管理:連続ブロックから1回の割り当てまでの制約

このページの目次

空きが8ページあるのに、連続する2ページのメモリを確保できません。あるノードはリソースが十分なのに、別のノードでは制限のある割り当て要求が失敗します。物理的な割り当てには、⁠数量、形状、位置、待機条件を同時に満たす必要があります。

まず16ページのバディモデルで分割とマージを行い、その後、NUMA、ゾーン、水位、GFP制約を段階的に戻していきます。

説明すべき現象読むべき箇所
order は具体的にどのくらいのメモリを表すのか?第1~2節:バディモデルと実験
空きページがあるのに、なぜ現在の要求に応じられないのか?第3~4節:位置、水位、GFP
直前に解放したページなのに、なぜ buddyinfo がすぐに変わらないのか?第5節:PCP バッチキャッシュ
ページは回収可能か、オフライン化できるか?第6~7節:参照、ホットプラグ、観測

1. order から割り当ての形状を理解する

alloc_pages(gfp, order) は 2^order 個の物理的に連続する基本ページを要求し、対応する struct page を返します。基本ページのサイズはアーキテクチャと設定によって決まります。P=4096バイトの場合、order 0、1、4 はそれぞれ 4、8、64 KiB となります。order はバイト数でも、ページ数そのものでもありません。

管理範囲がPFN 0から15の16ページしかない場合、最初は order 4 の空きブロックです。ここで order 2(つまり4ページ)を要求します。アロケータは16ページすべてを渡す必要はありません:まず [0,8) と [8,16) に分割し、前者をさらに [0,4) と [4,8) に分割します。その後続の割り当てのために、2つの未使用のバディを保持します。

リクエスト4ページ:分割されるのは連続ブロック、保持されるのは未使用のバディ order 4:PFN [0,16)、合計16ページ order 3:[0,8) order 3:[8,16) 空き [0,4) 呼び出し元に渡す [4,8) order 2 空き [0,4) を解放した後、同じ階のバディ [4,8) も空きで、マージが許可されている場合にのみ、段階的に大きなブロックに戻せます。

図に従って2回の解放を続けると、マージ条件が明確になります。

時点[0,4)[4,8)[0,8) を回復できるか
2回目の4ページ要求後使用中使用中できない
1回目の要求を解放空き使用中できない、バディが空きではない
2回目の要求をさらに解放空き空きできる、order 3 に合成

[8,16) もまだ空きであれば、最初の order 4 にさらにマージできます。

隣接だけでは不十分:同じ階で、アライメントされたバディでなければならない

2^order でアライメントされたブロックの起点 PFN に対して、同じ階のバディの起点は PFN XOR (1 << order) で計算できます。しかし、数値を得ることは候補を見つけるにすぎません:それがマージ可能な管理範囲に属し、実際に同じ階の空きブロックであり、関連するマイグレーションタイプなどのルールを満たしていることを検証する必要があります。アドレス上で隣接している2つのブロックが、必ずしもこの階のバディであるとは限りません。例えば、order 2 の [4,8) と [8,12) は、直接 8ページでアライメントされた order 3 ブロックに合成することはできません。

なぜ空きが8ページあっても2ページ取得できないのか

引き続き16ページを考え、偶数PFNがすべて占有され、奇数PFNがすべて空きであると仮定します。この場合、メモリ半分が空きですが、連続する2ページの空きはありません。総量は十分ですが、形状が満たされていません。これは外部フラグメンテーションであり、要求者が要求したバイト数が空き総量を超えているわけではありません。

占有ページがマイグレーション可能であれば、カーネルはそれらを片側に移動させて、より大きな連続範囲を空けることができます。重要な位置にマイグレーション不可能なオブジェクトや長期間固定されたページが占有されている場合、整理は成功しないかもしれません。ビジネスロジックが物理的に連続していることを根本的に必要としないのであれば、アロケータに任意のフラグメンテーション状態から連続領域を生成するよう要求するのではなく、インターフェース設計において高い order を避けるべきです。高い order の上限はターゲットカーネルの設定によって決まるため、MAX_ORDER=11 を固定してコピーしてはいけません。

以下では、要求する階数と占有レイアウトを直接変更します。空き総量が十分であっても、1回の要求失敗から、欠けているのがどのような形状なのかを見極めることができます。

図を準備しています
空きページは十分なのに、なぜ要求は失敗するのか?

order k は、2^k でアライメントされた連続する 2^k ページを必要とします。空き総量が十分だからといって、そのような形状が存在するわけではありません。解放後、同じ階でアライメントされた空きバディのみがマージできます。

2. 実行可能なバディモデル

以下では、Pythonの整数で表現された16ページのみを管理し、実際の物理メモリを割り当てたり移動したりしません。これは、分割、同じ階でのマージ、割り当て記録がどのように互いに制約されるかを示すもので、NUMA、水位、PCP、並行同期は省略しています。

free = [set() for _ in range(5)]
free[4].add(0)
allocated = {}

def alloc(order):
    if not 0 <= order < len(free):
        raise ValueError("invalid order")
    source = next((n for n in range(order, len(free)) if free[n]), None)
    if source is None:
        return None
    start = min(free[source])
    free[source].remove(start)
    while source > order:
        source -= 1
        free[source].add(start + (1 << source))
    allocated[start] = order
    return start

def release(start):
    order = allocated.pop(start)  # 未割り当て/重複解放の起点を拒否
    while order + 1 < len(free):
        buddy = start ^ (1 << order)
        if buddy not in free[order]:
            break
        free[order].remove(buddy)
        start = min(start, buddy)
        order += 1
    free[order].add(start)

a, b = alloc(2), alloc(2)
assert (a, b) == (0, 4)
assert free[3] == {8}
release(a)
assert free[2] == {0}
release(b)
assert free[4] == {0} and not allocated
assert all(not blocks for blocks in free[:4])
print("分割、バディブロックと段階的マージ:成功")

実際のアロケータは、ロックと会計プロトコルのもとでこれらの不変条件を維持する必要があり、モデル内の set 操作をロックフリーなリストに直接置き換えて使用することはできません。解放時に元の高い階の要求を行う際も、対応するAPIの階数と参照契約に従う必要があり、その中の任意のページを独立した割り当て結果として解放することはできません。

3. Node と zone が「利用可能」に位置条件を追加する

NUMA ノードはメモリアクセスの位置とコストを表し、ゾーンは同一ノード内の異なる使用制約を表します。ノードが必ずしもCPUソケットに対応するわけではありません。システムにはメモリのみを含むノードや、複数の物理アドレス範囲にまたがるノードが存在でき、単一ソケットマシンでもNUMAトポロジを公開する可能性があります。遠端へのアクセスコストはハードウェア、アクセスパターン、輻輳度によって異なり、固定して「30%遅い」と記述することはできません。

通常ノードでのローカル割り当ての優先度は遠端アクセスを減らしますが、ユーザーのメモリポリシー、cpuset、要求フラグが、許可される集合とフォールバックに影響を与えます。アプリケーションがスレッドをノード0にバインドしても、すでにノード1に存在するページは、スレッドの移動に伴って自動的にすべて移動するわけではありません。「first touch」は、実際にはバックアップページが作成された時点での配置動作と理解すべきであり、malloc の返却時刻をすべてのページの物理的な帰属時刻と見なしてはいけません。

zone役割区別を保持する必要がある点
DMA / DMA32特定のアーキテクチャのアドレス指定制約範囲境界はアーキテクチャ依存;デバイスDMAは実際の制約を表現するためにDMA APIを使用すべき
NORMALカーネルが通常アクセスできるメモリノード、水位、割り当て用途の制約を依然として受ける
HIGHMEM一部の32ビット構成では永久直接マッピングがないメモリ「メモリ使用量が高い」という意味ではない
MOVABLEできるだけマイグレーション可能なコンテンツを収容し、整理/オフライン化能力を改善任意のカーネルオブジェクトを配置できるわけでも、絶対的にオフライン化できるわけでもない
DEVICE一部のデバイスメモリに対して page などの管理サービスを提供通常のバディ管理された汎用RAMとは異なる

一般的なx86構成では、DMA/DMA32 は低アドレス範囲に関連していますが、「DMA32メモリを要求する」ことは、デバイスのDMAマスク、IOMMUマッピング、一貫性処理に取って代わるものではありません。CXL が提供するある範囲のメモリは、通常の System RAM として使用される可能性もあり、CXL が見えたら必ずしも ZONE_DEVICE に属するわけではありません。トポロジと構造の定義は Physical Memory を参照してください。

4. 水位は3つの固定された故障スイッチではない

空きページの一部分は余量として保持されており、これは突発的な要求や、回収を担当するパス自体がリソースを確保できるようにするためです。通常の要求がすべてのページを使い果たすと、回収に必要なI/Oやメタデータ要求も進行できなくなり、より解きにくい循環状態に陥る可能性があります。

水位簡易モデルでの役割
low近傍のバランス条件がトリガーされ、バックグラウンド回収が推進される
high一般的なバックグラウンド回復の目標
min緊急時の保留およびより厳格なアクセス判断に参加

実際には、order、低ゾーン保護、利用不可の空きページ、watermark boost も含まれます。これらは3つの固定スイッチではなく、「low 以下なら必ず直接回収、min 以下なら必ず OOM」と単純に導き出すことはできません。

例えば、NORMAL に空きページが1200個あり、min/low/high がそれぞれ 1000/1500/2000 と仮定します。この状態はバックグラウンドバランスを推進すべきことを示唆していますが、1200 という数字だけで order 4 が必ず成功すると決定することはできません:連続する16ページが見つからない場合や、別のノードやゾーンの制約によって要求が制限される可能性があります。逆に、待機できない要求が失敗した場合、呼び出し元に代わって同期的な回収を行うことなく、直接戻ることがあります。

1回の割り当て:段階的に使用可能なリソースを狭める 要求形状 order、連続性、用途、呼び出しコンテキスト 1 ノードとゾーンの選択 NUMA ポリシー、cpuset、アドレス指定制約 2 既存リソースの試行 PCP / buddy;水位と保留ルールを同時に満たす 3 スローパスは許可条件に従って推進 回収、整理、再試行、またはノードフォールバック 4 成功または失敗を返す スリープ可能だからといって必ず成功するわけではない;緊急割り当てにもリソース上限がある 5 ここでは制約関係を示しています;具体的なスローパスの順序はカーネル、order、GFP に応じて異なります。

GFP:呼び出し元はアロケータにどの程度の作業を許可するか

フラグ許可される行為の重点呼び出し元が依然として処理すべきこと
GFP_KERNEL適切なコンテキストではスリープして回収可能要求失敗とクリーンアップ
GFP_NOWAIT直接回収による待機を回避より制限された利用可能なリソース
GFP_ATOMICスリープできない重要な要求には、異なる保留アクセス条件がある予備の枯渇;任意のNMIパスには適さない

GFP は動作の制約であり、成功率を保証するものではありません。

__GFP_RECLAIM には直接回収と kswapd に関連する許可が含まれており、__GFP_DIRECT_RECLAIM の単なる新しい名前ではありません。__GFP_NORETRY も、すべての軽量回収試行を完全に禁止するわけではありません。ファイルシステムやブロックI/Oの内部で回収を行うと、保持しているリソースに対して再帰的に呼び出される可能性があるため、適切な NOFS/NOIO スコープ制御が必要です。安易にすべてを GFP_ATOMIC に変更するのは、単にリソースの選択肢を狭めているだけで、依存関係の設計は解決されていません。詳細は メモリ割り当てガイド を参照してください。

5. PCP キャッシュはロック競合をどのように低減し、なぜ観測に影響するのか

あるCPUが連続して多くの小階ページを要求・解放する場合、毎回ゾーン全体のグローバル空き構造を変更すると、ローカル操作が共有ロック競合に変わってしまいます。PCP pagesets は、一批のページをCPU側に保持し、一般的な要求はローカルのバッチキャッシュから取得し、枯渇時に補充します。解放はまずローカルキャッシュに入り、閾値に達してからバッチで返却されます。

これによりグローバル操作が分散されるため、直前に解放したページがすぐにバディの高位の空きリストに表示されるとは限りません。PCP が排空されて初めて、他のバディとマージされる可能性があります。観測時には、「システム全体の空きページ」と「現在のバディが階ごとに列挙したブロック」を明確に区別する必要があります。PCP がどの階をサポートし、どのようなローカル同期を採用するかは、バージョンと設定によって変化するため、「order 0 のみで完全にロックフリー」と永遠に記述することはできません。

マイグレーションタイプ:類似した用途を一緒にまとめる

PCP とは異なり、マイグレーションタイプは、主にマイグレーション可能性が類似した割り当てをグループ化し、マイグレーション不可能なオブジェクトが広い範囲の各所に散らばるのを防ごうとします。RECLAIMABLE は、所属するサブシステムがオブジェクトを回収する機会があることを示し、MOVABLE は物理的な位置を変更してもセマンティクスを維持できることを示します。これらは同じものではありません。フォールバックやランタイムでの固定参照は、当初の分類効果を弱める可能性があります。

6. page 記述子、参照、メモリオフライン化

struct page は管理メタデータであり、ページの内容そのものではありません。フィールドの意味はページの用途によって異なり、通常のRAM、複合ページ、slab、デバイスページに対して、偽造された構造レイアウトを無理やり適用すべきではありません。参照カウントとマッピングカウントは異なる質問に答えます:ユーザーPTEマッピングがないページでも、ページキャッシュやカーネルによって保持されている可能性があります。page ポインタを取得したからといって、自動的に無制限に延長可能な参照を持つことを意味しません。

基本ページが4 KiB で、ある構成で page 記述子が64バイトを占めると仮定すると、この項目だけでも 64/4096=1.5625% の比率になります。この計算はメタデータの規模を理解するためのものであり、64バイトをすべてのカーネルの固定サイズとして記述してはいけません。大規模メモリシステムでは、vmemmap の構成、保留領域、その他の管理構造も考慮する必要があります。

ホットプラグ:範囲の追加とアロケータへの委譲は2つのステップ

段階完了すべき状態変化
addメモリ範囲と管理メタデータの構築
onlineページアロケータがこれらのリソースを使用可能にする
offline追加の割り当てを停止し、占有コンテンツを移動または解放する
remove範囲と管理状態の削除

MOVABLE はオフライン化の機会を高めますが、長期ピン、マイグレーション不可能なコンテンツ、またはマイグレーション先が不足していると、offline が阻止される可能性があります。詳細は メモリホットプラグ を参照してください。

7. buddyinfo を読み、成功保証ではなく証拠を得る

from pathlib import Path
import os
import re

page_size = os.sysconf("SC_PAGE_SIZE")
for line in Path("/proc/buddyinfo").read_text().splitlines():
    match = re.match(r"Node\s+(\d+),\s+zone\s+(\S+)\s+(.*)", line)
    if not match:
        continue
    node, zone, rest = match.groups()
    blocks = [int(value) for value in rest.split()]
    pages = sum(count * (1 << order) for order, count in enumerate(blocks))
    highest = max((i for i, count in enumerate(blocks) if count), default=None)
    print(f"node={node} zone={zone} buddy_bytes={pages * page_size} "
          f"highest_nonempty_order={highest}")

各列はその order のブロック数であり、ページ数ではありません。2^order を掛けて基本ページのサイズを掛ける必要があります。最高非空階は、読み取り時にその階のブロックが存在することを示すだけです。並行要求、水位、マイグレーションタイプ、ノードポリシーにより、あなたの要求は依然として失敗する可能性があります。zoneinfo の水位/保護、プロセスのNUMAポリシー、割り当てコンテキストと組み合わせて初めて、失敗の原因を説明できます。

次の記事 Slab と kmalloc では、一批の物理ページをオブジェクトに分割します。ページ回収とスワップ では、スローパスがどのように使用中メモリを再び利用可能なリソースに変えるかを説明します。