管道通信:匿名管道

2021/5/9 CC++通信Windows

# 1 前言

上篇笔记([管道通信:输入输出重定向](./pipeline_io_redirection.md)记录了输入输出重定向的实现方法,本文总结一下匿名管道的内容,希望对自己与各位有所帮助。

# 2 管道的概念

简单理解,计算机中的管道(pipe)类似现实世界中的水管,从一端放入水流,另一端就会流出来。回到计算机中,这个水流就是数据。

管道(pipe)又分为 匿名管道命名管道。本文主要介绍如何使用匿名管道来实现输入输出重定向功能。

# 3 匿名管道的创建和使用

# 3.1 函数原型

# 3.1.1 CreatePipe

/* 创建管道 */
BOOL WINAPI CreatePipe(
  PHANDLE hReadPipe,                      // 管道输出(读取)端句柄
  PHANDLE hWritePipe,                     // 管道输入(写入)端句柄
  LPSECURITY_ATTRIBUTES lpPipeAttributes, // 管道的安全属性
  DWORDnSize                              // 管道缓冲区容量,设置0时使用默认大小
);

管道的安全属性通常设置如下:

SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;

注意:不能在管道的输出端写入数据,也不能在管道的输入端读取数据。

# 3.1.2 ReadFile

/* 从管道中读取数据 */
BOOL ReadFile(
  HANDLE hFile,                // 管道输出端(读取)句柄
  LPVOID lpBuffer,             // 数据缓冲区指针,读取的数据将写在该缓冲区中
  DWORD nNumberOfBytesToRead,  // 指定读取的字节数
  LPDWORD lpNumberOfBytesRead, // 返回实际读取到的字节数
  LPOVERLAPPED lpOverlapped    // 用于异步操作,一般传入NULL即可
);

# 3.1.3 WriteFile

/* 向管道写入数据 */
BOOL WriteFile(
  HANDLE hFile,                   // 管道输入端(写入)句柄,也可以是CreateFile()接口创建的文件句柄
  LPVOID lpBuffer,                // 待写入管道的数据缓冲区指针
  DWORD nNumberOfBytesToWrite,    // 指定写入的字节数
  LPDWORD lpNumberOfBytesWritten, // 返回实际写入管道的字节数
  LPOVERLAPPED lpOverlapped       // 用于异步操作,一般传入NULL即可
);

# 3.1.4 CloseHandle

/* 关闭管道端口句柄 */
BOOL CloseHandle(
  HANDLE hObject  // 想要关闭的句柄
);

注意:当管道输入输出端都被关闭后,系统会自动关闭该管道,并回收相关资源。

# 3.2 管道使用技巧

从以上管道相关的函数原型来看,管道的读写其实和文件的读写非常类似,实际上管道也是一种特殊的文件——内存映射文件。

使用管道时需要注意:读取和写入数据时,一定要注意顺序,如果管道中没有数据,调用ReadFile()会造成阻塞,直到有其它线程将数据写入管道。同样,当有线程正在管道中读取数据时,其它试图将数据写入管道的的线程也会被阻塞,更多内容请查阅MSDN。

# 3.3 示例代码

此处以管道通信:输入输出重定向中的示例程序作为子进程,在父进程中创建两个管道(输入/输出 管道),分别用于存放输入输出数据,将这两个管道重定向到输入输出文件(stdin.txt 和 stdout.txt)。

此时,在父进程中启动子进程,将子进程的标准输入输出改成从上述创建的 输入/输出 管道句柄,即实现了子进程的输入输出重定向,示例代码如下:

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

int main()
{
	char sz[3][50] = { "示例程序.exe", "infile.txt", "outfile.txt" };
	HANDLE hPipeInputRead = NULL;
	HANDLE hPipeInputWrite = NULL;
	HANDLE hPipeOutputRead = NULL;
	HANDLE hPipeOutputWrite = NULL;

	SECURITY_ATTRIBUTES sa;
	sa.nLength = sizeof(sa);
	sa.bInheritHandle = TRUE;
	sa.lpSecurityDescriptor = NULL;

	CreatePipe(&hPipeInputRead, &hPipeInputWrite, &sa, 0);   // 创建输入管道
	CreatePipe(&hPipeOutputRead, &hPipeOutputWrite, &sa, 0); // 创建输出管道

	HANDLE hInputFile = CreateFile(L"stdin.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
	HANDLE hOutputFile = CreateFile(L"stdout.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

	CHAR Buffer[1024 * 4] = { 0 };
	DWORD dwNumberOfBytesRead = 0;
	DWORD dwNumberOfBytesWritten = 0;
	BOOL  bRet =  FALSE;

	while (TRUE) { // 从文件stdin.txt中读取数据,写到输入管道中
		bRet = ReadFile(hInputFile, Buffer, sizeof(Buffer), &dwNumberOfBytesRead, NULL);
		if (!bRet || dwNumberOfBytesRead == 0)
			break;
		bRet = WriteFile(hPipeInputWrite, Buffer, dwNumberOfBytesRead, &dwNumberOfBytesWritten, NULL);
		if (!bRet)
			break;
	}
	// 关闭输入管道的写句柄
	CloseHandle(hInputFile);
	CloseHandle(hPipeInputWrite);

	// 启动demo.exe作为子进程
	STARTUPINFO si;
	si.cb = sizeof(si);
	GetStartupInfo(&si);
	si.hStdInput = hPipeInputRead;    // 输入由标准输入 -> 从管道中读取
	si.hStdOutput = hPipeOutputWrite; // 输出由标准输出 -> 输出到管道
	si.wShowWindow = SW_HIDE;
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
	PROCESS_INFORMATION pi;

	CreateProcess(L"demo.exe", NULL, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
	WaitForSingleObject(pi.hProcess, INFINITE);

	// 关闭进程相关句柄
	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
	CloseHandle(hPipeInputRead);
	CloseHandle(hPipeOutputWrite);

	while (TRUE) { // 从输出管道中读取数据,写到文件stdout.txt中
		bRet = ReadFile(hPipeOutputRead, Buffer, sizeof(Buffer), &dwNumberOfBytesRead, NULL);
		if (!bRet || dwNumberOfBytesRead == 0)
			break;
		bRet = WriteFile(hOutputFile, Buffer, dwNumberOfBytesRead, &dwNumberOfBytesWritten, NULL);
		if (!bRet)
			break;
	}

	//关闭输出管道的读句柄
	CloseHandle(hOutputFile);
	CloseHandle(hPipeOutputRead);

	return 0;
}

# 4 总结

从上述示例来看,匿名管道可以在不修改程序源码的情况下实现输入输出重定向,也就是说只要有exe文件,即可实现输入输出重定向,并且上述示例中,父进程也可以使用 输入/输出 管道写入或读取数据,即实现了父子进程间的通信。