IO模型、IO多路复用
IO多路復(fù)用
- 基礎(chǔ)概述
- 用戶空間和內(nèi)核空間
- PIO與DMA
- 緩存IO和直接IO
- 緩存IO
- 優(yōu)點(diǎn)
- 缺點(diǎn)
- 直接IO
- IO訪問方式
- 磁盤IO
- 網(wǎng)絡(luò)IO
- 磁盤IO和網(wǎng)絡(luò)IO對(duì)比
- Socket網(wǎng)絡(luò)編程
- 客戶端
- 服務(wù)端
- 同步IO和異步IO
- 阻塞IO和非阻塞IO
- IO設(shè)計(jì)模式之Reactor和Proactor
- 反應(yīng)器Reactor
- 概述
- 為什么使用Reactor模式
- Reactor模式結(jié)構(gòu)
- 業(yè)務(wù)流程及時(shí)序圖
- Proactor模式
- Proactor模式結(jié)構(gòu)
- 業(yè)務(wù)流程及時(shí)序圖
- Reactor和Proactor對(duì)比
- 主動(dòng)和被動(dòng)
- 實(shí)現(xiàn)
- 優(yōu)點(diǎn)
- 缺點(diǎn)
- 適用場景
- 漫談五種IO模型
- 高性能IO模型淺析
- IO模型舉例理解
- 例1
- 例2
- 同步阻塞IO
- 同步非阻塞IO
- IO多路復(fù)用
- 異步IO
- Redis的IO多路復(fù)用技術(shù)
- 為什么Redis中要使用I/O多路復(fù)用
- epoll實(shí)現(xiàn)機(jī)制
- redis epoll底層實(shí)現(xiàn)
基礎(chǔ)概述
用戶空間和內(nèi)核空間
- Linux 中有兩個(gè)詞:User space (用戶空間)和 Kernel space (內(nèi)核空間)
- 簡單說,Kernel space 是 Linux 內(nèi)核的運(yùn)行空間,User space 是用戶程序的運(yùn)行空間。為了安全起見,它們是隔離的,即使用戶的程序崩潰了,內(nèi)核也不受影響。
- 虛擬內(nèi)存被操作系統(tǒng)劃分為兩塊:內(nèi)核空間和用戶空間,內(nèi)核空間是內(nèi)核代碼運(yùn)行的地方,用戶空間是用戶程序代碼運(yùn)行的地方。當(dāng)進(jìn)程運(yùn)行在內(nèi)核空間時(shí)就處于內(nèi)核態(tài),當(dāng)進(jìn)程運(yùn)行在用戶空間時(shí)就處于用戶態(tài)。
- Kernel space 可以執(zhí)行任意命令,調(diào)用系統(tǒng)的一切資源;User space 只能執(zhí)行簡單的運(yùn)算,不能直接調(diào)用系統(tǒng)資源,必須通過系統(tǒng)接口(又稱 system call),才能向內(nèi)核發(fā)出指令。
- 通過系統(tǒng)接口,進(jìn)程可以從用戶空間切換到內(nèi)核空間。例如:
- 上面代碼中,第一行和第二行都是簡單的賦值運(yùn)算,在 User space 執(zhí)行。第三行需要寫入文件,就要切換到Kernel space,因?yàn)橛脩舨荒苤苯訉懳募?#xff0c;必須通過內(nèi)核安排。第四行又是賦值運(yùn)算,就切換回 User space。
- 查看 CPU 時(shí)間在 User space 與 Kernel Space 之間的分配情況,可以使用top命令。它的第三行輸出就是 CPU時(shí)間分配統(tǒng)計(jì)。
- 這一行有 8 項(xiàng)統(tǒng)計(jì)指標(biāo):其中,第一項(xiàng) 0.7 us(user 的縮寫)就是 CPU 消耗在 User space 的時(shí)間百分比,第二項(xiàng) 0.3 sy(system 的縮寫)是消耗在 Kernel space 的時(shí)間百分比。
- 其他 6 個(gè)指標(biāo)的含義:
- ni :niceness 的縮寫,CPU 消耗在 nice 進(jìn)程(低優(yōu)先級(jí))的時(shí)間百分比。
- id :idle 的縮寫,CPU 消耗在閑置進(jìn)程的時(shí)間百分比,這個(gè)值越低,表示 CPU 越忙。
- wa :wait 的縮寫,CPU 等待外部 I/O 的時(shí)間百分比,這段時(shí)間 CPU 不能干其他事,但是也沒有執(zhí)行運(yùn)算,這個(gè)值太高就說明外部設(shè)備有問題。
- hi :hardware interrupt 的縮寫,CPU 響應(yīng)硬件中斷請(qǐng)求的時(shí)間百分比。
- si :software interrupt 的縮寫,CPU 響應(yīng)軟件中斷請(qǐng)求的時(shí)間百分比。
- st :stole time 的縮寫,該項(xiàng)指標(biāo)只對(duì)虛擬機(jī)有效,表示分配給當(dāng)前虛擬機(jī)的 CPU 時(shí)間之中,被同一臺(tái)物理機(jī)上的其他虛擬機(jī)偷走的時(shí)間百分比。
PIO與DMA
- PIO:我們拿磁盤來說,很早以前,磁盤和內(nèi)存之間的數(shù)據(jù)傳輸是需要CPU控制的,也就是說如果我們讀取磁盤文件到內(nèi)存中,數(shù)據(jù)需要經(jīng)過 CPU 存儲(chǔ)轉(zhuǎn)發(fā),這種方式稱為 PIO。顯然這種方式非常不合理,需要占用大量的CPU時(shí)間來讀取文件,造成文件訪問時(shí)系統(tǒng)幾乎停止響應(yīng)。
- DMA:后來DMA(直接內(nèi)存訪問,Direct Memory Access)取代了PIO,它可以不經(jīng)過CPU而直接進(jìn)行磁盤和內(nèi)存(內(nèi)核空間)的數(shù)據(jù)交換。在DMA模式下,CPU只需要向DMA控制器下達(dá)指令,讓DMA控制器來處理數(shù)據(jù)在DMA模式下,CPU只需要向DMA控制器下達(dá)指令,讓DMA控制器來處理數(shù)據(jù)率,大大節(jié)省了系統(tǒng)資源,而它的傳輸速度與PIO的差異其實(shí)并不十分明顯,因?yàn)檫@主要取決于慢速設(shè)備的速度。
緩存IO和直接IO
- 緩存IO:數(shù)據(jù)從磁盤先通過DMA copy到內(nèi)核空間,再從內(nèi)核空間通過cpu copy到用戶空間。
- 直接IO:數(shù)據(jù)從磁盤通過DMA copy到用戶空間。
緩存IO
緩存I/O又被稱作標(biāo)準(zhǔn)I/O,大多數(shù)文件系統(tǒng)的默認(rèn)I/O操作都是緩存I/O。在Linux的緩存I/O機(jī)制中,數(shù)據(jù)先從磁盤復(fù)制到內(nèi)核空間的緩沖區(qū),然后從內(nèi)核空間緩沖區(qū)復(fù)制到應(yīng)用程序的地址空間。
- 讀操作:操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù),如果已經(jīng)緩存了,那么就直接從緩存中返回;否則從磁盤中讀取,然后緩存在操作系統(tǒng)的緩存中。
- 寫操作:將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中,這時(shí)對(duì)用戶程序來說寫操作就已經(jīng)完成,至于什么時(shí)候再寫到磁盤中由操作系統(tǒng)決定,除非顯示地調(diào)用了 sync 同步命令。【珍藏】linux 同步IO: sync、fsync與fdatasync
優(yōu)點(diǎn)
- 在一定程度上分離了內(nèi)核空間和用戶空間,保護(hù)系統(tǒng)本身的運(yùn)行安全。
- 可以減少讀盤的次數(shù),從而提高性能。
缺點(diǎn)
- 在緩存 I/O 機(jī)制中,DMA 方式可以將數(shù)據(jù)直接從磁盤讀到頁緩存中,或者將數(shù)據(jù)從頁緩存直接寫回到磁盤上,而不能直接在應(yīng)用程序地址空間和磁盤之間進(jìn)行數(shù)據(jù)傳輸,這樣,數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間(用戶空間)和緩存(內(nèi)核空間)之間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的CPU以及內(nèi)存開銷是非常大的。
直接IO
直接IO就是應(yīng)用程序直接訪問磁盤數(shù)據(jù),而不經(jīng)過內(nèi)核緩沖區(qū),也就是繞過內(nèi)核緩沖區(qū),自己管理I/O緩存區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制。
- 引入內(nèi)核緩沖區(qū)的目的在于提高磁盤文件的訪問性能,因?yàn)楫?dāng)進(jìn)程需要讀取磁盤文件時(shí),如果文件內(nèi)容已經(jīng)在內(nèi)核緩沖區(qū)中,那么就不需要再次訪問磁盤;而當(dāng)進(jìn)程需要向文件中寫入數(shù)據(jù)時(shí),實(shí)際上只是寫到了內(nèi)核緩沖區(qū)便告訴進(jìn)程已經(jīng)寫成功,而真正寫入磁盤是通過一定的策略進(jìn)行延遲的。
- 然而,對(duì)于一些較復(fù)雜的應(yīng)用,比如數(shù)據(jù)庫服務(wù)器,它們?yōu)榱顺浞痔岣咝阅?#xff0c;希望繞過內(nèi)核緩存區(qū),由自己在用戶態(tài)空間實(shí)現(xiàn)并管理I/O緩沖區(qū),包括緩存機(jī)制和寫延遲機(jī)制等,以支持獨(dú)特的查詢機(jī)制,比如數(shù)據(jù)庫可以根據(jù)更加合理的策略來提高查詢緩存命中率。另一方面,繞過內(nèi)核緩沖區(qū)也可以減少系統(tǒng)內(nèi)存的開銷,因?yàn)閮?nèi)核緩沖區(qū)本身就在使用系統(tǒng)內(nèi)存。
- 應(yīng)用程序直接訪問磁盤數(shù)據(jù),不經(jīng)過操作系統(tǒng)內(nèi)核數(shù)據(jù)緩沖區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制。這種方式通常是在對(duì)數(shù)據(jù)的緩存管理由應(yīng)用程序?qū)崿F(xiàn)的數(shù)據(jù)庫管理系統(tǒng)中。
- 直接I/O的缺點(diǎn)就是如果訪問的數(shù)據(jù)不在應(yīng)用程序緩存中,那么每次數(shù)據(jù)都會(huì)直接從磁盤進(jìn)行加載,這種直接加載會(huì)非常慢。通常直接I/O根異步I/O結(jié)合使用會(huì)得到較好的性能。
- Linux提供了對(duì)這種需求的支持,即在 open() 系統(tǒng)調(diào)用中增加參數(shù)選項(xiàng) O_DIRECT,用它打開的文件便可以繞過內(nèi)核緩沖區(qū)的直接訪問,這樣便有效避免了CPU和內(nèi)存的多余時(shí)間開銷。
- 順便提一下,與O_DIRECT類似的一個(gè)選項(xiàng)是O_SYNC,后者只對(duì)寫數(shù)據(jù)有效,它將寫入內(nèi)核緩沖區(qū)的數(shù)據(jù)立即寫入磁盤,將機(jī)器故障時(shí)數(shù)據(jù)的丟失減少到最小,但是它仍然要經(jīng)過內(nèi)核緩沖區(qū)。
IO訪問方式
磁盤IO
- 當(dāng)應(yīng)用程序調(diào)用read接口時(shí),操作系統(tǒng)檢查在內(nèi)核的高速緩存有沒有需要的數(shù)據(jù),如果已經(jīng)緩存了,那么就直接從緩存中返回,如果沒有,則從磁盤中讀取,然后緩存在操作系統(tǒng)的緩存中。
- 應(yīng)用程序調(diào)用write接口時(shí),將數(shù)據(jù)從用戶地址空間復(fù)制到內(nèi)核地址空間的緩存中,這時(shí)對(duì)用戶程序來說,寫操作已經(jīng)完成,至于什么時(shí)候再寫到磁盤中,由操作系統(tǒng)決定,除非顯示調(diào)用了sync同步命令。
網(wǎng)絡(luò)IO
- ① 操作系統(tǒng)將數(shù)據(jù)從磁盤復(fù)制到操作系統(tǒng)內(nèi)核的頁緩存中;② 應(yīng)用將數(shù)據(jù)從內(nèi)核緩存復(fù)制到應(yīng)用的緩存中;③應(yīng)用將數(shù)據(jù)寫回內(nèi)核的Socket緩存中;④操作系統(tǒng)將數(shù)據(jù)從Socket緩存區(qū)復(fù)制到網(wǎng)卡緩存,然后將其通過網(wǎng)絡(luò)發(fā)出。
- ① 當(dāng)調(diào)用read系統(tǒng)調(diào)用時(shí),通過DMA(Direct Memory Access)將數(shù)據(jù)copy到內(nèi)核模式;② 然后由CPU控制將內(nèi)核模式數(shù)據(jù)copy到用戶模式下的 buffer中;③ read調(diào)用完成后,write調(diào)用首先將用戶模式下 buffer中的數(shù)據(jù)copy到內(nèi)核模式下的socket buffer中;④最后通過DMA copy將內(nèi)核模式下的socket buffer中的數(shù)據(jù)copy到網(wǎng)卡設(shè)備中傳送。
- 從上面的過程可以看出,數(shù)據(jù)白白從內(nèi)核模式到用戶模式走了一圈,浪費(fèi)了兩次copy,而這兩次copy都是CPUcopy,即占用CPU資源。
磁盤IO和網(wǎng)絡(luò)IO對(duì)比
- 首先,磁盤IO主要的延時(shí)是由(以15000rpm硬盤為例):機(jī)械轉(zhuǎn)動(dòng)延時(shí)(機(jī)械磁盤的主要性能瓶頸,平均為2ms) + 尋址延時(shí)(2~3ms) + 塊傳輸延時(shí)(一般4k每塊,40m/s的傳輸速度,延時(shí)一般為0.1ms) 決定。(平均為5ms)。
- 而網(wǎng)絡(luò)IO主要延時(shí)是由:服務(wù)器響應(yīng)延時(shí) + 帶寬限制 + 網(wǎng)絡(luò)延時(shí) + 跳轉(zhuǎn)路由延時(shí) + 本地接收延時(shí) 決定。(一般為幾十到幾千毫秒,受環(huán)境干擾極大)。
- 所以兩者一般來說網(wǎng)絡(luò)IO延時(shí)要大于磁盤IO的延時(shí)。
Socket網(wǎng)絡(luò)編程
客戶端
public class SocketClient {public static void main(String args[]) throws Exception {// 要連接的服務(wù)端IP地址和端口 String host = "127.0.0.1"; int port = 55533;// 與服務(wù)端建立連接Socket socket = new Socket(host, port);// 建立連接后獲得輸出流OutputStream outputStream = socket.getOutputStream(); String message="你好 yiwangzhibujian";socket.getOutputStream().write(message.getBytes("UTF-8")); outputStream.close();socket.close();} }服務(wù)端
public class SocketServer {public static void main(String[] args) throws Exception {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port);// server將一直等待連接的到來 System.out.println("server將一直等待連接的到來"); Socket socket = server.accept();// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {//注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len,"UTF-8"));}System.out.println("get message from client: " + sb);inputStream.close();socket.close();server.close();} } public class SocketServer {public static void main(String args[]) throws Exception {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port); // server將一直等待連接的到來 System.out.println("server將一直等待連接的到來");//如果使用多線程,那就需要線程池,防止并發(fā)過高時(shí)創(chuàng)建過多線程耗盡資源 ExecutorService threadPool = Executors.newFixedThreadPool(100);while (true) {Socket socket = server.accept();Runnable runnable = ()->{try {// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {// 注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len, "UTF-8"));}System.out.println("get message from client: " + sb);inputStream.close();socket.close();} catch (Exception e) {e.printStackTrace();} };threadPool.submit(runnable);}} }同步IO和異步IO
同步和異步是針對(duì)應(yīng)用程序和內(nèi)核的交互而言的,同步指的是用戶進(jìn)程觸發(fā)IO操作并等待或者輪詢的去查看IO操作是否就緒,而異步是指用戶進(jìn)程觸發(fā)IO操作以后便開始做自己的事情,而當(dāng)IO操作已經(jīng)完成的時(shí)候會(huì)得到IO完成的通知。
- 指的是用戶空間和內(nèi)核空間數(shù)據(jù)交互的方式
- 同步:用戶空間要的數(shù)據(jù),必須等到內(nèi)核空間給它才做其他事情。
- 異步:用戶空間要的數(shù)據(jù),不需要等到內(nèi)核空間給它,才做其他事情。內(nèi)核空間會(huì)異步通知用戶進(jìn)程,并把數(shù)據(jù)直接給到用戶空間。
阻塞IO和非阻塞IO
阻塞方式下讀取或者寫入函數(shù)將一直等待,而非阻塞方式下,讀取或者寫入函數(shù)會(huì)立即返回一個(gè)狀態(tài)值。
- 指的是用戶空間和內(nèi)核空間IO操作的方式
- 阻塞:用戶空間通過系統(tǒng)調(diào)用(systemcall)和內(nèi)核空間發(fā)送IO操作時(shí),該調(diào)用是阻塞的。
- 非阻塞:用戶空間通過系統(tǒng)調(diào)用(systemcall)和內(nèi)核空間發(fā)送IO操作時(shí),該調(diào)用是不堵塞的,直接返回的,只是返回時(shí),可能沒有數(shù)據(jù)而已。
IO設(shè)計(jì)模式之Reactor和Proactor
- 平時(shí)接觸的開源產(chǎn)品如Redis、ACE,事件模型都使用的Reactor模式;而同樣做事件處理的Proactor,由于操作系統(tǒng)的原因,相關(guān)的開源產(chǎn)品也少。
反應(yīng)器Reactor
概述
反應(yīng)器設(shè)計(jì)模式(Reactor pattern)是一種為處理并發(fā)服務(wù)請(qǐng)求,并將請(qǐng)求提交到一個(gè)或者多個(gè)服務(wù)處理程序的事件設(shè)計(jì)模式。當(dāng)客戶端請(qǐng)求抵達(dá)后,服務(wù)處理程序使用多路分配策略,由一個(gè)非阻塞的線程來接收所有的請(qǐng)求,然后派發(fā)這些請(qǐng)求至相關(guān)的工作線程進(jìn)行處理。
Reactor模式主要包含下面幾部分內(nèi)容:
- 初始事件分發(fā)器(Initialization Dispatcher):用于管理Event Handler,定義注冊(cè)、移除EventHandler等。它還作為Reactor模式的入口調(diào)用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當(dāng)阻塞等待返回時(shí),根據(jù)事件發(fā)生的Handle將其分發(fā)給對(duì)應(yīng)的Event Handler處理,即回調(diào)EventHandler中的handle_event()方法。
- 同步(多路)事件分離器(Synchronous Event Demultiplexer):無限循環(huán)等待新事件的到來,一旦發(fā)現(xiàn)有新的事件到來,就會(huì)通知初始事件分發(fā)器去調(diào)取特定的事件處理器。
- 系統(tǒng)處理程序(Handles):操作系統(tǒng)中的句柄,是對(duì)資源在操作系統(tǒng)層面上的一種抽象,它可以是打開的文件、一個(gè)連接(Socket)、Timer等。由于Reactor模式一般使用在網(wǎng)絡(luò)編程中,因而這里一般指SocketHandle,即一個(gè)網(wǎng)絡(luò)連接(Connection,在Java NIO中的Channel)。這個(gè)Channel注冊(cè)到Synchronous Event Demultiplexer中,以監(jiān)聽Handle中發(fā)生的事件,對(duì)ServerSocketChannnel可以是CONNECT事件,對(duì) SocketChannel 可以是READ、WRITE、CLOSE事件等。
- 事件處理器(Event Handler):定義事件處理方法,以供Initialization Dispatcher回調(diào)使用。
- 對(duì)于Reactor模式,可以將其看做由兩部分組成,一部分是由Boss組成,另一部分是由worker組成。Boss就像老板一樣,主要是拉活兒、談項(xiàng)目,一旦Boss接到活兒了,就下發(fā)給下面的work去處理。也可以看做是項(xiàng)目經(jīng)理和程序員之間的關(guān)系。
為什么使用Reactor模式
并發(fā)系統(tǒng)常使用reactor模式代替常用的多線程的處理方式,節(jié)省系統(tǒng)的資源,提高系統(tǒng)的吞吐量。例如:在高并發(fā)的情況下,既可以使用多處理處理方式,也可以使用Reactor處理方式。
- 多線程處理:為每個(gè)單獨(dú)到來的請(qǐng)求,專門啟動(dòng)一條線程,這樣的話造成系統(tǒng)的開銷很大,并且在單核的機(jī)上,多線程并不能提高系統(tǒng)的性能,除非在有一些阻塞的情況發(fā)生。否則線程切換的開銷會(huì)使處理的速度變慢。
- Reactor模式處理:服務(wù)器端啟動(dòng)一條單線程,用于輪詢IO操作是否就緒,當(dāng)有就緒的才進(jìn)行相應(yīng)的讀寫操作,這樣的話就減少了服務(wù)器產(chǎn)生大量的線程,也不會(huì)出現(xiàn)線程之間的切換產(chǎn)生的性能消耗。(目前JAVA的NIO就采用的此種模式,這里引申出一個(gè)問題:在多核情況下NIO的擴(kuò)展問題)
- 以上兩種處理方式都是基于同步的,多線程的處理是我們傳統(tǒng)模式下對(duì)高并發(fā)的處理方式,Reactor模式的處理是現(xiàn)今面對(duì)高并發(fā)和高性能一種主流的處理方式。
Reactor模式結(jié)構(gòu)
Reactor包含如下角色:
- Handle 句柄:用來標(biāo)識(shí)socket連接或是打開文件;
- Synchronous Event Demultiplexer:同步事件多路分解器,由操作系統(tǒng)內(nèi)核實(shí)現(xiàn)的一個(gè)函數(shù);用于阻塞等 待發(fā)生在句柄集合上的一個(gè)或多個(gè)事件(如select/epoll)。
- Event Handler:事件處理接口。
- Concrete Event HandlerA:實(shí)現(xiàn)應(yīng)用程序所提供的特定事件處理邏輯。
- Reactor:反應(yīng)器,定義一個(gè)接口,實(shí)現(xiàn)以下功能
- 供應(yīng)用程序注冊(cè)和刪除關(guān)注的事件句柄;
- 運(yùn)行事件循環(huán)
- 有就緒事件到來時(shí),分發(fā)事件到之前注冊(cè)的回調(diào)函數(shù)上處理。
- Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注冊(cè)、移除 EventHandler等;另外,它還作為Reactor模式的入口調(diào)用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當(dāng)阻塞等待返回時(shí),根據(jù)事件發(fā)生的Handle將其分發(fā)給對(duì)應(yīng)的Event Handler處理, 即回調(diào)EventHandler中的handle_event()方法。
業(yè)務(wù)流程及時(shí)序圖
- 應(yīng)用啟動(dòng),將關(guān)注的事件handle注冊(cè)到Reactor中。
- 調(diào)用Reactor,進(jìn)入無限事件循環(huán),等待注冊(cè)的事件到來。
- 事件到來,select返回,Reactor將事件分發(fā)到之前注冊(cè)的回調(diào)函數(shù)中處理。
Proactor模式
運(yùn)用于異步I/O操作,Proactor模式中,應(yīng)用程序不需要進(jìn)行實(shí)際的讀寫過程,它只需要從緩存區(qū)讀取或者寫入即可,操作系統(tǒng)會(huì)讀取緩存區(qū)或者寫入緩存區(qū)到真正的IO設(shè)備。
Proactor中對(duì)于寫入操作和讀取操作,只感興趣的是寫入完成事件。
Proactor模式結(jié)構(gòu)
Proactor主動(dòng)器模式包含如下角色:
- Handle 句柄:用來標(biāo)識(shí)socket連接或是打開文件。
- Asynchronous Operation:異步操作。
- Asynchronous Operation Processor:異步操作處理器;負(fù)責(zé)執(zhí)行異步操作,一般由操作系統(tǒng)內(nèi)核實(shí)現(xiàn)。
- Completion Event Queue:完成事件隊(duì)列;異步操作完成的結(jié)果放到隊(duì)列中等待后續(xù)使用。
- Proactor:主動(dòng)器;為應(yīng)用程序進(jìn)程提供事件循環(huán);從完成事件隊(duì)列中取出異步操作的結(jié)果,分發(fā)調(diào)用相應(yīng)的后續(xù)處理邏輯。
- Completion Handler:完成事件接口;一般是由回調(diào)函數(shù)組成的接口。
- Concrete Completion Handler:完成事件處理邏輯;實(shí)現(xiàn)接口定義特定的應(yīng)用處理邏輯。
業(yè)務(wù)流程及時(shí)序圖
- 應(yīng)用程序啟動(dòng),調(diào)用異步操作處理器提供的異步操作接口函數(shù),調(diào)用之后應(yīng)用程序和異步操作處理就獨(dú)立運(yùn)行;應(yīng)用程序可以調(diào)用新的異步操作,而其它操作可以并發(fā)進(jìn)行。
- 應(yīng)用程序啟動(dòng)Proactor主動(dòng)器,進(jìn)行無限的事件循環(huán),等待完成事件到來。
- 異步操作處理器執(zhí)行異步操作,完成后將結(jié)果放入到完成事件隊(duì)列。
- 主動(dòng)器從完成事件隊(duì)列中取出結(jié)果,分發(fā)到相應(yīng)的完成事件回調(diào)函數(shù)處理邏輯中。
Reactor和Proactor對(duì)比
主動(dòng)和被動(dòng)
- Reactor將handle放到select(),等待可寫就緒,然后調(diào)用write()寫入數(shù)據(jù);寫完處理后續(xù)邏輯。
- Proactor調(diào)用aoi_write后立刻返回,由內(nèi)核負(fù)責(zé)寫操作,寫完后調(diào)用相應(yīng)的回調(diào)函數(shù)處理后續(xù)邏輯。
- 可以看出,Reactor被動(dòng)的等待指示事件的到來并做出反應(yīng);它有一個(gè)等待的過程,做什么都要先放入到監(jiān)聽事件集合中等待handler可用時(shí)再進(jìn)行操作; Proactor直接調(diào)用異步讀寫操作,調(diào)用完后立刻返回。
實(shí)現(xiàn)
- Reactor實(shí)現(xiàn)了一個(gè)被動(dòng)的事件分離和分發(fā)模型,服務(wù)等待請(qǐng)求事件的到來,再通過不受間斷的同步處理事件,從而做出反應(yīng)。
- Proactor實(shí)現(xiàn)了一個(gè)主動(dòng)的事件分離和分發(fā)模型;這種設(shè)計(jì)允許多個(gè)任務(wù)并發(fā)的執(zhí)行,從而提高吞吐量;并可執(zhí)行耗時(shí)長的任務(wù)(各個(gè)任務(wù)間互不影響)。
優(yōu)點(diǎn)
- Reactor實(shí)現(xiàn)相對(duì)簡單,對(duì)于耗時(shí)短的處理場景處理高效; 操作系統(tǒng)可以在多個(gè)事件源上等待,并且避免了多線程編程相關(guān)的性能開銷和編程復(fù)雜性; 事件的串行化對(duì)應(yīng)用是透明的,可以順序的同步執(zhí)行而不需要加鎖; 事務(wù)分離:將與應(yīng)用無關(guān)的多路分解和分配機(jī)制和與應(yīng)用相關(guān)的回調(diào)函數(shù)分離開來。
- Proactor性能更高,能夠處理耗時(shí)長的并發(fā)場景。
缺點(diǎn)
- Reactor處理耗時(shí)長的操作會(huì)造成事件分發(fā)的阻塞,影響到后續(xù)事件的處理。
- Proactor實(shí)現(xiàn)邏輯復(fù)雜,依賴操作系統(tǒng)對(duì)異步的支持,目前實(shí)現(xiàn)了純異步操作的操作系統(tǒng)少,實(shí)現(xiàn)優(yōu)秀的如windows IOCP,但由于其windows系統(tǒng)用于服務(wù)器的局限性,目前應(yīng)用范圍較小;而Unix/Linux系統(tǒng)對(duì)純異步的支持有限,應(yīng)用事件驅(qū)動(dòng)的主流還是通過select/epoll來實(shí)現(xiàn)。
適用場景
- Reactor:同時(shí)接收多個(gè)服務(wù)請(qǐng)求,并且依次同步的處理它們的事件驅(qū)動(dòng)程序。
- Proactor:異步接收和同時(shí)處理多個(gè)服務(wù)請(qǐng)求的事件驅(qū)動(dòng)程序。
漫談五種IO模型
高性能IO模型淺析
服務(wù)器端編程經(jīng)常需要構(gòu)造高性能的IO模型,常見的IO模型有四種:
- 同步阻塞IO(Blocking IO):即傳統(tǒng)的IO模型。
- 同步非阻塞IO(Non-blocking IO):默認(rèn)創(chuàng)建的socket都是阻塞的,非阻塞IO要求socket被設(shè)置為NONBLOCK。注意這里所說的NIO并非Java的NIO(New IO)庫。
- IO多路復(fù)用(IO Multiplexing):即經(jīng)典的Reactor設(shè)計(jì)模式,有時(shí)也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
- 異步IO(Asynchronous IO):即經(jīng)典的Proactor設(shè)計(jì)模式,也稱為異步非阻塞IO。
IO模型舉例理解
例1
- 阻塞IO,給女神發(fā)一條短信,說我來找你了,然后就默默的一直等著女神下樓,這個(gè)期間除了等待你不會(huì)做其他事情,屬于備胎做法。
- 非阻塞IO,給女神發(fā)短信,如果不回,接著再發(fā),一直發(fā)到女神下樓,這個(gè)期間你除了發(fā)短信等待不會(huì)做其他事情,屬于專一做法。
- IO多路復(fù)用,是找一個(gè)宿管大媽來幫你監(jiān)視下樓的女生,這個(gè)期間你可以些其他的事情。例如可以順便看看其他妹子,玩玩王者榮耀,上個(gè)廁所等。IO復(fù)用又包括 select、poll、epoll 模式。那么它們的區(qū)別是什么?
- select 大媽每一個(gè)女生下樓,select大媽都不知道這個(gè)是不是你的女神,她需要一個(gè)一個(gè)詢問,并且select大媽能力還有限,最多一次幫你監(jiān)視1024個(gè)妹子。
- poll大媽不限制盯著女生的數(shù)量,只要是經(jīng)過宿舍樓門口的女生,都會(huì)幫你去問是不是你女神。
- epoll大媽不限制盯著女生的數(shù)量,并且也不需要一個(gè)一個(gè)去問。那么如何做呢?epoll大媽會(huì)為每個(gè)進(jìn)宿舍樓的女生臉上貼上一個(gè)大字條,上面寫上女生自己的名字,只要女生下樓了,epoll大媽就知道這個(gè)是不是你女神了,然后大媽再通知你。
- 上面這些同步IO有一個(gè)共同點(diǎn)就是,當(dāng)女神走出宿舍門口的時(shí)候,你已經(jīng)站在宿舍門口等著女神的,此時(shí)你屬于阻塞狀態(tài)。
- 接下來是異步IO的情況:你告訴女神我來了,然后你就去王者榮耀了,一直到女神下樓了,發(fā)現(xiàn)找不見你了,女神再給你打電話通知你,說我下樓了,你在哪呢?這時(shí)候你才來到宿舍門口。此時(shí)屬于逆襲做法。
例2
- 阻塞I/O模型:老李去火車站買票,排隊(duì)三天買到一張退票。耗費(fèi):在車站吃喝拉撒睡 3天,其他事一件沒干。
- 非阻塞I/O模型:老李去火車站買票,隔12小時(shí)去火車站問有沒有退票,三天后買到一張票。耗費(fèi):往返車站6次,路上6小時(shí),其他時(shí)間做了好多事。
- I/O復(fù)用模型:
- select/poll:老李去火車站買票,委托黃牛,然后每隔6小時(shí)電話黃牛詢問,黃牛三天內(nèi)買到票,然后老李去火車站交錢領(lǐng)票。 耗費(fèi):往返車站2次,路上2小時(shí),黃牛手續(xù)費(fèi)100元,打電話17次。
- epoll:老李去火車站買票,委托黃牛,黃牛買到后即通知老李去領(lǐng),然后老李去火車站交錢領(lǐng)票。 耗費(fèi):往返車站2次,路上2小時(shí),黃牛手續(xù)費(fèi)100元,無需打電話。
- 信號(hào)驅(qū)動(dòng)I/O模型:老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李,然后老李去火車站交錢領(lǐng)票。 耗費(fèi):往返車站2次,路上2小時(shí),免黃牛費(fèi)100元,無需打電話。
- 異步I/O模型:老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李并快遞送票上門。 耗費(fèi):往返車站1次,路上1小時(shí),免黃牛費(fèi)100元,無需打電話。
同步阻塞IO
- 同步阻塞IO模型是最簡單的IO模型,用戶線程在內(nèi)核進(jìn)行IO操作時(shí)被阻塞。
- 用戶線程通過系統(tǒng)調(diào)用read發(fā)起IO讀操作,由用戶空間轉(zhuǎn)到內(nèi)核空間。內(nèi)核等到數(shù)據(jù)包到達(dá)后,然后將接收的數(shù)據(jù)拷貝到用戶空間,完成read操作。
- 用戶線程使用同步阻塞IO模型的偽代碼描述為:
- 即用戶需要等待read將socket中的數(shù)據(jù)讀取到buffer后,才繼續(xù)處理接收的數(shù)據(jù)。整個(gè)IO請(qǐng)求的過程中,用戶線程是被阻塞的,這導(dǎo)致用戶在發(fā)起IO請(qǐng)求時(shí),不能做任何事情,對(duì)CPU的資源利用率不夠。
同步非阻塞IO
- 同步非阻塞IO是在同步阻塞IO的基礎(chǔ)上,將socket設(shè)置為NONBLOCK。這樣做用戶線程可以在發(fā)起IO請(qǐng)求后可以立即返回。
- 由于socket是非阻塞的方式,因此用戶線程發(fā)起IO請(qǐng)求時(shí)立即返回。但并未讀取到任何數(shù)據(jù),用戶線程需要不斷地發(fā)起IO請(qǐng)求,直到數(shù)據(jù)到達(dá)后,才真正讀取到數(shù)據(jù),繼續(xù)執(zhí)行。
- 用戶線程使用同步非阻塞IO模型的偽代碼描述為:
IO多路復(fù)用
- IO多路復(fù)用模型是建立在內(nèi)核提供的多路分離函數(shù)select基礎(chǔ)之上的,使用select函數(shù)可以避免同步非阻塞IO模型中輪詢等待的問題。
- 用戶首先將需要進(jìn)行IO操作的socket添加到select中,然后阻塞等待select系統(tǒng)調(diào)用返回。當(dāng)數(shù)據(jù)到達(dá)時(shí),socket被激活,select函數(shù)返回。用戶線程正式發(fā)起read請(qǐng)求,讀取數(shù)據(jù)并繼續(xù)執(zhí)行。
- 從流程上來看,使用select函數(shù)進(jìn)行IO請(qǐng)求和同步阻塞模型沒有太大的區(qū)別,甚至還多了添加監(jiān)視socket,以及調(diào)用select函數(shù)的額外操作,效率更差。但是,使用select以后最大的優(yōu)勢是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè)socket的IO請(qǐng)求。用戶可以注冊(cè)多個(gè)socket,然后不斷地調(diào)用select讀取被激活的socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè)IO請(qǐng)求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達(dá)到這個(gè)目的。
- 用戶線程使用select函數(shù)的偽代碼描述為:
- 其中while循環(huán)前將socket添加到select監(jiān)視中,然后在while內(nèi)一直調(diào)用select獲取被激活的socket,一旦socket可讀,便調(diào)用read函數(shù)將socket中的數(shù)據(jù)讀取出來。
- 然而,使用select函數(shù)的優(yōu)點(diǎn)并不僅限于此。雖然上述方式允許單線程內(nèi)處理多個(gè)IO請(qǐng)求,但是每個(gè)IO請(qǐng)求的過程還是阻塞的(在select函數(shù)上阻塞),平均時(shí)間甚至比同步阻塞IO模型還要長。
- 如果用戶線程只注冊(cè)自己感興趣的socket或者IO請(qǐng)求,然后去做自己的事情,等到數(shù)據(jù)到來時(shí)再進(jìn)行處理,則可以提高CPU的利用率。
- IO多路復(fù)用模型使用了Reactor設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。
- 通過Reactor的方式,可以將用戶線程輪詢IO操作狀態(tài)的工作統(tǒng)一交給handle_events事件循環(huán)進(jìn)行處理。用戶線程注冊(cè)事件處理器之后可以繼續(xù)執(zhí)行做其他的工作(異步),而Reactor線程負(fù)責(zé)調(diào)用內(nèi)核的select函數(shù)檢查socket狀態(tài)。當(dāng)有socket被激活時(shí),則通知相應(yīng)的用戶線程(或執(zhí)行用戶線程的回調(diào)函數(shù)),執(zhí)行handle_event進(jìn)行數(shù)據(jù)讀取、處理的工作。由于select函數(shù)是阻塞的,因此多路IO復(fù)用模型也被稱為異步阻塞IO模型。注意,這里的所說的阻塞是指select函數(shù)執(zhí)行時(shí)線程被阻塞,而不是指socket。一般在使用IO多路復(fù)用模型時(shí),socket都是設(shè)置為NONBLOCK的,不過這并不會(huì)產(chǎn)生影響,因?yàn)橛脩舭l(fā)起IO請(qǐng)求時(shí),數(shù)據(jù)已經(jīng)到達(dá)了,用戶線程一定不會(huì)被阻塞。
- 用戶線程使用IO多路復(fù)用模型的偽代碼描述為:
- 用戶需要重寫EventHandler的handle_event函數(shù)進(jìn)行讀取數(shù)據(jù)、處理數(shù)據(jù)的工作,用戶線程只需要將自己的EventHandler注冊(cè)到Reactor即可。Reactor中handle_events事件循環(huán)的偽代碼大致如下。
- 事件循環(huán)不斷地調(diào)用select獲取被激活的socket,然后根據(jù)獲取socket對(duì)應(yīng)的EventHandler,執(zhí)行器handle_event函數(shù)即可。
- IO多路復(fù)用是最常使用的IO模型,但是其異步程度還不夠“徹底”,因?yàn)樗褂昧藭?huì)阻塞線程的select系統(tǒng)調(diào)用。因此IO多路復(fù)用只能稱為異步阻塞IO,而非真正的異步IO。
異步IO
- “真正”的異步IO需要操作系統(tǒng)更強(qiáng)的支持。在IO多路復(fù)用模型中,事件循環(huán)將文件句柄的狀態(tài)事件通知給用戶線程,由用戶線程自行讀取數(shù)據(jù)、處理數(shù)據(jù)。而在異步IO模型中,當(dāng)用戶線程收到通知時(shí),數(shù)據(jù)已經(jīng)被內(nèi)核讀取完畢,并放在了用戶線程指定的緩沖區(qū)內(nèi),內(nèi)核在IO完成后通知用戶線程直接使用即可。
- 異步IO模型使用了Proactor設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。
- 異步IO模型中,用戶線程直接使用內(nèi)核提供的異步IO API發(fā)起read請(qǐng)求,且發(fā)起后立即返回,繼續(xù)執(zhí)行用戶線程代碼。不過此時(shí)用戶線程已經(jīng)將調(diào)用的AsynchronousOperation和CompletionHandler注冊(cè)到內(nèi)核,然后操作系統(tǒng)開啟獨(dú)立的內(nèi)核線程去處理IO操作。當(dāng)read請(qǐng)求的數(shù)據(jù)到達(dá)時(shí),由內(nèi)核負(fù)責(zé)讀取socket中的數(shù)據(jù),并寫入用戶指定的緩沖區(qū)中。最后內(nèi)核將read的數(shù)據(jù)和用戶線程注冊(cè)的CompletionHandler分發(fā)給內(nèi)部Proactor,Proactor將IO完成的信息通知給用戶線程(一般通過調(diào)用用戶線程注冊(cè)的完成事件處理函數(shù)),完成異步IO。
- 用戶線程使用異步IO模型的偽代碼描述為:
- 用戶需要重寫CompletionHandler的handle_event函數(shù)進(jìn)行處理數(shù)據(jù)的工作,參數(shù)buffer表示Proactor已經(jīng)準(zhǔn)備好的數(shù)據(jù),用戶線程直接調(diào)用內(nèi)核提供的異步IO API,并將重寫的CompletionHandler注冊(cè)即可。
- 相比于IO多路復(fù)用模型,異步IO并不十分常用,不少高性能并發(fā)服務(wù)程序使用IO多路復(fù)用模型+多線程任務(wù)處理的架構(gòu)基本可以滿足需求。況且目前操作系統(tǒng)對(duì)異步IO的支持并非特別完善,更多的是采用IO多路復(fù)用模型模擬異步IO的方式(IO事件觸發(fā)時(shí)不直接通知用戶線程,而是將數(shù)據(jù)讀寫完畢后放到用戶指定的緩沖區(qū)中)。
Redis的IO多路復(fù)用技術(shù)
redis 是一個(gè)單線程卻性能非常好的內(nèi)存數(shù)據(jù)庫, 主要用來作為緩存系統(tǒng)。 redis 采用網(wǎng)絡(luò)IO多路復(fù)用技術(shù)來保證在多連接的時(shí)候, 系統(tǒng)的高吞吐量。
為什么Redis中要使用I/O多路復(fù)用
- 首先,Redis 是跑在單線程中的,所有的操作都是按照順序線性執(zhí)行的,但是由于讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會(huì)導(dǎo)致某一文件的 I/O 阻塞導(dǎo)致整個(gè)進(jìn)程無法對(duì)其它客戶提供服務(wù),而 I/O 多路復(fù)用就是為了解決這個(gè)問題而出現(xiàn)的。
- select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒,能夠通知程序進(jìn)行相應(yīng)的操作。
- redis的io模型主要是基于epoll實(shí)現(xiàn)的,不過它也提供了 select和kqueue的實(shí)現(xiàn),默認(rèn)采用epoll。
epoll實(shí)現(xiàn)機(jī)制
設(shè)想一下如下場景:有100萬個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的(事實(shí)上大部分場景都是這種情況)。如何實(shí)現(xiàn)這樣的高并發(fā)?
- 在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬個(gè)連接告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢這些套接字上是否有事件發(fā)生,輪詢完后,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài),讓服務(wù)器應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。
- 如果沒有I/O事件產(chǎn)生,我們的程序就會(huì)阻塞在select處。但是依然有個(gè)問題,我們從select那里僅僅知道了,有I/O事件發(fā)生了,但卻并不知道是那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能 所有流,找出能讀出數(shù)據(jù),或者寫入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。
- 但是使用select,我們有O(n)的無差別輪詢復(fù)雜度,同時(shí)處理的流越多,每一次無差別輪詢時(shí)間就越長。
- 總結(jié):select 和 poll 的缺點(diǎn)如下:
- 每次調(diào)用select/poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會(huì)很大。
- 同時(shí)每次調(diào)用select/poll都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大。
- 針對(duì)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024。
- select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件。
- select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。
- 相比select模型,poll使用鏈表保存文件描述符,因此沒有了監(jiān)視文件數(shù)量的限制,但其他三個(gè)缺點(diǎn)依然存在。
- epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll是poll的一種優(yōu)化,返回后不需要對(duì)所有的fd進(jìn)行遍歷,在內(nèi)核中維持了fd的列表。select和poll是將這個(gè)內(nèi)核列表維持在用戶態(tài),然后傳遞到內(nèi)核中。與poll/select不同,epoll不再是一個(gè)單獨(dú)的系統(tǒng)調(diào)用,而是由epoll_create、epoll_ctl、epoll_wait三個(gè)系統(tǒng)調(diào)用組成。
- epoll在2.6以后的內(nèi)核才支持。
- epoll通過在Linux內(nèi)核中申請(qǐng)一個(gè)簡易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹)。把原先的select/poll調(diào)用分成了3個(gè)部分:
- 調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)。
- 調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬個(gè)連接的套接字。
- 調(diào)用epoll_wait收集發(fā)生的事件的連接。
- 如此一來,要實(shí)現(xiàn)上面說是的場景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒有一股腦的向操作系統(tǒng)復(fù)制這100萬個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。
- 總結(jié)epoll優(yōu)點(diǎn):
- epoll 沒有最大并發(fā)連接的限制,上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于 2048, 一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大 ,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看。
- 效率提升, epoll 最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接 ,而跟連接總數(shù)無關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中, epoll 的效率就會(huì)遠(yuǎn)遠(yuǎn)高于 select 和 poll 。
- 內(nèi)存拷貝, epoll 在這點(diǎn)上使用了“共享內(nèi)存”,這個(gè)內(nèi)存拷貝也省略了。
redis epoll底層實(shí)現(xiàn)
- 當(dāng)某一進(jìn)程調(diào)用epoll_create方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。
- eventpoll結(jié)構(gòu)體如下所示:
- 每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件。這些事件都會(huì)掛載在紅黑樹中,如此,重復(fù)添加的事件就可以通過紅黑樹而高效的識(shí)別出來(紅黑樹的插入時(shí)間效率是lgn,其中n為樹的高度)。
- 而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。
- 在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:
-
當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。
-
優(yōu)勢:
- 不用重復(fù)傳遞:我們調(diào)用epoll_wait時(shí)就相當(dāng)于以往調(diào)用select/poll,但是這時(shí)卻不用傳遞socket句柄給內(nèi)核,因?yàn)閮?nèi)核已經(jīng)在epoll_ctl中拿到了要監(jiān)控的句柄列表。
- 在內(nèi)核里,一切皆文件:所以,epoll向內(nèi)核注冊(cè)了一個(gè)文件系統(tǒng),用于存儲(chǔ)上述的被監(jiān)控socket。當(dāng)你調(diào)用epoll_create時(shí),就會(huì)在這個(gè)虛擬的epoll文件系統(tǒng)里創(chuàng)建一個(gè)file結(jié)點(diǎn)。當(dāng)然這個(gè)file不是普通文件,它只服務(wù)于epoll。epoll在被內(nèi)核初始化時(shí)(操作系統(tǒng)啟動(dòng)),同時(shí)會(huì)開辟出epoll自己的內(nèi)核高速cache區(qū),用于安置每一個(gè)我們想監(jiān)控的socket,這些socket會(huì)以紅黑樹的形式保存在內(nèi)核cache里,以支持快速的查找、插入、刪除。這個(gè)內(nèi)核高速cache區(qū),就是建立連續(xù)的物理內(nèi)存頁,然后在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內(nèi)存對(duì)象,每次使用時(shí)都是使用空閑的已分配好的對(duì)象。
- 極其高效的原因:這是由于我們?cè)谡{(diào)用epoll_create時(shí),內(nèi)核除了幫我們?cè)趀poll文件系統(tǒng)里建了個(gè)file結(jié)點(diǎn),在內(nèi)核cache里建了個(gè)紅黑樹用于存儲(chǔ)以后epoll_ctl傳來的socket外,還會(huì)再建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件,當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察這個(gè)list鏈表里有沒有數(shù)據(jù)即可。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒數(shù)據(jù)也返回。所以,epoll_wait非常高效。
-
這個(gè)準(zhǔn)備就緒list鏈表是怎么維護(hù)的呢?
- 當(dāng)我們執(zhí)行epoll_ctl時(shí),除了把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上之外,還會(huì)給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所以,當(dāng)一個(gè)socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后就來把socket插入到準(zhǔn)備就緒鏈表里了。(注:好好理解這句話!)
- 從上面這句可以看出,epoll的基礎(chǔ)就是回調(diào)呀!
-
如此,一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,就幫我們解決了大并發(fā)下的socket處理問題。執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹和就緒鏈表,執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。
-
最后看看epoll獨(dú)有的兩種模式LT和ET。無論是LT和ET模式,都適用于以上所說的流程。區(qū)別是,LT模式下,只要一個(gè)句柄上的事件一次沒有處理完,會(huì)在以后調(diào)用epoll_wait時(shí)次次返回這個(gè)句柄,而ET模式僅在第一次返回。
-
關(guān)于LT,ET,有一端描述,LT和ET都是電子里面的術(shù)語,ET是邊緣觸發(fā),LT是水平觸發(fā),一個(gè)表示只有在變化的邊際觸發(fā),一個(gè)表示在某個(gè)階段都會(huì)觸發(fā)。
-
LT, ET這件事怎么做到的呢?當(dāng)一個(gè)socket句柄上有事件時(shí),內(nèi)核會(huì)把該句柄插入上面所說的準(zhǔn)備就緒list鏈表,這時(shí)我們調(diào)用epoll_wait,會(huì)把準(zhǔn)備就緒的socket拷貝到用戶態(tài)內(nèi)存,然后清空準(zhǔn)備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實(shí)有未處理的事件時(shí),又把該句柄放回到剛剛清空的準(zhǔn)備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會(huì)返回這個(gè)句柄。(從上面這段,可以看出,LT還有個(gè)回放的過程,低效了)
總結(jié)
以上是生活随笔為你收集整理的IO模型、IO多路复用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小儿推拿22种手法(动态图)轻松解决各种
- 下一篇: 过敏性鼻炎怎么治疗