如何從Linux內(nèi)核看socket底層的本質(zhì)(IO)(這兩點(diǎn)入手~)
一、I/O 模型
一個(gè)輸入操作通常包括兩個(gè)階段:
等待數(shù)據(jù)準(zhǔn)備好
從內(nèi)核向進(jìn)程復(fù)制數(shù)據(jù)
對(duì)于一個(gè)套接字上的輸入操作,第一步通常涉及等待數(shù)據(jù)從網(wǎng)絡(luò)中到達(dá)。當(dāng)所等待數(shù)據(jù)到達(dá)時(shí),它被復(fù)制到內(nèi)核中的某個(gè)緩沖區(qū)。第二步就是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。
Unix 有五種 I/O 模型:
阻塞式 I/O
非阻塞式 I/O
I/O 復(fù)用(select 和 poll)
信號(hào)驅(qū)動(dòng)式 I/O(SIGIO)
異步 I/O(AIO)
阻塞式 I/O
應(yīng)用進(jìn)程被阻塞,直到數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)中才返回。
應(yīng)該注意到,在阻塞的過(guò)程中,其它應(yīng)用進(jìn)程還可以執(zhí)行,因此阻塞不意味著整個(gè)操作系統(tǒng)都被阻塞。因?yàn)槠渌鼞?yīng)用進(jìn)程還可以執(zhí)行,所以不消耗 CPU 時(shí)間,這種模型的 CPU 利用率會(huì)比較高。
下圖中,recvfrom() 用于接收 Socket 傳來(lái)的數(shù)據(jù),并復(fù)制到應(yīng)用進(jìn)程的緩沖區(qū) buf 中。這里把 recvfrom() 當(dāng)成系統(tǒng)調(diào)用。

非阻塞式 I/O
應(yīng)用進(jìn)程執(zhí)行系統(tǒng)調(diào)用之后,內(nèi)核返回一個(gè)錯(cuò)誤碼。應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行,但是需要不斷的執(zhí)行系統(tǒng)調(diào)用來(lái)獲知 I/O 是否完成,這種方式稱(chēng)為輪詢(polling)。
由于 CPU 要處理更多的系統(tǒng)調(diào)用,因此這種模型的 CPU 利用率比較低。

I/O 復(fù)用
使用 select 或者 poll 等待數(shù)據(jù),并且可以等待多個(gè)套接字中的任何一個(gè)變?yōu)榭勺x。這一過(guò)程會(huì)被阻塞,當(dāng)某一個(gè)套接字可讀時(shí)返回,之后再使用 recvfrom 把數(shù)據(jù)從內(nèi)核復(fù)制到進(jìn)程中。
它可以讓單個(gè)進(jìn)程具有處理多個(gè) I/O 事件的能力。又被稱(chēng)為 Event Driven I/O,即事件驅(qū)動(dòng) I/O。
如果一個(gè) Web 服務(wù)器沒(méi)有 I/O 復(fù)用,那么每一個(gè) Socket 連接都需要?jiǎng)?chuàng)建一個(gè)線程去處理。如果同時(shí)有幾萬(wàn)個(gè)連接,那么就需要?jiǎng)?chuàng)建相同數(shù)量的線程。相比于多進(jìn)程和多線程技術(shù),I/O 復(fù)用不需要進(jìn)程線程創(chuàng)建和切換的開(kāi)銷(xiāo),系統(tǒng)開(kāi)銷(xiāo)更小。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ?


信號(hào)驅(qū)動(dòng) I/O
應(yīng)用進(jìn)程使用 sigaction 系統(tǒng)調(diào)用,內(nèi)核立即返回,應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行,也就是說(shuō)等待數(shù)據(jù)階段應(yīng)用進(jìn)程是非阻塞的。內(nèi)核在數(shù)據(jù)到達(dá)時(shí)向應(yīng)用進(jìn)程發(fā)送 SIGIO 信號(hào),應(yīng)用進(jìn)程收到之后在信號(hào)處理程序中調(diào)用 recvfrom 將數(shù)據(jù)從內(nèi)核復(fù)制到應(yīng)用進(jìn)程中。
相比于非阻塞式 I/O 的輪詢方式,信號(hào)驅(qū)動(dòng) I/O 的 CPU 利用率更高。

異步 I/O
應(yīng)用進(jìn)程執(zhí)行 aio_read 系統(tǒng)調(diào)用會(huì)立即返回,應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行,不會(huì)被阻塞,內(nèi)核會(huì)在所有操作完成之后向應(yīng)用進(jìn)程發(fā)送信號(hào)。
異步 I/O 與信號(hào)驅(qū)動(dòng) I/O 的區(qū)別在于,異步 I/O 的信號(hào)是通知應(yīng)用進(jìn)程 I/O 完成,而信號(hào)驅(qū)動(dòng) I/O 的信號(hào)是通知應(yīng)用進(jìn)程可以開(kāi)始 I/O。

五大 I/O 模型比較
同步 I/O:將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)的階段(第二階段),應(yīng)用進(jìn)程會(huì)阻塞。
異步 I/O:第二階段應(yīng)用進(jìn)程不會(huì)阻塞。
同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 復(fù)用和信號(hào)驅(qū)動(dòng) I/O ,它們的主要區(qū)別在第一個(gè)階段。
非阻塞式 I/O 、信號(hào)驅(qū)動(dòng) I/O 和異步 I/O 在第一階段不會(huì)阻塞。

二、I/O 復(fù)用
select/poll/epoll 都是 I/O 多路復(fù)用的具體實(shí)現(xiàn),select 出現(xiàn)的最早,之后是 poll,再是 epoll。
select
select 允許應(yīng)用程序監(jiān)視一組文件描述符,等待一個(gè)或者多個(gè)描述符成為就緒狀態(tài),從而完成 I/O 操作。
fd_set 使用數(shù)組實(shí)現(xiàn),數(shù)組大小使用 FD_SETSIZE 定義,所以只能監(jiān)聽(tīng)少于 FD_SETSIZE 數(shù)量的描述符。有三種類(lèi)型的描述符類(lèi)型:readset、writeset、exceptset,分別對(duì)應(yīng)讀、寫(xiě)、異常條件的描述符集合。
timeout 為超時(shí)參數(shù),調(diào)用 select 會(huì)一直阻塞直到有描述符的事件到達(dá)或者等待的時(shí)間超過(guò) timeout。
成功調(diào)用返回結(jié)果大于 0,出錯(cuò)返回結(jié)果為 -1,超時(shí)返回結(jié)果為 0。
poll
poll 的功能與 select 類(lèi)似,也是等待一組描述符中的一個(gè)成為就緒狀態(tài)。
poll 中的描述符是 pollfd 類(lèi)型的數(shù)組,pollfd 的定義如下:
比較
1. 功能
select 和 poll 的功能基本相同,不過(guò)在一些實(shí)現(xiàn)細(xì)節(jié)上有所不同。
select 會(huì)修改描述符,而 poll 不會(huì);
select 的描述符類(lèi)型使用數(shù)組實(shí)現(xiàn),F(xiàn)D_SETSIZE 大小默認(rèn)為 1024,因此默認(rèn)只能監(jiān)聽(tīng)少于 1024 個(gè)描述符。如果要監(jiān)聽(tīng)更多描述符的話,需要修改 FD_SETSIZE 之后重新編譯;而 poll 沒(méi)有描述符數(shù)量的限制;
poll 提供了更多的事件類(lèi)型,并且對(duì)描述符的重復(fù)利用上比 select 高。
如果一個(gè)線程對(duì)某個(gè)描述符調(diào)用了 select 或者 poll,另一個(gè)線程關(guān)閉了該描述符,會(huì)導(dǎo)致調(diào)用結(jié)果不確定。
2. 速度
select 和 poll 速度都比較慢,每次調(diào)用都需要將全部描述符從應(yīng)用進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)。
3. 可移植性
幾乎所有的系統(tǒng)都支持 select,但是只有比較新的系統(tǒng)支持 poll。
epoll
epoll_ctl() 用于向內(nèi)核注冊(cè)新的描述符或者是改變某個(gè)文件描述符的狀態(tài)。已注冊(cè)的描述符在內(nèi)核中會(huì)被維護(hù)在一棵紅黑樹(shù)上,通過(guò)回調(diào)函數(shù)內(nèi)核會(huì)將 I/O 準(zhǔn)備好的描述符加入到一個(gè)鏈表中管理,進(jìn)程調(diào)用 epoll_wait() 便可以得到事件完成的描述符。
從上面的描述可以看出,epoll 只需要將描述符從進(jìn)程緩沖區(qū)向內(nèi)核緩沖區(qū)拷貝一次,并且進(jìn)程不需要通過(guò)輪詢來(lái)獲得事件完成的描述符。
epoll 僅適用于 Linux OS。
epoll 比 select 和 poll 更加靈活而且沒(méi)有描述符數(shù)量限制。
epoll 對(duì)多線程編程更有友好,一個(gè)線程調(diào)用了 epoll_wait() 另一個(gè)線程關(guān)閉了同一個(gè)描述符也不會(huì)產(chǎn)生像 select 和 poll 的不確定情況。
工作模式
epoll 的描述符事件有兩種觸發(fā)模式:LT(level trigger)和 ET(edge trigger)。
1. LT 模式
當(dāng) epoll_wait() 檢測(cè)到描述符事件到達(dá)時(shí),將此事件通知進(jìn)程,進(jìn)程可以不立即處理該事件,下次調(diào)用 epoll_wait() 會(huì)再次通知進(jìn)程。是默認(rèn)的一種模式,并且同時(shí)支持 Blocking 和 No-Blocking。
2. ET 模式
和 LT 模式不同的是,通知之后進(jìn)程必須立即處理事件,下次再調(diào)用 epoll_wait() 時(shí)不會(huì)再得到事件到達(dá)的通知。
很大程度上減少了 epoll 事件被重復(fù)觸發(fā)的次數(shù),因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操作把處理多個(gè)文件描述符的任務(wù)餓死。
應(yīng)用場(chǎng)景
很容易產(chǎn)生一種錯(cuò)覺(jué)認(rèn)為只要用 epoll 就可以了,select 和 poll 都已經(jīng)過(guò)時(shí)了,其實(shí)它們都有各自的使用場(chǎng)景。
select 應(yīng)用場(chǎng)景
select 的 timeout 參數(shù)精度為微秒,而 poll 和 epoll 為毫秒,因此 select 更加適用于實(shí)時(shí)性要求比較高的場(chǎng)景,比如核反應(yīng)堆的控制。
select 可移植性更好,幾乎被所有主流平臺(tái)所支持。
poll 應(yīng)用場(chǎng)景
poll 沒(méi)有最大描述符數(shù)量的限制,如果平臺(tái)支持并且對(duì)實(shí)時(shí)性要求不高,應(yīng)該使用 poll 而不是 select。
epoll 應(yīng)用場(chǎng)景
只需要運(yùn)行在 Linux 平臺(tái)上,有大量的描述符需要同時(shí)輪詢,并且這些連接最好是長(zhǎng)連接。
需要同時(shí)監(jiān)控小于 1000 個(gè)描述符,就沒(méi)有必要使用 epoll,因?yàn)檫@個(gè)應(yīng)用場(chǎng)景下并不能體現(xiàn) epoll 的優(yōu)勢(shì)。
需要監(jiān)控的描述符狀態(tài)變化多,而且都是非常短暫的,也沒(méi)有必要使用 epoll。因?yàn)?epoll 中的所有描述符都存儲(chǔ)在內(nèi)核中,造成每次需要對(duì)描述符的狀態(tài)改變都需要通過(guò) epoll_ctl() 進(jìn)行系統(tǒng)調(diào)用,頻繁系統(tǒng)調(diào)用降低效率。并且 epoll 的描述符存儲(chǔ)在內(nèi)核,不容易調(diào)試。
