概要
MMU(Memory Management Unit、メモリ管理ユニット)は、CPUと物理メモリの間に位置し、プロセスが使用する「仮想アドレス」を実際のメモリチップ上の「物理アドレス」に変換するハードウェア回路である。この仮想アドレス変換機構により、LinuxをはじめとするOSは各プロセスに独立したアドレス空間を提供し、メモリ保護・スワッピング・共有メモリなどの機能を実現できる。
組み込みシステムの文脈では、「LinuxがどのMCUで動くか」を判断する最も重要な要素の一つがMMUの有無だ。ARM Cortex-A、x86、RISC-V(S拡張)などのアプリケーションプロセッサはMMUを標準搭載するが、ARM Cortex-M(M0〜M33の多く)はMMUを持たず、代わりにMPU(Memory Protection Unit)という簡易的なメモリ保護機能のみを持つ。MMUなしでLinuxを動かすことは原則不可能(μClinuxという例外的実装を除く)。
歴史・背景
仮想メモリの概念は1960年代に大型コンピュータで生まれた。当時、物理メモリ容量がプログラムサイズを下回ることが多く、プログラムの一部をディスクに退避(スワップ)しながら実行する仕組みが必要とされた。1970〜80年代にかけてUNIXの普及とともに仮想メモリ機能が一般化し、MMUはプロセッサの標準コンポーネントとなった。
ARMアーキテクチャでは、Cortex-Aシリーズ(2004年頃〜)にMMUが統合され、組み込みLinuxの実用化を大きく後押しした。Raspberry Pi(Cortex-A搭載)がLinuxを軽々と動かせるのもMMUのおかげだ。一方、Cortex-M系はリアルタイム制御を主目的とし、コスト・消費電力を抑えるためにMMUを省略した設計が採用されている。
RISC-VはSv32(32ビット向け)・Sv39(39ビット仮想アドレス)・Sv48(48ビット仮想アドレス)などのページテーブル形式を標準化しており、LinuxがサポートするRISC-V実装はこれらを実装している。
技術仕様
仮想アドレス変換の仕組み
MMUはページテーブルと呼ばれる変換テーブルをメモリ上に持ち、仮想ページ番号(VPN)から物理ページ番号(PPN)への対応を管理する。一般的なページサイズは4KB(12ビット)で、アドレスの下位12ビットはページオフセットとして変換せずそのまま使用する。
仮想アドレス(32ビット):
[31:12] 仮想ページ番号 (VPN) | [11:0] ページオフセット
↓ ページテーブルで変換
物理アドレス(32ビット):
[31:12] 物理ページ番号 (PPN) | [11:0] ページオフセット(同じ)
TLB(Translation Lookaside Buffer)
ページテーブルはメモリ上にあるため、変換のたびにメモリアクセスすると性能が低下する。この問題を解決するのがTLB(変換索引バッファ)で、最近使ったVPN→PPNの変換結果をキャッシュする小容量の連想メモリだ。TLBにヒットすれば1サイクル程度で変換できる。
TLBミスが発生するとハードウェア(または例外ハンドラ)がページテーブルを参照し、TLBをリロードする(ページテーブルウォーク)。コンテキストスイッチ時はTLBをフラッシュする必要があるため、タスク切り替えのオーバーヘッドとなる。ASID(Address Space ID)を使うとTLBエントリにプロセスIDを付与でき、コンテキストスイッチ時のTLBフラッシュを回避できる。
ページテーブルの階層構造
Linuxが多く使うARM Cortex-A(ARMv7-A/AArch64)のページテーブル構成を示す。
ARMv7-A(32ビット、LPAE: Large Physical Address Extension)
| 階層 | サイズ | ビット |
|---|---|---|
| L1(PGD) | 4エントリ(2GB分) | [31:30] |
| L2(PMD) | 512エントリ | [29:21] |
| L3(PTE) | 512エントリ | [20:12] |
| ページオフセット | 4KB | [11:0] |
AArch64(64ビット、Sv39相当)
48ビット仮想アドレス:
[47:39] L1インデックス | [38:30] L2インデックス | [29:21] L3インデックス | [20:12] L4インデックス | [11:0] オフセット
ページフォルトとスワッピング
物理メモリに存在しないページにアクセスすると「ページフォルト」例外が発生する。OSはこの例外を受けてディスク(スワップ領域)から必要なページを読み込み、ページテーブルを更新してプロセスの実行を再開する。この仕組みにより、物理メモリより大きなプログラムも実行できる(仮想メモリの本質)。
組み込みLinux環境ではスワップを持たない場合が多く、物理メモリが不足するとOOM Killer(Out of Memory Killer)がプロセスを強制終了する。
メモリ保護属性
各ページテーブルエントリにはアクセス制御ビットが含まれ、ページごとに以下の属性を設定できる。
| 属性 | 説明 |
|---|---|
| R(Read) | 読み取り許可 |
| W(Write) | 書き込み許可 |
| X(Execute) | 実行許可 |
| U(User) | ユーザーモードからのアクセス許可 |
| G(Global) | TLBフラッシュ対象外(カーネルページ等) |
| Dirty | ページが変更済み(ライトバック用) |
| Accessed | ページがアクセス済み(LRU置換用) |
NX(No Execute)ビットにより、データ領域でのコード実行を禁止でき、バッファオーバーフロー攻撃を防ぐことができる。
動作原理
アドレス変換フロー
CPUが仮想アドレスを発行
↓
TLBを検索(1サイクル)
↓
TLBヒット?
YES → 物理アドレス取得 → メモリアクセス
NO → ページテーブルウォーク(数十〜数百サイクル)
↓
ページが物理メモリに存在?
YES → TLBに登録 → 物理アドレス取得
NO → ページフォルト例外
↓
OSがスワップから読み込み
ページテーブル更新
例外からリターン
再実行
Linuxにおける仮想アドレス空間のレイアウト
ARM 32ビットLinuxの場合、仮想アドレス空間は以下のように分割される(カーネルとユーザー空間の詳細はユーザー空間とカーネル空間参照)。
0xFFFFFFFF ┌────────────────────┐
│ カーネル空間 │ 1GB(カーネルのみアクセス可)
0xC0000000 ├────────────────────┤
│ │
│ ユーザー空間 │ 3GB(各プロセスに独立した空間)
│ スタック ↓ │
│ ... │
│ ヒープ ↑ │
│ データセグメント │
│ テキスト(コード) │
0x00000000 └────────────────────┘
MMU無効化とバイパス
MMUは明示的に無効化できる。ブートローダー(U-Bootなど)は初期化時にMMUを無効にして動作し、OSの初期化コードでMMUを有効化する。一部のリアルタイム処理では、オーバーヘッドを最小化するためにMMUを使わない設計も存在するが、メモリ保護が失われるリスクがある。
用途・ユースケース
組み込みLinuxシステム
Embedded Linuxを動かすための最重要コンポーネント。Raspberry Pi、BeagleBone、NVIDIA JetsonなどのSBCはいずれもMMU搭載プロセッサを使用している。Linuxのプロセス分離・セキュリティ・メモリ管理機能はすべてMMUに依存している。
メモリ保護とセキュリティ
各プロセスが独立したアドレス空間を持つことで、あるプロセスのバグが他のプロセスのメモリを破壊することを防ぐ。また、カーネル空間へのユーザー空間からの不正アクセスも防止できる。SELinux等のセキュリティ機能もMMUのページ属性を活用している。
共有ライブラリ(.so)
共有ライブラリ(libc.so等)を複数プロセスで物理メモリを共有しつつ、各プロセスの仮想アドレス空間にマップできる。これによりメモリ効率が大幅に向上する。MMUが複数の仮想アドレスを同一物理ページに対応づけることで実現される。
デバイスドライバとデバイスメモリ
デバイスドライバはMMIOレジスタ(メモリマップドI/O)を仮想アドレス空間にマップして使用する。ioremap()関数がMMUのページテーブルを操作してこのマッピングを行う。
// Linuxドライバでのioremapの例
#define UART_BASE_PHYS 0x40011000UL
#define UART_SIZE 0x1000
void __iomem *uart_regs;
uart_regs = ioremap(UART_BASE_PHYS, UART_SIZE);
if (!uart_regs) {
pr_err("ioremap failed\n");
return -ENOMEM;
}
// レジスタアクセス(仮想アドレス経由)
writel(0x0001, uart_regs + UART_CR1_OFFSET);
uint32_t status = readl(uart_regs + UART_SR_OFFSET);
// 使い終わったら解放
iounmap(uart_regs);
mmap()によるファイルマッピング
mmap()システムコールを使うと、ファイルの内容を仮想アドレス空間に直接マップできる。これによりread()/write()を使わずにポインタ経由でファイルにアクセスでき、大容量データの処理が効率化する。組み込みLinuxではフラッシュストレージのXIPや設定ファイルのゼロコピーアクセスに使われる。
実装・開発のポイント
MMUの有無に基づくOSの選択
組み込みプロジェクトでOSを選定する際は最初にMMUの有無を確認する。
MMUあり(Cortex-A, x86, RISC-V with S-ext)
→ Linux, FreeBSD等の本格的なOS選択可能
→ メモリ保護・プロセス分離・豊富なソフトウェア資産が使える
MMUなし(Cortex-M, Cortex-R)
→ FreeRTOS, Zephyr, TOPPERS/ITRON等のRTOS
→ μClinux(特殊なLinux移植、使用を勧めない)
→ ベアメタル実装
ページテーブルの設定(カーネルドライバ開発)
デバイスツリーでメモリマップを定義し、Linuxカーネルがページテーブルを自動設定する。手動でページテーブルを操作する場面は少ないが、カスタムドライバでのioremap、vm_area_structの操作などで知識が必要になる。
// /proc/iomem で物理メモリマップを確認
// /proc/{PID}/maps でプロセスの仮想アドレスマップを確認
// /proc/{PID}/smaps で詳細なメモリ使用状況を確認
// Linuxユーザー空間からの仮想→物理アドレス変換(デバッグ用)
// /proc/{PID}/pagemap を読むことで確認可能
キャッシュとMMUの関係
MMUはキャッシュ動作とも密接に連携する。ページテーブルエントリにはキャッシュポリシー(Cacheable/Write-through/Write-back/Non-cacheable等)が設定でき、デバイスレジスタのようにキャッシュしてはいけないメモリ領域をNon-cacheableに設定する。誤った設定はデータ不整合の原因になる。
// Linuxでのioreampは自動的にNon-cacheableに設定される
// mmap()でデバイスメモリをユーザー空間にマップする場合
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *mapped = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, phys_addr);
// MAP_SHAREDとO_SYNCの組み合わせでNon-cacheableマッピングになる
TLBフラッシュとパフォーマンス
コンテキストスイッチの頻度が高い場合、TLBフラッシュがボトルネックになることがある。ASID機能を使えば各プロセスにIDを割り当て、TLBエントリのフラッシュを最小化できる。LinuxはARM64でASIDを活用して高速なコンテキストスイッチを実現している。
他技術との比較
| 項目 | MMU | MPU | なし(ベアメタル) |
|---|---|---|---|
| 仮想アドレス | あり | なし | なし |
| メモリ保護 | 強固(ページ単位) | 限定的(リージョン単位) | なし |
| プロセス分離 | 完全 | なし | なし |
| Linux対応 | 可 | 不可(通常) | 不可 |
| オーバーヘッド | 中(TLBミス時) | 小 | なし |
| コスト・消費電力 | 高め | 低い | 最小 |
| 主な搭載先 | Cortex-A, x86 | Cortex-M, Cortex-R | 小型MCU |
MPUとの違い
MPUはMMUの簡易版で、仮想アドレス変換機能を持たないが、特定の物理アドレス領域へのアクセス権限(Read/Write/Execute)を設定できる。FreeRTOSやZephyrはMPUを活用してタスク間のメモリ分離を実現する。MMUほど柔軟ではないが、リアルタイム性を損なわずメモリ保護を実現できる点で組み込みRTOSに適している。
キャッシュとの協調動作
CacheはMMUのページ属性と連携して動作する。Virtually Indexed Physically Tagged(VIPT)キャッシュは仮想アドレスでインデックス、物理アドレスでタグ付けする方式で、TLBと並行してキャッシュ検索が可能なため高速だ。ARMのL1キャッシュの多くがVIPT方式を採用している。