OSの起動後メモリ初期化とブートメモリアロケータ
通常のアロケータが無い起動初期に、カーネルはどう自分用メモリを確保するのか。メモリマップから memblock、buddy、struct page 配列までを原理から追い、起動時メモリ問題を読み解けます。
- 1.起動初期はファームウェアが渡すメモリマップを唯一の正とし、予約領域を避けながら memblock が大粒度でメモリを切り出す。
- 2.ページ管理に必要な struct page 配列そのものを置く場所を memblock で確保してから、各物理ページを buddy アロケータへ引き渡す。
- 3.引き渡し(mem_init)以降は memblock を退役させ、以後の物理ページ割当はすべて buddy が担う二段構えになっている。
卵が先か:アロケータを作るためのメモリをどう取るか
起動直後のカーネルには根本的な循環があります。物理ページを配るアロケータ(buddy)を立ち上げるには、その管理用データ構造を置くメモリが要る。だが、そのメモリを配るアロケータがまだ無い。 この鶏と卵を断ち切るのが、起動初期だけ使う簡易アロケータ memblock(旧称 bootmem)です。
全体の段取りを1本のタイムラインで押さえます。
ファームウェア(UEFI/BIOS)がメモリマップを構築
→ カーネルが E820 / EFI memory map を読み取る
→ 使用可能領域と予約領域を分類し memblock に登録
→ カーネル本体・initramfs・ページテーブル等を memblock.reserved に予約
→ 各ノード/ゾーンのページフレーム範囲(PFN)を確定
→ struct page 配列(mem_map)を memblock で確保し初期化
→ free_area_init() で buddy のフリーリストを構築
→ mem_init(): 空きページを buddy に投入、memblock を退役
→ 以降の物理ページ割当は alloc_pages(buddy)が担う
物理メモリの上に仮想記憶が立ち上がる流れ全体は ブートチェーンの内部(UEFI・ブートローダ・initramfs) で扱いました。本稿はその「カーネルが制御を受けた直後、メモリ管理が自立するまで」を解剖します。
ファームウェアのメモリマップ:唯一の正
カーネルは自分で DRAM の容量や配置を測りません。どこが使えて、どこが触ってはいけないか は、すべてファームウェアが提供する メモリマップ を正とします。x86 では BIOS の E820(割り込み 15h, AX=E820h で取得)または UEFI の EFI memory map が、アドレス範囲ごとに種別を持つエントリ列を返します。
| 代表的な種別 | 意味 | カーネルの扱い |
|---|---|---|
| Usable / Conventional | 自由に使える通常 RAM | memblock に「使用可能」として登録 |
| Reserved | ファームウェアやハードが占有 | 触らない(登録しない or 予約) |
| ACPI Reclaimable | ACPI テーブル。読み終えれば再利用可 | テーブル消費後に解放できる |
| ACPI NVS / Runtime | 退避や Runtime Services 用 | 保持し続ける |
| Bad / Unusable | 故障や MMIO 等で使えない | 除外する |
ここで重要なのは、RAM の物理アドレスは連続とは限らない ことです。MMIO 領域や予約穴があり、マップは「島」が飛び飛びに並ぶ構造になります。この穴の存在こそ、後段で struct page 配列のモデル(後述の sparsemem)が必要になる理由です。
ファームウェアのメモリマップにはバグや過少報告がしばしばあります。Reserved を Usable と誤報すれば、ファームが使う領域をカーネルが上書きして起動が壊れます。逆に過度に Reserved を増やせば使える RAM が減ります。カーネルは E820 の穴埋めや既知の不具合回避コードを持っており、マップを正としつつも個別に補正します。
memblock:大粒度で切り出す起動初期アロケータ
memblock は、メモリを アドレス範囲のリスト2本 で管理する素朴なアロケータです。memblock.memory(存在する物理メモリ)と memblock.reserved(既に予約済みの範囲)を持ち、各リストは [開始, サイズ, ノード番号, フラグ] のエントリ配列です。
確保の原理は単純で、memory に含まれ、かつ reserved と重ならない範囲 から要求サイズ分を切り出し、その範囲を reserved に追記するだけです。
memblock_alloc(size, align):
memblock.memory を走査して空き穴を探す
(memory にあり reserved に無い区間)
align 境界に丸めて size 分を確保
その範囲を memblock.reserved に追加
起動初期は通例トップダウン(高アドレス側)から取る
buddy のような分割・統合は持ちません。起動初期に確保するものの多くは 解放されない常駐データ(ページテーブル、struct page 配列、初期スラブ等)なので、断片化対策より単純さと、まだ仮想記憶やロック機構が不完全な環境でも動く堅牢さが優先されます。なお初期の確保で重要なのは、カーネル本体・initramfs・展開済みページテーブル・DTB/ACPI テーブル といった「既に物理メモリ上にある中身」を真っ先に reserved へ入れることです。これを忘れると後で上書きされます。
かつては bitmap ベースの bootmem が使われていましたが、巨大メモリ機ではビットマップ走査が重く、NUMA も扱いにくいものでした。現在は範囲リスト方式の memblock に統一され、CONFIG_NO_BOOTMEM 相当が標準です。API も memblock_alloc() 系に集約されています。
struct page 配列:1物理ページに1つのメタデータ
buddy をはじめカーネルのメモリ管理は、すべての物理ページに対応するメタデータ構造体 struct page を前提とします。各 struct page は参照カウント、フラグ(dirty/locked/LRU 等)、所属するゾーンやスラブ情報などを持ち、物理ページ1枚(典型 4KB)につき1個存在します。物理フレーム番号 PFN(Page Frame Number)= 物理アドレス / ページサイズ が配列添字になり、PFN → struct page と struct page → PFN を相互変換できます。
問題は この配列をどこに、どう置くか です。仮に 64GB の RAM があり 1 個 64 バイトとすると、配列だけで 1GB に達します。これを memblock で確保するのが mem_map の構築です。配置モデルは歴史的に複数あります。
| メモリモデル | struct page の置き方 | 向く構成 |
|---|---|---|
| FLATMEM | 全 PFN を1本の連続配列 mem_map に置く | 穴が少ない単純な構成 |
| DISCONTIGMEM | ノードごとに配列を分割(旧式) | NUMA だが現在は廃止傾向 |
| SPARSEMEM | 一定単位 section 毎に配列断片を持つ | 穴の多い構成・ホットプラグ |
現在の主流は SPARSEMEM です。物理アドレス空間を固定サイズの section に区切り、存在する section にだけ struct page 断片を割り当てます。これにより、物理アドレスが飛び飛び(メモリの「島」が分散)でも、穴の分の配列を無駄に確保せずに済みます。さらに SPARSEMEM_VMEMMAP は、全 struct page があたかも連続配列であるかのような仮想アドレス窓(vmemmap)を用意し、pfn_to_page() を単純な添字計算に落とします。物理が不連続でも、配列アクセスは連続配列同然の速さになるのが要点です。
struct page と PFN の相互変換は、ページフォルト処理・LRU 走査・I/O 完了など極めて高頻度の経路で呼ばれます。ここがポインタ追跡や検索になると全体性能に直撃します。vmemmap は「連続な仮想配列」という虚構を MMU に肩代わりさせ、変換を加減算だけに保つ最適化です。物理と仮想の対応は 仮想記憶(ページング) を参照してください。
ノードとゾーン:PFN 範囲の確定
struct page 配列を埋める前に、カーネルは物理メモリを ノード(NUMA ノード) と ゾーン に区切り、それぞれの PFN 範囲を確定します。ノードはメモリの物理的な近接性を表す単位で、NUMA の局所性は NUMAとメモリ局所性 で詳説しています。各ノードの内部はさらにゾーンに分かれます。
| ゾーン | 範囲(x86-64 の例) | 存在理由 |
|---|---|---|
| ZONE_DMA / DMA32 | 低位アドレス(16MB / 4GB 以下) | アドレス幅が狭い古い DMA デバイス向け |
| ZONE_NORMAL | カーネルが直接マップできる主要 RAM | 通常のカーネル割当の主戦場 |
| ZONE_MOVABLE | 移動可能ページ専用に分離した領域 | メモリホットプラグ・大ページ確保の連続性確保 |
ゾーンを分ける根本理由は、デバイスやカーネルがアクセスできるアドレス範囲に制約がある ことです。例えば 32bit までしか扱えない DMA デバイスのために低位ゾーンを温存します。free_area_init() 系の処理が各ノード・ゾーンの開始 PFN と終端 PFN を確定し、ゾーンごとに buddy の フリーリスト(order 0〜10)の空箱 を用意します。この時点ではまだページは投入されておらず、器だけができた状態です。
buddy への引き渡し:memblock の退役
最後の一手が、memblock がまだ自分の reserved に入れていない=誰も使っていない空きページを、すべて buddy に寄付する 処理です。アーキ依存の mem_init()(および memblock_free_all())がこれを行います。
mem_init() / memblock_free_all():
for each region in (memblock.memory - memblock.reserved):
その範囲の各ページについて:
struct page を初期化(参照カウント等)
__free_pages() で buddy のフリーリストへ投入
集計を totalram_pages 等に反映
memblock 用のデータ自身も解放し、memblock を以後使わない
ここを境に アロケータの主役が memblock から buddy へ交代 します。投入されたページは order 0 として入り、隣接する buddy 同士は統合(coalesce)されて大きな order のブロックへまとまっていきます。buddy の分割・統合アルゴリズムの詳細は メモリアロケータの内部(buddy systemとslab) を参照してください。以降のカーネル内割当(alloc_pages、その上の slab、さらに上のユーザー空間 malloc)は、すべてこの buddy を底面として積み上がります。
起動専用コード・データには __init 属性が付いており、初期化が済むと free_initmem() でまとめて解放され、ページが buddy に返されます。memblock の主要部やマップ補正コード、初期化関数群はここに含まれ、起動完了後はメモリから消えて常駐フットプリントを減らします。
「メモリマップ(E820/EFI)= 唯一の正」「memblock = 起動初期の範囲リスト型アロケータ・分割統合なし・常駐物を先に予約」「struct page 配列を memblock で確保(現主流は SPARSEMEM_VMEMMAP)」「mem_init で空きページを buddy へ寄付し memblock 退役」の4点を順序どおり言えると、起動時メモリ初期化の流れを問う設問に対応できます。
まとめ
- 起動初期は ファームウェアのメモリマップ(E820 / EFI memory map)を唯一の正 とし、使用可能領域と予約領域を分類して memblock に登録する。
- memblock は範囲リスト2本(memory / reserved)で大粒度に切り出す簡易アロケータで、分割・統合を持たず、カーネル本体やページテーブルなど常駐物をまず予約する。
- buddy が必要とする struct page 配列そのもの を memblock で確保・初期化する。穴の多い物理空間に対応するため現主流は SPARSEMEM_VMEMMAP。
mem_init()で 空きページを buddy のフリーリストへ寄付 し、memblock を退役させる。以降の物理ページ割当はすべて buddy が担う。
ここから上に積み上がる割当層は メモリアロケータの内部(buddy systemとslab)、ページテーブルの構築は 多段ページテーブルの仕組み と合わせて読むと、物理ページの誕生から仮想アドレス変換までが一本につながります。
OS Article
OSの起動後メモリ初期化とブートメモリアロケータを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブート
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ページ管理に必要な struct page 配列そのものを置く場所を memblock で確保してから、各物理ページを buddy アロケータへ引き渡す。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ブート / メモリ管理」に近いか確認する。
- 強みである「起動初期はファームウェアが渡すメモリマップを唯一の正とし、予約領域を避けながら memblock が大粒度でメモリを切り出す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。