五種 I/O 模型
先花費點時間了解這幾種 I/O 模型,有助于后面的理解,
阻塞 I/O 與非阻塞 I/O
阻塞和非阻塞的概念能應用于所有的檔案描述符,而不僅僅是 socket,我們稱阻塞的檔案描述符為阻塞 I/O,稱非阻塞的檔案描述符為非阻塞 I/O,
socket 在創建的時候默認是阻塞的,我們可以給 socket 系統呼叫的第 2 個引數傳遞 SOCK_NONBLOCK 標志,或者通過 fcntl 系統呼叫的 F_SETFL 命令將其設定為非阻塞的,
-
針對阻塞 I/O 執行的系統呼叫可能因為無法立即完成而被作業系統掛起,直到等待的事件發生為止,可能被阻塞的系統呼叫為 accept、send、recv 和 connect;
-
針對非阻塞 I/O 執行的系統呼叫則總是立即回傳,而不管事件是否已經發生,如果事件沒有立即發生,這些系統呼叫就回傳
-1,和出錯的情況一樣,此時我們必須根據errno來區分這兩種情況,-
對 accept、send、recv 而言,事件未發生時
errno通常被設定為EAGAIN(意為“再來一次”)或者EWOUDBLOCK(意為“期望阻塞”); -
對 connect 而言,
errno則被設定成EINPROGRESS(意為“在處理中”),
-
很顯然,只有在事件已經發生的情況下操作非阻塞 I/O(讀、寫等),才能提高程式的效率,因此,非阻塞 I/O 通常要和其他 I/O 通知機制一起使用,比如 I/O 復用和 SIGIO 信號,
筆者認為,我們使用非阻塞 I/O 的最佳情況是:【當我們進行系統呼叫的時候,它所需要的事件已經發生了】,這樣系統呼叫就不會被阻塞,直接進行處理,比如 accept 函式,I/O 復用的好處就是當我們呼叫 accept 函式的時候,已經有客戶端在請求連接,這樣直接呼叫 accept,提高運行效率,
I/O 復用
I/O 復用是一種 I/O 通知機制,而且是最常用的通知機制,
I/O 復用是指應用程式通過 I/O 復用函式(select、poll、epoll_wait)向內核注冊一組事件,內核通過 I/O 復用函式把其中就緒的事件通知給應用程式,
需要注意的是 I/O 復用函式本身是阻塞的,它們能提高程式效率的原因在于它們具有同時監聽多個 I/O 事件的能力,
信號驅動 I/O
為一個目標檔案描述符指定宿主行程,那么被指定的宿主行程將捕獲到 SIGIO 信號,這樣,當檔案描述符上有事件發生時,SIGIO 信號的信號處理函式將被觸發,我們也就可以在該信號處理函式中對目標檔案描述符執行非阻塞 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 是否是阻塞的,因為真正的讀寫操作已經由內核接管,
兩種高效的事件處理模式
Reactor 模式
Reactor 模式要求主執行緒(I/O 處理單元)只負責監聽檔案描述符上是否有事件發生,有的話立即通知作業執行緒(邏輯單元),讀寫資料、接受新的連接及處理客戶請求均在作業執行緒中完成,通常由同步I/O實作,
作業流程:
-
主執行緒向 epoll 內核事件表中注冊 socket 上的讀就緒事件;
告訴 socket 的對方(客戶端):我這邊準備好讀資料啦,你可以發資料啦!
-
主執行緒呼叫
epoll_wait()等待 socket 上有資料可讀; -
當 socket 上有資料可讀時,
epoll_wait()通知主執行緒,主執行緒將 socket 可讀事件插入請求佇列;主執行緒:干活啦干活啦,這有個活,你們看看誰干了它!
-
睡眠在請求佇列上的某個作業執行緒被喚醒,它從 socket 上讀取資料,并處理客戶請求,然后往 epoll 內核事件表中注冊該 socket 上的寫就緒事件;
某一個苦工(作業執行緒)干完活之后告訴 socket 的對方:我準備好寫了!
-
主執行緒呼叫
epoll_wait()等待 socket 可寫; -
當 socket 可寫時,
epoll_wait()通知主執行緒,主執行緒將 socket 可寫事件放入請求佇列;主執行緒:又來活啦,你們看看誰來干!
-
睡眠在請求佇列上的某個作業執行緒被喚醒,它往 socket 上寫入服務器處理客戶請求的結果,
某個苦工又被喚醒來干活,寫入處理結果,

Reacto 模式類似于老板(主執行緒)與苦工(作業執行緒)之間的關系,有活了老板就派給苦工來干(哭了.....莫名被 cue 到)
Proactor 模式
Proactor 模式將所有的 I/O 操作都交給主執行緒和內核來處理,作業執行緒僅僅負責業務邏輯,通常由異步 I/O (aio_read()和aid_write())實作,
作業流程
-
主執行緒呼叫
aio_read()向內核注冊 socket 上的讀完成事件,并告訴內核 用戶讀緩沖區的位置,以及讀操作完成時如何通知應用程式(這里以信號為例);主執行緒告訴內核:用戶這邊準備好識訓了,你直接把貨卸在這!你卸完了直接給用戶打電話!
-
主執行緒繼續處理其他邏輯;
溜了溜了,我先干點別的...
-
當 socket 上的資料被讀入用戶緩沖區后,內核將向應用程式發送一個信號,以通知應用程式資料已經可用;
苦逼的內核干完活,給用戶打了電話通知他活干完了...
-
應用程式預先定義好的信號處理函式選擇一個作業執行緒來處理客戶請求,作業執行緒處理完客戶請求之后,呼叫
aio_write()函式向內核注冊 socket 上的寫完成事件,并告訴內核 用戶寫緩沖區的位置,以及寫操作完成時如何通知應用程式(仍然以信號為例);用戶這邊有很多苦工(作業執行緒),預先指定好了一個苦工來對接這批貨物,這個苦工加工完所有的貨物,告訴內核我這邊貨加工好了,放在老地方了,你直接過來拿!
-
主執行緒繼續處理其他邏輯;
沒我什么事,繼續摸魚(bushi
-
當用戶緩沖區的資料被寫入 socket 之后,內核將向應用程式發送一個信號,以通知應用程式資料已經發送完畢;
然后,內核就來拉貨了,拉完貨之后又給用戶打電話:貨我全拉走了!
-
應用程式預先定義好的信號處理函式選擇一個作業執行緒來做善后處理,比如決定是否關閉 socket,
苦工收到訊息,來看看是否需要清理場地...

可以看到,Proactor 模式相當于找了個快遞員(內核)來幫助運輸貨物(讀寫資料),作業執行緒只需要處理業務邏輯,主執行緒只需要監聽連接事件,讀寫事件由內核和作業執行緒直接通信,
模擬 Proactor 模式
由于 Proactor 模式需要異步 I/O 來實作,這里提出使用同步 I/O 的方式模擬出 Proactor 模式的一種方法,其原理是:主執行緒執行資料讀寫操作,讀寫完成后,主執行緒向作業執行緒通知這一“完成事件”,那么從作業執行緒的角度來看,它們就直接獲得了資料讀寫的結果,接下來要做的只是對讀寫的結果進行邏輯處理,
作業流程
- 主執行緒往 epoll 內核事件表中注冊 socket 上的讀就緒事件;
- 主執行緒呼叫
epoll_wait()等待 socket 上有資料可讀; - 當 socket 上有資料可讀時,
epoll_wait()通知主執行緒,主執行緒從 socket 回圈讀取資料,直到沒有更多資料可讀,然后將讀到的資料封裝成一個請求物件并插入請求佇列; - 睡眠在請求佇列上的某個作業執行緒被喚醒,它獲得請求物件并處理客戶請求,然后往 epoll 內核事件表中注冊 socket 上的寫就緒事件;
- 主執行緒呼叫
epoll_wait()等待 socket 可寫; - 當 socket 可寫時,
epoll_wait()通知主執行緒,主執行緒往 socket 上寫入服務器處理客戶請求的結果,

可以看到,模擬 Proactor 模式其實就是主執行緒自己來充當快遞員(內核)的角色,所以在作業執行緒的角度來看與 Proactor 模式差不多,
本文參考自游雙大神的《Linux 高性能服務器編程》一書,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/469681.html
標籤:其他
上一篇:代碼塊的運行順序
下一篇:淺嘗Spring注解開發_簡單理解BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor、ApplicationListener
