目录
关于管道
管道是进程间用于通信的一种方式。
创建管道的进程是管道服务器。连接到管道的进程是管道客户端。一个进程将信息写入管道,然后另一个进程从管道读取信息。本概述介绍如何创建、管理和使用管道。
有两种类型的管道:匿名管道和命名管道。匿名管道比命名管道需要更少的开销,但提供的服务有限。
术语“管道”通常是指信息管道。从概念上讲,管道有两端。单向管道允许一端的进程向管道写入数据,并允许另一端的进程从管道读取数据。双向管道(或称双工管道)允许进程从管道的任意一端读取和写入。
匿名管道
匿名管道是一种未命名的单向管道,通常在父进程和子进程之间传输数据。匿名管道只能是本地的,不能用于网络通信。
匿名管道的使用
CreatePipe 函数创建一个匿名管道并返回两个句柄:管道的读取句柄和管道的写入句柄。读取句柄对管道具有只读访问权限,写入句柄对管道具有只写访问权限。若要使用管道进行通信,管道服务器必须将管道句柄传递给另一个进程。通常,这是通过继承完成的;也就是说,该进程允许子进程继承句柄。该进程还可以使用 DuplicateHandle 函数复制管道句柄,并使用某种形式的进程间通信(如 动态数据交换协议DDE 或共享内存)将其发送到不相关的进程。
管道服务器可以将读取句柄或写入句柄发送到管道客户端,具体取决于客户端是应使用匿名管道发送信息还是接收信息。若要从管道读取,请在调用 ReadFile 函数时使用管道的读取句柄。当另一个进程写入管道时,ReadFile 调用将返回。如果管道的所有写入句柄都已关闭,或者在读取操作完成之前发生错误,则 ReadFile 调用也可以返回。
若要写入管道,请在调用 WriteFile 函数时使用管道的写入句柄。WriteFile 调用在将指定的字节数写入管道或发生错误之前不会返回。如果管道缓冲区已满,并且有更多字节要写入,则在另一个进程从管道读取之前,WriteFile 不会返回,从而提供更多缓冲区空间可用。管道服务器在调用 CreatePipe 时指定管道的缓冲区大小。
匿名管道不支持异步读取和写入操作。这意味着不能将 ReadFileEx 和 WriteFileEx 函数与匿名管道一起使用。此外,当 ReadFile 和 WriteFile 的 lpOverlapped 参数与匿名管道一起使用时,将忽略这些参数。
匿名管道一直存在,直到所有管道句柄(读取和写入)都已关闭。进程可以使用 CloseHandle 函数关闭其管道句柄。当进程终止时,所有管道句柄也会关闭。
匿名管道是使用具有唯一名称的命名管道实现的。因此,通常也可以将匿名管道的句柄传递给需要命名管道句柄的函数。
匿名管道如何继承句柄
管道服务器通过以下方式控制是否可以继承其句柄:
- CreatePipe 函数的参数SECURITY_ATTRIBUTES结构体。如果管道服务器创建管道时将此结构体的 bInheritHandle 成员设置为 TRUE,则CreatePipe 创建的句柄可以被继承。
- 管道服务器可以使用 DuplicateHandle 函数通过复制句柄的方式,更改管道句柄的继承属性。管道服务器可以使用可以继承的管道句柄复制出不可继承的管道句柄副本,也可以使用不可继承的管道句柄复制出可继承的管道句柄副本。
- CreateProcess 函数使管道服务器能够指定子进程是继承其所有可继承句柄还是不继承其可继承句柄。
当子进程继承管道句柄时,系统使进程能够访问管道。但是,父进程必须将句柄值传达给子进程。父进程通常通过将标准输出句柄重定向到子进程来执行此操作,如以下步骤所示:
- 调用 GetStdHandle 函数以获取当前标准输出句柄;保存此句柄,以便在创建子进程后还原原始标准输出句柄。
- 调用 SetStdHandle 函数,将标准输出句柄设置为管道的写入句柄。现在,父进程可以创建子进程。
- 调用 CloseHandle 函数以关闭管道的写入句柄。子进程继承写入句柄后,父进程不再需要其副本。
- 调用 SetStdHandle 以还原原始标准输出句柄。
子进程使用 GetStdHandle 函数获取其标准输出句柄,该句柄现在是管道写入端的句柄。然后,子进程使用 WriteFile 函数将其输出发送到管道。当子进程使用完管道时,应通过调用 CloseHandle 或终止进程来关闭管道句柄,终止进程将自动关闭句柄。
父进程使用 ReadFile 函数从管道接收输入。数据作为字节流写入匿名管道。这意味着从管道读取的父进程无法区分单次写入操作中写入的字节数量,除非父进程和子进程之间使用协议来指示写入操作的结束位置。
当管道的所有写入句柄都关闭时,ReadFile 函数返回零。在调用 ReadFile 之前,父进程必须关闭其对管道写入端的句柄。如果不这样做,ReadFile 操作无法返回零,因为父进程具有管道写入端的打开句柄。
父子进程的读写关系不是固定的。重定向标准输入句柄的过程与重定向标准输出句柄的过程类似,不同之处在于管道的读取句柄用作子项的标准输入句柄。在这种情况下,父进程必须确保子进程不会继承管道的写入句柄。如果不这样做,则子进程执行的 ReadFile 操作无法返回零,因为子进程具有管道写入端的打开句柄。
代码示例
以下是父进程的代码:
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <strsafe.h>
#define BUFSIZE 4096
HANDLE g_hChildStd_IN_Rd = NULL;
HANDLE g_hChildStd_IN_Wr = NULL;
HANDLE g_hChildStd_OUT_Rd = NULL;
HANDLE g_hChildStd_OUT_Wr = NULL;
HANDLE g_hInputFile = NULL;
void CreateChildProcess(void);
void WriteToPipe(void);
void ReadFromPipe(void);
void ErrorExit(PTSTR);
int _tmain(int argc, TCHAR *argv[])
{
SECURITY_ATTRIBUTES saAttr;
printf("\n->Start of parent execution.\n");
// Set the bInheritHandle flag so pipe handles are inherited.
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
// Create a pipe for the child process's STDOUT.
if ( ! CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0) )
ErrorExit(TEXT("StdoutRd CreatePipe"));
// Ensure the read handle to the pipe for STDOUT is not inherited.
if ( ! SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0) )
ErrorExit(TEXT("Stdout SetHandleInformation"));
// Create a pipe for the child process's STDIN.
if (! CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
ErrorExit(TEXT("Stdin CreatePipe"));
// Ensure the write handle to the pipe for STDIN is not inherited.
if ( ! SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0) )
ErrorExit(TEXT("Stdin SetHandleInformation"));
// Create the child process.
CreateChildProcess();
// Get a handle to an input file for the parent.
// This example assumes a plain text file and uses string output to verify data flow.
if (argc == 1)
ErrorExit(TEXT("Please specify an input file.\n"));
g_hInputFile = CreateFile(
argv[1],
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_READONLY,
NULL);
if ( g_hInputFile == INVALID_HANDLE_VALUE )
ErrorExit(TEXT("CreateFile"));
// Write to the pipe that is the standard input for a child process.
// Data is written to the pipe's buffers, so it is not necessary to wait
// until the child process is running before writing data.
WriteToPipe();
printf( "\n->Contents of %S written to child STDIN pipe.\n", argv[1]);
// Read from pipe that is the standard output for child process.
printf( "\n->Contents of child process STDOUT:\n\n");
ReadFromPipe();
printf("\n->End of parent execution.\n");
// The remaining open handles are cleaned up when this process terminates.
// To avoid resource leaks in a larger application, close handles explicitly.
return 0;
}
void CreateChildProcess()
// Create a child process that uses the previously created pipes for STDIN and STDOUT.
{
TCHAR szCmdline[]=TEXT("child");
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bSuccess = FALSE;
// Set up members of the PROCESS_INFORMATION structure.
ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
// Set up members of the STARTUPINFO structure.
// This structure specifies the STDIN and STDOUT handles for redirection.
ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.hStdError = g_hChildStd_OUT_Wr;
siStartInfo.hStdOutput = g_hChildStd_OUT_Wr;
siStartInfo.hStdInput = g_hChildStd_IN_Rd;
siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
// Create the child process.
bSuccess = CreateProcess(NULL,
szCmdline, // command line
NULL, // process security attributes
NULL, // primary thread security attributes
TRUE, // handles are inherited
0, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
// If an error occurs, exit the application.
if ( ! bSuccess )
ErrorExit(TEXT("CreateProcess"));
else
{
// Close handles to the child process and its primary thread.
// Some applications might keep these handles to monitor the status
// of the child process, for example.
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
// Close handles to the stdin and stdout pipes no longer needed by the child process.
// If they are not explicitly closed, there is no way to recognize that the child process has ended.
CloseHandle(g_hChildStd_OUT_Wr);
CloseHandle(g_hChildStd_IN_Rd);
}
}
void WriteToPipe(void)
// Read from a file and write its contents to the pipe for the child's STDIN.
// Stop when there is no more data.
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];
BOOL bSuccess = FALSE;
for (;;)
{
bSuccess = ReadFile(g_hInputFile, chBuf, BUFSIZE, &dwRead, NULL);
if ( ! bSuccess || dwRead == 0 ) break;
bSuccess = WriteFile(g_hChildStd_IN_Wr, chBuf, dwRead, &dwWritten, NULL);
if ( ! bSuccess ) break;
}
// Close the pipe handle so the child process stops reading.
if ( ! CloseHandle(g_hChildStd_IN_Wr) )
ErrorExit(TEXT("StdInWr CloseHandle"));
}
void ReadFromPipe(void)
// Read output from the child process's pipe for STDOUT
// and write to the parent process's pipe for STDOUT.
// Stop when there is no more data.
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];
BOOL bSuccess = FALSE;
HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
for (;;)
{
bSuccess = ReadFile( g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
if( ! bSuccess || dwRead == 0 ) break;
bSuccess = WriteFile(hParentStdOut, chBuf,
dwRead, &dwWritten, NULL);
if (! bSuccess ) break;
}
}
void ErrorExit(PTSTR lpszFunction)
// Format a readable error message, display a message box,
// and exit from the application.
{
LPVOID lpMsgBuf;
LPVOID lpDisplayBuf;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
(lstrlen((LPCTSTR)lpMsgBuf)+lstrlen((LPCTSTR)lpszFunction)+40)*sizeof(TCHAR));
StringCchPrintf((LPTSTR)lpDisplayBuf,
LocalSize(lpDisplayBuf) / sizeof(TCHAR),
TEXT("%s failed with error %d: %s"),
lpszFunction, dw, lpMsgBuf);
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
LocalFree(lpMsgBuf);
LocalFree(lpDisplayBuf);
ExitProcess(1);
}
以下是子进程的代码。它使用 STDIN 和 STDOUT 的继承句柄来访问父级创建的管道。父进程从其输入文件中读取并将信息写入管道。子级使用 STDIN 通过管道接收文本信息,并使用 STDOUT 写入管道。父级从管道的读取端读取,并将信息显示到其 STDOUT。
#include <windows.h>
#include <stdio.h>
#define BUFSIZE 4096
int main(void)
{
CHAR chBuf[BUFSIZE];
DWORD dwRead, dwWritten;
HANDLE hStdin, hStdout;
BOOL bSuccess;
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
hStdin = GetStdHandle(STD_INPUT_HANDLE);
if (
(hStdout == INVALID_HANDLE_VALUE) ||
(hStdin == INVALID_HANDLE_VALUE)
)
ExitProcess(1);
// Send something to this process's stdout using printf.
printf("\n ** This is a message from the child process. ** \n");
// This simple algorithm uses the existence of the pipes to control execution.
// It relies on the pipe buffers to ensure that no data is lost.
// Larger applications would use more advanced process control.
for (;;)
{
// Read from standard input and stop on error or no data.
bSuccess = ReadFile(hStdin, chBuf, BUFSIZE, &dwRead, NULL);
if (! bSuccess || dwRead == 0)
break;
// Write to standard output and stop on error.
bSuccess = WriteFile(hStdout, chBuf, dwRead, &dwWritten, NULL);
if (! bSuccess)
break;
}
return 0;
}
匿名管道的安全性和访问权限
可以在调用 CreatePipe 函数时为管道指定安全描述符。安全描述符控制对管道读取端和写入端的访问。如果指定 NULL,管道将获取默认安全描述符。管道的默认安全描述符中的 ACL 来自创建者的主令牌或模拟令牌。
若要检索管道的安全描述符,请调用 GetSecurityInfo 函数。若要更改管道的安全描述符,请调用 SetSecurityInfo 函数。
CreatePipe 函数向匿名管道返回两个句柄:具有 GENERIC_READ 和 SYNC 访问权限的读取句柄;以及具有 GENERIC_WRITE 和同步访问权限的写入句柄。GENERIC_READ和GENERIC_WRITE的访问权限和命名管道相同。
匿名管道的GENERIC_READ访问权限结合了从管道读取数据、读取管道属性、读取扩展属性和读取管道的 DACL 的权限。
匿名管道的GENERIC_WRITE访问权限结合了将数据写入管道、将数据追加到管道、写入管道属性、写入扩展属性和读取管道的 DACL 的权限。
命名管道
命名管道是用于管道服务器与一个或多个管道客户端之间进行通信的有名字的、单向或双向的管道。命名管道的所有实例共享相同的管道名称,但每个实例都有自己的缓冲区和句柄,并为客户端/服务器通信提供单独的管道。使用实例使多个管道客户端能够同时使用相同的命名管道。
任何进程都可以访问命名管道,但要接受安全检查。
任何进程都可以充当服务器和客户端,使点对点通信成为可能。术语管道服务器是指创建命名管道的进程,术语管道客户端是指连接到命名管道实例的进程。用于实例化命名管道的服务器端函数是 CreateNamedPipe。用于接受连接的服务器端函数是 ConnectNamedPipe。客户端进程通过使用CreateFile或调用命名管道函数CallNamedPipe连接到命名管道。
命名管道可用于在同一台计算机上的进程之间或网络上不同计算机上的进程之间提供通信。如果服务器服务正在运行,则可以远程访问所有命名管道。如果打算仅在本地使用命名管道,请拒绝对 NT AUTHORITY\NETWORK 的访问或切换到本地 RPC。
命名管道的命名
每个命名管道都有一个唯一的名称,用于将其与系统命名对象列表中的其他命名管道区分开来。管道服务器在调用 CreateNamedPipe 函数以创建命名管道的一个或多个实例时指定管道的名称。
在 CreateFile、WaitNamedPipe 或 CallNamedPipe 函数中使用管道的名称时,请使用以下形式:
\\ServerName\pipe\PipeName
其中 ServerName 是远程计算机的名称或‘.’,当使用英文句号时表示是本地计算机。PipeName 指定的管道名称字符串可以包含反斜杠以外的任何字符,包括数字和特殊字符。整个管道名称字符串最多可以包含 256 个字符。管道名称不区分大小写。
管道服务器无法在另一台计算机上创建管道,因此 CreateNamedPipe 必须使用句点作为服务器名称,如以下示例所示:
\\.\pipe\PipeName
管道服务器可以向其管道客户端提供管道名称,以便它们可以连接到管道。管道客户端从某些持久性源(如注册表项、文件或其他应用程序)发现管道名称。否则,客户端必须在编译时知道管道名称。
命名管道的模式
管道服务器在 CreateNamedPipe 函数的 dwOpenMode 参数中指定管道的访问(Access)、异步(Overlapped)和直写(Write-Through)模式。管道客户端可以使用 CreateFile 函数为其管道句柄指定这些打开模式。
访问模式
设置管道访问模式等效于指定与管道服务器的句柄关联的读取或写入访问权限。下表显示了可以使用 CreateNamedPipe 指定的每种访问模式的等效通用访问权限。
访问模式 | 等效的通用访问权限 |
---|---|
PIPE_ACCESS_INBOUND | GENERIC_READ |
PIPE_ACCESS_OUTBOUND | GENERIC_WRITE |
PIPE_ACCESS_DUPLEX | GENERIC_READ |GENERIC_WRITE |
如果管道服务器创建具有PIPE_ACCESS_INBOUND的管道,则该管道对于管道服务器是只读的,对于管道客户端是只写的。如果管道服务器创建具有PIPE_ACCESS_OUTBOUND的管道,则该管道对于管道服务器是只写的,对于管道客户端是只读的。使用 PIPE_ACCESS_DUPLEX 创建的管道对于管道服务器和管道客户端都是可读/写的。
使用 CreateFile 连接到命名管道的管道客户端必须在 dwDesiredAccess 参数中指定与管道服务器指定的访问模式兼容的访问权限。例如,客户端必须指定GENERIC_READ访问权限才能打开管道服务器使用 PIPE_ACCESS_OUTBOUND 创建的管道的句柄。管道的所有实例的访问模式必须相同。
要读取管道属性(如读取模式或阻塞模式),管道句柄必须具有FILE_READ_ATTRIBUTES访问权限;要写入管道属性,管道句柄必须具有FILE_WRITE_ATTRIBUTES访问权限。这些访问权限可以与适用于管道的通用访问权限结合使用:
- 对于只读管道,GENERIC_READ具有FILE_WRITE_ATTRIBUTES
- 对于只写管道,GENERIC_WRITE具有FILE_READ_ATTRIBUTES
以这种方式限制访问权限可为管道提供更好的安全性。
异步模式
在异步模式下,执行冗长读取、写入和连接操作的函数可以立即返回。这使线程能够在后台执行耗时的操作时执行其他操作。若要指定异步模式,请使用 FILE_FLAG_OVERLAPPED 标志。
CreateFile 函数允许管道客户端使用 dwFlagsAndAttributes 参数为其管道句柄设置异步模式 (FILE_FLAG_OVERLAPPED)。
直写模式
使用 FILE_FLAG_WRITE_THROUGH 指定直写模式。此模式仅影响对不同计算机上的管道客户端和管道服务器之间的字节类型管道的写入操作。在直写模式下,写入命名管道的函数不会返回,直到数据通过网络传输到远程计算机上的管道缓冲区中。直写模式对于需要同步每个写入操作的应用程序非常有用。
如果未启用直写模式,系统将通过缓冲数据的方法来提高网络操作的效率,直到累积到发送数据所需的最小字节数或超时。缓冲使系统能够将多个写入操作组合到单次网络传输中。这意味着在系统将数据放入出站缓冲区之后,但是还没通过网络传输数据之前,写入操作就会返回成功。
CreateFile 函数允许管道客户端使用 dwFlagsAndAttributes 参数为其管道句柄设置直写模式 (FILE_FLAG_WRITE_THROUGH)。
管道客户端可以使用 SetNamedPipeHandleState 函数来控制禁用直写模式的管道传输之前的字节数和超时期限。对于只读管道,必须以GENERIC_READ和FILE_WRITE_ATTRIBUTES访问权限打开管道句柄。