Redis的I/O多路複用
幾種 I/O 模型
為什麼 Redis 中要使用 I/O 多路複用這種技術呢?
首先,Redis 是跑在單執行緒中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待使用者輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,
這會導致某一檔案的 I/O 阻塞導致整個程序無法對其它客戶提供服務,而 I/O 多路複用就是為了解決這個問題而出現的。
Blocking I/O
先來看一下傳統的阻塞 I/O 模型到底是如何工作的:
當使用 read 或者 write 對某一個檔案描述符(File Descriptor 以下簡稱 FD)進行讀寫時,如果當前 FD 不可讀或不可寫,整個 Redis 服務就不會對其它的操作作出響應,導致整個服務不可用。
這也就是傳統意義上的,也就是我們在程式設計中使用最多的阻塞模型:
阻塞模型雖然開發中非常常見也非常易於理解,但是由於它會影響其他 FD 對應的服務,所以在需要處理多個客戶端任務的時候,往往都不會使用阻塞模型。
I/O 多路複用
阻塞式的 I/O 模型並不能滿足這裡的需求,我們需要一種效率更高的 I/O 模型來支撐 Redis 的多個客戶(redis-cli),這裡涉及的就是 I/O 多路複用模型了:
在 I/O 多路複用模型中,最重要的函式呼叫就是 select, 該方法的能夠同時監控多個檔案描述符的可讀可寫情況,當其中的某些檔案描述符可讀或者可寫時,select 方法就會返回可讀以及可寫的檔案描述符個數與此同時也有其它的 I/O 多路複用函式 epoll/kqueue/evport,它們相比 select 效能更優秀,同時也能支撐更多的服務。
當如下任一情況發生時,會產生套接字的可讀事件:
- 該套接字的接收緩衝區中的資料位元組數大於等於套接字接收緩衝區低水位標記的大小;
- 該套接字的讀半部關閉(也就是收到了FIN),對這樣的套接字的讀操作將返回0(也就是返回EOF);
- 該套接字是一個監聽套接字且已完成的連線數不為0;
- 該套接字有錯誤待處理,對這樣的套接字的讀操作將返回-1。
當如下任一情況發生時,會產生套接字的可寫事件:
- 該套接字的傳送緩衝區中的可用空間位元組數大於等於套接字傳送緩衝區低水位標記的大小;
- 該套接字的寫半部關閉,繼續寫會產生SIGPIPE訊號;
- 非阻塞模式下,connect返回之後,該套接字連線成功或失敗;
- 該套接字有錯誤待處理,對這樣的套接字的寫操作將返回-1。
select(int nfds, fd_set *r, fd_set *w,fd_set *e, struct timeval *timeout)
對於select(),我們需要傳3個集合,r(讀),w(寫)和e。其中,r表示我們對哪些fd的可讀事件感興趣,w表示我們對哪些fd的可寫事件感興趣,每個集合其實是一個bitmap,通過0/1表示我們感興趣的fd例如,
如:我們對於fd為6的可讀事件感興趣,那麼r集合的第6個bit需要被設定為1這個系統呼叫會阻塞,直到我們感興趣的事件(至少一個)發生呼叫返回時,
核心同樣使用這3個集合來存放fd實際發生的事件資訊。也就是說,呼叫前這3個集合表示我們感興趣的事件,呼叫後這3個集合表示實際發生的事件
select為最早期的UNIX系統呼叫,它存在4個問題:
1)這3個bitmap有大小限制(FD_SETSIZE,通常為1024);
2)由於這3個集合在返回時會被核心修改,因此我們每次呼叫時都需要重新設定;
3)我們在呼叫完成後需要掃描這3個集合才能知道哪些fd的讀/寫事件發生了,一般情況下全量集合比較大而實際發生讀/寫事件的fd比較少,效率比較低下;
4)核心在每次呼叫都需要掃描這3個fd集合,然後檢視哪些fd的事件實際發生,在讀/寫比較稀疏的情況下同樣存在效率問題
由於存在這些問題,於是人們對select進行了改進,從而有了poll
參考:
連結:https://www.jianshu.com/p/311f9d276b2a