Redis的IO多路復(fù)用以及Select,Epoll的演進(jìn)
什么是阻塞,非阻塞,異步同步,select,poll,epoll?今天我們用一遍文章解開這多年的迷惑。
首先我們想要通過網(wǎng)絡(luò)接收消息,是這樣的一個(gè)步驟。
- 用戶空間向內(nèi)核空間請求網(wǎng)絡(luò)數(shù)據(jù)
- 內(nèi)核空間把網(wǎng)卡數(shù)據(jù)讀取到內(nèi)核緩沖區(qū)
- 將內(nèi)核緩沖區(qū)的數(shù)據(jù)復(fù)制到用戶緩沖區(qū)
根據(jù)我們請求數(shù)據(jù)的情況不同,以及內(nèi)核緩沖區(qū)到用戶緩沖區(qū)的不同,分為了阻塞,非阻塞,異步同步的區(qū)別。
在《UNIX網(wǎng)絡(luò)編程》一書中,總結(jié)歸納了5種I0模型:
- 阻塞 I0 ( Blocking I0)
- 非阻塞 I0 (Nonblocking I0)
- I0多路復(fù)用(I0 Multiplexing)
- 信號驅(qū)動I0 (Signal Driven I0 )
- 異步I0 (Asynchronous I0)
阻塞IO
- 用戶應(yīng)用請求內(nèi)核是否有新的網(wǎng)絡(luò)數(shù)據(jù)
- 如果沒有數(shù)據(jù),就阻塞直到有數(shù)據(jù)到來
- 等待內(nèi)核將數(shù)據(jù)拷貝到用戶空間
- 用戶應(yīng)用處理數(shù)據(jù)
以上可以看出來,根據(jù)等待數(shù)據(jù)的方式不同,分為阻塞和非阻塞。
阻塞IO在請求內(nèi)核數(shù)據(jù)的時(shí)候,沒有數(shù)據(jù)就會一直阻塞直到獲取數(shù)據(jù)。
非阻塞IO
- 用戶應(yīng)用請求內(nèi)核是否有新的網(wǎng)絡(luò)數(shù)據(jù)
- 如果沒有數(shù)據(jù),內(nèi)核直接返回沒數(shù)據(jù),用戶應(yīng)用可以隔一段時(shí)間再來請求。
- 等待內(nèi)核將數(shù)據(jù)拷貝到用戶空間
- 用戶應(yīng)用處理數(shù)據(jù)
非阻塞IO在等待內(nèi)核數(shù)據(jù)的時(shí)候,沒有數(shù)據(jù)就會得到?jīng)]數(shù)據(jù)的結(jié)果,應(yīng)用可以進(jìn)行其他動作。
同步IO
同步IO的主要看內(nèi)核數(shù)據(jù)到用戶空間的過程是同步進(jìn)行的就是同步IO
異步IO
異步IO首先是非阻塞IO,區(qū)別在于成功標(biāo)志的時(shí)機(jī)。異步IO連內(nèi)核到用戶態(tài)的數(shù)據(jù)拷貝都是異步的,直到數(shù)據(jù)拷貝完成,才會回調(diào)一個(gè)信號,通知一切已經(jīng)準(zhǔn)備完成。用戶應(yīng)用此時(shí)就可以直接處理結(jié)果了。
總結(jié)
阻塞非阻塞指的是在獲取結(jié)果上是否會阻塞等待結(jié)果完成
同步異步指的是是否會參與IO讀寫,或者是等待讀寫成功的回調(diào)
redis的IO多路復(fù)用
如果是阻塞IO也就是BIO,那么在一個(gè)fd(文件描述符)沒有數(shù)據(jù)的時(shí)候,就是阻塞一直等待,如果同時(shí)有多個(gè)fd,對于單線程來說,只能一直等第一個(gè)有數(shù)據(jù),然后再接著處理第二個(gè),效率很慢。
就像顧客點(diǎn)餐,要一直等到第一個(gè)人點(diǎn)完餐,后面的人才有機(jī)會。BIO也有個(gè)解決辦法,一般是增加多線程,每個(gè)線程都維護(hù)一個(gè)fd,就相當(dāng)于為每個(gè)顧客都添加一個(gè)點(diǎn)餐臺。在fd足夠多的情況下,會有大量的線程被創(chuàng)建,線程可是有上限的,開銷也大(更多線程需要更多的內(nèi)存空間)。
如果是非阻塞IO也就是NIO,會有顧客沒點(diǎn)完餐,然后造成CPU一直在詢問一直空轉(zhuǎn)的情況。
因此引入了IO多路復(fù)用模型:利用單個(gè)線程來同時(shí)監(jiān)聽多個(gè)FD,并在某個(gè)FD可讀、可寫時(shí)得到通知,從而避免無效的等待,充分利用CPU資源
文件描述符( File Descriptor) :簡稱FD,是一個(gè)從0開始遞增的無符號整數(shù),用來關(guān)聯(lián)Linux中的一一個(gè)文件。在Linux
中,一切皆文件,例如常規(guī)文件、視頻、硬件設(shè)備等,當(dāng)然也包括網(wǎng)絡(luò)套接字(Socket),
這時(shí)候每來一個(gè)顧客(FD?),我們就會給他一個(gè)開關(guān)(注冊進(jìn)監(jiān)聽事件?),一個(gè)服務(wù)員(一個(gè)線程?)等待開關(guān)亮起(阻塞等待事件?)。有顧客完成,就會按下開關(guān),一定的頻率下開關(guān)會亮起(事件通知?),服務(wù)員會選取按下開關(guān)的一批人,給他們點(diǎn)餐(批量處理事件)。
IO多路復(fù)用的實(shí)現(xiàn)有select,poll,epoll,我們來看看他們的優(yōu)缺點(diǎn)。
select
select是Linux中最早的I/O多路復(fù)用實(shí)現(xiàn)方案,并且windows操作系統(tǒng)上只支持select。這就是為啥window發(fā)揮不出redis的最大性能的一個(gè)原因。
select函數(shù)執(zhí)行流程
- 用戶空間創(chuàng)建fd_set,把需要監(jiān)聽的位置置1,比如 1,2,5
- 用戶空間拷貝fd_set(注冊的事件集合)到內(nèi)核空間
- 內(nèi)核遍歷所有fd文件,并將當(dāng)前進(jìn)程掛到每個(gè)fd的等待隊(duì)列中,當(dāng)某個(gè)fd文件設(shè)備收到消息后,會喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程,那么當(dāng)前進(jìn)程就會被喚醒
- 內(nèi)核如果遍歷完所有的fd沒有I/O事件,則當(dāng)前進(jìn)程進(jìn)入睡眠,當(dāng)有某個(gè)fd文件有I/O事件或當(dāng)前進(jìn)程睡眠超時(shí)后,當(dāng)前進(jìn)程重新喚醒再次遍歷所有fd文件
- 內(nèi)核有事件產(chǎn)生,會把fd_set中有事件的位置保留為1,沒有事件的位置擦除為0.
- 內(nèi)核拷貝fd_set給用戶空間
- 用戶空間線程被喚醒,遍歷fd_set為1的位置,確認(rèn)是哪些fd有就緒事件,然后開始處理
- 用戶空間處理完事件,再一次將要監(jiān)聽的fd_set設(shè)置為1,重復(fù)之前的監(jiān)聽動作
根據(jù)上面可以很清楚的看出整個(gè)執(zhí)行流程在用戶空間和內(nèi)核空間的切換。
select函數(shù)的缺點(diǎn)
- 單個(gè)進(jìn)程所打開的FD是有限制的,通過 FD_SETSIZE 設(shè)置,默認(rèn)1024
- 每次調(diào)用 select,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在 fd 很多時(shí)會很大
- 每次調(diào)用select都需要將進(jìn)程加入到所有監(jiān)視socket的等待隊(duì)列,每次喚醒都需要從每個(gè)隊(duì)列中移除
- select函數(shù)在每次調(diào)用之前都要對參數(shù)進(jìn)行重新設(shè)定,這樣做比較麻煩,而且會降低性能
- 進(jìn)程被喚醒后,程序并不知道哪些socket收到數(shù)據(jù),還需要遍歷一次
poll
poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對應(yīng)的設(shè)備狀態(tài), 但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的
poll運(yùn)行流程
①創(chuàng)建pollfd數(shù)組, 向其中添加關(guān)注的fd信息,數(shù)組大小自定義
②調(diào)用poll函數(shù),將pollfd數(shù)組拷貝到內(nèi)核空間,轉(zhuǎn)鏈表存儲,無上限
③內(nèi)核遍歷fd,判斷是否就緒
④數(shù)據(jù)就緒或超時(shí)后,拷貝pollfd數(shù)組到用戶空間,返回就緒fd數(shù)量n
⑤用戶進(jìn)程判斷n是否大于0
⑥大于0則遍歷pollfd數(shù)組,找到就緒的fd
與select對比
- select模式中的fd_ set大小固定為1024,而pollfd在內(nèi)核中采用鏈表,理論上無上限.
- 監(jiān)聽FD越多,每次遍歷消耗時(shí)間也越久,性能反而會下降
poll還是沒有解決需要遍歷判斷fd事件的方式,只是增加了監(jiān)聽數(shù)量,在fd很多的情況下,性能下降的更加嚴(yán)重
epoll
epoll可以理解為event pool,不同與select、poll的輪詢機(jī)制,epoll采用的是事件驅(qū)動機(jī)制,每個(gè)fd上有注冊有回調(diào)函數(shù),當(dāng)網(wǎng)卡接收到數(shù)據(jù)時(shí)會回調(diào)該函數(shù),同時(shí)將該fd的引用放入rdlist就緒列表中。
當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。
他主要有三個(gè)函數(shù),epoll的執(zhí)行流程
- 調(diào)用epoll_create創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體有一個(gè)監(jiān)聽事件紅黑色,和一個(gè)就緒鏈表(這個(gè)鏈表只會存放就緒fd,避免我們無效的遍歷所有fd)
- 調(diào)用epoll_ctl向eventpoll中注冊一個(gè)監(jiān)聽的fd,并且注冊上fd對應(yīng)事件的回調(diào)函數(shù)。
- 調(diào)用epoll_wait開始阻塞等待事件到來
- 內(nèi)核將監(jiān)聽到的事件添加一份到就緒隊(duì)列l(wèi)ist_head
- 內(nèi)核喚醒用戶線程,并將就緒鏈表拷貝到用戶空間
- 用戶應(yīng)用只需要關(guān)心這些就緒的fd事件,直接取出結(jié)構(gòu)體里關(guān)聯(lián)的回調(diào)函數(shù)進(jìn)行回調(diào)即可處理事件。
對應(yīng)的redis的server執(zhí)行流程
- 調(diào)用epoll_create創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體
- 調(diào)用epoll_ctl向eventpoll中注冊一個(gè)監(jiān)聽連接的serverSocket,并關(guān)聯(lián)上處理accept事件的函數(shù)
- 調(diào)用epoll_wait阻塞等待fd事件(等待客戶端連接)
- 用戶程序被喚醒,事件到來(現(xiàn)在只有連接事件)。根據(jù)生成的客戶端的FD,調(diào)用epoll_ctl注冊一個(gè)監(jiān)聽,并且關(guān)聯(lián)上處理read事件的函數(shù)和處理write事件的函數(shù)。
- 繼續(xù)調(diào)用epoll_wait阻塞等待fd事件(等待客戶端連接或客戶端命令執(zhí)行請求)
- 用戶程序被喚醒,事件到來(連接事件或者命令執(zhí)行請求),假設(shè)是客戶端執(zhí)行請求事件,根據(jù)客戶端的fd對應(yīng)的read事件直接調(diào)用綁定的回調(diào)函數(shù)來處理,將結(jié)果再寫回到fd緩存中。
- 繼續(xù)調(diào)用epoll_wait等待accept,read,write事件。
epoll優(yōu)點(diǎn)
- EPOLL支持的最大文件描述符上限是整個(gè)系統(tǒng)最大可打開的文件數(shù)目, 1G內(nèi)存理論上最大創(chuàng)建10萬個(gè)文件描述符
- 每個(gè)文件描述符上都有一個(gè)callback函數(shù),當(dāng)socket有事件發(fā)生時(shí)會回調(diào)這個(gè)函數(shù)將該fd的引用添加到就緒列表中,select和poll并不會明確指出是哪些文件描述符就緒,而epoll會。造成的區(qū)別就是,系統(tǒng)調(diào)用返回后,調(diào)用select和poll的程序需要遍歷監(jiān)聽的整個(gè)文件描述符找到是誰處于就緒,而epoll則直接處理即可
- select、poll采用輪詢的方式來檢查文件描述符是否處于就緒態(tài),而epoll采用回調(diào)機(jī)制。造成的結(jié)果就是,隨著fd的增加,select和poll的效率會線性降低,而epoll不會受到太大影響,除非活躍的socket很多
讀事件很好理解,有一個(gè)讀事件就立馬處理請求,怎么理解寫事件?
當(dāng)socket 寫緩沖區(qū)已滿,假如設(shè)置了非阻塞I/O,應(yīng)用程序調(diào)用send會返回EAGAIN,告訴應(yīng)用程序?qū)懢彌_區(qū)已滿,下次再來嘗試調(diào)用,這時(shí)候就有一個(gè)嘗試的時(shí)機(jī)問題,應(yīng)用程序怎么知道socket 緩沖區(qū)可寫呢?如果頻繁調(diào)用send,會浪費(fèi)CPU。這時(shí)候,epoll就排上用場了,對socket 設(shè)置寫事件,并添加到 epoll中,應(yīng)用程序調(diào)用epoll_wait,當(dāng)該socket 的寫緩沖有空余時(shí),就返回對應(yīng)的寫事件,應(yīng)用程序這時(shí)候就可以調(diào)用send,發(fā)送數(shù)據(jù)。
所以寫事件是用來告訴程序,寫緩沖是空余的。一般情況下fd都是有寫事件的。但是在寫緩沖區(qū)滿了的時(shí)候,就會頻繁觸發(fā)寫事件。所以我們可以一開始不監(jiān)聽寫事件,直到發(fā)現(xiàn)數(shù)據(jù)量可能大于緩沖區(qū),再監(jiān)聽寫事件
參考:高效處理寫事件
參考
select poll epoll
黑馬多路復(fù)用視頻