異步磁碟 I/O 在 Windows 上顯示為同步

本文可協助您解決 I/O 的預設行為是同步,但顯示為異步的問題。

原始產品版本: 窗戶
原始 KB 編號: 156932

摘要

Microsoft Windows 上的檔案 I/O 可以是同步或異步的。 I/O 的預設行為是同步的,其中會呼叫 I/O 函式,並在 I/O 完成時傳回 。 異步 I/O 可讓 I/O 函式立即將執行傳回給呼叫端,但在未來某個時間之前不會假設 I/O 完成。 操作系統會在 I/O 完成時通知呼叫端。 相反地,呼叫端可以使用操作系統的服務來判斷未完成 I/O 作業的狀態。

異步 I/O 的優點是呼叫端有時間在 I/O 作業完成時執行其他工作或發出更多要求。 「重疊 I/O」一詞經常用於異步 I/O,而非重疊 I/O 一詞則用於同步 I/O。 本文使用 I/O 作業的異步和同步詞彙。 本文假設讀者已熟悉檔案 I/O 函式,例如 CreateFile、、ReadFileWriteFile

異步 I/O 作業的運作方式通常與同步 I/O 一樣。 本文在後續章節中討論的某些條件,可讓 I/O 作業以同步方式完成。 呼叫端沒有背景工作的時間,因為 I/O 函式在 I/O 完成之前不會傳回。

有數個函式與同步和異步 I/O 相關。 本文使用 ReadFileWriteFile 作為範例。 不錯的替代方法是 ReadFileExWriteFileEx。 雖然本文僅特別討論磁碟 I/O,但許多原則都可以套用至其他類型的 I/O,例如序列 I/O 或網路 I/O。

設定異步 I/O

FILE_FLAG_OVERLAPPED 啟檔案時,必須在 中 CreateFile 指定 旗標。 此旗標可讓您以異步方式完成檔案上的 I/O 作業。 範例如下:

HANDLE hFile;

hFile = CreateFile(szFileName,
                      GENERIC_READ,
                      0,
                      NULL,
                      OPEN_EXISTING,
                      FILE_FLAG_NORMAL | FILE_FLAG_OVERLAPPED,
                      NULL);

if (hFile == INVALID_HANDLE_VALUE)
      ErrorOpeningFile();

當您撰寫異步 I/O 的程式代碼時請小心,因為系統會保留在需要時讓作業同步的許可權。 因此,最好是撰寫程式來正確處理可能以同步或異步方式完成的 I/O 作業。 範例程式代碼示範此考慮。

程式在等候異步操作完成時可以執行許多動作,例如佇列其他作業,或執行背景工作。 例如,下列程式代碼會正確處理讀取作業的重疊和非重疊完成。 它所做的只是等待未完成的 I/O 完成:

if (!ReadFile(hFile,
               pDataBuf,
               dwSizeOfBuffer,
               &NumberOfBytesRead,
               &osReadOperation )
{
   if (GetLastError() != ERROR_IO_PENDING)
   {
      // Some other error occurred while reading the file.
      ErrorReadingFile();
      ExitProcess(0);
   }
   else
      // Operation has been queued and
      // will complete in the future.
      fOverlapped = TRUE;
}
else
   // Operation has completed immediately.
   fOverlapped = FALSE;

if (fOverlapped)
{
   // Wait for the operation to complete before continuing.
   // You could do some background work if you wanted to.
   if (GetOverlappedResult( hFile,
                           &osReadOperation,
                           &NumberOfBytesTransferred,
                           TRUE))
      ReadHasCompleted(NumberOfBytesTransferred);
   else
      // Operation has completed, but it failed.
      ErrorReadingFile();
}
else
   ReadHasCompleted(NumberOfBytesRead);

注意事項

&NumberOfBytesRead 傳入 ReadFile 的不同於 &NumberOfBytesTransferred 傳遞至 GetOverlappedResult。 如果已將作業設為異步,則 GetOverlappedResult 會用來判斷作業完成之後,在作業中傳輸的實際位元組數目。 傳遞至 ReadFile&NumberOfBytesRead 沒有意義。

另一方面,如果作業立即完成,則 &NumberOfBytesRead 傳入 ReadFile 對讀取的位元元組數而言是有效的。 在此情況下,請忽略傳遞至ReadFileOVERLAPPED結構;請勿將它與 GetOverlappedResultWaitForSingleObject搭配使用。

異步操作的另一 OVERLAPPED 個注意事項是,在結構暫止的作業完成之前,您不得使用 結構。 換句話說,如果您有三個未完成的 I/O 作業,就必須使用三個 OVERLAPPED 結構。 如果您重複使用 OVERLAPPED 結構,I/O 作業會收到無法預期的結果,而且您可能會遇到數據損毀。 此外,您必須正確初始化它,讓任何剩餘的數據不會影響新的作業,才能第一 OVERLAPPED 次使用結構,或在稍早的作業完成之後重複使用它。

相同的限制類型適用於作業中使用的數據緩衝區。 數據緩衝區必須等到其對應的 I/O 作業完成後才能讀取或寫入;讀取或寫入緩衝區可能會造成錯誤和數據損毀。

異步 I/O 似乎仍為同步

不過,如果您遵循本文稍早的指示,所有 I/O 作業通常仍會以發出的順序同步完成,而且沒有任何ReadFile作業傳回 ERROR_IO_PENDINGFALSEGetLastError(),這表示您沒有任何時間進行任何背景工作。 為什麼會發生這種情況?

即使您已針對異步操作撰寫程式代碼,I/O 作業仍會以同步方式完成的原因有很多。

壓縮

異步操作的其中一個阻礙是新技術文件系統 (NTFS) 壓縮。 文件系統驅動程式不會以異步方式存取壓縮的檔案;相反地,所有作業都是同步的。 此阻礙不適用於使用類似 COMPRESS 或 PKZIP 的公用程式壓縮的檔案。

NTFS 加密

類似於壓縮,檔案加密會導致系統驅動程式將異步 I/O 轉換為同步。 如果檔案已解密,I/O 要求將會是異步的。

擴充檔案

I/O 作業同步完成的另一個原因是作業本身。 在 Windows 上,擴充其長度之檔案的任何寫入作業都將是同步的。

注意事項

應用程式可以使用 SetFileValidData 函式變更檔案的有效數據長度,然後發出 WriteFile,讓先前所述的寫入作業異步。

使用 SetFileValidData Windows XP 和更新版本) 上可用的 (,應用程式可以有效率地擴充檔案,而不會對零填滿檔案造成效能降低。

由於 NTFS 檔系統不會將數據填入至 VDL) 所定義 SetFileValidData的有效數據長度 (,因此此函式具有安全性影響,其中檔案可能會被指派為先前由其他檔案所佔用的叢集。 因此, SetFileValidData 要求呼叫端預設已啟用新的 SeManageVolumePrivilege (,這隻會指派給系統管理員) 。 Microsoft 建議獨立軟體廠商 (ISV) 仔細考慮使用這類功能的影響。

快取

大部分的 I/O 驅動程式 (磁碟、通訊和其他) 具有特殊案例程式代碼,如果可以立即完成 I/O 要求,作業將會完成,且 ReadFileWriteFile 函式會傳回 TRUE。 在所有方面,這些類型的作業看起來都是同步的。 對於磁碟裝置,一般而言,在記憶體中快取數據時,可以立即完成 I/O 要求。

數據不在快取中

不過,如果數據不在快取中,快取配置可以對您運作。 Windows 快取是使用檔案對應在內部實作。 Windows 中的記憶體管理員未提供異步頁面錯誤機制來管理快取管理員所使用的檔案對應。 快取管理員可以確認要求的頁面是否在記憶體中,因此如果您發出異步快取讀取,而且頁面不在記憶體中,文件系統驅動程式會假設您不希望線程遭到封鎖,而且要求將由有限的背景工作線程集區處理。 在您的呼叫之後,控制權會傳回給您的 ReadFile 程式,且讀取仍擱置中。

這適用於少數要求,但由於背景工作線程集區有限 (目前在16 MB系統) 上有三個,因此在特定時間仍然只會有少數要求排入磁碟驅動程式佇列。 如果您針對不在快取中的數據發出許多 I/O 作業,快取管理員和記憶體管理員就會飽和,而且您的要求會同步提出。

快取管理員的行為也會根據您是循序或隨機存取檔案而受到影響。 依序存取檔案時,快取的優點最為常見。 FILE_FLAG_SEQUENTIAL_SCAN呼叫中的 CreateFile 旗標會針對這種類型的存取優化快取。 不過,如果您以隨機方式存取檔案,請使用 FILE_FLAG_RANDOM_ACCESS 中的旗標 CreateFile 指示快取管理員優化其隨機存取的行為。

請勿使用快取

FILE_FLAG_NO_BUFFERING 標對於異步操作的文件系統行為影響最大。 這是保證 I/O 要求是異步的最佳方式。 它會指示文件系統完全不要使用任何快取機制。

注意事項

使用此旗標有一些限制,與數據緩衝區對齊方式和裝置的扇區大小有關。 如需詳細資訊,請參閱 CreateFile 函式文件中有關正確使用此旗標的函式參考。

真實世界測試結果

以下是來自範例程式代碼的一些測試結果。 數位的大小在這裡並不重要,而且會因計算機而異,但相較於彼此的數字關聯性會影響旗標對效能的一般影響。

您可能會看到類似下列其中一項的結果:

  • 測試 1

    Asynchronous, unbuffered I/O:  asynchio /f*.dat /n
    Operations completed out of the order in which they were requested.
       500 requests queued in 0.224264 second.
       500 requests completed in 4.982481 seconds.
    

    這項測試示範先前提到的程式會快速發出 500 個 I/O 要求,而且有更多時間可以執行其他工作或發出更多要求。

  • 測試 2

    Synchronous, unbuffered I/O: asynchio /f*.dat /s /n
        Operations completed in the order issued.
        500 requests queued and completed in 4.495806 seconds.
    

    這項測試示範此程式花費 4.495880 秒呼叫 ReadFile 來完成其作業,但測試 1 只花費 0.224264 秒來發出相同的要求。 在測試 2 中,沒有額外的時間讓程式執行任何背景工作。

  • 測試 3

    Asynchronous, buffered I/O: asynchio /f*.dat
        Operations completed in the order issued.
        500 requests issued and completed in 0.251670 second.
    

    這項測試會示範快取的同步本質。 所有讀取都是在 0.251670 秒發出並完成。 換句話說,異步要求是以同步方式完成。 這項測試也會示範當數據位於快取中時,快取管理員的高效能。

  • 測試 4

    Synchronous, buffered I/O: asynchio /f*.dat /s
        Operations completed in the order issued.
        500 requests and completed in 0.217011 seconds.
    

    此測試會示範與測試 3 中相同的結果。 快取中的同步讀取完成速度比從快取的異步讀取快一點。 這項測試也會示範當數據位於快取中時,快取管理員的高效能。

總結

您可以決定哪一種方法是最佳方法,因為它全都取決於程序執行的類型、大小和作業數目。

未指定任何特殊旗標的 CreateFile 預設檔案存取是同步和快取的作業。

注意事項

您會在此模式中取得一些自動異步行為,因為檔系統驅動程式會執行預測性異步預先讀取和異步延遲寫入修改過的數據。 雖然此行為不會讓應用程式的 I/O 成為異步,但這是大部分簡單應用程式的理想案例。

另一方面,如果您的應用程式並不簡單,您可能必須執行一些分析和效能監視來判斷最佳方法,類似於本文稍早所述的測試。 分析 或 WriteFile 函式中ReadFile所花費的時間,然後將這段時間與實際 I/O 作業完成所需的時間進行比較很有用。 如果大部分的時間都花在實際發出 I/O 上,則您的 I/O 會以同步方式完成。 不過,相較於 I/O 作業完成所需的時間,如果發出 I/O 要求所花費的時間相對較小,則會以異步方式處理您的作業。 本文稍早所述的範例程式代碼會使用 QueryPerformanceCounter 函式來執行自己的內部分析。

效能監視有助於判斷程式使用磁碟和快取的效率。 追蹤 Cache 物件的任何性能計數器會指出快取管理員的效能。 追蹤實體磁碟或邏輯磁碟物件的性能計數器會指出磁碟系統的效能。

有數個公用程式有助於效能監視。 PerfMonDiskPerf 特別有用。 若要讓系統收集磁碟系統效能的數據,您必須先發出 DiskPerf 命令。 發出命令之後,您必須重新啟動系統以啟動資料收集。

參考資料

同步和異步 I/O