?? 漫談兼容內核之十八:windows的lpc機制.txt
字號:
漫談兼容內核之十八: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 + -