📄
字号:
漫谈兼容内核之十八:Windows的LPC机制
毛德操
LPC是“本地过程调用(Local Procedure Call)”的缩写。所谓“本地过程调用”是与“远程过程调用”即RPC相对而言的。其实RPC是广义的,RPC可以发生在不同的主机之间,也可以发生在同一台主机上,发生在同一台主机上就是LPC。所以在Unix语境下就没有LPC这一说,即使发生在同一台主机上也称为RPC。在历史上,RPC是“开放软件基金会(OSF)”设计和提出的一种用以实现“Unix分布计算环境(Unix DCE)”的标准。实际上,微软的DCOM技术,就是建立在RPC基础上的。Win2000的RPC可以采用TCP/IP、SPX、NetBIOS、命名管道、以及“本地”作为底层的通信手段,这“本地”就是LPC。另一方面,Windows是一个带有许多微内核系统特征的操作系统(尽管它的内核不是微内核),系统中有不少“系统级”的服务进程,例如大家已经熟知的csrss、管理用户登录的“本地安全认证服务”进程LSASS等等,用户进程以及微软提供的系统工具软件经常需要调用由这些服务进程提供的服务,这里LPC就起着重要的作用。
LPC的基础是一种称为“端口(Port)”的进程间通信机制,类似于本地的(Unix域的)Socket。这种Port机制提供了面向报文传递(message passing)的进程间通信,而LPC则是建立在这个基础上的高层机制,目的是提供跨进程的过程调用。注意这里所谓“跨进程的过程调用”不同于以前所说的“跨进程操作”。前者是双方有约定、遵循一定规程的、有控制的服务提供,被调用者在向外提供一些什么服务、即提供哪些函数调用方面是自主的,而后者则可以是在不知不觉之间的被利用、被操纵。前者是良性的,而后者可以是恶性的。
“Microsoft Windows Internals”书中说LPC是“用于快速报文传递的进程间通信机制”。其实这是误导的,应该说Port才是这样的进程间通信机制,而LPC是建立在这上面的应用。然而这种说法已经被广泛接受和采纳,都把LPC和Port混淆起来了,所以本文也只好跟着说“Windows的LPC机制”,而实际上要说的则主要是Windows的Port机制。在下面的叙述中,凡说到LPC的地方往往实际上是在说Port,读者要注意区分。
端口是一种面向连接的通信机制,通信的双方需要先建立起“连接”。这种连接一般建立在用户进程之间。在建立了连接的双方之间有几种交换报文的方法:
? l 不带数据的纯报文。
? l 不大于256字节的短报文。
? l 如果是大于256字节的长报文,就要在双方之间建立两个共享内存区(Section)。双方通过共享内存区交换数据,但通过报文进行协调和同步。
大块数据之所以要通过共享内存区交换,一来是因为这样就为用于Port机制的缓冲区设置了一个上限,便于内存管理。而更重要的是提高了效率,因为否则便要在发送端把大块数据搬入内核空间,又在接收端把大块数据搬到用户空间。
Windows内核为基于端口的进程间通信机制提供了不少系统调用,包括(但并不限于):
[code]? l NtCreatePort()
? l NtCreateWaitablePort()
? l NtListenPort()
? l NtConnectPort()
? l NtAcceptConnectPort()
? l NtCompleteConnectPort()
? l NtRequestPort()
? l NtRequestWaiReplyPort()
? l NtReplyPort()
? l NtReplyWaitReceivePort()
? l NtReplyWaitReceivePortEx()。同上,但是带有超时控制
? l NtReadRequestData()
? l NtWriteRequestData()
? l NtQueryInformationPort()[/code]
这么多的系统调用(由此也可见LPC在Windows操作系统中的份量),当然不可能在这里一一加以介绍。本文只是从中拣几个关键而典型的作些介绍。另一方面,由于Port与Socket的相似性,对于兼容内核的开发而言应该比较容易把它嫁接到Socket机制上去。
值得一提的是,Port在Win32 API界面上是不可见的(实际上甚至LPC也不是直接可见的),而Windows的系统调用界面又不公开。这说明Port只是供微软“内部使用”的。读者后面就会看到,Port是一种既包括进程间的数据传输,又包括进程间的同步、数据量又可大可小的综合性的进程间通信机制。这样,由微软自己开发的软件、特别是一些系统工具,当然可以使用这些系统调用、也即利用Port这种功能比较强的进程间通信机制,而第三方开发的软件可就用不上了。
端口分“连接端口(connection port)”和“通信端口(communication port)”两种,各自扮演着不同的角色。连接端口用于连接的建立,通信端口才真正用于双方的通信。只有服务进程才需要有连接端口,但是通信双方都需要有通信端口。
虽然LPC一般发生在进程之间,但是实际参与通信的总是具体的线程,所以在下面的叙述中都以线程作为通信的两端。
典型的建立连接和通信的过程如下:
? l 需要建立LPC通信时,其中提供服务的一方、即服务线程首先要通过NtCreatePort()创建一个命名的连接端口、即Port对象。这个对象名应为请求服务的一方、即客户线程所知。
? l 建立了上述连接端口以后,服务线程应通过NtListenPort()等待接收来自客户线程的连接请求(服务线程被阻塞)。
? l 客户线程通过NtConnectPort()创建一个客户方的无名通信端口,并向上述命名的连接端口发出连接请求(客户线程被阻塞)。
? l 服务线程收到连接请求(因而被唤醒)以后,如果同意建立连接就通过NtAcceptConnectPort()创建一个服务方的无名通信端口、接受连接、并返回该无名通信端口的Handle。然后再通过NtCompleteConnectPort()唤醒客户线程。
? l 客户线程被唤醒,并返回所创建的无名通信端口的Handle。
? l 服务线程另创建一个新的线程,负责为客户线程提供LPC服务。该线程因企图从上述通信端口接收报文、等待来自客户端的服务请求而被阻塞。所以,新创建的线程时LPC服务线程,而原来的服务线程是端口服务进程。
? l 端口服务线程再次调用NtListenPort(),等待来自其它客户的连接请求。
? l 客户线程通过NtRequestWaiReplyPort()向对方发送报文,请求得到LPC服务,并因等待回答而被阻塞。
? l 服务端的相应线程、即LPC服务线程因接收到报文而被唤醒,并根据报文内容提供相应的LPC服务。
? l LPC服务线程通过NtReplyPort()向客户方发送回答报文(一般是计算结果)。客户线程解除阻塞。
如果回顾一下Wine进程与服务进程wineserver之间的通信,就可以明白Wine是用命名管道和Socket在模仿Windows的LPC通信,只不过那是在用户空间的模仿。另一方面,熟悉Socket通信的读者可以看到,Port与Socket是很相像的。
先看Port的创建。我们看系统调用NtCreatePort()的代码:
[code]NTSTATUS STDCALL
NtCreatePort (OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN ULONG MaxConnectInfoLength,
IN ULONG MaxDataLength,
IN ULONG MaxPoolUsage)
{
PEPORT Port;
NTSTATUS Status;
DPRINT("NtCreatePort() Name %x\n", ObjectAttributes->ObjectName->Buffer);
/* Verify parameters */
Status = LpcpVerifyCreateParameters (PortHandle, ObjectAttributes,
MaxConnectInfoLength, MaxDataLength, MaxPoolUsage);
. . . . . .
/* Ask Ob to create the object */
Status = ObCreateObject (ExGetPreviousMode(), LpcPortObjectType,
ObjectAttributes, ExGetPreviousMode(),
NULL, sizeof(EPORT), 0, 0, (PVOID*)&Port);
. . . . . .
Status = ObInsertObject ((PVOID)Port, NULL, PORT_ALL_ACCESS,
0, NULL, PortHandle);
. . . . . .
Status = LpcpInitializePort (Port, EPORT_TYPE_SERVER_RQST_PORT, NULL);
Port->MaxConnectInfoLength = PORT_MAX_DATA_LENGTH;
Port->MaxDataLength = PORT_MAX_MESSAGE_LENGTH;
Port->MaxPoolUsage = MaxPoolUsage;
ObDereferenceObject (Port);
return (Status);
}[/code]
参数ObjectAttributes、即OBJECT_ATTRIBUTES结构中有个Unicode字符串,那就是对象名,需要在调用NtCreatePort()之前加以设置,这跟创建/打开文件时的文件名设置是一样的。当然,除对象名以外,ObjectAttributes中还有别的信息,那就不是我们此刻所关心的了。其余参数的作用则不言自明。代码中的LpcpVerifyCreateParameters()对参数进行合理性检查,ObCreateObject()和ObInsertObject()就无需多说了,而LpcpInitializePort()主要是对代表着端口的EPORT数据结构进行初始化。EPORT数据结构的定义如下:
[code]typedef struct _EPORT
{
KSPIN_LOCK Lock;
KSEMAPHORE Semaphore;
USHORT Type;
USHORT State;
struct _EPORT *RequestPort;
struct _EPORT *OtherPort;
ULONG QueueLength;
LIST_ENTRY QueueListHead;
ULONG ConnectQueueLength;
LIST_ENTRY ConnectQueueListHead;
ULONG MaxDataLength;
ULONG MaxConnectInfoLength;
ULONG MaxPoolUsage; /* size of NP zone */
} EPORT, * PEPORT;[/code]
结构中的QueueListHead就是用来接收报文的队列。ConnectQueueListHead则是用来缓存连接请求的队列,这是因为一个Port可能会一下子接收到好几个连接请求。字段Type用来纪录端口的类型,一共有三种类型:
[code]#define EPORT_TYPE_SERVER_RQST_PORT (0)
#define EPORT_TYPE_SERVER_COMM_PORT (1)
#define EPORT_TYPE_CLIENT_COMM_PORT (2)[/code]
从上面的代码中可以看出,NtCreatePort()所创建的是“请求端口”,即类型为EPORT_TYPE_SERVER_RQST_PORT的端口,也就是“连接端口”。
字段state说明端口的状态和性质,例如EPORT_WAIT_FOR_CONNECT、EPORT_CONNECTED_CLIENT等等。
注意每个EPORT结构中都嵌有一个“信号量”结构Semaphore,这就是通信双方用来实现同步的手段。所以说,Port是集成了数据交换和行为同步的综合性的进程间通信机制。
还有个字段OtherPort也值得一说,这是个指针,互相指向已经建立了连接的对方端口。
LpcpInitializePort()的代码就不看了。只是要说明一下,端口对象的初始化也包括了对其“信号量”数据结构的初始化,并且信号量的初值是0,而最大值则为最大整数LONG_MAX,所以实际上没有限制。
创建了连接端口以后,服务进程就通过NtListenPort()等待连接请求,并因此而被阻塞进入睡眠。
[code]NTSTATUS STDCALL
NtListenPort (IN HANDLE PortHandle, IN PLPC_MESSAGE ConnectMsg)
{
NTSTATUS Status;
/* Wait forever for a connection request. */
for (;;)
{
Status = NtReplyWaitReceivePort(PortHandle, NULL, NULL, ConnectMsg);
/* Accept only LPC_CONNECTION_REQUEST requests. Drop any other message. */
if (!NT_SUCCESS(Status) ||
LPC_CONNECTION_REQUEST == ConnectMsg->MessageType)
{
DPRINT("Got message (type %x)\n", LPC_CONNECTION_REQUEST);
break;
}
DPRINT("Got message (type %x)\n", ConnectMsg->MessageType);
}
return (Status);
}[/code]
所谓“收听(Listen)”,就是在一个for循环中反复调用NtReplyWaitReceivePort(),等待接收来自客户方的报文,直至接收到报文、并且所收到报文的类型为“连接请求”、即LPC_CONNECTION_REQUEST时为止。如果接收到的报文不是连接请求就回过去再等,所以才把它放在无限for循环中。
NtReplyWaitReceivePort()本来的作用是“发送一个应答报文并等待接收”,但是这里的应答报文指针为NULL,也就是无应答报文可发,这样就成为只是等待来自客户方的请求了。另一方面,这个函数是不带超时(Timeout)的,只要没有收到客户方的请求就一直等待下去。
[code][NtListenPort() > NtReplyWaitReceivePort()]
NTSTATUS STDCALL
NtReplyWaitReceivePort (IN HANDLE PortHandle, OUT PULONG PortId,
IN PLPC_MESSAGE LpcReply, OUT PLPC_MESSAGE LpcMessage)
{
return(NtReplyWaitReceivePortEx (PortHandle, PortId, LpcReply, LpcMessage, NULL));
}[/code]
NtReplyWaitReceivePort()是不带超时的,另一个系统调用NtReplyWaitReceivePortEx()则有超时功能,所以前者是通过后者实现的,只是把(最后那个)参数Timeout设成NULL。
[code][NtListenPort() > NtReplyWaitReceivePort() > NtReplyWaitReceivePortEx()]
NTSTATUS STDCALL
NtReplyWaitReceivePortEx(IN HANDLE PortHandle, OUT PULONG PortId,
IN PLPC_MESSAGE LpcReply, OUT PLPC_MESSAGE LpcMessage,
IN PLARGE_INTEGER Timeout)
{
. . . . . .
if( Port->State == EPORT_DISCONNECTED )
{
/* If the port is disconnected, force the timeout to be 0
so we don't wait for new messages, because there won't be
any, only try to remove any existing messages */
Disconnected = TRUE;
to.QuadPart = 0;
Timeout = &to;
}
else Disconnected = FALSE;
Status = ObReferenceObjectByHandle(PortHandle, PORT_ALL_ACCESS,
LpcPortObjectType, UserMode, (PVOID*)&Port, NULL);
. . . . . .
/* Send the reply, only if port is connected */
if (LpcReply != NULL && !Disconnected)
{
Status = EiReplyOrRequestPort(Port->OtherPort, LpcReply, LPC_REPLY, Port);
KeReleaseSemaphore(&Port->OtherPort->Semaphore,
IO_NO_INCREMENT, 1, FALSE);
. . . . . .
}
/* Want for a message to be received */
Status = KeWaitForSingleObject(&Port->Semaphore, UserRequest,
UserMode, FALSE, Timeout);
if( Status == STATUS_TIMEOUT )
{
. . . . . .
}
. . . . . .
/* Dequeue the message */
KeAcquireSpinLock(&Port->Lock, &oldIrql);
Request = EiDequeueMessagePort(Port);
KeReleaseSpinLock(&Port->Lock, oldIrql);
if (Request->Message.MessageType == LPC_CONNECTION_REQUEST)
{
LPC_MESSAGE Header;
PEPORT_CONNECT_REQUEST_MESSAGE CRequest;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -