更新履歴(5件・最終更新 2026年09月07日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 主張・コード・注意事項を維持し、全3回の読み順と、仮想メモリの割り当て・物理ページの状態遷移・共有とコピーオンライトの仕組みおよび観測方法を、小見出しと比較表で読みやすく整理した。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの「SECTION_OBJECT_POINTERSはCache Managerと両立しない」という関係を、「Cache ManagerがSharedCacheMapを通じてこの構造を利用する(イメージフォルトの経路はCache Managerを通らない)」に改めました。本文の説明は変えていません。
- 文章のリズムと段落構成を見直して読みやすくリライトし、仕組みの説明に図を追加しました。技術的な内容、コード、参考リンクは変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.22054253)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windowsメモリの深層(第3回) ── セクションオブジェクトとコピーオンライト:DLLとファイルマッピングの正体」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22054253 https://comcomponent.com/blog/windows-memory-internals-section-copy-on-write/
- DOI(最新版)
- 10.5281/zenodo.22054253
- DOI(この版)
- 10.5281/zenodo.22556625
「同じkernel32.dllを100プロセスが使ったら、コードページも100組必要なのか」。第3回の出発点は、複数プロセスで同じ内容を使うときの疑問です。同じファイルを2プロセスがメモリマップした場合も、片方が読んだページをもう片方から使えるのでしょうか。
答えは、異なる仮想アドレスから、同じ物理ページへ対応付けられるという仕組みにあります。共有単位の中心がセクションオブジェクトです。そして、書き込み時に必要なページだけを私有化するのが、コピーオンライト(CoW)です。
前回の「Windowsメモリの深層(第2回) ── 物理ページの一生」では、Working Setから外れたページがModified、Standby、Free、Zeroedをどう移動するかを追いました。Standbyには、DLL、EXE、マップドファイル、ファイルキャッシュのページも残ります。今回は、そこに複数プロセスからの共有という視点を加えます。
EXE・DLL、データファイル、ページファイルバック共有メモリ、ファイルキャッシュを順に見たうえで、同じファイルストリーム上のキャッシュ・データ・イメージの経路を区別します。数字の読み方は、導入編「Windowsの『メモリ使用量』は何を表しているのか」を前提にします。
「Windowsメモリの深層」全3回
本連載は、物理ページを得る → 常駐・回収の流れを追う → 共有と私有化を理解する、という順に進みます。
| 回 | テーマ | この回で追うこと |
|---|---|---|
| 第1回 | 仮想アドレスとページフォルト | VirtualAllocした領域が、いつ物理RAMを得るか |
| 第2回 | 物理ページの一生 | Working Setから外れたページの状態遷移と、ページファイルの役割 |
| 第3回(本記事) | セクションオブジェクトとコピーオンライト | DLL・ファイルマッピング・共有メモリが、どのように物理ページを共有するか |
第3回が答える疑問は、ただ1つです。
同じDLLやファイルを、なぜ複数プロセスが1組の物理ページとして使えるのか。
| 読み始める前に | 内容 |
|---|---|
| 対象読者 | DLLの共有、CreateFileMapping、MapViewOfFile、共有メモリ、CoW、ファイルキャッシュの関係を内部構造から理解したい開発者・運用担当者 |
| 前提環境 | Windows 10/11または現行のWindows Server |
| 前提知識 | 仮想アドレス、ページフォルト、Working Setの基礎 |
| 難易度 | 中級。Control AreaやPrototype PTEという内部用語も扱う |
観測には、VMMap、Process Explorer、QueryWorkingSetExを使います。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全19件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論
全体像は、次の3点にまとめられます。
- 共有する内容と、各プロセスから見えるアドレスを分けます。 セクションオブジェクトは共有可能なメモリ範囲を表し、各プロセスはその一部をビューとしてマップします。同じセクションオフセットを使っていても、ビューの仮想アドレスは一致しなくてかまいません。12
- 何で内容を支えるか、どの経路で使うかを分けます。 セクションには実ファイルで支えるものとページファイルで支えるものがあります。EXE/DLLはイメージセクション、通常のファイルマッピングはデータセクションです。
SEC_IMAGEのページ保護はPE内の属性が決めます。キャッシュ・データ・イメージの経路も、1つにまとめてはいけません。234 - CoWの私有化は、ページ単位の共有状態で確認します。 読み取り中は共有し、書いたページだけコピーして、そのプロセスのPTEを差し替えます。
FILE_MAP_COPYはビュー全体へ先にCommitを課すため、Private Bytesだけでは発生を確定できません。確認にはQueryWorkingSetExのSharedビットを使います。567
一文でまとめるなら、共有されるのは仮想アドレスではなく、セクション内の内容と、その時点で対応する物理ページです。
| 知りたいこと | 読む節 |
|---|---|
| セクション・ビュー・バックストアの関係 | 2〜3節 |
| DLLの共有と、書き込み時のCoW | 4〜6節 |
| Cache Managerとの違いや、閉じても残る理由 | 7〜8節 |
| 2プロセスで共有と私有化を確かめたい | 9〜10節 |
2. セクションオブジェクトとビュー
Microsoftの定義では、セクションオブジェクトは共有できるメモリの区画を表し、ファイルをプロセスのアドレス空間へマップする機構でもあります。1
理解のコツは、セクションそのものと、各プロセスから見えるビューを分けて考えることです。
| 概念 | 役割 |
|---|---|
| セクションオブジェクト | 共有する内容、サイズ、バックストア、保護の上限を表す |
| ビュー | セクションの一部を、あるプロセスの仮想アドレス範囲へ見せる |
| PTE | ビュー内の各仮想ページを、現在の物理ページや未実体化状態へ結ぶ |
| PFN | 実際にRAMへ存在する物理ページを表す |
Win32 APIも、この役割の違いに沿って読み分けます。3
| 段階 | 得られるもの |
|---|---|
CreateFileMapping |
ファイルマッピングオブジェクトのハンドル |
MapViewOfFile |
プロセスの仮想空間に置かれたビュー |
| ビューへの初回アクセス | ページフォルトを通じた、アクセス対象ページの物理メモリへの対応付け |
まずは読み取り専用のファイルをマップする例です。
HANDLE mapping = CreateFileMappingW(
file,
nullptr,
PAGE_READONLY,
0,
0,
nullptr);
void* view = MapViewOfFile(
mapping,
FILE_MAP_READ,
0,
0,
0);
CreateFileMappingを呼んだだけでは、まだプロセスから読めるアドレスは得られません。さらに、ビューを作っても全ページが直ちにRAMへ入るわけではありません。最初に触れたページから、ページフォルトでファイル内容を読み込み、PTEへ物理ページを結んでいきます。8 つまり第1回で見たデマンドページングは、セクションビューにもそのまま適用されます。
2.1. 同じセクション、異なる仮想アドレス
プロセスAとBが同じセクションの同じオフセットをマップしても、ビューの開始アドレスは異なり得ます。
flowchart LR
accTitle: 異なる仮想アドレスから同じ物理ページへの対応付け
accDescr: プロセスAとプロセスBはそれぞれ別の仮想アドレスにビューを持つが、同じセクションオフセットを通じて同じ物理ページPFN Xへ到達する
viewA["Process A: 0x000001A00000 + 0x3000"] --> offset["セクションオフセット 0x3000"]
viewB["Process B: 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["同じ物理ページ PFN X"]
図1: 共有されるのはセクション内の内容と物理ページで、仮想アドレスではない。
共有メモリへ生ポインターを保存してはいけない理由がここにあります。プロセスAのポインター値は、プロセスBでは無関係なアドレスかもしれないからです。
共有構造には、ビュー先頭からのオフセット、固定幅整数、明示したバージョンと境界調整を使います。MicrosoftのMapViewOfFileExドキュメントも、同じアドレスが将来も利用できる保証はないため、ポインターではなくベースからのオフセットを保存するよう勧めています。6
3. ファイルバックとページファイルバック
セクションは、内容を復元する場所によって大きく2種類に分かれます。
flowchart TB
accTitle: ファイルバックとページファイルバックの分かれ方
accDescr: CreateFileMappingへ実ファイルを渡すとファイルバックセクションになり、cleanページは元ファイルから再読込できる。INVALID_HANDLE_VALUEを渡すとページファイルバックセクションになり、内容はページファイルが支え、オブジェクト破棄で消える
create["CreateFileMapping"] -->|実ファイルのハンドルを渡す| fileBacked["ファイルバックセクション"]
create -->|INVALID_HANDLE_VALUEを渡す| pfBacked["ページファイルバックセクション"]
fileBacked --> restore1["cleanページは元ファイルから再読込できる"]
pfBacked --> restore2["内容はページファイルが支え、破棄されると消える"]
図2: バックストアの違いが、内容を復元できる場所と寿命を決める。
3.1. ファイルバックセクション
実ファイルをCreateFileMappingへ渡すと、ファイルバックセクションになります。
- 読み取り専用ビューは、必要なページをファイルから読みます。
- 読み書きビューの変更は、そのファイルのデータとして扱われます。
- CoWビューの変更は元ファイルへ書かれず、私有ページになります。
ファイルバックページがcleanなら、物理ページを捨てても元ファイルから再読込できます。この性質が、第2回で見たStandbyとファイルキャッシュの効率を支えています。
3.2. ページファイルバックセクション
CreateFileMappingのhFileへINVALID_HANDLE_VALUEを渡し、サイズを指定すると、ページファイルバックセクションになります。
HANDLE mapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
64 * 1024,
L"Local\\KomuraMemoryDemo");
このセクションには明示的なデータファイルがなく、内容はページファイルで支えられます。初期内容はゼロです。複数プロセスから同じオブジェクトを使うには、名前、ハンドル継承、DuplicateHandleなどを使います。93
共有ページへの変更は見えますが、永続保存ではありません。 同じ共有ページをマップするプロセスは変更を参照できます。一方、セクションオブジェクトが破棄されれば内容も残らないため、永続ファイルの代わりには向きません。2
注意点として、共有メモリに排他制御が自動で付くわけではありません。mutex、semaphore、event、lock-freeプロトコルなどを別途設計します。9
4. イメージマッピングとデータマッピング
「EXE・DLLもファイルマッピング」と言うとき、通常のデータファイルとの違いは残しておく必要があります。
| 項目 | イメージマッピング | データマッピング |
|---|---|---|
| 主な用途 | EXE、DLLのロード | 通常ファイル、共有データ |
| 作成属性 | SEC_IMAGE |
PAGE_READONLY、PAGE_READWRITEなど |
| ページ保護 | PEイメージ内の属性が決める | マッピングとビューの指定が決める |
| 書き込み | writable sectionやCoWで私有化し得る | shared writeまたはCoWを選べる |
VirtualQueryのType |
MEM_IMAGE |
MEM_MAPPED |
SEC_IMAGEでは、CreateFileMappingへ渡した通常の保護値よりも、実行イメージ自身のセクション属性がビューのページ保護を決めます。3
4.1. DLL全体が必ず共有されるわけではない
コードのような変更されないページは、多数のプロセスで同じ物理ページを共有できます。プロセス固有の変更が必要なページは、CoWで分岐します。
ただし、ASLRによる再配置、ローダーの修正、ホットパッチ、実際のPEセクション属性などの影響があります。同じDLLをロードしていても、全ページが必ず共有されるわけではありません。
重要なのは、共有可能なページを先に共有し、変更が必要なページだけ遅延コピーするという設計です。
5. コピーオンライトの一部始終
2プロセスが同じCoWページを読んでいる状態から、Process Aだけが1バイト書くまでを追ってみます。
5.1. 書く前
書き込みが起きる前は、両プロセスのPTEは概念的に同じ共有ページへ到達し、読み取りはそのまま成功します。
flowchart LR
accTitle: コピーオンライト前の共有状態
accDescr: 書き込みが起きる前は、プロセスAとプロセスBのPTEがどちらも同じ共有ページPFN Xへ到達し、読み取りはそのまま成功する
pteA["Process A PTE"] --> pfnX["共有 PFN X(read / copy-on-write)"]
pteB["Process B PTE"] --> pfnX
図3: 書き込み前は、両プロセスのPTEが同じ物理ページを指している。
5.2. 書き込みで保護フォルト
CoWページは、最初から通常の共有書き込み可能ページにはなっていません。Process Aが書こうとすると、CPUは保護フォルトを発生させます。制御を受け取ったメモリマネージャーは、これが違法な書き込みではなく、CoW属性への書き込みだと判定します。
5.3. 新しい物理ページを作る
判定を受けて、Windowsは次を行います。
- Process A用の物理ページを1枚得る。
- PFN Xの内容を新ページPFN Yへコピーする。
- Process AのPTEをPFN Yへ差し替える。
- Process A側の保護を通常の読み書きへ変える。
- 失敗した書き込み命令を再実行する。
flowchart LR
accTitle: コピーオンライト後の分岐状態
accDescr: プロセスAの書き込みを受けて、内容をコピーした私有ページPFN YへプロセスAのPTEだけが差し替えられ、プロセスBのPTEは元の共有ページPFN Xを指し続ける
pteA2["Process A PTE"] --> pfnY["私有 PFN Y(read/write、変更後)"]
pteB2["Process B PTE"] --> pfnX2["共有 PFN X(元の内容)"]
pfnX2 -.->|書き込み時に内容をコピー| pfnY
図4: 書き込んだプロセスのPTEだけが新しい私有ページへ差し替わり、もう一方は元の内容を読み続ける。
Process Bは元の内容を読み続け、Process Aの変更を見ません。これがCopy-on-Writeです。DLLの共有も、FILE_MAP_COPYも、書くまでコピーしないという同じ原理を使っています。56
5.4. FILE_MAP_WRITEとの違い
選ぶ基準は、書き込みを相手にも見せたいのか、自分だけの変更にしたいのかです。6
| ビュー | 書き込み後に起きること |
|---|---|
FILE_MAP_WRITE |
共有書き込みとして、同じファイルマッピングを使う別ビューにも変更が見える |
FILE_MAP_COPY |
書いたページだけプロセス専用になる。変更は元ファイルへ書き戻されず、ビューを外すと失われる |
「共有メモリで更新を伝えたい」のか、「共通の初期データから各プロセスが私有変更したい」のか。目的によって選択が正反対になります。
6. CoW後もMEM_MAPPED / MEM_IMAGEのまま
ここでは、領域の由来と、今の物理ページの共有状態を分けます。
CoW後のページは物理的にはPrivateですが、VirtualQueryのTypeはMEM_PRIVATEへ変わりません。データビューならMEM_MAPPED、実行イメージならMEM_IMAGEのままです。Typeが示すのは、その領域がどの初期割り当てに由来するかだからです。7
ページ単位でCoW済みかどうかを見るには、次の手順を使います。
- 対象ページへアクセスし、常駐させる。
QueryWorkingSetExでページのWorking Set情報を取得する。Sharedビットを見る。Shared == 0なら、その常駐ページはPrivateです。
VMMapで確認する場合も、領域のTypeだけでなく、Working SetのPrivate/Shareable内訳を見ます。
6.1. Private Bytesが増えないこともある
FILE_MAP_COPYは、書いたページだけを私有化します。ただし、Commitを課すタイミングは別です。
将来ビュー内の全ページへ書く可能性に備え、Windowsはマップ時点でビュー全体に相当するCommit chargeを取ります。6 そのため、最初の1ページへ書いた瞬間にPrivate Bytesが4KiB増えるとは限りません。
CoWの観測で優先する指標は次のとおりです。
QueryWorkingSetExのSharedビット- VMMapのPrivate WS / Shareable WS
- RAMMapの物理ページ情報
- Private Bytesは補助情報
「書いた瞬間にPrivate Bytesが増えたか」だけを合否判定にすると、正しく動いているCoWを見落とします。
7. Cache Managerとの接点 ── 3つの経路を分ける
「EXE・DLLのロードも、ファイルキャッシュも、共有メモリも、全部セクション」と言えば全体像はつかめますが、実装を1個のオブジェクトへ潰してはいけません。
ファイルストリームには、メモリマネージャーとCache Managerが使うSECTION_OBJECT_POINTERSがあります。
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
この構造は、ファイルオブジェクトをファイルストリームのセクションと結び、メモリ上の内容とキャッシュ情報を追うものです。4 ただし、各フィールドが指す状態と処理経路は別です。
| 経路 | 対応するフィールド | 処理の役割 |
|---|---|---|
cached ReadFile / WriteFile |
SharedCacheMap |
Cache Managerがキャッシュビューを使う |
| データファイルのマッピングフォルト | DataSectionObject |
メモリマネージャーがデータセクション側で処理し、同じファイルストリーム上のcached I/Oと内容の整合性を保つよう協調する |
| EXE/DLLのイメージフォルト | ImageSectionObject |
メモリマネージャーがイメージセクションとページングI/Oで処理する。SharedCacheMapを経由する経路ではない |
I/O連載との接点は、この3つを同じファイルストリーム上で結び付けていることです。「どの経路もCache Managerを通る」と読み替えてはいけません。
flowchart TB
accTitle: 同じファイルストリームに結び付く3つの経路
accDescr: cached ReadFile/WriteFileはSharedCacheMapを、データマッピングのフォルトはDataSectionObjectを、EXE/DLLのイメージフォルトはImageSectionObjectを使い、3つはSECTION_OBJECT_POINTERSを介して同じファイルストリームに結び付く
cached["cached ReadFile / WriteFile"] --> scm["SharedCacheMap"]
dataFault["データマッピングのフォルト"] --> dso["DataSectionObject"]
imageFault["EXE/DLLのイメージフォルト"] --> iso["ImageSectionObject"]
scm --> stream["同じファイルストリーム(SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
図5: 3つの経路は別々に処理されるが、同じファイルストリーム上で結び付いている。
3つの共通点は「すべてがCache Managerへ入る」ことではなく、同じファイルストリームがSECTION_OBJECT_POINTERSを介して、キャッシュ、データセクション、イメージセクションという別々の状態を結び付けていることです。4 キャッシュ読み書き、Lazy Writer、Cc/Mmの関係は「Windows I/Oの深層(第4回) ── キャッシュマネージャーとWriteFile」で扱っています。
なお、メモリマップトビューとReadFile/WriteFileを混在させた場合、常に同じ瞬間の内容が見えるとは保証されません。同期、flush、ファイル共有モードを含めた設計が必要です。36
8. オブジェクトとビューの寿命
ハンドルを閉じることと、ビューを外すことは別です。 CreateFileMappingのハンドルを閉じただけでは、既存のビューは消えません。
ビューはセクションへの内部参照を持っています。すべてのビューをUnmapViewOfFileし、すべてのハンドルをCloseHandleして初めて、オブジェクトを破棄できる状態になります。3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
この寿命の分離は、「ファイルを閉じたのに使用中」という現象の原因になります。ファイルハンドルを閉じても、イメージセクションやデータビューがファイルストリームを参照していれば、ファイルの最終的なcloseは後になるのです。
I/O側のcleanup/closeとの関係は「Windows I/Oの深層(第1回)」、実装上の落とし穴は「共有メモリの落とし穴と実務ベストプラクティス」も参照してください。
9. 自分の目で確かめる
9.1. 同じDLLを2プロセスで見る
まずは既存のプロセスで、DLLの共有を確かめます。
- Process Explorerを管理者として起動します。
cmd.exeを2つ起動します。- View > Lower Pane View > DLLsを選びます。
- 両プロセスで同じDLLのパスとマッピングを確認します。
- VMMapで各
cmd.exeを開き、ImagesのWorking Set、Private、Shareableを比較します。
同じDLLの表示だけでは、PFNの一致まで証明できない
Process Explorerで同じDLLが見えることは、両方が同じイメージをマップしている証拠です。ただし、それだけでは各ページのPFN一致までは証明できません。VMMapのShareable内訳、RAMMap、QueryWorkingSetExを組み合わせて、ページ単位の共有を確認します。Process ExplorerとVMMapはSysinternalsが提供しています。1011
9.2. FILE_MAP_COPYを2プロセスで観測する
次のプログラムは、同じファイルをCoWビューとしてマップし、QueryWorkingSetExのSharedビットを表示します。ファイルマッピングオブジェクトはPAGE_READONLYで作成しますが、この保護はFILE_MAP_COPYのビューと互換で、ビュー側の最初の書き込みがCoWを発生させます。3
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cwchar>
#pragma comment(lib, "Psapi.lib")
void PrintPage(const char* stage, void* address)
{
MEMORY_BASIC_INFORMATION mbi{};
if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
std::printf("VirtualQuery failed: %lu\n", GetLastError());
return;
}
for (int attempt = 0; attempt < 3; ++attempt) {
// The page may have been trimmed while the user was waiting.
// Touch it immediately before querying the working-set attributes.
volatile unsigned char resident =
*static_cast<volatile unsigned char*>(address);
(void)resident;
PSAPI_WORKING_SET_EX_INFORMATION ws{};
ws.VirtualAddress = address;
if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
return;
}
if (!ws.VirtualAttributes.Valid) {
Sleep(0);
continue;
}
std::printf(
"%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
stage,
static_cast<unsigned long>(mbi.Type),
static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
return;
}
std::printf(
"%s: page is not resident; Shared/ShareCount were not interpreted\n",
stage);
}
int wmain(int argc, wchar_t** argv)
{
if (argc != 3) {
std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
return 2;
}
HANDLE file = CreateFileW(
argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return 3;
HANDLE mapping = CreateFileMappingW(
file, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!mapping) {
CloseHandle(file);
return 4;
}
auto* view = static_cast<unsigned char*>(
MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
if (!view) {
CloseHandle(mapping);
CloseHandle(file);
return 5;
}
volatile unsigned char value = view[0];
(void)value;
std::puts("Start the other process. When both are waiting, press Enter...");
(void)std::getchar();
PrintPage("before", view);
if (std::wcscmp(argv[2], L"write") == 0) {
std::puts("Press Enter to trigger copy-on-write...");
(void)std::getchar();
view[0] ^= 0x5a;
PrintPage("after write", view);
} else {
std::puts("After the writer changes its page, press Enter...");
(void)std::getchar();
PrintPage("reader after peer write", view);
}
std::puts("Press Enter to exit...");
(void)std::getchar();
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
}
ビルドして、同じファイルを2プロセスで開く
ビルドと準備を行います。
cl /std:c++20 /EHsc /W4 cow_demo.cpp
$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))
続いて、2つのコンソールで同じファイルを開きます。
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write
Enterを押す順番と、見る値を分ける
| 順序 | 操作 | 確認すること |
|---|---|---|
| 1 | 両方を起動し、まずread側、次にwrite側でEnterを押す | 両方のbeforeでSharedが立っている |
| 2 | write側でもう一度Enterを押す | 書いたプロセスのページはSharedが0になる |
| 3 | その後read側でEnterを押す | reader側は元ページを読み続ける |
VirtualQueryのTypeは、書き込み後もMEM_MAPPEDのままです。共有状態の変化とTypeの値を、別々に確認してください。
Validが立った結果だけを解釈する
なお、PrintPageは照会直前に対象ページを再タッチし、Valid == 0ならSharedとShareCountを解釈せずに最大3回再試行します。それでも常駐しなければ結果を出さず、その旨を表示します。タイミングやメモリ圧力でShareCountは変わり得るため、固定値ではなく、Valid == 1で確認できたSharedビットの変化を見てください。
10. 実務で避けたい5つの誤読
10.1. 「共有メモリは同じ仮想アドレスになる」
共有されるのはセクションと物理ページです。ビューの仮想アドレスはプロセスごとに違い得るため、生ポインターではなくオフセットを保存します。
10.2. 「同じDLLなら全ページが必ず共有される」
cleanなコードページは共有しやすい一方、再配置、writable section、CoW、計測時点の常駐状態によりPrivateページも生まれます。
10.3. 「CoW後はMEM_PRIVATEになる」
VirtualQueryのTypeはMEM_MAPPEDまたはMEM_IMAGEのままです。実際の共有状態はQueryWorkingSetExで確認します。7
10.4. 「Private Bytesが増えなければCoWしていない」
FILE_MAP_COPYはビュー全体へ先にCommitを課します。Private WSとSharedビットを優先します。6
10.5. 「共有ページなら同期は不要」
同じ物理ページが見えることと、複数CPUコアから安全に更新できることは別問題です。原子性、メモリ順序、排他、クラッシュ時の中間状態、バージョン互換性を設計します。
参照と寿命を分けて追うという考え方は、Excel COM相互運用でプロセスが残る問題にも通じます。「Excel COM相互運用でプロセスが残る原因」も参照してください。
11. まとめ
- セクションオブジェクトは共有可能なメモリ範囲を表し、各プロセスはビューとして自分の仮想空間へマップします。1
- 同じセクションの同じオフセットは、異なる仮想アドレスから同じ物理ページへ対応付けられます。2
- ファイルバックセクションは実ファイル、ページファイルバックセクションは名前付き共有メモリなどを支えます。9
- EXE/DLLはイメージセクション、通常ファイルはデータセクションとして扱われ、保護と書き戻し先が異なります。3
- CoWは読み取り中の物理ページを共有し、最初の書き込みでそのページだけコピーしてPTEを差し替えます。5
- CoW後も
VirtualQueryはMEM_MAPPED/MEM_IMAGEを返すため、QueryWorkingSetExのSharedビットで確認します。7 FILE_MAP_COPYではビュー全体のCommitが先に課されるため、Private BytesだけでCoWを判定できません。6- Cache Managerのcached I/O、データマッピング、イメージマッピングは、それぞれ
SharedCacheMap、DataSectionObject、ImageSectionObjectを使う別経路として、同じファイルストリーム上で結び付きます。4 - ビューとハンドルの寿命、同期、ACL、オフセット設計まで含めて初めて安全な共有メモリになります。
これで「Windowsメモリの深層」全3回は完結です。仮想アドレスをReserve/Commitし、ページフォルトで物理ページを得て、Working Setからページリストへ移し、セクションを通じて共有し、書いたページだけCoWで分岐する──Windowsのメモリ管理は、この一連の流れとしてつながっています。
関連記事
- Windowsメモリの深層(第1回) ── 仮想アドレスが物理RAMに変わる瞬間
- Windowsメモリの深層(第2回) ── 物理ページの一生
- Windows I/Oの深層(第4回) ── キャッシュマネージャーとWriteFile
- 共有メモリの落とし穴と実務ベストプラクティス
- Excel COM相互運用でプロセスが残る原因
- Process Explorer / Handle / VMMap実践
関連する相談領域
合同会社小村ソフトでは、Windowsアプリケーションの共有メモリ、ファイルマッピング、DLLロード、ファイルロック、プロセス間通信、メモリ使用量の不具合調査を扱っています。
参考リンク
-
Microsoft Learn, Section Objects and Views. セクションオブジェクトが共有可能なメモリ範囲を表し、各プロセスがセクションの一部をビューとしてマップすることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections. ファイルバック/ページファイルバックセクション、CoW、異なるプロセスの仮想アドレスから同じ物理メモリを共有できることについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function. ファイルマッピングオブジェクト、ページファイルバック、
SEC_IMAGE、ビューとハンドルの寿命、同じファイルを支えるビューの整合性について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, SECTION_OBJECT_POINTERS structure. DataSectionObject、SharedCacheMap、ImageSectionObjectが、ファイルストリームのマッピングとキャッシュ情報をメモリマネージャー/Cache Managerへ結ぶことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Memory Protection. 複数プロセスが同じDLLの物理ページを共有し、一方の書き込み時に新しい物理ページへコピーしてPTEを更新するCoWについて。 ↩ ↩2 ↩3
-
Microsoft Learn, MapViewOfFileEx function.
FILE_MAP_COPYのCoW、私有ページがページファイルで支えられること、ビュー全体へCommit chargeを課すこと、仮想アドレスではなくオフセットを保存すべきことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, VirtualQuery function. CoW後もTypeが
MEM_MAPPED/MEM_IMAGEのままで、QueryWorkingSetExのSharedビットにより私有化を確認できることについて。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections. ビューがアクセスされるまで物理メモリは割り当てられず、初回アクセスのページフォルトでファイル内容を読み込むことについて。 ↩
-
Microsoft Learn, Sharing Files and Memory. 同じファイルマッピングオブジェクトを名前・ハンドルで共有すること、
INVALID_HANDLE_VALUEでページファイルバック共有メモリを作ること、同期が別途必要なことについて。 ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals. Process Explorerがプロセスのハンドルやロード済みDLL/メモリマップトファイルを表示できることについて。 ↩
-
Microsoft Learn, VMMap - Sysinternals. VMMapがプロセスの仮想メモリをImage、Mapped File、Privateなどに分解し、Working SetのPrivate/Shareable内訳を表示することについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
DllMainとローダーロック ── 「DLLの初期化で何もするな」と言われる本当の理由
DllMainでLoadLibraryやスレッド同期をしてはいけないのはなぜか。全DLL通知を直列化するローダーロックの仕組みから、デッドロックが成立する典型シナリオ、遅延初期化などの正しい設計、ハング調査の手順までを一次情報で解説します。
Windowsメモリの深層(第2回) ── 物理ページの一生:5つのリストとページファイルの真実
PFNデータベース、Standby、Modified、メモリ圧縮、ページファイルをつなぎ、Working Setから外れた物理ページの行き先を解説します。
Windowsメモリの深層(第1回) ── 仮想アドレスが物理RAMに変わる瞬間:ページフォルトの一部始終
VirtualAlloc、VAD、ページテーブル、TLB、デマンドゼロ、ハードフォルトをつなぎ、仮想アドレスが物理RAMへ割り当てられる瞬間を解説します。
Windowsの「メモリ使用量」は何を表しているのか ── Working Set・Private Bytes・Commit・ページファイルを正しく読む
タスクマネージャーのメモリ、Working Set、Private Bytes、Commitは同じ値ではありません。Windowsの仮想メモリと物理メモリの関係、ページファイルの役割、メモリ不足やリーク調査で見るべき指標を解説します。
Windowsのプロセス間通信をどう選ぶか ── 名前付きパイプ / TCP / gRPC / 共有メモリ / COM 判断表
Windowsアプリ同士の連携手段をどう選ぶか。名前付きパイプ、ローカルTCP、gRPC、共有メモリ、ファイル連携、COMの得意分野と落とし穴を判断表で整理し、定番構成と名前付きパイプの実装例まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 同じDLLを使うプロセスごとに、DLL全体がRAMへ複製されますか?
- 通常は複製されません。同じイメージの変更されていないページは、各プロセスの異なる仮想アドレスから同じ物理ページへ対応付けられます。書き込みが必要なページだけ、コピーオンライトなどによりプロセス固有の物理ページになります。
- CreateFileMappingは、その時点でプロセスへメモリを割り当てますか?
- CreateFileMappingはファイルマッピングオブジェクトを作りますが、プロセスの仮想空間へ見えるようにするのはMapViewOfFileです。さらに、ビューの物理ページは通常、初めてアクセスしたページからページフォルトで実体化されます。
- FILE_MAP_WRITEとFILE_MAP_COPYは何が違いますか?
- FILE_MAP_WRITEの変更は共有されたファイルデータ側へ反映される書き込みです。FILE_MAP_COPYは初期ページを共有しますが、書いたページだけプロセス専用コピーとなり、変更は元ファイルへ書き戻されず、ビューを外すと失われます。
- コピーオンライト後はVirtualQueryがMEM_PRIVATEを返しますか?
- 返しません。データビューならMEM_MAPPED、イメージビューならMEM_IMAGEのままです。ページが実際に私有化されたかは、ページを常駐させたうえでQueryWorkingSetExのSharedビットを見るのが確実です。
- 共有メモリへ生のポインターを保存してよいですか?
- 通常は避けます。同じセクションでも各プロセスのビューが同じ仮想アドレスへ配置される保証はありません。共有構造ではベースからのオフセット、固定幅整数、明示したレイアウトと同期方式を使います。