TL

共有メモリIPCとメモリ同期の落とし穴

共有メモリは最速のIPCだが、同じ物理ページを2つのプロセスへ別アドレスで張る仕組みと、ロックを共有領域に置く作法、メモリバリアの要否を外すと壊れます。原理からつかめます。

応用共有メモリIPCmmap同期メモリバリアPOSIX最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.共有メモリは同一の物理ページを複数プロセスの仮想空間に多重マップする仕組み。コピーが起きないため最速だが、カーネルは中身の整合を一切保証せず、同期は完全にアプリの責任になる。
  • 2.ロックや条件変数は共有領域の中にPTHREAD_PROCESS_SHARED属性で置く。プロセスローカルのヒープに置いたミューテックスは相手から見えず機能しない。
  • 3.POSIX shm_open+mmapとSysV shmgetは、名前空間・ライフサイクル・サイズ変更可否が異なる。前者はファイル的に参照カウントとtmpfsで管理、後者はカーネル常駐でipcsに残り続ける。

なぜ共有メモリが最速なのか──コピーが消える

プロセス間通信 の手段の中で、共有メモリが最速とされる理由は単純です。データのコピーが一切起きないからです。

パイプやソケットでは、送信側のユーザバッファ → カーネルバッファ → 受信側のユーザバッファ、と最低2回のコピーとシステムコールが各転送で発生します。これに対し共有メモリは、同じ物理ページを送受信両プロセスの仮想アドレス空間に対応づけることで、書いた瞬間に相手から見える状態を作ります。確立後のデータ転送経路にカーネルは介在せず、システムコールもコピーもゼロです。

仕組みの核は 仮想記憶 のページテーブルにあります。各プロセスは独立した仮想アドレス空間を持ちますが、ページテーブルのエントリ(PTE)が指す物理フレームを共通にすれば、別々の仮想アドレスから同一の物理メモリを触れます。

プロセスA: 仮想 0x7f00_1000 ─┐
                            ├─→ 同一の物理フレーム 0x12345000
プロセスB: 仮想 0x55a0_2000 ─┘
マップされる仮想アドレスはプロセスごとに異なる

同じ共有領域でも、AとBで割り当てられる仮想アドレスは通常一致しません(mmapが返す番地はカーネル任せ)。そのため共有領域の中に生ポインタを書いてはいけません。Aの番地はBでは無効です。領域内で位置を指すなら、先頭からのオフセット(相対値)を使うのが鉄則です。連結リストやツリーを共有領域に置くなら、ポインタではなくインデックス/オフセットで参照を表現します。

カーネルは整合を保証しない──同期はすべてアプリの責任

ここが最大の落とし穴です。カーネルが提供するのは「同じ物理ページを両者に見せる」ことだけで、いつ・どの順で・どこまで書き込みが相手に見えるかについては何も約束しません。パイプには暗黙の順序とアトミックな境界(PIPE_BUF以下の書き込みは分断されない)がありますが、共有メモリにはそれすらありません

つまり「Aが構造体を書き終えてからBが読む」という当たり前の前提を、アプリ側が明示的に作らねばなりません。これを怠ると、Bが書きかけの中途半端な状態を読む、あるいは後述の並べ替えでフラグだけ先に見えてデータが見えないといった破綻が起きます。

IPC手段コピー回数同期・順序の保証
パイプ/FIFO2回(カーネル経由)順序保証あり・PIPE_BUF以下はアトミック
共有メモリ0回なし(アプリが全責任)
メッセージキュー2回メッセージ境界とFIFO/優先度順を保証

共有メモリの速さは「カーネルが手を引いた」ことの裏返しです。最速と引き換えに、同期の正しさという最も難しい部分が丸ごとアプリ側に降ってきます。

メモリバリアが要る理由──フラグとデータの順序

排他制御(同時に1つしか触らせない)だけでなく、書き込みが相手に見える順序も問題になります。よくある生産者・消費者パターンを考えます。

生産者: data = 42;        // ① データを書く
        ready = 1;        // ② 完成フラグを立てる

消費者: while (ready==0); // ③ フラグ待ち
        use(data);        // ④ データを使う

直感では①→②の順なので、③でフラグが見えたら④では必ず42が読めそうです。ところがそうとは限りません。CPUのストアバッファやコンパイラの最適化により、①と②の見える順序が入れ替わりうるからです。消費者側でも③と④の読み込みが投機的に並べ替えられえます。結果、フラグは1なのにdataは古い値という状態が観測されます。

これを防ぐのがメモリバリア(フェンス)です。生産者は②の前にreleaseバリア、消費者は③の後にacquireバリアを置くことで、「フラグが見えたならそれ以前の書き込みもすべて見える」順序を確立します。

単一プロセスでは見えなかった問題が表面化する

スレッド間と違い、共有メモリは別プロセス・別コアで動くのが普通です。同一プロセス内なら言語ランタイムやロックライブラリが暗黙にバリアを発行してくれることもありますが、自前のフラグ同期にはその保護がありません。さらに別コアではキャッシュの可視性タイミングが効いてきます。「ローカルのスレッドで動いたから大丈夫」という検証は、共有メモリでは通用しません。

実務では生バリアを自分で書くより、後述の 共有ミューテックス/セマフォを使うのが安全です。ロックの獲得・解放操作は内部で適切なバリアを発行するため、ロックで囲んだ区間の可視性は自動的に保証されます。

ロックは共有領域の中に置く──PROCESS_SHARED属性

共有メモリの同期で初学者が必ず踏むのが、ロックの配置です。pthreadミューテックスを普通に宣言すると、それはそのプロセスのヒープ/スタック上に作られ、相手プロセスからは見えません。各プロセスが「自分だけのロック」を取り合うので、排他はまったく機能しません。

正しくは、ミューテックス自体を共有メモリ領域の中に置き、かつ「複数プロセスで共有する」と宣言します。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// このミューテックスはプロセス間で共有されると宣言
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);

// shm の中(共有領域)にミューテックスを構築する
pthread_mutex_init(&shared->mutex, &attr);

PTHREAD_PROCESS_SHARED 属性が肝です。これがあると、ロックの待機・起床に使うfutex のキーが物理ページ基準で計算され、別プロセスからでも同じ待ち行列に合流できます。デフォルトの PTHREAD_PROCESS_PRIVATE ではプロセス内限定のキーになり、プロセスをまたいだ待ち合わせが成立しません。

ロック保持中にプロセスが死ぬと全員が固まる

共有ロックの怖さは、保持したプロセスがロックを握ったままクラッシュすると、残りの全プロセスが永久に待たされることです。スレッドと違い、プロセスの異常終了はカーネルがロックを自動解放してくれません。対策が ロバストミューテックスpthread_mutexattr_setrobust でPTHREAD_MUTEX_ROBUST指定)です。保持者が死ぬと、次にロックを取ろうとしたプロセスへ EOWNERDEAD が返り、状態を一貫させてから pthread_mutex_consistent で回復できます。共有メモリのロックでは事実上ロバスト指定が必須です。

POSIX shm_openとSysV shmget──同じ目的、違う流儀

共有メモリの作り方には大きく2系統あります。新規開発ならファイル的に扱えるPOSIX系(shm_open + mmap)が推奨されますが、既存システムでは古いSysV系(shmget)も現役です。原理上の違いを押さえます。

観点POSIX (shm_open + mmap)SysV (shmget + shmat)
名前空間/dev/shm 上の名前(/myshm)key_t(数値キー、ftokで生成)
実体tmpfs上のファイル。fdとして扱えるカーネル内オブジェクト
サイズ変更ftruncateで後から変更可作成時に固定、変更不可
ライフサイクル参照カウント方式。shm_unlinkで名前を消し、最後のmapが外れると解放明示的にipcrm/IPC_RMIDするまでカーネルに残存
後始末の漏れプロセス終了でmapは外れる(名前は残るが/dev/shmで可視)ipcsに残り続けやすく、リーク源になりがち

POSIXの shm_open が返すのはファイルディスクリプタで、その実体は /dev/shm(tmpfs)上のファイルです。だから mmap でアドレス空間に張れ、ftruncate でサイズを変えられ、ls /dev/shm で存在を確認できます。close してもファイルは残り、shm_unlink で名前を消します。「名前」と「マッピング」を分けて参照カウントで管理するのがファイル流の利点です。

MAP_SHAREDでなければ共有にならない

mmap でファイルや共有オブジェクトを張る際、フラグを MAP_SHARED にして初めて書き込みが他プロセスへ反映されます。MAP_PRIVATE を指定するとコピーオンライト になり、書き込んだ瞬間にそのページは自分専用のコピーに分岐します。つまり書いても相手には届きません。共有メモリのつもりで MAP_PRIVATE を使うのは典型的なミスです。匿名共有メモリ(親子間)なら mmap(NULL, len, ..., MAP_SHARED | MAP_ANONYMOUS, -1, 0) をforkの前に張る手もあります。

SysVの shmget はカーネル内に常駐オブジェクトを作り、shmat でアタッチします。明示的に IPC_RMID で削除しない限りカーネルに残り続けるため、異常終了でゴミが溜まりやすいのが弱点です。ipcs -m で残骸が見えるのはこのためで、運用では ipcrm での掃除が要ります。

落とし穴を避ける設計チェックリスト

試験・実務で問われる要点
  • コピーゼロが速さの源。確立後の転送にカーネルは介在しないが、その代わり同期・順序の保証は一切ない。すべてアプリ責任。
  • 生ポインタを共有領域に置かない。マップされる仮想アドレスはプロセスごとに違うため、参照はオフセット(相対位置)で持つ。
  • ロックは共有領域の中に置き PTHREAD_PROCESS_SHARED を指定する。プロセスローカルのミューテックスは相手から見えず無意味。
  • 保持者の死に備え ロバストミューテックスEOWNERDEAD 回復)を使う。
  • フラグ同期では メモリバリア(release/acquire)が必須。ロックで囲めば自動で発行される。
  • POSIXは参照カウント+tmpfsで後始末が素直、SysVはカーネル常駐でリークしやすい。mmapMAP_SHARED でなければ共有にならない。

まとめ──速さの代償は同期の全責任

まとめ

共有メモリは同一の物理フレームを複数プロセスの仮想空間に多重マップすることで、コピーもシステムコールもない最速のIPC を実現します。しかしカーネルが保証するのは「同じページが見える」ことだけで、いつ・どの順で書き込みが見えるかは何も約束しません。だから同期は完全にアプリの責任です。具体的には、(1) 参照は生ポインタでなくオフセットで持つ(マップ先アドレスがプロセスごとに違うため)、(2) ロックは共有領域の中PTHREAD_PROCESS_SHARED で置く、(3) 保持者の死に備えロバストミューテックスを使う、(4) フラグ同期ではメモリバリア を効かせる(ロックで囲めば自動)。作成APIは、参照カウントとtmpfsで素直に後始末できる**POSIX(shm_open+mmap)と、カーネル常駐でリークしやすいSysV(shmget)**に分かれ、mmapMAP_SHARED 指定がなければ共有になりません。最速という結果だけを見て同期を軽視すると、再現性の低いデータ破壊として跳ね返ってきます。

OS Article

共有メモリIPCとメモリ同期の落とし穴を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

共有メモリ

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

ロックや条件変数は共有領域の中にPTHREAD_PROCESS_SHARED属性で置く。プロセスローカルのヒープに置いたミューテックスは相手から見えず機能しない。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「共有メモリ / IPC」に近いか確認する。
  • 強みである「共有メモリは同一の物理ページを複数プロセスの仮想空間に多重マップする仕組み。コピーが起きないため最速だが、カーネルは中身の整合を一切保証せず、同期は完全にアプリの責任になる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

共有メモリIPCmmap同期メモリバリア共有メモリIPCmmap
参考: 公式情報