S-JIS[2013-01-10]

CreatePipe関数

CreatePipe()は、Windows API(C言語)でパイプを生成する関数。


概要

ここでいうパイプとは、バッファー(ファイル)のようなもの。
パイプに対してデータを書き込んでおき、そのパイプからデータを読み込むことが出来る。

これは一時ファイルのようなイメージなので、
パイプのハンドルを子プロセスに渡せば、プロセス間通信を行うことが出来る


とりあえずパイプの使い方の確認。

#include <stdio.h>
#include <windows.h>
HANDLE readPipe  = NULL;
HANDLE writePipe = NULL;

int execute(void)
{
	// パイプの作成
	if (!CreatePipe(&readPipe, &writePipe, NULL, 0)) {
		printError("CreatePipe");
		return -1;
	}

	// パイプへの書き込み
	const char message[] = "example-pipe";
	DWORD numberOfBytesWritten;
	if (!WriteFile(writePipe, message, strlen(message), &numberOfBytesWritten, NULL)) {
		printError("WriteFile");
		return -1;
	}
	// 書込パイプのクローズ
	if (!CloseHandle(writePipe)) {
		printError("CloseHandle(writePipe)");
		return -1;
	}
	writePipe = NULL;

	// パイプからの読み込み
	while(1) {
		char buf[256+1];
		DWORD numberOfBytesRead;
		if (!ReadFile(readPipe, buf, sizeof(buf)-1, &numberOfBytesRead, NULL)) {
			if (GetLastError() == ERROR_BROKEN_PIPE) {
				printf("pipe end\n");
				break; //ループ終了
			}
			printError("ReadFile");
			return -1;
		}

		buf[numberOfBytesRead] = '\0';
		printf("read=[%s]\n", buf);
	}
	// 読込パイプのクローズ
	if (!CloseHandle(readPipe)) {
		printError("CloseHandle(readPipe1)");
		return -1;
	}
	readPipe = NULL;

	return 0;
}

CreatePipe()が成功するとパイプの書込用ハンドルと読込用ハンドルが返ってくるので、後続処理ではそれを使う。
パイプの書き込み側のハンドルがクローズされると、読み込み側(ReadFile)ではERROR_BROKEN_PIPEが返ってくる。

int main()
{
	printf("start\n");

	int r = execute();

	if (readPipe != NULL) {
		if(!CloseHandle(readPipe)) {
			printError("CloseHandle(readPipe)");
		}
	}
	if (writePipe != NULL) {
		if(!CloseHandle(writePipe)) {
			printError("CloseHandle(writePipe)");
		}
	}

	printf("end\n");
	return r;
}

子プロセスとの通信の例

CreateProcess()を使って子プロセスを生成し、パイプを使って 親プロセスからの書き込みを子プロセスの標準入力で受け取る例。
(→UNIX(POSIX)でパイプを子プロセスの標準入力へ繋ぐ例

CreateProcessの引数のSTARTUPINFOには子プロセスで使用する標準入出力(stdin・stdout・stderr)のハンドルを渡すことが出来る。
これにより、子プロセスの標準入出力先をパイプにしてやれば、パイプを使ってプロセス間通信が出来る。

#include <stdio.h>
#include <windows.h>
HANDLE readPipe  = NULL;
HANDLE writePipe = NULL;

int createPipe(void)
{
	HANDLE readTemp;

	// パイプを作成(両ハンドルとも子プロセスへ継承不可)
	if (!CreatePipe(&readTemp, &writePipe, NULL, 0)) {
		printError("CreatePipe");
		return -1;
	}

	// 読込ハンドルを複製(子プロセスへ継承可能な権限の読込ハンドルを作成)
	if (!DuplicateHandle(
		GetCurrentProcess(), readTemp,
		GetCurrentProcess(), &readPipe,
		0, TRUE, DUPLICATE_SAME_ACCESS)
	) {
		printError("DuplicateHandle");
		if (!CloseHandle(readTemp)) {
			printError("CloseHandle(readTemp)");
		}
		return -1;
	}

	// 複製元のハンドルは使わないのでクローズ
	if (!CloseHandle(readTemp)) {
		printError("CloseHandle(readTemp)");
		return -1;
	}

	return 0;
}
HANDLE childProcess = NULL;

int execute(LPTSTR commandLine)
{
	BOOL  bInheritHandles = TRUE;
	DWORD creationFlags   = 0;

	STARTUPINFO si = {};
	si.cb         = sizeof(STARTUPINFO);
	si.dwFlags    = STARTF_USESTDHANDLES;
	si.hStdInput  = readPipe;	//子プロセスの標準入力は読込用パイプにする
	si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); //親プロセスの標準出力を継承
	si.hStdError  = GetStdHandle(STD_ERROR_HANDLE);  //親プロセスの標準エラーを継承

	if (si.hStdOutput == INVALID_HANDLE_VALUE) {
		printError("GetStdHandle(STD_OUTPUT_HANDLE)");
		return -1;
	}
	if (si.hStdError == INVALID_HANDLE_VALUE) {
		printError("GetStdHandle(STD_ERROR_HANDLE)");
		return -1;
	}

	// 子プロセスを起動
	PROCESS_INFORMATION pi = {};
	if (!CreateProcess(
		NULL,
		commandLine,
		NULL,	//プロセスのセキュリティー記述子
		NULL,	//スレッドのセキュリティー記述子
		bInheritHandles,
		creationFlags,
		NULL,	//環境変数は引き継ぐ
		NULL,	//カレントディレクトリーは同じ
		&si,
		&pi)
	) {
		printError("CreateProcess");
		return -1;
	}
	childProcess = pi.hProcess;
	if (!CloseHandle(pi.hThread)) {
		printError("CloseHandle(hThread)");
	}

	// 子プロセスを起動したら親プロセス側では読込ハンドルを使わないので、クローズする
	if(!CloseHandle(readPipe)) {
		printError("CloseHandle(readPipe)");
	}
	readPipe = NULL;

	// パイプへの書き込み(子プロセスへの送信)
	const char message[] = "example-pipe";
	DWORD numberOfBytesWritten;
	if (!WriteFile(writePipe, message, strlen(message), &numberOfBytesWritten, NULL)) {
		printError("WriteFile");
		return -1;
	}
	if(!CloseHandle(writePipe)) {
		printError("CloseHandle(writePipe)");
		return -1;
	}
	writePipe = NULL;

	// 子プロセスの終了待ち
	DWORD r = WaitForSingleObject(childProcess, INFINITE);
	switch(r) {
	case WAIT_FAILED:
		printError("wait result=WAIT_FAILED");
		return -1;
	case WAIT_ABANDONED:
		printf("wait result=WAIT_ABANDONED\n");
		return -1;
	case WAIT_OBJECT_0: //正常終了
		printf("wait result=WAIT_OBJECT_0\n");
		break;
	case WAIT_TIMEOUT:
		printf("wait result=WAIT_TIMEOUT\n");
		return -1;
	default:
		printf("wait result=%d\n", r);
		return -1;
	}

	return 0;
}
void closeHandle(void)
{
	if (childProcess != NULL) {
		if(!CloseHandle(childProcess)) {
			printError("CloseHandle(childProcess)");
		}
	}
	if (readPipe != NULL) {
		if(!CloseHandle(readPipe)) {
			printError("CloseHandle(readPipe)");
		}
	}
	if (writePipe != NULL) {
		if(!CloseHandle(writePipe)) {
			printError("CloseHandle(writePipe)");
		}
	}
}
int main()
{
	printf("start\n");

	int r = createPipe();
	if (r == 0) {
		LPTSTR commandLine = TEXT("child.exe");
		r = execute(commandLine);
	}
	closeHandle();

	printf("end\n");
	return r;
}

createPipe()で、読込用パイプハンドルは一旦readTempに作成して、DuplicateHandle(UNIXのdup2に相当)でreadPipeに複製している。
どうしてこういう面倒な事をしているのかと言うと、親プロセスから子プロセスへのハンドルの継承権限を設定する為。

最初のCreatePipe()の第3引数にNULLを指定することにより、生成されたハンドルは子プロセスへ継承できない設定にしている。
これは、書込用パイプを子プロセスへ渡さないようにする為。
しかし同時に読込用パイプも子プロセスで使えなくなってしまう。その為、DuplicateHandle()で第6引数(bInheritHandle)にTRUEを指定することにより、子プロセスへ継承可能なハンドルとして複製している。
複製元のパイプ(readTemp)は使わないのですぐにクローズしている。

これとは逆の発想でも出来る。
パイプ作成時に継承権限有りにして、書込用パイプを継承権限無しで複製する。

int createPipe(void)
{
	HANDLE writeTemp;

	// パイプを作成(両ハンドルとも子プロセスへ継承可能)
	SECURITY_ATTRIBUTES sa = {};
	sa.nLength              = sizeof(SECURITY_ATTRIBUTES);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle       = TRUE; //継承可能にする
	if (!CreatePipe(&readPipe, &writeTemp, &sa, 0)) {
		printError("CreatePipe");
		return -1;
	}

	// 書込ハンドルを複製(子プロセスへ継承できない権限の書込ハンドルを作成)
	if (!DuplicateHandle(
		GetCurrentProcess(), writeTemp,
		GetCurrentProcess(), &writePipe,
		0, FALSE, DUPLICATE_SAME_ACCESS)
	) {
		printError("DuplicateHandle");
		if (!CloseHandle(writeTemp)) {
			printError("CloseHandle(writeTemp)");
		}
		return -1;
	}

	// 複製元のハンドルは使わないのでクローズ
	if (!CloseHandle(writeTemp)) {
		printError("CloseHandle(writeTemp)");
		return -1;
	}

	return 0;
}

どちらの方式でも出来るが、SECURITY_ATTRIBUTESの変数を用意する手間の分だけ、最初のやり方の方が楽(笑)


そもそもなぜ子プロセスへの継承にこだわるのかと言うと、継承をきちんとしないとパイプが正常に(思った通りに)動作しない為。

  継承権有りのパイプ 継承権無しのパイプ
コード
int createPipe(void)
{
	SECURITY_ATTRIBUTES sa = {};
	sa.nLength = sizeof(SECURITY_ATTRIBUTES);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle = TRUE;
	if (!CreatePipe(&readPipe, &writePipe, &sa, 0)) {
		printError("CreatePipe");
		return -1;
	}

	return 0;
}
int createPipe(void)
{




	if (!CreatePipe(&readPipe, &writePipe, NULL, 0)) {
		printError("CreatePipe");
		return -1;
	}

	return 0;
}
動作 親プロセス側の書込ハンドルをクローズしても、
子プロセス側の読み込みが終了しない。
(EOFにならない)
親プロセスからパイプに書き込んでも、
子プロセスでは何も読み込めない。
理由 書込ハンドルも子プロセスに継承されるので、
親子両方でクローズされないとEOFにならないから。
(子プロセスには読込ハンドルは(標準入力として)伝わっているが
書込ハンドルは明示的には分からないので、永遠にクローズされない)
パイプハンドルが子プロセスで使えない(継承されていない)から。

CreateProcess()の引数にも注意が必要。

子プロセスが正常に起動できたら、親プロセス側のパイプ読込ハンドルは(不要なので)クローズする。(当たり前だが、プロセス起動前にクローズしたらダメw)
(親プロセス側の読込ハンドルをクローズしても、子プロセスに継承された読込ハンドルは子プロセス側で生きている)


おまけ

子プロセス側で標準入力から読めていることを確認する為のプログラム。
何の変哲も無いw

#include <stdio.h>

int main()
{
	printf("child start\n");

	while(1) {
		int c = fgetc(stdin);
		printf("child %c/%02x\n", c, c);
		if (c < 0) break;
	}

	printf("child end\n");
	return 0;
}

標準入力(stdin)が普通のコンソールなのか親プロセスのパイプなのかは全く気にしない。
パイプの(全ての)書き込み側がクローズされると、普通にEOF扱いになる。


C言語目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま