導言
寫一個Windows平台下的應用程式大多時候都是離不開讀寫檔案,網路通訊的。
比如一個服務應用程式來說,它可能從網路介面卡接受使用者的請求,對請求進行處理計算,最終將使用者端所需的資料返回,中間可能還涉及到對磁碟的讀寫,這些都是I/O操作,所以,要設計一個穩健的,高效的,伸縮性好的應用程式,就必須將Windows的I/O機制搞清楚。
一、 兩種 讀/寫 機制
輸入Input / 輸出Output,有兩種機制,他們是:
1 同步I/O: 線程執行一個輸入輸出函數時,輸入輸出工作執行完畢後,函數返回繼續執行以後的代碼。
2 非同步I/O: 線程執行一個輸入輸出函數時,函數不等待讀/寫操作完成便立即返回,線程可先去執行下文,執行下文時可查詢剛剛發起的讀/寫操作是否完成。
解讀:
可以看出,非同步I/O是更加高效的,同步I/O將會阻塞線程的執行,不應該被提倡。
有時候我們會認為,我們為每次執行I/O操作的時候,新開一個線程,讓他去執行同步I/O函數,然後我們的原線程去執行工作,這樣做是可以的,但是其實這樣就是我們自己類比實現了非同步I/O,其實我們調用非同步I/O函數的時候,作業系統內部也是維護了一個新線程去為我們進行I/O操作的。
但是,建立線程是一件很耗費CPU資源的事情,而且I/O操作時,CPU需要計算,需要從磁碟或網路介面卡的緩衝區,將資料讀取到記憶體中,或者將記憶體中的資料寫到磁碟或網路介面卡的緩衝區裡,所以,I/O操作的線程不是睡眠的,而是忙碌的,所以我們得出了一個結論,如果我們的電腦上只有兩個CPU,同時執行的I/O線程超過兩個以上時,CPU會因為I/O線程不是睡眠的而分配給他們時間片,於是線程間的頻繁切換,也會降低我們的應用程式的效能。
那麼,我們得出結論:同時執行的I/O線程的數量不應該大於CPU數量,客戶發起的I/O請求需要一個隊列,每次並發處理的I/O操作應該等於CPU數量,如果並發處理的I/O請求數量太多,CPU切換過於頻繁,會將CPU資源浪費線上程切換上,從而嚴重降低程式效能。
二、讀/寫 前後的工作:建立與釋放要讀/寫的裝置的核心對象
要對裝置進行讀/寫,必須Crowdsourced Security Testing道建立一個用於該裝置的讀寫的核心對象,在讀/寫之後,釋放該核心對象。
1. 建立I/O核心對象的函數: CreateFile(...) 與其他核心對象一樣,我們擷取的僅僅是一個控制代碼。
2. 關閉I/O核心對象的函數: CloseHandle(...)
解讀:
HANDLE CreateFile(
LPCTSTR lpFileName, //要讀寫的裝置
DWORD dwDesiredAccess, //訪問模式:0(不讀寫,如改變裝置配置) GENERIC_READ,GENERIC_WRITE(可異或)
DWORD dwShareMode, //共用模式:FILE_SHARE_DELETE FILE_SHARE_READ FILE_SHARE_WRITE
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全屬性 裡面可設定核心對象的繼承性
DWORD dwCreationDistribution, //建立方式 CREATE_NEW CREATE_ALWAYS OPEN_EXISTING OPEN_ALWAYS TRUNCATE_EXISTING
DWORD dwFlagsAndAttributes, //檔案屬性和標誌
HANDLE hTemplateFile //檔案屬性模板,如指定了這個參數,則忽略上個參數中的檔案屬性
);
/*
參數 dwFlagsAndAttributes
1)檔案屬性:
(FILE_ATTRIBUTE_)ARCHIVE COMPRESSED HIDDEN NORMAL OFFLINE READONLY SYSTEM TEMPORARY
2)標誌:
(FILE_FLAG_)WRITE_THROUGH OVERLAPPED NO_BUFFERING RANDOM_ACCESS SEQUENTIAL_SCAN DELETE_ON_CLOSE BACKUP_SEMANTICS POSIX_SEMANTICS
*/這個CreateFile函數返回了一個檔案核心物件控點,這裡說的檔案不是狹義上的磁碟檔案,也包括一些裝置,比如串口,並口,郵件槽,具名管道和匿名管道,另外,有些裝置控制代碼不是通過CreateFile建立的,下面是一個建立裝置核心對象的函數的說明表。
裝置 建立裝置核心對象的函數
檔案 CreateFile(pszName為路徑名或UNC路徑名)。
邏輯磁碟機 CreateFile(pszName為\\.\x:)。x是盤符,開啟磁碟機可以格式化或者檢測該磁碟機的大小。
物理磁碟機 CreateFile(pszName為\\.\PHYSICALDRIVEx)。x是物理磁碟機序號,比如PHYSICALDRIVE0
串口 CreateFile(pszName為"COMx")
並口 CreateFIle(pszName為"LPTx")
郵件槽伺服器 CreateMailslot(pszName為\\.\mailslot\mailslotname)
郵件槽用戶端 CreateFile(pszName為\\servername\mailslot\mailslotname)
具名管道伺服器 CreateNamedPipe(pszName為\\.\pipe\pipename)
具名管道用戶端 CreateFile(pszName為\\servername\pipe\pipename)
匿名管道 CreatePipe
通訊端 Socket,accept或AcceptEx
控制台 CreateConsoleScreenBuffer或GetStdHandle
以上這些函數,會建立一個I/O核心對象,並取得該對象的控制代碼值,然後我們可以調用與具體裝置相關的函數,並傳入得到的裝置核心物件控點,來和裝置進行通訊。
和其他核心對象一樣,可以調用函數CloseHandle來關閉CreateFile建立的I/O核心對象。
BOOL CloseHandle(HANDLE hObject);
但是如果裝置是通訊端,就必須調用closesocket。
int closesocket(SOCKET s);如果有一個裝置控制代碼,可以通過GetFileType來查處具體的裝置類型。DWORD GetFileType(HANDLE hDevice);
傳回值為:
| 值 |
描述 |
| FILE_TYPE_UNKNOWN |
未知類型 |
| FILE_TYPE_DISK |
磁碟檔案 |
| FILE_TYPE_CHAR |
字元檔案,一般是並口裝置或者控制台裝置 |
| FIEL_TYPE_PIPE |
指定的檔案時一個具名管道或匿名管道 |