一、IO是什麼
I/O(Input/Output),中文名為輸入/輸出,指的是一切操作程式或裝置與計算機之間發生的資料傳輸的過程。它分為IO裝置和IO介面兩個部分。
- IO裝置,就是指可以與計算機進行資料傳輸的硬體。最常見的I/O裝置有印表機、硬碟、鍵盤和滑鼠。從嚴格意義上來講,它們中有一些只能算是輸入裝置(比如說鍵盤和滑鼠);有一些只是輸出裝置(如印表機)。
- IO介面,就是是主機和外設之間的交接介面,透過介面可以實現主機和外設之間的資訊交換。
在計算機的世界裡,IO的本質就是計算機的核心(CPU和記憶體)與其它裝置之間資料轉移的過程。比如資料從磁碟讀入到記憶體,或記憶體的資料寫回到磁碟,都是IO操作。
二、IO如何進行互動?
IO有記憶體IO、網路IO和磁碟IO三種。通常,我們說的IO指的是後兩者。
使用者程序中的一個完整IO分為兩個階段:
- 使用者空間與核心空間互動
- 核心空間與裝置空間互動
三、什麼是緩衝區?
應用層的 IO 操作基本都是依賴作業系統提供的 read 和 write 兩大系統呼叫實現。但由於計算機外部裝置(磁碟、網路)與記憶體、CPU 的讀寫速度相差過大,若直接讀寫涉及作業系統中斷,因此為了減少 OS 頻繁中斷導致的效能損耗和提高吞吐量,引入了緩衝區的概念。根據記憶體空間的不同,又可分為核心緩衝區和程序緩衝區。作業系統會對核心緩衝區進行監控,等待緩衝區達到一定數量的時候,再進行 IO 裝置的中斷處理,集中執行物理裝置的實際 IO 操作,透過這種機制來提升系統的效能。至於具體什麼時候執行系統中斷(包括讀中斷、寫中斷)則由作業系統的核心來決定,應用程式不需要關心。
四、什麼是同步/非同步?什麼是阻塞/非阻塞?
同步與非同步
- 同步,就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不返回。也就是必須一件一件事做,等前一件做完了才能做下一件事。(死等結果)
- 非同步,就是當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果,呼叫者不用等待這件事完成,可以繼續做其他的事情。實際處理這個呼叫的部件在完成後,透過狀態、通知和回撥來通知呼叫者。(回撥通知)
阻塞與非阻塞
- 阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起(執行緒進入非可執行狀態,在這個狀態下,CPU不會給執行緒分配時間片,即執行緒暫停執行)。函式只有在得到結果之後才會返回。
- 非阻塞呼叫是指在不能立刻得到結果之前,該函式不會阻塞當前執行緒,而會立刻返回。
同步和非同步的概念描述的是使用者執行緒與核心的互動方式。同步是指使用者執行緒發起IO請求後需要等待或者輪詢核心IO操作完成後才能繼續執行;而非同步是指使用者執行緒發起IO請求後仍繼續執行,當核心IO操作完成後會通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式。
阻塞和非阻塞的概念描述的是使用者執行緒呼叫核心IO操作的方式。阻塞是指IO操作需要徹底完成後才返回到使用者空間;而非阻塞是指IO操作被呼叫後立即返回給使用者一個狀態值,無需等到IO操作徹底完成
同步與非同步是 兩個物件之間的關係,而阻塞與非阻塞是一個物件的狀態。
五、Linux中的五種IO模型
阻塞IO模型(blocking I/O)
應用程式呼叫一個IO函式,導致應用程式阻塞,等待資料準備好。 如果資料沒有準備好,一直等待….資料準備好了,從核心複製到使用者空間,IO函式返回成功指示。
當呼叫recv()函式時,系統首先查是否有準備好的資料。如果資料沒有準備好,那麼系統就處於等待狀態。當資料準備好後,將資料從系統緩衝區複製到使用者空間,然後該函式返回。在套接應用程式中,當呼叫recv()函式時,未必使用者空間就已經存在資料,那麼此時recv()函式就會處於等待狀態。
非阻塞IO模型(nonblocking I/O)
我們把一個SOCKET介面設定為非阻塞就是告訴核心,當所請求的I/O操作無法完成時,不要將程序睡眠,而是返回一個錯誤。這樣我們的I/O操作函式將不斷的測試資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。在這個不斷測試的過程中,會大量的佔用CPU的時間。上述模型絕不被推薦。
把SOCKET設定為非阻塞模式,即通知系統核心:在呼叫Windows Sockets API時,不要讓執行緒睡眠,而應該讓函式立即返回。在返回時,該函式返回一個錯誤程式碼。如圖所示,一個非阻塞模式套接字多次呼叫recv()函式的過程。前三次呼叫recv()函式時,核心資料還沒有準備好。因此,該函式立即返回WSAEWOULDBLOCK錯誤程式碼。第四次呼叫recv()函式時,資料已經準備好,被複制到應用程式的緩衝區中,recv()函式返回成功指示,應用程式開始處理資料。
IO多路複用模型(I/O multiplexing)
簡介:主要是select和epoll;對一個IO埠,兩次呼叫,兩次返回,比阻塞IO並沒有什麼優越性;關鍵是能實現同時對多個IO埠進行監聽;
I/O複用模型會用到select、poll、epoll函式,這幾個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式。
當使用者程序呼叫了select,那麼整個程序會被block;而同時,kernel會“監視”所有select負責的socket;當任何一個socket中的資料準備好了,select就會返回。這個時候,使用者程序再呼叫read操作,將資料從kernel複製到使用者程序。
這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因為這裡需要使用兩個系統呼叫(select和recvfrom),而blocking IO只呼叫了一個系統呼叫(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。)
在這種模型中,這時候並不是程序直接發起資源請求的系統呼叫去請求資源,程序不會被“全程阻塞”,程序是呼叫select或poll函式。程序不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll幫助使用者程序去輪詢那些IO操作是否完成。
不過你可以看到之前都只使用一個系統呼叫,在IO複用中反而是用了兩個系統呼叫,但是使用IO複用你就可以等待多個描述符也就是透過單程序單執行緒實現併發處理,同時還可以兼顧處理套接字描述符和其他描述符。
訊號驅動IO模型(signal blocking I/O)
允許Socket使用訊號驅動 I/O ,還要註冊一個 SIGIO 的處理函式,這時的系統呼叫將會立即返回。然後我們的程式可以繼續做其他的事情,當資料就緒時,程序收到系統傳送一個 SIGIO 訊號,可以在訊號處理函式中呼叫IO操作函式處理資料。
非同步IO模型(asynchronous I/O)
相對於同步IO,非同步IO不是順序執行。使用者程序進行aio_read系統呼叫之後,無論核心資料是否準備好,都會直接返回給使用者程序,然後使用者態程序可以去做別的事情。等到socket資料準備好了,核心直接複製資料給程序,然後從核心向程序傳送通知。IO的兩個階段,程序都是非阻塞的。
Linux提供了AIO庫函式實現非同步,但是用的很少。目前有很多開源的非同步IO庫,例如libevent、libev、libuv。
訊號驅動IO和非同步IO的區別
訊號驅動IO
1.當程序註冊訊號驅動I/O時,它會告訴核心,當某個檔案描述符(如套接字)準備好讀或寫時,透過傳送一個訊號(如SIGIO)通知程序。
2.程序接收到訊號後,會喚醒並執行一個訊號處理函式,此時程序需要重新呼叫read或write等系統呼叫來完成實際的資料讀寫操作。
3.訊號驅動I/O的關鍵點在於核心只負責通知程序可以開始I/O操作,而不會等到整個I/O操作完成。
非同步I/O
1.非同步I/O模型更進一步,當程序發起一個非同步I/O請求後(例如使用POSIX的aio_read或aio_write函式),程序可以立即返回而不被阻塞。
2.核心不僅負責在資料準備好時開始I/O操作,還會在I/O操作(包括資料從核心緩衝區複製到使用者空間)徹底完成後,透過回撥函式或其他形式通知程序。
3.程序在收到核心的通知時,就知道I/O操作已完成,無需再次呼叫系統呼叫來完成讀寫。
六、LInux IO模型總結如圖所示:
七、IO多路複用機制
select、poll、epoll都是IO多路複用的機制。IO多路複用就是透過一種機制,讓一個程序/執行緒可以監視多個描述符,一旦某個描述符就緒(一般是讀寫就緒),能夠通知應用程式進行相應的讀寫操作。
I/O多路複用在英文叫 I/O multiplexing,這裡面的 multiplexing 指的其實是在單個程序/執行緒透過記錄跟蹤每一個檔案描述符的狀態來同時管理多個I/O流。發明它的原因,是儘可能地提高伺服器的吞吐能力。
I/O複用雖然能同時監聽多個檔案描述符,當其本質上還是同步IO模型,因為需要在讀寫事件就緒後程式自己負責進行讀寫事件的處理,而這個讀寫過程是阻塞的。如果要實現併發,只能使用多程序/多執行緒等程式設計手段了。與多程序/多執行緒技術相比,I/O多路複用技術最大的優勢就是系統開銷小,系統不必建立大量程序/執行緒,也不必維護這些程序/執行緒,從而大大減少了系統的開銷。
select:使用 fd_set 結構體來存放被監聽的檔案描述符的,本質上是使用一個點陣圖結構來存放這些被監聽的檔案描述符的,因此select能夠監聽的檔案描述符數量是有限制的。同時,fd_set 沒有將檔案描述符和事件進行繫結,它僅僅是一個檔案描述符集合,因此,select需要提供3個fd_set型別的引數來分別傳入和傳出可讀、可寫及異常事件。一方面,使得select不能處理更多型別的事件,另一方面,由於核心對fd_set集合的線上修改,使得下次再呼叫select()函式前不得不重置這3個fd_set集合,這使得程式設計變成很麻煩,並且容易出錯。
poll:使用 struct pollfd結構體來存放被監聽的檔案描述符,它比select“聰明”的地方就在於它把檔案描述符和與其關聯的事件都定義在這個結構體中了,從而使得程式設計介面變得簡潔很多,同時核心每次修改的都是pollfd結構體的revents成員,而events成員保持不變,因此下次呼叫poll()函式時應用程式無須重置pollfd型別的事件集引數。
由於每次select 和 poll 呼叫都是返回整個使用者監聽的事件集合(其中包括就緒的和未就緒的),所以應用程式索引就緒檔案描述符的時間複雜度為O(n)。
epoll:採用與select 和 poll 完全不同的方式來管理使用者註冊的事件。它在核心中維護一個事件表,並提供了一個獨立的系統呼叫函式 epoll_ctl來控制往該核心事件表中新增、刪除、修改事件。這樣,每次呼叫epoll_wait()函式時,都是直接從核心事件表中取得使用者註冊的事件,而無須反覆從使用者空間將這些註冊事件讀入到核心區中,節省了複製的系統開銷。epoll_wait 系統呼叫中的 events 指標引數僅用來返回就緒的事件,這使得應用程式索引就緒檔案描述符的時間複雜度為O(1)。需要注意的是,epoll 和 poll一樣,也是將檔案描述符和與其關聯的事件是繫結在一起的,這樣做的好處是,程式設計介面變得簡潔,不像select那樣複雜。
參考:https://juejin.cn/post/7012061816394088484