1. 程式人生 > >windows虛擬網絡卡驅動開發

windows虛擬網絡卡驅動開發

詳見
     http://blog.csdn.net/fanxiushu/article/details/8526708
     http://blog.csdn.net/fanxiushu/article/details/8526719
     http://blog.csdn.net/fanxiushu/article/details/8525749
     http://blog.csdn.net/fanxiushu/article/details/8507638
前兩個是講述如何組成一個虛擬區域網,後邊的是如何在linux平臺下開發一個虛擬網絡卡
(當時提供的程式碼比較老,需要修改才能在新版本linux下使用,
或者懶得自己開發,直接使用linux自帶的tun驅動,linux底層這些驅動總比windows平臺方便得多)。
這些文章介紹過如何利用虛擬網絡卡組建區域網的原理:獲取應用層程式發給虛擬網絡卡的資料包,
然後通過真實的網路發給服務端, 服務端再轉發給另外一臺機器,這臺機器再把從網路獲取的資料包傳遞給虛擬網絡卡。
通過這樣的方式,就把處於不同真實網路環境中的機器連線到同一個虛擬區域網中。
只是當時沒介紹如何開發windows虛擬網絡卡驅動,這篇文章填補這個空白。

win7系統有最新的NDIS6.2框架,win8 的NDIS提高到6.3以上,win10 達到ndis6.4 。
最大變化是從NDIS5.x 到 NDIS6.x, 連最基本的包的定義等資料結構都發生了巨大變化。
但是windows有個最大優點,就是相容,在win7,win8,win10,等平臺可以執行ndis5.x框架的驅動,
(不過ndis5.x的中間驅動無法在win10上執行,這個估計是最大不方便了)
就跟TDI驅動能在各種windows平臺通吃一樣,NDIS5.x也能通吃各種windows平臺。
這裡採用 NDIS5.1框架,不是要抱著老的框架不放,而是許多使用者抱著WinXP 不放,
同時要相容 WinXP和WIN7,而且也不用開發兩套程式碼的最好選擇就是NDIS5.1 了。
如果你的程式只執行在WIN7系統以上,可以只使用NDIS6以上的版本的框架,
NDIS6雖然基本結構尤其是包結構改變了,但是我們開發的總體方式差不多。

首先在DriverEntry中宣告 NDIS_MINIPORT_CHARACTERISTICS 變數,它是一個包含多個回撥函式的資料結構,
在此結構中填寫好我們需要的各種回撥函式之後,呼叫 NdisMRegisterMiniport 函式註冊。

NdisMRegisterMiniport雖然沒開放原始碼,但是基本工作流程應該能想到,因為虛擬網絡卡驅動也是即插即用驅動模型,
因此在DriverEntry 函式中一樣需要實現 AddDevice,以及各種派遣函式,
只是 NdisMRegisterMiniport  使用它內部的某個函式 設定到AddDevice 回撥中,同時設定各種IRP_MJ_XXX派遣函式,
並且做一些其他初始化操作,當有裝置(也就是網絡卡)插上來,DriverObject->DriverExtension->AddDevice 函式被呼叫,
這時會進入到NdisMRegisterMiniport註冊的 某個內部函式中,
在這個函式中會呼叫 NDIS_MINIPORT_CHARACTERISTICS  匯出的 InitializeHandler 函式,
這樣就進入到我們註冊的網絡卡初始化函式。

在虛擬網絡卡驅動中,主要實現以下幾個回撥函式,基本上就能完成一個虛擬網絡卡的功能:

  InitializeHandler , 初始化網絡卡。也就是當我們安裝一塊網絡卡例項驅動的時候,這個函式被呼叫,
                                  在這個函式中,初始化各種資源,這個函式等同於普通的即插即用驅動的AddDevice函式,
                                  只是被NDIS框架封裝成 InitializeHandler 回撥函數了。
 HaltHandler , 解除安裝網絡卡,當我們解除安裝某個網絡卡驅動時候,這個函式被呼叫,
                         相當於普通即插即用驅動程式收到 IRP_MN_REMOVE_DEVICE等訊息之後觸發的回撥。

QueryInformationHandler, 查詢網絡卡 OID。其實就是查詢網絡卡的各種資訊,網絡卡包含的資訊很多,基本上有幾十個。
SetInformationHandler,      設定網絡卡OID。 設定我們感興趣的OID資訊。
ResetHandler, 是否重啟網絡卡,虛擬網絡卡驅動中,基本用不上。
CheckForHangHandler, 檢測網絡卡是否處於hang狀態,是的話,呼叫ResetHandler, 虛擬網絡卡基本上也用不著。

SendPacketsHandler, 處理網路資料包的核心函式之一,這個回撥函式表示網絡卡從應用層程式接收到乙太網資料包,
                                      比如在應用層呼叫套接字函式send 或sendto傳送資料,資料進入到核心的傳輸層,
                                      經過分析剝離,進入到NDIS協議驅動層,協議驅動層找到這個資料是朝哪個網絡卡傳送的,
                                      於是找到這個網絡卡註冊的  SendPacketsHandler 回撥函式地址,
                                      最後呼叫這個回撥函式實現資料包的真正傳送。                             
                                      在SendPacketsHandler  函式中處理的資料包是準備發給底層的物理鏈路的,
                                      虛擬網絡卡沒有物理鏈路,因此我們把這些資料包入隊,
                                      然後直接在驅動層通過WSK(TDI)方式(或者其他各種方式,如USB, 串列埠等)發給遠端裝置或電腦,
                                      或者把資料包傳遞到應用層, 讓我們的應用層程式做各種處理,為了開發的方便和簡潔,
                                      我們採用的是傳遞到應用層來處理。

ReturnPacketHandler, 這個函式與上邊的剛好相反,當物理鏈路有資料包到達(或者通過其他方式有資料包,如USB等),
                                      呼叫NDIS函式NdisMIndicateReceivePacket,通知上層有個資料包達到,
                                      等上層(這個上層就是處理TCP/IP等各種協議的協議層)處理完這個資料包之後,
                                      ReturnPacketHandler 就被呼叫。
                                      接著這個資料被上傳到傳輸層進一步分析處理,
                                      再進入到應用層,這時候呼叫 recv或者recvfrom等套接字函式的程式就接收到了資料。
                                      我們的虛擬網絡卡驅動在應用層程式通過某個IOCTL控制命令傳遞一個數據包到驅動,
                                      在驅動中直接呼叫NdisMIndicateReceivePacket通知上層有資料包到達。

CancelSendPacketsHandler, 這個是NDIS5.1框架中,提供的取消某些資料包傳送的回撥函式,也就是上層呼叫SendPacketsHandler,
                                     傳送資料包,但是我們的驅動還沒來得及處理,只是入隊等待處理,這個時候上層決定取消某些資料包的傳送,
                                     於是呼叫 CancelSendPacketsHandler 讓我們取消某些資料包的傳送。
PnPEventNotifyHandler, NDIS5.1框架的PnP通知事件,其實就是對應普通的即插即用驅動中的IRP_MJ_PNP請求的封裝。
AdapterShutdownHandler, NDIS5.1框架的網絡卡關閉事件。

因為我們的虛擬網絡卡驅動是把資料包傳遞到應用層來處理,也就是應用層相當於是“物理連線”,
必須建立一個控制裝置才能跟應用層交換資料,NDIS5.1框架提供了NdisMRegisterDevice 函式來建立一個控制裝置,
在 InitializeHandler 網絡卡例項初始化函式中可以呼叫這個函式建立控制裝置,
在 HaltHandler 網絡卡解除安裝函式中可以呼叫NdisMDeregisterDevice刪除這個裝置。
建立這個控制裝置時候,傳遞一些引數,包括派遣函式,我們感興趣的主要是IRP_MJ_DEVICE_CONTROL,以及CREATE /CLOSE 。
可以定義兩個IOCTL命令,一個用於資料包讀取,一個用於向驅動寫資料包,比如命名為 READ IOCTL 和WRITE IOCTL。
網絡卡處理的資料包是非常多的,
以100M乙太網來計算,乙太網資料包大小 1514,當全速傳輸時候, 100M*1024*1024/8/1514 =  大約 8千多個數據包, 
全速傳輸,每秒傳輸8千多個數據包甚至更多, 這個量是很大的,千兆網達到8萬多個包每秒,甚至更多。
為了儘量提高IO吞吐率,在應用層可以採用完成埠方式接收資料包,下邊會講到。
(開發虛擬網絡卡驅動相對而言不算太難,難得是如何調優,讓他的IO效能提升到更好的效果)

如此之多的資料包要在應用層和驅動層進行交換,驅動裡邊該採用什麼結構來處理,才能儘可能的提高IO效率呢?
這裡順帶提一下我測試IO效率的方式,
一個服務端程式,負責多個虛擬網絡卡資料包轉發,它讓多個虛擬網絡卡組成一個虛擬區域網,
虛擬網絡卡的客戶端程式採用TCP連線到服務端,轉發的虛擬網絡卡資料包也是通過TCP方式轉發。
服務端程式放到一臺效能還可以的WIN7的機器上(四代 i7 的 8核CPU,16G記憶體),
它的千兆網絡卡接到千兆交換機上,完全保證它達到千兆網絡卡的速度。
一臺裝有XP系統的古老機器,以前的ATOM的CPU,1G記憶體,它的網絡卡接到百兆交換機上。
一臺裝WIN10系統的古老CPU是Core2的機器,2G記憶體,它的網絡卡接到百兆交換機上。
測試以TCP傳輸為主,UDP基本不考慮,
測試時候使用FTP,HTTP,windows資料夾共享三種方式上傳或者下載一個超過1G大小的大檔案。
在100M網路環境下,以上的測試,XP和WIN10之間通過虛擬網路傳輸檔案,
基本達到5M-6MB的速度,也就是達到50-%60%的真實網絡卡利用率,
也許使用更好的機器效果會更高,在裝服務端程式的機器上也裝上虛擬網絡卡,讓他與WIN10傳輸檔案,
這個速度就比較高了,維持在7-11M的速度,基本上達到 70-95%的真實網絡卡利用率,峰值時候基本能達到飽和。
這種測試還跟客戶端服務端程式,機器配置等多種因素有關,因此不能保證換個環境就一定準確。
整體來看,WIN7以上的系統性能的表現比WINXP系統好,這應該是WIN7上的核心,微軟把整個網路層重構了一遍,跟WINXP完全不同。

我是基於以上測試來評定開發的虛擬網絡卡的IO效能。
為此想了許多辦法,也做了多種嘗試,往往寫好了一種方式的程式碼發現效果不太理想,之後想到另外一種處理方式可能會更好,
因此廢棄先前的程式碼,重新再實現新想到的處理方式,來來回回的折騰了多種處理方式,採用瞭如下的處理結構:
(不過任然是採用單幀接收和傳送的方式)
資料包傳遞以IRP 請求為主,一個IRP傳遞一個數據包。也就是上邊所說的定義兩個IOCTL命令,read ioctl和write ioctl,
每次都發送和接收一個數據包,都會產生一個IOCTL呼叫。

以read ioctl為例,
應用層投遞read ioctl請求到驅動,這個請求的 Irp 都被驅動掛載到某個 IRP佇列 比如 tx_irps_head,
上層資料包發到虛擬網絡卡驅動 ,也就是