深入講解Netty那些事兒之從內(nèi)核角度看IO模型(下)
接上文深入講解Netty那些事兒之從內(nèi)核角度看IO模型(上)
epoll
通過上邊對select,poll
核心原理的介紹,我們看到select,poll
的性能瓶頸主要體現(xiàn)在下面三個(gè)地方:
因?yàn)閮?nèi)核不會保存我們要監(jiān)聽的
socket
集合,所以在每次調(diào)用select,poll
的時(shí)候都需要傳入,傳出全量的socket
文件描述符集合。這導(dǎo)致了大量的文件描述符在用戶空間
和內(nèi)核空間
頻繁的來回復(fù)制。由于內(nèi)核不會通知具體
IO就緒
的socket
,只是在這些IO就緒
的socket上打好標(biāo)記,所以當(dāng)select
系統(tǒng)調(diào)用返回時(shí),在用戶空間
還是需要完整遍歷
一遍socket
文件描述符集合來獲取具體IO就緒
的socket
。在
內(nèi)核空間
中也是通過遍歷的方式來得到IO就緒
的socket
。
下面我們來看下epoll
是如何解決這些問題的。在介紹epoll
的核心原理之前,我們需要介紹下理解epoll
工作過程所需要的一些核心基礎(chǔ)知識。
Socket的創(chuàng)建
服務(wù)端線程調(diào)用accept
系統(tǒng)調(diào)用后開始阻塞
,當(dāng)有客戶端連接上來并完成TCP三次握手
后,內(nèi)核
會創(chuàng)建一個(gè)對應(yīng)的Socket
作為服務(wù)端與客戶端通信的內(nèi)核
接口。
在Linux內(nèi)核的角度看來,一切皆是文件,Socket
也不例外,當(dāng)內(nèi)核創(chuàng)建出Socket
之后,會將這個(gè)Socket
放到當(dāng)前進(jìn)程所打開的文件列表中管理起來。
下面我們來看下進(jìn)程管理這些打開的文件列表相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)是什么樣的?在了解完這些數(shù)據(jù)結(jié)構(gòu)后,我們會更加清晰的理解Socket
在內(nèi)核中所發(fā)揮的作用。并且對后面我們理解epoll
的創(chuàng)建過程有很大的幫助。
進(jìn)程中管理文件列表結(jié)構(gòu)

struct tast_struct
是內(nèi)核中用來表示進(jìn)程的一個(gè)數(shù)據(jù)結(jié)構(gòu),它包含了進(jìn)程的所有信息。本小節(jié)我們只列出和文件管理相關(guān)的屬性。
其中進(jìn)程內(nèi)打開的所有文件是通過一個(gè)數(shù)組fd_array
來進(jìn)行組織管理,數(shù)組的下標(biāo)即為我們常提到的文件描述符
,數(shù)組中存放的是對應(yīng)的文件數(shù)據(jù)結(jié)構(gòu)struct file
。每打開一個(gè)文件,內(nèi)核都會創(chuàng)建一個(gè)struct file
與之對應(yīng),并在fd_array
中找到一個(gè)空閑位置分配給它,數(shù)組中對應(yīng)的下標(biāo),就是我們在用戶空間
用到的文件描述符
。
對于任何一個(gè)進(jìn)程,默認(rèn)情況下,文件描述符?
0
表示?stdin 標(biāo)準(zhǔn)輸入
,文件描述符?1
表示stdout 標(biāo)準(zhǔn)輸出
,文件描述符2
表示stderr 標(biāo)準(zhǔn)錯(cuò)誤輸出
。
進(jìn)程中打開的文件列表fd_array
定義在內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct files_struct
中,在struct fdtable
結(jié)構(gòu)中有一個(gè)指針struct fd **fd
指向fd_array
。
由于本小節(jié)討論的是內(nèi)核網(wǎng)絡(luò)系統(tǒng)部分的數(shù)據(jù)結(jié)構(gòu),所以這里拿Socket
文件類型來舉例說明:
用于封裝文件元信息的內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct file
中的private_data
指針指向具體的Socket
結(jié)構(gòu)。
struct file
中的file_operations
屬性定義了文件的操作函數(shù),不同的文件類型,對應(yīng)的file_operations
是不同的,針對Socket
文件類型,這里的file_operations
指向socket_file_ops
。
我們在
用戶空間
對Socket
發(fā)起的讀寫等系統(tǒng)調(diào)用,進(jìn)入內(nèi)核首先會調(diào)用的是Socket
對應(yīng)的struct file
中指向的socket_file_ops
。比如:對Socket
發(fā)起write
寫操作,在內(nèi)核中首先被調(diào)用的就是socket_file_ops
中定義的sock_write_iter
。Socket
發(fā)起read
讀操作內(nèi)核中對應(yīng)的則是sock_read_iter
。
Socket內(nèi)核結(jié)構(gòu)

在我們進(jìn)行網(wǎng)絡(luò)程序的編寫時(shí)會首先創(chuàng)建一個(gè)Socket
,然后基于這個(gè)Socket
進(jìn)行bind
,listen
,我們先將這個(gè)Socket
稱作為監(jiān)聽Socket
。
當(dāng)我們調(diào)用
accept
后,內(nèi)核會基于監(jiān)聽Socket
創(chuàng)建出來一個(gè)新的Socket
專門用于與客戶端之間的網(wǎng)絡(luò)通信。并將監(jiān)聽Socket
中的Socket操作函數(shù)集合
(inet_stream_ops
)ops
賦值到新的Socket
的ops
屬性中。
這里需要注意的是,
監(jiān)聽的 socket
和真正用來網(wǎng)絡(luò)通信的?Socket
,是兩個(gè) Socket,一個(gè)叫作監(jiān)聽 Socket
,一個(gè)叫作已連接的Socket
。
接著內(nèi)核會為
已連接的Socket
創(chuàng)建struct file
并初始化,并把Socket文件操作函數(shù)集合(socket_file_ops
)賦值給struct file
中的f_ops
指針。然后將struct socket
中的file
指針指向這個(gè)新分配申請的struct file
結(jié)構(gòu)體。
內(nèi)核會維護(hù)兩個(gè)隊(duì)列:
一個(gè)是已經(jīng)完成
TCP三次握手
,連接狀態(tài)處于established
的連接隊(duì)列。內(nèi)核中為icsk_accept_queue
。一個(gè)是還沒有完成
TCP三次握手
,連接狀態(tài)處于syn_rcvd
的半連接隊(duì)列。
然后調(diào)用
socket->ops->accept
,從Socket內(nèi)核結(jié)構(gòu)圖
中我們可以看到其實(shí)調(diào)用的是inet_accept
,該函數(shù)會在icsk_accept_queue
中查找是否有已經(jīng)建立好的連接,如果有的話,直接從icsk_accept_queue
中獲取已經(jīng)創(chuàng)建好的struct sock
。并將這個(gè)struct sock
對象賦值給struct socket
中的sock
指針。
struct sock
在struct socket
中是一個(gè)非常核心的內(nèi)核對象,正是在這里定義了我們在介紹網(wǎng)絡(luò)包的接收發(fā)送流程
中提到的接收隊(duì)列
,發(fā)送隊(duì)列
,等待隊(duì)列
,數(shù)據(jù)就緒回調(diào)函數(shù)指針
,內(nèi)核協(xié)議棧操作函數(shù)集合
根據(jù)創(chuàng)建
Socket
時(shí)發(fā)起的系統(tǒng)調(diào)用sock_create
中的protocol
參數(shù)(對于TCP協(xié)議
這里的參數(shù)值為SOCK_STREAM
)查找到對于 tcp 定義的操作方法實(shí)現(xiàn)集合?inet_stream_ops
?和tcp_prot
。并把它們分別設(shè)置到socket->ops
和sock->sk_prot
上。
這里可以回看下本小節(jié)開頭的《Socket內(nèi)核結(jié)構(gòu)圖》捋一下他們之間的關(guān)系。
socket
相關(guān)的操作接口定義在inet_stream_ops
函數(shù)集合中,負(fù)責(zé)對上給用戶提供接口。而socket
與內(nèi)核協(xié)議棧之間的操作接口定義在struct sock
中的sk_prot
指針上,這里指向tcp_prot
協(xié)議操作函數(shù)集合。
之前提到的對
Socket
發(fā)起的系統(tǒng)IO調(diào)用,在內(nèi)核中首先會調(diào)用Socket
的文件結(jié)構(gòu)struct file
中的file_operations
文件操作集合,然后調(diào)用struct socket
中的ops
指向的inet_stream_ops
socket操作函數(shù),最終調(diào)用到struct sock
中sk_prot
指針指向的tcp_prot
內(nèi)核協(xié)議棧操作函數(shù)接口集合。

將
struct sock
?對象中的sk_data_ready
?函數(shù)指針設(shè)置為?sock_def_readable
,在Socket
數(shù)據(jù)就緒的時(shí)候內(nèi)核會回調(diào)該函數(shù)。struct sock
中的等待隊(duì)列
中存放的是系統(tǒng)IO調(diào)用發(fā)生阻塞的進(jìn)程fd
,以及相應(yīng)的回調(diào)函數(shù)
。記住這個(gè)地方,后邊介紹epoll的時(shí)候我們還會提到!
當(dāng)
struct file
,struct socket
,struct sock
這些核心的內(nèi)核對象創(chuàng)建好之后,最后就是把socket
對象對應(yīng)的struct file
放到進(jìn)程打開的文件列表fd_array
中。隨后系統(tǒng)調(diào)用accept
返回socket
的文件描述符fd
給用戶程序。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ?


阻塞IO中用戶進(jìn)程阻塞以及喚醒原理
在前邊小節(jié)我們介紹阻塞IO
的時(shí)候提到,當(dāng)用戶進(jìn)程發(fā)起系統(tǒng)IO調(diào)用時(shí),這里我們拿read
舉例,用戶進(jìn)程會在內(nèi)核態(tài)
查看對應(yīng)Socket
接收緩沖區(qū)是否有數(shù)據(jù)到來。
Socket
接收緩沖區(qū)有數(shù)據(jù),則拷貝數(shù)據(jù)到用戶空間
,系統(tǒng)調(diào)用返回。Socket
接收緩沖區(qū)沒有數(shù)據(jù),則用戶進(jìn)程讓出CPU
進(jìn)入阻塞狀態(tài)
,當(dāng)數(shù)據(jù)到達(dá)接收緩沖區(qū)時(shí),用戶進(jìn)程會被喚醒,從阻塞狀態(tài)
進(jìn)入就緒狀態(tài)
,等待CPU調(diào)度。
本小節(jié)我們就來看下用戶進(jìn)程是如何阻塞
在Socket
上,又是如何在Socket
上被喚醒的。理解這個(gè)過程很重要,對我們理解epoll的事件通知過程很有幫助
首先我們在用戶進(jìn)程中對
Socket
進(jìn)行read
系統(tǒng)調(diào)用時(shí),用戶進(jìn)程會從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
。在進(jìn)程的
struct task_struct
結(jié)構(gòu)找到fd_array
,并根據(jù)Socket
的文件描述符fd
找到對應(yīng)的struct file
,調(diào)用struct file
中的文件操作函數(shù)結(jié)合file_operations
,read
系統(tǒng)調(diào)用對應(yīng)的是sock_read_iter
。在
sock_read_iter
函數(shù)中找到struct file
指向的struct socket
,并調(diào)用socket->ops->recvmsg
,這里我們知道調(diào)用的是inet_stream_ops
集合中定義的inet_recvmsg
。在
inet_recvmsg
中會找到struct sock
,并調(diào)用sock->skprot->recvmsg
,這里調(diào)用的是tcp_prot
集合中定義的tcp_recvmsg
函數(shù)。
整個(gè)調(diào)用過程可以參考上邊的《系統(tǒng)IO調(diào)用結(jié)構(gòu)圖》
熟悉了內(nèi)核函數(shù)調(diào)用棧后,我們來看下系統(tǒng)IO調(diào)用在tcp_recvmsg
內(nèi)核函數(shù)中是如何將用戶進(jìn)程給阻塞掉的

首先會在DEFINE_WAIT
中創(chuàng)建struct sock
中等待隊(duì)列上的等待類型wait_queue_t
。
等待類型wait_queue_t
中的private
用來關(guān)聯(lián)阻塞
在當(dāng)前socket
上的用戶進(jìn)程fd
。func
用來關(guān)聯(lián)等待項(xiàng)上注冊的回調(diào)函數(shù)。這里注冊的是autoremove_wake_function
。
調(diào)用
sk_sleep(sk)
獲取struct sock
對象中的等待隊(duì)列頭指針wait_queue_head_t
。調(diào)用
prepare_to_wait
將新創(chuàng)建的等待項(xiàng)wait_queue_t
插入到等待隊(duì)列中,并將進(jìn)程設(shè)置為可打斷?INTERRUPTIBL
。調(diào)用
sk_wait_event
讓出CPU,進(jìn)程進(jìn)入睡眠狀態(tài)。
用戶進(jìn)程的阻塞過程
我們就介紹完了,關(guān)鍵是要理解記住struct sock
中定義的等待隊(duì)列上的等待類型wait_queue_t
的結(jié)構(gòu)。后面epoll
的介紹中我們還會用到它。
下面我們接著介紹當(dāng)數(shù)據(jù)就緒后,用戶進(jìn)程是如何被喚醒的
在本文開始介紹《網(wǎng)絡(luò)包接收過程》這一小節(jié)中我們提到:
當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)網(wǎng)卡時(shí),網(wǎng)卡通過
DMA
的方式將數(shù)據(jù)放到RingBuffer
中。然后向CPU發(fā)起硬中斷,在硬中斷響應(yīng)程序中創(chuàng)建
sk_buffer
,并將網(wǎng)絡(luò)數(shù)據(jù)拷貝至sk_buffer
中。隨后發(fā)起軟中斷,內(nèi)核線程
ksoftirqd
響應(yīng)軟中斷,調(diào)用poll函數(shù)
將sk_buffer
送往內(nèi)核協(xié)議棧做層層協(xié)議處理。在傳輸層
tcp_rcv 函數(shù)
中,去掉TCP頭,根據(jù)四元組(源IP,源端口,目的IP,目的端口)
查找對應(yīng)的Socket
。最后將
sk_buffer
放到Socket
中的接收隊(duì)列里。
上邊這些過程是內(nèi)核接收網(wǎng)絡(luò)數(shù)據(jù)的完整過程,下邊我們來看下,當(dāng)數(shù)據(jù)包接收完畢后,用戶進(jìn)程是如何被喚醒的。

當(dāng)軟中斷將
sk_buffer
放到Socket
的接收隊(duì)列上時(shí),接著就會調(diào)用數(shù)據(jù)就緒函數(shù)回調(diào)指針sk_data_ready
,前邊我們提到,這個(gè)函數(shù)指針在初始化的時(shí)候指向了sock_def_readable
函數(shù)。在
sock_def_readable
函數(shù)中會去獲取socket->sock->sk_wq
等待隊(duì)列。在wake_up_common
函數(shù)中從等待隊(duì)列sk_wq
中找出一個(gè)
等待項(xiàng)wait_queue_t
,回調(diào)注冊在該等待項(xiàng)上的func
回調(diào)函數(shù)(wait_queue_t->func
),創(chuàng)建等待項(xiàng)wait_queue_t
是我們提到,這里注冊的回調(diào)函數(shù)是autoremove_wake_function
。
即使是有多個(gè)進(jìn)程都阻塞在同一個(gè) socket 上,也只喚醒 1 個(gè)進(jìn)程。其作用是為了避免驚群。
在
autoremove_wake_function
函數(shù)中,根據(jù)等待項(xiàng)wait_queue_t
上的private
關(guān)聯(lián)的阻塞進(jìn)程fd
調(diào)用try_to_wake_up
喚醒阻塞在該Socket
上的進(jìn)程。
記住
wait_queue_t
中的func
函數(shù)指針,在epoll
中這里會注冊epoll
的回調(diào)函數(shù)。
現(xiàn)在理解epoll
所需要的基礎(chǔ)知識我們就介紹完了,嘮叨了這么多,下面終于正式進(jìn)入本小節(jié)的主題epoll
了。
epoll_create創(chuàng)建epoll對象
epoll_create
是內(nèi)核提供給我們創(chuàng)建epoll
對象的一個(gè)系統(tǒng)調(diào)用,當(dāng)我們在用戶進(jìn)程中調(diào)用epoll_create
時(shí),內(nèi)核會為我們創(chuàng)建一個(gè)struct eventpoll
對象,并且也有相應(yīng)的struct file
與之關(guān)聯(lián),同樣需要把這個(gè)struct eventpoll
對象所關(guān)聯(lián)的struct file
放入進(jìn)程打開的文件列表fd_array
中管理。
熟悉了
Socket
的創(chuàng)建邏輯,epoll
的創(chuàng)建邏輯也就不難理解了。
struct eventpoll
對象關(guān)聯(lián)的struct file
中的file_operations 指針
指向的是eventpoll_fops
操作函數(shù)集合。

wait_queue_head_t wq:
epoll中的等待隊(duì)列,隊(duì)列里存放的是阻塞
在epoll
上的用戶進(jìn)程。在IO就緒
的時(shí)候epoll
可以通過這個(gè)隊(duì)列找到這些阻塞
的進(jìn)程并喚醒它們,從而執(zhí)行IO調(diào)用
讀寫Socket
上的數(shù)據(jù)。
這里注意與
Socket
中的等待隊(duì)列區(qū)分?。?!
struct list_head rdllist:
epoll中的就緒隊(duì)列,隊(duì)列里存放的是都是IO就緒
的Socket
,被喚醒的用戶進(jìn)程可以直接讀取這個(gè)隊(duì)列獲取IO活躍
的Socket
。無需再次遍歷整個(gè)Socket
集合。
這里正是
epoll
比select ,poll
高效之處,select ,poll
返回的是全部的socket
連接,我們需要在用戶空間
再次遍歷找出真正IO活躍
的Socket
連接。而epoll
只是返回IO活躍
的Socket
連接。用戶進(jìn)程可以直接進(jìn)行IO操作。
struct rb_root rbr :
?由于紅黑樹在查找
,插入
,刪除
等綜合性能方面是最優(yōu)的,所以epoll內(nèi)部使用一顆紅黑樹來管理海量的Socket
連接。
select
用數(shù)組
管理連接,poll
用鏈表
管理連接。
epoll_ctl向epoll對象中添加監(jiān)聽的Socket
當(dāng)我們調(diào)用epoll_create
在內(nèi)核中創(chuàng)建出epoll
對象struct eventpoll
后,我們就可以利用epoll_ctl
向epoll
中添加我們需要管理的Socket
連接了。
首先要在epoll內(nèi)核中創(chuàng)建一個(gè)表示
Socket連接
的數(shù)據(jù)結(jié)構(gòu)struct epitem
,而在epoll
中為了綜合性能的考慮,采用一顆紅黑樹來管理這些海量socket連接
。所以struct epitem
是一個(gè)紅黑樹節(jié)點(diǎn)。

這里重點(diǎn)記住
struct epitem
結(jié)構(gòu)中的rdllink
以及epoll_filefd
成員,后面我們會用到。
在內(nèi)核中創(chuàng)建完表示
Socket連接
的數(shù)據(jù)結(jié)構(gòu)struct epitem
后,我們就需要在Socket
中的等待隊(duì)列上創(chuàng)建等待項(xiàng)wait_queue_t
并且注冊epoll的回調(diào)函數(shù)ep_poll_callback
。
通過《阻塞IO中用戶進(jìn)程阻塞以及喚醒原理》
小節(jié)的鋪墊,我想大家已經(jīng)猜到這一步的意義所在了吧!當(dāng)時(shí)在等待項(xiàng)wait_queue_t
中注冊的是autoremove_wake_function
回調(diào)函數(shù)。還記得嗎?
epoll的回調(diào)函數(shù)
ep_poll_callback
正是epoll
同步IO事件通知機(jī)制的核心所在,也是區(qū)別于select,poll
采用內(nèi)核輪詢方式的根本性能差異所在。

這里又出現(xiàn)了一個(gè)新的數(shù)據(jù)結(jié)構(gòu)struct eppoll_entry
,那它的作用是干什么的呢?大家可以結(jié)合上圖先猜測下它的作用!
我們知道socket->sock->sk_wq
等待隊(duì)列中的類型是wait_queue_t
,我們需要在struct epitem
所表示的socket
的等待隊(duì)列上注冊epoll
回調(diào)函數(shù)ep_poll_callback
。
這樣當(dāng)數(shù)據(jù)到達(dá)socket
中的接收隊(duì)列時(shí),內(nèi)核會回調(diào)sk_data_ready
,在阻塞IO中用戶進(jìn)程阻塞以及喚醒原理
這一小節(jié)中,我們知道這個(gè)sk_data_ready
函數(shù)指針會指向sk_def_readable
函數(shù),在sk_def_readable
中會回調(diào)注冊在等待隊(duì)列里的等待項(xiàng)wait_queue_t -> func
回調(diào)函數(shù)ep_poll_callback
。在ep_poll_callback
中需要找到epitem
,將IO就緒
的epitem
放入epoll
中的就緒隊(duì)列中。
而socket
等待隊(duì)列中類型是wait_queue_t
無法關(guān)聯(lián)到epitem
。所以就出現(xiàn)了struct eppoll_entry
結(jié)構(gòu)體,它的作用就是關(guān)聯(lián)Socket
等待隊(duì)列中的等待項(xiàng)wait_queue_t
和epitem
。
這樣在ep_poll_callback
回調(diào)函數(shù)中就可以根據(jù)Socket
等待隊(duì)列中的等待項(xiàng)wait
,通過container_of宏
找到eppoll_entry
,繼而找到epitem
了。
container_of
在Linux內(nèi)核中是一個(gè)常用的宏,用于從包含在某個(gè)結(jié)構(gòu)中的指針獲得結(jié)構(gòu)本身的指針,通俗地講就是通過結(jié)構(gòu)體變量中某個(gè)成員的首地址進(jìn)而獲得整個(gè)結(jié)構(gòu)體變量的首地址。
這里需要注意下這次等待項(xiàng)
wait_queue_t
中的private
設(shè)置的是null
,因?yàn)檫@里Socket
是交給epoll
來管理的,阻塞在Socket
上的進(jìn)程是也由epoll
來喚醒。在等待項(xiàng)wait_queue_t
注冊的func
是ep_poll_callback
而不是autoremove_wake_function
,阻塞進(jìn)程
并不需要autoremove_wake_function
來喚醒,所以這里設(shè)置private
為null
當(dāng)在
Socket
的等待隊(duì)列中創(chuàng)建好等待項(xiàng)wait_queue_t
并且注冊了epoll
的回調(diào)函數(shù)ep_poll_callback
,然后又通過eppoll_entry
關(guān)聯(lián)了epitem
后。剩下要做的就是將epitem
插入到epoll
中的紅黑樹struct rb_root rbr
中。
這里可以看到
epoll
另一個(gè)優(yōu)化的地方,epoll
將所有的socket
連接通過內(nèi)核中的紅黑樹來集中管理。每次添加或者刪除socket連接
都是增量添加刪除,而不是像select,poll
那樣每次調(diào)用都是全量socket連接
集合傳入內(nèi)核。避免了頻繁大量
的內(nèi)存拷貝
。
epoll_wait同步阻塞獲取IO就緒的Socket
用戶程序調(diào)用
epoll_wait
后,內(nèi)核首先會查找epoll中的就緒隊(duì)列eventpoll->rdllist
是否有IO就緒
的epitem
。epitem
里封裝了socket
的信息。如果就緒隊(duì)列中有就緒的epitem
,就將就緒的socket
信息封裝到epoll_event
返回。如果
eventpoll->rdllist
就緒隊(duì)列中沒有IO就緒
的epitem
,則會創(chuàng)建等待項(xiàng)wait_queue_t
,將用戶進(jìn)程的fd
關(guān)聯(lián)到wait_queue_t->private
上,并在等待項(xiàng)wait_queue_t->func
上注冊回調(diào)函數(shù)default_wake_function
。最后將等待項(xiàng)添加到epoll
中的等待隊(duì)列中。用戶進(jìn)程讓出CPU,進(jìn)入阻塞狀態(tài)
。

這里和
阻塞IO模型
中的阻塞原理是一樣的,只不過在阻塞IO模型
中注冊到等待項(xiàng)wait_queue_t->func
上的是autoremove_wake_function
,并將等待項(xiàng)添加到socket
中的等待隊(duì)列中。這里注冊的是default_wake_function
,將等待項(xiàng)添加到epoll
中的等待隊(duì)列上。

前邊做了那么多的知識鋪墊,下面終于到了
epoll
的整個(gè)工作流程了:

當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包在軟中斷中經(jīng)過內(nèi)核協(xié)議棧的處理到達(dá)
socket
的接收緩沖區(qū)時(shí),緊接著會調(diào)用socket的數(shù)據(jù)就緒回調(diào)指針sk_data_ready
,回調(diào)函數(shù)為sock_def_readable
。在socket
的等待隊(duì)列中找出等待項(xiàng),其中等待項(xiàng)中注冊的回調(diào)函數(shù)為ep_poll_callback
。在回調(diào)函數(shù)
ep_poll_callback
中,根據(jù)struct eppoll_entry
中的struct wait_queue_t wait
通過container_of宏
找到eppoll_entry
對象并通過它的base
指針找到封裝socket
的數(shù)據(jù)結(jié)構(gòu)struct epitem
,并將它加入到epoll
中的就緒隊(duì)列rdllist
中。隨后查看
epoll
中的等待隊(duì)列中是否有等待項(xiàng),也就是說查看是否有進(jìn)程阻塞在epoll_wait
上等待IO就緒
的socket
。如果沒有等待項(xiàng),則軟中斷處理完成。如果有等待項(xiàng),則回到注冊在等待項(xiàng)中的回調(diào)函數(shù)
default_wake_function
,在回調(diào)函數(shù)中喚醒阻塞進(jìn)程
,并將就緒隊(duì)列rdllist
中的epitem
的IO就緒
socket信息封裝到struct epoll_event
中返回。用戶進(jìn)程拿到
epoll_event
獲取IO就緒
的socket,發(fā)起系統(tǒng)IO調(diào)用讀取數(shù)據(jù)。
再談水平觸發(fā)和邊緣觸發(fā)
網(wǎng)上有大量的關(guān)于這兩種模式的講解,大部分講的比較模糊,感覺只是強(qiáng)行從概念上進(jìn)行描述,看完讓人難以理解。所以在這里,筆者想結(jié)合上邊epoll
的工作過程,再次對這兩種模式做下自己的解讀,力求清晰的解釋出這兩種工作模式的異同。
經(jīng)過上邊對epoll
工作過程的詳細(xì)解讀,我們知道,當(dāng)我們監(jiān)聽的socket
上有數(shù)據(jù)到來時(shí),軟中斷會執(zhí)行epoll
的回調(diào)函數(shù)ep_poll_callback
,在回調(diào)函數(shù)中會將epoll
中描述socket信息
的數(shù)據(jù)結(jié)構(gòu)epitem
插入到epoll
中的就緒隊(duì)列rdllist
中。隨后用戶進(jìn)程從epoll
的等待隊(duì)列中被喚醒,epoll_wait
將IO就緒
的socket
返回給用戶進(jìn)程,隨即epoll_wait
會清空rdllist
。
水平觸發(fā)和邊緣觸發(fā)最關(guān)鍵的區(qū)別就在于當(dāng)socket
中的接收緩沖區(qū)還有數(shù)據(jù)可讀時(shí)。epoll_wait
是否會清空rdllist
。
水平觸發(fā):在這種模式下,用戶線程調(diào)用
epoll_wait
獲取到IO就緒
的socket后,對Socket
進(jìn)行系統(tǒng)IO調(diào)用讀取數(shù)據(jù),假設(shè)socket
中的數(shù)據(jù)只讀了一部分沒有全部讀完,這時(shí)再次調(diào)用epoll_wait
,epoll_wait
會檢查這些Socket
中的接收緩沖區(qū)是否還有數(shù)據(jù)可讀,如果還有數(shù)據(jù)可讀,就將socket
重新放回rdllist
。所以當(dāng)socket
上的IO沒有被處理完時(shí),再次調(diào)用epoll_wait
依然可以獲得這些socket
,用戶進(jìn)程可以接著處理socket
上的IO事件。邊緣觸發(fā):?在這種模式下,
epoll_wait
就會直接清空rdllist
,不管socket
上是否還有數(shù)據(jù)可讀。所以在邊緣觸發(fā)模式下,當(dāng)你沒有來得及處理socket
接收緩沖區(qū)的剩下可讀數(shù)據(jù)時(shí),再次調(diào)用epoll_wait
,因?yàn)檫@時(shí)rdlist
已經(jīng)被清空了,socket
不會再次從epoll_wait
中返回,所以用戶進(jìn)程就不會再次獲得這個(gè)socket
了,也就無法在對它進(jìn)行IO處理了。除非,這個(gè)socket
上有新的IO數(shù)據(jù)到達(dá),根據(jù)epoll
的工作過程,該socket
會被再次放入rdllist
中。
如果你在
邊緣觸發(fā)模式
下,處理了部分socket
上的數(shù)據(jù),那么想要處理剩下部分的數(shù)據(jù),就只能等到這個(gè)socket
上再次有網(wǎng)絡(luò)數(shù)據(jù)到達(dá)。
在Netty
中實(shí)現(xiàn)的EpollSocketChannel
默認(rèn)的就是邊緣觸發(fā)
模式。JDK
的NIO
默認(rèn)是水平觸發(fā)
模式。
epoll對select,poll的優(yōu)化總結(jié)
epoll
在內(nèi)核中通過紅黑樹
管理海量的連接,所以在調(diào)用epoll_wait
獲取IO就緒
的socket時(shí),不需要傳入監(jiān)聽的socket文件描述符。從而避免了海量的文件描述符集合在用戶空間
和內(nèi)核空間
中來回復(fù)制。
select,poll
每次調(diào)用時(shí)都需要傳遞全量的文件描述符集合,導(dǎo)致大量頻繁的拷貝操作。
epoll
僅會通知IO就緒
的socket。避免了在用戶空間遍歷的開銷。
select,poll
只會在IO就緒
的socket上打好標(biāo)記,依然是全量返回,所以在用戶空間還需要用戶程序在一次遍歷全量集合找出具體IO就緒
的socket。
epoll
通過在socket
的等待隊(duì)列上注冊回調(diào)函數(shù)ep_poll_callback
通知用戶程序IO就緒
的socket。避免了在內(nèi)核中輪詢的開銷。
大部分情況下
socket
上并不總是IO活躍
的,在面對海量連接的情況下,select,poll
采用內(nèi)核輪詢的方式獲取IO活躍
的socket,無疑是性能低下的核心原因。
根據(jù)以上epoll
的性能優(yōu)勢,它是目前為止各大主流網(wǎng)絡(luò)框架,以及反向代理中間件使用到的網(wǎng)絡(luò)IO模型。
利用epoll
多路復(fù)用IO模型可以輕松的解決C10K
問題。
C100k
的解決方案也還是基于C10K
的方案,通過epoll
?配合線程池,再加上 CPU、內(nèi)存和網(wǎng)絡(luò)接口的性能和容量提升。大部分情況下,C100K
很自然就可以達(dá)到。
甚至C1000K
的解決方法,本質(zhì)上還是構(gòu)建在?epoll
?的多路復(fù)用 I/O 模型
上。只不過,除了 I/O 模型之外,還需要從應(yīng)用程序到 Linux 內(nèi)核、再到 CPU、內(nèi)存和網(wǎng)絡(luò)等各個(gè)層次的深度優(yōu)化,特別是需要借助硬件,來卸載那些原來通過軟件處理的大量功能(去掉大量的中斷響應(yīng)開銷
,以及內(nèi)核協(xié)議棧處理的開銷
)。
信號驅(qū)動IO

大家對這個(gè)裝備肯定不會陌生,當(dāng)我們?nèi)ヒ恍┟朗吵浅燥埖臅r(shí)候,點(diǎn)完餐付了錢,老板會給我們一個(gè)信號器。然后我們帶著這個(gè)信號器可以去找餐桌,或者干些其他的事情。當(dāng)信號器亮了的時(shí)候,這時(shí)代表飯餐已經(jīng)做好,我們可以去窗口取餐了。
這個(gè)典型的生活場景和我們要介紹的信號驅(qū)動IO模型
就很像。
在信號驅(qū)動IO模型
下,用戶進(jìn)程操作通過系統(tǒng)調(diào)用 sigaction 函數(shù)
發(fā)起一個(gè) IO 請求,在對應(yīng)的socket
注冊一個(gè)信號回調(diào)
,此時(shí)不阻塞
用戶進(jìn)程,進(jìn)程會繼續(xù)工作。當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí),內(nèi)核就為該進(jìn)程生成一個(gè)?SIGIO 信號
,通過信號回調(diào)通知進(jìn)程進(jìn)行相關(guān) IO 操作。
這里需要注意的是:
信號驅(qū)動式 IO 模型
依然是同步IO
,因?yàn)樗m然可以在等待數(shù)據(jù)的時(shí)候不被阻塞,也不會頻繁的輪詢,但是當(dāng)數(shù)據(jù)就緒,內(nèi)核信號通知后,用戶進(jìn)程依然要自己去讀取數(shù)據(jù),在數(shù)據(jù)拷貝階段
發(fā)生阻塞。
信號驅(qū)動 IO模型 相比于前三種 IO 模型,實(shí)現(xiàn)了在等待數(shù)據(jù)就緒時(shí),進(jìn)程不被阻塞,主循環(huán)可以繼續(xù)工作,所以
理論上
性能更佳。
但是實(shí)際上,使用TCP協(xié)議
通信時(shí),信號驅(qū)動IO模型
幾乎不會被采用
。原因如下:
信號IO 在大量 IO 操作時(shí)可能會因?yàn)樾盘栮?duì)列溢出導(dǎo)致沒法通知
SIGIO 信號
是一種 Unix 信號,信號沒有附加信息,如果一個(gè)信號源有多種產(chǎn)生信號的原因,信號接收者就無法確定究竟發(fā)生了什么。而 TCP socket 生產(chǎn)的信號事件有七種之多,這樣應(yīng)用程序收到 SIGIO,根本無從區(qū)分處理。
但信號驅(qū)動IO模型
可以用在?UDP
通信上,因?yàn)閁DP 只有一個(gè)數(shù)據(jù)請求事件
,這也就意味著在正常情況下 UDP 進(jìn)程只要捕獲 SIGIO 信號,就調(diào)用?read 系統(tǒng)調(diào)用
讀取到達(dá)的數(shù)據(jù)。如果出現(xiàn)異常,就返回一個(gè)異常錯(cuò)誤。
這里插句題外話,大家覺不覺得阻塞IO模型
在生活中的例子就像是我們在食堂排隊(duì)打飯。你自己需要排隊(duì)去打飯同時(shí)打飯師傅在配菜的過程中你需要等待。

IO多路復(fù)用模型
就像是我們在飯店門口排隊(duì)等待叫號。叫號器就好比select,poll,epoll
可以統(tǒng)一管理全部顧客的吃飯就緒
事件,客戶好比是socket
連接,誰可以去吃飯了,叫號器就通知誰。

##異步IO(AIO)
以上介紹的四種IO模型
均為同步IO
,它們都會阻塞在第二階段數(shù)據(jù)拷貝階段
。
通過在前邊小節(jié)《同步與異步》中的介紹,相信大家很容易就會理解異步IO模型
,在異步IO模型
下,IO操作在數(shù)據(jù)準(zhǔn)備階段
和數(shù)據(jù)拷貝階段
均是由內(nèi)核來完成,不會對應(yīng)用程序造成任何阻塞。應(yīng)用進(jìn)程只需要在指定的數(shù)組
中引用數(shù)據(jù)即可。
異步 IO
?與信號驅(qū)動 IO
?的主要區(qū)別在于:信號驅(qū)動 IO
?由內(nèi)核通知何時(shí)可以開始一個(gè) IO 操作
,而異步 IO
由內(nèi)核通知?IO 操作何時(shí)已經(jīng)完成
。
舉個(gè)生活中的例子:異步IO模型
就像我們?nèi)ヒ粋€(gè)高檔飯店里的包間吃飯,我們只需要坐在包間里面,點(diǎn)完餐(類比異步IO調(diào)用
)之后,我們就什么也不需要管,該喝酒喝酒,該聊天聊天,飯餐做好后服務(wù)員(類比內(nèi)核
)會自己給我們送到包間(類比用戶空間
)來。整個(gè)過程沒有任何阻塞。

異步IO
的系統(tǒng)調(diào)用需要操作系統(tǒng)內(nèi)核來支持,目前只有Window
中的IOCP
實(shí)現(xiàn)了非常成熟的異步IO機(jī)制
。
而Linux
系統(tǒng)對異步IO機(jī)制
實(shí)現(xiàn)的不夠成熟,且與NIO
的性能相比提升也不明顯。
但Linux kernel 在5.1版本由Facebook的大神Jens Axboe引入了新的異步IO庫
io_uring
?改善了原來Linux native AIO的一些性能問題。性能相比Epoll
以及之前原生的AIO
提高了不少,值得關(guān)注。
再加上信號驅(qū)動IO模型
不適用TCP協(xié)議
,所以目前大部分采用的還是IO多路復(fù)用模型
。
IO線程模型
在前邊內(nèi)容的介紹中,我們詳述了網(wǎng)絡(luò)數(shù)據(jù)包的接收和發(fā)送過程,并通過介紹5種IO模型
了解了內(nèi)核是如何讀取網(wǎng)絡(luò)數(shù)據(jù)并通知給用戶線程的。
前邊的內(nèi)容都是以內(nèi)核空間
的視角來剖析網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)模型,本小節(jié)我們站在用戶空間
的視角來看下如果對網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行收發(fā)。
相對內(nèi)核
來講,用戶空間的IO線程模型
相對就簡單一些。這些用戶空間
的IO線程模型
都是在討論當(dāng)多線程一起配合工作時(shí)誰負(fù)責(zé)接收連接,誰負(fù)責(zé)響應(yīng)IO 讀寫、誰負(fù)責(zé)計(jì)算、誰負(fù)責(zé)發(fā)送和接收,僅僅是用戶IO線程的不同分工模式罷了。
Reactor
Reactor
是利用NIO
對IO線程
進(jìn)行不同的分工:
使用前邊我們提到的
IO多路復(fù)用模型
比如select,poll,epoll,kqueue
,進(jìn)行IO事件的注冊和監(jiān)聽。將監(jiān)聽到
就緒的IO事件
分發(fā)dispatch
到各個(gè)具體的處理Handler
中進(jìn)行相應(yīng)的IO事件處理
。
通過IO多路復(fù)用技術(shù)
就可以不斷的監(jiān)聽IO事件
,不斷的分發(fā)dispatch
,就像一個(gè)反應(yīng)堆
一樣,看起來像不斷的產(chǎn)生IO事件
,因此我們稱這種模式為Reactor
模型。
下面我們來看下Reactor模型
的三種分類:
單Reactor單線程

Reactor模型
是依賴IO多路復(fù)用技術(shù)
實(shí)現(xiàn)監(jiān)聽IO事件
,從而源源不斷的產(chǎn)生IO就緒事件
,在Linux系統(tǒng)下我們使用epoll
來進(jìn)行IO多路復(fù)用
,我們以Linux系統(tǒng)為例:
單
Reactor
意味著只有一個(gè)epoll
對象,用來監(jiān)聽所有的事件,比如連接事件
,讀寫事件
。單線程
意味著只有一個(gè)線程來執(zhí)行epoll_wait
獲取IO就緒
的Socket
,然后對這些就緒的Socket
執(zhí)行讀寫,以及后邊的業(yè)務(wù)處理也依然是這個(gè)線程。
單Reactor單線程
模型就好比我們開了一個(gè)很小很小的小飯館,作為老板的我們需要一個(gè)人干所有的事情,包括:迎接顧客(accept事件
),為顧客介紹菜單等待顧客點(diǎn)菜(IO請求
),做菜(業(yè)務(wù)處理
),上菜(IO響應(yīng)
),送客(斷開連接
)。
單Reactor多線程
隨著客人的增多(并發(fā)請求
),顯然飯館里的事情只有我們一個(gè)人干(單線程
)肯定是忙不過來的,這時(shí)候我們就需要多招聘一些員工(多線程
)來幫著一起干上述的事情。
于是就有了單Reactor多線程
模型:

這種模式下,也是只有一個(gè)
epoll
對象來監(jiān)聽所有的IO事件
,一個(gè)線程來調(diào)用epoll_wait
獲取IO就緒
的Socket
。但是當(dāng)
IO就緒事件
產(chǎn)生時(shí),這些IO事件
對應(yīng)處理的業(yè)務(wù)Handler
,我們是通過線程池來執(zhí)行。這樣相比單Reactor單線程
模型提高了執(zhí)行效率,充分發(fā)揮了多核CPU的優(yōu)勢。
主從Reactor多線程
做任何事情都要區(qū)分事情的優(yōu)先級
,我們應(yīng)該優(yōu)先高效
的去做優(yōu)先級更高
的事情,而不是一股腦不分優(yōu)先級的全部去做。
當(dāng)我們的小飯館客人越來越多(并發(fā)量越來越大
),我們就需要擴(kuò)大飯店的規(guī)模,在這個(gè)過程中我們發(fā)現(xiàn),迎接客人
是飯店最重要的工作,我們要先把客人迎接進(jìn)來,不能讓客人一看人多就走掉,只要客人進(jìn)來了,哪怕菜做的慢一點(diǎn)也沒關(guān)系。
于是,主從Reactor多線程
模型就產(chǎn)生了:

我們由原來的
單Reactor
變?yōu)榱?code>多Reactor。主Reactor
用來優(yōu)先專門
做優(yōu)先級最高的事情,也就是迎接客人(處理連接事件
),對應(yīng)的處理Handler
就是圖中的acceptor
。當(dāng)創(chuàng)建好連接,建立好對應(yīng)的
socket
后,在acceptor
中將要監(jiān)聽的read事件
注冊到從Reactor
中,由從Reactor
來監(jiān)聽socket
上的讀寫
事件。最終將讀寫的業(yè)務(wù)邏輯處理交給線程池處理。
注意:這里向
從Reactor
注冊的只是read事件
,并沒有注冊write事件
,因?yàn)?code>read事件是由epoll內(nèi)核
觸發(fā)的,而write事件
則是由用戶業(yè)務(wù)線程觸發(fā)的(什么時(shí)候發(fā)送數(shù)據(jù)是由具體業(yè)務(wù)線程決定的
),所以write事件
理應(yīng)是由用戶業(yè)務(wù)線程
去注冊。
用戶線程注冊
write事件
的時(shí)機(jī)是只有當(dāng)用戶發(fā)送的數(shù)據(jù)無法一次性
全部寫入buffer
時(shí),才會去注冊write事件
,等待buffer重新可寫
時(shí),繼續(xù)寫入剩下的發(fā)送數(shù)據(jù)、如果用戶線程可以一股腦的將發(fā)送數(shù)據(jù)全部寫入buffer
,那么也就無需注冊write事件
到從Reactor
中。
主從Reactor多線程
模型是現(xiàn)在大部分主流網(wǎng)絡(luò)框架中采用的一種IO線程模型
。我們本系列的主題Netty
就是用的這種模型。
Proactor
Proactor
是基于AIO
對IO線程
進(jìn)行分工的一種模型。前邊我們介紹了異步IO模型
,它是操作系統(tǒng)內(nèi)核支持的一種全異步編程模型,在數(shù)據(jù)準(zhǔn)備階段
和數(shù)據(jù)拷貝階段
全程無阻塞。
ProactorIO線程模型
將IO事件的監(jiān)聽
,IO操作的執(zhí)行
,IO結(jié)果的dispatch
統(tǒng)統(tǒng)交給內(nèi)核
來做。

Proactor模型
組件介紹:
completion handler
?為用戶程序定義的異步IO操作回調(diào)函數(shù),在異步IO操作完成時(shí)會被內(nèi)核回調(diào)并通知IO結(jié)果。Completion Event Queue
?異步IO操作完成后,會產(chǎn)生對應(yīng)的IO完成事件
,將IO完成事件
放入該隊(duì)列中。Asynchronous Operation Processor
?負(fù)責(zé)異步IO
的執(zhí)行。執(zhí)行完成后產(chǎn)生IO完成事件
放入Completion Event Queue
?隊(duì)列中。Proactor
?是一個(gè)事件循環(huán)派發(fā)器,負(fù)責(zé)從Completion Event Queue
中獲取IO完成事件
,并回調(diào)與IO完成事件
關(guān)聯(lián)的completion handler
。Initiator
?初始化異步操作(asynchronous operation
)并通過Asynchronous Operation Processor
將completion handler
和proactor
注冊到內(nèi)核。
Proactor模型
執(zhí)行過程:
用戶線程發(fā)起
aio_read
,并告訴內(nèi)核
用戶空間中的讀緩沖區(qū)地址,以便內(nèi)核
完成IO操作
將結(jié)果放入用戶空間
的讀緩沖區(qū),用戶線程直接可以讀取結(jié)果(無任何阻塞
)。Initiator
?初始化aio_read
異步讀取操作(asynchronous operation
),并將completion handler
注冊到內(nèi)核。
在
Proactor
中我們關(guān)心的IO完成事件
:內(nèi)核已經(jīng)幫我們讀好數(shù)據(jù)并放入我們指定的讀緩沖區(qū),用戶線程可以直接讀取。在Reactor
中我們關(guān)心的是IO就緒事件
:數(shù)據(jù)已經(jīng)到來,但是需要用戶線程自己去讀取。
此時(shí)用戶線程就可以做其他事情了,無需等待IO結(jié)果。而內(nèi)核與此同時(shí)開始異步執(zhí)行IO操作。當(dāng)
IO操作
完成時(shí)會產(chǎn)生一個(gè)completion event
事件,將這個(gè)IO完成事件
放入completion event queue
中。Proactor
從completion event queue
中取出completion event
,并回調(diào)與IO完成事件
關(guān)聯(lián)的completion handler
。在
completion handler
中完成業(yè)務(wù)邏輯處理。
Reactor與Proactor對比
Reactor
是基于NIO
實(shí)現(xiàn)的一種IO線程模型
,Proactor
是基于AIO
實(shí)現(xiàn)的IO線程模型
。Reactor
關(guān)心的是IO就緒事件
,Proactor
關(guān)心的是IO完成事件
。在
Proactor
中,用戶程序需要向內(nèi)核傳遞用戶空間的讀緩沖區(qū)地址
。Reactor
則不需要。這也就導(dǎo)致了在Proactor
中每個(gè)并發(fā)操作都要求有獨(dú)立的緩存區(qū),在內(nèi)存上有一定的開銷。Proactor
?的實(shí)現(xiàn)邏輯復(fù)雜,編碼成本較?Reactor
要高很多。Proactor
?在處理高耗時(shí) IO
時(shí)的性能要高于?Reactor
,但對于低耗時(shí) IO
的執(zhí)行效率提升并不明顯
。
Netty的IO模型
在我們介紹完網(wǎng)絡(luò)數(shù)據(jù)包在內(nèi)核中的收發(fā)過程
以及五種IO模型
和兩種IO線程模型
后,現(xiàn)在我們來看下netty
中的IO模型是什么樣的。
在我們介紹Reactor IO線程模型
的時(shí)候提到有三種Reactor模型
:單Reactor單線程
,單Reactor多線程
,主從Reactor多線程
。
這三種Reactor模型
在netty
中都是支持的,但是我們常用的是主從Reactor多線程模型
。
而我們之前介紹的三種Reactor
只是一種模型,是一種設(shè)計(jì)思想。實(shí)際上各種網(wǎng)絡(luò)框架在實(shí)現(xiàn)中并不是嚴(yán)格按照模型來實(shí)現(xiàn)的,會有一些小的不同,但大體設(shè)計(jì)思想上是一樣的。
下面我們來看下netty
中的主從Reactor多線程模型
是什么樣子的?

Reactor
在netty
中是以group
的形式出現(xiàn)的,netty
中將Reactor
分為兩組,一組是MainReactorGroup
也就是我們在編碼中常??吹降?code>EventLoopGroup bossGroup,另一組是SubReactorGroup
也就是我們在編碼中常??吹降?code>EventLoopGroup workerGroup。MainReactorGroup
中通常只有一個(gè)Reactor
,專門負(fù)責(zé)做最重要的事情,也就是監(jiān)聽連接accept
事件。當(dāng)有連接事件產(chǎn)生時(shí),在對應(yīng)的處理handler acceptor
中創(chuàng)建初始化相應(yīng)的NioSocketChannel
(代表一個(gè)Socket連接
)。然后以負(fù)載均衡
的方式在SubReactorGroup
中選取一個(gè)Reactor
,注冊上去,監(jiān)聽Read事件
。
MainReactorGroup
中只有一個(gè)Reactor
的原因是,通常我們服務(wù)端程序只會綁定監(jiān)聽
一個(gè)端口,如果要綁定監(jiān)聽
多個(gè)端口,就會配置多個(gè)Reactor
。
SubReactorGroup
中有多個(gè)Reactor
,具體Reactor
的個(gè)數(shù)可以由系統(tǒng)參數(shù)?-D io.netty.eventLoopThreads
指定。默認(rèn)的Reactor
的個(gè)數(shù)為CPU核數(shù) * 2
。SubReactorGroup
中的Reactor
主要負(fù)責(zé)監(jiān)聽讀寫事件
,每一個(gè)Reactor
負(fù)責(zé)監(jiān)聽一組socket連接
。將全量的連接分?jǐn)?/code>在多個(gè)
Reactor
中。一個(gè)
Reactor
分配一個(gè)IO線程
,這個(gè)IO線程
負(fù)責(zé)從Reactor
中獲取IO就緒事件
,執(zhí)行IO調(diào)用獲取IO數(shù)據(jù)
,執(zhí)行PipeLine
。
Socket連接
在創(chuàng)建后就被固定的分配
給一個(gè)Reactor
,所以一個(gè)Socket連接
也只會被一個(gè)固定的IO線程
執(zhí)行,每個(gè)Socket連接
分配一個(gè)獨(dú)立的PipeLine
實(shí)例,用來編排這個(gè)Socket連接
上的IO處理邏輯
。這種無鎖串行化
的設(shè)計(jì)的目的是為了防止多線程并發(fā)執(zhí)行同一個(gè)socket連接上的IO邏輯處理
,防止出現(xiàn)線程安全問題
。同時(shí)使系統(tǒng)吞吐量達(dá)到最大化
由于每個(gè)
Reactor
中只有一個(gè)IO線程
,這個(gè)IO線程
既要執(zhí)行IO活躍Socket連接
對應(yīng)的PipeLine
中的ChannelHandler
,又要從Reactor
中獲取IO就緒事件
,執(zhí)行IO調(diào)用
。所以PipeLine
中ChannelHandler
中執(zhí)行的邏輯不能耗時(shí)太長,盡量將耗時(shí)的業(yè)務(wù)邏輯處理放入單獨(dú)的業(yè)務(wù)線程池中處理,否則會影響其他連接的IO讀寫
,從而近一步影響整個(gè)服務(wù)程序的IO吞吐
。
當(dāng)
IO請求
在業(yè)務(wù)線程中完成相應(yīng)的業(yè)務(wù)邏輯處理后,在業(yè)務(wù)線程中利用持有的ChannelHandlerContext
引用將響應(yīng)數(shù)據(jù)在PipeLine
中反向傳播,最終寫回給客戶端。
netty
中的IO模型
我們介紹完了,下面我們來簡單介紹下在netty
中是如何支持前邊提到的三種Reactor模型
的。
配置單Reactor單線程
配置多Reactor線程
配置主從Reactor多線程
總結(jié)
本文是一篇信息量比較大的文章,用了25
張圖,22336
個(gè)字從內(nèi)核如何處理網(wǎng)絡(luò)數(shù)據(jù)包的收發(fā)過程開始展開,隨后又在內(nèi)核角度
介紹了經(jīng)常容易混淆的阻塞與非阻塞
,同步與異步
的概念。以這個(gè)作為鋪墊,我們通過一個(gè)C10K
的問題,引出了五種IO模型
,隨后在IO多路復(fù)用
中以技術(shù)演進(jìn)的形式介紹了select,poll,epoll
的原理和它們綜合的對比。最后我們介紹了兩種IO線程模型
以及netty
中的Reactor模型
。
原文作者:bin的技術(shù)小屋
