by fanxiushu 2017-04-06 轉載或引用請註明原始作者。
很早前的文章介紹過windows和linux平臺的虛擬網卡技術,
詳見
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,
上層數據包發到虛擬網卡驅動 ,也就是 SendPacketsHandler 函數被調用時候,
把這些數據包掛載到 某個Packet隊列,比如 tx_pkts_head 。
兩者每次處理完後,都調用 KeInsertQueueDpc觸發DPC調用 ,在DPC執行函數中,檢查兩個隊列是否都不爲空,
都不爲空的話,分別取出一個IRP和一個Packet,把Packet數據copy到IRP,完成這個IRP和Packet,如此循環,直到某個隊列爲空。
可能有人會問,這裏爲何要多次一舉使用DPC調用,而不是直接執行這種檢查處理!?
一般是在硬件的中斷函數中,爲了儘快讓出中斷函數,讓稍微耗時的處理交給次一級的函數處理,
但是也是要求儘快處理完成而不被其他軟中斷打斷,DPC就處於這種地位。
對軟件來說,是很高的運行級別,在DISPACH_LEVEL,運行的時候是不會被調度到其他CPU或者被軟中斷打斷的運行級別。
對於網絡數據包的處理就是需要在這種DPC環境中運行,才能讓他達到更好的IO效率。
大致僞代碼如下,
在IRP_MJ_DEVICE_CONTROL請求中,響應 IOCTL_NCARD_READ_DATA(也就是read IOCTL宏)
case IOCTL_NCARD_READ_DATA:
// PENDING
status = STATUS_PENDING;
IoMarkIrpPending(Irp);
InitializeListHead(&Irp->Tail.Overlay.ListEntry);
Irp->Tail.Overlay.DriverContext[0] = a;
IoSetCancelRoutine(Irp, ioctl_cacnel_routine); ///
if (Irp->Cancel) {
if (IoSetCancelRoutine(Irp, NULL) != NULL) { //取消例程還沒被執行,自己取消
status = STATUS_CANCELLED;
complete_irp(Irp, status, 0);
}
////
}
else {
///加入到隊列 , 等待 上層有數據包發送 SendPacketsHandler 被調用,再從 tx_irps_head隊列取出IRP進行處理。
NdisInterlockedInsertTailList(&a->tx_irps_head, &Irp->Tail.Overlay.ListEntry, &a->tx_spinlock); ////
}
////// 觸發DPC調用,這裏採用DPC。
KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL); /////
.......
上邊的DPC的初始化操作,
KeInitializeDpc(&a->tx_dpc, adapter_complete_send_packets_dpc, a);
在 adapter_complete_send_packets_dpc 這個DPC執行函數中完成類似如下的操作:
///--------------------------------------------------------------------------------------------------
adapter_t* a = (adapter_t*)context;
adapter_inc(a);
PLIST_ENTRY entry;
while (TRUE) {
tx_lock(a);
if (IsListEmpty(&a->tx_irps_head) || IsListEmpty(&a->tx_pkts_head)) {
tx_unlock(a);
break;
}
entry = RemoveHeadList(&a->tx_irps_head);
PIRP Irp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry );
if (IoSetCancelRoutine(Irp, NULL) == NULL) { /// 應該檢查返回值,若爲空,說明取消例程已經被調用了
NdisInitializeListHead(&Irp->Tail.Overlay.ListEntry); //初始化,防止在取消例程出問題
tx_unlock(a);
continue;
}
entry = RemoveHeadList(&a->tx_pkts_head);
PNDIS_PACKET packet = CONTAINING_RECORD(entry, NDIS_PACKET, MiniportReserved);
tx_unlock(a);
/////
NTSTATUS status = adapter_send_packet_to_irp(a, packet, Irp); //// 複製 Packet 數據到 Irp
///////
}
adapter_dec(a);
/////------------------------------------------------------------------------------------------------------------
在網卡 SendPacketsHandler 函數中做類似如下處理:
for (index = 0; index < NumberOfPackets; ++index) {
PNDIS_PACKET packet = PacketArray[index]; ///
////
status = NDIS_STATUS_PENDING;
NDIS_SET_PACKET_STATUS(packet, status);
tx_inc(a); //增加send包計數
NdisInterlockedInsertTailList(&a->tx_pkts_head, (PLIST_ENTRY)packet->MiniportReserved, &a->tx_spinlock); ///掛載到隊列
///
}
.........
////觸發 DPC調用
KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL);
-------.........................................................
如上就完成一個數據包的交換。
write ioctl也做類似的處理。
回到應用層來,在應用層,一般做法都是阻塞調用 DeviceIoControl ,這樣沒什麼問題。
但是可以這樣考慮,我們在調用DeviceIoControl返回後,處理數據包,然後再接着調用DeviceIoControl,
在再次調用DeviceIocontrol之前這中間有空隙,如果一次多投遞讀請求,這樣數據包一來就被接收,中間就不存在空隙了。
最容易想到的就是多線程調用 DeviceIoControl ,但是這種密集型的IO,多線程反而會降低效率,
而且多線程還容易造成接收到的數據包亂序,這對TCP這種君子協議來說不是好事。
在一個線程裏,同時投遞多個請求,異步方式處理,這纔是解決這個問題的辦法。
完成端口就可以完成這件事,
大家所熟悉的完成端口,多用到網絡編程上,其實網絡套接字只是它的一個應用而已,
凡是具備異步讀寫的OVERLAPPED重疊請求的,都可以關聯到完成端口。
這裏也就不具體描述如何使用完成端口來異步投遞read ioctl請求了,因爲相信大家已經很熟悉。
以上開發的虛擬網卡測試的都是在100M網絡環境下進行的,但是在千兆網絡環境下的測試是很糟糕的。
我按照上邊的環境測試,1000Mbps 的環境下,最快只能達到 20 MBytes 每秒的速度,也就是相當於 千兆網的 五分之一的速度。
這個測試數據非常讓人氣餒。
歸根結底還是因爲每個數據包長度不超過 1514,千兆網每秒需要處理 8多萬甚至更多數據包,這麼多數據包,
按照每個包的取,而且取出來之後,再把每個包在應用層封裝一下再發到服務端,再服務端再轉發出去,
達到對方虛擬網卡,再傳遞到網卡驅動層接收,再等待ACK迴應,
這中間的延時比真正的物理硬件環境要高得多了,
解決這個問題也不是沒辦法,既然包太多了,那就減少包數量。如果在純粹的虛擬網絡中,可以設置虛擬網卡的MTU值,
讓他更大,比如設置4M這麼大的MTU值,這樣每個包就可以達到 4M,傳遞的包個數大大減少,這種情況下瘋狂傳輸文件的話,
立馬就能把千兆網絡跑滿。
但是如果把虛擬網卡和真實的網卡混合橋接,1514的包大小的限制是無法改變的,任然要面對大量的包造成的效率問題。
在寫這篇文章時候,想到另外一個把數據包傳遞到應用層的辦法,不採用IRP傳遞。
就是在應用層開闢一塊很大的內存,比如2M的內存,這塊內存映射到驅動,這個內存塊按照 1514 拆分成起碼1000多個小塊。
這些小塊組成循環隊列, 驅動不停的朝這個循環隊列寫數據包,應用層不停的從這個循環隊列讀數據包。
這樣就減少IRP調用的開銷,應該能提高效率,但在千兆網環境中能否得到質的提升,因爲沒實現,所以不能下定論。
不過即使提升了驅動和應用層的IO效率,但是應用層還得發數據包到網絡去轉發,這個無論如何也得不到實質的提升。
總體下來估計不會有質的提升。
範秀樹 2017-04-06 晚
很早前的文章介紹過windows和linux平臺的虛擬網卡技術,
詳見
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,
上層數據包發到虛擬網卡驅動 ,也就是 SendPacketsHandler 函數被調用時候,
把這些數據包掛載到 某個Packet隊列,比如 tx_pkts_head 。
兩者每次處理完後,都調用 KeInsertQueueDpc觸發DPC調用 ,在DPC執行函數中,檢查兩個隊列是否都不爲空,
都不爲空的話,分別取出一個IRP和一個Packet,把Packet數據copy到IRP,完成這個IRP和Packet,如此循環,直到某個隊列爲空。
可能有人會問,這裏爲何要多次一舉使用DPC調用,而不是直接執行這種檢查處理!?
一般是在硬件的中斷函數中,爲了儘快讓出中斷函數,讓稍微耗時的處理交給次一級的函數處理,
但是也是要求儘快處理完成而不被其他軟中斷打斷,DPC就處於這種地位。
對軟件來說,是很高的運行級別,在DISPACH_LEVEL,運行的時候是不會被調度到其他CPU或者被軟中斷打斷的運行級別。
對於網絡數據包的處理就是需要在這種DPC環境中運行,才能讓他達到更好的IO效率。
大致僞代碼如下,
在IRP_MJ_DEVICE_CONTROL請求中,響應 IOCTL_NCARD_READ_DATA(也就是read IOCTL宏)
case IOCTL_NCARD_READ_DATA:
// PENDING
status = STATUS_PENDING;
IoMarkIrpPending(Irp);
InitializeListHead(&Irp->Tail.Overlay.ListEntry);
Irp->Tail.Overlay.DriverContext[0] = a;
IoSetCancelRoutine(Irp, ioctl_cacnel_routine); ///
if (Irp->Cancel) {
if (IoSetCancelRoutine(Irp, NULL) != NULL) { //取消例程還沒被執行,自己取消
status = STATUS_CANCELLED;
complete_irp(Irp, status, 0);
}
////
}
else {
///加入到隊列 , 等待 上層有數據包發送 SendPacketsHandler 被調用,再從 tx_irps_head隊列取出IRP進行處理。
NdisInterlockedInsertTailList(&a->tx_irps_head, &Irp->Tail.Overlay.ListEntry, &a->tx_spinlock); ////
}
////// 觸發DPC調用,這裏採用DPC。
KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL); /////
.......
上邊的DPC的初始化操作,
KeInitializeDpc(&a->tx_dpc, adapter_complete_send_packets_dpc, a);
在 adapter_complete_send_packets_dpc 這個DPC執行函數中完成類似如下的操作:
///--------------------------------------------------------------------------------------------------
adapter_t* a = (adapter_t*)context;
adapter_inc(a);
PLIST_ENTRY entry;
while (TRUE) {
tx_lock(a);
if (IsListEmpty(&a->tx_irps_head) || IsListEmpty(&a->tx_pkts_head)) {
tx_unlock(a);
break;
}
entry = RemoveHeadList(&a->tx_irps_head);
PIRP Irp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry );
if (IoSetCancelRoutine(Irp, NULL) == NULL) { /// 應該檢查返回值,若爲空,說明取消例程已經被調用了
NdisInitializeListHead(&Irp->Tail.Overlay.ListEntry); //初始化,防止在取消例程出問題
tx_unlock(a);
continue;
}
entry = RemoveHeadList(&a->tx_pkts_head);
PNDIS_PACKET packet = CONTAINING_RECORD(entry, NDIS_PACKET, MiniportReserved);
tx_unlock(a);
/////
NTSTATUS status = adapter_send_packet_to_irp(a, packet, Irp); //// 複製 Packet 數據到 Irp
///////
}
adapter_dec(a);
/////------------------------------------------------------------------------------------------------------------
在網卡 SendPacketsHandler 函數中做類似如下處理:
for (index = 0; index < NumberOfPackets; ++index) {
PNDIS_PACKET packet = PacketArray[index]; ///
////
status = NDIS_STATUS_PENDING;
NDIS_SET_PACKET_STATUS(packet, status);
tx_inc(a); //增加send包計數
NdisInterlockedInsertTailList(&a->tx_pkts_head, (PLIST_ENTRY)packet->MiniportReserved, &a->tx_spinlock); ///掛載到隊列
///
}
.........
////觸發 DPC調用
KeInsertQueueDpc(&(a)->tx_dpc, NULL, NULL);
-------.........................................................
如上就完成一個數據包的交換。
write ioctl也做類似的處理。
回到應用層來,在應用層,一般做法都是阻塞調用 DeviceIoControl ,這樣沒什麼問題。
但是可以這樣考慮,我們在調用DeviceIoControl返回後,處理數據包,然後再接着調用DeviceIoControl,
在再次調用DeviceIocontrol之前這中間有空隙,如果一次多投遞讀請求,這樣數據包一來就被接收,中間就不存在空隙了。
最容易想到的就是多線程調用 DeviceIoControl ,但是這種密集型的IO,多線程反而會降低效率,
而且多線程還容易造成接收到的數據包亂序,這對TCP這種君子協議來說不是好事。
在一個線程裏,同時投遞多個請求,異步方式處理,這纔是解決這個問題的辦法。
完成端口就可以完成這件事,
大家所熟悉的完成端口,多用到網絡編程上,其實網絡套接字只是它的一個應用而已,
凡是具備異步讀寫的OVERLAPPED重疊請求的,都可以關聯到完成端口。
這裏也就不具體描述如何使用完成端口來異步投遞read ioctl請求了,因爲相信大家已經很熟悉。
以上開發的虛擬網卡測試的都是在100M網絡環境下進行的,但是在千兆網絡環境下的測試是很糟糕的。
我按照上邊的環境測試,1000Mbps 的環境下,最快只能達到 20 MBytes 每秒的速度,也就是相當於 千兆網的 五分之一的速度。
這個測試數據非常讓人氣餒。
歸根結底還是因爲每個數據包長度不超過 1514,千兆網每秒需要處理 8多萬甚至更多數據包,這麼多數據包,
按照每個包的取,而且取出來之後,再把每個包在應用層封裝一下再發到服務端,再服務端再轉發出去,
達到對方虛擬網卡,再傳遞到網卡驅動層接收,再等待ACK迴應,
這中間的延時比真正的物理硬件環境要高得多了,
解決這個問題也不是沒辦法,既然包太多了,那就減少包數量。如果在純粹的虛擬網絡中,可以設置虛擬網卡的MTU值,
讓他更大,比如設置4M這麼大的MTU值,這樣每個包就可以達到 4M,傳遞的包個數大大減少,這種情況下瘋狂傳輸文件的話,
立馬就能把千兆網絡跑滿。
但是如果把虛擬網卡和真實的網卡混合橋接,1514的包大小的限制是無法改變的,任然要面對大量的包造成的效率問題。
在寫這篇文章時候,想到另外一個把數據包傳遞到應用層的辦法,不採用IRP傳遞。
就是在應用層開闢一塊很大的內存,比如2M的內存,這塊內存映射到驅動,這個內存塊按照 1514 拆分成起碼1000多個小塊。
這些小塊組成循環隊列, 驅動不停的朝這個循環隊列寫數據包,應用層不停的從這個循環隊列讀數據包。
這樣就減少IRP調用的開銷,應該能提高效率,但在千兆網環境中能否得到質的提升,因爲沒實現,所以不能下定論。
不過即使提升了驅動和應用層的IO效率,但是應用層還得發數據包到網絡去轉發,這個無論如何也得不到實質的提升。
總體下來估計不會有質的提升。
範秀樹 2017-04-06 晚