国产精品天干天干,亚洲毛片在线,日韩gay小鲜肉啪啪18禁,女同Gay自慰喷水

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

深入講解Netty那些事兒之從內(nèi)核角度看IO模型(下)

2022-12-01 21:21 作者:補(bǔ)給站Linux內(nèi)核  | 我要投稿

接上文深入講解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)

圖片
進(jìn)程中管理文件列表結(jié)構(gòu).png

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_iterSocket發(fā)起read讀操作內(nèi)核中對應(yīng)的則是sock_read_iter。

Socket內(nèi)核結(jié)構(gòu)

圖片
Socket內(nèi)核結(jié)構(gòu).png

在我們進(jìn)行網(wǎng)絡(luò)程序的編寫時(shí)會首先創(chuàng)建一個(gè)Socket,然后基于這個(gè)Socket進(jìn)行bind,listen,我們先將這個(gè)Socket稱作為監(jiān)聽Socket。

  1. 當(dāng)我們調(diào)用accept后,內(nèi)核會基于監(jiān)聽Socket創(chuàng)建出來一個(gè)新的Socket專門用于與客戶端之間的網(wǎng)絡(luò)通信。并將監(jiān)聽Socket中的Socket操作函數(shù)集合inet_stream_opsops賦值到新的Socketops屬性中。

這里需要注意的是,監(jiān)聽的 socket和真正用來網(wǎng)絡(luò)通信的?Socket,是兩個(gè) Socket,一個(gè)叫作監(jiān)聽 Socket,一個(gè)叫作已連接的Socket。

  1. 接著內(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ì)列。

  1. 然后調(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 sockstruct 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->opssock->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_opssocket操作函數(shù),最終調(diào)用到struct socksk_prot指針指向的tcp_prot內(nèi)核協(xié)議棧操作函數(shù)接口集合。

圖片
系統(tǒng)IO調(diào)用結(jié)構(gòu).png
  • 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í)候我們還會提到!

  1. 當(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_operationsread系統(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)程給阻塞掉的

圖片
系統(tǒng)IO調(diào)用阻塞原理.png

首先會在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)程是如何被喚醒的。

圖片
系統(tǒng)IO調(diào)用喚醒原理.png
  • 當(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集合。

這里正是epollselect ,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_ctlepoll中添加我們需要管理的Socket連接了。

  1. 首先要在epoll內(nèi)核中創(chuàng)建一個(gè)表示Socket連接的數(shù)據(jù)結(jié)構(gòu)struct epitem,而在epoll中為了綜合性能的考慮,采用一顆紅黑樹來管理這些海量socket連接。所以struct epitem是一個(gè)紅黑樹節(jié)點(diǎn)。

圖片
struct epitem.png

這里重點(diǎn)記住struct epitem結(jié)構(gòu)中的rdllink以及epoll_filefd成員,后面我們會用到。

  1. 在內(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)核輪詢方式的根本性能差異所在。

圖片
epitem創(chuàng)建等待項(xiàng).png

這里又出現(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_tepitem。

這樣在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注冊的funcep_poll_callback而不是autoremove_wake_function,阻塞進(jìn)程并不需要autoremove_wake_function來喚醒,所以這里設(shè)置privatenull

  1. 當(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

  1. 用戶程序調(diào)用epoll_wait后,內(nèi)核首先會查找epoll中的就緒隊(duì)列eventpoll->rdllist是否有IO就緒epitem。epitem里封裝了socket的信息。如果就緒隊(duì)列中有就緒的epitem,就將就緒的socket信息封裝到epoll_event返回。

  2. 如果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)

圖片
epoll_wait同步獲取數(shù)據(jù).png

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

圖片
數(shù)據(jù)到來epoll_wait流程.png
  1. 前邊做了那么多的知識鋪墊,下面終于到了epoll的整個(gè)工作流程了:

圖片
epoll_wait處理過程.png
  • 當(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中的epitemIO就緒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_waitIO就緒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_waitepoll_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ā)模式。JDKNIO默認(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

圖片
信號驅(qū)動IO.png

大家對這個(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.png

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

圖片
IO多路復(fù)用.png

##異步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.png

異步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是利用NIOIO線程進(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單線程

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多線程模型:

圖片
單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多線程
  • 我們由原來的單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是基于AIOIO線程進(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.png

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 Processorcompletion handlerproactor注冊到內(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中。

  • Proactorcompletion 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多線程模型是什么樣子的?

圖片
netty中的reactor.png
  • Reactornetty中是以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)用。所以PipeLineChannelHandler中執(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ù)小屋



深入講解Netty那些事兒之從內(nèi)核角度看IO模型(下)的評論 (共 條)

分享到微博請遵守國家法律
体育| 廉江市| 巴彦县| 杂多县| 常山县| 临清市| 孟津县| 芜湖县| 集安市| 松阳县| 阿拉善右旗| 温泉县| 略阳县| 泸溪县| 绍兴县| 铁岭市| 出国| 工布江达县| 桃园市| 清镇市| 佛山市| 玉环县| 壶关县| 攀枝花市| 南宁市| 乐安县| 探索| 宜宾县| 泉州市| 哈密市| 滕州市| 东城区| 鄂州市| 岫岩| 巨野县| 温宿县| 隆林| 柳江县| 高密市| 香河县| 内黄县|