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