WSASend 異步IO發送數據的機理淺析
最近在摸索WSASend函數在IOCP網絡模型中的發送機制, 首先當我們使用Overlapped的Socket的時候, 其實已經就是在異步使用該Socket了, 這就有一個疑問, WSASend到底是如何發送數據, 在應用層又是如何處理發送的內存的呢, 帶着這個疑問查閱了Reactos的代碼, 終於有了一些答案, 針對網上一直說關於WSASend會鎖定內存的說法也有了一個比較清晰的答案, 雖然網上一直存在這個說法主要是源於國外的一篇高性能Socket的一篇譯文, 但是出到底鎖定的是什么基本沒人能完整的描述出來.
首先WSASend函數調用后, 是通過向驅動層發送IO指令, 提交irp請求, 驅動層核心代碼檢查如果當前的內存如果足以緩沖用戶提交的數據, 便直接Copy用戶的數據, 這個部分的數據會被安排在內核的非分頁緩沖池中, 因為非分頁緩沖池是有大小限制, 所以說並不是用戶發送多大的內存, 核心驅動都直接Copy的, 一旦Copy成功就代表IO的請求正確的提交, 也就是說只要WSASend返回的是0 或者是-1(GetLastError 為IO_PENDDING)我們就可以認為用戶層代碼提交的數據理論上是會正確被發送到客戶端的, 為什么說是理論呢, 因為那些異常情況可能包含網絡發送中斷線, 對方關閉連接等等異常的情況, 還有對方拒收信息等, 除此之外我們都可以認為系統會安全的幫我們緩存這些數據直到發送完畢這些數據, 那么如果非分頁緩沖池不足以提供和用戶提交的數據等量大小的內存, 那么就會啟用另外一種模式, 也就是網上一直說的”鎖定內存”, 這里不得不提一下驅動層關於I/O管理內存的3種方案, 這里我就不再提驅動層為什么不能直接訪問用戶虛擬內存的原因了, 這個網上驅動開發的資料很多.
這三種方式, 第一種就是我們剛才說的直接Copy, 在驅動層這個叫BufferedIO方式, 另外一直就是直接(Direct), 最后一種叫Neither故名思議就是直接使用用戶的虛擬內存, 這個是不安全, 主要是一些不需要提交用戶數據的情況下使用, 當驅動層非分頁緩沖池不足緩沖用戶數據時, 驅動便使用MDL(Memory Descriptor List)建立內核虛擬內存映射, 這種方式就是所謂的直接方式, 首先驅動會申請一片內存用於存放MDL結構和用戶虛擬內存的頁表, MDL結構主要描述的是內存的大小, 起始位置, 偏移量等等信息, 因為內存總是頁式管理的, 所以頁表里填充的都是用戶的虛擬內存對應的頁碼, 下一步便是填充這些頁碼, 通過MDL中提供的起始位置和大小計算和填充頁表中的虛擬內存頁碼, 再通過遍歷這些頁碼將它們轉換成物理內存的頁碼, 再這個過程中系統會做2件很重要的事, 第一個是檢查用戶的虛擬內存, 如果虛擬內存的頁面對應的內存不在物理內存里, 那么就得不到物理內存頁, 這個時候需要將虛擬內存頁面文件中的內容強制交換回物理內存中, 同時將這塊物理內存打叫一個標記(既鎖定)告訴操作系統無論內存夠不夠, 這塊物理頁面都不可以被置換回虛擬內存頁面文件中, 另外一個操作即增加該物理內存的引用計數, 防止用戶層釋放虛擬內存時導致這塊物理內存也被釋放, 有了這2層保障, 我們可以認為這塊物理內存已經被鎖定給內核使用了, 有興趣的朋友可以通過實驗進行驗證, 當然我已經測試過了.
這就是網上所說的鎖定內存的機制, 實際上當這些內存被鎖定時, 用戶層調用完Wsasend后完全可以釋放掉這些內存, 后面還會講到這樣做得有一個先決條件, 所謂用戶釋放掉這些內存不是代表物理內存也被釋放了, 因為物理內存頁都有引用計數的概念, 用戶層雖然釋放了內存, 但是這塊物理內存依然有其他對象訪問(即引用計數不為0), 所以不會直接就釋放了, 核心層在鎖定這塊物理內存后, 不是馬上將這塊內存映射為虛擬內存地址, 而是在要使用的時候才進行映射, 當內核發送數據的時候實際上是訪問內核虛擬內存地址, MMU通過虛擬地址訪問到相應的物理內存, 這塊內存也就是用戶之前發送的數據(鎖定的物理內存頁), 有沒有發現這個步驟實際上已經少了用戶層Copy數據到內核層的步驟, 可以從下圖中了解 用戶提交的數據是如何被內核層直接訪問的.
其實可以把這個想象成內存共享文件FileMapping的概念, 虛擬內存實際是連續的, 但是物理內存大部分情況下不是連續的.
前面我講到WSASend發送完數據后理論上是可以釋放掉這塊用戶層內存的, 但是這么做其實沒有什么價值,因為這塊內存釋放掉了, 不代表物理內存回收了, 由於物理內存被內核引用了, 系統只會回收銷毀該進程的虛擬內存, 由於進程的虛擬內存可以申請到的大小限制是2G的空間(32位Windows), 如果是因為這個理由防止應用程序虛擬空間不夠用倒沒什么, 我覺得釋放沒啥問題, 但是問題是另外一種情況, 當我們發送的數據是小內存塊的時候, 由於應用程序一般會有自己的內存管理器, 這種情況就危險了, 當我們發送數據后調用FreeMem的時候, 應用程序內存管理器(如delphi使用的是fastmm)會自動回收這個內存塊, 但是不會調用系統的VirtualFree釋放掉這塊內存, 也就是說這塊內存實際上沒有真正的銷毀, 如果這塊內存與物理還保持着映射(我們這里的假設是沒有被置換到虛擬內存頁面文件上), 那么下次再分配小內存塊的時候很可能依然得到同樣的虛擬地址, 重要的是這個地址指向的物理地址和上次發送數據的物理地址是同一個, 那么問題就來了, 如果我們改寫這塊數據, 必然會影響到系統正在發送中的數據, 雖然這種可能性極小, 但這要依賴應用程序當前使用的內存管理器是如何處理內存分配的, 為了保證安全性, 我們還是老老實實的等到Iocp的通知之后再去釋放這塊內存.