メモリマップとは?コンピュータのメモリ管理の重要な仕組みを徹底解説

プログラミング・IT

パソコンやスマートフォンで複数のアプリを同時に動かしている時、それぞれのプログラムがどこのメモリを使っているか、どうやって管理されているか考えたことはありますか?

この管理の仕組みがメモリマップ(Memory Map)です。メモリマップは、コンピュータのメモリ空間の配置図のようなもので、プログラムがどの領域を使えるのか、データがどこに保存されているのかを示す重要な情報なんです。

プログラミングをする上で、大容量ファイルの高速処理やハードウェア制御、そしてメモリ管理の理解には欠かせない概念です。一見難しそうですが、基本を押さえれば理解できます。

この記事では、メモリマップの基本から種類、実際の使い方、プログラミングでの活用方法まで、初心者の方にも分かりやすく解説していきます!


スポンサーリンク
  1. メモリマップとは?コンピュータのメモリ配置図
    1. 簡単に言うと
    2. メモリアドレスの基本
  2. メモリマップの主な種類
    1. 1. プロセスのメモリマップ(Virtual Memory Map)
    2. 2. メモリマップドファイル(Memory-Mapped File)
    3. 3. メモリマップドI/O(Memory-Mapped I/O)
    4. 4. 物理メモリマップ
  3. プロセスのメモリマップ:プログラムの内部構造
    1. 典型的なメモリマップの構造
    2. 各領域の詳細
    3. 64ビットシステムでの違い
  4. メモリマップドファイル:ファイルを高速処理
    1. 通常のファイル操作との違い
    2. メモリマップドファイルのメリット
    3. Linuxでの実装例
    4. Windowsでの実装例
    5. 実用例:データベース
  5. メモリマップドI/O:ハードウェア制御の基礎
    1. メモリマップドI/Oとは
    2. ポート I/O との比較
    3. 組み込みシステムでの実例
    4. 注意点
  6. メモリマップの確認方法
    1. Linuxでの確認方法
    2. Windowsでの確認方法
    3. プログラムからの確認
  7. メモリマップのメリットとデメリット
    1. メモリマップドファイルのメリット
    2. メモリマップドファイルのデメリット
    3. メモリマップドI/Oのメリット
    4. メモリマップドI/Oのデメリット
  8. 実践的な活用例
    1. 例1:大容量ログファイルの検索
    2. 例2:プロセス間共有メモリ
    3. 例3:バイナリファイルの高速解析
  9. トラブルシューティング
    1. 問題1:セグメンテーションフォルト
    2. 問題2:Out of Memory
    3. 問題3:パフォーマンスが期待より低い
    4. 問題4:書き込みがディスクに反映されない
    5. 問題5:複数プロセスでの同期
  10. まとめ:メモリマップはシステムプログラミングの基礎

メモリマップとは?コンピュータのメモリ配置図

メモリマップ(Memory Map)とは、コンピュータのメモリ空間の配置や使用状況を示す図や情報のことです。

簡単に言うと

イメージ:

  • 建物の間取り図のようなもの
  • 地図のようにメモリ空間を視覚化
  • 住所録のようにアドレスと内容を対応付け

何が分かるのか:

  • プログラムのコードがどこにあるか
  • データがどこに保存されているか
  • 空いているメモリ領域はどこか
  • ハードウェアにアクセスするアドレス

メモリアドレスの基本

コンピュータのメモリにはアドレス(番地)が割り振られています。

例:

アドレス      内容
0x00000000   プログラムコード開始
0x00001000   データ領域
0x00002000   スタック領域
0x7FFFFFFF   ユーザー空間の終わり
0x80000000   カーネル空間の開始

各アドレスには、プログラムの命令やデータが保存されているんですね。


メモリマップの主な種類

メモリマップという用語は、文脈によって異なる意味で使われます。

1. プロセスのメモリマップ(Virtual Memory Map)

実行中のプログラムが使用するメモリの配置図です。

含まれる領域:

  • テキストセグメント(プログラムコード)
  • データセグメント(グローバル変数)
  • ヒープ領域(動的メモリ確保)
  • スタック領域(関数呼び出し、ローカル変数)
  • 共有ライブラリ

用途:

  • プログラムのメモリ使用状況を把握
  • メモリリークのデバッグ
  • 最適化の分析

2. メモリマップドファイル(Memory-Mapped File)

ファイルの内容をメモリ空間にマッピングする技術です。

仕組み:

  • ファイルをメモリアドレスに対応付け
  • ファイルの読み書きをメモリアクセスとして実行
  • OSがページング機能を使って効率的に処理

用途:

  • 大容量ファイルの高速処理
  • プロセス間でのデータ共有
  • データベースの実装

3. メモリマップドI/O(Memory-Mapped I/O)

ハードウェアのレジスタをメモリアドレス空間にマッピングする技術です。

仕組み:

  • ハードウェアの制御レジスタに特定のアドレスを割り当て
  • メモリアクセスの命令でハードウェアを制御
  • 特別なI/O命令が不要

用途:

  • 組み込みシステム開発
  • デバイスドライバの実装
  • ハードウェア制御

4. 物理メモリマップ

コンピュータシステム全体の物理メモリの配置です。

含まれる領域:

  • RAM(実メモリ)
  • ROM(BIOS/UEFIなど)
  • ビデオメモリ
  • デバイスのメモリマップドレジスタ

用途:

  • システムの起動処理
  • OSの初期化
  • ハードウェア設計

プロセスのメモリマップ:プログラムの内部構造

実行中のプログラムがどのようにメモリを使っているか見ていきましょう。

典型的なメモリマップの構造

32ビットシステムの例:

高位アドレス(0xFFFFFFFF)
┌─────────────────────┐
│  カーネル空間        │ ← OSが使用(ユーザーからはアクセス不可)
├─────────────────────┤ 0x80000000
│  環境変数・引数      │
├─────────────────────┤
│  スタック ↓         │ ← 関数呼び出し、ローカル変数
│     (下に伸びる)   │
├─────────────────────┤
│                     │
│  未使用領域         │
│                     │
├─────────────────────┤
│  ヒープ ↑          │ ← malloc/newで確保
│   (上に伸びる)     │
├─────────────────────┤
│  BSS セグメント      │ ← 初期化されていないグローバル変数
├─────────────────────┤
│  データセグメント    │ ← 初期化済みグローバル変数
├─────────────────────┤
│  テキストセグメント  │ ← プログラムのコード(読み取り専用)
└─────────────────────┘
低位アドレス(0x00000000)

各領域の詳細

1. テキストセグメント(Text Segment / Code Segment)

特徴:

  • プログラムの実行コード(機械語命令)
  • 読み取り専用(書き込み禁止)
  • 複数プロセスで共有可能

内容:

  • コンパイルされたプログラム
  • 関数の機械語コード

例:

int add(int a, int b) {  // この関数のコードがテキストセグメントに
    return a + b;
}

2. データセグメント(Data Segment)

特徴:

  • 初期化済みのグローバル変数・静的変数
  • 読み書き可能
  • プログラム開始時に値が設定される

例:

int global_var = 100;        // データセグメント
static int static_var = 50;  // データセグメント

3. BSSセグメント(Block Started by Symbol)

特徴:

  • 初期化されていないグローバル変数・静的変数
  • プログラム開始時に自動的に0に初期化
  • 実行ファイルには含まれない(サイズ情報のみ)

例:

int uninitialized_var;              // BSSセグメント
static int uninitialized_static;    // BSSセグメント

なぜ分けるのか:
実行ファイルのサイズを小さくするため。値がないので、アドレスと初期値(0)の情報だけ持つ。

4. ヒープ(Heap)

特徴:

  • 動的メモリ確保の領域
  • プログラマが明示的に確保・解放
  • 低位アドレスから高位アドレスへ伸びる

確保方法:

// C言語
int *p = (int*)malloc(sizeof(int) * 100);  // ヒープから確保
free(p);  // 解放

// C++
int *arr = new int[100];  // ヒープから確保
delete[] arr;  // 解放

用途:

  • サイズが実行時にしか分からないデータ
  • 大きなデータ構造
  • 複数の関数で共有するデータ

5. スタック(Stack)

特徴:

  • ローカル変数と関数呼び出し情報を保存
  • 自動的に確保・解放される
  • 高位アドレスから低位アドレスへ伸びる
  • LIFO(Last In First Out:後入れ先出し)

内容:

  • 関数のローカル変数
  • 関数の引数
  • 戻りアドレス
  • 保存されたレジスタ値

例:

void function() {
    int local_var = 10;  // スタック上に確保
    int array[100];      // スタック上に確保
}  // 関数終了時に自動的に解放

スタックフレームの例:

main()呼び出し
  ↓
function1()呼び出し
  ↓
function2()呼び出し  ← 現在のスタックトップ

各関数が独自のスタックフレームを持ちます。

64ビットシステムでの違い

主な変化:

  • アドレス空間が大幅に拡大(理論上16EB = 約1600万TB)
  • 実際には48ビット(256TB)程度を使用
  • より大きなメモリを扱える
  • アドレスの表記が長くなる(0x00007FFF12345678など)

メモリマップドファイル:ファイルを高速処理

メモリマップドファイルは、大容量ファイルを効率的に扱う強力な技術です。

通常のファイル操作との違い

通常のファイル読み込み:

FILE *fp = fopen("data.txt", "r");
char buffer[1024];
fread(buffer, 1, 1024, fp);  // ファイル → バッファ → メモリ
fclose(fp);

処理の流れ:

  1. ファイルをオープン
  2. read()システムコールでカーネルに要求
  3. カーネルがディスクから読み込み
  4. カーネル空間からユーザー空間へコピー
  5. アプリケーションが処理

メモリマップドファイル:

int fd = open("data.txt", O_RDONLY);
char *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
// mappedを通常のメモリポインタとして使用
char c = mapped[100];  // 直接アクセス
munmap(mapped, file_size);
close(fd);

処理の流れ:

  1. ファイルをメモリ空間にマッピング
  2. メモリアクセスとして読み書き
  3. OSがページフォルト(ページング)で自動的に読み込み
  4. 余計なコピー不要

メモリマップドファイルのメリット

1. 高速なアクセス

  • システムコールのオーバーヘッド削減
  • データコピーが不要
  • OSのページキャッシュを活用

2. 大容量ファイルの効率的な処理

  • ファイル全体をメモリに読み込む必要がない
  • 必要な部分だけOSが自動的にロード
  • 物理メモリより大きなファイルも扱える

3. シンプルなコード

  • ポインタ操作でファイルアクセス
  • 複雑なバッファ管理が不要
  • seek操作が不要

4. プロセス間共有

  • 複数プロセスが同じファイルをマップ
  • 効率的なデータ共有
  • 共有メモリとして使用可能

Linuxでの実装例

基本的な使い方:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // ファイルを開く
    int fd = open("large_file.dat", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // ファイルサイズを取得
    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        return 1;
    }

    // ファイルをメモリにマップ
    char *mapped = mmap(NULL,           // カーネルがアドレスを決定
                       sb.st_size,      // ファイル全体
                       PROT_READ,       // 読み取り専用
                       MAP_PRIVATE,     // プライベートマッピング
                       fd,              // ファイルディスクリプタ
                       0);              // オフセット0から

    if (mapped == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // メモリアクセスのようにファイルを読む
    printf("First 10 bytes: ");
    for (int i = 0; i < 10 && i < sb.st_size; i++) {
        printf("%02x ", (unsigned char)mapped[i]);
    }
    printf("\n");

    // マッピングを解除
    if (munmap(mapped, sb.st_size) == -1) {
        perror("munmap");
        return 1;
    }

    close(fd);
    return 0;
}

Windowsでの実装例

#include <windows.h>
#include <stdio.h>

int main() {
    // ファイルを開く
    HANDLE hFile = CreateFile("large_file.dat",
                             GENERIC_READ,
                             FILE_SHARE_READ,
                             NULL,
                             OPEN_EXISTING,
                             FILE_ATTRIBUTE_NORMAL,
                             NULL);

    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFile failed\n");
        return 1;
    }

    // ファイルマッピングオブジェクトを作成
    HANDLE hMapFile = CreateFileMapping(hFile,
                                       NULL,
                                       PAGE_READONLY,
                                       0,
                                       0,  // ファイル全体
                                       NULL);

    if (hMapFile == NULL) {
        printf("CreateFileMapping failed\n");
        CloseHandle(hFile);
        return 1;
    }

    // ビューをマップ
    LPVOID mapped = MapViewOfFile(hMapFile,
                                 FILE_MAP_READ,
                                 0,  // オフセット上位
                                 0,  // オフセット下位
                                 0); // ファイル全体

    if (mapped == NULL) {
        printf("MapViewOfFile failed\n");
        CloseHandle(hMapFile);
        CloseHandle(hFile);
        return 1;
    }

    // メモリアクセス
    char *data = (char*)mapped;
    printf("First 10 bytes: ");
    for (int i = 0; i < 10; i++) {
        printf("%02x ", (unsigned char)data[i]);
    }
    printf("\n");

    // クリーンアップ
    UnmapViewOfFile(mapped);
    CloseHandle(hMapFile);
    CloseHandle(hFile);

    return 0;
}

実用例:データベース

SQLiteの内部実装:

  • データベースファイルをメモリマップ
  • 高速なデータアクセス
  • OSのページキャッシュを最大限活用

他の例:

  • MongoDB:データファイルのマッピング
  • Redis:永続化時のスナップショット
  • 各種組み込みデータベース

メモリマップドI/O:ハードウェア制御の基礎

組み込みシステムやデバイスドライバで重要な技術です。

メモリマップドI/Oとは

仕組み:
ハードウェアの制御レジスタをメモリアドレス空間に配置し、通常のメモリアクセス命令で制御する方式です。

例:GPIO(汎用入出力ピン)の制御

Raspberry Piの例:

// GPIOレジスタのベースアドレス
#define GPIO_BASE 0x3F200000

// レジスタのオフセット
#define GPFSEL1   (GPIO_BASE + 0x04)  // 機能選択
#define GPSET0    (GPIO_BASE + 0x1C)  // セット
#define GPCLR0    (GPIO_BASE + 0x28)  // クリア

// メモリマップドI/O
volatile unsigned int *gpio = (unsigned int *)GPIO_BASE;

// ピンを出力モードに設定
gpio[GPFSEL1/4] |= (1 << 3);

// LEDを点灯(ピンをHIGHに)
gpio[GPSET0/4] = (1 << 10);

// LEDを消灯(ピンをLOWに)
gpio[GPCLR0/4] = (1 << 10);

ポート I/O との比較

ポートI/O(Port-Mapped I/O):

  • 専用のI/O命令を使用(x86のIN/OUT命令)
  • メモリ空間とは別のI/O空間
  • 主にx86アーキテクチャ

例(x86アセンブリ):

in  al, 0x60    ; ポート0x60から読み込み
out 0x64, al    ; ポート0x64へ書き込み

メモリマップドI/O:

  • 通常のメモリアクセス命令
  • メモリ空間とI/O空間が統一
  • ARM、RISC-Vなど多くのアーキテクチャ

メリット:

  • プログラミングが簡単
  • コンパイラの最適化が効く
  • ポインタ操作が可能

組み込みシステムでの実例

STM32マイコンのLED制御:

// RCCレジスタ(クロック制御)
#define RCC_BASE    0x40021000
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x18))

// GPIOレジスタ
#define GPIOC_BASE  0x40011000
#define GPIOC_CRH   (*(volatile uint32_t *)(GPIOC_BASE + 0x04))
#define GPIOC_ODR   (*(volatile uint32_t *)(GPIOC_BASE + 0x0C))

void led_init() {
    // GPIOCのクロックを有効化
    RCC_APB2ENR |= (1 << 4);

    // PC13を出力に設定
    GPIOC_CRH &= ~(0xF << 20);
    GPIOC_CRH |= (0x2 << 20);
}

void led_on() {
    GPIOC_ODR |= (1 << 13);  // PC13をHIGHに
}

void led_off() {
    GPIOC_ODR &= ~(1 << 13); // PC13をLOWに
}

注意点

volatile修飾子が必須:

volatile unsigned int *reg = (unsigned int *)0x40000000;

理由:

  • コンパイラの最適化を防ぐ
  • ハードウェアが値を変更する可能性
  • 読み書きが副作用を持つ

例:

// volatileなし
unsigned int *reg = (unsigned int *)0x40000000;
int x = *reg;
int y = *reg;  // コンパイラが最適化してxを再利用する可能性

// volatileあり
volatile unsigned int *reg = (unsigned int *)0x40000000;
int x = *reg;
int y = *reg;  // 必ず2回読み込まれる

メモリマップの確認方法

実際に動いているプログラムのメモリマップを見てみましょう。

Linuxでの確認方法

1. /proc/[pid]/maps ファイル

実行中のプロセスのメモリマップを確認できます。

# 自分のシェルのメモリマップを見る
cat /proc/self/maps

# 特定のプロセスのメモリマップ
cat /proc/1234/maps

出力例:

00400000-00452000 r-xp 00000000 08:01 123456  /usr/bin/program
00651000-00652000 r--p 00051000 08:01 123456  /usr/bin/program
00652000-00653000 rw-p 00052000 08:01 123456  /usr/bin/program
00653000-00674000 rw-p 00000000 00:00 0       [heap]
7ffff7dd5000-7ffff7dfc000 r-xp 00000000 08:01 789012  /lib/ld-2.27.so
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0       [stack]

読み方:

  • 00400000-00452000:アドレス範囲
  • r-xp:権限(読み取り、実行、プライベート)
  • 00000000:ファイル内オフセット
  • /usr/bin/program:対応するファイル

権限の意味:

  • r:読み取り可能(read)
  • w:書き込み可能(write)
  • x:実行可能(execute)
  • p:プライベート(private、COW)
  • s:共有(shared)

2. pmapコマンド

より見やすい形式で表示:

pmap -x 1234

出力例:

Address           Kbytes  RSS    Dirty  Mode  Mapping
0000000000400000     328  328    0      r-x-- program
0000000000651000       4    4    4      r---- program
0000000000652000       4    4    4      rw--- program
0000000000653000     132   12   12      rw--- [heap]
...

3. psコマンド

プロセスのメモリ使用量を確認:

ps aux | grep program

Windowsでの確認方法

1. Process Explorer(Sysinternalsツール)

手順:

  1. Process Explorerをダウンロード・起動
  2. 対象のプロセスを選択
  3. プロパティ → Memory タブ
  4. “Memory” ボタンで詳細表示

2. VMMapツール

より詳細なメモリマップを視覚化:

VirtualAlloc allocations
Heap allocations
Stack regions
Image (executable and DLL) mappings
Mapped files

3. プログラムからの確認

#include <windows.h>
#include <psapi.h>

void print_memory_info() {
    PROCESS_MEMORY_COUNTERS pmc;

    if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
        printf("Working Set: %lu KB\n", pmc.WorkingSetSize / 1024);
        printf("Peak Working Set: %lu KB\n", pmc.PeakWorkingSetSize / 1024);
        printf("Page Faults: %lu\n", pmc.PageFaultCount);
    }
}

プログラムからの確認

Cプログラムで各領域のアドレスを表示:

#include <stdio.h>
#include <stdlib.h>

int global_initialized = 100;    // データセグメント
int global_uninitialized;        // BSSセグメント

void function() {
    int stack_var = 1;            // スタック
    printf("Stack variable:     %p\n", (void*)&stack_var);
}

int main() {
    static int static_var = 50;   // データセグメント
    int *heap_var = malloc(sizeof(int));  // ヒープ

    printf("Text segment (main): %p\n", (void*)main);
    printf("Data segment:        %p\n", (void*)&global_initialized);
    printf("BSS segment:         %p\n", (void*)&global_uninitialized);
    printf("Heap:                %p\n", (void*)heap_var);

    function();

    free(heap_var);
    return 0;
}

出力例:

Text segment (main): 0x400566
Data segment:        0x601040
BSS segment:         0x601044
Heap:                0x1234000
Stack variable:      0x7ffd1234

メモリマップのメリットとデメリット

それぞれの技術の長所と短所を理解しましょう。

メモリマップドファイルのメリット

✅ 高速なアクセス

  • システムコールのオーバーヘッド削減
  • カーネルとユーザー空間のコピー不要
  • OSのページキャッシュを活用

✅ シンプルなコード

  • ポインタでファイルアクセス
  • 複雑なバッファ管理が不要
  • seek操作が不要

✅ 効率的なメモリ使用

  • 必要な部分だけロード
  • 物理メモリより大きなファイルも扱える
  • 複数プロセスでページを共有

✅ プロセス間共有

  • 効率的なIPC(プロセス間通信)
  • 共有メモリとして使用可能

メモリマップドファイルのデメリット

❌ アドレス空間の制限

  • 32ビットシステムでは約2〜3GBが上限
  • 非常に大きなファイルは全体をマップできない

❌ ページサイズの制約

  • 通常4KB単位でマッピング
  • 小さなファイルでは非効率な場合も

❌ エラーハンドリングの難しさ

  • I/Oエラーがセグメンテーションフォルトに
  • 通常のエラーコードで検出できない

❌ ポータビリティの問題

  • OSごとにAPIが異なる
  • 挙動の違いに注意が必要

❌ 書き込みタイミングの不確実性

  • いつディスクに書き込まれるか不明
  • msync()で明示的に同期が必要

メモリマップドI/Oのメリット

✅ シンプルなプログラミング

  • 専用のI/O命令が不要
  • ポインタでアクセス可能

✅ コンパイラの最適化

  • 通常のメモリアクセスとして最適化
  • 高速な実行

✅ 統一的なアドレス空間

  • メモリとI/Oが同じ空間
  • 管理が容易

メモリマップドI/Oのデメリット

❌ アドレス空間の消費

  • メモリアドレス空間を占有
  • 32ビットシステムでは問題になる場合も

❌ キャッシュの問題

  • CPUキャッシュとの整合性に注意
  • 適切なメモリバリアが必要

❌ ハードウェア依存

  • アドレスマップがハードウェア固有
  • ポータビリティが低い

実践的な活用例

実際のプログラミングでの使い方を見ていきましょう。

例1:大容量ログファイルの検索

シナリオ:
10GBのログファイルから特定の文字列を検索

通常の方法:

FILE *fp = fopen("huge_log.txt", "r");
char line[1024];
while (fgets(line, sizeof(line), fp)) {
    if (strstr(line, "ERROR")) {
        printf("%s", line);
    }
}
fclose(fp);

メモリマップドファイル版:

int fd = open("huge_log.txt", O_RDONLY);
struct stat sb;
fstat(fd, &sb);

char *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

// 並列処理も容易
#pragma omp parallel for
for (size_t i = 0; i < sb.st_size; i++) {
    if (strncmp(&mapped[i], "ERROR", 5) == 0) {
        // 見つかった位置の処理
    }
}

munmap(mapped, sb.st_size);
close(fd);

利点:

  • ファイル全体を読み込まずに処理
  • 並列処理が容易
  • OSが効率的にキャッシュ

例2:プロセス間共有メモリ

シナリオ:
複数のプロセスでデータを共有

実装:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define SHARED_MEM_SIZE 4096
#define SHARED_MEM_NAME "/my_shared_memory"

// プロセス1:書き込み側
void writer_process() {
    // 共有メモリオブジェクトを作成
    int fd = shm_open(SHARED_MEM_NAME, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, SHARED_MEM_SIZE);

    // マッピング
    char *shared = mmap(NULL, SHARED_MEM_SIZE, 
                       PROT_READ | PROT_WRITE, 
                       MAP_SHARED, fd, 0);

    // データを書き込み
    strcpy(shared, "Hello from process 1!");

    // クリーンアップ(プロセス終了まで共有メモリは残る)
    munmap(shared, SHARED_MEM_SIZE);
    close(fd);
}

// プロセス2:読み取り側
void reader_process() {
    // 既存の共有メモリを開く
    int fd = shm_open(SHARED_MEM_NAME, O_RDONLY, 0666);

    // マッピング
    char *shared = mmap(NULL, SHARED_MEM_SIZE, 
                       PROT_READ, 
                       MAP_SHARED, fd, 0);

    // データを読み取り
    printf("Received: %s\n", shared);

    // クリーンアップ
    munmap(shared, SHARED_MEM_SIZE);
    close(fd);

    // 共有メモリオブジェクトを削除
    shm_unlink(SHARED_MEM_NAME);
}

例3:バイナリファイルの高速解析

シナリオ:
画像ファイルのヘッダー情報を解析

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

// BMP画像のヘッダー構造体
#pragma pack(push, 1)
typedef struct {
    uint16_t type;        // "BM"
    uint32_t size;        // ファイルサイズ
    uint16_t reserved1;
    uint16_t reserved2;
    uint32_t offset;      // 画像データへのオフセット
} BMPFileHeader;

typedef struct {
    uint32_t size;        // ヘッダサイズ
    int32_t  width;       // 画像幅
    int32_t  height;      // 画像高さ
    uint16_t planes;
    uint16_t bit_count;   // 色深度
    // ...その他のフィールド
} BMPInfoHeader;
#pragma pack(pop)

void analyze_bmp(const char *filename) {
    int fd = open(filename, O_RDONLY);
    struct stat sb;
    fstat(fd, &sb);

    // ファイルをマッピング
    void *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    // ヘッダーを直接参照
    BMPFileHeader *file_header = (BMPFileHeader *)mapped;
    BMPInfoHeader *info_header = (BMPInfoHeader *)((char *)mapped + sizeof(BMPFileHeader));

    // 情報を表示
    printf("Type: %c%c\n", file_header->type & 0xFF, file_header->type >> 8);
    printf("File size: %u bytes\n", file_header->size);
    printf("Width: %d pixels\n", info_header->width);
    printf("Height: %d pixels\n", info_header->height);
    printf("Bit depth: %u bits\n", info_header->bit_count);

    // クリーンアップ
    munmap(mapped, sb.st_size);
    close(fd);
}

利点:

  • 構造体を直接ファイルにマッピング
  • コピー不要で高速
  • コードが直感的

トラブルシューティング

よくある問題と解決方法です。

問題1:セグメンテーションフォルト

原因:

  • マップされた領域外にアクセス
  • 権限のない操作(書き込み禁止領域への書き込みなど)
  • アンマップ後のアクセス

対処法:

// アクセス前に範囲チェック
if (offset < file_size) {
    char c = mapped[offset];
}

// 適切な権限でマッピング
char *mapped = mmap(NULL, size, 
                   PROT_READ | PROT_WRITE,  // 読み書き両方
                   MAP_PRIVATE, fd, 0);

問題2:Out of Memory

原因:

  • アドレス空間の枯渇(32ビットシステム)
  • 物理メモリ不足
  • スワップ領域の不足

対処法:

// 部分的にマッピング
size_t chunk_size = 100 * 1024 * 1024;  // 100MB単位
for (off_t offset = 0; offset < file_size; offset += chunk_size) {
    size_t map_size = (offset + chunk_size > file_size) 
                     ? file_size - offset 
                     : chunk_size;

    char *mapped = mmap(NULL, map_size, PROT_READ, 
                       MAP_PRIVATE, fd, offset);

    // 処理
    process_data(mapped, map_size);

    munmap(mapped, map_size);
}

問題3:パフォーマンスが期待より低い

原因:

  • ページフォルトのオーバーヘッド
  • ランダムアクセスが多い
  • OSのキャッシュが効いていない

対処法:

// シーケンシャルアクセスのヒントを与える
madvise(mapped, size, MADV_SEQUENTIAL);

// 事前にページをロード
madvise(mapped, size, MADV_WILLNEED);

// または明示的にプリロード
for (size_t i = 0; i < size; i += 4096) {
    volatile char c = mapped[i];  // ページフォルトを起こす
}

問題4:書き込みがディスクに反映されない

原因:

  • OSがキャッシュしている
  • msync()を呼んでいない

対処法:

// データを書き込み
strcpy(mapped, "Important data");

// ディスクに同期
msync(mapped, strlen("Important data"), MS_SYNC);

// または非同期で同期開始
msync(mapped, strlen("Important data"), MS_ASYNC);

問題5:複数プロセスでの同期

原因:

  • 適切な排他制御がない
  • キャッシュコヒーレンシの問題

対処法:

#include <sys/file.h>

// ファイルロックを使用
int fd = open("shared_file.dat", O_RDWR);
flock(fd, LOCK_EX);  // 排他ロック

// マッピングと処理
char *mapped = mmap(NULL, size, PROT_READ | PROT_WRITE, 
                   MAP_SHARED, fd, 0);
// データ更新
msync(mapped, size, MS_SYNC);

flock(fd, LOCK_UN);  // ロック解除
munmap(mapped, size);
close(fd);

まとめ:メモリマップはシステムプログラミングの基礎

メモリマップは、効率的なプログラミングに欠かせない重要な概念です。

この記事のポイント:

  • メモリマップはメモリ空間の配置図
  • プロセスのメモリマップでプログラムの構造を理解
  • テキスト、データ、BSS、ヒープ、スタックの各セグメント
  • メモリマップドファイルで高速なファイルアクセス
  • メモリマップドI/Oでハードウェア制御
  • /proc/[pid]/mapsで実際のマップを確認可能
  • 大容量ファイル処理に最適
  • プロセス間共有に活用できる
  • システムコールのオーバーヘッド削減
  • 適切なエラーハンドリングと同期が重要

メモリマップを理解することで、プログラムがどのようにメモリを使用しているか把握でき、パフォーマンスの最適化やデバッグに役立ちます。

特にメモリマップドファイルは、大容量データの処理、データベースの実装、プロセス間通信など、様々な場面で威力を発揮します。

システムプログラミングやパフォーマンスを重視する開発では、メモリマップの知識が必須です。ぜひ実際のコードで試して、その効果を体験してみてください!

コメント

タイトルとURLをコピーしました