文章目录
Windows 管道的原理剖析
今天有朋友问我,管道是否可以支持多个连接?其实是可以的,但是和socket使用起来有点不同,下面是msdn中管道服务端的例子:
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#define BUFSIZE 512
DWORD WINAPI InstanceThread(LPVOID);
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD);
int _tmain(VOID)
{
BOOL fConnected = FALSE;
DWORD dwThreadId = 0;
HANDLE hPipe = INVALID_HANDLE_VALUE, hThread = NULL;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
for (;;)
{
_tprintf( TEXT("\nPipe Server: Main thread awaiting client connection on %s\n"), lpszPipename);
hPipe = CreateNamedPipe(
lpszPipename, // pipe name
PIPE_ACCESS_DUPLEX, // read/write access
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
PIPE_UNLIMITED_INSTANCES, // max. instances
BUFSIZE, // output buffer size
BUFSIZE, // input buffer size
0, // client time-out
NULL); // default security attribute
if (hPipe == INVALID_HANDLE_VALUE)
{
_tprintf(TEXT("CreateNamedPipe failed, GLE=%d.\n"), GetLastError());
return -1;
}
fConnected = ConnectNamedPipe(hPipe, NULL) ?
TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (fConnected)
{
printf("Client connected, creating a processing thread.\n");
// Create a thread for this client.
hThread = CreateThread(
NULL, // no security attribute
0, // default stack size
InstanceThread, // thread proc
(LPVOID) hPipe, // thread parameter
0, // not suspended
&dwThreadId); // returns thread ID
if (hThread == NULL)
{
_tprintf(TEXT("CreateThread failed, GLE=%d.\n"), GetLastError());
return -1;
}
else CloseHandle(hThread);
}
else
// The client could not connect, so close the pipe.
CloseHandle(hPipe);
}
return 0;
}
DWORD WINAPI InstanceThread(LPVOID lpvParam)
{
HANDLE hHeap = GetProcessHeap();
TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));
TCHAR* pchReply = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));
DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0;
BOOL fSuccess = FALSE;
HANDLE hPipe = NULL;
if (lpvParam == NULL)
{
printf( "\nERROR - Pipe Server Failure:\n");
printf( " InstanceThread got an unexpected NULL value in lpvParam.\n");
printf( " InstanceThread exitting.\n");
if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
return (DWORD)-1;
}
if (pchRequest == NULL)
{
printf( "\nERROR - Pipe Server Failure:\n");
printf( " InstanceThread got an unexpected NULL heap allocation.\n");
printf( " InstanceThread exitting.\n");
if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
return (DWORD)-1;
}
if (pchReply == NULL)
{
printf( "\nERROR - Pipe Server Failure:\n");
printf( " InstanceThread got an unexpected NULL heap allocation.\n");
printf( " InstanceThread exitting.\n");
if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
return (DWORD)-1;
}
// Print verbose messages. In production code, this should be for debugging only.
printf("InstanceThread created, receiving and processing messages.\n");
// The thread's parameter is a handle to a pipe object instance.
hPipe = (HANDLE) lpvParam;
// Loop until done reading
while (1)
{
fSuccess = ReadFile(
hPipe, // handle to pipe
pchRequest, // buffer to receive data
BUFSIZE*sizeof(TCHAR), // size of buffer
&cbBytesRead, // number of bytes read
NULL); // not overlapped I/O
if (!fSuccess || cbBytesRead == 0)
{
if (GetLastError() == ERROR_BROKEN_PIPE)
{
_tprintf(TEXT("InstanceThread: client disconnected.\n"), GetLastError());
}
else
{
_tprintf(TEXT("InstanceThread ReadFile failed, GLE=%d.\n"), GetLastError());
}
break;
}
// Process the incoming message.
GetAnswerToRequest(pchRequest, pchReply, &cbReplyBytes);
// Write the reply to the pipe.
fSuccess = WriteFile(
hPipe, // handle to pipe
pchReply, // buffer to write from
cbReplyBytes, // number of bytes to write
&cbWritten, // number of bytes written
NULL); // not overlapped I/O
if (!fSuccess || cbReplyBytes != cbWritten)
{
_tprintf(TEXT("InstanceThread WriteFile failed, GLE=%d.\n"), GetLastError());
break;
}
}
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
HeapFree(hHeap, 0, pchRequest);
HeapFree(hHeap, 0, pchReply);
printf("InstanceThread exitting.\n");
return 1;
}
VOID GetAnswerToRequest( LPTSTR pchRequest,
LPTSTR pchReply,
LPDWORD pchBytes )
{
_tprintf( TEXT("Client Request String:\"%s\"\n"), pchRequest );
// Check the outgoing message to make sure it's not too long for the buffer.
if (FAILED(StringCchCopy( pchReply, BUFSIZE, TEXT("default answer from server") )))
{
*pchBytes = 0;
pchReply[0] = 0;
printf("StringCchCopy failed, no outgoing message.\n");
return;
}
*pchBytes = (lstrlen(pchReply)+1)*sizeof(TCHAR);
}
在这个例子中:
- 循环使用
CreateNamedPipe
和ConnectNamedPipe
来建立一个实例连接。 - 使用线程针对这个实例进行读写控制。
本文我们就来探讨一下命名管道多个实例的原理。
1. 管道的使用
1.1 服务端
- 创建管道
HANDLE CreateNamedPipeA(
LPCSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
- 等待客户端连接
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped
);
- 读取管道
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
- 写入管道
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
其中,可以通过CreateNamedPipeA
创建一个管道的多个实例。
1.2 客户端
- 等待服务端管道可以连接
BOOL WaitNamedPipeA(
LPCSTR lpNamedPipeName,
DWORD nTimeOut
);
- 连接服务端
HANDLE CreateFileW(
LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
//或者
BOOL WINAPI CallNamedPipe(
_In_ LPCTSTR lpNamedPipeName,
_In_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_ LPDWORD lpBytesRead,
_In_ DWORD nTimeOut
);
- 读取管道
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
- 写入管道
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
2. 实现
管道也是采用设备驱动来实现的,原理如下分析。
2.1 调用堆栈
kd> kb 10
# ChildEBP RetAddr Args to Child
00 9397b970 83e48593 86310c10 8756ab60 8757d8ec Npfs!NpFsdCreate
01 9397b988 840582a9 87cf852f 9397bb30 00000000 nt!IofCallDriver+0x63
02 9397ba60 84037ac5 86310c10 857ea488 857ea770 nt!IopParseDevice+0xed7
03 9397badc 84047ed6 00000000 9397bb30 00000040 nt!ObpLookupObjectName+0x4fa
04 9397bb38 8403e9b4 0204ebac 857ea488 00008401 nt!ObOpenObjectByName+0x165
05 9397bbb4 84062218 0204ec08 c0100080 0204ebac nt!IopCreateFile+0x673
06 9397bc00 83e4f1ea 0204ec08 c0100080 0204ebac nt!NtCreateFile+0x34
07 9397bc00 771770b4 0204ec08 c0100080 0204ebac nt!KiFastCallEntry+0x12a
08 0204eb68 771755d4 753faa21 0204ec08 c0100080 ntdll!KiFastSystemCallRet
09 0204eb6c 753faa21 0204ec08 c0100080 0204ebac ntdll!NtCreateFile+0xc
采用标准的设备通信来实现,打开的路径如下:
kd> dS 008add48
0039c618 "\??\PIPE\browser"
其中\??\PIPE
是指向\device\NamedPipe
的符号链接。
2.2 驱动实现
管道是通过npfs.sys来实现的,其中回调函数信息如下:
NTSTATUS
NTAPI
DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
DriverObject->MajorFunction[IRP_MJ_CREATE] = NpFsdCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = NpFsdCreateNamedPipe;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = NpFsdClose;
DriverObject->MajorFunction[IRP_MJ_READ] = NpFsdRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = NpFsdWrite;
DriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = NpFsdQueryInformation;
DriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = NpFsdSetInformation;
DriverObject->MajorFunction[IRP_MJ_QUERY_VOLUME_INFORMATION] = NpFsdQueryVolumeInformation;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = NpFsdCleanup;
DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = NpFsdFlushBuffers;
DriverObject->MajorFunction[IRP_MJ_DIRECTORY_CONTROL] = NpFsdDirectoryControl;
DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = NpFsdFileSystemControl;
DriverObject->MajorFunction[IRP_MJ_QUERY_SECURITY] = NpFsdQuerySecurityInfo;
DriverObject->MajorFunction[IRP_MJ_SET_SECURITY] = NpFsdSetSecurityInfo;
DriverObject->DriverUnload = NULL;
DriverObject->FastIoDispatch = &NpFastIoDispatch;
RtlInitUnicodeString(&DeviceName, L"\\Device\\NamedPipe");
Status = IoCreateDevice(DriverObject,
sizeof(NP_VCB),
&DeviceName,
FILE_DEVICE_NAMED_PIPE,
0,
FALSE,
&DeviceObject);
}
2.3 CreateNamedPipeA原理
- 首先在文件对象的上下文中会存在如下结构体,这个结构体中
CcbList
代表创建的实例的个数;并且有一个CurrentInstances
表示当前创建的实例的个数。
typedef struct _NP_CB_HEADER
{
NODE_TYPE_CODE NodeType;
LIST_ENTRY DcbEntry;
PVOID ParentDcb;
ULONG CurrentInstances;
ULONG ServerOpenCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
} NP_CB_HEADER, *PNP_CB_HEADER;
typedef struct _NP_FCB
{
/* Common Header */
NP_CB_HEADER;
/* FCB-specific fields */
ULONG MaximumInstances;
USHORT NamedPipeConfiguration;
USHORT NamedPipeType;
LARGE_INTEGER Timeout;
LIST_ENTRY CcbList;
#ifdef _WIN64
PVOID Pad[2];
#endif
/* Common Footer */
NP_CB_FOOTER;
} NP_FCB, *PNP_FCB;
管理代码如下,我们知道,对于每个文件,文件对象的上下文FsContext
是一样的,所以管道的管理信息都是放在文件对象上下文中的,管理信息如下:
Fcb = (PNP_FCB)((ULONG_PTR)RelatedFileObject->FsContext & ~1);
- 实例结构如下
typedef struct _NP_CCB
{
NODE_TYPE_CODE NodeType;
UCHAR NamedPipeState;
UCHAR ReadMode[2];
UCHAR CompletionMode[2];
SECURITY_QUALITY_OF_SERVICE ClientQos;
LIST_ENTRY CcbEntry;
PNP_FCB Fcb;
PFILE_OBJECT FileObject[2];
PEPROCESS Process;
PVOID ClientSession;
PNP_NONPAGED_CCB NonPagedCcb;
NP_DATA_QUEUE DataQueue[2];
PSECURITY_CLIENT_CONTEXT ClientContext;
LIST_ENTRY IrpList;
} NP_CCB, *PNP_CCB;
- 当每次创建一个管道的时候(已经存在的管道),那么就会比较当前存在的实例数目是否大于最大的实例数目,如果大于,直接不创建。
if (Fcb->CurrentInstances >= Fcb->MaximumInstances)
{
IoStatus.Status = STATUS_INSTANCE_NOT_AVAILABLE;
TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
return IoStatus;
}
- 否则创建一个实例,将其插入到队列中
InsertTailList(&Fcb->CcbList, &Ccb->CcbEntry);
2.4 客户端CreateFile的原理
服务端创建好了管道之后,客户端就会连接上来了,连接的主要代码是:
IO_STATUS_BLOCK
NTAPI
NpCreateClientEnd(IN PNP_FCB Fcb,
IN PFILE_OBJECT FileObject,
IN ACCESS_MASK DesiredAccess,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos,
IN PACCESS_STATE AccessState,
IN KPROCESSOR_MODE PreviousMode,
IN PETHREAD Thread,
IN PLIST_ENTRY List)
{
ListHead = &Fcb->CcbList;
NextEntry = ListHead->Flink;
while (NextEntry != ListHead)
{
Ccb = CONTAINING_RECORD(NextEntry, NP_CCB, CcbEntry);
if (Ccb->NamedPipeState == FILE_PIPE_LISTENING_STATE) break;
NextEntry = NextEntry->Flink;
}
if (NextEntry == ListHead)
{
IoStatus.Status = STATUS_PIPE_NOT_AVAILABLE;
TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
return IoStatus;
}
IoStatus.Status = NpInitializeSecurity(Ccb, SecurityQos, Thread);
if (!NT_SUCCESS(IoStatus.Status)) return IoStatus;
IoStatus.Status = NpSetConnectedPipeState(Ccb, FileObject, List);
if (!NT_SUCCESS(IoStatus.Status))
{
NpUninitializeSecurity(Ccb);
TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
return IoStatus;
}
Ccb->ClientSession = NULL;
Ccb->Process = IoThreadToProcess(Thread);
}
这里主要是从服务端的实例中,取出一个可用的实例,标记为被连接的状态。
以后就可以根据这个文件对象来同行了。
3. 总结
3.1 多实例
从上面分析可以发现,一个命名管道可以有多个实例,因为在驱动中,第一次创建和后面创建实例走的流程不同,并且创建的实例都通过链接连接起来。每次调用CreateNamedPipe
都会创建一个管道的实例。
3.2 客户端和服务端
从上面我们也可以发现,客户端和服务端使用的API打开或者创建管道的方式也不一样,因为对于管道响应的函数也不同:
DriverObject->MajorFunction[IRP_MJ_CREATE] = NpFsdCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = NpFsdCreateNamedPipe;
其中CreateNamedPipe
使用如下方式创建:
return IoCreateFile(FileHandle,
DesiredAccess,
ObjectAttributes,
IoStatusBlock,
NULL,
0,
ShareAccess,
CreateDisposition,
CreateOptions,
NULL,
0,
CreateFileTypeNamedPipe,
(PVOID)&Buffer,
0);
针对CreateFileTypeNamedPipe
参数的IoCreateFile
调用会发送IRP_MJ_CREATE_NAMED_PIPE
的IRP请求,从而进入驱动的管道创建流程。
来源:CSDN
作者:xiangbaohui
链接:https://blog.csdn.net/xiangbaohui/article/details/103713403