0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

網(wǎng)絡(luò)IO的弊端以及多路復(fù)用IO的優(yōu)勢

開關(guān)電源芯片 ? 來源:低并發(fā)編程 ? 作者:閃客sun ? 2021-08-25 18:01 ? 次閱讀

為了講多路復(fù)用,當(dāng)然還是要跟風(fēng),采用鞭尸的思路,先講講傳統(tǒng)的網(wǎng)絡(luò) IO 的弊端,用拉踩的方式捧起多路復(fù)用 IO 的優(yōu)勢。

為了方便理解,以下所有代碼都是偽代碼,知道其表達(dá)的意思即可。

阻塞 IO

服務(wù)端為了處理客戶端的連接和請(qǐng)求的數(shù)據(jù),寫了如下代碼。

listenfd = socket(); // 打開一個(gè)網(wǎng)絡(luò)通信端口

bind(listenfd); // 綁定

listen(listenfd); // 監(jiān)聽while(1) {

connfd = accept(listenfd); // 阻塞建立連接

int n = read(connfd, buf); // 阻塞讀數(shù)據(jù)

doSomeThing(buf); // 利用讀到的數(shù)據(jù)做些什么

close(connfd); // 關(guān)閉連接,循環(huán)等待下一個(gè)連接

}

這段代碼會(huì)執(zhí)行得磕磕絆絆,就像這樣。

可以看到,服務(wù)端的線程阻塞在了兩個(gè)地方,一個(gè)是 accept 函數(shù),一個(gè)是 read 函數(shù)。

如果再把 read 函數(shù)的細(xì)節(jié)展開,我們會(huì)發(fā)現(xiàn)其阻塞在了兩個(gè)階段。

這就是傳統(tǒng)的阻塞 IO。

整體流程如下圖。

4f91990a-e3ff-11eb-a97a-12bb97331649.png

所以,如果這個(gè)連接的客戶端一直不發(fā)數(shù)據(jù),那么服務(wù)端線程將會(huì)一直阻塞在 read 函數(shù)上不返回,也無法接受其他客戶端連接。

這肯定是不行的。

非阻塞 IO

為了解決上面的問題,其關(guān)鍵在于改造這個(gè) read 函數(shù)。

有一種聰明的辦法是,每次都創(chuàng)建一個(gè)新的進(jìn)程或線程,去調(diào)用 read 函數(shù),并做業(yè)務(wù)處理。

while(1) {

connfd = accept(listenfd); // 阻塞建立連接

pthread_create(doWork); // 創(chuàng)建一個(gè)新的線程

}

void doWork() {

int n = read(connfd, buf); // 阻塞讀數(shù)據(jù)

doSomeThing(buf); // 利用讀到的數(shù)據(jù)做些什么

close(connfd); // 關(guān)閉連接,循環(huán)等待下一個(gè)連接

}

這樣,當(dāng)給一個(gè)客戶端建立好連接后,就可以立刻等待新的客戶端連接,而不用阻塞在原客戶端的 read 請(qǐng)求上。

不過,這不叫非阻塞 IO,只不過用了多線程的手段使得主線程沒有卡在 read 函數(shù)上不往下走罷了。操作系統(tǒng)為我們提供的 read 函數(shù)仍然是阻塞的。

所以真正的非阻塞 IO,不能是通過我們用戶層的小把戲,而是要懇請(qǐng)操作系統(tǒng)為我們提供一個(gè)非阻塞的 read 函數(shù)。

這個(gè) read 函數(shù)的效果是,如果沒有數(shù)據(jù)到達(dá)時(shí)(到達(dá)網(wǎng)卡并拷貝到了內(nèi)核緩沖區(qū)),立刻返回一個(gè)錯(cuò)誤值(-1),而不是阻塞地等待。

操作系統(tǒng)提供了這樣的功能,只需要在調(diào)用 read 前,將文件描述符設(shè)置為非阻塞即可。

fcntl(connfd, F_SETFL, O_NONBLOCK);

int n = read(connfd, buffer) != SUCCESS);

這樣,就需要用戶線程循環(huán)調(diào)用 read,直到返回值不為 -1,再開始處理業(yè)務(wù)。

這里我們注意到一個(gè)細(xì)節(jié)。

非阻塞的 read,指的是在數(shù)據(jù)到達(dá)前,即數(shù)據(jù)還未到達(dá)網(wǎng)卡,或者到達(dá)網(wǎng)卡但還沒有拷貝到內(nèi)核緩沖區(qū)之前,這個(gè)階段是非阻塞的。

當(dāng)數(shù)據(jù)已到達(dá)內(nèi)核緩沖區(qū),此時(shí)調(diào)用 read 函數(shù)仍然是阻塞的,需要等待數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū),才能返回。

整體流程如下圖

IO 多路復(fù)用

為每個(gè)客戶端創(chuàng)建一個(gè)線程,服務(wù)器端的線程資源很容易被耗光。

當(dāng)然還有個(gè)聰明的辦法,我們可以每 accept 一個(gè)客戶端連接后,將這個(gè)文件描述符(connfd)放到一個(gè)數(shù)組里。

fdlist.add(connfd);

然后弄一個(gè)新的線程去不斷遍歷這個(gè)數(shù)組,調(diào)用每一個(gè)元素的非阻塞 read 方法。

while(1) {

for(fd 《-- fdlist) {

if(read(fd) != -1) {

doSomeThing();

}

}

}

這樣,我們就成功用一個(gè)線程處理了多個(gè)客戶端連接。

你是不是覺得這有些多路復(fù)用的意思?

但這和我們用多線程去將阻塞 IO 改造成看起來是非阻塞 IO 一樣,這種遍歷方式也只是我們用戶自己想出的小把戲,每次遍歷遇到 read 返回 -1 時(shí)仍然是一次浪費(fèi)資源的系統(tǒng)調(diào)用。

在 while 循環(huán)里做系統(tǒng)調(diào)用,就好比你做分布式項(xiàng)目時(shí)在 while 里做 rpc 請(qǐng)求一樣,是不劃算的。

所以,還是得懇請(qǐng)操作系統(tǒng)老大,提供給我們一個(gè)有這樣效果的函數(shù),我們將一批文件描述符通過一次系統(tǒng)調(diào)用傳給內(nèi)核,由內(nèi)核層去遍歷,才能真正解決這個(gè)問題。

select

select 是操作系統(tǒng)提供的系統(tǒng)調(diào)用函數(shù),通過它,我們可以把一個(gè)文件描述符的數(shù)組發(fā)給操作系統(tǒng), 讓操作系統(tǒng)去遍歷,確定哪個(gè)文件描述符可以讀寫, 然后告訴我們?nèi)ヌ幚恚?/p>

select系統(tǒng)調(diào)用的函數(shù)定義如下。

int select(

int nfds,

fd_set *readfds,

fd_set *writefds,

fd_set *exceptfds,

struct timeval *timeout);

// nfds:監(jiān)控的文件描述符集里最大文件描述符加1// readfds:監(jiān)控有讀數(shù)據(jù)到達(dá)文件描述符集合,傳入傳出參數(shù)// writefds:監(jiān)控寫數(shù)據(jù)到達(dá)文件描述符集合,傳入傳出參數(shù)// exceptfds:監(jiān)控異常發(fā)生達(dá)文件描述符集合, 傳入傳出參數(shù)// timeout:定時(shí)阻塞監(jiān)控時(shí)間,3種情況// 1.NULL,永遠(yuǎn)等下去// 2.設(shè)置timeval,等待固定時(shí)間// 3.設(shè)置timeval里時(shí)間均為0,檢查描述字后立即返回,輪詢

服務(wù)端代碼,這樣來寫。

首先一個(gè)線程不斷接受客戶端連接,并把 socket 文件描述符放到一個(gè) list 里。

while(1) {

connfd = accept(listenfd);

fcntl(connfd, F_SETFL, O_NONBLOCK);

fdlist.add(connfd);

}

然后,另一個(gè)線程不再自己遍歷,而是調(diào)用 select,將這批文件描述符 list 交給操作系統(tǒng)去遍歷。

while(1) {

// 把一堆文件描述符 list 傳給 select 函數(shù)

// 有已就緒的文件描述符就返回,nready 表示有多少個(gè)就緒的

nready = select(list);

。..

}

不過,當(dāng) select 函數(shù)返回后,用戶依然需要遍歷剛剛提交給操作系統(tǒng)的 list。

只不過,操作系統(tǒng)會(huì)將準(zhǔn)備就緒的文件描述符做上標(biāo)識(shí),用戶層將不會(huì)再有無意義的系統(tǒng)調(diào)用開銷。

while(1) {

nready = select(list);

// 用戶層依然要遍歷,只不過少了很多無效的系統(tǒng)調(diào)用

for(fd 《-- fdlist) {

if(fd != -1) {

// 只讀已就緒的文件描述符

read(fd, buf);

// 總共只有 nready 個(gè)已就緒描述符,不用過多遍歷

if(--nready == 0) break;

}

}

}

正如剛剛的動(dòng)圖中所描述的,其直觀效果如下。(同一個(gè)動(dòng)圖消耗了你兩次流量,氣不氣?)

可以看出幾個(gè)細(xì)節(jié):

1. select 調(diào)用需要傳入 fd 數(shù)組,需要拷貝一份到內(nèi)核,高并發(fā)場景下這樣的拷貝消耗的資源是驚人的。(可優(yōu)化為不復(fù)制)2. select 在內(nèi)核層仍然是通過遍歷的方式檢查文件描述符的就緒狀態(tài),是個(gè)同步過程,只不過無系統(tǒng)調(diào)用切換上下文的開銷。(內(nèi)核層可優(yōu)化為異步事件通知)3. select 僅僅返回可讀文件描述符的個(gè)數(shù),具體哪個(gè)可讀還是要用戶自己遍歷。(可優(yōu)化為只返回給用戶就緒的文件描述符,無需用戶做無效的遍歷)

可以看到,這種方式,既做到了一個(gè)線程處理多個(gè)客戶端連接(文件描述符),又減少了系統(tǒng)調(diào)用的開銷(多個(gè)文件描述符只有一次 select 的系統(tǒng)調(diào)用 + n 次就緒狀態(tài)的文件描述符的 read 系統(tǒng)調(diào)用)。

poll

poll 也是操作系統(tǒng)提供的系統(tǒng)調(diào)用函數(shù)。

int poll(struct pollfd *fds, nfds_tnfds, int timeout);

struct pollfd {

intfd; /*文件描述符*/

shortevents; /*監(jiān)控的事件*/

shortrevents; /*監(jiān)控事件中滿足條件返回的事件*/

};

它和 select 的主要區(qū)別就是,去掉了 select 只能監(jiān)聽 1024 個(gè)文件描述符的限制。

epoll

epoll 是最終的大 boss,它解決了 select 和 poll 的一些問題。

還記得上面說的 select 的三個(gè)細(xì)節(jié)么?

1. select 調(diào)用需要傳入 fd 數(shù)組,需要拷貝一份到內(nèi)核,高并發(fā)場景下這樣的拷貝消耗的資源是驚人的。(可優(yōu)化為不復(fù)制)2. select 在內(nèi)核層仍然是通過遍歷的方式檢查文件描述符的就緒狀態(tài),是個(gè)同步過程,只不過無系統(tǒng)調(diào)用切換上下文的開銷。(內(nèi)核層可優(yōu)化為異步事件通知)3. select 僅僅返回可讀文件描述符的個(gè)數(shù),具體哪個(gè)可讀還是要用戶自己遍歷。(可優(yōu)化為只返回給用戶就緒的文件描述符,無需用戶做無效的遍歷)

所以 epoll 主要就是針對(duì)這三點(diǎn)進(jìn)行了改進(jìn)。

1. 內(nèi)核中保存一份文件描述符集合,無需用戶每次都重新傳入,只需告訴內(nèi)核修改的部分即可。2. 內(nèi)核不再通過輪詢的方式找到就緒的文件描述符,而是通過異步 IO 事件喚醒。3. 內(nèi)核僅會(huì)將有 IO 事件的文件描述符返回給用戶,用戶也無需遍歷整個(gè)文件描述符集合。

具體,操作系統(tǒng)提供了這三個(gè)函數(shù)。

第一步,創(chuàng)建一個(gè) epoll 句柄

int epoll_create(int size);

第二步,向內(nèi)核添加、修改或刪除要監(jiān)控的文件描述符。

int epoll_ctl(

int epfd, int op, int fd, struct epoll_event *event);

第三步,類似發(fā)起了 select() 調(diào)用

int epoll_wait(

int epfd, struct epoll_event *events, int max events, int timeout);

使用起來,其內(nèi)部原理就像如下一般絲滑。

如果你想繼續(xù)深入了解 epoll 的底層原理,推薦閱讀飛哥的《圖解 | 深入揭秘 epoll 是如何實(shí)現(xiàn) IO 多路復(fù)用的!》,從 linux 源碼級(jí)別,一行一行非常硬核地解讀 epoll 的實(shí)現(xiàn)原理,且配有大量方便理解的圖片,非常適合源碼控的小伙伴閱讀。

后記

大白話總結(jié)一下。

一切的開始,都起源于這個(gè) read 函數(shù)是操作系統(tǒng)提供的,而且是阻塞的,我們叫它 阻塞 IO。為了破這個(gè)局,程序員在用戶態(tài)通過多線程來防止主線程卡死。后來操作系統(tǒng)發(fā)現(xiàn)這個(gè)需求比較大,于是在操作系統(tǒng)層面提供了非阻塞的 read 函數(shù),這樣程序員就可以在一個(gè)線程內(nèi)完成多個(gè)文件描述符的讀取,這就是 非阻塞 IO。

但多個(gè)文件描述符的讀取就需要遍歷,當(dāng)高并發(fā)場景越來越多時(shí),用戶態(tài)遍歷的文件描述符也越來越多,相當(dāng)于在 while 循環(huán)里進(jìn)行了越來越多的系統(tǒng)調(diào)用。后來操作系統(tǒng)又發(fā)現(xiàn)這個(gè)場景需求量較大,于是又在操作系統(tǒng)層面提供了這樣的遍歷文件描述符的機(jī)制,這就是 IO 多路復(fù)用。多路復(fù)用有三個(gè)函數(shù),最開始是 select,然后又發(fā)明了 poll 解決了 select 文件描述符的限制,然后又發(fā)明了 epoll 解決 select 的三個(gè)不足。

所以,IO 模型的演進(jìn),其實(shí)就是時(shí)代的變化,倒逼著操作系統(tǒng)將更多的功能加到自己的內(nèi)核而已。如果你建立了這樣的思維,很容易發(fā)現(xiàn)網(wǎng)上的一些錯(cuò)誤。比如好多文章說,多路復(fù)用之所以效率高,是因?yàn)橛靡粋€(gè)線程就可以監(jiān)控多個(gè)文件描述符。這顯然是知其然而不知其所以然,多路復(fù)用產(chǎn)生的效果,完全可以由用戶態(tài)去遍歷文件描述符并調(diào)用其非阻塞的 read 函數(shù)實(shí)現(xiàn)。而多路復(fù)用快的原因在于,操作系統(tǒng)提供了這樣的系統(tǒng)調(diào)用,使得原來的 while 循環(huán)里多次系統(tǒng)調(diào)用,變成了一次系統(tǒng)調(diào)用 + 內(nèi)核層遍歷這些文件描述符。

就好比我們平時(shí)寫業(yè)務(wù)代碼,把原來 while 循環(huán)里調(diào) http 接口進(jìn)行批量,改成了讓對(duì)方提供一個(gè)批量添加的 http 接口,然后我們一次 rpc 請(qǐng)求就完成了批量添加。一個(gè)道理。

責(zé)任編輯:haq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 數(shù)據(jù)
    +關(guān)注

    關(guān)注

    8

    文章

    6715

    瀏覽量

    88311
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4671

    瀏覽量

    67767

原文標(biāo)題:你管這破玩意叫 IO 多路復(fù)用?

文章出處:【微信號(hào):gh_3980db2283cd,微信公眾號(hào):開關(guān)電源芯片】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    多路復(fù)用器應(yīng)用中的防護(hù)

    電子發(fā)燒友網(wǎng)站提供《多路復(fù)用器應(yīng)用中的防護(hù).pdf》資料免費(fèi)下載
    發(fā)表于 09-21 10:47 ?0次下載
    <b class='flag-5'>多路復(fù)用</b>器應(yīng)用中的防護(hù)

    需要選擇一顆并行io擴(kuò)展器件,擴(kuò)展16個(gè)io連接到外設(shè)io\'上,CD74HC4067滿足要求嗎?

    需要選擇一顆并行io擴(kuò)展器件,擴(kuò)展16個(gè)io連接到外設(shè)io\'上,那么CD74HC4067是滿足這個(gè)基本功能要求把?看著名字怎么叫模擬多路復(fù)用器,應(yīng)該可以用吧。
    發(fā)表于 08-26 06:59

    什么是多路復(fù)用器?它有哪些作用和應(yīng)用?

    在現(xiàn)代通信與數(shù)據(jù)處理領(lǐng)域,多路復(fù)用器(Multiplexer,簡稱MUX)作為一種關(guān)鍵設(shè)備,發(fā)揮著不可替代的作用。它能夠?qū)⒍鄠€(gè)輸入信號(hào)選擇性地合并到一個(gè)輸出信號(hào)中,從而實(shí)現(xiàn)了對(duì)通信信道的高效利用
    的頭像 發(fā)表于 05-23 16:38 ?2047次閱讀

    頻分多路復(fù)用和時(shí)分多路復(fù)用的區(qū)別有哪些

    頻分多路復(fù)用(FDM)和時(shí)分多路復(fù)用(TDM)是兩種主要的多路復(fù)用技術(shù),它們?cè)谕ㄐ畔到y(tǒng)中扮演著至關(guān)重要的角色。
    的頭像 發(fā)表于 05-07 15:24 ?1072次閱讀

    多路復(fù)用技術(shù)主要有幾種類型?它們各有什么特點(diǎn)?

    多路復(fù)用技術(shù)主要有幾種類型?它們各有什么特點(diǎn)? 多路復(fù)用技術(shù)主要有以下幾種類型:進(jìn)程多路復(fù)用、I/O多路復(fù)用、信號(hào)驅(qū)動(dòng)I/O和異步I/O。每種類型都有其特點(diǎn)和應(yīng)用場景。 1. 進(jìn)程
    的頭像 發(fā)表于 03-28 15:36 ?1645次閱讀

    一文詳解多路復(fù)用的類型

    多路復(fù)用最初是在電話中發(fā)展起來的。多個(gè)信號(hào)被組合在一起,通過一根電纜發(fā)送。
    的頭像 發(fā)表于 03-05 15:44 ?2406次閱讀
    一文詳解<b class='flag-5'>多路復(fù)用</b>的類型

    多路復(fù)用的原理 為什么要多路復(fù)用多路復(fù)用技術(shù)的應(yīng)用

    在計(jì)算機(jī)網(wǎng)絡(luò)中,多路復(fù)用是一種重要的通信技術(shù),它允許多個(gè)信號(hào)通過同一個(gè)通信信道進(jìn)行傳輸。
    的頭像 發(fā)表于 03-05 15:09 ?1658次閱讀
    <b class='flag-5'>多路復(fù)用</b>的原理 為什么要<b class='flag-5'>多路復(fù)用</b>?<b class='flag-5'>多路復(fù)用</b>技術(shù)的應(yīng)用

    頻分多路復(fù)用的原理 頻分多路復(fù)用方式的分類

    頻分多路復(fù)用(Frequency-division multiplexing,F(xiàn)DM),是指載波帶寬被劃分為多種不同頻帶的子信道,每個(gè)子信道可以并行傳送一路信號(hào)的一種多路復(fù)用技術(shù)。
    的頭像 發(fā)表于 03-05 14:10 ?916次閱讀
    頻分<b class='flag-5'>多路復(fù)用</b>的原理 頻分<b class='flag-5'>多路復(fù)用</b>方式的分類

    什么是io多路復(fù)用?IO多路復(fù)用的優(yōu)缺點(diǎn)

    IO多路復(fù)用是一種同步IO模型,它允許單個(gè)進(jìn)程/線程同時(shí)處理多個(gè)IO請(qǐng)求。具體來說,一個(gè)進(jìn)程/線程可以監(jiān)視多個(gè)文件句柄,一旦某個(gè)文件句柄就緒,就能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作。在沒
    的頭像 發(fā)表于 01-18 15:48 ?1401次閱讀

    一文了解多路復(fù)用器濾波器

    本章將更深入地介紹多路復(fù)用器濾波器,以及它們?nèi)绾斡糜诟鞣N應(yīng)用中。您將了解到多路復(fù)用器如何幫助設(shè)計(jì)人員創(chuàng)造出更復(fù)雜的無線產(chǎn)品。
    的頭像 發(fā)表于 01-16 10:53 ?1268次閱讀
    一文了解<b class='flag-5'>多路復(fù)用</b>器濾波器

    IO多路復(fù)用基本概念

    一、IO多路復(fù)用基本概念 select、poll、epoll都是IO多路復(fù)用的機(jī)制。IO多路復(fù)用
    的頭像 發(fā)表于 11-10 16:34 ?1095次閱讀
    <b class='flag-5'>IO</b><b class='flag-5'>多路復(fù)用</b>基本概念

    異步IO框架iouring介紹

    提供完善的異步IO(網(wǎng)絡(luò)IO、磁盤IO)機(jī)制。 在網(wǎng)絡(luò)編程中,我們通常使用epoll IO
    的頭像 發(fā)表于 11-09 09:30 ?1667次閱讀
    異步<b class='flag-5'>IO</b>框架iouring介紹

    linux異步io框架iouring應(yīng)用

    完善的異步IO(網(wǎng)絡(luò)IO、磁盤IO)機(jī)制。 在網(wǎng)絡(luò)編程中,我們通常使用epoll IO
    的頭像 發(fā)表于 11-08 15:39 ?498次閱讀
    linux異步<b class='flag-5'>io</b>框架iouring應(yīng)用

    多路IO復(fù)用模型和異步IO模型介紹

    多路 IO 復(fù)用模型 多路 IO 復(fù)用,有時(shí)也稱為事件驅(qū)動(dòng)
    的頭像 發(fā)表于 10-08 17:21 ?677次閱讀
    <b class='flag-5'>多路</b><b class='flag-5'>IO</b><b class='flag-5'>復(fù)用</b>模型和異步<b class='flag-5'>IO</b>模型介紹

    基于多路復(fù)用模型的Netty框架

    系統(tǒng)處理請(qǐng)求的能力。 有了NIO之后,一個(gè)線程即可處理多個(gè)連接事件,基于多路復(fù)用模型的Netty框架,不僅降低了使用NIO的復(fù)雜度, 優(yōu)點(diǎn) Netty是一款以java NIO為基礎(chǔ),基于事件驅(qū)動(dòng)模型支持異步、高并發(fā)的網(wǎng)絡(luò)應(yīng)用框架 API使用簡單,開發(fā)門檻低,簡化了NIO開
    的頭像 發(fā)表于 09-30 11:30 ?717次閱讀