Windows內核原理-同步IO與異步IO
背景
在前段時間檢查異常連接導致的內存泄漏排查的過程中,主要涉及到了windows異步I/O相關的知識,看了許多包括重疊I/O、完成端口、IRP、設備驅動程序等Windows下I/O相關的知識,雖然學習到了很多東西,但是仍然需要自頂而下的將所有知識進行梳理。
目的
本片文章主要講解同步I/O與異步I/O相關知識,希望通過編寫本篇文章為起點,對windows內核原理知識進行學習與梳理。發現並彌補遺漏的知識點並加以學習。同時通過理解windows內核原理,設計出更好、更合理的應用程序。
I/O
I/O即輸入輸出。在現在操作系統,輸入輸出是計算機完整功能必不可少的一部分。處理器負責各種計算任務,然后通過各種輸入輸出設備與外界進行交互。常見的輸入輸出設備包括鍵盤、鼠標、顯示器、硬盤、網絡適配器接口等。有了硬件設備,在軟件層面上,使得操作系統通過以一致的方式與設備驅動交互從而的操控硬件設備。而應用程序通過統一的接口與系統內核進行交互。
Windows從一開始就設計了可擴展的I/O接口。在應用層通過統一的Win32 API
,將I/O請求分配給正確的設備驅動程序。設備驅動程序調用設備控制器來操控硬件。而內核通過硬件抽象層與硬件進行交互。硬件抽象層提供了供內核和驅動調用的例程。
例程就是系統提供的API或服務。
在Windows下分為內核模式和用戶模式。應用程序運行在用戶模式下,操作系統和驅動程序運行在內核模式下。應用程序通過調用Win32 API
與Windows內核交互。
Windows內核則通過設備驅動程序與設備控制器進行通訊,而設備控制器則直接操控硬件設備。
設備驅動程序分為即插即用驅動程序、內核擴展驅動程序和文件系統驅動程序。其中文件系統驅動程序用於接收I/O請求,然后將請求轉換為真正的存儲設備或網絡設備的I/O請求。
設備控制器可以通過內存映射I/O的方式將設備的內存與主存映射,通過內存映射I/O后,處理器訪問的就不是主存而是設備控制器的寄存器內存。但是這種方式的訪問效率並不高,不適合大數據量I/O讀寫。通常硬盤和網絡驅動器采用直接訪問內存(DMA)的方式進行大量數據的I/O操作。DMA需要硬件支持,硬件會有DMA控制器,在硬件執行I/O操作的時候,不會占用CPU的指令周期,DMA控制器會和設備進行I/O操作。當數據傳輸完成后,DMA則會通知處理器I/O操作完成。
同步I/O
當我們要把文件從硬盤讀取到內存時,硬盤的讀取速度是遠小於內存的寫入速度的。因此當我們使用一個線程從硬盤讀取文件到內存中時。通常需要等待硬盤將數據從硬盤讀取到內存中,此時線程將被阻塞,但是不會消耗指令周期。當讀取完畢時,線程繼續執行后續操作。
雖然DMA執行的時候當前線程被阻塞,此時處理器可以獲取另一個線程內核執行其他操作,由於線程是非常昂貴的資源,因此使用同步I/O的方式若需要並發執行時,需要大量的創建線程資源,這就產生了大量的線程上下文切換。
在大多數x86和x64的多處理器,線程上下文切換時間間隔大約為15ms。
CPU每過大約15ms將CPU寄存器當前的線程上下文存回到該線程的上下文,然后該線程不在運行。然后系統檢查剩下的可調度線程內核對象,選擇一個線程的內核對象,將其上下文載入導CPU寄存器中。
關於Windows線程相關內容可以查閱《Windows via C/C++ 第五版》的第七章
異步I/O
前面提到了當硬件進行I/O傳輸時,實際上通常使用DMA技術執行I/O操作,不會占用CPU的指令周期。因此只要操作系統支持異步I/O,則可以極大的提升系統性能,最大程度的降低線程數量,減少線程上下文切換產生的性能損失。
在Windows下的異步I/O我們也可以稱之為重疊(overlapped)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完成通知
在Windows中一共支持四種接收完成通知的方式。分別為觸發設備內核對象、觸發時間內核對象、可提醒I/O以及I/O完成端口。
觸發設備內核
當設備驅動加載時會創建一個設備驅動對象,設備驅動程序還會為設備創建對應的設備對象。設備對象代表的是每一個物理設備或邏輯設備。設備對象描述了一個特定設備的狀態信息,包括I/O請求的狀態。在通過異步I/O將I/O請求添加到隊列之前,會將設備內核對象設置為未觸發,此時就可以使用該設備內核對象進行同步操作,當I/O請求完成后則會將設備內核對象設置為觸發狀態。使用設備內核對象進行線程同步時,無法區分當前完成通知的I/O是讀操作還是寫操作,因此無論是讀還是寫都會將其狀態設置為觸發狀態。
事件內核對象
通過設備內核對象進行I/O通知由於無法區分讀寫操作,因此並沒有什么用。通過事件內核對象我們可以將讀寫事件分離。在調用讀寫操作的時候會返回對應的讀寫事件內核對象。這樣我們就可以等待對應的事件內核對象知道是什么I/O操作完成。我們可以通過等待多個事件內核對象,但是一次性最多只能等待64個事件內核對象,即一個線程最多只能創建64個事件內核對象進行等待。若需要監控上萬個連接,則需要創建上百個線程進行監控。
可提醒I/O
在系統創建線程的時候會創建一個與線程相關的隊列,該隊列被稱為異步調用(APC)隊列,當發出一個I/O請求時,我們可以告訴設備驅動程序在調用線程的APC隊列中添加一項完成函數,在I/O完成通知時調用完成函數進行回調。I/O完成通知最大的問題是,請求時哪個線程調用的,必須由哪個線程回調。它不支持負載均衡機制。
完成端口
I/O完成端口的設計理論依據是並發編程的線程數必須有一個上限,即最佳並發線程數為CPU的邏輯線程數。I/O完成端口充分的發揮了並發編程的優勢的同時又避免了線程上下文切換帶來的性能損失。
完成端口可能是最復雜的內核對現象,但是它又是Windows下性能最佳的I/O通知方式。
首先我們需要創建一個I/O完成端口,創建完成端口的時候可以指定線程數量。通過將設備與I/O完成端口進行關聯。此使我們發出的I/O請求時,系統內核返回IO_PENDDING
狀態,然后線程就可以繼續處理其他事情。而DMA繼續執行I/O操作,將數據從設備讀取到設備控制器的緩沖區中,並對其進行必要的校驗后,將數據通過系統總線傳輸到內存中。當數據傳輸完成后,DMA發出中斷指令通知數據傳輸完畢,系統則會通過前面創建的I/O線程將I/O完成請求加入到I/O完成隊列中。
然后我們通過調用Win32 API
就可以獲取到對應的設備I/O完成請求通知,通知會將I/O完成請求從完成隊列移除。
總結
- 同步I/O會阻塞線程,想要提高執行速度必須增加線程,但是會由於線程上下文切換造成性能損失。
- Windows下大約每15ms會進行一次線程調度。減少windows線程能降低內存占用(默認線程棧大小為1M),降低線程上下文切換造成的性能損失。
- Windows支持原生的異步I/O。異步I/O也可以稱為重疊I/O。使用異步I/O時線程不會阻塞,系統底層將每個I/O請求生成I/O請求包(IRP)加入到設備驅動程序的請求隊列中,然后直接返回
IO_PENDDING
狀態表示請求受理成功,當底層設備完成了真實的I/O請求后會通過中斷控制器通過中斷操作通知CPU,CPU會調度一個線程通知上層設備驅動程序,將完成通知加入到完成隊列中。此時上層應用即可獲取到完成通知。 - 完成端口是windows下性能最佳的完成通知方式。它最大程度的減少線程上下文切換。
- 使用異步I/O和完成端口實現高性能I/O操作的主要原因有三點。一是減少I/O上下文切換;二是異步不阻塞線程,預先提供一個socket用於連接,而不是接受到時再創建socket(socket創建也是比較耗資源的);三是避免了內存復制。
- 如何減少線程,如何避免內存復制,如何提高線程利用率,避免線程阻塞。以上幾點是所有高性能框架或高性能應用程序必備的條件。
參考文檔
- cpu內存訪問速度,磁盤和網絡速度
- 手把手教你玩轉SOCKET模型:完成端口(Completion Port)詳解
- Reactor與Proactor的概念
- 如何深刻理解reactor和proactor?
- I/O Completion Ports
- 《Windows via C/C++ 第五版》
- 《Windows內核原理與實現》
- WaitForMultipleObjects用法詳解,一看就懂
微信掃一掃二維碼關注訂閱號傑哥技術分享
本文地址:https://www.cnblogs.com/Jack-Blog/p/11385686.html
作者博客:傑哥很忙
歡迎轉載,請在明顯位置給出出處及鏈接