メモリ共有してみた
Windows10で共有メモリによるプロセス間データ交換

どもです。

前回まで、名前付きパイプを使用したプロセス間通信について書いてきました。
その記事は、以下の投稿を参照してください。
プロセス間通信をしてみた(1) Windows10の名前付きパイプでの通信
プロセス間通信をしてみた(2) Windows10の名前付きパイプでの通信の続き
プロセス間通信をしてみた(3) サーバ側をマルチスレッド化してみた
プロセス間通信をしてみた(4) プロセス間通信でエコーバックしてみた
これらの記事で紹介した「名前付きパイプ」では、送信側/受信側で同期処理/送受信待ち制御が必要になります。
しかし、プロセス間でデータを交換する際に「制御」が必要ない場合もあるかと思います。
今回のエントリでは、こういった「制御」を行わずにプロセス間でデータを交換する方法として、

共有メモリ

を紹介します。

1.開発環境

今回の開発環境を、下表に記載します。
名前付きパイプの場合と完全に同じです。

開発環境
項目 内容
OS Windows10 Pro(1909)
CPU i7-8700
メモリ 16GB
IDE VisualStudioCommnuity 2019
version 16.5.1
※プロジェクトの設定

  • コンソールアプリケーション
  • MFC使用

2.共有メモリ

まず、共有メモリについて、簡単ですが解説します。

2.1.共有メモリとは

読んで字のごとく、「メモリの共有」です。
本来、複数のプロセスでは、メモリの共有はできません。
それに対して、複数のプロセスでデータの書き込み、読み出しができるようにしたメモリのことです。

2.2.名前付きパイプとの違い

共有メモリと名前付きパイプとの違いですが、簡単に下図のように示すことができると思います。


最初に書きましたが、名前付きパイプでは「送信側がデータを送るのを待つ」必要があります。
それに対して共有メモリでは、対象のメモリアクセスし、そのデータを待つことなく取得できます。
データを送信する側、書き込む側が何もしなければ、何もデータが取得できないという点では、共有メモリも名前付きパイプも同じです。
しかし、共有メモリでは「何も書き込まれていないデータ」は取得できます。
この点が、共有メモリと名前付きパイプの大きな違いかと考えます。

3.共有メモリの使い方

共有メモリがどんなものか、簡単に説明したところで、次は実装方法について書きます。

3.1.処理の順番

共有メモリを使用するための手順、および各手順の概要は、以下の通りです。

  1. ファイルマッピングを生成する
    • データを共有するためのオブジェクト(ファイルマッピングオブジェクト)の生成
  2. 生成したファイルマップを、メモリ上にマップする。
    • 共有するメモリ領域のサイズを指定して、ファイルマッピングオブジェクトをメモリ上に配置。
      共有されたメモリ領域のアドレスを取得。
  3. メモリへ書込み/読出しを行う

3.2.処理/手順の実装

各処理/手順を使用するために使用する関数について書きます。

3.2.1.ファイルマッピングを生成する

ファイルマッピング、即ちファイルマッピングオブジェクトを生成するためには、「CreateFileMapping」関数を使用します。
(この関数の詳細は、Microsoftのサイトを参照してください。)
この関数の使用例は、以下の通りです。

HANDLE sharedMemoryHandle = ::CreateFileMapping(
		INVALID_HANDLE_VALUE, 
		NULL, 
		PAGE_READWRITE, 
		0, 
		100, 
		_T("SAMPLE_SHARED_MEMORY_NAME"));

正常に処理が完了した場合には、生成したファイルマッピングオブジェクトのハンドルが返ります。
各引数の内容は、下表の通りです。

引数の内容
第1引数 生成するオブジェクトのハンドル。
INVALID_HANDLE_VALUEでOK!
第2引数 生成するオブジェクトのハンドルを子プロセスで継承する場合は、セキュリティ情報を設定する。
子プロセスがないのであれば、NULLでOK!
第3引数 アクセス制限
今回は、書き込み/読み出しの両方をできるように設定。
第4、5引数 共有メモリの最小/最大サイズ
バイト単位
第6引数 共有メモリの名前

3.2.2.メモリ上にマップ

生成したファイルマッピングオブジェクトをメモリ上に配置するためには、「MapViewOfFile 」を使用します。
この関数の詳細は、Microsoftのサイトを参照してください。
使用例を以下に示します。

BYTE* sharedMemory = (BYTE*)::MapViewOfFile(
		sharedMemoryHandle,
		FILE_MAP_WRITE | FILE_MAP_READ,
		0,
		0,
		100);

処理が正常に完了した場合には、マッピングしたメモリへのポインタが返ります。
(上述のコードでは、ポインタをBYTE型に変換しています。)
このポインタに対して、値の書き込みや読み出しを行います。

3.2.3.書込み/読出し

データの書込み/読出しについては、ポインタの実体への値のセット、実体からの値の取得と同じ実装になります。
例えば、0から100までの値を順番に書き込む場合には、以下のコードになります。

for (int index = 0; index < 100; index++) {
	sharedMemory[index] = index;
}

4.実装

共有メモリを実装します。
今回は、共有メモリを提供するクラスとして「CSharedMemory」クラスを定義し、このクラスを使用してデータの書込み、読出しを行います。

4.1.CSharedMemoryクラス

CSharedMemoryクラスの実装は、以下の通りです。
まずはヘッダファイル/クラスの定義です。
今回は、Read/Writeと、初期化用のInitializeのみを実装しています。
また、共有メモリのハンドルとポインタをメンバ変数として持つようにしました。

class CSharedMemory
{
public:
	CSharedMemory(int data_byte_size);
	virtual ~CSharedMemory();
	
	BYTE* Read(BYTE* data, int data_size);
	BYTE* Write(BYTE* data, int data_size);

protected:
	VOID	Initialize(int data_byte_size);

protected:
	HANDLE	shared_memory_handle_;
	BYTE* shared_memory_;

	int shared_memory_size_;
};

次にソースファイルです。
特別なことはしていません。

#include "pch.h"
#include "CSharedMemory.h"
CString	shared_memory_name = _T("SHARED_MEMORY_NAME");

CSharedMemory::CSharedMemory(int data_byte_size)
	: shared_memory_handle_(NULL)
	, shared_memory_(NULL)
	, shared_memory_size_(data_byte_size)
{
	this->Initialize(this->shared_memory_size_);
}

CSharedMemory::~CSharedMemory()
{
	if (NULL != this->shared_memory_) {
		::UnmapViewOfFile(this->shared_memory_);
		this->shared_memory_ = NULL;
	}
	if (NULL != this->shared_memory_handle_) {
		::CloseHandle(this->shared_memory_handle_);
		this->shared_memory_handle_ = NULL;
	}
}

VOID CSharedMemory::Initialize(int data_byte_size)
{
	HANDLE shared_memory_handle = ::CreateFileMapping(
		INVALID_HANDLE_VALUE,
		NULL,
		PAGE_READWRITE,
		0,
		data_byte_size,
		shared_memory_name);
	if (NULL == shared_memory_handle) {
		_tprintf(_T("Can not create file mapping\n"));

		return;
	}
	BYTE* shared_memory = (BYTE*)::MapViewOfFile(
		shared_memory_handle,
		FILE_MAP_READ | FILE_MAP_WRITE,
		0, 0,
		data_byte_size);
	if (NULL == shared_memory) {
		_tprintf(_T("Can not map file into file.\n"));
		CloseHandle(shared_memory_handle);

		return;
	}

	this->shared_memory_handle_ = shared_memory_handle;
	this->shared_memory_ = shared_memory;
	for (int index = 0; index < this->shared_memory_size_; index++) {
		this->shared_memory_[index] = 0x00;
	}
}

BYTE* CSharedMemory::Read(BYTE* data, const int data_size)
{
	for (int index = 0; index < data_size; index++) {
		*(data + index) = *(this->shared_memory_ + index);
	}
	return this->shared_memory_;
}

BYTE* CSharedMemory::Write(BYTE* data, const int data_size)
{
	for (int index = 0; index < data_size; index++) {
		*(this->shared_memory_ + index) = *(data + index);
	}

	return this->shared_memory_;
}

少し長いですが、これがCSharedMemoryクラス全体です。

4.2.書込み処理

データの書込みの実装は、以下のようになります。

//書き込みデータの作成
for (int data_index = 0; data_index < BUFFER_SIZE; data_index++) {
	write_data_buffer[data_index] = data_index + index;
}

shared_memory.Write(write_data_buffer, BUFFER_SIZE);

4.3.読出し処理

読出しについては簡単です。

shared_memory.Read(read_data_buffer, BUFFER_SIZE);

この1行。
説明は不要でしょう。

5.実行

読出しと書込みを行うプログラムをそれぞれ作成、実行した結果が下図です。
左側が書込み、右側が読出し処理です。
書込みは、書き込んだデータを表示しています。
また読出しでは、読み出したデータを表示しています。

この図の通り、書き込んだデータが読み出せている、即ちデータの共有ができていることが確認できました。

6.まとめ

今回は、Windows上での共有メモリについて書きました。
簡単なデータで、非同期でプロセス間のデータ交換ができることが分かりました。
また今回は書込みと読出しが1対1でしたが、n対1での処理も可能の様です。
このn対1の処理については、また試してみて、別のエントリで書こうと思ています。

ではっ!

ex.公開しています

今回紹介したコードは、GitHubで公開しています。
CSharedMemoryはもちろん、ほかの内容についても参考になれば嬉しいです。