一 內(nèi)核接受網(wǎng)卡流量的整個(gè)流程
預(yù)備知識(shí):
- 網(wǎng)絡(luò)編程的核心對象是socket,當(dāng)創(chuàng)建socket時(shí)在底層會(huì)創(chuàng)建一個(gè)由文件系統(tǒng)管理的socket對象。這個(gè)對象包括了發(fā)送緩沖區(qū),接收緩沖區(qū),等待隊(duì)列。
- recv函數(shù)用于從某一個(gè)socket中接受流量,但是這個(gè)函數(shù)在被調(diào)用入進(jìn)程會(huì)一直處于阻塞狀態(tài),直到從該socket收到數(shù)據(jù)為止。
網(wǎng)卡接收流量的流程:
- 步驟一:進(jìn)程中調(diào)用了recv函數(shù)請求接收指定socket的流量。
- 步驟二:操作系統(tǒng)將這個(gè)進(jìn)程加入到對應(yīng)socket的等待隊(duì)列中,并從CPU工作隊(duì)列中移除,經(jīng)過這一步后,進(jìn)程會(huì)處理阻塞狀態(tài)。
- 計(jì)算機(jī)接收到對端傳輸?shù)臄?shù)據(jù),網(wǎng)卡把數(shù)據(jù)寫入到內(nèi)存中。這一步不需要CPU參與(數(shù)據(jù)不經(jīng)過CPU直接從IO設(shè)備寫入內(nèi)存的技術(shù)叫作DMA技術(shù))
- 數(shù)據(jù)寫完后網(wǎng)卡會(huì)發(fā)送一個(gè)中斷信號(hào),中斷CPU,通知CPU有數(shù)據(jù)到達(dá)。
- CPU中斷程序響應(yīng)中斷,并把內(nèi)存中的數(shù)據(jù)寫入了對應(yīng)socket的緩沖區(qū)里
- CPU喚醒進(jìn)程,把進(jìn)程從socket的等待隊(duì)列中移除,然后加入到工作隊(duì)列等待系統(tǒng)調(diào)用。
上面的流程有一個(gè)問題:
revc函數(shù)只能監(jiān)控一個(gè)socket,并且會(huì)導(dǎo)致進(jìn)程一直阻塞在這個(gè)socket中,直到socket中有數(shù)據(jù)返回為止。如果有多個(gè)socket監(jiān)控,則需要?jiǎng)?chuàng)建多個(gè)進(jìn)程,非常浪費(fèi)資源。
而select和epoll就解決了上面的問題,它們讓一個(gè)進(jìn)程可以監(jiān)控多個(gè)socket,下面分別說一下兩個(gè)函數(shù)的實(shí)現(xiàn)細(xì)節(jié)。
二 select的實(shí)現(xiàn)細(xì)節(jié)
select一次監(jiān)控多個(gè)socket的原理很簡單:
- 它會(huì)把進(jìn)程加入到它需要監(jiān)控的所有socket的等待隊(duì)列中,然后將進(jìn)程從CPU 工作隊(duì)列中移除,進(jìn)入阻塞狀態(tài)。
- 當(dāng)這些socket中有一個(gè)socket有數(shù)據(jù)返回時(shí),中斷程序會(huì)把進(jìn)程從所以的socket等待隊(duì)列中移除,并把進(jìn)程重新加入到CPU工作隊(duì)列中,讓進(jìn)程進(jìn)就緒狀態(tài)。
- 進(jìn)程進(jìn)入被CPU調(diào)用到,只需要遍歷所有socket的狀態(tài),就可以知道哪些socket可以讀取數(shù)據(jù)了。
- 操作完這些可讀取數(shù)據(jù)的socket之后,又會(huì)重復(fù)第一步,把進(jìn)程加入到所有的 socket中,然后讓進(jìn)程進(jìn)入阻塞狀態(tài)。
通過上述的方式,select函數(shù)實(shí)現(xiàn)了在一個(gè)進(jìn)程中監(jiān)控多個(gè)socket的方法。但是這函數(shù)的性能并不高,因?yàn)樗枰貜?fù)把進(jìn)程從所有的socket中加入/移除。因此它監(jiān)控的socket數(shù)量不能太多,底層規(guī)定不能超過1024個(gè)。
epoll函數(shù)針對select的這個(gè)缺陷作了改進(jìn),接下來說說epoll函數(shù)的實(shí)現(xiàn)細(xì)節(jié)。
三 epoll函數(shù)的實(shí)現(xiàn)細(xì)節(jié)
當(dāng)進(jìn)程調(diào)用epoll監(jiān)控多個(gè)socket時(shí),會(huì)在底層創(chuàng)建一個(gè)eventpoll對象,這個(gè)對象中包含一個(gè)重要的隊(duì)列:就緒隊(duì)列
- 進(jìn)程調(diào)用epoll函數(shù)后,epoll會(huì)把這個(gè)進(jìn)程加入eventpoll對象的等待隊(duì)列中
- 然后把eventpoll對象加入到所有socket的等待隊(duì)列中,并讓CPU阻塞住
- 當(dāng)某一個(gè)socket有數(shù)據(jù)返回時(shí),CPU中斷程序會(huì)把這個(gè)socket加入到eventpoll對象的就緒隊(duì)列中,并把eventpoll中等待的進(jìn)程喚醒。
- 進(jìn)程被喚醒后直接從就緒隊(duì)列中獲取socket讀取數(shù)據(jù)
- 數(shù)據(jù)讀取完成后,epoll又會(huì)把進(jìn)程加入到eventpoll的等待隊(duì)列中,然后讓CPU阻塞住。
epoll針對select優(yōu)化的點(diǎn):
- 除了第一次外,epoll不需操作所有socket對象的等待隊(duì)列,只需要操作eventpoll的等待隊(duì)列即可
- 進(jìn)程被喚醒后,不需要遍歷即可直接知道哪些socket準(zhǔn)備好了。