網(wǎng)絡(luò)IO總結(jié)與分析

首先,本人沒有l(wèi)inux、驅(qū)動(dòng)開發(fā)經(jīng)驗(yàn)。平時(shí)的工作語(yǔ)言是java、python,c、c++的知識(shí)也停留在大學(xué)時(shí)代。所以關(guān)于網(wǎng)絡(luò)IO,我查閱了很多的文章才基本明白。文中可能會(huì)有錯(cuò)誤,或者不足的地方,希望諒解。所以參考的文章在末尾都一一列出,大家也可以查閱。

網(wǎng)絡(luò)io種類

  • blocking IO
  • nonblocking IO
  • IO multiplexing
  • signal driven IO
  • asynchronous IO
    由signal driven IO在實(shí)際中并不常用,所以主要介紹其余四種IO Model。

對(duì)于一個(gè)network IO (這里我們以read舉例),它會(huì)涉及到兩個(gè)系統(tǒng)對(duì)象,一個(gè)是調(diào)用這個(gè)IO的process (or thread),另一個(gè)就是系統(tǒng)內(nèi)核(kernel)。當(dāng)一個(gè)read操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:
1)等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)
2)將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中(Copying the data from the kernel to the process)
記住這兩點(diǎn)很重要,因?yàn)檫@些IO模型的區(qū)別就是在兩個(gè)階段上各有不同的情況。

阻塞IO(blocking IO)

image.png

blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段(等待數(shù)據(jù)和拷貝數(shù)據(jù)兩個(gè)階段)都被block了。

非阻塞IO(non-blocking IO)

image.png

non-blocking IO 在等待數(shù)據(jù)階段是非阻塞的,但是需要一直輪訓(xùn)。但是在數(shù)據(jù)拷貝階段同樣也是阻塞的

多路復(fù)用IO(IO multiplexing)

image.png

當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block,而同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程。
這個(gè)圖和blocking IO的圖其實(shí)并沒有太大的不同,事實(shí)上還更差一些。因?yàn)檫@里需要使用兩個(gè)系統(tǒng)調(diào)用(select和recvfrom),而blocking IO只調(diào)用了一個(gè)系統(tǒng)調(diào)用(recvfrom)。但是,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection。(多說一句:所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。)
在多路復(fù)用模型中,對(duì)于每一個(gè)socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個(gè)用戶的process其實(shí)是一直被block的。只不過process是被select這個(gè)函數(shù)block,而不是被socket IO給block。因此select()與非阻塞IO類似。

異步IO(Asynchronous I/O)

image.png

用戶進(jìn)程發(fā)起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當(dāng)它受到一個(gè)asynchronous read之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal,告訴它read操作完成了。注意這里不用用戶進(jìn)程主動(dòng)的去將數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)

  • blocking與non-blocking區(qū)別
    1.調(diào)用blocking IO會(huì)一直block住對(duì)應(yīng)的進(jìn)程直到操作完成
    2.non-blocking IO在kernel還在準(zhǔn)備數(shù)據(jù)的情況下會(huì)立刻返回
  • synchronous IO和asynchronous IO的區(qū)別
    Stevens給出的定義(其實(shí)是POSIX的定義)如下:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

其中關(guān)鍵就在于是否將用戶process阻塞。asynchronous IO則不一樣,當(dāng)進(jìn)程發(fā)起IO操作之后,就直接返回再也不理睬了,直到kernel發(fā)送一個(gè)信號(hào),告訴進(jìn)程說IO完成。在這整個(gè)過程中,進(jìn)程完全沒有被block。按照這個(gè)定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬于synchronous IO

網(wǎng)卡接收數(shù)據(jù)的過程

文章開頭提到,不同io的主要區(qū)別是如何處理(1)等待數(shù)據(jù)準(zhǔn)備(2)數(shù)據(jù)從內(nèi)核態(tài)拷貝到進(jìn)制中,這兩個(gè)階段的。那對(duì)于階段一,操作系統(tǒng)是如何準(zhǔn)備的呢?如下圖


image.png

為了方便理解,盡量簡(jiǎn)化技術(shù)細(xì)節(jié),可以把接收數(shù)據(jù)的過程分為4步:

  1. NIC(網(wǎng)卡) 接收到數(shù)據(jù),通過 DMA 方式寫入內(nèi)存(Ring Buffer 和 sk_buff)。
  2. NIC 發(fā)出中斷請(qǐng)求(IRQ),告訴內(nèi)核有新的數(shù)據(jù)過來(lái)了。
  3. Linux 內(nèi)核響應(yīng)中斷,系統(tǒng)切換為內(nèi)核態(tài),處理 Interrupt Handler,從RingBuffer 拿出一個(gè) Packet, 并處理協(xié)議棧,填充 Socket 并交給用戶進(jìn)程。
  4. 系統(tǒng)切換為用戶態(tài),用戶進(jìn)程處理數(shù)據(jù)內(nèi)容。

再談多路復(fù)用

目前有哪些IO多路復(fù)用的方案

  • Linux: select、poll、epoll
  • MacOS/FreeBSD: kqueue
  • Windows/Solaris: IOCP
    本文僅討linux場(chǎng)景下的select、poll、epoll的使用和區(qū)別。上文有提到過,多路復(fù)用是用本來(lái)需要用戶進(jìn)程自已等待數(shù)據(jù)準(zhǔn)備好(一階段,會(huì)阻塞)委托給其他系統(tǒng)調(diào)用(就是這里的select、poll、epoll)。所以就減少了用戶進(jìn)程自已的等待時(shí)間和系統(tǒng)開銷,那么就可以創(chuàng)建、維護(hù)更多的鏈接,從而提高系統(tǒng)性能。

select

select函數(shù)介紹:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

功能:輪詢掃描多個(gè)描述符中的任一描述符是否發(fā)生響應(yīng),或經(jīng)過一段時(shí)間后喚醒

image.png

其中fd_set數(shù)據(jù)結(jié)構(gòu)如下:

   #define __FD_SETSIZE    1024
   typedef __kernel_fd_set     fd_set;
   typedef struct {
       unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
   } 

所以我們看到,fd_set實(shí)際上一個(gè)long類型數(shù)組,大小等于1024/(8*4) = 32。如果數(shù)組的大小是32,那么是不是意味著內(nèi)核最多只能監(jiān)聽32個(gè)文件標(biāo)識(shí)符呢?肯定不是的,這里用到了bitmap的思路。實(shí)際是用long類型的每一位來(lái)表示一個(gè)文件描述符,一個(gè)long32個(gè)bit,那么一個(gè)long就能監(jiān)聽32個(gè)文件描述符,32個(gè)long就能監(jiān)聽1024個(gè)文件描述符。所以這是select的缺點(diǎn),poll就是改進(jìn)了這個(gè)缺點(diǎn),采用了鏈表的方式來(lái)突破這個(gè)限制。其他和select一樣,我們下面就不說了。如何把文件描述符添加大fd_set中呢?這里需要其他幾個(gè)方法

/初始化描述符集  
void FD_ZERO(fd_set *fdset);  
  
//將一個(gè)描述符添加到描述符集  
void FD_SET(int fd, fd_set *fdset);  
  
//將一個(gè)描述符從描述符集中刪除  
void FD_CLR(int fd, fd_set *fdset);  
  
//檢測(cè)指定的描述符是否有事件發(fā)生  
int FD_ISSET(int fd, fd_set *fdset);  

下面我們來(lái)看一個(gè)例子(來(lái)自參考文獻(xiàn)6)

while(1)  
{  
    fd_set rset;//創(chuàng)建一個(gè)描述符集rset  
    FD_ZERO(&rset);//對(duì)描述符集rset清零  
    FD_SET(0, &rset);//將描述符0加入到描述符集rset中  
    FD_SET(4, &rset);//將描述符4加入到描述符集rset中  
    FD_SET(5, &rset);//將描述符5加入到描述符集rset中  
      
    if(select(5+1, &rset, NULL, NULL, NULL) > 0)  
    {  
        if(FD_ISSET(0, &rset))  
        {  
            //描述符0可讀及相應(yīng)的處理代碼  
        }  
          
        if(FD_ISSET(4, &rset))  
        {  
            //描述符4可讀及相應(yīng)的處理代碼  
        }  
        if(FD_ISSET(5, &rset))  
        {  
            //描述符5可讀及相應(yīng)的處理代碼  
        }  
    }  
}  

下面是另外一個(gè)例子(來(lái)自參考文獻(xiàn)2)

image.png

epoll

epoll是用來(lái)改進(jìn)select的缺點(diǎn),缺點(diǎn)我下面會(huì)總結(jié)一下,epoll設(shè)計(jì)三個(gè)系統(tǒng)調(diào)用

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd: 由 epoll_create 生成的epoll專用的文件描述符;
//op: 要進(jìn)行的操作例如注冊(cè)事件,可能的取值EPOLL_CTL_ADD 注冊(cè)、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 刪除
//fd: 關(guān)聯(lián)的文件描述符
//event: 指向epoll_event的指針;
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

具體的含義如下:

(1)epoll_create調(diào)用后,內(nèi)核幫我們做了兩件事情

  • 在內(nèi)核cache里建了個(gè)紅黑樹用于存儲(chǔ)以后epoll_ctl傳來(lái)的socket;
  • 建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件

(2)epoll_ctl

  • 把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上
  • 給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里

(3)epoll_wait

  • 察list鏈表里有沒有數(shù)據(jù)。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒數(shù)據(jù)也返回。而且,通常情況下即使我們要監(jiān)控百萬(wàn)計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已,所以,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶態(tài)而已。

總結(jié)如下:

一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,解決了大并發(fā)下的socket處理問題。

  • 執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹和就緒鏈表;
  • 執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來(lái)臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù);
  • 執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。

select、epoll對(duì)比

  • select 監(jiān)聽描述符有數(shù)量限制,epoll沒有
  • select 將fd_set 傳給內(nèi)核,內(nèi)核需要變量來(lái)將數(shù)據(jù)準(zhǔn)備就緒的對(duì)應(yīng)位置set為1,同樣的,由于內(nèi)核共用的fd_set, 用戶進(jìn)程同樣也需要掃描fd_set來(lái)看那個(gè)socket準(zhǔn)備好了,這里有兩個(gè)問題
    1.時(shí)間復(fù)雜度O(n)
    2.每次都需要將fd_set 內(nèi)存拷貝,如果連接幾百萬(wàn)個(gè)(雖然一個(gè)fd_set是1024個(gè),我們可以用多個(gè)fd_set嘛),那么空間消耗大 ;
    但是epoll是用的紅黑樹,時(shí)間復(fù)雜度低O(1),用了回調(diào)函數(shù)將準(zhǔn)備就緒的文件描述符放到了一個(gè)鏈表中,一般情況下,這個(gè)鏈表比較小。所以空間復(fù)雜度也比較低
    所以epoll無(wú)論在時(shí)間復(fù)雜度還是空間復(fù)雜度上是都是比select優(yōu)越的。

參考文獻(xiàn)

  1. 5種網(wǎng)絡(luò)IO模型
  2. 深入理解 epoll
  3. 一文看懂IO多路復(fù)用
  4. select poll epoll的區(qū)別
  5. select深入理解
  6. Linux網(wǎng)絡(luò)編程——I/O復(fù)用之select詳解
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容