虛擬串口是壹種“虛擬”設備,在本地沒有對應的串口硬件設備時,為應用層提供與串口設備相同的系統調用接口,以兼容原本使用本地串口的應用軟件。本文給出了壹種在Windows平臺上實現虛擬串口的方法,這樣實現的“串口”具有與真實串口相同的系統調用接口。
許多應用都需要虛擬串口。比如在Modem卡出現之前,有壹個外置的Modem連接在電腦的串口上,各種撥號程序也是通過串口與外置的Modem進行通信。為了像外部調制解調器壹樣使用內置卡而不修改現有的撥號程序,內置卡的驅動程序需要虛擬化壹個串口設備。再比如現在工業上使用的壹些串口服務器,往往有8個或者16個或者更多的串口來連接多個串口設備,然後通過壹個網卡直接連接到以太網。與之處於同壹網絡的計算機通過以太網與掛在串口服務器上的串口設備進行通信。為了使以前使用本地串口的軟件在計算機上兼容,有必要在計算機上提供虛擬串口驅動程序。
虛擬串口設計的關鍵是“串口”必須有和真實串口完全壹樣的系統調用接口。為此,修改現有的串行設備驅動程序是最好的捷徑。本文以Windows NT下的串口驅動程序為基礎,介紹了可以在Windows NT、Windows 2000和Windows XP上運行的各種版本的虛擬串口驅動程序的開發。
串口驅動中使用的幾種鏈表
由於串口是雙工設備,在讀請求完成之前可以發出寫請求,驅動層的所有I/O請求都要求異步完成,即前壹個請求還沒有完成,下壹個相同的請求可能又來了。因此,串行驅動程序需要使用多個鏈表數據結構來處理各種IRP(I/O請求包)。當收到壹個IRP時,判斷是否可以立即完成,可以立即處理並返回。如果不允許,IRP將被插入到相應鏈表的末尾,並在適當的時候進行處理,比如設備空閑的時候。這時候往往會產生壹個硬件中斷來刺激DPC(Deferred Procedure Call,延遲過程調用)進程,DPC處理函數會把鏈表頭的IRP壹個壹個拿出來,嘗試完成。串行驅動程序中有以下鏈表和DPC(在serial.h中定義):
ReadQueue和CompleteReadDpc
用於保存Read IRP和DPC的鏈表以便調度。DPC對應的處理函數是SerialCompleteRead,在read.c文件中。該函數的主要任務是從ReadQueue中提取下壹個IRP,並嘗試完成它。
WriteQueue和CompleteWriteDpc
用於保存writeirps和相應DPC的鏈表。DPC對應的函數是SeriaCompleteWrite,在write.c中實現,這個函數負責從WriteQueue中提取IRP並嘗試完成。
MaskQueue和CommWaitDpc
這對鏈表用來處理Windows串口驅動的壹個特性:事件驅動機制。它允許應用程序預設壹個事件標誌,然後等待對應於該標誌事件發生。DPC調用的函數是SerialCompleteWait,在Waitmask.c文件中實現。這個函數還試圖從MaskQueue中提取IRP並完成它。
PurgeQueue
這個鏈表與前面的略有不同。它沒有與之對應的DPC機制。相反,每次收到PurgeRequest時,都會從PurgeQueue中逐個提取IRP,並嘗試完成它。如果因為某種原因不能完成,就插入鏈表。對應的函數是purge.c文件中的SerialStartPurge。
上述機制是串口驅動的重要實現方法,在虛擬串口驅動中需要保留,但不同的是,在硬件串口驅動中,ISR(中斷服務程序)根據接收、發送或調制解調器中斷來激勵對應的DPC,而在虛擬串口驅動中,由於實際情況不同,會有不同的激勵機制。
驅動入口的實現
DriverEntry是驅動程序的入口函數,相當於應用程序C語言中的main函數。開發壹個虛擬串口驅動首先要做的就是修改。它的函數實體在initunlo.c文件中。只是在虛擬串口驅動中,因為不處理具體的硬件,所以沒有硬件資源分析、硬件初始化、判斷其工作狀態等處理。它只需要為虛擬字符串建立設備對象、符號鏈接和初始化數據結構。典型的函數實現如下:
NTSTATUS DriverEntry(在PDRIVER_OBJECT DriverObject中,在PUNICODE_STRING RegistryPath中)
{
/*填寫driver object-& gt;MajorFunction[]數組*/
/*創建設備對象*/
/*初始化串行數據結構*/
status = IoCreateDevice(driver object,sizeof(SERIAL_DEVICE_EXTENSION),& ampuniNameString,FILE_DEVICE_SERIAL_PORT,0,TRUE & amp;device object);
//初始化所有鏈接列表
initializelishead(& amp;擴展->;read queue);
initializelishead(…);
…;
//初始化所有DPC
KeInitializeDpc(& amp;擴展->;CompleteReadDpc,SerailCompleteRead,extension);
KeInitializeDpc(…);
/*建立符號鏈接*/
SerialSetupExternalNaming(擴展);
退貨狀態;
}
SerialRead和SerialCompleteRead的實現
SerailRead和SerialCompleteRead函數決定了讀取IRP的響應策略,它們都存儲在read.c中以串口服務器使用的虛擬串口為例。串口服務器接收到外部數據後,會通過網絡發送給計算機,計算機會產生相應的網絡中斷,並對協議數據進行處理。網絡接收線程緩存新接收的數據並激活CompleteReadDpc,從而調用SerialCompleteReadIrp,然後調用CompleteReadIrp處理每個Irp。它們的實現大致如下:
NTSTATUS SerialRead(在PDEVICE_OBJECT設備對象中,在PIRP Irp中)
{
/*此處省略變量聲明和初始化*/
/*從IRP提取相關數據*/
stack = IoGetCurrentIrpStackLocation(Irp);
ReadLen = stack-& gt;參數。閱讀。長度;
/*先看看本地緩沖區有沒有數據。閱讀*/
if(擴展->;InCounter & gt0 )
{//註意,這裏要鎖定,防止數據訪問沖突。
KeAcquireSpinLock(& amp;擴展->;
ReadBufferLock和lIrql);
first read =(ReadLen & gt;擴展->;
InCounter)?擴展->;in counter:ReadLen;
RtlCopyMemory(Irp-& gt;AssociatedIrp。
系統緩沖區,擴展-& gt;pInBuffer,first read);
擴展->;in counter-= first read;
ReadLen-= first read;
KeReleaseSpinLock(& amp;擴展->;
ReadBufferLock,lIrql);//釋放鎖定
}
/*數據看夠了嗎?如果是,IRP*/
if( 0 == ReadLen)
{
狀態=狀態_成功;
IRP-& gt;IoStatus。地位=地位;
IRP-& gt;IoStatus。信息= FirstRead
IoCompleteRequest(Irp,0);
退貨狀態;
}
/*如果沒有,將IRP插入隊列,通過網絡向串口服務器發送數據讀取請求*/
IoMarkIrpPending(Irp);
InsertWaitList(擴展名-& gt;ReadQueue,Irp);
status = TdiSendAsync(擴展-& gt;ComChannel,pAckPacket,PacketLen(pAckPacket),(PVOID)ReadAckComplete,Irp);
/*返回待定,表示IRP尚未完成*/
返回狀態_待定;
}
Void CompleteReadIrp(在PSERIAL_DEVICE_EXTENSION擴展中,在PIRP Irp中,在PUCHAR pInData中,在ULONG長度中)
{
/*此處省略變量聲明和初始化*/
/*讀取新數據*/
ReadLen =(ReadLen & gt;長度)?長度:ReadLen
如果(ReadLen!= 0)
{
RtlCopyMemory(pread async-& gt;
pReadBuffer、pInData、ReadLen);
預同步-& gt;pread buffer+= ReadLen;
預同步-& gt;read already+= ReadLen;
擴展->;PerfStats。ReceivedCount +=
ReadLen
}
其他
{
/*因為串口服務器只有在擁有相應數據或者超時(此時Length=0)的情況下才會發送回復並激活這個DPC進程,所以此時已經超時。為了方便地結束這個IRP,這裏有意更改了TotalNeedRead,造成了完全接收的假象*/
預同步-& gt;TotalNeedRead =
預同步-& gt;ReadAlready
}
if(pread async-& gt;TotalNeedRead = = pread async-& gt;ReadAlready)
{
/*是否收到IRP,如果收到,則結束IRP。
IRP*/
EndReadIrp(Irp);
/*從讀取隊列中移除下壹個IRP*/
}
/*如果本次IRP未完成且未超時,繼續等待本次DPC下次激活,註意判斷是否需要取消IRP */
}
SerialWrite和SerailCompleteWrite的實現
SerialWrite和SerailCompleteWrite決定了寫IRP的實現。在SerialWrite中調用網絡發送函數TdiSendAsync。發送完成後,CompleteWriteDpc將被激活,SerialCompleteWrite函數將被調用。它主要取出當前的WriteIRP,設置已經發送的數據的數量,並調用CompleteWriteIrp來進壹步處理IRP。大致如下:
NTSTATUS SerialWrite(在PDEVICE_OBJECT設備對象中,在PIRP Irp中)
{
/*此處省略變量聲明和初始化*/
/*從IRP提取相關數據*/
stack = IoGetCurrentIrpStackLocation(Irp);
send len = stack-& gt;參數。寫。長度;
/*為網絡發送和異步操作分配緩沖區,在CompleteWrite中發送完所有數據後釋放緩沖區*/
pWriteAsync = ExAllocatePool(非頁面池,
send LEN+PACKET _ HEADER _ LEN+sizeof(WRITE _ ASYNC));
if(pWriteAsync == NULL)
{
//錯誤處理
}
//保存異步數據
…
//設置網絡發送數據包。
build data packet(PP packet,WRITE,(USHORT)SendLen,pWriteAsync-& gt;pWriteBuffer);
/*首先臨時阻塞IRP並將其插入隊列,在CompleteWrite中完成*/
IoMarkIrpPending(Irp);
InsertWaitList(擴展名-& gt;WriteQueue,Irp);
/*通過網絡將寫請求和相關數據發送到串口服務器,串口服務器負責將數據傳輸到特定的串口設備*/
status = TdiSendAsync(擴展-& gt;ComChannel,PP packet,packet len(PP packet),(PVOID)CompleteWriteIrp,Irp);
//統計數據累積
擴展->;PerfStats。transmitted count+= send len;
返回狀態_待定;
}
NTSTATUS CompleteWriteIrp(在PDEVICE_OBJECT設備對象中,在PIRP pIrp中,在PVOID上下文中)
{
/*此處省略變量聲明和初始化*/
send len = pWriteAsync-& gt;TotalNeedWrite-pWriteAsync-& gt;WroteAlready
If(SendLen == 0)//所有數據都已發送。
{
EndWaitWriteIrp(pWriteIrp,STATUS_SUCCESS,
pWriteAsync-& gt;WroteAlready,pWriteAsync);
//從WriteQueue中取出下壹個from
}
否則//發送剩余數據
{
if(pWriteIrp-& gt;取消)
{
//IRP被要求取消,WriteIrp已完成。
EndWaitWriteIrp(pWriteIrp,STATUS_CANCELLED,
pWriteAsync-& gt;WroteAlready,pWriteAsync);
退貨狀態_已取消;
}
其他
{
//重新設置網絡數據包並發送。
build data packet(…);
status = TdiSendAsync(…);
//統計數據累積
擴展->;PerfStats。TransmittedCount +=
森德倫;
返回STATUS _ MORE _ PROCESSING _ REQUIRED;
}
}
}
其他幾個接口功能的實現
除了讀/寫之外,調用SerialUnload、SerialCreateOpen、SerialClose、SerialCleanup、SerailFlush等接口都是硬件關聯性較弱的接口函數,所以基本上不做修改,只刪除原來的硬件操作部分。稍微復雜壹點的是SerialIoControl,它包含了大量設置和讀取串口硬件狀態的處理,可以建立壹個本地數據結構來隨時保存虛擬串口的當前硬件狀態。同時,為了保證串口服務器的真實串口狀態與上層軟件的要求壹致,所有的設置請求都需要通過網絡發送到服務器,服務器負責改變真實硬件的狀態。