<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          講講BIO和NIO以及IO多路復用

          共 2361字,需瀏覽 5分鐘

           ·

          2020-12-08 13:05

          點擊上方?泥瓦匠?輕松關注!

          及時獲取有趣有料的技術文章

          本文從操作系統(tǒng)的角度來解釋BIO,NIO,AIO的概念,含義和背后的那些事。本文主要分為3篇。

          • 第一篇 講解BIO和NIO以及IO多路復用
          • 第二篇 講解磁盤IO和AIO
          • 第三篇 講解在這些機制上的一些應用的實現(xiàn)方式,比如nginx,nodejs,Java NIO等

          到底什么是“IO Block”

          很多人說BIO不好,會“block”,但到底什么是IO的Block呢?考慮下面兩種情況:

          • 用系統(tǒng)調(diào)用read從socket里讀取一段數(shù)據(jù)
          • 用系統(tǒng)調(diào)用read從一個磁盤文件讀取一段數(shù)據(jù)到內(nèi)存

          如果你的直覺告訴你,這兩種都算“Block”,那么很遺憾,你的理解與Linux不同。Linux認為:

          • 對于第一種情況,算作block,因為Linux無法知道網(wǎng)絡上對方是否會發(fā)數(shù)據(jù)。如果沒數(shù)據(jù)發(fā)過來,對于調(diào)用read的程序來說,就只能“等”。
          • 對于第二種情況,不算做block

          是的,對于磁盤文件IO,Linux總是不視作Block。

          你可能會說,這不科學啊,磁盤讀寫偶爾也會因為硬件而卡殼啊,怎么能不算Block呢?但實際就是不算。

          一個解釋是,所謂“Block”是指操作系統(tǒng)可以預見這個Block會發(fā)生才會主動Block。例如當讀取TCP連接的數(shù)據(jù)時,如果發(fā)現(xiàn)Socket buffer里沒有數(shù)據(jù)就可以確定定對方還沒有發(fā)過來,于是Block;而對于普通磁盤文件的讀寫,也許磁盤運作期間會抖動,會短暫暫停,但是操作系統(tǒng)無法預見這種情況,只能視作不會Block,照樣執(zhí)行。

          基于這個基本的設定,在討論IO時,一定要嚴格區(qū)分網(wǎng)絡IO和磁盤文件IO。NIO和后文講到的IO多路復用只對網(wǎng)絡IO有意義。

          嚴格的說,O_NONBLOCK和IO多路復用,對標準輸入輸出描述符、管道和FIFO也都是有效的。但本文側(cè)重于討論高性能網(wǎng)絡服務器下各種IO的含義和關系,所以本文做了簡化,只提及網(wǎng)絡IO和磁盤文件IO兩種情況。

          本文先著重講一下網(wǎng)絡IO。

          BIO

          有了Block的定義,就可以討論BIO和NIO了。BIO是Blocking IO的意思。在類似于網(wǎng)絡中進行read, write, connect一類的系統(tǒng)調(diào)用時會被卡住。

          舉個例子,當用read去讀取網(wǎng)絡的數(shù)據(jù)時,是無法預知對方是否已經(jīng)發(fā)送數(shù)據(jù)的。因此在收到數(shù)據(jù)之前,能做的只有等待,直到對方把數(shù)據(jù)發(fā)過來,或者等到網(wǎng)絡超時。

          對于單線程的網(wǎng)絡服務,這樣做就會有卡死的問題。因為當?shù)却龝r,整個線程會被掛起,無法執(zhí)行,也無法做其他的工作。

          順便說一句,這種Block是不會影響同時運行的其他程序(進程)的,因為現(xiàn)代操作系統(tǒng)都是多任務的,任務之間的切換是搶占式的。這里Block只是指Block當前的進程。

          于是,網(wǎng)絡服務為了同時響應多個并發(fā)的網(wǎng)絡請求,必須實現(xiàn)為多線程的。每個線程處理一個網(wǎng)絡請求。線程數(shù)隨著并發(fā)連接數(shù)線性增長。這的確能奏效。實際上2000年之前很多網(wǎng)絡服務器就是這么實現(xiàn)的。但這帶來兩個問題:

          • 線程越多,Context Switch就越多,而Context Switch是一個比較重的操作,會無謂浪費大量的CPU。
          • 每個線程會占用一定的內(nèi)存作為線程的棧。比如有1000個線程同時運行,每個占用1MB內(nèi)存,就占用了1個G的內(nèi)存。

          也許現(xiàn)在看來1GB內(nèi)存不算什么,現(xiàn)在服務器上百G內(nèi)存的配置現(xiàn)在司空見慣了。但是倒退20年,1G內(nèi)存是很金貴的。并且,盡管現(xiàn)在通過使用大內(nèi)存,可以輕易實現(xiàn)并發(fā)1萬甚至10萬的連接。但是水漲船高,如果是要單機撐1千萬的連接呢?

          問題的關鍵在于,當調(diào)用read接受網(wǎng)絡請求時,有數(shù)據(jù)到了就用,沒數(shù)據(jù)到時,實際上是可以干別的。使用大量線程,僅僅是因為Block發(fā)生,沒有其他辦法。

          當然你可能會說,是不是可以弄個線程池呢?這樣既能并發(fā)的處理請求,又不會產(chǎn)生大量線程。但這樣會限制最大并發(fā)的連接數(shù)。比如你弄4個線程,那么最大4個線程都Block了就沒法響應更多請求了。

          要是操作IO接口時,操作系統(tǒng)能夠總是直接告訴有沒有數(shù)據(jù),而不是Block去等就好了。于是,NIO登場。

          NIO

          NIO是指將IO模式設為“Non-Blocking”模式。在Linux下,一般是這樣:

          void?setnonblocking(int?fd)?{
          ????int?flags?=?fcntl(fd,?F_GETFL,?0);
          ????fcntl(fd,?F_SETFL,?flags?|?O_NONBLOCK);
          }

          再強調(diào)一下,以上操作只對socket對應的文件描述符有意義;對磁盤文件的文件描述符做此設置總會成功,但是會直接被忽略。

          這時,BIO和NIO的區(qū)別是什么呢?

          在BIO模式下,調(diào)用read,如果發(fā)現(xiàn)沒數(shù)據(jù)已經(jīng)到達,就會Block住。

          在NIO模式下,調(diào)用read,如果發(fā)現(xiàn)沒數(shù)據(jù)已經(jīng)到達,就會立刻返回-1, 并且errno被設為EAGAIN

          在有些文檔中寫的是會返回EWOULDBLOCK。實際上,在Linux下EAGAINEWOULDBLOCK是一樣的,即#define EWOULDBLOCK EAGAIN

          于是,一段NIO的代碼,大概就可以寫成這個樣子。

          struct?timespec?sleep_interval{.tv_sec?=?0,?.tv_nsec?=?1000};
          ssize_t?nbytes;
          while?(1)?{
          ????/*?嘗試讀取?*/
          ????if?((nbytes?=?read(fd,?buf,?sizeof(buf)))?0)?{
          ????????if?(errno?==?EAGAIN)?{?//?沒數(shù)據(jù)到
          ????????????perror("nothing?can?be?read");
          ????????}?else?{
          ????????????perror("fatal?error");
          ????????????exit(EXIT_FAILURE);
          ????????}
          ????}?else?{?//?有數(shù)據(jù)
          ????????process_data(buf,?nbytes);
          ????}
          ????//?處理其他事情,做完了就等一會,再嘗試
          ????nanosleep(sleep_interval,?NULL);
          }

          這段代碼很容易理解,就是輪詢,不斷的嘗試有沒有數(shù)據(jù)到達,有了就處理,沒有(得到EWOULDBLOCK或者EAGAIN)就等一小會再試。這比之前BIO好多了,起碼程序不會被卡死了。

          但這樣會帶來兩個新問題:

          • 如果有大量文件描述符都要等,那么就得一個一個的read。這會帶來大量的Context Switch(read是系統(tǒng)調(diào)用,每調(diào)用一次就得在用戶態(tài)和核心態(tài)切換一次)
          • 休息一會的時間不好把握。這里是要猜多久之后數(shù)據(jù)才能到。等待時間設的太長,程序響應延遲就過大;設的太短,就會造成過于頻繁的重試,干耗CPU而已。

          要是操作系統(tǒng)能一口氣告訴程序,哪些數(shù)據(jù)到了就好了。

          于是IO多路復用被搞出來解決這個問題。

          IO多路復用

          IO多路復用(IO Multiplexing) 是這么一種機制:程序注冊一組socket文件描述符給操作系統(tǒng),表示“我要監(jiān)視這些fd是否有IO事件發(fā)生,有了就告訴程序處理”。

          IO多路復用是要和NIO一起使用的。盡管在操作系統(tǒng)級別,NIO和IO多路復用是兩個相對獨立的事情。NIO僅僅是指IO API總是能立刻返回,不會被Blocking;而IO多路復用僅僅是操作系統(tǒng)提供的一種便利的通知機制。操作系統(tǒng)并不會強制這倆必須得一起用——你可以用NIO,但不用IO多路復用,就像上一節(jié)中的代碼;也可以只用IO多路復用 + BIO,這時效果還是當前線程被卡住。但是,IO多路復用和NIO是要配合一起使用才有實際意義。因此,在使用IO多路復用之前,請總是先把fd設為O_NONBLOCK

          對IO多路復用,還存在一些常見的誤解,比如:

          • ?IO多路復用是指多個數(shù)據(jù)流共享同一個Socket。其實IO多路復用說的是多個Socket,只不過操作系統(tǒng)是一起監(jiān)聽他們的事件而已。

            多個數(shù)據(jù)流共享同一個TCP連接的場景的確是有,比如Http2 Multiplexing就是指Http2通訊中中多個邏輯的數(shù)據(jù)流共享同一個TCP連接。但這與IO多路復用是完全不同的問題。

          • ?IO多路復用是NIO,所以總是不Block的。其實IO多路復用的關鍵API調(diào)用(selectpollepoll_wait)總是Block的,正如下文的例子所講。

          • ?IO多路復用和NIO一起減少了IO。實際上,IO本身(網(wǎng)絡數(shù)據(jù)的收發(fā))無論用不用IO多路復用和NIO,都沒有變化。請求的數(shù)據(jù)該是多少還是多少;網(wǎng)絡上該傳輸多少數(shù)據(jù)還是多少數(shù)據(jù)。IO多路復用和NIO一起僅僅是解決了調(diào)度的問題,避免CPU在這個過程中的浪費,使系統(tǒng)的瓶頸更容易觸達到網(wǎng)絡帶寬,而非CPU或者內(nèi)存。要提高IO吞吐,還是提高硬件的容量(例如,用支持更大帶寬的網(wǎng)線、網(wǎng)卡和交換機)和依靠并發(fā)傳輸(例如HDFS的數(shù)據(jù)多副本并發(fā)傳輸)。

          操作系統(tǒng)級別提供了一些接口來支持IO多路復用,最老掉牙的是selectpoll

          select

          select長這樣:

          int?select(int?nfds,?fd_set?*readfds,?fd_set?*writefds,?fd_set?*exceptfds,?struct?timeval?*timeout);

          它接受3個文件描述符的數(shù)組,分別監(jiān)聽讀取(readfds),寫入(writefds)和異常(expectfds)事件。那么一個 IO多路復用的代碼大概是這樣:

          struct?timeval?tv?=?{.tv_sec?=?1,?.tv_usec?=?0};

          ssize_t?nbytes;
          while(1)?{
          ????FD_ZERO(&read_fds);
          ????setnonblocking(fd1);
          ????setnonblocking(fd2);
          ????FD_SET(fd1,?&read_fds);
          ????FD_SET(fd2,?&read_fds);
          ????//?把要監(jiān)聽的fd拼到一個數(shù)組里,而且每次循環(huán)都得重來一次...
          ????if?(select(FD_SETSIZE,?&read_fds,?NULL,?NULL,?&tv)?0)?{?//?block住,直到有事件到達
          ????????perror("select出錯了");
          ????????exit(EXIT_FAILURE);
          ????}
          ????for?(int?i?=?0;?i?????????if?(FD_ISSET(i,?&read_fds))?{
          ????????????/*?檢測到第[i]個讀取fd已經(jīng)收到了,這里假設buf總是大于到達的數(shù)據(jù),所以可以一次read完?*/
          ????????????if?((nbytes?=?read(i,?buf,?sizeof(buf)))?>=?0)?{
          ????????????????process_data(nbytes,?buf);
          ????????????}?else?{
          ????????????????perror("讀取出錯了");
          ????????????????exit(EXIT_FAILURE);
          ????????????}
          ????????}
          ????}
          }

          首先,為了select需要構造一個fd數(shù)組(這里為了簡化,沒有構造要監(jiān)聽寫入和異常事件的fd數(shù)組)。之后,用select監(jiān)聽了read_fds中的多個socket的讀取時間。調(diào)用select后,程序會Block住,直到一個事件發(fā)生了,或者等到最大1秒鐘(tv定義了這個時間長度)就返回。之后,需要遍歷所有注冊的fd,挨個檢查哪個fd有事件到達(FD_ISSET返回true)。如果是,就說明數(shù)據(jù)已經(jīng)到達了,可以讀取fd了。讀取后就可以進行數(shù)據(jù)的處理。

          select有一些發(fā)指的缺點:

          • select能夠支持的最大的fd數(shù)組的長度是1024。這對要處理高并發(fā)的web服務器是不可接受的。
          • fd數(shù)組按照監(jiān)聽的事件分為了3個數(shù)組,為了這3個數(shù)組要分配3段內(nèi)存去構造,而且每次調(diào)用select前都要重設它們(因為select會改這3個數(shù)組);調(diào)用select后,這3數(shù)組要從用戶態(tài)復制一份到內(nèi)核態(tài);事件到達后,要遍歷這3數(shù)組。很不爽。
          • select返回后要挨個遍歷fd,找到被“SET”的那些進行處理。這樣比較低效。
          • select是無狀態(tài)的,即每次調(diào)用select,內(nèi)核都要重新檢查所有被注冊的fd的狀態(tài)。select返回后,這些狀態(tài)就被返回了,內(nèi)核不會記住它們;到了下一次調(diào)用,內(nèi)核依然要重新檢查一遍。于是查詢的效率很低。

          poll

          pollselect類似于。它大概長這樣:

          int?poll(struct?pollfd?*fds,?nfds_t?nfds,?int?timeout);

          poll的代碼例子和select差不多,因此也就不贅述了。有意思的是poll這個單詞的意思是“輪詢”,所以很多中文資料都會提到對IO進行“輪詢”。

          上面說的select和下文說的epoll本質(zhì)上都是輪詢。

          poll優(yōu)化了select的一些問題。比如不再有3個數(shù)組,而是1個polldfd結構的數(shù)組了,并且也不需要每次重設了。數(shù)組的個數(shù)也沒有了1024的限制。但其他的問題依舊:

          • 依然是無狀態(tài)的,性能的問題與select差不多一樣;
          • 應用程序仍然無法很方便的拿到那些“有事件發(fā)生的fd“,還是需要遍歷所有注冊的fd。

          目前來看,高性能的web服務器都不會使用selectpoll。他們倆存在的意義僅僅是“兼容性”,因為很多操作系統(tǒng)都實現(xiàn)了這兩個系統(tǒng)調(diào)用。

          如果是追求性能的話,在BSD/macOS上提供了kqueue api;在Salorias中提供了/dev/poll(可惜該操作系統(tǒng)已經(jīng)涼涼);而在Linux上提供了epoll api。它們的出現(xiàn)徹底解決了selectpoll的問題。Java NIO,nginx等在對應的平臺的上都是使用這些api實現(xiàn)。

          因為大部分情況下我會用Linux做服務器,所以下文以Linux epoll為例子來解釋多路復用是怎么工作的。

          用epoll實現(xiàn)的IO多路復用

          epoll是Linux下的IO多路復用的實現(xiàn)。這里單開一章是因為它非常有代表性,并且Linux也是目前最廣泛被作為服務器的操作系統(tǒng)。細致的了解epoll對整個IO多路復用的工作原理非常有幫助。

          selectpoll不同,要使用epoll是需要先創(chuàng)建一下的。

          int?epfd?=?epoll_create(10);

          epoll_create在內(nèi)核層創(chuàng)建了一個數(shù)據(jù)表,接口會返回一個“epoll的文件描述符”指向這個表。注意,接口參數(shù)是一個表達要監(jiān)聽事件列表的長度的數(shù)值。但不用太在意,因為epoll內(nèi)部隨后會根據(jù)事件注冊和事件注銷動態(tài)調(diào)整epoll中表格的大小。

          img

          epoll創(chuàng)建

          為什么epoll要創(chuàng)建一個用文件描述符來指向的表呢?這里有兩個好處:

          • epoll是有狀態(tài)的,不像selectpoll那樣每次都要重新傳入所有要監(jiān)聽的fd,這避免了很多無謂的數(shù)據(jù)復制。epoll的數(shù)據(jù)是用接口epoll_ctl來管理的(增、刪、改)。
          • epoll文件描述符在進程被fork時,子進程是可以繼承的。這可以給對多進程共享一份epoll數(shù)據(jù),實現(xiàn)并行監(jiān)聽網(wǎng)絡請求帶來便利。但這超過了本文的討論范圍,就此打住。

          epoll創(chuàng)建后,第二步是使用epoll_ctl接口來注冊要監(jiān)聽的事件。

          int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);

          其中第一個參數(shù)就是上面創(chuàng)建的epfd。第二個參數(shù)op表示如何對文件名進行操作,共有3種。

          • EPOLL_CTL_ADD - 注冊一個事件
          • EPOLL_CTL_DEL - 取消一個事件的注冊
          • EPOLL_CTL_MOD - 修改一個事件的注冊

          第三個參數(shù)是要操作的fd,這里必須是支持NIO的fd(比如socket)。

          第四個參數(shù)是一個epoll_event的類型的數(shù)據(jù),表達了注冊的事件的具體信息。

          typedef?union?epoll_data?{
          ????void????*ptr;
          ????int??????fd;
          ????uint32_t?u32;
          ????uint64_t?u64;
          }?epoll_data_t;

          struct?epoll_event?{
          ????uint32_t?????events;????/*?Epoll?events?*/
          ????epoll_data_t?data;??????/*?User?data?variable?*/
          };

          比方說,想關注一個fd1的讀取事件事件,并采用邊緣觸發(fā)(下文會解釋什么是邊緣觸發(fā)),大概要這么寫:

          struct?epoll_data?ev;
          ev.events?=?EPOLLIN?|?EPOLLET;?// EPOLLIN表示讀事件;EPOLLET表示邊緣觸發(fā)
          ev.data.fd?=?fd1;

          通過epoll_ctl就可以靈活的注冊/取消注冊/修改注冊某個fd的某些事件。

          img

          管理fd事件注冊

          第三步,使用epoll_wait來等待事件的發(fā)生。

          int?epoll_wait(int?epfd,?struct?epoll_event?*evlist,?int?maxevents,?int?timeout);

          特別留意,這一步是"block"的。只有當注冊的事件至少有一個發(fā)生,或者timeout達到時,該調(diào)用才會返回。這與selectpoll幾乎一致。但不一樣的地方是evlist,它是epoll_wait的返回數(shù)組,里面只包含那些被觸發(fā)的事件對應的fd,而不是像selectpoll那樣返回所有注冊的fd。

          img

          監(jiān)聽fd事件

          綜合起來,一段比較完整的epoll代碼大概是這樣的。

          #define?MAX_EVENTS?10
          struct?epoll_event?ev,?events[MAX_EVENTS];
          int?nfds,?epfd,?fd1,?fd2;

          //?假設這里有兩個socket,fd1和fd2,被初始化好。
          //?設置為non?blocking
          setnonblocking(fd1);
          setnonblocking(fd2);

          //?創(chuàng)建epoll
          epfd?=?epoll_create(MAX_EVENTS);
          if?(epollfd?==?-1)?{
          ????perror("epoll_create1");
          ????exit(EXIT_FAILURE);
          }

          //注冊事件
          ev.events?=?EPOLLIN?|?EPOLLET;
          ev.data.fd?=?fd1;
          if?(epoll_ctl(epollfd,?EPOLL_CTL_ADD,?fd1,?&ev)?==?-1)?{
          ????perror("epoll_ctl:?error?register?fd1");
          ????exit(EXIT_FAILURE);
          }
          if?(epoll_ctl(epollfd,?EPOLL_CTL_ADD,?fd2,?&ev)?==?-1)?{
          ????perror("epoll_ctl:?error?register?fd2");
          ????exit(EXIT_FAILURE);
          }

          //?監(jiān)聽事件
          for?(;;)?{
          ????nfds?=?epoll_wait(epdf,?events,?MAX_EVENTS,?-1);
          ????if?(nfds?==?-1)?{
          ????????perror("epoll_wait");
          ????????exit(EXIT_FAILURE);
          ????}

          ????for?(n?=?0;?n?//?處理所有發(fā)生IO事件的fd
          ????????process_event(events[n].data.fd);
          ????????//?如果有必要,可以利用epoll_ctl繼續(xù)對本fd注冊下一次監(jiān)聽,然后重新epoll_wait
          ????}
          }

          此外,epoll的手冊 中也有一個簡單的例子。

          所有的基于IO多路復用的代碼都會遵循這樣的寫法:注冊——監(jiān)聽事件——處理——再注冊,無限循環(huán)下去。

          epoll的優(yōu)勢

          為什么epoll的性能比selectpoll要強呢?selectpoll每次都需要把完成的fd列表傳入到內(nèi)核,迫使內(nèi)核每次必須從頭掃描到尾。而epoll完全是反過來的。epoll在內(nèi)核的數(shù)據(jù)被建立好了之后,每次某個被監(jiān)聽的fd一旦有事件發(fā)生,內(nèi)核就直接標記之。epoll_wait調(diào)用時,會嘗試直接讀取到當時已經(jīng)標記好的fd列表,如果沒有就會進入等待狀態(tài)。

          同時,epoll_wait直接只返回了被觸發(fā)的fd列表,這樣上層應用寫起來也輕松愉快,再也不用從大量注冊的fd中篩選出有事件的fd了。

          簡單說就是selectpoll的代價是**"O(所有注冊事件fd的數(shù)量)",而epoll的代價是"O(發(fā)生事件fd的數(shù)量)"**。于是,高性能網(wǎng)絡服務器的場景特別適合用epoll來實現(xiàn)——因為大多數(shù)網(wǎng)絡服務器都有這樣的模式:同時要監(jiān)聽大量(幾千,幾萬,幾十萬甚至更多)的網(wǎng)絡連接,但是短時間內(nèi)發(fā)生的事件非常少。

          但是,假設發(fā)生事件的fd的數(shù)量接近所有注冊事件fd的數(shù)量,那么epoll的優(yōu)勢就沒有了,其性能表現(xiàn)會和pollselect差不多。

          epoll除了性能優(yōu)勢,還有一個優(yōu)點——同時支持水平觸發(fā)(Level Trigger)和邊沿觸發(fā)(Edge Trigger)。

          水平觸發(fā)和邊沿觸發(fā)

          默認情況下,epoll使用水平觸發(fā),這與selectpoll的行為完全一致。在水平觸發(fā)下,epoll頂多算是一個“跑得更快的poll”。

          而一旦在注冊事件時使用了EPOLLET標記(如上文中的例子),那么將其視為邊沿觸發(fā)(或者有地方叫邊緣觸發(fā),一個意思)。那么到底什么水平觸發(fā)和邊沿觸發(fā)呢?

          考慮下圖中的例子。有兩個socket的fd——fd1和fd2。我們設定監(jiān)聽f1的“水平觸發(fā)讀事件“,監(jiān)聽fd2的”邊沿觸發(fā)讀事件“。我們使用在時刻t1,使用epoll_wait監(jiān)聽他們的事件。在時刻t2時,兩個fd都到了100bytes數(shù)據(jù),于是在時刻t3, epoll_wait返回了兩個fd進行處理。在t4,我們故意不讀取所有的數(shù)據(jù)出來,只各自讀50bytes。然后在t5重新注冊兩個事件并監(jiān)聽。在t6時,只有fd1會返回,因為fd1里的數(shù)據(jù)沒有讀完,仍然處于“被觸發(fā)”狀態(tài);而fd2不會被返回,因為沒有新數(shù)據(jù)到達。

          img

          水平觸發(fā)和邊沿觸發(fā)

          這個例子很明確的顯示了水平觸發(fā)和邊沿觸發(fā)的區(qū)別。

          • 水平觸發(fā)只關心文件描述符中是否還有沒完成處理的數(shù)據(jù),如果有,不管怎樣epoll_wait,總是會被返回。簡單說——水平觸發(fā)代表了一種“狀態(tài)”。

          • 邊沿觸發(fā)只關心文件描述符是否有的事件產(chǎn)生,如果有,則返回;如果返回過一次,不管程序是否處理了,只要沒有新的事件產(chǎn)生,epoll_wait不會再認為這個fd被“觸發(fā)”了。簡單說——邊沿觸發(fā)代表了一個“事件”。

            那么邊沿觸發(fā)怎么才能迫使新事件產(chǎn)生呢?一般需要反復調(diào)用read/write這樣的IO接口,直到得到了EAGAIN錯誤碼,再去嘗試epoll_wait才有可能得到下次事件。

          那么為什么需要邊沿觸發(fā)呢?

          邊沿觸發(fā)把如何處理數(shù)據(jù)的控制權完全交給了開發(fā)者,提供了巨大的靈活性。比如,讀取一個http的請求,開發(fā)者可以決定只讀取http中的headers數(shù)據(jù)就停下來,然后根據(jù)業(yè)務邏輯判斷是否要繼續(xù)讀(比如需要調(diào)用另外一個服務來決定是否繼續(xù)讀)。而不是次次被socket尚有數(shù)據(jù)的狀態(tài)煩擾;寫入數(shù)據(jù)時也是如此。比如希望將一個資源A寫入到socket。當socket的buffer充足時,epoll_wait會返回這個fd是準備好的。但是資源A此時不一定準備好。如果使用水平觸發(fā),每次經(jīng)過epoll_wait也總會被打擾。在邊沿觸發(fā)下,開發(fā)者有機會更精細的定制這里的控制邏輯。

          但不好的一面時,邊沿觸發(fā)也大大的提高了編程的難度。一不留神,可能就會miss掉處理部分socket數(shù)據(jù)的機會。如果沒有很好的根據(jù)EAGAIN來“重置”一個fd,就會造成此fd永遠沒有新事件產(chǎn)生,進而導致餓死相關的處理代碼。

          再來思考一下什么是“Block”

          上面的所有介紹都在圍繞如何讓網(wǎng)絡IO不會被Block。但是網(wǎng)絡IO處理僅僅是整個數(shù)據(jù)處理中的一部分。如果你留意到上文例子中的“處理事件”代碼,就會發(fā)現(xiàn)這里可能是有問題的。

          • 處理代碼有可能需要讀寫文件,可能會很慢,從而干擾整個程序的效率;
          • 處理代碼有可能是一段復雜的數(shù)據(jù)計算,計算量很大的話,就會卡住整個執(zhí)行流程;
          • 處理代碼有bug,可能直接進入了一段死循環(huán)……

          這時你會發(fā)現(xiàn),這里的Block和本文之初講的O_NONBLOCK是不同的事情。在一個網(wǎng)絡服務中,如果處理程序的延遲遠遠小于網(wǎng)絡IO,那么這完全不成問題。但是如果處理程序的延遲已經(jīng)大到無法忽略了,就會對整個程序產(chǎn)生很大的影響。這時IO多路復用已經(jīng)不是問題的關鍵。

          試分析和比較下面兩個場景:

          • web proxy。程序通過IO多路復用接收到了請求之后,直接轉(zhuǎn)發(fā)給另外一個網(wǎng)絡服務。
          • web server。程序通過IO多路復用接收到了請求之后,需要讀取一個文件,并返回其內(nèi)容。

          它們有什么不同?它們的瓶頸可能出在哪里?

          總結

          小結一下本文:

          • 對于socket的文件描述符才有所謂BIO和NIO。
          • 多線程+BIO模式會帶來大量的資源浪費,而NIO+IO多路復用可以解決這個問題。
          • 在Linux下,基于epoll的IO多路復用是解決這個問題的最佳方案;epoll相比selectpoll有很大的性能優(yōu)勢和功能優(yōu)勢,適合實現(xiàn)高性能網(wǎng)絡服務。

          但是IO多路復用僅僅是解決了一部分問題,另外一部分問題如何解決呢?且聽下回分解。




          下方二維碼關注我

          互聯(lián)網(wǎng)草根,堅持分享技術創(chuàng)業(yè)產(chǎn)品心得和總結~



          點擊“閱讀原文”,領取 2020 年最新免費技術資料大全

          ↓↓↓
          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色天堂在线视频 | 麻豆传媒在线看免费版高清视频 | 萝莉操逼视频 | 国产在线第一页 | A级免费电影 |